Releases: ponylang/postgres
Release list
0.7.0
Drop support for Windows 10
Building postgres for Windows now requires ponyc 0.66.0 or later and Windows 11 or Windows Server 2022 or later. Windows 10 is no longer supported. Non-Windows platforms are unaffected.
[0.7.0] - 2026-06-29
Changed
- Drop support for Windows 10 (PR #236)
0.6.1
Fix connection hang after sending a large amount of data
After sending a large query or a large amount of data, a connection could stop receiving the server's responses — an in-flight query would never complete and the connection would hang. No error was raised and the connection was not closed; it simply went quiet, even though the server had already replied. This most often showed up with large statements or bulk data. Responses now arrive as expected.
[0.6.1] - 2026-06-22
Fixed
- Fix connection hang after sending a large amount of data (PR #235)
0.6.0
Require ponyc 0.64.0 or later
postgres now requires ponyc 0.64.0 or later. The previous minimum was 0.63.1.
This is driven by an update to lori 0.15.0, which requires ponyc 0.64.0 for changes to FFI declaration syntax and the runtime socket API. Older ponyc versions will fail to compile postgres.
[0.6.0] - 2026-05-28
Changed
- Require ponyc 0.64.0 or later (PR #232)
0.5.0
Fix crash on server rejection during startup
When PostgreSQL rejected a connection during startup with an error response — most commonly when max_connections had been exhausted — the driver crashed the process through an unreachable-state panic instead of delivering the failure to the application. Server rejections during startup now arrive through pg_session_connection_failed with the full ErrorResponseMessage available for inspection.
Consolidate authentication failure into connection failure
The pg_session_authentication_failed callback has been removed. All pre-ready failures — transport-level errors, TLS errors, unsupported authentication methods, bad passwords, and server rejections during startup — now arrive through the single pg_session_connection_failed callback.
This matches how other PostgreSQL clients describe startup failures and ensures every startup error reaches the application through one well-defined path. ConnectionFailureReason has been expanded to include all the variants previously covered by AuthenticationFailureReason, plus new variants for specific server-rejection scenarios.
Migration
Override pg_session_connection_failed instead of pg_session_authentication_failed. The AuthenticationFailureReason type has been removed; its members are now part of ConnectionFailureReason.
Before:
be pg_session_authentication_failed(session: Session,
reason: AuthenticationFailureReason)
=>
match reason
| InvalidPassword => _out.print("Bad password")
| InvalidAuthenticationSpecification => _out.print("Invalid user")
| UnsupportedAuthenticationMethod => _out.print("Unsupported method")
| ServerVerificationFailed => _out.print("Server verification failed")
end
be pg_session_connection_failed(session: Session,
reason: ConnectionFailureReason)
=>
_out.print("Connection failed")After:
be pg_session_connection_failed(session: Session,
reason: ConnectionFailureReason)
=>
match reason
| let r: InvalidPassword =>
_out.print("Bad password: " + r.response().message)
| let r: InvalidAuthorizationSpecification =>
_out.print("Invalid user: " + r.response().message)
| let r: TooManyConnections =>
_out.print("Too many connections: " + r.response().message)
| let r: InvalidDatabaseName =>
_out.print("Database does not exist: " + r.response().message)
| let r: ServerRejected =>
_out.print("Server rejected startup (SQLSTATE "
+ r.response().code + "): " + r.response().message)
| UnsupportedAuthenticationMethod => _out.print("Unsupported method")
| ServerVerificationFailed => _out.print("Server verification failed")
else
_out.print("Connection failed")
endSummary of API changes
pg_session_authentication_failedis removed.AuthenticationFailureReasonis removed; its members are part ofConnectionFailureReason.InvalidAuthenticationSpecificationis renamed toInvalidAuthorizationSpecification(matches the official SQLSTATE 28000 name).InvalidPasswordandInvalidAuthorizationSpecificationare nowclass valwrappers aroundErrorResponseMessage, accessed viaresponse(). A match arm that was| InvalidPassword =>must become| let r: InvalidPassword =>to bind the value. Identity comparisons such asreason is InvalidPasswordno longer match — two instances of the class are neveris-equal; use a match arm with a type binding instead.UnsupportedAuthenticationMethodandServerVerificationFailedremain primitives but are now delivered viapg_session_connection_failed.- New variants:
TooManyConnections(SQLSTATE 53300),InvalidDatabaseName(SQLSTATE 3D000), andServerRejected(fallback for any other server ErrorResponse during startup). All three areclass valwrappers aroundErrorResponseMessage. pg_session_shutdownnow fires after everypg_session_connection_failed. Previously, transport-level and TLS-negotiation failures fired onlypg_session_connection_failedwhile authentication and server-rejection failures fired both. The unified failure callback now always terminates withpg_session_shutdown, matching the "session is torn down" mental model regardless of which phase failed.
Close SCRAM mutual-authentication bypass
The driver now rejects SCRAM-SHA-256 exchanges in which the server skips, duplicates, or malforms authentication messages. Previously, a server could send AuthenticationOk without a preceding AuthenticationSASLFinal and the driver would authenticate the session without verifying the server's signature, defeating SCRAM's mutual-authentication property.
Protocol violations during SCRAM — a skipped AuthenticationSASLFinal, a duplicated AuthenticationSASLContinue, an AuthenticationSASLFinal arriving before AuthenticationSASLContinue, malformed SASLFinal content, or a malformed or nonce-mismatched AuthenticationSASLContinue — now fail the connection via pg_session_connection_failed with ServerVerificationFailed, matching the existing behavior for a mismatched server signature. Previously, several of these conditions caused the session to close silently without notifying the application.
Deliver server protocol violations to the application
A server can send bytes the driver can't parse, a wire-legal message that's invalid for the current connection state, or an unexpected byte during SSL negotiation. Any of those used to silently shut the session down or, worse, crash the client process through an illegal-state panic. Neither outcome gave an application trying to understand why its session died anything to work with.
All three paths now route through the state machine's own error handling. A pre-ready violation fires pg_session_connection_failed(ProtocolViolation) followed by pg_session_shutdown. A logged-in session with a query in flight delivers ProtocolViolation to that query's receiver — pg_query_failed, pg_prepare_failed, pg_copy_failed, pg_stream_failed, or pg_pipeline_failed — before pg_session_shutdown fires. Queries that were merely queued still receive SessionClosed, since only the in-flight query directly observed the violation.
For a pipeline, the currently-executing query receives ProtocolViolation and the remaining queries receive SessionClosed.
Add ProtocolViolation to ConnectionFailureReason and ClientQueryError
ProtocolViolation is a new primitive that now appears in both the ConnectionFailureReason union (delivered via pg_session_connection_failed) and the ClientQueryError union (delivered via pg_query_failed and its peers). It carries no diagnostic payload. Shipping server-supplied bytes or parser state with the failure would be an attack vector for log injection, DoS amplification, and running code on hostile input during error handling. Easier to add bounded symbolic detail later if a user need emerges than to remove it once shipped.
Migration
Any match \exhaustive\ on ConnectionFailureReason or ClientQueryError needs a new arm for ProtocolViolation. Non-exhaustive matches (with an else clause or no \exhaustive\ annotation) keep compiling without changes.
Before:
be pg_session_connection_failed(session: Session,
reason: ConnectionFailureReason)
=>
match \exhaustive\ reason
| ConnectionFailedDNS => _out.print("DNS")
| ConnectionFailedTCP => _out.print("TCP")
// ...
endAfter:
be pg_session_connection_failed(session: Session,
reason: ConnectionFailureReason)
=>
match \exhaustive\ reason
| ConnectionFailedDNS => _out.print("DNS")
| ConnectionFailedTCP => _out.print("TCP")
// ...
| ProtocolViolation => _out.print("Protocol violation")
endThe same applies to ClientQueryError: add a | ProtocolViolation => arm to any exhaustive match and handle the case the way you'd handle an unrecoverable session failure on the query you dispatched.
Guard against integer underflow on server-supplied message lengths
When a PostgreSQL server declared a message length smaller than the protocol's minimum, the driver performed an unsigned subtraction that wrapped to a huge value. The full consequences of the wrap were not fully characterized — one observed effect was silently consuming malformed bytes as a zero-payload acknowledgement before eventually reporting a protocol violation, but other downstream effects may have been reachable with different message shapes. The driver now validates length fields before arithmetic and rejects such messages as a protocol violation immediately.
Guard against malformed column lengths in data rows
When a PostgreSQL server declared a DataRow column length whose sign bit was set (other than the -1 NULL marker), the driver passed the value through to a buffered read without validating it. PostgreSQL declares column length as a signed Int32, so values in that range are protocol violations. The full consequences were not fully characterized — the bogus length triggered a downstream read failure that surfaced as a protocol violation, but the validation was incidental rather than explicit. The driver now validates column lengths at the parse site and rejects such messages as a protocol violation immediately.
Detect peer-initiated TCP close during any session state
If the server closed the TCP connection at any point — during SSL negotiation, pre-auth startup, mid-SCRAM, or after the session reached the ready state — the driver would hang indefinitely without notifying the application. Peer close is now detected and delivered through the state machine's own error handling.
A pre-ready peer close fires pg_session_connection_failed(ConnectionClosedByServer) followed by pg_session_shutdown. A logged-in session with a query in flight delivers SessionClosed to that query's receiver — pg_query_failed, pg_prepare_failed, pg_copy_failed, pg_stream_failed, or pg_pipeline_failed — before pg_session_shutdown fires. Que...
0.4.0
Fix potential connection hang when timer event subscription fails
On some platforms, if the operating system cannot allocate resources for a connection timer (e.g., ENOMEM on kqueue or epoll), connections could hang silently instead of reporting an error. Timer subscription failures are now detected and reported as connection failures.
Add ConnectionFailedTimerError to ConnectionFailureReason
ConnectionFailureReason now includes ConnectionFailedTimerError, which is reported when the connect timer's ASIO event subscription fails. If you match exhaustively on ConnectionFailureReason, you'll need to add a handler for the new variant.
Require ponyc 0.63.1 or later
postgres now requires ponyc 0.63.1 or later. Older ponyc versions are no longer supported.
[0.4.0] - 2026-04-12
Fixed
- Fix potential connection hang when timer event subscription fails (PR #202)
Changed
0.3.1
Fix connection stall after large write with backpressure
Sessions could stop processing incoming data after completing a large write that triggered backpressure, causing the connection to hang. Updated the lori dependency to 0.13.1 which fixes the underlying issue.
[0.3.1] - 2026-04-07
Fixed
- Fix connection stall after large write with backpressure (PR #201)
0.3.0
Update ponylang/ssl dependency to 1.0.1
We've updated the ponylang/ssl library dependency in this project to 1.0.1.
Fix typo in SesssionNeverOpened
SesssionNeverOpened has been renamed to SessionNeverOpened.
Before:
match error
| SesssionNeverOpened => "session never opened"
endAfter:
match error
| SessionNeverOpened => "session never opened"
endFix ErrorResponseMessage routine field never being populated
The 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.
Fix zero-row SELECT producing RowModifying instead of ResultSet
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.
Fix double-delivery of pg_query_failed on failed transactions
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.
Fix double-delivery of pg_query_failed when close() races with error processing
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.
Add parameterized queries via extended query protocol
You can now execute parameterized queries using PreparedQuery. Parameters are referenced as $1, $2, etc. in the query string and passed as an Array[FieldDataTypes] val. Typed values (I16, I32, I64, F32, F64, Bool, Array[U8] val, PgTimestamp, PgTime, PgDate, PgInterval) use binary wire format; String and None use text format. Use None for SQL NULL.
// Parameterized SELECT with typed parameter
let query = PreparedQuery("SELECT * FROM users WHERE id = $1",
recover val [as FieldDataTypes: I32(42)] end)
session.execute(query, receiver)
// INSERT with NULL parameter
let insert = PreparedQuery("INSERT INTO items (name, desc) VALUES ($1, $2)",
recover val [as FieldDataTypes: "widget"; None] end)
session.execute(insert, receiver)Each PreparedQuery must contain a single SQL statement. For multi-statement execution, use SimpleQuery.
Change ResultReceiver and Result to use Query union type
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 => // ...
endAdd named prepared statement support
You 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 FieldDataTypes: I32(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
endAdd SSL/TLS negotiation support
You can now encrypt connections to PostgreSQL using SSL/TLS. Pass SSLRequired(sslctx) via ServerConnectInfo 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(
ServerConnectInfo(auth, host, port, SSLRequired(sslctx)),
DatabaseConnectInfo(username, password, database),
notify)If the server accepts SSL, the connection is encrypted before authentication begins. If the server refuses, pg_session_connection_failed fires.
Change ResultReceiver and PrepareReceiver callbacks to take Session as first parameter
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))
=>
// ...Enable follow-up queries from ResultReceiver and PrepareReceiver callbacks
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()Add equality comparison for Field
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 namesAdd equality comparison for Row
Row 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 orderAdd equality comparison for Rows
Rows 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 // trueSend Terminate message before closing TCP connection
Session.close() now sends a Terminate message to the PostgreSQL server before closing the TCP connection. Previously, the connection was hard-closed without notifying the server, which could leave server-side resources (session state, prepared statements, temp tables) lingering until the server detected the broken connection on its next I/O attempt.
No code changes are needed — Session.close() handles this automatically.
Add query cancellation support
You can now cancel a running query by calling session.cancel(). This sends a PostgreSQL CancelRequest on a separate connection, requesting the server to abort the in-flight query. Cancellation is best-effort — the server may or may not honor it. If cancelled, the query's ResultReceiver receives pg_query_failed with an `ErrorResponseMessag...
0.2.2
Change SSL dependency
We've switched our SSL dependency from ponylang/crypto to ponylang/ssl. ponylang/crypto is deprecated and will soon receive no further updates.
As part of the change, we also had to update the ponylang/lori dependency to version 0.6.2.
[0.2.2] - 2025-07-16
Changed
- Changed SSL dependency (PR #54)
0.2.1
Update ponylang/lori dependency to 0.6.1
We've updated the dependency on ponylang/lori to version 0.6.1. The new version of lori comes with some stability improvements.
[0.2.1] - 2025-03-04
Changed
- Update ponylang/lori dependency to 0.6.1 (PR #52
0.2.0
Update ponylang/lori dependency to 0.6.0
We've updated the dependency on ponylang/lori to version 0.6.0. The new version of lori comes with a number of stability improvements that should make this library more reliable in "edge-case scenarios".
The new lori dependency has a transitive dependency on ponylang/net_ssl and as such, an SSL library is now required to build this library. Please see the net_ssl installation instructions for more information.
[0.2.0] - 2025-03-02
Changed
- Update ponylang/lori dependency to 0.6.0 (PR #51)