@atscript/db 0.1.127 → 0.1.128

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 (33) hide show
  1. package/dist/{db-readable-BkAGccv9.d.mts → db-readable-BTl4NzNN.d.mts} +289 -16
  2. package/dist/{db-readable-C0nDKX8A.d.cts → db-readable-CkbZn_z-.d.cts} +289 -16
  3. package/dist/{db-space-B_ASuDaR.d.mts → db-space-BEB5Jl8M.d.mts} +81 -29
  4. package/dist/{db-space-CSntT6yS.d.cts → db-space-BevOyVrC.d.cts} +81 -29
  5. package/dist/{db-view-BP0Qbeux.cjs → db-view-C8-MsjND.cjs} +696 -97
  6. package/dist/{db-view-C8rZM5_N.mjs → db-view-Ck0I-PMI.mjs} +643 -98
  7. package/dist/index.cjs +46 -2
  8. package/dist/index.d.cts +162 -37
  9. package/dist/index.d.mts +162 -37
  10. package/dist/index.mjs +36 -4
  11. package/dist/{ops-DJRnNTVo.d.cts → ops-AqhV7s9o.d.cts} +24 -1
  12. package/dist/{ops-DJRnNTVo.d.mts → ops-AqhV7s9o.d.mts} +24 -1
  13. package/dist/ops.cjs +43 -0
  14. package/dist/ops.d.cts +2 -2
  15. package/dist/ops.d.mts +2 -2
  16. package/dist/ops.mjs +43 -1
  17. package/dist/plugin.cjs +12 -5
  18. package/dist/plugin.mjs +12 -5
  19. package/dist/rel.d.cts +1 -1
  20. package/dist/rel.d.mts +1 -1
  21. package/dist/sync.cjs +1096 -361
  22. package/dist/sync.d.cts +234 -15
  23. package/dist/sync.d.mts +234 -15
  24. package/dist/sync.mjs +1095 -362
  25. package/dist/{validator-CSGug4vg.cjs → validator-BNUHCXIE.cjs} +31 -2
  26. package/dist/{validator-BcBtg8yW.d.cts → validator-DuAmWc8L.d.cts} +38 -1
  27. package/dist/{validator-BcBtg8yW.d.mts → validator-DuAmWc8L.d.mts} +38 -1
  28. package/dist/{validator-0vRXN51D.mjs → validator-eKYWf3xf.mjs} +20 -3
  29. package/dist/validator.cjs +7 -1
  30. package/dist/validator.d.cts +3 -3
  31. package/dist/validator.d.mts +3 -3
  32. package/dist/validator.mjs +4 -3
  33. package/package.json +8 -8
package/dist/sync.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { G as TDbStorageType, I as TDbFieldMeta, J as TExistingTableOption, L as TDbForeignKey, P as TDbDefaultValue, T as TColumnDiff, _t as TGenericLogger, at as TTableOptionDiff, q as TExistingColumn, t as AtscriptDbReadable } from "./db-readable-C0nDKX8A.cjs";
2
- import { i as AtscriptDbView, t as DbSpace } from "./db-space-CSntT6yS.cjs";
1
+ import { B as TDbForeignKey, K as TDbObjectKind, L as TDbDefaultValue, Nt as TGenericLogger, O as TColumnDiff, Z as TDbStorageType, at as TExistingForeignKey, ht as TReferencingForeignKey, it as TExistingColumn, mt as TPrimaryKeyChange, ot as TExistingTableOption, rt as TEnsureTableOptions, t as AtscriptDbReadable, yt as TTableOptionDiff, z as TDbFieldMeta } from "./db-readable-CkbZn_z-.cjs";
2
+ import { c as AtscriptQueryFieldRef$1, i as AtscriptDbView, l as AtscriptQueryNode$1, t as DbSpace } from "./db-space-BevOyVrC.cjs";
3
3
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/schema/sync-entry.d.ts
@@ -13,6 +13,13 @@ interface TSyncColors {
13
13
  underline(s: string): string;
14
14
  }
