Repository navigation
Replies: 1 comment
|
Three more reasons to drop describe-first (option D), from testing PR #177 on Hyperdrive As a reminder, describe-first works like this: the first time a connection runs a statement with a string, number or array value, postgres.js asks the server for the value types and waits for the answer before it sends the values.
Test runs: postgres.js, node-postgres. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Should postgres.js fetch PostgreSQL's type catalog at all? Dropping it would remove a recurring, billable cost and a whole class of connection-startup bugs, in exchange for an explicit cast or a
JSON.stringify()in a few places in user code.postgres.js downloads the array-type catalog on every new connection (
fetch_types: true, the default) so it can serialize and parse arrays by their server type. node-postgres (pg) never does. Below: what fetching buys, what it costs, and what users would do instead. Code links are pinned toc109bc3.What fetching the catalog buys
The query (
fetchArrayTypes) registers everypg_typewithtypcategory = 'A'. That gives exactly three things beyond the 22 built-in array types the fork already knows without it (builtInArrays):inet[],money[], …). Without a serializer for the OID the server reports, a JS array goes out as'' + x(a,b) and the server rejects it as a malformed array literal. That is the error people hit when they turn discovery off (Transactions always re-query all types leading to huge delay porsager/postgres#903).['a', 'b']instead of the string'{a,b}'. Elements stay strings, because nothing parses an enum or composite element unless a parser is registered by OID.sql.array(x)without a type for those element types, through the element-to-array map (index.js).It does not cover domains over arrays, which are not in category
'A'(#80, porsager#592).What it costs
When it is fetched. On every connection the client opens or re-opens: authentication resets the flag (
authenticate) and the catalog query runs before the first query is handed over (initialized).max_lifetimerotation (30–60 minutes by default), everyidle_timeoutclose and every dropped connection. A pool of 10 downloads the same catalog 10 times, then again on each rotation, roughly 320 times a day per process from lifetime rotation alone, multiplied by every replica.The catalog is the same for every connection to the same database. The most a discovery-based design can get is one fetch per pool (#7), and that does nothing on serverless, where the pool lives for one request.
Bytes. porsager#1219 measured a bare
select 1receiving 10,994 bytes with discovery and 686 without: 94% of what a fresh connection reads is the catalog. On a production Cloudflare Worker behind Hyperdrive, talking to a Supabase database, the query returns 448 rows (about 11 KB) and ran 9,064 times in the Worker's first ~21 hours: about 100 MB, against 110 MB of result bytes for everything. That is billable database egress, billable Workers CPU to parse (type registration was 4–6% of profile samples), and 17% of the statements counted against Hyperdrive's Free plan limit of 100,000 queries a day.A round trip before the first query of every connection; porsager#903 and porsager#952 report 200 ms or more of added delay per transaction from the type query.
Bugs and maintenance. The code is small; its failure surface is not, because it runs inside connection startup and reconnect ordering. Fixed in the fork, all caused by discovery:
sql.array()inferred the wrong array type on first use (Wrong inferred array types when invokingsql.array()for the first time. porsager/postgres#789)sql.reserve()hung on a pool that had not opened a connection, which also madefetch_types: falseunusable forreserve()users (Trying to reserve a connection without previously executing any sql hangs indefinitely. porsager/postgres#751, fetch_types: false, deadlocks reserve() and alternatively pulls ~10KB from server before any work and can burn GBs of egress per month porsager/postgres#1219, Cloudflare/workerd: sql.reserve() never resolves on a pool that has not yet opened a connection porsager/postgres#1203)Still open: the per-transaction delay reports (porsager#903, porsager#952), types created by a migration after a connection opened stay unknown until it reconnects, and the two proposals in this area both fetch more, not less: named types (#114, porsager#1162) fetch the whole
pg_type, and the domain fix (porsager#1187) adds a second lookup.What dropping it would ask of users
pgdoes for every array (arrayString)inet[],cidr[],macaddr[],money[],timetz[],point[], ranges)'{a,b}', as inpgcol::text[], or register a parsersql.array(x)with a non-built-in element typeThe same question one level down: describe-first
The catalog exists because postgres.js serializes by server type. For a statement not yet prepared on the connection, any value whose type JS can't tell (strings, numbers, every array) makes it send Parse + Describe + Flush and wait for the server's parameter types before Bind (
describeFirst).pgsends Parse, Bind, Describe, Execute and Sync in one round trip with types unspecified and lets the server infer them (query.js). Where connections live for one request, every parameterized statement pays that round trip.parameter_types: 'client'already skips it for scalars; arrays still describe first.Serializing arrays from their JS shape without describing would drop that round trip too. The burden is
pg's: a JS array bound to ajson/jsonbparameter becomes a PostgreSQL array literal, so usersJSON.stringifyarrays for JSON columns (plain objects are still sent as JSON), and a few contexts where the server cannot infer a type need an explicit cast.Options
fetch_types: falsetrade-off.false(a breaking change for readers of enum arrays, so 5.0), with the JS-shape fallback for writes and a wider static table; keepfetch_types: trueas an opt-in for apps that want enum arrays read as arrays.fetchArrayTypes, the startup state around it and the discovered part of the type map; reads of non-built-in arrays go through casts or registered parsers.JSON.stringifyrule for JSON columns.Measurements before deciding
fetch_typeson and off, on Node.js and on Workers (the edge A/B tooling covers the Worker side).pg_stat_statementsat the origin saw ~53k over the same window.fetch_types: false, before any fix.Related: #7, #45, #80, #114.
All reactions