@atscript/db 0.1.140 → 0.1.141

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/agg.d.cts +1 -1
  2. package/dist/agg.d.mts +1 -1
  3. package/dist/{buckets-C-27xmtq.d.cts → buckets-Bv4pah66.d.cts} +225 -22
  4. package/dist/{buckets-BFG2RYRW.d.mts → buckets-CjL7F-hp.d.mts} +225 -22
  5. package/dist/{column-diff-CgxgFKzx.cjs → column-diff-CfPNcP6e.cjs} +663 -239
  6. package/dist/{column-diff-BwOA5101.mjs → column-diff-CmFNXV8C.mjs} +638 -220
  7. package/dist/column-diff-DiBbXyLA.d.cts +211 -0
  8. package/dist/column-diff-n-k5KY0u.d.mts +211 -0
  9. package/dist/derived-rules-0sKn4f5C.mjs +44 -0
  10. package/dist/derived-rules-YstgIxG-.cjs +67 -0
  11. package/dist/index.cjs +38 -4
  12. package/dist/index.d.cts +23 -6
  13. package/dist/index.d.mts +23 -6
  14. package/dist/index.mjs +34 -4
  15. package/dist/{nested-writer-FWD5oOYh.mjs → nested-writer-BO3vhbkP.mjs} +8 -4
  16. package/dist/{nested-writer-BZNCuqI6.cjs → nested-writer-DYsRxZ5f.cjs} +8 -4
  17. package/dist/object-DSN0h9lB.d.cts +30 -0
  18. package/dist/object-DSN0h9lB.d.mts +30 -0
  19. package/dist/plugin.cjs +392 -139
  20. package/dist/plugin.mjs +392 -139
  21. package/dist/rel.cjs +2 -2
  22. package/dist/rel.d.cts +2 -2
  23. package/dist/rel.d.mts +2 -2
  24. package/dist/rel.mjs +2 -2
  25. package/dist/{relation-helpers-D3Zu0Mta.d.mts → relation-helpers-B59to_dG.d.mts} +5 -4
  26. package/dist/{relation-helpers-DxrvS6ar.d.cts → relation-helpers-DQ_nRsV9.d.cts} +5 -4
  27. package/dist/{relation-loader-6ZB_5KFq.cjs → relation-loader-CgJ8bK6X.cjs} +1 -1
  28. package/dist/{relation-loader-CTFaZpVa.mjs → relation-loader-CuhEBzFU.mjs} +1 -1
  29. package/dist/shared.cjs +6 -1
  30. package/dist/shared.d.cts +48 -9
  31. package/dist/shared.d.mts +48 -9
  32. package/dist/shared.mjs +2 -2
  33. package/dist/sync.cjs +331 -105
  34. package/dist/sync.d.cts +62 -163
  35. package/dist/sync.d.mts +62 -163
  36. package/dist/sync.mjs +331 -105
  37. package/dist/{validation-utils-B4h-GW4d.mjs → validation-utils-CMR4fe2M.mjs} +99 -34
  38. package/dist/{validation-utils-Dg0hW6dn.cjs → validation-utils-DOsB4e6G.cjs} +128 -33
  39. package/dist/{validator-Drb2N-YL.d.cts → validator-Bw6ks9Hy.d.cts} +1 -11
  40. package/dist/{validator-Drb2N-YL.d.mts → validator-Bw6ks9Hy.d.mts} +1 -11
  41. package/dist/{validator-Ch7UIQl9.mjs → validator-D8bPsXPN.mjs} +54 -2
  42. package/dist/{validator-BtZbcLN2.cjs → validator-DASnXf1j.cjs} +77 -1
  43. package/dist/validator.cjs +1 -1
  44. package/dist/validator.d.cts +2 -1
  45. package/dist/validator.d.mts +2 -1
  46. package/dist/validator.mjs +1 -1
  47. package/package.json +6 -6
  48. package/dist/column-diff-BmqvgBWw.d.cts +0 -24
  49. package/dist/column-diff-DPkbZIVE.d.mts +0 -24
package/dist/sync.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { $t as AtscriptDbReadable, Ft as DbSpace, G as TDbObjectKind, I as TDbDefaultValue, R as TDbFieldMeta, Rt as AtscriptDbView, Ut as AtscriptQueryFieldRef$1, Wt as AtscriptQueryNode$1, X as TDbStorageType, at as TExistingTableOption, ht as TReferencingForeignKey, it as TExistingForeignKey, mt as TPrimaryKeyChange, nt as TEnsureTableOptions, rt as TExistingColumn, sn as TGenericLogger, yt as TTableOptionDiff, z as TDbForeignKey } from "./buckets-C-27xmtq.cjs";
2
- import { t as computeColumnDiff } from "./column-diff-BmqvgBWw.cjs";
1
+ import { G as TDbObjectKind, Lt as DbSpace, R as TDbFieldMeta, _t as TReferencingForeignKey, gt as TPrimaryKeyChange, it as TEnsureTableOptions, nt as TDerivedChangeReason, ot as TExistingForeignKey, st as TExistingTableOption, un as TGenericLogger, xt as TTableOptionDiff, z as TDbForeignKey } from "./buckets-Bv4pah66.cjs";
2
+ import { a as TTableSnapshot, c as TViewSnapshot, d as computeTableHash, f as computeTableSnapshot, h as snapshotToExistingTableOptions, i as TForeignKeySnapshot, l as canonicalizeQueryNode, m as snapshotToExistingColumns, o as TViewColumnSnapshot, p as computeViewSnapshot, r as TFieldSnapshot, s as TViewJoinSnapshot, t as computeColumnDiff, u as computeSchemaHash } from "./column-diff-DiBbXyLA.cjs";
3
3
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/schema/sync-entry.d.ts
@@ -19,7 +19,17 @@ type TSyncEntryStatus = "create" | "alter" | "drop" | "in-sync" | "error";
19
19
  * recreate a destructive table-option change needs, and nullable/default
