@atscript/db 0.1.38 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +42 -303
  2. package/dist/agg.cjs +8 -3
  3. package/dist/agg.d.cts +7 -0
  4. package/dist/agg.d.mts +7 -0
  5. package/dist/agg.mjs +7 -3
  6. package/dist/control-DRgryKeg.cjs +14 -0
  7. package/dist/{control_as-bjmwe24C.mjs → control-IANbnfjG.mjs} +6 -18
  8. package/dist/db-readable-BQQzfguJ.d.cts +1249 -0
  9. package/dist/db-readable-Bbr4CjMb.d.mts +1249 -0
  10. package/dist/db-space-BUrQ5BFm.d.mts +309 -0
  11. package/dist/db-space-Vxpcnyt5.d.cts +309 -0
  12. package/dist/db-validator-plugin-07kDiis2.d.cts +22 -0
  13. package/dist/db-validator-plugin-CiqsHTI_.d.mts +22 -0
  14. package/dist/db-view-BntnAmXO.cjs +3071 -0
  15. package/dist/db-view-ZsoN91-q.mjs +2970 -0
  16. package/dist/index.cjs +95 -2801
  17. package/dist/index.d.cts +137 -0
  18. package/dist/index.d.mts +137 -0
  19. package/dist/index.mjs +55 -2761
  20. package/dist/{nested-writer-BkqL7cp3.cjs → nested-writer-BDXsDMPP.cjs} +196 -150
  21. package/dist/{nested-writer-NEN51mnR.mjs → nested-writer-Dmm1gbZV.mjs} +118 -70
  22. package/dist/ops-BdRAFLKY.d.mts +67 -0
  23. package/dist/ops-DXJ4Zw0P.d.cts +67 -0
  24. package/dist/ops.cjs +123 -0
  25. package/dist/ops.d.cts +2 -0
  26. package/dist/ops.d.mts +2 -0
  27. package/dist/ops.mjs +112 -0
  28. package/dist/plugin.cjs +90 -109
  29. package/dist/plugin.d.cts +6 -0
  30. package/dist/plugin.d.mts +6 -0
  31. package/dist/plugin.mjs +29 -49
  32. package/dist/rel.cjs +20 -20
  33. package/dist/rel.d.cts +119 -0
  34. package/dist/rel.d.mts +119 -0
  35. package/dist/rel.mjs +4 -5
  36. package/dist/{relation-helpers-guFL_oRf.cjs → relation-helpers-BYvsE1tR.cjs} +26 -22
  37. package/dist/{relation-helpers-DyBIlQnB.mjs → relation-helpers-CLasawQq.mjs} +11 -6
  38. package/dist/{relation-loader-Dv7qXYq7.mjs → relation-loader-BEOTXNcq.mjs} +63 -43
  39. package/dist/{relation-loader-CpnDRf9k.cjs → relation-loader-CRC5LcqM.cjs} +74 -49
  40. package/dist/shared.cjs +13 -13
  41. package/dist/{shared.d.ts → shared.d.cts} +14 -13
  42. package/dist/shared.d.mts +71 -0
  43. package/dist/shared.mjs +2 -3
  44. package/dist/sync.cjs +300 -252
  45. package/dist/sync.d.cts +369 -0
  46. package/dist/sync.d.mts +369 -0
  47. package/dist/sync.mjs +284 -233
  48. package/dist/{validation-utils-DEoCMmEb.cjs → validation-utils-DVJDijnB.cjs} +141 -109
  49. package/dist/{validation-utils-DhR_mtKa.mjs → validation-utils-DhjIjP1-.mjs} +71 -37
  50. package/package.json +31 -30
  51. package/LICENSE +0 -21
  52. package/dist/agg-BJFJ3dFQ.mjs +0 -8
  53. package/dist/agg-DnUWAOK8.cjs +0 -14
  54. package/dist/agg.d.ts +0 -13
  55. package/dist/chunk-CrpGerW8.cjs +0 -31
  56. package/dist/control_as-BFPERAF_.cjs +0 -28
  57. package/dist/index.d.ts +0 -1706
  58. package/dist/logger-B7oxCfLQ.mjs +0 -12
  59. package/dist/logger-Dt2v_-wb.cjs +0 -18
  60. package/dist/plugin.d.ts +0 -5
  61. package/dist/rel.d.ts +0 -1305
  62. package/dist/relation-loader-D4mTw6yH.cjs +0 -4
  63. package/dist/relation-loader-Ggy1ujwR.mjs +0 -4
  64. package/dist/sync.d.ts +0 -1878
package/dist/sync.cjs CHANGED
@@ -1,8 +1,9 @@
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) {
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ require("./nested-writer-BDXsDMPP.cjs");
3
+ const require_db_view = require("./db-view-BntnAmXO.cjs");
4
+ //#region src/schema/schema-hash.ts
5
+ /** Extracts sorted field snapshots from a readable's field descriptors. */
6
+ function extractFieldSnapshots(fields, typeMapper) {
6
7
  return fields.filter((f) => !f.ignored).map((f) => {
7
8
  const snap = {
8
9
  physicalName: f.physicalName,
@@ -14,8 +15,17 @@ const require_logger = require('./logger-Dt2v_-wb.cjs');
14
15
  if (f.defaultValue) snap.defaultValue = f.defaultValue;
15
16
  if (typeMapper) snap.mappedType = typeMapper(f);
16
17
  return snap;
17
- }).sort((a, b) => a.physicalName.localeCompare(b.physicalName));
18
+ }).toSorted((a, b) => a.physicalName.localeCompare(b.physicalName));
18
19
  }
