@atscript/db 0.1.163 → 0.1.165

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 (55) hide show
  1. package/dist/agg.cjs +1 -1
  2. package/dist/agg.d.cts +1 -1
  3. package/dist/agg.d.mts +1 -1
  4. package/dist/agg.mjs +1 -1
  5. package/dist/{aggregate-fns-DupYzFD7.cjs → aggregate-fns-CNtrSxGH.cjs} +1 -1
  6. package/dist/{aggregate-fns-DsS9UQVB.mjs → aggregate-fns-CmtSNIHm.mjs} +1 -1
  7. package/dist/{buckets-B8UOod-v.d.cts → buckets-BDxKfCNo.d.cts} +143 -13
  8. package/dist/{buckets-CHyspjYP.d.mts → buckets-CHUgky8T.d.mts} +143 -13
  9. package/dist/{column-diff-BfNIlMwX.mjs → column-diff-9pF0d8JP.mjs} +354 -31
  10. package/dist/{column-diff-BIGX3sRH.cjs → column-diff-DjeUj9kb.cjs} +371 -30
  11. package/dist/control-B5BJypqL.cjs +16 -0
  12. package/dist/control-CevYIJC4.mjs +16 -0
  13. package/dist/{db-error-hraC12B9.mjs → db-error-BMy4-OWq.mjs} +23 -1
  14. package/dist/{db-error-B_DRL1E9.d.cts → db-error-DQUw2cED.d.cts} +65 -9
  15. package/dist/{db-error-B_DRL1E9.d.mts → db-error-DQUw2cED.d.mts} +65 -9
  16. package/dist/{db-error-DmF1_xrz.cjs → db-error-G3sgvL87.cjs} +28 -0
  17. package/dist/{fk-diff-BNGawZi9.d.cts → fk-diff-DSM7O7hs.d.cts} +1 -1
  18. package/dist/{fk-diff-rj9N2QLw.d.mts → fk-diff-Dapr5Dk-.d.mts} +1 -1
  19. package/dist/index.cjs +17 -11
  20. package/dist/index.d.cts +38 -12
  21. package/dist/index.d.mts +38 -12
  22. package/dist/index.mjs +14 -12
  23. package/dist/{nested-writer-ClUX0DB0.mjs → nested-writer-BraWjS8a.mjs} +177 -26
  24. package/dist/{nested-writer-yCy6wGuN.cjs → nested-writer-DOvdbYG-.cjs} +194 -31
  25. package/dist/ops.cjs +1 -1
  26. package/dist/ops.mjs +1 -1
  27. package/dist/plugin.cjs +15 -12
  28. package/dist/plugin.mjs +15 -12
  29. package/dist/rel.cjs +2 -2
  30. package/dist/rel.d.cts +2 -2
  31. package/dist/rel.d.mts +2 -2
  32. package/dist/rel.mjs +2 -2
  33. package/dist/{relation-helpers-DloMywNs.d.cts → relation-helpers-C4UNWYQu.d.cts} +1 -1
  34. package/dist/{relation-helpers-zwt19E7r.d.mts → relation-helpers-hdbuqwjV.d.mts} +1 -1
  35. package/dist/{relation-loader-Qv3LrQ42.mjs → relation-loader-BOEVhcBe.mjs} +1 -1
  36. package/dist/{relation-loader-DujL_Mh9.cjs → relation-loader-BrkakVF_.cjs} +1 -1
  37. package/dist/shared.cjs +3 -1
  38. package/dist/shared.d.cts +13 -2
  39. package/dist/shared.d.mts +13 -2
  40. package/dist/shared.mjs +2 -2
  41. package/dist/sync.cjs +107 -25
  42. package/dist/sync.d.cts +53 -6
  43. package/dist/sync.d.mts +53 -6
  44. package/dist/sync.mjs +107 -25
  45. package/dist/{validator-DK9Ijpo7.cjs → validator-D7E7T0Oy.cjs} +1 -1
  46. package/dist/{validator-DuDWsIdQ.mjs → validator-UpgPhwvH.mjs} +1 -1
  47. package/dist/validator.cjs +2 -2
  48. package/dist/validator.d.cts +1 -1
  49. package/dist/validator.d.mts +1 -1
  50. package/dist/validator.mjs +2 -2
  51. package/dist/{view-validation-BiDLCYMw.cjs → view-validation-C0UVLx_H.cjs} +38 -10
  52. package/dist/{view-validation-BcgDq7M9.mjs → view-validation-KW77xIKn.mjs} +28 -12
  53. package/package.json +6 -6
  54. package/dist/control-D5cQmzFj.mjs +0 -14
  55. package/dist/control-jIVU2hkf.cjs +0 -14
