@voltro/database 0.41.0 → 0.43.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,7 +173,7 @@ export declare const attachEagerLoads: (rows: ReadonlyArray<Row_2>, spec: WithSp
173
173
  * Returns an object with the keys OMITTED rather than set to `undefined`, so an
174
174
  * unattributed event is byte-identical to one from before this existed — no
175
175
  * `traceId: undefined` appearing in a snapshot, a broadcast payload, or a
176
- * consumer's `Object.keys`.
176
+ * deployment's `Object.keys`.
177
177
  *
178
178
  * **Call it where the event is CREATED, not where it is delivered.** A
179
179
  * transactional write queues its events and flushes them after commit, by which
@@ -222,7 +222,7 @@ export declare const auditTableIndexes: (table: Table<string, Record<string, Col
222
222
  * so an insert need not supply them. Name-based on purpose: a hand-declared
223
223
  * `createdAt` with no default reads as optional here (the lenient direction —
224
224
  * never rejects valid code), while the mixin-managed ones are always filled.
225
- * Runtime tuple + the type derived from it, so a consumer that must skip these
225
+ * Runtime tuple + the type derived from it, so a deployment that must skip these
226
226
  * at runtime (e.g. `@voltro/testing`'s `fixtureRow`, which fills only
227
227
  * caller-owned required columns) reads the SAME list the insert-row type uses —
228
228
  * no second copy to drift. */
@@ -651,7 +651,7 @@ export declare type ChangeEvent = {
651
651
  * How the event reached this process. Absent/'inline' = emitted by this
652
652
  * process's own write path; 'injected' = delivered over a cross-instance
653
653
  * transport (`injectExternalChange` — the broadcast bus or a CDC
654
- * consumer). A consumer that must act exactly once per change
654
+ * consumer). A deployment that must act exactly once per change
655
655
  * FLEET-WIDE combines this with `DataStore.changeScope`: on a
656
656
  * `'local'`-scope store, acting on non-injected events covers each
657
657
  * change exactly once (on its writer); on a `'fleet'`-scope store every
@@ -765,6 +765,19 @@ export declare const chunkRowsForInsert: <T extends Record<string, unknown>>(row
765
765
  */
766
766
  export declare const claimPendingAttribution: (key: string) => WriteAttribution | undefined;
767
767
 
768
+ /**
769
+ * Classify a driver failure — already flattened by `extractDbCause` — into the
770
+ * safe facts above, or `undefined` when it is not a constraint violation at all
771
+ * (a deadlock, a dead connection, a timeout, a syntax error).
772
+ *
773
+ * `operation` is the write the framework was performing. It is consulted for
774
+ * ONE thing: sqlite reports both FK directions as the same bare
775
+ * `FOREIGN KEY constraint failed` with no name and no column, so a delete that
776
+ * trips it is `foreignKeyInUse` and an insert is `foreignKey`. Every other
777
+ * dialect states the direction itself and the operation is not consulted.
778
+ */
779
+ export declare const classifyConstraintViolation: (dbCause: Record<string, unknown>, operation?: string) => ConstraintFacts | undefined;
780
+
768
781
  /** Wipe — used by tests + the dev-loop hot-reload. */
769
782
  export declare const clearEnumRenames: () => void;
770
783
 
@@ -1865,6 +1878,48 @@ export declare interface ConnectionIdentityInput {
1865
1878
  readonly database?: string | undefined;
1866
1879
  }
1867
1880
 
1881
+ /**
1882
+ * The SAFE facts about a constraint violation: which rule, and the NAME of the
1883
+ * constraint / column it fired on. Never a value, and never the driver's
1884
+ * sentence.
1885
+ *
1886
+ * That exclusion is the whole design, and it is measured, not cautious:
1887
+ *
1888
+ * - postgres attaches `detail: "Failing row contains (t4, l1, null, null,
1889
+ * null)."` to a not-null and a check violation — **the entire row**, every
1890
+ * column, including anything a table marked `.sensitive()`.
1891
+ * - mysql/mariadb: `Duplicate entry 's1' for key 'tasks.slug'`.
1892
+ * - mssql: `The duplicate key value is (s1).` and, on truncation,
1893
+ * `Truncated value: '…'`.
1894
+ *
1895
+ * So "just pass the driver message through" is not a smaller version of this —
1896
+ * it is a different feature, one that ships row contents to whoever provoked
1897
+ * the error. A constraint NAME is schema, the same class of fact the framework
1898
+ * already puts on the wire in `TableValidationFailed.table`; a row is data.
1899
+ *
1900
+ * Everything here is captured as an anchored identifier group out of the
1901
+ * driver text — never a slice of it — so a regex that fails to match yields
1902
+ * `undefined` rather than a sentence that happens to contain a value.
1903
+ */
1904
+ export declare interface ConstraintFacts {
1905
+ readonly kind: ConstraintKind;
1906
+ /** The constraint / index name, when the dialect names one. */
1907
+ readonly constraint?: string;
1908
+ /** The column, when the dialect names one instead of (or beside) a constraint. */
1909
+ readonly column?: string;
1910
+ }
1911
+
1912
+ /**
1913
+ * Which integrity rule the database refused on.
1914
+ *
1915
+ * `foreignKey` and `foreignKeyInUse` are the two DIRECTIONS of one constraint
1916
+ * and mean opposite things to a caller — "the row you pointed at does not
1917
+ * exist" vs "you may not remove this row, others still point at it". A UI
1918
+ * branches differently on each, so they are separate members rather than one
1919
+ * member plus a string a caller would have to grep.
1920
+ */
1921
+ export declare type ConstraintKind = 'foreignKey' | 'foreignKeyInUse' | 'unique' | 'notNull' | 'check';
1922
+
1868
1923
  /** Case-insensitive substring match on a text column. Non-indexable —
1869
1924
  * always evaluated as an unindexed leaf (the matcher's pre-filter only
1870
1925
  * recognizes eq/in/range). Lowers to `ILIKE '%…%'` on Postgres. */
@@ -2803,7 +2858,7 @@ declare type EmptyMerge = unknown;
2803
2858
  *
2804
2859
  * It was three. The store wrote `encrypt(JSON.stringify(v))`, the escape hatch
2805
2860
  * wrote `encrypt(v)`, and the backfill wrote `encrypt(v)`. All three produce the
2806
- * `enc:v1:` envelope and NOTHING distinguishes them. A consumer wrote session
2861
+ * `enc:v1:` envelope and NOTHING distinguishes them. A deployment wrote session
2807
2862
  * rows through the escape hatch and read them through the store, which threw
2808
2863
  * `FieldDecryptionError` blaming the key — the key was right; the ENCODING was
2809
2864
  * not. Eight of their ten session rows, and the failure looked like a key
@@ -2970,6 +3025,8 @@ export declare const expires: () => MixinDefinition<{
2970
3025
  */
2971
3026
  export declare const externalChangeEvent: (event: ChangeEvent, attribution?: Partial<ChangeEventMeta>) => ChangeEvent;
2972
3027
 
3028
+ export declare const extractDbCause: (err: unknown) => Record<string, unknown>;
3029
+
2973
3030
  /** The subset of `@voltro/logger`'s logger this module needs. Structural so
2974
3031
  * this package keeps its browser-safe import surface. */
2975
3032
  export declare interface FallbackLogger {
@@ -3499,7 +3556,7 @@ export declare const isBranchNamespace: (namespace: string) => boolean;
3499
3556
  * `isRelationsSpec` rejects it (registering nothing has no meaning), which is
3500
3557
  * right, but the caller then cannot tell it apart from a module with no
3501
3558
  * `relations(...)` export at all. `voltro dev` reported both as *"no relations(...)
3502
- * export found"*, and a consumer with 368 relations files got exactly one warning
3559
+ * export found"*, and a deployment with 368 relations files got exactly one warning
3503
3560
  * pointing at a file that HAS the export:
3504
3561
  *
3505
3562
  * export const easyProxyIntegrationsRelations = relations(easyProxyIntegrations, ({ one }) => ({
@@ -3518,6 +3575,15 @@ export declare const isFieldDecryptionError: (e: unknown) => e is FieldDecryptio
3518
3575
  /** Type-guard for the discovery walker. */
3519
3576
  export declare const isFileMigration: (value: unknown) => value is FileMigration;
3520
3577
 
3578
+ /**
3579
+ * Foreign-key violation, either direction, across every dialect. Derived from
3580
+ * `classifyConstraintViolation` rather than re-listing the codes: this
3581
+ * predicate and the classifier disagreeing is invisible until a tenant-FK
3582
+ * failure quietly stops being typed, and mssql already spent a release in
3583
+ * exactly that state (547 was read off `.code`, where it never appears).
3584
+ */
3585
+ export declare const isForeignKeyViolation: (dbCause: Record<string, unknown>) => boolean;
3586
+
3521
3587
  /** True when `name` is a live table the framework / its runtime engines create
3522
3588
  * but no user schema declares — never plan a drop for it. */
3523
3589
  export declare const isFrameworkOwnedLiveTable: (name: string) => boolean;
@@ -3531,6 +3597,18 @@ export declare const isLocalFirst: (table: LocalFirstMarked) => boolean;
3531
3597
 
3532
3598
  export declare const isMigrationDefinition: (value: unknown) => value is MigrationDefinition;
3533
3599
 
3600
+ /**
3601
+ * MySQL and MariaDB. They share a driver, a quoting style and most of a
3602
+ * grammar — and they are NOT interchangeable in DDL.
3603
+ *
3604
+ * The one that has already cost us: **MySQL/InnoDB silently discards a
3605
+ * column-inline `REFERENCES` clause** (parsed, thrown away, no warning, CREATE
3606
+ * succeeds), while MariaDB honours it and creates the constraint. Measured on
3607
+ * mysql 8.4 and mariadb 11. Anything referential that this family emits has to
3608
+ * be table-level to work on both.
3609
+ */
3610
+ export declare const isMysqlFamily: (dialect: DialectId) => boolean;
3611
+
3534
3612
  /**
3535
3613
  * True when the connection string points at a Neon serverless-postgres host
3536
3614
  * (`*.neon.tech` / `*.neon.build`, including the `-pooler` endpoints). Neon's
@@ -3546,6 +3624,15 @@ export declare const isNull: <RowOf = Record<string, unknown>, K extends keyof R
3546
3624
 
3547
3625
  export declare const isPrimaryKeyConflictError: (err: unknown) => err is PrimaryKeyConflictError;
3548
3626
 
3627
+ /** A query cancelled for exceeding the `statementTimeoutMs` deadline, across
3628
+ * dialects: pg `57014` (query_canceled — what `statement_timeout` raises),
3629
+ * MySQL `3024` (ER_QUERY_TIMEOUT), MariaDB `1969` (ER_STATEMENT_TIMEOUT), mssql
3630
+ * `ETIMEOUT` (tedious request timeout), sqlite `SQLITE_INTERRUPT`. NOT a
3631
+ * transient error — a re-run just repeats the runaway, so it must not retry
3632
+ * (`servePipeline`'s transient set deliberately excludes it). Lets a deployment /
3633
+ * observability layer name the failure instead of reading an opaque SqlError. */
3634
+ export declare const isQueryTimeout: (dbCause: Record<string, unknown>) => boolean;
3635
+
3549
3636
  /** Is this ciphertext in the OLD raw encoding? Used by `voltro db encrypt-column`
3550
3637
  * to normalise a column without touching rows that are already current. Answers
3551
3638
  * `undefined` when the two forms coincide (a numeric column, or a value that
@@ -3838,7 +3925,7 @@ export declare interface ManyToManyRelation {
3838
3925
  * the dangerous one: an empty `IN ()` gets dropped, and a DROPPED predicate does
3839
3926
  * not narrow — it WIDENS, to the whole tenant. Here it is safe (the SQL
3840
3927
  * compiler emits `FALSE`, the in-memory evaluator returns false, and both are
3841
- * pinned by tests) but an adopting team wrote `eq('id', '')` instead, because
3928
+ * pinned by tests) but an adopting project wrote `eq('id', '')` instead, because
3842
3929
  * they could not tell from the outside and would not bet a visibility rule on
3843
3930
  * it. They were right not to.
3844
3931
  *
@@ -5347,7 +5434,7 @@ export declare const registerPendingAttribution: (key: string, attribution: Writ
5347
5434
  * Register a single `relations()` spec. Multiple specs on the same
5348
5435
  * source MERGE — a second spec adds to the first one. A name
5349
5436
  * collision between two DIFFERENT specs throws here so the offending
5350
- * declaration site is the error scope, not a downstream consumer.
5437
+ * declaration site is the error scope, not a downstream deployment.
5351
5438
  *
5352
5439
  * Re-registering the SAME relation object is a no-op, mirroring
5353
5440
  * `registerTable`'s `existing === table` tolerance — and for the same reason:
@@ -5374,7 +5461,7 @@ export declare const registerRelations: (spec: RelationsSpec) => void;
5374
5461
  *
5375
5462
  * ── Two registrations for one table used to be a silent last-write-wins ─────
5376
5463
  *
5377
- * A consumer registered `_voltro_schedule_claims` at 1 hour from a startup, and
5464
+ * A deployment registered `_voltro_schedule_claims` at 1 hour from a startup, and
5378
5465
  * one second later the framework registered its own default for the same table.
5379
5466
  * Ours won, nothing said so, and their startup went on logging `bounded to 1h`
5380
5467
  * at every boot while the table kept everything younger than the framework's
@@ -5665,7 +5752,7 @@ export declare interface RetentionSpec {
5665
5752
  * The environment variable that changes this TTL, for the boot announcement.
5666
5753
  *
5667
5754
  * A standing DELETE whose only control is a variable you have to already know
5668
- * the name of is how a consumer lost 1 944 freshly-migrated rows to a 180-day
5755
+ * the name of is how a deployment lost 1 944 freshly-migrated rows to a 180-day
5669
5756
  * default — and then got it wrong a second time by setting it in a running pod
5670
5757
  * rather than in a file, where the next deploy would have reverted it. Naming
5671
5758
  * it in the boot line is what turns "you have to know" into "you were told".
@@ -5859,7 +5946,7 @@ export declare type SchemaChangeSeedHook = (event: SchemaChangeSeedEvent) => Pro
5859
5946
  * inferred {@link Table} generic pulls its method signatures into the emitted
5860
5947
  * `.d.ts`, and those reference the private `ColumnBuilder` class — which
5861
5948
  * TypeScript refuses to name across the boundary (TS4094). `SchemaTable`
5862
- * exposes only the plain-data surface a consumer of a contributed table
5949
+ * exposes only the plain-data surface a deployment of a contributed table
5863
5950
  * actually reads (`.tableName`, `.fields`, `.appliedIndexes`), so it names
5864
5951
  * cleanly and stays browser-safe (pure interfaces, no builder, no server
5865
5952
  * imports). A concrete `Table<...>` is assignable to it.
@@ -6006,7 +6093,7 @@ export declare interface SeedStore {
6006
6093
  * The primitive an idempotent restore is actually built on: `upsertByUnique`
6007
6094
  * costs a read per row and OVERWRITES what it finds, which is wrong when the
6008
6095
  * live row is newer than the snapshot. This is one statement per row and
6009
- * leaves an existing row alone. Reported by a consumer restoring 1361 rows
6096
+ * leaves an existing row alone. Reported from a real deployment restoring 1361 rows
6010
6097
  * across 167 tables in multiple passes for FK order — with only
6011
6098
  * `upsertByUnique` available, every pass re-read and re-wrote everything.
6012
6099
  */
@@ -6255,7 +6342,7 @@ export declare interface StreamTableOptions {
6255
6342
  readonly where?: Predicate;
6256
6343
  /** Rows per DB page. Larger = fewer round-trips, more per-page memory.
6257
6344
  * Default 1000. This is the READ page size; it is independent of any
6258
- * downstream write/checkpoint chunking a consumer layers on. */
6345
+ * downstream write/checkpoint chunking a deployment layers on. */
6259
6346
  readonly chunkSize?: number;
6260
6347
  /** Pass-through of the reactive scope opt-outs. For a faithful physical
6261
6348
  * export use the RAW dialect store (no scoping applied at all); these
@@ -7537,7 +7624,7 @@ export declare interface WindowSpec {
7537
7624
  * request behind this write", so the result does not look like a defect; it looks
7538
7625
  * like a schedule. In a compliance trail that asymmetry is the whole problem.
7539
7626
  *
7540
- * A consumer could not reproduce it across 2700 writes at 96-way concurrency with
7627
+ * A deployment could not reproduce it across 2700 writes at 96-way concurrency with
7541
7628
  * every core saturated — which is good evidence the window is narrow at their load
7542
7629
  * and no evidence at all that it is closed. Their own framing is why this is being
7543
7630
  * closed structurally rather than left: *"impossible beats unlikely when the