@voltro/sql-sqlite 0.19.0 → 0.20.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/CHANGELOG.md CHANGED
@@ -39,6 +39,93 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.20.0] — 2026-07-29
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/plugin-versioning, @voltro/database, @voltro/voltro, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-sqlite, @voltro/sql-mssql** — `versioningPlugin({ timing: 'in-transaction' })` produced a WRONG trail, not merely a slow one. Reported and reproduced against MariaDB 11 by a team that wired both plugins and measured before migrating a single call site.
47
+
48
+ **It recorded every change twice.** The two timings are alternatives, but the post-commit change tap stayed wired when the in-transaction recorder was registered, so both ran. One `bookmarks.create` → two history rows.
49
+
50
+ **And the trail was mis-ordered, which is worse.** Each path numbered independently: one insert plus one update produced versions `0, 0, 1, 2` across four rows. `selectAsOf`, `sortHistory` and `diffVersionRows` all read `version`, so `rowAsOf` returned the wrong snapshot and `diffVersions` found nothing. A duplicate can be deduped; a wrong order cannot be detected from the data.
51
+
52
+ The recorder wrote a constant `version: 0` on purpose, with a design note arguing that ordering could come from `changedAt` and that a read per covered write was too expensive. Both halves were wrong: `changedAt` is millisecond-resolution, so two writes to one row inside one transaction tie routinely, and the number is what every reader consults.
53
+
54
+ **BREAKING —** a `WriteRecorder` now receives a PORT (`{ append, maxOf }`) rather than a bare `append`. `maxOf` is one aggregate with an equality filter on the connection the write already holds; it is what lets an append-only trail number its own entries. A recorder still cannot UPDATE, DELETE or open a nested transaction, and a throw from either operation still rolls the caller's write back. Apps that merely ENABLE the timing need no change — only a hand-written recorder does, and `tsc` names every site.
55
+
56
+ **Cost, stated rather than avoided:** `'in-transaction'` now takes TWO round-trips per recorded write, roughly doubling this timing's published per-write overhead. Both timings number from 1, so switching `timing` no longer shifts version numbers.
57
+
58
+ ### Fixed
59
+
60
+ - **@voltro/database, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-sqlite, @voltro/sql-mssql** — The correlation bridge did not survive a transaction, and did not survive CDC. Both are fixed, and both were found by measuring against live databases after a consumer isolated the symptom in a scratch app.
61
+
62
+ **Every write a framework mutation makes was unattributed.** `transactional()` is entered from the request's async-local scope, but its callback runs from inside the Effect the store builds — and measured against live postgres AND live mariadb, the scope is active at the call site and EMPTY inside the callback. Framework mutations are auto-transactional, so this was every handler write. Same class as the `bindMutation` defect fixed alongside it: a scope covering the construction of an Effect and not its execution. The caller's attribution is now captured at `transactional()` entry and re-established around the callback, in all four dialect stores.
63
+
64
+ **And the CDC transports could not carry it at all.** Under `changeStrategy: 'cdc'` — the DEFAULT — the event a subscriber receives is rebuilt from a postgres NOTIFY payload or a mysql binlog row image, neither of which can hold a request context. `registerPendingAttribution` / `claimPendingAttribution` (`@voltro/database`) let the write path hand its identity to the echo, keyed by `(table, op, id)` and claimed once. A write made on ANOTHER replica has nothing pending and stays unattributed, which is the correct answer rather than a gap.
65
+
66
+ **Plus one nobody had reported, found on the way:** on postgres under CDC the write path skipped `routeEvent` entirely, and `runWriteRecorders` lives inside it — so `versioningPlugin({ timing: 'in-transaction' })` with the default `CDC=1` recorded NOTHING. The mode whose entire promise is "if the change committed, the entry is there" wrote an empty trail, silently. `routeEvent` now runs in both modes; only the DELIVERY decision is strategy-dependent.
67
+
68
+ New live-dialect suites (`cdcAttribution.integration.test.ts` in `sql-postgres` and `sql-mysql`) pin all of it, and were verified red against the previous code.
69
+ - **@voltro/plugin-versioning, @voltro/runtime, @voltro/cli** — `_voltro_row_history.traceId` and `.subjectId` were NULL on every write. Three independent causes, all found from one consumer report whose evidence pinned the diagnosis before we looked: `subjectId` was NULL while `changedBy` on the SAME row carried the acting user — so the identity was known and was not travelling.
70
+
71
+ - **The adapter dropped them.** `dataStoreHistoryStore.append` hand-wrote its insert object and listed `changedBy` but not `traceId` / `subjectId`. This is the second time that shape has bitten in this file — the READ side (`rowToVersion`) had drifted identically. A row built by hand in one place and read by hand in another disagree exactly when a field is ADDED, because nothing fails. Both now spread the row. - **An Effect-returning handler was unattributed.** `bindMutation` established the scope around the CALL, which covers an async executor for its whole run — but an Effect-returning one is only CONSTRUCTED there and runs later. It is now forked inside the scope, with interruption and typed failures preserved (both pinned by tests). Verified by measurement, not assumption: an Effect forked inside an ALS scope keeps seeing it across `sleep`, `yieldNow` and a `setTimeout` promise, while the same effect merely constructed inside sees nothing. - **The devtools `/invoke` path never entered the scope at all.** It bypasses the rpc stack by design, and that also bypassed everything `bindMutation` sets up. The audit plugin recorded a traceId (it reads `requestContext.traceId`, which this path does build) while every write underneath carried none — three consumers of one call disagreeing about its trace. It also synthesised `inspect-<8 random chars>`, which no trace consumer can parse; the reporter's framing is the rule worth keeping — *a synthesised id produces a column that looks joinable and is not; NULL at least fails honestly.* It is a real 32-hex id now, and it reaches all three sinks.
72
+
73
+ `actingUserId` is imported at the new call site rather than re-derived — one answer to "who is writing", shared with what `audit()` stamps.
74
+ - **@voltro/cli** — Three ways a check reported nothing while checking nothing, all found by a consumer verifying the silence instead of trusting it.
75
+
76
+ - **`unexercised-row-filter` never fired on a typed registration.** The match was `/\bsetRowFilter\s*\(/`, which demands the paren directly after the name, so `setRowFilter<Ctx>({…})` — the spelling our own generic signature invites — broke it. The rule was blind for exactly the teams that had wired `load`/`predicate` carefully. It counts CALLS now. - **…and its test-side condition was satisfiable by a COMMENT.** It matched `rowFilter:` in raw text. Comments and string literals are stripped, and the suite must both call `makeTestContext` and bind `rowFilter` in real code. - **The same paren-adjacent shape sat in two shipped codemod gates.** `0.7.0/01` (row filter) and `0.7.0/02` (`invoke`) both gate on a generic export, so a typed call made `voltro update` print nothing at all: the upgrade reads as clean and the behaviour change lands unread. Both now use the shared `callPattern`, which allows type arguments including nested ones.
77
+
78
+ Two false-positive fixes in the hand-roll detector, from the same report:
79
+
80
+ - **A file that WIRES a plugin is no longer told to adopt it.** The `presence` rule reported `app.config.ts` (which calls `presencePlugin()`) to an app that had just deleted its hand-rolled table. Rules that recommend a package now declare it, and a file referencing that package is skipped. - **Generated files and `.d.ts` are out of the scan.** A recommendation aimed at a file the next boot overwrites is never actionable.
81
+
82
+ And one more of the first kind, found while checking why a withdrawn report's probe had stayed silent: `raw-fetch` counted only a BARE `fetch(…)` callee, so `globalThis.fetch(url)` / `self.fetch(url)` in a server file read as clean.
83
+ - **@voltro/cli** — `voltro doctor`'s `serverOnly: NOT CHECKED` line now names the failure, and the field exists in `--json`.
84
+
85
+ The refusal to claim a pass was right. What shipped with it was nothing to act on: the `catch` discarded the error entirely, so there was no reason, no failing module, and — because the field was absent from `--json` — no way for CI to assert "still unchecked" rather than reading silence as a pass.
86
+
87
+ A consumer's verdict, which is the useful part: *"The message is honest and that is the problem."* They had already verified that every descriptor, `app.config.ts` and the generated rpc group imported cleanly under `tsx` on their own, so the difference had to be in what `loadDiscovered` does BEYOND importing — and none of that was visible from outside. It matters more than its size because `.serverOnly()` is what guards their `sessions.tokenHash` and `apiKeys.keyHash`, markers they added after finding a query whose output schema shipped a hash over the wire.
88
+
89
+ `--json` now carries `serverOnly: { checked, reason?, leaks? }`. Gate CI on `checked === false`.
90
+ - **@voltro/plugin-versioning** — A version snapshot no longer copies `.serverOnly()` columns into `_voltro_row_history`. `.encrypted()` columns are KEPT, and that distinction is the whole finding.
91
+
92
+ Reported by a team choosing which tables to version: `sessions` holds `.encrypted()` PATs and a `tokenHash`, `apiKeys` holds a `keyHash`, and they could not determine from outside what the snapshot would contain. They excluded both tables — then went and measured it, which corrected their own report:
93
+
94
+ ```
95
+ probeItems.secret enc:v1:a56iziEV9THLhzmJ:Vk0ux+0bECleTLBJkCa0Rg==:3AtMwP…
96
+ _voltro_row_history {"secret":"enc:v1:a56iziEV9THLhzmJ:Vk0ux+0bECleTLBJkCa0Rg==:…"}
97
+ ```
98
+
99
+ **`.encrypted()` lands as ciphertext, byte-identical to the source column**, so versioning such a table widens nothing — the history is exactly as readable as the row it came from. Withholding it would have cost real audit data to prevent an exposure that does not exist.
100
+
101
+ **`.serverOnly()` is withheld**, and the reason is not "a second copy under different retention" — that argument is weak on its own, since the hash already sits in the source table. The decisive one: `crud.*` STRIPS `.serverOnly()` columns from every row it returns, and a snapshot would smuggle the same value back past that stripping inside a `json()` blob, where no column-level rule applies.
102
+
103
+ Withheld names are listed under `data._omitted`, so a reader can tell "this column was withheld" from "this column did not exist then". Both timings apply the same policy. `.sensitive()` is not involved: it is an export-masking marker for values that are legitimately readable in the app.
104
+ - **@voltro/cli** — A page that RE-EXPORTS its component (`export { default, renderMode } from '../page'`) no longer fails the codegen gate with "exports no default". The check required the literal words `as default`, so the one spelling that lets two routes share a screen without copying it was the one spelling it refused — and it refused in `voltro build`, while dev and tests stayed green because nothing prerenders there. `export { default as Screen }` is still correctly rejected: it renames the default away.
105
+
106
+ Two follow-ons from the same shape:
107
+
108
+ - The refusal message said the file "ends in `.page.tsx`" and offered "drop the `.page` suffix" as a fix. That is the 0.15.0 convention, replaced by directory routing in 0.17.0 — it named a convention that no longer exists and a fix that could not work. It now names `page.tsx` and both real fixes. - `scanRenderProfile` read a forwarded `renderMode` as absent and fell back to `'static'`, so `staticSafe` and the deploy-target classification could call an app CDN-deployable with an `ssr` route in it. The forward is now followed (relative specifiers, depth-capped); an unresolvable one still falls back rather than failing the scan.
109
+ - **@voltro/database** — `VOLTRO_SOFT_DROP=1` could never converge. The applier renames the object to `<name>__dropped_<ts>` instead of dropping it, which leaves it undeclared — and the differ read that as one more forgotten table, planning the drop again. The re-plan inside `applyPlan` then found an operation still outstanding and aborted with "the DDL for these operations is a no-op — this is a framework bug", which was a wrong diagnosis of a real defect: the DDL had worked. No fingerprint was recorded, so the migration counted as unapplied and every later `db apply` / boot hit the same wall. The only exit was a hard drop of the snapshot — exactly the recoverability the flag is chosen for.
110
+
111
+ The planner now treats `<name>__dropped_<YYYYMMDDHHMMSS>` as framework-managed, alongside `_voltro_*` / `cluster_*`. Deliberately not retention-aware: a planner whose output depends on the clock would produce different plans before and after midnight, and `db gc-snapshots` already owns expiry.
112
+
113
+ Reported against tables; the same defect existed one level down for soft-dropped COLUMNS, where it was worse — a re-planned `drop-column` carries no `dropped()` marker and so refuses to plan at all. Both are fixed.
114
+
115
+ The convergence message itself no longer asserts a cause it cannot know. It said "the DDL for these operations is a no-op", which was flatly wrong here and sent the reporter looking for dead DDL. It now names both causes — no-op DDL, and a planner that cannot see what the DDL did — and says which one an operation naming a just-renamed object usually is.
116
+
117
+ ### Internal (no consumer-facing effect)
118
+
119
+ - **The `0.20.0/01_write-recorder-port` codemod gains the gate test its two predecessors have.**
120
+
121
+ `codemodRegistry.test.ts` asserts that every `*.codemod.ts` on disk is registered and that ids are unique — registration, not behaviour. What it cannot see is the one way a `manual` codemod fails in practice: an `appliesTo` that is too broad, so the note prints for projects that have nothing to do. That is not a cosmetic problem. A note which fires on every app is how readers learn to skip notes, and the next one carries a boot refusal.
122
+
123
+ This codemod is the case where the silent direction matters most. The break is a TYPE error, so `tsc` already names every affected site; the note exists only to explain `maxOf`, which the compiler cannot. Apps that merely ENABLE `timing: 'in-transaction'` need to do nothing — `plugin-versioning` ships the recorder and it is already updated — and they are the large majority.
124
+
125
+ Four cases, covering both directions: a project registering its own recorder (note prints, and names `{ append }`, `maxOf`, and the `null`-is-not-zero distinction that a hand-written sequence gets wrong), an app that only enables the timing (silent), the identifier in a comment or a string (silent), and the generic call form `registerWriteRecorder<Row>(…)`, which `callPattern` admits and a naive match would miss.
126
+
127
+ ---
128
+
42
129
  ## [0.19.0] — 2026-07-29
