@atscript/db 0.1.140 → 0.1.141
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-C-27xmtq.d.cts → buckets-Bv4pah66.d.cts} +225 -22
- package/dist/{buckets-BFG2RYRW.d.mts → buckets-CjL7F-hp.d.mts} +225 -22
- package/dist/{column-diff-CgxgFKzx.cjs → column-diff-CfPNcP6e.cjs} +663 -239
- package/dist/{column-diff-BwOA5101.mjs → column-diff-CmFNXV8C.mjs} +638 -220
- package/dist/column-diff-DiBbXyLA.d.cts +211 -0
- package/dist/column-diff-n-k5KY0u.d.mts +211 -0
- package/dist/derived-rules-0sKn4f5C.mjs +44 -0
- package/dist/derived-rules-YstgIxG-.cjs +67 -0
- package/dist/index.cjs +38 -4
- package/dist/index.d.cts +23 -6
- package/dist/index.d.mts +23 -6
- package/dist/index.mjs +34 -4
- package/dist/{nested-writer-FWD5oOYh.mjs → nested-writer-BO3vhbkP.mjs} +8 -4
- package/dist/{nested-writer-BZNCuqI6.cjs → nested-writer-DYsRxZ5f.cjs} +8 -4
- package/dist/object-DSN0h9lB.d.cts +30 -0
- package/dist/object-DSN0h9lB.d.mts +30 -0
- package/dist/plugin.cjs +392 -139
- package/dist/plugin.mjs +392 -139
- package/dist/rel.cjs +2 -2
- package/dist/rel.d.cts +2 -2
- package/dist/rel.d.mts +2 -2
- package/dist/rel.mjs +2 -2
- package/dist/{relation-helpers-D3Zu0Mta.d.mts → relation-helpers-B59to_dG.d.mts} +5 -4
- package/dist/{relation-helpers-DxrvS6ar.d.cts → relation-helpers-DQ_nRsV9.d.cts} +5 -4
- package/dist/{relation-loader-6ZB_5KFq.cjs → relation-loader-CgJ8bK6X.cjs} +1 -1
- package/dist/{relation-loader-CTFaZpVa.mjs → relation-loader-CuhEBzFU.mjs} +1 -1
- package/dist/shared.cjs +6 -1
- package/dist/shared.d.cts +48 -9
- package/dist/shared.d.mts +48 -9
- package/dist/shared.mjs +2 -2
- package/dist/sync.cjs +331 -105
- package/dist/sync.d.cts +62 -163
- package/dist/sync.d.mts +62 -163
- package/dist/sync.mjs +331 -105
- package/dist/{validation-utils-B4h-GW4d.mjs → validation-utils-CMR4fe2M.mjs} +99 -34
- package/dist/{validation-utils-Dg0hW6dn.cjs → validation-utils-DOsB4e6G.cjs} +128 -33
- package/dist/{validator-Drb2N-YL.d.cts → validator-Bw6ks9Hy.d.cts} +1 -11
- package/dist/{validator-Drb2N-YL.d.mts → validator-Bw6ks9Hy.d.mts} +1 -11
- package/dist/{validator-Ch7UIQl9.mjs → validator-D8bPsXPN.mjs} +54 -2
- package/dist/{validator-BtZbcLN2.cjs → validator-DASnXf1j.cjs} +77 -1
- package/dist/validator.cjs +1 -1
- package/dist/validator.d.cts +2 -1
- package/dist/validator.d.mts +2 -1
- package/dist/validator.mjs +1 -1
- package/package.json +6 -6
- package/dist/column-diff-BmqvgBWw.d.cts +0 -24
- package/dist/column-diff-DPkbZIVE.d.mts +0 -24
package/dist/sync.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
2
|
+
const require_db_error = require("./db-error-DTkkeu5b.cjs");
|
|
3
|
+
const require_column_diff = require("./column-diff-CfPNcP6e.cjs");
|
|
3
4
|
//#region src/schema/table-option-diff.ts
|
|
4
5
|
/**
|
|
5
6
|
* Computes the difference between desired and existing table options.
|
|
@@ -100,6 +101,50 @@ function reachable(seeds, edges) {
|
|
|
100
101
|
}
|
|
101
102
|
return out;
|
|
102
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Orders `nodes` so that every node comes AFTER the nodes it depends on,
|
|
106
|
+
* keeping the given order wherever the dependencies allow it: a depth-first
|
|
107
|
+
* walk in `nodes` order that emits a node's dependencies (recursively, in
|
|
108
|
+
* their listed order) right before the node itself. Unlike {@link topoOrder}
|
|
109
|
+
* the result follows the caller's order, not the names — schema sync uses it
|
|
110
|
+
* for managed views, which are created upstream-first in inventory order.
|
|
111
|
+
*
|
|
112
|
+
* - Dependencies outside `nodes` are ignored.
|
|
113
|
+
* - A dependency cycle is reported as `cycle` (the closed path, e.g.
|
|
114
|
+
* `["a", "b", "a"]`) and the order is undefined.
|
|
115
|
+
* @since 0.1.141
|
|
116
|
+
*/
|
|
117
|
+
function stableTopo(nodes, dependsOn) {
|
|
118
|
+
const listed = [...new Set(nodes)];
|
|
119
|
+
const nodeSet = new Set(listed);
|
|
120
|
+
const done = /* @__PURE__ */ new Set();
|
|
121
|
+
const path = [];
|
|
122
|
+
const onPath = /* @__PURE__ */ new Set();
|
|
123
|
+
const order = [];
|
|
124
|
+
const visit = (node) => {
|
|
125
|
+
if (done.has(node)) return;
|
|
126
|
+
if (onPath.has(node)) return [...path.slice(path.indexOf(node)), node];
|
|
127
|
+
onPath.add(node);
|
|
128
|
+
path.push(node);
|
|
129
|
+
for (const dep of dependsOn(node)) {
|
|
130
|
+
if (dep === node || !nodeSet.has(dep)) continue;
|
|
131
|
+
const cycle = visit(dep);
|
|
132
|
+
if (cycle) return cycle;
|
|
133
|
+
}
|
|
134
|
+
path.pop();
|
|
135
|
+
onPath.delete(node);
|
|
136
|
+
done.add(node);
|
|
137
|
+
order.push(node);
|
|
138
|
+
};
|
|
139
|
+
for (const node of listed) {
|
|
140
|
+
const cycle = visit(node);
|
|
141
|
+
if (cycle) return {
|
|
142
|
+
order,
|
|
143
|
+
cycle
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return { order };
|
|
147
|
+
}
|
|
103
148
|
//#endregion
|
|
104
149
|
//#region src/schema/sync-store.ts
|
|
105
150
|
var SyncStore = class {
|
|
@@ -194,73 +239,93 @@ var SyncStore = class {
|
|
|
194
239
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
195
240
|
await this.writeControlValue("synced_tables", JSON.stringify(entries));
|
|
196
241
|
}
|
|
197
|
-
async
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
filter: { _id: { $eq: "sync_lock" } },
|
|
242
|
+
async readLock() {
|
|
243
|
+
return await this.controlTable.findOne({
|
|
244
|
+
filter: lockFilter(),
|
|
201
245
|
controls: {}
|
|
202
246
|
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Deletes the lock row only while it is still expired — a peer that took
|
|
250
|
+
* the expired lock over in the meantime keeps its fresh one.
|
|
251
|
+
*/
|
|
252
|
+
async deleteExpiredLock(now) {
|
|
253
|
+
await this.controlTable.deleteMany(lockFilter({ expiresAt: { $lt: now } }));
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* One acquisition attempt: `true` when this pod now holds the lock, `false`
|
|
257
|
+
* when a live lock (or a peer's concurrent insert) holds it. Any other error
|
|
258
|
+
* propagates — a failing insert is not contention.
|
|
259
|
+
*/
|
|
260
|
+
async tryAcquireLock(podId, ttlMs) {
|
|
261
|
+
const now = Date.now();
|
|
262
|
+
const existing = await this.readLock();
|
|
203
263
|
if (existing) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
else return false;
|
|
264
|
+
if (!isExpired(existing, now)) return false;
|
|
265
|
+
await this.deleteExpiredLock(now);
|
|
207
266
|
}
|
|
208
267
|
try {
|
|
209
268
|
await this.controlTable.insertOne({
|
|
210
|
-
_id:
|
|
269
|
+
_id: LOCK_ID,
|
|
211
270
|
lockedBy: podId,
|
|
212
271
|
lockedAt: now,
|
|
213
272
|
expiresAt: now + ttlMs
|
|
214
273
|
});
|
|
215
274
|
return true;
|
|
216
|
-
} catch {
|
|
217
|
-
return false;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (error instanceof require_db_error.DbError && error.code === "CONFLICT") return false;
|
|
277
|
+
throw error;
|
|
218
278
|
}
|
|
219
279
|
}
|
|
280
|
+
/** Extends this pod's lock; never touches a lock another pod holds. */
|
|
220
281
|
async refreshLock(podId, ttlMs) {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
});
|
|
225
|
-
if (!existing) return "missing";
|
|
226
|
-
if (existing.lockedBy !== podId) return "stolen";
|
|
227
|
-
await this.controlTable.replaceOne({
|
|
228
|
-
_id: "sync_lock",
|
|
229
|
-
lockedBy: podId,
|
|
230
|
-
lockedAt: existing.lockedAt,
|
|
231
|
-
expiresAt: Date.now() + ttlMs
|
|
232
|
-
});
|
|
233
|
-
return "refreshed";
|
|
282
|
+
const { matchedCount } = await this.controlTable.updateMany(lockFilter({ lockedBy: { $eq: podId } }), { expiresAt: Date.now() + ttlMs });
|
|
283
|
+
if (matchedCount > 0) return "refreshed";
|
|
284
|
+
return await this.readLock() ? "stolen" : "missing";
|
|
234
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Deletes this pod's lock. Resolves `false` when there was none to delete
|
|
288
|
+
* (it expired and a peer took it over, or it was removed); errors propagate.
|
|
289
|
+
*/
|
|
235
290
|
async releaseLock(podId) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
filter: { _id: { $eq: "sync_lock" } },
|
|
239
|
-
controls: {}
|
|
240
|
-
});
|
|
241
|
-
if (existing && existing.lockedBy === podId) await this.controlTable.deleteOne("sync_lock");
|
|
242
|
-
} catch {}
|
|
291
|
+
const { deletedCount } = await this.controlTable.deleteMany(lockFilter({ lockedBy: { $eq: podId } }));
|
|
292
|
+
return deletedCount > 0;
|
|
243
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Polls until the lock is free (or expired — then it is cleared). Resolves
|
|
296
|
+
* `true` then, `false` once `timeoutMs` has passed with the lock still held.
|
|
297
|
+
* Acquiring it is the caller's next step.
|
|
298
|
+
*/
|
|
244
299
|
async waitForLock(timeoutMs, pollIntervalMs) {
|
|
245
300
|
const deadline = Date.now() + timeoutMs;
|
|
246
301
|
while (Date.now() < deadline) {
|
|
247
|
-
const lock = await this.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (expiresAt && expiresAt < Date.now()) {
|
|
254
|
-
await this.controlTable.deleteOne("sync_lock");
|
|
255
|
-
return;
|
|
302
|
+
const lock = await this.readLock();
|
|
303
|
+
if (!lock) return true;
|
|
304
|
+
const now = Date.now();
|
|
305
|
+
if (isExpired(lock, now)) {
|
|
306
|
+
await this.deleteExpiredLock(now);
|
|
307
|
+
return true;
|
|
256
308
|
}
|
|
257
309
|
await new Promise((resolve) => {
|
|
258
|
-
setTimeout(resolve, pollIntervalMs);
|
|
310
|
+
setTimeout(resolve, Math.min(pollIntervalMs, Math.max(deadline - Date.now(), 0)));
|
|
259
311
|
});
|
|
260
312
|
}
|
|
261
|
-
|
|
313
|
+
return false;
|
|
262
314
|
}
|
|
263
315
|
};
|
|
316
|
+
const LOCK_ID = "sync_lock";
|
|
317
|
+
/** The lock row's filter, narrowed by `conditions` (the compare-and-set part). */
|
|
318
|
+
function lockFilter(conditions) {
|
|
319
|
+
return {
|
|
320
|
+
_id: { $eq: LOCK_ID },
|
|
321
|
+
...conditions
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/** A lock row without an expiry never expires (it blocks until the wait times out). */
|
|
325
|
+
function isExpired(lock, now) {
|
|
326
|
+
const expiresAt = Number(lock.expiresAt);
|
|
327
|
+
return expiresAt > 0 && expiresAt < now;
|
|
328
|
+
}
|
|
264
329
|
async function readStoredSnapshot(space, tableName, _asView) {
|
|
265
330
|
const { AtscriptControl } = await Promise.resolve().then(() => require("./control-jIVU2hkf.cjs"));
|
|
266
331
|
const table = space.getTable(AtscriptControl);
|
|
@@ -282,6 +347,11 @@ const noColor = {
|
|
|
282
347
|
dim: (s) => s,
|
|
283
348
|
underline: (s) => s
|
|
284
349
|
};
|
|
350
|
+
/** `regular → derived` / `expression changed` / `type changed`. */
|
|
351
|
+
function derivedChangeLabel(change) {
|
|
352
|
+
if (change.reason === "kind") return change.derived ? "regular → derived" : "derived → regular";
|
|
353
|
+
return change.reason === "expression" ? "expression changed" : "type changed";
|
|
354
|
+
}
|
|
285
355
|
var SyncEntry = class SyncEntry {
|
|
286
356
|
name;
|
|
287
357
|
/** 'V' = virtual view, 'M' = materialized view, 'E' = external view, undefined = table */
|
|
@@ -298,6 +368,8 @@ var SyncEntry = class SyncEntry {
|
|
|
298
368
|
fkAdded;
|
|
299
369
|
fkRemoved;
|
|
300
370
|
fkChanged;
|
|
371
|
+
/** @since 0.1.141 — see {@link TSyncEntryInit.derivedChanges}. */
|
|
372
|
+
derivedChanges;
|
|
301
373
|
columnsAdded;
|
|
302
374
|
columnsRenamed;
|
|
303
375
|
columnsDropped;
|
|
@@ -314,6 +386,8 @@ var SyncEntry = class SyncEntry {
|
|
|
314
386
|
dropGroup;
|
|
315
387
|
/** @since 0.1.128 — see {@link TSyncEntryInit.refused}. */
|
|
316
388
|
refused;
|
|
389
|
+
/** @since 0.1.141 — see {@link TSyncEntryInit.cascadeFrom}. */
|
|
390
|
+
cascadeFrom;
|
|
317
391
|
constructor(init) {
|
|
318
392
|
this.name = init.name;
|
|
319
393
|
this.viewType = init.viewType;
|
|
@@ -329,6 +403,7 @@ var SyncEntry = class SyncEntry {
|
|
|
329
403
|
this.fkAdded = init.fkAdded ?? [];
|
|
330
404
|
this.fkRemoved = init.fkRemoved ?? [];
|
|
331
405
|
this.fkChanged = init.fkChanged ?? [];
|
|
406
|
+
this.derivedChanges = init.derivedChanges ?? [];
|
|
332
407
|
this.columnsAdded = init.columnsAdded ?? [];
|
|
333
408
|
this.columnsRenamed = init.columnsRenamed ?? [];
|
|
334
409
|
this.columnsDropped = init.columnsDropped ?? [];
|
|
@@ -340,6 +415,7 @@ var SyncEntry = class SyncEntry {
|
|
|
340
415
|
this.dependsOn = init.dependsOn ?? [];
|
|
341
416
|
this.dropGroup = init.dropGroup;
|
|
342
417
|
this.refused = init.refused ?? false;
|
|
418
|
+
this.cascadeFrom = init.cascadeFrom ?? [];
|
|
343
419
|
}
|
|
344
420
|
/**
|
|
345
421
|
* The init object this entry was built from — lets callers derive a
|
|
@@ -376,7 +452,7 @@ var SyncEntry = class SyncEntry {
|
|
|
376
452
|
/** Whether this entry involves destructive operations (pending work safe mode skipped is not) */
|
|
377
453
|
get destructive() {
|
|
378
454
|
if (this.status === "drop") return this.viewType !== "V" && this.viewType !== "E";
|
|
379
|
-
return this.columnsToDrop.length > 0 || this.typeChanges.length > 0 && !this.skipped.includes("recreate") || this.recreated || this.pkChange?.rebuild === true || this.optionChanges.some((c) => c.destructive) && !this.skipped.includes("table-options");
|
|
455
|
+
return this.columnsToDrop.length > 0 || this.typeChanges.length > 0 && !this.skipped.includes("recreate") || this.recreated || this.pkChange?.rebuild === true || this.derivedChanges.some((dc) => dc.reason === "kind") && !this.skipped.includes("derived") || this.optionChanges.some((c) => c.destructive) && !this.skipped.includes("table-options");
|
|
380
456
|
}
|
|
381
457
|
/** Whether this entry represents any change (not in-sync) */
|
|
382
458
|
get hasChanges() {
|
|
@@ -420,6 +496,19 @@ var SyncEntry = class SyncEntry {
|
|
|
420
496
|
return ` ${c.red(`! ${tc.column}: ${tc.fromType} → ${tc.toType}${action}`)}`;
|
|
421
497
|
});
|
|
422
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* `~ col — derived column (expression changed) — drop + add` (plan) /
|
|
501
|
+
* `— rebuilt` (result), or `! … — skipped (safe mode)` when pending.
|
|
502
|
+
*/
|
|
503
|
+
printDerivedChanges(c, mode) {
|
|
504
|
+
const skipped = this.skipped.includes("derived");
|
|
505
|
+
return this.derivedChanges.map((dc) => {
|
|
506
|
+
const head = `${dc.column} — derived column (${derivedChangeLabel(dc)})`;
|
|
507
|
+
if (skipped) return ` ${c.yellow(`! ${head} — skipped (safe mode)`)}`;
|
|
508
|
+
const line = `${dc.reason === "kind" ? "!" : "~"} ${head} — ${mode === "plan" ? "drop + add" : "rebuilt"}`;
|
|
509
|
+
return ` ${dc.reason === "kind" ? c.red(line) : c.yellow(line)}`;
|
|
510
|
+
});
|
|
511
|
+
}
|
|
423
512
|
/** `~ col — nullable` / `~ col — default a → b`, `— skipped (safe mode)` when pending. */
|
|
424
513
|
printNullableDefaults(c) {
|
|
425
514
|
const suffix = this.skipped.includes("nullable-defaults") ? " — skipped (safe mode)" : "";
|
|
@@ -445,6 +534,10 @@ var SyncEntry = class SyncEntry {
|
|
|
445
534
|
if (this.dependsOn.length === 0) return [];
|
|
446
535
|
return [` ${c.dim(`· after: ${this.dependsOn.join(", ")}`)}`];
|
|
447
536
|
}
|
|
537
|
+
/** `· upstream view "x" recreated` — why an unchanged view is recreated. */
|
|
538
|
+
printCascade(c) {
|
|
539
|
+
return this.cascadeFrom.map((name) => ` ${c.dim(`· upstream view "${name}" recreated`)}`);
|
|
540
|
+
}
|
|
448
541
|
printDropGroup(c) {
|
|
449
542
|
const others = this.dropGroup?.filter((n) => n !== this.name) ?? [];
|
|
450
543
|
if (others.length === 0) return [];
|
|
@@ -463,7 +556,7 @@ var SyncEntry = class SyncEntry {
|
|
|
463
556
|
}
|
|
464
557
|
if (this.status === "create") return [
|
|
465
558
|
` ${c.green(`+ ${vp}${label} — create`)}`,
|
|
466
|
-
...this.columnsToAdd.map((col) => ` ${c.green(`+ ${col.physicalName} (${col.designType})${col.isPrimaryKey ? " PK" : ""}${col.optional ? " nullable" : ""} — add`)}`),
|
|
559
|
+
...this.columnsToAdd.map((col) => ` ${c.green(`+ ${col.physicalName} (${col.designType})${col.isPrimaryKey ? " PK" : ""}${col.optional ? " nullable" : ""}${col.derived ? " derived" : ""} — add`)}`),
|
|
467
560
|
...this.printDependsOn(c),
|
|
468
561
|
""
|
|
469
562
|
];
|
|
@@ -471,16 +564,18 @@ var SyncEntry = class SyncEntry {
|
|
|
471
564
|
const renameInfo = this.renamedFrom ? ` ${c.yellow(`(renamed from ${this.renamedFrom})`)}` : "";
|
|
472
565
|
return [
|
|
473
566
|
` ${c.cyan(`~ ${vp}${label} — alter${renameInfo}`)}`,
|
|
474
|
-
...this.columnsToAdd.map((col) => ` ${c.green(`+ ${col.physicalName} (${col.designType}) — add`)}`),
|
|
567
|
+
...this.columnsToAdd.map((col) => ` ${c.green(`+ ${col.physicalName} (${col.designType})${col.derived ? " derived" : ""} — add`)}`),
|
|
475
568
|
...this.columnsToRename.map((r) => ` ${c.yellow(`~ ${r.from} → ${r.to} — rename`)}`),
|
|
476
569
|
...this.printTypeChanges(c),
|
|
477
570
|
...this.printNullableDefaults(c),
|
|
571
|
+
...this.printDerivedChanges(c, "plan"),
|
|
478
572
|
...this.printPkChange(c, "plan"),
|
|
479
573
|
...this.columnsToDrop.map((col) => ` ${c.red(`- ${col} — drop`)}`),
|
|
480
574
|
...this.printOptionChanges(c, "plan"),
|
|
481
575
|
...this.fkAdded.map((fk) => ` ${c.green(`+ FK(${fk.fields.join(",")}) → ${fk.targetTable} — add`)}`),
|
|
482
576
|
...this.fkRemoved.map((fk) => ` ${c.red(`- FK(${fk.fields.join(",")}) → ${fk.targetTable} — remove`)}`),
|
|
483
577
|
...this.fkChanged.map((fk) => ` ${c.yellow(`~ FK(${fk.fields.join(",")}) → ${fk.targetTable} — ${fk.details}`)}`),
|
|
578
|
+
...this.printCascade(c),
|
|
484
579
|
...this.printDependsOn(c),
|
|
485
580
|
""
|
|
486
581
|
];
|
|
@@ -499,7 +594,7 @@ var SyncEntry = class SyncEntry {
|
|
|
499
594
|
...this.columnsAdded.map((col) => ` ${c.green(`+ ${col} — added`)}`),
|
|
500
595
|
""
|
|
501
596
|
];
|
|
502
|
-
if (this.columnsAdded.length > 0 || this.columnsRenamed.length > 0 || this.columnsDropped.length > 0 || this.optionChanges.length > 0 || this.pkChange !== void 0 || this.skipped.length > 0 || this.recreated || this.renamedFrom) {
|
|
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) {
|
|
503
598
|
const rlabel = this.recreated ? "recreated" : "altered";
|
|
504
599
|
const renameInfo = this.renamedFrom ? ` ${c.yellow(`(renamed from ${this.renamedFrom})`)}` : "";
|
|
505
600
|
return [
|
|
@@ -509,8 +604,10 @@ var SyncEntry = class SyncEntry {
|
|
|
509
604
|
...this.printPkChange(c, "result"),
|
|
510
605
|
...this.printTypeChanges(c),
|
|
511
606
|
...this.printNullableDefaults(c),
|
|
607
|
+
...this.printDerivedChanges(c, "result"),
|
|
512
608
|
...this.columnsDropped.map((col) => ` ${c.red(`- ${col} — dropped`)}`),
|
|
513
609
|
...this.printOptionChanges(c, "result"),
|
|
610
|
+
...this.printCascade(c),
|
|
514
611
|
""
|
|
515
612
|
];
|
|
516
613
|
}
|
|
@@ -555,6 +652,7 @@ function planTableInit(facts, safe, dependsOn) {
|
|
|
555
652
|
...init,
|
|
556
653
|
columnsToDrop: [],
|
|
557
654
|
typeChanges: skipped.includes("recreate") ? init.typeChanges : [],
|
|
655
|
+
derivedChanges: skipped.includes("derived") ? init.derivedChanges : [],
|
|
558
656
|
skipped: skipped.length > 0 ? skipped : void 0,
|
|
559
657
|
recreated: false
|
|
560
658
|
};
|
|
@@ -582,6 +680,10 @@ function typeChangeErrors(readable, diff) {
|
|
|
582
680
|
const advice = method ? `@db.sync.method "${method}" is declared but the adapter has no ${method === "recreate" ? "recreateTable" : "dropTable"} — migrate manually.` : `Add @db.sync.method "recreate" or "drop", or migrate manually.`;
|
|
583
681
|
return diff.typeChanged.map((tc) => `Type change on ${readable.tableName}.${tc.field.physicalName} (${tc.existingType} → ${tc.field.designType}). ${advice}`);
|
|
584
682
|
}
|
|
683
|
+
/** A derived-column rebuild on an adapter without `dropColumns` / `syncColumns`. */
|
|
684
|
+
function derivedRebuildUnsupported(name, columns) {
|
|
685
|
+
return `Derived column${columns.length === 1 ? "" : "s"} ${columns.map((c) => `"${c}"`).join(", ")} of "${name}" changed but the adapter cannot drop and re-add columns. Migrate manually and re-run.`;
|
|
686
|
+
}
|
|
585
687
|
/** `(a, b → c)` — the key change as the messages print it. */
|
|
586
688
|
function pkLabel(change) {
|
|
587
689
|
return `(${change.from.join(", ")} → ${change.to.join(", ")})`;
|
|
@@ -632,7 +734,7 @@ async function executeSyncTable(facts, safe, deps, exec) {
|
|
|
632
734
|
await adapter.ensureTable(ensureOpts);
|
|
633
735
|
init.status = "create";
|
|
634
736
|
} else if (existing.length > 0) {
|
|
635
|
-
const diff = facts.diff ?? require_column_diff.computeColumnDiff(readable.
|
|
737
|
+
const diff = facts.diff ?? require_column_diff.computeColumnDiff(readable.columnDescriptors, existing, adapter.typeMapper?.bind(adapter), { snapshot: storedSnapshot });
|
|
636
738
|
if (hasFkChanges && !adapter.syncForeignKeys && adapter.recreateTable) {
|
|
637
739
|
await adapter.recreateTable();
|
|
638
740
|
init.recreated = true;
|
|
@@ -657,7 +759,7 @@ async function executeSyncTable(facts, safe, deps, exec) {
|
|
|
657
759
|
const existed = adapter.tableExists ? await adapter.tableExists() : false;
|
|
658
760
|
await adapter.ensureTable(ensureOpts);
|
|
659
761
|
if (!existed) init.status = "create";
|
|
660
|
-
} else await applyColumnDiff(adapter, readable, facts.diff ?? require_column_diff.computeColumnDiff(readable.
|
|
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);
|
|
661
763
|
else {
|
|
662
764
|
const existed = adapter.tableExists ? await adapter.tableExists() : true;
|
|
663
765
|
if (!init.recreated) {
|
|
@@ -734,6 +836,10 @@ async function executeDeferredForeignKeys(readable, entry, deps) {
|
|
|
734
836
|
return entry.withError(msg);
|
|
735
837
|
}
|
|
736
838
|
}
|
|
839
|
+
/** Whether a plan drops and recreates the view (its own change, or an upstream's). */
|
|
840
|
+
function viewRecreated(plan) {
|
|
841
|
+
return plan.definitionChanged || (plan.upstreamRecreated?.length ?? 0) > 0;
|
|
842
|
+
}
|
|
737
843
|
/** Determines whether a view's predecessor (on rename) or stale definition must be dropped. */
|
|
738
844
|
async function planViewSync(view, trackedNames, store) {
|
|
739
845
|
const isRenamed = !!(view.renamedFrom && trackedNames.has(view.renamedFrom));
|
|
@@ -745,11 +851,11 @@ async function planViewSync(view, trackedNames, store) {
|
|
|
745
851
|
/** Drops the stale view a plan identified — views don't support ALTER VIEW. */
|
|
746
852
|
async function dropOutdatedView(view, plan, space) {
|
|
747
853
|
if (plan.isRenamed) await space.dropViewByName(view.renamedFrom);
|
|
748
|
-
else if (plan
|
|
854
|
+
else if (viewRecreated(plan)) await space.dropViewByName(view.tableName);
|
|
749
855
|
}
|
|
750
856
|
/** Status a managed view's entry gets for a given plan. */
|
|
751
857
|
function viewPlanStatus(view, plan, trackedNames) {
|
|
752
|
-
if (plan.isRenamed || plan
|
|
858
|
+
if (plan.isRenamed || viewRecreated(plan)) return "alter";
|
|
753
859
|
return trackedNames.has(view.tableName) ? "in-sync" : "create";
|
|
754
860
|
}
|
|
755
861
|
/**
|
|
@@ -761,6 +867,14 @@ async function executeSyncView(view, planned) {
|
|
|
761
867
|
await view.dbAdapter.ensureTable();
|
|
762
868
|
return planned;
|
|
763
869
|
}
|
|
870
|
+
/** The plan-shaped summary of a diff's derived-column rebuilds (`entry.derivedChanges`). */
|
|
871
|
+
function describeDerivedChanges(diff) {
|
|
872
|
+
return (diff.derivedChanged ?? []).map((dc) => ({
|
|
873
|
+
column: dc.field.physicalName,
|
|
874
|
+
reason: dc.reason,
|
|
875
|
+
derived: dc.field.derived !== void 0
|
|
876
|
+
}));
|
|
877
|
+
}
|
|
764
878
|
/** The plan-shaped summary of a diff's type changes (`entry.typeChanges`). */
|
|
765
879
|
function describeTypeChanges(diff) {
|
|
766
880
|
return diff.typeChanged.map((tc) => ({
|
|
@@ -813,6 +927,7 @@ function safeModeSkips(facts) {
|
|
|
813
927
|
if (strategy === "drop") out.push("recreate");
|
|
814
928
|
else if (strategy === "recreate") return out;
|
|
815
929
|
if (diff && (diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0) && needsDdlForNullableDefaults(adapter)) out.push("nullable-defaults");
|
|
930
|
+
if ((diff?.derivedChanged?.length ?? 0) > 0) out.push("derived");
|
|
816
931
|
if (diff?.primaryKeyChanged) out.push("pk-rebuild");
|
|
817
932
|
if (optionDiff?.changed.some((c) => c.destructive)) out.push("table-options");
|
|
818
933
|
return out;
|
|
@@ -904,6 +1019,34 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
|
|
|
904
1019
|
init.columnsRenamed = syncResult.renamed;
|
|
905
1020
|
if (syncResult.added.length > 0 || (syncResult.renamed?.length ?? 0) > 0 || needsSyncColumns) init.status = "alter";
|
|
906
1021
|
}
|
|
1022
|
+
const derivedChanged = diff.derivedChanged ?? [];
|
|
1023
|
+
if (derivedChanged.length > 0 && !init.recreated && init.status !== "error") {
|
|
1024
|
+
init.derivedChanges = describeDerivedChanges(diff);
|
|
1025
|
+
const columns = derivedChanged.map((dc) => dc.field.physicalName);
|
|
1026
|
+
if (safe) {
|
|
1027
|
+
markSkipped(init, "derived");
|
|
1028
|
+
init.status = "alter";
|
|
1029
|
+
logger.warn?.(`[schema-sync] Derived column change on "${name}" (${columns.join(", ")}) — rebuild skipped (safe mode)`);
|
|
1030
|
+
} else if (adapter.dropColumns && adapter.syncColumns) {
|
|
1031
|
+
if (adapter.dropIndexesForColumns) await adapter.dropIndexesForColumns(columns);
|
|
1032
|
+
await adapter.dropColumns(columns);
|
|
1033
|
+
await adapter.syncColumns({
|
|
1034
|
+
added: derivedChanged.map((dc) => dc.field),
|
|
1035
|
+
removed: [],
|
|
1036
|
+
renamed: [],
|
|
1037
|
+
typeChanged: [],
|
|
1038
|
+
nullableChanged: [],
|
|
1039
|
+
defaultChanged: [],
|
|
1040
|
+
conflicts: []
|
|
1041
|
+
});
|
|
1042
|
+
init.status = "alter";
|
|
1043
|
+
} else {
|
|
1044
|
+
const msg = derivedRebuildUnsupported(name, columns);
|
|
1045
|
+
logger.error?.(`[schema-sync] ${msg}`);
|
|
1046
|
+
init.errors = [...init.errors ?? [], msg];
|
|
1047
|
+
init.status = "error";
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
907
1050
|
if (diff.primaryKeyChanged && init.status !== "error") {
|
|
908
1051
|
const { from, to } = diff.primaryKeyChanged;
|
|
909
1052
|
if (init.recreated) init.pkChange = {
|
|
@@ -989,7 +1132,8 @@ const SKIPPED_LABELS = {
|
|
|
989
1132
|
"pk-rebuild": "primary-key rebuild",
|
|
990
1133
|
recreate: "drop-and-recreate",
|
|
991
1134
|
"table-options": "table-option recreate",
|
|
992
|
-
"nullable-defaults": "nullable/default change"
|
|
1135
|
+
"nullable-defaults": "nullable/default change",
|
|
1136
|
+
derived: "derived-column rebuild"
|
|
993
1137
|
};
|
|
994
1138
|
/**
|
|
995
1139
|
* The removed tables whose drop must precede `t`'s own drop/rebuild: those
|
|
@@ -1023,6 +1167,7 @@ var SchemaSync = class {
|
|
|
1023
1167
|
const views = [];
|
|
1024
1168
|
const externalViews = [];
|
|
1025
1169
|
for (const type of types) {
|
|
1170
|
+
if (require_column_diff.aliasTargetOf(type)) continue;
|
|
1026
1171
|
const readable = this.space.get(type);
|
|
1027
1172
|
if (readable.isView) {
|
|
1028
1173
|
const view = readable;
|
|
@@ -1104,13 +1249,17 @@ var SchemaSync = class {
|
|
|
1104
1249
|
* Starts a periodic heartbeat that extends the lock's TTL while sync runs.
|
|
1105
1250
|
* Returns a handle with `stop()` to cancel and `getAbortReason()` to check
|
|
1106
1251
|
* whether the lock was stolen or unexpectedly removed.
|
|
1252
|
+
*
|
|
1253
|
+
* At most one refresh is in flight; `stop()` resolves once it has settled,
|
|
1254
|
+
* so the lock is released only after the last refresh — a late refresh must
|
|
1255
|
+
* never race the release (or the next holder's lock).
|
|
1107
1256
|
*/
|
|
1108
1257
|
startHeartbeat(podId, ttlMs) {
|
|
1109
1258
|
let abortReason;
|
|
1110
1259
|
let stopped = false;
|
|
1260
|
+
let inFlight;
|
|
1111
1261
|
const intervalMs = Math.max(Math.floor(ttlMs / 3), 1e3);
|
|
1112
|
-
const
|
|
1113
|
-
if (stopped) return;
|
|
1262
|
+
const refresh = async () => {
|
|
1114
1263
|
try {
|
|
1115
1264
|
const status = await this.store.refreshLock(podId, ttlMs);
|
|
1116
1265
|
if (stopped) return;
|
|
@@ -1125,22 +1274,55 @@ var SchemaSync = class {
|
|
|
1125
1274
|
if (stopped) return;
|
|
1126
1275
|
this.logger.warn("[schema-sync] Failed to refresh lock heartbeat (will retry):", error instanceof Error ? error.message : error);
|
|
1127
1276
|
}
|
|
1277
|
+
};
|
|
1278
|
+
const timer = setInterval(() => {
|
|
1279
|
+
if (stopped || inFlight) return;
|
|
1280
|
+
inFlight = refresh().finally(() => {
|
|
1281
|
+
inFlight = void 0;
|
|
1282
|
+
});
|
|
1128
1283
|
}, intervalMs);
|
|
1129
1284
|
if (typeof timer === "object" && "unref" in timer) timer.unref();
|
|
1130
1285
|
return {
|
|
1131
|
-
stop() {
|
|
1286
|
+
async stop() {
|
|
1132
1287
|
stopped = true;
|
|
1133
1288
|
clearInterval(timer);
|
|
1289
|
+
await inFlight;
|
|
1134
1290
|
},
|
|
1135
1291
|
getAbortReason: () => abortReason
|
|
1136
1292
|
};
|
|
1137
1293
|
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Acquires the sync lock, waiting for a peer that holds it. Resolves
|
|
1296
|
+
* `"acquired"`, or `"synced-by-peer"` when a non-forced run finds, after a
|
|
1297
|
+
* wait, that the peer stored this schema's hash. A forced run never
|
|
1298
|
+
* short-circuits — it waits its turn and runs. Losing the race for a freed
|
|
1299
|
+
* lock to another waiter just waits again, within the same `waitTimeoutMs`.
|
|
1300
|
+
*/
|
|
1301
|
+
async acquireLock(podId, hash, opts) {
|
|
1302
|
+
const deadline = Date.now() + opts.waitTimeoutMs;
|
|
1303
|
+
while (!await this.store.tryAcquireLock(podId, opts.lockTtlMs)) {
|
|
1304
|
+
if (!await this.store.waitForLock(deadline - Date.now(), opts.pollIntervalMs)) throw new Error(`Schema sync lock wait timed out after ${opts.waitTimeoutMs}ms`);
|
|
1305
|
+
if (!opts.force && await this.store.readHash() === hash) return "synced-by-peer";
|
|
1306
|
+
}
|
|
1307
|
+
return "acquired";
|
|
1308
|
+
}
|
|
1138
1309
|
/** Throws if the heartbeat detected a stolen/missing lock. */
|
|
1139
1310
|
assertLockHeld(getAbortReason) {
|
|
1140
1311
|
const reason = getAbortReason();
|
|
1141
1312
|
if (reason) throw new Error(reason);
|
|
1142
1313
|
}
|
|
1143
1314
|
/**
|
|
1315
|
+
* Releases the lock without masking the run's outcome: a failed release is
|
|
1316
|
+
* logged (the row expires after `lockTtlMs`, blocking peers until then).
|
|
1317
|
+
*/
|
|
1318
|
+
async releaseLock(podId) {
|
|
1319
|
+
try {
|
|
1320
|
+
await this.store.releaseLock(podId);
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
this.logger.warn("[schema-sync] Failed to release the sync lock — peers wait until it expires:", error instanceof Error ? error.message : error);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1144
1326
|
* Runs schema synchronization with distributed locking, in three phases:
|
|
1145
1327
|
*
|
|
1146
1328
|
* 1. **Discover** (read-only, under the lock): introspect every table once,
|
|
@@ -1148,12 +1330,12 @@ var SchemaSync = class {
|
|
|
1148
1330
|
* 2. **Pre-flight** (pure): every change that no order of DDL can apply
|
|
1149
1331
|
* safely becomes a refusal. One refusal → `status: "refused"`, no DDL,
|
|
1150
1332
|
* nothing persisted, lock released.
|
|
1151
|
-
* 3. **Execute** in dependency order:
|
|
1152
|
-
*
|
|
1153
|
-
* blocks a table's drop-and-recreate
|
|
1154
|
-
* before it) → deferred FKs of cycles →
|
|
1155
|
-
*
|
|
1156
|
-
* snapshots/tracking/hash.
|
|
1333
|
+
* 3. **Execute** in dependency order: removed views, then stale views
|
|
1334
|
+
* (dependents first) → inbound FKs of key-changing tables → tables
|
|
1335
|
+
* (parents first; a removed table that blocks a table's drop-and-recreate
|
|
1336
|
+
* or key rebuild is dropped right before it) → deferred FKs of cycles →
|
|
1337
|
+
* managed views (upstream views first) → external-view checks →
|
|
1338
|
+
* remaining removed tables (children first) → snapshots/tracking/hash.
|
|
1157
1339
|
*/
|
|
1158
1340
|
async run(types, opts) {
|
|
1159
1341
|
this.logger = opts?.logger ?? this.logger;
|
|
@@ -1174,15 +1356,16 @@ var SchemaSync = class {
|
|
|
1174
1356
|
entries: []
|
|
1175
1357
|
};
|
|
1176
1358
|
}
|
|
1177
|
-
if (
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1359
|
+
if (await this.acquireLock(podId, hash, {
|
|
1360
|
+
lockTtlMs,
|
|
1361
|
+
waitTimeoutMs,
|
|
1362
|
+
pollIntervalMs,
|
|
1363
|
+
force
|
|
1364
|
+
}) === "synced-by-peer") return {
|
|
1365
|
+
status: "synced-by-peer",
|
|
1366
|
+
schemaHash: hash,
|
|
1367
|
+
entries: []
|
|
1368
|
+
};
|
|
1186
1369
|
const heartbeat = this.startHeartbeat(podId, lockTtlMs);
|
|
1187
1370
|
try {
|
|
1188
1371
|
if (!force) {
|
|
@@ -1208,15 +1391,15 @@ var SchemaSync = class {
|
|
|
1208
1391
|
this.reportOutcome(result, onError);
|
|
1209
1392
|
return result;
|
|
1210
1393
|
} finally {
|
|
1211
|
-
heartbeat.stop();
|
|
1212
|
-
await this.
|
|
1394
|
+
await heartbeat.stop();
|
|
1395
|
+
await this.releaseLock(podId);
|
|
1213
1396
|
}
|
|
1214
1397
|
}
|
|
1215
1398
|
async discover(resolved, safe) {
|
|
1216
|
-
const { tables, views, externalViews, hash } = resolved;
|
|
1399
|
+
const { tables, views: inventoryViews, externalViews, hash } = resolved;
|
|
1217
1400
|
const allReadables = [
|
|
1218
1401
|
...tables,
|
|
1219
|
-
...
|
|
1402
|
+
...inventoryViews,
|
|
1220
1403
|
...externalViews
|
|
1221
1404
|
];
|
|
1222
1405
|
const previouslyTracked = await this.store.readTrackedList();
|
|
@@ -1227,6 +1410,16 @@ var SchemaSync = class {
|
|
|
1227
1410
|
const tableNames = new Set(tableFacts.map((t) => t.name));
|
|
1228
1411
|
const tableByName = new Map(tableFacts.map((t) => [t.name, t]));
|
|
1229
1412
|
const tableByDbName = new Map(tableFacts.map((t) => [t.dbName, t]));
|
|
1413
|
+
const viewNames = new Set(inventoryViews.map((v) => v.tableName));
|
|
1414
|
+
const externalViewNames = new Set(externalViews.map((v) => v.tableName));
|
|
1415
|
+
const viewSources = /* @__PURE__ */ new Map();
|
|
1416
|
+
for (const view of inventoryViews) {
|
|
1417
|
+
const plan = view.viewPlan;
|
|
1418
|
+
viewSources.set(view.tableName, sortedUnique([plan.entryTable, ...plan.joins.map((j) => j.targetTable)]));
|
|
1419
|
+
}
|
|
1420
|
+
const viewTopo = stableTopo(inventoryViews.map((v) => v.tableName), (name) => viewSources.get(name) ?? []);
|
|
1421
|
+
const viewByName = new Map(inventoryViews.map((v) => [v.tableName, v]));
|
|
1422
|
+
const views = viewTopo.cycle ? inventoryViews : viewTopo.order.map((name) => viewByName.get(name));
|
|
1230
1423
|
const viewFacts = await Promise.all(views.map(async (view) => ({
|
|
1231
1424
|
plan: await planViewSync(view, trackedNames, this.store),
|
|
1232
1425
|
kind: await view.dbAdapter.getObjectKind?.(view.tableName)
|
|
@@ -1234,8 +1427,13 @@ var SchemaSync = class {
|
|
|
1234
1427
|
const viewPlans = /* @__PURE__ */ new Map();
|
|
1235
1428
|
const viewEntries = [];
|
|
1236
1429
|
const viewObjectKinds = /* @__PURE__ */ new Map();
|
|
1430
|
+
/** Managed views the run drops and recreates (own change, rename, or cascade). */
|
|
1431
|
+
const recreatedViews = /* @__PURE__ */ new Set();
|
|
1237
1432
|
for (const [i, view] of views.entries()) {
|
|
1238
1433
|
const { plan, kind } = viewFacts[i];
|
|
1434
|
+
const upstreamRecreated = viewSources.get(view.tableName).filter((n) => recreatedViews.has(n));
|
|
1435
|
+
if (upstreamRecreated.length > 0 && trackedNames.has(view.tableName) && !plan.isRenamed && !plan.definitionChanged) plan.upstreamRecreated = upstreamRecreated;
|
|
1436
|
+
if (plan.isRenamed || viewRecreated(plan)) recreatedViews.add(view.tableName);
|
|
1239
1437
|
viewPlans.set(view.tableName, plan);
|
|
1240
1438
|
viewObjectKinds.set(view.tableName, kind);
|
|
1241
1439
|
viewEntries.push(new SyncEntry({
|
|
@@ -1243,8 +1441,9 @@ var SchemaSync = class {
|
|
|
1243
1441
|
status: viewPlanStatus(view, plan, trackedNames),
|
|
1244
1442
|
viewType: view.viewPlan.materialized ? "M" : "V",
|
|
1245
1443
|
renamedFrom: plan.isRenamed ? view.renamedFrom : void 0,
|
|
1246
|
-
recreated: plan
|
|
1247
|
-
|
|
1444
|
+
recreated: viewRecreated(plan) || void 0,
|
|
1445
|
+
cascadeFrom: plan.upstreamRecreated,
|
|
1446
|
+
dependsOn: viewSources.get(view.tableName).filter((n) => tableNames.has(n) || viewNames.has(n) || externalViewNames.has(n))
|
|
1248
1447
|
}));
|
|
1249
1448
|
}
|
|
1250
1449
|
const externalEntries = await Promise.all(externalViews.map((v) => this.checkExternalView(v)));
|
|
@@ -1257,6 +1456,7 @@ var SchemaSync = class {
|
|
|
1257
1456
|
const removedByName = new Map(removed.map((r) => [r.name, r]));
|
|
1258
1457
|
const externalTargetAdapters = /* @__PURE__ */ new Map();
|
|
1259
1458
|
for (const t of tableFacts) for (const fk of t.readable.foreignKeys.values()) if (fk.targetTable !== t.name && !tableNames.has(fk.targetTable)) externalTargetAdapters.set(fk.targetTable, t.readable.dbAdapter);
|
|
1459
|
+
for (const view of views) for (const source of viewSources.get(view.tableName)) if (!tableNames.has(source) && !viewNames.has(source)) externalTargetAdapters.set(source, view.dbAdapter);
|
|
1260
1460
|
const externalTargets = new Map(await Promise.all([...externalTargetAdapters].map(async ([target, adapter]) => [target, await this.objectPresent(adapter, target)])));
|
|
1261
1461
|
const createEdges = [];
|
|
1262
1462
|
const tableDependsOn = /* @__PURE__ */ new Map();
|
|
@@ -1291,6 +1491,12 @@ var SchemaSync = class {
|
|
|
1291
1491
|
tableDependsOn.set(name, sortedUnique([...tableDependsOn.get(name), ...groups.flat()]));
|
|
1292
1492
|
}
|
|
1293
1493
|
const lateDropOrder = dropOrder.filter((group) => !earlyGroups.has(group));
|
|
1494
|
+
const removedViewNames = removed.filter((r) => r.isView).map((r) => r.name);
|
|
1495
|
+
const removedViewTopo = stableTopo(removedViewNames, (name) => {
|
|
1496
|
+
const snapshot = removedByName.get(name)?.snapshot;
|
|
1497
|
+
return snapshot && "viewType" in snapshot ? require_column_diff.viewSnapshotSources(snapshot) : [];
|
|
1498
|
+
});
|
|
1499
|
+
const removedViewOrder = removedViewTopo.cycle ? removedViewNames : removedViewTopo.order.toReversed();
|
|
1294
1500
|
const dropEntries = /* @__PURE__ */ new Map();
|
|
1295
1501
|
for (const r of removed) if (r.isView) dropEntries.set(r.name, new SyncEntry({
|
|
1296
1502
|
name: r.name,
|
|
@@ -1314,6 +1520,9 @@ var SchemaSync = class {
|
|
|
1314
1520
|
trackedNames,
|
|
1315
1521
|
viewPlans,
|
|
1316
1522
|
viewEntries,
|
|
1523
|
+
viewSources,
|
|
1524
|
+
viewCycle: viewTopo.cycle,
|
|
1525
|
+
removedViewOrder,
|
|
1317
1526
|
viewObjectKinds,
|
|
1318
1527
|
externalEntries,
|
|
1319
1528
|
removed,
|
|
@@ -1368,22 +1577,22 @@ var SchemaSync = class {
|
|
|
1368
1577
|
facts.existing = pendingRename && liveColumns?.length === 0 ? void 0 : liveColumns;
|
|
1369
1578
|
if (facts.existing && facts.existing.length === 0) {
|
|
1370
1579
|
init.status = "create";
|
|
1371
|
-
init.columnsToAdd = readable.
|
|
1580
|
+
init.columnsToAdd = [...readable.columnDescriptors];
|
|
1372
1581
|
} else if (facts.existing && facts.existing.length > 0) {
|
|
1373
1582
|
const typeMapper = adapter.typeMapper?.bind(adapter);
|
|
1374
|
-
facts.diff = require_column_diff.computeColumnDiff(readable.
|
|
1583
|
+
facts.diff = require_column_diff.computeColumnDiff(readable.columnDescriptors, facts.existing, typeMapper, { snapshot: storedSnapshot });
|
|
1375
1584
|
this.populatePlanFromDiff(facts.diff, init, readable, safe);
|
|
1376
1585
|
}
|
|
1377
1586
|
} else if (adapter.syncColumns) if (!storedSnapshot) {
|
|
1378
1587
|
if (!pendingRename) {
|
|
1379
1588
|
if (!(adapter.tableExists ? await adapter.tableExists() : false)) {
|
|
1380
1589
|
init.status = "create";
|
|
1381
|
-
init.columnsToAdd = readable.
|
|
1590
|
+
init.columnsToAdd = [...readable.columnDescriptors];
|
|
1382
1591
|
}
|
|
1383
1592
|
}
|
|
1384
1593
|
} else {
|
|
1385
|
-
const existing = require_column_diff.snapshotToExistingColumns(storedSnapshot);
|
|
1386
|
-
facts.diff = require_column_diff.computeColumnDiff(readable.
|
|
1594
|
+
const existing = require_column_diff.snapshotToExistingColumns(storedSnapshot, readable);
|
|
1595
|
+
facts.diff = require_column_diff.computeColumnDiff(readable.columnDescriptors, existing, this.resolveTypeMapper(adapter), { snapshot: storedSnapshot });
|
|
1387
1596
|
this.populatePlanFromDiff(facts.diff, init, readable, safe);
|
|
1388
1597
|
}
|
|
1389
1598
|
else if (adapter.tableExists) {
|
|
@@ -1460,20 +1669,20 @@ var SchemaSync = class {
|
|
|
1460
1669
|
if (d.externalTargets.get(fk.targetTable) === false) addRefusal(refusals, t.name, `FK ${t.name}.${fk.fields.join(",")} references "${fk.targetTable}" which is neither in the sync inventory nor present in the database`);
|
|
1461
1670
|
}
|
|
1462
1671
|
}
|
|
1672
|
+
if (d.viewCycle) for (const name of new Set(d.viewCycle)) addRefusal(refusals, name, `Managed views form a dependency cycle: ${d.viewCycle.join(" → ")}`);
|
|
1463
1673
|
for (const view of d.views) {
|
|
1464
1674
|
if (d.viewObjectKinds.get(view.tableName) === "table") addRefusal(refusals, view.tableName, `A physical table "${view.tableName}" exists where managed view "${view.tableName}" is declared — drop or rename it`);
|
|
1465
1675
|
const fns = view.dbAdapter.aggregateFns();
|
|
1466
1676
|
for (const col of view.getViewColumnMappings()) if (col.aggFn && !fns.has(col.aggFn)) addRefusal(refusals, view.tableName, `View "${view.tableName}" field "${col.viewPath}": aggregate "${col.aggFn}" is not supported by this adapter (aggregateFns())`);
|
|
1677
|
+
for (const source of d.viewSources.get(view.tableName)) if (d.externalTargets.get(source) === false) addRefusal(refusals, view.tableName, `View "${view.tableName}" reads "${source}" which is neither in the sync inventory nor present in the database`);
|
|
1467
1678
|
}
|
|
1468
1679
|
if (!safe) for (const r of d.removed) {
|
|
1469
|
-
if (r.isView) continue;
|
|
1470
1680
|
const referencers = [];
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
const plan = view.viewPlan;
|
|
1474
|
-
if (plan.entryTable === r.name || plan.joins.some((j) => j.targetTable === r.name)) referencers.push(`view "${view.tableName}"`);
|
|
1681
|
+
if (!r.isView) {
|
|
1682
|
+
for (const t of d.tables) for (const fk of t.readable.foreignKeys.values()) if (fk.targetTable === r.name) referencers.push(`${t.name}.${fk.fields.join(",")} (@db.rel.FK)`);
|
|
1475
1683
|
}
|
|
1476
|
-
|
|
1684
|
+
for (const view of d.views) if (d.viewSources.get(view.tableName).includes(r.name)) referencers.push(`view "${view.tableName}"`);
|
|
1685
|
+
if (referencers.length > 0) addRefusal(refusals, r.name, `Cannot drop ${r.isView ? "view " : ""}"${r.name}": it is still referenced by ${referencers.join(", ")}. Add "${r.name}" to the sync inventory or remove the reference.`);
|
|
1477
1686
|
}
|
|
1478
1687
|
return refusals;
|
|
1479
1688
|
}
|
|
@@ -1507,9 +1716,9 @@ var SchemaSync = class {
|
|
|
1507
1716
|
kind: "external",
|
|
1508
1717
|
index
|
|
1509
1718
|
};
|
|
1510
|
-
for (const
|
|
1719
|
+
for (const name of d.removedViewOrder) yield {
|
|
1511
1720
|
kind: "drop-view",
|
|
1512
|
-
name
|
|
1721
|
+
name
|
|
1513
1722
|
};
|
|
1514
1723
|
for (const group of d.lateDropOrder) yield {
|
|
1515
1724
|
kind: "drop",
|
|
@@ -1549,7 +1758,11 @@ var SchemaSync = class {
|
|
|
1549
1758
|
/** Removed entries that were NOT dropped — they stay tracked (I2). */
|
|
1550
1759
|
const retained = [];
|
|
1551
1760
|
const droppedNames = /* @__PURE__ */ new Set();
|
|
1552
|
-
/**
|
|
1761
|
+
/**
|
|
1762
|
+
* Managed views whose step failed, with the message: a stale definition
|
|
1763
|
+
* that could not be dropped (→ error entry, not recreated), or a create
|
|
1764
|
+
* that errored — their dependents are not created over a missing source.
|
|
1765
|
+
*/
|
|
1553
1766
|
const failedViews = /* @__PURE__ */ new Map();
|
|
1554
1767
|
/**
|
|
1555
1768
|
* Pending renames that did NOT run (the rename failed, or the entry
|
|
@@ -1558,22 +1771,9 @@ var SchemaSync = class {
|
|
|
1558
1771
|
* rename as pending again. Keyed by the readable's (new) name.
|
|
1559
1772
|
*/
|
|
1560
1773
|
const stillUnderOldName = /* @__PURE__ */ new Map();
|
|
1561
|
-
for (const view of d.views) {
|
|
1562
|
-
const plan = d.viewPlans.get(view.tableName);
|
|
1563
|
-
try {
|
|
1564
|
-
await dropOutdatedView(view, plan, this.space);
|
|
1565
|
-
} catch (error) {
|
|
1566
|
-
failedViews.set(view.tableName, this.stepFailed(error, "View sync", view.tableName));
|
|
1567
|
-
if (plan.isRenamed) stillUnderOldName.set(view.tableName, {
|
|
1568
|
-
name: view.renamedFrom,
|
|
1569
|
-
isView: true,
|
|
1570
|
-
viewType: view.viewPlan.materialized ? "M" : "V"
|
|
1571
|
-
});
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
1774
|
const droppedViewEntries = /* @__PURE__ */ new Map();
|
|
1575
|
-
for (const
|
|
1576
|
-
|
|
1775
|
+
for (const name of d.removedViewOrder) {
|
|
1776
|
+
const r = d.removedByName.get(name);
|
|
1577
1777
|
if (safe) {
|
|
1578
1778
|
retained.push(r);
|
|
1579
1779
|
continue;
|
|
@@ -1594,6 +1794,19 @@ var SchemaSync = class {
|
|
|
1594
1794
|
retained.push(r);
|
|
1595
1795
|
}
|
|
1596
1796
|
}
|
|
1797
|
+
for (const view of d.views.toReversed()) {
|
|
1798
|
+
const plan = d.viewPlans.get(view.tableName);
|
|
1799
|
+
try {
|
|
1800
|
+
await dropOutdatedView(view, plan, this.space);
|
|
1801
|
+
} catch (error) {
|
|
1802
|
+
failedViews.set(view.tableName, this.stepFailed(error, "View sync", view.tableName));
|
|
1803
|
+
if (plan.isRenamed) stillUnderOldName.set(view.tableName, {
|
|
1804
|
+
name: view.renamedFrom,
|
|
1805
|
+
isView: true,
|
|
1806
|
+
viewType: view.viewPlan.materialized ? "M" : "V"
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1597
1810
|
/** FK keys dropped here per child (by its name) — named by a later failure on that child. */
|
|
1598
1811
|
const preDroppedFks = /* @__PURE__ */ new Map();
|
|
1599
1812
|
if (!safe) for (const t of d.tables) {
|
|
@@ -1651,12 +1864,20 @@ var SchemaSync = class {
|
|
|
1651
1864
|
const view = d.views[step.index];
|
|
1652
1865
|
const planned = d.viewEntries[step.index];
|
|
1653
1866
|
let msg = failedViews.get(view.tableName);
|
|
1867
|
+
if (msg === void 0) {
|
|
1868
|
+
const failedUpstream = d.viewSources.get(view.tableName).filter((n) => failedViews.has(n));
|
|
1869
|
+
if (failedUpstream.length > 0) {
|
|
1870
|
+
msg = `Upstream view ${failedUpstream.map((n) => `"${n}"`).join(", ")} failed — "${view.tableName}" was not created over it`;
|
|
1871
|
+
this.logger.error?.(`[schema-sync] ${msg}`);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1654
1874
|
if (msg === void 0) try {
|
|
1655
1875
|
entries.push(await executeSyncView(view, planned));
|
|
1656
1876
|
break;
|
|
1657
1877
|
} catch (error) {
|
|
1658
1878
|
msg = this.stepFailed(error, "View sync", view.tableName);
|
|
1659
1879
|
}
|
|
1880
|
+
failedViews.set(view.tableName, msg);
|
|
1660
1881
|
entries.push(planned.withError(msg));
|
|
1661
1882
|
break;
|
|
1662
1883
|
}
|
|
@@ -1812,8 +2033,9 @@ var SchemaSync = class {
|
|
|
1812
2033
|
}));
|
|
1813
2034
|
init.typeChanges = describeTypeChanges(diff);
|
|
1814
2035
|
Object.assign(init, describeNullableDefaults(diff));
|
|
2036
|
+
init.derivedChanges = describeDerivedChanges(diff);
|
|
1815
2037
|
init.columnsToDrop = diff.removed.map((c) => c.name);
|
|
1816
|
-
if (diff.added.length > 0 || diff.renamed.length > 0 || diff.typeChanged.length > 0 || diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0 || diff.removed.length > 0 || diff.primaryKeyChanged !== void 0) init.status = "alter";
|
|
2038
|
+
if (diff.added.length > 0 || diff.renamed.length > 0 || diff.typeChanged.length > 0 || diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0 || diff.removed.length > 0 || init.derivedChanges.length > 0 || diff.primaryKeyChanged !== void 0) init.status = "alter";
|
|
1817
2039
|
if (diff.conflicts.length > 0) {
|
|
1818
2040
|
init.status = "error";
|
|
1819
2041
|
init.errors = [...init.errors ?? [], ...diff.conflicts.map((c) => `Column rename conflict on ${name}: cannot rename "${c.oldName}" → "${c.field.physicalName}" because "${c.conflictsWith}" already exists.`)];
|
|
@@ -1822,6 +2044,10 @@ var SchemaSync = class {
|
|
|
1822
2044
|
init.status = "error";
|
|
1823
2045
|
init.errors = [...init.errors ?? [], ...typeChangeErrors(readable, diff)];
|
|
1824
2046
|
}
|
|
2047
|
+
if (init.derivedChanges.length > 0 && !safe && (!adapter.dropColumns || !adapter.syncColumns) && init.status !== "error") {
|
|
2048
|
+
init.status = "error";
|
|
2049
|
+
init.errors = [...init.errors ?? [], derivedRebuildUnsupported(name, init.derivedChanges.map((dc) => dc.column))];
|
|
2050
|
+
}
|
|
1825
2051
|
if (diff.primaryKeyChanged && !safe && !adapter.rebuildPrimaryKey && !adapter.recreateTable && init.status !== "error") {
|
|
1826
2052
|
init.status = "error";
|
|
1827
2053
|
init.errors = [...init.errors ?? [], pkRebuildUnsupported(name, diff.primaryKeyChanged)];
|