@typed-sql/compiler 1.0.0-rc.0 → 2.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +36 -38
  3. package/dist/packages/compiler/src/compatibility.d.ts +66 -0
  4. package/dist/packages/compiler/src/compatibility.d.ts.map +1 -0
  5. package/dist/packages/compiler/src/compatibility.js +786 -0
  6. package/dist/packages/compiler/src/compatibility.js.map +1 -0
  7. package/dist/packages/compiler/src/compiler.d.ts +16 -1
  8. package/dist/packages/compiler/src/compiler.d.ts.map +1 -1
  9. package/dist/packages/compiler/src/compiler.js +67 -4
  10. package/dist/packages/compiler/src/compiler.js.map +1 -1
  11. package/dist/packages/compiler/src/index.d.ts +13 -3
  12. package/dist/packages/compiler/src/index.d.ts.map +1 -1
  13. package/dist/packages/compiler/src/index.js +6 -1
  14. package/dist/packages/compiler/src/index.js.map +1 -1
  15. package/dist/packages/compiler/src/manifest.d.ts +505 -0
  16. package/dist/packages/compiler/src/manifest.d.ts.map +1 -0
  17. package/dist/packages/compiler/src/manifest.js +628 -0
  18. package/dist/packages/compiler/src/manifest.js.map +1 -0
  19. package/dist/packages/compiler/src/plans.d.ts +88 -0
  20. package/dist/packages/compiler/src/plans.d.ts.map +1 -0
  21. package/dist/packages/compiler/src/plans.js +459 -0
  22. package/dist/packages/compiler/src/plans.js.map +1 -0
  23. package/dist/packages/compiler/src/project.d.ts +8 -0
  24. package/dist/packages/compiler/src/project.d.ts.map +1 -0
  25. package/dist/packages/compiler/src/project.js +62 -0
  26. package/dist/packages/compiler/src/project.js.map +1 -0
  27. package/dist/packages/compiler/src/scanner.d.ts +6 -0
  28. package/dist/packages/compiler/src/scanner.d.ts.map +1 -1
  29. package/dist/packages/compiler/src/scanner.js +46 -0
  30. package/dist/packages/compiler/src/scanner.js.map +1 -1
  31. package/dist/packages/compiler/src/verification.d.ts +106 -0
  32. package/dist/packages/compiler/src/verification.d.ts.map +1 -0
  33. package/dist/packages/compiler/src/verification.js +516 -0
  34. package/dist/packages/compiler/src/verification.js.map +1 -0
  35. package/package.json +4 -3
