@prisma-next/target-postgres 0.3.0-dev.5 → 0.3.0-dev.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +7 -1
  2. package/dist/control.d.mts +16 -0
  3. package/dist/control.d.mts.map +1 -0
  4. package/dist/control.mjs +2590 -0
  5. package/dist/control.mjs.map +1 -0
  6. package/dist/descriptor-meta-DxB8oZzB.mjs +13 -0
  7. package/dist/descriptor-meta-DxB8oZzB.mjs.map +1 -0
  8. package/dist/pack.d.mts +7 -0
  9. package/dist/pack.d.mts.map +1 -0
  10. package/dist/pack.mjs +9 -0
  11. package/dist/pack.mjs.map +1 -0
  12. package/dist/runtime.d.mts +9 -0
  13. package/dist/runtime.d.mts.map +1 -0
  14. package/dist/runtime.mjs +21 -0
  15. package/dist/runtime.mjs.map +1 -0
  16. package/package.json +33 -33
  17. package/src/core/migrations/planner.ts +329 -31
  18. package/src/core/migrations/runner.ts +27 -20
  19. package/src/core/migrations/statement-builders.ts +9 -7
  20. package/src/core/types.ts +5 -0
  21. package/src/exports/control.ts +1 -3
  22. package/src/exports/runtime.ts +7 -12
  23. package/dist/chunk-RKEXRSSI.js +0 -14
  24. package/dist/chunk-RKEXRSSI.js.map +0 -1
  25. package/dist/core/descriptor-meta.d.ts +0 -9
  26. package/dist/core/descriptor-meta.d.ts.map +0 -1
  27. package/dist/core/migrations/planner.d.ts +0 -14
  28. package/dist/core/migrations/planner.d.ts.map +0 -1
  29. package/dist/core/migrations/runner.d.ts +0 -8
  30. package/dist/core/migrations/runner.d.ts.map +0 -1
  31. package/dist/core/migrations/statement-builders.d.ts +0 -30
  32. package/dist/core/migrations/statement-builders.d.ts.map +0 -1
  33. package/dist/exports/control.d.ts +0 -8
  34. package/dist/exports/control.d.ts.map +0 -1
  35. package/dist/exports/control.js +0 -1255
  36. package/dist/exports/control.js.map +0 -1
  37. package/dist/exports/pack.d.ts +0 -4
  38. package/dist/exports/pack.d.ts.map +0 -1
  39. package/dist/exports/pack.js +0 -11
  40. package/dist/exports/pack.js.map +0 -1
  41. package/dist/exports/runtime.d.ts +0 -12
  42. package/dist/exports/runtime.d.ts.map +0 -1
  43. package/dist/exports/runtime.js +0 -19
  44. package/dist/exports/runtime.js.map +0 -1
