@voltro/database 0.20.2 → 0.22.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
@@ -173,9 +173,10 @@ export declare const attachEagerLoads: (rows: ReadonlyArray<Row_2>, spec: WithSp
173
173
  * time the async-local scope is gone. Stamping at creation is what makes the
174
174
  * transactional path carry identity at all.
175
175
  */
176
- export declare const attributionFields: () => {
176
+ export declare const attributionFields: (explicit?: WriteAttribution | undefined) => {
177
177
  traceId?: string;
178
178
  subjectId?: string | null;
179
+ procedure?: string;
179
180
  };
180
181
 
181
182
  /** The write's identity, as both sides can compute it. NOT the row image: the
@@ -245,6 +246,12 @@ export declare interface BackfillSpec<TsType = unknown> {
245
246
  readonly sleepMs?: number;
246
247
  }
247
248
 
249
+ /** Mark a non-transactional write to `table` as started. Pairs with
250
+ * `endLocalWrite` in a `finally` — an unpaired begin holds that table's echoes
251
+ * until `DEFER_BOUND_MS`, which is why the pairing is the store's job and not
252
+ * the caller's. */
253
+ export declare const beginLocalWrite: (table: string) => void;
254
+
248
255
  /**
249
256
  * 64-bit integer column — `BIGINT` on postgres / mysql / mariadb / mssql,
250
257
  * `INTEGER` (which is 64-bit) on sqlite / turso. Use for values that can
@@ -515,6 +522,17 @@ export declare type ChangeEvent = {
515
522
  * `undefined` = no request context at all. The two are different facts.
516
523
  */
517
524
  readonly subjectId?: string | null;
525
+ /**
526
+ * The rpc tag of the call that caused this write (`teams.removeSubTeamMember`).
527
+ *
528
+ * `traceId` says WHICH call; this says WHAT CALL IT WAS. The difference
529
+ * matters to anything rendering history for a human: a row diff carries no
530
+ * intent, and the same delete on a join table is a member removal, a cascade,
531
+ * or an expiry depending only on who asked. Absent for a write with no
532
+ * procedure behind it (seed, startup, migration, schedule) — same meaning as
533
+ * an absent `traceId`.
534
+ */
535
+ readonly procedure?: string;
518
536
  };
519
537
 
520
538
  /**
@@ -2111,6 +2129,11 @@ export declare const encryptedColumnsOf: (table: TableLike) => ReadonlyArray<str
2111
2129
  */
2112
2130
  export declare const encryptFieldsForWrite: (row: Row, table: TableLike | undefined, cipher: FieldCipher | undefined) => Row;
2113
2131
 
2132
+ /** Mark it finished — AFTER its `routeEvent` has registered, not after the
2133
+ * statement. That is the whole point: the window being closed is exactly the
2134
+ * gap between the committing statement and the registration. */
2135
+ export declare const endLocalWrite: (table: string) => void;
2136
+
2114
2137
  /**
2115
2138
  * Enforce the storage-cost caps: evict live branches past their TTL, and report
2116
2139
  * whether a new branch would exceed `maxBranches` (counting only what survives
@@ -4169,6 +4192,10 @@ export declare interface RecordedWrite {
4169
4192
  /** Request identity, when the write had a request behind it. */
4170
4193
  readonly traceId?: string | undefined;
4171
4194
  readonly subjectId?: string | null | undefined;
4195
+ /** The rpc tag of the call doing the writing. Same meaning and same absence
4196
+ * rules as `traceId`; carried so a transactional write records WHICH CALL
4197
+ * changed a row, not only which trace. */
4198
+ readonly procedure?: string | undefined;
4172
4199
  }
4173
4200
 
4174
4201
  /**
@@ -4401,6 +4428,18 @@ declare type Resolve<T> = {
4401
4428
  */
4402
4429
  export declare const resolveBranchMechanism: (opts: BranchMechanismOptions) => BranchMechanism;
