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/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(/::(.*?)(? 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 originalColumnDefault; + } + 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..802934835b --- /dev/null +++ b/drizzle-kit/tests/pg-json-defaults.test.ts @@ -0,0 +1,61 @@ +import { expect, test } from 'vitest'; +import { introspectPgDB } from '../src/api'; +import type { DrizzlePgDB } from '../src/api'; +import { schemaToTypeScript } from '../src/introspect-pg'; +import { fromDatabase } from '../src/serializer/pgSerializer'; + +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`); +}); + +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}\`)`); +});