package/package.json CHANGED
@@ -1,29 +1,32 @@
1
1
  {
2
2
  "name": "@prisma-next/target-postgres",
3
- "version": "0.3.0-dev.5",
3
+ "version": "0.3.0-dev.52",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Postgres target pack for Prisma Next",
7
7
  "dependencies": {
8
8
  "arktype": "^2.0.0",
9
- "@prisma-next/contract": "0.3.0-dev.5",
10
- "@prisma-next/core-control-plane": "0.3.0-dev.5",
11
- "@prisma-next/core-execution-plane": "0.3.0-dev.5",
12
- "@prisma-next/family-sql": "0.3.0-dev.5",
13
- "@prisma-next/sql-contract": "0.3.0-dev.5",
14
- "@prisma-next/sql-errors": "0.3.0-dev.5",
15
- "@prisma-next/sql-schema-ir": "0.3.0-dev.5",
16
- "@prisma-next/utils": "0.3.0-dev.5",
17
- "@prisma-next/cli": "0.3.0-dev.5"
9
+ "@prisma-next/cli": "0.3.0-dev.52",
10
+ "@prisma-next/contract": "0.3.0-dev.52",
11
+ "@prisma-next/core-control-plane": "0.3.0-dev.52",
12
+ "@prisma-next/core-execution-plane": "0.3.0-dev.52",
13
+ "@prisma-next/family-sql": "0.3.0-dev.52",
14
+ "@prisma-next/sql-contract": "0.3.0-dev.52",
15
+ "@prisma-next/sql-errors": "0.3.0-dev.52",
16
+ "@prisma-next/sql-relational-core": "0.3.0-dev.52",
17
+ "@prisma-next/sql-schema-ir": "0.3.0-dev.52",
18
+ "@prisma-next/utils": "0.3.0-dev.52",
19
+ "@prisma-next/sql-runtime": "0.3.0-dev.52"
18
20
  },
19
21
  "devDependencies": {
20
- "@vitest/coverage-v8": "4.0.16",
21
- "tsup": "8.5.1",
22
+ "tsdown": "0.18.4",
22
23
  "typescript": "5.9.3",
23
- "vitest": "4.0.16",
24
- "@prisma-next/adapter-postgres": "0.3.0-dev.5",
25
- "@prisma-next/driver-postgres": "0.3.0-dev.5",
26
- "@prisma-next/test-utils": "0.0.1"
24
+ "vitest": "4.0.17",
25
+ "@prisma-next/adapter-postgres": "0.3.0-dev.52",
26
+ "@prisma-next/driver-postgres": "0.3.0-dev.52",
27
+ "@prisma-next/test-utils": "0.0.1",
28
+ "@prisma-next/tsdown": "0.0.0",
29
+ "@prisma-next/tsconfig": "0.0.0"
27
30
  },
28
31
  "files": [
29
32
  "dist",
@@ -31,27 +34,24 @@
31
34
  "packs"
32
35
  ],
33
36
  "exports": {
34
- "./control": {
35
- "types": "./dist/exports/control.d.ts",
36
- "import": "./dist/exports/control.js"
37
- },
38
- "./runtime": {
39
- "types": "./dist/exports/runtime.d.ts",
40
- "import": "./dist/exports/runtime.js"
41
- },
42
- "./pack": {
43
- "types": "./dist/exports/pack.d.ts",
44
- "import": "./dist/exports/pack.js"
45
- }
37
+ "./control": "./dist/control.mjs",
38
+ "./pack": "./dist/pack.mjs",
39
+ "./runtime": "./dist/runtime.mjs",
40
+ "./package.json": "./package.json"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/prisma/prisma-next.git",
45
+ "directory": "packages/3-targets/3-targets/postgres"
46
46
  },
47
47
  "scripts": {
48
- "build": "tsup --config tsup.config.ts && tsc --project tsconfig.build.json",
48
+ "build": "tsdown",
49
49
  "test": "vitest run --passWithNoTests",
50
50
  "test:coverage": "vitest run --coverage --passWithNoTests",
51
51
  "typecheck": "tsc --project tsconfig.json --noEmit",
52
- "lint": "biome check . --config-path ../../../../biome.json --error-on-warnings",
53
- "lint:fix": "biome check --write . --config-path ../../../biome.json",
54
- "lint:fix:unsafe": "biome check --write --unsafe . --config-path ../../../biome.json",
55
- "clean": "node ../../../../scripts/clean.mjs"
52
+ "lint": "biome check . --error-on-warnings",
53
+ "lint:fix": "biome check --write .",
54
+ "lint:fix:unsafe": "biome check --write --unsafe .",
55
+ "clean": "rm -rf dist dist-tsc dist-tsc-prod coverage .tmp-output"
56
56
  }
57
57
  }
@@ -1,5 +1,14 @@
1
+ import {
2
+ escapeLiteral,
3
+ expandParameterizedNativeType,
4
+ normalizeSchemaNativeType,
5
+ parsePostgresDefault,
6
+ quoteIdentifier,
7
+ } from '@prisma-next/adapter-postgres/control';
8
+ import { isTaggedBigInt } from '@prisma-next/contract/types';
1
9
  import type { SchemaIssue } from '@prisma-next/core-control-plane/types';
