New
query_timeout(#1278). Seconds a query may take, waiting for a connection and running together, before it is canceled and rejects withQUERY_TIMEOUT. Off by default. Unlikestatement_timeoutit also ends a query whose server or network has gone silent, which TCP alone takes many minutes to notice. Transaction control queries andforEachare limited too; cursors, streams andsql.listenare not.cancel_timeout, 5 seconds by default: how long a canceled query may take to end before its connection is closed.sql.batch([queries])sends queries in one round trip with a single Sync: outside a transaction they commit or roll back together, and the batch rejects with the failing query's position inerror.index.jsonon a transform (#1169). The built-in transforms rename the keys of JSON values in results but send JSON values as they are.json: falseleaves JSON values as they are both ways, andjson: truerenames their keys when sending too, with the transform'scolumn.to. Without it nothing changes.eager_types:truereads array types and domains when the pool starts instead of when a query first meets one.sql.identifier(...parts)(#1292). An identifier from its parts, each quoted as it is and joined with., for a name that contains a dot, whichsql(name)reads as a separator:sql.identifier('acme.inc', 'users')gives"acme.inc"."users". Thetransformis not applied. With no parts it throwsNO_PARTS.- Node.js 18 is supported again:
enginesis>=18and CI runs it. Nothing in Postgres.js needs 20;using,await usingand tracing are used where the runtime has them. Loading it withrequirestill needs Node.js 20.19 or 22.12. sql`...`.parsers(parsers)(#1294). A query's own parsers, for its rows only: by type,{ [oid]: parse }, on top of the instance'stypes, or a function that gets each column and returns the parser for it,nullfor the server's text. A library that knows how to decode some queries' columns can decode them once, inside the row reader, while every other query on the instance and its pool keeps the instance's parsers. Such a query receives every column as text, sobyteaand the integer arrays otherwise read in binary reach its parsers as text. A query without.parsers()reads its rows as before.
Fixed
sql.listenkeeps its connection after the lastunlisten()(#1298). The connection was closed so the process could exit (#1022), which made every followinglisten()connect again: about 3.5 ms instead of 0.1 ms perlisten. It now stays open without keeping the process alive once nothing is listened.PGSSLMODEis read (#1307). It was ignored, soPGSSLMODE=verify-fullconnected in plain text without an error. This may change how existing setups connect:requirenow uses TLS and fails against a server without it, andverify-caandverify-fullcheck the certificate against Node's CAs, not~/.postgresql/root.crtorPGSSLROOTCERT. A url or anssloption still comes first.- An insert helper after a quoted schema and an unquoted table builds the insert (#1304). In
insert into ${ sql('s') }.t ${ sql(row) }orinsert into "s".t, the quoted schema and the table were read as two words, so the row was taken for a list of identifiers and threwx.replace is not a function. sql.listenkeeps reconnecting when its first reconnect fails (#1285, #1286). After the listen connection closed, it was reopened once, and if that attempt failed, as it does while a restarting server still refuses connections, nothing tried again, soonlistenwas never called and notifications were lost. A failed reconnect is now retried afterbackoff, until it succeeds, the channel is unlistened or the pool ends.- A canceled query whose server never answers no longer waits forever.
.cancel(),.signal()and nowquery_timeoutsend a cancel request and waited for the server to end the query, so a server or network gone silent left both the query and the promise.cancel()returns pending until TCP gave up. This applies to every cancel, also withoutquery_timeout: when the server doesn't answer withincancel_timeout, the connection is closed and the pool connects again. Whether the queries on it ran is unknown, so they reject withCONNECTION_DESTROYED(a timed out one withQUERY_TIMEOUT) whosecauseisCANCEL_UNANSWERED, not with57014. The cancel request's own connection is limited bycancel_timeouttoo, and a cancel is no longer sent for a query whose result has already arrived, where it could only hit the query after it. A cancel request whose own connection fails, as over TLS to a server gone silent, no longer crashes the process with an unhandled rejection. A query that times out while it waits behind another one on its connection is canceled only after it has had its ownquery_timeoutonce its turn comes, instead of the moment its turn comes, when it had usually finished and the cancel stopped the next query; if the server sends nothing forcancel_timeoutwhile it waits, the connection is closed. A query waiting for types to be read rejects withQUERY_TIMEOUTwithout closing its connection, and the read of array types and domains is limited byquery_timeouttoo. sslrootcert,sslcertandsslkeyin a url are used (#1308). They were sent to the server as startup parameters, so?sslrootcert=systemfailed withunrecognized configuration parameter. Files are read intoca,certandkey. As in libpq, a root certificate withrequireorverify-cachecks the CA but not the host name, andverify-fullchecks both, as doesssl: true. A file without ansslmodemeansrequire.- A transaction the server ends rejects with the server's error (#1283, #1284). When a
transaction_timeout(25P04), anidle_in_transaction_session_timeout(25P03) orpg_terminate_backend(57P01) ended the session,sql.beginrejected withCONNECTION_CLOSED, so a timeout or a termination looked the same as a network drop; it now rejects with the FATAL error the server sent before closing, as a query in flight already did, and a later query on that connection rejects withCONNECTION_CLOSEDthat has it ascause. A server restart or failover ends sessions with57P01, so code that retries a transaction onCONNECTION_CLOSEDshould retry on that code too. A FATAL in answer toCOMMITrejects withCONNECTION_CLOSEDthat has it ascause, as the outcome is unknown, as it is when the connection is lost duringCOMMIT. - A server certificate is verified against an IP host (#1309). With
ssl: 'verify-full'orssl: trueand an IP as the host, a connection upgraded to TLS checked the certificate againstlocalhost, so a certificate forlocalhostfrom a trusted CA passed for any IP and a correct certificate for the IP failed. Direct TLS was not affected. - A savepoint or nested
beginrejects when its transaction's connection is lost (#1281, #1282). One whose callback was still running when the connection went resolved with the callback's result after the transaction had already rejected; it now rejects with the transaction's error. - A connection closed while the pool reads its array and domain types counts as a failed connection attempt (#1279). Reconnecting follows
backoffand waiting queries reject onceconnect_timeouthas passed, instead of reconnecting as fast as the server accepts for as long as it keeps closing. Withconnect_timeoutset to 0, null or false the pool now keeps trying for as long as it takes, as the option says, also after a refused or closed startup, which used to reject the waiting queries at once. - A query failed by a
FATALerror gives that error to the queries pipelined behind it as well, instead ofCONNECTION_CLOSED. - A FATAL error with the severity only in the
Sfield is recognised as one. The non-localizedVfield is optional in the protocol: PostgreSQL sends it since 9.6, and a pooler that builds its own errors, such as PgBouncer, may send only the localizedS. Such an error was not seen as FATAL, so the query, asql.beginand the queries pipelined behind it rejected withCONNECTION_CLOSEDwithout acauseinstead of with the error.error.severitynow falls back toseverity_localwhenVis missing. - Each query on a connection that closes gets its own error. They shared one error object, so every query pipelined on the connection reported the first one's
queryandparameters. - A cached prepared statement no longer keeps the query that prepared it (#1287). Each connection's entry for a prepared statement kept that query's values and rows for as long as the statement stayed cached, up to 1,000 statements per connection, so an app with many distinct statements grew by their results until every cache was full: about 1.1 GB with 10 connections and 50-row results. They are let go once the query settles. On 4.0.0,
prepare: falseavoids it. - Destroying a
copy ... to stdoutstream frees the connection (#1306). Afterdestroy(), breaking out offor awaitor a failedpipeline, the connection stayed paused for good and the queries behind it never ran. The rest of the copy is now read and dropped, so the connection is free once the server has sent it. The query is not canceled, so in a transaction the next query still succeeds.
Performance
- An
sslobject with acabuilds its TLS context once (#1280). The connections made with it share one context instead of each parsing the wholecalist again, so with a provider's CA bundle a new connection takes about half the time and CPU, and a burst of them no longer blocks the event loop. Replacing one of the object's values, as with a renewedcert, builds a new context for the connections after it. - A new statement whose untyped values are all numbers or
nullis prepared, described and run in one round trip, as one whose parameter types are all known is, since those values are written the same for any type the server picks; one with other values only the server can type is still described first, once per connection. - Array types and domains are read when a query first meets a type the pool does not know (an oid from 16384 up), instead of when the pool starts: a pool that only uses built-in types never sends that query, and a new pool's first query costs no more than any other. The connection that met the type waits for it, read on another connection. With custom types given by name they are still read at startup, as their oids are needed before the first query. Leases and values typed as a type the pool hasn't read yet still wait for it. A read whose connection fails is retried after
backoff, and onceconnect_timeouthas passed the connections waiting for it are closed and their queries reject, as for a failed startup.eager_types: truereads them when the pool starts, as before. timestamptz, andtimestampanddateread as UTC, are decoded straight from the bytes when they use the built-in parsers, with the text parser for anything else (BC,infinity, years before 1000); about 20% more rows per second for rows with timestamps.byteaand arrays ofint2,int4,int8andboolare fetched in binary once a prepared statement's columns are known on the connection, when they use the built-in parsers: the values are the same as from the text, about 4.5 times faster to read forbyteaand 3 times for anint4[]. A custom parser for the type or its elements, andraw(), still get the text.- A connection that becomes ready while others are still opening no longer takes the queries meant for those; 50 connections opened at once each run one of 50 queries.