@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.
- package/CHANGELOG.md +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +1571 -0
- package/dist/index.d.ts +187 -0
- package/dist/index.js +376 -0
- package/package.json +46 -0
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|