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