dust_db_postgres 0.2.0
dust_db_postgres: ^0.2.0 copied to clipboard
PostgreSQL runtime for Dust Database. Executes build-time validated SQL and generated row mapping over package:postgres.
Changelog #
All notable changes to dust_db_postgres are documented in this file.
The format is based on Keep a Changelog.
[Unreleased] #
0.2.0 - 2026-09-13 #
First release. PostgreSQL runtime for generated Database code, wrapping
package:postgres the way dust_db_sqlite3 wraps package:sqlite3.
Performance #
-
Prepared statements are held per pooled connection rather than parsed and closed on every call.
Session.executesends Parse and waits for it, then Bind/Execute, then Close and waits for that — three server round trips where a held statement needs one. A single-rowSELECTagainst a local server measured 1023us that way and 321us reusing a statement, which is the ratio the round trips predict. End to end,fetchOnewent 975us to ~355us andfetchScalar1025us to ~305us.A statement belongs to the connection that parsed it, so queries run through
withConnectionand each connection keeps its own, bounded at 64. A transaction does not cache: its statements would be parsed and thrown away with it.PostgreSQL refuses a held statement whose result type changed under it, which a migration applied while the process runs will cause. That one error is retried once with a freshly parsed statement, so a schema change costs one failed call rather than every call after it.
-
A retired connection's held statements are dropped with it. The cache is keyed by connection and the pool retires them on its own schedule — an age limit, a session limit, an error — so without this the map grew for the life of the process, holding statements whose connection was already gone.
-
A one-row terminal builds no column-name index unless a name is read. There is one row, so there is nothing to share an index with, and
fetchScalarreads column zero and never needs one. -
Row reads resolve column names through one index per result, and typed terminals no longer build a list of row adapters before mapping.
Added #
PostgresDriver, a pool that also runs statements, opened from a connection URL, withPgPoolas thesqlx-postgresalias.PostgresExecutor,PgConnectOptions,PostgresRow, andPostgresUnsafeSql.PgConnectOptions.maxConnectionAge, how long the pool keeps a connection before retiring it. Worth setting behind a proxy that drops idle connections on its own schedule: retiring first means the pool replaces a connection rather than handing out one the far end has already closed.- 23 examples in
example/, one per question, indexed byexample/README.md. - Column names resolve through one index per result rather than
ResultRow.toColumnMap(), which allocates a map of every value for every row. Over 20k rows of 12 columns the removed step measured 23-34ms against 9ms. The index is lazy, sofetchScalarand anyreadIndexbuild none at all. fetchScalar<T?>answersOk(null)for a NULL value and for no row, rather than a decode or cardinality error. This is whatQueryScalar.fetchOptionalasks for, so an aggregate over no rows now reads as optional on both drivers; it already behaved this way on SQLite.?sslmode=is read from the connection URL —disable,requireorverify-full— so a URL that works withpsqlworks here. ExplicitPgConnectOptionswin over it. libpq'spreferandalloware rejected rather than mapped, since they mean "try TLS, fall back to plaintext" and guessing either way would decide something the caller left to the connection.- Migrations applied in name order inside one transaction, guarded by a PostgreSQL advisory lock. Unlike SQLite, where one process holds the file, several servers can start against the same database at once.
Testing #
- 103 tests at 100% line coverage, gated in CI against a
postgres:16service. Everything that needs a server is skipped — and reported as skipped — whenDUST_DATABASE_URLis unset, since there is no in-memory PostgreSQL to fall back to. - Every file in
example/is run by the suite and asserted on its output. An example that compiles but prints the wrong answer is still broken, and only running it catches that — it is how the nullable-scalar bug above was found.
Notes #
-
Requires
dust_dart0.2.0 or newer, and a Dust CLI that generates PostgreSQL code. Native targets only: PostgreSQL is reached over a socket, so this does not run on the web. -
The SQL reaches the server unchanged. Postgres reads
$1natively, and values bind with an unspecified type so the server infers them. Nothing here rewrites query text — the SQLite runtime does the opposite, rewriting$nto?at bind time. -
A Dart
Listbinds as a PostgreSQL array, so= ANY($1)needs no encoding at the call site. SQLite reaches the same place throughjson_eachand a driver-side JSON encode. -
ExecResult.lastInsertIdis always null. PostgreSQL has no counterpart to SQLite'slast_insert_rowid();RETURNINGis the portable answer. -
Nested transactions are savepoints.
package:postgresexposes no savepoint API — a transaction session cannot open a transaction of its own — so this package issuesSAVEPOINT,RELEASEandROLLBACK TOitself.