package/dist/sync.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { Ct as TEnsureTableOptions, Et as TExistingTableOption, G as TDbFieldMeta, Pt as TIndexChange, Tt as TExistingForeignKey, Vt as TReferencingForeignKey, at as TDbObjectKind, cr as TGenericLogger, gn as DbSpace, qt as TTableOptionDiff, xt as TDerivedChangeReason, zt as TPrimaryKeyChange } from "./buckets-B8UOod-v.cjs";
2
- import { S as snapshotToExistingTableOptions, _ as computeSchemaHash, a as hasForeignKeyChanges, b as computeViewSnapshot, d as TForeignKeySnapshot, f as TTableSnapshot, g as canonicalizeQueryNode, h as TViewSnapshot, i as fkKey, l as sameDefaultValue, m as TViewJoinSnapshot, n as computeForeignKeyDiff, o as TColumnDiffOptions, p as TViewColumnSnapshot, s as computeColumnDiff, t as TForeignKeyDiff, u as TFieldSnapshot, v as computeTableHash, x as snapshotToExistingColumns, y as computeTableSnapshot } from "./fk-diff-BNGawZi9.cjs";
1
+ import { Bt as TPrimaryKeyChange, Ct as TEnsureTableOptions, Et as TExistingTableOption, G as TDbFieldMeta, Ht as TReferencingForeignKey, Jt as TTableOptionDiff, Pt as TIndexChange, Tt as TExistingForeignKey, at as TDbObjectKind, fr as TGenericLogger, xt as TDerivedChangeReason, yn as DbSpace } from "./buckets-BDxKfCNo.cjs";
2
+ import { S as snapshotToExistingTableOptions, _ as computeSchemaHash, a as hasForeignKeyChanges, b as computeViewSnapshot, d as TForeignKeySnapshot, f as TTableSnapshot, g as canonicalizeQueryNode, h as TViewSnapshot, i as fkKey, l as sameDefaultValue, m as TViewJoinSnapshot, n as computeForeignKeyDiff, o as TColumnDiffOptions, p as TViewColumnSnapshot, s as computeColumnDiff, t as TForeignKeyDiff, u as TFieldSnapshot, v as computeTableHash, x as snapshotToExistingColumns, y as computeTableSnapshot } from "./fk-diff-DSM7O7hs.cjs";
3
3
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/schema/sync-entry.d.ts
@@ -329,6 +329,27 @@ interface TSyncPlan {
329
329
  status: "up-to-date" | "changes-needed";
330
330
  schemaHash: string;
331
331
  entries: SyncEntry[];
332
+ /**
333
+ * Ids of the `dataSteps` passed to `planSchema` that have not run yet
334
+ * (since 0.1.165); a pending step makes the plan `"changes-needed"`.
335
+ * Absent when no steps were passed.
336
+ */
337
+ pendingDataSteps?: string[];
338
+ }
339
+ /**
340
+ * A one-time data step of {@link TSyncOptions.dataSteps} (since 0.1.165) —
341
+ * a backfill or data fix that must run exactly once per database, after the
342
+ * schema change it needs.
343
+ */
344
+ interface TDataStep {
345
+ /** Stable, unique id — the step is recorded under it (`data_step:<id>` in `__atscript_control`). */
346
+ id: string;
347
+ /**
348
+ * The step's work. Runs in its own transaction (on adapters with
349
+ * transactions), the record of the step committed in the same one: a step
350
+ * that throws is rolled back, not recorded, and runs again on the next sync.
351
+ */
352
+ run(space: DbSpace): Promise<void> | void;
332
353
  }
