@voltro/sql-sqlite 0.43.1 → 0.44.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,71 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.44.0] — 2026-08-19
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/data-transfer, @voltro/cli** — `runImport` returns an `ImportOutcome` instead of the bundle's `Manifest`, and `--mode replace` runs as ONE transaction by default.
47
+
48
+ **Why the return type changed.** The summary line counted the rows the BUNDLE carries, not the rows the run wrote. Those differ most exactly where it matters: a bundle directory carries its own resume ledger, so a copied directory imports nothing — correctly, with a warning naming the file to delete — and the run then printed `import complete … rows: 242950` over a target it had not touched. The warning was one line above, which is one line too far for anyone piping the output through `tail -1`. The outcome carries `rowsWritten`, `rowsSkipped` and `fullyResumed`, the CLI reports written-vs-carried, and a fully-skipped run says so on its LAST line.
49
+
50
+ Migration: `runImport(...)` now resolves to `{ manifest, tablesWritten, rowsWritten, tablesSkipped, rowsSkipped, fullyResumed }`. Read `.manifest` where you read the manifest before.
51
+
52
+ **Why replace is atomic now.** The all-or-nothing guarantee was written for the emptying step, and read — reasonably — as covering the run. A replace that died partway through the LOAD left the target emptied of its old rows and holding part of the new ones, measured on a live instance over sixteen minutes. There is no useful state for a replace to stop in, so it is a default rather than a flag you have to know about. It also closes the window that produced that failure: with the tables emptied and the load uncommitted, a concurrent writer in the application waits instead of inserting a row the bundle is about to insert too.
53
+
54
+ `--no-atomic` (CLI) / `atomic: false` (API) opts out. The trade is stated where it bites: every write to those tables waits for the load, and on postgres the bulk `COPY` loader cannot join a transaction it does not own — an atomic run now says that once rather than being quietly slower.
55
+
56
+ Two smaller things from the same report: `--mode replace` against a running instance warns about the empty-target window when it is NOT atomic, and the `--target api` upload reports progress per chunk plus a line explaining that the final request stays open for the whole import — sixteen minutes of silence is indistinguishable from a hang, and one operator killed a run that had finished.
57
+
58
+ **`voltro update` carries you across this** — codemod `0.44.0/01_import-outcome`.
59
+
60
+ ### Fixed
61
+
62
+ - **@voltro/sql-mysql, @voltro/sql-postgres, @voltro/sql-sqlite, @voltro/sql-mssql, @voltro/data-transfer** — A write and its write-recorders now succeed or fail together on every SQL dialect, and the data importer's retry is idempotent.
63
+
64
+ A recorder (a versioning trail, an audit log) runs on the caller's connection and is ALLOWED to fail — a recorder that throws must take the write down with it, that is its contract. Outside a caller transaction the two were not one unit: the row's statement committed on its own, and the recorder's INSERT ran afterwards as a second autocommit statement. So a recorder that threw left a COMMITTED row behind a write that reported failure. Measured directly: `insert` throws, the row is in the table, and a second attempt at the same row is `ER_DUP_ENTRY` on PRIMARY.
65
+
66
+ Anything that retries a failed write then meets its own row. The data importer retries by design — it holds a row whose write failed and tries again once the remaining tables have streamed — so a `--mode replace` that had just emptied a table failed on a duplicate key IN that table. From outside, that is the impossible-looking thing: an import that emptied a table and then failed because something was already in it. Reproduced verbatim, including the table name and `[ER_DUP_ENTRY/1062]`, by putting the old retry back.
67
+
68
+ Both ends are closed, and they are independent on purpose:
69
+
70
+ - **The cause.** A table that HAS recorders writes inside a transaction now, so the row and the trail commit together or not at all — on mysql, mariadb, postgres, sqlite and mssql, verified by one suite that asks all five the same question. A table with no recorders — the default — takes the direct path unchanged; `recordsTable` is a Map-size check first, so it costs one comparison. - **The defence.** The importer's retry upserts instead of inserting in `replace` mode. That covers every OTHER way a write can land while reporting failure: a driver timeout on a write the server applied, a connection lost after the commit, a concurrent writer inserting the same key. It is sound precisely because the table was emptied by this same run — there is nothing in it that is not ours.
71
+
72
+ The test that pinned the divergence went red when the fix landed, exactly as its own note said it would, and is inverted with that note kept.
73
+
74
+ ---
75
+
76
+ ## [0.43.2] — 2026-08-18
77
+
78
+ ### Fixed
79
+
80
+ - **@voltro/sql-mysql, @voltro/database, @voltro/cli** — The mysql-family binlog reader no longer keeps a table excluded after the very migration that fixed it.
81
+
82
+ A UNIQUE on an unbounded text column is a MariaDB hash long-unique, whose hidden `DB_ROW_HASH_n` column the reader can never account for — so the table is held out of binlog capture and the exclusion is reported. `voltro dev` builds its store BEFORE it migrates (kv, cross-replica broadcast and the analytics mirror all need one), so on a boot whose own auto-migration bounds the column, that finding was drawn from a schema that stopped existing about a second later. The exclusion then outlived its cause for the life of the process, and the message — correct when written, and typically the only error line in the boot log — went on describing the pre-migration database.
83
+
84
+ Three changes:
85
+
86
+ - `DataStore.refreshChangeCaptureExclusions()` (optional; implemented by the mysql-family store) re-runs the probe and re-points the LIVE reader, in both directions — schema work that CREATES the condition now excludes the table immediately instead of after three failed writes. `voltro dev` calls it once, after all its schema work. `voltro serve` needs no equivalent: it builds its store after every schema step and never applies DDL itself. - The definitive message now states its own durability — that it is the schema as read at reader attach, and that applying the remedy does not by itself lift the exclusion. - On a boot that will re-check, the finding is reported as a provisional note rather than as a verdict, so a boot that fixes the condition leaves no error line about it. The note escalates to the full verdict on its own if the re-check never runs. - Two reader fixes the re-check depended on: applying a new exclusion set now WAITS for a reconnect already in flight (it is what applies the set, so resolving before it landed meant the caller's next write hit the old filter), and the reconnect loop no longer keeps resuming from an offset it has just jumped away from — that turned one purged offset into a reconnect every watchdog interval, forever, delivering nothing.
87
+ - **@voltro/data-transfer, @voltro/database, @voltro/sql-mysql, @voltro/sql-postgres, @voltro/sql-sqlite, @voltro/sql-mssql, @voltro/cli** — `voltro data import --mode replace` no longer leaves a target in neither state, and now works against schemas that have foreign keys.
88
+
89
+ The delete step ran table by table and stopped at the first refusal, so a run that could not finish left dozens of tables emptied and nothing loaded — and a second attempt destroyed more than the first, because it got further before hitting the same wall. The wall itself was not exotic: MySQL, MariaDB and SQL Server check a foreign key as each ROW is deleted, so a table that references ITSELF cannot be emptied by any ordering of the tables. `createdBy → actors` on the `actors` table is exactly that shape, and it is what an audit mixin on an actor table produces.
90
+
91
+ - `DataStore.emptyTables()` (per dialect) empties the whole set as one unit, in one transaction, with referential integrity suspended for the duration — `FOREIGN_KEY_CHECKS` on the mysql family, a multi-table `TRUNCATE` on postgres, `defer_foreign_keys` on sqlite, per-table `NOCHECK`/`WITH CHECK CHECK` on mssql. All-or-nothing on every engine, including under `--atomic`, where it runs on the transaction the import already holds. - A **pre-flight refusal**: if a table OUTSIDE the bundle holds rows referencing one inside it, the import refuses before deleting anything and names the tables, the columns and the row counts. Those rows cannot be restored from the bundle, so forcing it is not an option. An EMPTY outside table blocks nothing. - Table-level failures carry the driver's own reason and code, the way row-level failures already did. `truncate <table> failed: Failed to execute statement` fits every plausible cause equally; the classification that produced `foreign key <name>: … [ER_NO_REFERENCED_ROW_2/1452]` one level down now applies one level up. The word "truncate" is gone from the message too — the step issues DELETE, and naming a statement it does not run sends whoever reads it to reproduce the wrong thing. - A typed refusal reaching the `--target api` transport keeps its text: the admin import endpoint answers `409` with the reason instead of flattening it to `import failed`, on the one transport where the operator has no other way to see it.
92
+
93
+ The bulk empty emits no change events, where the per-table loop emitted one per row. An import through `--target api` now asks every live subscription to re-read once it lands — the coarse refresh the framework already uses after a broadcast gap — so neither the missing deletes nor a table the bundle carries EMPTY leaves a subscriber holding rows that are gone. Wired where the route is mounted, which is the one place both boot paths share.
94
+
95
+ On postgres the emptying is a `DELETE` per table, not a `TRUNCATE`, and the difference is not performance: postgres refuses `TRUNCATE` on a table with ANY incoming foreign key, rows or not, while the mysql family refuses a DELETE only when rows actually reference the doomed ones. A `TRUNCATE` version made an EMPTY table outside the bundle block a replace on postgres and not on mariadb — one import, refused on one engine and accepted on the other, over a table holding nothing.
96
+
97
+ `--target api` also no longer times out on a full bundle. Both api-target calls went through `fetch`, whose undici default gives up after 300 s — a bound on the caller's database size, on a call whose response arrives only when the import does. They wait as long as the instance needs now, stream the body instead of buffering the whole bundle, and take `--timeout <seconds>` when a deadline is wanted. If one is hit, the message says the instance is probably still importing — and gives different advice for `replace` than for the idempotent modes, since re-running the first while it is mid-flight would empty the target under it.
98
+
99
+ Two more, found by measuring rather than by reading:
100
+
101
+ - **`--atomic` on postgres could not import a bundle that needed the deferred-FK repair at all.** A failed statement aborts the transaction there, and that repair depends on a row whose parent has not loaded yet failing, being held, and being retried — so the first such row poisoned every write after it. Every tolerated write now runs inside a savepoint. Per-row savepoints measured 2.40x the time of none on 5 000 rows, so they are amortised: one savepoint per batch of 200, and a batch that fails rolls back whole and replays row by row. The mysql family and sqlite leave a transaction usable after a failed statement and pay nothing for any of this. - **The replace pre-flight asked the caller's snapshot.** Over `--target api` that is the app's DECLARED schema, which cannot show a table the app stopped declaring but the database still has — and rows in a table nobody declares are exactly the rows nobody is watching. `DataStore.incomingForeignKeys()` reads the live catalog per dialect; the snapshot remains the fallback for stores without one.
102
+
103
+ A bundle bigger than one chunk is now uploaded as a series of short requests, so a proxy body cap or an ingress read timeout has nothing large to choke on, and the switch is automatic — the packer's stream is buffered one chunk ahead, so a small bundle is sent exactly as before and nobody has to know in advance which table is the big one. The import still runs ONCE, at the end, over the whole bundle. Resume is byte-exact (`packBundle` is deterministic over a directory, which this package now asserts), guarded by a bundle key so a different bundle under the same upload id is refused rather than spliced into the partial one, and by a contiguity check so a mis-ordered append cannot produce an archive that only fails later during decode. `--chunk-size <mb>` overrides the 16 MiB default.
104
+
105
+ ---
106
+
42
107
  ## [0.43.1] — 2026-08-18
43
108
 
44
109
  ### Fixed
package/dist/index.d.ts CHANGED
@@ -136,6 +136,16 @@ export declare class SqliteStore implements DataStore {
136
136
  * ask the question at all.
137
137
  */
138
138
  private localWrite;
139
+ /**
140
+ * A write and its write-recorders, as ONE unit — see `MysqlStore.
141
+ * writeWithRecorders` for the failure this closes.
142
+ *
143
+ * The same divergence exists here even though sqlite is one process: the row's
144
+ * statement commits on its own and the recorder's INSERT is a second one, so a
145
+ * recorder that throws leaves a committed row behind a write that reported
146
+ * failure — and a caller that retries meets its own row.
147
+ */
148
+ private writeWithRecorders;
139
149
  insert(table: string, row: Row): Promise<Row>;
140
150
  insertMany(table: string, rows: ReadonlyArray<Row>): Promise<ReadonlyArray<Row>>;
141
151
  patchJson(table: string, primaryKey: string, path: string, value: unknown): Promise<Row | null>;
@@ -179,6 +189,30 @@ export declare class SqliteStore implements DataStore {
179
189
  * defect, preserving crash semantics). Without this the Turso MVCC
180
190
  * retry — the whole point of `BEGIN CONCURRENT` — would never fire.
181
191
  */
192
+ /**
193
+ * Empty `tables` as one unit — see `DataStore.emptyTables`.
194
+ *
195
+ * SQLite ships the exact switch for this: `defer_foreign_keys` holds every
196
+ * foreign-key check until COMMIT, and unlike `PRAGMA foreign_keys` it is
197
+ * settable INSIDE a transaction (that one is a silent no-op there, which is
198
+ * the trap to avoid). So the whole set empties in any order and integrity is
199
+ * verified once, at the end — which is what makes a self-referencing table
200
+ * possible to clear at all.
201
+ *
202
+ * The pragma resets itself at COMMIT, so there is nothing to restore; the
203
+ * explicit reset below covers the ROLLBACK path, where it does not.
204
+ */
205
+ emptyTables(tables: ReadonlyArray<string>): Promise<void>;
206
+ /** See `DataStore.incomingForeignKeys`. SQLite has no catalog VIEW of foreign
207
+ * keys — `PRAGMA foreign_key_list(t)` answers per table — so this walks the
208
+ * table list once. That is fine at the one call site (a pre-flight, once per
209
+ * import) and is why it is not a general-purpose helper. */
210
+ incomingForeignKeys(tables: ReadonlyArray<string>): Promise<ReadonlyArray<{
211
+ from: string;
212
+ column: string;
213
+ to: string;
214
+ }>>;
215
+ /* Excluded from this release type: emptyTablesOn */
182
216
  transactional<T>(work: (tx: DataStore) => Promise<T>): Promise<T>;
183
217
  /**
184
218
  * The DIALECT half of the shared transaction bracket
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { SqliteClient as e } from "@effect/sql-sqlite-node";
2
- import { Config as t, Context as n, Effect as r, Layer as i, ManagedRuntime as a } from "effect";
3
- import { EventEmitter as o } from "node:events";
4
- import { createLogger as s } from "@voltro/logger";
5
- import { SqlClient as c, TransactionConnection as l } from "@effect/sql/SqlClient";
6
- import { EagerCardinalityError as u, attachEagerLoads as d, attributionFields as f, bulkInsertLimitsFor as p, chunkRowsForInsert as m, compileEagerJson as h, compilePredicate as g, compileRawFragment as _, compileSelect as v, decodeRowsFromSchema as y, externalChangeEvent as b, hasEagerLoads as x, isTableReactive as S, makeEagerFallbackReporter as C, observeDbOp as w, qualifyTable as T, raiseChangeListenerCeiling as E, recordsTable as D, requireTable as O, runStoreTransaction as k, runWriteRecorders as A, stampGeneratedId as j, stampGeneratedIds as M, withCapturedAttribution as N } from "@voltro/database";
2
+ import { Config as t, Context as n, Effect as r, Layer as i, ManagedRuntime as a, Option as o } from "effect";
3
+ import { EventEmitter as s } from "node:events";
4
+ import { createLogger as c } from "@voltro/logger";
5
+ import { SqlClient as l, TransactionConnection as u } from "@effect/sql/SqlClient";
6
+ import { EagerCardinalityError as d, attachEagerLoads as f, attributionFields as p, bulkInsertLimitsFor as m, chunkRowsForInsert as h, compileEagerJson as g, compilePredicate as _, compileRawFragment as v, compileSelect as y, decodeRowsFromSchema as b, externalChangeEvent as x, hasEagerLoads as S, isTableReactive as C, makeEagerFallbackReporter as w, observeDbOp as T, qualifyTable as E, quoteIdent as D, raiseChangeListenerCeiling as O, recordsTable as k, requireTable as A, runStoreTransaction as j, runWriteRecorders as M, stampGeneratedId as N, stampGeneratedIds as P, withCapturedAttribution as F } from "@voltro/database";
7
7
  //#region src/sqlLayer.ts
8
- var P = (r) => e.layerConfig({
8
+ var I = (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`)), F = (e) => {
11
+ }).pipe(i.tap((t) => n.get(t, e.SqliteClient)`PRAGMA foreign_keys = ON`)), L = (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 P = (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
- }, I = (e) => P(F(e)), L = /* @__PURE__ */ new Set([
19
+ }, R = (e) => I(L(e)), z = /* @__PURE__ */ new Set([
20
20
  "SQLITE_BUSY",
21
21
  "SQLITE_LOCKED",
22
22
  "5",
23
23
  "6"
24
- ]), R = (e) => {
24
+ ]), B = (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,18 @@ var P = (r) => e.layerConfig({
29
29
  if (typeof e == "number") return String(e);
30
30
  t = t.cause;
31
31
  }
32
- }, z = (e) => {
33
- let t = R(e);
34
- return t !== void 0 && L.has(t);
35
- }, B = (e) => z(e) ? "retry" : "noRetry", V = s({ scope: "voltro:sqlite" }), H = async (e) => {
32
+ }, V = (e) => {
33
+ let t = B(e);
34
+ return t !== void 0 && z.has(t);
35
+ }, H = (e) => V(e) ? "retry" : "noRetry", U = c({ scope: "voltro:sqlite" }), W = async (e) => {
36
36
  let t = e.tracerLayer ? i.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, n = a.make(t);
37
- return new G(await n.runPromise(c), n, null, void 0, e.isRetryable ?? z, e.systemName ?? "sqlite", e.wrapTransaction);
38
- }, U = (e) => e, W = (e) => {
37
+ return new q(await n.runPromise(l), n, null, void 0, e.isRetryable ?? V, e.systemName ?? "sqlite", e.wrapTransaction);
38
+ }, G = (e) => e, K = (e) => {
39
39
  if (typeof e != "object" || !e) return e;
40
40
  let t = {};
41
41
  for (let [n, r] of Object.entries(e)) t[n] = r instanceof Date ? r.toISOString() : typeof r == "boolean" ? +!!r : r;
42
42
  return t;
43
- }, G = class e {
43
+ }, q = class e {
44
44
  sql;
45
45
  runtime;
46
46
  namespace;
@@ -50,36 +50,36 @@ var P = (r) => e.layerConfig({
50
50
  emitter;
51
51
  inflightTxns = 0;
52
52
  reportEagerFallback;
53
- constructor(e, t, n = null, r, i = z, a = "sqlite", s = U) {
54
- this.sql = e, this.runtime = t, this.namespace = n, this.isRetryable = i, this.systemName = a, this.wrapTransaction = s, this.emitter = r ?? new o(), E(this.emitter), this.reportEagerFallback = C(V);
53
+ constructor(e, t, n = null, r, i = V, a = "sqlite", o = G) {
54
+ this.sql = e, this.runtime = t, this.namespace = n, this.isRetryable = i, this.systemName = a, this.wrapTransaction = o, this.emitter = r ?? new s(), O(this.emitter), this.reportEagerFallback = w(U);
55
55
  }
56
56
  withNamespace(t) {
57
57
  return t === this.namespace ? this : new e(this.sql, this.runtime, t, this.emitter, this.isRetryable, this.systemName, this.wrapTransaction);
58
58
  }
59
59
  nsT(e) {
60
- return T(this.namespace, e);
60
+ return E(this.namespace, e);
61
61
  }
62
62
  async attachNamespace(e, t) {
63
63
  let n = this.sql, r = t ?? `${e}.db`;
64
64
  await this.runtime.runPromise(n`ATTACH DATABASE ${r} AS ${n(e)}`);
65
65
  }
66
66
  async executeQuery(e, t, n) {
67
- let i = v(e, this.sql, this.namespace), a = t ? r.provideService(i, l, t) : i, o = await w(this.systemName, "select", () => this.runtime.runPromise(a));
68
- return y(o, e.table, "sqlite");
67
+ let i = y(e, this.sql, this.namespace), a = t ? r.provideService(i, u, t) : i, o = await T(this.systemName, "select", () => this.runtime.runPromise(a));
68
+ return b(o, e.table, "sqlite");
69
69
  }
70
70
  async executeInsert(e, t, n, i, a) {
71
- t = j(e, t);
72
- let o = this.sql, s = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(W(t))} RETURNING *`, c = n ? r.provideService(s, l, n) : s, u = (await this.runtime.runPromise(c))[0];
73
- if (!u) throw Error(`SqliteStore.insert: no row returned for table '${e}'`);
71
+ t = N(e, t);
72
+ let o = this.sql, s = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(K(t))} RETURNING *`, c = n ? r.provideService(s, u, n) : s, l = (await this.runtime.runPromise(c))[0];
73
+ if (!l) throw Error(`SqliteStore.insert: no row returned for table '${e}'`);
74
74
  return await this.routeEvent({
75
75
  table: e,
76
76
  op: "insert",
77
77
  old: null,
78
- new: u
79
- }, i, n, a), u;
78
+ new: l
79
+ }, i, n, a), l;
80
80
  }
81
81
  async executeUpdate(e, t, n, i, a, o) {
82
- let s = this.sql, c = s`UPDATE ${s(this.nsT(e))} SET ${s.update(W(n))} WHERE ${s("id")} = ${t} RETURNING *`, u = i ? r.provideService(c, l, i) : c, d = (await this.runtime.runPromise(u))[0];
82
+ let s = this.sql, c = s`UPDATE ${s(this.nsT(e))} SET ${s.update(K(n))} WHERE ${s("id")} = ${t} RETURNING *`, l = i ? r.provideService(c, u, i) : c, d = (await this.runtime.runPromise(l))[0];
83
83
  return d ? (await this.routeEvent({
84
84
  table: e,
85
85
  op: "update",
@@ -88,16 +88,16 @@ var P = (r) => e.layerConfig({
88
88
  }, a, i, o), d) : null;
89
89
  }
90
90
  async executeInsertMany(e, t, n, i, a) {
91
- if (t = M(e, t), t.length === 0) return [];
92
- let o = this.sql, s = t.map((e) => W(e)), c = m(s, p("sqlite")), u = (t) => o`INSERT INTO ${o(this.nsT(e))} ${o.insert(t)} RETURNING *`, d;
91
+ if (t = P(e, t), t.length === 0) return [];
92
+ let o = this.sql, s = t.map((e) => K(e)), c = h(s, m("sqlite")), l = (t) => o`INSERT INTO ${o(this.nsT(e))} ${o.insert(t)} RETURNING *`, d;
93
93
  if (c.length === 1) {
94
- let e = u(c[0]), t = n ? r.provideService(e, l, n) : e;
94
+ let e = l(c[0]), t = n ? r.provideService(e, u, n) : e;
95
95
  d = await this.runtime.runPromise(t);
96
96
  } else if (n) {
97
97
  let e = [];
98
- for (let t of c) e.push(...await this.runtime.runPromise(r.provideService(u(t), l, n)));
98
+ for (let t of c) e.push(...await this.runtime.runPromise(r.provideService(l(t), u, n)));
99
99
  d = e;
100
- } else d = await this.runtime.runPromise(o.withTransaction(r.map(r.forEach(c, u, { concurrency: 1 }), (e) => e.flat())));
100
+ } else d = await this.runtime.runPromise(o.withTransaction(r.map(r.forEach(c, l, { concurrency: 1 }), (e) => e.flat())));
101
101
  for (let t of d) await this.routeEvent({
102
102
  table: e,
103
103
  op: "insert",
@@ -107,7 +107,7 @@ var P = (r) => e.layerConfig({
107
107
  return d;
108
108
  }
109
109
  async executePatchJson(e, t, n, i, a, o, s) {
110
- let c = this.sql, u = n.split("."), d = u[0], f = u.slice(1), p = JSON.stringify(i ?? null), m = f.length === 0 ? c`${c(d)} = json_patch(COALESCE(${c(d)}, '{}'), json(${p}))` : c`${c(d)} = json_set(COALESCE(${c(d)}, '{}'), ${`$.${f.join(".")}`}, json(${p}))`, h = c`UPDATE ${c(this.nsT(e))} SET ${m} WHERE ${c("id")} = ${t} RETURNING *`, g = a ? r.provideService(h, l, a) : h, _ = (await this.runtime.runPromise(g))[0];
110
+ let c = this.sql, l = n.split("."), d = l[0], f = l.slice(1), p = JSON.stringify(i ?? null), m = f.length === 0 ? c`${c(d)} = json_patch(COALESCE(${c(d)}, '{}'), json(${p}))` : c`${c(d)} = json_set(COALESCE(${c(d)}, '{}'), ${`$.${f.join(".")}`}, json(${p}))`, h = c`UPDATE ${c(this.nsT(e))} SET ${m} WHERE ${c("id")} = ${t} RETURNING *`, g = a ? r.provideService(h, u, a) : h, _ = (await this.runtime.runPromise(g))[0];
111
111
  return _ ? (await this.routeEvent({
112
112
  table: e,
113
113
  op: "update",
@@ -116,27 +116,27 @@ var P = (r) => e.layerConfig({
116
116
  }, o, a, s), _) : null;
117
117
  }
118
118
  async executeDelete(e, t, n, i, a) {
119
- let o = this.sql, s = o`DELETE FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t} RETURNING *`, c = n ? r.provideService(s, l, n) : s, u = (await this.runtime.runPromise(c))[0];
120
- return u ? (await this.routeEvent({
119
+ let o = this.sql, s = o`DELETE FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t} RETURNING *`, c = n ? r.provideService(s, u, n) : s, l = (await this.runtime.runPromise(c))[0];
120
+ return l ? (await this.routeEvent({
121
121
  table: e,
122
122
  op: "delete",
123
- old: u,
123
+ old: l,
124
124
  new: null
125
125
  }, i, n, a), !0) : !1;
126
126
  }
127
127
  async appendInTxn(e, t, n) {
128
- let i = this.sql, a = i`INSERT INTO ${i(this.nsT(e))} ${i.insert(W(t))}`;
129
- await this.runtime.runPromise(n ? r.provideService(a, l, n) : a);
128
+ let i = this.sql, a = i`INSERT INTO ${i(this.nsT(e))} ${i.insert(K(t))}`;
129
+ await this.runtime.runPromise(n ? r.provideService(a, u, n) : a);
130
130
  }
131
131
  async maxInTxn(e, t, n, i) {
132
- 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, l, i) : s))[0]?.m;
132
+ 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, u, i) : s))[0]?.m;
133
133
  return c == null ? null : Number(c);
134
134
  }
135
135
  async routeEvent(e, t, n = null, r) {
136
136
  e = {
137
- ...f(r),
137
+ ...p(r),
138
138
  ...e
139
- }, D(e.table) && await A({
139
+ }, k(e.table) && await M({
140
140
  append: (e, t) => this.appendInTxn(e, t, n),
141
141
  maxOf: (e, t, r) => this.maxInTxn(e, t, r, n)
142
142
  }, {
@@ -146,14 +146,14 @@ var P = (r) => e.layerConfig({
146
146
  prev: e.old,
147
147
  traceId: e.traceId,
148
148
  subjectId: e.subjectId
149
- }), S(e.table) && (t === null ? this.emitter.emit("change", e) : t.push(e));
149
+ }), C(e.table) && (t === null ? this.emitter.emit("change", e) : t.push(e));
150
150
  }
151
151
  async executeUpsert(e, t, n, i, a, o) {
152
152
  if (typeof n.update == "function") {
153
153
  let r = await this.findByConflict(e, t, n.conflictColumns, i);
154
154
  return r ? await this.executeUpdate(e, r.id, n.update(r), i, a) ?? r : this.executeInsert(e, t, i, a);
155
155
  }
156
- let s = this.sql, c = n.conflictColumns.map((e) => s`${s(e)}`), u = Object.keys(t).filter((e) => t[e] !== void 0), d = n.update === void 0 ? u.filter((e) => e !== "id" && !n.conflictColumns.includes(e)) : n.update, f = d.length > 0 ? s.csv(d.map((e) => s`${s(e)} = excluded.${s(e)}`)) : s`${s(n.conflictColumns[0])} = excluded.${s(n.conflictColumns[0])}`, p = s`INSERT INTO ${s(this.nsT(e))} ${s.insert(W(t))} ON CONFLICT (${s.csv(c)}) DO UPDATE SET ${f} RETURNING *`, m = i ? r.provideService(p, l, i) : p, h = (await this.runtime.runPromise(m))[0];
156
+ let s = this.sql, c = n.conflictColumns.map((e) => s`${s(e)}`), l = Object.keys(t).filter((e) => t[e] !== void 0), d = n.update === void 0 ? l.filter((e) => e !== "id" && !n.conflictColumns.includes(e)) : n.update, f = d.length > 0 ? s.csv(d.map((e) => s`${s(e)} = excluded.${s(e)}`)) : s`${s(n.conflictColumns[0])} = excluded.${s(n.conflictColumns[0])}`, p = s`INSERT INTO ${s(this.nsT(e))} ${s.insert(K(t))} ON CONFLICT (${s.csv(c)}) DO UPDATE SET ${f} RETURNING *`, m = i ? r.provideService(p, u, i) : p, h = (await this.runtime.runPromise(m))[0];
157
157
  if (!h) throw Error(`SqliteStore.upsert: no row returned for table '${e}'`);
158
158
  let g = t.id !== void 0 && t.id === h.id ? "insert" : "update";
159
159
  return await this.routeEvent({
@@ -164,8 +164,8 @@ var P = (r) => e.layerConfig({
164
164
  }, a, i, o), h;
165
165
  }
166
166
  async executeInsertIgnore(e, t, n, i, a, o) {
167
- t = j(e, t);
168
- let s = this.sql, c = n.conflictColumns.map((e) => s`${s(e)}`), u = s`INSERT INTO ${s(this.nsT(e))} ${s.insert(W(t))} ON CONFLICT (${s.csv(c)}) DO NOTHING RETURNING *`, d = i ? r.provideService(u, l, i) : u, f = (await this.runtime.runPromise(d))[0];
167
+ t = N(e, t);
168
+ let s = this.sql, c = n.conflictColumns.map((e) => s`${s(e)}`), l = s`INSERT INTO ${s(this.nsT(e))} ${s.insert(K(t))} ON CONFLICT (${s.csv(c)}) DO NOTHING RETURNING *`, d = i ? r.provideService(l, u, i) : l, f = (await this.runtime.runPromise(d))[0];
169
169
  if (f) return await this.routeEvent({
170
170
  table: e,
171
171
  op: "insert",
@@ -178,24 +178,24 @@ var P = (r) => e.layerConfig({
178
178
  }
179
179
  async findByConflict(e, t, n, i) {
180
180
  if (n.length === 0) return;
181
- let a = this.sql, o = n.map((e) => a`${a(e)} = ${t[e]}`), s = a`SELECT * FROM ${a(this.nsT(e))} WHERE ${a.and(o)} LIMIT 1`, c = i ? r.provideService(s, l, i) : s;
181
+ let a = this.sql, o = n.map((e) => a`${a(e)} = ${t[e]}`), s = a`SELECT * FROM ${a(this.nsT(e))} WHERE ${a.and(o)} LIMIT 1`, c = i ? r.provideService(s, u, i) : s;
182
182
  return (await this.runtime.runPromise(c))[0];
183
183
  }
184
184
  query(e) {
185
185
  return this.runWithEager(e, null);
186
186
  }
187
187
  raw(e, t) {
188
- let n = _(e, this.sql);
189
- return w(this.systemName, "raw", () => this.runtime.runPromise(n));
188
+ let n = v(e, this.sql);
189
+ return T(this.systemName, "raw", () => this.runtime.runPromise(n));
190
190
  }
191
191
  async runWithEager(e, t) {
192
- if (!x(e)) return this.executeQuery(e, t);
193
- let n = this.namespace === null ? h(e, this.sql, "sqlite") : null;
192
+ if (!S(e)) return this.executeQuery(e, t);
193
+ let n = this.namespace === null ? g(e, this.sql, "sqlite") : null;
194
194
  if (n !== null) try {
195
- let e = t ? r.provideService(n.fragment, l, t) : n.fragment, i = await w(this.systemName, "select", () => this.runtime.runPromise(e));
195
+ let e = t ? r.provideService(n.fragment, u, t) : n.fragment, i = await T(this.systemName, "select", () => this.runtime.runPromise(e));
196
196
  return n.decode(i);
197
197
  } catch (t) {
198
- if (t instanceof u) throw t;
198
+ if (t instanceof d) throw t;
199
199
  this.reportEagerFallback({
200
200
  dialect: this.systemName,
201
201
  table: e.table,
@@ -209,31 +209,40 @@ var P = (r) => e.layerConfig({
209
209
  reason: "not-compilable"
210
210
  });
211
211
  let i = await this.executeQuery(e, t);
212
- return d(i, e.eager, e.sourceTable ?? O(e.table), (e) => this.executeQuery(e, t));
212
+ return f(i, e.eager, e.sourceTable ?? A(e.table), (e) => this.executeQuery(e, t));
213
213
  }
214
214
  getInternalRunWithEager() {
215
215
  return (e, t) => this.runWithEager(e, t);
216
216
  }
217
- localWrite(e, t) {
218
- return w(this.systemName, e, () => N(t));
217
+ localWrite(e, t, n) {
218
+ let r = (e) => k(t) ? this.writeWithRecorders(n, e) : n(e, null, null);
219
+ return T(this.systemName, e, () => F(r));
220
+ }
221
+ async writeWithRecorders(e, t) {
222
+ let n = [], i = this.sql, a = await this.runtime.runPromise(i.withTransaction(r.flatMap(r.serviceOption(u), (i) => o.isNone(i) ? r.fail(/* @__PURE__ */ Error("SqliteStore.write: TransactionConnection missing.")) : r.tryPromise({
223
+ try: () => e(t, i.value, n),
224
+ catch: (e) => e
225
+ }))));
226
+ for (let e of n) this.emitChange(e);
227
+ return a;
219
228
  }
220
229
  insert(e, t) {
221
- return this.localWrite("insert", (n) => this.executeInsert(e, t, null, null, n));
230
+ return this.localWrite("insert", e, (n, r, i) => this.executeInsert(e, t, r, i, n));
222
231
  }
223
232
  insertMany(e, t) {
224
- return this.localWrite("insert", (n) => this.executeInsertMany(e, t, null, null, n));
233
+ return this.localWrite("insert", e, (n, r, i) => this.executeInsertMany(e, t, r, i, n));
225
234
  }
226
235
  patchJson(e, t, n, r) {
227
- return this.localWrite("update", (i) => this.executePatchJson(e, t, n, r, null, null, i));
236
+ return this.localWrite("update", e, (i, a, o) => this.executePatchJson(e, t, n, r, a, o, i));
228
237
  }
229
238
  update(e, t, n) {
230
- return this.localWrite("update", (r) => this.executeUpdate(e, t, n, null, null, r));
239
+ return this.localWrite("update", e, (r, i, a) => this.executeUpdate(e, t, n, i, a, r));
231
240
  }
232
241
  delete(e, t) {
233
- return this.localWrite("delete", (n) => this.executeDelete(e, t, null, null, n));
242
+ return this.localWrite("delete", e, (n, r, i) => this.executeDelete(e, t, r, i, n));
234
243
  }
235
244
  async updateMany(e, t, n) {
236
- let r = this.sql, i = g(n.where, r, this.namespace), a = r`UPDATE ${r(this.nsT(e))} SET ${r.update(W(t))} WHERE ${i} RETURNING *`, o = await w(this.systemName, "update", () => this.runtime.runPromise(a));
245
+ let r = this.sql, i = _(n.where, r, this.namespace), a = r`UPDATE ${r(this.nsT(e))} SET ${r.update(K(t))} WHERE ${i} RETURNING *`, o = await T(this.systemName, "update", () => this.runtime.runPromise(a));
237
246
  for (let t of o) await this.routeEvent({
238
247
  table: e,
239
248
  op: "update",
@@ -243,7 +252,7 @@ var P = (r) => e.layerConfig({
243
252
  return o.length;
244
253
  }
245
254
  async deleteMany(e, t) {
246
- let n = this.sql, r = g(t.where, n, this.namespace), i = n`DELETE FROM ${n(this.nsT(e))} WHERE ${r} RETURNING *`, a = await w(this.systemName, "delete", () => this.runtime.runPromise(i));
255
+ let n = this.sql, r = _(t.where, n, this.namespace), i = n`DELETE FROM ${n(this.nsT(e))} WHERE ${r} RETURNING *`, a = await T(this.systemName, "delete", () => this.runtime.runPromise(i));
247
256
  for (let t of a) await this.routeEvent({
248
257
  table: e,
249
258
  op: "delete",
@@ -253,10 +262,10 @@ var P = (r) => e.layerConfig({
253
262
  return a.length;
254
263
  }
255
264
  upsert(e, t, n) {
256
- return this.localWrite("upsert", (r) => this.executeUpsert(e, t, n, null, null, r));
265
+ return this.localWrite("upsert", e, (r, i, a) => this.executeUpsert(e, t, n, i, a, r));
257
266
  }
258
267
  insertIgnore(e, t, n) {
259
- return this.localWrite("upsert", (r) => this.executeInsertIgnore(e, t, n, null, null, r));
268
+ return this.localWrite("upsert", e, (r, i, a) => this.executeInsertIgnore(e, t, n, i, a, r));
260
269
  }
261
270
  emitChange(e) {
262
271
  this.emitter.emit("change", e);
@@ -285,13 +294,35 @@ var P = (r) => e.layerConfig({
285
294
  getInternalExecuteInsertIgnore() {
286
295
  return this.executeInsertIgnore.bind(this);
287
296
  }
297
+ async emptyTables(e) {
298
+ await this.emptyTablesOn(e, null);
299
+ }
300
+ async incomingForeignKeys(e) {
301
+ if (e.length === 0) return [];
302
+ let t = this.sql, n = new Set(e), r = await this.runtime.runPromise(t`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`), i = [];
303
+ for (let e of r) {
304
+ if (n.has(e.name)) continue;
305
+ let r = await this.runtime.runPromise(t`${t.unsafe(`PRAGMA foreign_key_list(${D(e.name, "sqlite")})`)}`).catch(() => []);
306
+ for (let t of r) n.has(t.table) && i.push({
307
+ from: e.name,
308
+ column: t.from,
309
+ to: t.table
310
+ });
311
+ }
312
+ return i;
313
+ }
314
+ async emptyTablesOn(e, t) {
315
+ if (e.length === 0) return;
316
+ let n = this.sql, i = (e) => this.nsT(e), a = r.acquireUseRelease(n.unsafe("PRAGMA defer_foreign_keys = ON"), () => r.forEach(e, (e) => n`DELETE FROM ${n(i(e))}`, { discard: !0 }), () => r.orDie(r.ignore(n.unsafe("PRAGMA defer_foreign_keys = OFF")))), o = t === null ? n.withTransaction(a) : r.provideService(a, u, t);
317
+ await this.runtime.runPromise(o);
318
+ }
288
319
  async transactional(e) {
289
320
  this.inflightTxns++;
290
321
  try {
291
- return await k({
322
+ return await j({
292
323
  ...this.txnSpec("SqliteStore.transactional"),
293
324
  work: e,
294
- makeView: (e, t) => new K(this, e, t)
325
+ makeView: (e, t) => new J(this, e, t)
295
326
  });
296
327
  } finally {
297
328
  this.inflightTxns--;
@@ -320,7 +351,7 @@ var P = (r) => e.layerConfig({
320
351
  };
321
352
  }
322
353
  injectExternalChange(e) {
323
- S(e.table) && this.emitter.emit("change", b(e));
354
+ C(e.table) && this.emitter.emit("change", x(e));
324
355
  }
325
356
  run(e) {
326
357
  return this.runtime.runPromise(e);
@@ -329,7 +360,7 @@ var P = (r) => e.layerConfig({
329
360
  if (this.inflightTxns > 0) {
330
361
  let t = Date.now() + e;
331
362
  for (; this.inflightTxns > 0 && Date.now() < t;) await new Promise((e) => setTimeout(e, 25));
332
- this.inflightTxns > 0 && V.warn("close: grace period expired with in-flight transactions", {
363
+ this.inflightTxns > 0 && U.warn("close: grace period expired with in-flight transactions", {
333
364
  gracePeriodMs: e,
334
365
  inflight: this.inflightTxns
335
366
  });
@@ -339,7 +370,7 @@ var P = (r) => e.layerConfig({
339
370
  async ping() {
340
371
  await this.runtime.runPromise(this.sql`SELECT 1`);
341
372
  }
342
- }, K = class {
373
+ }, J = class {
343
374
  parent;
344
375
  txn;
345
376
  attr;
@@ -351,6 +382,9 @@ var P = (r) => e.layerConfig({
351
382
  query(e) {
352
383
  return this.parent.getInternalRunWithEager()(e, this.txn);
353
384
  }
385
+ emptyTables(e) {
386
+ return this.parent.emptyTablesOn(e, this.txn);
387
+ }
354
388
  insert(e, t) {
355
389
  return this.parent.getInternalExecuteInsert()(e, t, this.txn, this.events, this.attr);
356
390
  }
@@ -412,12 +446,12 @@ var P = (r) => e.layerConfig({
412
446
  this.events.length = 0;
413
447
  }
414
448
  }
415
- }, q = {
449
+ }, Y = {
416
450
  id: "sqlite",
417
- makeSqlLayer: (e) => I(e),
418
- makeStore: (e) => H(e),
451
+ makeSqlLayer: (e) => R(e),
452
+ makeStore: (e) => W(e),
419
453
  compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
420
- retryFilter: B
454
+ retryFilter: H
421
455
  };
422
456
  //#endregion
423
- export { e as SqliteClient, F as connectionFromConfig, P as makeSqliteSqlLayer, I as makeSqliteSqlLayerFromConfig, H as makeSqliteStore, q as sqliteDialect, B as sqliteRetryFilter };
457
+ export { e as SqliteClient, L as connectionFromConfig, I as makeSqliteSqlLayer, R as makeSqliteSqlLayerFromConfig, W as makeSqliteStore, Y as sqliteDialect, H as sqliteRetryFilter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/sql-sqlite",
3
- "version": "0.43.1",
3
+ "version": "0.44.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",
@@ -35,8 +35,8 @@
35
35
  "dependencies": {
36
36
  "@effect/sql": "^0.52.0",
37
37
  "@effect/sql-sqlite-node": "^0.53.0",
38
- "@voltro/database": "0.43.1",
39
- "@voltro/logger": "0.43.1"
38
+ "@voltro/database": "0.44.0",
39
+ "@voltro/logger": "0.44.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "effect": "^3.22.0"