@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.
- package/README.md +7 -1
- package/dist/control.d.mts +16 -0
- package/dist/control.d.mts.map +1 -0
- package/dist/control.mjs +2590 -0
- package/dist/control.mjs.map +1 -0
- package/dist/descriptor-meta-DxB8oZzB.mjs +13 -0
- package/dist/descriptor-meta-DxB8oZzB.mjs.map +1 -0
- package/dist/pack.d.mts +7 -0
- package/dist/pack.d.mts.map +1 -0
- package/dist/pack.mjs +9 -0
- package/dist/pack.mjs.map +1 -0
- package/dist/runtime.d.mts +9 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +21 -0
- package/dist/runtime.mjs.map +1 -0
- package/package.json +33 -33
- package/src/core/migrations/planner.ts +329 -31
- package/src/core/migrations/runner.ts +27 -20
- package/src/core/migrations/statement-builders.ts +9 -7
- package/src/core/types.ts +5 -0
- package/src/exports/control.ts +1 -3
- package/src/exports/runtime.ts +7 -12
- package/dist/chunk-RKEXRSSI.js +0 -14
- package/dist/chunk-RKEXRSSI.js.map +0 -1
- package/dist/core/descriptor-meta.d.ts +0 -9
- package/dist/core/descriptor-meta.d.ts.map +0 -1
- package/dist/core/migrations/planner.d.ts +0 -14
- package/dist/core/migrations/planner.d.ts.map +0 -1
- package/dist/core/migrations/runner.d.ts +0 -8
- package/dist/core/migrations/runner.d.ts.map +0 -1
- package/dist/core/migrations/statement-builders.d.ts +0 -30
- package/dist/core/migrations/statement-builders.d.ts.map +0 -1
- package/dist/exports/control.d.ts +0 -8
- package/dist/exports/control.d.ts.map +0 -1
- package/dist/exports/control.js +0 -1255
- package/dist/exports/control.js.map +0 -1
- package/dist/exports/pack.d.ts +0 -4
- package/dist/exports/pack.d.ts.map +0 -1
- package/dist/exports/pack.js +0 -11
- package/dist/exports/pack.js.map +0 -1
- package/dist/exports/runtime.d.ts +0 -12
- package/dist/exports/runtime.d.ts.map +0 -1
- package/dist/exports/runtime.js +0 -19
- 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.
|
|
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/
|
|
10
|
-
"@prisma-next/
|
|
11
|
-
"@prisma-next/core-
|
|
12
|
-
"@prisma-next/
|
|
13
|
-
"@prisma-next/sql
|
|
14
|
-
"@prisma-next/sql-
|
|
15
|
-
"@prisma-next/sql-
|
|
16
|
-
"@prisma-next/
|
|
17
|
-
"@prisma-next/
|
|
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
|
-
"
|
|
21
|
-
"tsup": "8.5.1",
|
|
22
|
+
"tsdown": "0.18.4",
|
|
22
23
|
"typescript": "5.9.3",
|
|
23
|
-
"vitest": "4.0.
|
|
24
|
-
"@prisma-next/adapter-postgres": "0.3.0-dev.
|
|
25
|
-
"@prisma-next/driver-postgres": "0.3.0-dev.
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
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": "
|
|
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 . --
|
|
53
|
-
"lint:fix": "biome check --write .
|
|
54
|
-
"lint:fix:unsafe": "biome check --write --unsafe .
|
|
55
|
-
"clean": "
|
|
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 {
|
|
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
|
-
|
|
112
|
-
...(
|
|
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
|
-
...(
|
|
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:
|
|
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
|
|
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
|
-
...(
|
|
587
|
-
...(
|
|
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
|
|
595
|
-
...(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
|
|
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)} ${
|
|
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
|
|
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
|
|
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
|
+
}
|