333
354
  interface TSyncOptions {
334
355
  /** Pod/instance identifier for distributed locking. Default: random UUID. */
@@ -365,6 +386,15 @@ interface TSyncOptions {
365
386
  * - `"silent"` — legacy behavior: outcome is only observable on the result.
366
387
  */
367
388
  onError?: "throw" | "warn" | "silent";
389
+ /**
390
+ * One-time data steps (since 0.1.165), run in order after the schema DDL,
391
+ * under the sync lock — also when the schema itself is up to date. Each
392
+ * step runs once per database: once it committed, its id is recorded and
393
+ * later syncs skip it. A step that throws stops the run (the error is
394
+ * rethrown after the lock is released); the steps after it stay pending.
395
+ * Steps do not run when the sync is refused or an entry errored.
396
+ */
397
+ dataSteps?: readonly TDataStep[];
368
398
  }
369
399
  interface TSyncResult {
370
400
  /**
@@ -379,6 +409,15 @@ interface TSyncResult {
379
409
  status: "up-to-date" | "synced" | "synced-by-peer" | "refused";
380
410
  schemaHash: string;
381
411
  entries: SyncEntry[];
412
+ /**
413
+ * The {@link TSyncOptions.dataSteps} this call ran, and those still pending
414
+ * (a refused or errored run leaves them all pending). Absent when no steps
415
+ * were passed. @since 0.1.165
416
+ */
417
+ dataSteps?: {
418
+ ran: string[];
419
+ pending: string[];
420
+ };
382
421
  }
383
422
  declare class SchemaSync {
384
423
  private readonly space;
@@ -413,7 +452,8 @@ declare class SchemaSync {
413
452
  /**
414
453
  * Acquires the sync lock, waiting for a peer that holds it. Resolves
415
454
  * `"acquired"`, or `"synced-by-peer"` when a non-forced run finds, after a
416
- * wait, that the peer stored this schema's hash. A forced run never
455
+ * wait, that the peer stored this schema's hash (and `peerDone` agrees —
456
+ * no data step left pending). A forced run never
417
457
  * short-circuits — it waits its turn and runs. Losing the race for a freed
418
458
  * lock to another waiter just waits again, within the same `waitTimeoutMs`.
419
459
  */
@@ -425,6 +465,13 @@ declare class SchemaSync {
425
465
  * logged (the row expires after `lockTtlMs`, blocking peers until then).
426
466
  */
427
467
  private releaseLock;
468
+ /** Ids of the steps not recorded yet, in order. */
469
+ private pendingDataSteps;
470
+ /**
471
+ * Runs the pending steps in order, each with its record in one transaction.
472
+ * A throwing step propagates (rolled back, not recorded). Returns the ids run.
473
+ */
474
+ private runDataSteps;
428
475
  /**
429
476
  * Runs schema synchronization with distributed locking, in three phases:
430
477
  *
@@ -498,7 +545,7 @@ declare class SchemaSync {
498
545
  * any DDL. Entries come back in execution order; pre-flight refusals appear
499
546
  * as `error` entries (`refused: true`) exactly as `run()` would report them.
500
547
  */
501
- plan(types: readonly TAtscriptAnnotatedType[], opts?: Pick<TSyncOptions, "force" | "safe">): Promise<TSyncPlan>;
548
+ plan(types: readonly TAtscriptAnnotatedType[], opts?: Pick<TSyncOptions, "force" | "safe" | "dataSteps">): Promise<TSyncPlan>;
502
549
  /** Fallback typeMapper for snapshot-based Path B: compares designType directly, skips unions. */
503
550
  private resolveTypeMapper;
504
551
  /**
@@ -600,6 +647,6 @@ declare function syncSchema(space: DbSpace, types: readonly TAtscriptAnnotatedTy
600
647
  * Note: reading the stored baseline still requires a live DB connection (the
601
648
  * snapshot lives in the `__atscript_control` table).
602
649
  */
603
- declare function planSchema(space: DbSpace, types: readonly TAtscriptAnnotatedType[], opts?: Pick<TSyncOptions, "force" | "safe">): Promise<TSyncPlan>;
650
+ declare function planSchema(space: DbSpace, types: readonly TAtscriptAnnotatedType[], opts?: Pick<TSyncOptions, "force" | "safe" | "dataSteps">): Promise<TSyncPlan>;
604
651
  //#endregion
605
- export { SchemaSync, SyncEntry, type TColumnDiffOptions, 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, sameDefaultValue, snapshotToExistingColumns, snapshotToExistingTableOptions, syncSchema, topoOrder };
652
+ export { SchemaSync, SyncEntry, type TColumnDiffOptions, type TDataStep, 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, sameDefaultValue, snapshotToExistingColumns, snapshotToExistingTableOptions, syncSchema, topoOrder };
package/dist/sync.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { Ct as TEnsureTableOptions, Et as TExistingTableOption, G as TDbFieldMeta, Pt as TIndexChange, Tt as TExistingForeignKey, Vt as TReferencingForeignKey, at as TDbObjectKind, cr as TGenericLogger, gn as DbSpace, qt as TTableOptionDiff, xt as TDerivedChangeReason, zt as TPrimaryKeyChange } from "./buckets-CHyspjYP.mjs";
2
- import { S as snapshotToExistingTableOptions, _ as computeSchemaHash, a as hasForeignKeyChanges, b as computeViewSnapshot, d as TForeignKeySnapshot, f as TTableSnapshot, g as canonicalizeQueryNode, h as TViewSnapshot, i as fkKey, l as sameDefaultValue, m as TViewJoinSnapshot, n as computeForeignKeyDiff, o as TColumnDiffOptions, p as TViewColumnSnapshot, s as computeColumnDiff, t as TForeignKeyDiff, u as TFieldSnapshot, v as computeTableHash, x as snapshotToExistingColumns, y as computeTableSnapshot } from "./fk-diff-rj9N2QLw.mjs";
1
+ import { Bt as TPrimaryKeyChange, Ct as TEnsureTableOptions, Et as TExistingTableOption, G as TDbFieldMeta, Ht as TReferencingForeignKey, Jt as TTableOptionDiff, Pt as TIndexChange, Tt as TExistingForeignKey, at as TDbObjectKind, fr as TGenericLogger, xt as TDerivedChangeReason, yn as DbSpace } from "./buckets-CHUgky8T.mjs";
2
+ import { S as snapshotToExistingTableOptions, _ as computeSchemaHash, a as hasForeignKeyChanges, b as computeViewSnapshot, d as TForeignKeySnapshot, f as TTableSnapshot, g as canonicalizeQueryNode, h as TViewSnapshot, i as fkKey, l as sameDefaultValue, m as TViewJoinSnapshot, n as computeForeignKeyDiff, o as TColumnDiffOptions, p as TViewColumnSnapshot, s as computeColumnDiff, t as TForeignKeyDiff, u as TFieldSnapshot, v as computeTableHash, x as snapshotToExistingColumns, y as computeTableSnapshot } from "./fk-diff-Dapr5Dk-.mjs";
3
3
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/schema/sync-entry.d.ts
@@ -329,6 +329,27 @@ interface TSyncPlan {
329
329
  status: "up-to-date" | "changes-needed";
330
330
  schemaHash: string;
331
331
  entries: SyncEntry[];
332
+ /**
333
+ * Ids of the `dataSteps` passed to `planSchema` that have not run yet
334
+ * (since 0.1.165); a pending step makes the plan `"changes-needed"`.
335
+ * Absent when no steps were passed.
336
+ */
337
+ pendingDataSteps?: string[];
338
+ }
339
+ /**
340
+ * A one-time data step of {@link TSyncOptions.dataSteps} (since 0.1.165) —
341
+ * a backfill or data fix that must run exactly once per database, after the
342
+ * schema change it needs.
343
+ */
344
+ interface TDataStep {
345
+ /** Stable, unique id — the step is recorded under it (`data_step:<id>` in `__atscript_control`). */
346
+ id: string;
347
+ /**
348
+ * The step's work. Runs in its own transaction (on adapters with
349
+ * transactions), the record of the step committed in the same one: a step
350
+ * that throws is rolled back, not recorded, and runs again on the next sync.
351
+ */
352
+ run(space: DbSpace): Promise<void> | void;
332
353
  }
333
354
  interface TSyncOptions {
334
355
  /** Pod/instance identifier for distributed locking. Default: random UUID. */
@@ -365,6 +386,15 @@ interface TSyncOptions {
365
386
  * - `"silent"` — legacy behavior: outcome is only observable on the result.
366
387
  */
367
388
  onError?: "throw" | "warn" | "silent";
389
+ /**
390
+ * One-time data steps (since 0.1.165), run in order after the schema DDL,
391
+ * under the sync lock — also when the schema itself is up to date. Each
392
+ * step runs once per database: once it committed, its id is recorded and
393
+ * later syncs skip it. A step that throws stops the run (the error is
394
+ * rethrown after the lock is released); the steps after it stay pending.
395
+ * Steps do not run when the sync is refused or an entry errored.
396
+ */
397
+ dataSteps?: readonly TDataStep[];
368
398
  }
369
399
  interface TSyncResult {
370
400
  /**
@@ -379,6 +409,15 @@ interface TSyncResult {
379
409
  status: "up-to-date" | "synced" | "synced-by-peer" | "refused";
380
410
  schemaHash: string;
381
411
  entries: SyncEntry[];
412
+ /**
413
+ * The {@link TSyncOptions.dataSteps} this call ran, and those still pending
414
+ * (a refused or errored run leaves them all pending). Absent when no steps
415
+ * were passed. @since 0.1.165
416
+ */
417
+ dataSteps?: {
418
+ ran: string[];
419
+ pending: string[];
420
+ };
382
421
  }
383
422
  declare class SchemaSync {
384
423
  private readonly space;
@@ -413,7 +452,8 @@ declare class SchemaSync {
413
452
  /**
414
453
  * Acquires the sync lock, waiting for a peer that holds it. Resolves
415
454
  * `"acquired"`, or `"synced-by-peer"` when a non-forced run finds, after a
416
- * wait, that the peer stored this schema's hash. A forced run never
455
+ * wait, that the peer stored this schema's hash (and `peerDone` agrees —
456
+ * no data step left pending). A forced run never
417
457
  * short-circuits — it waits its turn and runs. Losing the race for a freed
418
458
  * lock to another waiter just waits again, within the same `waitTimeoutMs`.
419
459
  */
@@ -425,6 +465,13 @@ declare class SchemaSync {
425
465
  * logged (the row expires after `lockTtlMs`, blocking peers until then).
426
466
  */
427
467
  private releaseLock;
468
+ /** Ids of the steps not recorded yet, in order. */
469
+ private pendingDataSteps;
470
+ /**
471
+ * Runs the pending steps in order, each with its record in one transaction.
472
+ * A throwing step propagates (rolled back, not recorded). Returns the ids run.
473
+ */
474
+ private runDataSteps;
428
475
  /**
429
476
  * Runs schema synchronization with distributed locking, in three phases:
430
477
  *
@@ -498,7 +545,7 @@ declare class SchemaSync {
498
545
  * any DDL. Entries come back in execution order; pre-flight refusals appear
499
546
  * as `error` entries (`refused: true`) exactly as `run()` would report them.
500
547
  */
501
- plan(types: readonly TAtscriptAnnotatedType[], opts?: Pick<TSyncOptions, "force" | "safe">): Promise<TSyncPlan>;
548
+ plan(types: readonly TAtscriptAnnotatedType[], opts?: Pick<TSyncOptions, "force" | "safe" | "dataSteps">): Promise<TSyncPlan>;
502
549
  /** Fallback typeMapper for snapshot-based Path B: compares designType directly, skips unions. */
503
550
  private resolveTypeMapper;
504
551
  /**
@@ -600,6 +647,6 @@ declare function syncSchema(space: DbSpace, types: readonly TAtscriptAnnotatedTy
600
647
  * Note: reading the stored baseline still requires a live DB connection (the
601
648
  * snapshot lives in the `__atscript_control` table).
602
649
  */
603
- declare function planSchema(space: DbSpace, types: readonly TAtscriptAnnotatedType[], opts?: Pick<TSyncOptions, "force" | "safe">): Promise<TSyncPlan>;
650
+ declare function planSchema(space: DbSpace, types: readonly TAtscriptAnnotatedType[], opts?: Pick<TSyncOptions, "force" | "safe" | "dataSteps">): Promise<TSyncPlan>;
604
651
  //#endregion
605
- export { SchemaSync, SyncEntry, type TColumnDiffOptions, 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, sameDefaultValue, snapshotToExistingColumns, snapshotToExistingTableOptions, syncSchema, topoOrder };
652
+ export { SchemaSync, SyncEntry, type TColumnDiffOptions, type TDataStep, 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, sameDefaultValue, snapshotToExistingColumns, snapshotToExistingTableOptions, syncSchema, topoOrder };
package/dist/sync.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { r as DbError } from "./db-error-hraC12B9.mjs";
2
- import { A as hasForeignKeyChanges, D as computeForeignKeyDiff, O as fkColumns, P as resolveDesignType, St as NoopLogger, a as computeSchemaHash, c as computeViewSnapshot, d as snapshotToExistingTableOptions, f as viewSnapshotSources, gt as aliasTargetOf, ht as relationalColumnName, i as canonicalizeQueryNode, k as fkKey, l as serializeDefaultValue, o as computeTableHash, r as sameDefaultValue, s as computeTableSnapshot, t as computeColumnDiff, u as snapshotToExistingColumns } from "./column-diff-BfNIlMwX.mjs";
3
- import { f as unionValueMembers } from "./validator-DuDWsIdQ.mjs";
1
+ import { A as fkKey, F as resolveDesignType, O as computeForeignKeyDiff, _t as relationalColumnName, a as computeSchemaHash, c as computeViewSnapshot, d as snapshotToExistingTableOptions, f as viewSnapshotSources, i as canonicalizeQueryNode, j as hasForeignKeyChanges, k as fkColumns, l as serializeDefaultValue, o as computeTableHash, r as sameDefaultValue, s as computeTableSnapshot, t as computeColumnDiff, u as snapshotToExistingColumns, vt as aliasTargetOf, wt as NoopLogger } from "./column-diff-9pF0d8JP.mjs";
2
+ import { r as DbError } from "./db-error-BMy4-OWq.mjs";
3
+ import { f as unionValueMembers } from "./validator-UpgPhwvH.mjs";
4
4
  //#region src/schema/json-column-copy.ts
5
5
  const NONE = {
6
6
  copies: [],
@@ -322,11 +322,28 @@ var SyncStore = class {
322
322
  }
323
323
  async ensureControlTable() {
324
324
  if (!this.controlTable) {
325
- const { AtscriptControl } = await import("./control-D5cQmzFj.mjs");
325
+ const { AtscriptControl } = await import("./control-CevYIJC4.mjs");
326
326
  this.controlTable = this.space.getTable(AtscriptControl);
327
327
  }
328
328
  await this.controlTable.ensureTable();
329
329
  }
330
+ /**
331
+ * Runs `fn` in a transaction of the control table's adapter family — the
332
+ * space's tables on the same driver join it (since 0.1.165, data steps).
333
+ */
334
+ async inTransaction(fn) {
335
+ const { AtscriptControl } = await import("./control-CevYIJC4.mjs");
336
+ return this.space.getAdapter(AtscriptControl).withTransaction(fn);
337
+ }
338
+ /** Which of `ids` have a control row — one query (since 0.1.165, data steps). */
339
+ async presentControlIds(ids) {
340
+ if (ids.length === 0) return /* @__PURE__ */ new Set();
341
+ const rows = await this.controlTable.findMany({
342
+ filter: { _id: { $in: [...ids] } },
343
+ controls: { $select: ["_id"] }
344
+ });
345
+ return new Set(rows.map((r) => r._id));
346
+ }
330
347
  async readControlValue(_id) {
331
348
  return (await this.controlTable.findOne({
332
349
  filter: { _id: { $eq: _id } },
@@ -494,7 +511,7 @@ function isExpired(lock, now) {
494
511
  return expiresAt > 0 && expiresAt < now;
495
512
  }
496
513
  async function readStoredSnapshot(space, tableName, _asView) {
497
- const { AtscriptControl } = await import("./control-D5cQmzFj.mjs");
514
+ const { AtscriptControl } = await import("./control-CevYIJC4.mjs");
498
515
  const table = space.getTable(AtscriptControl);
499
516
  await table.ensureTable();
500
517
  const value = (await table.findOne({
@@ -1570,6 +1587,33 @@ function removedBlockers(t, removedTables, removedTableSet, dropEdges) {
1570
1587
  } else for (const r of removedTables) if (r.snapshot && "foreignKeys" in r.snapshot && r.snapshot.foreignKeys.some((fk) => fk.targetTable === t.dbName)) direct.push(r.name);
1571
1588
  return reachable(direct, dropEdges);
1572
1589
  }
1590
+ /** The `__atscript_control` key a data step is recorded under. */
1591
+ function dataStepKey(id) {
1592
+ return `data_step:${id}`;
1593
+ }
1594
+ /** The steps as given, after checking their ids are non-empty and unique. */
1595
+ function checkDataSteps(steps) {
1596
+ if (!steps?.length) return [];
1597
+ const seen = /* @__PURE__ */ new Set();
1598
+ for (const step of steps) {
1599
+ if (typeof step.id !== "string" || step.id === "") throw new Error("syncSchema: every data step needs a non-empty id");
1600
+ if (seen.has(step.id)) throw new Error(`syncSchema: duplicate data step id "${step.id}"`);
1601
+ seen.add(step.id);
1602
+ }
1603
+ return steps;
1604
+ }
1605
+ /** `result` with its `dataSteps` report — unchanged when no steps were passed. */
1606
+ function withSteps(result, steps, ran) {
1607
+ if (steps.length === 0) return result;
1608
+ const done = new Set(ran);
1609
+ return {
1610
+ ...result,
1611
+ dataSteps: {
1612
+ ran: [...ran],
1613
+ pending: steps.map((s) => s.id).filter((id) => !done.has(id))
1614
+ }
1615
+ };
1616
+ }
1573
1617
  var SchemaSync = class {
1574
1618
  space;
1575
1619
  store;
@@ -1587,7 +1631,7 @@ var SchemaSync = class {
1587
1631
  const tables = [];
1588
1632
  const views = [];
1589
1633
  const externalViews = [];
1590
- for (const type of types) {
1634
+ for (const type of new Set(types)) {
1591
1635
  if (aliasTargetOf(type)) continue;
1592
1636
  const readable = this.space.get(type);
1593
1637
  if (readable.isView) {
@@ -1715,7 +1759,8 @@ var SchemaSync = class {
1715
1759
  /**
1716
1760
  * Acquires the sync lock, waiting for a peer that holds it. Resolves
1717
1761
  * `"acquired"`, or `"synced-by-peer"` when a non-forced run finds, after a
1718
- * wait, that the peer stored this schema's hash. A forced run never
1762
+ * wait, that the peer stored this schema's hash (and `peerDone` agrees —
1763
+ * no data step left pending). A forced run never
1719
1764
  * short-circuits — it waits its turn and runs. Losing the race for a freed
1720
1765
  * lock to another waiter just waits again, within the same `waitTimeoutMs`.
1721
1766
  */
@@ -1723,7 +1768,7 @@ var SchemaSync = class {
1723
1768
  const deadline = Date.now() + opts.waitTimeoutMs;
1724
1769
  while (!await this.store.tryAcquireLock(podId, opts.lockTtlMs)) {
1725
1770
  if (!await this.store.waitForLock(deadline - Date.now(), opts.pollIntervalMs)) throw new Error(`Schema sync lock wait timed out after ${opts.waitTimeoutMs}ms`);
1726
- if (!opts.force && await this.store.readHash() === hash) return "synced-by-peer";
1771
+ if (!opts.force && await this.store.readHash() === hash && (!opts.peerDone || await opts.peerDone())) return "synced-by-peer";
1727
1772
  }
1728
1773
  return "acquired";
1729
1774
  }
@@ -1743,6 +1788,30 @@ var SchemaSync = class {
1743
1788
  this.logger.warn("[schema-sync] Failed to release the sync lock — peers wait until it expires:", error instanceof Error ? error.message : error);
1744
1789
  }
1745
1790
  }
1791
+ /** Ids of the steps not recorded yet, in order. */
1792
+ async pendingDataSteps(steps) {
1793
+ const done = await this.store.presentControlIds(steps.map((step) => dataStepKey(step.id)));
1794
+ return steps.filter((step) => !done.has(dataStepKey(step.id))).map((step) => step.id);
1795
+ }
1796
+ /**
1797
+ * Runs the pending steps in order, each with its record in one transaction.
1798
+ * A throwing step propagates (rolled back, not recorded). Returns the ids run.
1799
+ */
1800
+ async runDataSteps(steps, getAbortReason) {
1801
+ const ran = [];
1802
+ const pending = new Set(await this.pendingDataSteps(steps));
1803
+ for (const step of steps) {
1804
+ if (!pending.has(step.id)) continue;
1805
+ this.assertLockHeld(getAbortReason);
1806
+ await this.store.inTransaction(async () => {
1807
+ await step.run(this.space);
1808
+ await this.store.writeControlValue(dataStepKey(step.id), JSON.stringify({ ranAt: Date.now() }));
1809
+ });
1810
+ this.logger.info?.(`[schema-sync] data step "${step.id}" applied`);
1811
+ ran.push(step.id);
1812
+ }
1813
+ return ran;
1814
+ }
1746
1815
  /**
1747
1816
  * Runs schema synchronization with distributed locking, in three phases:
1748
1817
  *
@@ -1767,21 +1836,25 @@ var SchemaSync = class {
1767
1836
  const force = opts?.force ?? false;
1768
1837
  const safe = opts?.safe ?? false;
1769
1838
  const onError = opts?.onError ?? "warn";
1839
+ const steps = checkDataSteps(opts?.dataSteps);
1770
1840
  const resolved = await this.resolveAndHash(types);
1771
1841
  const { hash } = resolved;
1772
1842
  await this.store.ensureControlTable();
1773
1843
  if (!force) {
1774
- if (await this.store.readHash() === hash) return {
1775
- status: "up-to-date",
1776
- schemaHash: hash,
1777
- entries: []
1778
- };
1844
+ if (await this.store.readHash() === hash) {
1845
+ if ((await this.pendingDataSteps(steps)).length === 0) return withSteps({
1846
+ status: "up-to-date",
1847
+ schemaHash: hash,
1848
+ entries: []
1849
+ }, steps, []);
1850
+ }
1779
1851
  }
1780
1852
  if (await this.acquireLock(podId, hash, {
1781
1853
  lockTtlMs,
1782
1854
  waitTimeoutMs,
1783
1855
  pollIntervalMs,
1784
- force
1856
+ force,
1857
+ peerDone: async () => (await this.pendingDataSteps(steps)).length === 0
1785
1858
  }) === "synced-by-peer") return {
1786
1859
  status: "synced-by-peer",
1787
1860
  schemaHash: hash,
@@ -1790,11 +1863,14 @@ var SchemaSync = class {
1790
1863
  const heartbeat = this.startHeartbeat(podId, lockTtlMs);
1791
1864
  try {
1792
1865
  if (!force) {
1793
- if (await this.store.readHash() === hash) return {
1794
- status: "synced-by-peer",
1795
- schemaHash: hash,
1796
- entries: []
1797
- };
1866
+ if (await this.store.readHash() === hash) {
1867
+ const ran = await this.runDataSteps(steps, heartbeat.getAbortReason);
1868
+ return withSteps({
1869
+ status: ran.length > 0 ? "synced" : "synced-by-peer",
1870
+ schemaHash: hash,
1871
+ entries: []
1872
+ }, steps, ran);
1873
+ }
1798
1874
  }
1799
1875
  const d = await this.discover(resolved, safe);
1800
1876
  this.assertLockHeld(heartbeat.getAbortReason);
@@ -1807,11 +1883,11 @@ var SchemaSync = class {
1807
1883
  entries: this.buildPlanEntries(d, safe, refusals)
1808
1884
  };
1809
1885
  this.reportOutcome(result, onError);
1810
- return result;
1886
+ return withSteps(result, steps, []);
1811
1887
  }
1812
1888
  const result = await this.execute(d, safe, heartbeat.getAbortReason);
1813
1889
  this.reportOutcome(result, onError);
1814
- return result;
1890
+ return withSteps(result, steps, result.entries.some((e) => e.status === "error") ? [] : await this.runDataSteps(steps, heartbeat.getAbortReason));
1815
1891
  } finally {
1816
1892
  await heartbeat.stop();
1817
1893
  await this.releaseLock(podId);
@@ -2436,6 +2512,7 @@ var SchemaSync = class {
2436
2512
  * as `error` entries (`refused: true`) exactly as `run()` would report them.
2437
2513
  */
2438
2514
  async plan(types, opts) {
2515
+ const steps = checkDataSteps(opts?.dataSteps);
2439
2516
  const force = opts?.force ?? false;
2440
2517
  const safe = opts?.safe ?? false;
2441
2518
  const resolved = await this.resolveAndHash(types);
@@ -2445,18 +2522,23 @@ var SchemaSync = class {
2445
2522
  const refusals = this.preflight(d, safe);
2446
2523
  await this.planLiveWork(d, safe);
2447
2524
  const entries = this.buildPlanEntries(d, safe, refusals);
2525
+ const pending = steps.length > 0 ? await this.pendingDataSteps(steps) : void 0;
2526
+ const withPending = (plan) => pending ? {
2527
+ ...plan,
2528
+ pendingDataSteps: pending
2529
+ } : plan;
2448
2530
  if (!force) {
2449
- if (await this.store.readHash() === hash) return {
2531
+ if (await this.store.readHash() === hash && !pending?.length) return withPending({
2450
2532
  status: "up-to-date",
2451
2533
  schemaHash: hash,
2452
2534
  entries
2453
- };
2535
+ });
2454
2536
  }
2455
- return {
2537
+ return withPending({
2456
2538
  status: "changes-needed",
2457
2539
  schemaHash: hash,
2458
2540
  entries
2459
- };
2541
+ });
2460
2542
  }
2461
2543
  /** Fallback typeMapper for snapshot-based Path B: compares designType directly, skips unions. */
2462
2544
  resolveTypeMapper(adapter) {
@@ -1,4 +1,4 @@
1
- const require_db_error = require("./db-error-DmF1_xrz.cjs");
1
+ const require_db_error = require("./db-error-G3sgvL87.cjs");
2
2
  const require_ops = require("./ops.cjs");
3
3
  let _atscript_typescript_utils = require("@atscript/typescript/utils");
4
4
  //#region src/shared/union-shape.ts
@@ -1,4 +1,4 @@
1
- import { i as DepthLimitExceededError } from "./db-error-hraC12B9.mjs";
1
+ import { i as DepthLimitExceededError } from "./db-error-BMy4-OWq.mjs";
2
2
  import { getDbFieldOp } from "./ops.mjs";
3
3
  import { createAnnotatedTypeNode, flattenAnnotatedType } from "@atscript/typescript/utils";
4
4
  //#region src/shared/union-shape.ts
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_db_error = require("./db-error-DmF1_xrz.cjs");
3
- const require_validator = require("./validator-DK9Ijpo7.cjs");
2
+ const require_db_error = require("./db-error-G3sgvL87.cjs");
3
+ const require_validator = require("./validator-D7E7T0Oy.cjs");
4
4
  const require_server_stamped = require("./server-stamped-B6XbUQ4p.cjs");
5
5
  const require_object = require("./object-GjsuPNqo.cjs");
6
6
  const require_ops = require("./ops.cjs");
@@ -1,4 +1,4 @@
1
- import { r as DbError } from "./db-error-B_DRL1E9.cjs";
1
+ import { r as DbError } from "./db-error-DQUw2cED.cjs";
2
2
  import { a as $mul, c as $update, d as TDbFieldOp, g as separateCas, h as reconcileCas, i as $insert, l as $upsert, m as isDbFieldOp, n as $dec, o as $remove, r as $inc, s as $replace, t as $cas, u as TDbCas } from "./ops-AqhV7s9o.cjs";
3
3
  import { a as isPlainObject, i as isEmptyObject } from "./object-Czia_qD4.cjs";
4
4
  import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-wRqMYNBI.cjs";
@@ -1,4 +1,4 @@
1
- import { r as DbError } from "./db-error-B_DRL1E9.mjs";
1
+ import { r as DbError } from "./db-error-DQUw2cED.mjs";
2
2
  import { a as $mul, c as $update, d as TDbFieldOp, g as separateCas, h as reconcileCas, i as $insert, l as $upsert, m as isDbFieldOp, n as $dec, o as $remove, r as $inc, s as $replace, t as $cas, u as TDbCas } from "./ops-AqhV7s9o.mjs";
3
3
  import { a as isPlainObject, i as isEmptyObject } from "./object-Czia_qD4.mjs";
4
4
  import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-wRqMYNBI.mjs";
@@ -1,5 +1,5 @@
1
- import { r as DbError } from "./db-error-hraC12B9.mjs";
2
- import { a as forceNavNonOptional, c as getKeyProps, i as dbPlugin, n as buildPatchPartial, o as isNavRelation, r as buildValidationContext, s as createDbValidatorPlugin, t as buildDbValidator } from "./validator-DuDWsIdQ.mjs";
1
+ import { r as DbError } from "./db-error-BMy4-OWq.mjs";
2
+ import { a as forceNavNonOptional, c as getKeyProps, i as dbPlugin, n as buildPatchPartial, o as isNavRelation, r as buildValidationContext, s as createDbValidatorPlugin, t as buildDbValidator } from "./validator-UpgPhwvH.mjs";
3
3
  import { n as isServerStampedOnUpdate, t as SERVER_STAMPED_ON_UPDATE_KEYS } from "./server-stamped-BoP56K9M.mjs";
4
4
  import { a as isPlainObject, i as isEmptyObject } from "./object-PQ1J8jri.mjs";
5
5
  import { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, isDbFieldOp, reconcileCas, separateCas } from "./ops.mjs";
@@ -1,4 +1,4 @@
1
- const require_aggregate_fns = require("./aggregate-fns-DupYzFD7.cjs");
1
+ const require_aggregate_fns = require("./aggregate-fns-CNtrSxGH.cjs");
2
2
  const require_search_fields = require("./search-fields-B49-RCWd.cjs");
3
3
  let _atscript_core = require("@atscript/core");
4
4
  //#region src/shared/annotation-utils.ts
@@ -62,17 +62,21 @@ function primitiveBaseType(def) {
62
62
  return typeof ct === "object" ? ct.kind === "final" ? ct.value : ct.kind : ct;
63
63
  }
64
64
  /**
65
+ * `null` in a type expression: the `null` reference or the `null` primitive
66
+ * (whose `config.type` is `"null"`).
67
+ */
68
+ function isNullMember(node) {
69
+ return (0, _atscript_core.isRef)(node) && !node.hasChain && node.id === "null" || primitiveBaseType(node) === "null";
70
+ }
71
+ /**
65
72
  * Whether a definition is a union with a `null` member — `T | null`, or a
66
73
  * ref to an alias of one (compile-time mirror of the runtime nullable union,
67
74
  * since 0.1.155).
68
75
  */
69
76
  function isNullableUnion(def, doc, depth = 0) {
70
77
  if (!def || depth > 8) return false;
71
- if ((0, _atscript_core.isRef)(def)) {
72
- if (def.id === "null") return true;
73
- return isNullableUnion(doc.unwindType(def.id, def.chain)?.def, doc, depth + 1);
74
- }
75
- if (primitiveBaseType(def) === "null") return true;
78
+ if (isNullMember(def)) return true;
79
+ if ((0, _atscript_core.isRef)(def)) return isNullableUnion(doc.unwindType(def.id, def.chain)?.def, doc, depth + 1);
76
80
  const group = def;
77
81
  if (!group.isGroup || group.op !== "|" || (0, _atscript_core.isStructure)(def)) return false;
78
82
  return group.unwrap().some((member) => isNullableUnion(member, doc, depth + 1));
@@ -369,6 +373,20 @@ function findViewCycle(owner, doc) {
369
373
  return walk(owner, doc, [start]);
370
374
  }
371
375
  /**
376
+ * The chain reference (`User.id`) an `@db.rel.FK` field's type names, else
377
+ * `undefined`. `User.id | null` names it too (since 0.1.165) — the same
378
+ * optional foreign key as `field?: User.id`; any other union does not.
379
+ */
380
+ function fkChainRef(def) {
381
+ if (def && (0, _atscript_core.isRef)(def)) return def.hasChain ? def : void 0;
382
+ if (!def || !(0, _atscript_core.isGroup)(def) || def.op !== "|") return void 0;
383
+ const members = def.unwrap();
384
+ const values = members.filter((m) => !isNullMember(m));
385
+ if (values.length !== 1 || values.length === members.length) return void 0;
386
+ const [only] = values;
387
+ return only && (0, _atscript_core.isRef)(only) && only.hasChain ? only : void 0;
388
+ }
389
+ /**
372
390
  * Find all `@db.rel.FK` fields on a type that reference `targetTypeName`.
373
391
  * Resolves `extends` to include inherited fields.
374
392
  */
@@ -383,10 +401,8 @@ function findFKFieldsPointingTo(doc, iface, targetTypeName, alias) {
383
401
  if (!struct) return results;
384
402
  for (const [name, prop] of struct.props) {
385
403
  if (prop.countAnnotations("db.rel.FK") === 0) continue;
386
- const def = prop.getDefinition();
387
- if (!def || !(0, _atscript_core.isRef)(def)) continue;
388
- const ref = def;
389
- if (!ref.hasChain) continue;
404
+ const ref = fkChainRef(prop.getDefinition());
405
+ if (!ref) continue;
390
406
  const refTypeName = ref.id;
391
407
  const refField = ref.chain.map((c) => c.text).join(".");
392
408
  if (refTypeName !== targetTypeName) continue;
@@ -866,6 +882,12 @@ Object.defineProperty(exports, "findViewCycle", {
866
882
  return findViewCycle;
867
883
  }
868
884
  });
885
+ Object.defineProperty(exports, "fkChainRef", {
886
+ enumerable: true,
887
+ get: function() {
888
+ return fkChainRef;
889
+ }
890
+ });
869
891
  Object.defineProperty(exports, "forEachFieldRef", {
870
892
  enumerable: true,
871
893
  get: function() {
@@ -932,6 +954,12 @@ Object.defineProperty(exports, "isNavProp", {
932
954
  return isNavProp;
933
955
  }
934
956
  });
957
+ Object.defineProperty(exports, "isNullMember", {
958
+ enumerable: true,
959
+ get: function() {
960
+ return isNullMember;
961
+ }
962
+ });
935
963
  Object.defineProperty(exports, "isNullableUnion", {
936
964
  enumerable: true,
937
965
  get: function() {