@@ -0,0 +1,786 @@
1
+ import { createHash } from "node:crypto";
2
+ import { calculateSchemaHash } from "@typed-sql/schema";
3
+ import { parseQueryManifest, serializeQueryManifest } from "./manifest.js";
4
+ export const SCHEMA_COMPATIBILITY_FORMAT_VERSION = 1;
5
+ export const SCHEMA_COMPATIBILITY_ANALYZER_VERSION = "typed-sql-v1";
6
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
7
+ const compareText = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
8
+ const relationKey = (key) => `relation:${key}`;
9
+ const relationWriteKey = (key) => `relation-write:${key}`;
10
+ const columnKey = (table, column) => `column:${table}.${column}`;
11
+ const functionKey = (key) => `function:${key}`;
12
+ const functionNameKey = (schema, name) => `function-name:${schema ?? ""}.${name}`;
13
+ const typeKey = (kind, key) => `type:${kind}:${key}`;
14
+ const changeKinds = new Set([
15
+ "relation-added",
16
+ "relation-removed",
17
+ "column-added",
18
+ "column-removed",
19
+ "column-database-type",
20
+ "column-typescript-type",
21
+ "column-nullability",
22
+ "column-array",
23
+ "column-default",
24
+ "enum-added",
25
+ "enum-removed",
26
+ "enum-values",
27
+ "domain-added",
28
+ "domain-removed",
29
+ "domain-database-type",
30
+ "domain-typescript-type",
31
+ "domain-nullability",
32
+ "function-added",
33
+ "function-removed",
34
+ "function-return-type",
35
+ "function-nullability",
36
+ "function-set-returning",
37
+ "function-volatility",
38
+ "dialect-version",
39
+ "server-version",
40
+ "query-contract",
41
+ ]);
42
+ const targetKinds = new Set([
43
+ "schema",
44
+ "relation",
45
+ "column",
46
+ "enum",
47
+ "domain",
48
+ "function",
49
+ "query",
50
+ ]);
51
+ const directions = new Set(["before-app-after-database", "after-app-before-database"]);
52
+ const classifications = new Set([
53
+ "compatible",
54
+ "deployment-order-sensitive",
55
+ "source-breaking",
56
+ "runtime-breaking",
57
+ "unknown",
58
+ ]);
59
+ const severities = new Set(["info", "warning", "error"]);
60
+ function record(value) {
61
+ return typeof value === "object" && value !== null && !Array.isArray(value);
62
+ }
63
+ function assertNonEmptyString(value, description) {
64
+ if (typeof value !== "string" || value.length === 0)
65
+ throw new TypeError(`${description} must be a non-empty string`);
66
+ }
67
+ function assertOptionalString(value, description) {
68
+ if (value !== undefined)
69
+ assertNonEmptyString(value, description);
70
+ }
71
+ function assertHash(value, description) {
72
+ if (typeof value !== "string" || !/^sha256:[a-f\d]{64}$/u.test(value)) {
73
+ throw new TypeError(`${description} must be a sha256 fingerprint`);
74
+ }
75
+ }
76
+ function assertDigest(value, description) {
77
+ if (typeof value !== "string" || !/^[a-f\d]{64}$/u.test(value)) {
78
+ throw new TypeError(`${description} must be a sha256 digest`);
79
+ }
80
+ }
81
+ function assertRange(value, description) {
82
+ if (!record(value) ||
83
+ !Number.isSafeInteger(value.start) ||
84
+ value.start < 0 ||
85
+ !Number.isSafeInteger(value.end) ||
86
+ value.end < value.start ||
87
+ !Number.isSafeInteger(value.line) ||
88
+ value.line < 1 ||
89
+ !Number.isSafeInteger(value.column) ||
90
+ value.column < 1) {
91
+ throw new TypeError(`${description} must contain a valid source range`);
92
+ }
93
+ }
94
+ function assertLocation(value, description) {
95
+ if (!record(value))
96
+ throw new TypeError(`${description} must be an object`);
97
+ assertNonEmptyString(value.file, `${description}.file`);
98
+ if (/^(?:\/|\\|[A-Za-z]:[\\/])/u.test(value.file) || value.file.includes("\0")) {
99
+ throw new TypeError(`${description}.file must be a portable relative path`);
100
+ }
101
+ assertRange(value.range, `${description}.range`);
102
+ }
103
+ function assertEvidence(value, description) {
104
+ if (!record(value))
105
+ throw new TypeError(`${description} must be an object`);
106
+ for (const item of Object.values(value)) {
107
+ if (item !== null &&
108
+ typeof item !== "string" &&
109
+ typeof item !== "boolean" &&
110
+ !(Array.isArray(item) && item.every((entry) => typeof entry === "string"))) {
111
+ throw new TypeError(`${description} contains an unsupported evidence value`);
112
+ }
113
+ }
114
+ }
115
+ function assertQueryReference(value, description) {
116
+ if (!record(value))
117
+ throw new TypeError(`${description} must be an object`);
118
+ assertHash(value.queryId, `${description}.queryId`);
119
+ assertHash(value.variantFingerprint, `${description}.variantFingerprint`);
120
+ assertLocation(value.source, `${description}.source`);
121
+ if (value.dependencyRange !== undefined)
122
+ assertRange(value.dependencyRange, `${description}.dependencyRange`);
123
+ }
124
+ function canonicalize(value) {
125
+ if (Array.isArray(value))
126
+ return value.map(canonicalize);
127
+ if (typeof value !== "object" || value === null)
128
+ return value;
129
+ return Object.fromEntries(Object.entries(value)
130
+ .sort(([left], [right]) => compareText(left, right))
131
+ .map(([key, item]) => [key, canonicalize(item)]));
132
+ }
133
+ function targetIdentity(target) {
134
+ return `${target.kind}\0${target.key}\0${target.schema ?? ""}\0${target.parent ?? ""}\0${target.name}`;
135
+ }
136
+ function change(kind, target, before, after) {
137
+ const identity = JSON.stringify(canonicalize({ kind, target, before, after }));
138
+ return {
139
+ id: `sha256:${sha256(identity)}`,
140
+ kind,
141
+ target,
142
+ ...(before === undefined ? {} : { before }),
143
+ ...(after === undefined ? {} : { after }),
144
+ };
145
+ }
146
+ function relationTarget(key, table) {
147
+ return {
148
+ kind: "relation",
149
+ key,
150
+ name: table.name,
151
+ ...(table.schema === undefined ? {} : { schema: table.schema }),
152
+ };
153
+ }
154
+ function columnTarget(tableKey, table, name) {
155
+ return {
156
+ kind: "column",
157
+ key: `${tableKey}.${name}`,
158
+ name,
159
+ parent: tableKey,
160
+ ...(table.schema === undefined ? {} : { schema: table.schema }),
161
+ };
162
+ }
163
+ function typeTarget(kind, key, name = key.split(".").at(-1) ?? key) {
164
+ const dot = key.lastIndexOf(".");
165
+ return {
166
+ kind,
167
+ key,
168
+ name,
169
+ ...(dot === -1 ? {} : { schema: key.slice(0, dot) }),
170
+ };
171
+ }
172
+ function functionTarget(key, value) {
173
+ return {
174
+ kind: "function",
175
+ key,
176
+ name: value.name,
177
+ ...(value.schema === undefined ? {} : { schema: value.schema }),
178
+ };
179
+ }
180
+ function defaultEvidence(value) {
181
+ return value === undefined ? { present: false, fingerprint: null } : { present: true, fingerprint: sha256(value) };
182
+ }
183
+ function enumChanges(before, after) {
184
+ const output = [];
185
+ const keys = [...new Set([...Object.keys(before.enums ?? {}), ...Object.keys(after.enums ?? {})])].sort(compareText);
186
+ for (const key of keys) {
187
+ const left = before.enums?.[key];
188
+ const right = after.enums?.[key];
189
+ const target = typeTarget("enum", key);
190
+ if (left === undefined && right !== undefined) {
191
+ const item = change("enum-added", target, undefined, { values: right });
192
+ output.push({ change: item, keys: [typeKey("enum", key)] });
193
+ }
194
+ else if (left !== undefined && right === undefined) {
195
+ const item = change("enum-removed", target, { values: left }, undefined);
196
+ output.push({ change: item, keys: [typeKey("enum", key)] });
197
+ }
198
+ else if (left !== undefined && right !== undefined && JSON.stringify(left) !== JSON.stringify(right)) {
199
+ const item = change("enum-values", target, { values: left }, { values: right });
200
+ output.push({ change: item, keys: [typeKey("enum", key)] });
201
+ }
202
+ }
203
+ return output;
204
+ }
205
+ function domainChanges(before, after) {
206
+ const output = [];
207
+ const keys = [...new Set([...Object.keys(before.domains ?? {}), ...Object.keys(after.domains ?? {})])].sort(compareText);
208
+ for (const key of keys) {
209
+ const left = before.domains?.[key];
210
+ const right = after.domains?.[key];
211
+ const target = typeTarget("domain", key, left?.name ?? right?.name);
212
+ if (left === undefined && right !== undefined) {
213
+ const item = change("domain-added", target, undefined, {
214
+ databaseType: right.databaseType,
215
+ tsType: right.tsType,
216
+ nullable: right.nullable,
217
+ });
218
+ output.push({ change: item, keys: [typeKey("domain", key)] });
219
+ continue;
220
+ }
221
+ if (left !== undefined && right === undefined) {
222
+ const item = change("domain-removed", target, { databaseType: left.databaseType, tsType: left.tsType, nullable: left.nullable }, undefined);
223
+ output.push({ change: item, keys: [typeKey("domain", key)] });
224
+ continue;
225
+ }
226
+ if (left === undefined || right === undefined)
227
+ continue;
228
+ for (const [kind, property] of [
229
+ ["domain-database-type", "databaseType"],
230
+ ["domain-typescript-type", "tsType"],
231
+ ["domain-nullability", "nullable"],
232
+ ]) {
233
+ if (left[property] === right[property])
234
+ continue;
235
+ const item = change(kind, target, { [property]: left[property] }, { [property]: right[property] });
236
+ output.push({ change: item, keys: [typeKey("domain", key)] });
237
+ }
238
+ }
239
+ return output;
240
+ }
241
+ function functionEvidence(value) {
242
+ return {
243
+ arguments: value.argumentTypes,
244
+ databaseReturnType: value.databaseReturnType ?? null,
245
+ returnType: value.returnType,
246
+ nullable: value.nullable,
247
+ setReturning: value.setReturning ?? false,
248
+ volatility: value.volatility ?? null,
249
+ };
250
+ }
251
+ function functionChanges(before, after) {
252
+ const output = [];
253
+ const keys = [...new Set([...Object.keys(before.functions ?? {}), ...Object.keys(after.functions ?? {})])].sort(compareText);
254
+ for (const key of keys) {
255
+ const left = before.functions?.[key];
256
+ const right = after.functions?.[key];
257
+ const value = left ?? right;
258
+ if (value === undefined)
259
+ continue;
260
+ const target = functionTarget(key, value);
261
+ const impactKeys = [functionKey(key), functionNameKey(value.schema, value.name)];
262
+ if (left === undefined && right !== undefined) {
263
+ output.push({ change: change("function-added", target, undefined, functionEvidence(right)), keys: impactKeys });
264
+ continue;
265
+ }
266
+ if (left !== undefined && right === undefined) {
267
+ output.push({ change: change("function-removed", target, functionEvidence(left), undefined), keys: impactKeys });
268
+ continue;
269
+ }
270
+ if (left === undefined || right === undefined)
271
+ continue;
272
+ for (const [kind, property, evidenceName] of [
273
+ ["function-return-type", "databaseReturnType", "databaseReturnType"],
274
+ ["function-return-type", "returnType", "returnType"],
275
+ ["function-nullability", "nullable", "nullable"],
276
+ ["function-set-returning", "setReturning", "setReturning"],
277
+ ["function-volatility", "volatility", "volatility"],
278
+ ]) {
279
+ const leftValue = left[property] ?? (property === "setReturning" ? false : null);
280
+ const rightValue = right[property] ?? (property === "setReturning" ? false : null);
281
+ if (leftValue === rightValue)
282
+ continue;
283
+ const item = change(kind, target, { [evidenceName]: leftValue }, { [evidenceName]: rightValue });
284
+ output.push({ change: item, keys: impactKeys });
285
+ }
286
+ }
287
+ return output;
288
+ }
289
+ function schemaChanges(before, after) {
290
+ const output = [
291
+ ...enumChanges(before, after),
292
+ ...domainChanges(before, after),
293
+ ...functionChanges(before, after),
294
+ ];
295
+ const tableKeys = [...new Set([...Object.keys(before.tables), ...Object.keys(after.tables)])].sort(compareText);
296
+ for (const tableKey of tableKeys) {
297
+ const leftTable = before.tables[tableKey];
298
+ const rightTable = after.tables[tableKey];
299
+ const table = leftTable ?? rightTable;
300
+ if (table === undefined)
301
+ continue;
302
+ if (leftTable === undefined && rightTable !== undefined) {
303
+ output.push({
304
+ change: change("relation-added", relationTarget(tableKey, rightTable), undefined, {}),
305
+ keys: [relationKey(tableKey)],
306
+ });
307
+ continue;
308
+ }
309
+ if (leftTable !== undefined && rightTable === undefined) {
310
+ output.push({
311
+ change: change("relation-removed", relationTarget(tableKey, leftTable), {}, undefined),
312
+ keys: [relationKey(tableKey)],
313
+ });
314
+ continue;
315
+ }
316
+ if (leftTable === undefined || rightTable === undefined)
317
+ continue;
318
+ const columnNames = [...new Set([...Object.keys(leftTable.columns), ...Object.keys(rightTable.columns)])].sort(compareText);
319
+ for (const name of columnNames) {
320
+ const left = leftTable.columns[name];
321
+ const right = rightTable.columns[name];
322
+ const target = columnTarget(tableKey, table, name);
323
+ if (left === undefined && right !== undefined) {
324
+ output.push({
325
+ change: change("column-added", target, undefined, {
326
+ databaseType: right.databaseType,
327
+ tsType: right.tsType,
328
+ nullable: right.nullable,
329
+ ...defaultEvidence(right.defaultExpression),
330
+ }),
331
+ keys: [columnKey(tableKey, name)],
332
+ relationWriteKey: relationWriteKey(tableKey),
333
+ addedMandatoryColumn: !right.nullable && right.defaultExpression === undefined,
334
+ });
335
+ continue;
336
+ }
337
+ if (left !== undefined && right === undefined) {
338
+ output.push({
339
+ change: change("column-removed", target, {
340
+ databaseType: left.databaseType,
341
+ tsType: left.tsType,
342
+ nullable: left.nullable,
343
+ ...defaultEvidence(left.defaultExpression),
344
+ }, undefined),
345
+ keys: [columnKey(tableKey, name)],
346
+ relationWriteKey: relationWriteKey(tableKey),
347
+ removedMandatoryColumn: !left.nullable && left.defaultExpression === undefined,
348
+ });
349
+ continue;
350
+ }
351
+ if (left === undefined || right === undefined)
352
+ continue;
353
+ for (const [kind, property] of [
354
+ ["column-database-type", "databaseType"],
355
+ ["column-typescript-type", "tsType"],
356
+ ["column-nullability", "nullable"],
357
+ ]) {
358
+ if (left[property] === right[property])
359
+ continue;
360
+ output.push({
361
+ change: change(kind, target, { [property]: left[property] }, { [property]: right[property] }),
362
+ keys: [columnKey(tableKey, name)],
363
+ relationWriteKey: relationWriteKey(tableKey),
364
+ });
365
+ }
366
+ if ((left.array ?? false) !== (right.array ?? false)) {
367
+ output.push({
368
+ change: change("column-array", target, { array: left.array ?? false }, { array: right.array ?? false }),
369
+ keys: [columnKey(tableKey, name)],
370
+ });
371
+ }
372
+ if (left.defaultExpression !== right.defaultExpression) {
373
+ output.push({
374
+ change: change("column-default", target, defaultEvidence(left.defaultExpression), defaultEvidence(right.defaultExpression)),
375
+ keys: [columnKey(tableKey, name)],
376
+ relationWriteKey: relationWriteKey(tableKey),
377
+ });
378
+ }
379
+ }
380
+ }
381
+ if (before.version !== after.version) {
382
+ output.push({
383
+ change: change("server-version", { kind: "schema", key: before.dialect, name: before.dialect }, { version: before.version ?? null }, { version: after.version ?? null }),
384
+ keys: ["all-queries"],
385
+ });
386
+ }
387
+ if (before.dialectVersion !== after.dialectVersion) {
388
+ output.push({
389
+ change: change("dialect-version", { kind: "schema", key: before.dialect, name: before.dialect }, { version: before.dialectVersion ?? null }, { version: after.dialectVersion ?? null }),
390
+ keys: ["all-queries"],
391
+ });
392
+ }
393
+ return output.sort((left, right) => compareText(targetIdentity(left.change.target), targetIdentity(right.change.target)) ||
394
+ compareText(left.change.kind, right.change.kind));
395
+ }
396
+ function matchingTables(snapshot, name, schema) {
397
+ return Object.entries(snapshot.tables).filter(([key, table]) => (key === name || table.name === name || key.endsWith(`.${name}`)) &&
398
+ (schema === undefined || table.schema === schema));
399
+ }
400
+ function matchingColumns(tables, name) {
401
+ return tables.flatMap(([tableKey, table]) => Object.entries(table.columns)
402
+ .filter(([columnKey, column]) => columnKey === name || column.name === name)
403
+ .map(([columnKey]) => [tableKey, columnKey]));
404
+ }
405
+ function matchingTypes(values, name, schema) {
406
+ const matches = Object.keys(values).filter((key) => key === name || key.endsWith(`.${name}`));
407
+ if (schema === undefined)
408
+ return matches;
409
+ const qualified = matches.filter((key) => key === `${schema}.${name}`);
410
+ return qualified.length === 0 ? matches.filter((key) => !key.includes(".")) : qualified;
411
+ }
412
+ function addReference(map, key, reference) {
413
+ map.set(key, [...(map.get(key) ?? []), reference]);
414
+ }
415
+ function reference(entryId, variant, source, dependency) {
416
+ return {
417
+ queryId: entryId,
418
+ variantFingerprint: variant.fingerprint,
419
+ source,
420
+ ...(dependency === undefined ? {} : { dependencyRange: dependency.range }),
421
+ };
422
+ }
423
+ function manifestReferences(manifest, snapshot) {
424
+ const byKey = new Map();
425
+ const unknown = [];
426
+ for (const entry of manifest.queries) {
427
+ if (entry.status === "unresolved") {
428
+ unknown.push({ queryId: entry.id, variantFingerprint: entry.id, source: entry.source });
429
+ continue;
430
+ }
431
+ for (const variant of entry.variants) {
432
+ if (variant.semantics.operation.value === "unknown" ||
433
+ variant.semantics.cardinality.maximum === "unknown" ||
434
+ variant.semantics.volatility.value === "unknown" ||
435
+ variant.semantics.locking.value === "unknown" ||
436
+ variant.semantics.connectionAffinity.value === "unknown") {
437
+ unknown.push(reference(entry.id, variant, entry.source));
438
+ }
439
+ const relationMatches = new Map();
440
+ for (const dependency of variant.semantics.dependencies.filter((item) => item.kind === "relation")) {
441
+ const matches = matchingTables(snapshot, dependency.name, dependency.schema);
442
+ relationMatches.set(dependency, matches);
443
+ const item = reference(entry.id, variant, entry.source, dependency);
444
+ if (matches.length === 1) {
445
+ addReference(byKey, relationKey(matches[0][0]), item);
446
+ if (dependency.access === "write")
447
+ addReference(byKey, relationWriteKey(matches[0][0]), item);
448
+ }
449
+ else
450
+ unknown.push(item);
451
+ }
452
+ for (const dependency of variant.semantics.dependencies.filter((item) => item.kind !== "relation")) {
453
+ const item = reference(entry.id, variant, entry.source, dependency);
454
+ if (dependency.kind === "column") {
455
+ let matches = dependency.parent === undefined ? [] : matchingTables(snapshot, dependency.parent);
456
+ if (matches.length === 0) {
457
+ matches = [...new Map([...relationMatches.values()].flat().map((value) => [value[0], value])).values()];
458
+ }
459
+ const columns = matchingColumns(matches, dependency.name);
460
+ if (columns.length === 1) {
461
+ addReference(byKey, columnKey(columns[0][0], columns[0][1]), item);
462
+ }
463
+ else
464
+ unknown.push(item);
465
+ }
466
+ else if (dependency.kind === "function") {
467
+ const matches = Object.entries(snapshot.functions ?? {}).filter(([, fn]) => fn.name === dependency.name && (dependency.schema === undefined || fn.schema === dependency.schema));
468
+ if (matches.length === 1)
469
+ addReference(byKey, functionKey(matches[0][0]), item);
470
+ if (matches.length > 0)
471
+ addReference(byKey, functionNameKey(dependency.schema ?? matches[0]?.[1].schema, dependency.name), item);
472
+ else if (dependency.certainty === "resolved")
473
+ unknown.push(item);
474
+ }
475
+ else if (dependency.kind === "type") {
476
+ const enums = matchingTypes(snapshot.enums ?? {}, dependency.name, dependency.schema);
477
+ const domains = matchingTypes(snapshot.domains ?? {}, dependency.name, dependency.schema);
478
+ if (enums.length + domains.length === 1) {
479
+ if (enums.length === 1)
480
+ addReference(byKey, typeKey("enum", enums[0]), item);
481
+ else
482
+ addReference(byKey, typeKey("domain", domains[0]), item);
483
+ }
484
+ else if (enums.length + domains.length > 1 || dependency.certainty === "resolved")
485
+ unknown.push(item);
486
+ }
487
+ else if (dependency.kind === "unknown" || dependency.kind === "sequence")
488
+ unknown.push(item);
489
+ }
490
+ addReference(byKey, "all-queries", reference(entry.id, variant, entry.source));
491
+ }
492
+ }
493
+ return { byKey, unknown: uniqueReferences(unknown) };
494
+ }
495
+ function uniqueReferences(values) {
496
+ const unique = new Map();
497
+ for (const value of values) {
498
+ const key = `${value.queryId}\0${value.variantFingerprint}\0${value.dependencyRange?.start ?? -1}`;
499
+ unique.set(key, value);
500
+ }
501
+ return [...unique.values()].sort((left, right) => compareText(left.source.file, right.source.file) ||
502
+ left.source.range.start - right.source.range.start ||
503
+ compareText(left.variantFingerprint, right.variantFingerprint) ||
504
+ (left.dependencyRange?.start ?? -1) - (right.dependencyRange?.start ?? -1));
505
+ }
506
+ function affected(change, references, direction) {
507
+ const values = change.keys.flatMap((key) => references.byKey.get(key) ?? []);
508
+ if (change.relationWriteKey !== undefined &&
509
+ ((change.addedMandatoryColumn === true && direction === "before-app-after-database") ||
510
+ (change.removedMandatoryColumn === true && direction === "after-app-before-database") ||
511
+ change.change.kind === "column-default" ||
512
+ change.change.kind === "column-nullability")) {
513
+ values.push(...(references.byKey.get(change.relationWriteKey) ?? []));
514
+ }
515
+ return uniqueReferences(values);
516
+ }
517
+ function risk(item, direction, queries) {
518
+ const kind = item.change.kind;
519
+ if (queries.length === 0)
520
+ return {
521
+ classification: "compatible",
522
+ severity: "info",
523
+ reason: "No compiled query in this application version depends on the changed contract.",
524
+ };
525
+ if (kind === "server-version" || kind === "dialect-version")
526
+ return {
527
+ classification: "unknown",
528
+ severity: "warning",
529
+ reason: "Database-version compatibility requires live verification in a representative environment.",
530
+ };
531
+ if (kind === "column-typescript-type" || kind === "domain-typescript-type" || kind === "query-contract") {
532
+ return {
533
+ classification: "source-breaking",
534
+ severity: "error",
535
+ reason: "The inferred TypeScript query contract changes.",
536
+ };
537
+ }
538
+ if (kind === "function-volatility" || kind === "column-default") {
539
+ return {
540
+ classification: "deployment-order-sensitive",
541
+ severity: "warning",
542
+ reason: "The query can depend on changed execution or default-value semantics during a rolling deployment.",
543
+ };
544
+ }
545
+ if (kind.endsWith("-added")) {
546
+ if (direction === "after-app-before-database")
547
+ return {
548
+ classification: "runtime-breaking",
549
+ severity: "error",
550
+ reason: "The newer application references a contract that the older database does not provide.",
551
+ };
552
+ if (kind === "function-added" || item.addedMandatoryColumn === true)
553
+ return {
554
+ classification: "deployment-order-sensitive",
555
+ severity: "error",
556
+ reason: "The added contract can change overload resolution or make existing writes incomplete.",
557
+ };
558
+ return {
559
+ classification: "compatible",
560
+ severity: "info",
561
+ reason: "The older application does not require the additive contract.",
562
+ };
563
+ }
564
+ if (kind.endsWith("-removed")) {
565
+ if (direction === "before-app-after-database")
566
+ return {
567
+ classification: "runtime-breaking",
568
+ severity: "error",
569
+ reason: "The older application references a contract removed from the newer database.",
570
+ };
571
+ if (kind === "function-removed")
572
+ return {
573
+ classification: "deployment-order-sensitive",
574
+ severity: "warning",
575
+ reason: "An overload present only on the older database can change function resolution for the newer application.",
576
+ };
577
+ if (item.removedMandatoryColumn === true)
578
+ return {
579
+ classification: "deployment-order-sensitive",
580
+ severity: "error",
581
+ reason: "The newer application can omit a value still required by the older database.",
582
+ };
583
+ return {
584
+ classification: "compatible",
585
+ severity: "info",
586
+ reason: "The newer application does not require the removed contract.",
587
+ };
588
+ }
589
+ return {
590
+ classification: "runtime-breaking",
591
+ severity: "error",
592
+ reason: "The database contract used by the query changes incompatibly across deployment versions.",
593
+ };
594
+ }
595
+ function manifestHash(manifest) {
596
+ return `sha256:${sha256(serializeQueryManifest(manifest))}`;
597
+ }
598
+ function validateInputs(options) {
599
+ parseQueryManifest(options.beforeManifest);
600
+ parseQueryManifest(options.afterManifest);
601
+ const dialect = options.before.dialect;
602
+ if (options.after.dialect !== dialect ||
603
+ options.beforeManifest.dialect.id !== dialect ||
604
+ options.afterManifest.dialect.id !== dialect) {
605
+ throw new TypeError("Compatibility snapshots and manifests must use the same dialect");
606
+ }
607
+ const beforeHash = calculateSchemaHash(options.before);
608
+ const afterHash = calculateSchemaHash(options.after);
609
+ if (options.beforeManifest.schemaHash !== beforeHash)
610
+ throw new TypeError("Before manifest is stale for the before snapshot");
611
+ if (options.afterManifest.schemaHash !== afterHash)
612
+ throw new TypeError("After manifest is stale for the after snapshot");
613
+ }
614
+ function queryContractChanges(before, after) {
615
+ const output = [];
616
+ const afterEntries = new Map(after.queries
617
+ .filter((entry) => entry.status === "resolved")
618
+ .map((entry) => [`${entry.source.file}\0${entry.source.range.start}\0${entry.fingerprint}`, entry]));
619
+ for (const entry of before.queries) {
620
+ if (entry.status !== "resolved")
621
+ continue;
622
+ const other = afterEntries.get(`${entry.source.file}\0${entry.source.range.start}\0${entry.fingerprint}`);
623
+ if (other === undefined)
624
+ continue;
625
+ const otherVariants = new Map(other.variants.map((variant) => [variant.fingerprint, variant]));
626
+ for (const variant of entry.variants) {
627
+ const afterVariant = otherVariants.get(variant.fingerprint);
628
+ if (afterVariant === undefined ||
629
+ (variant.rowType === afterVariant.rowType && variant.parameterType === afterVariant.parameterType))
630
+ continue;
631
+ const target = { kind: "query", key: entry.id, name: entry.id };
632
+ const item = change("query-contract", target, { rowType: variant.rowType, parameterType: variant.parameterType }, { rowType: afterVariant.rowType, parameterType: afterVariant.parameterType });
633
+ output.push({
634
+ change: item,
635
+ keys: [`query:${item.id}`],
636
+ queryReferences: {
637
+ "before-app-after-database": reference(entry.id, variant, entry.source),
638
+ "after-app-before-database": reference(other.id, afterVariant, other.source),
639
+ },
640
+ });
641
+ }
642
+ }
643
+ return output;
644
+ }
645
+ export function analyzeSchemaCompatibility(options) {
646
+ validateInputs(options);
647
+ const beforeReferences = manifestReferences(options.beforeManifest, options.before);
648
+ const afterReferences = manifestReferences(options.afterManifest, options.after);
649
+ const changes = [
650
+ ...schemaChanges(options.before, options.after),
651
+ ...queryContractChanges(options.beforeManifest, options.afterManifest),
652
+ ].sort((left, right) => compareText(targetIdentity(left.change.target), targetIdentity(right.change.target)) ||
653
+ compareText(left.change.kind, right.change.kind));
654
+ const assessments = [];
655
+ for (const item of changes) {
656
+ for (const direction of ["before-app-after-database", "after-app-before-database"]) {
657
+ const references = direction === "before-app-after-database" ? beforeReferences : afterReferences;
658
+ const explicit = item.queryReferences?.[direction];
659
+ const queries = explicit === undefined ? affected(item, references, direction) : [explicit];
660
+ assessments.push({ direction, changeId: item.change.id, ...risk(item, direction, queries), queries });
661
+ }
662
+ }
663
+ for (const [direction, references] of [
664
+ ["before-app-after-database", beforeReferences],
665
+ ["after-app-before-database", afterReferences],
666
+ ]) {
667
+ for (const query of references.unknown) {
668
+ assessments.push({
669
+ direction,
670
+ classification: "unknown",
671
+ severity: "warning",
672
+ reason: "The manifest contains an unresolved query or dependency, so compatibility cannot be proven.",
673
+ queries: [query],
674
+ });
675
+ }
676
+ }
677
+ assessments.sort((left, right) => compareText(left.direction, right.direction) ||
678
+ compareText(left.changeId ?? "", right.changeId ?? "") ||
679
+ compareText(left.queries[0]?.source.file ?? "", right.queries[0]?.source.file ?? "") ||
680
+ (left.queries[0]?.source.range.start ?? -1) - (right.queries[0]?.source.range.start ?? -1));
681
+ const report = {
682
+ formatVersion: SCHEMA_COMPATIBILITY_FORMAT_VERSION,
683
+ analyzerVersion: SCHEMA_COMPATIBILITY_ANALYZER_VERSION,
684
+ dialect: options.before.dialect,
685
+ before: {
686
+ schemaHash: options.beforeManifest.schemaHash,
687
+ manifestHash: manifestHash(options.beforeManifest),
688
+ ...(options.before.version === undefined ? {} : { version: options.before.version }),
689
+ },
690
+ after: {
691
+ schemaHash: options.afterManifest.schemaHash,
692
+ manifestHash: manifestHash(options.afterManifest),
693
+ ...(options.after.version === undefined ? {} : { version: options.after.version }),
694
+ },
695
+ changes: changes.map((item) => item.change),
696
+ assessments,
697
+ summary: {
698
+ info: assessments.filter((item) => item.severity === "info").length,
699
+ warning: assessments.filter((item) => item.severity === "warning").length,
700
+ error: assessments.filter((item) => item.severity === "error").length,
701
+ },
702
+ };
703
+ return report;
704
+ }
705
+ export function serializeSchemaCompatibilityReport(report) {
706
+ return `${JSON.stringify(canonicalize(report), null, 2)}\n`;
707
+ }
708
+ export function parseSchemaCompatibilityReport(value) {
709
+ if (!record(value))
710
+ throw new TypeError("Compatibility report must be an object");
711
+ const report = value;
712
+ if (report.formatVersion !== SCHEMA_COMPATIBILITY_FORMAT_VERSION)
713
+ throw new TypeError(`Unsupported compatibility report format ${String(report.formatVersion)}`);
714
+ if (report.analyzerVersion !== SCHEMA_COMPATIBILITY_ANALYZER_VERSION)
715
+ throw new TypeError(`Unsupported compatibility analyzer ${String(report.analyzerVersion)}`);
716
+ assertNonEmptyString(report.dialect, "Compatibility report dialect");
717
+ for (const [name, artifact] of [
718
+ ["before", report.before],
719
+ ["after", report.after],
720
+ ]) {
721
+ if (!record(artifact))
722
+ throw new TypeError(`Compatibility report ${name} artifact is invalid`);
723
+ assertDigest(artifact.schemaHash, `Compatibility report ${name}.schemaHash`);
724
+ assertHash(artifact.manifestHash, `Compatibility report ${name}.manifestHash`);
725
+ assertOptionalString(artifact.version, `Compatibility report ${name}.version`);
726
+ }
727
+ if (!Array.isArray(report.changes))
728
+ throw new TypeError("Compatibility report changes must be an array");
729
+ const changeIds = new Set();
730
+ for (const [index, item] of report.changes.entries()) {
731
+ const description = `Compatibility report change ${index}`;
732
+ if (!record(item) || !changeKinds.has(item.kind) || !record(item.target)) {
733
+ throw new TypeError(`${description} is invalid`);
734
+ }
735
+ const target = item.target;
736
+ if (!targetKinds.has(target.kind)) {
737
+ throw new TypeError(`${description}.target.kind is invalid`);
738
+ }
739
+ assertNonEmptyString(target.key, `${description}.target.key`);
740
+ assertNonEmptyString(target.name, `${description}.target.name`);
741
+ assertOptionalString(target.schema, `${description}.target.schema`);
742
+ assertOptionalString(target.parent, `${description}.target.parent`);
743
+ if (item.before !== undefined)
744
+ assertEvidence(item.before, `${description}.before`);
745
+ if (item.after !== undefined)
746
+ assertEvidence(item.after, `${description}.after`);
747
+ assertHash(item.id, `${description}.id`);
748
+ const expected = change(item.kind, target, item.before, item.after).id;
749
+ if (item.id !== expected)
750
+ throw new TypeError(`${description}.id does not match its canonical evidence`);
751
+ if (changeIds.has(item.id))
752
+ throw new TypeError(`${description}.id is duplicated`);
753
+ changeIds.add(item.id);
754
+ }
755
+ if (!Array.isArray(report.assessments))
756
+ throw new TypeError("Compatibility report assessments must be an array");
757
+ const counted = { info: 0, warning: 0, error: 0 };
758
+ for (const [index, item] of report.assessments.entries()) {
759
+ const description = `Compatibility report assessment ${index}`;
760
+ if (!record(item) ||
761
+ !directions.has(item.direction) ||
762
+ !classifications.has(item.classification) ||
763
+ !severities.has(item.severity) ||
764
+ !Array.isArray(item.queries)) {
765
+ throw new TypeError(`${description} is invalid`);
766
+ }
767
+ assertNonEmptyString(item.reason, `${description}.reason`);
768
+ if (item.changeId !== undefined) {
769
+ assertHash(item.changeId, `${description}.changeId`);
770
+ if (!changeIds.has(item.changeId))
771
+ throw new TypeError(`${description}.changeId does not identify a report change`);
772
+ }
773
+ for (const [queryIndex, query] of item.queries.entries()) {
774
+ assertQueryReference(query, `${description}.queries[${queryIndex}]`);
775
+ }
776
+ counted[item.severity] += 1;
777
+ }
778
+ if (!record(report.summary))
779
+ throw new TypeError("Compatibility report summary is invalid");
780
+ for (const severity of ["info", "warning", "error"]) {
781
+ if (!Number.isSafeInteger(report.summary[severity]) || report.summary[severity] !== counted[severity])
782
+ throw new TypeError("Compatibility report summary is invalid");
783
+ }
784
+ return value;
785
+ }
786
+ //# sourceMappingURL=compatibility.js.map