20
+ /**
21
+ * Extracts a canonical, serializable snapshot from a readable's metadata.
22
+ * Sorted deterministically so the hash is stable across runs.
23
+ *
24
+ * @param readable - The table/view readable.
25
+ * @param typeMapper - Optional adapter-specific type mapper. When provided,
26
+ * each field's mapped type (e.g., "VARCHAR(255)") is stored in the snapshot
27
+ * for precise type change detection.
28
+ */
19
29
  function computeTableSnapshot(readable, typeMapper, tableOptions) {
20
30
  const fields = extractFieldSnapshots(readable.fieldDescriptors, typeMapper);
21
31
  const indexes = [...readable.indexes.values()].map((idx) => ({
@@ -25,23 +35,28 @@ function computeTableSnapshot(readable, typeMapper, tableOptions) {
25
35
  name: f.name,
26
36
  sort: f.sort
27
37
  }))
28
- })).sort((a, b) => a.key.localeCompare(b.key));
38
+ })).toSorted((a, b) => a.key.localeCompare(b.key));
29
39
  const foreignKeys = [...readable.foreignKeys.values()].map((fk) => ({
30
- fields: [...fk.fields].sort(),
40
+ fields: [...fk.fields].toSorted(),
31
41
  targetTable: fk.targetTable,
32
- targetFields: [...fk.targetFields].sort(),
42
+ targetFields: [...fk.targetFields].toSorted(),
33
43
  onDelete: fk.onDelete,
34
44
  onUpdate: fk.onUpdate
35
- })).sort((a, b) => a.fields.join(",").localeCompare(b.fields.join(",")));
45
+ })).toSorted((a, b) => a.fields.join(",").localeCompare(b.fields.join(",")));
36
46
  const snapshot = {
37
47
  tableName: readable.tableName,
38
48
  fields,
39
49
  indexes,
40
50
  foreignKeys
41
51
  };
42
- if (tableOptions?.length) snapshot.tableOptions = [...tableOptions].sort((a, b) => a.key.localeCompare(b.key));
52
+ if (tableOptions?.length) snapshot.tableOptions = [...tableOptions].toSorted((a, b) => a.key.localeCompare(b.key));
43
53
  return snapshot;
44
54
  }
55
+ /**
56
+ * Extracts a canonical, serializable snapshot from a view's metadata.
57
+ * Captures view plan (entry table, joins, filter, materialization) for
58
+ * detecting view definition changes.
59
+ */
45
60
  function computeViewSnapshot(view) {
46
61
  const fields = extractFieldSnapshots(view.fieldDescriptors);
47
62
  if (view.isExternal) return {
@@ -55,20 +70,35 @@ function computeViewSnapshot(view) {
55
70
  viewType: plan.materialized ? "M" : "V",
56
71
  entryTable: plan.entryTable,
57
72
  joinTables: plan.joins.map((j) => j.targetTable),
58
- materialized: plan.materialized || undefined,
73
+ materialized: plan.materialized || void 0,
59
74
  fields
60
75
  };
61
76
  if (plan.filter) result.filterHash = fnv1a(JSON.stringify(plan.filter, (_, v) => typeof v === "function" ? "[fn]" : v));
62
77
  return result;
63
78
  }
79
+ /**
80
+ * Computes a deterministic hash string from multiple table snapshots.
81
+ * Uses FNV-1a for speed — not cryptographic, just needs stability + collision resistance.
82
+ */
64
83
  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);
84
+ const sorted = [...snapshots].toSorted((a, b) => a.tableName.localeCompare(b.tableName));
85
+ return fnv1a(JSON.stringify(sorted));
68
86
  }
87
+ /**
88
+ * Computes a hash for a single table/view snapshot.
89
+ * Used for per-table change detection via stored snapshots.
90
+ */
69
91
  function computeTableHash(snapshot) {
70
92
  return fnv1a(JSON.stringify(snapshot));
71
93
  }