2
10
  import type {
11
+ CodecControlHooks,
3
12
  MigrationOperationPolicy,
4
13
  SqlMigrationPlanner,
5
14
  SqlMigrationPlannerPlanOptions,
@@ -8,20 +17,29 @@ import type {
8
17
  } from '@prisma-next/family-sql/control';
9
18
  import {
10
19
  createMigrationPlan,
20
+ extractCodecControlHooks,
11
21
  plannerFailure,
12
22
  plannerSuccess,
13
23
  } from '@prisma-next/family-sql/control';
14
- import { arraysEqual, verifySqlSchema } from '@prisma-next/family-sql/schema-verify';
24
+ import {
25
+ arraysEqual,
26
+ isIndexSatisfied,
27
+ isUniqueConstraintSatisfied,
28
+ verifySqlSchema,
29
+ } from '@prisma-next/family-sql/schema-verify';
15
30
  import type {
16
31
  ForeignKey,
32
+ ReferentialAction,
17
33
  SqlContract,
18
34
  SqlStorage,
19
35
  StorageColumn,
20
36
  StorageTable,
21
37
  } from '@prisma-next/sql-contract/types';
22
38
  import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
39
+ import { ifDefined } from '@prisma-next/utils/defined';
40
+ import type { PostgresColumnDefault } from '../types';
23
41
 
24
- type OperationClass = 'extension' | 'table' | 'unique' | 'index' | 'foreignKey';
42
+ type OperationClass = 'extension' | 'type' | 'table' | 'unique' | 'index' | 'foreignKey';
25
43
 
26
44
  type PlannerFrameworkComponents = SqlMigrationPlannerPlanOptions extends {
27
45
  readonly frameworkComponents: infer T;
@@ -83,11 +101,21 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
83
101
  return plannerFailure(classification.conflicts);
84
102
  }
85
103
 
104
+ // Extract codec control hooks once at entry point for reuse across all operations.
105
+ // This avoids repeated iteration over frameworkComponents for each method that needs hooks.
106
+ const codecHooks = extractCodecControlHooks(options.frameworkComponents);
107
+
86
108
  const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
87
109
 
110
+ const storageTypePlan = this.buildStorageTypeOperations(options, schemaName, codecHooks);
111
+ if (storageTypePlan.conflicts.length > 0) {
112
+ return plannerFailure(storageTypePlan.conflicts);
113
+ }
114
+
88
115
  // Build extension operations from component-owned database dependencies
89
116
  operations.push(
90
117
  ...this.buildDatabaseDependencyOperations(options),
118
+ ...storageTypePlan.operations,
91
119
  ...this.buildTableOperations(options.contract.storage.tables, options.schema, schemaName),
92
120
  ...this.buildColumnOperations(options.contract.storage.tables, options.schema, schemaName),
93
121
  ...this.buildPrimaryKeyOperations(
@@ -97,6 +125,11 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
97
125
  ),
98
126
  ...this.buildUniqueOperations(options.contract.storage.tables, options.schema, schemaName),
99
127
  ...this.buildIndexOperations(options.contract.storage.tables, options.schema, schemaName),
128
+ ...this.buildFkBackingIndexOperations(
129
+ options.contract.storage.tables,
130
+ options.schema,
131
+ schemaName,
132
+ ),
100
133
  ...this.buildForeignKeyOperations(
101
134
  options.contract.storage.tables,
102
135
  options.schema,
@@ -108,8 +141,8 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
108
141
  targetId: 'postgres',
109
142
  origin: null,
110
143
  destination: {
111
- coreHash: options.contract.coreHash,
112
- ...(options.contract.profileHash ? { profileHash: options.contract.profileHash } : {}),
144
+ storageHash: options.contract.storageHash,
145
+ ...ifDefined('profileHash', options.contract.profileHash),
113
146
  },
114
147
  operations,
115
148
  });
@@ -166,6 +199,55 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
166
199
 
167
200
  return operations;
168
201
  }
