@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.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { NoopLogger } from "./logger-B7oxCfLQ.mjs";
2
-
3
- //#region packages/db/src/schema/schema-hash.ts
4
- /** Extracts sorted field snapshots from a readable's field descriptors. */ function extractFieldSnapshots(fields, typeMapper) {
1
+ import { _ as NoopLogger } from "./db-view-ZsoN91-q.mjs";
2
+ //#region src/schema/schema-hash.ts
3
+ /** Extracts sorted field snapshots from a readable's field descriptors. */
4
+ function extractFieldSnapshots(fields, typeMapper) {
5
5
  return fields.filter((f) => !f.ignored).map((f) => {
6
6
  const snap = {
7
7
  physicalName: f.physicalName,
@@ -13,8 +13,17 @@ import { NoopLogger } from "./logger-B7oxCfLQ.mjs";
13
13
  if (f.defaultValue) snap.defaultValue = f.defaultValue;
14
14
  if (typeMapper) snap.mappedType = typeMapper(f);
15
15
  return snap;
16
- }).sort((a, b) => a.physicalName.localeCompare(b.physicalName));
16
+ }).toSorted((a, b) => a.physicalName.localeCompare(b.physicalName));
17
17
  }
18
+ /**
19
+ * Extracts a canonical, serializable snapshot from a readable's metadata.
20
+ * Sorted deterministically so the hash is stable across runs.
21
+ *
22
+ * @param readable - The table/view readable.
23
+ * @param typeMapper - Optional adapter-specific type mapper. When provided,
24
+ * each field's mapped type (e.g., "VARCHAR(255)") is stored in the snapshot
25
+ * for precise type change detection.
26
+ */
18
27
  function computeTableSnapshot(readable, typeMapper, tableOptions) {
19
28
  const fields = extractFieldSnapshots(readable.fieldDescriptors, typeMapper);
20
29
  const indexes = [...readable.indexes.values()].map((idx) => ({
@@ -24,23 +33,28 @@ function computeTableSnapshot(readable, typeMapper, tableOptions) {
24
33
  name: f.name,
25
34
  sort: f.sort
26
35
  }))
27
- })).sort((a, b) => a.key.localeCompare(b.key));
36
+ })).toSorted((a, b) => a.key.localeCompare(b.key));
28
37
  const foreignKeys = [...readable.foreignKeys.values()].map((fk) => ({
29
- fields: [...fk.fields].sort(),
38
+ fields: [...fk.fields].toSorted(),
30
39
  targetTable: fk.targetTable,
31
- targetFields: [...fk.targetFields].sort(),
40
+ targetFields: [...fk.targetFields].toSorted(),
32
41
  onDelete: fk.onDelete,
33
42
  onUpdate: fk.onUpdate
34
- })).sort((a, b) => a.fields.join(",").localeCompare(b.fields.join(",")));
43
+ })).toSorted((a, b) => a.fields.join(",").localeCompare(b.fields.join(",")));
35
44
  const snapshot = {
36
45
  tableName: readable.tableName,
37
46
  fields,
38
47
  indexes,
39
48
  foreignKeys
40
49
  };
41
- if (tableOptions?.length) snapshot.tableOptions = [...tableOptions].sort((a, b) => a.key.localeCompare(b.key));
50
+ if (tableOptions?.length) snapshot.tableOptions = [...tableOptions].toSorted((a, b) => a.key.localeCompare(b.key));
42
51
  return snapshot;
43
52
  }