43
130
 
44
131
  ### ⚠ BREAKING
package/dist/index.d.ts CHANGED
@@ -102,6 +102,12 @@ export declare class SqliteStore implements DataStore {
102
102
  * or rolls back with the write it records.
103
103
  */
104
104
  private appendInTxn;
105
+ /**
106
+ * The other half of the recorder port: ONE aggregate on the caller's
107
+ * connection, so an append-only trail can number its own entries. Equality
108
+ * filter only — see `writeRecorder.ts` for why it is this narrow.
109
+ */
110
+ private maxInTxn;
105
111
  private routeEvent;
106
112
  private executeUpsert;
107
113
  private executeInsertIgnore;
package/dist/index.js CHANGED
@@ -3,12 +3,12 @@ import { Config as t, Context as n, Effect as r, Layer as i, ManagedRuntime as a
3
3
  import { EventEmitter as c } from "node:events";
4
4
  import { createLogger as l } from "@voltro/logger";
5
5
  import { SqlClient as u, TransactionConnection as d } from "@effect/sql/SqlClient";
6
- import { EagerCardinalityError as f, attachEagerLoads as p, attributionFields as m, compileEagerJson as h, compilePredicate as g, compileRawFragment as _, compileSelect as v, decodeRowsFromSchema as y, hasEagerLoads as b, isTableReactive as x, qualifyTable as S, raiseChangeListenerCeiling as C, recordsTable as w, requireTable as T, runWriteRecorders as E, stampGeneratedId as D, stampGeneratedIds as O } from "@voltro/database";
6
+ import { EagerCardinalityError as f, attachEagerLoads as p, attributionFields as m, compileEagerJson as h, compilePredicate as g, compileRawFragment as _, compileSelect as v, currentWriteAttribution as y, decodeRowsFromSchema as b, hasEagerLoads as x, isTableReactive as S, qualifyTable as C, raiseChangeListenerCeiling as w, recordsTable as T, requireTable as E, runWithWriteAttribution as D, runWriteRecorders as O, stampGeneratedId as k, stampGeneratedIds as A } from "@voltro/database";
7
7
  //#region src/sqlLayer.ts
8
- var k = (r) => e.layerConfig({
8
+ var j = (r) => e.layerConfig({
9
9
  filename: t.succeed(r.filename),
10
10
  ...r.readonly === void 0 ? {} : { readonly: t.succeed(r.readonly) }
11
- }).pipe(i.tap((t) => n.get(t, e.SqliteClient)`PRAGMA foreign_keys = ON`)), A = (e) => {
11
+ }).pipe(i.tap((t) => n.get(t, e.SqliteClient)`PRAGMA foreign_keys = ON`)), M = (e) => {
12
12
  if (e.url) {
13
13
  if (e.url === ":memory:") return { filename: ":memory:" };
14
14
  if (e.url.startsWith("file:")) return { filename: e.url.slice(5) };
@@ -16,12 +16,12 @@ var k = (r) => e.layerConfig({
16
16
  }
17
17
  if (e.database) return { filename: e.database };
18
18
  throw Error("@voltro/sql-sqlite: no filename supplied. Set DB_URL=file:./db.sqlite or DB_DATABASE=path/to/db.sqlite.");
19
- }, j = (e) => k(A(e)), M = /* @__PURE__ */ new Set([
19
+ }, N = (e) => j(M(e)), P = /* @__PURE__ */ new Set([
20
20
  "SQLITE_BUSY",
21
21
  "SQLITE_LOCKED",
22
22
  "5",
23
23
  "6"
24
- ]), N = (e) => {
24
+ ]), F = (e) => {
25
25
  let t = e;
26
26
  for (let e = 0; e < 5 && typeof t == "object" && t; e++) {
27
27
  let e = t.code;
@@ -29,18 +29,21 @@ var k = (r) => e.layerConfig({
29
29
  if (typeof e == "number") return String(e);
30
30
  t = t.cause;
31
31
  }
32
- }, P = (e) => {
33
- let t = N(e);
34
- return t !== void 0 && M.has(t);
35
- }, F = (e) => P(e) ? "retry" : "noRetry", I = l({ scope: "voltro:sqlite" }), L = async (e) => {
32
+ }, I = (e) => {
33
+ let t = F(e);
34
+ return t !== void 0 && P.has(t);
35
+ }, L = (e) => I(e) ? "retry" : "noRetry", R = l({ scope: "voltro:sqlite" }), z = async (e) => {
36
36
  let t = e.tracerLayer ? i.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, n = a.make(t);
37
- return new B(await n.runPromise(u), n, null, void 0, e.isRetryable ?? P, e.systemName ?? "sqlite", e.wrapTransaction);
38
- }, R = (e) => e, z = (e) => {
37
+ return new U(await n.runPromise(u), n, null, void 0, e.isRetryable ?? I, e.systemName ?? "sqlite", e.wrapTransaction);
38
+ }, B = (e) => e, V = (e) => {
39
39
  if (typeof e != "object" || !e) return e;
40
40
  let t = {};
41
41
  for (let [n, r] of Object.entries(e)) r instanceof Date ? t[n] = r.toISOString() : typeof r == "boolean" ? t[n] = +!!r : t[n] = r;
42
42
  return t;
43
- }, B = class e {
43
+ }, H = () => {
44
+ let e = y();
45
+ return e === void 0 ? (e) => e() : (t) => D(e, t);
46
+ }, U = class e {
44
47
  sql;
45
48
  runtime;
46
49
  namespace;
@@ -49,14 +52,14 @@ var k = (r) => e.layerConfig({
49
52
  wrapTransaction;
50
53
  emitter;
51
54
  inflightTxns = 0;
52
- constructor(e, t, n = null, r, i = P, a = "sqlite", o = R) {
53
- this.sql = e, this.runtime = t, this.namespace = n, this.isRetryable = i, this.systemName = a, this.wrapTransaction = o, this.emitter = r ?? new c(), C(this.emitter);
55
+ constructor(e, t, n = null, r, i = I, a = "sqlite", o = B) {
56
+ this.sql = e, this.runtime = t, this.namespace = n, this.isRetryable = i, this.systemName = a, this.wrapTransaction = o, this.emitter = r ?? new c(), w(this.emitter);
54
57
  }
55
58
  withNamespace(t) {
56
59
  return t === this.namespace ? this : new e(this.sql, this.runtime, t, this.emitter, this.isRetryable, this.systemName, this.wrapTransaction);
57
60
  }
58
61
  nsT(e) {
59
- return S(this.namespace, e);
62
+ return C(this.namespace, e);
60
63
  }
61
64
  async attachNamespace(e, t) {
62
65
  let n = this.sql, r = t ?? `${e}.db`;
@@ -64,11 +67,11 @@ var k = (r) => e.layerConfig({
64
67
  }
65
68
  async executeQuery(e, t) {
66
69
  let n = v(e, this.sql, this.namespace), i = t ? r.provideService(n, d, t) : n;
67
- return y(await this.runtime.runPromise(i), e.table, "sqlite");
70
+ return b(await this.runtime.runPromise(i), e.table, "sqlite");
68
71
  }
69
72
  async executeInsert(e, t, n, i) {
70
- t = D(e, t);
71
- let a = this.sql, o = a`INSERT INTO ${a(this.nsT(e))} ${a.insert(z(t))} RETURNING *`, s = n ? r.provideService(o, d, n) : o, c = (await this.runtime.runPromise(s))[0];
73
+ t = k(e, t);
74
+ let a = this.sql, o = a`INSERT INTO ${a(this.nsT(e))} ${a.insert(V(t))} RETURNING *`, s = n ? r.provideService(o, d, n) : o, c = (await this.runtime.runPromise(s))[0];
72
75
  if (!c) throw Error(`SqliteStore.insert: no row returned for table '${e}'`);
73
76
  return await this.routeEvent({
74
77
  table: e,
@@ -78,7 +81,7 @@ var k = (r) => e.layerConfig({
78
81
  }, i, n), c;
79
82
  }
80
83
  async executeUpdate(e, t, n, i, a) {
81
- let o = this.sql, s = o`UPDATE ${o(this.nsT(e))} SET ${o.update(z(n))} WHERE ${o("id")} = ${t} RETURNING *`, c = i ? r.provideService(s, d, i) : s, l = (await this.runtime.runPromise(c))[0];
84
+ let o = this.sql, s = o`UPDATE ${o(this.nsT(e))} SET ${o.update(V(n))} WHERE ${o("id")} = ${t} RETURNING *`, c = i ? r.provideService(s, d, i) : s, l = (await this.runtime.runPromise(c))[0];
82
85
  return l ? (await this.routeEvent({
83
86
  table: e,
84
87
  op: "update",
@@ -87,8 +90,8 @@ var k = (r) => e.layerConfig({
87
90
  }, a, i), l) : null;
88
91
  }
89
92
  async executeInsertMany(e, t, n, i) {
90
- if (t = O(e, t), t.length === 0) return [];
91
- let a = this.sql, o = t.map((e) => z(e)), s = a`INSERT INTO ${a(this.nsT(e))} ${a.insert(o)} RETURNING *`, c = n ? r.provideService(s, d, n) : s, l = await this.runtime.runPromise(c);
93
+ if (t = A(e, t), t.length === 0) return [];
94
+ let a = this.sql, o = t.map((e) => V(e)), s = a`INSERT INTO ${a(this.nsT(e))} ${a.insert(o)} RETURNING *`, c = n ? r.provideService(s, d, n) : s, l = await this.runtime.runPromise(c);
92
95
  for (let t of l) await this.routeEvent({
93
96
  table: e,
94
97
  op: "insert",
@@ -116,28 +119,35 @@ var k = (r) => e.layerConfig({
116
119
  }, i, n), !0) : !1;
117
120
  }
118
121
  async appendInTxn(e, t, n) {
119
- let i = this.sql, a = i`INSERT INTO ${i(this.nsT(e))} ${i.insert(z(t))}`;
122
+ let i = this.sql, a = i`INSERT INTO ${i(this.nsT(e))} ${i.insert(V(t))}`;
120
123
  await this.runtime.runPromise(n ? r.provideService(a, d, n) : a);
121
124
  }
125
+ async maxInTxn(e, t, n, i) {
126
+ let a = this.sql, o = Object.entries(n).map(([e, t]) => a`${a(e)} = ${t}`), s = a`SELECT MAX(${a(t)}) AS ${a("m")} FROM ${a(this.nsT(e))} WHERE ${a.and(o)}`, c = (await this.runtime.runPromise(i ? r.provideService(s, d, i) : s))[0]?.m;
127
+ return c == null ? null : Number(c);
128
+ }
122
129
  async routeEvent(e, t, n = null) {
123
130
  e = {
124
131
  ...m(),
125
132
  ...e
126
- }, w(e.table) && await E((e, t) => this.appendInTxn(e, t, n), {
133
+ }, T(e.table) && await O({
134
+ append: (e, t) => this.appendInTxn(e, t, n),
135
+ maxOf: (e, t, r) => this.maxInTxn(e, t, r, n)
136
+ }, {
127
137
  table: e.table,
128
138
  op: e.op,
129
139
  next: e.new,
130
140
  prev: e.old,
131
141
  traceId: e.traceId,
132
142
  subjectId: e.subjectId
133
- }), x(e.table) && (t === null ? this.emitter.emit("change", e) : t.push(e));
143
+ }), S(e.table) && (t === null ? this.emitter.emit("change", e) : t.push(e));
134
144
  }
135
145
  async executeUpsert(e, t, n, i, a) {
136
146
  if (typeof n.update == "function") {
137
147
  let r = await this.findByConflict(e, t, n.conflictColumns, i);
138
148
  return r ? await this.executeUpdate(e, r.id, n.update(r), i, a) ?? r : this.executeInsert(e, t, i, a);
139
149
  }
140
- let o = this.sql, s = n.conflictColumns.map((e) => o`${o(e)}`), c = Object.keys(t).filter((e) => t[e] !== void 0), l = n.update === void 0 ? c.filter((e) => e !== "id" && !n.conflictColumns.includes(e)) : n.update, u = l.length > 0 ? o.csv(l.map((e) => o`${o(e)} = excluded.${o(e)}`)) : o`${o(n.conflictColumns[0])} = excluded.${o(n.conflictColumns[0])}`, f = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(z(t))} ON CONFLICT (${o.csv(s)}) DO UPDATE SET ${u} RETURNING *`, p = i ? r.provideService(f, d, i) : f, m = (await this.runtime.runPromise(p))[0];
150
+ let o = this.sql, s = n.conflictColumns.map((e) => o`${o(e)}`), c = Object.keys(t).filter((e) => t[e] !== void 0), l = n.update === void 0 ? c.filter((e) => e !== "id" && !n.conflictColumns.includes(e)) : n.update, u = l.length > 0 ? o.csv(l.map((e) => o`${o(e)} = excluded.${o(e)}`)) : o`${o(n.conflictColumns[0])} = excluded.${o(n.conflictColumns[0])}`, f = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(V(t))} ON CONFLICT (${o.csv(s)}) DO UPDATE SET ${u} RETURNING *`, p = i ? r.provideService(f, d, i) : f, m = (await this.runtime.runPromise(p))[0];
141
151
  if (!m) throw Error(`SqliteStore.upsert: no row returned for table '${e}'`);
142
152
  let h = t.id !== void 0 && t.id === m.id ? "insert" : "update";
143
153
  return await this.routeEvent({
@@ -148,8 +158,8 @@ var k = (r) => e.layerConfig({
148
158
  }, a, i), m;
149
159
  }
150
160
  async executeInsertIgnore(e, t, n, i, a) {
151
- t = D(e, t);
152
- let o = this.sql, s = n.conflictColumns.map((e) => o`${o(e)}`), c = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(z(t))} ON CONFLICT (${o.csv(s)}) DO NOTHING RETURNING *`, l = i ? r.provideService(c, d, i) : c, u = (await this.runtime.runPromise(l))[0];
161
+ t = k(e, t);
162
+ let o = this.sql, s = n.conflictColumns.map((e) => o`${o(e)}`), c = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(V(t))} ON CONFLICT (${o.csv(s)}) DO NOTHING RETURNING *`, l = i ? r.provideService(c, d, i) : c, u = (await this.runtime.runPromise(l))[0];
153
163
  if (u) return await this.routeEvent({
154
164
  table: e,
155
165
  op: "insert",
@@ -173,37 +183,37 @@ var k = (r) => e.layerConfig({
173
183
  return this.runtime.runPromise(n);
174
184
  }
175
185
  async runWithEager(e, t) {
176
- if (!b(e)) return this.executeQuery(e, t);
186
+ if (!x(e)) return this.executeQuery(e, t);
177
187
  let n = this.namespace === null ? h(e, this.sql, "sqlite") : null;
178
188
  if (n !== null) try {
179
189
  let e = t ? r.provideService(n.fragment, d, t) : n.fragment, i = await this.runtime.runPromise(e);
180
190
  return n.decode(i);
181
191
  } catch (e) {
182
192
  if (e instanceof f) throw e;
183
- I.warn("sqlite JSON-agg eager-load failed; falling back to walker", { err: e });
193
+ R.warn("sqlite JSON-agg eager-load failed; falling back to walker", { err: e });
184
194
  }
185
- return p(await this.executeQuery(e, t), e.eager, e.sourceTable ?? T(e.table), (e) => this.executeQuery(e, t));
195
+ return p(await this.executeQuery(e, t), e.eager, e.sourceTable ?? E(e.table), (e) => this.executeQuery(e, t));
186
196
  }
187
197
  getInternalRunWithEager() {
188
198
  return (e, t) => this.runWithEager(e, t);
189
199
  }
190
200
  insert(e, t) {
191
- return this.executeInsert(e, t, null, null);
201
+ return H()(() => this.executeInsert(e, t, null, null));
192
202
  }
193
203
  insertMany(e, t) {
194
- return this.executeInsertMany(e, t, null, null);
204
+ return H()(() => this.executeInsertMany(e, t, null, null));
195
205
  }
196
206
  patchJson(e, t, n, r) {
197
- return this.executePatchJson(e, t, n, r, null, null);
207
+ return H()(() => this.executePatchJson(e, t, n, r, null, null));
198
208
  }
199
209
  update(e, t, n) {
200
- return this.executeUpdate(e, t, n, null, null);
210
+ return H()(() => this.executeUpdate(e, t, n, null, null));
201
211
  }
202
212
  delete(e, t) {
203
- return this.executeDelete(e, t, null, null);
213
+ return H()(() => this.executeDelete(e, t, null, null));
204
214
  }
205
215
  async updateMany(e, t, n) {
206
- let r = this.sql, i = g(n.where, r, this.namespace), a = r`UPDATE ${r(this.nsT(e))} SET ${r.update(z(t))} WHERE ${i} RETURNING *`, o = await this.runtime.runPromise(a);
216
+ let r = this.sql, i = g(n.where, r, this.namespace), a = r`UPDATE ${r(this.nsT(e))} SET ${r.update(V(t))} WHERE ${i} RETURNING *`, o = await this.runtime.runPromise(a);
207
217
  for (let t of o) await this.routeEvent({
208
218
  table: e,
209
219
  op: "update",
@@ -223,10 +233,10 @@ var k = (r) => e.layerConfig({
223
233
  return a.length;
224
234
  }
225
235
  upsert(e, t, n) {
226
- return this.executeUpsert(e, t, n, null, null);
236
+ return H()(() => this.executeUpsert(e, t, n, null, null));
227
237
  }
228
238
  insertIgnore(e, t, n) {
229
- return this.executeInsertIgnore(e, t, n, null, null);
239
+ return H()(() => this.executeInsertIgnore(e, t, n, null, null));
230
240
  }
231
241
  emitChange(e) {
232
242
  this.emitter.emit("change", e);
@@ -257,26 +267,26 @@ var k = (r) => e.layerConfig({
257
267
  }
258
268
  async transactional(e) {
259
269
  this.inflightTxns++;
260
- let t = 0, n = r.suspend(() => {
261
- let n = ++t;
262
- return this.sql.withTransaction(r.flatMap(r.serviceOption(d), (t) => {
263
- if (o.isNone(t)) return r.fail(/* @__PURE__ */ Error("SqliteStore.transactional: TransactionConnection unexpectedly missing."));
264
- let i = new V(this, t.value);
270
+ let t = H(), n = 0, i = r.suspend(() => {
271
+ let i = ++n;
272
+ return this.sql.withTransaction(r.flatMap(r.serviceOption(d), (n) => {
273
+ if (o.isNone(n)) return r.fail(/* @__PURE__ */ Error("SqliteStore.transactional: TransactionConnection unexpectedly missing."));
274
+ let a = new W(this, n.value);
265
275
  return r.tryPromise({
266
- try: () => e(i).then((e) => ({
276
+ try: () => t(() => e(a)).then((e) => ({
267
277
  result: e,
268
- view: i,
269
- attempt: n
278
+ view: a,
279
+ attempt: i
270
280
  })),
271
281
  catch: (e) => e
272
282
  });
273
283
  })).pipe(r.catchAllDefect((e) => this.isRetryable(e) ? r.fail(e) : r.die(e)));
274
- }), i = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(this.isRetryable)), a = n.pipe(r.retry(i), r.withSpan("store.transactional", { attributes: {
284
+ }), a = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(this.isRetryable)), c = i.pipe(r.retry(a), r.withSpan("store.transactional", { attributes: {
275
285
  "db.system": this.systemName,
276
286
  "db.operation": "transaction"
277
287
  } }));
278
288
  try {
279
- let e = await this.runtime.runPromise(this.wrapTransaction(a));
289
+ let e = await this.runtime.runPromise(this.wrapTransaction(c));
280
290
  return e.view.commitEvents(), e.result;
281
291
  } finally {
282
292
  this.inflightTxns--;
@@ -288,7 +298,7 @@ var k = (r) => e.layerConfig({
288
298
  };
289
299
  }
290
300
  injectExternalChange(e) {
291
- x(e.table) && this.emitter.emit("change", {
301
+ S(e.table) && this.emitter.emit("change", {
292
302
  ...e,
293
303
  origin: "injected"
294
304
  });
@@ -300,7 +310,7 @@ var k = (r) => e.layerConfig({
300
310
  if (this.inflightTxns > 0) {
301
311
  let t = Date.now() + e;
302
312
  for (; this.inflightTxns > 0 && Date.now() < t;) await new Promise((e) => setTimeout(e, 25));
303
- this.inflightTxns > 0 && I.warn("close: grace period expired with in-flight transactions", {
313
+ this.inflightTxns > 0 && R.warn("close: grace period expired with in-flight transactions", {
304
314
  gracePeriodMs: e,
305
315
  inflight: this.inflightTxns
306
316
  });
@@ -310,7 +320,7 @@ var k = (r) => e.layerConfig({
310
320
  async ping() {
311
321
  await this.runtime.runPromise(this.sql`SELECT 1`);
312
322
  }
313
- }, V = class {
323
+ }, W = class {
314
324
  parent;
315
325
  txn;
316
326
  events = [];
@@ -382,12 +392,12 @@ var k = (r) => e.layerConfig({
382
392
  this.events.length = 0;
383
393
  }
384
394
  }
385
- }, H = {
395
+ }, G = {
386
396
  id: "sqlite",
387
- makeSqlLayer: (e) => j(e),
388
- makeStore: (e) => L(e),
397
+ makeSqlLayer: (e) => N(e),
398
+ makeStore: (e) => z(e),
389
399
  compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
390
- retryFilter: F
400
+ retryFilter: L
391
401
  };
392
402
  //#endregion
393
- export { e as SqliteClient, A as connectionFromConfig, k as makeSqliteSqlLayer, j as makeSqliteSqlLayerFromConfig, L as makeSqliteStore, H as sqliteDialect, F as sqliteRetryFilter };
403
+ export { e as SqliteClient, M as connectionFromConfig, j as makeSqliteSqlLayer, N as makeSqliteSqlLayerFromConfig, z as makeSqliteStore, G as sqliteDialect, L as sqliteRetryFilter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/sql-sqlite",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "SQLite dialect adapter for Voltro's cross-dialect DataStore (single-process; in-process change events).",
5
5
  "keywords": [
6
6
  "voltro",
@@ -34,8 +34,8 @@
34
34
  "dependencies": {
35
35
  "@effect/sql": "^0.51.1",
36
36
  "@effect/sql-sqlite-node": "^0.52.0",
37
- "@voltro/database": "0.19.0",
38
- "@voltro/logger": "0.19.0"
37
+ "@voltro/database": "0.20.0",
38
+ "@voltro/logger": "0.20.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "effect": "^3.21.4"