@atscript/db 0.1.153 → 0.1.155
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/agg.d.cts +1 -1
- package/dist/agg.d.mts +1 -1
- package/dist/{buckets-jy8BfswO.d.cts → buckets-DcWm3SLt.d.cts} +120 -10
- package/dist/{buckets-CBKL_ZWE.d.mts → buckets-goCoky8v.d.mts} +120 -10
- package/dist/{column-diff-DrE4ho_m.mjs → column-diff-C68V_eYM.mjs} +348 -69
- package/dist/{column-diff-CbdWi_uZ.cjs → column-diff-DMkGYAGM.cjs} +359 -68
- package/dist/{fk-diff-BIJ8l_hF.d.mts → fk-diff-BYLNWoF9.d.mts} +2 -1
- package/dist/{fk-diff-D1mrLMjU.d.cts → fk-diff-DblOPBLS.d.cts} +2 -1
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +10 -5
- package/dist/index.d.mts +10 -5
- package/dist/index.mjs +2 -2
- package/dist/plugin.cjs +3 -3
- package/dist/plugin.mjs +3 -3
- package/dist/rel.d.cts +2 -2
- package/dist/rel.d.mts +2 -2
- package/dist/{relation-helpers-BUsxNNGU.d.mts → relation-helpers-B9MVBrtx.d.mts} +1 -1
- package/dist/{relation-helpers-D_8bydc2.d.cts → relation-helpers-D9kMFfum.d.cts} +1 -1
- package/dist/shared.cjs +2 -1
- package/dist/shared.d.cts +10 -3
- package/dist/shared.d.mts +10 -3
- package/dist/shared.mjs +2 -2
- package/dist/sync.cjs +288 -30
- package/dist/sync.d.cts +34 -5
- package/dist/sync.d.mts +34 -5
- package/dist/sync.mjs +288 -30
- package/dist/{validation-utils-DqZAH6aT.mjs → validation-utils-DxHIqa36.mjs} +23 -5
- package/dist/{validation-utils-debIYpq0.cjs → validation-utils-_PA7iSLm.cjs} +28 -4
- package/dist/{validator-BACewoRj.mjs → validator-BC3nwFi4.mjs} +178 -10
- package/dist/{validator-BxWwh3wf.cjs → validator-DHwSyBej.cjs} +201 -9
- package/dist/validator.cjs +1 -1
- package/dist/validator.mjs +1 -1
- package/package.json +6 -6
package/dist/sync.mjs
CHANGED
|
@@ -1,5 +1,173 @@
|
|
|
1
1
|
import { r as DbError } from "./db-error-eSsQX7rt.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { a as computeTableHash, c as serializeDefaultValue, d as viewSnapshotSources, f as computeForeignKeyDiff, ft as relationalColumnName, h as hasForeignKeyChanges, i as computeSchemaHash, j as resolveDesignType, l as snapshotToExistingColumns, m as fkKey, o as computeTableSnapshot, p as fkColumns, pt as aliasTargetOf, r as canonicalizeQueryNode, s as computeViewSnapshot, t as computeColumnDiff, u as snapshotToExistingTableOptions, yt as NoopLogger } from "./column-diff-C68V_eYM.mjs";
|
|
3
|
+
import { f as unionValueMembers } from "./validator-BC3nwFi4.mjs";
|
|
4
|
+
//#region src/schema/json-column-copy.ts
|
|
5
|
+
const NONE = {
|
|
6
|
+
copies: [],
|
|
7
|
+
jsonify: [],
|
|
8
|
+
errors: []
|
|
9
|
+
};
|
|
10
|
+
/** A text column type of the old layout (`TEXT`, `LONGTEXT`, `VARCHAR(n)`, `character varying`). */
|
|
11
|
+
const TEXT_TYPE = /text|char|clob/i;
|
|
12
|
+
/**
|
|
13
|
+
* Design types a stored snapshot gives a field whose column held a whole
|
|
14
|
+
* value as JSON text: the old union layout (`union`), or `@db.json`.
|
|
15
|
+
*/
|
|
16
|
+
const JSON_TEXT_DESIGN_TYPES = new Set([
|
|
17
|
+
"union",
|
|
18
|
+
"json",
|
|
19
|
+
"object"
|
|
20
|
+
]);
|
|
21
|
+
/** How a target column reads its value out of the JSON, or why it cannot. */
|
|
22
|
+
function targetKind(fd) {
|
|
23
|
+
if (fd.encrypted) return { problem: "it is @db.encrypted" };
|
|
24
|
+
if (fd.isGeoPoint) return { problem: "it is a db.geoPoint" };
|
|
25
|
+
if (fd.storage === "json") return "json";
|
|
26
|
+
return fd.designType === "boolean" ? "boolean" : "text";
|
|
27
|
+
}
|
|
28
|
+
/** Whether `type` is a union one of whose value members is a string. */
|
|
29
|
+
function hasStringMember(type) {
|
|
30
|
+
if (type?.type.kind !== "union") return false;
|
|
31
|
+
return unionValueMembers(type).members.some((m) => resolveDesignType(m) === "string");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Plans {@link TJsonColumnMigration} for one table from its live column diff
|
|
35
|
+
* (and its stored snapshot — on SQLite the old text column and a JSON
|
|
36
|
+
* column have the same type).
|
|
37
|
+
*/
|
|
38
|
+
function planJsonColumnMigration(readable, diff, snapshot) {
|
|
39
|
+
const meta = readable.getMetadata();
|
|
40
|
+
if (meta.nestedObjects) return NONE;
|
|
41
|
+
const adapter = readable.dbAdapter;
|
|
42
|
+
const name = readable.tableName;
|
|
43
|
+
const removed = new Set(diff.removed.map((c) => c.name));
|
|
44
|
+
const snapshotFields = new Map(snapshot?.fields.map((f) => [f.physicalName, f]));
|
|
45
|
+
const out = {
|
|
46
|
+
copies: [],
|
|
47
|
+
jsonify: [],
|
|
48
|
+
errors: []
|
|
49
|
+
};
|
|
50
|
+
if (removed.size > 0) for (const parent of meta.flattenedParents) {
|
|
51
|
+
const source = relationalColumnName(parent, void 0, parent.includes("."));
|
|
52
|
+
if (!removed.has(source)) continue;
|
|
53
|
+
const was = snapshotFields.get(source);
|
|
54
|
+
if (was && !JSON_TEXT_DESIGN_TYPES.has(was.designType)) continue;
|
|
55
|
+
const prefix = `${parent}.`;
|
|
56
|
+
const targets = [];
|
|
57
|
+
const problems = [];
|
|
58
|
+
for (const fd of meta.storedDescriptors) {
|
|
59
|
+
if (!fd.path.startsWith(prefix)) continue;
|
|
60
|
+
const kind = targetKind(fd);
|
|
61
|
+
if (typeof kind === "object") {
|
|
62
|
+
problems.push(`"${fd.physicalName}" (${kind.problem})`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
targets.push({
|
|
66
|
+
column: fd.physicalName,
|
|
67
|
+
path: fd.path.slice(prefix.length).split("."),
|
|
68
|
+
kind,
|
|
69
|
+
field: fd
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (targets.length === 0 && problems.length === 0) continue;
|
|
73
|
+
if (problems.length > 0 || !adapter.copyFromJsonColumn) {
|
|
74
|
+
const why = problems.length > 0 ? `cannot fill ${problems.join(", ")} from it` : "the adapter cannot copy JSON values into columns";
|
|
75
|
+
out.errors.push(`Column "${source}" of ${name} holds "${parent}" as JSON text (the layout of atscript-db ≤ 0.1.154) and ${why}. Annotate "${parent}" with @db.json to keep it in one column, or migrate the data and drop "${source}" manually.`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
out.copies.push({
|
|
79
|
+
source,
|
|
80
|
+
targets
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const typeChanged = new Map(diff.typeChanged.map((tc) => [tc.field.physicalName, tc]));
|
|
84
|
+
for (const fd of meta.storedDescriptors) {
|
|
85
|
+
if (fd.storage !== "json" || !hasStringMember(meta.flatMap.get(fd.path))) continue;
|
|
86
|
+
const tc = typeChanged.get(fd.physicalName);
|
|
87
|
+
const old = snapshotFields.get(fd.physicalName);
|
|
88
|
+
const dotted = `${fd.physicalName}.`;
|
|
89
|
+
if (!(tc !== void 0 && TEXT_TYPE.test(tc.existingType) || old !== void 0 && old.storage !== "json" && old.designType !== "json" || diff.removed.some((c) => c.name.startsWith(dotted)))) continue;
|
|
90
|
+
if (!adapter.jsonifyTextColumn) {
|
|
91
|
+
out.errors.push(`Column "${fd.physicalName}" of ${name} becomes a JSON column but the adapter cannot convert its text values. Migrate them manually.`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
out.jsonify.push(fd.physicalName);
|
|
95
|
+
}
|
|
96
|
+
return out.copies.length > 0 || out.jsonify.length > 0 || out.errors.length > 0 ? out : NONE;
|
|
97
|
+
}
|
|
98
|
+
/** The copy targets {@link applyJsonColumnMigration} adds: those not in the table yet. */
|
|
99
|
+
function copyTargetsToAdd(migration, diff) {
|
|
100
|
+
const targets = new Set(migration.copies.flatMap((c) => c.targets.map((t) => t.column)));
|
|
101
|
+
return diff.added.filter((fd) => targets.has(fd.physicalName));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The column diff the rest of the table's sync applies after
|
|
105
|
+
* {@link applyJsonColumnMigration}: without the copy targets it added, with
|
|
106
|
+
* their NOT NULL and model default as nullability / default changes. Also
|
|
107
|
+
* what `plan({ safe: true })` reports as skipped — safe mode adds and fills
|
|
108
|
+
* the targets but leaves them nullable and without their default.
|
|
109
|
+
*/
|
|
110
|
+
function diffAfterJsonCopy(migration, diff) {
|
|
111
|
+
const toAdd = copyTargetsToAdd(migration, diff);
|
|
112
|
+
if (toAdd.length === 0) return diff;
|
|
113
|
+
return {
|
|
114
|
+
...diff,
|
|
115
|
+
added: diff.added.filter((fd) => !toAdd.includes(fd)),
|
|
116
|
+
nullableChanged: [...diff.nullableChanged, ...toAdd.filter((fd) => !fd.optional).map((fd) => ({
|
|
117
|
+
field: fd,
|
|
118
|
+
wasNullable: true
|
|
119
|
+
}))],
|
|
120
|
+
defaultChanged: [...diff.defaultChanged, ...toAdd.filter((fd) => fd.defaultValue !== void 0).map((fd) => ({
|
|
121
|
+
field: fd,
|
|
122
|
+
newDefault: serializeDefaultValue(fd.defaultValue)
|
|
123
|
+
}))]
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Runs a planned {@link TJsonColumnMigration} — first, before any other
|
|
128
|
+
* column work of the table: text columns become JSON text, the copy targets
|
|
129
|
+
* that do not exist yet are added, and the values are copied. Returns the
|
|
130
|
+
* diff without the columns it added ({@link diffAfterJsonCopy}). Idempotent:
|
|
131
|
+
* a re-run converts nothing twice and copies only into rows whose targets
|
|
132
|
+
* are all NULL.
|
|
133
|
+
*
|
|
134
|
+
* The targets are added nullable and WITHOUT a default — `ADD COLUMN …
|
|
135
|
+
* DEFAULT`, or the type default an adapter gives a required column (a
|
|
136
|
+
* leaf every member of a non-null union of objects declares), fills every
|
|
137
|
+
* existing row, so the copy would skip them all. A model default and NOT
|
|
138
|
+
* NULL are returned as default / nullability changes of the diff, which the
|
|
139
|
+
* rest of the table's sync applies after the copy (`SET DEFAULT` / `SET NOT
|
|
140
|
+
* NULL` / `MODIFY COLUMN`, a table recreation on SQLite). Rows the copy
|
|
141
|
+
* leaves NULL keep NULL in a nullable column; a required one backfills them.
|
|
142
|
+
*/
|
|
143
|
+
async function applyJsonColumnMigration(adapter, migration, diff) {
|
|
144
|
+
for (const column of migration.jsonify) await adapter.jsonifyTextColumn(column);
|
|
145
|
+
if (migration.copies.length === 0) return {
|
|
146
|
+
diff,
|
|
147
|
+
added: []
|
|
148
|
+
};
|
|
149
|
+
const toAdd = copyTargetsToAdd(migration, diff);
|
|
150
|
+
let added = [];
|
|
151
|
+
if (toAdd.length > 0) added = (await adapter.syncColumns({
|
|
152
|
+
added: toAdd.map((fd) => ({
|
|
153
|
+
...fd,
|
|
154
|
+
optional: true,
|
|
155
|
+
defaultValue: void 0
|
|
156
|
+
})),
|
|
157
|
+
removed: [],
|
|
158
|
+
renamed: [],
|
|
159
|
+
typeChanged: [],
|
|
160
|
+
nullableChanged: [],
|
|
161
|
+
defaultChanged: [],
|
|
162
|
+
conflicts: []
|
|
163
|
+
})).added;
|
|
164
|
+
for (const copy of migration.copies) await adapter.copyFromJsonColumn(copy.source, copy.targets);
|
|
165
|
+
return {
|
|
166
|
+
diff: diffAfterJsonCopy(migration, diff),
|
|
167
|
+
added
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
3
171
|
//#region src/schema/table-option-diff.ts
|
|
4
172
|
/**
|
|
5
173
|
* Computes the difference between desired and existing table options.
|
|
@@ -369,6 +537,10 @@ var SyncEntry = class SyncEntry {
|
|
|
369
537
|
fkChanged;
|
|
370
538
|
/** @since 0.1.141 — see {@link TSyncEntryInit.derivedChanges}. */
|
|
371
539
|
derivedChanges;
|
|
540
|
+
/** @since 0.1.155 — see {@link TSyncEntryInit.jsonCopies}. */
|
|
541
|
+
jsonCopies;
|
|
542
|
+
/** @since 0.1.155 — see {@link TSyncEntryInit.jsonified}. */
|
|
543
|
+
jsonified;
|
|
372
544
|
columnsAdded;
|
|
373
545
|
columnsRenamed;
|
|
374
546
|
columnsDropped;
|
|
@@ -403,6 +575,8 @@ var SyncEntry = class SyncEntry {
|
|
|
403
575
|
this.fkRemoved = init.fkRemoved ?? [];
|
|
404
576
|
this.fkChanged = init.fkChanged ?? [];
|
|
405
577
|
this.derivedChanges = init.derivedChanges ?? [];
|
|
578
|
+
this.jsonCopies = init.jsonCopies ?? [];
|
|
579
|
+
this.jsonified = init.jsonified ?? [];
|
|
406
580
|
this.columnsAdded = init.columnsAdded ?? [];
|
|
407
581
|
this.columnsRenamed = init.columnsRenamed ?? [];
|
|
408
582
|
this.columnsDropped = init.columnsDropped ?? [];
|
|
@@ -476,13 +650,13 @@ var SyncEntry = class SyncEntry {
|
|
|
476
650
|
const head = this.refused ? `✖ refused: ${vp}${label}` : `✗ ${vp}${label} — error`;
|
|
477
651
|
return [` ${c.red(head)}`, ...this.errors.map((err) => ` ${c.red(err)}`)];
|
|
478
652
|
}
|
|
479
|
-
/** `! PK (id) → (code) — rebuild (table is empty)` / `— skipped (safe mode)` */
|
|
653
|
+
/** `! PK (id) → (code) — rebuild (table is empty)` / `— rebuild (rows satisfy the new key)` / `— skipped (safe mode)` */
|
|
480
654
|
printPkChange(c, mode) {
|
|
481
655
|
const pk = this.pkChange;
|
|
482
656
|
if (!pk) return [];
|
|
483
657
|
const cols = `PK (${pk.from.join(", ")}) → (${pk.to.join(", ")})`;
|
|
484
658
|
if (!pk.rebuild) return [` ${c.yellow(`! ${cols} — skipped (safe mode)`)}`];
|
|
485
|
-
return mode === "plan" ? [` ${c.red(`! ${cols} — rebuild (table is empty)`)}`] : [` ${c.yellow(`~ ${cols} — rebuilt`)}`];
|
|
659
|
+
return mode === "plan" ? [` ${c.red(`! ${cols} — rebuild (${pk.populated ? "rows satisfy the new key" : "table is empty"})`)}`] : [` ${c.yellow(`~ ${cols} — rebuilt`)}`];
|
|
486
660
|
}
|
|
487
661
|
/**
|
|
488
662
|
* `! col: t1 → t2 — drop` (plan), or `! type col (t1 → t2) — skipped (safe
|
|
@@ -508,6 +682,11 @@ var SyncEntry = class SyncEntry {
|
|
|
508
682
|
return ` ${dc.reason === "kind" ? c.red(line) : c.yellow(line)}`;
|
|
509
683
|
});
|
|
510
684
|
}
|
|
685
|
+
/** `~ addr → addr__street, addr__zip — copy JSON values` / `~ extra — text values as JSON`. */
|
|
686
|
+
printJsonMigration(c, mode) {
|
|
687
|
+
const copy = mode === "plan" ? "copy JSON values" : "JSON values copied";
|
|
688
|
+
return [...this.jsonified.map((col) => ` ${c.yellow(`~ ${col} — text values as JSON`)}`), ...this.jsonCopies.map((jc) => ` ${c.yellow(`~ ${jc.from} → ${jc.to.join(", ")} — ${copy}`)}`)];
|
|
689
|
+
}
|
|
511
690
|
/** `~ col — nullable` / `~ col — default a → b`, `— skipped (safe mode)` when pending. */
|
|
512
691
|
printNullableDefaults(c) {
|
|
513
692
|
const suffix = this.skipped.includes("nullable-defaults") ? " — skipped (safe mode)" : "";
|
|
@@ -565,6 +744,7 @@ var SyncEntry = class SyncEntry {
|
|
|
565
744
|
` ${c.cyan(`~ ${vp}${label} — alter${renameInfo}`)}`,
|
|
566
745
|
...this.columnsToAdd.map((col) => ` ${c.green(`+ ${col.physicalName} (${col.designType})${col.derived ? " derived" : ""} — add`)}`),
|
|
567
746
|
...this.columnsToRename.map((r) => ` ${c.yellow(`~ ${r.from} → ${r.to} — rename`)}`),
|
|
747
|
+
...this.printJsonMigration(c, "plan"),
|
|
568
748
|
...this.printTypeChanges(c),
|
|
569
749
|
...this.printNullableDefaults(c),
|
|
570
750
|
...this.printDerivedChanges(c, "plan"),
|
|
@@ -593,13 +773,14 @@ var SyncEntry = class SyncEntry {
|
|
|
593
773
|
...this.columnsAdded.map((col) => ` ${c.green(`+ ${col} — added`)}`),
|
|
594
774
|
""
|
|
595
775
|
];
|
|
596
|
-
if (this.columnsAdded.length > 0 || this.columnsRenamed.length > 0 || this.columnsDropped.length > 0 || this.optionChanges.length > 0 || this.pkChange !== void 0 || this.derivedChanges.length > 0 || this.skipped.length > 0 || this.recreated || this.renamedFrom) {
|
|
776
|
+
if (this.columnsAdded.length > 0 || this.columnsRenamed.length > 0 || this.columnsDropped.length > 0 || this.optionChanges.length > 0 || this.pkChange !== void 0 || this.derivedChanges.length > 0 || this.jsonCopies.length > 0 || this.jsonified.length > 0 || this.skipped.length > 0 || this.recreated || this.renamedFrom) {
|
|
597
777
|
const rlabel = this.recreated ? "recreated" : "altered";
|
|
598
778
|
const renameInfo = this.renamedFrom ? ` ${c.yellow(`(renamed from ${this.renamedFrom})`)}` : "";
|
|
599
779
|
return [
|
|
600
780
|
` ${(this.recreated ? (s) => c.yellow(s) : (s) => c.cyan(s))(`~ ${vp}${label} — ${rlabel}${renameInfo}`)}`,
|
|
601
781
|
...this.columnsAdded.map((col) => ` ${c.green(`+ ${col} — added`)}`),
|
|
602
782
|
...this.columnsRenamed.map((col) => ` ${c.yellow(`~ ${col} — renamed`)}`),
|
|
783
|
+
...this.printJsonMigration(c, "result"),
|
|
603
784
|
...this.printPkChange(c, "result"),
|
|
604
785
|
...this.printTypeChanges(c),
|
|
605
786
|
...this.printNullableDefaults(c),
|
|
@@ -641,14 +822,19 @@ function planTableInit(facts, safe, dependsOn) {
|
|
|
641
822
|
};
|
|
642
823
|
const skipped = safe ? safeModeSkips(facts) : [];
|
|
643
824
|
const pk = facts.diff?.primaryKeyChanged;
|
|
644
|
-
if (pk)
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
825
|
+
if (pk) {
|
|
826
|
+
init.pkChange = {
|
|
827
|
+
from: pk.from,
|
|
828
|
+
to: pk.to,
|
|
829
|
+
rebuild: !skipped.includes("pk-rebuild")
|
|
830
|
+
};
|
|
831
|
+
if (facts.populated === true) init.pkChange.populated = true;
|
|
832
|
+
}
|
|
649
833
|
if (!safe) return init;
|
|
834
|
+
const diff = afterJsonCopy(facts);
|
|
650
835
|
return {
|
|
651
836
|
...init,
|
|
837
|
+
...diff && describeNullableDefaults(diff),
|
|
652
838
|
columnsToDrop: [],
|
|
653
839
|
typeChanges: skipped.includes("recreate") ? init.typeChanges : [],
|
|
654
840
|
derivedChanges: skipped.includes("derived") ? init.derivedChanges : [],
|
|
@@ -666,6 +852,7 @@ function typeChangeStrategy(readable, diff) {
|
|
|
666
852
|
const adapter = readable.dbAdapter;
|
|
667
853
|
if (willDropRecreate(readable, diff, void 0)) return "drop";
|
|
668
854
|
if (readable.syncMethod === "recreate" && adapter.recreateTable) return "recreate";
|
|
855
|
+
if (diff.primaryKeyChanged && !adapter.rebuildPrimaryKey && adapter.recreateTable) return "recreate";
|
|
669
856
|
if (adapter.supportsColumnModify && adapter.syncColumns) return "modify";
|
|
670
857
|
return "none";
|
|
671
858
|
}
|
|
@@ -733,11 +920,23 @@ async function executeSyncTable(facts, safe, deps, exec) {
|
|
|
733
920
|
await adapter.ensureTable(ensureOpts);
|
|
734
921
|
init.status = "create";
|
|
735
922
|
} else if (existing.length > 0) {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
923
|
+
let diff = facts.diff ?? computeColumnDiff(readable.columnDescriptors, existing, adapter.typeMapper?.bind(adapter), {
|
|
924
|
+
snapshot: storedSnapshot,
|
|
925
|
+
nativeDefaultFns: adapter.nativeDefaultFns()
|
|
926
|
+
});
|
|
927
|
+
const migration = facts.jsonMigration ?? planJsonColumnMigration(readable, diff, storedSnapshot);
|
|
928
|
+
if (migration.errors.length > 0) throw new Error(migration.errors.join(" "));
|
|
929
|
+
if (migration.copies.length > 0 || migration.jsonify.length > 0) {
|
|
930
|
+
phase = "JSON column copy";
|
|
931
|
+
const applied = await applyJsonColumnMigration(adapter, migration, diff);
|
|
932
|
+
diff = applied.diff;
|
|
933
|
+
Object.assign(init, describeJsonMigration(migration));
|
|
934
|
+
if (applied.added.length > 0) init.columnsAdded = applied.added;
|
|
740
935
|
init.status = "alter";
|
|
936
|
+
phase = "Column sync";
|
|
937
|
+
}
|
|
938
|
+
if (hasFkChanges && !adapter.syncForeignKeys && adapter.recreateTable) {
|
|
939
|
+
await recreateKeepingRenames(adapter, diff, init);
|
|
741
940
|
if (diff.primaryKeyChanged) init.pkChange = {
|
|
742
941
|
...diff.primaryKeyChanged,
|
|
743
942
|
rebuild: true
|
|
@@ -758,7 +957,10 @@ async function executeSyncTable(facts, safe, deps, exec) {
|
|
|
758
957
|
const existed = adapter.tableExists ? await adapter.tableExists() : false;
|
|
759
958
|
await adapter.ensureTable(ensureOpts);
|
|
760
959
|
if (!existed) init.status = "create";
|
|
761
|
-
} else await applyColumnDiff(adapter, readable, facts.diff ?? computeColumnDiff(readable.columnDescriptors, snapshotToExistingColumns(storedSnapshot, readable), deps.resolveTypeMapper(adapter), {
|
|
960
|
+
} else await applyColumnDiff(adapter, readable, facts.diff ?? computeColumnDiff(readable.columnDescriptors, snapshotToExistingColumns(storedSnapshot, readable), deps.resolveTypeMapper(adapter), {
|
|
961
|
+
snapshot: storedSnapshot,
|
|
962
|
+
nativeDefaultFns: adapter.nativeDefaultFns()
|
|
963
|
+
}), init, safe, deps.logger, ensureOpts);
|
|
762
964
|
else {
|
|
763
965
|
const existed = adapter.tableExists ? await adapter.tableExists() : true;
|
|
764
966
|
if (!init.recreated) {
|
|
@@ -900,6 +1102,13 @@ function willDropRecreate(readable, diff, optionDiff) {
|
|
|
900
1102
|
if (readable.syncMethod === "drop" && (diff?.typeChanged.length ?? 0) > 0) return true;
|
|
901
1103
|
return (optionDiff?.changed.some((c) => c.destructive) ?? false) && !(readable.syncMethod === "recreate" && !!adapter.recreateTable);
|
|
902
1104
|
}
|
|
1105
|
+
/**
|
|
1106
|
+
* The column diff the executor applies after the table's JSON column copy —
|
|
1107
|
+
* its targets' NOT NULL and defaults deferred (see {@link diffAfterJsonCopy}).
|
|
1108
|
+
*/
|
|
1109
|
+
function afterJsonCopy(facts) {
|
|
1110
|
+
return facts.diff && facts.jsonMigration ? diffAfterJsonCopy(facts.jsonMigration, facts.diff) : facts.diff;
|
|
1111
|
+
}
|
|
903
1112
|
/** Records work safe mode skipped on the entry (see `SyncEntry.skipped`). */
|
|
904
1113
|
function markSkipped(init, kind) {
|
|
905
1114
|
init.skipped = [...init.skipped ?? [], kind];
|
|
@@ -917,7 +1126,8 @@ function needsDdlForNullableDefaults(adapter) {
|
|
|
917
1126
|
* DDL either way.
|
|
918
1127
|
*/
|
|
919
1128
|
function safeModeSkips(facts) {
|
|
920
|
-
const { readable,
|
|
1129
|
+
const { readable, optionDiff, fkDiff } = facts;
|
|
1130
|
+
const diff = afterJsonCopy(facts);
|
|
921
1131
|
const adapter = readable.dbAdapter;
|
|
922
1132
|
const out = [];
|
|
923
1133
|
if (facts.planEntry.status === "error") return out;
|
|
@@ -931,6 +1141,16 @@ function safeModeSkips(facts) {
|
|
|
931
1141
|
if (optionDiff?.changed.some((c) => c.destructive)) out.push("table-options");
|
|
932
1142
|
return out;
|
|
933
1143
|
}
|
|
1144
|
+
/** The plan-shaped summary of a {@link TJsonColumnMigration}. */
|
|
1145
|
+
function describeJsonMigration(migration) {
|
|
1146
|
+
return {
|
|
1147
|
+
jsonCopies: migration.copies.length > 0 ? migration.copies.map((c) => ({
|
|
1148
|
+
from: c.source,
|
|
1149
|
+
to: c.targets.map((t) => t.column)
|
|
1150
|
+
})) : void 0,
|
|
1151
|
+
jsonified: migration.jsonify.length > 0 ? migration.jsonify : void 0
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
934
1154
|
/** The plan-shaped nullable / default change summaries of a diff. */
|
|
935
1155
|
function describeNullableDefaults(diff) {
|
|
936
1156
|
return {
|
|
@@ -962,6 +1182,28 @@ async function dropAndRecreate(adapter, init, name, ensureOpts) {
|
|
|
962
1182
|
init.recreated = true;
|
|
963
1183
|
init.status = "alter";
|
|
964
1184
|
}
|
|
1185
|
+
/**
|
|
1186
|
+
* `recreateTable()` before the column diff is applied: it copies the rows by
|
|
1187
|
+
* column name, so the columns renamed in this run are renamed first — under
|
|
1188
|
+
* the old name their values would not be copied.
|
|
1189
|
+
*/
|
|
1190
|
+
async function recreateKeepingRenames(adapter, diff, init) {
|
|
1191
|
+
if (diff.renamed.length > 0 && adapter.syncColumns) {
|
|
1192
|
+
const { renamed } = await adapter.syncColumns({
|
|
1193
|
+
added: [],
|
|
1194
|
+
removed: [],
|
|
1195
|
+
renamed: diff.renamed,
|
|
1196
|
+
typeChanged: [],
|
|
1197
|
+
nullableChanged: [],
|
|
1198
|
+
defaultChanged: [],
|
|
1199
|
+
conflicts: []
|
|
1200
|
+
});
|
|
1201
|
+
init.columnsRenamed = renamed;
|
|
1202
|
+
}
|
|
1203
|
+
await adapter.recreateTable();
|
|
1204
|
+
init.recreated = true;
|
|
1205
|
+
init.status = "alter";
|
|
1206
|
+
}
|
|
965
1207
|
async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensureOpts) {
|
|
966
1208
|
const name = readable.tableName;
|
|
967
1209
|
if (diff.conflicts.length > 0) {
|
|
@@ -980,11 +1222,8 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
|
|
|
980
1222
|
const cols = init.typeChanges.map((tc) => `${tc.column}: ${tc.fromType} → ${tc.toType}`).join(", ");
|
|
981
1223
|
logger.warn?.(`[schema-sync] Type change on "${name}" (${cols}) — drop-and-recreate skipped (safe mode)`);
|
|
982
1224
|
} else await dropAndRecreate(adapter, init, name, ensureOpts);
|
|
983
|
-
else if (strategy === "recreate")
|
|
984
|
-
|
|
985
|
-
init.recreated = true;
|
|
986
|
-
init.status = "alter";
|
|
987
|
-
} else if (strategy === "modify") {
|
|
1225
|
+
else if (strategy === "recreate") await recreateKeepingRenames(adapter, diff, init);
|
|
1226
|
+
else if (strategy === "modify") {
|
|
988
1227
|
needsSyncColumns = true;
|
|
989
1228
|
init.status = "alter";
|
|
990
1229
|
} else {
|
|
@@ -998,11 +1237,8 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
|
|
|
998
1237
|
if (!safe && !init.recreated && init.status !== "error") if (adapter.supportsColumnModify && adapter.syncColumns) {
|
|
999
1238
|
needsSyncColumns = true;
|
|
1000
1239
|
init.status = "alter";
|
|
1001
|
-
} else if (adapter.recreateTable)
|
|
1002
|
-
|
|
1003
|
-
init.recreated = true;
|
|
1004
|
-
init.status = "alter";
|
|
1005
|
-
} else init.status = "alter";
|
|
1240
|
+
} else if (adapter.recreateTable) await recreateKeepingRenames(adapter, diff, init);
|
|
1241
|
+
else init.status = "alter";
|
|
1006
1242
|
else if (init.status !== "error") {
|
|
1007
1243
|
if (safe && !init.recreated && needsDdlForNullableDefaults(adapter)) {
|
|
1008
1244
|
Object.assign(init, describeNullableDefaults(diff));
|
|
@@ -1014,7 +1250,7 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
|
|
|
1014
1250
|
}
|
|
1015
1251
|
if (!init.recreated && init.status !== "error" && (diff.added.length > 0 || diff.renamed.length > 0 || needsSyncColumns) && adapter.syncColumns) {
|
|
1016
1252
|
const syncResult = await adapter.syncColumns(diff);
|
|
1017
|
-
init.columnsAdded = syncResult.added;
|
|
1253
|
+
init.columnsAdded = [...init.columnsAdded ?? [], ...syncResult.added];
|
|
1018
1254
|
init.columnsRenamed = syncResult.renamed;
|
|
1019
1255
|
if (syncResult.added.length > 0 || (syncResult.renamed?.length ?? 0) > 0 || needsSyncColumns) init.status = "alter";
|
|
1020
1256
|
}
|
|
@@ -1579,8 +1815,18 @@ var SchemaSync = class {
|
|
|
1579
1815
|
init.columnsToAdd = [...readable.columnDescriptors];
|
|
1580
1816
|
} else if (facts.existing && facts.existing.length > 0) {
|
|
1581
1817
|
const typeMapper = adapter.typeMapper?.bind(adapter);
|
|
1582
|
-
facts.diff = computeColumnDiff(readable.columnDescriptors, facts.existing, typeMapper, {
|
|
1818
|
+
facts.diff = computeColumnDiff(readable.columnDescriptors, facts.existing, typeMapper, {
|
|
1819
|
+
snapshot: storedSnapshot,
|
|
1820
|
+
nativeDefaultFns: adapter.nativeDefaultFns()
|
|
1821
|
+
});
|
|
1583
1822
|
this.populatePlanFromDiff(facts.diff, init, readable, safe);
|
|
1823
|
+
const migration = planJsonColumnMigration(readable, facts.diff, storedSnapshot);
|
|
1824
|
+
facts.jsonMigration = migration;
|
|
1825
|
+
Object.assign(init, describeJsonMigration(migration));
|
|
1826
|
+
if (migration.errors.length > 0) {
|
|
1827
|
+
init.status = "error";
|
|
1828
|
+
init.errors = [...init.errors ?? [], ...migration.errors];
|
|
1829
|
+
} else if (init.status === "in-sync" && (migration.copies.length > 0 || migration.jsonify.length > 0)) init.status = "alter";
|
|
1584
1830
|
}
|
|
1585
1831
|
} else if (adapter.syncColumns) if (!storedSnapshot) {
|
|
1586
1832
|
if (!pendingRename) {
|
|
@@ -1591,7 +1837,10 @@ var SchemaSync = class {
|
|
|
1591
1837
|
}
|
|
1592
1838
|
} else {
|
|
1593
1839
|
const existing = snapshotToExistingColumns(storedSnapshot, readable);
|
|
1594
|
-
facts.diff = computeColumnDiff(readable.columnDescriptors, existing, this.resolveTypeMapper(adapter), {
|
|
1840
|
+
facts.diff = computeColumnDiff(readable.columnDescriptors, existing, this.resolveTypeMapper(adapter), {
|
|
1841
|
+
snapshot: storedSnapshot,
|
|
1842
|
+
nativeDefaultFns: adapter.nativeDefaultFns()
|
|
1843
|
+
});
|
|
1595
1844
|
this.populatePlanFromDiff(facts.diff, init, readable, safe);
|
|
1596
1845
|
}
|
|
1597
1846
|
else if (adapter.tableExists) {
|
|
@@ -1626,7 +1875,16 @@ var SchemaSync = class {
|
|
|
1626
1875
|
}, fk.existing)
|
|
1627
1876
|
}));
|
|
1628
1877
|
}
|
|
1629
|
-
if (facts.diff?.primaryKeyChanged)
|
|
1878
|
+
if (facts.diff?.primaryKeyChanged) {
|
|
1879
|
+
const tableName = pendingRename ? dbName : void 0;
|
|
1880
|
+
facts.populated = await adapter.hasRows(tableName) ?? "unknown";
|
|
1881
|
+
const { added, renamed } = facts.diff;
|
|
1882
|
+
const { to } = facts.diff.primaryKeyChanged;
|
|
1883
|
+
if (facts.populated === true && adapter.countKeyViolations && !added.some((f) => to.includes(f.physicalName))) {
|
|
1884
|
+
const liveName = new Map(renamed.map((r) => [r.field.physicalName, r.oldName]));
|
|
1885
|
+
facts.keyViolations = await adapter.countKeyViolations(to.map((c) => liveName.get(c) ?? c), tableName);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1630
1888
|
facts.dropsTable = !!facts.diff?.primaryKeyChanged || willDropRecreate(readable, facts.diff, facts.optionDiff);
|
|
1631
1889
|
if (facts.diff?.primaryKeyChanged || probeRecreateInbound && facts.dropsTable) facts.inboundFks = await adapter.getReferencingForeignKeys?.(dbName);
|
|
1632
1890
|
if (adapter.getObjectKind) facts.objectKind = await adapter.getObjectKind(dbName);
|
|
@@ -1643,7 +1901,7 @@ var SchemaSync = class {
|
|
|
1643
1901
|
const pk = t.diff?.primaryKeyChanged;
|
|
1644
1902
|
if (pk) {
|
|
1645
1903
|
const label = pkLabel(pk);
|
|
1646
|
-
if (t.populated === true) addRefusal(refusals, t.name, `Primary key of "${t.name}" changed ${label} but the table has rows; schema sync cannot rebuild a populated primary key. Migrate manually (or empty the table) and re-run.`);
|
|
1904
|
+
if (t.populated === true && t.keyViolations !== 0) addRefusal(refusals, t.name, t.keyViolations === void 0 ? `Primary key of "${t.name}" changed ${label} but the table has rows; schema sync cannot rebuild a populated primary key. Migrate manually (or empty the table) and re-run.` : `Primary key of "${t.name}" changed ${label} but ${t.keyViolations} row${t.keyViolations === 1 ? " has" : "s have"} a NULL or duplicate (${pk.to.join(", ")}) — fix or remove ${t.keyViolations === 1 ? "it" : "them"} (or migrate manually) and re-run.`);
|
|
1647
1905
|
else if (t.populated === "unknown") addRefusal(refusals, t.name, `Primary key of "${t.name}" changed ${label} but the adapter cannot tell whether the table has rows under its old name "${t.dbName}" — implement hasRows(tableName) on the adapter, or rename the table in a separate run first.`);
|
|
1648
1906
|
if (!safe) {
|
|
1649
1907
|
const liveInbound = (t.inboundFks ?? []).filter((fk) => {
|
|
@@ -18,6 +18,22 @@ function primitiveBaseType(def) {
|
|
|
18
18
|
const ct = def.config.type;
|
|
19
19
|
return typeof ct === "object" ? ct.kind === "final" ? ct.value : ct.kind : ct;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Whether a definition is a union with a `null` member — `T | null`, or a
|
|
23
|
+
* ref to an alias of one (compile-time mirror of the runtime nullable union,
|
|
24
|
+
* since 0.1.155).
|
|
25
|
+
*/
|
|
26
|
+
function isNullableUnion(def, doc, depth = 0) {
|
|
27
|
+
if (!def || depth > 8) return false;
|
|
28
|
+
if (isRef(def)) {
|
|
29
|
+
if (def.id === "null") return true;
|
|
30
|
+
return isNullableUnion(doc.unwindType(def.id, def.chain)?.def, doc, depth + 1);
|
|
31
|
+
}
|
|
32
|
+
if (primitiveBaseType(def) === "null") return true;
|
|
33
|
+
const group = def;
|
|
34
|
+
if (!group.isGroup || group.op !== "|" || isStructure(def)) return false;
|
|
35
|
+
return group.unwrap().some((member) => isNullableUnion(member, doc, depth + 1));
|
|
36
|
+
}
|
|
21
37
|
/** Resolves a field/prop's primitive base type, or `undefined` if it isn't a ref-to-primitive. */
|
|
22
38
|
function getPrimitiveBaseType(node, doc) {
|
|
23
39
|
const def = node.getDefinition();
|
|
@@ -50,16 +66,18 @@ function searchFieldVerdict(field, doc) {
|
|
|
50
66
|
/**
|
|
51
67
|
* Why an integer fulltext member is not index-backed (its exact-number match
|
|
52
68
|
* would scan — or fail on MongoDB next to `$text`), or `undefined`. Backed:
|
|
53
|
-
* `_id
|
|
54
|
-
*
|
|
69
|
+
* `_id` or the first `@meta.id` of an interface's own fields (an inline
|
|
70
|
+
* nested object's are no key of the row, like at runtime), or the first field
|
|
71
|
+
* (in declaration order) of a `@db.index.plain` / `@db.index.unique` group.
|
|
55
72
|
*/
|
|
56
73
|
function integerMemberIndexProblem(token) {
|
|
57
74
|
const field = token.parentNode;
|
|
58
75
|
const struct = getParentStruct(token);
|
|
59
76
|
if (!struct) return void 0;
|
|
60
|
-
if (field.id === "_id") return void 0;
|
|
61
77
|
const props = [...struct.props.values()];
|
|
62
|
-
|
|
78
|
+
const topLevel = !!struct.ownerNode && isInterface(struct.ownerNode);
|
|
79
|
+
if (topLevel && field.id === "_id") return void 0;
|
|
80
|
+
if (topLevel && field.countAnnotations("meta.id") > 0 && props.find((p) => p.countAnnotations("meta.id") > 0) === field) return;
|
|
63
81
|
for (const ann of ["db.index.plain", "db.index.unique"]) for (const own of field.annotations?.filter((a) => a.name === ann) ?? []) {
|
|
64
82
|
const group = own.args[0]?.text ?? field.id;
|
|
65
83
|
if (props.find((p) => p.annotations?.some((a) => a.name === ann && (a.args[0]?.text ?? p.id) === group)) === field) return void 0;
|
|
@@ -450,4 +468,4 @@ function forEachFieldRef(expr, fn) {
|
|
|
450
468
|
}
|
|
451
469
|
}
|
|
452
470
|
//#endregion
|
|
453
|
-
export {
|
|
471
|
+
export { validateExclusiveWith as C, searchFieldVerdict as S, validateSiblingStringField as T, getParentTypeName as _, hasAnyViewAnnotation as a, primitiveBaseType as b, joinTargets as c, viewJoins as d, viewScopeTypes as f, getParentStruct as g, getNavTargetTypeName as h, forEachFieldRef as i, validateQueryScope as l, getDbTableOwner as m, findFKFieldsPointingTo as n, isAliasDecl as o, getAnnotationAlias as p, findViewCycle as r, isDbSourceDecl as s, earlierJoinTargets as t, validateRefArgument as u, integerMemberIndexProblem as v, validateFieldBaseType as w, refActionAnnotation as x, isNullableUnion as y };
|
|
@@ -18,6 +18,22 @@ function primitiveBaseType(def) {
|
|
|
18
18
|
const ct = def.config.type;
|
|
19
19
|
return typeof ct === "object" ? ct.kind === "final" ? ct.value : ct.kind : ct;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Whether a definition is a union with a `null` member — `T | null`, or a
|
|
23
|
+
* ref to an alias of one (compile-time mirror of the runtime nullable union,
|
|
24
|
+
* since 0.1.155).
|
|
25
|
+
*/
|
|
26
|
+
function isNullableUnion(def, doc, depth = 0) {
|
|
27
|
+
if (!def || depth > 8) return false;
|
|
28
|
+
if ((0, _atscript_core.isRef)(def)) {
|
|
29
|
+
if (def.id === "null") return true;
|
|
30
|
+
return isNullableUnion(doc.unwindType(def.id, def.chain)?.def, doc, depth + 1);
|
|
31
|
+
}
|
|
32
|
+
if (primitiveBaseType(def) === "null") return true;
|
|
33
|
+
const group = def;
|
|
34
|
+
if (!group.isGroup || group.op !== "|" || (0, _atscript_core.isStructure)(def)) return false;
|
|
35
|
+
return group.unwrap().some((member) => isNullableUnion(member, doc, depth + 1));
|
|
36
|
+
}
|
|
21
37
|
/** Resolves a field/prop's primitive base type, or `undefined` if it isn't a ref-to-primitive. */
|
|
22
38
|
function getPrimitiveBaseType(node, doc) {
|
|
23
39
|
const def = node.getDefinition();
|
|
@@ -50,16 +66,18 @@ function searchFieldVerdict(field, doc) {
|
|
|
50
66
|
/**
|
|
51
67
|
* Why an integer fulltext member is not index-backed (its exact-number match
|
|
52
68
|
* would scan — or fail on MongoDB next to `$text`), or `undefined`. Backed:
|
|
53
|
-
* `_id
|
|
54
|
-
*
|
|
69
|
+
* `_id` or the first `@meta.id` of an interface's own fields (an inline
|
|
70
|
+
* nested object's are no key of the row, like at runtime), or the first field
|
|
71
|
+
* (in declaration order) of a `@db.index.plain` / `@db.index.unique` group.
|
|
55
72
|
*/
|
|
56
73
|
function integerMemberIndexProblem(token) {
|
|
57
74
|
const field = token.parentNode;
|
|
58
75
|
const struct = getParentStruct(token);
|
|
59
76
|
if (!struct) return void 0;
|
|
60
|
-
if (field.id === "_id") return void 0;
|
|
61
77
|
const props = [...struct.props.values()];
|
|
62
|
-
|
|
78
|
+
const topLevel = !!struct.ownerNode && (0, _atscript_core.isInterface)(struct.ownerNode);
|
|
79
|
+
if (topLevel && field.id === "_id") return void 0;
|
|
80
|
+
if (topLevel && field.countAnnotations("meta.id") > 0 && props.find((p) => p.countAnnotations("meta.id") > 0) === field) return;
|
|
63
81
|
for (const ann of ["db.index.plain", "db.index.unique"]) for (const own of field.annotations?.filter((a) => a.name === ann) ?? []) {
|
|
64
82
|
const group = own.args[0]?.text ?? field.id;
|
|
65
83
|
if (props.find((p) => p.annotations?.some((a) => a.name === ann && (a.args[0]?.text ?? p.id) === group)) === field) return void 0;
|
|
@@ -528,6 +546,12 @@ Object.defineProperty(exports, "isDbSourceDecl", {
|
|
|
528
546
|
return isDbSourceDecl;
|
|
529
547
|
}
|
|
530
548
|
});
|
|
549
|
+
Object.defineProperty(exports, "isNullableUnion", {
|
|
550
|
+
enumerable: true,
|
|
551
|
+
get: function() {
|
|
552
|
+
return isNullableUnion;
|
|
553
|
+
}
|
|
554
|
+
});
|
|
531
555
|
Object.defineProperty(exports, "joinTargets", {
|
|
532
556
|
enumerable: true,
|
|
533
557
|
get: function() {
|