15
15
  type TSyncEntryStatus = "create" | "alter" | "drop" | "in-sync" | "error";
16
+ /**
17
+ * Desired work safe mode did not apply (since 0.1.128): the primary-key
18
+ * rebuild, the `@db.sync.method 'drop'` recreate of a type change, the
19
+ * recreate a destructive table-option change needs, and nullable/default
20
+ * changes on adapters that need DDL for them.
21
+ */
22
+ type TSyncSkippedWork = "pk-rebuild" | "recreate" | "table-options" | "nullable-defaults";
16
23
  interface TSyncEntryInit {
17
24
  name: string;
18
25
  /** 'V' = virtual view, 'M' = materialized view, 'E' = external view, undefined = table */
@@ -59,6 +66,47 @@ interface TSyncEntryInit {
59
66
  recreated?: boolean;
60
67
  errors?: string[];
61
68
  renamedFrom?: string;
69
+ /**
70
+ * The table's primary-key field set changes. `rebuild: true` when sync
71
+ * rebuilds the key (empty table); `false` when the rebuild is skipped
72
+ * (safe mode). A populated table is refused instead (see `refused`).
73
+ * @since 0.1.128
74
+ */
75
+ pkChange?: {
76
+ from: string[];
77
+ to: string[];
78
+ rebuild: boolean;
79
+ };
80
+ /**
81
+ * The work safe mode skipped on this table: `"pk-rebuild"` (`pkChange`
82
+ * kept with `rebuild: false`), `"recreate"` (`typeChanges` kept),
83
+ * `"table-options"` (`optionChanges` kept) and `"nullable-defaults"`
84
+ * (`nullableChanges` / `defaultChanges` kept). Each is pending: the
85
+ * table's snapshot and the schema hash are withheld, and the next run
86
+ * without `safe` applies it. Printed as `… — skipped (safe mode)`.
87
+ * Identical in the plan and in the run.
88
+ * @since 0.1.128
89
+ */
90
+ skipped?: ReadonlyArray<TSyncSkippedWork>;
91
+ /**
92
+ * Names of the tables this entry's DDL waits for (FK parents for tables,
93
+ * entry/join tables for views, referencing children for drops).
94
+ * Informational — schema sync already executes in that order.
95
+ * @since 0.1.128
96
+ */
97
+ dependsOn?: string[];
98
+ /**
99
+ * Present when the table is dropped together with the other members of a
100
+ * foreign-key cycle (one group operation).
101
+ * @since 0.1.128
102
+ */
103
+ dropGroup?: string[];
104
+ /**
105
+ * `true` when this `error` entry is a pre-flight refusal: schema sync
106
+ * detected a change it cannot apply safely and issued no DDL at all.
107
+ * @since 0.1.128
108
+ */
109
+ refused?: boolean;
62
110
  }
63
111
  declare class SyncEntry {
64
112
  readonly name: string;
@@ -106,8 +154,37 @@ declare class SyncEntry {
106
154
  readonly recreated: boolean;
107
155
  readonly errors: string[];
108
156
  readonly renamedFrom?: string;
157
+ /** @since 0.1.128 — see {@link TSyncEntryInit.pkChange}. */
158
+ readonly pkChange?: {
159
+ from: string[];
160
+ to: string[];
161
+ rebuild: boolean;
162
+ };
163
+ /** @since 0.1.128 — see {@link TSyncEntryInit.skipped}. */
164
+ readonly skipped: ReadonlyArray<TSyncSkippedWork>;
165
+ /** @since 0.1.128 — see {@link TSyncEntryInit.dependsOn}. */
166
+ readonly dependsOn: string[];
167
+ /** @since 0.1.128 — see {@link TSyncEntryInit.dropGroup}. */
168
+ readonly dropGroup?: string[];
169
+ /** @since 0.1.128 — see {@link TSyncEntryInit.refused}. */
170
+ readonly refused: boolean;
109
171
  constructor(init: TSyncEntryInit);
110
- /** Whether this entry involves destructive operations */
172
+ /**
173
+ * The init object this entry was built from — lets callers derive a
174
+ * modified copy (`new SyncEntry({ ...entry.toInit(), status: "error" })`).
175
+ * @since 0.1.128
176
+ */
177
+ toInit(): TSyncEntryInit;
178
+ /**
179
+ * Whether desired work is still pending after this entry — DDL that was
180
+ * not issued because the entry errored or safe mode skipped it (see
181
+ * `skipped`). A pending entry withholds its snapshot and the schema hash,
182
+ * so the next run retries / applies it. External views are advisory and
183
+ * never pending.
184
+ * @since 0.1.128
185
+ */
186
+ get pending(): boolean;
187
+ /** Whether this entry involves destructive operations (pending work safe mode skipped is not) */
111
188
  get destructive(): boolean;
112
189
  /** Whether this entry represents any change (not in-sync) */
113
190
  get hasChanges(): boolean;
@@ -117,6 +194,24 @@ declare class SyncEntry {
117
194
  print(mode: "plan" | "result", colors?: TSyncColors): string[];
118
195
  private labelAndPrefix;
119
196
  private printError;
197
+ /** `! PK (id) → (code) — rebuild (table is empty)` / `— skipped (safe mode)` */
198
+ private printPkChange;
199
+ /**
200
+ * `! col: t1 → t2 — drop` (plan), or `! type col (t1 → t2) — skipped (safe
201
+ * mode)` in plan and result when the `'drop'` recreate was skipped.
202
+ */
203
+ private printTypeChanges;
204
+ /** `~ col — nullable` / `~ col — default a → b`, `— skipped (safe mode)` when pending. */
205
+ private printNullableDefaults;
206
+ /**
207
+ * Plan: `~ option k: a → b`, or `! option k: a → b — requires recreation`
208
+ * for a destructive change. Result: `~ option k: a → b` for an applied
209
+ * change. Both: `! option k: a → b — skipped (safe mode)` when pending.
210
+ */
211
+ private printOptionChanges;
212
+ /** `· after: a, b` (plan only — the executor already runs in this order). */
213
+ private printDependsOn;
214
+ private printDropGroup;
120
215
  private printPlan;
121
216
  private printResult;
122
217
  private printInSync;
@@ -158,12 +253,30 @@ interface TTableSnapshot {
158
253
  /** Adapter-specific table-level options (e.g., MySQL engine/charset, MongoDB capped). */
159
254
  tableOptions?: TExistingTableOption[];
160
255
  }
256
+ /**
257
+ * One join of a managed view as stored in its snapshot.
258
+ * @since 0.1.128 — `joinTables` elements were bare target-table names before;
259
+ * the ON predicate is now part of the view definition.
260
+ */
261
+ interface TViewJoinSnapshot {
262
+ targetTable: string;
263
+ /** Canonical JSON of the join condition (see {@link canonicalizeQueryNode}). */
264
+ condition: string;
265
+ /** Reserved for optional joins; absent today so it does not perturb the hash. */
266
+ kind?: "inner" | "left";
267
+ }
161
268
  interface TViewSnapshot {
162
269
  tableName: string;
163
270
  viewType: "V" | "M" | "E";
164
271
  entryTable?: string;
165
- joinTables?: string[];
272
+ /**
273
+ * Joins in declaration order. The key keeps its historical name so a
274
+ * join-less view (`[]`) serializes byte-identically to older snapshots.
275
+ */
276
+ joinTables?: TViewJoinSnapshot[];
166
277
  filterHash?: string;
278
+ /** @since 0.1.128 — hash of the canonical `@db.view.having` predicate. */
279
+ havingHash?: string;
167
280
  materialized?: boolean;
168
281
  fields: TFieldSnapshot[];
169
282
  }
@@ -183,6 +296,30 @@ declare function computeTableSnapshot(readable: AtscriptDbReadable, typeMapper?:
183
296
  * detecting view definition changes.
184
297
  */
185
298
  declare function computeViewSnapshot(view: AtscriptDbView): TViewSnapshot;
299
+ /** Canonical (table-qualified, fixed-key-order) form of a view predicate. */
300
+ type TCanonicalQueryNode = {
301
+ and: TCanonicalQueryNode[];
302
+ } | {
303
+ or: TCanonicalQueryNode[];
304
+ } | {
305
+ not: TCanonicalQueryNode;
306
+ } /** `r` is `{ f: "<table>.<field>" }` for a field-to-field comparison, else the literal. */ | {
307
+ l: string;
308
+ op: string;
309
+ r?: unknown;
310
+ };
311
+ /**
312
+ * Converts a view predicate (join condition, `@db.view.filter`,
313
+ * `@db.view.having`) into a serializable structure whose JSON is a stable
314
+ * function of its MEANING: field refs become `qualify(ref)` — the view's
315
+ * `resolveFieldRef(ref, (n) => n)`, i.e. `"<table>.<field>"`, so a predicate
316
+ * retargeted to another table with the same field name changes — operators
317
+ * and literal values are kept as-is, `$and`/`$or` keep declaration order, and
318
+ * no function references survive. Two identical models produce byte-identical
319
+ * JSON.
320
+ * @since 0.1.128
321
+ */
322
+ declare function canonicalizeQueryNode(node: AtscriptQueryNode$1, qualify: (ref: AtscriptQueryFieldRef$1) => string): TCanonicalQueryNode;
186
323
  /**
187
324
  * Computes a deterministic hash string from multiple table snapshots.
188
325
  * Uses FNV-1a for speed — not cryptographic, just needs stability + collision resistance.
@@ -233,7 +370,13 @@ interface TSyncOptions {
233
370
  pollIntervalMs?: number;
234
371
  /** Force sync even if hash matches. Default: false. */
235
372
  force?: boolean;
236
- /** Safe mode — skip destructive operations (column drops, table drops). Default: false. */
373
+ /**
374
+ * Safe mode — skip destructive operations (column/table drops, `'drop'`
375
+ * recreates, destructive table-option recreates, key rebuilds, nullable /
376
+ * default DDL). Skipped work is pending (`entry.skipped`): the table's
377
+ * snapshot and the hash are withheld until a run without `safe` applies
378
+ * it. Default: false.
379
+ */
237
380
  safe?: boolean;
238
381
  /**
239
382
  * Logger for sync progress and failures (index/FK DDL errors are logged,
@@ -242,7 +385,7 @@ interface TSyncOptions {
242
385
  logger?: TGenericLogger;
243
386
  /**
244
387
  * What to do when the run finishes with errored entries (failed index/FK
245
- * DDL, external-view checks, …):
388
+ * DDL, external-view checks, pre-flight refusals, …):
246
389
  * - `"warn"` (default) — emit a one-line summary plus per-entry error lines
247
390
  * via the configured logger, **falling back to `console` when no logger
248
391
  * is set** (errors are never silently swallowed by the NoopLogger default);
@@ -252,7 +395,16 @@ interface TSyncOptions {
252
395
  onError?: "throw" | "warn" | "silent";
253
396
  }
254
397
  interface TSyncResult {
255
- status: "up-to-date" | "synced" | "synced-by-peer";
398
+ /**
399
+ * - `"up-to-date"` — stored hash matches, nothing ran;
400
+ * - `"synced"` — the run executed (inspect `entries` for per-table status);
401
+ * - `"synced-by-peer"` — another pod synced while this one waited for the lock;
402
+ * - `"refused"` (since 0.1.128) — pre-flight found a change that cannot be
403
+ * applied safely: NO DDL ran, tracking/snapshots/hash are untouched, the
404
+ * lock is released. `entries` is the full plan with the refused entries
405
+ * as `error` entries (`refused: true`).
406
+ */
407
+ status: "up-to-date" | "synced" | "synced-by-peer" | "refused";
256
408
  schemaHash: string;
257
409
  entries: SyncEntry[];
258
410
  }
@@ -272,8 +424,8 @@ declare class SchemaSync {
272
424
  */
273
425
  private checkExternalView;
274
426
  /**
275
- * Detects tables/views present in the previous sync but absent from the current schema.
276
- * Returns SyncEntry instances with status 'drop'.
427
+ * Detects tables/views present in the previous sync but absent from the
428
+ * current schema (external views excluded — sync owns no DDL for them).
277
429
  */
278
430
  private detectRemoved;
279
431
  /**
@@ -285,9 +437,55 @@ declare class SchemaSync {
285
437
  /** Throws if the heartbeat detected a stolen/missing lock. */
286
438
  private assertLockHeld;
287
439
  /**
288
- * Runs schema synchronization with distributed locking.
440
+ * Runs schema synchronization with distributed locking, in three phases:
441
+ *
442
+ * 1. **Discover** (read-only, under the lock): introspect every table once,
443
+ * diff columns/FKs, read tracking, build the dependency graph.
444
+ * 2. **Pre-flight** (pure): every change that no order of DDL can apply
445
+ * safely becomes a refusal. One refusal → `status: "refused"`, no DDL,
446
+ * nothing persisted, lock released.
447
+ * 3. **Execute** in dependency order: stale/removed views → inbound FKs of
448
+ * key-changing tables → tables (parents first; a removed table that
449
+ * blocks a table's drop-and-recreate or key rebuild is dropped right
450
+ * before it) → deferred FKs of cycles → managed views → external-view
451
+ * checks → remaining removed tables (children first) →
452
+ * snapshots/tracking/hash.
289
453
  */
290
454
  run(types: readonly TAtscriptAnnotatedType[], opts?: TSyncOptions): Promise<TSyncResult>;
455
+ private discover;
456
+ /** Whether an object named `name` exists, or `undefined` when the adapter cannot tell. */
457
+ private objectPresent;
458
+ /**
459
+ * Introspects one desired table and computes its plan entry.
460
+ * `probeRecreateInbound` — whether a drop-and-recreate's live inbound FKs
461
+ * are worth probing (a removed table exists that could block it).
462
+ */
463
+ private discoverTable;
464
+ /**
465
+ * Pure validation over the discovery facts. Every refusal is a change that
466
+ * no order of DDL can apply safely; the messages are user-facing.
467
+ */
468
+ private preflight;
469
+ /**
470
+ * The one ordering walk `plan()` and `run()` share: tables parents-first,
471
+ * each preceded by the removed tables that block its drop/rebuild; the
472
+ * deferred FK pass; managed views; external-view checks; removed views;
473
+ * the remaining removed tables children-first.
474
+ */
475
+ private orderedSteps;
476
+ /** The plan entries in execution order, refusals folded in as `error` entries. */
477
+ private buildPlanEntries;
478
+ private execute;
479
+ /**
480
+ * Drops one removed-table group (a table, or a foreign-key cycle as one
481
+ * operation) and returns its entries. A live inbound FK from outside the
482
+ * removed set blocks the group — error entries, kept tracked — instead of
483
+ * leaving a dangling constraint or cascading. The probe runs at execution
484
+ * time, so an inventory child whose FK to the group was dropped earlier in
485
+ * this run does not block it. Called from the walk for early groups (right
486
+ * before the table they block) and late groups (after the views) alike.
487
+ */
488
+ private dropRemovedGroup;
291
489
  /**
292
490
  * Surfaces the run outcome per the `onError` policy. Reporting must never be
293
491
  * silently lost to the NoopLogger default — when no real logger is
@@ -296,22 +494,25 @@ declare class SchemaSync {
296
494
  */
297
495
  private reportOutcome;
298
496
  /**
299
- * Computes a dry-run plan showing what `run()` would do, without executing any DDL.
497
+ * Computes a dry-run plan showing what `run()` would do, without executing
498
+ * any DDL. Entries come back in execution order; pre-flight refusals appear
499
+ * as `error` entries (`refused: true`) exactly as `run()` would report them.
300
500
  */
301
501
  plan(types: readonly TAtscriptAnnotatedType[], opts?: Pick<TSyncOptions, "force" | "safe">): Promise<TSyncPlan>;
302
502
  /** Fallback typeMapper for snapshot-based Path B: compares designType directly, skips unions. */
303
503
  private resolveTypeMapper;
304
- private planTable;
305
504
  /**
306
505
  * Populates plan init from a column diff (shared by Path A and Path B).
307
506
  */
308
507
  private populatePlanFromDiff;
309
508
  /**
310
509
  * Computes table option diff using DB-first introspection with snapshot fallback.
311
- * Returns null if the adapter has no table options.
510
+ * Returns null if the adapter has no table options. `tableName` is the name
511
+ * the table has in the database right now — the OLD name of a pending
512
+ * `@db.table.renamed` (the adapter's bound name is the new one, which does
513
+ * not exist yet); omitted for a table that is not being renamed.
312
514
  */
313
515
  private diffTableOptions;
314
- private planView;
315
516
  private buildExecutorDeps;
316
517
  }
317
518
  //#endregion
@@ -365,6 +566,24 @@ declare function computeForeignKeyDiff(desired: ReadonlyMap<string, TDbForeignKe
365
566
  /** Whether the FK diff contains any changes. */
366
567
  declare function hasForeignKeyChanges(diff: TForeignKeyDiff): boolean;
367
568
  //#endregion
569
+ //#region src/schema/dependency-order.d.ts
570
+ /** A directed edge `from → to`: `from` depends on `to` (child → parent). */
571
+ type TDependencyEdge = readonly [from: string, to: string];
572
+ /**
573
+ * Orders `nodes` so that dependencies come first. Returns the strongly-
574
+ * connected components in dependency order: every group appears AFTER each
575
+ * group it depends on (parents first). Singleton groups are ordinary tables;
576
+ * groups of size > 1 are cycles. Members are name-sorted.
577
+ *
578
+ * - Edges referencing a name outside `nodes` are ignored (external tables
579
+ * impose no ordering).
580
+ * - Self-loops are ignored (a self-referential FK is legal inline everywhere).
581
+ * - Deterministic: nodes and adjacency lists are sorted by name before the
582
+ * traversal, so two processes with different inventory orders compute the
583
+ * same plan.
584
+ */
585
+ declare function topoOrder(nodes: Iterable<string>, edges: Iterable<TDependencyEdge>): string[][];
586
+ //#endregion
368
587
  //#region src/sync.d.ts
369
588
  /**
370
589
  * Synchronizes database schema with distributed locking.
@@ -412,4 +631,4 @@ declare function syncSchema(space: DbSpace, types: readonly TAtscriptAnnotatedTy
412
631
  */
413
632
  declare function planSchema(space: DbSpace, types: readonly TAtscriptAnnotatedType[], opts?: Pick<TSyncOptions, "force" | "safe">): Promise<TSyncPlan>;
414
633
  //#endregion
415
- export { SchemaSync, SyncEntry, type TFieldSnapshot, type TForeignKeyDiff, type TForeignKeySnapshot, type TSyncColors, type TSyncEntryStatus, type TSyncOptions, type TSyncPlan, type TSyncResult, type TTableSnapshot, type TViewSnapshot, computeColumnDiff, computeForeignKeyDiff, computeSchemaHash, computeTableHash, computeTableOptionDiff, computeTableSnapshot, computeViewSnapshot, fkKey, hasForeignKeyChanges, planSchema, readStoredSnapshot, snapshotToExistingColumns, snapshotToExistingTableOptions, syncSchema };
634
+ 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 TViewJoinSnapshot, type TViewSnapshot, canonicalizeQueryNode, computeColumnDiff, computeForeignKeyDiff, computeSchemaHash, computeTableHash, computeTableOptionDiff, computeTableSnapshot, computeViewSnapshot, fkKey, hasForeignKeyChanges, planSchema, readStoredSnapshot, snapshotToExistingColumns, snapshotToExistingTableOptions, syncSchema, topoOrder };