4403
4430
 
4431
+ /**
4432
+ * Answer a transport echo with the attribution of the local write that produced
4433
+ * it, or with `undefined` when the write came from another replica.
4434
+ *
4435
+ * `deliver` is called EXACTLY once — synchronously in the common case, and on a
4436
+ * later turn only when a local write to the same table is mid-registration.
4437
+ * Stores must emit through this rather than calling `claimPendingAttribution`
4438
+ * themselves: the claim is the easy half, and the two dialect stores had a
4439
+ * hand-written copy of it each, which is how a decision drifts.
4440
+ */
4441
+ export declare const resolveEchoAttribution: (table: string, op: string, primaryKey: unknown, deliver: (attribution: WriteAttribution | undefined) => void) => void;
4442
+
4404
4443
  /**
4405
4444
  * Resolve a `.fullTextIndex(name, ...)` declaration off the source
4406
4445
  * table so `.matching()` can stamp the covered columns + config onto
@@ -4977,6 +5016,23 @@ export declare interface Table<Name extends string, Fields extends Record<string
4977
5016
  readonly appliedUniques: ReadonlyArray<TableUnique>;
4978
5017
  readonly appliedFullText: ReadonlyArray<TableFullTextIndex>;
4979
5018
  readonly appliedChecks: ReadonlyArray<TableCheck>;
5019
+ /**
5020
+ * The name this table used to have, set by `.renamedFrom('old_name')`.
5021
+ * Named apart from the METHOD that sets it — one interface cannot carry both.
5022
+ *
5023
+ * The table-level twin of a column's `.renamedFrom()`, and it exists for the
5024
+ * same reason: a rename and a drop+create are structurally identical to a
5025
+ * differ (old table gone, new table present) and mean completely different
5026
+ * things. Without the marker the planner has to assume drop + create, which
5027
+ * for a TABLE is data loss, so it refuses — and the rename can only be done
5028
+ * by hand.
5029
+ *
5030
+ * A chained method, matching a column's `.renamedFrom()` — the two mean the
5031
+ * same thing one level apart, so they read the same. It survives every
5032
+ * subsequent `.index(...)` / `.with(...)` because each rebuild spreads its
5033
+ * options through.
5034
+ */
5035
+ readonly previousTableName?: string;
4980
5036
  /**
4981
5037
  * Composite PRIMARY KEY column set, declared via `.primaryKey(['a','b'])`.
4982
5038
  * When set it REPLACES the single-column `id()` PK assumption: no column
@@ -5313,6 +5369,23 @@ export declare interface Table<Name extends string, Fields extends Record<string
5313
5369
  * says so at boot.
5314
5370
  */
5315
5371
  nonReactive: () => Table<Name, Fields, false, IxNames>;
5372
+ /**
5373
+ * Declare the name this table used to have, so the planner folds what would
5374
+ * otherwise be DROP + CREATE — the loss of every row — into an in-place
5375
+ * `rename-table`.
5376
+ *
5377
+ * Deliberately the SAME SHAPE as a column's `.renamedFrom()`, because the two
5378
+ * mean the same thing one level apart, and a reader who learned one will reach
5379
+ * for the other. It first shipped as a third argument to `table()`; that made
5380
+ * the one API in this family that had to be looked up.
5381
+ *
5382
+ * export const notes = table('archive_notes', { … }).renamedFrom('notes')
5383
+ *
5384
+ * Drop the marker once the rename has been applied in every environment. A
5385
+ * marker whose old table is absent is a silent no-op, so it can sit in source
5386
+ * across a staged rollout.
5387
+ */
5388
+ renamedFrom: (oldName: string) => Table<Name, Fields, Reactive, IxNames>;
5316
5389
  }
5317
5390
 
5318
5391
  export declare const table: <const Name extends string, const Input extends FieldsInput>(name: Name, fields: Input) => Table<Name, FieldDefinitions<Input>, true, never>;