94
+ /**
95
+ * Converts stored snapshot fields to `TExistingColumn[]` format
96
+ * for use with `computeColumnDiff`. Used by adapters that lack
97
+ * native column introspection (e.g., MongoDB).
98
+ *
99
+ * The `type` field uses `mappedType` when available (adapter-specific),
100
+ * falling back to `designType`.
101
+ */
72
102
  function snapshotToExistingColumns(snapshot) {
73
103
  return snapshot.fields.map((f) => ({
74
104
  name: f.physicalName,
@@ -78,15 +108,21 @@ function snapshotToExistingColumns(snapshot) {
78
108
  dflt_value: serializeDefaultValue(f.defaultValue)
79
109
  }));
80
110
  }
111
+ /**
112
+ * Extracts table options from a stored snapshot for diff comparison.
113
+ * Used as fallback when an adapter lacks native table option introspection.
114
+ */
81
115
  function snapshotToExistingTableOptions(snapshot) {
82
116
  return snapshot.tableOptions ?? [];
83
117
  }
118
+ /** Serializes a TDbDefaultValue to a comparable string. */
84
119
  function serializeDefaultValue(dv) {
85
- if (!dv) return undefined;
120
+ if (!dv) return;
86
121
  if (dv.kind === "value") return dv.value;
87
122
  return `fn:${dv.fn}`;
88
123
  }
89
- /** FNV-1a 32-bit hash → hex string */ function fnv1a(str) {
124
+ /** FNV-1a 32-bit hash → hex string */
125
+ function fnv1a(str) {
90
126
  let hash = 2166136261;
91
127
  for (let i = 0; i < str.length; i++) {
92
128
  hash ^= str.codePointAt(i);
@@ -94,13 +130,22 @@ function serializeDefaultValue(dv) {
94
130
  }
95
131
  return Math.trunc(hash).toString(16).padStart(8, "0");
96
132
  }
97
-
98
133
  //#endregion
99
- //#region packages/db/src/schema/column-diff.ts
134
+ //#region src/schema/column-diff.ts
135
+ /**
136
+ * Computes the difference between desired schema fields and existing database columns.
137
+ *
138
+ * @param desired - Field descriptors from the Atscript type (after flattening).
139
+ * @param existing - Columns currently in the database (from introspection).
140
+ * @param typeMapper - Optional function to map field metadata to DB-native type strings.
141
+ * Receives the full field meta (design type, annotations, PK status, etc.)
142
+ * so adapters can produce context-aware types (e.g., `VARCHAR(255)` from maxLength).
143
+ * Required for type change detection.
144
+ */
100
145
  function computeColumnDiff(desired, existing, typeMapper) {
101
146
  const existingByName = new Map(existing.map((c) => [c.name, c]));
102
- const desiredByName = new Map();
103
- const renamedOldNames = new Set();
147
+ const desiredByName = /* @__PURE__ */ new Map();
148
+ const renamedOldNames = /* @__PURE__ */ new Set();
104
149
  const added = [];
105
150
  const renamed = [];
106
151
  const typeChanged = [];
@@ -120,8 +165,7 @@ function computeColumnDiff(desired, existing, typeMapper) {
120
165
  renamedOldNames.add(field.renamedFrom);
121
166
  } else {
122
167
  if (typeMapper) {
123
- const expectedType = typeMapper(field);
124
- if (expectedType.toUpperCase() !== existingCol.type.toUpperCase()) typeChanged.push({
168
+ if (typeMapper(field).toUpperCase() !== existingCol.type.toUpperCase()) typeChanged.push({
125
169
  field,
126
170
  existingType: existingCol.type
127
171
  });
@@ -134,13 +178,13 @@ function computeColumnDiff(desired, existing, typeMapper) {
134
178
  });
135
179
  }
136
180
  const desiredDefault = serializeDefaultValue(field.defaultValue);
137
- if (existingCol.dflt_value !== undefined && existingCol.dflt_value !== desiredDefault) defaultChanged.push({
181
+ if (existingCol.dflt_value !== void 0 && existingCol.dflt_value !== desiredDefault) defaultChanged.push({
138
182
  field,
139
183
  oldDefault: existingCol.dflt_value,
140
184
  newDefault: desiredDefault
141
185
  });
142
186
  }
143
- else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
187
+ else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
144
188
  renamed.push({
145
189
  field,
146
190
  oldName: field.renamedFrom
@@ -148,10 +192,9 @@ else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
148
192
  renamedOldNames.add(field.renamedFrom);
149
193
  } else added.push(field);
150
194
  }
151
- const removed = existing.filter((c) => !desiredByName.has(c.name) && !renamedOldNames.has(c.name));
152
195
  return {
153
196
  added,
154
- removed,
197
+ removed: existing.filter((c) => !desiredByName.has(c.name) && !renamedOldNames.has(c.name)),
155
198
  renamed,
156
199
  typeChanged,
157
200
  nullableChanged,
@@ -159,25 +202,30 @@ else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
159
202
  conflicts
160
203
  };
161
204
  }
162
-
163
205
  //#endregion
164
- //#region packages/db/src/schema/fk-diff.ts
206
+ //#region src/schema/fk-diff.ts
207
+ /** Canonical key for an FK: sorted local field names, comma-joined. */
165
208
  function fkKey(fields) {
166
- return [...fields].sort().join(",");
209
+ return [...fields].toSorted().join(",");
167
210
  }
211
+ /**
212
+ * Compares desired FK constraints against stored snapshot to detect
213
+ * additions, removals, and property changes (target table, target fields,
214
+ * onDelete, onUpdate).
215
+ */
168
216
  function computeForeignKeyDiff(desired, existingSnapshot) {
169
217
  const added = [];
170
218
  const removed = [];
171
219
  const changed = [];
172
- const existingByKey = new Map();
220
+ const existingByKey = /* @__PURE__ */ new Map();
173
221
  for (const fk of existingSnapshot) existingByKey.set(fkKey(fk.fields), fk);
174
- const desiredKeys = new Set();
222
+ const desiredKeys = /* @__PURE__ */ new Set();
175
223
  for (const fk of desired.values()) {
176
224
  const key = fkKey(fk.fields);
177
225
  desiredKeys.add(key);
178
226
  const existing = existingByKey.get(key);
179
227
  if (!existing) added.push(fk);
180
- else if (fkPropertiesDiffer(fk, existing)) changed.push({
228
+ else if (fkPropertiesDiffer(fk, existing)) changed.push({
181
229
  desired: fk,
182
230
  existing
183
231
  });
@@ -189,25 +237,36 @@ else if (fkPropertiesDiffer(fk, existing)) changed.push({
189
237
  changed
190
238
  };
191
239
  }
240
+ /** Whether the FK diff contains any changes. */
192
241
  function hasForeignKeyChanges(diff) {
193
242
  return diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
194
243
  }
195
244
  function fkPropertiesDiffer(desired, existing) {
196
245
  if (desired.targetTable !== existing.targetTable) return true;
197
246
  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;
247
+ if ((desired.onDelete ?? void 0) !== (existing.onDelete ?? void 0)) return true;
248
+ if ((desired.onUpdate ?? void 0) !== (existing.onUpdate ?? void 0)) return true;
200
249
  return false;
201
250
  }
202
-
203
251
  //#endregion
204
- //#region packages/db/src/schema/table-option-diff.ts
252
+ //#region src/schema/table-option-diff.ts
253
+ /**
254
+ * Computes the difference between desired and existing table options.
255
+ *
256
+ * Options present in desired but absent from existing are ignored (initial state).
257
+ * Options present in existing but absent from desired are ignored (sticky options).
258
+ * Only value changes on matching keys are tracked.
259
+ *
260
+ * @param desired - Options from Atscript annotations (via adapter.getDesiredTableOptions()).
261
+ * @param existing - Options from DB introspection or snapshot fallback.
262
+ * @param destructiveKeys - Option keys where a value change requires table recreation.
263
+ */
205
264
  function computeTableOptionDiff(desired, existing, destructiveKeys) {
206
265
  const existingByKey = new Map(existing.map((o) => [o.key, o.value]));
207
266
  const changed = [];
208
267
  for (const opt of desired) {
209
268
  const existingValue = existingByKey.get(opt.key);
210
- if (existingValue !== undefined && existingValue !== opt.value) changed.push({
269
+ if (existingValue !== void 0 && existingValue !== opt.value) changed.push({
211
270
  key: opt.key,
212
271
  oldValue: existingValue,
213
272
  newValue: opt.value,
@@ -216,43 +275,32 @@ function computeTableOptionDiff(desired, existing, destructiveKeys) {
216
275
  }
217
276
  return { changed };
218
277
  }
219
-
220
278
  //#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
- }
279
+ //#region src/schema/sync-store.ts
232
280
  var SyncStore = class {
281
+ controlTable;
282
+ constructor(space) {
283
+ this.space = space;
284
+ }
233
285
  async ensureControlTable() {
234
286
  if (!this.controlTable) {
235
- const { AtscriptControl } = await Promise.resolve().then(function() {
236
- return require("./control_as-BFPERAF_.cjs");
237
- });
287
+ const { AtscriptControl } = await Promise.resolve().then(() => require("./control-DRgryKeg.cjs"));
238
288
  this.controlTable = this.space.getTable(AtscriptControl);
239
289
  }
240
290
  await this.controlTable.ensureTable();
241
291
  }
242
292
  async readControlValue(_id) {
243
- const row = await this.controlTable.findOne({
293
+ return (await this.controlTable.findOne({
244
294
  filter: { _id: { $eq: _id } },
245
295
  controls: {}
246
- });
247
- return row?.value ?? null;
296
+ }))?.value ?? null;
248
297
  }
249
298
  async writeControlValue(_id, value) {
250
- const existing = await this.readControlValue(_id);
251
- if (existing !== null) await this.controlTable.replaceOne({
299
+ if (await this.readControlValue(_id) !== null) await this.controlTable.replaceOne({
252
300
  _id,
253
301
  value
254
302
  });
255
- else await this.controlTable.insertOne({
303
+ else await this.controlTable.insertOne({
256
304
  _id,
257
305
  value
258
306
  });
@@ -283,10 +331,9 @@ else await this.controlTable.insertOne({
283
331
  name,
284
332
  isView: false
285
333
  }));
286
- return parsed.map((e) => ({
287
- ...e,
288
- viewType: e.viewType ?? (e.isView ? "V" : undefined)
289
- }));
334
+ const entries = parsed;
335
+ for (const e of entries) e.viewType ??= e.isView ? "V" : void 0;
336
+ return entries;
290
337
  }
291
338
  async writeTrackedList(readables) {
292
339
  const entries = readables.map((r) => {
@@ -314,7 +361,7 @@ else await this.controlTable.insertOne({
314
361
  if (existing) {
315
362
  const expiresAt = existing.expiresAt;
316
363
  if (expiresAt && expiresAt < now) await this.controlTable.deleteOne("sync_lock");
317
- else return false;
364
+ else return false;
318
365
  }
319
366
  try {
320
367
  await this.controlTable.insertOne({
@@ -371,38 +418,19 @@ else return false;
371
418
  }
372
419
  throw new Error(`Schema sync lock wait timed out after ${timeoutMs}ms`);
373
420
  }
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
421
  };
380
422
  async function readStoredSnapshot(space, tableName, _asView) {
381
- const { AtscriptControl } = await Promise.resolve().then(function() {
382
- return require("./control_as-BFPERAF_.cjs");
383
- });
423
+ const { AtscriptControl } = await Promise.resolve().then(() => require("./control-DRgryKeg.cjs"));
384
424
  const table = space.getTable(AtscriptControl);
385
425
  await table.ensureTable();
386
- const row = await table.findOne({
426
+ const value = (await table.findOne({
387
427
  filter: { _id: { $eq: `table_snapshot:${tableName}` } },
388
428
  controls: {}
389
- });
390
- const value = row?.value ?? null;
429
+ }))?.value ?? null;
391
430
  return value ? JSON.parse(value) : null;
392
431
  }
393
-
394
432
  //#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
- }
433
+ //#region src/schema/sync-entry.ts
406
434
  const noColor = {
407
435
  green: (s) => s,
408
436
  red: (s) => s,
@@ -413,17 +441,64 @@ const noColor = {
413
441
  underline: (s) => s
414
442
  };
415
443
  var SyncEntry = class {
416
- /** Whether this entry involves destructive operations */ get destructive() {
444
+ name;
445
+ /** 'V' = virtual view, 'M' = materialized view, 'E' = external view, undefined = table */
446
+ viewType;
447
+ status;
448
+ syncMethod;
449
+ columnsToAdd;
450
+ columnsToRename;
451
+ typeChanges;
452
+ nullableChanges;
453
+ defaultChanges;
454
+ columnsToDrop;
455
+ optionChanges;
456
+ fkAdded;
457
+ fkRemoved;
458
+ fkChanged;
459
+ columnsAdded;
460
+ columnsRenamed;
461
+ columnsDropped;
462
+ recreated;
463
+ errors;
464
+ renamedFrom;
465
+ constructor(init) {
466
+ this.name = init.name;
467
+ this.viewType = init.viewType;
468
+ this.status = init.status;
469
+ this.syncMethod = init.syncMethod;
470
+ this.columnsToAdd = init.columnsToAdd ?? [];
471
+ this.columnsToRename = init.columnsToRename ?? [];
472
+ this.typeChanges = init.typeChanges ?? [];
473
+ this.nullableChanges = init.nullableChanges ?? [];
474
+ this.defaultChanges = init.defaultChanges ?? [];
475
+ this.columnsToDrop = init.columnsToDrop ?? [];
476
+ this.optionChanges = init.optionChanges ?? [];
477
+ this.fkAdded = init.fkAdded ?? [];
478
+ this.fkRemoved = init.fkRemoved ?? [];
479
+ this.fkChanged = init.fkChanged ?? [];
480
+ this.columnsAdded = init.columnsAdded ?? [];
481
+ this.columnsRenamed = init.columnsRenamed ?? [];
482
+ this.columnsDropped = init.columnsDropped ?? [];
483
+ this.recreated = init.recreated ?? false;
484
+ this.errors = init.errors ?? [];
485
+ this.renamedFrom = init.renamedFrom;
486
+ }
487
+ /** Whether this entry involves destructive operations */
488
+ get destructive() {
417
489
  if (this.status === "drop") return this.viewType !== "V" && this.viewType !== "E";
418
490
  return this.columnsToDrop.length > 0 || this.typeChanges.length > 0 || this.recreated || this.optionChanges.some((c) => c.destructive);
419
491
  }
420
- /** Whether this entry represents any change (not in-sync) */ get hasChanges() {
492
+ /** Whether this entry represents any change (not in-sync) */
493
+ get hasChanges() {
421
494
  return this.status !== "in-sync" && this.status !== "error";
422
495
  }
423
- /** Whether this entry has errors */ get hasErrors() {
496
+ /** Whether this entry has errors */
497
+ get hasErrors() {
424
498
  return this.status === "error" || this.errors.length > 0;
425
499
  }
426
- /** Render this entry for display */ print(mode, colors) {
500
+ /** Render this entry for display */
501
+ print(mode, colors) {
427
502
  const c = colors ?? noColor;
428
503
  return mode === "plan" ? this.printPlan(c) : this.printResult(c);
429
504
  }
@@ -486,13 +561,11 @@ var SyncEntry = class {
486
561
  ...this.columnsAdded.map((col) => ` ${c.green(`+ ${col} — added`)}`),
487
562
  ""
488
563
  ];
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) {
564
+ if (this.columnsAdded.length > 0 || this.columnsRenamed.length > 0 || this.columnsDropped.length > 0 || this.optionChanges.length > 0 || this.recreated || this.renamedFrom) {
491
565
  const rlabel = this.recreated ? "recreated" : "altered";
492
566
  const renameInfo = this.renamedFrom ? ` ${c.yellow(`(renamed from ${this.renamedFrom})`)}` : "";
493
- const color = this.recreated ? c.yellow : c.cyan;
494
567
  return [
495
- ` ${color(`~ ${vp}${label} — ${rlabel}${renameInfo}`)}`,
568
+ ` ${(this.recreated ? (s) => c.yellow(s) : (s) => c.cyan(s))(`~ ${vp}${label} — ${rlabel}${renameInfo}`)}`,
496
569
  ...this.columnsAdded.map((col) => ` ${c.green(`+ ${col} — added`)}`),
497
570
  ...this.columnsRenamed.map((col) => ` ${c.yellow(`~ ${col} — renamed`)}`),
498
571
  ...this.columnsDropped.map((col) => ` ${c.red(`- ${col} — dropped`)}`),
@@ -508,52 +581,10 @@ var SyncEntry = class {
508
581
  const prefix = this.viewType ? `${c.dim(`[${this.viewType}]`)} ` : "";
509
582
  return ` ${c.green("✓")} ${prefix}${c.bold(this.name)} ${c.dim("— in sync")}`;
510
583
  }
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
584
  };
554
-
555
585
  //#endregion
556
- //#region packages/db/src/schema/sync-executor.ts
586
+ //#region src/schema/sync-executor.ts
587
+ /** Checks if a tracked view's definition changed since the last stored snapshot. */
557
588
  async function viewDefinitionChanged(view, store) {
558
589
  const storedSnapshot = await store.readTableSnapshot(view.tableName, true);
559
590
  if (!storedSnapshot) return false;
@@ -596,8 +627,7 @@ async function executeSyncTable(readable, safe, trackedNames, deps) {
596
627
  }
597
628
  }
598
629
  const typeMapper = adapter.typeMapper?.bind(adapter);
599
- const diff = computeColumnDiff(readable.fieldDescriptors, existing, typeMapper);
600
- await applyColumnDiff(adapter, readable, diff, init, safe, deps.logger);
630
+ await applyColumnDiff(adapter, readable, computeColumnDiff(readable.fieldDescriptors, existing, typeMapper), init, safe, deps.logger);
601
631
  }
602
632
  } else if (adapter.syncColumns) if (!storedSnapshot) {
603
633
  const existed = adapter.tableExists ? await adapter.tableExists() : false;
@@ -605,10 +635,9 @@ async function executeSyncTable(readable, safe, trackedNames, deps) {
605
635
  if (!existed) init.status = "create";
606
636
  } else {
607
637
  const existing = snapshotToExistingColumns(storedSnapshot);
608
- const diff = computeColumnDiff(readable.fieldDescriptors, existing, deps.resolveTypeMapper(adapter));
609
- await applyColumnDiff(adapter, readable, diff, init, safe, deps.logger);
638
+ await applyColumnDiff(adapter, readable, computeColumnDiff(readable.fieldDescriptors, existing, deps.resolveTypeMapper(adapter)), init, safe, deps.logger);
610
639
  }
611
- else {
640
+ else {
612
641
  const existed = adapter.tableExists ? await adapter.tableExists() : true;
613
642
  if (!init.recreated) {
614
643
  await adapter.ensureTable();
@@ -626,8 +655,7 @@ else {
626
655
  init.status = "alter";
627
656
  }
628
657
  if (hasDestructive) {
629
- const syncMethod = readable.syncMethod;
630
- if (syncMethod === "recreate" && adapter.recreateTable) {
658
+ if (readable.syncMethod === "recreate" && adapter.recreateTable) {
631
659
  deps.logger.warn?.(`[schema-sync] Destructive table option change on "${name}" — recreating with data preservation`);
632
660
  await adapter.recreateTable();
633
661
  init.status = "alter";
@@ -660,14 +688,14 @@ async function executeSyncView(view, trackedNames, deps) {
660
688
  const viewType = view.viewPlan.materialized ? "M" : "V";
661
689
  let status;
662
690
  if (isRenamed || definitionChanged) status = "alter";
663
- else if (trackedNames.has(view.tableName)) status = "in-sync";
664
- else status = "create";
691
+ else if (trackedNames.has(view.tableName)) status = "in-sync";
692
+ else status = "create";
665
693
  return new SyncEntry({
666
694
  name: view.tableName,
667
695
  status,
668
696
  viewType,
669
- renamedFrom: isRenamed ? renamedFrom : undefined,
670
- recreated: definitionChanged || undefined
697
+ renamedFrom: isRenamed ? renamedFrom : void 0,
698
+ recreated: definitionChanged || void 0
671
699
  });
672
700
  }
673
701
  async function applyColumnDiff(adapter, readable, diff, init, safe, logger) {
@@ -696,7 +724,7 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger) {
696
724
  } else {
697
725
  const errors = [];
698
726
  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.`;
727
+ 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
728
  logger.error?.(`[schema-sync] ${msg}`);
701
729
  errors.push(msg);
702
730
  }
@@ -712,7 +740,7 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger) {
712
740
  init.recreated = true;
713
741
  init.status = "alter";
714
742
  } else init.status = "alter";
715
- else if (diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0) init.status = "alter";
743
+ else if (diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0) init.status = "alter";
716
744
  if (!init.recreated && init.status !== "error" && (diff.added.length > 0 || diff.renamed.length > 0 || needsSyncColumns) && adapter.syncColumns) {
717
745
  const syncResult = await adapter.syncColumns(diff);
718
746
  init.columnsAdded = syncResult.added;
@@ -726,32 +754,30 @@ else if (diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0) init
726
754
  init.status = "alter";
727
755
  }
728
756
  }
729
-
730
757
  //#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) {
758
+ //#region src/schema/schema-sync.ts
759
+ /** Builds a human-readable description of what changed in an FK constraint. */
760
+ function buildFkChangeDetails(desired, existing) {
743
761
  const parts = [];
744
762
  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"}`);
763
+ if ([...existing.targetFields].toSorted().join(",") !== [...desired.targetFields].toSorted().join(",")) parts.push(`fields ${existing.targetFields.join(",")} → ${desired.targetFields.join(",")}`);
764
+ if ((existing.onDelete ?? void 0) !== (desired.onDelete ?? void 0)) parts.push(`onDelete ${existing.onDelete ?? "noAction"} → ${desired.onDelete ?? "noAction"}`);
765
+ if ((existing.onUpdate ?? void 0) !== (desired.onUpdate ?? void 0)) parts.push(`onUpdate ${existing.onUpdate ?? "noAction"} → ${desired.onUpdate ?? "noAction"}`);
748
766
  return parts.join(", ");
749
767
  }
750
768
  var SchemaSync = class {
769
+ store;
770
+ logger;
771
+ constructor(space, logger) {
772
+ this.space = space;
773
+ this.logger = logger || require_db_view.NoopLogger;
774
+ this.store = new SyncStore(space);
775
+ }
751
776
  /**
752
777
  * Resolves types into categorized readables and computes the schema hash.
753
778
  * Passes each adapter's typeMapper for precise type tracking in snapshots.
754
- */ async resolveAndHash(types) {
779
+ */
780
+ async resolveAndHash(types) {
755
781
  const tables = [];
756
782
  const views = [];
757
783
  const externalViews = [];
@@ -760,32 +786,29 @@ var SchemaSync = class {
760
786
  if (readable.isView) {
761
787
  const view = readable;
762
788
  if (view.isExternal) externalViews.push(view);
763
- else views.push(readable);
789
+ else views.push(readable);
764
790
  } else tables.push(readable);
765
791
  }
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
792
  return {
779
793
  tables,
780
794
  views,
781
795
  externalViews,
782
- hash
796
+ hash: computeSchemaHash([
797
+ ...tables,
798
+ ...views,
799
+ ...externalViews
800
+ ].map((r) => {
801
+ if (r.isView) return computeViewSnapshot(r);
802
+ const tm = r.dbAdapter.typeMapper?.bind(r.dbAdapter);
803
+ return computeTableSnapshot(r, tm, (r.fieldDescriptors, r.dbAdapter.getDesiredTableOptions?.()));
804
+ }))
783
805
  };
784
806
  }
785
807
  /**
786
808
  * Checks an external view: verifies it exists in the DB and columns match.
787
809
  * Returns a SyncEntry with status 'in-sync' or 'error'.
788
- */ async checkExternalView(view) {
810
+ */
811
+ async checkExternalView(view) {
789
812
  const adapter = view.dbAdapter;
790
813
  const name = view.tableName;
791
814
  if (adapter.getExistingColumns) {
@@ -805,8 +828,7 @@ else views.push(readable);
805
828
  errors: [`External view "${name}" is missing columns: ${missing.join(", ")}`]
806
829
  });
807
830
  } else if (adapter.tableExists) {
808
- const exists = await adapter.tableExists();
809
- if (!exists) return new SyncEntry({
831
+ if (!await adapter.tableExists()) return new SyncEntry({
810
832
  name,
811
833
  viewType: "E",
812
834
  status: "error",
@@ -822,8 +844,9 @@ else views.push(readable);
822
844
  /**
823
845
  * Detects tables/views present in the previous sync but absent from the current schema.
824
846
  * Returns SyncEntry instances with status 'drop'.
825
- */ async detectRemoved(currentReadables, previous) {
826
- previous ?? (previous = await this.store.readTrackedList());
847
+ */
848
+ async detectRemoved(currentReadables, previous) {
849
+ previous ??= await this.store.readTrackedList();
827
850
  const currentSet = new Set(currentReadables.map((t) => t.tableName));
828
851
  const renameFromSet = new Set(currentReadables.map((r) => r.renamedFrom).filter(Boolean));
829
852
  const removed = [];
@@ -838,7 +861,8 @@ else views.push(readable);
838
861
  * Starts a periodic heartbeat that extends the lock's TTL while sync runs.
839
862
  * Returns a handle with `stop()` to cancel and `getAbortReason()` to check
840
863
  * whether the lock was stolen or unexpectedly removed.
841
- */ startHeartbeat(podId, ttlMs) {
864
+ */
865
+ startHeartbeat(podId, ttlMs) {
842
866
  let abortReason;
843
867
  let stopped = false;
844
868
  const intervalMs = Math.max(Math.floor(ttlMs / 3), 1e3);
@@ -868,13 +892,15 @@ else views.push(readable);
868
892
  getAbortReason: () => abortReason
869
893
  };
870
894
  }
871
- /** Throws if the heartbeat detected a stolen/missing lock. */ assertLockHeld(getAbortReason) {
895
+ /** Throws if the heartbeat detected a stolen/missing lock. */
896
+ assertLockHeld(getAbortReason) {
872
897
  const reason = getAbortReason();
873
898
  if (reason) throw new Error(reason);
874
899
  }
875
900
  /**
876
901
  * Runs schema synchronization with distributed locking.
877
- */ async run(types, opts) {
902
+ */
903
+ async run(types, opts) {
878
904
  const podId = opts?.podId ?? crypto.randomUUID();
879
905
  const lockTtlMs = opts?.lockTtlMs ?? 3e4;
880
906
  const waitTimeoutMs = opts?.waitTimeoutMs ?? 6e4;
@@ -884,30 +910,25 @@ else views.push(readable);
884
910
  const { tables, views, externalViews, hash } = await this.resolveAndHash(types);
885
911
  await this.store.ensureControlTable();
886
912
  if (!force) {
887
- const storedHash = await this.store.readHash();
888
- if (storedHash === hash) return {
913
+ if (await this.store.readHash() === hash) return {
889
914
  status: "up-to-date",
890
915
  schemaHash: hash,
891
916
  entries: []
892
917
  };
893
918
  }
894
- const acquired = await this.store.tryAcquireLock(podId, lockTtlMs);
895
- if (!acquired) {
919
+ if (!await this.store.tryAcquireLock(podId, lockTtlMs)) {
896
920
  await this.store.waitForLock(waitTimeoutMs, pollIntervalMs);
897
- const storedHash = await this.store.readHash();
898
- if (storedHash === hash) return {
921
+ if (await this.store.readHash() === hash) return {
899
922
  status: "synced-by-peer",
900
923
  schemaHash: hash,
901
924
  entries: []
902
925
  };
903
- const retryAcquired = await this.store.tryAcquireLock(podId, lockTtlMs);
904
- if (!retryAcquired) throw new Error("Failed to acquire schema sync lock after waiting");
926
+ if (!await this.store.tryAcquireLock(podId, lockTtlMs)) throw new Error("Failed to acquire schema sync lock after waiting");
905
927
  }
906
928
  const heartbeat = this.startHeartbeat(podId, lockTtlMs);
907
929
  try {
908
930
  if (!force) {
909
- const storedHash = await this.store.readHash();
910
- if (storedHash === hash) return {
931
+ if (await this.store.readHash() === hash) return {
911
932
  status: "synced-by-peer",
912
933
  schemaHash: hash,
913
934
  entries: []
@@ -939,7 +960,7 @@ else views.push(readable);
939
960
  for (const entry of removed) {
940
961
  if (entry.viewType === "E") continue;
941
962
  if (entry.viewType) await this.space.dropViewByName(entry.name);
942
- else await this.space.dropTableByName(entry.name);
963
+ else await this.space.dropTableByName(entry.name);
943
964
  }
944
965
  entries.push(...removed.filter((e) => e.viewType !== "E"));
945
966
  }
@@ -947,8 +968,8 @@ else await this.space.dropTableByName(entry.name);
947
968
  for (const readable of allReadables) {
948
969
  const adapter = readable.dbAdapter;
949
970
  const tm = adapter.typeMapper?.bind(adapter);
950
- const opts$1 = adapter.getDesiredTableOptions?.();
951
- const snapshot = readable.isView ? computeViewSnapshot(readable) : computeTableSnapshot(readable, tm, opts$1);
971
+ const opts = adapter.getDesiredTableOptions?.();
972
+ const snapshot = readable.isView ? computeViewSnapshot(readable) : computeTableSnapshot(readable, tm, opts);
952
973
  await this.store.writeTableSnapshot(readable.tableName, snapshot);
953
974
  }
954
975
  if (!safe) for (const entry of removed) {
@@ -970,7 +991,8 @@ else await this.space.dropTableByName(entry.name);
970
991
  }
971
992
  /**
972
993
  * Computes a dry-run plan showing what `run()` would do, without executing any DDL.
973
- */ async plan(types, opts) {
994
+ */
995
+ async plan(types, opts) {
974
996
  const force = opts?.force ?? false;
975
997
  const safe = opts?.safe ?? false;
976
998
  const { tables, views, externalViews, hash } = await this.resolveAndHash(types);
@@ -986,8 +1008,7 @@ else await this.space.dropTableByName(entry.name);
986
1008
  const viewEntries = await Promise.all(views.map((v) => this.planView(v, trackedNames)));
987
1009
  const externalEntries = await Promise.all(externalViews.map((v) => this.checkExternalView(v)));
988
1010
  if (!force) {
989
- const storedHash = await this.store.readHash();
990
- if (storedHash === hash) return {
1011
+ if (await this.store.readHash() === hash) return {
991
1012
  status: "up-to-date",
992
1013
  schemaHash: hash,
993
1014
  entries: [
@@ -1000,7 +1021,23 @@ else await this.space.dropTableByName(entry.name);
1000
1021
  let removed = await this.detectRemoved(allReadables, previouslyTracked);
1001
1022
  if (safe) {
1002
1023
  planEntries = planEntries.map((e) => new SyncEntry({
1003
- ...e,
1024
+ name: e.name,
1025
+ viewType: e.viewType,
1026
+ status: e.status,
1027
+ syncMethod: e.syncMethod,
1028
+ columnsToAdd: e.columnsToAdd,
1029
+ columnsToRename: e.columnsToRename,
1030
+ nullableChanges: e.nullableChanges,
1031
+ defaultChanges: e.defaultChanges,
1032
+ optionChanges: e.optionChanges,
1033
+ fkAdded: e.fkAdded,
1034
+ fkRemoved: e.fkRemoved,
1035
+ fkChanged: e.fkChanged,
1036
+ columnsAdded: e.columnsAdded,
1037
+ columnsRenamed: e.columnsRenamed,
1038
+ columnsDropped: e.columnsDropped,
1039
+ errors: e.errors,
1040
+ renamedFrom: e.renamedFrom,
1004
1041
  columnsToDrop: [],
1005
1042
  typeChanges: [],
1006
1043
  recreated: false
@@ -1019,7 +1056,8 @@ else await this.space.dropTableByName(entry.name);
1019
1056
  ]
1020
1057
  };
1021
1058
  }
1022
- /** Fallback typeMapper for snapshot-based Path B: compares designType directly, skips unions. */ resolveTypeMapper(adapter) {
1059
+ /** Fallback typeMapper for snapshot-based Path B: compares designType directly, skips unions. */
1060
+ resolveTypeMapper(adapter) {
1023
1061
  return adapter.typeMapper?.bind(adapter) ?? ((f) => f.designType === "union" ? "union" : f.designType);
1024
1062
  }
1025
1063
  async planTable(readable, trackedNames) {
@@ -1050,8 +1088,7 @@ else await this.space.dropTableByName(entry.name);
1050
1088
  }
1051
1089
  } else if (adapter.syncColumns) if (!storedSnapshot) {
1052
1090
  if (!pendingRename) {
1053
- const exists = adapter.tableExists ? await adapter.tableExists() : false;
1054
- if (!exists) {
1091
+ if (!(adapter.tableExists ? await adapter.tableExists() : false)) {
1055
1092
  init.status = "create";
1056
1093
  init.columnsToAdd = readable.fieldDescriptors.filter((f) => !f.ignored);
1057
1094
  }
@@ -1061,9 +1098,8 @@ else await this.space.dropTableByName(entry.name);
1061
1098
  const diff = computeColumnDiff(readable.fieldDescriptors, existing, this.resolveTypeMapper(adapter));
1062
1099
  this.populatePlanFromDiff(diff, init, name, readable.syncMethod, adapter.supportsColumnModify);
1063
1100
  }
1064
- else if (adapter.tableExists) {
1065
- const exists = await adapter.tableExists();
1066
- if (!exists) init.status = "create";
1101
+ else if (adapter.tableExists) {
1102
+ if (!await adapter.tableExists()) init.status = "create";
1067
1103
  } else init.status = "create";
1068
1104
  if (init.status !== "create") {
1069
1105
  const optionDiff = await this.diffTableOptions(readable);
@@ -1096,7 +1132,8 @@ else if (adapter.tableExists) {
1096
1132
  }
1097
1133
  /**
1098
1134
  * Populates plan init from a column diff (shared by Path A and Path B).
1099
- */ populatePlanFromDiff(diff, init, name, syncMethod, adapterSupportsModify) {
1135
+ */
1136
+ populatePlanFromDiff(diff, init, name, syncMethod, adapterSupportsModify) {
1100
1137
  init.columnsToAdd = diff.added;
1101
1138
  init.columnsToRename = diff.renamed.map((r) => ({
1102
1139
  from: r.oldName,
@@ -1117,27 +1154,27 @@ else if (adapter.tableExists) {
1117
1154
  newDefault: dc.newDefault
1118
1155
  }));
1119
1156
  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";
1157
+ 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.status = "alter";
1122
1158
  if (diff.conflicts.length > 0) {
1123
1159
  init.status = "error";
1124
1160
  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
1161
  }
1126
1162
  if (diff.typeChanged.length > 0 && !syncMethod && !adapterSupportsModify) {
1127
1163
  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.`)];
1164
+ 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
1165
  }
1130
1166
  }
1131
1167
  /**
1132
1168
  * Computes table option diff using DB-first introspection with snapshot fallback.
1133
1169
  * Returns null if the adapter has no table options.
1134
- */ async diffTableOptions(readable) {
1170
+ */
1171
+ async diffTableOptions(readable) {
1135
1172
  const adapter = readable.dbAdapter;
1136
1173
  const desired = adapter.getDesiredTableOptions?.();
1137
1174
  if (!desired || desired.length === 0) return null;
1138
1175
  let existing;
1139
1176
  if (adapter.getExistingTableOptions) existing = await adapter.getExistingTableOptions();
1140
- else {
1177
+ else {
1141
1178
  const snapshot = await this.store.readTableSnapshot(readable.tableName);
1142
1179
  existing = snapshot ? snapshotToExistingTableOptions(snapshot) : [];
1143
1180
  }
@@ -1151,14 +1188,14 @@ else {
1151
1188
  const isRenamed = renamedFrom && trackedNames.has(renamedFrom);
1152
1189
  let status;
1153
1190
  if (isRenamed) status = "alter";
1154
- else if (trackedNames.has(view.tableName)) status = await viewDefinitionChanged(view, this.store) ? "alter" : "in-sync";
1155
- else status = "create";
1191
+ else if (trackedNames.has(view.tableName)) status = await viewDefinitionChanged(view, this.store) ? "alter" : "in-sync";
1192
+ else status = "create";
1156
1193
  return new SyncEntry({
1157
1194
  name: view.tableName,
1158
1195
  status,
1159
1196
  viewType,
1160
- renamedFrom: isRenamed ? renamedFrom : undefined,
1161
- recreated: status === "alter" && !isRenamed ? true : undefined
1197
+ renamedFrom: isRenamed ? renamedFrom : void 0,
1198
+ recreated: status === "alter" && !isRenamed ? true : void 0
1162
1199
  });
1163
1200
  }
1164
1201
  buildExecutorDeps() {
@@ -1170,36 +1207,47 @@ else status = "create";
1170
1207
  diffTableOptions: this.diffTableOptions.bind(this)
1171
1208
  };
1172
1209
  }
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
1210
  };
1182
-
1183
1211
  //#endregion
1184
- //#region packages/db/src/sync.ts
1212
+ //#region src/sync.ts
1213
+ /**
1214
+ * Synchronizes database schema with distributed locking.
1215
+ * Safe to call from multiple concurrent processes/pods.
1216
+ *
1217
+ * ```typescript
1218
+ * import { syncSchema } from '@atscript/db/sync'
1219
+ *
1220
+ * const db = new DbSpace(() => new SqliteAdapter(driver))
1221
+ * await syncSchema(db, [UsersType, PostsType, CommentsType])
1222
+ * ```
1223
+ *
1224
+ * The function:
1225
+ * 1. Creates an `__atscript_control` table for lock coordination
1226
+ * 2. Computes a schema hash — skips entirely if nothing changed
1227
+ * 3. Acquires a distributed lock so only one process syncs
1228
+ * 4. Creates tables, adds new columns, syncs indexes
1229
+ * 5. Stores the new hash and releases the lock
1230
+ *
1231
+ * @param space - The DbSpace containing the adapter factory.
1232
+ * @param types - Atscript annotated types to synchronize.
1233
+ * @param opts - Lock TTL, wait timeout, force mode, etc.
1234
+ */
1185
1235
  async function syncSchema(space, types, opts) {
1186
- const sync = new SchemaSync(space);
1187
- return sync.run(types, opts);
1236
+ return new SchemaSync(space).run(types, opts);
1188
1237
  }
1189
-
1190
1238
  //#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
1239
+ exports.SchemaSync = SchemaSync;
1240
+ exports.SyncEntry = SyncEntry;
1241
+ exports.computeColumnDiff = computeColumnDiff;
1242
+ exports.computeForeignKeyDiff = computeForeignKeyDiff;
1243
+ exports.computeSchemaHash = computeSchemaHash;
1244
+ exports.computeTableHash = computeTableHash;
1245
+ exports.computeTableOptionDiff = computeTableOptionDiff;
1246
+ exports.computeTableSnapshot = computeTableSnapshot;
1247
+ exports.computeViewSnapshot = computeViewSnapshot;
1248
+ exports.fkKey = fkKey;
1249
+ exports.hasForeignKeyChanges = hasForeignKeyChanges;
1250
+ exports.readStoredSnapshot = readStoredSnapshot;
1251
+ exports.snapshotToExistingColumns = snapshotToExistingColumns;
1252
+ exports.snapshotToExistingTableOptions = snapshotToExistingTableOptions;
1253
+ exports.syncSchema = syncSchema;