1.0 release readiness: protocol fixes, org-transfer cleanup, API polish - #5
1.0 release readiness: protocol fixes, org-transfer cleanup, API polish#5quinnj wants to merge 17 commits into
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>
| Postgres.get_server_parameter(conn, name) -> Union{String, Nothing} | ||
|
|
||
| Return the server-reported value of runtime parameter `name` (e.g. | ||
| `"server_version"`, `"TimeZone"`), or `nothing` if the server has not reported it. |
There was a problem hiding this comment.
[P1] Keep reported server parameters current after SET
The getter only reflects startup values because the execution paths discard ParameterStatus (S) messages. I reproduced this against PostgreSQL 16: get_server_parameter(conn, "application_name") returned ""; after DBInterface.execute(conn, "SET application_name = 'review_changed'"), SHOW application_name returned review_changed but this getter still returned "". PostgreSQL sends ParameterStatus for reported GUC changes during normal query execution. Please parse those messages into conn.server_parameters in both extended and simple-query paths, and add a regression test. A public 1.0 API must not return stale server state.
There was a problem hiding this comment.
c1ae7ba makes this stale cache observable immediately after connect. I created a role with DateStyle = SQL, DMY and IntervalStyle = iso_8601. align_session_formats! correctly changes the live session, and date/interval results parse. However, it runs SET through the simple-query reader, which discards the resulting ParameterStatus messages. The new connection therefore reports get_server_parameter(..., "DateStyle") == "SQL, DMY" and IntervalStyle == "iso_8601" while current_setting is ISO, MDY / postgres.
Please route all S messages through one connection-aware updater, including these post-auth alignment queries. The new regression should assert both current_setting and the public getters against the non-default database.
| """ | ||
| function release(pool::ConnectionPool, conn::Connection) | ||
| if pool_isvalid(conn) | ||
| Pools.release(pool.pool, conn) |
There was a problem hiding this comment.
[P1] Do not return an open transaction to the pool
pool_isvalid checks only the socket, so release puts a connection with an active or failed transaction back into circulation. I reproduced this with a limit-1 pool: borrower A began a transaction, inserted 42, and released; borrower B received the same connection, in_transaction was still true, and it could read A’s uncommitted row. It could also commit or roll back A’s work. On release, fully roll back a transaction before reuse, or close/drop the connection if cleanup fails. Do not rely only on the current manual flag: a borrower can also issue raw BEGIN through DBInterface.execute, while the driver currently discards the transaction-status byte in ReadyForQuery. Track that server status or reset the session defensively. Add regression tests for both helper-started and SQL-started transactions. This cross-borrower state leak blocks a 1.0 release.
There was a problem hiding this comment.
A second transaction-status failure causes a false successful commit. At exact head e8451c0, I ran Postgres.transaction, inserted one row, caught a SELECT 1/0 error inside the callback, and returned :reported_success. PostgreSQL marked the transaction failed, so its later COMMIT command completed as ROLLBACK. The helper returned :reported_success, Postgres.in_transaction(conn) became false, and the table contained zero rows.
This is silent data loss: the public transaction helper reports success although nothing committed. Please track the ReadyForQuery status byte and/or validate the transaction-control CommandComplete tag. commit must not report success when the server reports ROLLBACK. Add the caught-inner-error case for Postgres.transaction, DBInterface.transaction, @transaction, and manual start_transaction/commit.
There was a problem hiding this comment.
Retested pool release at exact head 007ceaa. The new cleanup fixes the helper-started case: a connection released after Postgres.start_transaction is rolled back, the next borrower sees in_transaction == false, and the abandoned row is absent.
The raw-SQL case from the original finding remains. I acquired the limit-1 pool connection, ran BEGIN and an insert through DBInterface.execute, then released it. The next acquire returned the same object, local in_transaction was false, and the uncommitted row was visible. reset_pooled_connection! returns early because the driver still discards the ReadyForQuery transaction status. Please keep this open until release is safe for both helper-started and SQL-started transactions.
There was a problem hiding this comment.
Retested on eab05bd. The new ReadyForQuery state does not fix this case because both a successful COMMIT and PostgreSQL rewriting COMMIT to ROLLBACK end with status I.
All three helpers still return :body_returned as success after the callback catches SELECT 1/0; the inserted row count is then 0. Please validate the simple-query CommandComplete tag for transaction control. A top-level commit must receive COMMIT, not ROLLBACK. Keep the Ready status for pool cleanup, but it cannot prove commit success by itself.
|
|
||
| """ | ||
| Postgres.cursor(conn, sql, params=nothing; fetchsize=1000) -> Cursor | ||
| Postgres.cursor(stmt, params=nothing; fetchsize=1000) -> Cursor |
There was a problem hiding this comment.
[P1] Start a transaction for the advertised Statement cursor overload
The cursor(stmt, ...) overload does not start or require a transaction. Outside an explicit transaction, PostgreSQL ends the implicit transaction at the first Sync and drops the suspended portal. I reproduced the documented overload with SELECT generate_series(1, 5) and fetchsize=2: it returns rows 1 and 2, then fails with SQLSTATE 34000, portal ... does not exist. Please make this overload use the same owned-transaction setup and failure cleanup as cursor(conn, ...) (or reject it unless a transaction is already active), and add a multi-batch regression test for the Statement form.
| """ | ||
| Postgres.set_statement_cache_maxsize!(conn, maxsize) | ||
|
|
||
| Set the maximum number of prepared statements the connection caches (LRU |
There was a problem hiding this comment.
[P2] Enforce cache policy when a removed Statement is reused
An evicted Statement remains cached=true. checkstmt re-prepares it and inserts it into conn.statements without running LRU eviction. I reproduced this with statement_cache_maxsize=1: prepare SELECT 1, prepare SELECT 2 (cache size 1), then execute the retained first Statement; the cache grows to 2. The same path can repopulate the cache after clear_statement_cache! or when the maximum is 0. Repeating this with retained handles can grow the cache without the documented bound. Please route reinsertion through the active cache policy, or mark removed statements uncached, and add retained-handle regression cases for eviction and disabled caching.
There was a problem hiding this comment.
There is a second retained-handle failure when the SQL key is repopulated by a different Statement. With cache size 1, I prepared SELECT 1 as old, evicted it with SELECT 2, then prepared SELECT 1 again as a new object. Executing old sees the SQL key and skips re-prepare, but its server name was closed; PostgreSQL returns SQLSTATE 26000, prepared statement does not exist. The validity check must require conn.statements[stmt.sql] === stmt, not only haskey.
There was a problem hiding this comment.
A second exact-head case shows that stale Statement handles also break the configured cache bound.
With statement_cache_maxsize=1, I prepare A and then B, which evicts A. Executing the still-live A handle makes checkstmt reprepare it and insert it directly into conn.statements without eviction:
AFTER_EVICTION: size=1 keys=[B]
STALE_A_RESULT: 1
AFTER_STALE_REUSE: size=2 max=1 keys=[A, B]
It also defeats disabling the cache:
set_statement_cache_maxsize!(conn, 0)
AFTER_DISABLE: size=0
DBInterface.execute(old_b)
AFTER_REUSE_DISABLED: size=1 max=0
Please make stale-handle reuse follow the active cache policy. It must not insert when the max is zero, and it must evict before insertion when the cache is full. Tests should keep old handles across LRU eviction, clear_statement_cache!, and set_statement_cache_maxsize!(..., 0).
There was a problem hiding this comment.
A third handle-lifetime failure is independent caller aliasing. At exact head e8451c0, two DBInterface.prepare(conn, "SELECT 1") calls return the same mutable object (s1 === s2). Closing s2 makes DBInterface.execute(s1) throw statement has been closed.
The standard DBInterface function form makes this happen without an explicit second handle: hold s3 = prepare(conn, "SELECT 2"), then call DBInterface.execute(f, conn, "SELECT 2", nothing). DBInterface prepares internally and closes that handle in finally; because the cache returns s3 itself, the call succeeds but leaves the independently held s3 closed.
Please separate cache ownership from caller handle ownership, or add correct lease/reference tracking. Independent prepare calls and generic function-form execution must not close each other. Add both exact regressions alongside the eviction/stale-handle cases.
There was a problem hiding this comment.
A fourth cache-disabled path leaks server statements through the standard DBInterface API. With statement_cache_maxsize=0, I called connection-form DBInterface.executemany(conn, sql, params) five times. The generic DBInterface method calls this driver prepare method, but it never receives or closes the uncached handle. pg_prepared_statements grew from 0 to 5 while get_cached_statements(conn) stayed empty.
Please override the connection-form bulk path or otherwise guarantee that an uncached internally prepared statement is closed in finally. Add repeated executemany(conn, ...) and executemultiple(conn, ...) cases with cache size zero, checking the live server catalog, alongside the stale-handle and alias-lifetime regressions.
There was a problem hiding this comment.
The public cache contract is also inverted. The manual says Postgres.jl caches prepared statements internally and exposes statement_cache_maxsize, but three repeated DBInterface.execute(conn, "SELECT 42") calls leave get_cached_statements(conn) at size 0. Connection-form execute always uses the unnamed statement and bypasses the LRU. The cache is used only by explicit prepare calls, which is what creates the independent-handle aliasing and close-lifetime defects in this thread. Please settle the 1.0 ownership design: use an internal cache for connection-form execution if that is the documented feature, while explicit prepare returns independently closeable handles, or rewrite the docs and option names to describe exact behavior. Add repeated direct-execute cache hit/eviction tests.
| connection is found dead (default `false`; never reconnects mid-transaction) | ||
| - `style`: a custom [`AbstractPostgresStyle`](@ref Postgres.API.AbstractPostgresStyle) | ||
| for query logging / notice / notification behavior | ||
| - `debug`: log wire protocol messages. Authentication messages are redacted, |
There was a problem hiding this comment.
[P2] Apply connection-level debug to normal prepare and execute calls
conn.debug is not combined with the per-call debug flag in DBInterface.prepare, either DBInterface.execute overload, or their describe/exec calls. I verified this with a TestLogger: a connection created with debug=true produced zero wire-log records for DBInterface.execute(conn, "SELECT 1"), while the same call with debug=true explicitly produced 19. This also means the newly honored ConnectionParams.debug field does not affect the main query path. Please use debug || conn.debug consistently and add a log-capture regression test.
| `sslservername`, `statement_timeout`, `statement_cache_maxsize`, `debug`, and | ||
| `reconnect`. | ||
| """ | ||
| struct ConnectionParams |
There was a problem hiding this comment.
[P1] Redact the password when ConnectionParams is displayed
The default struct display prints the password verbatim. For example, repr(parse_dsn("postgresql://alice:s3cr3t@localhost/db")) currently includes "s3cr3t". This makes a public configuration value leak credentials when it is the last REPL expression, logged, or included in an error report, despite the wire-debug redaction in this PR. Please define redacted show methods for ConnectionParams and add a test that the password never appears in either compact or text/plain output.
| Prefer query parameters (`\$1`, `\$2`, ...) over literal interpolation | ||
| whenever possible — parameters are never parsed as SQL. This helper assumes | ||
| the server's `standard_conforming_strings` is `on` (the default since | ||
| PostgreSQL 9.1); with it turned off, backslashes in the literal are escape |
There was a problem hiding this comment.
[P1] Make escape_literal safe for every supported string mode
Documenting this assumption does not make the public escaping helper safe. I reproduced a predicate injection on PostgreSQL 16 after SET standard_conforming_strings=off: an input containing a backslash, quote, and OR true -- escapes the first generated quote, and a query whose original WHERE condition is false returns a row. Please emit an escape-string literal with both backslashes and quotes handled, or make escaping connection-aware, so the result is safe independent of this session GUC. Add the off-mode regression test. Parameters remain preferred, but an API named escape_literal must not have a server-supported injection mode.
| elseif mt == UInt8('C') | ||
| skipbytes!(socket, len) | ||
| elseif mt == UInt8('G') | ||
| # a second CopyInResponse (multi-statement query string): the |
There was a problem hiding this comment.
[P2] Reject every extra statement, not only a second COPY FROM
The single-statement guard detects only another CopyInResponse. Other statements are still executed and silently drained. I reproduced copy_from(conn, "COPY t FROM STDIN; COPY t TO STDOUT", "7\\n") returning success after discarding the second copy’s output. I also reproduced copy_to(conn, "COPY t TO STDOUT; COPY (SELECT 8) TO STDOUT") returning the ambiguous concatenation "7\\n8\\n". Track command/statement boundaries and reject any response sequence after the one expected COPY operation, while still draining safely. Add mixed-direction and two-copy-out cases to the regression suite.
| actual_maxsize = isnothing(statement_cache_maxsize) ? params.statement_cache_maxsize : statement_cache_maxsize | ||
| connector = () -> DBInterface.connect(Connection, params; debug=debug, reconnect=reconnect, statement_cache_maxsize=actual_maxsize) | ||
| return ConnectionPool(connector; limit=limit) | ||
| function ConnectionPool(dsn::String; debug::Union{Bool, Nothing}=nothing, reconnect::Union{Bool, Nothing}=nothing, statement_cache_maxsize::Union{Integer, Nothing}=nothing, limit::Integer=10, style::API.AbstractPostgresStyle=PostgresStyle()) |
There was a problem hiding this comment.
[P1] Make closing a pool terminal and handle checked-out connections
DBInterface.close!(pool) only closes connections currently in the idle values list. It records no closed state. I reproduced this by acquiring the sole connection, closing the pool, then releasing and acquiring again: the checked-out connection remained open, the closed pool accepted it, and acquire returned the same live connection. Please add pool lifecycle state: reject new acquires after close, close idle connections immediately, and close checked-out connections when they are later released. Add this checked-out-close regression test.
| Postgres.register_composite!(conn, name; schema="public") | ||
|
|
||
| Look up the composite type `schema.name` on the server and register it so | ||
| values are returned as `NamedTuple`s with the composite's field names. |
There was a problem hiding this comment.
[P2] Do not promise arbitrary julia_type conversion without a parser
Only Symbol gets a parser. For any other custom julia_type, the registry advertises that type but parse_value falls through and returns the wire String. I verified this directly with a wrapper type: type_info(...).julia_type was the wrapper while the parsed value type was String. Either restrict and document this keyword to the actually supported choices, or accept/build an explicit parser for custom enum types. Add a test that the declared row schema and actual value type agree.
There was a problem hiding this comment.
This also affects the generic register_type! contract. Its new doc says that omitting parser returns String, but TypeInfo still records the caller supplied julia_type, so Tables.schema advertises that type while row values are strings. Please make the stored schema type match the parser result, or require a parser for non-String registrations. Cover both register_enum! and register_type! with Tables.schema plus actual-value assertions.
There was a problem hiding this comment.
6323566 makes the schema truthful only after a non-NULL value is parsed. The contract is still inconsistent for empty and all-NULL results. With register_type!(conn, 23, Wrap) and no parser, a nonempty result now has schema String, an empty result has schema Wrap, and an all-NULL result has schema Union{Missing, Wrap} even though no Wrap value can ever be produced. Please fix the registry metadata at registration time instead of inferring the parser result from observed rows.
- 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>
|
|
||
| const DATETIME_OPTIONS = Parsers.Options(dateformat=dateformat"yyyy-mm-dd HH:MM:SS.s") | ||
|
|
||
| @inline function tzoffset_seconds(offset::AbstractString) |
There was a problem hiding this comment.
[P1] Control or honor DateStyle before parsing fixed-layout dates
The date/time parsers assume ISO output, but the connection neither requests ISO nor parses according to the server-reported DateStyle. This can be a role or database default, not only a user change. I reproduced it with SET DateStyle = 'SQL, DMY'; selecting DATE '2026-08-06' then throws Month: 2552 out of range because the wire value is 06/08/2026. Please establish the required text format during startup (and cover other parser assumptions such as IntervalStyle), or dispatch parsing from current server parameters. Add a connection whose initial role/database settings are non-default, not only a post-connect SET test.
There was a problem hiding this comment.
Additional live cases show that ISO alone is not enough. SELECT make_date(10000,1,2) fails with Month 2530 out of range. SELECT make_date(-1,1,2) silently returns 0001-01-02 AD even though PostgreSQL sent a BC date. The valid date value infinity also throws a generic parser error. Please define the 1.0 support contract for PostgreSQL date/timestamp ranges and special values, then parse supported values without silent era changes. Add these cases with the DateStyle tests.
There was a problem hiding this comment.
More live temporal failures confirm this needs a full format contract. With IntervalStyle=sql_standard, a valid interval fails while parsing +1-2; with IntervalStyle=postgres_verbose, it fails on @. Valid TIME 24:00:00 throws because Julia Time rejects hour 24. A historical timestamptz whose server text ends in -04:56:02 is silently converted as if the offset were -04:56, producing a value two seconds early. Please include IntervalStyle, the PostgreSQL 24:00 boundary, and offset seconds in the temporal support decision and regression matrix.
There was a problem hiding this comment.
There is also silent subsecond loss in the default temporal mappings. Live TIME 12:34:56.123456 returned Time(12, 34, 56, 123), even though Julia Time can hold finer precision. TIMESTAMP 2026-01-02 12:34:56.123456 returned a millisecond DateTime. Please preserve all representable time precision, and make the timestamp/timestamptz precision limit explicit or use a lossless representation. Add microsecond round trips so the 1.0 behavior is deliberate rather than silent.
There was a problem hiding this comment.
One IntervalStyle case is silent data corruption, not only a parse failure. With IntervalStyle=iso_8601, PostgreSQL sent P1Y2M3DT4H5M6S for a nonzero interval. parse_interval found no recognized periods and returned Dates.Millisecond(0). Please ensure unsupported formats fail closed if they are not parsed, and test that no valid nonzero interval can become zero.
There was a problem hiding this comment.
Behavior is fixed at 0da51f2 for initial role/database defaults. I created a login role with DateStyle = SQL, DMY and IntervalStyle = iso_8601, then connected through Postgres.jl. The startup options overrode both to ISO, MDY and postgres; a date and a compound interval parsed correctly.
Please strengthen the committed regression before resolving this. It currently asserts those two settings against an ordinary PostgreSQL image whose defaults are already ISO, MDY and postgres, so the test passes even if the new startup options are deleted. Configure a non-default role/database in the test as the original finding requested, then assert both status values and decoded data. Also document that changing these required formats after connection is unsupported or detect it: SET DateStyle TO SQL, DMY still makes the next valid date fail in the parser.
There was a problem hiding this comment.
Exact-head follow-up at eab05bd: the initial-default case is fixed, but a valid later SET still breaks both the decoder and the public state API. After SET DateStyle = SQL, DMY, get_server_parameter(conn, "DateStyle") still reports "ISO, MDY", while SELECT DATE "2026-08-06" throws Month: 2552 out of range. After SET IntervalStyle = sql_standard, the getter still reports "postgres", while a normal mixed interval throws invalid base 10 digit in "+1-2". Please either maintain the enforced formats after every ParameterStatus change or parse according to the live value. The getter must report the server value. Add post-connect SET regressions for both settings, not only non-default initial role/database settings.
| options): | ||
|
|
||
| - `dbname`, `port`, `application_name` | ||
| - `connect_timeout` (seconds), `statement_timeout` (milliseconds) |
There was a problem hiding this comment.
[P1] Pin client_encoding to UTF8 for Julia strings
The startup message leaves client_encoding at the server, role, or database default, while every query and result is encoded/decoded as Julia UTF-8. I reproduced the data error after setting client_encoding=LATIN1: the string é appears to round-trip, but PostgreSQL reports length($1::text) == 2 because the two UTF-8 bytes were interpreted as two LATIN1 characters. A non-UTF8 role default causes this from the first query. Please request UTF8 in the StartupMessage and verify the returned ParameterStatus before normal traffic. Add a non-ASCII parameter/result test from a connection whose initial client encoding is non-UTF8.
| options): | ||
|
|
||
| - `dbname`, `port`, `application_name` | ||
| - `connect_timeout` (seconds), `statement_timeout` (milliseconds) |
There was a problem hiding this comment.
[P1] Apply connect_timeout to the full handshake
connect_timeout currently bounds only the TCP dial and TLS handshake. All pre-TLS, startup, and authentication reads can still block forever after a peer accepts the socket. I reproduced this with a local server that accepts and never answers: connect_timeout=1 was still blocked after a two-second timedwait, and returned only after I closed the accepted socket. Please install one connection-establishment deadline across SSL negotiation, StartupMessage, and authentication, then clear it when the connection becomes ready. Add stalled plain and TLS-negotiation server tests.
| return nothing | ||
| end | ||
|
|
||
| # PostgreSQL's own protocol maximum (PQ_LARGE_MESSAGE_LIMIT): no valid message |
There was a problem hiding this comment.
[P1] Treat a short discard as EOF instead of success
The skipbytes! helper immediately above ignores the count returned by readbytes! and subtracts the requested size. Reseau documents that this call can stop short at EOF, so truncated message bodies can be reported as fully consumed. A final expected message can therefore make an operation return success with a broken stream. Please require readbytes! to return the requested count (otherwise throw EOFError so the caller closes the socket), and add short-body tests for the helper and a final protocol message.
| end | ||
| # The deadline covers only the first byte: if it expires there, | ||
| # nothing of a message has been consumed and polling again is safe. | ||
| # Once a byte arrives the rest of the message is read without a |
There was a problem hiding this comment.
[P1] Keep the caller timeout after the first byte
Clearing the deadline here makes timeout ineffective once any byte arrives. A peer can send only the message-type byte, or a partial header/body, and wait_for_notification(timeout=...) will then block forever in the following reads. Keep the first-byte polling behavior, but after that byte set an absolute deadline for the caller’s remaining timeout while reading the rest. If that deadline expires mid-message, close the desynchronized socket and throw; only a timeout before the first byte can safely return nothing or poll again. Add a trickle test that sends one byte and stalls past the requested timeout.
There was a problem hiding this comment.
This commit does not resolve the blocker. It now documents that the caller timeout ends when a message starts, so a peer that sends one type byte can still block the call forever. There is also a direct contradiction in the new TLS note: timeout === nothing still installs a 100 ms read deadline on every poll, so the claimed blocking wait is exposed to the same split-record path. If TLS reads cannot resume after a transport deadline, the no-timeout path must use a true blocking read, and the finite-timeout path must not treat a deadline as safe unless it knows no TLS-record bytes were consumed.
| end | ||
| found == 0 && break | ||
| else | ||
| # any other message (notices, notifications, ...): discard the |
There was a problem hiding this comment.
[P1] Preserve asynchronous messages instead of silently dropping them
This fixes stream alignment but loses valid notifications and notices. I reproduced LISTEN, then NOTIFY from a second connection, then DBInterface.prepare on the listener: the prepare succeeds now, but wait_for_notification(timeout=0.2) returns nothing because this branch discarded the event. Custom notification/notice callbacks are also bypassed. Thread connection/style context through these setup waits and queue notifications for wait_for_notification while invoking the documented callbacks. Extend the regression test to assert delivery, not only that the next query survives.
| end | ||
| end | ||
|
|
||
| """ |
There was a problem hiding this comment.
[P1] Scope every information_schema join to the requested schema
The query matches key_column_usage only by table and column names. The following joins also omit catalog and schema keys. I reproduced this with schemas review_a and review_b. Each schema had parent(id) and child(id, parent_id REFERENCES parent). Postgres.describe(conn, "child"; schema="review_a") returned 12 rows instead of 2. The rows included constraints from both schemas.
Please join on table_catalog/table_schema, constraint_catalog/constraint_schema, and the relevant ordinal-position keys. Add a regression with duplicate table and constraint names in two schemas. Composite foreign keys also need position-aware joins so each local column maps to the correct referenced column.
There was a problem hiding this comment.
The query also needs a one-row-per-column invariant. In one schema, a column with both UNIQUE and FOREIGN KEY constraints already produces duplicate describe rows because the join emits one row per constraint. Keep c.ordinal_position, aggregate or use scoped EXISTS/lateral lookups, and ORDER BY ordinal_position. Add a column with multiple constraints plus a composite foreign key to the regression.
| @@ -1,6 +1,6 @@ | |||
| The Example.jl package is licensed under the MIT "Expat" License: | |||
| The Postgres.jl package is licensed under the MIT "Expat" License: | |||
There was a problem hiding this comment.
[P2] Use a machine-detectable MIT license file before 1.0
GitHub currently classifies this repository as Other with SPDX id NOASSERTION. This file wraps the license text as a Markdown blockquote and adds a custom lead sentence, so automated consumers do not recognize the standard MIT text.
Please use the canonical MIT license text as plain text, preferably in LICENSE, with only the copyright line customized. Verify that GitHub reports MIT after merge, or validate the file with a license detector before merge.
|
[P1] Preserve the transaction body error when cleanup also fails All three transaction helpers replace the primary failure with a rollback failure. At exact head Postgres.transaction(conn) do conn
close(conn.socket)
error("primary failure")
endThe result is not I reproduced the same replacement through Please make rollback best-effort during unwinding. Preserve the body exception as the primary error. If cleanup also fails, attach or report that secondary failure without replacing the primary cause. When the transport is already dead, clear the local transaction bookkeeping because the server-side transaction ended with the session. Add the same regression test for all three helpers. Retested at exact head This commit also exposes the same error-precedence bug on the normal commit path. With a Verified fixed at |
| closed_cleanly = false | ||
| try | ||
| @lock cursor.conn.lock begin | ||
| if !cursor.done |
There was a problem hiding this comment.
[P1] Close an exhausted named portal too
done means the Execute reached CommandComplete; it does not mean PostgreSQL dropped the named portal. This guard therefore makes DBInterface.close!(cursor) a no-op for a fully consumed cursor inside a caller-owned transaction.
I verified this at fa9bad1 with PostgreSQL 16: start a transaction, build cursor(stmt; fetchsize=2), consume all five rows, and query pg_cursors by cursor.portal. The portal count is 1 after exhaustion and remains 1 after DBInterface.close!(cursor). It disappears only when the transaction ends. Repeating completed cursors in a long transaction accumulates server-side portals even though callers close each cursor.
Please track portal closure separately from row completion and send Close(P) once for every named portal that is still registered, including exhausted ones. Add a live pg_cursors regression test that remains inside the transaction and observes the portal disappear immediately after close.
There was a problem hiding this comment.
Retested at exact head 2cf22bf. The named portal is still present in pg_cursors after full exhaustion and remains present after DBInterface.close!(cursor). I matched pg_cursors.name against the exact UUID in cursor.portal, so the inspection query portal is excluded. The new double-close guard is useful but does not address this resource leak.
- commit/rollback left in_transaction set when the statement failed, but the
server ends the transaction either way. The stale flag then made checkconn
refuse to reconnect ("reconnect disabled during transaction") forever, and
made the next cursor skip its BEGIN and die with "portal does not exist".
State is now cleared whether the statement succeeds, fails, or can't be
sent at all because the socket is already gone — the last case has to be
handled before checkconn, which would otherwise throw first.
- closing an already-closed cursor committed whatever transaction the caller
had open at that moment: owns_transaction was never cleared, so a second
close! ran the cleanup again, silently turning a later rollback into a
commit. The cursor now takes responsibility exactly once.
- an asynchronous FATAL error during wait_for_notification closes the socket
again, so the next use reports that error instead of a bare EOFError from a
connection the server had already dropped. Non-fatal errors and notice
callbacks still leave the connection usable.
- an empty debug=/reconnect= value means "unset", matching how the integer
parameters already treat an empty value from an unexpanded ${VAR}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'S' severity field is translated per the server's lc_messages, so comparing it against "FATAL" (as the notification path now does to decide whether the server is terminating the session) only works on an English server. PostgreSQL 9.6+ also sends 'V', the non-localized severity, which was being discarded. Error.severity now prefers it when present. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| using Postgres, DBInterface, Tables | ||
| conn = DBInterface.connect(Postgres.Connection, "host=127.0.0.1;user=postgres;password=postgres;dbname=postgres") | ||
| rows = Tables.rowtable(DBInterface.execute(conn, "SELECT $1::int AS val", (42,))) | ||
| rows = Tables.rowtable(DBInterface.execute(conn, raw"SELECT $1::int AS val", (42,))) |
There was a problem hiding this comment.
[P1] Make the documented first run work after the stated install step
Pkg.add("Postgres") followed by the quick-start import fails in a clean environment with ArgumentError: Package DBInterface not found in current path. DBInterface, Tables, and StructUtils are transitive dependencies, so users cannot import them directly unless they also add them to their environment. The manual has the same problem with using Postgres, Tables, and it closes conn before later snippets reuse it.
Please make the default quick start runnable after only Pkg.add("Postgres"). Use the DBInterface binding that Postgres intentionally exports and avoid Tables in the minimal first example. For examples that require Tables or StructUtils, state that users must add those optional integration packages. Please keep the package export surface narrow; exporting more dependencies is not the fix.
|
[P1] Make duplicate result labels compatible with Tables.jl PostgreSQL permits duplicate output labels, including the ordinary unlabeled query Please normalize result labels to stable unique symbols before constructing |
|
[P1] Do not leave The macro has only a At exact head Please make every exit path finish the transaction, and evaluate the |
- transaction/@transaction/DBInterface.transaction rolled back unguarded in
their catch. Since the previous commit clears transaction state when COMMIT
fails, that rollback threw "no transaction in progress" from inside the
catch and replaced the server's error — so a deferred-constraint violation
or a 40001 serialization failure raised at commit reached the caller as an
opaque client error with no SQLSTATE to retry on. Guarded on
in_transaction. (Regression from the previous commit; covered now by a
DEFERRABLE INITIALLY DEFERRED test across all three wrappers, which is the
test gap that let it through.)
- releasing a connection to the pool mid-transaction handed that transaction
to the next borrower: their start_transaction issued a SAVEPOINT instead of
BEGIN and their commit only decremented the depth, so their writes were
silently discarded and the backend sat idle-in-transaction holding locks.
The pool now rolls back before reuse, and drops the connection if it can't.
- cursor setup failure on a dead connection cleared no transaction state,
permanently blocking reconnect; it now clears it like the close path does.
- PANIC terminates the session as FATAL does, so it closes the socket too.
- an empty sslmode= (unexpanded ${PGSSLMODE}) means unset rather than an
invalid mode.
- docs: reconnect and debug ARE available as DSN options; sslcapath is a
fallback CA file ignored when sslrootcert is set.
- tests: the empty-boolean DSN case, the non-localized severity preference,
and a SQLSTATE check in place of a localized-message match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Review follow-up at The commit-failure half is fixed. I retested a deferred unique violation through The body-error half remains. In each helper, I closed the session socket and then threw |
Found by an adversarial sweep against a live server; all pre-existing. - tsrange/tstzrange were completely undecodable. PostgreSQL quotes any range bound containing whitespace, so every timestamp bound arrives quoted, and the quotes were passed straight to the element parser: selecting any tsrange threw "Month: 2530 out of range". Bounds are now unquoted (with their backslash escapes) first. The registry advertises these types, so they were advertised-but-broken. - a "char" column holding the zero value renders as an empty string, and the parser indexed position 1 unconditionally — so ordinary catalog queries like SELECT attidentity FROM pg_attribute threw BoundsError. Both the OID path and the typed-struct lift now yield '\0'. - the session now pins DateStyle=ISO,MDY and IntervalStyle=postgres. The text parsers only understand those formats; against a server or role configured otherwise every date and timestamp failed with an error that pointed nowhere near the cause. - infinity/-infinity timestamps and dates report that they can't be represented, matching how numeric NaN/Infinity is handled, instead of "invalid postgres timestamp". - a NotificationResponse shorter than its 4-byte pid no longer reads into the following message. 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 —
|
In-depth 1.0 readiness pass: wire-protocol and type-handling bug fixes, post-org-transfer cleanup, API polish, and test coverage for all of it. Developed through repeated adversarial self-review — twelve independent review rounds against a live PostgreSQL 16, each fed the previous round's fixes. Several findings below were bugs introduced by an earlier round's fix and caught by the next.
Local: 1180/1180 tests (Docker integration, TLS-certificate fixture,
--trimcompile check at 74/92 verifier errors). Docs build clean. Full CI matrix green.Silent data loss / corruption (all pre-existing on
main)]were truncated. PostgreSQL only quotes elements containing",\,{,},,or whitespace, so]arrives unquoted — and was treated as a terminator.["a]b", "x[1]", "plain"]round-tripped back as["a"], silently dropping two elements. File paths,x[1]-style names and embedded JSON all hit this.[0:2]={a,b,c}); the[was taken for an array-open bracket, soarray_fill(..., ARRAY[0]),arr[0:2]slices andarray_prependresults returned the dimension string (["0:2"]) or threw.tsrange/tstzrangewere 100% undecodable. Range bounds containing whitespace are quoted, and the quotes were passed straight to the element parser, so every timestamp range threwMonth: 2530 out of range. The type registry advertised these types.with_connectionblock that threw mid-transaction returned the connection with that transaction open; the next borrower'sBEGINbecame aSAVEPOINTand theircommitonly decremented a counter, so their writes were silently discarded while their code reported success. Connections are now reset on release, using both the client flag and the server's own ReadyForQuery status (so a rawexecute(conn, "BEGIN")is caught too)."char"zero values crashed catalog queries.SELECT attidentity FROM pg_attributethrewBoundsError.DateStylebroke all date parsing with an error pointing nowhere near the cause. The session now alignsDateStyle/IntervalStylewhen the server reports something the text parsers can't read — viaSET, not startupoptions, since poolers commonly reject the latter.Protocol correctness
copy_fromhung forever when the COPY statement errored beforeCopyInResponse; COPY misuse viaexecute/cursor/wrong-direction now aborts cleanly instead of deadlocking (with a freshSyncafterCopyFail, since the pre-sent one is ignored in copy-in mode).NOTIFYdelivered while the same connection ran queries desynced the stream and destroyed the connection —waitforhad no fallback branch and left the message body on the wire.wait_for_notificationnever worked over TLS (the deadline arrives wrapped in aTLSError), and could leave an expired read deadline set, permanently poisoning the connection.unsafe_string(pointer(...))parsing is GC-preserved and bounds-checked against the buffer actually received (readreturns a short buffer at EOF).COMMIT, permanently blocking reconnect and making the next cursor skip itsBEGIN.Security
cancel_query!sends the cancel key over TLS when the connection uses TLS, and refuses to send it in the clear underrequire/verify-full(throwing rather than failing silently).ssl_mode=verify-full(a typo) previously leftsslmodeunset and silently fell back to an unauthenticated connection. Real libpq keywords this driver doesn't implement are accepted and ignored, so provider-issued URIs keep working.Error.severityuses the non-localized field, so severity checks don't depend on server locale.API surface
binarykwarg is removed fromDBInterface.execute.ConnectionParams.debug/.reconnectwere silently ignored;sslservernameandstyleare now available on every connect and pool path.PostgresInterfaceError <: Exception; consistent taxonomy (server errors arePostgres.Errorwith SQLSTATE, client-side failures arePostgresInterfaceError).publicdeclarations for the supported surface on Julia 1.11+, and docstrings throughout.Org transfer / metadata
deploydocsand badges point atJuliaDatabases; LICENSE named Example.jl; README/docs showed a removedset_query_logger!API and$1examples that are a syntax error when copy-pasted. Dead code removed,Parserscompat tightened, unusedLoggingdependency dropped.🤖 Generated with Claude Code