20
20
  * changes on adapters that need DDL for them.
21
21
  */
22
- type TSyncSkippedWork = "pk-rebuild" | "recreate" | "table-options" | "nullable-defaults";
22
+ type TSyncSkippedWork = "pk-rebuild" | "recreate" | "table-options" | "nullable-defaults" /** A derived column's drop + add (kind / expression / type change) — since 0.1.141. */ | "derived";
23
+ /**
24
+ * One derived-column rebuild as plan and run entries report it (since 0.1.141):
25
+ * `reason` as the column diff classified it, `derived` whether the MODEL side
26
+ * is the derived one (`kind` reads `regular → derived` / `derived → regular`).
27
+ */
28
+ interface TSyncDerivedChange {
29
+ column: string;
30
+ reason: TDerivedChangeReason;
31
+ derived: boolean;
32
+ }
23
33
  interface TSyncEntryInit {
24
34
  name: string;
25
35
  /** 'V' = virtual view, 'M' = materialized view, 'E' = external view, undefined = table */
@@ -60,6 +70,13 @@ interface TSyncEntryInit {
60
70
  targetTable: string;
61
71
  details: string;
62
72
  }>;
73
+ /**
74
+ * Derived columns the run drops and re-adds (a generated column's
75
+ * expression cannot be altered in place) — kept on the entry when safe
76
+ * mode skipped the rebuild (`skipped` includes `'derived'`).
77
+ * @since 0.1.141
78
+ */
79
+ derivedChanges?: TSyncDerivedChange[];
63
80
  columnsAdded?: string[];
64
81
  columnsRenamed?: string[];
65
82
  columnsDropped?: string[];
@@ -107,6 +124,13 @@ interface TSyncEntryInit {
107
124
  * @since 0.1.128
108
125
  */
109
126
  refused?: boolean;
127
+ /**
128
+ * Managed views this view reads whose recreate forces this view's own
129
+ * drop-and-recreate (its definition is unchanged). Printed as
130
+ * `· upstream view "x" recreated`.
131
+ * @since 0.1.141
132
+ */
133
+ cascadeFrom?: string[];
110
134
  }
111
135
  declare class SyncEntry {
112
136
  readonly name: string;
@@ -148,6 +172,8 @@ declare class SyncEntry {
148
172
  targetTable: string;
149
173
  details: string;
150
174
  }>;
175
+ /** @since 0.1.141 — see {@link TSyncEntryInit.derivedChanges}. */
176
+ readonly derivedChanges: TSyncDerivedChange[];
151
177
  readonly columnsAdded: string[];
152
178
  readonly columnsRenamed: string[];
153
179
  readonly columnsDropped: string[];
@@ -168,6 +194,8 @@ declare class SyncEntry {
168
194
  readonly dropGroup?: string[];
169
195
  /** @since 0.1.128 — see {@link TSyncEntryInit.refused}. */
170
196
  readonly refused: boolean;
197
+ /** @since 0.1.141 — see {@link TSyncEntryInit.cascadeFrom}. */
198
+ readonly cascadeFrom: string[];
171
199
  constructor(init: TSyncEntryInit);
172
200
  /**
173
201
  * The init object this entry was built from — lets callers derive a
@@ -208,6 +236,11 @@ declare class SyncEntry {
208
236
  * mode)` in plan and result when the `'drop'` recreate was skipped.
209
237
  */
210
238
  private printTypeChanges;
239
+ /**
240
+ * `~ col — derived column (expression changed) — drop + add` (plan) /
241
+ * `— rebuilt` (result), or `! … — skipped (safe mode)` when pending.
242
+ */
243
+ private printDerivedChanges;
211
244
  /** `~ col — nullable` / `~ col — default a → b`, `— skipped (safe mode)` when pending. */
212
245
  private printNullableDefaults;
213
246
  /**
@@ -218,165 +251,14 @@ declare class SyncEntry {
218
251
  private printOptionChanges;
219
252
  /** `· after: a, b` (plan only — the executor already runs in this order). */
220
253
  private printDependsOn;
254
+ /** `· upstream view "x" recreated` — why an unchanged view is recreated. */
255
+ private printCascade;
221
256
  private printDropGroup;
222
257
  private printPlan;
223
258
  private printResult;
224
259
  private printInSync;
225
260
  }
226
261
  //#endregion
227
- //#region src/schema/schema-hash.d.ts
228
- interface TFieldSnapshot {
229
- physicalName: string;
230
- designType: string;
231
- optional: boolean;
232
- isPrimaryKey: boolean;
233
- storage: TDbStorageType;
234
- defaultValue?: TDbDefaultValue;
235
- /** Adapter-specific mapped type (e.g., "VARCHAR(255)", "INTEGER"). */
236
- mappedType?: string;
237
- /** `@db.encrypted` — toggling encryption changes the snapshot hash on every adapter. */
238
- encrypted?: boolean;
239
- }
240
- interface TIndexSnapshot {
241
- key: string;
242
- type: string;
243
- fields: Array<{
244
- name: string;
245
- sort: string;
246
- }>;
247
- }
248
- interface TForeignKeySnapshot {
249
- fields: string[];
250
- targetTable: string;
251
- targetFields: string[];
252
- onDelete?: string;
253
- onUpdate?: string;
254
- }
255
- interface TTableSnapshot {
256
- tableName: string;
257
- fields: TFieldSnapshot[];
258
- indexes: TIndexSnapshot[];
259
- foreignKeys: TForeignKeySnapshot[];
260
- /** Adapter-specific table-level options (e.g., MySQL engine/charset, MongoDB capped). */
261
- tableOptions?: TExistingTableOption[];
262
- }
263
- /**
264
- * One join of a managed view as stored in its snapshot.
265
- * @since 0.1.128 — `joinTables` elements were bare target-table names before;
266
- * the ON predicate is now part of the view definition.
267
- */
268
- interface TViewJoinSnapshot {
269
- targetTable: string;
270
- /** Canonical JSON of the join condition (see {@link canonicalizeQueryNode}). */
271
- condition: string;
272
- /** Emitted only for `"left"` — an inner join (the default) carries no key. @since 0.1.136 */
273
- kind?: "inner" | "left";
274
- }
275
- /**
276
- * One column of a managed view as stored in its snapshot — the PHYSICAL
277
- * source it reads, so a source rename (`@db.column`, flattening, a moved
278
- * JSON leaf) or an aggregate change recreates the view.
279
- * @since 0.1.136
280
- */
281
- interface TViewColumnSnapshot {
282
- column: string;
283
- sourceTable: string;
284
- sourceColumn: string;
285
- /** JSON array of the path segments inside a JSON source column. */
286
- jsonPath?: string;
287
- jsonType?: string;
288
- aggFn?: string;
289
- aggField?: string;
290
- /** Canonical JSON of a conditional aggregate's predicate. */
291
- aggFilter?: string;
292
- }
293
- interface TViewSnapshot {
294
- tableName: string;
295
- viewType: "V" | "M" | "E";
296
- entryTable?: string;
297
- /**
298
- * Joins in declaration order. The key keeps its historical name so a
299
- * join-less view (`[]`) serializes byte-identically to older snapshots.
300
- */
301
- joinTables?: TViewJoinSnapshot[];
302
- /** @since 0.1.136 — view columns and their physical sources, sorted by `column`. */
303
- columns?: TViewColumnSnapshot[];
304
- filterHash?: string;
305
- /** @since 0.1.128 — hash of the canonical `@db.view.having` predicate. */
306
- havingHash?: string;
307
- materialized?: boolean;
308
- /**
309
- * @since 0.1.137 — the adapter's `viewRenderRevision()`, present only when
310
- * the adapter defines one (managed views only).
311
- */
312
- renderRevision?: string;
313
- fields: TFieldSnapshot[];
314
- }
315
- /**
316
- * Extracts a canonical, serializable snapshot from a readable's metadata.
317
- * Sorted deterministically so the hash is stable across runs.
318
- *
319
- * @param readable - The table/view readable.
320
- * @param typeMapper - Optional adapter-specific type mapper. When provided,
321
- * each field's mapped type (e.g., "VARCHAR(255)") is stored in the snapshot
322
- * for precise type change detection.
323
- */
324
- declare function computeTableSnapshot(readable: AtscriptDbReadable, typeMapper?: (field: TDbFieldMeta) => string, tableOptions?: TExistingTableOption[]): TTableSnapshot;
325
- /**
326
- * Extracts a canonical, serializable snapshot from a view's metadata.
327
- * Captures view plan (entry table, joins, filter, materialization) for
328
- * detecting view definition changes.
329
- */
330
- declare function computeViewSnapshot(view: AtscriptDbView): TViewSnapshot;
331
- /** Canonical (table-qualified, fixed-key-order) form of a view predicate. */
332
- type TCanonicalQueryNode = {
333
- and: TCanonicalQueryNode[];
334
- } | {
335
- or: TCanonicalQueryNode[];
336
- } | {
337
- not: TCanonicalQueryNode;
338
- } /** `r` is `{ f: "<table>.<field>" }` for a field-to-field comparison, else the literal. */ | {
339
- l: string;
340
- op: string;
341
- r?: unknown;
342
- };
343
- /**
344
- * Converts a view predicate (join condition, `@db.view.filter`,
345
- * `@db.view.having`) into a serializable structure whose JSON is a stable
346
- * function of its MEANING: field refs become `qualify(ref)` — the view's
347
- * `resolveFieldRef(ref, (n) => n)`, i.e. `"<table>.<field>"`, so a predicate
348
- * retargeted to another table with the same field name changes — operators
349
- * and literal values are kept as-is, `$and`/`$or` keep declaration order, and
350
- * no function references survive. Two identical models produce byte-identical
351
- * JSON.
352
- * @since 0.1.128
353
- */
354
- declare function canonicalizeQueryNode(node: AtscriptQueryNode$1, qualify: (ref: AtscriptQueryFieldRef$1) => string): TCanonicalQueryNode;
355
- /**
356
- * Computes a deterministic hash string from multiple table snapshots.
357
- * Uses FNV-1a for speed — not cryptographic, just needs stability + collision resistance.
358
- */
359
- declare function computeSchemaHash(snapshots: Array<TTableSnapshot | TViewSnapshot>): string;
360
- /**
361
- * Computes a hash for a single table/view snapshot.
362
- * Used for per-table change detection via stored snapshots.
363
- */
364
- declare function computeTableHash(snapshot: TTableSnapshot | TViewSnapshot): string;
365
- /**
366
- * Converts stored snapshot fields to `TExistingColumn[]` format
367
- * for use with `computeColumnDiff`. Used by adapters that lack
368
- * native column introspection (e.g., MongoDB).
369
- *
370
- * The `type` field uses `mappedType` when available (adapter-specific),
371
- * falling back to `designType`.
372
- */
373
- declare function snapshotToExistingColumns(snapshot: TTableSnapshot): TExistingColumn[];
374
- /**
375
- * Extracts table options from a stored snapshot for diff comparison.
376
- * Used as fallback when an adapter lacks native table option introspection.
377
- */
378
- declare function snapshotToExistingTableOptions(snapshot: TTableSnapshot): TExistingTableOption[];
379
- //#endregion
380
262
  //#region src/schema/sync-store.d.ts
381
263
  /**
382
264
  * Reads a stored table snapshot from the control table.
@@ -465,10 +347,27 @@ declare class SchemaSync {
465
347
  * Starts a periodic heartbeat that extends the lock's TTL while sync runs.
466
348
  * Returns a handle with `stop()` to cancel and `getAbortReason()` to check
467
349
  * whether the lock was stolen or unexpectedly removed.
350
+ *
351
+ * At most one refresh is in flight; `stop()` resolves once it has settled,
352
+ * so the lock is released only after the last refresh — a late refresh must
353
+ * never race the release (or the next holder's lock).
468
354
  */
469
355
  private startHeartbeat;
356
+ /**
357
+ * Acquires the sync lock, waiting for a peer that holds it. Resolves
358
+ * `"acquired"`, or `"synced-by-peer"` when a non-forced run finds, after a
359
+ * wait, that the peer stored this schema's hash. A forced run never
360
+ * short-circuits — it waits its turn and runs. Losing the race for a freed
361
+ * lock to another waiter just waits again, within the same `waitTimeoutMs`.
362
+ */
363
+ private acquireLock;
470
364
  /** Throws if the heartbeat detected a stolen/missing lock. */
471
365
  private assertLockHeld;
366
+ /**
367
+ * Releases the lock without masking the run's outcome: a failed release is
368
+ * logged (the row expires after `lockTtlMs`, blocking peers until then).
369
+ */
370
+ private releaseLock;
472
371
  /**
473
372
  * Runs schema synchronization with distributed locking, in three phases:
474
373
  *
@@ -477,12 +376,12 @@ declare class SchemaSync {
477
376
  * 2. **Pre-flight** (pure): every change that no order of DDL can apply
478
377
  * safely becomes a refusal. One refusal → `status: "refused"`, no DDL,
479
378
  * nothing persisted, lock released.
480
- * 3. **Execute** in dependency order: stale/removed views → inbound FKs of
481
- * key-changing tables → tables (parents first; a removed table that
482
- * blocks a table's drop-and-recreate or key rebuild is dropped right
483
- * before it) → deferred FKs of cycles → managed views → external-view
484
- * checks → remaining removed tables (children first) →
485
- * snapshots/tracking/hash.
379
+ * 3. **Execute** in dependency order: removed views, then stale views
380
+ * (dependents first) → inbound FKs of key-changing tables → tables
381
+ * (parents first; a removed table that blocks a table's drop-and-recreate
382
+ * or key rebuild is dropped right before it) → deferred FKs of cycles →
383
+ * managed views (upstream views first) → external-view checks →
384
+ * remaining removed tables (children first) → snapshots/tracking/hash.
486
385
  */
487
386
  run(types: readonly TAtscriptAnnotatedType[], opts?: TSyncOptions): Promise<TSyncResult>;
488
387
  private discover;
@@ -661,4 +560,4 @@ declare function syncSchema(space: DbSpace, types: readonly TAtscriptAnnotatedTy
661
560
  */
662
561
  declare function planSchema(space: DbSpace, types: readonly TAtscriptAnnotatedType[], opts?: Pick<TSyncOptions, "force" | "safe">): Promise<TSyncPlan>;
663
562
  //#endregion
664
- export { SchemaSync, SyncEntry, type TDbObjectKind, type TDependencyEdge, type TEnsureTableOptions, type TExistingForeignKey, type TFieldSnapshot, type TForeignKeyDiff, type TForeignKeySnapshot, type TPrimaryKeyChange, type TReferencingForeignKey, type TSyncColors, type TSyncEntryInit, type TSyncEntryStatus, type TSyncOptions, type TSyncPlan, type TSyncResult, type TSyncSkippedWork, type TTableSnapshot, type TViewColumnSnapshot, type TViewJoinSnapshot, type TViewSnapshot, canonicalizeQueryNode, computeColumnDiff, computeForeignKeyDiff, computeSchemaHash, computeTableHash, computeTableOptionDiff, computeTableSnapshot, computeViewSnapshot, fkKey, hasForeignKeyChanges, planSchema, readStoredSnapshot, snapshotToExistingColumns, snapshotToExistingTableOptions, syncSchema, topoOrder };
563
+ export { SchemaSync, SyncEntry, type TDbObjectKind, type TDependencyEdge, type TEnsureTableOptions, type TExistingForeignKey, type TFieldSnapshot, type TForeignKeyDiff, type TForeignKeySnapshot, type TPrimaryKeyChange, type TReferencingForeignKey, type TSyncColors, type TSyncDerivedChange, type TSyncEntryInit, type TSyncEntryStatus, type TSyncOptions, type TSyncPlan, type TSyncResult, type TSyncSkippedWork, type TTableSnapshot, type TViewColumnSnapshot, type TViewJoinSnapshot, type TViewSnapshot, canonicalizeQueryNode, computeColumnDiff, computeForeignKeyDiff, computeSchemaHash, computeTableHash, computeTableOptionDiff, computeTableSnapshot, computeViewSnapshot, fkKey, hasForeignKeyChanges, planSchema, readStoredSnapshot, snapshotToExistingColumns, snapshotToExistingTableOptions, syncSchema, topoOrder };
package/dist/sync.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { $t as AtscriptDbReadable, Ft as DbSpace, G as TDbObjectKind, I as TDbDefaultValue, R as TDbFieldMeta, Rt as AtscriptDbView, Ut as AtscriptQueryFieldRef$1, Wt as AtscriptQueryNode$1, X as TDbStorageType, at as TExistingTableOption, ht as TReferencingForeignKey, it as TExistingForeignKey, mt as TPrimaryKeyChange, nt as TEnsureTableOptions, rt as TExistingColumn, sn as TGenericLogger, yt as TTableOptionDiff, z as TDbForeignKey } from "./buckets-BFG2RYRW.mjs";
2
- import { t as computeColumnDiff } from "./column-diff-DPkbZIVE.mjs";
1
+ import { G as TDbObjectKind, Lt as DbSpace, R as TDbFieldMeta, _t as TReferencingForeignKey, gt as TPrimaryKeyChange, it as TEnsureTableOptions, nt as TDerivedChangeReason, ot as TExistingForeignKey, st as TExistingTableOption, un as TGenericLogger, xt as TTableOptionDiff, z as TDbForeignKey } from "./buckets-CjL7F-hp.mjs";
2
+ import { a as TTableSnapshot, c as TViewSnapshot, d as computeTableHash, f as computeTableSnapshot, h as snapshotToExistingTableOptions, i as TForeignKeySnapshot, l as canonicalizeQueryNode, m as snapshotToExistingColumns, o as TViewColumnSnapshot, p as computeViewSnapshot, r as TFieldSnapshot, s as TViewJoinSnapshot, t as computeColumnDiff, u as computeSchemaHash } from "./column-diff-n-k5KY0u.mjs";
3
3
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/schema/sync-entry.d.ts
@@ -19,7 +19,17 @@ type TSyncEntryStatus = "create" | "alter" | "drop" | "in-sync" | "error";
19
19
  * recreate a destructive table-option change needs, and nullable/default
20
20
  * changes on adapters that need DDL for them.
21
21
  */
22
- type TSyncSkippedWork = "pk-rebuild" | "recreate" | "table-options" | "nullable-defaults";
22
+ type TSyncSkippedWork = "pk-rebuild" | "recreate" | "table-options" | "nullable-defaults" /** A derived column's drop + add (kind / expression / type change) — since 0.1.141. */ | "derived";
23
+ /**
24
+ * One derived-column rebuild as plan and run entries report it (since 0.1.141):
25
+ * `reason` as the column diff classified it, `derived` whether the MODEL side
26
+ * is the derived one (`kind` reads `regular → derived` / `derived → regular`).
27
+ */
28
+ interface TSyncDerivedChange {
29
+ column: string;
30
+ reason: TDerivedChangeReason;
31
+ derived: boolean;
32
+ }
23
33
  interface TSyncEntryInit {
24
34
  name: string;
25
35
  /** 'V' = virtual view, 'M' = materialized view, 'E' = external view, undefined = table */
@@ -60,6 +70,13 @@ interface TSyncEntryInit {
60
70
  targetTable: string;
61
71
  details: string;
62
72
  }>;
73
+ /**
74
+ * Derived columns the run drops and re-adds (a generated column's
75
+ * expression cannot be altered in place) — kept on the entry when safe
76
+ * mode skipped the rebuild (`skipped` includes `'derived'`).
77
+ * @since 0.1.141
78
+ */
79
+ derivedChanges?: TSyncDerivedChange[];
63
80
  columnsAdded?: string[];
64
81
  columnsRenamed?: string[];
65
82
  columnsDropped?: string[];
@@ -107,6 +124,13 @@ interface TSyncEntryInit {
107
124
  * @since 0.1.128
108
125
  */
109
126
  refused?: boolean;
127
+ /**
128
+ * Managed views this view reads whose recreate forces this view's own
129
+ * drop-and-recreate (its definition is unchanged). Printed as
130
+ * `· upstream view "x" recreated`.
131
+ * @since 0.1.141
132
+ */
133
+ cascadeFrom?: string[];
110
134
  }
111
135
  declare class SyncEntry {
112
136
  readonly name: string;
@@ -148,6 +172,8 @@ declare class SyncEntry {
148
172
  targetTable: string;
149
173
  details: string;
150
174
  }>;
175
+ /** @since 0.1.141 — see {@link TSyncEntryInit.derivedChanges}. */
176
+ readonly derivedChanges: TSyncDerivedChange[];
151
177
  readonly columnsAdded: string[];
152
178
  readonly columnsRenamed: string[];
153
179
  readonly columnsDropped: string[];
@@ -168,6 +194,8 @@ declare class SyncEntry {
168
194
  readonly dropGroup?: string[];
169
195
  /** @since 0.1.128 — see {@link TSyncEntryInit.refused}. */
170
196
  readonly refused: boolean;
197
+ /** @since 0.1.141 — see {@link TSyncEntryInit.cascadeFrom}. */
198
+ readonly cascadeFrom: string[];
171
199
  constructor(init: TSyncEntryInit);
172
200
  /**
173
201
  * The init object this entry was built from — lets callers derive a
@@ -208,6 +236,11 @@ declare class SyncEntry {
208
236
  * mode)` in plan and result when the `'drop'` recreate was skipped.
209
237
  */
210
238
  private printTypeChanges;
239
+ /**
240
+ * `~ col — derived column (expression changed) — drop + add` (plan) /
241
+ * `— rebuilt` (result), or `! … — skipped (safe mode)` when pending.
242
+ */
243
+ private printDerivedChanges;
211
244
  /** `~ col — nullable` / `~ col — default a → b`, `— skipped (safe mode)` when pending. */
212
245
  private printNullableDefaults;
213
246
  /**
@@ -218,165 +251,14 @@ declare class SyncEntry {
218
251
  private printOptionChanges;
219
252
  /** `· after: a, b` (plan only — the executor already runs in this order). */
220
253
  private printDependsOn;
254
+ /** `· upstream view "x" recreated` — why an unchanged view is recreated. */
255
+ private printCascade;
221
256
  private printDropGroup;
222
257
  private printPlan;
223
258
  private printResult;
224
259
  private printInSync;
225
260
  }
226
261
  //#endregion
227
- //#region src/schema/schema-hash.d.ts
228
- interface TFieldSnapshot {
229
- physicalName: string;
230
- designType: string;
231
- optional: boolean;
232
- isPrimaryKey: boolean;
233
- storage: TDbStorageType;
234
- defaultValue?: TDbDefaultValue;
235
- /** Adapter-specific mapped type (e.g., "VARCHAR(255)", "INTEGER"). */
236
- mappedType?: string;
237
- /** `@db.encrypted` — toggling encryption changes the snapshot hash on every adapter. */
238
- encrypted?: boolean;
239
- }
240
- interface TIndexSnapshot {
241
- key: string;
242
- type: string;
243
- fields: Array<{
244
- name: string;
245
- sort: string;
246
- }>;
247
- }
248
- interface TForeignKeySnapshot {
249
- fields: string[];
250
- targetTable: string;
251
- targetFields: string[];
252
- onDelete?: string;
253
- onUpdate?: string;
254
- }
255
- interface TTableSnapshot {
256
- tableName: string;
257
- fields: TFieldSnapshot[];
258
- indexes: TIndexSnapshot[];
259
- foreignKeys: TForeignKeySnapshot[];
260
- /** Adapter-specific table-level options (e.g., MySQL engine/charset, MongoDB capped). */
261
- tableOptions?: TExistingTableOption[];
262
- }
263
- /**
264
- * One join of a managed view as stored in its snapshot.
265
- * @since 0.1.128 — `joinTables` elements were bare target-table names before;
266
- * the ON predicate is now part of the view definition.
267
- */
268
- interface TViewJoinSnapshot {
269
- targetTable: string;
270
- /** Canonical JSON of the join condition (see {@link canonicalizeQueryNode}). */
271
- condition: string;
272
- /** Emitted only for `"left"` — an inner join (the default) carries no key. @since 0.1.136 */
273
- kind?: "inner" | "left";
274
- }
275
- /**
276
- * One column of a managed view as stored in its snapshot — the PHYSICAL
277
- * source it reads, so a source rename (`@db.column`, flattening, a moved
278
- * JSON leaf) or an aggregate change recreates the view.
279
- * @since 0.1.136
280
- */
281
- interface TViewColumnSnapshot {
282
- column: string;
283
- sourceTable: string;
284
- sourceColumn: string;
285
- /** JSON array of the path segments inside a JSON source column. */
286
- jsonPath?: string;
287
- jsonType?: string;
288
- aggFn?: string;
289
- aggField?: string;
290
- /** Canonical JSON of a conditional aggregate's predicate. */
291
- aggFilter?: string;
292
- }
293
- interface TViewSnapshot {
294
- tableName: string;
295
- viewType: "V" | "M" | "E";
296
- entryTable?: string;
297
- /**
298
- * Joins in declaration order. The key keeps its historical name so a
299
- * join-less view (`[]`) serializes byte-identically to older snapshots.
300
- */
301
- joinTables?: TViewJoinSnapshot[];
302
- /** @since 0.1.136 — view columns and their physical sources, sorted by `column`. */
303
- columns?: TViewColumnSnapshot[];
304
- filterHash?: string;
305
- /** @since 0.1.128 — hash of the canonical `@db.view.having` predicate. */
306
- havingHash?: string;
307
- materialized?: boolean;
308
- /**
309
- * @since 0.1.137 — the adapter's `viewRenderRevision()`, present only when
310
- * the adapter defines one (managed views only).
311
- */
312
- renderRevision?: string;
313
- fields: TFieldSnapshot[];
314
- }
315
- /**
316
- * Extracts a canonical, serializable snapshot from a readable's metadata.
317
- * Sorted deterministically so the hash is stable across runs.
318
- *
319
- * @param readable - The table/view readable.
320
- * @param typeMapper - Optional adapter-specific type mapper. When provided,
321
- * each field's mapped type (e.g., "VARCHAR(255)") is stored in the snapshot
322
- * for precise type change detection.
323
- */
324
- declare function computeTableSnapshot(readable: AtscriptDbReadable, typeMapper?: (field: TDbFieldMeta) => string, tableOptions?: TExistingTableOption[]): TTableSnapshot;
325
- /**
326
- * Extracts a canonical, serializable snapshot from a view's metadata.
327
- * Captures view plan (entry table, joins, filter, materialization) for
328
- * detecting view definition changes.
329
- */
330
- declare function computeViewSnapshot(view: AtscriptDbView): TViewSnapshot;
331
- /** Canonical (table-qualified, fixed-key-order) form of a view predicate. */
332
- type TCanonicalQueryNode = {
333
- and: TCanonicalQueryNode[];
334
- } | {
335
- or: TCanonicalQueryNode[];
336
- } | {
337
- not: TCanonicalQueryNode;
338
- } /** `r` is `{ f: "<table>.<field>" }` for a field-to-field comparison, else the literal. */ | {
339
- l: string;
340
- op: string;
341
- r?: unknown;
342
- };
343
- /**
344
- * Converts a view predicate (join condition, `@db.view.filter`,
345
- * `@db.view.having`) into a serializable structure whose JSON is a stable
346
- * function of its MEANING: field refs become `qualify(ref)` — the view's
347
- * `resolveFieldRef(ref, (n) => n)`, i.e. `"<table>.<field>"`, so a predicate
348
- * retargeted to another table with the same field name changes — operators
349
- * and literal values are kept as-is, `$and`/`$or` keep declaration order, and
350
- * no function references survive. Two identical models produce byte-identical
351
- * JSON.
352
- * @since 0.1.128
353
- */
354
- declare function canonicalizeQueryNode(node: AtscriptQueryNode$1, qualify: (ref: AtscriptQueryFieldRef$1) => string): TCanonicalQueryNode;
355
- /**
356
- * Computes a deterministic hash string from multiple table snapshots.
357
- * Uses FNV-1a for speed — not cryptographic, just needs stability + collision resistance.
358
- */
359
- declare function computeSchemaHash(snapshots: Array<TTableSnapshot | TViewSnapshot>): string;
360
- /**
361
- * Computes a hash for a single table/view snapshot.
362
- * Used for per-table change detection via stored snapshots.
363
- */
364
- declare function computeTableHash(snapshot: TTableSnapshot | TViewSnapshot): string;
365
- /**
366
- * Converts stored snapshot fields to `TExistingColumn[]` format
367
- * for use with `computeColumnDiff`. Used by adapters that lack
368
- * native column introspection (e.g., MongoDB).
369
- *
370
- * The `type` field uses `mappedType` when available (adapter-specific),
371
- * falling back to `designType`.
372
- */
373
- declare function snapshotToExistingColumns(snapshot: TTableSnapshot): TExistingColumn[];
374
- /**
375
- * Extracts table options from a stored snapshot for diff comparison.
376
- * Used as fallback when an adapter lacks native table option introspection.
377
- */
378
- declare function snapshotToExistingTableOptions(snapshot: TTableSnapshot): TExistingTableOption[];
379
- //#endregion
380
262
  //#region src/schema/sync-store.d.ts
381
263
  /**
382
264
  * Reads a stored table snapshot from the control table.
@@ -465,10 +347,27 @@ declare class SchemaSync {
465
347
  * Starts a periodic heartbeat that extends the lock's TTL while sync runs.
466
348
  * Returns a handle with `stop()` to cancel and `getAbortReason()` to check
467
349
  * whether the lock was stolen or unexpectedly removed.
350
+ *
351
+ * At most one refresh is in flight; `stop()` resolves once it has settled,
352
+ * so the lock is released only after the last refresh — a late refresh must
353
+ * never race the release (or the next holder's lock).
468
354
  */
469
355
  private startHeartbeat;
356
+ /**
357
+ * Acquires the sync lock, waiting for a peer that holds it. Resolves
358
+ * `"acquired"`, or `"synced-by-peer"` when a non-forced run finds, after a
359
+ * wait, that the peer stored this schema's hash. A forced run never
360
+ * short-circuits — it waits its turn and runs. Losing the race for a freed
361
+ * lock to another waiter just waits again, within the same `waitTimeoutMs`.
362
+ */
363
+ private acquireLock;
470
364
  /** Throws if the heartbeat detected a stolen/missing lock. */
471
365
  private assertLockHeld;
366
+ /**
367
+ * Releases the lock without masking the run's outcome: a failed release is
368
+ * logged (the row expires after `lockTtlMs`, blocking peers until then).
369
+ */
370
+ private releaseLock;
472
371
  /**
473
372
  * Runs schema synchronization with distributed locking, in three phases:
474
373
  *
@@ -477,12 +376,12 @@ declare class SchemaSync {
477
376
  * 2. **Pre-flight** (pure): every change that no order of DDL can apply
478
377
  * safely becomes a refusal. One refusal → `status: "refused"`, no DDL,
479
378
  * nothing persisted, lock released.
480
- * 3. **Execute** in dependency order: stale/removed views → inbound FKs of
481
- * key-changing tables → tables (parents first; a removed table that
482
- * blocks a table's drop-and-recreate or key rebuild is dropped right
483
- * before it) → deferred FKs of cycles → managed views → external-view
484
- * checks → remaining removed tables (children first) →
485
- * snapshots/tracking/hash.
379
+ * 3. **Execute** in dependency order: removed views, then stale views
380
+ * (dependents first) → inbound FKs of key-changing tables → tables
381
+ * (parents first; a removed table that blocks a table's drop-and-recreate
382
+ * or key rebuild is dropped right before it) → deferred FKs of cycles →
383
+ * managed views (upstream views first) → external-view checks →
384
+ * remaining removed tables (children first) → snapshots/tracking/hash.
486
385
  */
487
386
  run(types: readonly TAtscriptAnnotatedType[], opts?: TSyncOptions): Promise<TSyncResult>;
488
387
  private discover;
@@ -661,4 +560,4 @@ declare function syncSchema(space: DbSpace, types: readonly TAtscriptAnnotatedTy
661
560
  */
662
561
  declare function planSchema(space: DbSpace, types: readonly TAtscriptAnnotatedType[], opts?: Pick<TSyncOptions, "force" | "safe">): Promise<TSyncPlan>;
663
562
  //#endregion
664
- export { SchemaSync, SyncEntry, type TDbObjectKind, type TDependencyEdge, type TEnsureTableOptions, type TExistingForeignKey, type TFieldSnapshot, type TForeignKeyDiff, type TForeignKeySnapshot, type TPrimaryKeyChange, type TReferencingForeignKey, type TSyncColors, type TSyncEntryInit, type TSyncEntryStatus, type TSyncOptions, type TSyncPlan, type TSyncResult, type TSyncSkippedWork, type TTableSnapshot, type TViewColumnSnapshot, type TViewJoinSnapshot, type TViewSnapshot, canonicalizeQueryNode, computeColumnDiff, computeForeignKeyDiff, computeSchemaHash, computeTableHash, computeTableOptionDiff, computeTableSnapshot, computeViewSnapshot, fkKey, hasForeignKeyChanges, planSchema, readStoredSnapshot, snapshotToExistingColumns, snapshotToExistingTableOptions, syncSchema, topoOrder };
563
+ export { SchemaSync, SyncEntry, type TDbObjectKind, type TDependencyEdge, type TEnsureTableOptions, type TExistingForeignKey, type TFieldSnapshot, type TForeignKeyDiff, type TForeignKeySnapshot, type TPrimaryKeyChange, type TReferencingForeignKey, type TSyncColors, type TSyncDerivedChange, type TSyncEntryInit, type TSyncEntryStatus, type TSyncOptions, type TSyncPlan, type TSyncResult, type TSyncSkippedWork, type TTableSnapshot, type TViewColumnSnapshot, type TViewJoinSnapshot, type TViewSnapshot, canonicalizeQueryNode, computeColumnDiff, computeForeignKeyDiff, computeSchemaHash, computeTableHash, computeTableOptionDiff, computeTableSnapshot, computeViewSnapshot, fkKey, hasForeignKeyChanges, planSchema, readStoredSnapshot, snapshotToExistingColumns, snapshotToExistingTableOptions, syncSchema, topoOrder };