From 3939fca5707e39731842bd5e7ea5af82937df99b Mon Sep 17 00:00:00 2001 From: Joe Thom Date: Thu, 10 Sep 2026 06:32:04 -0700 Subject: [PATCH 1/2] Handle PostgreSQL JSON expression defaults --- changelogs/drizzle-kit/0.32.4.md | 2 + drizzle-kit/src/serializer/pgSerializer.ts | 20 +++++++++ drizzle-kit/tests/pg-json-defaults.test.ts | 51 ++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 drizzle-kit/tests/pg-json-defaults.test.ts diff --git a/changelogs/drizzle-kit/0.32.4.md b/changelogs/drizzle-kit/0.32.4.md index 44cea828c5..491987e8a6 100644 --- a/changelogs/drizzle-kit/0.32.4.md +++ b/changelogs/drizzle-kit/0.32.4.md @@ -1,3 +1,5 @@ ### PostgreSQL bug fixes Added columns are now created before primary keys that use them. Unique constraints and indexes are now created before dependent foreign keys. + +PostgreSQL introspection now preserves JSON and JSONB expression defaults instead of parsing them as JSON literals. diff --git a/drizzle-kit/src/serializer/pgSerializer.ts b/drizzle-kit/src/serializer/pgSerializer.ts index 88a74ab409..0a14df842f 100644 --- a/drizzle-kit/src/serializer/pgSerializer.ts +++ b/drizzle-kit/src/serializer/pgSerializer.ts @@ -2082,6 +2082,26 @@ export const defaultForColumn = (column: any, internals: PgKitInternals, tableNa // if numeric(1,1) and used '99' -> psql stores like '99'::numeric return columnDefaultAsString.includes("'") ? columnDefaultAsString : `'${columnDefaultAsString}'`; } else if (column.data_type === 'json' || column.data_type === 'jsonb') { + if (!columnDefaultAsString.startsWith("'") || !columnDefaultAsString.endsWith("'")) { + if (typeof internals!.tables![tableName] === 'undefined') { + internals!.tables![tableName] = { + columns: { + [columnName]: { + isDefaultAnExpression: true, + }, + }, + }; + } else if (typeof internals!.tables![tableName]!.columns[columnName] === 'undefined') { + internals!.tables![tableName]!.columns[columnName] = { + isDefaultAnExpression: true, + }; + } else { + internals!.tables![tableName]!.columns[columnName]!.isDefaultAnExpression = true; + } + + return columnDefaultAsString; + } + const jsonWithoutSpaces = JSON.stringify(JSON.parse(columnDefaultAsString.slice(1, -1))); return `'${jsonWithoutSpaces}'::${column.data_type}`; } else if (column.data_type === 'boolean') { diff --git a/drizzle-kit/tests/pg-json-defaults.test.ts b/drizzle-kit/tests/pg-json-defaults.test.ts new file mode 100644 index 0000000000..96f49d377f --- /dev/null +++ b/drizzle-kit/tests/pg-json-defaults.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from 'vitest'; +import { introspectPgDB } from '../src/api'; +import type { DrizzlePgDB } from '../src/api'; + +const columnRow = { + table_name: 'items', + is_nullable: 'YES', + array_dimensions: 0, + data_type: 'jsonb', + seq_name: null, + additional_dt: 'jsonb', + enum_name: 'jsonb', + is_generated: 'NEVER', + generation_expression: null, + is_identity: 'NO', + identity_generation: null, + identity_start: null, + identity_increment: null, + identity_maximum: null, + identity_minimum: null, + identity_cycle: 'NO', + type_schema: 'pg_catalog', +}; + +function databaseWithDefault(columnDefault: string): DrizzlePgDB { + const query = async (sql: string): Promise[]> => { + if (sql.includes("c.relkind IN ('r', 'v', 'm')")) { + return [{ table_schema: 'public', table_name: 'items', type: 'table', rls_enabled: false }]; + } + if (sql.includes('a.attndims AS array_dimensions')) { + return [{ ...columnRow, column_name: 'payload', column_default: columnDefault }]; + } + + return []; + }; + + return { query } as DrizzlePgDB; +} + +test('introspects a jsonb expression default without parsing it as JSON', async () => { + const expression = "jsonb_build_object('kind'::text, 'record'::text)"; + const schema = await introspectPgDB(databaseWithDefault(expression), [], ['public']); + + expect(schema.tables['public.items']!.columns.payload!.default).toBe(expression); +}); + +test('normalizes a quoted jsonb literal default', async () => { + const schema = await introspectPgDB(databaseWithDefault(`'{"kind": "record"}'::jsonb`), [], ['public']); + + expect(schema.tables['public.items']!.columns.payload!.default).toBe(`'{"kind":"record"}'::jsonb`); +}); From 8e216e054a0f66b001b7a5181174d924882aff9f Mon Sep 17 00:00:00 2001 From: Joe Thom Date: Thu, 10 Sep 2026 06:49:07 -0700 Subject: [PATCH 2/2] Preserve JSON expression casts in generated schemas --- drizzle-kit/src/introspect-pg.ts | 8 ++++++++ drizzle-kit/src/serializer/pgSerializer.ts | 3 ++- drizzle-kit/tests/pg-json-defaults.test.ts | 10 ++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/drizzle-kit/src/introspect-pg.ts b/drizzle-kit/src/introspect-pg.ts index 4bb65ee0ca..9f22681e35 100644 --- a/drizzle-kit/src/introspect-pg.ts +++ b/drizzle-kit/src/introspect-pg.ts @@ -775,6 +775,10 @@ const mapDefault = ( } if (lowered.startsWith('jsonb')) { + if (isExpression) { + return `.default(${mapColumnDefault(defaultValue, true)})`; + } + const def = typeof defaultValue !== 'undefined' ? defaultValue.replace(/::(.*?)(? { expect(schema.tables['public.items']!.columns.payload!.default).toBe(`'{"kind":"record"}'::jsonb`); }); + +test('preserves casts and generates SQL for jsonb expression defaults', async () => { + const expression = "current_setting('app.payload')::jsonb"; + const schema = await fromDatabase(databaseWithDefault(expression), () => true, ['public']); + + expect(schema.tables['public.items']!.columns.payload!.default).toBe(expression); + expect(schemaToTypeScript(schema, 'preserve').file).toContain(`.default(sql\`${expression}\`)`); +});