fix db:push emitting bound parameters in check constraints
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 <noreply@anthropic.com>
This commit is contained in:
@@ -28,8 +28,12 @@ export const serverIntegrations = pgTable(
|
|||||||
(table) => [
|
(table) => [
|
||||||
check(
|
check(
|
||||||
'ck_server_integrations_provider',
|
'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(
|
sql`${table.provider} IN (${sql.join(
|
||||||
SERVER_PROVIDERS.map((p) => sql`${p}`),
|
SERVER_PROVIDERS.map((p) => sql.raw(`'${p}'`)),
|
||||||
sql`, `,
|
sql`, `,
|
||||||
)})`,
|
)})`,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ import { sql } from 'drizzle-orm';
|
|||||||
import { serverIntegrations } from './server';
|
import { serverIntegrations } from './server';
|
||||||
|
|
||||||
export const userSettings = pgTable('user_settings', {
|
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({}),
|
settings: jsonb('settings').notNull().default({}),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userState = pgTable('user_state', {
|
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({}),
|
state: jsonb('state').notNull().default({}),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
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.
|
// Per-user integrations the code knows how to read. Same reasoning as SERVER_PROVIDERS in ./server.
|
||||||
const USER_PROVIDERS = ['google', 'browser-relay'] as const;
|
const USER_PROVIDERS = ['google', 'browser-relay'] as const;
|
||||||
|
|
||||||
export const userIntegrations = pgTable('user_integrations', {
|
export const userIntegrations = pgTable(
|
||||||
id: serial('id').primaryKey(),
|
'user_integrations',
|
||||||
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
{
|
||||||
provider: text('provider').notNull(),
|
id: serial('id').primaryKey(),
|
||||||
serverIntegrationId: integer('server_integration_id').references(() => serverIntegrations.id, { onDelete: 'set null' }),
|
userId: integer('user_id')
|
||||||
config: jsonb('config').notNull().default({}),
|
.notNull()
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
.references(() => users.id, { onDelete: 'cascade' }),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
provider: text('provider').notNull(),
|
||||||
}, (table) => [
|
serverIntegrationId: integer('server_integration_id').references(() => serverIntegrations.id, {
|
||||||
unique('uq_user_integrations_user_provider').on(table.userId, table.provider),
|
onDelete: 'set null',
|
||||||
check(
|
}),
|
||||||
'ck_user_integrations_provider',
|
config: jsonb('config').notNull().default({}),
|
||||||
sql`${table.provider} IN (${sql.join(USER_PROVIDERS.map((p) => sql`${p}`), sql`, `)})`,
|
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', {
|
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([]),
|
paths: jsonb('paths').notNull().default([]),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user