@atscript/db 0.1.154 → 0.1.156
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-CBKL_ZWE.d.mts → buckets-8mG67Mtw.d.mts} +168 -16
- package/dist/{buckets-jy8BfswO.d.cts → buckets-B4T_alUe.d.cts} +168 -16
- package/dist/{column-diff-DAIAq6XR.mjs → column-diff-DR1glykD.mjs} +419 -75
- package/dist/{column-diff-qnzx1o3c.cjs → column-diff-DpeUbhEI.cjs} +430 -74
- package/dist/{fk-diff-BIJ8l_hF.d.mts → fk-diff-5gX42hBt.d.mts} +2 -1
- package/dist/{fk-diff-D1mrLMjU.d.cts → fk-diff-BWdQ7kRZ.d.cts} +2 -1
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +15 -9
- package/dist/index.d.mts +15 -9
- package/dist/index.mjs +3 -3
- package/dist/plugin.cjs +21 -4
- package/dist/plugin.mjs +21 -4
- package/dist/rel.d.cts +2 -2
- package/dist/rel.d.mts +2 -2
- package/dist/{relation-helpers-BUsxNNGU.d.mts → relation-helpers-17_2f5hX.d.mts} +1 -1
- package/dist/{relation-helpers-D_8bydc2.d.cts → relation-helpers-BLBrLQYi.d.cts} +1 -1
- package/dist/{search-fields-DBIZKwyb.mjs → search-fields-CE8cmj9X.mjs} +1 -0
- package/dist/{search-fields-DwrsJUC1.cjs → search-fields-CT_UZ9fg.cjs} +1 -0
- 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 +317 -35
- package/dist/sync.d.cts +34 -5
- package/dist/sync.d.mts +34 -5
- package/dist/sync.mjs +317 -35
- package/dist/{validation-utils-DqZAH6aT.mjs → validation-utils-BGjqc2jU.mjs} +24 -6
- package/dist/{validation-utils-debIYpq0.cjs → validation-utils-CtLNlBtB.cjs} +29 -5
- package/dist/{validator-BACewoRj.mjs → validator-B3fPJjS1.mjs} +178 -10
- package/dist/{validator-BxWwh3wf.cjs → validator-B4YDLUkj.cjs} +201 -9
- package/dist/validator.cjs +1 -1
- package/dist/validator.mjs +1 -1
- package/package.json +6 -6
package/dist/sync.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
1
|
+
import { At as TPrimaryKeyChange, Jn as TGenericLogger, Lt as TTableOptionDiff, U as TDbFieldMeta, et as TDbObjectKind, gt as TEnsureTableOptions, jt as TReferencingForeignKey, mt as TDerivedChangeReason, nn as DbSpace, vt as TExistingForeignKey, yt as TExistingTableOption } from "./buckets-8mG67Mtw.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-5gX42hBt.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
|
|
89
|
-
* (
|
|
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.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-DR1glykD.mjs";
|
|
3
|
+
import { f as unionValueMembers } from "./validator-B3fPJjS1.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
|
|
@@ -746,8 +945,11 @@ async function executeSyncTable(facts, safe, deps, exec) {
|
|
|
746
945
|
if (hasFkChanges && fkDiff && adapter.dropForeignKeys) {
|
|
747
946
|
const keysToDrop = [...fkDiff.removed.map((fk) => fkKey(fk.fields)), ...fkDiff.changed.map((fk) => fkKey(fkColumns(fk.desired).fields))];
|
|
748
947
|
if (keysToDrop.length > 0) {
|
|
749
|
-
|
|
750
|
-
|
|
948
|
+
const fresh = keysToDrop.filter((key) => !droppedFks.includes(key));
|
|
949
|
+
if (fresh.length > 0) {
|
|
950
|
+
await adapter.dropForeignKeys(fresh);
|
|
951
|
+
droppedFks.push(...fresh);
|
|
952
|
+
}
|
|
751
953
|
init.status = "alter";
|
|
752
954
|
}
|
|
753
955
|
}
|
|
@@ -758,7 +960,10 @@ async function executeSyncTable(facts, safe, deps, exec) {
|
|
|
758
960
|
const existed = adapter.tableExists ? await adapter.tableExists() : false;
|
|
759
961
|
await adapter.ensureTable(ensureOpts);
|
|
760
962
|
if (!existed) init.status = "create";
|
|
761
|
-
} else await applyColumnDiff(adapter, readable, facts.diff ?? computeColumnDiff(readable.columnDescriptors, snapshotToExistingColumns(storedSnapshot, readable), deps.resolveTypeMapper(adapter), {
|
|
963
|
+
} else await applyColumnDiff(adapter, readable, facts.diff ?? computeColumnDiff(readable.columnDescriptors, snapshotToExistingColumns(storedSnapshot, readable), deps.resolveTypeMapper(adapter), {
|
|
964
|
+
snapshot: storedSnapshot,
|
|
965
|
+
nativeDefaultFns: adapter.nativeDefaultFns()
|
|
966
|
+
}), init, safe, deps.logger, ensureOpts);
|
|
762
967
|
else {
|
|
763
968
|
const existed = adapter.tableExists ? await adapter.tableExists() : true;
|
|
764
969
|
if (!init.recreated) {
|
|
@@ -797,9 +1002,10 @@ async function executeSyncTable(facts, safe, deps, exec) {
|
|
|
797
1002
|
}
|
|
798
1003
|
}
|
|
799
1004
|
}
|
|
1005
|
+
if (init.pkChange && facts.populated === true) init.pkChange.populated = true;
|
|
800
1006
|
if (init.status === "error") return new SyncEntry(init);
|
|
801
1007
|
phase = "Index/FK sync";
|
|
802
|
-
await adapter.syncIndexes();
|
|
1008
|
+
await adapter.syncIndexes(keptIndexes(init));
|
|
803
1009
|
if (adapter.syncForeignKeys && !exec.deferForeignKeysTo) await adapter.syncForeignKeys();
|
|
804
1010
|
if (adapter.afterSyncTable) await adapter.afterSyncTable();
|
|
805
1011
|
} catch (error) {
|
|
@@ -900,13 +1106,40 @@ function willDropRecreate(readable, diff, optionDiff) {
|
|
|
900
1106
|
if (readable.syncMethod === "drop" && (diff?.typeChanged.length ?? 0) > 0) return true;
|
|
901
1107
|
return (optionDiff?.changed.some((c) => c.destructive) ?? false) && !(readable.syncMethod === "recreate" && !!adapter.recreateTable);
|
|
902
1108
|
}
|
|
1109
|
+
/**
|
|
1110
|
+
* The column diff the executor applies after the table's JSON column copy —
|
|
1111
|
+
* its targets' NOT NULL and defaults deferred (see {@link diffAfterJsonCopy}).
|
|
1112
|
+
*/
|
|
1113
|
+
function afterJsonCopy(facts) {
|
|
1114
|
+
return facts.diff && facts.jsonMigration ? diffAfterJsonCopy(facts.jsonMigration, facts.diff) : facts.diff;
|
|
1115
|
+
}
|
|
903
1116
|
/** Records work safe mode skipped on the entry (see `SyncEntry.skipped`). */
|
|
904
1117
|
function markSkipped(init, kind) {
|
|
905
1118
|
init.skipped = [...init.skipped ?? [], kind];
|
|
906
1119
|
}
|
|
907
|
-
/**
|
|
1120
|
+
/**
|
|
1121
|
+
* Whether nullable/default changes need DDL on this adapter: in-place modify,
|
|
1122
|
+
* or a recreate on a live-introspected one (SQLite). Snapshot-based adapters
|
|
1123
|
+
* (MongoDB) have no column constraint to change — they only update the
|
|
1124
|
+
* snapshot, even with a `recreateTable`.
|
|
1125
|
+
*/
|
|
908
1126
|
function needsDdlForNullableDefaults(adapter) {
|
|
909
|
-
return !!adapter.supportsColumnModify && !!adapter.syncColumns || !!adapter.recreateTable;
|
|
1127
|
+
return !!adapter.supportsColumnModify && !!adapter.syncColumns || !!adapter.recreateTable && !!adapter.getExistingColumns;
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* The indexes `syncIndexes` keeps as they are because safe mode skipped
|
|
1131
|
+
* the change they depend on — a skipped key rebuild, the columns of a
|
|
1132
|
+
* skipped type-change recreate (see `TSyncIndexesOptions`).
|
|
1133
|
+
*/
|
|
1134
|
+
function keptIndexes(init) {
|
|
1135
|
+
const skipped = init.skipped ?? [];
|
|
1136
|
+
const keepPrimaryKey = skipped.includes("pk-rebuild");
|
|
1137
|
+
const keepColumns = skipped.includes("recreate") ? (init.typeChanges ?? []).map((tc) => tc.column) : [];
|
|
1138
|
+
if (!keepPrimaryKey && keepColumns.length === 0) return;
|
|
1139
|
+
return {
|
|
1140
|
+
keepPrimaryKey,
|
|
1141
|
+
keepColumns
|
|
1142
|
+
};
|
|
910
1143
|
}
|
|
911
1144
|
/**
|
|
912
1145
|
* The work `run({ safe: true })` skips on this table, in the order the
|
|
@@ -917,7 +1150,8 @@ function needsDdlForNullableDefaults(adapter) {
|
|
|
917
1150
|
* DDL either way.
|
|
918
1151
|
*/
|
|
919
1152
|
function safeModeSkips(facts) {
|
|
920
|
-
const { readable,
|
|
1153
|
+
const { readable, optionDiff, fkDiff } = facts;
|
|
1154
|
+
const diff = afterJsonCopy(facts);
|
|
921
1155
|
const adapter = readable.dbAdapter;
|
|
922
1156
|
const out = [];
|
|
923
1157
|
if (facts.planEntry.status === "error") return out;
|
|
@@ -931,6 +1165,16 @@ function safeModeSkips(facts) {
|
|
|
931
1165
|
if (optionDiff?.changed.some((c) => c.destructive)) out.push("table-options");
|
|
932
1166
|
return out;
|
|
933
1167
|
}
|
|
1168
|
+
/** The plan-shaped summary of a {@link TJsonColumnMigration}. */
|
|
1169
|
+
function describeJsonMigration(migration) {
|
|
1170
|
+
return {
|
|
1171
|
+
jsonCopies: migration.copies.length > 0 ? migration.copies.map((c) => ({
|
|
1172
|
+
from: c.source,
|
|
1173
|
+
to: c.targets.map((t) => t.column)
|
|
1174
|
+
})) : void 0,
|
|
1175
|
+
jsonified: migration.jsonify.length > 0 ? migration.jsonify : void 0
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
934
1178
|
/** The plan-shaped nullable / default change summaries of a diff. */
|
|
935
1179
|
function describeNullableDefaults(diff) {
|
|
936
1180
|
return {
|
|
@@ -962,6 +1206,28 @@ async function dropAndRecreate(adapter, init, name, ensureOpts) {
|
|
|
962
1206
|
init.recreated = true;
|
|
963
1207
|
init.status = "alter";
|
|
964
1208
|
}
|
|
1209
|
+
/**
|
|
1210
|
+
* `recreateTable()` before the column diff is applied: it copies the rows by
|
|
1211
|
+
* column name, so the columns renamed in this run are renamed first — under
|
|
1212
|
+
* the old name their values would not be copied.
|
|
1213
|
+
*/
|
|
1214
|
+
async function recreateKeepingRenames(adapter, diff, init) {
|
|
1215
|
+
if (diff.renamed.length > 0 && adapter.syncColumns) {
|
|
1216
|
+
const { renamed } = await adapter.syncColumns({
|
|
1217
|
+
added: [],
|
|
1218
|
+
removed: [],
|
|
1219
|
+
renamed: diff.renamed,
|
|
1220
|
+
typeChanged: [],
|
|
1221
|
+
nullableChanged: [],
|
|
1222
|
+
defaultChanged: [],
|
|
1223
|
+
conflicts: []
|
|
1224
|
+
});
|
|
1225
|
+
init.columnsRenamed = renamed;
|
|
1226
|
+
}
|
|
1227
|
+
await adapter.recreateTable();
|
|
1228
|
+
init.recreated = true;
|
|
1229
|
+
init.status = "alter";
|
|
1230
|
+
}
|
|
965
1231
|
async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensureOpts) {
|
|
966
1232
|
const name = readable.tableName;
|
|
967
1233
|
if (diff.conflicts.length > 0) {
|
|
@@ -980,11 +1246,8 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
|
|
|
980
1246
|
const cols = init.typeChanges.map((tc) => `${tc.column}: ${tc.fromType} → ${tc.toType}`).join(", ");
|
|
981
1247
|
logger.warn?.(`[schema-sync] Type change on "${name}" (${cols}) — drop-and-recreate skipped (safe mode)`);
|
|
982
1248
|
} 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") {
|
|
1249
|
+
else if (strategy === "recreate") await recreateKeepingRenames(adapter, diff, init);
|
|
1250
|
+
else if (strategy === "modify") {
|
|
988
1251
|
needsSyncColumns = true;
|
|
989
1252
|
init.status = "alter";
|
|
990
1253
|
} else {
|
|
@@ -998,11 +1261,8 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
|
|
|
998
1261
|
if (!safe && !init.recreated && init.status !== "error") if (adapter.supportsColumnModify && adapter.syncColumns) {
|
|
999
1262
|
needsSyncColumns = true;
|
|
1000
1263
|
init.status = "alter";
|
|
1001
|
-
} else if (adapter
|
|
1002
|
-
|
|
1003
|
-
init.recreated = true;
|
|
1004
|
-
init.status = "alter";
|
|
1005
|
-
} else init.status = "alter";
|
|
1264
|
+
} else if (needsDdlForNullableDefaults(adapter)) await recreateKeepingRenames(adapter, diff, init);
|
|
1265
|
+
else init.status = "alter";
|
|
1006
1266
|
else if (init.status !== "error") {
|
|
1007
1267
|
if (safe && !init.recreated && needsDdlForNullableDefaults(adapter)) {
|
|
1008
1268
|
Object.assign(init, describeNullableDefaults(diff));
|
|
@@ -1014,7 +1274,7 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
|
|
|
1014
1274
|
}
|
|
1015
1275
|
if (!init.recreated && init.status !== "error" && (diff.added.length > 0 || diff.renamed.length > 0 || needsSyncColumns) && adapter.syncColumns) {
|
|
1016
1276
|
const syncResult = await adapter.syncColumns(diff);
|
|
1017
|
-
init.columnsAdded = syncResult.added;
|
|
1277
|
+
init.columnsAdded = [...init.columnsAdded ?? [], ...syncResult.added];
|
|
1018
1278
|
init.columnsRenamed = syncResult.renamed;
|
|
1019
1279
|
if (syncResult.added.length > 0 || (syncResult.renamed?.length ?? 0) > 0 || needsSyncColumns) init.status = "alter";
|
|
1020
1280
|
}
|
|
@@ -1579,8 +1839,18 @@ var SchemaSync = class {
|
|
|
1579
1839
|
init.columnsToAdd = [...readable.columnDescriptors];
|
|
1580
1840
|
} else if (facts.existing && facts.existing.length > 0) {
|
|
1581
1841
|
const typeMapper = adapter.typeMapper?.bind(adapter);
|
|
1582
|
-
facts.diff = computeColumnDiff(readable.columnDescriptors, facts.existing, typeMapper, {
|
|
1842
|
+
facts.diff = computeColumnDiff(readable.columnDescriptors, facts.existing, typeMapper, {
|
|
1843
|
+
snapshot: storedSnapshot,
|
|
1844
|
+
nativeDefaultFns: adapter.nativeDefaultFns()
|
|
1845
|
+
});
|
|
1583
1846
|
this.populatePlanFromDiff(facts.diff, init, readable, safe);
|
|
1847
|
+
const migration = planJsonColumnMigration(readable, facts.diff, storedSnapshot);
|
|
1848
|
+
facts.jsonMigration = migration;
|
|
1849
|
+
Object.assign(init, describeJsonMigration(migration));
|
|
1850
|
+
if (migration.errors.length > 0) {
|
|
1851
|
+
init.status = "error";
|
|
1852
|
+
init.errors = [...init.errors ?? [], ...migration.errors];
|
|
1853
|
+
} else if (init.status === "in-sync" && (migration.copies.length > 0 || migration.jsonify.length > 0)) init.status = "alter";
|
|
1584
1854
|
}
|
|
1585
1855
|
} else if (adapter.syncColumns) if (!storedSnapshot) {
|
|
1586
1856
|
if (!pendingRename) {
|
|
@@ -1591,7 +1861,10 @@ var SchemaSync = class {
|
|
|
1591
1861
|
}
|
|
1592
1862
|
} else {
|
|
1593
1863
|
const existing = snapshotToExistingColumns(storedSnapshot, readable);
|
|
1594
|
-
facts.diff = computeColumnDiff(readable.columnDescriptors, existing, this.resolveTypeMapper(adapter), {
|
|
1864
|
+
facts.diff = computeColumnDiff(readable.columnDescriptors, existing, this.resolveTypeMapper(adapter), {
|
|
1865
|
+
snapshot: storedSnapshot,
|
|
1866
|
+
nativeDefaultFns: adapter.nativeDefaultFns()
|
|
1867
|
+
});
|
|
1595
1868
|
this.populatePlanFromDiff(facts.diff, init, readable, safe);
|
|
1596
1869
|
}
|
|
1597
1870
|
else if (adapter.tableExists) {
|
|
@@ -1626,7 +1899,16 @@ var SchemaSync = class {
|
|
|
1626
1899
|
}, fk.existing)
|
|
1627
1900
|
}));
|
|
1628
1901
|
}
|
|
1629
|
-
if (facts.diff?.primaryKeyChanged)
|
|
1902
|
+
if (facts.diff?.primaryKeyChanged) {
|
|
1903
|
+
const tableName = pendingRename ? dbName : void 0;
|
|
1904
|
+
facts.populated = await adapter.hasRows(tableName) ?? "unknown";
|
|
1905
|
+
const { added, renamed } = facts.diff;
|
|
1906
|
+
const { to } = facts.diff.primaryKeyChanged;
|
|
1907
|
+
if (facts.populated === true && adapter.countKeyViolations && !added.some((f) => to.includes(f.physicalName))) {
|
|
1908
|
+
const liveName = new Map(renamed.map((r) => [r.field.physicalName, r.oldName]));
|
|
1909
|
+
facts.keyViolations = await adapter.countKeyViolations(to.map((c) => liveName.get(c) ?? c), tableName);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1630
1912
|
facts.dropsTable = !!facts.diff?.primaryKeyChanged || willDropRecreate(readable, facts.diff, facts.optionDiff);
|
|
1631
1913
|
if (facts.diff?.primaryKeyChanged || probeRecreateInbound && facts.dropsTable) facts.inboundFks = await adapter.getReferencingForeignKeys?.(dbName);
|
|
1632
1914
|
if (adapter.getObjectKind) facts.objectKind = await adapter.getObjectKind(dbName);
|
|
@@ -1643,7 +1925,7 @@ var SchemaSync = class {
|
|
|
1643
1925
|
const pk = t.diff?.primaryKeyChanged;
|
|
1644
1926
|
if (pk) {
|
|
1645
1927
|
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.`);
|
|
1928
|
+
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
1929
|
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
1930
|
if (!safe) {
|
|
1649
1931
|
const liveInbound = (t.inboundFks ?? []).filter((fk) => {
|