We've updated the ponylang/ssl library dependency in this project to 1.0.1.
SesssionNeverOpened has been renamed to SessionNeverOpened.
Before:
match error
| SesssionNeverOpened => "session never opened"
endAfter:
match error
| SessionNeverOpened => "session never opened"
endThe error response parser incorrectly mapped the 'R' (Routine) protocol field to line instead of routine on ErrorResponseMessage. The routine field was never populated as a result. It now correctly contains the name of the source-code routine that reported the error.
A SELECT query returning zero rows (e.g., SELECT 1 WHERE false) incorrectly produced a RowModifying result instead of a ResultSet with zero rows. This made it impossible to distinguish a zero-row SELECT from an INSERT/UPDATE/DELETE at the result level. Zero-row SELECTs now correctly produce a ResultSet.
When a query error occurred inside a PostgreSQL transaction, the ResultReceiver could receive pg_query_failed twice for the same query — once with the original error, and again with SessionClosed if close() was called before the session became idle. The errored query now correctly completes after ReadyForQuery regardless of transaction status.
When close() was called while the session was between processing an error response and processing the subsequent ready-for-query message, the ResultReceiver could receive pg_query_failed twice — once with the original error and again with SessionClosed. Query cycle messages are now processed synchronously, preventing other operations from interleaving.
You can now execute parameterized queries using PreparedQuery. Parameters are referenced as $1, $2, etc. in the query string and passed as an array of (String | None) values. Use None for SQL NULL.
// Parameterized SELECT
let query = PreparedQuery("SELECT * FROM users WHERE id = $1",
recover val [as (String | None): "42"] end)
session.execute(query, receiver)
// INSERT with NULL parameter
let insert = PreparedQuery("INSERT INTO items (name, desc) VALUES ($1, $2)",
recover val [as (String | None): "widget"; None] end)
session.execute(insert, receiver)Each PreparedQuery must contain a single SQL statement. For multi-statement execution, use SimpleQuery.
ResultReceiver.pg_query_failed and Result.query() now use Query (a union of SimpleQuery | PreparedQuery | NamedPreparedQuery) instead of SimpleQuery.
Before:
be pg_query_failed(query: SimpleQuery,
failure: (ErrorResponseMessage | ClientQueryError))
=>
// handle failureAfter:
be pg_query_failed(session: Session, query: Query,
failure: (ErrorResponseMessage | ClientQueryError))
=>
match query
| let sq: SimpleQuery => // ...
| let pq: PreparedQuery => // ...
| let nq: NamedPreparedQuery => // ...
endYou can now create server-side named prepared statements with Session.prepare(), execute them with NamedPreparedQuery, and destroy them with Session.close_statement(). Named statements are parsed once and can be executed multiple times with different parameters, avoiding repeated parsing overhead.
// Prepare a named statement
session.prepare("find_user", "SELECT * FROM users WHERE id = $1", receiver)
// In the PrepareReceiver callback:
be pg_statement_prepared(session: Session, name: String) =>
// Execute with different parameters
session.execute(
NamedPreparedQuery("find_user",
recover val [as (String | None): "42"] end),
result_receiver)
// Clean up when done
session.close_statement("find_user")The Query union type now includes NamedPreparedQuery, so exhaustive matches on Query need a new branch:
match query
| let sq: SimpleQuery => sq.string
| let pq: PreparedQuery => pq.string
| let nq: NamedPreparedQuery => nq.name
endYou can now encrypt connections to PostgreSQL using SSL/TLS. Pass SSLRequired(sslctx) to Session.create() to enable SSL negotiation before authentication. The default SSLDisabled preserves the existing plaintext behavior.
use "ssl/net"
use "postgres"
// Create an SSLContext (configure certificates/verification as needed)
let sslctx = recover val
SSLContext
.> set_client_verify(false)
.> set_server_verify(false)
end
// Connect with SSL
let session = Session(
auth,
notify,
host,
port,
username,
password,
database,
SSLRequired(sslctx))If the server accepts SSL, the connection is encrypted before authentication begins. If the server refuses, pg_session_connection_failed fires.
All ResultReceiver and PrepareReceiver callbacks now take Session as their first parameter, matching the convention used by SessionStatusNotify. This enables receivers to execute follow-up queries directly from callbacks without storing a session reference (see "Enable follow-up queries from ResultReceiver and PrepareReceiver callbacks" below).
Before:
be pg_query_result(result: Result) =>
// ...
be pg_query_failed(query: Query,
failure: (ErrorResponseMessage | ClientQueryError))
=>
// ...
be pg_statement_prepared(name: String) =>
// ...
be pg_prepare_failed(name: String,
failure: (ErrorResponseMessage | ClientQueryError))
=>
// ...After:
be pg_query_result(session: Session, result: Result) =>
// ...
be pg_query_failed(session: Session, query: Query,
failure: (ErrorResponseMessage | ClientQueryError))
=>
// ...
be pg_statement_prepared(session: Session, name: String) =>
// ...
be pg_prepare_failed(session: Session, name: String,
failure: (ErrorResponseMessage | ClientQueryError))
=>
// ...ResultReceiver and PrepareReceiver callbacks now receive the Session, so receivers can execute follow-up queries, close the session, or chain operations directly from callbacks without needing to store a session reference at construction time.
actor MyReceiver is ResultReceiver
// no need to store session — it's passed to every callback
be pg_query_result(session: Session, result: Result) =>
// execute a follow-up query using the session from the callback
session.execute(SimpleQuery("SELECT 1"), this)
be pg_query_failed(session: Session, query: Query,
failure: (ErrorResponseMessage | ClientQueryError))
=>
session.close()Field now implements Equatable, enabling == and != comparisons. A Field holds a column name and a typed value. Two fields are equal when they have the same name and the same value — the values must be the same type and compare equal using that type's own equality.
Field("id", I32(42)) == Field("id", I32(42)) // true
Field("id", I32(42)) == Field("id", I64(42)) // false — different types
Field("id", I32(42)) == Field("name", I32(42)) // false — different namesRow now implements Equatable, enabling == and != comparisons. A Row holds an ordered sequence of Field values representing a single result row. Two rows are equal when they have the same number of fields and each corresponding pair of fields is equal. Field order matters — the same fields in a different order are not equal.
let r1 = Row(recover val [Field("id", I32(1)); Field("name", "Alice")] end)
let r2 = Row(recover val [Field("id", I32(1)); Field("name", "Alice")] end)
r1 == r2 // true
let r3 = Row(recover val [Field("name", "Alice"); Field("id", I32(1))] end)
r1 == r3 // false — same fields, different orderRows now implements Equatable, enabling == and != comparisons. A Rows holds an ordered collection of Row values representing a query result set. Two Rows are equal when they have the same number of rows and each corresponding pair of rows is equal. Row order matters — the same rows in a different order are not equal.
let rs1 = Rows(recover val
[Row(recover val [Field("id", I32(1))] end)]
end)
let rs2 = Rows(recover val
[Row(recover val [Field("id", I32(1))] end)]
end)
rs1 == rs2 // true