@voltro/database 0.54.0 → 0.55.0

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.
package/dist/index.d.ts CHANGED
@@ -2478,6 +2478,38 @@ export declare interface DataStore {
2478
2478
  * callers guard with `if (store.refreshChangeCaptureExclusions)`.
2479
2479
  */
2480
2480
  refreshChangeCaptureExclusions?(): Promise<ChangeCaptureExclusionRefresh>;
2481
+ /**
2482
+ * Run an Effect that needs this store's own `SqlClient` — the raw-SQL seam.
2483
+ *
2484
+ * The capability the STAGED SWAP is built on. `--mode replace` loads into
2485
+ * `_voltro_staging_<t>` tables and then swaps the CONTENT in one short
2486
+ * transaction (`DELETE FROM t; INSERT INTO t (…) SELECT … FROM
2487
+ * _voltro_staging_t`), which moves the long, network-bound load OUT of the
2488
+ * destructive transaction. None of that is expressible through the typed row
2489
+ * API: it is DDL plus a server-side `INSERT … SELECT`, and the whole point is
2490
+ * that no row crosses the wire.
2491
+ *
2492
+ * OPTIONAL on the interface, and declared here rather than duck-typed at the
2493
+ * call site for the reason the house rule gives: the four dialect stores are
2494
+ * four hand-written copies of one interface, and a capability nothing declares
2495
+ * is a capability nothing can be red about. A store that loses it now fails to
2496
+ * satisfy this member instead of silently falling out of `canStage()` and
2497
+ * taking the weaker path forever, on one dialect, in silence.
2498
+ *
2499
+ * The DECISION does not live per store — that would be the four-copies trap
2500
+ * wearing a different hat. `@voltro/data-transfer`'s `stagedSwap.ts` writes
2501
+ * the per-dialect SQL ONCE and sends it down this seam; the store contributes
2502
+ * a connection, not a policy.
2503
+ *
2504
+ * Callers guard with `if (store.run)` (or `canStage(store)`), and a store
2505
+ * without it keeps the path it already has: the in-memory store has no SQL
2506
+ * and no catalog, and neither does a bare `DataStore` view.
2507
+ *
2508
+ * `R` is deliberately `SqlClient.SqlClient`: the effect asks for the client
2509
+ * and the store provides its own, which is what makes this a SEAM rather than
2510
+ * a way to hand a store somebody else's connection.
2511
+ */
2512
+ run?<A, E>(effect: Effect.Effect<A, E, SqlClient.SqlClient>): Promise<A>;
2481
2513
  }
2482
2514
 
2483
2515
  /** One transfer, as the inspector reads it back. */
@@ -3828,6 +3860,13 @@ export declare interface InterruptedReplace {
3828
3860
  readonly tables: number | null;
3829
3861
  readonly capture: string | null;
3830
3862
  readonly via: string;
3863
+ /** The staging tables the run created. Empty for every kind but `staging`. */
3864
+ readonly staging: ReadonlyArray<string>;
3865
+ /** Last sign of life from the run, or `null` when it never wrote one. */
3866
+ readonly heartbeatAt: Date | null;
3867
+ /** `true` when the run was started `--no-atomic` — its staging is a resume
3868
+ * point, not a leftover. */
3869
+ readonly resumable: boolean;
3831
3870
  }
3832
3871
 
3833
3872
  /**
@@ -3972,6 +4011,41 @@ export declare const isSeedDefinition: (value: unknown) => value is SeedDefiniti
3972
4011
  */
3973
4012
  export declare const isSqliteFamily: (dialect: DialectId) => boolean;
3974
4013
 
