Prepare Postgres.jl for the 1.0 release - #5
Conversation
Protocol/correctness fixes: - copy_from no longer hangs forever when the COPY statement errors before CopyInResponse (e.g. missing table): ReadyForQuery now terminates the wait loop and the server error is surfaced with the connection usable - copy_from/copy_to now reject non-COPY and wrong-direction statements cleanly (CopyFail abort) instead of deadlocking or silently succeeding - COPY statements via DBInterface.execute or Postgres.cursor now abort the server-side copy (CopyFail + fresh Sync, since the pre-copy Sync is ignored in copy-in mode) and throw a clear error pointing at copy_from/copy_to, keeping the connection usable - IPv6 host literals are bracketed when forming the transport address ([::1]:5432); unix socket paths get a clear unsupported error - GC.@preserve added around unsafe_string(pointer(buf)) parsing loops (error/notice/notification/parameter-status/describe/command-tag) - password material (cleartext and md5 hash) is redacted from debug logs - PostgresInterfaceError now subtypes Exception - parse_numeric throws a clear error for NaN/Infinity numeric values - cursor(conn, sql) rolls back its own transaction if cursor setup fails API surface polish: - ConnectionParams.debug/reconnect are honored by connect (kwargs still override); sslservername plumbed through ConnectionParams, keyword DSNs, URIs, and all ConnectionPool constructors; style kwarg available on the DSN/params connect and pool paths - removed the accepted-but-ignored `binary` kwarg from execute - public declarations for the supported API surface (Julia 1.11+) - docstrings for the public API (connection, pool, transactions, COPY, LISTEN/NOTIFY, type registry, statement cache, cursor, errors, types) Org-transfer/metadata cleanup: - deploydocs points at JuliaDatabases/Postgres.jl - LICENSE names Postgres.jl (was template text naming Example.jl) - README: CI/docs/codecov badges, raw"..." on $1 examples so they are copy-pasteable, style-based query logging (set_query_logger! was removed pre-1.0 but docs still showed it), sslservername documented - docs: same fixes, plus reference blocks for the newly documented types - Parsers compat tightened to "2" (0.3/1 predate APIs in use); unused Logging dependency dropped; dead code removed (ERROR_CODE, DATETIME_OPTIONS, parse_to_julia_array, commented-out escape/lastrowid) Tests: COPY misuse/error-path coverage, ConnectionParams debug/reconnect, sslservername parsing, IPv6/unix-socket address formation, numeric special values, updated export-surface test for public names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- add the GC.@preserve missed in update_server_parameters! (same unsafe_string(pointer) pattern fixed elsewhere in this PR) - consistent exception taxonomy: COPY misuse (non-COPY, wrong-direction, via execute/cursor) now throws PostgresInterfaceError everywhere instead of Error in some paths and PostgresInterfaceError in others; the Error docstring now documents the residual protocol-level client uses - copy-out error precedence: a genuine mid-stream server error (e.g. COPY (SELECT 1/0) TO STDOUT) is surfaced instead of being masked by the misuse error in the execute and cursor paths - copy_in's post-data drain aborts a second CopyInResponse (multi-statement query strings) with CopyFail instead of deadlocking - README: drop inaccurate "pure-Julia" (TLS uses OpenSSL_jll via Reseau) - tests for all of the above Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A failing data source in copy_from now aborts the copy with CopyFail and drains to ReadyForQuery, so the connection stays usable and the source's error propagates. A failing dest IO (or socket failure) mid copy-out closes the socket instead of leaving a desynced connection that still looks valid to pool_isvalid — matching the mid-stream bail behavior of the execute and cursor paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- redact SASL/SCRAM messages from the debug log: the earlier redaction covered only cleartext and md5, leaving the client proof (and thus an offline brute-force oracle for the password) in the log for SCRAM-SHA-256, the default auth method on modern PostgreSQL - bound all message-field parsing by the buffer actually received. Reseau's read returns a short buffer at EOF without throwing, so loops bounded on the server-declared length, and the unbounded unsafe_string(pointer(buf)) NUL scans, could read past the allocation and surface heap bytes as column names, command tags, or error text. Adds cstring_at and uses it in the error/notice/notification/command-tag parsers; describeprepared and the DataRow value loop now bounds-check every offset before reading. - send CancelRequest over TLS when the connection being cancelled uses TLS (the cancel key is a credential valid for that backend's lifetime), and refuse to send it in the clear under sslmode=require/verify-full - bound the numeric exponent so bogus server text can't drive an enormous BigInt scaling - reject embedded NULs in escape_identifier/escape_literal, and document that escape_literal assumes standard_conforming_strings=on - document that sslservername is also the name verified against the certificate under verify-full (not merely an SNI override), that require encrypts without authenticating the server, and that query_logger receives bound parameter values Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- cancel_query! no longer fails silently. cancel_request's refusal to send the key in cleartext now throws instead of returning false into a discarded return value, and cancel_query! throws when the request could not be delivered at all — previously a refused or failed cancel left the caller believing a runaway query had been cancelled. - the cancel connection requires TLS when the connection being cancelled actually negotiated TLS, not merely when sslmode said so: under the default "prefer" the main session can be on TLS while the cancel connection silently fell back to cleartext. - bound the server-declared message length in readheader (and in the pre-TLS, pre-auth SSLRequest error path, which bypasses it) to PostgreSQL's own 1 GiB protocol maximum. Reseau's read allocates the requested size up front, so a 5-byte header claiming ~2 GB committed that much memory before a single body byte arrived — reachable by an on-path attacker before authentication. - parse_numeric reports an out-of-range exponent consistently instead of letting an oversized one surface as OverflowError - document that debug logging redacts authentication messages but not bind parameter values Adds coverage that cancellation works over TLS against the SSL fixture server, and that the cleartext refusal throws. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two bugs in the previous commit, plus adjacent hardening: - the prefer->require TLS upgrade for the cancel connection was inside `if trylock(conn.lock)`, so it was skipped in cancel_query!'s primary use case: another task holds the lock running the very query being cancelled. A default-sslmode connection that had negotiated TLS would still let the cancel key fall back to cleartext — exactly the downgrade the upgrade was added to prevent. The socket check needs no lock (host/pid/skey are read unlocked too), so it now happens before the trylock. - the new message-length check threw API.Error, which describeprepared treats as "the stream is clean, at ReadyForQuery" — so a bogus length left a desynchronized socket open and reusable by the pool. It now closes the socket before throwing, like the other protocol-corruption paths. - DataRow's column count is signed on the wire: a negative passed the upper-bound check and produced an unfilled row (UndefRefError downstream) instead of a clean protocol error. Also bounds against typeIds, not just names. - the remaining ParameterStatus field loop is bounded by the buffer length rather than the server-declared length, matching the others. - the three COPY mid-stream catches surface an already-received server error instead of the raw IO error, matching the execute path. Adds DataRow malformed-message unit tests, and makes the TLS cancel test use the default sslmode so it covers the upgrade path with the lock held. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- waitfor's message loop had no fallback branch, so any message type it wasn't waiting for had its header read and its body left on the wire — the body was then read as the next message header. Reproducible against a real server: LISTEN on a connection, have another connection NOTIFY, then run a query on the listener; the NotificationResponse arrives interleaved with the query's messages and destroys the connection. Now discards the body like every other read loop. Covered by a regression test (verified by mutation: removing the fix fails that test). - wait_for_notification's read deadline now covers only the first byte of a message. Previously it covered the body too, so a message straddling the 100ms poll boundary left the stream parked mid-message, and the swallowed DeadlineExceededError meant the loop resumed reading body bytes as a header. It also no longer runs _clear_read_deadline! on a socket that the message-length check just closed, which replaced the protocol diagnostic with a Reseau-internal NetClosingError. - DataRow's column count must equal the described column count, not merely not exceed it: a short count left the caller's row partly unfilled. - extract the cancel TLS-upgrade decision into cancel_sslmode and test it directly — the end-to-end test could not pin it (mutation-verified: the SSL fixture always offers TLS, so removing the upgrade changed nothing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Review follow-up at The valid scalar zero value now reads as The requested lossless boundary is not complete. A live |
The previous commit pinned DateStyle/IntervalStyle via the startup `options` parameter, which is a deployment regression: connections that set no statement_timeout previously sent no `options` at all, and poolers such as pgbouncer reject `options` unless it is explicitly allowlisted — so working setups would start failing at connect. The server already reports both settings in its startup ParameterStatus, so the driver now corrects them with a SET only when they actually differ from what the text parsers require. A default server pays nothing and its startup packet is unchanged; a server or role configured otherwise is fixed up on the one connection that needs it. Covered by a test that sets the database default to 'German, DMY' / 'sql_standard' and asserts a fresh connection still decodes timestamps and intervals correctly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
[P1] Do not pass a trim check that did not compile or run On exact head The failures are also not all one StructUtils construction error. The verifier reports Base64/SCRAM paths (errors 1-8 and 31-38), typed StructUtils sinks, and Dates/Base formatting paths. Thus the current green job cannot establish the PR description trim-compile claim and would allow many new errors before it failed. Please make the supported contract explicit. If trim-safe compilation is a 1.0 claim, require zero verifier errors, a successful compiler exit, and execution of the produced binary against PostgreSQL. If it is not a 1.0 support claim, mark this as an expected-failure or unsupported lane and remove the statement that the trim-compile check passes. An error budget must not count a failed compile as a passing integration test. |
|
[P1] Preserve PostgreSQL microseconds for Exact-head live repro on PostgreSQL 16: SELECT
make_time(12,34,56.123456) AS t,
ARRAY[make_time(12,34,56.123456)] AS ta,
make_interval(secs => 0.123456) AS i;The driver returns
The fixed-position date parser also silently changes eras. There is a second silent timestamp error for real historical zone offsets. After |
|
[P1] Do not reject valid three-dimensional built-in arrays On exact head SELECT ARRAY[[[make_date(2024,1,2)]]];
SELECT ARRAY[[[to_jsonb(1)]]];These are valid standard PostgreSQL values. The limit also affects the other built-in arrays that use Please make all registered built-in array parsers handle every PostgreSQL array dimension. If trim compilation cannot support that implementation, document and enforce the reduced support contract explicitly instead of calling valid built-in values exotic only in a source comment. Add a live three-dimensional regression for at least one direct parser and one OID-based parser. |
|
[P1] Decode doubled quotes in composite fields PostgreSQL emits a quote inside a composite field by doubling it. This live value has four fields: CREATE TYPE pg_temp.codex_comp AS (a text, b text, c text, d text);
SELECT ROW('a"b', E'c\\d', 'x,y', '')::pg_temp.codex_comp::text;The server text is The quoted-field loop treats every |
- align_session_formats! treated an unreported IntervalStyle as already
correct while treating an unreported DateStyle as needing a fix. A pooler
that doesn't forward IntervalStyle (pgbouncer before 1.21, or without it in
track_extra_parameters) against a server set to sql_standard therefore left
every interval decoding to zero, silently — in exactly the deployment the
ParameterStatus approach was chosen to support. Both now correct an absent
value, and the recorded server parameters are updated to match.
- the pool reset only saw transactions opened through start_transaction, so a
raw DBInterface.execute(conn, "BEGIN") sailed through: the next borrower
inherited the open transaction and its commit committed the previous
borrower's abandoned writes. The connection now also tracks the server's own
ReadyForQuery transaction status, which sees transactions however they were
opened, and the pool consults both.
- empty sslmode is no longer treated as unset. libpq rejects it, and the
previous commit's "empty means unset" reasoning is wrong here specifically:
an unexpanded ${PGSSLMODE} intended as verify-full would have silently
become the unauthenticated default. It fails loudly again.
- a failing rollback in the transaction wrappers no longer replaces the error
that caused it, which was still losing the SQLSTATE when the body failed
because the session died.
- @transaction binds its connection expression once: `@transaction
acquire(pool) ...` previously acquired a different connection for the
BEGIN, the COMMIT and the ROLLBACK, and leaked pool permits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
[P2] Make The public signature accepts any Live exact-head repro: @enum CodexMood sad ok happy
Postgres.register_enum!(conn, "codex_mood"; schema=temp_schema, julia_type=CodexMood)
row = only(Tables.rowtable(DBInterface.execute(conn,
"SELECT 'ok'::pg_temp.codex_mood AS mood")))
|
|
[P1] Prevent callback queries from interleaving the active protocol stream Notice and notification callbacks run synchronously before the active query reaches I reproduced this on exact head with a style whose first Please defer user callbacks until the current exchange is back at a safe message boundary, or reject same-connection reentry before any bytes are written while preserving the outer stream. Apply the rule to notice and notification callbacks in execute, cursor, COPY, setup waits, and notification waiting. Add a callback that attempts a same-connection query and prove it cannot corrupt or hang the active connection. Document whether callback reentry is supported. The guard must cover all user code invoked during result consumption, not only style hooks. A registered type-parser closure or custom StructUtils lift can also capture the connection and reenter while |
|
[P2] Define the array-type support boundary before 1.0 The manual says that arrays map to Julia arrays, but the default registry covers only selected array OIDs. Exact-head live results for common built-in types are raw Internal |
|
[P2] Accept ordinary Julia matrices as PostgreSQL array parameters Parameter encoding only dispatches on Exact-head live repro: matrix = reshape(Int32[1, 2, 3, 4], 2, 2)
DBInterface.execute(conn, raw"SELECT $1::int4[]", (matrix,))The driver sends The manual states that arrays map to Julia arrays. Please encode rectangular |
|
[P2] Make the public The driver returns standard range columns as its public Exact-head live round trip: r = only(Tables.rowtable(DBInterface.execute(conn,
"SELECT int4range(1, 3, '[)') AS r"))).r
DBInterface.execute(conn, raw"SELECT $1::int4range", (r,))The bound text is Please serialize |
|
[P1] Do not turn a logger failure into an apparent query failure after commit The success-side I reproduced this on exact head with a custom style whose logger throws only for a successful This can make application retry logic duplicate a committed write. It affects direct and prepared execute plus both COPY wrappers. Please isolate logger failures from database outcome reporting. Either contain and report callback errors separately, or define an explicit callback-failure policy that cannot label a completed query as failed. Add a write regression that throws in the success logger and proves the caller cannot confuse the callback failure with a server/transaction failure. |
|
[P1] Do not let a cursor commit a transaction started with raw SQL
Live exact-head sequence:
Please make ownership distinct from server transaction state. |
|
[P1] Do not allow a pre-authentication peer to request a 1 GiB allocation The new This is reachable even when the caller requested Please use a small, message-specific cap for pre-authentication and diagnostic/control messages. For potentially large authenticated result messages, use a deliberate configurable limit or bounded/streamed handling. Add a fake-server test that declares a large SSLRequest error body but sends no body, and prove the client rejects the header without allocating in proportion to it. I did not execute the full-size allocation because that would risk the review host. |
|
[P2] Define parameter encoding for the JSON value type that queries return
Exact-head live read-then-bind: j = only(Tables.rowtable(DBInterface.execute(conn,
"SELECT '{\"a\":1}'::jsonb AS j"))).j
DBInterface.execute(conn, raw"SELECT $1::jsonb", (j,))The driver sends text beginning Please encode the public JSON result type as JSON, or document it as read-only and provide the exact supported JSON parameter forms. This and |
|
[P1] Do not send The format-alignment change correctly avoids startup Exact-head live test through PgBouncer 1.25.2 in transaction mode: DBInterface.connect(Postgres.Connection,
"127.0.0.1", "postgres", "postgres";
port=56432, dbname="postgres", sslmode="disable",
statement_timeout=1234)Connection setup fails with FATAL SQLSTATE A post-authentication session Please define a pooler-safe contract. If connection-level enforcement is promised through transaction pooling, apply it at a boundary that follows each backend assignment, such as a transaction-local/query boundary, and verify it cannot leak. Otherwise detect and document the unsupported combination instead of reporting a value the server is not enforcing. Preserve the configured behavior across reconnects. Test both startup and |
|
Temporal follow-up: handle PostgreSQL's valid The earlier precision finding is not the only valid Please include this in the explicit temporal support boundary. Use a lossless representation if |
|
[P2] Make the documented exception taxonomy true for built-in conversions The PR description and Exact-head examples include:
Please define the 1.0 exception boundary precisely. Wrap driver-owned decoding and protocol validation failures in |
Review checkpoint —
|
Round-12 review findings, all verified against a live server:
- register_range! was advertised-but-broken for element types outside the
six builtins: registration succeeded, then every value threw. The parser
now binds the element type discovered at registration time.
- The DateStyle correction set 'ISO, MDY' unconditionally, silently
reinterpreting ambiguous input literals like '01/02/2020' for a
DMY-configured database (and hard-erroring on '13/02/2020'). Only the
output-format half is corrected now; the configured field order is kept.
- statement_timeout moved from the startup `options` parameter to a
post-connect SET: pgbouncer rejects unknown startup options outright, so
sending it there failed the whole connection.
- parse_interval silently returned a zero interval for text it didn't
understand (sql_standard, iso_8601, postgres_verbose renderings). Only a
genuine "00:00:00" decodes to zero; anything unrecognized now throws,
naming the IntervalStyle requirement.
- BC dates decoded silently as AD -- a different year numbering with no
year zero. They now throw, including the timestamptz rendering where
" BC" follows the zone offset, out of sight of the datetime parser.
- Years beyond 9999 misparsed into "Month: NNNN out of range" errors; the
year field is scanned rather than assumed 4 digits wide.
- "char" values for high-bit bytes arrive as backslash-octal escapes
("\377") and decoded to '\\'; they now decode to the byte value, on the
OID path, the typed-struct lift, and the array element path alike.
"char"[] (oid 1002) was unregistered entirely and returned raw literals.
- libpq keywords that are accepted-but-ignored now warn when set to a
value that requests a security or connection-selection behavior
(channel_binding=require, sslcrl, sslcrldir, requiressl,
target_session_attrs, options); silently dropping them left callers
believing a protection was in place.
- A raw-SQL transaction's tracked server status survived reconnect,
triggering a spurious ROLLBACK warning on the fresh session.
- Documented the session-format requirement (ISO / postgres), the values
with no Julia representation (infinity, BC, numeric NaN), and that a
'\0' read from a "char" column cannot be bound back as text.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Exact-head The new post-connect Please keep Parse/Bind/Execute ownership atomic across the pooler and either implement a safe session-setting strategy or reject transaction-pooling use clearly before any query. This head remains NOT READY. |
Round-13 review findings, all verified against a live server:
- Driver transaction helpers consulted only the client-side flag, so over
a transaction opened with raw SQL (execute(conn, "BEGIN")) they issued a
BEGIN the server ignored and their commit committed the caller's
transaction -- the caller's own ROLLBACK then silently no-oped.
start_transaction now nests inside a raw transaction with a savepoint
(tracked by owns_base_transaction); the outermost driver commit releases
that savepoint and rollback rolls back to it, leaving the caller's
transaction open and under their control. cursor treats the
server-reported transaction as "already in one", as its docstring says.
- Range bounds ending in a multibyte character threw StringIndexError
(byte-index slicing): every textrange('α','ω')-style value was
undecodable after a successful registration.
- Timezone offsets carrying a seconds field ("+05:21:10", the rendering of
LMT-era timestamps in named zones) silently lost the seconds.
- The variable-width year scan accepted fewer than 4 year digits, so
"03-04-2020" -- Postgres-style output after a mid-session SET DateStyle
-- silently decoded as year 3 where the old fixed-width parser threw.
The year is now bounded to 4..9 digits and both separators are checked,
which also closes an Int-overflow and short-input BoundsErrors on
adversarial input.
- An unreported DateStyle (a pooler not forwarding ParameterStatus) was
corrected to 'ISO, MDY', force-flipping DMY sessions; setting just 'ISO'
preserves the server-side field order we can't see.
- server_in_transaction went stale on every failed statement: error paths
skipped the status copy, so a failed COMMIT left it true and drew a
spurious ROLLBACK ("no transaction in progress") on the next pool
release. The status is recorded in a finally on the prepared path and
published through a Ref on the simple-query path.
- postgres's valid time '24:00:00' threw a raw ArgumentError mid-decode;
it now reports PostgresInterfaceError like other unrepresentable values.
- register_enum!/register_composite!/register_range! register the type's
array OID alongside it, so mood[]/composite[]/range[] values decode
instead of returning the raw literal string. register_range! documents
that element types must be registered before ranges over them.
- The reconnect test now triggers checkconn directly: the previous version
passed even without the fix because the follow-up statement refreshed
the flag from its own ReadyForQuery.
Mutation-tested: the raw-transaction savepoint base, the cursor ownership
check, and the checkconn reset each fail their new tests when reverted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix transaction and statement ownership, make unnamed execution safe through transaction-mode PgBouncer, harden protocol and TLS handling, enforce the supported type and DSN contracts, and add the release validation matrix and support policy.
This closes the correctness, protocol, security, compatibility, and packaging blockers found during the 1.0 review.
What changed
"char"decoding and round trips across PostgreSQL 14 through 18.LICENSE, runnable first-install examples, and an explicit 1.0 support policy.--trimis explicitly outside the 1.0 support boundary.Exact-head validation
Candidate:
70486441be2be067c3001f68eab2efcbabc6b276Pkg.add("Postgres")passed the README quick start.Post-1.0 support expansion is tracked in #6.
Co-authored by Claude Code
Co-authored by Codex