202
+
203
+ private buildStorageTypeOperations(
204
+ options: PlannerOptionsWithComponents,
205
+ schemaName: string,
206
+ codecHooks: Map<string, CodecControlHooks>,
207
+ ): {
208
+ readonly operations: readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[];
209
+ readonly conflicts: readonly SqlPlannerConflict[];
210
+ } {
211
+ const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
212
+ const conflicts: SqlPlannerConflict[] = [];
213
+ const storageTypes = options.contract.storage.types ?? {};
214
+
215
+ for (const [typeName, typeInstance] of sortedEntries(storageTypes)) {
216
+ const hook = codecHooks.get(typeInstance.codecId);
217
+ const planResult = hook?.planTypeOperations?.({
218
+ typeName,
219
+ typeInstance,
220
+ contract: options.contract,
221
+ schema: options.schema,
222
+ schemaName,
223
+ policy: options.policy,
224
+ });
225
+ if (!planResult) {
226
+ continue;
227
+ }
228
+ for (const operation of planResult.operations) {
229
+ if (!options.policy.allowedOperationClasses.includes(operation.operationClass)) {
230
+ conflicts.push({
231
+ kind: 'missingButNonAdditive',
232
+ summary: `Storage type "${typeName}" requires "${operation.operationClass}" operation "${operation.id}"`,
233
+ location: {
234
+ type: typeName,
235
+ },
236
+ });
237
+ continue;
238
+ }
239
+ operations.push({
240
+ ...operation,
241
+ target: {
242
+ id: operation.target.id,
243
+ details: this.buildTargetDetails('type', typeName, schemaName),
244
+ },
245
+ });
246
+ }
247
+ }
248
+
249
+ return { operations, conflicts };
250
+ }
169
251
  private collectDependencies(
170
252
  options: PlannerOptionsWithComponents,
171
253
  ): ReadonlyArray<PlannerDatabaseDependency> {
@@ -258,15 +340,20 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
258
340
  ): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
259
341
  const qualified = qualifyTableName(schema, tableName);
260
342
  const notNull = column.nullable === false;
