@atscript/db 0.1.154 → 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-DAIAq6XR.mjs → column-diff-C68V_eYM.mjs} +346 -65
  6. package/dist/{column-diff-qnzx1o3c.cjs → column-diff-DMkGYAGM.cjs} +357 -64
  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.cjs CHANGED
@@ -1,6 +1,174 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_db_error = require("./db-error-JcfJqqpk.cjs");
3
- const require_column_diff = require("./column-diff-qnzx1o3c.cjs");
3
+ const require_column_diff = require("./column-diff-DMkGYAGM.cjs");
4
+ const require_validator = require("./validator-DHwSyBej.cjs");
5
+ //#region src/schema/json-column-copy.ts
6
+ const NONE = {
7
+ copies: [],
8
+ jsonify: [],
9
+ errors: []
10
+ };
11
+ /** A text column type of the old layout (`TEXT`, `LONGTEXT`, `VARCHAR(n)`, `character varying`). */
12
+ const TEXT_TYPE = /text|char|clob/i;
13
+ /**
14
+ * Design types a stored snapshot gives a field whose column held a whole
15
+ * value as JSON text: the old union layout (`union`), or `@db.json`.
16
+ */
17
+ const JSON_TEXT_DESIGN_TYPES = new Set([
18
+ "union",
19
+ "json",
20
+ "object"
21
+ ]);
22
+ /** How a target column reads its value out of the JSON, or why it cannot. */
23
+ function targetKind(fd) {
24
+ if (fd.encrypted) return { problem: "it is @db.encrypted" };
25
+ if (fd.isGeoPoint) return { problem: "it is a db.geoPoint" };
26
+ if (fd.storage === "json") return "json";
27
+ return fd.designType === "boolean" ? "boolean" : "text";
28
+ }
29
+ /** Whether `type` is a union one of whose value members is a string. */
30
+ function hasStringMember(type) {
31
+ if (type?.type.kind !== "union") return false;
32
+ return require_validator.unionValueMembers(type).members.some((m) => require_column_diff.resolveDesignType(m) === "string");
33
+ }
34
+ /**
35
+ * Plans {@link TJsonColumnMigration} for one table from its live column diff
36
+ * (and its stored snapshot — on SQLite the old text column and a JSON
37
+ * column have the same type).
38
+ */
39
+ function planJsonColumnMigration(readable, diff, snapshot) {
40
+ const meta = readable.getMetadata();
41
+ if (meta.nestedObjects) return NONE;
42
+ const adapter = readable.dbAdapter;
43
+ const name = readable.tableName;
44
+ const removed = new Set(diff.removed.map((c) => c.name));
45
+ const snapshotFields = new Map(snapshot?.fields.map((f) => [f.physicalName, f]));
46
+ const out = {
47
+ copies: [],
48
+ jsonify: [],
49
+ errors: []
50
+ };
51
+ if (removed.size > 0) for (const parent of meta.flattenedParents) {
52
+ const source = require_column_diff.relationalColumnName(parent, void 0, parent.includes("."));
53
+ if (!removed.has(source)) continue;
54
+ const was = snapshotFields.get(source);
55
+ if (was && !JSON_TEXT_DESIGN_TYPES.has(was.designType)) continue;
56
+ const prefix = `${parent}.`;
57
+ const targets = [];
58
+ const problems = [];
59
+ for (const fd of meta.storedDescriptors) {
60
+ if (!fd.path.startsWith(prefix)) continue;
61
+ const kind = targetKind(fd);
62
+ if (typeof kind === "object") {
63
+ problems.push(`"${fd.physicalName}" (${kind.problem})`);
64
+ continue;
65
+ }
66
+ targets.push({
67
+ column: fd.physicalName,
68
+ path: fd.path.slice(prefix.length).split("."),
69
+ kind,
70
+ field: fd
71
+ });
72
+ }
73
+ if (targets.length === 0 && problems.length === 0) continue;
74
+ if (problems.length > 0 || !adapter.copyFromJsonColumn) {
75
+ const why = problems.length > 0 ? `cannot fill ${problems.join(", ")} from it` : "the adapter cannot copy JSON values into columns";
76
+ 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.`);
77
+ continue;
78
+ }
79
+ out.copies.push({
80
+ source,
81
+ targets
82
+ });
83
+ }
84
+ const typeChanged = new Map(diff.typeChanged.map((tc) => [tc.field.physicalName, tc]));
85
+ for (const fd of meta.storedDescriptors) {
86
+ if (fd.storage !== "json" || !hasStringMember(meta.flatMap.get(fd.path))) continue;
87
+ const tc = typeChanged.get(fd.physicalName);
88
+ const old = snapshotFields.get(fd.physicalName);
89
+ const dotted = `${fd.physicalName}.`;
90
+ 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;
91
+ if (!adapter.jsonifyTextColumn) {
92
+ out.errors.push(`Column "${fd.physicalName}" of ${name} becomes a JSON column but the adapter cannot convert its text values. Migrate them manually.`);
93
+ continue;
94
+ }
95
+ out.jsonify.push(fd.physicalName);
96
+ }
97
+ return out.copies.length > 0 || out.jsonify.length > 0 || out.errors.length > 0 ? out : NONE;
98
+ }
99
+ /** The copy targets {@link applyJsonColumnMigration} adds: those not in the table yet. */
100
+ function copyTargetsToAdd(migration, diff) {
101
+ const targets = new Set(migration.copies.flatMap((c) => c.targets.map((t) => t.column)));
102
+ return diff.added.filter((fd) => targets.has(fd.physicalName));
103
+ }
104
+ /**
105
+ * The column diff the rest of the table's sync applies after
106
+ * {@link applyJsonColumnMigration}: without the copy targets it added, with
107
+ * their NOT NULL and model default as nullability / default changes. Also
108
+ * what `plan({ safe: true })` reports as skipped — safe mode adds and fills
109
+ * the targets but leaves them nullable and without their default.
110
+ */
111
+ function diffAfterJsonCopy(migration, diff) {
112
+ const toAdd = copyTargetsToAdd(migration, diff);
113
+ if (toAdd.length === 0) return diff;
114
+ return {
115
+ ...diff,
116
+ added: diff.added.filter((fd) => !toAdd.includes(fd)),
117
+ nullableChanged: [...diff.nullableChanged, ...toAdd.filter((fd) => !fd.optional).map((fd) => ({
118
+ field: fd,
119
+ wasNullable: true
120
+ }))],
121
+ defaultChanged: [...diff.defaultChanged, ...toAdd.filter((fd) => fd.defaultValue !== void 0).map((fd) => ({
122
+ field: fd,
123
+ newDefault: require_column_diff.serializeDefaultValue(fd.defaultValue)
124
+ }))]
125
+ };
126
+ }
127
+ /**
128
+ * Runs a planned {@link TJsonColumnMigration} — first, before any other
129
+ * column work of the table: text columns become JSON text, the copy targets
130
+ * that do not exist yet are added, and the values are copied. Returns the
131
+ * diff without the columns it added ({@link diffAfterJsonCopy}). Idempotent:
132
+ * a re-run converts nothing twice and copies only into rows whose targets
133
+ * are all NULL.
134
+ *
135
+ * The targets are added nullable and WITHOUT a default — `ADD COLUMN …
136
+ * DEFAULT`, or the type default an adapter gives a required column (a
137
+ * leaf every member of a non-null union of objects declares), fills every
138
+ * existing row, so the copy would skip them all. A model default and NOT
139
+ * NULL are returned as default / nullability changes of the diff, which the
140
+ * rest of the table's sync applies after the copy (`SET DEFAULT` / `SET NOT
141
+ * NULL` / `MODIFY COLUMN`, a table recreation on SQLite). Rows the copy
142
+ * leaves NULL keep NULL in a nullable column; a required one backfills them.
143
+ */
144
+ async function applyJsonColumnMigration(adapter, migration, diff) {
145
+ for (const column of migration.jsonify) await adapter.jsonifyTextColumn(column);
146
+ if (migration.copies.length === 0) return {
147
+ diff,
148
+ added: []
149
+ };
150
+ const toAdd = copyTargetsToAdd(migration, diff);
151
+ let added = [];
152
+ if (toAdd.length > 0) added = (await adapter.syncColumns({
153
+ added: toAdd.map((fd) => ({
154
+ ...fd,
155
+ optional: true,
156
+ defaultValue: void 0
157
+ })),
158
+ removed: [],
159
+ renamed: [],
160
+ typeChanged: [],
161
+ nullableChanged: [],
162
+ defaultChanged: [],
163
+ conflicts: []
164
+ })).added;
165
+ for (const copy of migration.copies) await adapter.copyFromJsonColumn(copy.source, copy.targets);
166
+ return {
167
+ diff: diffAfterJsonCopy(migration, diff),
168
+ added
169
+ };
170
+ }
171
+ //#endregion
4
172
  //#region src/schema/table-option-diff.ts
5
173
  /**
6
174
  * Computes the difference between desired and existing table options.
@@ -370,6 +538,10 @@ var SyncEntry = class SyncEntry {
370
538
  fkChanged;
371
539
  /** @since 0.1.141 — see {@link TSyncEntryInit.derivedChanges}. */
372
540
  derivedChanges;
541
+ /** @since 0.1.155 — see {@link TSyncEntryInit.jsonCopies}. */
542
+ jsonCopies;
543
+ /** @since 0.1.155 — see {@link TSyncEntryInit.jsonified}. */
544
+ jsonified;
373
545
  columnsAdded;
374
546
  columnsRenamed;
375
547
  columnsDropped;
@@ -404,6 +576,8 @@ var SyncEntry = class SyncEntry {
404
576
  this.fkRemoved = init.fkRemoved ?? [];
405
577
  this.fkChanged = init.fkChanged ?? [];
406
578
  this.derivedChanges = init.derivedChanges ?? [];
579
+ this.jsonCopies = init.jsonCopies ?? [];
580
+ this.jsonified = init.jsonified ?? [];
407
581
  this.columnsAdded = init.columnsAdded ?? [];
408
582
  this.columnsRenamed = init.columnsRenamed ?? [];
409
583
  this.columnsDropped = init.columnsDropped ?? [];
@@ -477,13 +651,13 @@ var SyncEntry = class SyncEntry {
477
651
  const head = this.refused ? `✖ refused: ${vp}${label}` : `✗ ${vp}${label} — error`;
478
652
  return [` ${c.red(head)}`, ...this.errors.map((err) => ` ${c.red(err)}`)];
479
653
  }
480
- /** `! PK (id) → (code) — rebuild (table is empty)` / `— skipped (safe mode)` */
654
+ /** `! PK (id) → (code) — rebuild (table is empty)` / `— rebuild (rows satisfy the new key)` / `— skipped (safe mode)` */
481
655
  printPkChange(c, mode) {
482
656
  const pk = this.pkChange;
483
657
  if (!pk) return [];
484
658
  const cols = `PK (${pk.from.join(", ")}) → (${pk.to.join(", ")})`;
485
659
  if (!pk.rebuild) return [` ${c.yellow(`! ${cols} — skipped (safe mode)`)}`];
486
- return mode === "plan" ? [` ${c.red(`! ${cols} — rebuild (table is empty)`)}`] : [` ${c.yellow(`~ ${cols} — rebuilt`)}`];
660
+ return mode === "plan" ? [` ${c.red(`! ${cols} — rebuild (${pk.populated ? "rows satisfy the new key" : "table is empty"})`)}`] : [` ${c.yellow(`~ ${cols} — rebuilt`)}`];
487
661
  }
488
662
  /**
489
663
  * `! col: t1 → t2 — drop` (plan), or `! type col (t1 → t2) — skipped (safe
@@ -509,6 +683,11 @@ var SyncEntry = class SyncEntry {
509
683
  return ` ${dc.reason === "kind" ? c.red(line) : c.yellow(line)}`;
510
684
  });
511
685
  }
686
+ /** `~ addr → addr__street, addr__zip — copy JSON values` / `~ extra — text values as JSON`. */
687
+ printJsonMigration(c, mode) {
688
+ const copy = mode === "plan" ? "copy JSON values" : "JSON values copied";
689
+ return [...this.jsonified.map((col) => ` ${c.yellow(`~ ${col} — text values as JSON`)}`), ...this.jsonCopies.map((jc) => ` ${c.yellow(`~ ${jc.from} → ${jc.to.join(", ")} — ${copy}`)}`)];
690
+ }
512
691
  /** `~ col — nullable` / `~ col — default a → b`, `— skipped (safe mode)` when pending. */
513
692
  printNullableDefaults(c) {
514
693
  const suffix = this.skipped.includes("nullable-defaults") ? " — skipped (safe mode)" : "";
@@ -566,6 +745,7 @@ var SyncEntry = class SyncEntry {
566
745
  ` ${c.cyan(`~ ${vp}${label} — alter${renameInfo}`)}`,
567
746
  ...this.columnsToAdd.map((col) => ` ${c.green(`+ ${col.physicalName} (${col.designType})${col.derived ? " derived" : ""} — add`)}`),
568
747
  ...this.columnsToRename.map((r) => ` ${c.yellow(`~ ${r.from} → ${r.to} — rename`)}`),
748
+ ...this.printJsonMigration(c, "plan"),
569
749
  ...this.printTypeChanges(c),
570
750
  ...this.printNullableDefaults(c),
571
751
  ...this.printDerivedChanges(c, "plan"),
@@ -594,13 +774,14 @@ var SyncEntry = class SyncEntry {
594
774
  ...this.columnsAdded.map((col) => ` ${c.green(`+ ${col} — added`)}`),
595
775
  ""
596
776
  ];
597
- 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) {
777
+ 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) {
598
778
  const rlabel = this.recreated ? "recreated" : "altered";
599
779
  const renameInfo = this.renamedFrom ? ` ${c.yellow(`(renamed from ${this.renamedFrom})`)}` : "";
600
780
  return [
601
781
  ` ${(this.recreated ? (s) => c.yellow(s) : (s) => c.cyan(s))(`~ ${vp}${label} — ${rlabel}${renameInfo}`)}`,
602
782
  ...this.columnsAdded.map((col) => ` ${c.green(`+ ${col} — added`)}`),
603
783
  ...this.columnsRenamed.map((col) => ` ${c.yellow(`~ ${col} — renamed`)}`),
784
+ ...this.printJsonMigration(c, "result"),
604
785
  ...this.printPkChange(c, "result"),
605
786
  ...this.printTypeChanges(c),
606
787
  ...this.printNullableDefaults(c),
@@ -642,14 +823,19 @@ function planTableInit(facts, safe, dependsOn) {
642
823
  };
643
824
  const skipped = safe ? safeModeSkips(facts) : [];
644
825
  const pk = facts.diff?.primaryKeyChanged;
645
- if (pk) init.pkChange = {
646
- from: pk.from,
647
- to: pk.to,
648
- rebuild: !skipped.includes("pk-rebuild")
649
- };
826
+ if (pk) {
827
+ init.pkChange = {
828
+ from: pk.from,
829
+ to: pk.to,
830
+ rebuild: !skipped.includes("pk-rebuild")
831
+ };
832
+ if (facts.populated === true) init.pkChange.populated = true;
833
+ }
650
834
  if (!safe) return init;
835
+ const diff = afterJsonCopy(facts);
651
836
  return {
652
837
  ...init,
838
+ ...diff && describeNullableDefaults(diff),
653
839
  columnsToDrop: [],
654
840
  typeChanges: skipped.includes("recreate") ? init.typeChanges : [],
655
841
  derivedChanges: skipped.includes("derived") ? init.derivedChanges : [],
@@ -667,6 +853,7 @@ function typeChangeStrategy(readable, diff) {
667
853
  const adapter = readable.dbAdapter;
668
854
  if (willDropRecreate(readable, diff, void 0)) return "drop";
669
855
  if (readable.syncMethod === "recreate" && adapter.recreateTable) return "recreate";
856
+ if (diff.primaryKeyChanged && !adapter.rebuildPrimaryKey && adapter.recreateTable) return "recreate";
670
857
  if (adapter.supportsColumnModify && adapter.syncColumns) return "modify";
671
858
  return "none";
672
859
  }
@@ -734,11 +921,23 @@ async function executeSyncTable(facts, safe, deps, exec) {
734
921
  await adapter.ensureTable(ensureOpts);
735
922
  init.status = "create";
736
923
  } else if (existing.length > 0) {
737
- const diff = facts.diff ?? require_column_diff.computeColumnDiff(readable.columnDescriptors, existing, adapter.typeMapper?.bind(adapter), { snapshot: storedSnapshot });
738
- if (hasFkChanges && !adapter.syncForeignKeys && adapter.recreateTable) {
739
- await adapter.recreateTable();
740
- init.recreated = true;
924
+ let diff = facts.diff ?? require_column_diff.computeColumnDiff(readable.columnDescriptors, existing, adapter.typeMapper?.bind(adapter), {
925
+ snapshot: storedSnapshot,
926
+ nativeDefaultFns: adapter.nativeDefaultFns()
927
+ });
928
+ const migration = facts.jsonMigration ?? planJsonColumnMigration(readable, diff, storedSnapshot);
929
+ if (migration.errors.length > 0) throw new Error(migration.errors.join(" "));
930
+ if (migration.copies.length > 0 || migration.jsonify.length > 0) {
931
+ phase = "JSON column copy";
932
+ const applied = await applyJsonColumnMigration(adapter, migration, diff);
933
+ diff = applied.diff;
934
+ Object.assign(init, describeJsonMigration(migration));
935
+ if (applied.added.length > 0) init.columnsAdded = applied.added;
741
936
  init.status = "alter";
937
+ phase = "Column sync";
938
+ }
939
+ if (hasFkChanges && !adapter.syncForeignKeys && adapter.recreateTable) {
940
+ await recreateKeepingRenames(adapter, diff, init);
742
941
  if (diff.primaryKeyChanged) init.pkChange = {
743
942
  ...diff.primaryKeyChanged,
744
943
  rebuild: true
@@ -759,7 +958,10 @@ async function executeSyncTable(facts, safe, deps, exec) {
759
958
  const existed = adapter.tableExists ? await adapter.tableExists() : false;
760
959
  await adapter.ensureTable(ensureOpts);
761
960
  if (!existed) init.status = "create";
762
- } else await applyColumnDiff(adapter, readable, facts.diff ?? require_column_diff.computeColumnDiff(readable.columnDescriptors, require_column_diff.snapshotToExistingColumns(storedSnapshot, readable), deps.resolveTypeMapper(adapter), { snapshot: storedSnapshot }), init, safe, deps.logger, ensureOpts);
961
+ } else await applyColumnDiff(adapter, readable, facts.diff ?? require_column_diff.computeColumnDiff(readable.columnDescriptors, require_column_diff.snapshotToExistingColumns(storedSnapshot, readable), deps.resolveTypeMapper(adapter), {
962
+ snapshot: storedSnapshot,
963
+ nativeDefaultFns: adapter.nativeDefaultFns()
964
+ }), init, safe, deps.logger, ensureOpts);
763
965
  else {
764
966
  const existed = adapter.tableExists ? await adapter.tableExists() : true;
765
967
  if (!init.recreated) {
@@ -901,6 +1103,13 @@ function willDropRecreate(readable, diff, optionDiff) {
901
1103
  if (readable.syncMethod === "drop" && (diff?.typeChanged.length ?? 0) > 0) return true;
902
1104
  return (optionDiff?.changed.some((c) => c.destructive) ?? false) && !(readable.syncMethod === "recreate" && !!adapter.recreateTable);
903
1105
  }
1106
+ /**
1107
+ * The column diff the executor applies after the table's JSON column copy —
1108
+ * its targets' NOT NULL and defaults deferred (see {@link diffAfterJsonCopy}).
1109
+ */
1110
+ function afterJsonCopy(facts) {
1111
+ return facts.diff && facts.jsonMigration ? diffAfterJsonCopy(facts.jsonMigration, facts.diff) : facts.diff;
1112
+ }
904
1113
  /** Records work safe mode skipped on the entry (see `SyncEntry.skipped`). */
905
1114
  function markSkipped(init, kind) {
906
1115
  init.skipped = [...init.skipped ?? [], kind];
@@ -918,7 +1127,8 @@ function needsDdlForNullableDefaults(adapter) {
918
1127
  * DDL either way.
919
1128
  */
920
1129
  function safeModeSkips(facts) {
921
- const { readable, diff, optionDiff, fkDiff } = facts;
1130
+ const { readable, optionDiff, fkDiff } = facts;
1131
+ const diff = afterJsonCopy(facts);
922
1132
  const adapter = readable.dbAdapter;
923
1133
  const out = [];
924
1134
  if (facts.planEntry.status === "error") return out;
@@ -932,6 +1142,16 @@ function safeModeSkips(facts) {
932
1142
  if (optionDiff?.changed.some((c) => c.destructive)) out.push("table-options");
933
1143
  return out;
934
1144
  }
1145
+ /** The plan-shaped summary of a {@link TJsonColumnMigration}. */
1146
+ function describeJsonMigration(migration) {
1147
+ return {
1148
+ jsonCopies: migration.copies.length > 0 ? migration.copies.map((c) => ({
1149
+ from: c.source,
1150
+ to: c.targets.map((t) => t.column)
1151
+ })) : void 0,
1152
+ jsonified: migration.jsonify.length > 0 ? migration.jsonify : void 0
1153
+ };
1154
+ }
935
1155
  /** The plan-shaped nullable / default change summaries of a diff. */
936
1156
  function describeNullableDefaults(diff) {
937
1157
  return {
@@ -963,6 +1183,28 @@ async function dropAndRecreate(adapter, init, name, ensureOpts) {
963
1183
  init.recreated = true;
964
1184
  init.status = "alter";
965
1185
  }
1186
+ /**
1187
+ * `recreateTable()` before the column diff is applied: it copies the rows by
1188
+ * column name, so the columns renamed in this run are renamed first — under
1189
+ * the old name their values would not be copied.
1190
+ */
1191
+ async function recreateKeepingRenames(adapter, diff, init) {
1192
+ if (diff.renamed.length > 0 && adapter.syncColumns) {
1193
+ const { renamed } = await adapter.syncColumns({
1194
+ added: [],
1195
+ removed: [],
1196
+ renamed: diff.renamed,
1197
+ typeChanged: [],
1198
+ nullableChanged: [],
1199
+ defaultChanged: [],
1200
+ conflicts: []
1201
+ });
1202
+ init.columnsRenamed = renamed;
1203
+ }
1204
+ await adapter.recreateTable();
1205
+ init.recreated = true;
1206
+ init.status = "alter";
1207
+ }
966
1208
  async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensureOpts) {
967
1209
  const name = readable.tableName;
968
1210
  if (diff.conflicts.length > 0) {
@@ -981,11 +1223,8 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
981
1223
  const cols = init.typeChanges.map((tc) => `${tc.column}: ${tc.fromType} → ${tc.toType}`).join(", ");
982
1224
  logger.warn?.(`[schema-sync] Type change on "${name}" (${cols}) — drop-and-recreate skipped (safe mode)`);
983
1225
  } else await dropAndRecreate(adapter, init, name, ensureOpts);
984
- else if (strategy === "recreate") {
985
- await adapter.recreateTable();
986
- init.recreated = true;
987
- init.status = "alter";
988
- } else if (strategy === "modify") {
1226
+ else if (strategy === "recreate") await recreateKeepingRenames(adapter, diff, init);
1227
+ else if (strategy === "modify") {
989
1228
  needsSyncColumns = true;
990
1229
  init.status = "alter";
991
1230
  } else {
@@ -999,11 +1238,8 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
999
1238
  if (!safe && !init.recreated && init.status !== "error") if (adapter.supportsColumnModify && adapter.syncColumns) {
1000
1239
  needsSyncColumns = true;
1001
1240
  init.status = "alter";
1002
- } else if (adapter.recreateTable) {
1003
- await adapter.recreateTable();
1004
- init.recreated = true;
1005
- init.status = "alter";
1006
- } else init.status = "alter";
1241
+ } else if (adapter.recreateTable) await recreateKeepingRenames(adapter, diff, init);
1242
+ else init.status = "alter";
1007
1243
  else if (init.status !== "error") {
1008
1244
  if (safe && !init.recreated && needsDdlForNullableDefaults(adapter)) {
1009
1245
  Object.assign(init, describeNullableDefaults(diff));
@@ -1015,7 +1251,7 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
1015
1251
  }
1016
1252
  if (!init.recreated && init.status !== "error" && (diff.added.length > 0 || diff.renamed.length > 0 || needsSyncColumns) && adapter.syncColumns) {
1017
1253
  const syncResult = await adapter.syncColumns(diff);
1018
- init.columnsAdded = syncResult.added;
1254
+ init.columnsAdded = [...init.columnsAdded ?? [], ...syncResult.added];
1019
1255
  init.columnsRenamed = syncResult.renamed;
1020
1256
  if (syncResult.added.length > 0 || (syncResult.renamed?.length ?? 0) > 0 || needsSyncColumns) init.status = "alter";
1021
1257
  }
@@ -1580,8 +1816,18 @@ var SchemaSync = class {
1580
1816
  init.columnsToAdd = [...readable.columnDescriptors];
1581
1817
  } else if (facts.existing && facts.existing.length > 0) {
1582
1818
  const typeMapper = adapter.typeMapper?.bind(adapter);
1583
- facts.diff = require_column_diff.computeColumnDiff(readable.columnDescriptors, facts.existing, typeMapper, { snapshot: storedSnapshot });
1819
+ facts.diff = require_column_diff.computeColumnDiff(readable.columnDescriptors, facts.existing, typeMapper, {
1820
+ snapshot: storedSnapshot,
1821
+ nativeDefaultFns: adapter.nativeDefaultFns()
1822
+ });
1584
1823
  this.populatePlanFromDiff(facts.diff, init, readable, safe);
1824
+ const migration = planJsonColumnMigration(readable, facts.diff, storedSnapshot);
1825
+ facts.jsonMigration = migration;
1826
+ Object.assign(init, describeJsonMigration(migration));
1827
+ if (migration.errors.length > 0) {
1828
+ init.status = "error";
1829
+ init.errors = [...init.errors ?? [], ...migration.errors];
1830
+ } else if (init.status === "in-sync" && (migration.copies.length > 0 || migration.jsonify.length > 0)) init.status = "alter";
1585
1831
  }
1586
1832
  } else if (adapter.syncColumns) if (!storedSnapshot) {
1587
1833
  if (!pendingRename) {
@@ -1592,7 +1838,10 @@ var SchemaSync = class {
1592
1838
  }
1593
1839
  } else {
1594
1840
  const existing = require_column_diff.snapshotToExistingColumns(storedSnapshot, readable);
1595
- facts.diff = require_column_diff.computeColumnDiff(readable.columnDescriptors, existing, this.resolveTypeMapper(adapter), { snapshot: storedSnapshot });
1841
+ facts.diff = require_column_diff.computeColumnDiff(readable.columnDescriptors, existing, this.resolveTypeMapper(adapter), {
1842
+ snapshot: storedSnapshot,
1843
+ nativeDefaultFns: adapter.nativeDefaultFns()
1844
+ });
1596
1845
  this.populatePlanFromDiff(facts.diff, init, readable, safe);
1597
1846
  }
1598
1847
  else if (adapter.tableExists) {
@@ -1627,7 +1876,16 @@ var SchemaSync = class {
1627
1876
  }, fk.existing)
1628
1877
  }));
1629
1878
  }
1630
- if (facts.diff?.primaryKeyChanged) facts.populated = await adapter.hasRows(pendingRename ? dbName : void 0) ?? "unknown";
1879
+ if (facts.diff?.primaryKeyChanged) {
1880
+ const tableName = pendingRename ? dbName : void 0;
1881
+ facts.populated = await adapter.hasRows(tableName) ?? "unknown";
1882
+ const { added, renamed } = facts.diff;
1883
+ const { to } = facts.diff.primaryKeyChanged;
1884
+ if (facts.populated === true && adapter.countKeyViolations && !added.some((f) => to.includes(f.physicalName))) {
1885
+ const liveName = new Map(renamed.map((r) => [r.field.physicalName, r.oldName]));
1886
+ facts.keyViolations = await adapter.countKeyViolations(to.map((c) => liveName.get(c) ?? c), tableName);
1887
+ }
1888
+ }
1631
1889
  facts.dropsTable = !!facts.diff?.primaryKeyChanged || willDropRecreate(readable, facts.diff, facts.optionDiff);
1632
1890
  if (facts.diff?.primaryKeyChanged || probeRecreateInbound && facts.dropsTable) facts.inboundFks = await adapter.getReferencingForeignKeys?.(dbName);
1633
1891
  if (adapter.getObjectKind) facts.objectKind = await adapter.getObjectKind(dbName);
@@ -1644,7 +1902,7 @@ var SchemaSync = class {
1644
1902
  const pk = t.diff?.primaryKeyChanged;
1645
1903
  if (pk) {
1646
1904
  const label = pkLabel(pk);
1647
- 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.`);
1905
+ 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.`);
1648
1906
  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.`);
1649
1907
  if (!safe) {
1650
1908
  const liveInbound = (t.inboundFks ?? []).filter((fk) => {
package/dist/sync.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { $t as DbSpace, At as TReferencingForeignKey, Ft as TTableOptionDiff, Gn as TGenericLogger, U as TDbFieldMeta, et as TDbObjectKind, gt as TEnsureTableOptions, kt as TPrimaryKeyChange, mt as TDerivedChangeReason, vt as TExistingForeignKey, yt as TExistingTableOption } from "./buckets-jy8BfswO.cjs";
2
- import { _ as computeTableSnapshot, a as hasForeignKeyChanges, b as snapshotToExistingTableOptions, c as TFieldSnapshot, d as TViewColumnSnapshot, f as TViewJoinSnapshot, g as computeTableHash, h as computeSchemaHash, i as fkKey, l as TForeignKeySnapshot, m as canonicalizeQueryNode, n as computeForeignKeyDiff, o as computeColumnDiff, p as TViewSnapshot, t as TForeignKeyDiff, u as TTableSnapshot, v as computeViewSnapshot, y as snapshotToExistingColumns } from "./fk-diff-D1mrLMjU.cjs";
1
+ import { At as TPrimaryKeyChange, It as TTableOptionDiff, U as TDbFieldMeta, et as TDbObjectKind, gt as TEnsureTableOptions, jt as TReferencingForeignKey, mt as TDerivedChangeReason, qn as TGenericLogger, tn as DbSpace, vt as TExistingForeignKey, yt as TExistingTableOption } from "./buckets-DcWm3SLt.cjs";
2
+ import { _ as computeTableSnapshot, a as hasForeignKeyChanges, b as snapshotToExistingTableOptions, c as TFieldSnapshot, d as TViewColumnSnapshot, f as TViewJoinSnapshot, g as computeTableHash, h as computeSchemaHash, i as fkKey, l as TForeignKeySnapshot, m as canonicalizeQueryNode, n as computeForeignKeyDiff, o as computeColumnDiff, p as TViewSnapshot, t as TForeignKeyDiff, u as TTableSnapshot, v as computeViewSnapshot, y as snapshotToExistingColumns } from "./fk-diff-DblOPBLS.cjs";
3
3
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/schema/sync-entry.d.ts
@@ -77,6 +77,22 @@ interface TSyncEntryInit {
77
77
  * @since 0.1.141
78
78
  */
79
79
  derivedChanges?: TSyncDerivedChange[];
80
+ /**
81
+ * Text columns of the atscript-db ≤ 0.1.154 union layout whose JSON values
82
+ * the run copies into the columns that replace them, before the old
83
+ * column is dropped (see `planJsonColumnMigration`).
84
+ * @since 0.1.155
85
+ */
86
+ jsonCopies?: Array<{
87
+ from: string;
88
+ to: string[];
89
+ }>;
90
+ /**
91
+ * Text columns of that layout becoming JSON columns: their plain-text
92
+ * values are rewritten as JSON strings first.
93
+ * @since 0.1.155
94
+ */
95
+ jsonified?: string[];
80
96
  columnsAdded?: string[];
81
97
  columnsRenamed?: string[];
82
98
  columnsDropped?: string[];
@@ -85,14 +101,17 @@ interface TSyncEntryInit {
85
101
  renamedFrom?: string;
86
102
  /**
87
103
  * The table's primary-key field set changes. `rebuild: true` when sync
88
- * rebuilds the key (empty table); `false` when the rebuild is skipped
89
- * (safe mode). A populated table is refused instead (see `refused`).
104
+ * rebuilds the key; `false` when the rebuild is skipped (safe mode).
105
+ * `populated: true` (since 0.1.155) when the table has rows — they satisfy
106
+ * the new key; a populated table whose rows do not is refused instead (see
107
+ * `refused`).
90
108
  * @since 0.1.128
91
109
  */
92
110
  pkChange?: {
93
111
  from: string[];
94
112
  to: string[];
95
113
  rebuild: boolean;
114
+ populated?: boolean;
96
115
  };
97
116
  /**
98
117
  * The work safe mode skipped on this table: `"pk-rebuild"` (`pkChange`
@@ -174,6 +193,13 @@ declare class SyncEntry {
174
193
  }>;
175
194
  /** @since 0.1.141 — see {@link TSyncEntryInit.derivedChanges}. */
176
195
  readonly derivedChanges: TSyncDerivedChange[];
196
+ /** @since 0.1.155 — see {@link TSyncEntryInit.jsonCopies}. */
197
+ readonly jsonCopies: Array<{
198
+ from: string;
199
+ to: string[];
200
+ }>;
201
+ /** @since 0.1.155 — see {@link TSyncEntryInit.jsonified}. */
202
+ readonly jsonified: string[];
177
203
  readonly columnsAdded: string[];
178
204
  readonly columnsRenamed: string[];
179
205
  readonly columnsDropped: string[];
@@ -185,6 +211,7 @@ declare class SyncEntry {
185
211
  from: string[];
186
212
  to: string[];
187
213
  rebuild: boolean;
214
+ populated?: boolean;
188
215
  };
189
216
  /** @since 0.1.128 — see {@link TSyncEntryInit.skipped}. */
190
217
  readonly skipped: ReadonlyArray<TSyncSkippedWork>;
@@ -229,7 +256,7 @@ declare class SyncEntry {
229
256
  print(mode: "plan" | "result", colors?: TSyncColors): string[];
230
257
  private labelAndPrefix;
231
258
  private printError;
232
- /** `! PK (id) → (code) — rebuild (table is empty)` / `— skipped (safe mode)` */
259
+ /** `! PK (id) → (code) — rebuild (table is empty)` / `— rebuild (rows satisfy the new key)` / `— skipped (safe mode)` */
233
260
  private printPkChange;
234
261
  /**
235
262
  * `! col: t1 → t2 — drop` (plan), or `! type col (t1 → t2) — skipped (safe
@@ -241,6 +268,8 @@ declare class SyncEntry {
241
268
  * `— rebuilt` (result), or `! … — skipped (safe mode)` when pending.
242
269
  */
243
270
  private printDerivedChanges;
271
+ /** `~ addr → addr__street, addr__zip — copy JSON values` / `~ extra — text values as JSON`. */
272
+ private printJsonMigration;
244
273
  /** `~ col — nullable` / `~ col — default a → b`, `— skipped (safe mode)` when pending. */
245
274
  private printNullableDefaults;
246
275
  /**
package/dist/sync.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { $t as DbSpace, At as TReferencingForeignKey, Ft as TTableOptionDiff, Gn as TGenericLogger, U as TDbFieldMeta, et as TDbObjectKind, gt as TEnsureTableOptions, kt as TPrimaryKeyChange, mt as TDerivedChangeReason, vt as TExistingForeignKey, yt as TExistingTableOption } from "./buckets-CBKL_ZWE.mjs";
2
- import { _ as computeTableSnapshot, a as hasForeignKeyChanges, b as snapshotToExistingTableOptions, c as TFieldSnapshot, d as TViewColumnSnapshot, f as TViewJoinSnapshot, g as computeTableHash, h as computeSchemaHash, i as fkKey, l as TForeignKeySnapshot, m as canonicalizeQueryNode, n as computeForeignKeyDiff, o as computeColumnDiff, p as TViewSnapshot, t as TForeignKeyDiff, u as TTableSnapshot, v as computeViewSnapshot, y as snapshotToExistingColumns } from "./fk-diff-BIJ8l_hF.mjs";
1
+ import { At as TPrimaryKeyChange, It as TTableOptionDiff, U as TDbFieldMeta, et as TDbObjectKind, gt as TEnsureTableOptions, jt as TReferencingForeignKey, mt as TDerivedChangeReason, qn as TGenericLogger, tn as DbSpace, vt as TExistingForeignKey, yt as TExistingTableOption } from "./buckets-goCoky8v.mjs";
2
+ import { _ as computeTableSnapshot, a as hasForeignKeyChanges, b as snapshotToExistingTableOptions, c as TFieldSnapshot, d as TViewColumnSnapshot, f as TViewJoinSnapshot, g as computeTableHash, h as computeSchemaHash, i as fkKey, l as TForeignKeySnapshot, m as canonicalizeQueryNode, n as computeForeignKeyDiff, o as computeColumnDiff, p as TViewSnapshot, t as TForeignKeyDiff, u as TTableSnapshot, v as computeViewSnapshot, y as snapshotToExistingColumns } from "./fk-diff-BYLNWoF9.mjs";
3
3
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/schema/sync-entry.d.ts
@@ -77,6 +77,22 @@ interface TSyncEntryInit {
77
77
  * @since 0.1.141
78
78
  */
79
79
  derivedChanges?: TSyncDerivedChange[];
80
+ /**
81
+ * Text columns of the atscript-db ≤ 0.1.154 union layout whose JSON values
82
+ * the run copies into the columns that replace them, before the old
83
+ * column is dropped (see `planJsonColumnMigration`).
84
+ * @since 0.1.155
85
+ */
86
+ jsonCopies?: Array<{
87
+ from: string;
88
+ to: string[];
89
+ }>;
90
+ /**
91
+ * Text columns of that layout becoming JSON columns: their plain-text
92
+ * values are rewritten as JSON strings first.
93
+ * @since 0.1.155
94
+ */
95
+ jsonified?: string[];
80
96
  columnsAdded?: string[];
81
97
  columnsRenamed?: string[];
82
98
  columnsDropped?: string[];
@@ -85,14 +101,17 @@ interface TSyncEntryInit {
85
101
  renamedFrom?: string;
86
102
  /**
87
103
  * The table's primary-key field set changes. `rebuild: true` when sync
88
- * rebuilds the key (empty table); `false` when the rebuild is skipped
89
- * (safe mode). A populated table is refused instead (see `refused`).
104
+ * rebuilds the key; `false` when the rebuild is skipped (safe mode).
105
+ * `populated: true` (since 0.1.155) when the table has rows — they satisfy
106
+ * the new key; a populated table whose rows do not is refused instead (see
107
+ * `refused`).
90
108
  * @since 0.1.128
91
109
  */
92
110
  pkChange?: {
93
111
  from: string[];
94
112
  to: string[];
95
113
  rebuild: boolean;
114
+ populated?: boolean;
96
115
  };
97
116
  /**
98
117
  * The work safe mode skipped on this table: `"pk-rebuild"` (`pkChange`
@@ -174,6 +193,13 @@ declare class SyncEntry {
174
193
  }>;
175
194
  /** @since 0.1.141 — see {@link TSyncEntryInit.derivedChanges}. */
176
195
  readonly derivedChanges: TSyncDerivedChange[];
196
+ /** @since 0.1.155 — see {@link TSyncEntryInit.jsonCopies}. */
197
+ readonly jsonCopies: Array<{
198
+ from: string;
199
+ to: string[];
200
+ }>;
201
+ /** @since 0.1.155 — see {@link TSyncEntryInit.jsonified}. */
202
+ readonly jsonified: string[];
177
203
  readonly columnsAdded: string[];
178
204
  readonly columnsRenamed: string[];
179
205
  readonly columnsDropped: string[];
@@ -185,6 +211,7 @@ declare class SyncEntry {
185
211
  from: string[];
186
212
  to: string[];
187
213
  rebuild: boolean;
214
+ populated?: boolean;
188
215
  };
189
216
  /** @since 0.1.128 — see {@link TSyncEntryInit.skipped}. */
190
217
  readonly skipped: ReadonlyArray<TSyncSkippedWork>;
@@ -229,7 +256,7 @@ declare class SyncEntry {
229
256
  print(mode: "plan" | "result", colors?: TSyncColors): string[];
230
257
  private labelAndPrefix;
231
258
  private printError;
232
- /** `! PK (id) → (code) — rebuild (table is empty)` / `— skipped (safe mode)` */
259
+ /** `! PK (id) → (code) — rebuild (table is empty)` / `— rebuild (rows satisfy the new key)` / `— skipped (safe mode)` */
233
260
  private printPkChange;
234
261
  /**
235
262
  * `! col: t1 → t2 — drop` (plan), or `! type col (t1 → t2) — skipped (safe
@@ -241,6 +268,8 @@ declare class SyncEntry {
241
268
  * `— rebuilt` (result), or `! … — skipped (safe mode)` when pending.
242
269
  */
243
270
  private printDerivedChanges;
271
+ /** `~ addr → addr__street, addr__zip — copy JSON values` / `~ extra — text values as JSON`. */
272
+ private printJsonMigration;
244
273
  /** `~ col — nullable` / `~ col — default a → b`, `— skipped (safe mode)` when pending. */
245
274
  private printNullableDefaults;
246
275
  /**