@@ -5962,6 +6035,34 @@ export declare interface WindowSpec {
5962
6035
  }>;
5963
6036
  }
5964
6037
 
6038
+ /**
6039
+ * Capture the attribution ONCE, at a point that is synchronously inside the
6040
+ * request, and hand it to `fn` — while ALSO re-entering the async-local scope for
6041
+ * everything `fn` reaches that has not been threaded.
6042
+ *
6043
+ * WHY BOTH. Re-entering the scope alone is not enough, and that is measured
6044
+ * rather than assumed. `AsyncLocalStorage` propagates through continuations the
6045
+ * current context CREATES; a continuation scheduled by ANOTHER context — which is
6046
+ * exactly what a connection-pool handoff does when an acquisition queues — resumes
6047
+ * with whatever store that other context had. So a `routeEvent` running after
6048
+ * `await runPromise(...)` can read an EMPTY scope under contention, and the write
6049
+ * lands with `traceId`/`subjectId` NULL. NULL is a legal value there meaning "no
6050
+ * request behind this write", so the result does not look like a defect; it looks
6051
+ * like a schedule. In a compliance trail that asymmetry is the whole problem.
6052
+ *
6053
+ * A consumer could not reproduce it across 2700 writes at 96-way concurrency with
6054
+ * every core saturated — which is good evidence the window is narrow at their load
6055
+ * and no evidence at all that it is closed. Their own framing is why this is being
6056
+ * closed structurally rather than left: *"impossible beats unlikely when the
6057
+ * failure is invisible."*
6058
+ *
6059
+ * The captured value is passed EXPLICITLY down the write path; the re-entered
6060
+ * scope is the fallback for any path not yet threaded. That ordering is deliberate
6061
+ * — a site that has not been threaded behaves exactly as it does today rather than
6062
+ * worse, so the threading can be verified site by site instead of all at once.
6063
+ */
6064
+ export declare const withCapturedAttribution: <T>(fn: (attribution: WriteAttribution | undefined) => T) => T;
6065
+
5965
6066
  /** Tree of relations to eager-load. */
5966
6067
  export declare type WithSpec = Readonly<Record<string, true | EagerLoadSpec>>;
5967
6068
 
@@ -5976,6 +6077,25 @@ export declare type WithSpec = Readonly<Record<string, true | EagerLoadSpec>>;
5976
6077
  */
5977
6078
  export declare interface WriteAttribution {
5978
6079
  readonly traceId?: string;
6080
+ /**
6081
+ * The rpc tag of the call doing the writing (`teams.removeSubTeamMember`).
6082
+ *
6083
+ * WHY IT IS HERE AND NOT DERIVED. A row change carries no intent. The same
6084
+ * DELETE on `userTeams` is a member being removed, a team being deleted, a
6085
+ * user being deleted, or a membership expiring — and the before/after diff
6086
+ * cannot tell those apart, because the difference is not in the data. A
6087
+ * consumer building an audit UI hit exactly this: `_voltro_row_history` could
6088
+ * say "row X of userTeams changed" and show the JSON, and could not say "Anna
6089
+ * removed Bernd from the Frontend sub-team".
6090
+ *
6091
+ * The tag is the missing anchor, and it is already known at the boundary that
6092
+ * establishes this attribution, so it costs one field rather than an app-side
6093
+ * hook that would have to reconstruct intent from shape.
6094
+ *
6095
+ * Absent for a write with no procedure behind it — a seed, a `*.startup.tsx`,
6096
+ * a migration. Same meaning as an absent `traceId`.
6097
+ */
6098
+ readonly procedure?: string;
5979
6099
  /** `null` is meaningful: a resolved subject with no acting user (an
5980
6100
  * anonymous or system call), as distinct from `undefined` = no request. */
5981
6101
  readonly subjectId?: string | null;