@voltro/sql-sqlite 0.1.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.
@@ -0,0 +1,187 @@
1
+ import { ChangeEvent } from '@voltro/database';
2
+ import { ChangeStrategy } from '@voltro/database';
3
+ import { ConfigError } from 'effect';
4
+ import { ConfigError as ConfigError_2 } from 'effect/ConfigError';
5
+ import { ConnectionConfig } from '@voltro/database';
6
+ import { Context } from 'effect';
7
+ import { DataStore } from '@voltro/database';
8
+ import { Effect } from 'effect';
9
+ import { EventEmitter } from 'node:events';
10
+ import { Layer } from 'effect';
11
+ import { ManagedRuntime } from 'effect';
12
+ import { Predicate } from '@voltro/database';
13
+ import { QueryDescriptor } from '@voltro/database';
14
+ import { QueryNamespace } from '@voltro/database';
15
+ import { RawSqlFragment } from '@voltro/database/sql';
16
+ import { RetryDecision } from '@voltro/database';
17
+ import { Row } from '@voltro/database';
18
+ import { SqlClient } from '@effect/sql';
19
+ import { SqlClient as SqlClient_2 } from '@effect/sql/SqlClient';
20
+ import { SqlDialect } from '@voltro/database';
21
+ import { SqlError } from '@effect/sql';
22
+ import { SqlError as SqlError_2 } from '@effect/sql/SqlError';
23
+ import { SqliteClient } from '@effect/sql-sqlite-node';
24
+ import { TransactionConnection } from '@effect/sql/SqlClient';
25
+
26
+ /**
27
+ * Parse a `ConnectionConfig` into the sqlite-specific `SqliteConnection`
28
+ * shape. Sqlite is the simplest dialect — it only needs a filename.
29
+ *
30
+ * Resolution order:
31
+ * 1. `url` — accepted forms documented at file head
32
+ * 2. `database` — treated as the filename
33
+ * 3. Throw with a clear message
34
+ */
35
+ export declare const connectionFromConfig: (config: ConnectionConfig) => SqliteConnection;
36
+
37
+ export declare const makeSqliteSqlLayer: (options: SqliteConnection) => SqliteSqlLayer;
38
+
39
+ export declare const makeSqliteSqlLayerFromConfig: (config: ConnectionConfig) => SqliteSqlLayer;
40
+
41
+ export declare const makeSqliteStore: (options: SqliteStoreOptions) => Promise<SqliteStore>;
42
+
43
+ export { SqliteClient }
44
+
45
+ export declare interface SqliteConnection {
46
+ /** Absolute or `file:` path, or `:memory:` for ephemeral. */
47
+ readonly filename: string;
48
+ readonly readonly?: boolean;
49
+ }
50
+
51
+ export declare const sqliteDialect: SqlDialect;
52
+
53
+ export declare const sqliteRetryFilter: (err: unknown) => RetryDecision;
54
+
55
+ /**
56
+ * The connection layer this package produces: the driver's client layer plus
57
+ * the per-connection `PRAGMA foreign_keys = ON` tap (which adds `SqlError`
58
+ * to the error channel — matching the cross-dialect `sqlLayer` shape in
59
+ * `@voltro/database`).
60
+ */
61
+ declare type SqliteSqlLayer = Layer.Layer<SqliteClient.SqliteClient | SqlClient_2, ConfigError_2 | SqlError_2>;
62
+
63
+ export declare class SqliteStore implements DataStore {
64
+ private readonly sql;
65
+ private readonly runtime;
66
+ private readonly namespace;
67
+ private readonly isRetryable;
68
+ private readonly systemName;
69
+ private readonly wrapTransaction;
70
+ private readonly emitter;
71
+ private inflightTxns;
72
+ constructor(sql: SqlClient.SqlClient, runtime: ManagedRuntime.ManagedRuntime<SqlClient.SqlClient, never>, namespace?: QueryNamespace, sharedEmitter?: EventEmitter, isRetryable?: (e: unknown) => boolean, systemName?: string, wrapTransaction?: <A, E, R>(e: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>);
73
+ /**
74
+ * Bind a per-request tenant namespace, returning a view that qualifies
75
+ * every table reference to `<namespace>.<table>`. The view SHARES this
76
+ * store's connection + emitter (sqlite is single-process). The
77
+ * namespace's attached database must already exist — see
78
+ * `attachNamespace` / the migrator's namespace fan-out.
79
+ */
80
+ withNamespace(namespace: string | null): DataStore;
81
+ /** Qualify a table to this view's namespace (no-op when unset). */
82
+ private nsT;
83
+ /**
84
+ * Provision a tenant's namespace: `ATTACH DATABASE '<file>' AS
85
+ * <namespace>`. Idempotent enough for first-use lazy-create — sqlite
86
+ * errors if the alias is already attached, which the caller treats as
87
+ * "already provisioned". The file path defaults to `<namespace>.db`
88
+ * beside the main database.
89
+ */
90
+ attachNamespace(namespace: string, file?: string): Promise<void>;
91
+ private executeQuery;
92
+ private executeInsert;
93
+ private executeUpdate;
94
+ private executeInsertMany;
95
+ private executePatchJson;
96
+ private executeDelete;
97
+ private routeEvent;
98
+ private executeUpsert;
99
+ private executeInsertIgnore;
100
+ private findByConflict;
101
+ query(descriptor: QueryDescriptor): Promise<ReadonlyArray<Row>>;
102
+ raw<T extends object = Row>(fragment: RawSqlFragment, _opts?: {
103
+ dependsOn?: ReadonlyArray<string>;
104
+ }): Promise<ReadonlyArray<T>>;
105
+ private runWithEager;
106
+ getInternalRunWithEager(): (d: QueryDescriptor, txn: TxnContext | null) => Promise<ReadonlyArray<Row>>;
107
+ insert(table: string, row: Row): Promise<Row>;
108
+ insertMany(table: string, rows: ReadonlyArray<Row>): Promise<ReadonlyArray<Row>>;
109
+ patchJson(table: string, primaryKey: string, path: string, value: unknown): Promise<Row | null>;
110
+ update(table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>): Promise<Row | null>;
111
+ delete(table: string, primaryKey: string): Promise<boolean>;
112
+ updateMany(table: string, patch: Readonly<Record<string, unknown>>, options: {
113
+ where: Predicate;
114
+ }): Promise<number>;
115
+ deleteMany(table: string, options: {
116
+ where: Predicate;
117
+ }): Promise<number>;
118
+ upsert(table: string, row: Row, options: {
119
+ conflictColumns: ReadonlyArray<string>;
120
+ update?: ReadonlyArray<string> | ((existing: Row) => Readonly<Record<string, unknown>>);
121
+ }): Promise<Row>;
122
+ insertIgnore(table: string, row: Row, options: {
123
+ conflictColumns: ReadonlyArray<string>;
124
+ }): Promise<Row>;
125
+ /* Excluded from this release type: emitChange */
126
+ /* Excluded from this release type: getInternalExecuteQuery */
127
+ /* Excluded from this release type: getInternalExecuteInsert */
128
+ /* Excluded from this release type: getInternalExecuteInsertMany */
129
+ /* Excluded from this release type: getInternalExecutePatchJson */
130
+ /* Excluded from this release type: getInternalExecuteUpdate */
131
+ /* Excluded from this release type: getInternalExecuteDelete */
132
+ /* Excluded from this release type: getInternalExecuteUpsert */
133
+ /* Excluded from this release type: getInternalExecuteInsertIgnore */
134
+ /**
135
+ * Run `work` inside a real transaction. Same architecture as the
136
+ * postgres impl: per-call view + captured TxnContext + events buffer;
137
+ * commit drains, throw discards. Each attempt builds a FRESH view, so a
138
+ * retried attempt cannot double-emit (events drain only on success).
139
+ *
140
+ * Retry uses the injected `isRetryable` predicate — sqlite's
141
+ * `SQLITE_BUSY` / `SQLITE_LOCKED`, or (Turso dialect) the MVCC
142
+ * `"Write-write conflict"` from `BEGIN CONCURRENT`. A conflict can
143
+ * surface from the COMMIT, which `@effect/sql` runs as `Effect.orDie`
144
+ * → it arrives as a DEFECT, not a typed failure, and `Effect.retry`
145
+ * does not retry defects. So we promote a retryable defect back to a
146
+ * typed failure before the schedule sees it (and re-die on a genuine
147
+ * defect, preserving crash semantics). Without this the Turso MVCC
148
+ * retry — the whole point of `BEGIN CONCURRENT` — would never fire.
149
+ */
150
+ transactional<T>(work: (tx: DataStore) => Promise<T>): Promise<T>;
151
+ onChange(listener: (event: ChangeEvent) => void): () => void;
152
+ /** Cross-instance reactivity seam — emit an externally-sourced event to
153
+ * local subscribers without re-persisting. sqlite is single-process so
154
+ * this is unused in practice, but the seam keeps the DataStore contract
155
+ * uniform. See `DataStore.injectExternalChange`. */
156
+ injectExternalChange(event: ChangeEvent): void;
157
+ run<A, E>(effect: Effect.Effect<A, E, SqlClient.SqlClient>): Promise<A>;
158
+ close(gracePeriodMs?: number): Promise<void>;
159
+ /** Liveness probe — `SELECT 1`. Rejects if the pool can't answer. */
160
+ ping(): Promise<void>;
161
+ }
162
+
163
+ export declare interface SqliteStoreOptions {
164
+ readonly sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>;
165
+ readonly tracerLayer?: Layer.Layer<never, never, never>;
166
+ /** Accepted for cross-dialect symmetry; ignored (always in-process). */
167
+ readonly changeStrategy?: ChangeStrategy;
168
+ /**
169
+ * Predicate deciding whether a failed transaction is a transient
170
+ * contention error worth retrying. Defaults to sqlite's busy/locked
171
+ * codes. The Turso dialect injects its own (matches the
172
+ * `"Write-write conflict"` message its MVCC `BEGIN CONCURRENT` raises).
173
+ */
174
+ readonly isRetryable?: (e: unknown) => boolean;
175
+ /** `db.system` span attribute on `store.transactional`. Default `'sqlite'`. */
176
+ readonly systemName?: string;
177
+ /**
178
+ * Wraps each `transactional()` program before it runs. The Turso dialect uses
179
+ * it to set a fiber flag that upgrades `BEGIN` → `BEGIN CONCURRENT` for the
180
+ * store's DML (DDL paths keep plain BEGIN). Default: identity (sqlite).
181
+ */
182
+ readonly wrapTransaction?: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
183
+ }
184
+
185
+ declare type TxnContext = Context.Tag.Service<typeof TransactionConnection>;
186
+
187
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,376 @@
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, Option as o, Schedule as s } from "effect";
3
+ import { EventEmitter as c } from "node:events";
4
+ import { createLogger as l } from "@voltro/logger";
5
+ import { SqlClient as u, TransactionConnection as d } from "@effect/sql/SqlClient";
6
+ import { attachEagerLoads as f, compileEagerJson as p, compilePredicate as m, compileRawFragment as h, compileSelect as g, decodeRowsFromSchema as _, hasEagerLoads as v, qualifyTable as y, requireTable as b } from "@voltro/database";
7
+ //#region src/sqlLayer.ts
8
+ var x = (r) => e.layerConfig({
9
+ filename: t.succeed(r.filename),
10
+ ...r.readonly === void 0 ? {} : { readonly: t.succeed(r.readonly) }
11
+ }).pipe(i.tap((t) => n.get(t, e.SqliteClient)`PRAGMA foreign_keys = ON`)), S = (e) => {
12
+ if (e.url) {
13
+ if (e.url === ":memory:") return { filename: ":memory:" };
14
+ if (e.url.startsWith("file:")) return { filename: e.url.slice(5) };
15
+ throw Error(`@voltro/sql-sqlite: unsupported url '${e.url}'. Accepted: 'file:./path.sqlite', 'file:/abs/path.sqlite', ':memory:'.`);
16
+ }
17
+ if (e.database) return { filename: e.database };
18
+ throw Error("@voltro/sql-sqlite: no filename supplied. Set DB_URL=file:./db.sqlite or DB_DATABASE=path/to/db.sqlite.");
19
+ }, C = (e) => x(S(e)), w = /* @__PURE__ */ new Set([
20
+ "SQLITE_BUSY",
21
+ "SQLITE_LOCKED",
22
+ "5",
23
+ "6"
24
+ ]), T = (e) => {
25
+ let t = e;
26
+ for (let e = 0; e < 5 && typeof t == "object" && t; e++) {
27
+ let e = t.code;
28
+ if (typeof e == "string") return e;
29
+ if (typeof e == "number") return String(e);
30
+ t = t.cause;
31
+ }
32
+ }, E = (e) => {
33
+ let t = T(e);
34
+ return t !== void 0 && w.has(t);
35
+ }, D = (e) => E(e) ? "retry" : "noRetry", O = l({ scope: "voltro:sqlite" }), k = async (e) => {
36
+ let t = e.tracerLayer ? i.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, n = a.make(t);
37
+ return new M(await n.runPromise(u), n, null, void 0, e.isRetryable ?? E, e.systemName ?? "sqlite", e.wrapTransaction);
38
+ }, A = (e) => e, j = (e) => {
39
+ if (typeof e != "object" || !e) return e;
40
+ let t = {};
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
+ return t;
43
+ }, M = class e {
44
+ sql;
45
+ runtime;
46
+ namespace;
47
+ isRetryable;
48
+ systemName;
49
+ wrapTransaction;
50
+ emitter;
51
+ inflightTxns = 0;
52
+ constructor(e, t, n = null, r, i = E, a = "sqlite", o = A) {
53
+ this.sql = e, this.runtime = t, this.namespace = n, this.isRetryable = i, this.systemName = a, this.wrapTransaction = o, this.emitter = r ?? new c();
54
+ }
55
+ withNamespace(t) {
56
+ return t === this.namespace ? this : new e(this.sql, this.runtime, t, this.emitter, this.isRetryable, this.systemName, this.wrapTransaction);
57
+ }
58
+ nsT(e) {
59
+ return y(this.namespace, e);
60
+ }
61
+ async attachNamespace(e, t) {
62
+ let n = this.sql, r = t ?? `${e}.db`;
63
+ await this.runtime.runPromise(n`ATTACH DATABASE ${r} AS ${n(e)}`);
64
+ }
65
+ async executeQuery(e, t) {
66
+ let n = g(e, this.sql, this.namespace), i = t ? r.provideService(n, d, t) : n;
67
+ return _(await this.runtime.runPromise(i), e.table, "sqlite");
68
+ }
69
+ async executeInsert(e, t, n, i) {
70
+ let a = this.sql, o = a`INSERT INTO ${a(this.nsT(e))} ${a.insert(j(t))} RETURNING *`, s = n ? r.provideService(o, d, n) : o, c = (await this.runtime.runPromise(s))[0];
71
+ if (!c) throw Error(`SqliteStore.insert: no row returned for table '${e}'`);
72
+ return this.routeEvent({
73
+ table: e,
74
+ op: "insert",
75
+ old: null,
76
+ new: c
77
+ }, i), c;
78
+ }
79
+ async executeUpdate(e, t, n, i, a) {
80
+ let o = this.sql, s = o`UPDATE ${o(this.nsT(e))} SET ${o.update(j(n))} WHERE ${o("id")} = ${t} RETURNING *`, c = i ? r.provideService(s, d, i) : s, l = (await this.runtime.runPromise(c))[0];
81
+ return l ? (this.routeEvent({
82
+ table: e,
83
+ op: "update",
84
+ old: null,
85
+ new: l
86
+ }, a), l) : null;
87
+ }
88
+ async executeInsertMany(e, t, n, i) {
89
+ if (t.length === 0) return [];
90
+ let a = this.sql, o = t.map((e) => j(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);
91
+ for (let t of l) this.routeEvent({
92
+ table: e,
93
+ op: "insert",
94
+ old: null,
95
+ new: t
96
+ }, i);
97
+ return l;
98
+ }
99
+ async executePatchJson(e, t, n, i, a, o) {
100
+ let s = this.sql, c = n.split("."), l = c[0], u = c.slice(1), f = JSON.stringify(i ?? null), p = u.length === 0 ? s`${s(l)} = json_patch(COALESCE(${s(l)}, '{}'), json(${f}))` : s`${s(l)} = json_set(COALESCE(${s(l)}, '{}'), ${`$.${u.join(".")}`}, json(${f}))`, m = s`UPDATE ${s(this.nsT(e))} SET ${p} WHERE ${s("id")} = ${t} RETURNING *`, h = a ? r.provideService(m, d, a) : m, g = (await this.runtime.runPromise(h))[0];
101
+ return g ? (this.routeEvent({
102
+ table: e,
103
+ op: "update",
104
+ old: null,
105
+ new: g
106
+ }, o), g) : null;
107
+ }
108
+ async executeDelete(e, t, n, i) {
109
+ let a = this.sql, o = a`DELETE FROM ${a(this.nsT(e))} WHERE ${a("id")} = ${t} RETURNING *`, s = n ? r.provideService(o, d, n) : o, c = (await this.runtime.runPromise(s))[0];
110
+ return c ? (this.routeEvent({
111
+ table: e,
112
+ op: "delete",
113
+ old: c,
114
+ new: null
115
+ }, i), !0) : !1;
116
+ }
117
+ routeEvent(e, t) {
118
+ t === null ? this.emitter.emit("change", e) : t.push(e);
119
+ }
120
+ async executeUpsert(e, t, n, i, a) {
121
+ if (typeof n.update == "function") {
122
+ let r = await this.findByConflict(e, t, n.conflictColumns, i);
123
+ return r ? await this.executeUpdate(e, r.id, n.update(r), i, a) ?? r : this.executeInsert(e, t, i, a);
124
+ }
125
+ 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(j(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];
126
+ if (!m) throw Error(`SqliteStore.upsert: no row returned for table '${e}'`);
127
+ let h = t.id !== void 0 && t.id === m.id ? "insert" : "update";
128
+ return this.routeEvent({
129
+ table: e,
130
+ op: h,
131
+ old: null,
132
+ new: m
133
+ }, a), m;
134
+ }
135
+ async executeInsertIgnore(e, t, n, i, a) {
136
+ let o = this.sql, s = n.conflictColumns.map((e) => o`${o(e)}`), c = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(j(t))} ON CONFLICT (${o.csv(s)}) DO NOTHING RETURNING *`, l = i ? r.provideService(c, d, i) : c, u = (await this.runtime.runPromise(l))[0];
137
+ if (u) return this.routeEvent({
138
+ table: e,
139
+ op: "insert",
140
+ old: null,
141
+ new: u
142
+ }, a), u;
143
+ let f = await this.findByConflict(e, t, n.conflictColumns, i);
144
+ if (!f) throw Error(`SqliteStore.insertIgnore: conflict fired but no existing row found in '${e}'`);
145
+ return f;
146
+ }
147
+ async findByConflict(e, t, n, i) {
148
+ if (n.length === 0) return;
149
+ 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, d, i) : s;
150
+ return (await this.runtime.runPromise(c))[0];
151
+ }
152
+ query(e) {
153
+ return this.runWithEager(e, null);
154
+ }
155
+ raw(e, t) {
156
+ let n = h(e, this.sql);
157
+ return this.runtime.runPromise(n);
158
+ }
159
+ async runWithEager(e, t) {
160
+ if (!v(e)) return this.executeQuery(e, t);
161
+ let n = this.namespace === null ? p(e, this.sql, "sqlite") : null;
162
+ if (n !== null) try {
163
+ let e = t ? r.provideService(n.fragment, d, t) : n.fragment, i = await this.runtime.runPromise(e);
164
+ return n.decode(i);
165
+ } catch (e) {
166
+ O.warn("sqlite JSON-agg eager-load failed; falling back to walker", { err: e });
167
+ }
168
+ return f(await this.executeQuery(e, t), e.eager, e.sourceTable ?? b(e.table), (e) => this.executeQuery(e, t));
169
+ }
170
+ getInternalRunWithEager() {
171
+ return (e, t) => this.runWithEager(e, t);
172
+ }
173
+ insert(e, t) {
174
+ return this.executeInsert(e, t, null, null);
175
+ }
176
+ insertMany(e, t) {
177
+ return this.executeInsertMany(e, t, null, null);
178
+ }
179
+ patchJson(e, t, n, r) {
180
+ return this.executePatchJson(e, t, n, r, null, null);
181
+ }
182
+ update(e, t, n) {
183
+ return this.executeUpdate(e, t, n, null, null);
184
+ }
185
+ delete(e, t) {
186
+ return this.executeDelete(e, t, null, null);
187
+ }
188
+ async updateMany(e, t, n) {
189
+ let r = this.sql, i = m(n.where, r, this.namespace), a = r`UPDATE ${r(this.nsT(e))} SET ${r.update(j(t))} WHERE ${i} RETURNING *`, o = await this.runtime.runPromise(a);
190
+ for (let t of o) this.routeEvent({
191
+ table: e,
192
+ op: "update",
193
+ old: null,
194
+ new: t
195
+ }, null);
196
+ return o.length;
197
+ }
198
+ async deleteMany(e, t) {
199
+ let n = this.sql, r = m(t.where, n, this.namespace), i = n`DELETE FROM ${n(this.nsT(e))} WHERE ${r} RETURNING *`, a = await this.runtime.runPromise(i);
200
+ for (let t of a) this.routeEvent({
201
+ table: e,
202
+ op: "delete",
203
+ old: t,
204
+ new: null
205
+ }, null);
206
+ return a.length;
207
+ }
208
+ upsert(e, t, n) {
209
+ return this.executeUpsert(e, t, n, null, null);
210
+ }
211
+ insertIgnore(e, t, n) {
212
+ return this.executeInsertIgnore(e, t, n, null, null);
213
+ }
214
+ emitChange(e) {
215
+ this.emitter.emit("change", e);
216
+ }
217
+ getInternalExecuteQuery() {
218
+ return this.executeQuery.bind(this);
219
+ }
220
+ getInternalExecuteInsert() {
221
+ return this.executeInsert.bind(this);
222
+ }
223
+ getInternalExecuteInsertMany() {
224
+ return this.executeInsertMany.bind(this);
225
+ }
226
+ getInternalExecutePatchJson() {
227
+ return this.executePatchJson.bind(this);
228
+ }
229
+ getInternalExecuteUpdate() {
230
+ return this.executeUpdate.bind(this);
231
+ }
232
+ getInternalExecuteDelete() {
233
+ return this.executeDelete.bind(this);
234
+ }
235
+ getInternalExecuteUpsert() {
236
+ return this.executeUpsert.bind(this);
237
+ }
238
+ getInternalExecuteInsertIgnore() {
239
+ return this.executeInsertIgnore.bind(this);
240
+ }
241
+ async transactional(e) {
242
+ this.inflightTxns++;
243
+ let t = 0, n = r.suspend(() => {
244
+ let n = ++t;
245
+ return this.sql.withTransaction(r.flatMap(r.serviceOption(d), (t) => {
246
+ if (o.isNone(t)) return r.fail(/* @__PURE__ */ Error("SqliteStore.transactional: TransactionConnection unexpectedly missing."));
247
+ let i = new N(this, t.value);
248
+ return r.tryPromise({
249
+ try: () => e(i).then((e) => ({
250
+ result: e,
251
+ view: i,
252
+ attempt: n
253
+ })),
254
+ catch: (e) => e
255
+ });
256
+ })).pipe(r.catchAllDefect((e) => this.isRetryable(e) ? r.fail(e) : r.die(e)));
257
+ }), 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: {
258
+ "db.system": this.systemName,
259
+ "db.operation": "transaction"
260
+ } }));
261
+ try {
262
+ let e = await this.runtime.runPromise(this.wrapTransaction(a));
263
+ return e.view.commitEvents(), e.result;
264
+ } finally {
265
+ this.inflightTxns--;
266
+ }
267
+ }
268
+ onChange(e) {
269
+ return this.emitter.on("change", e), () => {
270
+ this.emitter.off("change", e);
271
+ };
272
+ }
273
+ injectExternalChange(e) {
274
+ this.emitter.emit("change", {
275
+ ...e,
276
+ origin: "injected"
277
+ });
278
+ }
279
+ run(e) {
280
+ return this.runtime.runPromise(e);
281
+ }
282
+ async close(e = 5e3) {
283
+ if (this.inflightTxns > 0) {
284
+ let t = Date.now() + e;
285
+ for (; this.inflightTxns > 0 && Date.now() < t;) await new Promise((e) => setTimeout(e, 25));
286
+ this.inflightTxns > 0 && O.warn("close: grace period expired with in-flight transactions", {
287
+ gracePeriodMs: e,
288
+ inflight: this.inflightTxns
289
+ });
290
+ }
291
+ await this.runtime.dispose();
292
+ }
293
+ async ping() {
294
+ await this.runtime.runPromise(this.sql`SELECT 1`);
295
+ }
296
+ }, N = class {
297
+ parent;
298
+ txn;
299
+ events = [];
300
+ committed = !1;
301
+ constructor(e, t) {
302
+ this.parent = e, this.txn = t;
303
+ }
304
+ query(e) {
305
+ return this.parent.getInternalRunWithEager()(e, this.txn);
306
+ }
307
+ insert(e, t) {
308
+ return this.parent.getInternalExecuteInsert()(e, t, this.txn, this.events);
309
+ }
310
+ insertMany(e, t) {
311
+ return this.parent.getInternalExecuteInsertMany()(e, t, this.txn, this.events);
312
+ }
313
+ patchJson(e, t, n, r) {
314
+ return this.parent.getInternalExecutePatchJson()(e, t, n, r, this.txn, this.events);
315
+ }
316
+ update(e, t, n) {
317
+ return this.parent.getInternalExecuteUpdate()(e, t, n, this.txn, this.events);
318
+ }
319
+ delete(e, t) {
320
+ return this.parent.getInternalExecuteDelete()(e, t, this.txn, this.events);
321
+ }
322
+ async updateMany(e, t, n) {
323
+ let r = await this.query({
324
+ table: e,
325
+ predicate: n.where,
326
+ order: [],
327
+ take: void 0,
328
+ skip: void 0,
329
+ projection: ["id"]
330
+ }), i = 0;
331
+ for (let n of r) await this.update(e, n.id, t) && i++;
332
+ return i;
333
+ }
334
+ async deleteMany(e, t) {
335
+ let n = await this.query({
336
+ table: e,
337
+ predicate: t.where,
338
+ order: [],
339
+ take: void 0,
340
+ skip: void 0,
341
+ projection: ["id"]
342
+ }), r = 0;
343
+ for (let t of n) await this.delete(e, t.id) && r++;
344
+ return r;
345
+ }
346
+ upsert(e, t, n) {
347
+ return this.parent.getInternalExecuteUpsert()(e, t, n, this.txn, this.events);
348
+ }
349
+ insertIgnore(e, t, n) {
350
+ return this.parent.getInternalExecuteInsertIgnore()(e, t, n, this.txn, this.events);
351
+ }
352
+ transactional(e) {
353
+ return Promise.reject(/* @__PURE__ */ Error("SqliteStore.transactional: nested transactions are not supported. Mutations should execute a single top-level transaction."));
354
+ }
355
+ onChange(e) {
356
+ throw Error("SqliteTransactionalView.onChange: subscribing from inside a transaction is not supported.");
357
+ }
358
+ injectExternalChange(e) {
359
+ throw Error("SqliteTransactionalView.injectExternalChange: injecting external changes from inside a transaction is not supported.");
360
+ }
361
+ commitEvents() {
362
+ if (!this.committed) {
363
+ this.committed = !0;
364
+ for (let e of this.events) this.parent.emitChange(e);
365
+ this.events.length = 0;
366
+ }
367
+ }
368
+ }, P = {
369
+ id: "sqlite",
370
+ makeSqlLayer: (e) => C(e),
371
+ makeStore: (e) => k(e),
372
+ compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
373
+ retryFilter: D
374
+ };
375
+ //#endregion
376
+ export { e as SqliteClient, S as connectionFromConfig, x as makeSqliteSqlLayer, C as makeSqliteSqlLayerFromConfig, k as makeSqliteStore, P as sqliteDialect, D as sqliteRetryFilter };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@voltro/sql-sqlite",
3
+ "version": "0.1.0",
4
+ "description": "SQLite dialect adapter for Voltro's cross-dialect DataStore (single-process; in-process change events).",
5
+ "keywords": [
6
+ "voltro",
7
+ "typescript",
8
+ "framework"
9
+ ],
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "homepage": "https://voltro.dev",
12
+ "bugs": {
13
+ "email": "support@voltro.dev"
14
+ },
15
+ "author": {
16
+ "name": "Voltro UG",
17
+ "url": "https://voltro.dev"
18
+ },
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "main": "./dist/index.js",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "sideEffects": false,
31
+ "engines": {
32
+ "node": ">=24.0.0"
33
+ },
34
+ "dependencies": {
35
+ "@effect/sql": "^0.51.1",
36
+ "@effect/sql-sqlite-node": "^0.52.0",
37
+ "@voltro/database": "0.1.0",
38
+ "@voltro/logger": "0.1.0"
39
+ },
40
+ "peerDependencies": {
41
+ "effect": "^3.21.4"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }