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
package/dist/codegen.mjs
ADDED
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
import { l as decodeSchemaSnapshot } from "./canonical-BbnqavJm.mjs";
|
|
2
|
+
import { t as mapCatalogToSnapshot } from "./snapshot-CWPgzxNx.mjs";
|
|
3
|
+
//#region src/codegen/source.ts
|
|
4
|
+
const header = `/* Generated by Qubu. Do not edit this machine-owned file. */
|
|
5
|
+
import * as _qubu from 'qubu'
|
|
6
|
+
`;
|
|
7
|
+
const catalogColumnHelper = `type _CatalogColumnOptions = Omit<_qubu.ColumnOptions, 'storage'> & {
|
|
8
|
+
readonly storage?: never
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const _catalogColumn = <
|
|
12
|
+
TOutput = unknown,
|
|
13
|
+
TInsert = unknown,
|
|
14
|
+
TUpdate = unknown,
|
|
15
|
+
TSqlType extends _qubu.AnySqlType = _qubu.SqlUnknown,
|
|
16
|
+
>() =>
|
|
17
|
+
<
|
|
18
|
+
const TDialect extends string,
|
|
19
|
+
const TDeclaration extends string,
|
|
20
|
+
const TOptions extends _CatalogColumnOptions,
|
|
21
|
+
>(
|
|
22
|
+
dialect: TDialect,
|
|
23
|
+
declaration: TDeclaration,
|
|
24
|
+
options: TOptions
|
|
25
|
+
) =>
|
|
26
|
+
_qubu.nativeColumn<
|
|
27
|
+
TOutput,
|
|
28
|
+
TInsert,
|
|
29
|
+
TUpdate,
|
|
30
|
+
TOptions,
|
|
31
|
+
TSqlType,
|
|
32
|
+
TDialect,
|
|
33
|
+
TDeclaration
|
|
34
|
+
>(_qubu.nativeStorage(dialect, declaration), options)
|
|
35
|
+
`;
|
|
36
|
+
const catalogPredicateHelper = `const _catalogPredicate = <
|
|
37
|
+
const TDialect extends _qubu.SchemaDialectName,
|
|
38
|
+
>(input: _qubu.CatalogCheckSql<TDialect>) =>
|
|
39
|
+
_qubu.catalogCheck(input).expression
|
|
40
|
+
`;
|
|
41
|
+
function printSchemaSource(schema, diagnostics) {
|
|
42
|
+
const tables = schema.tables.map((table) => printTable(schema, table, diagnostics));
|
|
43
|
+
if (tables.some((table) => table === void 0)) return void 0;
|
|
44
|
+
const registry = schema.tables.map((table) => ` ${literal(table.name)}: ${table.name},`).join("\n");
|
|
45
|
+
const predicateHelper = schema.tables.some((table) => table.indexes.some((index) => index.snapshot.predicate !== void 0)) ? `\n${catalogPredicateHelper}` : "";
|
|
46
|
+
const columnHelper = schema.tables.some((table) => table.columns.length > 0) ? `\n${catalogColumnHelper}` : "";
|
|
47
|
+
return `${header}${columnHelper}${predicateHelper}\n${tables.join("\n\n")}\n\nexport const ${schema.name} = _qubu.schema(\n {\n${registry}\n },\n { namespace: ${literal(schema.namespace)} }\n)\n`;
|
|
48
|
+
}
|
|
49
|
+
function printTable(schema, table, diagnostics) {
|
|
50
|
+
const columns = table.columns.map((column) => printColumn(schema, table, column, diagnostics));
|
|
51
|
+
const constraints = table.constraints.map((constraint) => printConstraint(schema, table, constraint, diagnostics));
|
|
52
|
+
const indexes = table.indexes.map((index) => printIndex(schema, table, index, diagnostics));
|
|
53
|
+
if (columns.some((value) => value === void 0) || constraints.some((value) => value === void 0) || indexes.some((value) => value === void 0)) return;
|
|
54
|
+
const typePrefix = `_${capitalize(table.name)}`;
|
|
55
|
+
const definitionsName = `${typePrefix}Definitions`;
|
|
56
|
+
const columnsName = `${typePrefix}Columns`;
|
|
57
|
+
const constraintsName = `${typePrefix}Constraints`;
|
|
58
|
+
const indexesName = `${typePrefix}Indexes`;
|
|
59
|
+
const tableTypeName = `${typePrefix}Table`;
|
|
60
|
+
const constraintTypes = table.constraints.map((constraint) => printConstraintType(table, constraint, columnsName, diagnostics));
|
|
61
|
+
const indexTypes = table.indexes.map((index) => printIndexType(table, index, columnsName, diagnostics));
|
|
62
|
+
if (constraintTypes.some((value) => value === void 0) || indexTypes.some((value) => value === void 0)) return;
|
|
63
|
+
return `const ${definitionsName} = {
|
|
64
|
+
${columns.map((value) => indent(value ?? "", 2)).join("\n")}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type ${columnsName} = _qubu.TableColumns<
|
|
68
|
+
typeof ${definitionsName},
|
|
69
|
+
_qubu.TableIdentity<${literal(table.snapshot.physicalName)}>
|
|
70
|
+
>
|
|
71
|
+
|
|
72
|
+
type ${constraintsName} = {
|
|
73
|
+
${constraintTypes.map((value) => indent(value ?? "", 2)).join("\n")}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type ${indexesName} = {
|
|
77
|
+
${indexTypes.map((value) => indent(value ?? "", 2)).join("\n")}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type ${tableTypeName} = _qubu.Table<
|
|
81
|
+
${literal(table.snapshot.physicalName)},
|
|
82
|
+
typeof ${definitionsName},
|
|
83
|
+
${constraintsName},
|
|
84
|
+
${indexesName}
|
|
85
|
+
> & {
|
|
86
|
+
readonly columns: ${columnsName}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const ${table.name}: ${tableTypeName} = _qubu.table(
|
|
90
|
+
${literal(table.snapshot.physicalName)},
|
|
91
|
+
${definitionsName},
|
|
92
|
+
_current => ({
|
|
93
|
+
constraints: {
|
|
94
|
+
${constraints.map((value) => indent(value ?? "", 6)).join("\n")}
|
|
95
|
+
},
|
|
96
|
+
indexes: {
|
|
97
|
+
${indexes.map((value) => indent(value ?? "", 6)).join("\n")}
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
)`;
|
|
101
|
+
}
|
|
102
|
+
function printConstraintType(table, constraint, columnsName, diagnostics) {
|
|
103
|
+
const snapshot = constraint.snapshot;
|
|
104
|
+
const path = [
|
|
105
|
+
"snapshot",
|
|
106
|
+
"tables",
|
|
107
|
+
table.snapshot.id,
|
|
108
|
+
"constraints",
|
|
109
|
+
snapshot.id
|
|
110
|
+
];
|
|
111
|
+
if (snapshot.kind === "check") return `readonly ${literal(constraint.name)}: _qubu.CheckConstraint`;
|
|
112
|
+
const columns = printColumnTypeTuple(table, snapshot.columns, columnsName, path, diagnostics);
|
|
113
|
+
if (columns === void 0) return void 0;
|
|
114
|
+
if (snapshot.kind === "foreign-key") return `readonly ${literal(constraint.name)}: _qubu.ForeignKeyConstraint<${columns}>`;
|
|
115
|
+
if (snapshot.kind === "unique-constraint") return `readonly ${literal(constraint.name)}: _qubu.UniqueConstraint<
|
|
116
|
+
${columns},
|
|
117
|
+
${literal(snapshot.nulls)}
|
|
118
|
+
>`;
|
|
119
|
+
return `readonly ${literal(constraint.name)}: _qubu.KeyConstraint<
|
|
120
|
+
${literal(snapshot.kind)},
|
|
121
|
+
${columns}
|
|
122
|
+
>`;
|
|
123
|
+
}
|
|
124
|
+
function printIndexType(table, index, columnsName, diagnostics) {
|
|
125
|
+
const path = [
|
|
126
|
+
"snapshot",
|
|
127
|
+
"tables",
|
|
128
|
+
table.snapshot.id,
|
|
129
|
+
"indexes",
|
|
130
|
+
index.snapshot.id
|
|
131
|
+
];
|
|
132
|
+
const terms = index.snapshot.terms.map((term, position) => printIndexTermType(table, term, columnsName, [
|
|
133
|
+
...path,
|
|
134
|
+
"terms",
|
|
135
|
+
position
|
|
136
|
+
], diagnostics));
|
|
137
|
+
if (terms.some((term) => term === void 0)) return void 0;
|
|
138
|
+
const properties = [
|
|
139
|
+
`readonly terms: readonly [${terms.join(", ")}]`,
|
|
140
|
+
`readonly unique: ${index.snapshot.unique ? "true" : "false"}`,
|
|
141
|
+
`readonly candidateKey: ${index.snapshot.candidateKey ? "true" : "false"}`,
|
|
142
|
+
`readonly predicate: ${index.snapshot.predicate === void 0 ? "undefined" : "_qubu.CatalogCheckExpression"}`
|
|
143
|
+
];
|
|
144
|
+
if (index.snapshot.includedColumns !== void 0) {
|
|
145
|
+
const included = printColumnTypeTuple(table, index.snapshot.includedColumns, columnsName, [...path, "includedColumns"], diagnostics);
|
|
146
|
+
if (included === void 0) return void 0;
|
|
147
|
+
properties.push(`readonly includedColumns?: ${included}`);
|
|
148
|
+
} else properties.push("readonly includedColumns?: undefined");
|
|
149
|
+
return `readonly ${literal(index.name)}: {
|
|
150
|
+
readonly kind: 'index'
|
|
151
|
+
${properties.map((property) => ` ${property}`).join("\n")}
|
|
152
|
+
}`;
|
|
153
|
+
}
|
|
154
|
+
function printIndexTermType(table, term, columnsName, path, diagnostics) {
|
|
155
|
+
if (term.kind !== "order") return printIndexTermExpressionType(table, term, columnsName, path, diagnostics);
|
|
156
|
+
const expression = printIndexTermExpressionType(table, term.expression, columnsName, [...path, "expression"], diagnostics);
|
|
157
|
+
return expression === void 0 ? void 0 : `_qubu.OrderTerm<unknown, ${expression}>`;
|
|
158
|
+
}
|
|
159
|
+
function printIndexTermExpressionType(table, expression, columnsName, path, diagnostics) {
|
|
160
|
+
if (expression.kind === "expression") return "_qubu.AnyExpression";
|
|
161
|
+
const column = table.columnsBySnapshotId.get(expression.column);
|
|
162
|
+
if (column === void 0) {
|
|
163
|
+
diagnostics.push(sourceError(`Index column "${expression.column}" was not resolved`, [...path, "column"]));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
return `${columnsName}[${literal(column.name)}]`;
|
|
167
|
+
}
|
|
168
|
+
function printColumnTypeTuple(table, ids, columnsName, path, diagnostics) {
|
|
169
|
+
const columns = [];
|
|
170
|
+
for (const [position, id] of ids.entries()) {
|
|
171
|
+
const column = table.columnsBySnapshotId.get(id);
|
|
172
|
+
if (column === void 0) {
|
|
173
|
+
diagnostics.push(sourceError(`Column "${id}" was not resolved`, [
|
|
174
|
+
...path,
|
|
175
|
+
"columns",
|
|
176
|
+
position
|
|
177
|
+
]));
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
columns.push(`${columnsName}[${literal(column.name)}]`);
|
|
181
|
+
}
|
|
182
|
+
return columns.length === ids.length && columns.length > 0 ? `readonly [${columns.join(", ")}]` : void 0;
|
|
183
|
+
}
|
|
184
|
+
function printColumn(schema, table, column, diagnostics) {
|
|
185
|
+
const path = [
|
|
186
|
+
"snapshot",
|
|
187
|
+
"tables",
|
|
188
|
+
table.snapshot.id,
|
|
189
|
+
"columns",
|
|
190
|
+
column.snapshot.id
|
|
191
|
+
];
|
|
192
|
+
const snapshot = column.snapshot;
|
|
193
|
+
if (snapshot.storage?.kind !== "native") {
|
|
194
|
+
diagnostics.push(sourceError("Generated columns require native storage", [...path, "storage"]));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const options = [`nullable: ${snapshot.nullable ? "true" : "false"}`, `sqlName: ${literal(snapshot.physicalName)}`];
|
|
198
|
+
if (snapshot.default !== void 0) {
|
|
199
|
+
if (snapshot.default.kind === "external") options.push("default: _qubu.externalDefault()");
|
|
200
|
+
else if (snapshot.default.kind === "literal") {
|
|
201
|
+
const value = printSnapshotLiteral(snapshot.default.value, path, diagnostics);
|
|
202
|
+
if (value === void 0) return void 0;
|
|
203
|
+
options.push(`default: ${value}`);
|
|
204
|
+
} else {
|
|
205
|
+
const value = printExpression(snapshot.default.expression, schema, [
|
|
206
|
+
...path,
|
|
207
|
+
"default",
|
|
208
|
+
"expression"
|
|
209
|
+
], diagnostics);
|
|
210
|
+
if (value === void 0) return void 0;
|
|
211
|
+
options.push(`default: ${value}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (snapshot.generatedColumn !== void 0) {
|
|
215
|
+
if (snapshot.generatedColumn.kind === "external") options.push("generatedColumn: _qubu.externalGeneratedColumn()");
|
|
216
|
+
else {
|
|
217
|
+
const expression = printExpression(snapshot.generatedColumn.expression, schema, [
|
|
218
|
+
...path,
|
|
219
|
+
"generatedColumn",
|
|
220
|
+
"expression"
|
|
221
|
+
], diagnostics);
|
|
222
|
+
if (expression === void 0) return void 0;
|
|
223
|
+
options.push(`generatedColumn: _qubu.generatedColumn(${expression}, ${literal(snapshot.generatedColumn.mode)})`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (snapshot.identity !== void 0) {
|
|
227
|
+
const extension = snapshot.identity.dialect ? printExtension(snapshot.identity.dialect, schema, [
|
|
228
|
+
...path,
|
|
229
|
+
"identity",
|
|
230
|
+
"dialect"
|
|
231
|
+
], diagnostics) : void 0;
|
|
232
|
+
if (snapshot.identity.dialect !== void 0 && extension === void 0) return;
|
|
233
|
+
options.push(extension === void 0 ? `identity: _qubu.identityColumn(${literal(snapshot.identity.generation)})` : `identity: _qubu.identityColumn(${literal(snapshot.identity.generation)}, { dialect: ${extension} })`);
|
|
234
|
+
}
|
|
235
|
+
if (snapshot.onUpdate !== void 0) {
|
|
236
|
+
const expression = printExpression(snapshot.onUpdate, schema, [...path, "onUpdate"], diagnostics);
|
|
237
|
+
if (expression === void 0) return void 0;
|
|
238
|
+
options.push(`onUpdate: ${expression}`);
|
|
239
|
+
}
|
|
240
|
+
const types = [
|
|
241
|
+
applicationType(column.output),
|
|
242
|
+
applicationType(column.insert),
|
|
243
|
+
applicationType(column.update),
|
|
244
|
+
sqlDomainType(column.sqlDomain)
|
|
245
|
+
].join(", ");
|
|
246
|
+
const optionSource = `{
|
|
247
|
+
${options.map((option) => ` ${option},`).join("\n")}
|
|
248
|
+
}`;
|
|
249
|
+
return `${literal(column.name)}: _catalogColumn<${types}>()(
|
|
250
|
+
${literal(snapshot.storage.dialect)},
|
|
251
|
+
${literal(snapshot.storage.type)},
|
|
252
|
+
${indent(optionSource, 2)}
|
|
253
|
+
),`;
|
|
254
|
+
}
|
|
255
|
+
function printConstraint(schema, table, constraint, diagnostics) {
|
|
256
|
+
const snapshot = constraint.snapshot;
|
|
257
|
+
const path = [
|
|
258
|
+
"snapshot",
|
|
259
|
+
"tables",
|
|
260
|
+
table.snapshot.id,
|
|
261
|
+
"constraints",
|
|
262
|
+
snapshot.id
|
|
263
|
+
];
|
|
264
|
+
const options = printConstraintOptions(snapshot, schema, path, diagnostics);
|
|
265
|
+
if (options === void 0) return void 0;
|
|
266
|
+
let expression;
|
|
267
|
+
if (snapshot.kind === "check") {
|
|
268
|
+
const sql = catalogExpressionData(snapshot.expression, schema, [...path, "expression"], diagnostics);
|
|
269
|
+
if (sql === void 0) return void 0;
|
|
270
|
+
expression = `_qubu.catalogCheck(${sql}, ${options})`;
|
|
271
|
+
} else if (snapshot.kind === "foreign-key") {
|
|
272
|
+
const local = printColumns(table, snapshot.columns, path, diagnostics);
|
|
273
|
+
const targetTable = schema.tablesBySnapshotId.get(snapshot.target.table);
|
|
274
|
+
if (targetTable === void 0) {
|
|
275
|
+
diagnostics.push(sourceError(`Foreign-key target table "${snapshot.target.table}" was not resolved`, [
|
|
276
|
+
...path,
|
|
277
|
+
"target",
|
|
278
|
+
"table"
|
|
279
|
+
]));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const target = printColumns(targetTable, snapshot.target.columns, [...path, "target"], diagnostics, targetTable.name);
|
|
283
|
+
if (local === void 0 || target === void 0) return void 0;
|
|
284
|
+
expression = `_qubu.catalogForeignKey(
|
|
285
|
+
[${local.join(", ")}],
|
|
286
|
+
() => _qubu.references(${targetTable.name}, ${target.join(", ")}),
|
|
287
|
+
${options}
|
|
288
|
+
)`;
|
|
289
|
+
} else {
|
|
290
|
+
const columns = printColumns(table, snapshot.columns, path, diagnostics);
|
|
291
|
+
if (columns === void 0) return void 0;
|
|
292
|
+
expression = `_qubu.${snapshot.kind === "primary-key" ? "primaryKey" : snapshot.kind === "unique" ? "unique" : "uniqueConstraint"}(${columns.join(", ")}, ${options})`;
|
|
293
|
+
}
|
|
294
|
+
return `${literal(constraint.name)}: ${expression},`;
|
|
295
|
+
}
|
|
296
|
+
function printConstraintOptions(constraint, schema, path, diagnostics) {
|
|
297
|
+
const properties = [`physicalName: ${literal(constraint.physicalName)}`];
|
|
298
|
+
if (constraint.kind === "unique-constraint") properties.push(`nulls: ${literal(constraint.nulls)}`);
|
|
299
|
+
if (constraint.kind === "foreign-key") {
|
|
300
|
+
if (constraint.onUpdate !== void 0) properties.push(`onUpdate: ${literal(constraint.onUpdate)}`);
|
|
301
|
+
if (constraint.onDelete !== void 0) properties.push(`onDelete: ${literal(constraint.onDelete)}`);
|
|
302
|
+
if (constraint.match !== void 0) properties.push(`match: ${literal(constraint.match)}`);
|
|
303
|
+
}
|
|
304
|
+
if (constraint.deferrable !== void 0) properties.push(`deferrable: ${constraint.deferrable ? "true" : "false"}`);
|
|
305
|
+
if (constraint.initially !== void 0) properties.push(`initially: ${literal(constraint.initially)}`);
|
|
306
|
+
if (constraint.dialect !== void 0) {
|
|
307
|
+
const extension = printExtension(constraint.dialect, schema, [...path, "dialect"], diagnostics);
|
|
308
|
+
if (extension === void 0) return void 0;
|
|
309
|
+
properties.push(`dialect: ${extension}`);
|
|
310
|
+
}
|
|
311
|
+
return `{ ${properties.join(", ")} }`;
|
|
312
|
+
}
|
|
313
|
+
function printIndex(schema, table, index, diagnostics) {
|
|
314
|
+
const path = [
|
|
315
|
+
"snapshot",
|
|
316
|
+
"tables",
|
|
317
|
+
table.snapshot.id,
|
|
318
|
+
"indexes",
|
|
319
|
+
index.snapshot.id
|
|
320
|
+
];
|
|
321
|
+
const terms = index.snapshot.terms.map((term, termIndex) => printIndexTerm(schema, table, term, [
|
|
322
|
+
...path,
|
|
323
|
+
"terms",
|
|
324
|
+
termIndex
|
|
325
|
+
], diagnostics));
|
|
326
|
+
if (terms.some((term) => term === void 0)) return void 0;
|
|
327
|
+
const options = [`physicalName: ${literal(index.snapshot.physicalName)}`, `unique: ${index.snapshot.unique ? "true" : "false"}`];
|
|
328
|
+
if (index.snapshot.predicate !== void 0) {
|
|
329
|
+
const predicate = catalogExpressionData(index.snapshot.predicate, schema, [...path, "predicate"], diagnostics);
|
|
330
|
+
if (predicate === void 0) return void 0;
|
|
331
|
+
options.push(`where: _catalogPredicate(${predicate})`);
|
|
332
|
+
}
|
|
333
|
+
if (index.snapshot.includedColumns !== void 0) {
|
|
334
|
+
const included = printColumns(table, index.snapshot.includedColumns, [...path, "includedColumns"], diagnostics);
|
|
335
|
+
if (included === void 0) return void 0;
|
|
336
|
+
options.push(`include: [${included.join(", ")}]`);
|
|
337
|
+
}
|
|
338
|
+
if (index.snapshot.dialect !== void 0) {
|
|
339
|
+
const extension = printExtension(index.snapshot.dialect, schema, [...path, "dialect"], diagnostics);
|
|
340
|
+
if (extension === void 0) return void 0;
|
|
341
|
+
options.push(`dialect: ${extension}`);
|
|
342
|
+
}
|
|
343
|
+
return `${literal(index.name)}: _qubu.index(
|
|
344
|
+
[${terms.join(", ")}],
|
|
345
|
+
{ ${options.join(", ")} }
|
|
346
|
+
),`;
|
|
347
|
+
}
|
|
348
|
+
function printIndexTerm(schema, table, term, path, diagnostics) {
|
|
349
|
+
if (term.kind !== "order") return printIndexTermExpression(schema, table, term, path, diagnostics);
|
|
350
|
+
const expression = printIndexTermExpression(schema, table, term.expression, [...path, "expression"], diagnostics);
|
|
351
|
+
if (expression === void 0) return void 0;
|
|
352
|
+
return `_qubu.order(${expression}, ${term.direction === void 0 ? "undefined" : literal(term.direction)}, ${term.nulls === void 0 ? "undefined" : literal(term.nulls)})`;
|
|
353
|
+
}
|
|
354
|
+
function printIndexTermExpression(schema, table, expression, path, diagnostics) {
|
|
355
|
+
if (expression.kind === "expression") return printExpression(expression.expression, schema, [...path, "expression"], diagnostics);
|
|
356
|
+
const column = table.columnsBySnapshotId.get(expression.column);
|
|
357
|
+
if (column === void 0) {
|
|
358
|
+
diagnostics.push(sourceError(`Index column "${expression.column}" was not resolved`, [...path, "column"]));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
return `_current.columns[${literal(column.name)}]`;
|
|
362
|
+
}
|
|
363
|
+
function printColumns(table, ids, path, diagnostics, binding = "_current") {
|
|
364
|
+
const result = [];
|
|
365
|
+
for (const [index, id] of ids.entries()) {
|
|
366
|
+
const column = table.columnsBySnapshotId.get(id);
|
|
367
|
+
if (column === void 0) {
|
|
368
|
+
diagnostics.push(sourceError(`Column "${id}" was not resolved`, [
|
|
369
|
+
...path,
|
|
370
|
+
"columns",
|
|
371
|
+
index
|
|
372
|
+
]));
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
result.push(`${binding}.columns[${literal(column.name)}]`);
|
|
376
|
+
}
|
|
377
|
+
return result.length === ids.length && result.length > 0 ? result : void 0;
|
|
378
|
+
}
|
|
379
|
+
function printExpression(expression, schema, path, diagnostics) {
|
|
380
|
+
const data = catalogExpressionData(expression, schema, path, diagnostics);
|
|
381
|
+
return data === void 0 ? void 0 : `_qubu.unsafeSchemaSql(${data})`;
|
|
382
|
+
}
|
|
383
|
+
function catalogExpressionData(expression, schema, path, diagnostics) {
|
|
384
|
+
if (expression.expressionKind !== "unsafe" || expression.dialect !== schema.dialect || expression.sql.trim().length === 0) {
|
|
385
|
+
diagnostics.push(sourceError("Catalog expressions must be non-empty, unsafe, and tagged for the selected dialect", path));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
return `{ dialect: ${literal(expression.dialect)}, sql: ${literal(expression.sql)} }`;
|
|
389
|
+
}
|
|
390
|
+
function printExtension(extension, schema, path, diagnostics) {
|
|
391
|
+
if (extension.dialect !== schema.dialect || extension.version !== 1 || !isJsonObject(extension.data) || Object.hasOwn(extension.data, "dialect")) {
|
|
392
|
+
diagnostics.push(sourceError("Dialect extensions must be v1 object data for the selected dialect and cannot contain a data field named \"dialect\"", path));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const data = extension.data;
|
|
396
|
+
const entries = Object.keys(data).sort().map((key) => `[${literal(key)}]: ${printJsonValue(data[key])}`);
|
|
397
|
+
return `{ dialect: ${literal(extension.dialect)}${entries.length === 0 ? "" : `, ${entries.join(", ")}`} }`;
|
|
398
|
+
}
|
|
399
|
+
function printSnapshotLiteral(value, path, diagnostics) {
|
|
400
|
+
switch (value.kind) {
|
|
401
|
+
case "null": return "null";
|
|
402
|
+
case "boolean": return value.value ? "true" : "false";
|
|
403
|
+
case "string": return literal(value.value);
|
|
404
|
+
case "bigint": return `BigInt(${literal(value.value)})`;
|
|
405
|
+
case "number": {
|
|
406
|
+
const number = Number(value.value);
|
|
407
|
+
if (!Number.isFinite(number)) {
|
|
408
|
+
diagnostics.push(sourceError("Default number literals must be finite", [...path, "default"]));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
return Object.is(number, -0) ? "0" : String(number);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function printJsonValue(value) {
|
|
416
|
+
if (value === null) return "null";
|
|
417
|
+
if (typeof value === "string") return literal(value);
|
|
418
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
419
|
+
if (typeof value === "number") return Object.is(value, -0) ? "0" : String(value);
|
|
420
|
+
if (Array.isArray(value)) return `[${value.map(printJsonValue).join(", ")}]`;
|
|
421
|
+
return `{ ${Object.keys(value).sort().map((key) => `[${literal(key)}]: ${printJsonValue(value[key])}`).join(", ")} }`;
|
|
422
|
+
}
|
|
423
|
+
function applicationType(value) {
|
|
424
|
+
return value;
|
|
425
|
+
}
|
|
426
|
+
function sqlDomainType(value) {
|
|
427
|
+
switch (value) {
|
|
428
|
+
case "unknown": return "_qubu.SqlUnknown";
|
|
429
|
+
case "integer": return "_qubu.SqlInteger";
|
|
430
|
+
case "decimal": return "_qubu.SqlDecimal";
|
|
431
|
+
case "text": return "_qubu.SqlText";
|
|
432
|
+
case "boolean": return "_qubu.SqlBoolean";
|
|
433
|
+
case "date": return "_qubu.SqlDate";
|
|
434
|
+
case "timestamp": return "_qubu.SqlTimestamp";
|
|
435
|
+
case "uuid": return "_qubu.SqlUuid";
|
|
436
|
+
case "json": return "_qubu.SqlJson";
|
|
437
|
+
case "bigint": return "_qubu.SqlBigInt";
|
|
438
|
+
case "binary": return "_qubu.SqlBinary";
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function sourceError(message, path) {
|
|
442
|
+
return {
|
|
443
|
+
severity: "error",
|
|
444
|
+
code: "unrepresentable-fact",
|
|
445
|
+
message,
|
|
446
|
+
path
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function literal(value) {
|
|
450
|
+
return JSON.stringify(value).replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
|
|
451
|
+
}
|
|
452
|
+
function capitalize(value) {
|
|
453
|
+
return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
|
|
454
|
+
}
|
|
455
|
+
function indent(value, spaces) {
|
|
456
|
+
const prefix = " ".repeat(spaces);
|
|
457
|
+
return value.split("\n").map((line) => `${prefix}${line}`).join("\n");
|
|
458
|
+
}
|
|
459
|
+
function isJsonObject(value) {
|
|
460
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/codegen/generate.ts
|
|
464
|
+
const supportedDialects = /* @__PURE__ */ new Set([
|
|
465
|
+
"postgresql",
|
|
466
|
+
"sqlite",
|
|
467
|
+
"mysql"
|
|
468
|
+
]);
|
|
469
|
+
const applicationTypes = /* @__PURE__ */ new Set([
|
|
470
|
+
"unknown",
|
|
471
|
+
"string",
|
|
472
|
+
"number",
|
|
473
|
+
"boolean",
|
|
474
|
+
"bigint",
|
|
475
|
+
"Date",
|
|
476
|
+
"Uint8Array"
|
|
477
|
+
]);
|
|
478
|
+
const sqlDomains = /* @__PURE__ */ new Set([
|
|
479
|
+
"unknown",
|
|
480
|
+
"integer",
|
|
481
|
+
"decimal",
|
|
482
|
+
"text",
|
|
483
|
+
"boolean",
|
|
484
|
+
"date",
|
|
485
|
+
"timestamp",
|
|
486
|
+
"uuid",
|
|
487
|
+
"json",
|
|
488
|
+
"bigint",
|
|
489
|
+
"binary"
|
|
490
|
+
]);
|
|
491
|
+
const orderableDomains = /* @__PURE__ */ new Set([
|
|
492
|
+
"unknown",
|
|
493
|
+
"integer",
|
|
494
|
+
"decimal",
|
|
495
|
+
"text",
|
|
496
|
+
"date",
|
|
497
|
+
"timestamp",
|
|
498
|
+
"bigint"
|
|
499
|
+
]);
|
|
500
|
+
const reservedBindings = /* @__PURE__ */ new Set([
|
|
501
|
+
"arguments",
|
|
502
|
+
"await",
|
|
503
|
+
"break",
|
|
504
|
+
"case",
|
|
505
|
+
"catch",
|
|
506
|
+
"class",
|
|
507
|
+
"const",
|
|
508
|
+
"continue",
|
|
509
|
+
"debugger",
|
|
510
|
+
"default",
|
|
511
|
+
"delete",
|
|
512
|
+
"do",
|
|
513
|
+
"else",
|
|
514
|
+
"enum",
|
|
515
|
+
"eval",
|
|
516
|
+
"export",
|
|
517
|
+
"extends",
|
|
518
|
+
"false",
|
|
519
|
+
"finally",
|
|
520
|
+
"for",
|
|
521
|
+
"function",
|
|
522
|
+
"if",
|
|
523
|
+
"implements",
|
|
524
|
+
"import",
|
|
525
|
+
"in",
|
|
526
|
+
"interface",
|
|
527
|
+
"instanceof",
|
|
528
|
+
"let",
|
|
529
|
+
"new",
|
|
530
|
+
"null",
|
|
531
|
+
"package",
|
|
532
|
+
"private",
|
|
533
|
+
"protected",
|
|
534
|
+
"public",
|
|
535
|
+
"return",
|
|
536
|
+
"static",
|
|
537
|
+
"super",
|
|
538
|
+
"switch",
|
|
539
|
+
"this",
|
|
540
|
+
"throw",
|
|
541
|
+
"true",
|
|
542
|
+
"try",
|
|
543
|
+
"typeof",
|
|
544
|
+
"var",
|
|
545
|
+
"void",
|
|
546
|
+
"while",
|
|
547
|
+
"with",
|
|
548
|
+
"yield"
|
|
549
|
+
]);
|
|
550
|
+
const excludedFamilies = [
|
|
551
|
+
["views", "views"],
|
|
552
|
+
["sequences", "sequences"],
|
|
553
|
+
["enums", "enums"],
|
|
554
|
+
["domains", "domains"],
|
|
555
|
+
["collations", "collations"],
|
|
556
|
+
["triggers", "triggers"],
|
|
557
|
+
["routines", "routines"],
|
|
558
|
+
["partitions", "partitions"],
|
|
559
|
+
["policies", "policies"],
|
|
560
|
+
["extensionObjects", "extension objects"],
|
|
561
|
+
["deferredObjects", "deferred objects"],
|
|
562
|
+
["opaqueObjects", "opaque objects"],
|
|
563
|
+
["comments", "comments"],
|
|
564
|
+
["ownership", "ownership records"]
|
|
565
|
+
];
|
|
566
|
+
/**
|
|
567
|
+
* Generate a deterministic TypeScript module from one introspection result.
|
|
568
|
+
*
|
|
569
|
+
* @remarks
|
|
570
|
+
* Generation accepts only a successful, non-lossy Snapshot v1 result for one
|
|
571
|
+
* PostgreSQL, SQLite, or MySQL namespace. It is pure: it opens no connection,
|
|
572
|
+
* performs no filesystem writes, and never evaluates catalog SQL. Existing
|
|
573
|
+
* introspection diagnostics are copied into the result. Any error returns no
|
|
574
|
+
* source.
|
|
575
|
+
*
|
|
576
|
+
* The generated module exports every ordinary table plus one schema registry.
|
|
577
|
+
* Physical names and native declarations remain escaped string literals.
|
|
578
|
+
* Catalog expressions use dialect-tagged schema data, checks use
|
|
579
|
+
* `catalogCheck()`, and foreign keys use lazy `catalogForeignKey()` targets.
|
|
580
|
+
* Foreign keys must have equal local and target arity and target an exact
|
|
581
|
+
* primary key, strict unique key, or candidate index. Nullable database UNIQUE
|
|
582
|
+
* constraints do not provide that candidate-key proof and fail generation.
|
|
583
|
+
*
|
|
584
|
+
* Application output, insert, and update types default independently to
|
|
585
|
+
* `unknown`. The generator may attach a conservative SQL semantic domain, and
|
|
586
|
+
* {@link SchemaCodegenOptions.mapColumn} can override either type axis with a
|
|
587
|
+
* fixed token. Naming callbacks can replace suggested camelCase IDs, but they
|
|
588
|
+
* cannot supply source syntax.
|
|
589
|
+
*
|
|
590
|
+
* @param input A completed introspection operation. Failed and lossy results
|
|
591
|
+
* are reported as diagnostics rather than thrown errors.
|
|
592
|
+
* @param options Controlled logical-name and column-type mappings.
|
|
593
|
+
* @returns Generated source and diagnostics, or diagnostics without source.
|
|
594
|
+
*/
|
|
595
|
+
function generateSchemaSource(input, options = {}) {
|
|
596
|
+
const diagnostics = [];
|
|
597
|
+
try {
|
|
598
|
+
if (!isOptions(options)) {
|
|
599
|
+
diagnostics.push(errorDiagnostic("invalid-option", "Code generation options must be an object", ["options"]));
|
|
600
|
+
return failure(diagnostics);
|
|
601
|
+
}
|
|
602
|
+
if (!isIntrospectionResult(input)) {
|
|
603
|
+
diagnostics.push(errorDiagnostic("invalid-input", "Code generation requires an introspection result", []));
|
|
604
|
+
return failure(diagnostics);
|
|
605
|
+
}
|
|
606
|
+
diagnostics.push(...input.diagnostics);
|
|
607
|
+
appendExcludedFamilyDiagnostics(input.catalog, diagnostics);
|
|
608
|
+
if (!input.ok) {
|
|
609
|
+
diagnostics.push(errorDiagnostic("invalid-input", "Code generation requires a successful introspection result", []));
|
|
610
|
+
return failure(diagnostics);
|
|
611
|
+
}
|
|
612
|
+
if (input.lossy) diagnostics.push(errorDiagnostic("lossy-input", "Code generation rejects lossy introspection results", ["lossy"], "Run introspection in strict mode and resolve every reported omission."));
|
|
613
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) return failure(diagnostics);
|
|
614
|
+
const decoded = decodeSchemaSnapshot(input.snapshot);
|
|
615
|
+
if (!decoded.ok) {
|
|
616
|
+
diagnostics.push(...decoded.diagnostics.map((diagnostic) => errorDiagnostic("unsupported-snapshot", diagnostic.message, diagnostic.path)));
|
|
617
|
+
return failure(diagnostics);
|
|
618
|
+
}
|
|
619
|
+
const snapshot = decoded.value;
|
|
620
|
+
validateInputEnvelope(input.catalog, snapshot, diagnostics);
|
|
621
|
+
validatePhysicalFacts(input.catalog, snapshot, diagnostics);
|
|
622
|
+
validateSchemaProofs(snapshot, diagnostics);
|
|
623
|
+
if (hasErrors(diagnostics)) return failure(diagnostics);
|
|
624
|
+
const resolved = resolveSchema(input.catalog, snapshot, options, diagnostics);
|
|
625
|
+
if (resolved === void 0 || hasErrors(diagnostics)) return failure(diagnostics);
|
|
626
|
+
validateResolvedDomains(resolved, diagnostics);
|
|
627
|
+
if (hasErrors(diagnostics)) return failure(diagnostics);
|
|
628
|
+
const source = printSchemaSource(resolved, diagnostics);
|
|
629
|
+
if (source === void 0 || hasErrors(diagnostics)) return failure(diagnostics);
|
|
630
|
+
return Object.freeze({
|
|
631
|
+
ok: true,
|
|
632
|
+
source,
|
|
633
|
+
diagnostics: freezeDiagnostics(diagnostics)
|
|
634
|
+
});
|
|
635
|
+
} catch (caught) {
|
|
636
|
+
diagnostics.push(errorDiagnostic("unsafe-source", caught instanceof Error ? `Schema source generation failed safely: ${caught.message}` : "Schema source generation failed safely", []));
|
|
637
|
+
return failure(diagnostics);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function validateInputEnvelope(catalog, snapshot, diagnostics) {
|
|
641
|
+
if (!supportedDialects.has(catalog.dialect)) diagnostics.push(errorDiagnostic("unsupported-snapshot", `Code generation does not support catalog dialect "${catalog.dialect}"`, ["catalog", "dialect"]));
|
|
642
|
+
if (snapshot.dialect.name !== catalog.dialect) diagnostics.push(errorDiagnostic("unsupported-snapshot", `Snapshot dialect "${snapshot.dialect.name}" does not match catalog dialect "${catalog.dialect}"`, ["snapshot", "dialect"], void 0, [["catalog", "dialect"]]));
|
|
643
|
+
if (snapshot.dialect.version !== 1) diagnostics.push(errorDiagnostic("unsupported-snapshot", `Code generation supports Snapshot v1 dialect extensions, received version ${snapshot.dialect.version}`, [
|
|
644
|
+
"snapshot",
|
|
645
|
+
"dialect",
|
|
646
|
+
"version"
|
|
647
|
+
]));
|
|
648
|
+
if (snapshot.namespace !== catalog.namespace.name) diagnostics.push(errorDiagnostic("unsupported-snapshot", "Snapshot and catalog namespaces do not match", ["snapshot", "namespace"], void 0, [[
|
|
649
|
+
"catalog",
|
|
650
|
+
"namespace",
|
|
651
|
+
"name"
|
|
652
|
+
]]));
|
|
653
|
+
}
|
|
654
|
+
function validatePhysicalFacts(catalog, snapshot, diagnostics) {
|
|
655
|
+
const remapped = mapCatalogToSnapshot(catalog, {
|
|
656
|
+
namespace: catalog.namespace.name,
|
|
657
|
+
mode: "strict"
|
|
658
|
+
});
|
|
659
|
+
if (!remapped.ok) {
|
|
660
|
+
diagnostics.push(errorDiagnostic("omitted-fact", "The normalized catalog cannot be mapped to a complete strict Snapshot v1", ["catalog", "tables"], "Resolve the strict introspection diagnostics before generating source."));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const expected = JSON.stringify(toPhysicalFacts(remapped.snapshot));
|
|
664
|
+
if (JSON.stringify(toPhysicalFacts(snapshot)) !== expected) diagnostics.push(errorDiagnostic("omitted-fact", "The introspection snapshot does not contain the catalog's complete Snapshot v1 table facts", ["snapshot", "tables"], "Use the snapshot returned with this exact strict catalog result."));
|
|
665
|
+
}
|
|
666
|
+
function validateSchemaProofs(snapshot, diagnostics) {
|
|
667
|
+
const tables = new Map(snapshot.tables.map((table) => [table.id, table]));
|
|
668
|
+
for (const table of snapshot.tables) {
|
|
669
|
+
const columns = new Map(table.columns.map((column) => [column.id, column]));
|
|
670
|
+
const path = [
|
|
671
|
+
"snapshot",
|
|
672
|
+
"tables",
|
|
673
|
+
table.id
|
|
674
|
+
];
|
|
675
|
+
for (const constraint of table.constraints) {
|
|
676
|
+
if (constraint.kind !== "primary-key" && constraint.kind !== "unique") continue;
|
|
677
|
+
if (constraint.columns.filter((column) => columns.get(column)?.nullable === true).length > 0) diagnostics.push(errorDiagnostic("unrepresentable-fact", `Strict key constraint "${constraint.physicalName}" contains nullable columns`, [
|
|
678
|
+
...path,
|
|
679
|
+
"constraints",
|
|
680
|
+
constraint.id,
|
|
681
|
+
"columns"
|
|
682
|
+
], "Qubu primary and strict unique keys require every column to be non-nullable."));
|
|
683
|
+
}
|
|
684
|
+
for (const index of table.indexes) {
|
|
685
|
+
if (!index.candidateKey) continue;
|
|
686
|
+
const candidateColumns = candidateIndexColumns(index);
|
|
687
|
+
if (candidateColumns === void 0 || candidateColumns.some((column) => columns.get(column)?.nullable !== false)) diagnostics.push(errorDiagnostic("unrepresentable-fact", `Index "${index.physicalName}" is marked as a candidate key without an exact non-null column key`, [
|
|
688
|
+
...path,
|
|
689
|
+
"indexes",
|
|
690
|
+
index.id,
|
|
691
|
+
"candidateKey"
|
|
692
|
+
]));
|
|
693
|
+
}
|
|
694
|
+
for (const constraint of table.constraints) {
|
|
695
|
+
if (constraint.kind !== "foreign-key") continue;
|
|
696
|
+
const constraintPath = [
|
|
697
|
+
...path,
|
|
698
|
+
"constraints",
|
|
699
|
+
constraint.id
|
|
700
|
+
];
|
|
701
|
+
if (constraint.columns.length !== constraint.target.columns.length) {
|
|
702
|
+
diagnostics.push(errorDiagnostic("unrepresentable-fact", `Foreign key "${constraint.physicalName}" has unequal local and target arity`, [
|
|
703
|
+
...constraintPath,
|
|
704
|
+
"target",
|
|
705
|
+
"columns"
|
|
706
|
+
]));
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
const target = tables.get(constraint.target.table);
|
|
710
|
+
if (target === void 0) continue;
|
|
711
|
+
if (!hasCandidateKey(target, constraint.target.columns)) diagnostics.push(errorDiagnostic("unrepresentable-fact", `Foreign key "${constraint.physicalName}" does not target an exact primary key, strict unique key, or candidate index`, [
|
|
712
|
+
...constraintPath,
|
|
713
|
+
"target",
|
|
714
|
+
"columns"
|
|
715
|
+
], "Nullable UNIQUE constraints are database uniqueness metadata, not Qubu candidate-key proofs."));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function hasCandidateKey(table, targetColumns) {
|
|
720
|
+
const columns = new Map(table.columns.map((column) => [column.id, column]));
|
|
721
|
+
if (table.constraints.some((constraint) => (constraint.kind === "primary-key" || constraint.kind === "unique") && constraint.columns.every((column) => columns.get(column)?.nullable === false) && sameColumns(constraint.columns, targetColumns))) return true;
|
|
722
|
+
return table.indexes.some((index) => {
|
|
723
|
+
if (!index.candidateKey) return false;
|
|
724
|
+
const candidateColumns = candidateIndexColumns(index);
|
|
725
|
+
return candidateColumns !== void 0 && candidateColumns.every((column) => columns.get(column)?.nullable === false) && sameColumns(candidateColumns, targetColumns);
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
function candidateIndexColumns(index) {
|
|
729
|
+
if (!index.candidateKey || !index.unique || index.predicate !== void 0) return;
|
|
730
|
+
const columns = [];
|
|
731
|
+
for (const term of index.terms) {
|
|
732
|
+
const expression = term.kind === "order" ? term.expression : term;
|
|
733
|
+
if (expression.kind !== "column") return void 0;
|
|
734
|
+
columns.push(expression.column);
|
|
735
|
+
}
|
|
736
|
+
return columns.length > 0 ? columns : void 0;
|
|
737
|
+
}
|
|
738
|
+
function sameColumns(left, right) {
|
|
739
|
+
return left.length === right.length && left.every((column, index) => column === right[index]);
|
|
740
|
+
}
|
|
741
|
+
function resolveSchema(catalog, snapshot, options, diagnostics) {
|
|
742
|
+
const schemaSuggested = `${camelCase(catalog.namespace.name)}Schema`;
|
|
743
|
+
const schemaName = resolveName({
|
|
744
|
+
kind: "schema",
|
|
745
|
+
physicalName: catalog.namespace.name,
|
|
746
|
+
suggestedName: schemaSuggested
|
|
747
|
+
}, options, ["schema"], true, diagnostics);
|
|
748
|
+
const catalogTables = groupUnique(catalog.tables, (table) => table.physicalName, ["catalog", "tables"], diagnostics);
|
|
749
|
+
const tables = [];
|
|
750
|
+
const tableNames = /* @__PURE__ */ new Map();
|
|
751
|
+
for (const table of [...snapshot.tables].sort(comparePhysicalName)) {
|
|
752
|
+
const catalogTable = catalogTables.get(table.physicalName);
|
|
753
|
+
if (catalogTable === void 0) {
|
|
754
|
+
diagnostics.push(errorDiagnostic("omitted-fact", `Snapshot table "${table.physicalName}" is missing from the catalog`, [
|
|
755
|
+
"snapshot",
|
|
756
|
+
"tables",
|
|
757
|
+
table.id
|
|
758
|
+
]));
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
const path = [
|
|
762
|
+
"snapshot",
|
|
763
|
+
"tables",
|
|
764
|
+
table.id
|
|
765
|
+
];
|
|
766
|
+
const name = resolveName({
|
|
767
|
+
kind: "table",
|
|
768
|
+
physicalName: table.physicalName,
|
|
769
|
+
suggestedName: camelCase(table.physicalName)
|
|
770
|
+
}, options, path, true, diagnostics);
|
|
771
|
+
if (name === void 0) continue;
|
|
772
|
+
addUniqueName(name, path, tableNames, diagnostics);
|
|
773
|
+
const resolvedTable = resolveTable(table, catalogTable, name, catalog, options, diagnostics);
|
|
774
|
+
if (resolvedTable !== void 0) tables.push(resolvedTable);
|
|
775
|
+
}
|
|
776
|
+
if (schemaName !== void 0) {
|
|
777
|
+
const tableCollision = tableNames.get(schemaName);
|
|
778
|
+
if (tableCollision !== void 0) diagnostics.push(errorDiagnostic("name-collision", `Schema export and table export both resolve to "${schemaName}"`, ["schema"], void 0, [tableCollision]));
|
|
779
|
+
}
|
|
780
|
+
if (schemaName === void 0 || hasErrors(diagnostics)) return void 0;
|
|
781
|
+
const orderedTables = tables.sort(compareName);
|
|
782
|
+
return {
|
|
783
|
+
name: schemaName,
|
|
784
|
+
namespace: catalog.namespace.name,
|
|
785
|
+
dialect: catalog.dialect,
|
|
786
|
+
tables: orderedTables,
|
|
787
|
+
tablesBySnapshotId: new Map(orderedTables.map((table) => [table.snapshot.id, table]))
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function resolveTable(snapshot, catalogTable, tableName, catalog, options, diagnostics) {
|
|
791
|
+
const columnCatalog = groupUnique(catalogTable.columns, (column) => column.physicalName, [
|
|
792
|
+
"catalog",
|
|
793
|
+
"tables",
|
|
794
|
+
catalogTable.id,
|
|
795
|
+
"columns"
|
|
796
|
+
], diagnostics);
|
|
797
|
+
const columns = [];
|
|
798
|
+
const columnNames = /* @__PURE__ */ new Map();
|
|
799
|
+
for (const column of [...snapshot.columns].sort(comparePhysicalName)) {
|
|
800
|
+
const path = [
|
|
801
|
+
"snapshot",
|
|
802
|
+
"tables",
|
|
803
|
+
snapshot.id,
|
|
804
|
+
"columns",
|
|
805
|
+
column.id
|
|
806
|
+
];
|
|
807
|
+
const catalogColumn = columnCatalog.get(column.physicalName);
|
|
808
|
+
if (catalogColumn === void 0) {
|
|
809
|
+
diagnostics.push(errorDiagnostic("omitted-fact", `Snapshot column "${column.physicalName}" is missing from its catalog table`, path));
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (column.storage?.kind !== "native") {
|
|
813
|
+
diagnostics.push(errorDiagnostic("unrepresentable-fact", `Introspected column "${column.physicalName}" must retain exact native storage`, [...path, "storage"]));
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (column.storage.dialect !== catalog.dialect) {
|
|
817
|
+
diagnostics.push(errorDiagnostic("unrepresentable-fact", `Column storage dialect "${column.storage.dialect}" does not match "${catalog.dialect}"`, [
|
|
818
|
+
...path,
|
|
819
|
+
"storage",
|
|
820
|
+
"dialect"
|
|
821
|
+
]));
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (column.storage.type.trim().length === 0) {
|
|
825
|
+
diagnostics.push(errorDiagnostic("unrepresentable-fact", "Native storage declarations cannot be empty", [
|
|
826
|
+
...path,
|
|
827
|
+
"storage",
|
|
828
|
+
"type"
|
|
829
|
+
]));
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (!hasCompleteColumnBehavior(column)) {
|
|
833
|
+
diagnostics.push(errorDiagnostic("omitted-fact", `Column "${column.physicalName}" omits metadata required to reconstruct its write behavior`, path));
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
const name = resolveName({
|
|
837
|
+
kind: "column",
|
|
838
|
+
physicalName: column.physicalName,
|
|
839
|
+
suggestedName: camelCase(column.physicalName),
|
|
840
|
+
tablePhysicalName: snapshot.physicalName,
|
|
841
|
+
tableName
|
|
842
|
+
}, options, path, false, diagnostics);
|
|
843
|
+
if (name === void 0) continue;
|
|
844
|
+
addUniqueName(name, path, columnNames, diagnostics);
|
|
845
|
+
const suggestedSqlDomain = inferSqlDomain(catalog.dialect, catalogColumn);
|
|
846
|
+
const mapping = resolveColumnMapping(catalog, catalogTable.physicalName, tableName, catalogColumn, name, suggestedSqlDomain, options, path, diagnostics);
|
|
847
|
+
if (mapping === void 0) continue;
|
|
848
|
+
columns.push({
|
|
849
|
+
name,
|
|
850
|
+
snapshot: column,
|
|
851
|
+
catalog: catalogColumn,
|
|
852
|
+
output: mapping.output ?? "unknown",
|
|
853
|
+
insert: mapping.insert ?? "unknown",
|
|
854
|
+
update: mapping.update ?? "unknown",
|
|
855
|
+
sqlDomain: mapping.sqlDomain ?? suggestedSqlDomain,
|
|
856
|
+
explicitSqlDomain: mapping.sqlDomain !== void 0
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
const constraints = resolveObjects(snapshot.constraints, "constraint", snapshot, tableName, options, diagnostics);
|
|
860
|
+
const indexes = resolveObjects(snapshot.indexes, "index", snapshot, tableName, options, diagnostics);
|
|
861
|
+
if (hasErrors(diagnostics)) return void 0;
|
|
862
|
+
const orderedColumns = columns.sort(compareName);
|
|
863
|
+
return {
|
|
864
|
+
name: tableName,
|
|
865
|
+
snapshot,
|
|
866
|
+
catalog: catalogTable,
|
|
867
|
+
columns: orderedColumns,
|
|
868
|
+
columnsBySnapshotId: new Map(orderedColumns.map((column) => [column.snapshot.id, column])),
|
|
869
|
+
constraints,
|
|
870
|
+
indexes
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function resolveObjects(objects, kind, table, tableName, options, diagnostics) {
|
|
874
|
+
const names = /* @__PURE__ */ new Map();
|
|
875
|
+
const result = [];
|
|
876
|
+
for (const object of [...objects].sort(comparePhysicalName)) {
|
|
877
|
+
const path = [
|
|
878
|
+
"snapshot",
|
|
879
|
+
"tables",
|
|
880
|
+
table.id,
|
|
881
|
+
kind === "constraint" ? "constraints" : "indexes",
|
|
882
|
+
object.id
|
|
883
|
+
];
|
|
884
|
+
const name = resolveName({
|
|
885
|
+
kind,
|
|
886
|
+
physicalName: object.physicalName,
|
|
887
|
+
suggestedName: camelCase(object.physicalName),
|
|
888
|
+
tablePhysicalName: table.physicalName,
|
|
889
|
+
tableName
|
|
890
|
+
}, options, path, false, diagnostics);
|
|
891
|
+
if (name === void 0) continue;
|
|
892
|
+
addUniqueName(name, path, names, diagnostics);
|
|
893
|
+
result.push({
|
|
894
|
+
name,
|
|
895
|
+
snapshot: object
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
return result.sort(compareName);
|
|
899
|
+
}
|
|
900
|
+
function resolveColumnMapping(catalog, tablePhysicalName, tableName, column, columnName, suggestedSqlDomain, options, path, diagnostics) {
|
|
901
|
+
if (options.mapColumn === void 0) return {};
|
|
902
|
+
if (typeof options.mapColumn !== "function") {
|
|
903
|
+
diagnostics.push(errorDiagnostic("invalid-option", "mapColumn must be a function", ["options", "mapColumn"]));
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
let mapping;
|
|
907
|
+
try {
|
|
908
|
+
mapping = options.mapColumn({
|
|
909
|
+
dialect: catalog.dialect,
|
|
910
|
+
namespace: catalog.namespace.name,
|
|
911
|
+
tablePhysicalName,
|
|
912
|
+
tableName,
|
|
913
|
+
columnPhysicalName: column.physicalName,
|
|
914
|
+
columnName,
|
|
915
|
+
nativeType: column.storage.nativeType,
|
|
916
|
+
portableType: column.storage.portable?.type,
|
|
917
|
+
classificationConfidence: column.storage.portable?.confidence,
|
|
918
|
+
suggestedSqlDomain
|
|
919
|
+
});
|
|
920
|
+
} catch (caught) {
|
|
921
|
+
diagnostics.push(errorDiagnostic("invalid-option", caught instanceof Error ? `Column mapping failed: ${caught.message}` : "Column mapping failed", [...path, "mapping"]));
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (mapping === void 0) return {};
|
|
925
|
+
if (typeof mapping !== "object" || mapping === null || Array.isArray(mapping)) {
|
|
926
|
+
diagnostics.push(errorDiagnostic("invalid-option", "Column mappings must return an object or undefined", [...path, "mapping"]));
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
for (const axis of [
|
|
930
|
+
"output",
|
|
931
|
+
"insert",
|
|
932
|
+
"update"
|
|
933
|
+
]) {
|
|
934
|
+
const value = mapping[axis];
|
|
935
|
+
if (value !== void 0 && !applicationTypes.has(value)) diagnostics.push(errorDiagnostic("invalid-option", `Unsupported ${axis} application type "${String(value)}"`, [
|
|
936
|
+
...path,
|
|
937
|
+
"mapping",
|
|
938
|
+
axis
|
|
939
|
+
]));
|
|
940
|
+
}
|
|
941
|
+
if (mapping.sqlDomain !== void 0 && !sqlDomains.has(mapping.sqlDomain)) diagnostics.push(errorDiagnostic("invalid-option", `Unsupported SQL domain "${String(mapping.sqlDomain)}"`, [
|
|
942
|
+
...path,
|
|
943
|
+
"mapping",
|
|
944
|
+
"sqlDomain"
|
|
945
|
+
]));
|
|
946
|
+
return hasErrors(diagnostics) ? void 0 : mapping;
|
|
947
|
+
}
|
|
948
|
+
function validateResolvedDomains(schema, diagnostics) {
|
|
949
|
+
for (const table of schema.tables) for (const index of table.indexes) for (const term of index.snapshot.terms) {
|
|
950
|
+
const ordered = term.kind === "order" ? term : void 0;
|
|
951
|
+
if (ordered?.expression.kind !== "column") continue;
|
|
952
|
+
const column = table.columnsBySnapshotId.get(ordered.expression.column);
|
|
953
|
+
if (column === void 0 || orderableDomains.has(column.sqlDomain)) continue;
|
|
954
|
+
if (column.explicitSqlDomain) diagnostics.push(errorDiagnostic("invalid-option", `SQL domain "${column.sqlDomain}" cannot represent an ordered index term`, [
|
|
955
|
+
"snapshot",
|
|
956
|
+
"tables",
|
|
957
|
+
table.snapshot.id,
|
|
958
|
+
"indexes",
|
|
959
|
+
index.snapshot.id,
|
|
960
|
+
"terms"
|
|
961
|
+
]));
|
|
962
|
+
else column.sqlDomain = "unknown";
|
|
963
|
+
}
|
|
964
|
+
for (const table of schema.tables) for (const constraint of table.constraints) {
|
|
965
|
+
if (constraint.snapshot.kind !== "foreign-key") continue;
|
|
966
|
+
const targetTable = schema.tablesBySnapshotId.get(constraint.snapshot.target.table);
|
|
967
|
+
if (targetTable === void 0) continue;
|
|
968
|
+
for (const [index, localId] of constraint.snapshot.columns.entries()) {
|
|
969
|
+
const targetId = constraint.snapshot.target.columns[index];
|
|
970
|
+
const local = table.columnsBySnapshotId.get(localId);
|
|
971
|
+
const target = targetId ? targetTable.columnsBySnapshotId.get(targetId) : void 0;
|
|
972
|
+
if (local === void 0 || target === void 0 || local.sqlDomain === "unknown" || target.sqlDomain === "unknown" || local.sqlDomain === target.sqlDomain) continue;
|
|
973
|
+
if (local.explicitSqlDomain && target.explicitSqlDomain) diagnostics.push(errorDiagnostic("invalid-option", `Mapped foreign-key domains differ: "${local.sqlDomain}" and "${target.sqlDomain}"`, [
|
|
974
|
+
"snapshot",
|
|
975
|
+
"tables",
|
|
976
|
+
table.snapshot.id,
|
|
977
|
+
"constraints",
|
|
978
|
+
constraint.snapshot.id,
|
|
979
|
+
"columns",
|
|
980
|
+
index
|
|
981
|
+
]));
|
|
982
|
+
else if (!local.explicitSqlDomain) local.sqlDomain = "unknown";
|
|
983
|
+
else target.sqlDomain = "unknown";
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function inferSqlDomain(dialect, column) {
|
|
988
|
+
const portable = column.storage.portable;
|
|
989
|
+
if (portable?.confidence === "exact") return portableDomain(portable.type);
|
|
990
|
+
const declaration = column.storage.nativeType.trim().toLowerCase();
|
|
991
|
+
if (dialect === "postgresql") switch (declaration) {
|
|
992
|
+
case "smallint":
|
|
993
|
+
case "integer": return "integer";
|
|
994
|
+
case "bigint": return "bigint";
|
|
995
|
+
case "numeric": return "decimal";
|
|
996
|
+
case "text": return "text";
|
|
997
|
+
case "boolean": return "boolean";
|
|
998
|
+
case "date": return "date";
|
|
999
|
+
case "timestamp without time zone":
|
|
1000
|
+
case "timestamp with time zone": return "timestamp";
|
|
1001
|
+
case "uuid": return "uuid";
|
|
1002
|
+
case "json":
|
|
1003
|
+
case "jsonb": return "json";
|
|
1004
|
+
case "bytea": return "binary";
|
|
1005
|
+
default: return "unknown";
|
|
1006
|
+
}
|
|
1007
|
+
if (dialect === "mysql") switch (declaration) {
|
|
1008
|
+
case "tinyint":
|
|
1009
|
+
case "smallint":
|
|
1010
|
+
case "mediumint":
|
|
1011
|
+
case "int": return "integer";
|
|
1012
|
+
case "bigint": return "bigint";
|
|
1013
|
+
case "decimal": return "decimal";
|
|
1014
|
+
case "text":
|
|
1015
|
+
case "tinytext":
|
|
1016
|
+
case "mediumtext":
|
|
1017
|
+
case "longtext": return "text";
|
|
1018
|
+
case "date": return "date";
|
|
1019
|
+
case "datetime":
|
|
1020
|
+
case "timestamp": return "timestamp";
|
|
1021
|
+
case "json": return "json";
|
|
1022
|
+
case "blob":
|
|
1023
|
+
case "tinyblob":
|
|
1024
|
+
case "mediumblob":
|
|
1025
|
+
case "longblob": return "binary";
|
|
1026
|
+
default: return "unknown";
|
|
1027
|
+
}
|
|
1028
|
+
return "unknown";
|
|
1029
|
+
}
|
|
1030
|
+
function portableDomain(portable) {
|
|
1031
|
+
return portable === "numeric" ? "decimal" : portable;
|
|
1032
|
+
}
|
|
1033
|
+
function resolveName(context, options, path, exportBinding, diagnostics) {
|
|
1034
|
+
let name = context.suggestedName;
|
|
1035
|
+
if (options.naming !== void 0) {
|
|
1036
|
+
if (typeof options.naming !== "function") {
|
|
1037
|
+
diagnostics.push(errorDiagnostic("invalid-option", "naming must be a function", ["options", "naming"]));
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
try {
|
|
1041
|
+
name = options.naming(context) ?? context.suggestedName;
|
|
1042
|
+
} catch (caught) {
|
|
1043
|
+
diagnostics.push(errorDiagnostic("invalid-option", caught instanceof Error ? `Naming callback failed: ${caught.message}` : "Naming callback failed", [...path, "name"]));
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (typeof name !== "string" || !/^[a-z][A-Za-z0-9]*$/u.test(name) || exportBinding && reservedBindings.has(name)) {
|
|
1048
|
+
diagnostics.push(errorDiagnostic("unsafe-name", `Physical ${context.kind} name ${JSON.stringify(context.physicalName)} does not resolve to a safe camelCase${exportBinding ? " export" : ""} identifier`, [...path, "name"], "Return a safe camelCase ID from the naming callback."));
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
return name;
|
|
1052
|
+
}
|
|
1053
|
+
function camelCase(value) {
|
|
1054
|
+
const words = value.replace(/([a-z0-9])([A-Z])/gu, "$1 $2").split(/[^A-Za-z0-9]+/u).filter(Boolean).map((word) => word.toLowerCase());
|
|
1055
|
+
return `${words.shift() ?? ""}${words.map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`).join("")}`;
|
|
1056
|
+
}
|
|
1057
|
+
function hasCompleteColumnBehavior(column) {
|
|
1058
|
+
if ([
|
|
1059
|
+
column.default,
|
|
1060
|
+
column.generatedColumn,
|
|
1061
|
+
column.identity
|
|
1062
|
+
].filter((value) => value !== void 0).length > 1) return false;
|
|
1063
|
+
if (column.generatedColumn !== void 0 || column.identity !== void 0) return column.generated && !column.hasDefault;
|
|
1064
|
+
if (column.default !== void 0) return !column.generated && column.hasDefault;
|
|
1065
|
+
return !column.generated && !column.hasDefault;
|
|
1066
|
+
}
|
|
1067
|
+
function toPhysicalFacts(snapshot) {
|
|
1068
|
+
const tableById = new Map(snapshot.tables.map((table) => [table.id, table]));
|
|
1069
|
+
const columnNames = new Map(snapshot.tables.map((table) => [table.id, new Map(table.columns.map((column) => [column.id, column.physicalName]))]));
|
|
1070
|
+
return {
|
|
1071
|
+
dialect: snapshot.dialect,
|
|
1072
|
+
namespace: snapshot.namespace ?? null,
|
|
1073
|
+
tables: [...snapshot.tables].sort(comparePhysicalName).map((table) => ({
|
|
1074
|
+
physicalName: table.physicalName,
|
|
1075
|
+
columns: [...table.columns].sort(comparePhysicalName).map((column) => ({
|
|
1076
|
+
...column,
|
|
1077
|
+
id: column.physicalName
|
|
1078
|
+
})),
|
|
1079
|
+
constraints: [...table.constraints].sort(comparePhysicalName).map((constraint) => physicalConstraint(constraint, table, tableById, columnNames)),
|
|
1080
|
+
indexes: [...table.indexes].sort(comparePhysicalName).map((index) => physicalIndex(index, table, columnNames))
|
|
1081
|
+
}))
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function physicalConstraint(constraint, table, tables, columns) {
|
|
1085
|
+
if (constraint.kind === "check") return {
|
|
1086
|
+
...constraint,
|
|
1087
|
+
id: constraint.physicalName
|
|
1088
|
+
};
|
|
1089
|
+
const localColumns = constraint.columns.map((id) => columns.get(table.id)?.get(id) ?? `missing:${id}`);
|
|
1090
|
+
if (constraint.kind !== "foreign-key") return {
|
|
1091
|
+
...constraint,
|
|
1092
|
+
id: constraint.physicalName,
|
|
1093
|
+
columns: localColumns
|
|
1094
|
+
};
|
|
1095
|
+
const targetTable = tables.get(constraint.target.table);
|
|
1096
|
+
return {
|
|
1097
|
+
...constraint,
|
|
1098
|
+
id: constraint.physicalName,
|
|
1099
|
+
columns: localColumns,
|
|
1100
|
+
target: {
|
|
1101
|
+
table: targetTable?.physicalName ?? `missing:${constraint.target.table}`,
|
|
1102
|
+
columns: constraint.target.columns.map((id) => columns.get(constraint.target.table)?.get(id) ?? `missing:${id}`)
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
function physicalIndex(index, table, columns) {
|
|
1107
|
+
const names = columns.get(table.id);
|
|
1108
|
+
return {
|
|
1109
|
+
...index,
|
|
1110
|
+
id: index.physicalName,
|
|
1111
|
+
terms: index.terms.map((term) => physicalIndexTerm(term, names)),
|
|
1112
|
+
...index.includedColumns === void 0 ? {} : { includedColumns: index.includedColumns.map((id) => names?.get(id) ?? `missing:${id}`) }
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
function physicalIndexTerm(term, columns) {
|
|
1116
|
+
if (term.kind === "column") return {
|
|
1117
|
+
...term,
|
|
1118
|
+
column: columns?.get(term.column) ?? `missing:${term.column}`
|
|
1119
|
+
};
|
|
1120
|
+
if (term.kind === "expression") return term;
|
|
1121
|
+
return {
|
|
1122
|
+
...term,
|
|
1123
|
+
expression: term.expression.kind === "column" ? {
|
|
1124
|
+
...term.expression,
|
|
1125
|
+
column: columns?.get(term.expression.column) ?? `missing:${term.expression.column}`
|
|
1126
|
+
} : term.expression
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function appendExcludedFamilyDiagnostics(catalog, diagnostics) {
|
|
1130
|
+
if (catalog === void 0) return;
|
|
1131
|
+
for (const [property, label] of excludedFamilies) {
|
|
1132
|
+
const values = catalog[property];
|
|
1133
|
+
if (values === void 0 || values.length === 0) continue;
|
|
1134
|
+
diagnostics.push({
|
|
1135
|
+
severity: "warning",
|
|
1136
|
+
code: "excluded-object-family",
|
|
1137
|
+
message: `Snapshot v1 source generation excludes ${values.length} ${label}`,
|
|
1138
|
+
path: Object.freeze(["catalog", property]),
|
|
1139
|
+
remediation: "Use the complete normalized catalog or Snapshot v2 for this object family."
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
function groupUnique(values, keyOf, path, diagnostics) {
|
|
1144
|
+
const result = /* @__PURE__ */ new Map();
|
|
1145
|
+
for (const [index, value] of values.entries()) {
|
|
1146
|
+
const key = keyOf(value);
|
|
1147
|
+
if (result.has(key)) diagnostics.push(errorDiagnostic("name-collision", `Physical name ${JSON.stringify(key)} occurs more than once`, [...path, index]));
|
|
1148
|
+
else result.set(key, value);
|
|
1149
|
+
}
|
|
1150
|
+
return result;
|
|
1151
|
+
}
|
|
1152
|
+
function addUniqueName(name, path, names, diagnostics) {
|
|
1153
|
+
const existing = names.get(name);
|
|
1154
|
+
if (existing === void 0) {
|
|
1155
|
+
names.set(name, path);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
diagnostics.push(errorDiagnostic("name-collision", `More than one physical object resolves to logical ID "${name}"`, [...path, "name"], "Override one logical ID with the naming callback.", [existing]));
|
|
1159
|
+
}
|
|
1160
|
+
function errorDiagnostic(code, message, path, remediation, relatedPaths) {
|
|
1161
|
+
return {
|
|
1162
|
+
severity: "error",
|
|
1163
|
+
code,
|
|
1164
|
+
message,
|
|
1165
|
+
path,
|
|
1166
|
+
...remediation === void 0 ? {} : { remediation },
|
|
1167
|
+
...relatedPaths === void 0 ? {} : { relatedPaths }
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function failure(diagnostics) {
|
|
1171
|
+
return Object.freeze({
|
|
1172
|
+
ok: false,
|
|
1173
|
+
diagnostics: freezeDiagnostics(diagnostics)
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
function freezeDiagnostics(diagnostics) {
|
|
1177
|
+
return Object.freeze(diagnostics.map((diagnostic) => Object.freeze({
|
|
1178
|
+
...diagnostic,
|
|
1179
|
+
path: Object.freeze([...diagnostic.path]),
|
|
1180
|
+
relatedPaths: diagnostic.relatedPaths === void 0 ? void 0 : Object.freeze(diagnostic.relatedPaths.map((path) => Object.freeze([...path])))
|
|
1181
|
+
})));
|
|
1182
|
+
}
|
|
1183
|
+
function hasErrors(diagnostics) {
|
|
1184
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
1185
|
+
}
|
|
1186
|
+
function comparePhysicalName(left, right) {
|
|
1187
|
+
return left.physicalName < right.physicalName ? -1 : left.physicalName > right.physicalName ? 1 : 0;
|
|
1188
|
+
}
|
|
1189
|
+
function compareName(left, right) {
|
|
1190
|
+
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
1191
|
+
}
|
|
1192
|
+
function isOptions(value) {
|
|
1193
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1194
|
+
}
|
|
1195
|
+
function isIntrospectionResult(value) {
|
|
1196
|
+
return typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean" && "lossy" in value && typeof value.lossy === "boolean" && "diagnostics" in value && Array.isArray(value.diagnostics);
|
|
1197
|
+
}
|
|
1198
|
+
//#endregion
|
|
1199
|
+
export { generateSchemaSource };
|