From 066c118723e755e0d152e05d2a3b23234d7fd701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 31 Jul 2026 15:13:07 +0000 Subject: [PATCH] fix db:push emitting bound parameters in check constraints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bun db:push failed for the whole schema with "there is no parameter $1" (42P02), after "Pulling schema from database" succeeded. It was not caused by any recent change — pushing with the new wallet table stashed reproduced it identically on HEAD. The two provider CHECK constraints built their allowed-value lists with sql`${p}` over JavaScript strings. Interpolating a JS value into a sql template binds it as a parameter, so the constraint was emitted as CHECK ("provider" IN ($1, $2)) and Postgres rejects a parameter reference inside a CHECK. sql.raw renders the literals instead. Safe because both lists are compile-time const tuples, not input. Push now generates valid SQL. It still stops on server_integrations, where three rows (discord, telegram, whatsapp) predate the constraint and violate it — real data, and the owner's to decide about, since their config holds bot tokens. Unrelated but worth recording: drizzle wants to name a user_integrations foreign key at 65 characters and Postgres truncates identifiers at 63, so the name it reads back never matches the name it asked for and that constraint is proposed for drop/recreate on every push. Cosmetic churn, not addressed here. Co-Authored-By: Claude Opus 5 --- src/databases/officer_db/src/schema/server.ts | 6 +- .../officer_db/src/schema/user-data.ts | 55 +++++++++++++------ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/databases/officer_db/src/schema/server.ts b/src/databases/officer_db/src/schema/server.ts index 752f888e..494363ab 100644 --- a/src/databases/officer_db/src/schema/server.ts +++ b/src/databases/officer_db/src/schema/server.ts @@ -28,8 +28,12 @@ export const serverIntegrations = pgTable( (table) => [ check( 'ck_server_integrations_provider', + // sql.raw, not `sql`${p}``: interpolating a JS string binds it as a parameter, so the constraint + // is emitted as `IN ($1, $2)` and Postgres rejects it ("there is no parameter $1") — which breaks + // `bun db:push` for the entire schema, not just this table. Safe because the values are compile-time + // literals from the const tuple above. sql`${table.provider} IN (${sql.join( - SERVER_PROVIDERS.map((p) => sql`${p}`), + SERVER_PROVIDERS.map((p) => sql.raw(`'${p}'`)), sql`, `, )})`, ), diff --git a/src/databases/officer_db/src/schema/user-data.ts b/src/databases/officer_db/src/schema/user-data.ts index fcd59a93..d9298451 100644 --- a/src/databases/officer_db/src/schema/user-data.ts +++ b/src/databases/officer_db/src/schema/user-data.ts @@ -4,13 +4,17 @@ import { sql } from 'drizzle-orm'; import { serverIntegrations } from './server'; export const userSettings = pgTable('user_settings', { - userId: integer('user_id').primaryKey().references(() => users.id, { onDelete: 'cascade' }), + userId: integer('user_id') + .primaryKey() + .references(() => users.id, { onDelete: 'cascade' }), settings: jsonb('settings').notNull().default({}), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }); export const userState = pgTable('user_state', { - userId: integer('user_id').primaryKey().references(() => users.id, { onDelete: 'cascade' }), + userId: integer('user_id') + .primaryKey() + .references(() => users.id, { onDelete: 'cascade' }), state: jsonb('state').notNull().default({}), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }); @@ -18,24 +22,39 @@ export const userState = pgTable('user_state', { // Per-user integrations the code knows how to read. Same reasoning as SERVER_PROVIDERS in ./server. const USER_PROVIDERS = ['google', 'browser-relay'] as const; -export const userIntegrations = pgTable('user_integrations', { - id: serial('id').primaryKey(), - userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), - provider: text('provider').notNull(), - serverIntegrationId: integer('server_integration_id').references(() => serverIntegrations.id, { onDelete: 'set null' }), - config: jsonb('config').notNull().default({}), - createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), - updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), -}, (table) => [ - unique('uq_user_integrations_user_provider').on(table.userId, table.provider), - check( - 'ck_user_integrations_provider', - sql`${table.provider} IN (${sql.join(USER_PROVIDERS.map((p) => sql`${p}`), sql`, `)})`, - ), -]); +export const userIntegrations = pgTable( + 'user_integrations', + { + id: serial('id').primaryKey(), + userId: integer('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + provider: text('provider').notNull(), + serverIntegrationId: integer('server_integration_id').references(() => serverIntegrations.id, { + onDelete: 'set null', + }), + config: jsonb('config').notNull().default({}), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [ + unique('uq_user_integrations_user_provider').on(table.userId, table.provider), + check( + 'ck_user_integrations_provider', + // sql.raw — see the note on ck_server_integrations_provider; an interpolated JS string becomes a + // bound `$1` that Postgres will not accept in a CHECK. + sql`${table.provider} IN (${sql.join( + USER_PROVIDERS.map((p) => sql.raw(`'${p}'`)), + sql`, `, + )})`, + ), + ], +); export const dockConfigs = pgTable('dock_configs', { - userId: integer('user_id').primaryKey().references(() => users.id, { onDelete: 'cascade' }), + userId: integer('user_id') + .primaryKey() + .references(() => users.id, { onDelete: 'cascade' }), paths: jsonb('paths').notNull().default([]), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), });