343
+ const hasDefault = column.default !== undefined;
344
+ // Only require empty table for NOT NULL columns WITHOUT defaults.
345
+ // PostgreSQL allows adding NOT NULL columns with defaults to non-empty tables
346
+ // because the default value is applied to existing rows.
347
+ const requiresEmptyTable = notNull && !hasDefault;
261
348
  const precheck = [
262
349
  {
263
350
  description: `ensure column "${columnName}" is missing`,
264
351
  sql: columnExistsCheck({ schema, table: tableName, column: columnName, exists: false }),
265
352
  },
266
- ...(notNull
353
+ ...(requiresEmptyTable
267
354
  ? [
268
355
  {
269
- description: `ensure table "${tableName}" is empty before adding NOT NULL column`,
356
+ description: `ensure table "${tableName}" is empty before adding NOT NULL column without default`,
270
357
  sql: tableIsEmptyCheck(qualified),
271
358
  },
272
359
  ]
@@ -454,6 +541,65 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
454
541
  return operations;
455
542
  }
456
543
 
544
+ /**
545
+ * Generates FK-backing index operations for FKs with `index: true`,
546
+ * but only when no matching user-declared index exists in `contractTable.indexes`.
547
+ */
548
+ private buildFkBackingIndexOperations(
549
+ tables: SqlContract<SqlStorage>['storage']['tables'],
550
+ schema: SqlSchemaIR,
551
+ schemaName: string,
552
+ ): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
553
+ const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
554
+ for (const [tableName, table] of sortedEntries(tables)) {
555
+ const schemaTable = schema.tables[tableName];
556
+ // Collect column sets of user-declared indexes to avoid duplicates
557
+ const declaredIndexColumns = new Set(table.indexes.map((idx) => idx.columns.join(',')));
558
+
559
+ for (const fk of table.foreignKeys) {
560
+ if (fk.index === false) continue;
561
+ // Skip if user already declared an index with these columns
562
+ if (declaredIndexColumns.has(fk.columns.join(','))) continue;
563
+ // Skip if the index already exists in the database
564
+ if (schemaTable && hasIndex(schemaTable, fk.columns)) continue;
565
+
566
+ const indexName = `${tableName}_${fk.columns.join('_')}_idx`;
567
+ operations.push({
568
+ id: `index.${tableName}.${indexName}`,
569
+ label: `Create FK-backing index ${indexName} on ${tableName}`,
570
+ summary: `Creates FK-backing index ${indexName} on ${tableName}`,
571
+ operationClass: 'additive',
572
+ target: {
573
+ id: 'postgres',
574
+ details: this.buildTargetDetails('index', indexName, schemaName, tableName),
575
+ },
576
+ precheck: [
577
+ {
578
+ description: `ensure index "${indexName}" is missing`,
579
+ sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, indexName)}) IS NULL`,
580
+ },
581
+ ],
582
+ execute: [
583
+ {
584
+ description: `create FK-backing index "${indexName}"`,
585
+ sql: `CREATE INDEX ${quoteIdentifier(indexName)} ON ${qualifyTableName(
586
+ schemaName,
587
+ tableName,
588
+ )} (${fk.columns.map(quoteIdentifier).join(', ')})`,
589
+ },
590
+ ],
591
+ postcheck: [
592
+ {
593
+ description: `verify index "${indexName}" exists`,
594
+ sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, indexName)}) IS NOT NULL`,
595
+ },
596
+ ],
597
+ });
598
+ }
599
+ }
600
+ return operations;
601
+ }
602
+
457
603
  private buildForeignKeyOperations(
458
604
  tables: SqlContract<SqlStorage>['storage']['tables'],
459
605
  schema: SqlSchemaIR,
@@ -463,6 +609,7 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
463
609
  for (const [tableName, table] of sortedEntries(tables)) {
464
610
  const schemaTable = schema.tables[tableName];
465
611
  for (const foreignKey of table.foreignKeys) {
612
+ if (foreignKey.constraint === false) continue;
466
613
  if (schemaTable && hasForeignKey(schemaTable, foreignKey)) {
467
614
  continue;
468
615
  }
@@ -489,12 +636,7 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
489
636
  execute: [
490
637
  {
491
638
  description: `add foreign key "${fkName}"`,
492
- sql: `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
493
- ADD CONSTRAINT ${quoteIdentifier(fkName)}
494
- FOREIGN KEY (${foreignKey.columns.map(quoteIdentifier).join(', ')})
495
- REFERENCES ${qualifyTableName(schemaName, foreignKey.references.table)} (${foreignKey.references.columns
496
- .map(quoteIdentifier)
497
- .join(', ')})`,
639
+ sql: buildForeignKeySql(schemaName, tableName, fkName, foreignKey),
498
640
  },
499
641
  ],
500
642
  postcheck: [
@@ -519,7 +661,7 @@ REFERENCES ${qualifyTableName(schemaName, foreignKey.references.table)} (${forei
519
661
  schema,
520
662
  objectType,
521
663
  name,
522
- ...(table ? { table } : {}),
664
+ ...ifDefined('table', table),
523
665
  };
524
666
  }
525
667
 
@@ -535,6 +677,8 @@ REFERENCES ${qualifyTableName(schemaName, foreignKey.references.table)} (${forei
535
677
  strict: false,
536
678
  typeMetadataRegistry: new Map(),
537
679
  frameworkComponents: options.frameworkComponents,
680
+ normalizeDefault: parsePostgresDefault,
681
+ normalizeNativeType: normalizeSchemaNativeType,
538
682
  };
539
683
  const verifyResult = verifySqlSchema(verifyOptions);
540
684
 
@@ -583,16 +727,16 @@ REFERENCES ${qualifyTableName(schemaName, foreignKey.references.table)} (${forei
583
727
  const meta =
584
728
  issue.expected || issue.actual
585
729
  ? Object.freeze({
586
- ...(issue.expected ? { expected: issue.expected } : {}),
587
- ...(issue.actual ? { actual: issue.actual } : {}),
730
+ ...ifDefined('expected', issue.expected),
731
+ ...ifDefined('actual', issue.actual),
588
732
  })
589
733
  : undefined;
590
734
 
591
735
  return {
592
736
  kind,
593
737
  summary: issue.message,
594
- ...(location ? { location } : {}),
595
- ...(meta ? { meta } : {}),
738
+ ...ifDefined('location', location),
739
+ ...ifDefined('meta', meta),
596
740
  };
597
741
  }
598
742
  }
@@ -633,7 +777,8 @@ function buildCreateTableSql(qualifiedTableName: string, table: StorageTable): s
633
777
  ([columnName, column]: [string, StorageColumn]) => {
634
778
  const parts = [
635
779
  quoteIdentifier(columnName),
636
- column.nativeType,
780
+ buildColumnTypeSql(column),
781
+ buildColumnDefaultSql(column.default, column),
637
782
  column.nullable ? '' : 'NOT NULL',
638
783
  ].filter(Boolean);
639
784
  return parts.join(' ');
@@ -651,6 +796,117 @@ function buildCreateTableSql(qualifiedTableName: string, table: StorageTable): s
651
796
  return `CREATE TABLE ${qualifiedTableName} (\n ${allDefinitions.join(',\n ')}\n)`;
652
797
  }
653
798
 
799
+ /**
800
+ * Builds the column type SQL, handling autoincrement as a special case.
801
+ * For autoincrement on int4/int8, we use SERIAL/BIGSERIAL types.
802
+ */
803
+ function buildColumnTypeSql(column: StorageColumn): string {
804
+ const columnDefault = column.default;
805
+
806
+ // For autoincrement, use SERIAL/BIGSERIAL types instead of int4/int8
807
+ if (columnDefault?.kind === 'function' && columnDefault.expression === 'autoincrement()') {
808
+ if (column.nativeType === 'int4' || column.nativeType === 'integer') {
809
+ return 'SERIAL';
810
+ }
811
+ if (column.nativeType === 'int8' || column.nativeType === 'bigint') {
812
+ return 'BIGSERIAL';
813
+ }
814
+ if (column.nativeType === 'int2' || column.nativeType === 'smallint') {
815
+ return 'SMALLSERIAL';
816
+ }
817
+ }
818
+
819
+ if (column.typeRef) {
820
+ return quoteIdentifier(column.nativeType);
821
+ }
822
+
823
+ return renderParameterizedTypeSql(column) ?? column.nativeType;
824
+ }
825
+
826
+ /**
827
+ * Renders parameterized type SQL for a column, returning null if no expansion is needed.
828
+ *
829
+ * Uses the shared expandParameterizedNativeType utility from the postgres adapter.
830
+ * Returns null when the column has no typeParams, allowing the caller to fall back
831
+ * to the base nativeType.
832
+ */
833
+ function renderParameterizedTypeSql(column: StorageColumn): string | null {
834
+ if (!column.typeParams) {
835
+ return null;
836
+ }
837
+
838
+ const expanded = expandParameterizedNativeType({
839
+ nativeType: column.nativeType,
840
+ codecId: column.codecId,
841
+ typeParams: column.typeParams,
842
+ });
843
+
844
+ // If no expansion happened (returned the same base type), return null
845
+ // so caller can decide whether to use nativeType directly
846
+ return expanded !== column.nativeType ? expanded : null;
847
+ }
848
+
849
+ /**
850
+ * Builds the DEFAULT clause for a column definition.
851
+ * Returns empty string if no default is defined.
852
+ *
853
+ * Note: autoincrement is handled specially via SERIAL types, so we skip it here.
854
+ */
855
+ function buildColumnDefaultSql(
856
+ columnDefault: PostgresColumnDefault | undefined,
857
+ column?: StorageColumn,
858
+ ): string {
859
+ if (!columnDefault) {
860
+ return '';
861
+ }
862
+
863
+ switch (columnDefault.kind) {
864
+ case 'literal':
865
+ return `DEFAULT ${renderDefaultLiteral(columnDefault.value, column)}`;
866
+ case 'function': {
867
+ // autoincrement is handled by SERIAL type, no explicit DEFAULT needed
868
+ if (columnDefault.expression === 'autoincrement()') {
869
+ return '';
870
+ }
871
+ return `DEFAULT ${columnDefault.expression}`;
872
+ }
873
+ case 'sequence':
874
+ // Sequence names use quoteIdentifier for safe identifier handling
875
+ return `DEFAULT nextval(${quoteIdentifier(columnDefault.name)}::regclass)`;
876
+ }
877
+ }
878
+
879
+ function renderDefaultLiteral(value: unknown, column?: StorageColumn): string {
880
+ const isJsonColumn = column?.nativeType === 'json' || column?.nativeType === 'jsonb';
881
+
882
+ if (value instanceof Date) {
883
+ return `'${escapeLiteral(value.toISOString())}'`;
884
+ }
885
+ if (!isJsonColumn && isTaggedBigInt(value)) {
886
+ if (!/^-?\d+$/.test(value.value)) {
887
+ throw new Error(`Invalid tagged bigint value: "${value.value}" is not a valid integer`);
888
+ }
889
+ return value.value;
890
+ }
891
+ if (typeof value === 'bigint') {
892
+ return value.toString();
893
+ }
894
+ if (typeof value === 'string') {
895
+ return `'${escapeLiteral(value)}'`;
896
+ }
897
+ if (typeof value === 'number' || typeof value === 'boolean') {
898
+ return String(value);
899
+ }
900
+ if (value === null) {
901
+ return 'NULL';
902
+ }
903
+ const json = JSON.stringify(value);
904
+ if (isJsonColumn) {
905
+ return `'${escapeLiteral(json)}'::${column.nativeType}`;
906
+ }
907
+ return `'${escapeLiteral(json)}'`;
908
+ }
909
+
654
910
  function qualifyTableName(schema: string, table: string): string {
655
911
  return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
656
912
  }
@@ -660,16 +916,6 @@ function toRegclassLiteral(schema: string, name: string): string {
660
916
  return `'${escapeLiteral(regclass)}'`;
661
917
  }
662
918
 
663
- /** Escapes and quotes a SQL identifier (table, column, schema name). */
664
- function quoteIdentifier(identifier: string): string {
665
- // TypeScript enforces string type - no runtime check needed for internal callers
666
- return `"${identifier.replace(/"/g, '""')}"`;
667
- }
668
-
669
- function escapeLiteral(value: string): string {
670
- return value.replace(/'/g, "''");
671
- }
672
-
673
919
  function sortedEntries<V>(record: Readonly<Record<string, V>>): Array<[string, V]> {
674
920
  return Object.entries(record).sort(([a], [b]) => a.localeCompare(b)) as Array<[string, V]>;
675
921
  }
@@ -741,9 +987,12 @@ function buildAddColumnSql(
741
987
  columnName: string,
742
988
  column: StorageColumn,
743
989
  ): string {
990
+ const typeSql = buildColumnTypeSql(column);
991
+ const defaultSql = buildColumnDefaultSql(column.default, column);
744
992
  const parts = [
745
993
  `ALTER TABLE ${qualifiedTableName}`,
746
- `ADD COLUMN ${quoteIdentifier(columnName)} ${column.nativeType}`,
994
+ `ADD COLUMN ${quoteIdentifier(columnName)} ${typeSql}`,
995
+ defaultSql,
747
996
  column.nullable ? '' : 'NOT NULL',
748
997
  ].filter(Boolean);
749
998
  return parts.join(' ');
@@ -772,15 +1021,23 @@ function tableHasPrimaryKeyCheck(
772
1021
  )`;
773
1022
  }
774
1023
 
1024
+ /**
1025
+ * Checks if table has a unique constraint satisfied by the given columns.
1026
+ * Uses shared semantic satisfaction predicate from verify-helpers.
1027
+ */
775
1028
  function hasUniqueConstraint(
776
1029
  table: SqlSchemaIR['tables'][string],
777
1030
  columns: readonly string[],
778
1031
  ): boolean {
779
- return table.uniques.some((unique) => arraysEqual(unique.columns, columns));
1032
+ return isUniqueConstraintSatisfied(table.uniques, table.indexes, columns);
780
1033
  }
781
1034
 
1035
+ /**
1036
+ * Checks if table has an index satisfied by the given columns.
1037
+ * Uses shared semantic satisfaction predicate from verify-helpers.
1038
+ */
782
1039
  function hasIndex(table: SqlSchemaIR['tables'][string], columns: readonly string[]): boolean {
783
- return table.indexes.some((index) => !index.unique && arraysEqual(index.columns, columns));
1040
+ return isIndexSatisfied(table.indexes, table.uniques, columns);
784
1041
  }
785
1042
 
786
1043
  function hasForeignKey(table: SqlSchemaIR['tables'][string], fk: ForeignKey): boolean {
@@ -794,6 +1051,8 @@ function hasForeignKey(table: SqlSchemaIR['tables'][string], fk: ForeignKey): bo
794
1051
 
795
1052
  function isAdditiveIssue(issue: SchemaIssue): boolean {
796
1053
  switch (issue.kind) {
1054
+ case 'type_missing':
1055
+ case 'type_values_mismatch':
797
1056
  case 'missing_table':
798
1057
  case 'missing_column':
799
1058
  case 'extension_missing':
@@ -860,3 +1119,42 @@ function compareStrings(a?: string, b?: string): number {
860
1119
  }
861
1120
  return a < b ? -1 : 1;
862
1121
  }
1122
+
1123
+ const REFERENTIAL_ACTION_SQL: Record<ReferentialAction, string> = {
1124
+ noAction: 'NO ACTION',
1125
+ restrict: 'RESTRICT',
1126
+ cascade: 'CASCADE',
1127
+ setNull: 'SET NULL',
1128
+ setDefault: 'SET DEFAULT',
1129
+ };
1130
+
1131
+ function buildForeignKeySql(
1132
+ schemaName: string,
1133
+ tableName: string,
1134
+ fkName: string,
1135
+ foreignKey: ForeignKey,
1136
+ ): string {
1137
+ let sql = `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
1138
+ ADD CONSTRAINT ${quoteIdentifier(fkName)}
1139
+ FOREIGN KEY (${foreignKey.columns.map(quoteIdentifier).join(', ')})
1140
+ REFERENCES ${qualifyTableName(schemaName, foreignKey.references.table)} (${foreignKey.references.columns
1141
+ .map(quoteIdentifier)
1142
+ .join(', ')})`;
1143
+
1144
+ if (foreignKey.onDelete !== undefined) {
1145
+ const action = REFERENTIAL_ACTION_SQL[foreignKey.onDelete];
1146
+ if (!action) {
1147
+ throw new Error(`Unknown referential action for onDelete: ${String(foreignKey.onDelete)}`);
1148
+ }
1149
+ sql += `\nON DELETE ${action}`;
1150
+ }
1151
+ if (foreignKey.onUpdate !== undefined) {
1152
+ const action = REFERENTIAL_ACTION_SQL[foreignKey.onUpdate];
1153
+ if (!action) {
1154
+ throw new Error(`Unknown referential action for onUpdate: ${String(foreignKey.onUpdate)}`);
1155
+ }
1156
+ sql += `\nON UPDATE ${action}`;
1157
+ }
1158
+
1159
+ return sql;
1160
+ }