@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.
Files changed (33) hide show
  1. package/dist/agg.d.cts +1 -1
  2. package/dist/agg.d.mts +1 -1
  3. package/dist/{buckets-jy8BfswO.d.cts → buckets-DcWm3SLt.d.cts} +120 -10
  4. package/dist/{buckets-CBKL_ZWE.d.mts → buckets-goCoky8v.d.mts} +120 -10
  5. package/dist/{column-diff-DrE4ho_m.mjs → column-diff-C68V_eYM.mjs} +348 -69
  6. package/dist/{column-diff-CbdWi_uZ.cjs → column-diff-DMkGYAGM.cjs} +359 -68
  7. package/dist/{fk-diff-BIJ8l_hF.d.mts → fk-diff-BYLNWoF9.d.mts} +2 -1
  8. package/dist/{fk-diff-D1mrLMjU.d.cts → fk-diff-DblOPBLS.d.cts} +2 -1
  9. package/dist/index.cjs +2 -2
  10. package/dist/index.d.cts +10 -5
  11. package/dist/index.d.mts +10 -5
  12. package/dist/index.mjs +2 -2
  13. package/dist/plugin.cjs +3 -3
  14. package/dist/plugin.mjs +3 -3
  15. package/dist/rel.d.cts +2 -2
  16. package/dist/rel.d.mts +2 -2
  17. package/dist/{relation-helpers-BUsxNNGU.d.mts → relation-helpers-B9MVBrtx.d.mts} +1 -1
  18. package/dist/{relation-helpers-D_8bydc2.d.cts → relation-helpers-D9kMFfum.d.cts} +1 -1
  19. package/dist/shared.cjs +2 -1
  20. package/dist/shared.d.cts +10 -3
  21. package/dist/shared.d.mts +10 -3
  22. package/dist/shared.mjs +2 -2
  23. package/dist/sync.cjs +288 -30
  24. package/dist/sync.d.cts +34 -5
  25. package/dist/sync.d.mts +34 -5
  26. package/dist/sync.mjs +288 -30
  27. package/dist/{validation-utils-DqZAH6aT.mjs → validation-utils-DxHIqa36.mjs} +23 -5
  28. package/dist/{validation-utils-debIYpq0.cjs → validation-utils-_PA7iSLm.cjs} +28 -4
  29. package/dist/{validator-BACewoRj.mjs → validator-BC3nwFi4.mjs} +178 -10
  30. package/dist/{validator-BxWwh3wf.cjs → validator-DHwSyBej.cjs} +201 -9
  31. package/dist/validator.cjs +1 -1
  32. package/dist/validator.mjs +1 -1
  33. 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 { _t as NoopLogger, a as computeTableHash, c as snapshotToExistingColumns, d as computeForeignKeyDiff, dt as aliasTargetOf, f as fkColumns, i as computeSchemaHash, l as snapshotToExistingTableOptions, m as hasForeignKeyChanges, o as computeTableSnapshot, p as fkKey, r as canonicalizeQueryNode, s as computeViewSnapshot, t as computeColumnDiff, u as viewSnapshotSources } from "./column-diff-DrE4ho_m.mjs";
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) init.pkChange = {
645
- from: pk.from,
646
- to: pk.to,
647
- rebuild: !skipped.includes("pk-rebuild")
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
- const diff = facts.diff ?? computeColumnDiff(readable.columnDescriptors, existing, adapter.typeMapper?.bind(adapter), { snapshot: storedSnapshot });
737
- if (hasFkChanges && !adapter.syncForeignKeys && adapter.recreateTable) {
738
- await adapter.recreateTable();
739
- init.recreated = true;
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), { snapshot: storedSnapshot }), init, safe, deps.logger, ensureOpts);
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, diff, optionDiff, fkDiff } = facts;
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
- await adapter.recreateTable();
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
- await adapter.recreateTable();
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, { snapshot: storedSnapshot });
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), { snapshot: storedSnapshot });
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) facts.populated = await adapter.hasRows(pendingRename ? dbName : void 0) ?? "unknown";
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`, the first `@meta.id` of the type, or the first field (in declaration
54
- * order) of a `@db.index.plain` / `@db.index.unique` group.
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
- if (field.countAnnotations("meta.id") > 0 && props.find((p) => p.countAnnotations("meta.id") > 0) === field) return;
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 { validateFieldBaseType as C, validateExclusiveWith as S, getParentTypeName as _, hasAnyViewAnnotation as a, refActionAnnotation 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, validateSiblingStringField as w, searchFieldVerdict as x, primitiveBaseType as y };
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`, the first `@meta.id` of the type, or the first field (in declaration
54
- * order) of a `@db.index.plain` / `@db.index.unique` group.
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
- if (field.countAnnotations("meta.id") > 0 && props.find((p) => p.countAnnotations("meta.id") > 0) === field) return;
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() {