4014
+ /**
4015
+ * True when `name` is a `--mode replace` staging table.
4016
+ *
4017
+ * ── This is a THIRD rule, and it must not be folded into the other two ─────
4018
+ *
4019
+ * The framework-table filter above is deliberately ASYMMETRIC: a framework
4020
+ * table nobody declares is never planned for a drop, and one we DO declare
4021
+ * diffs like any other table. Collapsing those two into one filter is what
4022
+ * produced a postgres-only evolution path for the framework's own tables, and
4023
+ * the comment in `boot.ts` exists to stop it being collapsed again.
4024
+ *
4025
+ * A staging table satisfies NEITHER half. It is not a schema fact at all — it is
4026
+ * a scratch copy of a target table that exists for the length of one import and
4027
+ * is dropped after the swap. So:
4028
+ *
4029
+ * - the DROP direction is already covered, because `_voltro_staging_` starts
4030
+ * with `_voltro_` and `isFrameworkOwnedLiveTable` therefore says "never
4031
+ * drop it". Correct: a `replace` in flight on another replica is loading
4032
+ * into it right now.
4033
+ * - the CREATE direction is NOT, and that is the gap this closes. `--mode
4034
+ * replace` REGISTERS each staging table as a clone of its target for the
4035
+ * duration of the load — it has to, or a typed write cannot resolve the
4036
+ * column types and a `json()` column is written unencoded. A registered
4037
+ * table is a DECLARED table, and by the asymmetry above a declared
4038
+ * framework table diffs like any other: a plan computed while an import is
4039
+ * in flight proposes `create-table _voltro_staging_notes` and, once
4040
+ * applied, a table nothing will ever declare again is kept forever.
4041
+ *
4042
+ * So staging is excluded from the DECLARED side (in `declaredSnapshot`, which is
4043
+ * the one input both the plan and its fingerprint are built from) and left to
4044
+ * the existing prefix rule on the live side. Two different rules, two different
4045
+ * places, on purpose.
4046
+ */
4047
+ export declare const isStagingTableName: (name: string) => boolean;
4048
+
3975
4049
  /**
3976
4050
  * Does this table emit change events at all?
3977
4051
  *
@@ -6593,6 +6667,86 @@ export declare interface SqlDialect {
6593
6667
  readonly retryFilter: (error: unknown) => RetryDecision;
6594
6668
  }
6595
6669
 
6670
+ /**
6671
+ * How long a staged run's silence has to last before its scratch tables are
6672
+ * treated as abandoned.
6673
+ *
6674
+ * Generous on purpose. The beat is written every couple of seconds while rows
6675
+ * land, so this is not "how long a load may take" — it is how long a load may
6676
+ * be stalled, or its bookkeeping write failing, before a boot on another
6677
+ * replica decides nobody is coming back. The cost of being wrong here is that
6678
+ * an in-flight import's swap fails with a missing table and the operator
6679
+ * re-runs it; the target is untouched either way, which is what makes an
6680
+ * automatic sweep affordable at all.
6681
+ *
6682
+ * Declared as `dataTransfer.stagingStaleMinutes` in `app.config.ts`;
6683
+ * `VOLTRO_STAGING_STALE_MINUTES` overrides that in turn, on the same rule every
6684
+ * other tunable here follows — an operator acting on a running deployment
6685
+ * outranks what the project declared.
6686
+ */
6687
+ export declare const STAGING_STALE_MINUTES = 30;
6688
+
6689
+ /**
6690
+ * The prefix a STAGING table carries: `_voltro_staging_<target>`.
6691
+ *
6692
+ * Defined in this leaf module, not in `@voltro/data-transfer` where the swap
6693
+ * lives, because the differ has to know it and `@voltro/database` cannot depend
6694
+ * on the package that imports it. One definition; `stagedSwap.ts` re-exports
6695
+ * this one rather than spelling the prefix a second time.
6696
+ */
6697
+ export declare const STAGING_TABLE_PREFIX = "_voltro_staging_";
6698
+
6699
+ /**
6700
+ * What a boot says about them.
6701
+ *
6702
+ * A NOTE, never a refusal — the target of a staged run is untouched by
6703
+ * definition. But it is said out loud, because the alternative is what shipped:
6704
+ * a set of tables holding a whole bundle that only an introspection could find,
6705
+ * so nobody knew they existed until a disk filled up.
6706
+ */
6707
+ export declare const stagingLeftoverReport: (leftovers: StagingLeftovers, now: number) => string | undefined;
6708
+
6709
+ /** One staged run's leftovers, and what a boot may do about them. */
6710
+ export declare interface StagingLeftovers {
6711
+ /** Every `kind: 'staging'` row, for the report. */
6712
+ readonly all: ReadonlyArray<InterruptedReplace>;
6713
+ /**
6714
+ * The rows whose tables a boot may DROP: the beat is stale AND the run was
6715
+ * not resumable.
6716
+ *
6717
+ * Both halves are load-bearing and neither is enough alone. A fresh beat is
6718
+ * an import in flight — on this pod or another replica — and dropping its
6719
+ * staging would destroy a load in progress. A resumable run's staging is a
6720
+ * RESUME POINT that is meant to outlive the process, so no amount of silence
6721
+ * makes it collectable.
6722
+ */
6723
+ readonly droppable: ReadonlyArray<InterruptedReplace>;
6724
+ }
6725
+
6726
+ /**
6727
+ * Decide, from the marker rows alone, which staging sets a boot may collect.
6728
+ *
6729
+ * Pure, and separated from the boot so the DECISION can be tested without a
6730
+ * database — the rule is three lines and every one of them has a way to be
6731
+ * quietly wrong in the direction that destroys someone's work.
6732
+ */
6733
+ export declare const stagingLeftovers: (rows: ReadonlyArray<InterruptedReplace>, now: number, staleAfterMs: number) => StagingLeftovers;
6734
+
6735
+ /** Split a `kind: 'staging'` row's comma-separated name list. */
6736
+ export declare const stagingNamesOf: (raw: unknown) => ReadonlyArray<string>;
6737
+
6738
+ /**
6739
+ * The threshold in ms: the env override, else what the app declared, else the
6740
+ * default above.
6741
+ *
6742
+ * A non-positive or unparseable value at EITHER level falls through to the next
6743
+ * one rather than failing. This runs on a boot path whose whole contract is that
6744
+ * scratch-table housekeeping cannot stop a boot, so a typo in a tunable must not
6745
+ * be the thing that does — and the fall-through lands on a threshold that is
6746
+ * generous in the safe direction.
6747
+ */
6748
+ export declare const stagingStaleAfterMs: (env: Record<string, string | undefined>, declaredMinutes?: number) => number;
6749
+
6596
6750
  /**
6597
6751
  * Fill `row.id` from the table's declared scheme when it is absent.
6598
6752
  *