@prisma-next/family-sql 0.5.0-dev.9 → 0.6.0-dev.1
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 +2 -3
- package/dist/{authoring-type-constructors-BAR65pSK.mjs → authoring-type-constructors-F4JpCJl7.mjs} +14 -15
- package/dist/authoring-type-constructors-F4JpCJl7.mjs.map +1 -0
- package/dist/control-adapter.d.mts +26 -2
- package/dist/control-adapter.d.mts.map +1 -1
- package/dist/control-adapter.mjs +1 -1
- package/dist/control.d.mts +125 -45
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +1174 -54
- package/dist/control.mjs.map +1 -1
- package/dist/migration.d.mts +22 -24
- package/dist/migration.d.mts.map +1 -1
- package/dist/migration.mjs +25 -24
- package/dist/migration.mjs.map +1 -1
- package/dist/pack.d.mts +35 -23
- package/dist/pack.d.mts.map +1 -1
- package/dist/pack.mjs +3 -5
- package/dist/pack.mjs.map +1 -1
- package/dist/runtime.d.mts +19 -2
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +26 -4
- package/dist/runtime.mjs.map +1 -1
- package/dist/schema-verify.d.mts +4 -15
- package/dist/schema-verify.d.mts.map +1 -1
- package/dist/schema-verify.mjs +2 -3
- package/dist/test-utils.d.mts +2 -2
- package/dist/test-utils.mjs +2 -3
- package/dist/timestamp-now-generator-BWp8S2sa.mjs +86 -0
- package/dist/timestamp-now-generator-BWp8S2sa.mjs.map +1 -0
- package/dist/types-mhjAPuMn.d.mts +470 -0
- package/dist/types-mhjAPuMn.d.mts.map +1 -0
- package/dist/verify-pRYxnpiG.mjs +81 -0
- package/dist/verify-pRYxnpiG.mjs.map +1 -0
- package/dist/{verify-sql-schema-Ovz7RXR5.mjs → verify-sql-schema-1tDh3x5x.mjs} +18 -72
- package/dist/verify-sql-schema-1tDh3x5x.mjs.map +1 -0
- package/dist/{verify-sql-schema-BBhkqEDo.d.mts → verify-sql-schema-CPHiuYHR.d.mts} +2 -3
- package/dist/verify-sql-schema-CPHiuYHR.d.mts.map +1 -0
- package/dist/verify.d.mts +16 -21
- package/dist/verify.d.mts.map +1 -1
- package/dist/verify.mjs +2 -3
- package/package.json +23 -21
- package/src/core/authoring-field-presets.ts +35 -23
- package/src/core/control-adapter.ts +32 -0
- package/src/core/control-descriptor.ts +2 -1
- package/src/core/control-instance.ts +117 -30
- package/src/core/migrations/contract-to-schema-ir.ts +4 -29
- package/src/core/migrations/field-event-planner.ts +194 -0
- package/src/core/migrations/plan-helpers.ts +4 -0
- package/src/core/migrations/types.ts +207 -59
- package/src/core/operation-preview.ts +62 -0
- package/src/core/psl-contract-infer/default-mapping.ts +56 -0
- package/src/core/psl-contract-infer/name-transforms.ts +178 -0
- package/src/core/psl-contract-infer/postgres-default-mapping.ts +16 -0
- package/src/core/psl-contract-infer/postgres-type-map.ts +165 -0
- package/src/core/psl-contract-infer/printer-config.ts +55 -0
- package/src/core/psl-contract-infer/raw-default-parser.ts +91 -0
- package/src/core/psl-contract-infer/relation-inference.ts +196 -0
- package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +832 -0
- package/src/core/schema-verify/verify-helpers.ts +47 -70
- package/src/core/schema-verify/verify-sql-schema.ts +1 -6
- package/src/core/sql-migration.ts +25 -23
- package/src/core/timestamp-now-generator.ts +74 -0
- package/src/core/timestamp-now-runtime-generator.ts +24 -0
- package/src/core/verify.ts +46 -108
- package/src/exports/control.ts +11 -4
- package/src/exports/runtime.ts +2 -0
- package/src/exports/schema-verify.ts +0 -1
- package/src/exports/test-utils.ts +0 -1
- package/src/exports/verify.ts +1 -1
- package/dist/authoring-type-constructors-BAR65pSK.mjs.map +0 -1
- package/dist/types-C6K4mxDM.d.mts +0 -301
- package/dist/types-C6K4mxDM.d.mts.map +0 -1
- package/dist/verify-4GshvY4p.mjs +0 -122
- package/dist/verify-4GshvY4p.mjs.map +0 -1
- package/dist/verify-sql-schema-BBhkqEDo.d.mts.map +0 -1
- package/dist/verify-sql-schema-Ovz7RXR5.mjs.map +0 -1
package/dist/control.mjs
CHANGED
|
@@ -1,15 +1,1014 @@
|
|
|
1
|
-
import { n as sqlFamilyAuthoringFieldPresets, t as sqlFamilyAuthoringTypes } from "./authoring-type-constructors-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { n as sqlFamilyAuthoringFieldPresets, t as sqlFamilyAuthoringTypes } from "./authoring-type-constructors-F4JpCJl7.mjs";
|
|
2
|
+
import { a as extractCodecControlHooks, t as verifySqlSchema } from "./verify-sql-schema-1tDh3x5x.mjs";
|
|
3
|
+
import { t as collectSupportedCodecTypeIds } from "./verify-pRYxnpiG.mjs";
|
|
4
|
+
import { n as temporalAuthoringPresets, r as timestampNowControlDescriptor } from "./timestamp-now-generator-BWp8S2sa.mjs";
|
|
4
5
|
import { sqlEmission } from "@prisma-next/sql-contract-emitter";
|
|
5
6
|
import { emptyCodecLookup } from "@prisma-next/framework-components/codec";
|
|
6
|
-
import { SchemaTreeNode, VERIFY_CODE_HASH_MISMATCH, VERIFY_CODE_MARKER_MISSING, VERIFY_CODE_TARGET_MISMATCH, assembleAuthoringContributions } from "@prisma-next/framework-components/control";
|
|
7
|
+
import { APP_SPACE_ID, SchemaTreeNode, VERIFY_CODE_HASH_MISMATCH, VERIFY_CODE_MARKER_MISSING, VERIFY_CODE_TARGET_MISMATCH, assembleAuthoringContributions } from "@prisma-next/framework-components/control";
|
|
8
|
+
import { assertDescriptorSelfConsistency } from "@prisma-next/migration-tools/spaces";
|
|
7
9
|
import { validateContract } from "@prisma-next/sql-contract/validate";
|
|
8
10
|
import { ensureSchemaStatement, ensureTableStatement, writeContractMarker } from "@prisma-next/sql-runtime";
|
|
9
11
|
import { defaultIndexName } from "@prisma-next/sql-schema-ir/naming";
|
|
10
12
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
11
13
|
import { notOk, ok } from "@prisma-next/utils/result";
|
|
12
|
-
|
|
14
|
+
//#region src/core/operation-preview.ts
|
|
15
|
+
function isDdlStatement(sqlStatement) {
|
|
16
|
+
const trimmed = sqlStatement.trim().toLowerCase();
|
|
17
|
+
return trimmed.startsWith("create ") || trimmed.startsWith("alter ") || trimmed.startsWith("drop ");
|
|
18
|
+
}
|
|
19
|
+
function hasExecuteSteps(operation) {
|
|
20
|
+
const candidate = operation;
|
|
21
|
+
if (!("execute" in candidate) || !Array.isArray(candidate["execute"])) return false;
|
|
22
|
+
return candidate["execute"].every((step) => typeof step === "object" && step !== null && "sql" in step);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Extracts a best-effort SQL DDL preview for CLI plan output.
|
|
26
|
+
* Presentation-only: never used to decide migration correctness.
|
|
27
|
+
*/
|
|
28
|
+
function extractSqlDdl(operations) {
|
|
29
|
+
const statements = [];
|
|
30
|
+
for (const operation of operations) {
|
|
31
|
+
if (!hasExecuteSteps(operation)) continue;
|
|
32
|
+
for (const step of operation.execute) if (typeof step.sql === "string" && isDdlStatement(step.sql)) statements.push(step.sql.trim());
|
|
33
|
+
}
|
|
34
|
+
return statements;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Wraps `extractSqlDdl` into the family-agnostic `OperationPreview` shape.
|
|
38
|
+
* Each statement carries `language: 'sql'`.
|
|
39
|
+
*/
|
|
40
|
+
function sqlOperationsToPreview(operations) {
|
|
41
|
+
return { statements: extractSqlDdl(operations).map((text) => ({
|
|
42
|
+
text,
|
|
43
|
+
language: "sql"
|
|
44
|
+
})) };
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/core/psl-contract-infer/default-mapping.ts
|
|
48
|
+
const DEFAULT_FUNCTION_ATTRIBUTES = {
|
|
49
|
+
"autoincrement()": "@default(autoincrement())",
|
|
50
|
+
"now()": "@default(now())"
|
|
51
|
+
};
|
|
52
|
+
function mapDefault(columnDefault, options) {
|
|
53
|
+
switch (columnDefault.kind) {
|
|
54
|
+
case "literal": return { attribute: `@default(${formatLiteralValue(columnDefault.value)})` };
|
|
55
|
+
case "function": {
|
|
56
|
+
const attribute = options?.functionAttributes?.[columnDefault.expression] ?? DEFAULT_FUNCTION_ATTRIBUTES[columnDefault.expression] ?? options?.fallbackFunctionAttribute?.(columnDefault.expression);
|
|
57
|
+
return attribute ? { attribute } : { comment: `// Raw default: ${columnDefault.expression.replace(/[\r\n]+/g, " ")}` };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function formatLiteralValue(value) {
|
|
62
|
+
if (value === null) return "null";
|
|
63
|
+
switch (typeof value) {
|
|
64
|
+
case "boolean":
|
|
65
|
+
case "number": return String(value);
|
|
66
|
+
case "string": return quoteString(value);
|
|
67
|
+
default: return quoteString(JSON.stringify(value));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function quoteString(str) {
|
|
71
|
+
return `"${escapeString(str)}"`;
|
|
72
|
+
}
|
|
73
|
+
function escapeString(str) {
|
|
74
|
+
return JSON.stringify(str).slice(1, -1);
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/core/psl-contract-infer/name-transforms.ts
|
|
78
|
+
const PSL_RESERVED_WORDS = new Set([
|
|
79
|
+
"model",
|
|
80
|
+
"enum",
|
|
81
|
+
"types",
|
|
82
|
+
"type",
|
|
83
|
+
"generator",
|
|
84
|
+
"datasource"
|
|
85
|
+
]);
|
|
86
|
+
const IDENTIFIER_PART_PATTERN = /[A-Za-z0-9]+/g;
|
|
87
|
+
function hasSeparators(input) {
|
|
88
|
+
return /[^A-Za-z0-9]/.test(input);
|
|
89
|
+
}
|
|
90
|
+
function extractIdentifierParts(input) {
|
|
91
|
+
return input.match(IDENTIFIER_PART_PATTERN) ?? [];
|
|
92
|
+
}
|
|
93
|
+
function createSyntheticIdentifier(input) {
|
|
94
|
+
let hash = 2166136261;
|
|
95
|
+
for (const char of input) {
|
|
96
|
+
hash ^= char.codePointAt(0) ?? 0;
|
|
97
|
+
hash = Math.imul(hash, 16777619);
|
|
98
|
+
}
|
|
99
|
+
return `x${(hash >>> 0).toString(16)}`;
|
|
100
|
+
}
|
|
101
|
+
function sanitizeIdentifierCharacters(input) {
|
|
102
|
+
const sanitized = input.replace(/[^\w]/g, "");
|
|
103
|
+
return sanitized.length > 0 ? sanitized : createSyntheticIdentifier(input);
|
|
104
|
+
}
|
|
105
|
+
function capitalize(word) {
|
|
106
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
107
|
+
}
|
|
108
|
+
function snakeToPascalCase(input) {
|
|
109
|
+
const parts = extractIdentifierParts(input);
|
|
110
|
+
if (parts.length === 0) return capitalize(sanitizeIdentifierCharacters(input));
|
|
111
|
+
return parts.map(capitalize).join("");
|
|
112
|
+
}
|
|
113
|
+
function snakeToCamelCase(input) {
|
|
114
|
+
const parts = extractIdentifierParts(input);
|
|
115
|
+
if (parts.length === 0) return sanitizeIdentifierCharacters(input);
|
|
116
|
+
const [firstPart = input, ...rest] = parts;
|
|
117
|
+
return firstPart.charAt(0).toLowerCase() + firstPart.slice(1) + rest.map(capitalize).join("");
|
|
118
|
+
}
|
|
119
|
+
function needsEscaping(name) {
|
|
120
|
+
return PSL_RESERVED_WORDS.has(name.toLowerCase()) || /^\d/.test(name);
|
|
121
|
+
}
|
|
122
|
+
function escapeName(name) {
|
|
123
|
+
return `_${name}`;
|
|
124
|
+
}
|
|
125
|
+
function escapeIfNeeded(name) {
|
|
126
|
+
return needsEscaping(name) ? escapeName(name) : name;
|
|
127
|
+
}
|
|
128
|
+
function toModelName(tableName) {
|
|
129
|
+
let name;
|
|
130
|
+
if (hasSeparators(tableName)) name = snakeToPascalCase(tableName);
|
|
131
|
+
else name = tableName.charAt(0).toUpperCase() + tableName.slice(1);
|
|
132
|
+
if (needsEscaping(name)) return {
|
|
133
|
+
name: escapeName(name),
|
|
134
|
+
map: tableName
|
|
135
|
+
};
|
|
136
|
+
if (name !== tableName) return {
|
|
137
|
+
name,
|
|
138
|
+
map: tableName
|
|
139
|
+
};
|
|
140
|
+
return { name };
|
|
141
|
+
}
|
|
142
|
+
function toFieldName(columnName) {
|
|
143
|
+
let name;
|
|
144
|
+
if (hasSeparators(columnName)) name = snakeToCamelCase(columnName);
|
|
145
|
+
else name = columnName.charAt(0).toLowerCase() + columnName.slice(1);
|
|
146
|
+
if (needsEscaping(name)) return {
|
|
147
|
+
name: escapeName(name),
|
|
148
|
+
map: columnName
|
|
149
|
+
};
|
|
150
|
+
if (name !== columnName) return {
|
|
151
|
+
name,
|
|
152
|
+
map: columnName
|
|
153
|
+
};
|
|
154
|
+
return { name };
|
|
155
|
+
}
|
|
156
|
+
function toEnumName(pgTypeName) {
|
|
157
|
+
let name;
|
|
158
|
+
if (hasSeparators(pgTypeName)) name = snakeToPascalCase(pgTypeName);
|
|
159
|
+
else name = pgTypeName.charAt(0).toUpperCase() + pgTypeName.slice(1);
|
|
160
|
+
if (needsEscaping(name)) return {
|
|
161
|
+
name: escapeName(name),
|
|
162
|
+
map: pgTypeName
|
|
163
|
+
};
|
|
164
|
+
if (name !== pgTypeName) return {
|
|
165
|
+
name,
|
|
166
|
+
map: pgTypeName
|
|
167
|
+
};
|
|
168
|
+
return { name };
|
|
169
|
+
}
|
|
170
|
+
function pluralize(word) {
|
|
171
|
+
if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z") || word.endsWith("ch") || word.endsWith("sh")) return `${word}es`;
|
|
172
|
+
if (word.endsWith("y") && !/[aeiou]y$/i.test(word)) return `${word.slice(0, -1)}ies`;
|
|
173
|
+
return `${word}s`;
|
|
174
|
+
}
|
|
175
|
+
function deriveRelationFieldName(fkColumns, referencedTableName) {
|
|
176
|
+
if (fkColumns.length === 1) {
|
|
177
|
+
const [col = referencedTableName] = fkColumns;
|
|
178
|
+
const stripped = col.replace(/_id$/i, "").replace(/Id$/, "");
|
|
179
|
+
if (stripped.length > 0 && stripped !== col) return escapeIfNeeded(snakeToCamelCase(stripped));
|
|
180
|
+
return escapeIfNeeded(snakeToCamelCase(referencedTableName));
|
|
181
|
+
}
|
|
182
|
+
return escapeIfNeeded(snakeToCamelCase(referencedTableName));
|
|
183
|
+
}
|
|
184
|
+
function deriveBackRelationFieldName(childModelName, isOneToOne) {
|
|
185
|
+
const base = childModelName.charAt(0).toLowerCase() + childModelName.slice(1);
|
|
186
|
+
return isOneToOne ? base : pluralize(base);
|
|
187
|
+
}
|
|
188
|
+
function toNamedTypeName(columnName) {
|
|
189
|
+
let name;
|
|
190
|
+
if (hasSeparators(columnName)) name = snakeToPascalCase(columnName);
|
|
191
|
+
else name = columnName.charAt(0).toUpperCase() + columnName.slice(1);
|
|
192
|
+
return escapeIfNeeded(name);
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/core/psl-contract-infer/postgres-default-mapping.ts
|
|
196
|
+
const POSTGRES_FUNCTION_ATTRIBUTES = { "gen_random_uuid()": "@default(dbgenerated(\"gen_random_uuid()\"))" };
|
|
197
|
+
function formatDbGeneratedAttribute(expression) {
|
|
198
|
+
return `@default(dbgenerated(${JSON.stringify(expression)}))`;
|
|
199
|
+
}
|
|
200
|
+
function createPostgresDefaultMapping() {
|
|
201
|
+
return {
|
|
202
|
+
functionAttributes: POSTGRES_FUNCTION_ATTRIBUTES,
|
|
203
|
+
fallbackFunctionAttribute: formatDbGeneratedAttribute
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/core/psl-contract-infer/postgres-type-map.ts
|
|
208
|
+
const POSTGRES_TO_PSL = {
|
|
209
|
+
text: "String",
|
|
210
|
+
bool: "Boolean",
|
|
211
|
+
boolean: "Boolean",
|
|
212
|
+
int4: "Int",
|
|
213
|
+
integer: "Int",
|
|
214
|
+
int8: "BigInt",
|
|
215
|
+
bigint: "BigInt",
|
|
216
|
+
float8: "Float",
|
|
217
|
+
"double precision": "Float",
|
|
218
|
+
numeric: "Decimal",
|
|
219
|
+
decimal: "Decimal",
|
|
220
|
+
timestamptz: "DateTime",
|
|
221
|
+
"timestamp with time zone": "DateTime",
|
|
222
|
+
jsonb: "Json",
|
|
223
|
+
bytea: "Bytes"
|
|
224
|
+
};
|
|
225
|
+
const PRESERVED_NATIVE_TYPES = {
|
|
226
|
+
"character varying": {
|
|
227
|
+
pslType: "String",
|
|
228
|
+
attributeName: "db.VarChar"
|
|
229
|
+
},
|
|
230
|
+
character: {
|
|
231
|
+
pslType: "String",
|
|
232
|
+
attributeName: "db.Char"
|
|
233
|
+
},
|
|
234
|
+
char: {
|
|
235
|
+
pslType: "String",
|
|
236
|
+
attributeName: "db.Char"
|
|
237
|
+
},
|
|
238
|
+
varchar: {
|
|
239
|
+
pslType: "String",
|
|
240
|
+
attributeName: "db.VarChar"
|
|
241
|
+
},
|
|
242
|
+
uuid: {
|
|
243
|
+
pslType: "String",
|
|
244
|
+
attributeName: "db.Uuid"
|
|
245
|
+
},
|
|
246
|
+
int2: {
|
|
247
|
+
pslType: "Int",
|
|
248
|
+
attributeName: "db.SmallInt"
|
|
249
|
+
},
|
|
250
|
+
smallint: {
|
|
251
|
+
pslType: "Int",
|
|
252
|
+
attributeName: "db.SmallInt"
|
|
253
|
+
},
|
|
254
|
+
float4: {
|
|
255
|
+
pslType: "Float",
|
|
256
|
+
attributeName: "db.Real"
|
|
257
|
+
},
|
|
258
|
+
real: {
|
|
259
|
+
pslType: "Float",
|
|
260
|
+
attributeName: "db.Real"
|
|
261
|
+
},
|
|
262
|
+
timestamp: {
|
|
263
|
+
pslType: "DateTime",
|
|
264
|
+
attributeName: "db.Timestamp"
|
|
265
|
+
},
|
|
266
|
+
"timestamp without time zone": {
|
|
267
|
+
pslType: "DateTime",
|
|
268
|
+
attributeName: "db.Timestamp"
|
|
269
|
+
},
|
|
270
|
+
date: {
|
|
271
|
+
pslType: "DateTime",
|
|
272
|
+
attributeName: "db.Date"
|
|
273
|
+
},
|
|
274
|
+
time: {
|
|
275
|
+
pslType: "DateTime",
|
|
276
|
+
attributeName: "db.Time"
|
|
277
|
+
},
|
|
278
|
+
"time without time zone": {
|
|
279
|
+
pslType: "DateTime",
|
|
280
|
+
attributeName: "db.Time"
|
|
281
|
+
},
|
|
282
|
+
timetz: {
|
|
283
|
+
pslType: "DateTime",
|
|
284
|
+
attributeName: "db.Timetz"
|
|
285
|
+
},
|
|
286
|
+
"time with time zone": {
|
|
287
|
+
pslType: "DateTime",
|
|
288
|
+
attributeName: "db.Timetz"
|
|
289
|
+
},
|
|
290
|
+
json: {
|
|
291
|
+
pslType: "Json",
|
|
292
|
+
attributeName: "db.Json"
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const PARAMETERIZED_NATIVE_TYPES = {
|
|
296
|
+
"character varying": {
|
|
297
|
+
pslType: "String",
|
|
298
|
+
attributeName: "db.VarChar"
|
|
299
|
+
},
|
|
300
|
+
character: {
|
|
301
|
+
pslType: "String",
|
|
302
|
+
attributeName: "db.Char"
|
|
303
|
+
},
|
|
304
|
+
char: {
|
|
305
|
+
pslType: "String",
|
|
306
|
+
attributeName: "db.Char"
|
|
307
|
+
},
|
|
308
|
+
varchar: {
|
|
309
|
+
pslType: "String",
|
|
310
|
+
attributeName: "db.VarChar"
|
|
311
|
+
},
|
|
312
|
+
numeric: {
|
|
313
|
+
pslType: "Decimal",
|
|
314
|
+
attributeName: "db.Numeric"
|
|
315
|
+
},
|
|
316
|
+
timestamp: {
|
|
317
|
+
pslType: "DateTime",
|
|
318
|
+
attributeName: "db.Timestamp"
|
|
319
|
+
},
|
|
320
|
+
timestamptz: {
|
|
321
|
+
pslType: "DateTime",
|
|
322
|
+
attributeName: "db.Timestamptz"
|
|
323
|
+
},
|
|
324
|
+
time: {
|
|
325
|
+
pslType: "DateTime",
|
|
326
|
+
attributeName: "db.Time"
|
|
327
|
+
},
|
|
328
|
+
timetz: {
|
|
329
|
+
pslType: "DateTime",
|
|
330
|
+
attributeName: "db.Timetz"
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const PARAMETERIZED_TYPE_PATTERN = /^(.+?)\((.+)\)$/;
|
|
334
|
+
const ENUM_CODEC_ID = "pg/enum@1";
|
|
335
|
+
function getOwnMappingValue(map, key) {
|
|
336
|
+
return Object.hasOwn(map, key) ? map[key] : void 0;
|
|
337
|
+
}
|
|
338
|
+
function getOwnRecordValue(map, key) {
|
|
339
|
+
return Object.hasOwn(map, key) ? map[key] : void 0;
|
|
340
|
+
}
|
|
341
|
+
function createNativeTypeAttribute(name, args) {
|
|
342
|
+
return args && args.length > 0 ? {
|
|
343
|
+
name,
|
|
344
|
+
args
|
|
345
|
+
} : { name };
|
|
346
|
+
}
|
|
347
|
+
function splitTypeParameterList(params) {
|
|
348
|
+
return params.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
349
|
+
}
|
|
350
|
+
function createPostgresTypeMap(enumTypeNames) {
|
|
351
|
+
return { resolve(nativeType) {
|
|
352
|
+
if (enumTypeNames?.has(nativeType)) return {
|
|
353
|
+
pslType: nativeType,
|
|
354
|
+
nativeType
|
|
355
|
+
};
|
|
356
|
+
const paramMatch = nativeType.match(PARAMETERIZED_TYPE_PATTERN);
|
|
357
|
+
if (paramMatch) {
|
|
358
|
+
const [, baseType = nativeType, params = ""] = paramMatch;
|
|
359
|
+
const template = getOwnRecordValue(PARAMETERIZED_NATIVE_TYPES, baseType);
|
|
360
|
+
if (template) return {
|
|
361
|
+
pslType: template.pslType,
|
|
362
|
+
nativeType,
|
|
363
|
+
typeParams: {
|
|
364
|
+
baseType,
|
|
365
|
+
params
|
|
366
|
+
},
|
|
367
|
+
nativeTypeAttribute: createNativeTypeAttribute(template.attributeName, splitTypeParameterList(params))
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const preservedType = getOwnRecordValue(PRESERVED_NATIVE_TYPES, nativeType);
|
|
371
|
+
if (preservedType) return {
|
|
372
|
+
pslType: preservedType.pslType,
|
|
373
|
+
nativeType,
|
|
374
|
+
nativeTypeAttribute: createNativeTypeAttribute(preservedType.attributeName)
|
|
375
|
+
};
|
|
376
|
+
const pslType = getOwnMappingValue(POSTGRES_TO_PSL, nativeType);
|
|
377
|
+
if (pslType) return {
|
|
378
|
+
pslType,
|
|
379
|
+
nativeType
|
|
380
|
+
};
|
|
381
|
+
return {
|
|
382
|
+
unsupported: true,
|
|
383
|
+
nativeType
|
|
384
|
+
};
|
|
385
|
+
} };
|
|
386
|
+
}
|
|
387
|
+
function extractEnumInfo(annotations) {
|
|
388
|
+
const storageTypes = (annotations?.["pg"])?.["storageTypes"];
|
|
389
|
+
const typeNames = /* @__PURE__ */ new Set();
|
|
390
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
391
|
+
if (storageTypes) {
|
|
392
|
+
for (const [key, typeInstance] of Object.entries(storageTypes)) if (typeInstance.codecId === ENUM_CODEC_ID) {
|
|
393
|
+
typeNames.add(key);
|
|
394
|
+
const values = typeInstance.typeParams?.["values"];
|
|
395
|
+
if (Array.isArray(values)) definitions.set(key, values);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
typeNames,
|
|
400
|
+
definitions
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
//#endregion
|
|
404
|
+
//#region src/core/psl-contract-infer/raw-default-parser.ts
|
|
405
|
+
const NEXTVAL_PATTERN = /^nextval\s*\(/i;
|
|
406
|
+
const NOW_FUNCTION_PATTERN = /^(now\s*\(\s*\)|CURRENT_TIMESTAMP)$/i;
|
|
407
|
+
const CLOCK_TIMESTAMP_PATTERN = /^clock_timestamp\s*\(\s*\)$/i;
|
|
408
|
+
const TIMESTAMP_CAST_SUFFIX = /::timestamp(?:tz|\s+(?:with|without)\s+time\s+zone)?$/i;
|
|
409
|
+
const TEXT_CAST_SUFFIX = /::text$/i;
|
|
410
|
+
const NOW_LITERAL_PATTERN = /^'now'$/i;
|
|
411
|
+
const UUID_PATTERN = /^gen_random_uuid\s*\(\s*\)$/i;
|
|
412
|
+
const UUID_OSSP_PATTERN = /^uuid_generate_v4\s*\(\s*\)$/i;
|
|
413
|
+
const NULL_PATTERN = /^NULL(?:::.+)?$/i;
|
|
414
|
+
const TRUE_PATTERN = /^true$/i;
|
|
415
|
+
const FALSE_PATTERN = /^false$/i;
|
|
416
|
+
const NUMERIC_PATTERN = /^-?\d+(\.\d+)?$/;
|
|
417
|
+
const JSON_CAST_SUFFIX = /::jsonb?$/i;
|
|
418
|
+
const STRING_LITERAL_PATTERN = /^'((?:[^']|'')*)'(?:::(?:"[^"]+"|[\w\s]+)(?:\(\d+\))?)?$/;
|
|
419
|
+
function canonicalizeTimestampDefault(expr) {
|
|
420
|
+
if (NOW_FUNCTION_PATTERN.test(expr)) return "now()";
|
|
421
|
+
if (CLOCK_TIMESTAMP_PATTERN.test(expr)) return "clock_timestamp()";
|
|
422
|
+
if (!TIMESTAMP_CAST_SUFFIX.test(expr)) return void 0;
|
|
423
|
+
let inner = expr.replace(TIMESTAMP_CAST_SUFFIX, "").trim();
|
|
424
|
+
if (inner.startsWith("(") && inner.endsWith(")")) inner = inner.slice(1, -1).trim();
|
|
425
|
+
if (NOW_FUNCTION_PATTERN.test(inner)) return "now()";
|
|
426
|
+
if (CLOCK_TIMESTAMP_PATTERN.test(inner)) return "clock_timestamp()";
|
|
427
|
+
inner = inner.replace(TEXT_CAST_SUFFIX, "").trim();
|
|
428
|
+
if (NOW_LITERAL_PATTERN.test(inner)) return "now()";
|
|
429
|
+
}
|
|
430
|
+
function parseRawDefault(rawDefault, nativeType) {
|
|
431
|
+
const trimmed = rawDefault.trim();
|
|
432
|
+
const normalizedType = nativeType?.toLowerCase();
|
|
433
|
+
if (NEXTVAL_PATTERN.test(trimmed)) return {
|
|
434
|
+
kind: "function",
|
|
435
|
+
expression: "autoincrement()"
|
|
436
|
+
};
|
|
437
|
+
const canonicalTimestamp = canonicalizeTimestampDefault(trimmed);
|
|
438
|
+
if (canonicalTimestamp) return {
|
|
439
|
+
kind: "function",
|
|
440
|
+
expression: canonicalTimestamp
|
|
441
|
+
};
|
|
442
|
+
if (UUID_PATTERN.test(trimmed) || UUID_OSSP_PATTERN.test(trimmed)) return {
|
|
443
|
+
kind: "function",
|
|
444
|
+
expression: "gen_random_uuid()"
|
|
445
|
+
};
|
|
446
|
+
if (NULL_PATTERN.test(trimmed)) return {
|
|
447
|
+
kind: "literal",
|
|
448
|
+
value: null
|
|
449
|
+
};
|
|
450
|
+
if (TRUE_PATTERN.test(trimmed)) return {
|
|
451
|
+
kind: "literal",
|
|
452
|
+
value: true
|
|
453
|
+
};
|
|
454
|
+
if (FALSE_PATTERN.test(trimmed)) return {
|
|
455
|
+
kind: "literal",
|
|
456
|
+
value: false
|
|
457
|
+
};
|
|
458
|
+
if (NUMERIC_PATTERN.test(trimmed)) return {
|
|
459
|
+
kind: "literal",
|
|
460
|
+
value: Number(trimmed)
|
|
461
|
+
};
|
|
462
|
+
const stringMatch = trimmed.match(STRING_LITERAL_PATTERN);
|
|
463
|
+
if (stringMatch?.[1] !== void 0) {
|
|
464
|
+
const unescaped = stringMatch[1].replace(/''/g, "'");
|
|
465
|
+
if (normalizedType === "json" || normalizedType === "jsonb") {
|
|
466
|
+
if (JSON_CAST_SUFFIX.test(trimmed)) return {
|
|
467
|
+
kind: "function",
|
|
468
|
+
expression: trimmed
|
|
469
|
+
};
|
|
470
|
+
try {
|
|
471
|
+
return {
|
|
472
|
+
kind: "literal",
|
|
473
|
+
value: JSON.parse(unescaped)
|
|
474
|
+
};
|
|
475
|
+
} catch {}
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
kind: "literal",
|
|
479
|
+
value: unescaped
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
kind: "function",
|
|
484
|
+
expression: trimmed
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
//#endregion
|
|
488
|
+
//#region src/core/psl-contract-infer/relation-inference.ts
|
|
489
|
+
const DEFAULT_ON_DELETE = "noAction";
|
|
490
|
+
const DEFAULT_ON_UPDATE = "noAction";
|
|
491
|
+
const REFERENTIAL_ACTION_PSL = {
|
|
492
|
+
noAction: "NoAction",
|
|
493
|
+
restrict: "Restrict",
|
|
494
|
+
cascade: "Cascade",
|
|
495
|
+
setNull: "SetNull",
|
|
496
|
+
setDefault: "SetDefault"
|
|
497
|
+
};
|
|
498
|
+
function inferRelations(tables, modelNameMap) {
|
|
499
|
+
const relationsByTable = /* @__PURE__ */ new Map();
|
|
500
|
+
const fkCountByPair = /* @__PURE__ */ new Map();
|
|
501
|
+
for (const table of Object.values(tables)) for (const fk of table.foreignKeys) {
|
|
502
|
+
const pairKey = `${table.name}→${fk.referencedTable}`;
|
|
503
|
+
fkCountByPair.set(pairKey, (fkCountByPair.get(pairKey) ?? 0) + 1);
|
|
504
|
+
}
|
|
505
|
+
const usedFieldNames = /* @__PURE__ */ new Map();
|
|
506
|
+
for (const table of Object.values(tables)) {
|
|
507
|
+
const names = /* @__PURE__ */ new Set();
|
|
508
|
+
for (const col of Object.values(table.columns)) names.add(col.name);
|
|
509
|
+
usedFieldNames.set(table.name, names);
|
|
510
|
+
}
|
|
511
|
+
for (const table of Object.values(tables)) for (const fk of table.foreignKeys) {
|
|
512
|
+
const childTableName = table.name;
|
|
513
|
+
const parentTableName = fk.referencedTable;
|
|
514
|
+
const childUsed = usedFieldNames.get(childTableName);
|
|
515
|
+
const childModelName = modelNameMap.get(childTableName) ?? childTableName;
|
|
516
|
+
const parentModelName = modelNameMap.get(parentTableName) ?? parentTableName;
|
|
517
|
+
const pairKey = `${childTableName}→${parentTableName}`;
|
|
518
|
+
const isSelfRelation = childTableName === parentTableName;
|
|
519
|
+
const needsRelationName = fkCountByPair.get(pairKey) > 1 || isSelfRelation;
|
|
520
|
+
const isOneToOne = detectOneToOne(fk, table);
|
|
521
|
+
const childRelFieldName = resolveUniqueFieldName(deriveRelationFieldName(fk.columns, parentTableName), childUsed, parentModelName);
|
|
522
|
+
const relationName = needsRelationName ? deriveRelationName(fk, childRelFieldName, parentModelName, isSelfRelation) : void 0;
|
|
523
|
+
addRelationField(relationsByTable, childTableName, buildChildRelationField(childRelFieldName, parentModelName, fk, fk.columns.some((columnName) => table.columns[columnName]?.nullable ?? false), relationName));
|
|
524
|
+
childUsed.add(childRelFieldName);
|
|
525
|
+
const parentUsed = usedFieldNames.get(parentTableName) ?? /* @__PURE__ */ new Set();
|
|
526
|
+
usedFieldNames.set(parentTableName, parentUsed);
|
|
527
|
+
const backRelFieldName = resolveUniqueFieldName(deriveBackRelationFieldName(childModelName, isOneToOne), parentUsed, childModelName);
|
|
528
|
+
addRelationField(relationsByTable, parentTableName, {
|
|
529
|
+
fieldName: backRelFieldName,
|
|
530
|
+
typeName: childModelName,
|
|
531
|
+
optional: isOneToOne,
|
|
532
|
+
list: !isOneToOne,
|
|
533
|
+
relationName
|
|
534
|
+
});
|
|
535
|
+
parentUsed.add(backRelFieldName);
|
|
536
|
+
}
|
|
537
|
+
return { relationsByTable };
|
|
538
|
+
}
|
|
539
|
+
function detectOneToOne(fk, table) {
|
|
540
|
+
const fkCols = [...fk.columns].sort();
|
|
541
|
+
if (table.primaryKey) {
|
|
542
|
+
const pkCols = [...table.primaryKey.columns].sort();
|
|
543
|
+
if (pkCols.length === fkCols.length && pkCols.every((c, i) => c === fkCols[i])) return true;
|
|
544
|
+
}
|
|
545
|
+
for (const unique of table.uniques) {
|
|
546
|
+
const uniqueCols = [...unique.columns].sort();
|
|
547
|
+
if (uniqueCols.length === fkCols.length && uniqueCols.every((c, i) => c === fkCols[i])) return true;
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
function deriveRelationName(fk, childRelationFieldName, parentModelName, isSelfRelation) {
|
|
552
|
+
if (fk.name) return fk.name;
|
|
553
|
+
if (isSelfRelation) return `${childRelationFieldName.charAt(0).toUpperCase() + childRelationFieldName.slice(1)}${pluralize(parentModelName)}`;
|
|
554
|
+
return fk.columns.join("_");
|
|
555
|
+
}
|
|
556
|
+
function buildChildRelationField(fieldName, parentModelName, fk, optional, relationName) {
|
|
557
|
+
const onDelete = fk.onDelete && fk.onDelete !== DEFAULT_ON_DELETE ? fk.onDelete : void 0;
|
|
558
|
+
const onUpdate = fk.onUpdate && fk.onUpdate !== DEFAULT_ON_UPDATE ? fk.onUpdate : void 0;
|
|
559
|
+
return {
|
|
560
|
+
fieldName,
|
|
561
|
+
typeName: parentModelName,
|
|
562
|
+
referencedTableName: fk.referencedTable,
|
|
563
|
+
optional,
|
|
564
|
+
list: false,
|
|
565
|
+
relationName,
|
|
566
|
+
fkName: fk.name,
|
|
567
|
+
fields: fk.columns,
|
|
568
|
+
references: fk.referencedColumns,
|
|
569
|
+
onDelete: onDelete ? REFERENTIAL_ACTION_PSL[onDelete] : void 0,
|
|
570
|
+
onUpdate: onUpdate ? REFERENTIAL_ACTION_PSL[onUpdate] : void 0
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function resolveUniqueFieldName(desired, usedNames, fallbackSuffix) {
|
|
574
|
+
if (!usedNames.has(desired)) return desired;
|
|
575
|
+
const withSuffix = `${desired}${fallbackSuffix}`;
|
|
576
|
+
if (!usedNames.has(withSuffix)) return withSuffix;
|
|
577
|
+
let counter = 2;
|
|
578
|
+
while (usedNames.has(`${desired}${counter}`)) counter++;
|
|
579
|
+
return `${desired}${counter}`;
|
|
580
|
+
}
|
|
581
|
+
function addRelationField(map, tableName, field) {
|
|
582
|
+
const existing = map.get(tableName);
|
|
583
|
+
if (existing) existing.push(field);
|
|
584
|
+
else map.set(tableName, [field]);
|
|
585
|
+
}
|
|
586
|
+
//#endregion
|
|
587
|
+
//#region src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts
|
|
588
|
+
const SYNTHETIC_SPAN = {
|
|
589
|
+
start: {
|
|
590
|
+
offset: 0,
|
|
591
|
+
line: 1,
|
|
592
|
+
column: 1
|
|
593
|
+
},
|
|
594
|
+
end: {
|
|
595
|
+
offset: 0,
|
|
596
|
+
line: 1,
|
|
597
|
+
column: 1
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
const PSL_SCALAR_TYPE_NAMES = new Set([
|
|
601
|
+
"String",
|
|
602
|
+
"Boolean",
|
|
603
|
+
"Int",
|
|
604
|
+
"BigInt",
|
|
605
|
+
"Float",
|
|
606
|
+
"Decimal",
|
|
607
|
+
"DateTime",
|
|
608
|
+
"Json",
|
|
609
|
+
"Bytes"
|
|
610
|
+
]);
|
|
611
|
+
/**
|
|
612
|
+
* Converts a SQL schema IR into a PSL AST suitable for `printPsl`.
|
|
613
|
+
*
|
|
614
|
+
* This function owns all SQL-specific concerns: native type mapping (Postgres),
|
|
615
|
+
* relation inference from foreign keys, enum extraction, and raw default parsing.
|
|
616
|
+
* The output is a fully-formed `PslDocumentAst` with synthetic spans.
|
|
617
|
+
*/
|
|
618
|
+
function sqlSchemaIrToPslAst(schemaIR) {
|
|
619
|
+
const enumInfo = extractEnumInfo(schemaIR.annotations);
|
|
620
|
+
return buildPslDocumentAst(schemaIR, {
|
|
621
|
+
typeMap: createPostgresTypeMap(enumInfo.typeNames),
|
|
622
|
+
defaultMapping: createPostgresDefaultMapping(),
|
|
623
|
+
enumInfo,
|
|
624
|
+
parseRawDefault
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
function buildPslDocumentAst(schemaIR, options) {
|
|
628
|
+
const { typeMap, defaultMapping, enumInfo, parseRawDefault: rawDefaultParser } = options;
|
|
629
|
+
const { typeNames: enumTypeNames, definitions: enumDefinitions } = enumInfo ?? {
|
|
630
|
+
typeNames: /* @__PURE__ */ new Set(),
|
|
631
|
+
definitions: /* @__PURE__ */ new Map()
|
|
632
|
+
};
|
|
633
|
+
const modelNames = buildTopLevelNameMap(Object.keys(schemaIR.tables), toModelName, "model", "table");
|
|
634
|
+
const enumNames = buildTopLevelNameMap(enumTypeNames, toEnumName, "enum", "enum type");
|
|
635
|
+
assertNoCrossKindNameCollisions(modelNames, enumNames);
|
|
636
|
+
const modelNameMap = new Map([...modelNames].map(([tableName, result]) => [tableName, result.name]));
|
|
637
|
+
const enumNameMap = new Map([...enumNames].map(([pgTypeName, result]) => [pgTypeName, result.name]));
|
|
638
|
+
const reservedNamedTypeNames = createReservedNamedTypeNames(modelNames, enumNames);
|
|
639
|
+
const fieldNamesByTable = buildFieldNamesByTable(schemaIR.tables);
|
|
640
|
+
const { relationsByTable } = inferRelations(schemaIR.tables, modelNameMap);
|
|
641
|
+
const namedTypes = seedNamedTypeRegistry(schemaIR, typeMap, enumNameMap, reservedNamedTypeNames);
|
|
642
|
+
const models = [];
|
|
643
|
+
for (const table of Object.values(schemaIR.tables)) models.push(buildModel(table, typeMap, enumNameMap, fieldNamesByTable, namedTypes, defaultMapping, rawDefaultParser, relationsByTable.get(table.name) ?? []));
|
|
644
|
+
const sortedModels = topologicalSort(models, schemaIR.tables, modelNameMap);
|
|
645
|
+
const enums = [];
|
|
646
|
+
for (const [pgTypeName, values] of enumDefinitions) {
|
|
647
|
+
const enumName = enumNames.get(pgTypeName);
|
|
648
|
+
enums.push(buildEnum(enumName, values));
|
|
649
|
+
}
|
|
650
|
+
enums.sort((a, b) => a.name.localeCompare(b.name));
|
|
651
|
+
const namedTypeEntries = [...namedTypes.entriesByKey.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
652
|
+
const types = namedTypeEntries.length > 0 ? {
|
|
653
|
+
kind: "types",
|
|
654
|
+
declarations: namedTypeEntries.map(buildNamedTypeDeclaration),
|
|
655
|
+
span: SYNTHETIC_SPAN
|
|
656
|
+
} : void 0;
|
|
657
|
+
return {
|
|
658
|
+
kind: "document",
|
|
659
|
+
sourceId: "<sql-schema-ir>",
|
|
660
|
+
models: sortedModels,
|
|
661
|
+
enums,
|
|
662
|
+
compositeTypes: [],
|
|
663
|
+
...types ? { types } : {},
|
|
664
|
+
span: SYNTHETIC_SPAN
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function buildModel(table, typeMap, enumNameMap, fieldNamesByTable, namedTypes, defaultMapping, rawDefaultParser, relationFields) {
|
|
668
|
+
const { name: modelName, map: mapName } = toModelName(table.name);
|
|
669
|
+
const fieldNameMap = fieldNamesByTable.get(table.name);
|
|
670
|
+
const pkColumns = new Set(table.primaryKey?.columns ?? []);
|
|
671
|
+
const isSinglePk = pkColumns.size === 1;
|
|
672
|
+
const singlePkConstraintName = isSinglePk ? table.primaryKey?.name : void 0;
|
|
673
|
+
const uniqueColumns = /* @__PURE__ */ new Map();
|
|
674
|
+
for (const unique of table.uniques) if (unique.columns.length === 1) {
|
|
675
|
+
const [columnName = ""] = unique.columns;
|
|
676
|
+
const existingConstraintName = uniqueColumns.get(columnName);
|
|
677
|
+
if (!uniqueColumns.has(columnName) || existingConstraintName === void 0 && unique.name) uniqueColumns.set(columnName, unique.name);
|
|
678
|
+
}
|
|
679
|
+
const fields = [];
|
|
680
|
+
for (const column of Object.values(table.columns)) fields.push(buildScalarField(column, table, typeMap, enumNameMap, fieldNameMap, namedTypes, defaultMapping, rawDefaultParser, pkColumns, isSinglePk, singlePkConstraintName, uniqueColumns));
|
|
681
|
+
const usedFieldNames = new Set(fields.map((field) => field.name));
|
|
682
|
+
for (const rel of relationFields) fields.push(buildRelationField(rel, table.name, fieldNamesByTable, usedFieldNames));
|
|
683
|
+
const modelAttributes = [];
|
|
684
|
+
if (table.primaryKey && table.primaryKey.columns.length > 1) {
|
|
685
|
+
const pkFieldNames = table.primaryKey.columns.map((columnName) => resolveColumnFieldName(fieldNamesByTable, table.name, columnName));
|
|
686
|
+
modelAttributes.push(buildModelConstraintAttribute("id", pkFieldNames, table.primaryKey.name));
|
|
687
|
+
}
|
|
688
|
+
for (const unique of table.uniques) if (unique.columns.length > 1) {
|
|
689
|
+
const uniqueFieldNames = unique.columns.map((columnName) => resolveColumnFieldName(fieldNamesByTable, table.name, columnName));
|
|
690
|
+
modelAttributes.push(buildModelConstraintAttribute("unique", uniqueFieldNames, unique.name));
|
|
691
|
+
}
|
|
692
|
+
for (const index of table.indexes) if (!index.unique) {
|
|
693
|
+
const indexFieldNames = index.columns.map((columnName) => resolveColumnFieldName(fieldNamesByTable, table.name, columnName));
|
|
694
|
+
modelAttributes.push(buildModelConstraintAttribute("index", indexFieldNames, index.name));
|
|
695
|
+
}
|
|
696
|
+
if (mapName) modelAttributes.push(buildMapAttribute("model", mapName));
|
|
697
|
+
const comment = table.primaryKey ? void 0 : "// WARNING: This table has no primary key in the database";
|
|
698
|
+
return {
|
|
699
|
+
kind: "model",
|
|
700
|
+
name: modelName,
|
|
701
|
+
fields,
|
|
702
|
+
attributes: modelAttributes,
|
|
703
|
+
span: SYNTHETIC_SPAN,
|
|
704
|
+
...comment !== void 0 ? { comment } : {}
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function buildScalarField(column, table, typeMap, enumNameMap, fieldNameMap, namedTypes, defaultMapping, rawDefaultParser, pkColumns, isSinglePk, singlePkConstraintName, uniqueColumns) {
|
|
708
|
+
const resolvedField = fieldNameMap?.get(column.name);
|
|
709
|
+
const fieldName = resolvedField?.fieldName ?? toFieldName(column.name).name;
|
|
710
|
+
const fieldMap = resolvedField?.fieldMap;
|
|
711
|
+
const resolution = typeMap.resolve(column.nativeType, table.annotations);
|
|
712
|
+
if ("unsupported" in resolution) {
|
|
713
|
+
const attrs = [];
|
|
714
|
+
if (fieldMap !== void 0) attrs.push(buildMapAttribute("field", fieldMap));
|
|
715
|
+
return {
|
|
716
|
+
kind: "field",
|
|
717
|
+
name: fieldName,
|
|
718
|
+
typeName: `Unsupported("${escapePslString(resolution.nativeType)}")`,
|
|
719
|
+
optional: column.nullable,
|
|
720
|
+
list: false,
|
|
721
|
+
attributes: attrs,
|
|
722
|
+
span: SYNTHETIC_SPAN
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
let typeName = resolution.pslType;
|
|
726
|
+
const enumPslName = enumNameMap.get(column.nativeType);
|
|
727
|
+
if (enumPslName) typeName = enumPslName;
|
|
728
|
+
if (resolution.nativeTypeAttribute && !enumPslName) typeName = resolveNamedTypeName(namedTypes, resolution);
|
|
729
|
+
const attributes = [];
|
|
730
|
+
const isId = isSinglePk && pkColumns.has(column.name);
|
|
731
|
+
if (isId) attributes.push(buildSimpleConstraintFieldAttribute("id", singlePkConstraintName));
|
|
732
|
+
if (column.default !== void 0) {
|
|
733
|
+
const parsed = parseColumnDefault(column.default, column.nativeType, rawDefaultParser);
|
|
734
|
+
if (parsed) {
|
|
735
|
+
const result = mapDefault(parsed, defaultMapping);
|
|
736
|
+
if ("attribute" in result) attributes.push(parseDefaultAttributeString(result.attribute));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (uniqueColumns.has(column.name) && !isId) {
|
|
740
|
+
const uniqueConstraintName = uniqueColumns.get(column.name);
|
|
741
|
+
attributes.push(buildSimpleConstraintFieldAttribute("unique", uniqueConstraintName));
|
|
742
|
+
}
|
|
743
|
+
if (fieldMap !== void 0) attributes.push(buildMapAttribute("field", fieldMap));
|
|
744
|
+
return {
|
|
745
|
+
kind: "field",
|
|
746
|
+
name: fieldName,
|
|
747
|
+
typeName,
|
|
748
|
+
optional: column.nullable,
|
|
749
|
+
list: false,
|
|
750
|
+
attributes,
|
|
751
|
+
span: SYNTHETIC_SPAN
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function buildRelationField(rel, hostTableName, fieldNamesByTable, usedFieldNames) {
|
|
755
|
+
const fieldName = createUniqueFieldName(rel.fieldName, usedFieldNames);
|
|
756
|
+
usedFieldNames.add(fieldName);
|
|
757
|
+
const args = [];
|
|
758
|
+
if (rel.fields && rel.references) {
|
|
759
|
+
if (rel.relationName) args.push(namedArg("name", `"${escapePslString(rel.relationName)}"`));
|
|
760
|
+
args.push(namedArg("fields", `[${rel.fields.map((columnName) => resolveColumnFieldName(fieldNamesByTable, hostTableName, columnName)).join(", ")}]`));
|
|
761
|
+
args.push(namedArg("references", `[${rel.references.map((columnName) => resolveColumnFieldName(fieldNamesByTable, rel.referencedTableName ?? "", columnName)).join(", ")}]`));
|
|
762
|
+
if (rel.onDelete) args.push(namedArg("onDelete", rel.onDelete));
|
|
763
|
+
if (rel.onUpdate) args.push(namedArg("onUpdate", rel.onUpdate));
|
|
764
|
+
if (rel.fkName) args.push(namedArg("map", `"${escapePslString(rel.fkName)}"`));
|
|
765
|
+
} else if (rel.relationName) args.push(namedArg("name", `"${escapePslString(rel.relationName)}"`));
|
|
766
|
+
const attrs = args.length > 0 ? [buildAttribute("field", "relation", args)] : [];
|
|
767
|
+
return {
|
|
768
|
+
kind: "field",
|
|
769
|
+
name: fieldName,
|
|
770
|
+
typeName: rel.typeName,
|
|
771
|
+
optional: rel.optional,
|
|
772
|
+
list: rel.list,
|
|
773
|
+
attributes: attrs,
|
|
774
|
+
span: SYNTHETIC_SPAN
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function buildModelConstraintAttribute(name, fields, constraintName) {
|
|
778
|
+
const args = [positionalArg(`[${fields.join(", ")}]`)];
|
|
779
|
+
if (constraintName !== void 0) args.push(namedArg("map", `"${escapePslString(constraintName)}"`));
|
|
780
|
+
return buildAttribute("model", name, args);
|
|
781
|
+
}
|
|
782
|
+
function buildSimpleConstraintFieldAttribute(name, constraintName) {
|
|
783
|
+
if (constraintName === void 0) return buildAttribute("field", name, []);
|
|
784
|
+
return buildAttribute("field", name, [namedArg("map", `"${escapePslString(constraintName)}"`)]);
|
|
785
|
+
}
|
|
786
|
+
function parseDefaultAttributeString(attributeText) {
|
|
787
|
+
return buildAttribute("field", "default", [positionalArg(attributeText.replace(/^@default\(/, "").replace(/\)$/, ""))]);
|
|
788
|
+
}
|
|
789
|
+
function buildMapAttribute(target, mapName) {
|
|
790
|
+
return buildAttribute(target, "map", [positionalArg(`"${escapePslString(mapName)}"`)]);
|
|
791
|
+
}
|
|
792
|
+
function buildAttribute(target, name, args) {
|
|
793
|
+
return {
|
|
794
|
+
kind: "attribute",
|
|
795
|
+
target,
|
|
796
|
+
name,
|
|
797
|
+
args,
|
|
798
|
+
span: SYNTHETIC_SPAN
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function positionalArg(value) {
|
|
802
|
+
return {
|
|
803
|
+
kind: "positional",
|
|
804
|
+
value,
|
|
805
|
+
span: SYNTHETIC_SPAN
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function namedArg(name, value) {
|
|
809
|
+
return {
|
|
810
|
+
kind: "named",
|
|
811
|
+
name,
|
|
812
|
+
value,
|
|
813
|
+
span: SYNTHETIC_SPAN
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function buildEnum(name, values) {
|
|
817
|
+
const attrs = [];
|
|
818
|
+
if (name.map) attrs.push(buildMapAttribute("enum", name.map));
|
|
819
|
+
return {
|
|
820
|
+
kind: "enum",
|
|
821
|
+
name: name.name,
|
|
822
|
+
values: values.map((value) => ({
|
|
823
|
+
kind: "enumValue",
|
|
824
|
+
name: value,
|
|
825
|
+
span: SYNTHETIC_SPAN
|
|
826
|
+
})),
|
|
827
|
+
attributes: attrs,
|
|
828
|
+
span: SYNTHETIC_SPAN
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
function buildNamedTypeDeclaration(entry) {
|
|
832
|
+
const attribute = buildAttribute("namedType", entry.nativeTypeAttribute.name, (entry.nativeTypeAttribute.args ?? []).map(positionalArg));
|
|
833
|
+
return {
|
|
834
|
+
kind: "namedType",
|
|
835
|
+
name: entry.name,
|
|
836
|
+
baseType: entry.baseType,
|
|
837
|
+
attributes: [attribute],
|
|
838
|
+
span: SYNTHETIC_SPAN
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function escapePslString(value) {
|
|
842
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Resolves a `SqlColumnIR.default` value into a normalized {@link ColumnDefault}.
|
|
846
|
+
*
|
|
847
|
+
* `SqlSchemaIR` types the column default as `string` (a raw database default
|
|
848
|
+
* expression). Some legacy fixtures and tests still pass already-normalized
|
|
849
|
+
* `ColumnDefault` objects in the same slot, so we accept either shape
|
|
850
|
+
* defensively at runtime.
|
|
851
|
+
*/
|
|
852
|
+
function parseColumnDefault(value, nativeType, rawDefaultParser) {
|
|
853
|
+
if (typeof value === "string") return rawDefaultParser ? rawDefaultParser(value, nativeType) : void 0;
|
|
854
|
+
if (value !== null && typeof value === "object" && "kind" in value) return value;
|
|
855
|
+
}
|
|
856
|
+
function buildFieldNamesByTable(tables) {
|
|
857
|
+
const fieldNamesByTable = /* @__PURE__ */ new Map();
|
|
858
|
+
for (const table of Object.values(tables)) {
|
|
859
|
+
const assignmentOrder = [...Object.values(table.columns).map((column, index) => {
|
|
860
|
+
const { name, map } = toFieldName(column.name);
|
|
861
|
+
return {
|
|
862
|
+
columnName: column.name,
|
|
863
|
+
desiredFieldName: name,
|
|
864
|
+
fieldMap: map,
|
|
865
|
+
index
|
|
866
|
+
};
|
|
867
|
+
})].sort((left, right) => {
|
|
868
|
+
const mapComparison = Number(left.fieldMap !== void 0) - Number(right.fieldMap !== void 0);
|
|
869
|
+
if (mapComparison !== 0) return mapComparison;
|
|
870
|
+
return left.index - right.index;
|
|
871
|
+
});
|
|
872
|
+
const usedFieldNames = /* @__PURE__ */ new Set();
|
|
873
|
+
const tableFieldNames = /* @__PURE__ */ new Map();
|
|
874
|
+
for (const column of assignmentOrder) {
|
|
875
|
+
const fieldName = createUniqueFieldName(column.desiredFieldName, usedFieldNames);
|
|
876
|
+
usedFieldNames.add(fieldName);
|
|
877
|
+
tableFieldNames.set(column.columnName, {
|
|
878
|
+
fieldName,
|
|
879
|
+
fieldMap: column.fieldMap
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
fieldNamesByTable.set(table.name, tableFieldNames);
|
|
883
|
+
}
|
|
884
|
+
return fieldNamesByTable;
|
|
885
|
+
}
|
|
886
|
+
function resolveColumnFieldName(fieldNamesByTable, tableName, columnName) {
|
|
887
|
+
return fieldNamesByTable.get(tableName)?.get(columnName)?.fieldName ?? toFieldName(columnName).name;
|
|
888
|
+
}
|
|
889
|
+
function createUniqueFieldName(desiredName, usedFieldNames) {
|
|
890
|
+
if (!usedFieldNames.has(desiredName)) return desiredName;
|
|
891
|
+
let counter = 2;
|
|
892
|
+
while (usedFieldNames.has(`${desiredName}${counter}`)) counter++;
|
|
893
|
+
return `${desiredName}${counter}`;
|
|
894
|
+
}
|
|
895
|
+
function buildTopLevelNameMap(sources, normalize, kind, sourceKind) {
|
|
896
|
+
const results = /* @__PURE__ */ new Map();
|
|
897
|
+
const normalizedToSources = /* @__PURE__ */ new Map();
|
|
898
|
+
for (const source of sources) {
|
|
899
|
+
const normalized = normalize(source);
|
|
900
|
+
results.set(source, normalized);
|
|
901
|
+
normalizedToSources.set(normalized.name, [...normalizedToSources.get(normalized.name) ?? [], source]);
|
|
902
|
+
}
|
|
903
|
+
const duplicates = [...normalizedToSources.entries()].filter(([, conflictingSources]) => conflictingSources.length > 1);
|
|
904
|
+
if (duplicates.length > 0) {
|
|
905
|
+
const details = duplicates.map(([normalizedName, conflictingSources]) => `- ${kind} "${normalizedName}" from ${sourceKind}s ${conflictingSources.map((source) => `"${source}"`).join(", ")}`);
|
|
906
|
+
throw new Error(`PSL ${kind} name collisions detected:\n${details.join("\n")}`);
|
|
907
|
+
}
|
|
908
|
+
return results;
|
|
909
|
+
}
|
|
910
|
+
function assertNoCrossKindNameCollisions(modelNames, enumNames) {
|
|
911
|
+
const enumSourceByName = new Map([...enumNames].map(([source, result]) => [result.name, source]));
|
|
912
|
+
const collisions = [...modelNames.entries()].map(([tableName, result]) => {
|
|
913
|
+
const enumSource = enumSourceByName.get(result.name);
|
|
914
|
+
return enumSource ? `- identifier "${result.name}" from table "${tableName}" collides with enum type "${enumSource}"` : void 0;
|
|
915
|
+
}).filter((detail) => detail !== void 0);
|
|
916
|
+
if (collisions.length > 0) throw new Error(`PSL top-level name collisions detected:\n${collisions.join("\n")}`);
|
|
917
|
+
}
|
|
918
|
+
function createReservedNamedTypeNames(modelNames, enumNames) {
|
|
919
|
+
const reservedNames = new Set(PSL_SCALAR_TYPE_NAMES);
|
|
920
|
+
for (const result of modelNames.values()) reservedNames.add(result.name);
|
|
921
|
+
for (const result of enumNames.values()) reservedNames.add(result.name);
|
|
922
|
+
return reservedNames;
|
|
923
|
+
}
|
|
924
|
+
function seedNamedTypeRegistry(schemaIR, typeMap, enumNameMap, reservedNames) {
|
|
925
|
+
const seeds = /* @__PURE__ */ new Map();
|
|
926
|
+
for (const tableName of Object.keys(schemaIR.tables).sort()) {
|
|
927
|
+
const table = schemaIR.tables[tableName];
|
|
928
|
+
if (!table) continue;
|
|
929
|
+
for (const columnName of Object.keys(table.columns).sort()) {
|
|
930
|
+
const column = table.columns[columnName];
|
|
931
|
+
if (!column) continue;
|
|
932
|
+
const resolution = typeMap.resolve(column.nativeType, table.annotations);
|
|
933
|
+
if ("unsupported" in resolution || enumNameMap.has(column.nativeType) || !resolution.nativeTypeAttribute) continue;
|
|
934
|
+
const signatureKey = createNamedTypeSignatureKey(resolution);
|
|
935
|
+
if (!seeds.has(signatureKey)) seeds.set(signatureKey, {
|
|
936
|
+
baseType: resolution.pslType,
|
|
937
|
+
desiredName: toNamedTypeName(column.name),
|
|
938
|
+
nativeTypeAttribute: resolution.nativeTypeAttribute
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const registry = {
|
|
943
|
+
entriesByKey: /* @__PURE__ */ new Map(),
|
|
944
|
+
usedNames: new Set(reservedNames)
|
|
945
|
+
};
|
|
946
|
+
const sortedSeeds = [...seeds.entries()].sort((left, right) => {
|
|
947
|
+
const desiredNameComparison = left[1].desiredName.localeCompare(right[1].desiredName);
|
|
948
|
+
if (desiredNameComparison !== 0) return desiredNameComparison;
|
|
949
|
+
return left[0].localeCompare(right[0]);
|
|
950
|
+
});
|
|
951
|
+
for (const [signatureKey, seed] of sortedSeeds) {
|
|
952
|
+
const name = createUniqueFieldName(seed.desiredName, registry.usedNames);
|
|
953
|
+
registry.entriesByKey.set(signatureKey, {
|
|
954
|
+
name,
|
|
955
|
+
baseType: seed.baseType,
|
|
956
|
+
nativeTypeAttribute: seed.nativeTypeAttribute
|
|
957
|
+
});
|
|
958
|
+
registry.usedNames.add(name);
|
|
959
|
+
}
|
|
960
|
+
return registry;
|
|
961
|
+
}
|
|
962
|
+
function resolveNamedTypeName(registry, resolution) {
|
|
963
|
+
const key = createNamedTypeSignatureKey(resolution);
|
|
964
|
+
const existing = registry.entriesByKey.get(key);
|
|
965
|
+
if (existing) return existing.name;
|
|
966
|
+
throw new Error(`Named type registry was not seeded for native type "${resolution.nativeType}"`);
|
|
967
|
+
}
|
|
968
|
+
function createNamedTypeSignatureKey(resolution) {
|
|
969
|
+
return JSON.stringify({
|
|
970
|
+
baseType: resolution.pslType,
|
|
971
|
+
nativeTypeAttribute: resolution.nativeTypeAttribute ? {
|
|
972
|
+
name: resolution.nativeTypeAttribute.name,
|
|
973
|
+
args: resolution.nativeTypeAttribute.args ?? null
|
|
974
|
+
} : null
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
function topologicalSort(models, tables, modelNameMap) {
|
|
978
|
+
const modelByName = /* @__PURE__ */ new Map();
|
|
979
|
+
for (const model of models) modelByName.set(model.name, model);
|
|
980
|
+
const deps = /* @__PURE__ */ new Map();
|
|
981
|
+
const tableToModel = /* @__PURE__ */ new Map();
|
|
982
|
+
for (const tableName of Object.keys(tables)) {
|
|
983
|
+
const modelName = modelNameMap.get(tableName);
|
|
984
|
+
tableToModel.set(tableName, modelName);
|
|
985
|
+
deps.set(modelName, /* @__PURE__ */ new Set());
|
|
986
|
+
}
|
|
987
|
+
for (const [tableName, table] of Object.entries(tables)) {
|
|
988
|
+
const modelName = tableToModel.get(tableName);
|
|
989
|
+
for (const fk of table.foreignKeys) {
|
|
990
|
+
const refModelName = tableToModel.get(fk.referencedTable);
|
|
991
|
+
if (refModelName && refModelName !== modelName) deps.get(modelName).add(refModelName);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const result = [];
|
|
995
|
+
const visited = /* @__PURE__ */ new Set();
|
|
996
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
997
|
+
const sortedNames = [...deps.keys()].sort();
|
|
998
|
+
function visit(name) {
|
|
999
|
+
if (visited.has(name)) return;
|
|
1000
|
+
if (visiting.has(name)) return;
|
|
1001
|
+
visiting.add(name);
|
|
1002
|
+
const sortedDeps = [...deps.get(name)].sort();
|
|
1003
|
+
for (const dep of sortedDeps) visit(dep);
|
|
1004
|
+
visiting.delete(name);
|
|
1005
|
+
visited.add(name);
|
|
1006
|
+
result.push(modelByName.get(name));
|
|
1007
|
+
}
|
|
1008
|
+
for (const name of sortedNames) visit(name);
|
|
1009
|
+
return result;
|
|
1010
|
+
}
|
|
1011
|
+
//#endregion
|
|
13
1012
|
//#region src/core/control-instance.ts
|
|
14
1013
|
function extractCodecTypeIdsFromContract(contract) {
|
|
15
1014
|
const typeIds = /* @__PURE__ */ new Set();
|
|
@@ -49,7 +1048,7 @@ function createVerifyResult(options) {
|
|
|
49
1048
|
return result;
|
|
50
1049
|
}
|
|
51
1050
|
function isSqlControlAdapter(value) {
|
|
52
|
-
return typeof value === "object" && value !== null && "introspect" in value && typeof value.introspect === "function";
|
|
1051
|
+
return typeof value === "object" && value !== null && "introspect" in value && typeof value.introspect === "function" && "readMarker" in value && typeof value.readMarker === "function" && "readAllMarkers" in value && typeof value.readAllMarkers === "function";
|
|
53
1052
|
}
|
|
54
1053
|
function buildSqlTypeMetadataRegistry(options) {
|
|
55
1054
|
const { target, adapter, extensionPacks: extensions } = options;
|
|
@@ -77,16 +1076,30 @@ function createSqlFamilyInstance(stack) {
|
|
|
77
1076
|
const target = stack.target;
|
|
78
1077
|
const adapter = stack.adapter;
|
|
79
1078
|
const extensions = stack.extensionPacks;
|
|
80
|
-
const
|
|
1079
|
+
for (const extension of extensions) if (extension.contractSpace) {
|
|
1080
|
+
const { contractJson, headRef } = extension.contractSpace;
|
|
1081
|
+
assertDescriptorSelfConsistency({
|
|
1082
|
+
extensionId: extension.id,
|
|
1083
|
+
target: contractJson.target,
|
|
1084
|
+
targetFamily: contractJson.targetFamily,
|
|
1085
|
+
storage: contractJson.storage,
|
|
1086
|
+
headRefHash: headRef.hash
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
const { codecTypeImports, extensionIds } = stack;
|
|
81
1090
|
const typeMetadataRegistry = buildSqlTypeMetadataRegistry({
|
|
82
1091
|
target,
|
|
83
1092
|
adapter,
|
|
84
1093
|
extensionPacks: extensions
|
|
85
1094
|
});
|
|
1095
|
+
const getControlAdapter = () => {
|
|
1096
|
+
const controlAdapter = adapter.create(stack);
|
|
1097
|
+
if (!isSqlControlAdapter(controlAdapter)) throw new Error("Adapter does not implement SqlControlAdapter (missing introspect, readMarker, or readAllMarkers)");
|
|
1098
|
+
return controlAdapter;
|
|
1099
|
+
};
|
|
86
1100
|
return {
|
|
87
1101
|
familyId: "sql",
|
|
88
1102
|
codecTypeImports,
|
|
89
|
-
operationTypeImports,
|
|
90
1103
|
extensionIds,
|
|
91
1104
|
typeMetadataRegistry,
|
|
92
1105
|
validateContract(contractJson) {
|
|
@@ -99,7 +1112,7 @@ function createSqlFamilyInstance(stack) {
|
|
|
99
1112
|
const contractStorageHash = contract.storage.storageHash;
|
|
100
1113
|
const contractProfileHash = contract.profileHash;
|
|
101
1114
|
const contractTarget = contract.target;
|
|
102
|
-
const marker = await readMarker(driver);
|
|
1115
|
+
const marker = await getControlAdapter().readMarker(driver, APP_SPACE_ID);
|
|
103
1116
|
let missingCodecs;
|
|
104
1117
|
let codecCoverageSkipped = false;
|
|
105
1118
|
const supportedTypeIds = collectSupportedCodecTypeIds([
|
|
@@ -186,8 +1199,7 @@ function createSqlFamilyInstance(stack) {
|
|
|
186
1199
|
async schemaVerify(options) {
|
|
187
1200
|
const { driver, contract: contractInput, strict, context, frameworkComponents } = options;
|
|
188
1201
|
const contract = validateContract(contractInput, emptyCodecLookup);
|
|
189
|
-
const controlAdapter =
|
|
190
|
-
if (!isSqlControlAdapter(controlAdapter)) throw new Error("Adapter does not implement SqlControlAdapter.introspect()");
|
|
1202
|
+
const controlAdapter = getControlAdapter();
|
|
191
1203
|
return verifySqlSchema({
|
|
192
1204
|
contract,
|
|
193
1205
|
schema: await controlAdapter.introspect(driver, contractInput),
|
|
@@ -199,6 +1211,19 @@ function createSqlFamilyInstance(stack) {
|
|
|
199
1211
|
...ifDefined("normalizeNativeType", controlAdapter.normalizeNativeType)
|
|
200
1212
|
});
|
|
201
1213
|
},
|
|
1214
|
+
schemaVerifyAgainstSchema(options) {
|
|
1215
|
+
const contract = validateContract(options.contract, emptyCodecLookup);
|
|
1216
|
+
const controlAdapter = getControlAdapter();
|
|
1217
|
+
return verifySqlSchema({
|
|
1218
|
+
contract,
|
|
1219
|
+
schema: options.schema,
|
|
1220
|
+
strict: options.strict,
|
|
1221
|
+
typeMetadataRegistry,
|
|
1222
|
+
frameworkComponents: options.frameworkComponents,
|
|
1223
|
+
...ifDefined("normalizeDefault", controlAdapter.normalizeDefault),
|
|
1224
|
+
...ifDefined("normalizeNativeType", controlAdapter.normalizeNativeType)
|
|
1225
|
+
});
|
|
1226
|
+
},
|
|
202
1227
|
async sign(options) {
|
|
203
1228
|
const { driver, contract: contractInput, contractPath, configPath } = options;
|
|
204
1229
|
const startTime = Date.now();
|
|
@@ -208,12 +1233,13 @@ function createSqlFamilyInstance(stack) {
|
|
|
208
1233
|
const contractTarget = contract.target;
|
|
209
1234
|
await driver.query(ensureSchemaStatement.sql, ensureSchemaStatement.params);
|
|
210
1235
|
await driver.query(ensureTableStatement.sql, ensureTableStatement.params);
|
|
211
|
-
const existingMarker = await readMarker(driver);
|
|
1236
|
+
const existingMarker = await getControlAdapter().readMarker(driver, APP_SPACE_ID);
|
|
212
1237
|
let markerCreated = false;
|
|
213
1238
|
let markerUpdated = false;
|
|
214
1239
|
let previousHashes;
|
|
215
1240
|
if (!existingMarker) {
|
|
216
1241
|
const write = writeContractMarker({
|
|
1242
|
+
space: APP_SPACE_ID,
|
|
217
1243
|
storageHash: contractStorageHash,
|
|
218
1244
|
profileHash: contractProfileHash,
|
|
219
1245
|
contractJson: contractInput,
|
|
@@ -230,6 +1256,7 @@ function createSqlFamilyInstance(stack) {
|
|
|
230
1256
|
profileHash: existingProfileHash
|
|
231
1257
|
};
|
|
232
1258
|
const write = writeContractMarker({
|
|
1259
|
+
space: APP_SPACE_ID,
|
|
233
1260
|
storageHash: contractStorageHash,
|
|
234
1261
|
profileHash: contractProfileHash,
|
|
235
1262
|
contractJson: contractInput,
|
|
@@ -268,13 +1295,19 @@ function createSqlFamilyInstance(stack) {
|
|
|
268
1295
|
};
|
|
269
1296
|
},
|
|
270
1297
|
async readMarker(options) {
|
|
271
|
-
return readMarker(options.driver);
|
|
1298
|
+
return getControlAdapter().readMarker(options.driver, options.space);
|
|
1299
|
+
},
|
|
1300
|
+
async readAllMarkers(options) {
|
|
1301
|
+
return getControlAdapter().readAllMarkers(options.driver);
|
|
272
1302
|
},
|
|
273
1303
|
async introspect(options) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return
|
|
1304
|
+
return getControlAdapter().introspect(options.driver, options.contract);
|
|
1305
|
+
},
|
|
1306
|
+
inferPslContract(schemaIR) {
|
|
1307
|
+
return sqlSchemaIrToPslAst(schemaIR);
|
|
1308
|
+
},
|
|
1309
|
+
toOperationPreview(operations) {
|
|
1310
|
+
return sqlOperationsToPreview(operations);
|
|
278
1311
|
},
|
|
279
1312
|
toSchemaView(schema) {
|
|
280
1313
|
const tableNodes = Object.entries(schema.tables).map(([tableName, table]) => {
|
|
@@ -356,25 +1389,15 @@ function createSqlFamilyInstance(stack) {
|
|
|
356
1389
|
...children.length > 0 ? { children } : {}
|
|
357
1390
|
});
|
|
358
1391
|
});
|
|
359
|
-
const dependencyNodes = schema.dependencies.map((dep) => {
|
|
360
|
-
const shortName = dep.id.split(".").pop() ?? dep.id;
|
|
361
|
-
return new SchemaTreeNode({
|
|
362
|
-
kind: "dependency",
|
|
363
|
-
id: `dependency-${dep.id}`,
|
|
364
|
-
label: `${shortName} dependency is installed`
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
const rootChildren = [...tableNodes, ...dependencyNodes];
|
|
368
1392
|
return { root: new SchemaTreeNode({
|
|
369
1393
|
kind: "root",
|
|
370
1394
|
id: "sql-schema",
|
|
371
1395
|
label: "database",
|
|
372
|
-
...
|
|
1396
|
+
...tableNodes.length > 0 ? { children: tableNodes } : {}
|
|
373
1397
|
}) };
|
|
374
1398
|
}
|
|
375
1399
|
};
|
|
376
1400
|
}
|
|
377
|
-
|
|
378
1401
|
//#endregion
|
|
379
1402
|
//#region src/core/control-descriptor.ts
|
|
380
1403
|
var SqlFamilyDescriptor = class {
|
|
@@ -391,7 +1414,6 @@ var SqlFamilyDescriptor = class {
|
|
|
391
1414
|
return createSqlFamilyInstance(stack);
|
|
392
1415
|
}
|
|
393
1416
|
};
|
|
394
|
-
|
|
395
1417
|
//#endregion
|
|
396
1418
|
//#region src/core/migrations/contract-to-schema-ir.ts
|
|
397
1419
|
function convertColumn(name, column, storageTypes, expandNativeType, renderDefault) {
|
|
@@ -501,10 +1523,9 @@ function detectDestructiveChanges(from, to) {
|
|
|
501
1523
|
/**
|
|
502
1524
|
* Converts a `Contract` to `SqlSchemaIR`.
|
|
503
1525
|
*
|
|
504
|
-
* Reads `contract.storage` for tables
|
|
505
|
-
* annotations
|
|
506
|
-
*
|
|
507
|
-
* Storage-type annotations are written under `options.annotationNamespace`.
|
|
1526
|
+
* Reads `contract.storage` for tables and `contract.storage.types` for type
|
|
1527
|
+
* annotations. Storage-type annotations are written under
|
|
1528
|
+
* `options.annotationNamespace`.
|
|
508
1529
|
*
|
|
509
1530
|
* Drops codec metadata (`codecId`, `typeRef`) since the schema IR only represents
|
|
510
1531
|
* structural information. When `expandNativeType` is provided, parameterized types
|
|
@@ -515,38 +1536,135 @@ function detectDestructiveChanges(from, to) {
|
|
|
515
1536
|
*/
|
|
516
1537
|
function contractToSchemaIR(contract, options) {
|
|
517
1538
|
if (options.annotationNamespace.length === 0) throw new Error("annotationNamespace must be a non-empty string");
|
|
518
|
-
if (!contract) return {
|
|
519
|
-
tables: {},
|
|
520
|
-
dependencies: []
|
|
521
|
-
};
|
|
1539
|
+
if (!contract) return { tables: {} };
|
|
522
1540
|
const storage = contract.storage;
|
|
523
1541
|
const storageTypes = storage.types ?? {};
|
|
524
1542
|
const tables = {};
|
|
525
1543
|
for (const [tableName, tableDef] of Object.entries(storage.tables)) tables[tableName] = convertTable(tableName, tableDef, storageTypes, options.expandNativeType, options.renderDefault);
|
|
526
1544
|
return {
|
|
527
1545
|
tables,
|
|
528
|
-
dependencies: deduplicateDependencyIRs(collectInitDependencies(options.frameworkComponents ?? [])),
|
|
529
1546
|
...ifDefined("annotations", deriveAnnotations(storage, options.annotationNamespace))
|
|
530
1547
|
};
|
|
531
1548
|
}
|
|
532
|
-
function deduplicateDependencyIRs(deps) {
|
|
533
|
-
const seen = /* @__PURE__ */ new Set();
|
|
534
|
-
const result = [];
|
|
535
|
-
for (const dep of deps) {
|
|
536
|
-
if (dep.id.trim().length === 0) throw new Error("Dependency id must be a non-empty string");
|
|
537
|
-
if (seen.has(dep.id)) continue;
|
|
538
|
-
seen.add(dep.id);
|
|
539
|
-
result.push({ id: dep.id });
|
|
540
|
-
}
|
|
541
|
-
return result;
|
|
542
|
-
}
|
|
543
1549
|
function deriveAnnotations(storage, annotationNamespace) {
|
|
544
1550
|
if (!storage.types || Object.keys(storage.types).length === 0) return void 0;
|
|
545
1551
|
const byNativeType = {};
|
|
546
1552
|
for (const typeInstance of Object.values(storage.types)) byNativeType[typeInstance.nativeType] = typeInstance;
|
|
547
1553
|
return { [annotationNamespace]: { storageTypes: byNativeType } };
|
|
548
1554
|
}
|
|
549
|
-
|
|
1555
|
+
//#endregion
|
|
1556
|
+
//#region src/core/migrations/field-event-planner.ts
|
|
1557
|
+
function planFieldEventOperations(options) {
|
|
1558
|
+
const priorTables = options.priorContract?.storage.tables ?? {};
|
|
1559
|
+
const newTables = options.newContract.storage.tables;
|
|
1560
|
+
const added = [];
|
|
1561
|
+
const dropped = [];
|
|
1562
|
+
const altered = [];
|
|
1563
|
+
const tableNames = unionSorted(Object.keys(priorTables), Object.keys(newTables));
|
|
1564
|
+
for (const tableName of tableNames) {
|
|
1565
|
+
const priorTable = priorTables[tableName];
|
|
1566
|
+
const newTable = newTables[tableName];
|
|
1567
|
+
const fieldNames = unionSorted(priorTable ? Object.keys(priorTable.columns) : [], newTable ? Object.keys(newTable.columns) : []);
|
|
1568
|
+
for (const fieldName of fieldNames) {
|
|
1569
|
+
const priorField = priorTable?.columns[fieldName];
|
|
1570
|
+
const newField = newTable?.columns[fieldName];
|
|
1571
|
+
const entry = {
|
|
1572
|
+
tableName,
|
|
1573
|
+
fieldName,
|
|
1574
|
+
priorTable,
|
|
1575
|
+
newTable,
|
|
1576
|
+
priorField,
|
|
1577
|
+
newField
|
|
1578
|
+
};
|
|
1579
|
+
if (priorField === void 0 && newField !== void 0) added.push(entry);
|
|
1580
|
+
else if (priorField !== void 0 && newField === void 0) dropped.push(entry);
|
|
1581
|
+
else if (priorField !== void 0 && newField !== void 0) {
|
|
1582
|
+
if (isAlteration(priorField, newField)) altered.push(entry);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
const calls = [];
|
|
1587
|
+
appendCalls("added", added, options.codecHooks, calls, (e) => e.newField?.codecId);
|
|
1588
|
+
appendCalls("dropped", dropped, options.codecHooks, calls, (e) => e.priorField?.codecId);
|
|
1589
|
+
appendCalls("altered", altered, options.codecHooks, calls, (e) => e.newField?.codecId);
|
|
1590
|
+
return calls;
|
|
1591
|
+
}
|
|
1592
|
+
function appendCalls(event, entries, codecHooks, calls, pickCodecId) {
|
|
1593
|
+
for (const entry of entries) {
|
|
1594
|
+
const codecId = pickCodecId(entry);
|
|
1595
|
+
if (codecId === void 0) continue;
|
|
1596
|
+
const hook = codecHooks.get(codecId);
|
|
1597
|
+
if (!hook?.onFieldEvent) continue;
|
|
1598
|
+
const ctx = buildContext(event, entry);
|
|
1599
|
+
const emitted = hook.onFieldEvent(event, ctx);
|
|
1600
|
+
for (const call of emitted) calls.push(call);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* The context's prior/new sides are scoped to the event:
|
|
1605
|
+
*
|
|
1606
|
+
* - `'added'` — only `newTable` / `newField` populated.
|
|
1607
|
+
* - `'dropped'` — only `priorTable` / `priorField` populated.
|
|
1608
|
+
* - `'altered'` — both sides populated.
|
|
1609
|
+
*/
|
|
1610
|
+
function buildContext(event, entry) {
|
|
1611
|
+
const base = {
|
|
1612
|
+
tableName: entry.tableName,
|
|
1613
|
+
fieldName: entry.fieldName
|
|
1614
|
+
};
|
|
1615
|
+
if (event === "added") return {
|
|
1616
|
+
...base,
|
|
1617
|
+
...entry.newTable !== void 0 ? { newTable: entry.newTable } : {},
|
|
1618
|
+
...entry.newField !== void 0 ? { newField: entry.newField } : {}
|
|
1619
|
+
};
|
|
1620
|
+
if (event === "dropped") return {
|
|
1621
|
+
...base,
|
|
1622
|
+
...entry.priorTable !== void 0 ? { priorTable: entry.priorTable } : {},
|
|
1623
|
+
...entry.priorField !== void 0 ? { priorField: entry.priorField } : {}
|
|
1624
|
+
};
|
|
1625
|
+
return {
|
|
1626
|
+
...base,
|
|
1627
|
+
...entry.priorTable !== void 0 ? { priorTable: entry.priorTable } : {},
|
|
1628
|
+
...entry.newTable !== void 0 ? { newTable: entry.newTable } : {},
|
|
1629
|
+
...entry.priorField !== void 0 ? { priorField: entry.priorField } : {},
|
|
1630
|
+
...entry.newField !== void 0 ? { newField: entry.newField } : {}
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* `'altered'` predicate. Returns `false` whenever `codecId` differs —
|
|
1635
|
+
* any codec change suppresses the `altered` event entirely, including
|
|
1636
|
+
* cases where another property also differs in the same diff. Codec
|
|
1637
|
+
* rotation is a v1 non-goal; avoiding the mixed event keeps the
|
|
1638
|
+
* migration semantics for codec changes explicit rather than smuggling
|
|
1639
|
+
* them through as `altered`.
|
|
1640
|
+
*
|
|
1641
|
+
* For non-`codecId` diffs, returns `true` iff any other column property
|
|
1642
|
+
* differs.
|
|
1643
|
+
*/
|
|
1644
|
+
function isAlteration(prior, current) {
|
|
1645
|
+
if (prior.codecId !== current.codecId) return false;
|
|
1646
|
+
return !sameStorageColumn(prior, current);
|
|
1647
|
+
}
|
|
1648
|
+
function sameStorageColumn(a, b) {
|
|
1649
|
+
if (a === b) return true;
|
|
1650
|
+
if (a.nativeType !== b.nativeType) return false;
|
|
1651
|
+
if (a.nullable !== b.nullable) return false;
|
|
1652
|
+
if (a.typeRef !== b.typeRef) return false;
|
|
1653
|
+
if (!sameJson(a.typeParams, b.typeParams)) return false;
|
|
1654
|
+
if (!sameJson(a.default, b.default)) return false;
|
|
1655
|
+
return true;
|
|
1656
|
+
}
|
|
1657
|
+
function sameJson(a, b) {
|
|
1658
|
+
if (a === b) return true;
|
|
1659
|
+
if (a === void 0 || b === void 0) return false;
|
|
1660
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1661
|
+
}
|
|
1662
|
+
function unionSorted(a, b) {
|
|
1663
|
+
const set = /* @__PURE__ */ new Set();
|
|
1664
|
+
for (const name of a) set.add(name);
|
|
1665
|
+
for (const name of b) set.add(name);
|
|
1666
|
+
return [...set].sort((x, y) => x < y ? -1 : x > y ? 1 : 0);
|
|
1667
|
+
}
|
|
550
1668
|
//#endregion
|
|
551
1669
|
//#region src/core/migrations/plan-helpers.ts
|
|
552
1670
|
const readOnlyEmptyObject = Object.freeze({});
|
|
@@ -559,6 +1677,7 @@ function freezeSteps(steps) {
|
|
|
559
1677
|
return Object.freeze(steps.map((step) => Object.freeze({
|
|
560
1678
|
description: step.description,
|
|
561
1679
|
sql: step.sql,
|
|
1680
|
+
...step.params ? { params: Object.freeze([...step.params]) } : {},
|
|
562
1681
|
...step.meta ? { meta: cloneRecord(step.meta) } : {}
|
|
563
1682
|
})));
|
|
564
1683
|
}
|
|
@@ -580,6 +1699,7 @@ function freezeOperation(operation) {
|
|
|
580
1699
|
label: operation.label,
|
|
581
1700
|
...operation.summary ? { summary: operation.summary } : {},
|
|
582
1701
|
operationClass: operation.operationClass,
|
|
1702
|
+
...operation.invariantId ? { invariantId: operation.invariantId } : {},
|
|
583
1703
|
target: freezeTargetDetails(operation.target),
|
|
584
1704
|
precheck: freezeSteps(operation.precheck),
|
|
585
1705
|
execute: freezeSteps(operation.execute),
|
|
@@ -594,9 +1714,11 @@ function freezeOperations(operations) {
|
|
|
594
1714
|
function createMigrationPlan(options) {
|
|
595
1715
|
return Object.freeze({
|
|
596
1716
|
targetId: options.targetId,
|
|
1717
|
+
spaceId: options.spaceId,
|
|
597
1718
|
...options.origin !== void 0 ? { origin: options.origin ? Object.freeze({ ...options.origin }) : null } : {},
|
|
598
1719
|
destination: Object.freeze({ ...options.destination }),
|
|
599
1720
|
operations: freezeOperations(options.operations),
|
|
1721
|
+
providedInvariants: Object.freeze([...options.providedInvariants]),
|
|
600
1722
|
...options.meta ? { meta: cloneRecord(options.meta) } : {}
|
|
601
1723
|
});
|
|
602
1724
|
}
|
|
@@ -638,18 +1760,16 @@ function runnerFailure(code, summary, options) {
|
|
|
638
1760
|
...options?.meta ? { meta: cloneRecord(options.meta) } : {}
|
|
639
1761
|
}));
|
|
640
1762
|
}
|
|
641
|
-
|
|
642
1763
|
//#endregion
|
|
643
1764
|
//#region src/core/migrations/policies.ts
|
|
644
1765
|
/**
|
|
645
1766
|
* Policy used by `db init`: additive-only operations, no widening/destructive steps.
|
|
646
1767
|
*/
|
|
647
1768
|
const INIT_ADDITIVE_POLICY = Object.freeze({ allowedOperationClasses: Object.freeze(["additive"]) });
|
|
648
|
-
|
|
649
1769
|
//#endregion
|
|
650
1770
|
//#region src/exports/control.ts
|
|
651
1771
|
var control_default = new SqlFamilyDescriptor();
|
|
652
|
-
|
|
653
1772
|
//#endregion
|
|
654
|
-
export { INIT_ADDITIVE_POLICY, assembleAuthoringContributions,
|
|
1773
|
+
export { INIT_ADDITIVE_POLICY, assembleAuthoringContributions, contractToSchemaIR, createMigrationPlan, control_default as default, detectDestructiveChanges, extractCodecControlHooks, planFieldEventOperations, plannerFailure, plannerSuccess, runnerFailure, runnerSuccess, temporalAuthoringPresets, timestampNowControlDescriptor };
|
|
1774
|
+
|
|
655
1775
|
//# sourceMappingURL=control.mjs.map
|