refactor(db): swap the query builder from Drizzle to Kysely (#305)
One commit, because a main that carries both builders is one where the next person converting a query has to guess which to reach for, and where two generated mirrors of one database can disagree. There was nothing to stage anyway: one file used the builder. The safety property that motivated adopting a builder at all is untouched, and was never the thing being traded. A value interpolated into a sql template becomes a bind parameter in either library, so #202's invariant stays a property of the type system and #180's hotspots retire either way. What changes is the three ways the old library made it easy to be quietly wrong, each verified in #297 against the SQL actually emitted: an array interpolating as a placeholder list unless every site remembered sql.param(), a column reference inside a raw fragment silently losing its table so a correlated subquery correlated with itself, and a camelCase mirror that had to be mapped back at every select or the JSON contract changed with no test noticing. CATEGORY_COLUMNS stops being a translation layer and becomes what it looks like — four column names four selects share. The generated types carry parent_id and sort_order because kysely-codegen emits the database's own names, so there is nothing left to map and nothing left to get wrong by forgetting to. The drift guard survives the swap rather than being rewritten, and loses its library name in the process: it is schemaMirror.integration.test.ts now, so the next such change renames nothing. It also got stricter for free. The Drizzle version had to match each column two ways and its own comment called that deliberately loose; a generated Kysely interface spells the database's name verbatim as a bare key, so one exact match is the whole rule and snakeToCamel is gone. isUniqueViolation keeps accepting both error shapes and now has a test behind it. Kysely uses the pg driver directly and should leave the SQLSTATE on err.code, but "should" is the word that turned two 409s into 500s when the last conversion moved it to err.cause.code with nothing failing to compile. Migrations are untouched. #219 stands, they remain hand-written node-pg-migrate files, and Kysely has no generator to refuse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+20
-16
@@ -1,6 +1,6 @@
|
||||
import { Pool } from 'pg';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import * as schema from './db-drizzle/schema';
|
||||
import { Kysely, PostgresDialect } from 'kysely';
|
||||
import type { DB } from './db-kysely/schema';
|
||||
|
||||
export const pool = new Pool({
|
||||
host: process.env.PGHOST,
|
||||
@@ -11,25 +11,29 @@ export const pool = new Pool({
|
||||
});
|
||||
|
||||
/**
|
||||
* Drizzle over the same pool, alongside `pool` rather than instead of it.
|
||||
* Kysely over the same pool, alongside `pool` rather than instead of it.
|
||||
*
|
||||
* Both have to work at once: the conversion decided in #216 is file by file
|
||||
* across 187 call sites, so for a long time most queries will still be raw `pg`
|
||||
* and the two must share one set of connections. Handing drizzle the existing
|
||||
* pool rather than letting it open its own is what makes that true — otherwise
|
||||
* a transaction started on one would be invisible to the other, and the pool
|
||||
* Both have to work at once: the conversion is file by file across 238 call
|
||||
* sites, so for a long time most queries will still be raw `pg` and the two
|
||||
* must share one set of connections. Handing Kysely the existing pool rather
|
||||
* than letting it open its own is what makes that true — otherwise a
|
||||
* transaction started on one would be invisible to the other, and the pool
|
||||
* limits would silently double.
|
||||
*
|
||||
* The value of this over raw `pg` is not brevity. In a Drizzle `sql` template
|
||||
* `${value}` emits a **bind parameter**, never text, so there is no way to
|
||||
* spell "interpolate this as SQL" by accident — the escape hatch that looks
|
||||
* like a plain template literal does not behave like one. That makes the #202
|
||||
* invariant structural instead of a comment plus two mutation tests, and it is
|
||||
* the main reason this adoption is worth doing.
|
||||
* The value of this over raw `pg` is not brevity. In a Kysely `sql` template
|
||||
* `${value}` emits a bind parameter, never text, so there is no way to spell
|
||||
* "interpolate this as SQL" by accident. That makes the #202 invariant
|
||||
* structural instead of a comment plus two mutation tests, and it is the main
|
||||
* reason a builder is here at all.
|
||||
*
|
||||
* The trap that goes with it is arrays. See db-drizzle/CONVENTIONS.md.
|
||||
* Kysely rather than Drizzle since #305. The safety property above was true of
|
||||
* both; what decided it is that three of the four hazards in the old
|
||||
* CONVENTIONS.md — an array needing sql.param(), a column reference silently
|
||||
* losing its table inside a raw fragment, and a camelCase mirror that had to be
|
||||
* mapped back at every select — were properties of Drizzle rather than of
|
||||
* type-safe query building. See #297 for the SQL each one actually emitted.
|
||||
*/
|
||||
export const db = drizzle(pool, { schema });
|
||||
export const db = new Kysely<DB>({ dialect: new PostgresDialect({ pool }) });
|
||||
|
||||
/**
|
||||
* The single row a query is guaranteed to have returned.
|
||||
|
||||
Reference in New Issue
Block a user