@atscript/db 0.1.38

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/sync.cjs ADDED
@@ -0,0 +1,1205 @@
1
+ "use strict";
2
+ const require_logger = require('./logger-Dt2v_-wb.cjs');
3
+
4
+ //#region packages/db/src/schema/schema-hash.ts
5
+ /** Extracts sorted field snapshots from a readable's field descriptors. */ function extractFieldSnapshots(fields, typeMapper) {
6
+ return fields.filter((f) => !f.ignored).map((f) => {
7
+ const snap = {
8
+ physicalName: f.physicalName,
9
+ designType: f.designType,
10
+ optional: f.optional,
11
+ isPrimaryKey: f.isPrimaryKey,
12
+ storage: f.storage
13
+ };
14
+ if (f.defaultValue) snap.defaultValue = f.defaultValue;
15
+ if (typeMapper) snap.mappedType = typeMapper(f);
16
+ return snap;
17
+ }).sort((a, b) => a.physicalName.localeCompare(b.physicalName));
18
+ }
19
+ function computeTableSnapshot(readable, typeMapper, tableOptions) {
20
+ const fields = extractFieldSnapshots(readable.fieldDescriptors, typeMapper);
21
+ const indexes = [...readable.indexes.values()].map((idx) => ({
22
+ key: idx.key,
23
+ type: idx.type,
24
+ fields: idx.fields.map((f) => ({
25
+ name: f.name,
26
+ sort: f.sort
27
+ }))
28
+ })).sort((a, b) => a.key.localeCompare(b.key));
29
+ const foreignKeys = [...readable.foreignKeys.values()].map((fk) => ({
30
+ fields: [...fk.fields].sort(),
31
+ targetTable: fk.targetTable,
32
+ targetFields: [...fk.targetFields].sort(),
33
+ onDelete: fk.onDelete,
34
+ onUpdate: fk.onUpdate
35
+ })).sort((a, b) => a.fields.join(",").localeCompare(b.fields.join(",")));
36
+ const snapshot = {
37
+ tableName: readable.tableName,
38
+ fields,
39
+ indexes,
40
+ foreignKeys
41
+ };
42
+ if (tableOptions?.length) snapshot.tableOptions = [...tableOptions].sort((a, b) => a.key.localeCompare(b.key));
43
+ return snapshot;
44
+ }
45
+ function computeViewSnapshot(view) {
46
+ const fields = extractFieldSnapshots(view.fieldDescriptors);
47
+ if (view.isExternal) return {
48
+ tableName: view.tableName,
49
+ viewType: "E",
50
+ fields
51
+ };
52
+ const plan = view.viewPlan;
53
+ const result = {
54
+ tableName: view.tableName,
55
+ viewType: plan.materialized ? "M" : "V",
56
+ entryTable: plan.entryTable,
57
+ joinTables: plan.joins.map((j) => j.targetTable),
58
+ materialized: plan.materialized || undefined,
59
+ fields
60
+ };
61
+ if (plan.filter) result.filterHash = fnv1a(JSON.stringify(plan.filter, (_, v) => typeof v === "function" ? "[fn]" : v));
62
+ return result;
63
+ }
64
+ function computeSchemaHash(snapshots) {
65
+ const sorted = [...snapshots].sort((a, b) => a.tableName.localeCompare(b.tableName));
66
+ const json = JSON.stringify(sorted);
67
+ return fnv1a(json);
68
+ }
69
+ function computeTableHash(snapshot) {
70
+ return fnv1a(JSON.stringify(snapshot));
71
+ }
72
+ function snapshotToExistingColumns(snapshot) {
73
+ return snapshot.fields.map((f) => ({
74
+ name: f.physicalName,
75
+ type: f.mappedType ?? f.designType,
76
+ notnull: !f.optional,
77
+ pk: f.isPrimaryKey,
78
+ dflt_value: serializeDefaultValue(f.defaultValue)
79
+ }));
80
+ }
81
+ function snapshotToExistingTableOptions(snapshot) {
82
+ return snapshot.tableOptions ?? [];
83
+ }
84
+ function serializeDefaultValue(dv) {
85
+ if (!dv) return undefined;
86
+ if (dv.kind === "value") return dv.value;
87
+ return `fn:${dv.fn}`;
88
+ }
89
+ /** FNV-1a 32-bit hash → hex string */ function fnv1a(str) {
90
+ let hash = 2166136261;
91
+ for (let i = 0; i < str.length; i++) {
92
+ hash ^= str.codePointAt(i);
93
+ hash = Math.imul(hash, 16777619);
94
+ }
95
+ return Math.trunc(hash).toString(16).padStart(8, "0");
96
+ }
97
+
98
+ //#endregion
99
+ //#region packages/db/src/schema/column-diff.ts
100
+ function computeColumnDiff(desired, existing, typeMapper) {
101
+ const existingByName = new Map(existing.map((c) => [c.name, c]));
102
+ const desiredByName = new Map();
103
+ const renamedOldNames = new Set();
104
+ const added = [];
105
+ const renamed = [];
106
+ const typeChanged = [];
107
+ const nullableChanged = [];
108
+ const defaultChanged = [];
109
+ const conflicts = [];
110
+ for (const field of desired) {
111
+ if (field.ignored) continue;
112
+ desiredByName.set(field.physicalName, field);
113
+ const existingCol = existingByName.get(field.physicalName);
114
+ if (existingCol) if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
115
+ conflicts.push({
116
+ field,
117
+ oldName: field.renamedFrom,
118
+ conflictsWith: field.physicalName
119
+ });
120
+ renamedOldNames.add(field.renamedFrom);
121
+ } else {
122
+ if (typeMapper) {
123
+ const expectedType = typeMapper(field);
124
+ if (expectedType.toUpperCase() !== existingCol.type.toUpperCase()) typeChanged.push({
125
+ field,
126
+ existingType: existingCol.type
127
+ });
128
+ }
129
+ if (!field.isPrimaryKey && !existingCol.pk) {
130
+ const desiredNotNull = !field.optional;
131
+ if (existingCol.notnull !== desiredNotNull) nullableChanged.push({
132
+ field,
133
+ wasNullable: !existingCol.notnull
134
+ });
135
+ }
136
+ const desiredDefault = serializeDefaultValue(field.defaultValue);
137
+ if (existingCol.dflt_value !== undefined && existingCol.dflt_value !== desiredDefault) defaultChanged.push({
138
+ field,
139
+ oldDefault: existingCol.dflt_value,
140
+ newDefault: desiredDefault
141
+ });
142
+ }
143
+ else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
144
+ renamed.push({
145
+ field,
146
+ oldName: field.renamedFrom
147
+ });
148
+ renamedOldNames.add(field.renamedFrom);
149
+ } else added.push(field);
150
+ }
151
+ const removed = existing.filter((c) => !desiredByName.has(c.name) && !renamedOldNames.has(c.name));
152
+ return {
153
+ added,
154
+ removed,
155
+ renamed,
156
+ typeChanged,
157
+ nullableChanged,
158
+ defaultChanged,
159
+ conflicts
160
+ };
161
+ }
162
+
163
+ //#endregion
164
+ //#region packages/db/src/schema/fk-diff.ts
165
+ function fkKey(fields) {
166
+ return [...fields].sort().join(",");
167
+ }
168
+ function computeForeignKeyDiff(desired, existingSnapshot) {
169
+ const added = [];
170
+ const removed = [];
171
+ const changed = [];
172
+ const existingByKey = new Map();
173
+ for (const fk of existingSnapshot) existingByKey.set(fkKey(fk.fields), fk);
174
+ const desiredKeys = new Set();
175
+ for (const fk of desired.values()) {
176
+ const key = fkKey(fk.fields);
177
+ desiredKeys.add(key);
178
+ const existing = existingByKey.get(key);
179
+ if (!existing) added.push(fk);
180
+ else if (fkPropertiesDiffer(fk, existing)) changed.push({
181
+ desired: fk,
182
+ existing
183
+ });
184
+ }
185
+ for (const [key, fk] of existingByKey) if (!desiredKeys.has(key)) removed.push(fk);
186
+ return {
187
+ added,
188
+ removed,
189
+ changed
190
+ };
191
+ }
192
+ function hasForeignKeyChanges(diff) {
193
+ return diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
194
+ }
195
+ function fkPropertiesDiffer(desired, existing) {
196
+ if (desired.targetTable !== existing.targetTable) return true;
197
+ if (fkKey(desired.targetFields) !== fkKey(existing.targetFields)) return true;
198
+ if ((desired.onDelete ?? undefined) !== (existing.onDelete ?? undefined)) return true;
199
+ if ((desired.onUpdate ?? undefined) !== (existing.onUpdate ?? undefined)) return true;
200
+ return false;
201
+ }
202
+
203
+ //#endregion
204
+ //#region packages/db/src/schema/table-option-diff.ts
205
+ function computeTableOptionDiff(desired, existing, destructiveKeys) {
206
+ const existingByKey = new Map(existing.map((o) => [o.key, o.value]));
207
+ const changed = [];
208
+ for (const opt of desired) {
209
+ const existingValue = existingByKey.get(opt.key);
210
+ if (existingValue !== undefined && existingValue !== opt.value) changed.push({
211
+ key: opt.key,
212
+ oldValue: existingValue,
213
+ newValue: opt.value,
214
+ destructive: destructiveKeys?.has(opt.key) ?? false
215
+ });
216
+ }
217
+ return { changed };
218
+ }
219
+
220
+ //#endregion
221
+ //#region packages/db/src/schema/sync-store.ts
222
+ function _define_property$2(obj, key, value) {
223
+ if (key in obj) Object.defineProperty(obj, key, {
224
+ value,
225
+ enumerable: true,
226
+ configurable: true,
227
+ writable: true
228
+ });
229
+ else obj[key] = value;
230
+ return obj;
231
+ }
232
+ var SyncStore = class {
233
+ async ensureControlTable() {
234
+ if (!this.controlTable) {
235
+ const { AtscriptControl } = await Promise.resolve().then(function() {
236
+ return require("./control_as-BFPERAF_.cjs");
237
+ });
238
+ this.controlTable = this.space.getTable(AtscriptControl);
239
+ }
240
+ await this.controlTable.ensureTable();
241
+ }
242
+ async readControlValue(_id) {
243
+ const row = await this.controlTable.findOne({
244
+ filter: { _id: { $eq: _id } },
245
+ controls: {}
246
+ });
247
+ return row?.value ?? null;
248
+ }
249
+ async writeControlValue(_id, value) {
250
+ const existing = await this.readControlValue(_id);
251
+ if (existing !== null) await this.controlTable.replaceOne({
252
+ _id,
253
+ value
254
+ });
255
+ else await this.controlTable.insertOne({
256
+ _id,
257
+ value
258
+ });
259
+ }
260
+ async readHash() {
261
+ return this.readControlValue("schema_version");
262
+ }
263
+ async writeHash(hash) {
264
+ await this.writeControlValue("schema_version", hash);
265
+ }
266
+ async readTableSnapshot(tableName, _asView) {
267
+ const value = await this.readControlValue(`table_snapshot:${tableName}`);
268
+ return value ? JSON.parse(value) : null;
269
+ }
270
+ async writeTableSnapshot(tableName, snapshot) {
271
+ await this.writeControlValue(`table_snapshot:${tableName}`, JSON.stringify(snapshot));
272
+ }
273
+ async deleteTableSnapshot(tableName) {
274
+ try {
275
+ await this.controlTable.deleteOne(`table_snapshot:${tableName}`);
276
+ } catch {}
277
+ }
278
+ async readTrackedList() {
279
+ const value = await this.readControlValue("synced_tables");
280
+ if (!value) return [];
281
+ const parsed = JSON.parse(value);
282
+ if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") return parsed.map((name) => ({
283
+ name,
284
+ isView: false
285
+ }));
286
+ return parsed.map((e) => ({
287
+ ...e,
288
+ viewType: e.viewType ?? (e.isView ? "V" : undefined)
289
+ }));
290
+ }
291
+ async writeTrackedList(readables) {
292
+ const entries = readables.map((r) => {
293
+ const isView = r.isView;
294
+ let viewType;
295
+ if (isView) {
296
+ const view = r;
297
+ viewType = view.isExternal ? "E" : view.viewPlan.materialized ? "M" : "V";
298
+ }
299
+ return {
300
+ name: r.tableName,
301
+ isView,
302
+ viewType
303
+ };
304
+ });
305
+ entries.sort((a, b) => a.name.localeCompare(b.name));
306
+ await this.writeControlValue("synced_tables", JSON.stringify(entries));
307
+ }
308
+ async tryAcquireLock(podId, ttlMs) {
309
+ const now = Date.now();
310
+ const existing = await this.controlTable.findOne({
311
+ filter: { _id: { $eq: "sync_lock" } },
312
+ controls: {}
313
+ });
314
+ if (existing) {
315
+ const expiresAt = existing.expiresAt;
316
+ if (expiresAt && expiresAt < now) await this.controlTable.deleteOne("sync_lock");
317
+ else return false;
318
+ }
319
+ try {
320
+ await this.controlTable.insertOne({
321
+ _id: "sync_lock",
322
+ lockedBy: podId,
323
+ lockedAt: now,
324
+ expiresAt: now + ttlMs
325
+ });
326
+ return true;
327
+ } catch {
328
+ return false;
329
+ }
330
+ }
331
+ async refreshLock(podId, ttlMs) {
332
+ const existing = await this.controlTable.findOne({
333
+ filter: { _id: { $eq: "sync_lock" } },
334
+ controls: {}
335
+ });
336
+ if (!existing) return "missing";
337
+ if (existing.lockedBy !== podId) return "stolen";
338
+ await this.controlTable.replaceOne({
339
+ _id: "sync_lock",
340
+ lockedBy: podId,
341
+ lockedAt: existing.lockedAt,
342
+ expiresAt: Date.now() + ttlMs
343
+ });
344
+ return "refreshed";
345
+ }
346
+ async releaseLock(podId) {
347
+ try {
348
+ const existing = await this.controlTable.findOne({
349
+ filter: { _id: { $eq: "sync_lock" } },
350
+ controls: {}
351
+ });
352
+ if (existing && existing.lockedBy === podId) await this.controlTable.deleteOne("sync_lock");
353
+ } catch {}
354
+ }
355
+ async waitForLock(timeoutMs, pollIntervalMs) {
356
+ const deadline = Date.now() + timeoutMs;
357
+ while (Date.now() < deadline) {
358
+ const lock = await this.controlTable.findOne({
359
+ filter: { _id: { $eq: "sync_lock" } },
360
+ controls: {}
361
+ });
362
+ if (!lock) return;
363
+ const expiresAt = lock.expiresAt;
364
+ if (expiresAt && expiresAt < Date.now()) {
365
+ await this.controlTable.deleteOne("sync_lock");
366
+ return;
367
+ }
368
+ await new Promise((resolve) => {
369
+ setTimeout(resolve, pollIntervalMs);
370
+ });
371
+ }
372
+ throw new Error(`Schema sync lock wait timed out after ${timeoutMs}ms`);
373
+ }
374
+ constructor(space) {
375
+ _define_property$2(this, "space", void 0);
376
+ _define_property$2(this, "controlTable", void 0);
377
+ this.space = space;
378
+ }
379
+ };
380
+ async function readStoredSnapshot(space, tableName, _asView) {
381
+ const { AtscriptControl } = await Promise.resolve().then(function() {
382
+ return require("./control_as-BFPERAF_.cjs");
383
+ });
384
+ const table = space.getTable(AtscriptControl);
385
+ await table.ensureTable();
386
+ const row = await table.findOne({
387
+ filter: { _id: { $eq: `table_snapshot:${tableName}` } },
388
+ controls: {}
389
+ });
390
+ const value = row?.value ?? null;
391
+ return value ? JSON.parse(value) : null;
392
+ }
393
+
394
+ //#endregion
395
+ //#region packages/db/src/schema/sync-entry.ts
396
+ function _define_property$1(obj, key, value) {
397
+ if (key in obj) Object.defineProperty(obj, key, {
398
+ value,
399
+ enumerable: true,
400
+ configurable: true,
401
+ writable: true
402
+ });
403
+ else obj[key] = value;
404
+ return obj;
405
+ }
406
+ const noColor = {
407
+ green: (s) => s,
408
+ red: (s) => s,
409
+ cyan: (s) => s,
410
+ yellow: (s) => s,
411
+ bold: (s) => s,
412
+ dim: (s) => s,
413
+ underline: (s) => s
414
+ };
415
+ var SyncEntry = class {
416
+ /** Whether this entry involves destructive operations */ get destructive() {
417
+ if (this.status === "drop") return this.viewType !== "V" && this.viewType !== "E";
418
+ return this.columnsToDrop.length > 0 || this.typeChanges.length > 0 || this.recreated || this.optionChanges.some((c) => c.destructive);
419
+ }
420
+ /** Whether this entry represents any change (not in-sync) */ get hasChanges() {
421
+ return this.status !== "in-sync" && this.status !== "error";
422
+ }
423
+ /** Whether this entry has errors */ get hasErrors() {
424
+ return this.status === "error" || this.errors.length > 0;
425
+ }
426
+ /** Render this entry for display */ print(mode, colors) {
427
+ const c = colors ?? noColor;
428
+ return mode === "plan" ? this.printPlan(c) : this.printResult(c);
429
+ }
430
+ labelAndPrefix(c) {
431
+ return {
432
+ label: c.bold(c.underline(this.name)),
433
+ vp: this.viewType ? `${c.dim(`[${this.viewType}]`)} ` : ""
434
+ };
435
+ }
436
+ printError(c, label, vp) {
437
+ return [` ${c.red(`✗ ${vp}${label} — error`)}`, ...this.errors.map((err) => ` ${c.red(err)}`)];
438
+ }
439
+ printPlan(c) {
440
+ const { label, vp } = this.labelAndPrefix(c);
441
+ if (this.status === "error") return this.printError(c, label, vp);
442
+ if (this.status === "drop") {
443
+ const kind = this.viewType ? "drop view" : "drop table";
444
+ return [` ${c.red(`- ${vp}${label} — ${kind}`)}`];
445
+ }
446
+ if (this.status === "create") return [
447
+ ` ${c.green(`+ ${vp}${label} — create`)}`,
448
+ ...this.columnsToAdd.map((col) => ` ${c.green(`+ ${col.physicalName} (${col.designType})${col.isPrimaryKey ? " PK" : ""}${col.optional ? " nullable" : ""} — add`)}`),
449
+ ""
450
+ ];
451
+ if (this.status === "alter") {
452
+ const renameInfo = this.renamedFrom ? ` ${c.yellow(`(renamed from ${this.renamedFrom})`)}` : "";
453
+ return [
454
+ ` ${c.cyan(`~ ${vp}${label} — alter${renameInfo}`)}`,
455
+ ...this.columnsToAdd.map((col) => ` ${c.green(`+ ${col.physicalName} (${col.designType}) — add`)}`),
456
+ ...this.columnsToRename.map((r) => ` ${c.yellow(`~ ${r.from} → ${r.to} — rename`)}`),
457
+ ...this.typeChanges.map((tc) => {
458
+ const action = this.syncMethod ? ` — ${this.syncMethod}` : " — requires migration";
459
+ return ` ${c.red(`! ${tc.column}: ${tc.fromType} → ${tc.toType}${action}`)}`;
460
+ }),
461
+ ...this.nullableChanges.map((nc) => ` ${c.yellow(`~ ${nc.column} — ${nc.toNullable ? "nullable" : "non-nullable"}`)}`),
462
+ ...this.defaultChanges.map((dc) => ` ${c.yellow(`~ ${dc.column} — default ${dc.oldDefault ?? "none"} → ${dc.newDefault ?? "none"}`)}`),
463
+ ...this.columnsToDrop.map((col) => ` ${c.red(`- ${col} — drop`)}`),
464
+ ...this.optionChanges.map((oc) => {
465
+ const tag = oc.destructive ? c.red("!") : c.yellow("~");
466
+ const action = oc.destructive ? " — requires recreation" : "";
467
+ return ` ${tag} ${c.cyan(`option ${oc.key}`)}: ${oc.oldValue} → ${oc.newValue}${action}`;
468
+ }),
469
+ ...this.fkAdded.map((fk) => ` ${c.green(`+ FK(${fk.fields.join(",")}) → ${fk.targetTable} — add`)}`),
470
+ ...this.fkRemoved.map((fk) => ` ${c.red(`- FK(${fk.fields.join(",")}) → ${fk.targetTable} — remove`)}`),
471
+ ...this.fkChanged.map((fk) => ` ${c.yellow(`~ FK(${fk.fields.join(",")}) → ${fk.targetTable} — ${fk.details}`)}`),
472
+ ""
473
+ ];
474
+ }
475
+ return [this.printInSync(c)];
476
+ }
477
+ printResult(c) {
478
+ const { label, vp } = this.labelAndPrefix(c);
479
+ if (this.status === "error") return this.printError(c, label, vp);
480
+ if (this.status === "drop") {
481
+ const kind = this.viewType ? "dropped view" : "dropped table";
482
+ return [` ${c.red(`- ${vp}${label} — ${kind}`)}`];
483
+ }
484
+ if (this.status === "create") return [
485
+ ` ${c.green(`+ ${vp}${label} — created`)}`,
486
+ ...this.columnsAdded.map((col) => ` ${c.green(`+ ${col} — added`)}`),
487
+ ""
488
+ ];
489
+ const hasChanges = this.columnsAdded.length > 0 || this.columnsRenamed.length > 0 || this.columnsDropped.length > 0 || this.optionChanges.length > 0;
490
+ if (hasChanges || this.recreated || this.renamedFrom) {
491
+ const rlabel = this.recreated ? "recreated" : "altered";
492
+ const renameInfo = this.renamedFrom ? ` ${c.yellow(`(renamed from ${this.renamedFrom})`)}` : "";
493
+ const color = this.recreated ? c.yellow : c.cyan;
494
+ return [
495
+ ` ${color(`~ ${vp}${label} — ${rlabel}${renameInfo}`)}`,
496
+ ...this.columnsAdded.map((col) => ` ${c.green(`+ ${col} — added`)}`),
497
+ ...this.columnsRenamed.map((col) => ` ${c.yellow(`~ ${col} — renamed`)}`),
498
+ ...this.columnsDropped.map((col) => ` ${c.red(`- ${col} — dropped`)}`),
499
+ ...this.optionChanges.map((oc) => ` ${c.cyan(`~ option ${oc.key}: ${oc.oldValue} → ${oc.newValue}`)}`),
500
+ ""
501
+ ];
502
+ }
503
+ const lines = [this.printInSync(c)];
504
+ if (this.errors.length > 0) lines.push(...this.errors.map((err) => ` ${c.red(`Error: ${err}`)}`));
505
+ return lines;
506
+ }
507
+ printInSync(c) {
508
+ const prefix = this.viewType ? `${c.dim(`[${this.viewType}]`)} ` : "";
509
+ return ` ${c.green("✓")} ${prefix}${c.bold(this.name)} ${c.dim("— in sync")}`;
510
+ }
511
+ constructor(init) {
512
+ _define_property$1(this, "name", void 0);
513
+ /** 'V' = virtual view, 'M' = materialized view, 'E' = external view, undefined = table */ _define_property$1(this, "viewType", void 0);
514
+ _define_property$1(this, "status", void 0);
515
+ _define_property$1(this, "syncMethod", void 0);
516
+ _define_property$1(this, "columnsToAdd", void 0);
517
+ _define_property$1(this, "columnsToRename", void 0);
518
+ _define_property$1(this, "typeChanges", void 0);
519
+ _define_property$1(this, "nullableChanges", void 0);
520
+ _define_property$1(this, "defaultChanges", void 0);
521
+ _define_property$1(this, "columnsToDrop", void 0);
522
+ _define_property$1(this, "optionChanges", void 0);
523
+ _define_property$1(this, "fkAdded", void 0);
524
+ _define_property$1(this, "fkRemoved", void 0);
525
+ _define_property$1(this, "fkChanged", void 0);
526
+ _define_property$1(this, "columnsAdded", void 0);
527
+ _define_property$1(this, "columnsRenamed", void 0);
528
+ _define_property$1(this, "columnsDropped", void 0);
529
+ _define_property$1(this, "recreated", void 0);
530
+ _define_property$1(this, "errors", void 0);
531
+ _define_property$1(this, "renamedFrom", void 0);
532
+ this.name = init.name;
533
+ this.viewType = init.viewType;
534
+ this.status = init.status;
535
+ this.syncMethod = init.syncMethod;
536
+ this.columnsToAdd = init.columnsToAdd ?? [];
537
+ this.columnsToRename = init.columnsToRename ?? [];
538
+ this.typeChanges = init.typeChanges ?? [];
539
+ this.nullableChanges = init.nullableChanges ?? [];
540
+ this.defaultChanges = init.defaultChanges ?? [];
541
+ this.columnsToDrop = init.columnsToDrop ?? [];
542
+ this.optionChanges = init.optionChanges ?? [];
543
+ this.fkAdded = init.fkAdded ?? [];
544
+ this.fkRemoved = init.fkRemoved ?? [];
545
+ this.fkChanged = init.fkChanged ?? [];
546
+ this.columnsAdded = init.columnsAdded ?? [];
547
+ this.columnsRenamed = init.columnsRenamed ?? [];
548
+ this.columnsDropped = init.columnsDropped ?? [];
549
+ this.recreated = init.recreated ?? false;
550
+ this.errors = init.errors ?? [];
551
+ this.renamedFrom = init.renamedFrom;
552
+ }
553
+ };
554
+
555
+ //#endregion
556
+ //#region packages/db/src/schema/sync-executor.ts
557
+ async function viewDefinitionChanged(view, store) {
558
+ const storedSnapshot = await store.readTableSnapshot(view.tableName, true);
559
+ if (!storedSnapshot) return false;
560
+ const currentHash = computeTableHash(computeViewSnapshot(view));
561
+ return computeTableHash(storedSnapshot) !== currentHash;
562
+ }
563
+ async function executeSyncTable(readable, safe, trackedNames, deps) {
564
+ const adapter = readable.dbAdapter;
565
+ const name = readable.tableName;
566
+ const init = {
567
+ name,
568
+ status: "in-sync",
569
+ syncMethod: readable.syncMethod
570
+ };
571
+ if (readable.renamedFrom && trackedNames.has(readable.renamedFrom) && adapter.renameTable) {
572
+ await adapter.renameTable(readable.renamedFrom);
573
+ init.renamedFrom = readable.renamedFrom;
574
+ init.status = "alter";
575
+ }
576
+ let fkDiff;
577
+ const fkSnapshotName = init.renamedFrom ?? name;
578
+ const storedSnapshot = await deps.store.readTableSnapshot(fkSnapshotName);
579
+ if (storedSnapshot) fkDiff = computeForeignKeyDiff(readable.foreignKeys, storedSnapshot.foreignKeys);
580
+ const hasFkChanges = fkDiff ? hasForeignKeyChanges(fkDiff) : false;
581
+ if (adapter.getExistingColumns && adapter.syncColumns) {
582
+ const existing = await adapter.getExistingColumns();
583
+ if (existing.length === 0 && !init.renamedFrom) {
584
+ await adapter.ensureTable();
585
+ init.status = "create";
586
+ } else if (existing.length > 0) if (hasFkChanges && !init.recreated && !adapter.syncForeignKeys && adapter.recreateTable) {
587
+ await adapter.recreateTable();
588
+ init.recreated = true;
589
+ init.status = "alter";
590
+ } else {
591
+ if (hasFkChanges && fkDiff && adapter.dropForeignKeys) {
592
+ const keysToDrop = [...fkDiff.removed.map((fk) => fkKey(fk.fields)), ...fkDiff.changed.map((fk) => fkKey(fk.desired.fields))];
593
+ if (keysToDrop.length > 0) {
594
+ await adapter.dropForeignKeys(keysToDrop);
595
+ init.status = "alter";
596
+ }
597
+ }
598
+ const typeMapper = adapter.typeMapper?.bind(adapter);
599
+ const diff = computeColumnDiff(readable.fieldDescriptors, existing, typeMapper);
600
+ await applyColumnDiff(adapter, readable, diff, init, safe, deps.logger);
601
+ }
602
+ } else if (adapter.syncColumns) if (!storedSnapshot) {
603
+ const existed = adapter.tableExists ? await adapter.tableExists() : false;
604
+ await adapter.ensureTable();
605
+ if (!existed) init.status = "create";
606
+ } else {
607
+ const existing = snapshotToExistingColumns(storedSnapshot);
608
+ const diff = computeColumnDiff(readable.fieldDescriptors, existing, deps.resolveTypeMapper(adapter));
609
+ await applyColumnDiff(adapter, readable, diff, init, safe, deps.logger);
610
+ }
611
+ else {
612
+ const existed = adapter.tableExists ? await adapter.tableExists() : true;
613
+ if (!init.recreated) {
614
+ await adapter.ensureTable();
615
+ if (!existed) init.status = "create";
616
+ }
617
+ }
618
+ if (init.status !== "create" && !init.recreated && !safe) {
619
+ const optionDiff = await deps.diffTableOptions(readable);
620
+ if (optionDiff && optionDiff.changed.length > 0) {
621
+ init.optionChanges = optionDiff.changed;
622
+ const hasDestructive = optionDiff.changed.some((c) => c.destructive);
623
+ const nonDestructive = optionDiff.changed.filter((c) => !c.destructive);
624
+ if (nonDestructive.length > 0 && adapter.applyTableOptions) {
625
+ await adapter.applyTableOptions(nonDestructive);
626
+ init.status = "alter";
627
+ }
628
+ if (hasDestructive) {
629
+ const syncMethod = readable.syncMethod;
630
+ if (syncMethod === "recreate" && adapter.recreateTable) {
631
+ deps.logger.warn?.(`[schema-sync] Destructive table option change on "${name}" — recreating with data preservation`);
632
+ await adapter.recreateTable();
633
+ init.status = "alter";
634
+ init.recreated = true;
635
+ } else if (adapter.dropTable) {
636
+ deps.logger.warn?.(`[schema-sync] Destructive table option change on "${name}" — dropping and recreating`);
637
+ await adapter.dropTable();
638
+ await adapter.ensureTable();
639
+ init.status = "alter";
640
+ init.recreated = true;
641
+ }
642
+ }
643
+ }
644
+ }
645
+ await adapter.syncIndexes();
646
+ if (adapter.syncForeignKeys) await adapter.syncForeignKeys();
647
+ if (adapter.afterSyncTable) await adapter.afterSyncTable();
648
+ return new SyncEntry(init);
649
+ }
650
+ async function executeSyncView(view, trackedNames, deps) {
651
+ const renamedFrom = view.renamedFrom;
652
+ const isRenamed = renamedFrom && trackedNames.has(renamedFrom);
653
+ if (isRenamed) await deps.space.dropViewByName(renamedFrom);
654
+ let definitionChanged = false;
655
+ if (!isRenamed && trackedNames.has(view.tableName)) {
656
+ definitionChanged = await viewDefinitionChanged(view, deps.store);
657
+ if (definitionChanged) await deps.space.dropViewByName(view.tableName);
658
+ }
659
+ await view.dbAdapter.ensureTable();
660
+ const viewType = view.viewPlan.materialized ? "M" : "V";
661
+ let status;
662
+ if (isRenamed || definitionChanged) status = "alter";
663
+ else if (trackedNames.has(view.tableName)) status = "in-sync";
664
+ else status = "create";
665
+ return new SyncEntry({
666
+ name: view.tableName,
667
+ status,
668
+ viewType,
669
+ renamedFrom: isRenamed ? renamedFrom : undefined,
670
+ recreated: definitionChanged || undefined
671
+ });
672
+ }
673
+ async function applyColumnDiff(adapter, readable, diff, init, safe, logger) {
674
+ const name = readable.tableName;
675
+ if (diff.conflicts.length > 0) {
676
+ const errors = diff.conflicts.map((c) => `Column rename conflict on ${name}: cannot rename "${c.oldName}" → "${c.field.physicalName}" because "${c.conflictsWith}" already exists.`);
677
+ for (const msg of errors) logger.error?.(`[schema-sync] ${msg}`);
678
+ init.errors = [...init.errors ?? [], ...errors];
679
+ init.status = "error";
680
+ }
681
+ let needsSyncColumns = false;
682
+ if (diff.typeChanged.length > 0 && init.status !== "error") {
683
+ const syncMethod = readable.syncMethod;
684
+ if (syncMethod === "drop" && adapter.dropTable) {
685
+ await adapter.dropTable();
686
+ await adapter.ensureTable();
687
+ init.recreated = true;
688
+ init.status = "alter";
689
+ } else if (syncMethod === "recreate" && adapter.recreateTable) {
690
+ await adapter.recreateTable();
691
+ init.recreated = true;
692
+ init.status = "alter";
693
+ } else if (adapter.supportsColumnModify && adapter.syncColumns) {
694
+ needsSyncColumns = true;
695
+ init.status = "alter";
696
+ } else {
697
+ const errors = [];
698
+ for (const change of diff.typeChanged) {
699
+ const msg = `Type change on ${name}.${change.field.physicalName} ` + `(${change.existingType} → ${change.field.designType}). ` + `Add @db.sync.method "recreate" or "drop", or migrate manually.`;
700
+ logger.error?.(`[schema-sync] ${msg}`);
701
+ errors.push(msg);
702
+ }
703
+ init.errors = errors;
704
+ init.status = "error";
705
+ }
706
+ }
707
+ if (!safe && !init.recreated && init.status !== "error" && (diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0)) if (adapter.supportsColumnModify && adapter.syncColumns) {
708
+ needsSyncColumns = true;
709
+ init.status = "alter";
710
+ } else if (adapter.recreateTable) {
711
+ await adapter.recreateTable();
712
+ init.recreated = true;
713
+ init.status = "alter";
714
+ } else init.status = "alter";
715
+ else if (diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0) init.status = "alter";
716
+ if (!init.recreated && init.status !== "error" && (diff.added.length > 0 || diff.renamed.length > 0 || needsSyncColumns) && adapter.syncColumns) {
717
+ const syncResult = await adapter.syncColumns(diff);
718
+ init.columnsAdded = syncResult.added;
719
+ init.columnsRenamed = syncResult.renamed;
720
+ if (syncResult.added.length > 0 || (syncResult.renamed?.length ?? 0) > 0 || needsSyncColumns) init.status = "alter";
721
+ }
722
+ if (!safe && !init.recreated && init.status !== "error" && diff.removed.length > 0 && adapter.dropColumns) {
723
+ const colNames = diff.removed.map((c) => c.name);
724
+ await adapter.dropColumns(colNames);
725
+ init.columnsDropped = colNames;
726
+ init.status = "alter";
727
+ }
728
+ }
729
+
730
+ //#endregion
731
+ //#region packages/db/src/schema/schema-sync.ts
732
+ function _define_property(obj, key, value) {
733
+ if (key in obj) Object.defineProperty(obj, key, {
734
+ value,
735
+ enumerable: true,
736
+ configurable: true,
737
+ writable: true
738
+ });
739
+ else obj[key] = value;
740
+ return obj;
741
+ }
742
+ /** Builds a human-readable description of what changed in an FK constraint. */ function buildFkChangeDetails(desired, existing) {
743
+ const parts = [];
744
+ if (existing.targetTable !== desired.targetTable) parts.push(`retarget ${existing.targetTable} → ${desired.targetTable}`);
745
+ if ([...existing.targetFields].sort().join(",") !== [...desired.targetFields].sort().join(",")) parts.push(`fields ${existing.targetFields.join(",")} → ${desired.targetFields.join(",")}`);
746
+ if ((existing.onDelete ?? undefined) !== (desired.onDelete ?? undefined)) parts.push(`onDelete ${existing.onDelete ?? "noAction"} → ${desired.onDelete ?? "noAction"}`);
747
+ if ((existing.onUpdate ?? undefined) !== (desired.onUpdate ?? undefined)) parts.push(`onUpdate ${existing.onUpdate ?? "noAction"} → ${desired.onUpdate ?? "noAction"}`);
748
+ return parts.join(", ");
749
+ }
750
+ var SchemaSync = class {
751
+ /**
752
+ * Resolves types into categorized readables and computes the schema hash.
753
+ * Passes each adapter's typeMapper for precise type tracking in snapshots.
754
+ */ async resolveAndHash(types) {
755
+ const tables = [];
756
+ const views = [];
757
+ const externalViews = [];
758
+ for (const type of types) {
759
+ const readable = this.space.get(type);
760
+ if (readable.isView) {
761
+ const view = readable;
762
+ if (view.isExternal) externalViews.push(view);
763
+ else views.push(readable);
764
+ } else tables.push(readable);
765
+ }
766
+ const allReadables = [
767
+ ...tables,
768
+ ...views,
769
+ ...externalViews
770
+ ];
771
+ const snapshots = allReadables.map((r) => {
772
+ if (r.isView) return computeViewSnapshot(r);
773
+ const tm = r.dbAdapter.typeMapper?.bind(r.dbAdapter);
774
+ const opts = (r.fieldDescriptors, r.dbAdapter.getDesiredTableOptions?.());
775
+ return computeTableSnapshot(r, tm, opts);
776
+ });
777
+ const hash = computeSchemaHash(snapshots);
778
+ return {
779
+ tables,
780
+ views,
781
+ externalViews,
782
+ hash
783
+ };
784
+ }
785
+ /**
786
+ * Checks an external view: verifies it exists in the DB and columns match.
787
+ * Returns a SyncEntry with status 'in-sync' or 'error'.
788
+ */ async checkExternalView(view) {
789
+ const adapter = view.dbAdapter;
790
+ const name = view.tableName;
791
+ if (adapter.getExistingColumns) {
792
+ const existing = await adapter.getExistingColumns();
793
+ if (existing.length === 0) return new SyncEntry({
794
+ name,
795
+ viewType: "E",
796
+ status: "error",
797
+ errors: [`External view "${name}" not found in the database`]
798
+ });
799
+ const existingNames = new Set(existing.map((c) => c.name));
800
+ const missing = view.fieldDescriptors.filter((f) => !f.ignored && !existingNames.has(f.physicalName)).map((f) => f.physicalName);
801
+ if (missing.length > 0) return new SyncEntry({
802
+ name,
803
+ viewType: "E",
804
+ status: "error",
805
+ errors: [`External view "${name}" is missing columns: ${missing.join(", ")}`]
806
+ });
807
+ } else if (adapter.tableExists) {
808
+ const exists = await adapter.tableExists();
809
+ if (!exists) return new SyncEntry({
810
+ name,
811
+ viewType: "E",
812
+ status: "error",
813
+ errors: [`External view "${name}" not found in the database`]
814
+ });
815
+ }
816
+ return new SyncEntry({
817
+ name,
818
+ viewType: "E",
819
+ status: "in-sync"
820
+ });
821
+ }
822
+ /**
823
+ * Detects tables/views present in the previous sync but absent from the current schema.
824
+ * Returns SyncEntry instances with status 'drop'.
825
+ */ async detectRemoved(currentReadables, previous) {
826
+ previous ?? (previous = await this.store.readTrackedList());
827
+ const currentSet = new Set(currentReadables.map((t) => t.tableName));
828
+ const renameFromSet = new Set(currentReadables.map((r) => r.renamedFrom).filter(Boolean));
829
+ const removed = [];
830
+ for (const entry of previous) if (!currentSet.has(entry.name) && !renameFromSet.has(entry.name)) removed.push(new SyncEntry({
831
+ name: entry.name,
832
+ viewType: entry.viewType,
833
+ status: "drop"
834
+ }));
835
+ return removed;
836
+ }
837
+ /**
838
+ * Starts a periodic heartbeat that extends the lock's TTL while sync runs.
839
+ * Returns a handle with `stop()` to cancel and `getAbortReason()` to check
840
+ * whether the lock was stolen or unexpectedly removed.
841
+ */ startHeartbeat(podId, ttlMs) {
842
+ let abortReason;
843
+ let stopped = false;
844
+ const intervalMs = Math.max(Math.floor(ttlMs / 3), 1e3);
845
+ const timer = setInterval(async () => {
846
+ if (stopped) return;
847
+ try {
848
+ const status = await this.store.refreshLock(podId, ttlMs);
849
+ if (stopped) return;
850
+ if (status === "stolen") {
851
+ abortReason = "Schema sync lock was stolen by another pod";
852
+ this.logger.warn("[schema-sync] Lock stolen by another pod — aborting after current operation");
853
+ } else if (status === "missing") {
854
+ abortReason = "Schema sync lock was unexpectedly removed";
855
+ this.logger.warn("[schema-sync] Lock row missing — aborting after current operation");
856
+ }
857
+ } catch (error) {
858
+ if (stopped) return;
859
+ this.logger.warn("[schema-sync] Failed to refresh lock heartbeat (will retry):", error instanceof Error ? error.message : error);
860
+ }
861
+ }, intervalMs);
862
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
863
+ return {
864
+ stop() {
865
+ stopped = true;
866
+ clearInterval(timer);
867
+ },
868
+ getAbortReason: () => abortReason
869
+ };
870
+ }
871
+ /** Throws if the heartbeat detected a stolen/missing lock. */ assertLockHeld(getAbortReason) {
872
+ const reason = getAbortReason();
873
+ if (reason) throw new Error(reason);
874
+ }
875
+ /**
876
+ * Runs schema synchronization with distributed locking.
877
+ */ async run(types, opts) {
878
+ const podId = opts?.podId ?? crypto.randomUUID();
879
+ const lockTtlMs = opts?.lockTtlMs ?? 3e4;
880
+ const waitTimeoutMs = opts?.waitTimeoutMs ?? 6e4;
881
+ const pollIntervalMs = opts?.pollIntervalMs ?? 500;
882
+ const force = opts?.force ?? false;
883
+ const safe = opts?.safe ?? false;
884
+ const { tables, views, externalViews, hash } = await this.resolveAndHash(types);
885
+ await this.store.ensureControlTable();
886
+ if (!force) {
887
+ const storedHash = await this.store.readHash();
888
+ if (storedHash === hash) return {
889
+ status: "up-to-date",
890
+ schemaHash: hash,
891
+ entries: []
892
+ };
893
+ }
894
+ const acquired = await this.store.tryAcquireLock(podId, lockTtlMs);
895
+ if (!acquired) {
896
+ await this.store.waitForLock(waitTimeoutMs, pollIntervalMs);
897
+ const storedHash = await this.store.readHash();
898
+ if (storedHash === hash) return {
899
+ status: "synced-by-peer",
900
+ schemaHash: hash,
901
+ entries: []
902
+ };
903
+ const retryAcquired = await this.store.tryAcquireLock(podId, lockTtlMs);
904
+ if (!retryAcquired) throw new Error("Failed to acquire schema sync lock after waiting");
905
+ }
906
+ const heartbeat = this.startHeartbeat(podId, lockTtlMs);
907
+ try {
908
+ if (!force) {
909
+ const storedHash = await this.store.readHash();
910
+ if (storedHash === hash) return {
911
+ status: "synced-by-peer",
912
+ schemaHash: hash,
913
+ entries: []
914
+ };
915
+ }
916
+ const allReadables = [
917
+ ...tables,
918
+ ...views,
919
+ ...externalViews
920
+ ];
921
+ const previouslyTracked = await this.store.readTrackedList();
922
+ const trackedNames = new Set(previouslyTracked.map((e) => e.name));
923
+ const deps = this.buildExecutorDeps();
924
+ const entries = [];
925
+ for (const readable of tables) {
926
+ this.assertLockHeld(heartbeat.getAbortReason);
927
+ entries.push(await executeSyncTable(readable, safe, trackedNames, deps));
928
+ }
929
+ this.assertLockHeld(heartbeat.getAbortReason);
930
+ const removed = await this.detectRemoved(allReadables, previouslyTracked);
931
+ for (const readable of views) {
932
+ this.assertLockHeld(heartbeat.getAbortReason);
933
+ entries.push(await executeSyncView(readable, trackedNames, deps));
934
+ }
935
+ const externalEntries = await Promise.all(externalViews.map((v) => this.checkExternalView(v)));
936
+ entries.push(...externalEntries);
937
+ this.assertLockHeld(heartbeat.getAbortReason);
938
+ if (!safe) {
939
+ for (const entry of removed) {
940
+ if (entry.viewType === "E") continue;
941
+ if (entry.viewType) await this.space.dropViewByName(entry.name);
942
+ else await this.space.dropTableByName(entry.name);
943
+ }
944
+ entries.push(...removed.filter((e) => e.viewType !== "E"));
945
+ }
946
+ this.assertLockHeld(heartbeat.getAbortReason);
947
+ for (const readable of allReadables) {
948
+ const adapter = readable.dbAdapter;
949
+ const tm = adapter.typeMapper?.bind(adapter);
950
+ const opts$1 = adapter.getDesiredTableOptions?.();
951
+ const snapshot = readable.isView ? computeViewSnapshot(readable) : computeTableSnapshot(readable, tm, opts$1);
952
+ await this.store.writeTableSnapshot(readable.tableName, snapshot);
953
+ }
954
+ if (!safe) for (const entry of removed) {
955
+ if (entry.viewType === "E") continue;
956
+ await this.store.deleteTableSnapshot(entry.name);
957
+ }
958
+ for (const readable of allReadables) if (readable.renamedFrom) await this.store.deleteTableSnapshot(readable.renamedFrom);
959
+ await this.store.writeTrackedList(allReadables);
960
+ await this.store.writeHash(hash);
961
+ return {
962
+ status: "synced",
963
+ schemaHash: hash,
964
+ entries
965
+ };
966
+ } finally {
967
+ heartbeat.stop();
968
+ await this.store.releaseLock(podId);
969
+ }
970
+ }
971
+ /**
972
+ * Computes a dry-run plan showing what `run()` would do, without executing any DDL.
973
+ */ async plan(types, opts) {
974
+ const force = opts?.force ?? false;
975
+ const safe = opts?.safe ?? false;
976
+ const { tables, views, externalViews, hash } = await this.resolveAndHash(types);
977
+ const allReadables = [
978
+ ...tables,
979
+ ...views,
980
+ ...externalViews
981
+ ];
982
+ await this.store.ensureControlTable();
983
+ const previouslyTracked = await this.store.readTrackedList();
984
+ const trackedNames = new Set(previouslyTracked.map((e) => e.name));
985
+ let planEntries = await Promise.all(tables.map((r) => this.planTable(r, trackedNames)));
986
+ const viewEntries = await Promise.all(views.map((v) => this.planView(v, trackedNames)));
987
+ const externalEntries = await Promise.all(externalViews.map((v) => this.checkExternalView(v)));
988
+ if (!force) {
989
+ const storedHash = await this.store.readHash();
990
+ if (storedHash === hash) return {
991
+ status: "up-to-date",
992
+ schemaHash: hash,
993
+ entries: [
994
+ ...planEntries,
995
+ ...viewEntries,
996
+ ...externalEntries
997
+ ]
998
+ };
999
+ }
1000
+ let removed = await this.detectRemoved(allReadables, previouslyTracked);
1001
+ if (safe) {
1002
+ planEntries = planEntries.map((e) => new SyncEntry({
1003
+ ...e,
1004
+ columnsToDrop: [],
1005
+ typeChanges: [],
1006
+ recreated: false
1007
+ }));
1008
+ removed = [];
1009
+ }
1010
+ removed = removed.filter((e) => e.viewType !== "E");
1011
+ return {
1012
+ status: "changes-needed",
1013
+ schemaHash: hash,
1014
+ entries: [
1015
+ ...planEntries,
1016
+ ...viewEntries,
1017
+ ...externalEntries,
1018
+ ...removed
1019
+ ]
1020
+ };
1021
+ }
1022
+ /** Fallback typeMapper for snapshot-based Path B: compares designType directly, skips unions. */ resolveTypeMapper(adapter) {
1023
+ return adapter.typeMapper?.bind(adapter) ?? ((f) => f.designType === "union" ? "union" : f.designType);
1024
+ }
1025
+ async planTable(readable, trackedNames) {
1026
+ const adapter = readable.dbAdapter;
1027
+ const name = readable.tableName;
1028
+ const init = {
1029
+ name,
1030
+ status: "in-sync",
1031
+ syncMethod: readable.syncMethod
1032
+ };
1033
+ const renamedFrom = readable.renamedFrom;
1034
+ const pendingRename = renamedFrom && trackedNames.has(renamedFrom);
1035
+ if (pendingRename) {
1036
+ init.renamedFrom = renamedFrom;
1037
+ init.status = "alter";
1038
+ }
1039
+ const fkSnapshotName = pendingRename ? renamedFrom : name;
1040
+ const storedSnapshot = await this.store.readTableSnapshot(fkSnapshotName);
1041
+ if (adapter.getExistingColumns) {
1042
+ const existing = pendingRename && adapter.getExistingColumnsForTable ? await adapter.getExistingColumnsForTable(renamedFrom) : await adapter.getExistingColumns();
1043
+ if (existing.length === 0 && !pendingRename) {
1044
+ init.status = "create";
1045
+ init.columnsToAdd = readable.fieldDescriptors.filter((f) => !f.ignored);
1046
+ } else if (existing.length > 0) {
1047
+ const typeMapper = adapter.typeMapper?.bind(adapter);
1048
+ const diff = computeColumnDiff(readable.fieldDescriptors, existing, typeMapper);
1049
+ this.populatePlanFromDiff(diff, init, name, readable.syncMethod, adapter.supportsColumnModify);
1050
+ }
1051
+ } else if (adapter.syncColumns) if (!storedSnapshot) {
1052
+ if (!pendingRename) {
1053
+ const exists = adapter.tableExists ? await adapter.tableExists() : false;
1054
+ if (!exists) {
1055
+ init.status = "create";
1056
+ init.columnsToAdd = readable.fieldDescriptors.filter((f) => !f.ignored);
1057
+ }
1058
+ }
1059
+ } else {
1060
+ const existing = snapshotToExistingColumns(storedSnapshot);
1061
+ const diff = computeColumnDiff(readable.fieldDescriptors, existing, this.resolveTypeMapper(adapter));
1062
+ this.populatePlanFromDiff(diff, init, name, readable.syncMethod, adapter.supportsColumnModify);
1063
+ }
1064
+ else if (adapter.tableExists) {
1065
+ const exists = await adapter.tableExists();
1066
+ if (!exists) init.status = "create";
1067
+ } else init.status = "create";
1068
+ if (init.status !== "create") {
1069
+ const optionDiff = await this.diffTableOptions(readable);
1070
+ if (optionDiff && optionDiff.changed.length > 0) {
1071
+ init.status = "alter";
1072
+ init.optionChanges = optionDiff.changed;
1073
+ if (optionDiff.changed.some((c) => c.destructive)) init.recreated = true;
1074
+ }
1075
+ }
1076
+ if (init.status !== "create" && storedSnapshot) {
1077
+ const fkDiff = computeForeignKeyDiff(readable.foreignKeys, storedSnapshot.foreignKeys);
1078
+ if (hasForeignKeyChanges(fkDiff)) {
1079
+ init.status = "alter";
1080
+ init.fkAdded = fkDiff.added.map((fk) => ({
1081
+ fields: fk.fields,
1082
+ targetTable: fk.targetTable
1083
+ }));
1084
+ init.fkRemoved = fkDiff.removed.map((fk) => ({
1085
+ fields: fk.fields,
1086
+ targetTable: fk.targetTable
1087
+ }));
1088
+ init.fkChanged = fkDiff.changed.map((fk) => ({
1089
+ fields: fk.desired.fields,
1090
+ targetTable: fk.desired.targetTable,
1091
+ details: buildFkChangeDetails(fk.desired, fk.existing)
1092
+ }));
1093
+ }
1094
+ }
1095
+ return new SyncEntry(init);
1096
+ }
1097
+ /**
1098
+ * Populates plan init from a column diff (shared by Path A and Path B).
1099
+ */ populatePlanFromDiff(diff, init, name, syncMethod, adapterSupportsModify) {
1100
+ init.columnsToAdd = diff.added;
1101
+ init.columnsToRename = diff.renamed.map((r) => ({
1102
+ from: r.oldName,
1103
+ to: r.field.physicalName
1104
+ }));
1105
+ init.typeChanges = diff.typeChanged.map((tc) => ({
1106
+ column: tc.field.physicalName,
1107
+ fromType: tc.existingType,
1108
+ toType: tc.field.designType
1109
+ }));
1110
+ init.nullableChanges = diff.nullableChanged.map((nc) => ({
1111
+ column: nc.field.physicalName,
1112
+ toNullable: nc.field.optional
1113
+ }));
1114
+ init.defaultChanges = diff.defaultChanged.map((dc) => ({
1115
+ column: dc.field.physicalName,
1116
+ oldDefault: dc.oldDefault,
1117
+ newDefault: dc.newDefault
1118
+ }));
1119
+ init.columnsToDrop = diff.removed.map((c) => c.name);
1120
+ const hasChanges = diff.added.length > 0 || diff.renamed.length > 0 || diff.typeChanged.length > 0 || diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0 || diff.removed.length > 0;
1121
+ if (hasChanges) init.status = "alter";
1122
+ if (diff.conflicts.length > 0) {
1123
+ init.status = "error";
1124
+ 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.`)];
1125
+ }
1126
+ if (diff.typeChanged.length > 0 && !syncMethod && !adapterSupportsModify) {
1127
+ init.status = "error";
1128
+ init.errors = [...init.errors ?? [], ...diff.typeChanged.map((tc) => `Type change on ${name}.${tc.field.physicalName} ` + `(${tc.existingType} → ${tc.field.designType}). ` + `Add @db.sync.method "recreate" or "drop", or migrate manually.`)];
1129
+ }
1130
+ }
1131
+ /**
1132
+ * Computes table option diff using DB-first introspection with snapshot fallback.
1133
+ * Returns null if the adapter has no table options.
1134
+ */ async diffTableOptions(readable) {
1135
+ const adapter = readable.dbAdapter;
1136
+ const desired = adapter.getDesiredTableOptions?.();
1137
+ if (!desired || desired.length === 0) return null;
1138
+ let existing;
1139
+ if (adapter.getExistingTableOptions) existing = await adapter.getExistingTableOptions();
1140
+ else {
1141
+ const snapshot = await this.store.readTableSnapshot(readable.tableName);
1142
+ existing = snapshot ? snapshotToExistingTableOptions(snapshot) : [];
1143
+ }
1144
+ if (existing.length === 0) return null;
1145
+ const destructiveKeys = adapter.destructiveOptionKeys?.();
1146
+ return computeTableOptionDiff(desired, existing, destructiveKeys);
1147
+ }
1148
+ async planView(view, trackedNames) {
1149
+ const viewType = view.viewPlan.materialized ? "M" : "V";
1150
+ const renamedFrom = view.renamedFrom;
1151
+ const isRenamed = renamedFrom && trackedNames.has(renamedFrom);
1152
+ let status;
1153
+ if (isRenamed) status = "alter";
1154
+ else if (trackedNames.has(view.tableName)) status = await viewDefinitionChanged(view, this.store) ? "alter" : "in-sync";
1155
+ else status = "create";
1156
+ return new SyncEntry({
1157
+ name: view.tableName,
1158
+ status,
1159
+ viewType,
1160
+ renamedFrom: isRenamed ? renamedFrom : undefined,
1161
+ recreated: status === "alter" && !isRenamed ? true : undefined
1162
+ });
1163
+ }
1164
+ buildExecutorDeps() {
1165
+ return {
1166
+ store: this.store,
1167
+ space: this.space,
1168
+ logger: this.logger,
1169
+ resolveTypeMapper: this.resolveTypeMapper.bind(this),
1170
+ diffTableOptions: this.diffTableOptions.bind(this)
1171
+ };
1172
+ }
1173
+ constructor(space, logger) {
1174
+ _define_property(this, "space", void 0);
1175
+ _define_property(this, "store", void 0);
1176
+ _define_property(this, "logger", void 0);
1177
+ this.space = space;
1178
+ this.logger = logger || require_logger.NoopLogger;
1179
+ this.store = new SyncStore(space);
1180
+ }
1181
+ };
1182
+
1183
+ //#endregion
1184
+ //#region packages/db/src/sync.ts
1185
+ async function syncSchema(space, types, opts) {
1186
+ const sync = new SchemaSync(space);
1187
+ return sync.run(types, opts);
1188
+ }
1189
+
1190
+ //#endregion
1191
+ exports.SchemaSync = SchemaSync
1192
+ exports.SyncEntry = SyncEntry
1193
+ exports.computeColumnDiff = computeColumnDiff
1194
+ exports.computeForeignKeyDiff = computeForeignKeyDiff
1195
+ exports.computeSchemaHash = computeSchemaHash
1196
+ exports.computeTableHash = computeTableHash
1197
+ exports.computeTableOptionDiff = computeTableOptionDiff
1198
+ exports.computeTableSnapshot = computeTableSnapshot
1199
+ exports.computeViewSnapshot = computeViewSnapshot
1200
+ exports.fkKey = fkKey
1201
+ exports.hasForeignKeyChanges = hasForeignKeyChanges
1202
+ exports.readStoredSnapshot = readStoredSnapshot
1203
+ exports.snapshotToExistingColumns = snapshotToExistingColumns
1204
+ exports.snapshotToExistingTableOptions = snapshotToExistingTableOptions
1205
+ exports.syncSchema = syncSchema