qubu 0.0.0 → 0.3.3
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/dist/canonical-BbnqavJm.mjs +972 -0
- package/dist/codegen.d.mts +107 -0
- package/dist/codegen.mjs +1199 -0
- package/dist/column-CXMxx8Hq.mjs +118 -0
- package/dist/complete-D5Djh-zo.mjs +1788 -0
- package/dist/complete-types-BdFqUfbb.d.mts +371 -0
- package/dist/core.d.mts +33 -0
- package/dist/core.mjs +36 -0
- package/dist/ddl.d.mts +118 -0
- package/dist/ddl.mjs +1120 -0
- package/dist/dialect-b2-Z6uBF.mjs +13 -0
- package/dist/diff.d.mts +2 -0
- package/dist/diff.mjs +883 -0
- package/dist/drizzle-mysql.d.mts +24 -0
- package/dist/drizzle-mysql.mjs +72 -0
- package/dist/drizzle-postgres.d.mts +24 -0
- package/dist/drizzle-postgres.mjs +73 -0
- package/dist/drizzle-sqlite.d.mts +24 -0
- package/dist/drizzle-sqlite.mjs +82 -0
- package/dist/drizzle.d.mts +13 -0
- package/dist/drizzle.mjs +2 -0
- package/dist/errors-BGCoLe_r.mjs +14 -0
- package/dist/errors-Dxv73YJu.mjs +26 -0
- package/dist/explain-CkIK13L_.mjs +98 -0
- package/dist/index-Ds7-mhJi.d.mts +146 -0
- package/dist/index-Dug5HnLB.d.mts +204 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1276 -0
- package/dist/introspection.d.mts +117 -0
- package/dist/introspection.mjs +4341 -0
- package/dist/json-CUZlv4HT.mjs +169 -0
- package/dist/migration.d.mts +215 -0
- package/dist/migration.mjs +1161 -0
- package/dist/mysql-DqkqXB6A.mjs +355 -0
- package/dist/mysql.d.mts +5 -0
- package/dist/mysql.mjs +50 -0
- package/dist/naming-QVCOnSj2.mjs +20 -0
- package/dist/omit-OxV58AwX.mjs +5 -0
- package/dist/on-conflict-BxnxubMb.mjs +143 -0
- package/dist/postgres-DEBBeh52.mjs +235 -0
- package/dist/postgres.d.mts +123 -0
- package/dist/postgres.mjs +52 -0
- package/dist/registry-BufIskVN.mjs +569 -0
- package/dist/relational-DCZrrNia.mjs +77 -0
- package/dist/runtime-BTr-MTlo.mjs +193 -0
- package/dist/schema.d.mts +2 -0
- package/dist/schema.mjs +5 -0
- package/dist/serialize-PF1cfH2P.mjs +556 -0
- package/dist/snapshot-CWPgzxNx.mjs +392 -0
- package/dist/snapshot.d.mts +4 -0
- package/dist/snapshot.mjs +8 -0
- package/dist/source-DUoJVXmL.mjs +68 -0
- package/dist/sqlite-BU6DBxef.mjs +320 -0
- package/dist/sqlite.d.mts +5 -0
- package/dist/sqlite.mjs +48 -0
- package/dist/standard-BTVYKh_F.mjs +12 -0
- package/dist/table-llv9tsZ8.mjs +384 -0
- package/dist/types-4Q076HKo.d.mts +3636 -0
- package/dist/types-BX0mckiU.d.mts +644 -0
- package/dist/types-CO1KaRNc.d.mts +46 -0
- package/dist/types-Cec0xzo4.mjs +116 -0
- package/dist/value-BvilP0oz.mjs +29 -0
- package/dist/vite/ambient.d.ts +449 -0
- package/dist/vite.d.mts +35 -0
- package/dist/vite.mjs +498 -0
- package/docs/config.json +21 -0
- package/docs/dialects-and-execution.md +427 -0
- package/docs/getting-started.md +96 -0
- package/docs/guides/compose-queries.md +225 -0
- package/docs/guides/drizzle.md +129 -0
- package/docs/guides/extensions/dialects.md +40 -0
- package/docs/guides/extensions/overview.md +36 -0
- package/docs/guides/extensions/sources-and-clauses.md +82 -0
- package/docs/guides/extensions/typed-expressions.md +94 -0
- package/docs/guides/extensions/unsafe-syntax.md +27 -0
- package/docs/guides/json.md +65 -0
- package/docs/guides/mutations.md +138 -0
- package/docs/guides/select/conditions.md +110 -0
- package/docs/guides/select/grouping-and-windows.md +82 -0
- package/docs/guides/select/ordering-and-pagination.md +78 -0
- package/docs/guides/select/overview.md +139 -0
- package/docs/guides/sql-templates.md +157 -0
- package/docs/guides/vite-plugin.md +92 -0
- package/docs/index.md +111 -0
- package/docs/query-model/fragments.md +109 -0
- package/docs/query-model/result-shapes.md +125 -0
- package/docs/query-model/source-scope.md +182 -0
- package/docs/reference/introspection-support.md +196 -0
- package/docs/reference/mysql-snapshot.md +55 -0
- package/docs/reference/postgres-snapshot.md +61 -0
- package/docs/reference/sqlite-snapshot.md +53 -0
- package/docs/reference/supported-surface.md +104 -0
- package/docs/schema/catalog-model.md +58 -0
- package/docs/schema/code-generation.md +139 -0
- package/docs/schema/columns-and-writes.md +130 -0
- package/docs/schema/constraints-and-indexes.md +180 -0
- package/docs/schema/ddl-emission.md +75 -0
- package/docs/schema/diff.md +75 -0
- package/docs/schema/introspection.md +214 -0
- package/docs/schema/migration-plans.md +76 -0
- package/docs/schema/snapshots.md +93 -0
- package/docs/schema/storage-and-schema-sql.md +104 -0
- package/docs/schema/tables-and-names.md +106 -0
- package/docs/sql-semantic-types.md +141 -0
- package/docs/troubleshooting.md +151 -0
- package/package.json +132 -10
- package/skills/qubu/SKILL.md +42 -0
- package/skills/qubu/agents/openai.yaml +6 -0
- package/readme.md +0 -1
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
//#region src/snapshot/types.ts
|
|
2
|
+
/** The JSON object tag used by Qubu schema snapshots. */
|
|
3
|
+
const schemaSnapshotFormat = "qubu-schema";
|
|
4
|
+
/** The first canonical schema snapshot format version. */
|
|
5
|
+
const schemaSnapshotVersion = 1;
|
|
6
|
+
/** The first version of the dialect-extension envelope. */
|
|
7
|
+
const schemaSnapshotDialectVersion = 1;
|
|
8
|
+
/** The first version of the built-in naming-policy description. */
|
|
9
|
+
const schemaSnapshotNamingPolicyVersion = 1;
|
|
10
|
+
/** The neutral dialect used when no SQL engine adapter is selected. */
|
|
11
|
+
const neutralSnapshotDialect = Object.freeze({
|
|
12
|
+
name: "neutral",
|
|
13
|
+
version: 1
|
|
14
|
+
});
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/snapshot/decode.ts
|
|
17
|
+
/** Error raised by throwing snapshot APIs after collecting diagnostics. */
|
|
18
|
+
var SnapshotValidationError = class extends TypeError {
|
|
19
|
+
name;
|
|
20
|
+
diagnostics;
|
|
21
|
+
issues;
|
|
22
|
+
constructor(diagnostics) {
|
|
23
|
+
const frozen = Object.freeze(diagnostics.map((diagnostic) => Object.freeze({
|
|
24
|
+
...diagnostic,
|
|
25
|
+
path: Object.freeze([...diagnostic.path]),
|
|
26
|
+
relatedPaths: diagnostic.relatedPaths ? Object.freeze(diagnostic.relatedPaths.map((path) => Object.freeze([...path]))) : void 0
|
|
27
|
+
})));
|
|
28
|
+
super(frozen.map((diagnostic) => diagnostic.message).join("\n"));
|
|
29
|
+
this.name = "SnapshotValidationError";
|
|
30
|
+
this.diagnostics = frozen;
|
|
31
|
+
this.issues = frozen;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** Decode and strictly validate a JSON string or parsed snapshot value. */
|
|
35
|
+
function decodeSchemaSnapshot(input) {
|
|
36
|
+
let value = input;
|
|
37
|
+
const diagnostics = [];
|
|
38
|
+
if (typeof input === "string") try {
|
|
39
|
+
value = JSON.parse(input);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
diagnostics: [diagnostic("invalid-snapshot", `Snapshot JSON could not be parsed: ${error instanceof Error ? error.message : String(error)}`, [])]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (!isRecord(value)) return {
|
|
47
|
+
ok: false,
|
|
48
|
+
diagnostics: [diagnostic("invalid-snapshot", "Snapshot root must be an object", [])]
|
|
49
|
+
};
|
|
50
|
+
const snapshot = validateSnapshot(value, diagnostics);
|
|
51
|
+
if (diagnostics.length > 0 || snapshot === void 0) return {
|
|
52
|
+
ok: false,
|
|
53
|
+
diagnostics: Object.freeze(diagnostics)
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
value: freezeSnapshot(snapshot)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Validate a snapshot and throw one structured error if it is malformed. */
|
|
61
|
+
function assertSchemaSnapshot(input) {
|
|
62
|
+
const result = decodeSchemaSnapshot(input);
|
|
63
|
+
if (!result.ok) throw new SnapshotValidationError(result.diagnostics);
|
|
64
|
+
return result.value;
|
|
65
|
+
}
|
|
66
|
+
/** Return a fixed-order, deeply immutable copy of a valid snapshot. */
|
|
67
|
+
function canonicalizeSchemaSnapshot(input) {
|
|
68
|
+
return assertSchemaSnapshot(input);
|
|
69
|
+
}
|
|
70
|
+
function validateSnapshot(value, diagnostics) {
|
|
71
|
+
requireKeys(value, [
|
|
72
|
+
"format",
|
|
73
|
+
"version",
|
|
74
|
+
"dialect",
|
|
75
|
+
"namingPolicy",
|
|
76
|
+
"namespace",
|
|
77
|
+
"tables"
|
|
78
|
+
], [], diagnostics, ["namespace"]);
|
|
79
|
+
if (value.format !== "qubu-schema") diagnostics.push(diagnostic("invalid-snapshot", `Snapshot format must be "${schemaSnapshotFormat}"`, ["format"]));
|
|
80
|
+
if (value.version !== 1) diagnostics.push(diagnostic(typeof value.version === "number" && value.version > 1 ? "future-version" : "invalid-snapshot", `Unsupported schema snapshot version: ${String(value.version)}`, ["version"]));
|
|
81
|
+
const dialect = validateDialect(value.dialect, ["dialect"], diagnostics);
|
|
82
|
+
const namingPolicy = validateNamingPolicy(value.namingPolicy, ["namingPolicy"], diagnostics);
|
|
83
|
+
if (value.namespace !== void 0) {
|
|
84
|
+
if (typeof value.namespace !== "string" || value.namespace.length === 0 || value.namespace !== value.namespace.trim() || /[.\\/\u0000-\u001f\u007f"']/u.test(value.namespace)) diagnostics.push(diagnostic("invalid-snapshot", "Snapshot namespace must be a non-empty identifier", ["namespace"]));
|
|
85
|
+
}
|
|
86
|
+
const tablesValue = value.tables;
|
|
87
|
+
const tables = [];
|
|
88
|
+
if (!Array.isArray(tablesValue)) diagnostics.push(diagnostic("invalid-snapshot", "Snapshot tables must be an array", ["tables"]));
|
|
89
|
+
else {
|
|
90
|
+
validateSortedIds(tablesValue, ["tables"], diagnostics);
|
|
91
|
+
for (const [index, table] of tablesValue.entries()) {
|
|
92
|
+
const validated = validateTable(table, ["tables", index], dialect?.name, diagnostics);
|
|
93
|
+
if (validated !== void 0) tables.push(validated);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (dialect === void 0 || namingPolicy === void 0) return void 0;
|
|
97
|
+
validateCrossReferences(tables, diagnostics);
|
|
98
|
+
return {
|
|
99
|
+
format: schemaSnapshotFormat,
|
|
100
|
+
version: 1,
|
|
101
|
+
dialect,
|
|
102
|
+
namingPolicy,
|
|
103
|
+
...value.namespace !== void 0 ? { namespace: value.namespace } : {},
|
|
104
|
+
tables
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function validateDialect(value, path, diagnostics) {
|
|
108
|
+
if (!isRecord(value)) {
|
|
109
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Dialect must be an object", path));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
requireKeys(value, ["name", "version"], path, diagnostics);
|
|
113
|
+
if (typeof value.name !== "string" || value.name.length === 0) diagnostics.push(diagnostic("invalid-snapshot", "Dialect name must be non-empty", [...path, "name"]));
|
|
114
|
+
if (typeof value.version !== "number" || !Number.isSafeInteger(value.version) || value.version < 1) diagnostics.push(diagnostic("invalid-snapshot", "Dialect version must be a positive integer", [...path, "version"]));
|
|
115
|
+
else if (value.version > 1) diagnostics.push(diagnostic("future-version", `Unsupported dialect extension version: ${value.version}`, [...path, "version"]));
|
|
116
|
+
if (typeof value.name !== "string" || typeof value.version !== "number") return void 0;
|
|
117
|
+
return {
|
|
118
|
+
name: value.name,
|
|
119
|
+
version: value.version
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function validateNamingPolicy(value, path, diagnostics) {
|
|
123
|
+
if (!isRecord(value)) {
|
|
124
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Naming policy must be an object", path));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
requireKeys(value, ["name", "version"], path, diagnostics);
|
|
128
|
+
if (typeof value.name !== "string" || value.name.length === 0) diagnostics.push(diagnostic("invalid-snapshot", "Naming-policy name must be non-empty", [...path, "name"]));
|
|
129
|
+
if (value.version !== 1) diagnostics.push(diagnostic(typeof value.version === "number" && value.version > 1 ? "future-version" : "invalid-snapshot", `Unsupported naming-policy version: ${String(value.version)}`, [...path, "version"]));
|
|
130
|
+
if (typeof value.name !== "string" || typeof value.version !== "number") return void 0;
|
|
131
|
+
return {
|
|
132
|
+
name: value.name,
|
|
133
|
+
version: value.version
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function validateTable(value, path, dialect, diagnostics) {
|
|
137
|
+
if (!isRecord(value)) {
|
|
138
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Table must be an object", path));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
requireKeys(value, [
|
|
142
|
+
"id",
|
|
143
|
+
"physicalName",
|
|
144
|
+
"columns",
|
|
145
|
+
"constraints",
|
|
146
|
+
"indexes"
|
|
147
|
+
], path, diagnostics);
|
|
148
|
+
const id = validateId(value.id, [...path, "id"], diagnostics);
|
|
149
|
+
const physicalName = validateName(value.physicalName, [...path, "physicalName"], diagnostics);
|
|
150
|
+
const columns = validateArray(value.columns, [...path, "columns"], diagnostics, (item, itemPath) => validateColumn(item, itemPath, dialect, diagnostics));
|
|
151
|
+
const constraints = validateArray(value.constraints, [...path, "constraints"], diagnostics, (item, itemPath) => validateConstraint(item, itemPath, dialect, diagnostics));
|
|
152
|
+
const indexes = validateArray(value.indexes, [...path, "indexes"], diagnostics, (item, itemPath) => validateIndex(item, itemPath, dialect, diagnostics));
|
|
153
|
+
validateSortedIds(value.columns, [...path, "columns"], diagnostics);
|
|
154
|
+
validateSortedIds(value.constraints, [...path, "constraints"], diagnostics);
|
|
155
|
+
validateSortedIds(value.indexes, [...path, "indexes"], diagnostics);
|
|
156
|
+
if (id === void 0 || physicalName === void 0 || columns === void 0 || constraints === void 0 || indexes === void 0) return void 0;
|
|
157
|
+
return {
|
|
158
|
+
id,
|
|
159
|
+
physicalName,
|
|
160
|
+
columns,
|
|
161
|
+
constraints,
|
|
162
|
+
indexes
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function validateColumn(value, path, dialect, diagnostics) {
|
|
166
|
+
if (!isRecord(value)) {
|
|
167
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Column must be an object", path));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
requireKeys(value, [
|
|
171
|
+
"id",
|
|
172
|
+
"physicalName",
|
|
173
|
+
"nullable",
|
|
174
|
+
"hasDefault",
|
|
175
|
+
"generated",
|
|
176
|
+
"storage",
|
|
177
|
+
"default",
|
|
178
|
+
"generatedColumn",
|
|
179
|
+
"identity",
|
|
180
|
+
"onUpdate"
|
|
181
|
+
], path, diagnostics, [
|
|
182
|
+
"storage",
|
|
183
|
+
"default",
|
|
184
|
+
"generatedColumn",
|
|
185
|
+
"identity",
|
|
186
|
+
"onUpdate"
|
|
187
|
+
]);
|
|
188
|
+
const id = validateId(value.id, [...path, "id"], diagnostics);
|
|
189
|
+
const physicalName = validateName(value.physicalName, [...path, "physicalName"], diagnostics);
|
|
190
|
+
const nullable = validateBoolean(value.nullable, [...path, "nullable"], diagnostics);
|
|
191
|
+
const hasDefault = validateBoolean(value.hasDefault, [...path, "hasDefault"], diagnostics);
|
|
192
|
+
const generated = validateBoolean(value.generated, [...path, "generated"], diagnostics);
|
|
193
|
+
const storage = value.storage === void 0 ? void 0 : validateStorage(value.storage, [...path, "storage"], dialect, diagnostics);
|
|
194
|
+
const defaultValue = value.default === void 0 ? void 0 : validateDefault(value.default, [...path, "default"], dialect, diagnostics);
|
|
195
|
+
const generatedColumn = value.generatedColumn === void 0 ? void 0 : validateGenerated(value.generatedColumn, [...path, "generatedColumn"], dialect, diagnostics);
|
|
196
|
+
const identity = value.identity === void 0 ? void 0 : validateIdentity(value.identity, [...path, "identity"], dialect, diagnostics);
|
|
197
|
+
const onUpdate = value.onUpdate === void 0 ? void 0 : validateExpression(value.onUpdate, [...path, "onUpdate"], "default", dialect, diagnostics);
|
|
198
|
+
if (onUpdate !== void 0 && dialect !== "mysql") diagnostics.push(diagnostic("dialect-mismatch", "Column onUpdate metadata is only valid for MySQL snapshots", [...path, "onUpdate"]));
|
|
199
|
+
if (id === void 0 || physicalName === void 0 || nullable === void 0 || hasDefault === void 0 || generated === void 0) return void 0;
|
|
200
|
+
return {
|
|
201
|
+
id,
|
|
202
|
+
physicalName,
|
|
203
|
+
nullable,
|
|
204
|
+
hasDefault,
|
|
205
|
+
generated,
|
|
206
|
+
...storage === void 0 ? {} : { storage },
|
|
207
|
+
...defaultValue === void 0 ? {} : { default: defaultValue },
|
|
208
|
+
...generatedColumn === void 0 ? {} : { generatedColumn },
|
|
209
|
+
...identity === void 0 ? {} : { identity },
|
|
210
|
+
...onUpdate === void 0 ? {} : { onUpdate }
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function validateStorage(value, path, snapshotDialect, diagnostics) {
|
|
214
|
+
if (!isRecord(value)) {
|
|
215
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Storage must be an object", path));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (value.kind === "portable") {
|
|
219
|
+
requireKeys(value, ["kind", "type"], path, diagnostics);
|
|
220
|
+
const type = validateName(value.type, [...path, "type"], diagnostics);
|
|
221
|
+
return type === void 0 ? void 0 : {
|
|
222
|
+
kind: "portable",
|
|
223
|
+
type
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (value.kind === "native") {
|
|
227
|
+
requireKeys(value, [
|
|
228
|
+
"kind",
|
|
229
|
+
"dialect",
|
|
230
|
+
"type",
|
|
231
|
+
"affinity"
|
|
232
|
+
], path, diagnostics, ["affinity"]);
|
|
233
|
+
const storageDialect = validateName(value.dialect, [...path, "dialect"], diagnostics);
|
|
234
|
+
const type = validateName(value.type, [...path, "type"], diagnostics);
|
|
235
|
+
if (snapshotDialect !== void 0 && storageDialect !== void 0 && storageDialect !== snapshotDialect) diagnostics.push(diagnostic("dialect-mismatch", `Native storage belongs to "${storageDialect}" but snapshot dialect is "${snapshotDialect}"`, [...path, "dialect"]));
|
|
236
|
+
const affinityValue = value.affinity;
|
|
237
|
+
const affinity = affinityValue === void 0 || affinityValue === "blob" || affinityValue === "integer" || affinityValue === "numeric" || affinityValue === "real" || affinityValue === "text" ? affinityValue : void 0;
|
|
238
|
+
if (affinityValue !== void 0 && affinity === void 0) diagnostics.push(diagnostic("invalid-snapshot", "Storage affinity is invalid", [...path, "affinity"]));
|
|
239
|
+
if (affinity !== void 0 && snapshotDialect !== "sqlite") diagnostics.push(diagnostic("dialect-mismatch", "Storage affinity is only valid for SQLite snapshots", [...path, "affinity"]));
|
|
240
|
+
return storageDialect === void 0 || type === void 0 ? void 0 : {
|
|
241
|
+
kind: "native",
|
|
242
|
+
dialect: storageDialect,
|
|
243
|
+
type,
|
|
244
|
+
...affinity === void 0 ? {} : { affinity }
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Storage kind must be portable or native", [...path, "kind"]));
|
|
248
|
+
}
|
|
249
|
+
function validateDefault(value, path, dialect, diagnostics) {
|
|
250
|
+
if (!isRecord(value)) {
|
|
251
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Default must be an object", path));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (value.kind === "external") {
|
|
255
|
+
requireKeys(value, ["kind"], path, diagnostics);
|
|
256
|
+
return { kind: "external" };
|
|
257
|
+
}
|
|
258
|
+
if (value.kind === "literal") {
|
|
259
|
+
requireKeys(value, ["kind", "value"], path, diagnostics);
|
|
260
|
+
const literal = validateLiteral(value.value, [...path, "value"], diagnostics);
|
|
261
|
+
return literal === void 0 ? void 0 : {
|
|
262
|
+
kind: "literal",
|
|
263
|
+
value: literal
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (value.kind === "expression") {
|
|
267
|
+
requireKeys(value, ["kind", "expression"], path, diagnostics);
|
|
268
|
+
const expression = validateExpression(value.expression, [...path, "expression"], "default", dialect, diagnostics);
|
|
269
|
+
return expression === void 0 ? void 0 : {
|
|
270
|
+
kind: "expression",
|
|
271
|
+
expression
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Default kind is invalid", [...path, "kind"]));
|
|
275
|
+
}
|
|
276
|
+
function validateGenerated(value, path, dialect, diagnostics) {
|
|
277
|
+
if (!isRecord(value)) {
|
|
278
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Generated-column metadata must be an object", path));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (value.kind === "external") {
|
|
282
|
+
requireKeys(value, ["kind"], path, diagnostics);
|
|
283
|
+
return { kind: "external" };
|
|
284
|
+
}
|
|
285
|
+
if (value.kind === "expression") {
|
|
286
|
+
requireKeys(value, [
|
|
287
|
+
"kind",
|
|
288
|
+
"expression",
|
|
289
|
+
"mode"
|
|
290
|
+
], path, diagnostics);
|
|
291
|
+
const expression = validateExpression(value.expression, [...path, "expression"], "generated", dialect, diagnostics);
|
|
292
|
+
const mode = value.mode === "stored" || value.mode === "virtual" ? value.mode : void 0;
|
|
293
|
+
if (mode === void 0) diagnostics.push(diagnostic("invalid-snapshot", "Generated-column mode must be stored or virtual", [...path, "mode"]));
|
|
294
|
+
return expression === void 0 || mode === void 0 ? void 0 : {
|
|
295
|
+
kind: "expression",
|
|
296
|
+
expression,
|
|
297
|
+
mode
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Generated-column kind is invalid", [...path, "kind"]));
|
|
301
|
+
}
|
|
302
|
+
function validateIdentity(value, path, dialect, diagnostics) {
|
|
303
|
+
if (!isRecord(value)) {
|
|
304
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Identity must be an object", path));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
requireKeys(value, [
|
|
308
|
+
"kind",
|
|
309
|
+
"generation",
|
|
310
|
+
"dialect"
|
|
311
|
+
], path, diagnostics, ["dialect"]);
|
|
312
|
+
if (value.kind !== "identity") diagnostics.push(diagnostic("invalid-snapshot", "Identity kind must be identity", [...path, "kind"]));
|
|
313
|
+
if (value.generation !== "always" && value.generation !== "by-default") diagnostics.push(diagnostic("invalid-snapshot", "Identity generation is invalid", [...path, "generation"]));
|
|
314
|
+
if (value.kind !== "identity" || value.generation !== "always" && value.generation !== "by-default") return void 0;
|
|
315
|
+
const extension = validateExtension(value.dialect, [...path, "dialect"], dialect, diagnostics);
|
|
316
|
+
return {
|
|
317
|
+
kind: "identity",
|
|
318
|
+
generation: value.generation,
|
|
319
|
+
...extension === void 0 ? {} : { dialect: extension }
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function validateConstraint(value, path, dialect, diagnostics) {
|
|
323
|
+
if (!isRecord(value)) {
|
|
324
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Constraint must be an object", path));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const common = validateConstraintCommon(value, path, diagnostics);
|
|
328
|
+
if (common === void 0) return void 0;
|
|
329
|
+
if (value.kind === "primary-key" || value.kind === "unique") {
|
|
330
|
+
requireKeys(value, [
|
|
331
|
+
"id",
|
|
332
|
+
"kind",
|
|
333
|
+
"physicalName",
|
|
334
|
+
"columns",
|
|
335
|
+
"deferrable",
|
|
336
|
+
"initially",
|
|
337
|
+
"dialect"
|
|
338
|
+
], path, diagnostics, [
|
|
339
|
+
"deferrable",
|
|
340
|
+
"initially",
|
|
341
|
+
"dialect"
|
|
342
|
+
]);
|
|
343
|
+
const columns = validateColumnIds(value.columns, [...path, "columns"], diagnostics);
|
|
344
|
+
const extra = validateConstraintTiming(value, path, diagnostics);
|
|
345
|
+
const extension = validateExtension(value.dialect, [...path, "dialect"], dialect, diagnostics);
|
|
346
|
+
if (columns === void 0 || extra === void 0) return void 0;
|
|
347
|
+
return {
|
|
348
|
+
...common,
|
|
349
|
+
kind: value.kind,
|
|
350
|
+
columns,
|
|
351
|
+
...extra,
|
|
352
|
+
...extension === void 0 ? {} : { dialect: extension }
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (value.kind === "unique-constraint") {
|
|
356
|
+
requireKeys(value, [
|
|
357
|
+
"id",
|
|
358
|
+
"kind",
|
|
359
|
+
"physicalName",
|
|
360
|
+
"columns",
|
|
361
|
+
"nulls",
|
|
362
|
+
"deferrable",
|
|
363
|
+
"initially",
|
|
364
|
+
"dialect"
|
|
365
|
+
], path, diagnostics, [
|
|
366
|
+
"deferrable",
|
|
367
|
+
"initially",
|
|
368
|
+
"dialect"
|
|
369
|
+
]);
|
|
370
|
+
const columns = validateColumnIds(value.columns, [...path, "columns"], diagnostics);
|
|
371
|
+
const extra = validateConstraintTiming(value, path, diagnostics);
|
|
372
|
+
const extension = validateExtension(value.dialect, [...path, "dialect"], dialect, diagnostics);
|
|
373
|
+
if (value.nulls !== "distinct" && value.nulls !== "not-distinct") diagnostics.push(diagnostic("invalid-snapshot", "Unique-constraint null semantics are invalid", [...path, "nulls"]));
|
|
374
|
+
if (columns === void 0 || extra === void 0 || value.nulls !== "distinct" && value.nulls !== "not-distinct") return void 0;
|
|
375
|
+
return {
|
|
376
|
+
...common,
|
|
377
|
+
kind: "unique-constraint",
|
|
378
|
+
columns,
|
|
379
|
+
nulls: value.nulls,
|
|
380
|
+
...extra,
|
|
381
|
+
...extension === void 0 ? {} : { dialect: extension }
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (value.kind === "foreign-key") {
|
|
385
|
+
requireKeys(value, [
|
|
386
|
+
"id",
|
|
387
|
+
"kind",
|
|
388
|
+
"physicalName",
|
|
389
|
+
"columns",
|
|
390
|
+
"target",
|
|
391
|
+
"onUpdate",
|
|
392
|
+
"onDelete",
|
|
393
|
+
"match",
|
|
394
|
+
"deferrable",
|
|
395
|
+
"initially",
|
|
396
|
+
"dialect"
|
|
397
|
+
], path, diagnostics, [
|
|
398
|
+
"onUpdate",
|
|
399
|
+
"onDelete",
|
|
400
|
+
"match",
|
|
401
|
+
"deferrable",
|
|
402
|
+
"initially",
|
|
403
|
+
"dialect"
|
|
404
|
+
]);
|
|
405
|
+
const columns = validateColumnIds(value.columns, [...path, "columns"], diagnostics);
|
|
406
|
+
const target = validateForeignKeyTarget(value.target, [...path, "target"], diagnostics);
|
|
407
|
+
const extra = validateForeignKeyOptions(value, path, diagnostics);
|
|
408
|
+
const extension = validateExtension(value.dialect, [...path, "dialect"], dialect, diagnostics);
|
|
409
|
+
if (columns === void 0 || target === void 0 || extra === void 0) return void 0;
|
|
410
|
+
return {
|
|
411
|
+
...common,
|
|
412
|
+
kind: "foreign-key",
|
|
413
|
+
columns,
|
|
414
|
+
target,
|
|
415
|
+
...extra,
|
|
416
|
+
...extension === void 0 ? {} : { dialect: extension }
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (value.kind === "check") {
|
|
420
|
+
requireKeys(value, [
|
|
421
|
+
"id",
|
|
422
|
+
"kind",
|
|
423
|
+
"physicalName",
|
|
424
|
+
"expression",
|
|
425
|
+
"deferrable",
|
|
426
|
+
"initially",
|
|
427
|
+
"dialect"
|
|
428
|
+
], path, diagnostics, [
|
|
429
|
+
"deferrable",
|
|
430
|
+
"initially",
|
|
431
|
+
"dialect"
|
|
432
|
+
]);
|
|
433
|
+
const expression = validateExpression(value.expression, [...path, "expression"], "check", dialect, diagnostics);
|
|
434
|
+
const extra = validateConstraintTiming(value, path, diagnostics);
|
|
435
|
+
const extension = validateExtension(value.dialect, [...path, "dialect"], dialect, diagnostics);
|
|
436
|
+
if (expression === void 0 || extra === void 0) return void 0;
|
|
437
|
+
return {
|
|
438
|
+
...common,
|
|
439
|
+
kind: "check",
|
|
440
|
+
expression,
|
|
441
|
+
...extra,
|
|
442
|
+
...extension === void 0 ? {} : { dialect: extension }
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
diagnostics.push(diagnostic("invalid-snapshot", `Unknown constraint kind: ${String(value.kind)}`, [...path, "kind"]));
|
|
446
|
+
}
|
|
447
|
+
function validateConstraintCommon(value, path, diagnostics) {
|
|
448
|
+
const id = validateId(value.id, [...path, "id"], diagnostics);
|
|
449
|
+
const physicalName = validateName(value.physicalName, [...path, "physicalName"], diagnostics);
|
|
450
|
+
if (id === void 0 || physicalName === void 0) return void 0;
|
|
451
|
+
return {
|
|
452
|
+
id,
|
|
453
|
+
kind: value.kind,
|
|
454
|
+
physicalName
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function validateConstraintTiming(value, path, diagnostics) {
|
|
458
|
+
const deferrable = validateOptionalBoolean(value.deferrable, [...path, "deferrable"], diagnostics);
|
|
459
|
+
const initially = value.initially === void 0 ? void 0 : value.initially === "immediate" || value.initially === "deferred" ? value.initially : void 0;
|
|
460
|
+
if (value.initially !== void 0 && initially === void 0) diagnostics.push(diagnostic("invalid-snapshot", "Constraint timing is invalid", [...path, "initially"]));
|
|
461
|
+
if (deferrable === void 0 && value.deferrable !== void 0) return void 0;
|
|
462
|
+
if (value.initially !== void 0 && initially === void 0) return void 0;
|
|
463
|
+
return {
|
|
464
|
+
...deferrable === void 0 ? {} : { deferrable },
|
|
465
|
+
...initially === void 0 ? {} : { initially }
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function validateForeignKeyOptions(value, path, diagnostics) {
|
|
469
|
+
const action = (key) => {
|
|
470
|
+
const candidate = value[key];
|
|
471
|
+
if (candidate === void 0) return void 0;
|
|
472
|
+
if (candidate === "no-action" || candidate === "restrict" || candidate === "cascade" || candidate === "set-null" || candidate === "set-default") return candidate;
|
|
473
|
+
diagnostics.push(diagnostic("invalid-snapshot", `Foreign-key ${key} action is invalid`, [...path, key]));
|
|
474
|
+
};
|
|
475
|
+
const onUpdate = action("onUpdate");
|
|
476
|
+
const onDelete = action("onDelete");
|
|
477
|
+
const match = value.match === void 0 ? void 0 : value.match === "simple" || value.match === "full" || value.match === "partial" ? value.match : void 0;
|
|
478
|
+
if (value.match !== void 0 && match === void 0) diagnostics.push(diagnostic("invalid-snapshot", "Foreign-key match mode is invalid", [...path, "match"]));
|
|
479
|
+
const timing = validateConstraintTiming(value, path, diagnostics);
|
|
480
|
+
if (timing === void 0 || value.onUpdate !== void 0 && onUpdate === void 0 || value.onDelete !== void 0 && onDelete === void 0 || value.match !== void 0 && match === void 0) return void 0;
|
|
481
|
+
return {
|
|
482
|
+
...onUpdate === void 0 ? {} : { onUpdate },
|
|
483
|
+
...onDelete === void 0 ? {} : { onDelete },
|
|
484
|
+
...match === void 0 ? {} : { match },
|
|
485
|
+
...timing
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function validateForeignKeyTarget(value, path, diagnostics) {
|
|
489
|
+
if (!isRecord(value)) {
|
|
490
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Foreign-key target must be an object", path));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
requireKeys(value, ["table", "columns"], path, diagnostics);
|
|
494
|
+
const table = validateId(value.table, [...path, "table"], diagnostics);
|
|
495
|
+
const columns = validateColumnIds(value.columns, [...path, "columns"], diagnostics);
|
|
496
|
+
return table === void 0 || columns === void 0 ? void 0 : {
|
|
497
|
+
table,
|
|
498
|
+
columns
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function validateIndex(value, path, dialect, diagnostics) {
|
|
502
|
+
if (!isRecord(value)) {
|
|
503
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Index must be an object", path));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
requireKeys(value, [
|
|
507
|
+
"id",
|
|
508
|
+
"kind",
|
|
509
|
+
"physicalName",
|
|
510
|
+
"terms",
|
|
511
|
+
"unique",
|
|
512
|
+
"candidateKey",
|
|
513
|
+
"predicate",
|
|
514
|
+
"includedColumns",
|
|
515
|
+
"dialect"
|
|
516
|
+
], path, diagnostics, [
|
|
517
|
+
"predicate",
|
|
518
|
+
"includedColumns",
|
|
519
|
+
"dialect"
|
|
520
|
+
]);
|
|
521
|
+
const id = validateId(value.id, [...path, "id"], diagnostics);
|
|
522
|
+
const physicalName = validateName(value.physicalName, [...path, "physicalName"], diagnostics);
|
|
523
|
+
if (value.kind !== "index") diagnostics.push(diagnostic("invalid-snapshot", "Index kind must be index", [...path, "kind"]));
|
|
524
|
+
const unique = validateBoolean(value.unique, [...path, "unique"], diagnostics);
|
|
525
|
+
const candidateKey = validateBoolean(value.candidateKey, [...path, "candidateKey"], diagnostics);
|
|
526
|
+
const terms = validateArray(value.terms, [...path, "terms"], diagnostics, (item, itemPath) => validateIndexTerm(item, itemPath, dialect, diagnostics));
|
|
527
|
+
const predicate = value.predicate === void 0 ? void 0 : validateExpression(value.predicate, [...path, "predicate"], "index", dialect, diagnostics);
|
|
528
|
+
const includedColumns = value.includedColumns === void 0 ? void 0 : validateColumnIds(value.includedColumns, [...path, "includedColumns"], diagnostics);
|
|
529
|
+
const extension = validateExtension(value.dialect, [...path, "dialect"], dialect, diagnostics);
|
|
530
|
+
if (id === void 0 || physicalName === void 0 || unique === void 0 || candidateKey === void 0 || terms === void 0 || value.kind !== "index" || terms.length === 0) return void 0;
|
|
531
|
+
return {
|
|
532
|
+
id,
|
|
533
|
+
kind: "index",
|
|
534
|
+
physicalName,
|
|
535
|
+
terms,
|
|
536
|
+
unique,
|
|
537
|
+
candidateKey,
|
|
538
|
+
...predicate === void 0 ? {} : { predicate },
|
|
539
|
+
...includedColumns === void 0 ? {} : { includedColumns },
|
|
540
|
+
...extension === void 0 ? {} : { dialect: extension }
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function validateIndexTerm(value, path, dialect, diagnostics) {
|
|
544
|
+
if (!isRecord(value)) {
|
|
545
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Index term must be an object", path));
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (value.kind === "column") {
|
|
549
|
+
requireKeys(value, ["kind", "column"], path, diagnostics);
|
|
550
|
+
const column = validateId(value.column, [...path, "column"], diagnostics);
|
|
551
|
+
return column === void 0 ? void 0 : {
|
|
552
|
+
kind: "column",
|
|
553
|
+
column
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
if (value.kind === "expression") {
|
|
557
|
+
requireKeys(value, ["kind", "expression"], path, diagnostics);
|
|
558
|
+
const expression = validateExpression(value.expression, [...path, "expression"], "index", dialect, diagnostics);
|
|
559
|
+
return expression === void 0 ? void 0 : {
|
|
560
|
+
kind: "expression",
|
|
561
|
+
expression
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
if (value.kind === "order") {
|
|
565
|
+
requireKeys(value, [
|
|
566
|
+
"kind",
|
|
567
|
+
"expression",
|
|
568
|
+
"direction",
|
|
569
|
+
"nulls"
|
|
570
|
+
], path, diagnostics, ["direction", "nulls"]);
|
|
571
|
+
const expression = validateIndexTermExpression(value.expression, [...path, "expression"], dialect, diagnostics);
|
|
572
|
+
const direction = value.direction === void 0 ? void 0 : value.direction === "ASC" || value.direction === "DESC" ? value.direction : void 0;
|
|
573
|
+
const nulls = value.nulls === void 0 ? void 0 : value.nulls === "FIRST" || value.nulls === "LAST" ? value.nulls : void 0;
|
|
574
|
+
if (value.direction !== void 0 && direction === void 0) diagnostics.push(diagnostic("invalid-snapshot", "Index order direction is invalid", [...path, "direction"]));
|
|
575
|
+
if (value.nulls !== void 0 && nulls === void 0) diagnostics.push(diagnostic("invalid-snapshot", "Index NULL ordering is invalid", [...path, "nulls"]));
|
|
576
|
+
if (expression === void 0 || value.direction !== void 0 && direction === void 0 || value.nulls !== void 0 && nulls === void 0) return void 0;
|
|
577
|
+
return {
|
|
578
|
+
kind: "order",
|
|
579
|
+
expression,
|
|
580
|
+
...direction === void 0 ? {} : { direction },
|
|
581
|
+
...nulls === void 0 ? {} : { nulls }
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Index term kind is invalid", [...path, "kind"]));
|
|
585
|
+
}
|
|
586
|
+
function validateIndexTermExpression(value, path, dialect, diagnostics) {
|
|
587
|
+
if (!isRecord(value)) {
|
|
588
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Index expression must be an object", path));
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (value.kind === "column") {
|
|
592
|
+
requireKeys(value, ["kind", "column"], path, diagnostics);
|
|
593
|
+
const column = validateId(value.column, [...path, "column"], diagnostics);
|
|
594
|
+
return column === void 0 ? void 0 : {
|
|
595
|
+
kind: "column",
|
|
596
|
+
column
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
if (value.kind === "expression") {
|
|
600
|
+
requireKeys(value, ["kind", "expression"], path, diagnostics);
|
|
601
|
+
const expression = validateExpression(value.expression, [...path, "expression"], "index", dialect, diagnostics);
|
|
602
|
+
return expression === void 0 ? void 0 : {
|
|
603
|
+
kind: "expression",
|
|
604
|
+
expression
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Index expression kind is invalid", [...path, "kind"]));
|
|
608
|
+
}
|
|
609
|
+
function validateExpression(value, path, _mode, dialect, diagnostics) {
|
|
610
|
+
if (!isRecord(value)) {
|
|
611
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Expression must be an object", path));
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
requireKeys(value, [
|
|
615
|
+
"kind",
|
|
616
|
+
"expressionKind",
|
|
617
|
+
"sql",
|
|
618
|
+
"dialect"
|
|
619
|
+
], path, diagnostics, ["dialect"]);
|
|
620
|
+
const expressionKind = validateName(value.expressionKind, [...path, "expressionKind"], diagnostics);
|
|
621
|
+
const sql = typeof value.sql === "string" ? value.sql : void 0;
|
|
622
|
+
if (sql === void 0) diagnostics.push(diagnostic("invalid-snapshot", "Expression SQL must be a string", [...path, "sql"]));
|
|
623
|
+
if (value.kind !== "expression") diagnostics.push(diagnostic("invalid-snapshot", "Expression kind must be expression", [...path, "kind"]));
|
|
624
|
+
const expressionDialect = value.dialect === void 0 ? void 0 : validateName(value.dialect, [...path, "dialect"], diagnostics);
|
|
625
|
+
if (expressionDialect !== void 0 && dialect !== void 0 && expressionDialect !== dialect) diagnostics.push(diagnostic("dialect-mismatch", `Expression dialect "${expressionDialect}" does not match snapshot dialect "${dialect}"`, [...path, "dialect"]));
|
|
626
|
+
if (expressionDialect !== void 0 && value.expressionKind !== "unsafe") diagnostics.push(diagnostic("invalid-snapshot", "Only unsafe expressions may carry a dialect tag", [...path, "dialect"]));
|
|
627
|
+
if (expressionDialect === void 0 && value.expressionKind === "unsafe") diagnostics.push(diagnostic("invalid-snapshot", "Unsafe expressions must carry a dialect tag", [...path, "dialect"]));
|
|
628
|
+
if (sql !== void 0 && sql !== sql.replace(/\r\n?/g, "\n")) diagnostics.push(diagnostic("non-canonical", "Expression SQL must use LF line endings", [...path, "sql"]));
|
|
629
|
+
if (expressionKind === void 0 || sql === void 0 || value.kind !== "expression") return void 0;
|
|
630
|
+
return {
|
|
631
|
+
kind: "expression",
|
|
632
|
+
expressionKind,
|
|
633
|
+
sql,
|
|
634
|
+
...expressionDialect === void 0 ? {} : { dialect: expressionDialect }
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function validateLiteral(value, path, diagnostics) {
|
|
638
|
+
if (!isRecord(value)) {
|
|
639
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Literal must be an object", path));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (value.kind === "null") {
|
|
643
|
+
requireKeys(value, ["kind"], path, diagnostics);
|
|
644
|
+
return { kind: "null" };
|
|
645
|
+
}
|
|
646
|
+
if (value.kind === "boolean") {
|
|
647
|
+
requireKeys(value, ["kind", "value"], path, diagnostics);
|
|
648
|
+
if (typeof value.value !== "boolean") diagnostics.push(diagnostic("invalid-snapshot", "Boolean literal value must be boolean", [...path, "value"]));
|
|
649
|
+
return typeof value.value === "boolean" ? {
|
|
650
|
+
kind: "boolean",
|
|
651
|
+
value: value.value
|
|
652
|
+
} : void 0;
|
|
653
|
+
}
|
|
654
|
+
if (value.kind === "string") {
|
|
655
|
+
requireKeys(value, ["kind", "value"], path, diagnostics);
|
|
656
|
+
if (typeof value.value !== "string") diagnostics.push(diagnostic("invalid-snapshot", "String literal value must be string", [...path, "value"]));
|
|
657
|
+
return typeof value.value === "string" ? {
|
|
658
|
+
kind: "string",
|
|
659
|
+
value: value.value
|
|
660
|
+
} : void 0;
|
|
661
|
+
}
|
|
662
|
+
if (value.kind === "number" || value.kind === "bigint") {
|
|
663
|
+
requireKeys(value, ["kind", "value"], path, diagnostics);
|
|
664
|
+
if (typeof value.value !== "string" || !isCanonicalIntegerOrNumber(value.value, value.kind === "bigint")) diagnostics.push(diagnostic("invalid-snapshot", `${value.kind} literal value is not canonical`, [...path, "value"]));
|
|
665
|
+
return typeof value.value === "string" && isCanonicalIntegerOrNumber(value.value, value.kind === "bigint") ? {
|
|
666
|
+
kind: value.kind,
|
|
667
|
+
value: value.value
|
|
668
|
+
} : void 0;
|
|
669
|
+
}
|
|
670
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Literal kind is invalid", [...path, "kind"]));
|
|
671
|
+
}
|
|
672
|
+
function validateExtension(value, path, dialect, diagnostics) {
|
|
673
|
+
if (value === void 0) return void 0;
|
|
674
|
+
if (!isRecord(value)) {
|
|
675
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Dialect extension must be an object", path));
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
requireKeys(value, [
|
|
679
|
+
"dialect",
|
|
680
|
+
"version",
|
|
681
|
+
"data"
|
|
682
|
+
], path, diagnostics);
|
|
683
|
+
const extensionDialect = validateName(value.dialect, [...path, "dialect"], diagnostics);
|
|
684
|
+
const version = value.version;
|
|
685
|
+
if (typeof version !== "number" || !Number.isSafeInteger(version) || version < 1) diagnostics.push(diagnostic("invalid-snapshot", "Dialect extension version must be a positive integer", [...path, "version"]));
|
|
686
|
+
else if (version > 1) diagnostics.push(diagnostic("future-version", `Unsupported dialect extension version: ${version}`, [...path, "version"]));
|
|
687
|
+
if (extensionDialect !== void 0 && dialect !== void 0 && extensionDialect !== dialect) diagnostics.push(diagnostic("dialect-mismatch", `Dialect extension belongs to "${extensionDialect}" but snapshot dialect is "${dialect}"`, [...path, "dialect"]));
|
|
688
|
+
const data = validateJsonValue(value.data, [...path, "data"], diagnostics);
|
|
689
|
+
if (extensionDialect === void 0 || typeof version !== "number" || data === void 0) return void 0;
|
|
690
|
+
return {
|
|
691
|
+
dialect: extensionDialect,
|
|
692
|
+
version,
|
|
693
|
+
data
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function validateJsonValue(value, path, diagnostics, seen = /* @__PURE__ */ new WeakSet()) {
|
|
697
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
698
|
+
if (typeof value === "number") {
|
|
699
|
+
if (!Number.isFinite(value)) diagnostics.push(diagnostic("invalid-value", "Non-finite JSON numbers must use a tagged value", path));
|
|
700
|
+
return Number.isFinite(value) ? value : void 0;
|
|
701
|
+
}
|
|
702
|
+
if (typeof value !== "object") {
|
|
703
|
+
diagnostics.push(diagnostic("invalid-value", "Dialect data contains an unsupported value", path));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (seen.has(value)) {
|
|
707
|
+
diagnostics.push(diagnostic("invalid-value", "Dialect data cannot be cyclic", path));
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
seen.add(value);
|
|
711
|
+
try {
|
|
712
|
+
if (Array.isArray(value)) return value.map((item, index) => validateJsonValue(item, [...path, index], diagnostics, seen));
|
|
713
|
+
if (!isRecord(value)) {
|
|
714
|
+
diagnostics.push(diagnostic("invalid-value", "Dialect data must use plain objects", path));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (Object.keys(value).length === 1 && "$bigint" in value) {
|
|
718
|
+
if (typeof value.$bigint !== "string" || /^-(0|[1-9]\d*)$/u.test(value.$bigint) === false && /^(0|[1-9]\d*)$/u.test(value.$bigint) === false) diagnostics.push(diagnostic("invalid-value", "Tagged bigint is not canonical", [...path, "$bigint"]));
|
|
719
|
+
return typeof value.$bigint === "string" ? { $bigint: value.$bigint } : void 0;
|
|
720
|
+
}
|
|
721
|
+
if (Object.keys(value).length === 1 && "$number" in value) {
|
|
722
|
+
if (value.$number !== "NaN" && value.$number !== "Infinity" && value.$number !== "-Infinity") diagnostics.push(diagnostic("invalid-value", "Tagged number is invalid", [...path, "$number"]));
|
|
723
|
+
return value.$number === "NaN" || value.$number === "Infinity" || value.$number === "-Infinity" ? { $number: value.$number } : void 0;
|
|
724
|
+
}
|
|
725
|
+
const result = {};
|
|
726
|
+
for (const key of Object.keys(value).sort()) {
|
|
727
|
+
const nested = validateJsonValue(value[key], [...path, key], diagnostics, seen);
|
|
728
|
+
if (nested !== void 0) result[key] = nested;
|
|
729
|
+
}
|
|
730
|
+
return result;
|
|
731
|
+
} finally {
|
|
732
|
+
seen.delete(value);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
function validateCrossReferences(tables, diagnostics) {
|
|
736
|
+
const tableMap = new Map(tables.map((table) => [table.id, table]));
|
|
737
|
+
for (const [tableIndex, table] of tables.entries()) {
|
|
738
|
+
const columnSet = new Set(table.columns.map((column) => column.id));
|
|
739
|
+
for (const [constraintIndex, constraint] of table.constraints.entries()) {
|
|
740
|
+
if (constraint.kind !== "check") {
|
|
741
|
+
for (const [columnIndex, column] of constraint.columns.entries()) if (!columnSet.has(column)) diagnostics.push(diagnostic("invalid-cross-reference", `Constraint column "${column}" is not declared on table "${table.id}"`, [
|
|
742
|
+
"tables",
|
|
743
|
+
tableIndex,
|
|
744
|
+
"constraints",
|
|
745
|
+
constraintIndex,
|
|
746
|
+
"columns",
|
|
747
|
+
columnIndex
|
|
748
|
+
]));
|
|
749
|
+
}
|
|
750
|
+
if (constraint.kind === "foreign-key") {
|
|
751
|
+
const target = tableMap.get(constraint.target.table);
|
|
752
|
+
if (target === void 0) diagnostics.push(diagnostic("invalid-cross-reference", `Foreign-key target table "${constraint.target.table}" does not exist`, [
|
|
753
|
+
"tables",
|
|
754
|
+
tableIndex,
|
|
755
|
+
"constraints",
|
|
756
|
+
constraintIndex,
|
|
757
|
+
"target",
|
|
758
|
+
"table"
|
|
759
|
+
]));
|
|
760
|
+
else for (const [columnIndex, column] of constraint.target.columns.entries()) if (!target.columns.some((targetColumn) => targetColumn.id === column)) diagnostics.push(diagnostic("invalid-cross-reference", `Foreign-key target column "${column}" is not declared on table "${target.id}"`, [
|
|
761
|
+
"tables",
|
|
762
|
+
tableIndex,
|
|
763
|
+
"constraints",
|
|
764
|
+
constraintIndex,
|
|
765
|
+
"target",
|
|
766
|
+
"columns",
|
|
767
|
+
columnIndex
|
|
768
|
+
]));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
for (const [indexIndex, index] of table.indexes.entries()) {
|
|
772
|
+
for (const [termIndex, term] of index.terms.entries()) {
|
|
773
|
+
const expression = term.kind === "order" ? term.expression : term;
|
|
774
|
+
if (expression.kind === "column" && !columnSet.has(expression.column)) diagnostics.push(diagnostic("invalid-cross-reference", `Index column "${expression.column}" is not declared on table "${table.id}"`, [
|
|
775
|
+
"tables",
|
|
776
|
+
tableIndex,
|
|
777
|
+
"indexes",
|
|
778
|
+
indexIndex,
|
|
779
|
+
"terms",
|
|
780
|
+
termIndex
|
|
781
|
+
]));
|
|
782
|
+
}
|
|
783
|
+
for (const [columnIndex, column] of (index.includedColumns ?? []).entries()) if (!columnSet.has(column)) diagnostics.push(diagnostic("invalid-cross-reference", `Included index column "${column}" is not declared on table "${table.id}"`, [
|
|
784
|
+
"tables",
|
|
785
|
+
tableIndex,
|
|
786
|
+
"indexes",
|
|
787
|
+
indexIndex,
|
|
788
|
+
"includedColumns",
|
|
789
|
+
columnIndex
|
|
790
|
+
]));
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function validateColumnIds(value, path, diagnostics) {
|
|
795
|
+
if (!Array.isArray(value)) {
|
|
796
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Column references must be an array", path));
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const result = [];
|
|
800
|
+
for (const [index, item] of value.entries()) {
|
|
801
|
+
const id = validateId(item, [...path, index], diagnostics);
|
|
802
|
+
if (id !== void 0) result.push(id);
|
|
803
|
+
}
|
|
804
|
+
if (result.length === 0) diagnostics.push(diagnostic("invalid-snapshot", "Column references cannot be empty", path));
|
|
805
|
+
return result;
|
|
806
|
+
}
|
|
807
|
+
function validateArray(value, path, diagnostics, validate) {
|
|
808
|
+
if (!Array.isArray(value)) {
|
|
809
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Value must be an array", path));
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const result = [];
|
|
813
|
+
for (const [index, item] of value.entries()) {
|
|
814
|
+
const validated = validate(item, [...path, index]);
|
|
815
|
+
if (validated !== void 0) result.push(validated);
|
|
816
|
+
}
|
|
817
|
+
return result;
|
|
818
|
+
}
|
|
819
|
+
function validateSortedIds(value, path, diagnostics) {
|
|
820
|
+
if (!Array.isArray(value)) return;
|
|
821
|
+
let previous;
|
|
822
|
+
for (const [index, item] of value.entries()) {
|
|
823
|
+
if (!isRecord(item) || typeof item.id !== "string") continue;
|
|
824
|
+
if (previous !== void 0 && item.id <= previous) diagnostics.push(diagnostic("non-canonical", "Entities must be sorted by stable logical ID", [
|
|
825
|
+
...path,
|
|
826
|
+
index,
|
|
827
|
+
"id"
|
|
828
|
+
]));
|
|
829
|
+
previous = item.id;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function validateId(value, path, diagnostics) {
|
|
833
|
+
if (typeof value !== "string" || value.length === 0 || value !== value.trim() || /[.\\/\u0000-\u001f\u007f]/u.test(value)) {
|
|
834
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Logical IDs must be non-empty identifiers", path));
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
return value;
|
|
838
|
+
}
|
|
839
|
+
function validateName(value, path, diagnostics) {
|
|
840
|
+
if (typeof value !== "string" || value.length === 0 || value !== value.trim() || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
841
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Physical names must be non-empty strings without control characters", path));
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
return value;
|
|
845
|
+
}
|
|
846
|
+
function validateBoolean(value, path, diagnostics) {
|
|
847
|
+
if (typeof value !== "boolean") {
|
|
848
|
+
diagnostics.push(diagnostic("invalid-snapshot", "Value must be boolean", path));
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
return value;
|
|
852
|
+
}
|
|
853
|
+
function validateOptionalBoolean(value, path, diagnostics) {
|
|
854
|
+
return value === void 0 ? void 0 : validateBoolean(value, path, diagnostics);
|
|
855
|
+
}
|
|
856
|
+
function isCanonicalIntegerOrNumber(value, integer) {
|
|
857
|
+
if (integer) return /^-(0|[1-9]\d*)$/u.test(value) || /^(0|[1-9]\d*)$/u.test(value);
|
|
858
|
+
if (value === "0") return true;
|
|
859
|
+
return /^-(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/u.test(value) || /^(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/u.test(value);
|
|
860
|
+
}
|
|
861
|
+
function requireKeys(value, required, path, diagnostics, optional = []) {
|
|
862
|
+
for (const key of required) if (!(key in value) && !optional.includes(key)) diagnostics.push(diagnostic("invalid-snapshot", `Missing required field "${key}"`, [...path, key]));
|
|
863
|
+
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
864
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) diagnostics.push(diagnostic("unknown-field", `Unknown snapshot field "${key}"`, [...path, key]));
|
|
865
|
+
}
|
|
866
|
+
function isRecord(value) {
|
|
867
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
868
|
+
}
|
|
869
|
+
function diagnostic(code, message, path) {
|
|
870
|
+
return {
|
|
871
|
+
code,
|
|
872
|
+
message,
|
|
873
|
+
path: Object.freeze([...path])
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function freezeSnapshot(snapshot) {
|
|
877
|
+
return deepFreeze(snapshot);
|
|
878
|
+
}
|
|
879
|
+
function deepFreeze(value) {
|
|
880
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
|
|
881
|
+
for (const nested of Object.values(value)) deepFreeze(nested);
|
|
882
|
+
return Object.freeze(value);
|
|
883
|
+
}
|
|
884
|
+
//#endregion
|
|
885
|
+
//#region src/snapshot/canonical.ts
|
|
886
|
+
/** Error raised when a value cannot cross the canonical JSON boundary. */
|
|
887
|
+
var SnapshotCanonicalError = class extends TypeError {
|
|
888
|
+
path;
|
|
889
|
+
constructor(message, path = []) {
|
|
890
|
+
super(message);
|
|
891
|
+
this.name = "SnapshotCanonicalError";
|
|
892
|
+
this.path = Object.freeze([...path]);
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
/** Convert a JavaScript value to the explicit JSON-safe snapshot domain. */
|
|
896
|
+
function toSnapshotJsonValue(value, path = [], seen = /* @__PURE__ */ new WeakSet()) {
|
|
897
|
+
if (value === null) return null;
|
|
898
|
+
switch (typeof value) {
|
|
899
|
+
case "string":
|
|
900
|
+
case "boolean": return value;
|
|
901
|
+
case "number":
|
|
902
|
+
if (Number.isNaN(value)) return { $number: "NaN" };
|
|
903
|
+
if (value === Infinity) return { $number: "Infinity" };
|
|
904
|
+
if (value === -Infinity) return { $number: "-Infinity" };
|
|
905
|
+
return Object.is(value, -0) ? 0 : value;
|
|
906
|
+
case "bigint": return { $bigint: value.toString() };
|
|
907
|
+
case "undefined":
|
|
908
|
+
case "function":
|
|
909
|
+
case "symbol": throw new SnapshotCanonicalError(`Snapshot values cannot contain ${typeof value}`, path);
|
|
910
|
+
case "object": break;
|
|
911
|
+
default: throw new SnapshotCanonicalError("Unsupported snapshot value", path);
|
|
912
|
+
}
|
|
913
|
+
if (seen.has(value)) throw new SnapshotCanonicalError("Snapshot values cannot be cyclic", path);
|
|
914
|
+
seen.add(value);
|
|
915
|
+
try {
|
|
916
|
+
if (Array.isArray(value)) return Object.freeze(value.map((item, index) => toSnapshotJsonValue(item, [...path, index], seen)));
|
|
917
|
+
const prototype = Object.getPrototypeOf(value);
|
|
918
|
+
if (prototype !== Object.prototype && prototype !== null) throw new SnapshotCanonicalError("Snapshot values must be plain objects, arrays, or scalar values", path);
|
|
919
|
+
const result = {};
|
|
920
|
+
for (const key of Object.keys(value).sort()) result[key] = toSnapshotJsonValue(value[key], [...path, key], seen);
|
|
921
|
+
return Object.freeze(result);
|
|
922
|
+
} finally {
|
|
923
|
+
seen.delete(value);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
/** Render a JSON-safe value with deterministic scalar and object handling. */
|
|
927
|
+
function canonicalJson(value) {
|
|
928
|
+
if (value === null) return "null";
|
|
929
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
930
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
931
|
+
if (typeof value === "number") {
|
|
932
|
+
if (!Number.isFinite(value)) throw new SnapshotCanonicalError("Non-finite numbers must use a tagged snapshot number");
|
|
933
|
+
return Object.is(value, -0) ? "0" : JSON.stringify(value);
|
|
934
|
+
}
|
|
935
|
+
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
936
|
+
if (isSpecialNumber(value)) return `{"$number":${JSON.stringify(value.$number)}}`;
|
|
937
|
+
if (isBigInt(value)) return `{"$bigint":${JSON.stringify(value.$bigint)}}`;
|
|
938
|
+
const objectValue = value;
|
|
939
|
+
return `{${Object.keys(objectValue).map((key) => `${JSON.stringify(key)}:${canonicalJson(objectValue[key])}`).join(",")}}`;
|
|
940
|
+
}
|
|
941
|
+
/** Canonical JSON for a schema snapshot after fixed-order reconstruction. */
|
|
942
|
+
function encodeCanonicalSnapshot(snapshot) {
|
|
943
|
+
return canonicalJson(snapshot);
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Compute a small deterministic content digest for cache keys and diagnostics.
|
|
947
|
+
* It is deliberately not an entity identity, signature, or migration lineage.
|
|
948
|
+
*/
|
|
949
|
+
function schemaSnapshotDigest(snapshot) {
|
|
950
|
+
const source = encodeCanonicalSnapshot(assertSchemaSnapshot(snapshot));
|
|
951
|
+
let hash = 14695981039346656037n;
|
|
952
|
+
const prime = 1099511628211n;
|
|
953
|
+
const mask = 18446744073709551615n;
|
|
954
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
955
|
+
const codePoint = source.codePointAt(index) ?? 0;
|
|
956
|
+
if (codePoint > 65535) index += 1;
|
|
957
|
+
const bytes = new TextEncoder().encode(String.fromCodePoint(codePoint));
|
|
958
|
+
for (const byte of bytes) {
|
|
959
|
+
hash ^= BigInt(byte);
|
|
960
|
+
hash = hash * prime & mask;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return `fnv1a64:${hash.toString(16).padStart(16, "0")}`;
|
|
964
|
+
}
|
|
965
|
+
function isSpecialNumber(value) {
|
|
966
|
+
return Object.keys(value).length === 1 && "$number" in value && value.$number !== void 0;
|
|
967
|
+
}
|
|
968
|
+
function isBigInt(value) {
|
|
969
|
+
return Object.keys(value).length === 1 && "$bigint" in value && typeof value.$bigint === "string";
|
|
970
|
+
}
|
|
971
|
+
//#endregion
|
|
972
|
+
export { toSnapshotJsonValue as a, canonicalizeSchemaSnapshot as c, schemaSnapshotDialectVersion as d, schemaSnapshotFormat as f, schemaSnapshotDigest as i, decodeSchemaSnapshot as l, schemaSnapshotVersion as m, canonicalJson as n, SnapshotValidationError as o, schemaSnapshotNamingPolicyVersion as p, encodeCanonicalSnapshot as r, assertSchemaSnapshot as s, SnapshotCanonicalError as t, neutralSnapshotDialect as u };
|