@orkestrel/sqlite 0.0.1 → 0.0.2
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/README.md +8 -4
- package/dist/src/server/index.cjs +73 -19
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +316 -0
- package/dist/src/server/index.d.ts +316 -7
- package/dist/src/server/index.js +339 -0
- package/dist/src/server/index.js.map +1 -0
- package/package.json +13 -6
- package/dist/src/server/SQLiteDatabase.d.ts +0 -29
- package/dist/src/server/SQLiteStatement.d.ts +0 -23
- package/dist/src/server/constants.d.ts +0 -18
- package/dist/src/server/errors.d.ts +0 -34
- package/dist/src/server/factories.d.ts +0 -25
- package/dist/src/server/helpers.d.ts +0 -38
- package/dist/src/server/types.d.ts +0 -101
|
@@ -1,7 +1,316 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { StatementSync } from 'node:sqlite';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize {@link SQLiteParameters} to the binding shape a native `StatementSync`
|
|
5
|
+
* call expects.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Positional parameters (an array) bind to `?` placeholders and are spread into
|
|
9
|
+
* the call; named parameters (a record) bind to bare `:name` placeholders and are
|
|
10
|
+
* passed as a single leading object. Returning a discriminated result keeps the
|
|
11
|
+
* `SQLiteStatement` dispatch typed against the native overloads without `as`.
|
|
12
|
+
*
|
|
13
|
+
* @param parameters - The wrapper parameters, or `undefined` for none
|
|
14
|
+
* @returns `{ positional }` for an array (empty when omitted) or `{ named }` for a record
|
|
15
|
+
*/
|
|
16
|
+
export declare function bindParameters(parameters?: SQLiteParameters): {
|
|
17
|
+
readonly positional: readonly SQLiteValue[];
|
|
18
|
+
} | {
|
|
19
|
+
readonly named: Readonly<Record<string, SQLiteValue>>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a synchronous SQLite database over `node:sqlite`.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* The wrapper connects lazily — call `connect` (or it is required by the first
|
|
27
|
+
* operation, which throws `CLOSED` until then). This is the lower-level native
|
|
28
|
+
* handle a higher-level typed database engine would be built on; here it ships
|
|
29
|
+
* as the standalone, server-native SQLite surface.
|
|
30
|
+
*
|
|
31
|
+
* @param options - The database `path` (a file path, or `':memory:'` by default)
|
|
32
|
+
* @returns A typed {@link SQLiteDatabaseInterface}
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { createSQLiteDatabase } from '@orkestrel/sqlite'
|
|
37
|
+
*
|
|
38
|
+
* const db = createSQLiteDatabase({ path: ':memory:' })
|
|
39
|
+
* db.connect()
|
|
40
|
+
* db.exec('CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT)')
|
|
41
|
+
* db.prepare('INSERT INTO users VALUES (?, ?)').run(['u1', 'Ada'])
|
|
42
|
+
* db.prepare('SELECT name FROM users WHERE id = ?').get(['u1']) // { name: 'Ada' }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function createSQLiteDatabase(options?: SQLiteDatabaseOptions): SQLiteDatabaseInterface;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether a value is a {@link SQLiteError}.
|
|
49
|
+
*
|
|
50
|
+
* @param value - The value to test
|
|
51
|
+
* @returns `true` when `value` is a `SQLiteError`
|
|
52
|
+
*/
|
|
53
|
+
export declare function isSQLiteError(value: unknown): value is SQLiteError;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* SQLite result code for a locked-database fault (`SQLITE_BUSY`).
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* A native `errcode` packs the primary result in its low byte, with extended codes
|
|
60
|
+
* in the high bits, so the low byte is masked off before comparing. Read only by
|
|
61
|
+
* `wrapError`.
|
|
62
|
+
*/
|
|
63
|
+
export declare const SQLITE_BUSY = 5;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* SQLite result code for a constraint violation (`SQLITE_CONSTRAINT`).
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* A native `errcode` packs the primary result in its low byte, with extended codes
|
|
70
|
+
* in the high bits (e.g. `SQLITE_CONSTRAINT_UNIQUE`), so the low byte is masked off
|
|
71
|
+
* before comparing. Read only by `wrapError`.
|
|
72
|
+
*/
|
|
73
|
+
export declare const SQLITE_CONSTRAINT = 19;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A synchronous SQLite database over `node:sqlite`'s `DatabaseSync`.
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* Created by `createSQLiteDatabase`. It connects lazily (`connect` opens the
|
|
80
|
+
* underlying `DatabaseSync`, idempotent) and every operation routes through a
|
|
81
|
+
* private gate that throws a `CLOSED` `SQLiteError` before `connect` or after
|
|
82
|
+
* `close`. `exec` runs result-less SQL; `prepare` compiles a `SQLiteStatement`;
|
|
83
|
+
* `transaction` wraps a scope in `BEGIN` / `COMMIT`, rolling back on a throw;
|
|
84
|
+
* `begin` / `commit` / `rollback` expose those same primitives directly for a
|
|
85
|
+
* long-lived or externally-driven transaction; `pragma` reads (or sets then
|
|
86
|
+
* reads) one PRAGMA value — `name` is trusted
|
|
87
|
+
* internal use only, never untrusted input, since pragma names cannot be bound
|
|
88
|
+
* as parameters. `transacting` reports whether a transaction is currently open
|
|
89
|
+
* (node:sqlite's `isTransaction`), `false` when not connected. A native fault
|
|
90
|
+
* surfaces as a `SQLiteError` mapped at the boundary.
|
|
91
|
+
*/
|
|
92
|
+
export declare class SQLiteDatabase implements SQLiteDatabaseInterface {
|
|
93
|
+
#private;
|
|
94
|
+
constructor(options: SQLiteDatabaseOptions);
|
|
95
|
+
get path(): string;
|
|
96
|
+
get connected(): boolean;
|
|
97
|
+
get transacting(): boolean;
|
|
98
|
+
connect(): void;
|
|
99
|
+
close(): void;
|
|
100
|
+
/** Close the connection — enables `using db = createSQLiteDatabase(...)`. */
|
|
101
|
+
[Symbol.dispose](): void;
|
|
102
|
+
exec(sql: string): void;
|
|
103
|
+
prepare(sql: string): SQLiteStatementInterface;
|
|
104
|
+
transaction<R>(scope: () => R): R;
|
|
105
|
+
begin(): void;
|
|
106
|
+
commit(): void;
|
|
107
|
+
rollback(): void;
|
|
108
|
+
pragma(name: string, value?: string | number): SQLiteValue | undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A synchronous SQLite database over `node:sqlite`'s `DatabaseSync` — a lean,
|
|
113
|
+
* typed, zero-dependency layer exposing prepared statements, transactions, and
|
|
114
|
+
* pragmas. Synchronous because `node:sqlite` is; the SQLite *driver* (Chunk 3)
|
|
115
|
+
* adapts it to the async `DriverInterface`.
|
|
116
|
+
*
|
|
117
|
+
* @remarks
|
|
118
|
+
* Connects lazily — `connect` opens the underlying `DatabaseSync` (idempotent),
|
|
119
|
+
* and every operation requires an open connection, throwing a `CLOSED`
|
|
120
|
+
* {@link SQLiteError} before `connect` or after `close`. `exec` runs SQL with no
|
|
121
|
+
* results (DDL, pragmas); `prepare` compiles a {@link SQLiteStatementInterface};
|
|
122
|
+
* `transaction` runs a scope between `BEGIN` and `COMMIT`, rolling back on a
|
|
123
|
+
* throw; `pragma` reads (or sets then reads) a single PRAGMA value — `name` is
|
|
124
|
+
* trusted internal use only, never untrusted input, since pragma names cannot
|
|
125
|
+
* be bound as parameters. `transacting` reports whether a transaction is
|
|
126
|
+
* currently open on this connection — node:sqlite's `isTransaction` (wraps
|
|
127
|
+
* `sqlite3_get_autocommit()`); `false` when not connected. `begin` / `commit` /
|
|
128
|
+
* `rollback` are the same `BEGIN` / `COMMIT` / `ROLLBACK` primitives
|
|
129
|
+
* `transaction` composes internally, exposed directly for a long-lived or
|
|
130
|
+
* externally-driven transaction that a single synchronous scope cannot
|
|
131
|
+
* express — `transaction(scope)` remains the right tool whenever the whole
|
|
132
|
+
* transaction fits in one synchronous scope. `[Symbol.dispose]` closes the
|
|
133
|
+
* connection (same as `close`), enabling `using` to release it
|
|
134
|
+
* deterministically at the end of a block.
|
|
135
|
+
*/
|
|
136
|
+
export declare interface SQLiteDatabaseInterface {
|
|
137
|
+
readonly path: string;
|
|
138
|
+
readonly connected: boolean;
|
|
139
|
+
readonly transacting: boolean;
|
|
140
|
+
connect(): void;
|
|
141
|
+
close(): void;
|
|
142
|
+
exec(sql: string): void;
|
|
143
|
+
prepare(sql: string): SQLiteStatementInterface;
|
|
144
|
+
transaction<R>(scope: () => R): R;
|
|
145
|
+
/**
|
|
146
|
+
* Open a transaction (`BEGIN`). Throws the native fault — including a
|
|
147
|
+
* nested `BEGIN` while one is already open — as a {@link SQLiteError}; a
|
|
148
|
+
* caller composing its own transaction alongside others should branch on
|
|
149
|
+
* {@link SQLiteDatabaseInterface.transacting} first rather than catch this
|
|
150
|
+
* (see the Practices section in `guides/src/sqlite.md`).
|
|
151
|
+
*/
|
|
152
|
+
begin(): void;
|
|
153
|
+
/** Commit the currently open transaction (`COMMIT`); throws the native fault as a {@link SQLiteError} when none is open. */
|
|
154
|
+
commit(): void;
|
|
155
|
+
/** Roll back the currently open transaction (`ROLLBACK`); throws the native fault as a {@link SQLiteError} when none is open. */
|
|
156
|
+
rollback(): void;
|
|
157
|
+
pragma(name: string, value?: string | number): SQLiteValue | undefined;
|
|
158
|
+
[Symbol.dispose](): void;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Options for `createSQLiteDatabase`.
|
|
163
|
+
*
|
|
164
|
+
* @remarks
|
|
165
|
+
* `path` is the database file path, or the special name `':memory:'` for an
|
|
166
|
+
* in-memory database (the default when omitted). `readonly` opens the
|
|
167
|
+
* connection read-only (native `readOnly`) — an absent file fails to open
|
|
168
|
+
* rather than being created. `timeout` is the busy-timeout in milliseconds
|
|
169
|
+
* (native `timeout`) — how long SQLite retries a locked database before
|
|
170
|
+
* failing with a `BUSY` {@link SQLiteError}; defaults to `0` (fail
|
|
171
|
+
* immediately) when omitted. `foreignKeys` enables foreign-key constraint
|
|
172
|
+
* enforcement (native `enableForeignKeyConstraints`); `node:sqlite` defaults
|
|
173
|
+
* this to `true` when omitted. `bigints` reads `INTEGER` columns back as
|
|
174
|
+
* `bigint` (native `readBigInts`) — writes already accept `bigint` regardless
|
|
175
|
+
* of this option, so a stored integer beyond `Number.MAX_SAFE_INTEGER` throws
|
|
176
|
+
* on read unless `bigints` is enabled; enabling it returns EVERY integer
|
|
177
|
+
* column as `bigint`, not just out-of-range ones, closing that read/write
|
|
178
|
+
* asymmetry at the cost of `bigint` values for ordinary small integers too.
|
|
179
|
+
*/
|
|
180
|
+
export declare interface SQLiteDatabaseOptions {
|
|
181
|
+
readonly path?: string;
|
|
182
|
+
readonly readonly?: boolean;
|
|
183
|
+
readonly timeout?: number;
|
|
184
|
+
readonly foreignKeys?: boolean;
|
|
185
|
+
readonly bigints?: boolean;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* An error thrown by the SQLite wrapper.
|
|
190
|
+
*
|
|
191
|
+
* @remarks
|
|
192
|
+
* Carries a {@link SQLiteErrorCode} and an optional `context` record (e.g. the
|
|
193
|
+
* native SQLite `errcode`). Construct it directly for the `CLOSED`
|
|
194
|
+
* wrapper-lifecycle fault; the internal `wrapError` maps a native `node:sqlite`
|
|
195
|
+
* error to the right code at the boundary. Narrow a caught value with
|
|
196
|
+
* {@link isSQLiteError}.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* try {
|
|
201
|
+
* statement.run({ id: 'u1' })
|
|
202
|
+
* } catch (error) {
|
|
203
|
+
* if (isSQLiteError(error) && error.code === 'CONSTRAINT') {
|
|
204
|
+
* // a UNIQUE / PRIMARY KEY violation
|
|
205
|
+
* }
|
|
206
|
+
* }
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export declare class SQLiteError extends Error {
|
|
210
|
+
readonly code: SQLiteErrorCode;
|
|
211
|
+
readonly context?: Readonly<Record<string, unknown>>;
|
|
212
|
+
constructor(code: SQLiteErrorCode, message: string, context?: Readonly<Record<string, unknown>>);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* A machine-readable {@link SQLiteError} code.
|
|
217
|
+
*
|
|
218
|
+
* @remarks
|
|
219
|
+
* `'BUSY'` is retryable — it means a locked database was still held by another
|
|
220
|
+
* connection when the `timeout` (see {@link SQLiteDatabaseOptions}) elapsed; a
|
|
221
|
+
* caller may retry the operation, typically after backing off briefly.
|
|
222
|
+
*/
|
|
223
|
+
export declare type SQLiteErrorCode = 'CLOSED' | 'CONSTRAINT' | 'BUSY' | 'UNKNOWN';
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Bind parameters for a prepared statement — positional (an array, bound to `?`)
|
|
227
|
+
* or named (a record, bound to bare `:name` placeholders).
|
|
228
|
+
*/
|
|
229
|
+
export declare type SQLiteParameters = readonly SQLiteValue[] | Readonly<Record<string, SQLiteValue>>;
|
|
230
|
+
|
|
231
|
+
/** A result row — a record of column name to {@link SQLiteValue}. */
|
|
232
|
+
export declare type SQLiteRow = Record<string, SQLiteValue>;
|
|
233
|
+
|
|
234
|
+
/** The outcome of a non-query statement (`INSERT` / `UPDATE` / `DELETE` / DDL). */
|
|
235
|
+
export declare interface SQLiteRunResult {
|
|
236
|
+
readonly changes: number;
|
|
237
|
+
readonly rowid: number;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* A prepared statement over `node:sqlite`'s `StatementSync` — the only way the
|
|
242
|
+
* wrapper runs SQL.
|
|
243
|
+
*
|
|
244
|
+
* @remarks
|
|
245
|
+
* Created by `database.prepare(sql)`, which threads a liveness check (internal —
|
|
246
|
+
* this constructor's second parameter is not part of the documented surface).
|
|
247
|
+
* Bare named parameters are enabled on construction so a record's keys bind
|
|
248
|
+
* without the SQL prefix character. Each method first gates on that liveness
|
|
249
|
+
* check, throwing a `CLOSED` `SQLiteError` once the owning connection has been
|
|
250
|
+
* closed — a statement prepared on a connection that is later closed and then
|
|
251
|
+
* reconnected stays `CLOSED`; a fresh statement must be prepared on the new
|
|
252
|
+
* connection. Each method then binds the optional parameters (an array spread
|
|
253
|
+
* to `?` placeholders, a record passed as a single named object) and executes
|
|
254
|
+
* synchronously, mapping any native fault to a `SQLiteError` — including a
|
|
255
|
+
* mid-stream fault from `iterate`'s lazy native iterator, stepped inside its own
|
|
256
|
+
* try/catch so a fault on a later row is mapped exactly like an eager one. Row
|
|
257
|
+
* values arrive as the native {@link SQLiteRow} types; the typed layer above
|
|
258
|
+
* (the driver) imposes a precise shape through a contract rather than
|
|
259
|
+
* re-narrowing here (AGENTS §14).
|
|
260
|
+
*/
|
|
261
|
+
export declare class SQLiteStatement implements SQLiteStatementInterface {
|
|
262
|
+
#private;
|
|
263
|
+
constructor(statement: StatementSync, closed: () => boolean);
|
|
264
|
+
run(parameters?: SQLiteParameters): SQLiteRunResult;
|
|
265
|
+
get(parameters?: SQLiteParameters): SQLiteRow | undefined;
|
|
266
|
+
all(parameters?: SQLiteParameters): readonly SQLiteRow[];
|
|
267
|
+
iterate(parameters?: SQLiteParameters): IterableIterator<SQLiteRow>;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* A prepared statement — the only way the wrapper runs SQL (no query DSL; the
|
|
272
|
+
* core database layer owns querying, exactly as the IndexedDB wrapper does).
|
|
273
|
+
*
|
|
274
|
+
* @remarks
|
|
275
|
+
* Reached through `database.prepare(sql)`. Each method binds the optional
|
|
276
|
+
* `parameters` (an array spread to positional `?` placeholders, or a record bound
|
|
277
|
+
* to bare named placeholders) and executes synchronously: `run` for a non-query,
|
|
278
|
+
* `get` for the first row, `all` for every row, `iterate` for a lazy stream. A
|
|
279
|
+
* native fault surfaces as a {@link SQLiteError}.
|
|
280
|
+
*/
|
|
281
|
+
export declare interface SQLiteStatementInterface {
|
|
282
|
+
run(parameters?: SQLiteParameters): SQLiteRunResult;
|
|
283
|
+
get(parameters?: SQLiteParameters): SQLiteRow | undefined;
|
|
284
|
+
all(parameters?: SQLiteParameters): readonly SQLiteRow[];
|
|
285
|
+
iterate(parameters?: SQLiteParameters): IterableIterator<SQLiteRow>;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* A value SQLite stores and returns natively — the SQL ↔ JS bridge.
|
|
290
|
+
*
|
|
291
|
+
* @remarks
|
|
292
|
+
* `node:sqlite` maps `NULL` / `INTEGER` / `REAL` / `TEXT` / `BLOB` to exactly
|
|
293
|
+
* these JS types (integers arrive as `number`, or `bigint` only past 2^53).
|
|
294
|
+
*/
|
|
295
|
+
export declare type SQLiteValue = null | number | bigint | string | Uint8Array;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Convert a thrown native `node:sqlite` error into a typed {@link SQLiteError}.
|
|
299
|
+
*
|
|
300
|
+
* @remarks
|
|
301
|
+
* The single boundary mapping for the wrapper. The thrown value arrives as
|
|
302
|
+
* `unknown` and is narrowed with `isObject` + the `in` operator (never `as`, and
|
|
303
|
+
* not `isRecord` — a native `node:sqlite` error is an `Error` instance, so its
|
|
304
|
+
* prototype fails the plain-record test) to read its `errcode` — a numeric SQLite
|
|
305
|
+
* result code whose low byte identifies a constraint violation
|
|
306
|
+
* (`SQLITE_CONSTRAINT`), mapped to code `'CONSTRAINT'`; a locked-database fault
|
|
307
|
+
* (`SQLITE_BUSY`) is mapped to `'BUSY'`; anything else is `'UNKNOWN'`. The
|
|
308
|
+
* original message is preserved and `{ errcode }` carried in `context` when
|
|
309
|
+
* present. An already-typed `SQLiteError` is returned unchanged.
|
|
310
|
+
*
|
|
311
|
+
* @param error - The thrown value to convert
|
|
312
|
+
* @returns The equivalent `SQLiteError`
|
|
313
|
+
*/
|
|
314
|
+
export declare function wrapError(error: unknown): SQLiteError;
|
|
315
|
+
|
|
316
|
+
export { }
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { isArray, isObject, isPromiseLike } from "@orkestrel/contract";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
//#region src/server/constants.ts
|
|
4
|
+
/**
|
|
5
|
+
* SQLite result code for a constraint violation (`SQLITE_CONSTRAINT`).
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* A native `errcode` packs the primary result in its low byte, with extended codes
|
|
9
|
+
* in the high bits (e.g. `SQLITE_CONSTRAINT_UNIQUE`), so the low byte is masked off
|
|
10
|
+
* before comparing. Read only by `wrapError`.
|
|
11
|
+
*/
|
|
12
|
+
var SQLITE_CONSTRAINT = 19;
|
|
13
|
+
/**
|
|
14
|
+
* SQLite result code for a locked-database fault (`SQLITE_BUSY`).
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* A native `errcode` packs the primary result in its low byte, with extended codes
|
|
18
|
+
* in the high bits, so the low byte is masked off before comparing. Read only by
|
|
19
|
+
* `wrapError`.
|
|
20
|
+
*/
|
|
21
|
+
var SQLITE_BUSY = 5;
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/server/errors.ts
|
|
24
|
+
/**
|
|
25
|
+
* An error thrown by the SQLite wrapper.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* Carries a {@link SQLiteErrorCode} and an optional `context` record (e.g. the
|
|
29
|
+
* native SQLite `errcode`). Construct it directly for the `CLOSED`
|
|
30
|
+
* wrapper-lifecycle fault; the internal `wrapError` maps a native `node:sqlite`
|
|
31
|
+
* error to the right code at the boundary. Narrow a caught value with
|
|
32
|
+
* {@link isSQLiteError}.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* try {
|
|
37
|
+
* statement.run({ id: 'u1' })
|
|
38
|
+
* } catch (error) {
|
|
39
|
+
* if (isSQLiteError(error) && error.code === 'CONSTRAINT') {
|
|
40
|
+
* // a UNIQUE / PRIMARY KEY violation
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
var SQLiteError = class extends Error {
|
|
46
|
+
code;
|
|
47
|
+
context;
|
|
48
|
+
constructor(code, message, context) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = "SQLiteError";
|
|
51
|
+
this.code = code;
|
|
52
|
+
this.context = context;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Whether a value is a {@link SQLiteError}.
|
|
57
|
+
*
|
|
58
|
+
* @param value - The value to test
|
|
59
|
+
* @returns `true` when `value` is a `SQLiteError`
|
|
60
|
+
*/
|
|
61
|
+
function isSQLiteError(value) {
|
|
62
|
+
return value instanceof SQLiteError;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/server/helpers.ts
|
|
66
|
+
/**
|
|
67
|
+
* Convert a thrown native `node:sqlite` error into a typed {@link SQLiteError}.
|
|
68
|
+
*
|
|
69
|
+
* @remarks
|
|
70
|
+
* The single boundary mapping for the wrapper. The thrown value arrives as
|
|
71
|
+
* `unknown` and is narrowed with `isObject` + the `in` operator (never `as`, and
|
|
72
|
+
* not `isRecord` — a native `node:sqlite` error is an `Error` instance, so its
|
|
73
|
+
* prototype fails the plain-record test) to read its `errcode` — a numeric SQLite
|
|
74
|
+
* result code whose low byte identifies a constraint violation
|
|
75
|
+
* (`SQLITE_CONSTRAINT`), mapped to code `'CONSTRAINT'`; a locked-database fault
|
|
76
|
+
* (`SQLITE_BUSY`) is mapped to `'BUSY'`; anything else is `'UNKNOWN'`. The
|
|
77
|
+
* original message is preserved and `{ errcode }` carried in `context` when
|
|
78
|
+
* present. An already-typed `SQLiteError` is returned unchanged.
|
|
79
|
+
*
|
|
80
|
+
* @param error - The thrown value to convert
|
|
81
|
+
* @returns The equivalent `SQLiteError`
|
|
82
|
+
*/
|
|
83
|
+
function wrapError(error) {
|
|
84
|
+
if (error instanceof SQLiteError) return error;
|
|
85
|
+
const errcode = isObject(error) && "errcode" in error && typeof error.errcode === "number" ? error.errcode : void 0;
|
|
86
|
+
const message = error instanceof Error ? error.message : "Unknown SQLite error";
|
|
87
|
+
return new SQLiteError(errcode !== void 0 && (errcode & 255) === 19 ? "CONSTRAINT" : errcode !== void 0 && (errcode & 255) === 5 ? "BUSY" : "UNKNOWN", message, errcode !== void 0 ? { errcode } : void 0);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Normalize {@link SQLiteParameters} to the binding shape a native `StatementSync`
|
|
91
|
+
* call expects.
|
|
92
|
+
*
|
|
93
|
+
* @remarks
|
|
94
|
+
* Positional parameters (an array) bind to `?` placeholders and are spread into
|
|
95
|
+
* the call; named parameters (a record) bind to bare `:name` placeholders and are
|
|
96
|
+
* passed as a single leading object. Returning a discriminated result keeps the
|
|
97
|
+
* `SQLiteStatement` dispatch typed against the native overloads without `as`.
|
|
98
|
+
*
|
|
99
|
+
* @param parameters - The wrapper parameters, or `undefined` for none
|
|
100
|
+
* @returns `{ positional }` for an array (empty when omitted) or `{ named }` for a record
|
|
101
|
+
*/
|
|
102
|
+
function bindParameters(parameters) {
|
|
103
|
+
if (parameters === void 0) return { positional: [] };
|
|
104
|
+
if (isArray(parameters)) return { positional: parameters };
|
|
105
|
+
return { named: parameters };
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/server/SQLiteStatement.ts
|
|
109
|
+
/**
|
|
110
|
+
* A prepared statement over `node:sqlite`'s `StatementSync` — the only way the
|
|
111
|
+
* wrapper runs SQL.
|
|
112
|
+
*
|
|
113
|
+
* @remarks
|
|
114
|
+
* Created by `database.prepare(sql)`, which threads a liveness check (internal —
|
|
115
|
+
* this constructor's second parameter is not part of the documented surface).
|
|
116
|
+
* Bare named parameters are enabled on construction so a record's keys bind
|
|
117
|
+
* without the SQL prefix character. Each method first gates on that liveness
|
|
118
|
+
* check, throwing a `CLOSED` `SQLiteError` once the owning connection has been
|
|
119
|
+
* closed — a statement prepared on a connection that is later closed and then
|
|
120
|
+
* reconnected stays `CLOSED`; a fresh statement must be prepared on the new
|
|
121
|
+
* connection. Each method then binds the optional parameters (an array spread
|
|
122
|
+
* to `?` placeholders, a record passed as a single named object) and executes
|
|
123
|
+
* synchronously, mapping any native fault to a `SQLiteError` — including a
|
|
124
|
+
* mid-stream fault from `iterate`'s lazy native iterator, stepped inside its own
|
|
125
|
+
* try/catch so a fault on a later row is mapped exactly like an eager one. Row
|
|
126
|
+
* values arrive as the native {@link SQLiteRow} types; the typed layer above
|
|
127
|
+
* (the driver) imposes a precise shape through a contract rather than
|
|
128
|
+
* re-narrowing here (AGENTS §14).
|
|
129
|
+
*/
|
|
130
|
+
var SQLiteStatement = class {
|
|
131
|
+
#statement;
|
|
132
|
+
#closed;
|
|
133
|
+
constructor(statement, closed) {
|
|
134
|
+
this.#statement = statement;
|
|
135
|
+
this.#closed = closed;
|
|
136
|
+
this.#statement.setAllowBareNamedParameters(true);
|
|
137
|
+
}
|
|
138
|
+
run(parameters) {
|
|
139
|
+
this.#require();
|
|
140
|
+
try {
|
|
141
|
+
const bound = bindParameters(parameters);
|
|
142
|
+
const result = "named" in bound ? this.#statement.run(bound.named) : this.#statement.run(...bound.positional);
|
|
143
|
+
return {
|
|
144
|
+
changes: Number(result.changes),
|
|
145
|
+
rowid: Number(result.lastInsertRowid)
|
|
146
|
+
};
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw wrapError(error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
get(parameters) {
|
|
152
|
+
this.#require();
|
|
153
|
+
try {
|
|
154
|
+
const bound = bindParameters(parameters);
|
|
155
|
+
return "named" in bound ? this.#statement.get(bound.named) : this.#statement.get(...bound.positional);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw wrapError(error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
all(parameters) {
|
|
161
|
+
this.#require();
|
|
162
|
+
try {
|
|
163
|
+
const bound = bindParameters(parameters);
|
|
164
|
+
return "named" in bound ? this.#statement.all(bound.named) : this.#statement.all(...bound.positional);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw wrapError(error);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
iterate(parameters) {
|
|
170
|
+
this.#require();
|
|
171
|
+
let native;
|
|
172
|
+
try {
|
|
173
|
+
const bound = bindParameters(parameters);
|
|
174
|
+
native = "named" in bound ? this.#statement.iterate(bound.named) : this.#statement.iterate(...bound.positional);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
throw wrapError(error);
|
|
177
|
+
}
|
|
178
|
+
return this.#stream(native);
|
|
179
|
+
}
|
|
180
|
+
#require() {
|
|
181
|
+
if (this.#closed()) throw new SQLiteError("CLOSED", "Statement is closed — its connection was closed; prepare a new statement");
|
|
182
|
+
}
|
|
183
|
+
*#stream(native) {
|
|
184
|
+
for (;;) {
|
|
185
|
+
let result;
|
|
186
|
+
try {
|
|
187
|
+
result = native.next();
|
|
188
|
+
} catch (error) {
|
|
189
|
+
throw wrapError(error);
|
|
190
|
+
}
|
|
191
|
+
if (result.done === true) return;
|
|
192
|
+
yield result.value;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/server/SQLiteDatabase.ts
|
|
198
|
+
/**
|
|
199
|
+
* A synchronous SQLite database over `node:sqlite`'s `DatabaseSync`.
|
|
200
|
+
*
|
|
201
|
+
* @remarks
|
|
202
|
+
* Created by `createSQLiteDatabase`. It connects lazily (`connect` opens the
|
|
203
|
+
* underlying `DatabaseSync`, idempotent) and every operation routes through a
|
|
204
|
+
* private gate that throws a `CLOSED` `SQLiteError` before `connect` or after
|
|
205
|
+
* `close`. `exec` runs result-less SQL; `prepare` compiles a `SQLiteStatement`;
|
|
206
|
+
* `transaction` wraps a scope in `BEGIN` / `COMMIT`, rolling back on a throw;
|
|
207
|
+
* `begin` / `commit` / `rollback` expose those same primitives directly for a
|
|
208
|
+
* long-lived or externally-driven transaction; `pragma` reads (or sets then
|
|
209
|
+
* reads) one PRAGMA value — `name` is trusted
|
|
210
|
+
* internal use only, never untrusted input, since pragma names cannot be bound
|
|
211
|
+
* as parameters. `transacting` reports whether a transaction is currently open
|
|
212
|
+
* (node:sqlite's `isTransaction`), `false` when not connected. A native fault
|
|
213
|
+
* surfaces as a `SQLiteError` mapped at the boundary.
|
|
214
|
+
*/
|
|
215
|
+
var SQLiteDatabase = class {
|
|
216
|
+
#path;
|
|
217
|
+
#readonly;
|
|
218
|
+
#timeout;
|
|
219
|
+
#foreignKeys;
|
|
220
|
+
#bigints;
|
|
221
|
+
#database;
|
|
222
|
+
constructor(options) {
|
|
223
|
+
this.#path = options.path ?? ":memory:";
|
|
224
|
+
this.#readonly = options.readonly;
|
|
225
|
+
this.#timeout = options.timeout;
|
|
226
|
+
this.#foreignKeys = options.foreignKeys;
|
|
227
|
+
this.#bigints = options.bigints;
|
|
228
|
+
}
|
|
229
|
+
get path() {
|
|
230
|
+
return this.#path;
|
|
231
|
+
}
|
|
232
|
+
get connected() {
|
|
233
|
+
return this.#database !== void 0;
|
|
234
|
+
}
|
|
235
|
+
get transacting() {
|
|
236
|
+
return this.#database?.isTransaction ?? false;
|
|
237
|
+
}
|
|
238
|
+
connect() {
|
|
239
|
+
if (this.#database === void 0) this.#database = new DatabaseSync(this.#path, {
|
|
240
|
+
readOnly: this.#readonly,
|
|
241
|
+
timeout: this.#timeout,
|
|
242
|
+
enableForeignKeyConstraints: this.#foreignKeys,
|
|
243
|
+
readBigInts: this.#bigints
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
close() {
|
|
247
|
+
this.#database?.close();
|
|
248
|
+
this.#database = void 0;
|
|
249
|
+
}
|
|
250
|
+
/** Close the connection — enables `using db = createSQLiteDatabase(...)`. */
|
|
251
|
+
[Symbol.dispose]() {
|
|
252
|
+
this.close();
|
|
253
|
+
}
|
|
254
|
+
exec(sql) {
|
|
255
|
+
try {
|
|
256
|
+
this.#require().exec(sql);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
throw wrapError(error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
prepare(sql) {
|
|
262
|
+
try {
|
|
263
|
+
const database = this.#require();
|
|
264
|
+
return new SQLiteStatement(database.prepare(sql), () => this.#database !== database);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
throw wrapError(error);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
transaction(scope) {
|
|
270
|
+
this.begin();
|
|
271
|
+
let result;
|
|
272
|
+
try {
|
|
273
|
+
result = scope();
|
|
274
|
+
} catch (error) {
|
|
275
|
+
try {
|
|
276
|
+
this.rollback();
|
|
277
|
+
} catch {}
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
if (isPromiseLike(result)) {
|
|
281
|
+
try {
|
|
282
|
+
this.rollback();
|
|
283
|
+
} catch {}
|
|
284
|
+
throw new SQLiteError("UNKNOWN", "transaction(scope) requires a synchronous scope — an async or Promise-returning scope cannot commit or roll back before its awaited work runs; use begin() / commit() / rollback() directly for a transaction that spans async caller code");
|
|
285
|
+
}
|
|
286
|
+
this.commit();
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
begin() {
|
|
290
|
+
this.exec("BEGIN");
|
|
291
|
+
}
|
|
292
|
+
commit() {
|
|
293
|
+
this.exec("COMMIT");
|
|
294
|
+
}
|
|
295
|
+
rollback() {
|
|
296
|
+
this.exec("ROLLBACK");
|
|
297
|
+
}
|
|
298
|
+
pragma(name, value) {
|
|
299
|
+
if (value !== void 0) this.exec("PRAGMA " + name + " = " + value);
|
|
300
|
+
const row = this.prepare("PRAGMA " + name).get();
|
|
301
|
+
return row ? Object.values(row)[0] : void 0;
|
|
302
|
+
}
|
|
303
|
+
#require() {
|
|
304
|
+
if (this.#database === void 0) throw new SQLiteError("CLOSED", "Database is not connected — call connect() first");
|
|
305
|
+
return this.#database;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/server/factories.ts
|
|
310
|
+
/**
|
|
311
|
+
* Create a synchronous SQLite database over `node:sqlite`.
|
|
312
|
+
*
|
|
313
|
+
* @remarks
|
|
314
|
+
* The wrapper connects lazily — call `connect` (or it is required by the first
|
|
315
|
+
* operation, which throws `CLOSED` until then). This is the lower-level native
|
|
316
|
+
* handle a higher-level typed database engine would be built on; here it ships
|
|
317
|
+
* as the standalone, server-native SQLite surface.
|
|
318
|
+
*
|
|
319
|
+
* @param options - The database `path` (a file path, or `':memory:'` by default)
|
|
320
|
+
* @returns A typed {@link SQLiteDatabaseInterface}
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```ts
|
|
324
|
+
* import { createSQLiteDatabase } from '@orkestrel/sqlite'
|
|
325
|
+
*
|
|
326
|
+
* const db = createSQLiteDatabase({ path: ':memory:' })
|
|
327
|
+
* db.connect()
|
|
328
|
+
* db.exec('CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT)')
|
|
329
|
+
* db.prepare('INSERT INTO users VALUES (?, ?)').run(['u1', 'Ada'])
|
|
330
|
+
* db.prepare('SELECT name FROM users WHERE id = ?').get(['u1']) // { name: 'Ada' }
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
function createSQLiteDatabase(options) {
|
|
334
|
+
return new SQLiteDatabase(options ?? {});
|
|
335
|
+
}
|
|
336
|
+
//#endregion
|
|
337
|
+
export { SQLITE_BUSY, SQLITE_CONSTRAINT, SQLiteDatabase, SQLiteError, SQLiteStatement, bindParameters, createSQLiteDatabase, isSQLiteError, wrapError };
|
|
338
|
+
|
|
339
|
+
//# sourceMappingURL=index.js.map
|