53
+ /**
54
+ * Extracts a canonical, serializable snapshot from a view's metadata.
55
+ * Captures view plan (entry table, joins, filter, materialization) for
56
+ * detecting view definition changes.
57
+ */
44
58
  function computeViewSnapshot(view) {
45
59
  const fields = extractFieldSnapshots(view.fieldDescriptors);
46
60
  if (view.isExternal) return {
@@ -54,20 +68,35 @@ function computeViewSnapshot(view) {
54
68
  viewType: plan.materialized ? "M" : "V",
55
69
  entryTable: plan.entryTable,
56
70
  joinTables: plan.joins.map((j) => j.targetTable),
57
- materialized: plan.materialized || undefined,
71
+ materialized: plan.materialized || void 0,
58
72
  fields
59
73
  };
60
74
  if (plan.filter) result.filterHash = fnv1a(JSON.stringify(plan.filter, (_, v) => typeof v === "function" ? "[fn]" : v));
61
75
  return result;
62
76
  }
77
+ /**
78
+ * Computes a deterministic hash string from multiple table snapshots.
79
+ * Uses FNV-1a for speed — not cryptographic, just needs stability + collision resistance.
80
+ */
63
81
  function computeSchemaHash(snapshots) {
64
- const sorted = [...snapshots].sort((a, b) => a.tableName.localeCompare(b.tableName));
65
- const json = JSON.stringify(sorted);
66
- return fnv1a(json);
82
+ const sorted = [...snapshots].toSorted((a, b) => a.tableName.localeCompare(b.tableName));
83
+ return fnv1a(JSON.stringify(sorted));
67
84
  }
85
+ /**
86
+ * Computes a hash for a single table/view snapshot.
87
+ * Used for per-table change detection via stored snapshots.
88
+ */
68
89
  function computeTableHash(snapshot) {
69
90
  return fnv1a(JSON.stringify(snapshot));
70
91
  }
92
+ /**
93
+ * Converts stored snapshot fields to `TExistingColumn[]` format
94
+ * for use with `computeColumnDiff`. Used by adapters that lack
95
+ * native column introspection (e.g., MongoDB).
96
+ *
97
+ * The `type` field uses `mappedType` when available (adapter-specific),
98
+ * falling back to `designType`.
99
+ */
71
100
  function snapshotToExistingColumns(snapshot) {
72
101
  return snapshot.fields.map((f) => ({
73
102
  name: f.physicalName,
@@ -77,15 +106,21 @@ function snapshotToExistingColumns(snapshot) {
77
106
  dflt_value: serializeDefaultValue(f.defaultValue)
78
107
  }));
79
108
  }
109
+ /**
110
+ * Extracts table options from a stored snapshot for diff comparison.
111
+ * Used as fallback when an adapter lacks native table option introspection.
112
+ */
80
113
  function snapshotToExistingTableOptions(snapshot) {
81
114
  return snapshot.tableOptions ?? [];
82
115
  }
116
+ /** Serializes a TDbDefaultValue to a comparable string. */
83
117
  function serializeDefaultValue(dv) {
84
- if (!dv) return undefined;
118
+ if (!dv) return;
85
119
  if (dv.kind === "value") return dv.value;
86
120
  return `fn:${dv.fn}`;
87
121
  }
88
- /** FNV-1a 32-bit hash → hex string */ function fnv1a(str) {
122
+ /** FNV-1a 32-bit hash → hex string */
123
+ function fnv1a(str) {
89
124
  let hash = 2166136261;
90
125
  for (let i = 0; i < str.length; i++) {
91
126
  hash ^= str.codePointAt(i);
@@ -93,13 +128,22 @@ function serializeDefaultValue(dv) {
93
128
  }
94
129
  return Math.trunc(hash).toString(16).padStart(8, "0");
95
130
  }
96
-
97
131
  //#endregion
98
- //#region packages/db/src/schema/column-diff.ts
132
+ //#region src/schema/column-diff.ts
133
+ /**
134
+ * Computes the difference between desired schema fields and existing database columns.
135
+ *
136
+ * @param desired - Field descriptors from the Atscript type (after flattening).
137
+ * @param existing - Columns currently in the database (from introspection).
138
+ * @param typeMapper - Optional function to map field metadata to DB-native type strings.
139
+ * Receives the full field meta (design type, annotations, PK status, etc.)
140
+ * so adapters can produce context-aware types (e.g., `VARCHAR(255)` from maxLength).
141
+ * Required for type change detection.
142
+ */
99
143
  function computeColumnDiff(desired, existing, typeMapper) {
100
144
  const existingByName = new Map(existing.map((c) => [c.name, c]));
101
- const desiredByName = new Map();
102
- const renamedOldNames = new Set();
145
+ const desiredByName = /* @__PURE__ */ new Map();
146
+ const renamedOldNames = /* @__PURE__ */ new Set();
103
147
  const added = [];
104
148
  const renamed = [];
105
149
  const typeChanged = [];
@@ -119,8 +163,7 @@ function computeColumnDiff(desired, existing, typeMapper) {
119
163
  renamedOldNames.add(field.renamedFrom);
120
164
  } else {
121
165
  if (typeMapper) {
122
- const expectedType = typeMapper(field);
123
- if (expectedType.toUpperCase() !== existingCol.type.toUpperCase()) typeChanged.push({
166
+ if (typeMapper(field).toUpperCase() !== existingCol.type.toUpperCase()) typeChanged.push({
124
167
  field,
125
168
  existingType: existingCol.type
126
169
  });
@@ -133,13 +176,13 @@ function computeColumnDiff(desired, existing, typeMapper) {
133
176
  });
134
177
  }
135
178
  const desiredDefault = serializeDefaultValue(field.defaultValue);
136
- if (existingCol.dflt_value !== undefined && existingCol.dflt_value !== desiredDefault) defaultChanged.push({
179
+ if (existingCol.dflt_value !== void 0 && existingCol.dflt_value !== desiredDefault) defaultChanged.push({
137
180
  field,
138
181
  oldDefault: existingCol.dflt_value,
139
182
  newDefault: desiredDefault
140
183
  });
141
184
  }
142
- else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
185
+ else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
143
186
  renamed.push({
144
187
  field,
145
188
  oldName: field.renamedFrom
@@ -147,10 +190,9 @@ else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
147
190
  renamedOldNames.add(field.renamedFrom);
148
191
  } else added.push(field);
149
192
  }
150
- const removed = existing.filter((c) => !desiredByName.has(c.name) && !renamedOldNames.has(c.name));
151
193
  return {
152
194
  added,
153
- removed,
195
+ removed: existing.filter((c) => !desiredByName.has(c.name) && !renamedOldNames.has(c.name)),
154
196
  renamed,
155
197
  typeChanged,
156
198
  nullableChanged,
@@ -158,25 +200,30 @@ else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
158
200
  conflicts
159
201
  };
160
202
  }
161
-
162
203
  //#endregion
163
- //#region packages/db/src/schema/fk-diff.ts
204
+ //#region src/schema/fk-diff.ts
205
+ /** Canonical key for an FK: sorted local field names, comma-joined. */
164
206
  function fkKey(fields) {
165
- return [...fields].sort().join(",");
207
+ return [...fields].toSorted().join(",");
166
208
  }
209
+ /**
210
+ * Compares desired FK constraints against stored snapshot to detect
211
+ * additions, removals, and property changes (target table, target fields,
212
+ * onDelete, onUpdate).
213
+ */
167
214
  function computeForeignKeyDiff(desired, existingSnapshot) {
168
215
  const added = [];
169
216
  const removed = [];
170
217
  const changed = [];
171
- const existingByKey = new Map();
218
+ const existingByKey = /* @__PURE__ */ new Map();
172
219
  for (const fk of existingSnapshot) existingByKey.set(fkKey(fk.fields), fk);
173
- const desiredKeys = new Set();
220
+ const desiredKeys = /* @__PURE__ */ new Set();
174
221
  for (const fk of desired.values()) {
175
222
  const key = fkKey(fk.fields);
176
223
  desiredKeys.add(key);
177
224
  const existing = existingByKey.get(key);
178
225
  if (!existing) added.push(fk);
179
- else if (fkPropertiesDiffer(fk, existing)) changed.push({
226
+ else if (fkPropertiesDiffer(fk, existing)) changed.push({
180
227
  desired: fk,
181
228
  existing
182
229
  });
@@ -188,25 +235,36 @@ else if (fkPropertiesDiffer(fk, existing)) changed.push({
188
235
  changed
189
236
  };
190
237
  }
238
+ /** Whether the FK diff contains any changes. */
191
239
  function hasForeignKeyChanges(diff) {
192
240
  return diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
193
241
  }
194
242
  function fkPropertiesDiffer(desired, existing) {
195
243
  if (desired.targetTable !== existing.targetTable) return true;
196
244
  if (fkKey(desired.targetFields) !== fkKey(existing.targetFields)) return true;
197
- if ((desired.onDelete ?? undefined) !== (existing.onDelete ?? undefined)) return true;
198
- if ((desired.onUpdate ?? undefined) !== (existing.onUpdate ?? undefined)) return true;
245
+ if ((desired.onDelete ?? void 0) !== (existing.onDelete ?? void 0)) return true;
246
+ if ((desired.onUpdate ?? void 0) !== (existing.onUpdate ?? void 0)) return true;
199
247
  return false;
200
248
  }
201
-
202
249
  //#endregion
203
- //#region packages/db/src/schema/table-option-diff.ts
250
+ //#region src/schema/table-option-diff.ts
251
+ /**
252
+ * Computes the difference between desired and existing table options.
253
+ *
254
+ * Options present in desired but absent from existing are ignored (initial state).
255
+ * Options present in existing but absent from desired are ignored (sticky options).
256
+ * Only value changes on matching keys are tracked.
257
+ *
258
+ * @param desired - Options from Atscript annotations (via adapter.getDesiredTableOptions()).
259
+ * @param existing - Options from DB introspection or snapshot fallback.
260
+ * @param destructiveKeys - Option keys where a value change requires table recreation.
261
+ */
204
262
  function computeTableOptionDiff(desired, existing, destructiveKeys) {
205
263
  const existingByKey = new Map(existing.map((o) => [o.key, o.value]));
206
264
  const changed = [];
207
265
  for (const opt of desired) {
208
266
  const existingValue = existingByKey.get(opt.key);
209
- if (existingValue !== undefined && existingValue !== opt.value) changed.push({
267
+ if (existingValue !== void 0 && existingValue !== opt.value) changed.push({
210
268
  key: opt.key,
211
269
  oldValue: existingValue,
212
270
  newValue: opt.value,
@@ -215,41 +273,32 @@ function computeTableOptionDiff(desired, existing, destructiveKeys) {
215
273
  }
216
274
  return { changed };
217
275
  }
218
-
219
276
  //#endregion
220
- //#region packages/db/src/schema/sync-store.ts
221
- function _define_property$2(obj, key, value) {
222
- if (key in obj) Object.defineProperty(obj, key, {
223
- value,
224
- enumerable: true,
225
- configurable: true,
226
- writable: true
227
- });
228
- else obj[key] = value;
229
- return obj;
230
- }
277
+ //#region src/schema/sync-store.ts
231
278
  var SyncStore = class {
279
+ controlTable;
280
+ constructor(space) {
281
+ this.space = space;
282
+ }
232
283
  async ensureControlTable() {
233
284
  if (!this.controlTable) {
234
- const { AtscriptControl } = await import("./control_as-bjmwe24C.mjs");
285
+ const { AtscriptControl } = await import("./control-IANbnfjG.mjs");
235
286
  this.controlTable = this.space.getTable(AtscriptControl);
236
287
  }
237
288
  await this.controlTable.ensureTable();
238
289
  }
239
290
  async readControlValue(_id) {
240
- const row = await this.controlTable.findOne({
291
+ return (await this.controlTable.findOne({
241
292
  filter: { _id: { $eq: _id } },
242
293
  controls: {}
243
- });
244
- return row?.value ?? null;
294
+ }))?.value ?? null;
245
295
  }
246
296
  async writeControlValue(_id, value) {
247
- const existing = await this.readControlValue(_id);
248
- if (existing !== null) await this.controlTable.replaceOne({
297
+ if (await this.readControlValue(_id) !== null) await this.controlTable.replaceOne({
249
298
  _id,
250
299
  value
251
300
  });
252
- else await this.controlTable.insertOne({
301
+ else await this.controlTable.insertOne({
253
302
  _id,
254
303
  value
255
304
  });
@@ -280,10 +329,9 @@ else await this.controlTable.insertOne({
280
329
  name,
281
330
  isView: false
282
331
  }));
283
- return parsed.map((e) => ({
284
- ...e,
285
- viewType: e.viewType ?? (e.isView ? "V" : undefined)
286
- }));
332
+ const entries = parsed;
333
+ for (const e of entries) e.viewType ??= e.isView ? "V" : void 0;
334
+ return entries;
287
335
  }
288
336
  async writeTrackedList(readables) {
289
337
  const entries = readables.map((r) => {
@@ -311,7 +359,7 @@ else await this.controlTable.insertOne({
311
359
  if (existing) {
312
360
  const expiresAt = existing.expiresAt;
313
361
  if (expiresAt && expiresAt < now) await this.controlTable.deleteOne("sync_lock");
314
- else return false;
362
+ else return false;
315
363
  }
316
364
  try {
317
365
  await this.controlTable.insertOne({
@@ -368,36 +416,19 @@ else return false;
368
416
  }
369
417
  throw new Error(`Schema sync lock wait timed out after ${timeoutMs}ms`);
370
418
  }
371
- constructor(space) {
372
- _define_property$2(this, "space", void 0);
373
- _define_property$2(this, "controlTable", void 0);
374
- this.space = space;
375
- }
376
419
  };
377
420
  async function readStoredSnapshot(space, tableName, _asView) {
378
- const { AtscriptControl } = await import("./control_as-bjmwe24C.mjs");
421
+ const { AtscriptControl } = await import("./control-IANbnfjG.mjs");
379
422
  const table = space.getTable(AtscriptControl);
380
423
  await table.ensureTable();
381
- const row = await table.findOne({
424
+ const value = (await table.findOne({
382
425
  filter: { _id: { $eq: `table_snapshot:${tableName}` } },
383
426
  controls: {}
384
- });
385
- const value = row?.value ?? null;
427
+ }))?.value ?? null;
386
428
  return value ? JSON.parse(value) : null;
387
429
  }
388
-
389
430
  //#endregion
390
- //#region packages/db/src/schema/sync-entry.ts
391
- function _define_property$1(obj, key, value) {
392
- if (key in obj) Object.defineProperty(obj, key, {
393
- value,
394
- enumerable: true,
395
- configurable: true,
396
- writable: true
397
- });
398
- else obj[key] = value;
399
- return obj;
400
- }
431
+ //#region src/schema/sync-entry.ts
401
432
  const noColor = {
402
433
  green: (s) => s,
403
434
  red: (s) => s,
@@ -408,17 +439,64 @@ const noColor = {
408
439
  underline: (s) => s
409
440
  };
410
441
  var SyncEntry = class {
411
- /** Whether this entry involves destructive operations */ get destructive() {
442
+ name;
443
+ /** 'V' = virtual view, 'M' = materialized view, 'E' = external view, undefined = table */
444
+ viewType;
445
+ status;
446
+ syncMethod;
447
+ columnsToAdd;
448
+ columnsToRename;
449
+ typeChanges;
450
+ nullableChanges;
451
+ defaultChanges;
452
+ columnsToDrop;
453
+ optionChanges;
454
+ fkAdded;
455
+ fkRemoved;
456
+ fkChanged;
457
+ columnsAdded;
458
+ columnsRenamed;
459
+ columnsDropped;
460
+ recreated;
461
+ errors;
462
+ renamedFrom;
463
+ constructor(init) {
464
+ this.name = init.name;
465
+ this.viewType = init.viewType;
466
+ this.status = init.status;
467
+ this.syncMethod = init.syncMethod;
468
+ this.columnsToAdd = init.columnsToAdd ?? [];
469
+ this.columnsToRename = init.columnsToRename ?? [];
470
+ this.typeChanges = init.typeChanges ?? [];
471
+ this.nullableChanges = init.nullableChanges ?? [];
472
+ this.defaultChanges = init.defaultChanges ?? [];
473
+ this.columnsToDrop = init.columnsToDrop ?? [];
474
+ this.optionChanges = init.optionChanges ?? [];
475
+ this.fkAdded = init.fkAdded ?? [];
476
+ this.fkRemoved = init.fkRemoved ?? [];
477
+ this.fkChanged = init.fkChanged ?? [];
478
+ this.columnsAdded = init.columnsAdded ?? [];
479
+ this.columnsRenamed = init.columnsRenamed ?? [];
480
+ this.columnsDropped = init.columnsDropped ?? [];
481
+ this.recreated = init.recreated ?? false;
482
+ this.errors = init.errors ?? [];
483
+ this.renamedFrom = init.renamedFrom;
484
+ }
485
+ /** Whether this entry involves destructive operations */
486
+ get destructive() {
412
487
  if (this.status === "drop") return this.viewType !== "V" && this.viewType !== "E";
413
488
  return this.columnsToDrop.length > 0 || this.typeChanges.length > 0 || this.recreated || this.optionChanges.some((c) => c.destructive);
414
489
  }
415
- /** Whether this entry represents any change (not in-sync) */ get hasChanges() {
490
+ /** Whether this entry represents any change (not in-sync) */
491
+ get hasChanges() {
416
492
  return this.status !== "in-sync" && this.status !== "error";
417
493
  }
418
- /** Whether this entry has errors */ get hasErrors() {
494
+ /** Whether this entry has errors */
495
+ get hasErrors() {
419
496
  return this.status === "error" || this.errors.length > 0;
420
497
  }
421
- /** Render this entry for display */ print(mode, colors) {
498
+ /** Render this entry for display */
499
+ print(mode, colors) {
422
500
  const c = colors ?? noColor;
423
501
  return mode === "plan" ? this.printPlan(c) : this.printResult(c);
424
502
  }
@@ -481,13 +559,11 @@ var SyncEntry = class {
481
559
  ...this.columnsAdded.map((col) => ` ${c.green(`+ ${col} — added`)}`),
482
560
  ""
483
561
  ];
484
- const hasChanges = this.columnsAdded.length > 0 || this.columnsRenamed.length > 0 || this.columnsDropped.length > 0 || this.optionChanges.length > 0;
485
- if (hasChanges || this.recreated || this.renamedFrom) {
562
+ if (this.columnsAdded.length > 0 || this.columnsRenamed.length > 0 || this.columnsDropped.length > 0 || this.optionChanges.length > 0 || this.recreated || this.renamedFrom) {
486
563
  const rlabel = this.recreated ? "recreated" : "altered";
487
564
  const renameInfo = this.renamedFrom ? ` ${c.yellow(`(renamed from ${this.renamedFrom})`)}` : "";
488
- const color = this.recreated ? c.yellow : c.cyan;
489
565
  return [
490
- ` ${color(`~ ${vp}${label} — ${rlabel}${renameInfo}`)}`,
566
+ ` ${(this.recreated ? (s) => c.yellow(s) : (s) => c.cyan(s))(`~ ${vp}${label} — ${rlabel}${renameInfo}`)}`,
491
567
  ...this.columnsAdded.map((col) => ` ${c.green(`+ ${col} — added`)}`),
492
568
  ...this.columnsRenamed.map((col) => ` ${c.yellow(`~ ${col} — renamed`)}`),
493
569
  ...this.columnsDropped.map((col) => ` ${c.red(`- ${col} — dropped`)}`),
@@ -503,52 +579,10 @@ var SyncEntry = class {
503
579
  const prefix = this.viewType ? `${c.dim(`[${this.viewType}]`)} ` : "";
504
580
  return ` ${c.green("✓")} ${prefix}${c.bold(this.name)} ${c.dim("— in sync")}`;
505
581
  }
506
- constructor(init) {
507
- _define_property$1(this, "name", void 0);
508
- /** 'V' = virtual view, 'M' = materialized view, 'E' = external view, undefined = table */ _define_property$1(this, "viewType", void 0);
509
- _define_property$1(this, "status", void 0);
510
- _define_property$1(this, "syncMethod", void 0);
511
- _define_property$1(this, "columnsToAdd", void 0);
512
- _define_property$1(this, "columnsToRename", void 0);
513
- _define_property$1(this, "typeChanges", void 0);
514
- _define_property$1(this, "nullableChanges", void 0);
515
- _define_property$1(this, "defaultChanges", void 0);
516
- _define_property$1(this, "columnsToDrop", void 0);
517
- _define_property$1(this, "optionChanges", void 0);
518
- _define_property$1(this, "fkAdded", void 0);
519
- _define_property$1(this, "fkRemoved", void 0);
520
- _define_property$1(this, "fkChanged", void 0);
521
- _define_property$1(this, "columnsAdded", void 0);
522
- _define_property$1(this, "columnsRenamed", void 0);
523
- _define_property$1(this, "columnsDropped", void 0);
524
- _define_property$1(this, "recreated", void 0);
525
- _define_property$1(this, "errors", void 0);
526
- _define_property$1(this, "renamedFrom", void 0);
527
- this.name = init.name;
528
- this.viewType = init.viewType;
529
- this.status = init.status;
530
- this.syncMethod = init.syncMethod;
531
- this.columnsToAdd = init.columnsToAdd ?? [];
532
- this.columnsToRename = init.columnsToRename ?? [];
533
- this.typeChanges = init.typeChanges ?? [];
534
- this.nullableChanges = init.nullableChanges ?? [];
535
- this.defaultChanges = init.defaultChanges ?? [];
536
- this.columnsToDrop = init.columnsToDrop ?? [];
537
- this.optionChanges = init.optionChanges ?? [];
538
- this.fkAdded = init.fkAdded ?? [];
539
- this.fkRemoved = init.fkRemoved ?? [];
540
- this.fkChanged = init.fkChanged ?? [];
541
- this.columnsAdded = init.columnsAdded ?? [];
542
- this.columnsRenamed = init.columnsRenamed ?? [];
543
- this.columnsDropped = init.columnsDropped ?? [];
544
- this.recreated = init.recreated ?? false;
545
- this.errors = init.errors ?? [];
546
- this.renamedFrom = init.renamedFrom;
547
- }
548
582
  };
549
-
550
583
  //#endregion
551
- //#region packages/db/src/schema/sync-executor.ts
584
+ //#region src/schema/sync-executor.ts
585
+ /** Checks if a tracked view's definition changed since the last stored snapshot. */
552
586
  async function viewDefinitionChanged(view, store) {
553
587
  const storedSnapshot = await store.readTableSnapshot(view.tableName, true);
554
588
  if (!storedSnapshot) return false;
@@ -591,8 +625,7 @@ async function executeSyncTable(readable, safe, trackedNames, deps) {
591
625
  }
592
626
  }
593
627
  const typeMapper = adapter.typeMapper?.bind(adapter);
594
- const diff = computeColumnDiff(readable.fieldDescriptors, existing, typeMapper);
595
- await applyColumnDiff(adapter, readable, diff, init, safe, deps.logger);
628
+ await applyColumnDiff(adapter, readable, computeColumnDiff(readable.fieldDescriptors, existing, typeMapper), init, safe, deps.logger);
596
629
  }
597
630
  } else if (adapter.syncColumns) if (!storedSnapshot) {
598
631
  const existed = adapter.tableExists ? await adapter.tableExists() : false;
@@ -600,10 +633,9 @@ async function executeSyncTable(readable, safe, trackedNames, deps) {
600
633
  if (!existed) init.status = "create";
601
634
  } else {
602
635
  const existing = snapshotToExistingColumns(storedSnapshot);
603
- const diff = computeColumnDiff(readable.fieldDescriptors, existing, deps.resolveTypeMapper(adapter));
604
- await applyColumnDiff(adapter, readable, diff, init, safe, deps.logger);
636
+ await applyColumnDiff(adapter, readable, computeColumnDiff(readable.fieldDescriptors, existing, deps.resolveTypeMapper(adapter)), init, safe, deps.logger);
605
637
  }
606
- else {
638
+ else {
607
639
  const existed = adapter.tableExists ? await adapter.tableExists() : true;
608
640
  if (!init.recreated) {
609
641
  await adapter.ensureTable();
@@ -621,8 +653,7 @@ else {
621
653
  init.status = "alter";
622
654
  }
623
655
  if (hasDestructive) {
624
- const syncMethod = readable.syncMethod;
625
- if (syncMethod === "recreate" && adapter.recreateTable) {
656
+ if (readable.syncMethod === "recreate" && adapter.recreateTable) {
626
657
  deps.logger.warn?.(`[schema-sync] Destructive table option change on "${name}" — recreating with data preservation`);
627
658
  await adapter.recreateTable();
628
659
  init.status = "alter";
@@ -655,14 +686,14 @@ async function executeSyncView(view, trackedNames, deps) {
655
686
  const viewType = view.viewPlan.materialized ? "M" : "V";
656
687
  let status;
657
688
  if (isRenamed || definitionChanged) status = "alter";
658
- else if (trackedNames.has(view.tableName)) status = "in-sync";
659
- else status = "create";
689
+ else if (trackedNames.has(view.tableName)) status = "in-sync";
690
+ else status = "create";
660
691
  return new SyncEntry({
661
692
  name: view.tableName,
662
693
  status,
663
694
  viewType,
664
- renamedFrom: isRenamed ? renamedFrom : undefined,
665
- recreated: definitionChanged || undefined
695
+ renamedFrom: isRenamed ? renamedFrom : void 0,
696
+ recreated: definitionChanged || void 0
666
697
  });
667
698
  }
668
699
  async function applyColumnDiff(adapter, readable, diff, init, safe, logger) {
@@ -691,7 +722,7 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger) {
691
722
  } else {
692
723
  const errors = [];
693
724
  for (const change of diff.typeChanged) {
694
- const msg = `Type change on ${name}.${change.field.physicalName} ` + `(${change.existingType} → ${change.field.designType}). ` + `Add @db.sync.method "recreate" or "drop", or migrate manually.`;
725
+ const msg = `Type change on ${name}.${change.field.physicalName} (${change.existingType} → ${change.field.designType}). Add @db.sync.method "recreate" or "drop", or migrate manually.`;
695
726
  logger.error?.(`[schema-sync] ${msg}`);
696
727
  errors.push(msg);
697
728
  }
@@ -707,7 +738,7 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger) {
707
738
  init.recreated = true;
708
739
  init.status = "alter";
709
740
  } else init.status = "alter";
710
- else if (diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0) init.status = "alter";
741
+ else if (diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0) init.status = "alter";
711
742
  if (!init.recreated && init.status !== "error" && (diff.added.length > 0 || diff.renamed.length > 0 || needsSyncColumns) && adapter.syncColumns) {
712
743
  const syncResult = await adapter.syncColumns(diff);
713
744
  init.columnsAdded = syncResult.added;
@@ -721,32 +752,30 @@ else if (diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0) init
721
752
  init.status = "alter";
722
753
  }
723
754
  }
724
-
725
755
  //#endregion
726
- //#region packages/db/src/schema/schema-sync.ts
727
- function _define_property(obj, key, value) {
728
- if (key in obj) Object.defineProperty(obj, key, {
729
- value,
730
- enumerable: true,
731
- configurable: true,
732
- writable: true
733
- });
734
- else obj[key] = value;
735
- return obj;
736
- }
737
- /** Builds a human-readable description of what changed in an FK constraint. */ function buildFkChangeDetails(desired, existing) {
756
+ //#region src/schema/schema-sync.ts
757
+ /** Builds a human-readable description of what changed in an FK constraint. */
758
+ function buildFkChangeDetails(desired, existing) {
738
759
  const parts = [];
739
760
  if (existing.targetTable !== desired.targetTable) parts.push(`retarget ${existing.targetTable} → ${desired.targetTable}`);
740
- if ([...existing.targetFields].sort().join(",") !== [...desired.targetFields].sort().join(",")) parts.push(`fields ${existing.targetFields.join(",")} → ${desired.targetFields.join(",")}`);
741
- if ((existing.onDelete ?? undefined) !== (desired.onDelete ?? undefined)) parts.push(`onDelete ${existing.onDelete ?? "noAction"} → ${desired.onDelete ?? "noAction"}`);
742
- if ((existing.onUpdate ?? undefined) !== (desired.onUpdate ?? undefined)) parts.push(`onUpdate ${existing.onUpdate ?? "noAction"} → ${desired.onUpdate ?? "noAction"}`);
761
+ if ([...existing.targetFields].toSorted().join(",") !== [...desired.targetFields].toSorted().join(",")) parts.push(`fields ${existing.targetFields.join(",")} → ${desired.targetFields.join(",")}`);
762
+ if ((existing.onDelete ?? void 0) !== (desired.onDelete ?? void 0)) parts.push(`onDelete ${existing.onDelete ?? "noAction"} → ${desired.onDelete ?? "noAction"}`);
763
+ if ((existing.onUpdate ?? void 0) !== (desired.onUpdate ?? void 0)) parts.push(`onUpdate ${existing.onUpdate ?? "noAction"} → ${desired.onUpdate ?? "noAction"}`);
743
764
  return parts.join(", ");
744
765
  }
745
766
  var SchemaSync = class {
767
+ store;
768
+ logger;
769
+ constructor(space, logger) {
770
+ this.space = space;
771
+ this.logger = logger || NoopLogger;
772
+ this.store = new SyncStore(space);
773
+ }
746
774
  /**
747
775
  * Resolves types into categorized readables and computes the schema hash.
748
776
  * Passes each adapter's typeMapper for precise type tracking in snapshots.
749
- */ async resolveAndHash(types) {
777
+ */
778
+ async resolveAndHash(types) {
750
779
  const tables = [];
751
780
  const views = [];
752
781
  const externalViews = [];
@@ -755,32 +784,29 @@ var SchemaSync = class {
755
784
  if (readable.isView) {
756
785
  const view = readable;
757
786
  if (view.isExternal) externalViews.push(view);
758
- else views.push(readable);
787
+ else views.push(readable);
759
788
  } else tables.push(readable);
760
789
  }
761
- const allReadables = [
762
- ...tables,
763
- ...views,
764
- ...externalViews
765
- ];
766
- const snapshots = allReadables.map((r) => {
767
- if (r.isView) return computeViewSnapshot(r);
768
- const tm = r.dbAdapter.typeMapper?.bind(r.dbAdapter);
769
- const opts = (r.fieldDescriptors, r.dbAdapter.getDesiredTableOptions?.());
770
- return computeTableSnapshot(r, tm, opts);
771
- });
772
- const hash = computeSchemaHash(snapshots);
773
790
  return {
774
791
  tables,
775
792
  views,
776
793
  externalViews,
777
- hash
794
+ hash: computeSchemaHash([
795
+ ...tables,
796
+ ...views,
797
+ ...externalViews
798
+ ].map((r) => {
799
+ if (r.isView) return computeViewSnapshot(r);
800
+ const tm = r.dbAdapter.typeMapper?.bind(r.dbAdapter);
801
+ return computeTableSnapshot(r, tm, (r.fieldDescriptors, r.dbAdapter.getDesiredTableOptions?.()));
802
+ }))
778
803
  };
779
804
  }
780
805
  /**
781
806
  * Checks an external view: verifies it exists in the DB and columns match.
782
807
  * Returns a SyncEntry with status 'in-sync' or 'error'.
783
- */ async checkExternalView(view) {
808
+ */
809
+ async checkExternalView(view) {
784
810
  const adapter = view.dbAdapter;
785
811
  const name = view.tableName;
786
812
  if (adapter.getExistingColumns) {
@@ -800,8 +826,7 @@ else views.push(readable);
800
826
  errors: [`External view "${name}" is missing columns: ${missing.join(", ")}`]
801
827
  });
802
828
  } else if (adapter.tableExists) {
803
- const exists = await adapter.tableExists();
804
- if (!exists) return new SyncEntry({
829
+ if (!await adapter.tableExists()) return new SyncEntry({
805
830
  name,
806
831
  viewType: "E",
807
832
  status: "error",
@@ -817,8 +842,9 @@ else views.push(readable);
817
842
  /**
818
843
  * Detects tables/views present in the previous sync but absent from the current schema.
819
844
  * Returns SyncEntry instances with status 'drop'.
820
- */ async detectRemoved(currentReadables, previous) {
821
- previous ?? (previous = await this.store.readTrackedList());
845
+ */
846
+ async detectRemoved(currentReadables, previous) {
847
+ previous ??= await this.store.readTrackedList();
822
848
  const currentSet = new Set(currentReadables.map((t) => t.tableName));
823
849
  const renameFromSet = new Set(currentReadables.map((r) => r.renamedFrom).filter(Boolean));
824
850
  const removed = [];
@@ -833,7 +859,8 @@ else views.push(readable);
833
859
  * Starts a periodic heartbeat that extends the lock's TTL while sync runs.
834
860
  * Returns a handle with `stop()` to cancel and `getAbortReason()` to check
835
861
  * whether the lock was stolen or unexpectedly removed.
836
- */ startHeartbeat(podId, ttlMs) {
862
+ */
863
+ startHeartbeat(podId, ttlMs) {
837
864
  let abortReason;
838
865
  let stopped = false;
839
866
  const intervalMs = Math.max(Math.floor(ttlMs / 3), 1e3);
@@ -863,13 +890,15 @@ else views.push(readable);
863
890
  getAbortReason: () => abortReason
864
891
  };
865
892
  }
866
- /** Throws if the heartbeat detected a stolen/missing lock. */ assertLockHeld(getAbortReason) {
893
+ /** Throws if the heartbeat detected a stolen/missing lock. */
894
+ assertLockHeld(getAbortReason) {
867
895
  const reason = getAbortReason();
868
896
  if (reason) throw new Error(reason);
869
897
  }
870
898
  /**
871
899
  * Runs schema synchronization with distributed locking.
872
- */ async run(types, opts) {
900
+ */
901
+ async run(types, opts) {
873
902
  const podId = opts?.podId ?? crypto.randomUUID();
874
903
  const lockTtlMs = opts?.lockTtlMs ?? 3e4;
875
904
  const waitTimeoutMs = opts?.waitTimeoutMs ?? 6e4;
@@ -879,30 +908,25 @@ else views.push(readable);
879
908
  const { tables, views, externalViews, hash } = await this.resolveAndHash(types);
880
909
  await this.store.ensureControlTable();
881
910
  if (!force) {
882
- const storedHash = await this.store.readHash();
883
- if (storedHash === hash) return {
911
+ if (await this.store.readHash() === hash) return {
884
912
  status: "up-to-date",
885
913
  schemaHash: hash,
886
914
  entries: []
887
915
  };
888
916
  }
889
- const acquired = await this.store.tryAcquireLock(podId, lockTtlMs);
890
- if (!acquired) {
917
+ if (!await this.store.tryAcquireLock(podId, lockTtlMs)) {
891
918
  await this.store.waitForLock(waitTimeoutMs, pollIntervalMs);
892
- const storedHash = await this.store.readHash();
893
- if (storedHash === hash) return {
919
+ if (await this.store.readHash() === hash) return {
894
920
  status: "synced-by-peer",
895
921
  schemaHash: hash,
896
922
  entries: []
897
923
  };
898
- const retryAcquired = await this.store.tryAcquireLock(podId, lockTtlMs);
899
- if (!retryAcquired) throw new Error("Failed to acquire schema sync lock after waiting");
924
+ if (!await this.store.tryAcquireLock(podId, lockTtlMs)) throw new Error("Failed to acquire schema sync lock after waiting");
900
925
  }
901
926
  const heartbeat = this.startHeartbeat(podId, lockTtlMs);
902
927
  try {
903
928
  if (!force) {
904
- const storedHash = await this.store.readHash();
905
- if (storedHash === hash) return {
929
+ if (await this.store.readHash() === hash) return {
906
930
  status: "synced-by-peer",
907
931
  schemaHash: hash,
908
932
  entries: []
@@ -934,7 +958,7 @@ else views.push(readable);
934
958
  for (const entry of removed) {
935
959
  if (entry.viewType === "E") continue;
936
960
  if (entry.viewType) await this.space.dropViewByName(entry.name);
937
- else await this.space.dropTableByName(entry.name);
961
+ else await this.space.dropTableByName(entry.name);
938
962
  }
939
963
  entries.push(...removed.filter((e) => e.viewType !== "E"));
940
964
  }
@@ -942,8 +966,8 @@ else await this.space.dropTableByName(entry.name);
942
966
  for (const readable of allReadables) {
943
967
  const adapter = readable.dbAdapter;
944
968
  const tm = adapter.typeMapper?.bind(adapter);
945
- const opts$1 = adapter.getDesiredTableOptions?.();
946
- const snapshot = readable.isView ? computeViewSnapshot(readable) : computeTableSnapshot(readable, tm, opts$1);
969
+ const opts = adapter.getDesiredTableOptions?.();
970
+ const snapshot = readable.isView ? computeViewSnapshot(readable) : computeTableSnapshot(readable, tm, opts);
947
971
  await this.store.writeTableSnapshot(readable.tableName, snapshot);
948
972
  }
949
973
  if (!safe) for (const entry of removed) {
@@ -965,7 +989,8 @@ else await this.space.dropTableByName(entry.name);
965
989
  }
966
990
  /**
967
991
  * Computes a dry-run plan showing what `run()` would do, without executing any DDL.
968
- */ async plan(types, opts) {
992
+ */
993
+ async plan(types, opts) {
969
994
  const force = opts?.force ?? false;
970
995
  const safe = opts?.safe ?? false;
971
996
  const { tables, views, externalViews, hash } = await this.resolveAndHash(types);
@@ -981,8 +1006,7 @@ else await this.space.dropTableByName(entry.name);
981
1006
  const viewEntries = await Promise.all(views.map((v) => this.planView(v, trackedNames)));
982
1007
  const externalEntries = await Promise.all(externalViews.map((v) => this.checkExternalView(v)));
983
1008
  if (!force) {
984
- const storedHash = await this.store.readHash();
985
- if (storedHash === hash) return {
1009
+ if (await this.store.readHash() === hash) return {
986
1010
  status: "up-to-date",
987
1011
  schemaHash: hash,
988
1012
  entries: [
@@ -995,7 +1019,23 @@ else await this.space.dropTableByName(entry.name);
995
1019
  let removed = await this.detectRemoved(allReadables, previouslyTracked);
996
1020
  if (safe) {
997
1021
  planEntries = planEntries.map((e) => new SyncEntry({
998
- ...e,
1022
+ name: e.name,
1023
+ viewType: e.viewType,
1024
+ status: e.status,
1025
+ syncMethod: e.syncMethod,
1026
+ columnsToAdd: e.columnsToAdd,
1027
+ columnsToRename: e.columnsToRename,
1028
+ nullableChanges: e.nullableChanges,
1029
+ defaultChanges: e.defaultChanges,
1030
+ optionChanges: e.optionChanges,
1031
+ fkAdded: e.fkAdded,
1032
+ fkRemoved: e.fkRemoved,
1033
+ fkChanged: e.fkChanged,
1034
+ columnsAdded: e.columnsAdded,
1035
+ columnsRenamed: e.columnsRenamed,
1036
+ columnsDropped: e.columnsDropped,
1037
+ errors: e.errors,
1038
+ renamedFrom: e.renamedFrom,
999
1039
  columnsToDrop: [],
1000
1040
  typeChanges: [],
1001
1041
  recreated: false
@@ -1014,7 +1054,8 @@ else await this.space.dropTableByName(entry.name);
1014
1054
  ]
1015
1055
  };
1016
1056
  }
1017
- /** Fallback typeMapper for snapshot-based Path B: compares designType directly, skips unions. */ resolveTypeMapper(adapter) {
1057
+ /** Fallback typeMapper for snapshot-based Path B: compares designType directly, skips unions. */
1058
+ resolveTypeMapper(adapter) {
1018
1059
  return adapter.typeMapper?.bind(adapter) ?? ((f) => f.designType === "union" ? "union" : f.designType);
1019
1060
  }
1020
1061
  async planTable(readable, trackedNames) {
@@ -1045,8 +1086,7 @@ else await this.space.dropTableByName(entry.name);
1045
1086
  }
1046
1087
  } else if (adapter.syncColumns) if (!storedSnapshot) {
1047
1088
  if (!pendingRename) {
1048
- const exists = adapter.tableExists ? await adapter.tableExists() : false;
1049
- if (!exists) {
1089
+ if (!(adapter.tableExists ? await adapter.tableExists() : false)) {
1050
1090
  init.status = "create";
1051
1091
  init.columnsToAdd = readable.fieldDescriptors.filter((f) => !f.ignored);
1052
1092
  }
@@ -1056,9 +1096,8 @@ else await this.space.dropTableByName(entry.name);
1056
1096
  const diff = computeColumnDiff(readable.fieldDescriptors, existing, this.resolveTypeMapper(adapter));
1057
1097
  this.populatePlanFromDiff(diff, init, name, readable.syncMethod, adapter.supportsColumnModify);
1058
1098
  }
1059
- else if (adapter.tableExists) {
1060
- const exists = await adapter.tableExists();
1061
- if (!exists) init.status = "create";
1099
+ else if (adapter.tableExists) {
1100
+ if (!await adapter.tableExists()) init.status = "create";
1062
1101
  } else init.status = "create";
1063
1102
  if (init.status !== "create") {
1064
1103
  const optionDiff = await this.diffTableOptions(readable);
@@ -1091,7 +1130,8 @@ else if (adapter.tableExists) {
1091
1130
  }
1092
1131
  /**
1093
1132
  * Populates plan init from a column diff (shared by Path A and Path B).
1094
- */ populatePlanFromDiff(diff, init, name, syncMethod, adapterSupportsModify) {
1133
+ */
1134
+ populatePlanFromDiff(diff, init, name, syncMethod, adapterSupportsModify) {
1095
1135
  init.columnsToAdd = diff.added;
1096
1136
  init.columnsToRename = diff.renamed.map((r) => ({
1097
1137
  from: r.oldName,
@@ -1112,27 +1152,27 @@ else if (adapter.tableExists) {
1112
1152
  newDefault: dc.newDefault
1113
1153
  }));
1114
1154
  init.columnsToDrop = diff.removed.map((c) => c.name);
1115
- 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;
1116
- if (hasChanges) init.status = "alter";
1155
+ 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";
1117
1156
  if (diff.conflicts.length > 0) {
1118
1157
  init.status = "error";
1119
1158
  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.`)];
1120
1159
  }
1121
1160
  if (diff.typeChanged.length > 0 && !syncMethod && !adapterSupportsModify) {
1122
1161
  init.status = "error";
1123
- 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.`)];
1162
+ 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.`)];
1124
1163
  }
1125
1164
  }
1126
1165
  /**
1127
1166
  * Computes table option diff using DB-first introspection with snapshot fallback.
1128
1167
  * Returns null if the adapter has no table options.
1129
- */ async diffTableOptions(readable) {
1168
+ */
1169
+ async diffTableOptions(readable) {
1130
1170
  const adapter = readable.dbAdapter;
1131
1171
  const desired = adapter.getDesiredTableOptions?.();
1132
1172
  if (!desired || desired.length === 0) return null;
1133
1173
  let existing;
1134
1174
  if (adapter.getExistingTableOptions) existing = await adapter.getExistingTableOptions();
1135
- else {
1175
+ else {
1136
1176
  const snapshot = await this.store.readTableSnapshot(readable.tableName);
1137
1177
  existing = snapshot ? snapshotToExistingTableOptions(snapshot) : [];
1138
1178
  }
@@ -1146,14 +1186,14 @@ else {
1146
1186
  const isRenamed = renamedFrom && trackedNames.has(renamedFrom);
1147
1187
  let status;
1148
1188
  if (isRenamed) status = "alter";
1149
- else if (trackedNames.has(view.tableName)) status = await viewDefinitionChanged(view, this.store) ? "alter" : "in-sync";
1150
- else status = "create";
1189
+ else if (trackedNames.has(view.tableName)) status = await viewDefinitionChanged(view, this.store) ? "alter" : "in-sync";
1190
+ else status = "create";
1151
1191
  return new SyncEntry({
1152
1192
  name: view.tableName,
1153
1193
  status,
1154
1194
  viewType,
1155
- renamedFrom: isRenamed ? renamedFrom : undefined,
1156
- recreated: status === "alter" && !isRenamed ? true : undefined
1195
+ renamedFrom: isRenamed ? renamedFrom : void 0,
1196
+ recreated: status === "alter" && !isRenamed ? true : void 0
1157
1197
  });
1158
1198
  }
1159
1199
  buildExecutorDeps() {
@@ -1165,22 +1205,33 @@ else status = "create";
1165
1205
  diffTableOptions: this.diffTableOptions.bind(this)
1166
1206
  };
1167
1207
  }
1168
- constructor(space, logger) {
1169
- _define_property(this, "space", void 0);
1170
- _define_property(this, "store", void 0);
1171
- _define_property(this, "logger", void 0);
1172
- this.space = space;
1173
- this.logger = logger || NoopLogger;
1174
- this.store = new SyncStore(space);
1175
- }
1176
1208
  };
1177
-
1178
1209
  //#endregion
1179
- //#region packages/db/src/sync.ts
1210
+ //#region src/sync.ts
1211
+ /**
1212
+ * Synchronizes database schema with distributed locking.
1213
+ * Safe to call from multiple concurrent processes/pods.
1214
+ *
1215
+ * ```typescript
1216
+ * import { syncSchema } from '@atscript/db/sync'
1217
+ *
1218
+ * const db = new DbSpace(() => new SqliteAdapter(driver))
1219
+ * await syncSchema(db, [UsersType, PostsType, CommentsType])
1220
+ * ```
1221
+ *
1222
+ * The function:
1223
+ * 1. Creates an `__atscript_control` table for lock coordination
1224
+ * 2. Computes a schema hash — skips entirely if nothing changed
1225
+ * 3. Acquires a distributed lock so only one process syncs
1226
+ * 4. Creates tables, adds new columns, syncs indexes
1227
+ * 5. Stores the new hash and releases the lock
1228
+ *
1229
+ * @param space - The DbSpace containing the adapter factory.
1230
+ * @param types - Atscript annotated types to synchronize.
1231
+ * @param opts - Lock TTL, wait timeout, force mode, etc.
1232
+ */
1180
1233
  async function syncSchema(space, types, opts) {
1181
- const sync = new SchemaSync(space);
1182
- return sync.run(types, opts);
1234
+ return new SchemaSync(space).run(types, opts);
1183
1235
  }
1184
-
1185
1236
  //#endregion
1186
- export { SchemaSync, SyncEntry, computeColumnDiff, computeForeignKeyDiff, computeSchemaHash, computeTableHash, computeTableOptionDiff, computeTableSnapshot, computeViewSnapshot, fkKey, hasForeignKeyChanges, readStoredSnapshot, snapshotToExistingColumns, snapshotToExistingTableOptions, syncSchema };
1237
+ export { SchemaSync, SyncEntry, computeColumnDiff, computeForeignKeyDiff, computeSchemaHash, computeTableHash, computeTableOptionDiff, computeTableSnapshot, computeViewSnapshot, fkKey, hasForeignKeyChanges, readStoredSnapshot, snapshotToExistingColumns, snapshotToExistingTableOptions, syncSchema };