@orkestrel/database 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 +43 -10
- package/dist/src/browser/index.d.ts +342 -0
- package/dist/src/browser/index.js +686 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/core/index.cjs +2453 -0
- package/dist/src/core/index.cjs.map +1 -0
- package/dist/src/core/index.d.cts +1730 -0
- package/dist/src/core/index.d.ts +1730 -11
- package/dist/src/core/index.js +209 -1152
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +836 -153
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1054 -0
- package/dist/src/server/index.d.ts +1054 -5
- package/dist/src/server/index.js +1663 -0
- package/dist/src/server/index.js.map +1 -0
- package/package.json +40 -16
- package/dist/src/core/Clause.d.ts +0 -30
- package/dist/src/core/Cursor.d.ts +0 -21
- package/dist/src/core/Database.d.ts +0 -80
- package/dist/src/core/Query.d.ts +0 -47
- package/dist/src/core/Table.d.ts +0 -69
- package/dist/src/core/constants.d.ts +0 -22
- package/dist/src/core/drivers/MemoryDriver.d.ts +0 -94
- package/dist/src/core/errors.d.ts +0 -38
- package/dist/src/core/factories.d.ts +0 -43
- package/dist/src/core/helpers.d.ts +0 -383
- package/dist/src/core/types.d.ts +0 -739
- package/dist/src/server/compilers.d.ts +0 -169
- package/dist/src/server/drivers/JSONDriver.d.ts +0 -106
- package/dist/src/server/factories.d.ts +0 -31
- package/dist/src/server/helpers.d.ts +0 -222
- package/dist/src/server/types.d.ts +0 -36
|
@@ -1,128 +1,189 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _orkestrel_contract = require("@orkestrel/contract");
|
|
2
3
|
let node_crypto = require("node:crypto");
|
|
3
|
-
let _src_core = require("../core/index.
|
|
4
|
+
let _src_core = require("../core/index.cjs");
|
|
4
5
|
let node_fs_promises = require("node:fs/promises");
|
|
5
6
|
let node_path = require("node:path");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"boolean",
|
|
9
|
-
"object",
|
|
10
|
-
"array",
|
|
11
|
-
"number",
|
|
12
|
-
"integer",
|
|
13
|
-
"string"
|
|
14
|
-
]);
|
|
15
|
-
/** Determine whether a value is a string. */
|
|
16
|
-
function isString(value) {
|
|
17
|
-
return typeof value === "string";
|
|
18
|
-
}
|
|
19
|
-
/** Determine whether a value is a boolean. */
|
|
20
|
-
function isBoolean(value) {
|
|
21
|
-
return typeof value === "boolean";
|
|
22
|
-
}
|
|
7
|
+
let _orkestrel_sqlite = require("@orkestrel/sqlite");
|
|
8
|
+
//#region src/server/helpers.ts
|
|
23
9
|
/**
|
|
24
|
-
*
|
|
10
|
+
* Generate a fresh unique key — a v4 UUID string, backed by `node:crypto`.
|
|
25
11
|
*
|
|
26
12
|
* @remarks
|
|
27
|
-
*
|
|
28
|
-
*
|
|
13
|
+
* Supply this as {@link import('@orkestrel/database').DatabaseOptions.key} so a table mints
|
|
14
|
+
* a key when a written row lacks its primary-key value. Strings work as keys on
|
|
15
|
+
* every backend; supply your own key values directly to use numeric keys instead.
|
|
16
|
+
*
|
|
17
|
+
* @returns A new UUID string
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const db = createDatabase({ driver, tables, key: generateKey })
|
|
22
|
+
* ```
|
|
29
23
|
*/
|
|
30
|
-
function
|
|
31
|
-
return
|
|
24
|
+
function generateKey() {
|
|
25
|
+
return (0, node_crypto.randomUUID)();
|
|
32
26
|
}
|
|
33
27
|
/**
|
|
34
|
-
*
|
|
35
|
-
* not
|
|
28
|
+
* The declared {@link ColumnType}s whose SQL EQUALITY comparisons (`equals` /
|
|
29
|
+
* `not` / `any` / `none`) and `starts` / `ends` compiles are provably
|
|
30
|
+
* engine-exact under declared-type trust — `text` / `integer` / `real` /
|
|
31
|
+
* `boolean`; a `json` or `blob` column always refines instead.
|
|
36
32
|
*
|
|
37
33
|
* @remarks
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
/** Determine whether a value is an array. */
|
|
60
|
-
function isArray(value) {
|
|
61
|
-
return Array.isArray(value);
|
|
62
|
-
}
|
|
34
|
+
* This set governs equality and prefix/suffix matching only. RANGE
|
|
35
|
+
* comparisons (`above` / `below` / `from` / `to` / `between`) and `ORDER BY`
|
|
36
|
+
* are exact for `integer` / `real` / `boolean` but NOT for `text`: compiled
|
|
37
|
+
* SQL orders/ranges under SQLite's default BINARY collation, which compares
|
|
38
|
+
* TEXT byte-for-byte as UTF-8 — equivalent to Unicode CODE-POINT order —
|
|
39
|
+
* while the core engine's `compareValues` orders JS strings with `<`, which
|
|
40
|
+
* compares UTF-16 CODE-UNIT order. The two orders diverge for supplementary-
|
|
41
|
+
* plane characters (code points ≥ U+10000, e.g. many emoji): a lead surrogate
|
|
42
|
+
* (`\uD800`–`\uDBFF`) sorts BELOW ``–`` in code-unit order, while
|
|
43
|
+
* its code point sorts ABOVE them. So `isExactCondition`'s range family and
|
|
44
|
+
* `isExactOrder` exclude `text`, refining through the core engine instead. A
|
|
45
|
+
* future opt-in "trusted collation" mode (the caller vouches the column's
|
|
46
|
+
* values are BMP-only, or a custom SQLite collation matching `compareValues`
|
|
47
|
+
* is registered) could restore native text ranges/ordering.
|
|
48
|
+
*/
|
|
49
|
+
var EXACT_COLUMN_TYPES = [
|
|
50
|
+
"text",
|
|
51
|
+
"integer",
|
|
52
|
+
"real",
|
|
53
|
+
"boolean"
|
|
54
|
+
];
|
|
63
55
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
56
|
+
* The declared {@link ColumnType}s whose SQL RANGE comparisons
|
|
57
|
+
* (`above` / `below` / `from` / `to` / `between`) and `ORDER BY` compiles are
|
|
58
|
+
* provably engine-exact — `integer` / `real` / `boolean` only. `text` is
|
|
59
|
+
* excluded: see {@link EXACT_COLUMN_TYPES}'s remarks for the BINARY-collation
|
|
60
|
+
* (code-point) vs. JS `<` (code-unit) divergence on supplementary-plane
|
|
61
|
+
* characters.
|
|
62
|
+
*/
|
|
63
|
+
var EXACT_RANGE_COLUMN_TYPES = [
|
|
64
|
+
"integer",
|
|
65
|
+
"real",
|
|
66
|
+
"boolean"
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* Whether a value's runtime type matches a column's declared exact type —
|
|
70
|
+
* the operand side of the declared-type-trust proof.
|
|
66
71
|
*
|
|
67
72
|
* @remarks
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* `try`/`catch`.
|
|
75
|
-
*
|
|
76
|
-
* @param callback - The callback to invoke with no arguments
|
|
77
|
-
* @returns A `Success` carrying the return value, or a `Failure` carrying the
|
|
78
|
-
* thrown reason normalised to an `Error`
|
|
73
|
+
* `text` ↔ string, `integer` / `real` ↔ FINITE number (`NaN` / `±Infinity`
|
|
74
|
+
* fail), `boolean` ↔ boolean. Backs {@link isExactCondition}'s operand checks.
|
|
75
|
+
*
|
|
76
|
+
* @param value - The condition operand to test
|
|
77
|
+
* @param type - The column's declared portable type
|
|
78
|
+
* @returns `true` when the operand's runtime type matches the declared type
|
|
79
79
|
*
|
|
80
80
|
* @example
|
|
81
81
|
* ```ts
|
|
82
|
-
*
|
|
83
|
-
*
|
|
82
|
+
* matchesDeclaredType('Ada', 'text') // true
|
|
83
|
+
* matchesDeclaredType(Number.NaN, 'integer') // false — only finite numbers
|
|
84
84
|
* ```
|
|
85
85
|
*/
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
value: callback()
|
|
91
|
-
};
|
|
92
|
-
} catch (reason) {
|
|
93
|
-
if (reason instanceof Error) return {
|
|
94
|
-
success: false,
|
|
95
|
-
error: reason
|
|
96
|
-
};
|
|
97
|
-
let message = "Unknown thrown value";
|
|
98
|
-
try {
|
|
99
|
-
message = String(reason);
|
|
100
|
-
} catch {}
|
|
101
|
-
return {
|
|
102
|
-
success: false,
|
|
103
|
-
error: new Error(message)
|
|
104
|
-
};
|
|
105
|
-
}
|
|
86
|
+
function matchesDeclaredType(value, type) {
|
|
87
|
+
if (type === "text") return (0, _orkestrel_contract.isString)(value);
|
|
88
|
+
if (type === "boolean") return (0, _orkestrel_contract.isBoolean)(value);
|
|
89
|
+
return (0, _orkestrel_contract.isFiniteNumber)(value);
|
|
106
90
|
}
|
|
107
|
-
//#endregion
|
|
108
|
-
//#region src/server/helpers.ts
|
|
109
91
|
/**
|
|
110
|
-
*
|
|
92
|
+
* Whether one {@link Condition} compiles to SQL that is PROVABLY identical to
|
|
93
|
+
* the core engine's `matchesCondition` for every value its column's declared
|
|
94
|
+
* type can store.
|
|
111
95
|
*
|
|
112
96
|
* @remarks
|
|
113
|
-
*
|
|
114
|
-
* a
|
|
115
|
-
*
|
|
97
|
+
* `false` for a nested `FieldPath` (an array), a column absent from `schema`,
|
|
98
|
+
* or a column whose declared type is not `text` / `integer` / `real` /
|
|
99
|
+
* `boolean` (a `json` / `blob` column) — EXCEPT `absent` / `present`, which
|
|
100
|
+
* compile to `IS NULL` / `IS NOT NULL` and match `decodeRow`'s "a stored NULL
|
|
101
|
+
* decodes to `undefined`" rule for every column type, so they are exact
|
|
102
|
+
* regardless of declared type. `equals` / `not` require a operand matching the
|
|
103
|
+
* column's declared type (a `null` / `undefined` operand is never exact here —
|
|
104
|
+
* `encodeRow` stores both an explicit `null` and an absent field as SQL NULL,
|
|
105
|
+
* so native `IS NULL` semantics cannot match the engine's `deepEqual`-over-
|
|
106
|
+
* decoded-rows truth). `above` / `below` / `from` / `to` / `between` are exact
|
|
107
|
+
* ONLY for a declared type in {@link EXACT_RANGE_COLUMN_TYPES} (`integer` /
|
|
108
|
+
* `real` / `boolean`) — a `text` column's range conditions REFINE, because
|
|
109
|
+
* SQLite's default BINARY collation orders TEXT by Unicode CODE POINT while
|
|
110
|
+
* the core engine's `compareValues` orders JS strings by UTF-16 CODE UNIT,
|
|
111
|
+
* and the two diverge for supplementary-plane characters (see
|
|
112
|
+
* {@link EXACT_COLUMN_TYPES}'s remarks for the full rationale).
|
|
113
|
+
* `any` / `none` require a NON-EMPTY list where every element matches (an empty
|
|
114
|
+
* list is exact under neither: the engine's `any([])` matches nothing while
|
|
115
|
+
* `none([])` matches everything, and SQL `IN ()` is a syntax error) — these
|
|
116
|
+
* stay exact on `text` (byte equality is collation-independent and engine-
|
|
117
|
+
* identical). `starts` / `ends` are exact only on a `text` column with a
|
|
118
|
+
* string operand (case-sensitive `substr` compile, see {@link fragment}) —
|
|
119
|
+
* likewise collation-independent. `like` / `glob` are NEVER exact — SQLite
|
|
120
|
+
* `LIKE` folds case ASCII-only against the engine's Unicode fold, and `GLOB`
|
|
121
|
+
* has character classes the engine treats literally.
|
|
122
|
+
*
|
|
123
|
+
* @param condition - The condition to test
|
|
124
|
+
* @param schema - The table's schema
|
|
125
|
+
* @returns Whether `condition` is exact
|
|
126
|
+
*/
|
|
127
|
+
function isExactCondition(condition, schema) {
|
|
128
|
+
if (!(0, _orkestrel_contract.isString)(condition.column)) return false;
|
|
129
|
+
const column = schema.columns.find((candidate) => candidate.name === condition.column);
|
|
130
|
+
if (column === void 0) return false;
|
|
131
|
+
if (condition.operator === "absent" || condition.operator === "present") return true;
|
|
132
|
+
if (!EXACT_COLUMN_TYPES.some((type) => type === column.type)) return false;
|
|
133
|
+
const first = condition.values[0];
|
|
134
|
+
const second = condition.values[1];
|
|
135
|
+
switch (condition.operator) {
|
|
136
|
+
case "equals":
|
|
137
|
+
case "not": return matchesDeclaredType(first, column.type);
|
|
138
|
+
case "above":
|
|
139
|
+
case "below":
|
|
140
|
+
case "from":
|
|
141
|
+
case "to": return EXACT_RANGE_COLUMN_TYPES.some((type) => type === column.type) && matchesDeclaredType(first, column.type);
|
|
142
|
+
case "between": return EXACT_RANGE_COLUMN_TYPES.some((type) => type === column.type) && matchesDeclaredType(first, column.type) && matchesDeclaredType(second, column.type);
|
|
143
|
+
case "any":
|
|
144
|
+
case "none": return condition.values.length > 0 && condition.values.every((value) => matchesDeclaredType(value, column.type));
|
|
145
|
+
case "starts":
|
|
146
|
+
case "ends": return column.type === "text" && (0, _orkestrel_contract.isString)(first);
|
|
147
|
+
case "like":
|
|
148
|
+
case "glob": return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Whether one {@link Order} term's column compiles to an `ORDER BY` that
|
|
153
|
+
* matches the engine's {@link import('@src/core').sortRows} exactly.
|
|
116
154
|
*
|
|
117
|
-
* @
|
|
155
|
+
* @remarks
|
|
156
|
+
* `false` for a nested `FieldPath`, a column absent from `schema`, or a
|
|
157
|
+
* declared type outside {@link EXACT_RANGE_COLUMN_TYPES} (`integer` / `real` /
|
|
158
|
+
* `boolean`). `text` is NOT exact here: SQLite's default BINARY collation
|
|
159
|
+
* orders TEXT by Unicode code point while the core engine's `compareValues`
|
|
160
|
+
* orders JS strings by UTF-16 code unit, and the two diverge for
|
|
161
|
+
* supplementary-plane characters (see {@link EXACT_COLUMN_TYPES}'s remarks) —
|
|
162
|
+
* a `text` order term REFINES through the core engine instead.
|
|
163
|
+
*
|
|
164
|
+
* @param order - The order term to test
|
|
165
|
+
* @param schema - The table's schema
|
|
166
|
+
* @returns Whether `order` is exact
|
|
167
|
+
*/
|
|
168
|
+
function isExactOrder(order, schema) {
|
|
169
|
+
if (!(0, _orkestrel_contract.isString)(order.column)) return false;
|
|
170
|
+
const column = schema.columns.find((candidate) => candidate.name === order.column);
|
|
171
|
+
if (column === void 0) return false;
|
|
172
|
+
return EXACT_RANGE_COLUMN_TYPES.some((type) => type === column.type);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Whether a whole {@link Criteria} is exact — every condition and every order
|
|
176
|
+
* term is exact. `limit` / `offset` never affect exactness (SQL `LIMIT` /
|
|
177
|
+
* `OFFSET` are always engine-identical).
|
|
118
178
|
*
|
|
119
|
-
* @
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* ```
|
|
179
|
+
* @param criteria - The criteria to test
|
|
180
|
+
* @param schema - The table's schema
|
|
181
|
+
* @returns Whether every part of `criteria` is exact
|
|
123
182
|
*/
|
|
124
|
-
function
|
|
125
|
-
|
|
183
|
+
function isExactCriteria(criteria, schema) {
|
|
184
|
+
const conditions = criteria.conditions ?? [];
|
|
185
|
+
const order = criteria.order ?? [];
|
|
186
|
+
return conditions.every((condition) => isExactCondition(condition, schema)) && order.every((term) => isExactOrder(term, schema));
|
|
126
187
|
}
|
|
127
188
|
/**
|
|
128
189
|
* Map a portable {@link ColumnType} to its SQLite column type.
|
|
@@ -192,7 +253,7 @@ function quote(identifier) {
|
|
|
192
253
|
* ```
|
|
193
254
|
*/
|
|
194
255
|
function fieldColumn(path) {
|
|
195
|
-
if (isString(path)) return quote(path);
|
|
256
|
+
if ((0, _orkestrel_contract.isString)(path)) return quote(path);
|
|
196
257
|
const rest = path.slice(1).map((key) => "." + key.replaceAll("'", "''")).join("");
|
|
197
258
|
return "json_extract(" + quote(path[0]) + ", '$" + rest + "')";
|
|
198
259
|
}
|
|
@@ -362,12 +423,40 @@ function schemaToTable(schema) {
|
|
|
362
423
|
return "CREATE TABLE IF NOT EXISTS " + quote(schema.name) + " (" + columns.join(", ") + ", PRIMARY KEY (" + quote(schema.primary) + "))";
|
|
363
424
|
}
|
|
364
425
|
/**
|
|
426
|
+
* Build a collision-free SQL index name for a table + column-group index —
|
|
427
|
+
* shared by {@link schemaToIndexes} (an `open`-time `CREATE INDEX`) and
|
|
428
|
+
* {@link stepToSQL}'s `index.add` / `index.remove` (a migration-time DDL),
|
|
429
|
+
* so a plan-built index name always matches one `open` would have created.
|
|
430
|
+
*
|
|
431
|
+
* @remarks
|
|
432
|
+
* A naive `idx_<table>_<cols joined by _>` is AMBIGUOUS: table `'a_b'` with
|
|
433
|
+
* column `'c'` and table `'a'` with columns `['b', 'c']` both produce
|
|
434
|
+
* `idx_a_b_c`. This encodes each part (the table name, then each column name)
|
|
435
|
+
* length-prefixed (`<len>_<part>`) so the boundary between parts is always
|
|
436
|
+
* unambiguous, however the names themselves are punctuated.
|
|
437
|
+
*
|
|
438
|
+
* @param table - The table name
|
|
439
|
+
* @param columns - The index's column names, in order
|
|
440
|
+
* @returns The deterministic, collision-free index identifier (unquoted)
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```ts
|
|
444
|
+
* indexName('users', ['name']) // 'idx_5_users_4_name'
|
|
445
|
+
* indexName('a_b', ['c']) // 'idx_3_a_b_1_c'
|
|
446
|
+
* indexName('a', ['b', 'c']) // 'idx_1_a_1_b_1_c'
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
function indexName(table, columns) {
|
|
450
|
+
return "idx_" + [table, ...columns].map((part) => String(part.length) + "_" + part).join("_");
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
365
453
|
* Project a {@link TableSchema} to the `CREATE INDEX IF NOT EXISTS` statements a
|
|
366
454
|
* SQLite driver's `open` issues for its declared indexes.
|
|
367
455
|
*
|
|
368
456
|
* @remarks
|
|
369
|
-
* One statement per index group; the index name is
|
|
370
|
-
*
|
|
457
|
+
* One statement per index group; the index name is built by {@link indexName}
|
|
458
|
+
* (collision-free and deterministic), matching the driver's naming so a
|
|
459
|
+
* repeated `open` is idempotent.
|
|
371
460
|
*
|
|
372
461
|
* @param schema - The table's schema
|
|
373
462
|
* @returns One `CREATE INDEX IF NOT EXISTS …` statement per declared index
|
|
@@ -375,15 +464,114 @@ function schemaToTable(schema) {
|
|
|
375
464
|
* @example
|
|
376
465
|
* ```ts
|
|
377
466
|
* schemaToIndexes(schema)
|
|
378
|
-
* // ['CREATE INDEX IF NOT EXISTS "
|
|
467
|
+
* // ['CREATE INDEX IF NOT EXISTS "idx_5_users_4_name" ON "users" ("name")']
|
|
379
468
|
* ```
|
|
380
469
|
*/
|
|
381
470
|
function schemaToIndexes(schema) {
|
|
382
|
-
return schema.indexes.map((group) => "CREATE INDEX IF NOT EXISTS " + quote(
|
|
471
|
+
return schema.indexes.map((group) => "CREATE INDEX IF NOT EXISTS " + quote(indexName(schema.name, group)) + " ON " + quote(schema.name) + " (" + group.map(quote).join(", ") + ")");
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Project one {@link MigrationStep} to the DDL statement(s) a SQLite driver's
|
|
475
|
+
* `migrate` executes for it.
|
|
476
|
+
*
|
|
477
|
+
* @remarks
|
|
478
|
+
* `table.add` emits the `CREATE TABLE` plus one `CREATE INDEX` per declared
|
|
479
|
+
* index (via {@link schemaToTable} / {@link schemaToIndexes}); `table.remove`
|
|
480
|
+
* emits `DROP TABLE IF EXISTS`; `column.add` / `column.remove` emit `ALTER
|
|
481
|
+
* TABLE … ADD COLUMN` / `… DROP COLUMN`; `index.add` / `index.remove` emit
|
|
482
|
+
* `CREATE INDEX IF NOT EXISTS` / `DROP INDEX IF EXISTS`, naming the index the
|
|
483
|
+
* same way `schemaToIndexes` does (`idx_<table>_<columns joined by _>`) so a
|
|
484
|
+
* plan-built index matches one `open` would have created. Whether the named
|
|
485
|
+
* table actually exists is the caller's concern (a driver's `migrate` checks
|
|
486
|
+
* its own declared schema before running these statements) — this projection
|
|
487
|
+
* is pure and never inspects live state.
|
|
488
|
+
*
|
|
489
|
+
* @param step - The migration step to project
|
|
490
|
+
* @returns The DDL statement(s) that apply the step
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* ```ts
|
|
494
|
+
* stepToSQL({ operation: 'column.remove', table: 'users', column: 'legacy' })
|
|
495
|
+
* // ['ALTER TABLE "users" DROP COLUMN "legacy"']
|
|
496
|
+
* ```
|
|
497
|
+
*/
|
|
498
|
+
function stepToSQL(step) {
|
|
499
|
+
switch (step.operation) {
|
|
500
|
+
case "table.add": return [schemaToTable(step.table), ...schemaToIndexes(step.table)];
|
|
501
|
+
case "table.remove": return ["DROP TABLE IF EXISTS " + quote(step.table)];
|
|
502
|
+
case "column.add": return ["ALTER TABLE " + quote(step.table) + " ADD COLUMN " + quote(step.column.name) + " " + columnSQL(step.column.type)];
|
|
503
|
+
case "column.remove": return ["ALTER TABLE " + quote(step.table) + " DROP COLUMN " + quote(step.column)];
|
|
504
|
+
case "index.add": return ["CREATE INDEX IF NOT EXISTS " + quote(indexName(step.table, step.index)) + " ON " + quote(step.table) + " (" + step.index.map(quote).join(", ") + ")"];
|
|
505
|
+
case "index.remove": return ["DROP INDEX IF EXISTS " + quote(indexName(step.table, step.index))];
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Project one {@link MigrationStep} onto its table's declared {@link TableSchema}
|
|
510
|
+
* — the bookkeeping counterpart to {@link stepToSQL} (which projects the DDL a
|
|
511
|
+
* driver's `migrate` runs against the live database).
|
|
512
|
+
*
|
|
513
|
+
* @remarks
|
|
514
|
+
* `column.add` / `column.remove` add / filter the named column;
|
|
515
|
+
* `index.add` / `index.remove` add / filter the matching index group (an exact
|
|
516
|
+
* ordered match on `index`). `table.add` / `table.remove` act on a WHOLE
|
|
517
|
+
* schema map rather than one table's shape, so they are the caller's concern
|
|
518
|
+
* (a driver's `migrate` applies them directly against its table map) — passed
|
|
519
|
+
* here, they return `schema` unchanged.
|
|
520
|
+
*
|
|
521
|
+
* @param schema - The table's current declared schema
|
|
522
|
+
* @param step - The migration step to project onto it
|
|
523
|
+
* @returns The table's schema after the step
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```ts
|
|
527
|
+
* stepToSchema(schema, { operation: 'column.remove', table: 'users', column: 'legacy' })
|
|
528
|
+
* // schema with the 'legacy' column dropped from `columns`
|
|
529
|
+
* ```
|
|
530
|
+
*/
|
|
531
|
+
function stepToSchema(schema, step) {
|
|
532
|
+
switch (step.operation) {
|
|
533
|
+
case "column.add": return {
|
|
534
|
+
...schema,
|
|
535
|
+
columns: [...schema.columns, step.column]
|
|
536
|
+
};
|
|
537
|
+
case "column.remove": return {
|
|
538
|
+
...schema,
|
|
539
|
+
columns: schema.columns.filter((column) => column.name !== step.column)
|
|
540
|
+
};
|
|
541
|
+
case "index.add": return {
|
|
542
|
+
...schema,
|
|
543
|
+
indexes: [...schema.indexes, step.index]
|
|
544
|
+
};
|
|
545
|
+
case "index.remove": return {
|
|
546
|
+
...schema,
|
|
547
|
+
indexes: schema.indexes.filter((group) => !(group.length === step.index.length && group.every((name, position) => name === step.index[position])))
|
|
548
|
+
};
|
|
549
|
+
case "table.add":
|
|
550
|
+
case "table.remove": return schema;
|
|
551
|
+
}
|
|
383
552
|
}
|
|
384
553
|
//#endregion
|
|
385
554
|
//#region src/server/compilers.ts
|
|
386
555
|
/**
|
|
556
|
+
* Compile a NESTED {@link FieldPath} to the `json_type(<col>, <path>)` SQL
|
|
557
|
+
* expression — the {@link fieldColumn} `json_extract` sibling used to tell a
|
|
558
|
+
* PRESENT JSON `null` apart from an ABSENT path (both read back as SQL `NULL`
|
|
559
|
+
* through `json_extract`, but `json_type` reports `'null'` for the former and
|
|
560
|
+
* SQL `NULL` for the latter).
|
|
561
|
+
*
|
|
562
|
+
* @param path - The nested field path (a column plus its JSON keys)
|
|
563
|
+
* @returns The SQL expression reading the value's JSON type
|
|
564
|
+
*
|
|
565
|
+
* @example
|
|
566
|
+
* ```ts
|
|
567
|
+
* jsonTypeColumn(['payload', 'user', 'id']) // "json_type(\"payload\", '$.user.id')"
|
|
568
|
+
* ```
|
|
569
|
+
*/
|
|
570
|
+
function jsonTypeColumn(path) {
|
|
571
|
+
const rest = path.slice(1).map((key) => "." + key.replaceAll("'", "''")).join("");
|
|
572
|
+
return "json_type(" + quote(path[0]) + ", '$" + rest + "')";
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
387
575
|
* Escape `\`, `%`, and `_` (each with a leading `\`) so a `starts` / `ends`
|
|
388
576
|
* operand is matched literally under the `LIKE … ESCAPE '\'` clause.
|
|
389
577
|
*
|
|
@@ -443,7 +631,7 @@ function valueType(value) {
|
|
|
443
631
|
}
|
|
444
632
|
/**
|
|
445
633
|
* Compile one condition to its `<column> <operator>` SQL fragment and the params
|
|
446
|
-
* it binds.
|
|
634
|
+
* it binds — engine-exact under SQL's three-valued NULL logic.
|
|
447
635
|
*
|
|
448
636
|
* @remarks
|
|
449
637
|
* Every operand is run through `encodeValue`, so a bound value matches the SQL
|
|
@@ -452,10 +640,53 @@ function valueType(value) {
|
|
|
452
640
|
* encodes each operand as the NATIVE scalar `json_extract` returns, derived from
|
|
453
641
|
* the operand's runtime type (per-operand, since `between` / `any` / `none` can
|
|
454
642
|
* mix types). `any` / `none` collapse an empty list to a constant (`0` matches
|
|
455
|
-
* nothing, `1` matches all) with no params.
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
643
|
+
* nothing, `1` matches all) with no params.
|
|
644
|
+
*
|
|
645
|
+
* The core engine's total order ranks `undefined` (rank 0) BELOW `null`
|
|
646
|
+
* (rank 1) (see `compareValues`), so a MISSING/`NULL` column MATCHES
|
|
647
|
+
* `below` / `to` / a scalar `not` / `none` — the opposite of raw SQL, where a
|
|
648
|
+
* comparison against `NULL` is `NULL` (excluded). This fragment replicates the
|
|
649
|
+
* engine exactly. Truth table (`value` = the engine's decoded field read; a
|
|
650
|
+
* FLAT column's stored `NULL` decodes to `undefined` per `decodeRow`, so a
|
|
651
|
+
* flat `value` is NEVER a present `null` — only a NESTED path can be
|
|
652
|
+
* present-but-`null`):
|
|
653
|
+
*
|
|
654
|
+
* ```text
|
|
655
|
+
* operator | value=undefined (absent) | value=null (nested only) | value=scalar
|
|
656
|
+
* --------------------|--------------------------------|---------------------------|-------------
|
|
657
|
+
* equals, first=null | no match | MATCH | no match
|
|
658
|
+
* equals, first=X | no match | no match | value===X
|
|
659
|
+
* not, first=null | MATCH (flat: unconditionally; | no match | MATCH
|
|
660
|
+
* | nested: absent still matches)| |
|
|
661
|
+
* not, first=X | MATCH | MATCH | value!==X
|
|
662
|
+
* below/to, first=X | MATCH (rank 0 < rank(X)) | MATCH (rank 1 < rank(X)) | rank compare
|
|
663
|
+
* none, list=[…] | MATCH (no scalar rank-equal) | MATCH | not-in-list
|
|
664
|
+
* any, list=[…] | no match | no match | in-list
|
|
665
|
+
* above/from/between | no match | no match | rank compare
|
|
666
|
+
* like/glob/starts/… | no match (not a string) | no match | string test
|
|
667
|
+
* present | false | false | true
|
|
668
|
+
* absent | true | true | false
|
|
669
|
+
* ```
|
|
670
|
+
*
|
|
671
|
+
* Because a flat column's `NULL` always decodes to `undefined`, `equals`
|
|
672
|
+
* against a `null` operand needs no special flat compilation (`col = ?`
|
|
673
|
+
* binding a `NULL` param is already always-false in SQL, matching "no match"
|
|
674
|
+
* above) — but flat `not` against `null` must match EVERY row (both the
|
|
675
|
+
* absent and the scalar rows), which `col != ? OR col IS NULL` cannot express
|
|
676
|
+
* (it only catches the `IS NULL` row), so a flat `not`-with-`null`-operand
|
|
677
|
+
* compiles to the constant `1`.
|
|
678
|
+
*
|
|
679
|
+
* A NESTED path can be present-but-`null` (a stored JSON `null`), which
|
|
680
|
+
* `json_extract` reads back as SQL `NULL` — indistinguishable from an ABSENT
|
|
681
|
+
* path. `json_type(col, path)` disambiguates them (`'null'` for present-null,
|
|
682
|
+
* SQL `NULL` for absent), so nested `equals` / `not` against a `null` operand
|
|
683
|
+
* compile through `json_type` instead of `IS NULL` / `IS NOT NULL`.
|
|
684
|
+
*
|
|
685
|
+
* Every other MATCH-on-null-or-absent row is expressed uniformly (flat and
|
|
686
|
+
* nested alike) as `(<column> <op> ? OR <column> IS NULL)` — for a nested
|
|
687
|
+
* path, `json_extract` already collapses BOTH absent and present-null to SQL
|
|
688
|
+
* `NULL`, so `IS NULL` catches both in one clause; for a flat column there is
|
|
689
|
+
* only the absent case to catch.
|
|
459
690
|
*
|
|
460
691
|
* @param condition - The condition to compile
|
|
461
692
|
* @param schema - The table's schema (for declared column types)
|
|
@@ -465,20 +696,23 @@ function valueType(value) {
|
|
|
465
696
|
* ```ts
|
|
466
697
|
* fragment({ column: 'age', operator: 'above', values: [18], connector: 'and' }, schema)
|
|
467
698
|
* // { sql: '"age" > ?', params: [18] }
|
|
699
|
+
* fragment({ column: 'age', operator: 'below', values: [18], connector: 'and' }, schema)
|
|
700
|
+
* // { sql: '("age" < ? OR "age" IS NULL)', params: [18] }
|
|
468
701
|
* ```
|
|
469
702
|
*/
|
|
470
703
|
function fragment(condition, schema) {
|
|
471
704
|
const column = fieldColumn(condition.column);
|
|
472
|
-
const nested = !isString(condition.column);
|
|
473
|
-
const declared = isString(condition.column) ? declaredType(condition.column, schema) : void 0;
|
|
705
|
+
const nested = !(0, _orkestrel_contract.isString)(condition.column);
|
|
706
|
+
const declared = (0, _orkestrel_contract.isString)(condition.column) ? declaredType(condition.column, schema) : void 0;
|
|
474
707
|
const encode = (value) => encodeValue(value, nested ? valueType(value) : declared ?? "json");
|
|
475
708
|
const first = condition.values[0];
|
|
476
709
|
const second = condition.values[1];
|
|
477
|
-
const nullOperand =
|
|
710
|
+
const nullOperand = first === null || first === void 0;
|
|
711
|
+
const jsonType = !(0, _orkestrel_contract.isString)(condition.column) ? jsonTypeColumn(condition.column) : "";
|
|
478
712
|
switch (condition.operator) {
|
|
479
713
|
case "equals":
|
|
480
|
-
if (nullOperand) return {
|
|
481
|
-
sql:
|
|
714
|
+
if (nullOperand && nested) return {
|
|
715
|
+
sql: jsonType + " = 'null'",
|
|
482
716
|
params: []
|
|
483
717
|
};
|
|
484
718
|
return {
|
|
@@ -486,12 +720,18 @@ function fragment(condition, schema) {
|
|
|
486
720
|
params: [encode(first)]
|
|
487
721
|
};
|
|
488
722
|
case "not":
|
|
489
|
-
if (nullOperand)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
723
|
+
if (nullOperand) {
|
|
724
|
+
if (nested) return {
|
|
725
|
+
sql: "(" + jsonType + " IS NULL OR " + jsonType + " != 'null')",
|
|
726
|
+
params: []
|
|
727
|
+
};
|
|
728
|
+
return {
|
|
729
|
+
sql: "1",
|
|
730
|
+
params: []
|
|
731
|
+
};
|
|
732
|
+
}
|
|
493
733
|
return {
|
|
494
|
-
sql: column + " != ?",
|
|
734
|
+
sql: "(" + column + " != ? OR " + column + " IS NULL)",
|
|
495
735
|
params: [encode(first)]
|
|
496
736
|
};
|
|
497
737
|
case "above": return {
|
|
@@ -499,7 +739,7 @@ function fragment(condition, schema) {
|
|
|
499
739
|
params: [encode(first)]
|
|
500
740
|
};
|
|
501
741
|
case "below": return {
|
|
502
|
-
sql: column + " < ?",
|
|
742
|
+
sql: "(" + column + " < ? OR " + column + " IS NULL)",
|
|
503
743
|
params: [encode(first)]
|
|
504
744
|
};
|
|
505
745
|
case "from": return {
|
|
@@ -507,7 +747,7 @@ function fragment(condition, schema) {
|
|
|
507
747
|
params: [encode(first)]
|
|
508
748
|
};
|
|
509
749
|
case "to": return {
|
|
510
|
-
sql: column + " <= ?",
|
|
750
|
+
sql: "(" + column + " <= ? OR " + column + " IS NULL)",
|
|
511
751
|
params: [encode(first)]
|
|
512
752
|
};
|
|
513
753
|
case "between": return {
|
|
@@ -522,14 +762,30 @@ function fragment(condition, schema) {
|
|
|
522
762
|
sql: column + " GLOB ?",
|
|
523
763
|
params: [encode(first)]
|
|
524
764
|
};
|
|
525
|
-
case "starts":
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
765
|
+
case "starts": {
|
|
766
|
+
const text = (0, _orkestrel_contract.isString)(first) ? first : "";
|
|
767
|
+
if (text === "") return {
|
|
768
|
+
sql: "typeof(" + column + ") = 'text'",
|
|
769
|
+
params: []
|
|
770
|
+
};
|
|
771
|
+
const length = Array.from(text).length;
|
|
772
|
+
return {
|
|
773
|
+
sql: "(typeof(" + column + ") = 'text' AND substr(" + column + ", 1, " + length + ") = ?)",
|
|
774
|
+
params: [encode(first)]
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
case "ends": {
|
|
778
|
+
const text = (0, _orkestrel_contract.isString)(first) ? first : "";
|
|
779
|
+
if (text === "") return {
|
|
780
|
+
sql: "typeof(" + column + ") = 'text'",
|
|
781
|
+
params: []
|
|
782
|
+
};
|
|
783
|
+
const length = Array.from(text).length;
|
|
784
|
+
return {
|
|
785
|
+
sql: "(typeof(" + column + ") = 'text' AND substr(" + column + ", -" + length + ") = ?)",
|
|
786
|
+
params: [encode(first)]
|
|
787
|
+
};
|
|
788
|
+
}
|
|
533
789
|
case "any":
|
|
534
790
|
if (condition.values.length === 0) return {
|
|
535
791
|
sql: "0",
|
|
@@ -545,7 +801,7 @@ function fragment(condition, schema) {
|
|
|
545
801
|
params: []
|
|
546
802
|
};
|
|
547
803
|
return {
|
|
548
|
-
sql: column + " NOT IN (" + condition.values.map(() => "?").join(", ") + ")",
|
|
804
|
+
sql: "(" + column + " NOT IN (" + condition.values.map(() => "?").join(", ") + ") OR " + column + " IS NULL)",
|
|
549
805
|
params: condition.values.map(encode)
|
|
550
806
|
};
|
|
551
807
|
case "absent": return {
|
|
@@ -564,6 +820,10 @@ function fragment(condition, schema) {
|
|
|
564
820
|
*
|
|
565
821
|
* @remarks
|
|
566
822
|
* The first condition's connector is ignored, per the {@link Condition} types.
|
|
823
|
+
* Every fragment (see {@link fragment}'s truth table) replicates the core
|
|
824
|
+
* engine's total order EXACTLY under SQL's three-valued NULL logic, so this
|
|
825
|
+
* clause matches `applyCriteria` row-for-row over the same table — a native
|
|
826
|
+
* `records` / `count` read never disagrees with a scan-and-filter fallback.
|
|
567
827
|
*
|
|
568
828
|
* @param conditions - The conditions to fold
|
|
569
829
|
* @param schema - The table's schema
|
|
@@ -622,7 +882,7 @@ function compileWhere(conditions, schema) {
|
|
|
622
882
|
*/
|
|
623
883
|
function compileOrder(order, schema) {
|
|
624
884
|
const terms = (order ?? []).map((term) => fieldColumn(term.column) + (term.direction === "descending" ? " DESC" : " ASC"));
|
|
625
|
-
if (!(order ?? []).some((term) => isString(term.column) && term.column === schema.primary)) terms.push(quote(schema.primary));
|
|
885
|
+
if (!(order ?? []).some((term) => (0, _orkestrel_contract.isString)(term.column) && term.column === schema.primary)) terms.push(quote(schema.primary));
|
|
626
886
|
return terms.length === 0 ? "" : "ORDER BY " + terms.join(", ");
|
|
627
887
|
}
|
|
628
888
|
/**
|
|
@@ -702,6 +962,18 @@ function compileCriteria(criteria, schema) {
|
|
|
702
962
|
};
|
|
703
963
|
}
|
|
704
964
|
//#endregion
|
|
965
|
+
//#region src/server/constants.ts
|
|
966
|
+
/**
|
|
967
|
+
* The reserved metadata table the {@link SQLiteDriver} creates on `open` to
|
|
968
|
+
* persist its stamped `DriverMeta` (`version` + declared schema JSON) — the
|
|
969
|
+
* SQLite realization of the `meta` / `stamp` driver hooks.
|
|
970
|
+
*
|
|
971
|
+
* @remarks
|
|
972
|
+
* A single-row table (`id = 1`). A user table named `_meta` collides with the
|
|
973
|
+
* reservation — the caller's concern to avoid, documented on the driver class.
|
|
974
|
+
*/
|
|
975
|
+
var META_TABLE = "_meta";
|
|
976
|
+
//#endregion
|
|
705
977
|
//#region src/server/drivers/JSONDriver.ts
|
|
706
978
|
/**
|
|
707
979
|
* A persistent {@link DriverInterface} backed by a single JSON file — the
|
|
@@ -890,38 +1162,19 @@ var JSONDriver = class {
|
|
|
890
1162
|
} catch {
|
|
891
1163
|
return;
|
|
892
1164
|
}
|
|
893
|
-
if (!isRecord(parsed) || !isRecord(parsed.tables)) return;
|
|
1165
|
+
if (!(0, _orkestrel_contract.isRecord)(parsed) || !(0, _orkestrel_contract.isRecord)(parsed.tables)) return;
|
|
894
1166
|
const tables = parsed.tables;
|
|
895
1167
|
for (const table of this.#schema) {
|
|
896
1168
|
const rows = tables[table.name];
|
|
897
1169
|
if (!Array.isArray(rows)) continue;
|
|
898
1170
|
for (const entry of rows) {
|
|
899
|
-
if (!isRecord(entry)) continue;
|
|
1171
|
+
if (!(0, _orkestrel_contract.isRecord)(entry)) continue;
|
|
900
1172
|
const key = (0, _src_core.extractKey)(entry, table.primary);
|
|
901
1173
|
if (key === void 0) continue;
|
|
902
1174
|
await this.#memory.write(table.name, key, entry);
|
|
903
1175
|
}
|
|
904
1176
|
}
|
|
905
|
-
|
|
906
|
-
"text",
|
|
907
|
-
"integer",
|
|
908
|
-
"real",
|
|
909
|
-
"boolean",
|
|
910
|
-
"json",
|
|
911
|
-
"blob"
|
|
912
|
-
];
|
|
913
|
-
const isColumnType = (value) => isString(value) && COLUMN_TYPES.some((type) => type === value);
|
|
914
|
-
const isColumnSchema = (value) => isRecord(value) && isString(value.name) && isColumnType(value.type) && isBoolean(value.nullable);
|
|
915
|
-
const isIndexGroup = (value) => isArray(value) && value.every(isString);
|
|
916
|
-
const isTableSchema = (value) => isRecord(value) && isString(value.name) && isString(value.primary) && isArray(value.columns) && value.columns.every(isColumnSchema) && isArray(value.indexes) && value.indexes.every(isIndexGroup);
|
|
917
|
-
if (isRecord(parsed.meta)) {
|
|
918
|
-
const version = parsed.meta.version;
|
|
919
|
-
const schema = parsed.meta.schema;
|
|
920
|
-
if (typeof version === "number" && Number.isFinite(version) && isArray(schema) && schema.every(isTableSchema)) this.#meta = {
|
|
921
|
-
version,
|
|
922
|
-
schema
|
|
923
|
-
};
|
|
924
|
-
}
|
|
1177
|
+
if ((0, _src_core.isDriverMeta)(parsed.meta)) this.#meta = parsed.meta;
|
|
925
1178
|
}
|
|
926
1179
|
async #flush() {
|
|
927
1180
|
const next = this.#chain.then(() => this.#serialize());
|
|
@@ -955,12 +1208,390 @@ var JSONDriver = class {
|
|
|
955
1208
|
}
|
|
956
1209
|
};
|
|
957
1210
|
//#endregion
|
|
1211
|
+
//#region src/server/drivers/SQLiteDriver.ts
|
|
1212
|
+
/**
|
|
1213
|
+
* The SQLite {@link DriverInterface} — the server-native, trusted-mode backend
|
|
1214
|
+
* built on the published `@orkestrel/sqlite` synchronous wrapper.
|
|
1215
|
+
*
|
|
1216
|
+
* @remarks
|
|
1217
|
+
* A thin adapter: it implements the storage primitives the core database layer
|
|
1218
|
+
* needs by delegating to the wrapper's prepared statements — it never touches
|
|
1219
|
+
* raw `node:sqlite`. `open` issues `CREATE TABLE IF NOT EXISTS` with real typed
|
|
1220
|
+
* columns (mapped from each {@link TableSchema}'s portable column types) and a
|
|
1221
|
+
* `PRIMARY KEY`, plus a `CREATE INDEX IF NOT EXISTS` per declared index (both
|
|
1222
|
+
* reopen-safe), and readies a reserved `_meta` single-row table `meta()` /
|
|
1223
|
+
* `stamp()` read and write — **a user table named `_meta` collides with it**;
|
|
1224
|
+
* avoid the name. Rows cross the boundary through the codecs in `helpers.ts`
|
|
1225
|
+
* (`json` columns store / parse JSON text, a `boolean` stores `1` / `0`), so the
|
|
1226
|
+
* typed layer above imposes the exact shape (AGENTS §14). `write` is an
|
|
1227
|
+
* `INSERT OR REPLACE` upsert — the `Table` layer detects a `CONFLICT` via a
|
|
1228
|
+
* prior `has`, so this never translates a constraint error; a backend
|
|
1229
|
+
* `SQLiteError` otherwise propagates unchanged. Querying, ordering, paging, and
|
|
1230
|
+
* aggregation are native: `records` / `count` / `stream` compile a `Criteria`
|
|
1231
|
+
* to SQL with `compileCriteria`, and `aggregate` runs a SQL
|
|
1232
|
+
* `COUNT`/`SUM`/`AVG`/`MIN`/`MAX` (via `aggregateSQL`) over the same compiled
|
|
1233
|
+
* WHERE. `transaction` wraps native `BEGIN` / `COMMIT` / `ROLLBACK` with
|
|
1234
|
+
* double-settle guards. `migrate` runs the plan's projected DDL
|
|
1235
|
+
* ({@link import('../helpers.js').stepToSQL}) inside whichever native
|
|
1236
|
+
* transaction is active: joined into an already-open `transaction()` handle
|
|
1237
|
+
* when one exists (the core's versioned reconcile path wraps migrate + stamp
|
|
1238
|
+
* in one native `BEGIN`, and node:sqlite rejects a nested `BEGIN`), or inside
|
|
1239
|
+
* its own `database.transaction` otherwise — a mid-plan failure rolls back
|
|
1240
|
+
* atomically either way, an improvement over the non-atomic `MemoryDriver` /
|
|
1241
|
+
* `JSONDriver` migrate; a step referencing an undeclared table throws
|
|
1242
|
+
* `DatabaseError` `MIGRATION` before any DDL for that step runs. `snapshot` is
|
|
1243
|
+
* capture-replay (SELECT the
|
|
1244
|
+
* named tables' rows, replay via DELETE + INSERT OR REPLACE inside a native
|
|
1245
|
+
* transaction on rollback) rather than a SQL `SAVEPOINT`, since the core
|
|
1246
|
+
* `transaction` calls the rollback thunk only on failure with no commit-on-
|
|
1247
|
+
* success signal — a long-lived `SAVEPOINT` would leave the connection
|
|
1248
|
+
* uncommitted (lost on close). Every backend interaction runs through `#guard`,
|
|
1249
|
+
* which maps a thrown backend `SQLiteError` (or any unexpected non-`SQLiteError`
|
|
1250
|
+
* throw) to a typed {@link DatabaseError} — never a raw backend error escapes
|
|
1251
|
+
* `DriverInterface`: `CONSTRAINT` → `CONFLICT`, the wrapper's own `CLOSED` →
|
|
1252
|
+
* `CLOSED`, `BUSY` (a locked database that outlasted the configured `timeout`)
|
|
1253
|
+
* → a retryable `DRIVER` (`context.retryable` is `true`), and `UNKNOWN` / any
|
|
1254
|
+
* other throw → `DRIVER`. The original error is preserved as `context.cause`.
|
|
1255
|
+
* A `DatabaseError` this driver throws directly (`CLOSED` from the `#require`
|
|
1256
|
+
* gate, `NOT_FOUND` from `#table`, `MIGRATION` from a migration-plan fault)
|
|
1257
|
+
* passes through `#guard` unchanged, never re-wrapped.
|
|
1258
|
+
*/
|
|
1259
|
+
var SQLiteDriver = class {
|
|
1260
|
+
#path;
|
|
1261
|
+
#options;
|
|
1262
|
+
#database;
|
|
1263
|
+
#schema = /* @__PURE__ */ new Map();
|
|
1264
|
+
#transacting = false;
|
|
1265
|
+
constructor(path, options) {
|
|
1266
|
+
this.#path = path;
|
|
1267
|
+
this.#options = options ?? {};
|
|
1268
|
+
}
|
|
1269
|
+
async open(schema) {
|
|
1270
|
+
if (schema.some((table) => table.name === "_meta")) throw new _src_core.DatabaseError("VALIDATION", `A declared table cannot be named '${META_TABLE}' — it is reserved for driver metadata`, { table: META_TABLE });
|
|
1271
|
+
this.#guard(() => {
|
|
1272
|
+
this.#database?.close();
|
|
1273
|
+
const database = (0, _orkestrel_sqlite.createSQLiteDatabase)({
|
|
1274
|
+
path: this.#path,
|
|
1275
|
+
readonly: this.#options.readonly,
|
|
1276
|
+
timeout: this.#options.timeout,
|
|
1277
|
+
foreignKeys: this.#options.foreignKeys
|
|
1278
|
+
});
|
|
1279
|
+
database.connect();
|
|
1280
|
+
for (const [name, value] of Object.entries(this.#options.pragmas ?? {})) database.pragma(name, value);
|
|
1281
|
+
const map = /* @__PURE__ */ new Map();
|
|
1282
|
+
for (const table of schema) {
|
|
1283
|
+
map.set(table.name, table);
|
|
1284
|
+
database.exec(schemaToTable(table));
|
|
1285
|
+
for (const sql of schemaToIndexes(table)) database.exec(sql);
|
|
1286
|
+
}
|
|
1287
|
+
database.exec("CREATE TABLE IF NOT EXISTS " + quote(META_TABLE) + " (\"id\" INTEGER, \"version\" INTEGER, \"schema\" TEXT, PRIMARY KEY (\"id\"))");
|
|
1288
|
+
this.#schema = map;
|
|
1289
|
+
this.#database = database;
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
async close() {
|
|
1293
|
+
this.#database?.close();
|
|
1294
|
+
this.#database = void 0;
|
|
1295
|
+
}
|
|
1296
|
+
async read(table, key) {
|
|
1297
|
+
const schema = this.#table(table);
|
|
1298
|
+
return this.#guard(() => {
|
|
1299
|
+
const row = this.#require().prepare("SELECT * FROM " + quote(table) + " WHERE " + quote(schema.primary) + " = ?").get([this.#key(key, schema)]);
|
|
1300
|
+
return row === void 0 ? void 0 : decodeRow(row, schema);
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
async write(table, key, row) {
|
|
1304
|
+
const schema = this.#table(table);
|
|
1305
|
+
this.#guard(() => {
|
|
1306
|
+
const encoded = encodeRow({
|
|
1307
|
+
...row,
|
|
1308
|
+
[schema.primary]: key
|
|
1309
|
+
}, schema);
|
|
1310
|
+
const names = schema.columns.map((column) => column.name);
|
|
1311
|
+
const values = names.map((name) => encoded[name]);
|
|
1312
|
+
this.#require().prepare("INSERT OR REPLACE INTO " + quote(table) + " (" + names.map(quote).join(", ") + ") VALUES (" + names.map(() => "?").join(", ") + ")").run(values);
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
async delete(table, key) {
|
|
1316
|
+
const schema = this.#table(table);
|
|
1317
|
+
return this.#guard(() => {
|
|
1318
|
+
return this.#require().prepare("DELETE FROM " + quote(table) + " WHERE " + quote(schema.primary) + " = ?").run([this.#key(key, schema)]).changes > 0;
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
async keys(table) {
|
|
1322
|
+
const schema = this.#table(table);
|
|
1323
|
+
return this.#guard(() => {
|
|
1324
|
+
const primary = quote(schema.primary);
|
|
1325
|
+
const rows = this.#require().prepare("SELECT " + primary + " FROM " + quote(table) + " ORDER BY " + primary).all();
|
|
1326
|
+
const keys = [];
|
|
1327
|
+
for (const row of rows) {
|
|
1328
|
+
const value = row[schema.primary];
|
|
1329
|
+
if (typeof value === "string" || typeof value === "number") keys.push(value);
|
|
1330
|
+
}
|
|
1331
|
+
return keys;
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
async *scan(table) {
|
|
1335
|
+
const schema = this.#table(table);
|
|
1336
|
+
const iterator = this.#guard(() => this.#require().prepare("SELECT * FROM " + quote(table) + " ORDER BY " + quote(schema.primary)).iterate())[Symbol.iterator]();
|
|
1337
|
+
while (true) {
|
|
1338
|
+
const step = this.#guard(() => iterator.next());
|
|
1339
|
+
if (step.done === true) return;
|
|
1340
|
+
yield this.#guard(() => decodeRow(step.value, schema));
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
async clear(table) {
|
|
1344
|
+
this.#table(table);
|
|
1345
|
+
this.#guard(() => {
|
|
1346
|
+
this.#require().prepare("DELETE FROM " + quote(table)).run();
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
async records(table, criteria) {
|
|
1350
|
+
const schema = this.#table(table);
|
|
1351
|
+
if (isExactCriteria(criteria, schema)) return this.#guard(() => {
|
|
1352
|
+
const { sql, params } = compileCriteria(criteria, schema);
|
|
1353
|
+
return this.#require().prepare("SELECT * FROM " + quote(table) + (sql === "" ? "" : " " + sql)).all(params).map((row) => decodeRow(row, schema));
|
|
1354
|
+
});
|
|
1355
|
+
const rows = [];
|
|
1356
|
+
for await (const row of this.scan(table)) rows.push(row);
|
|
1357
|
+
return (0, _src_core.applyCriteria)(rows, criteria);
|
|
1358
|
+
}
|
|
1359
|
+
async count(table, criteria) {
|
|
1360
|
+
const schema = this.#table(table);
|
|
1361
|
+
const conditions = criteria.conditions ?? [];
|
|
1362
|
+
if (conditions.every((condition) => isExactCondition(condition, schema))) return this.#guard(() => {
|
|
1363
|
+
const { sql, params } = compileWhere(conditions, schema);
|
|
1364
|
+
const value = this.#require().prepare("SELECT COUNT(*) AS count FROM " + quote(table) + (sql === "" ? "" : " " + sql)).get(params)?.count;
|
|
1365
|
+
return typeof value === "number" || typeof value === "bigint" ? Number(value) : 0;
|
|
1366
|
+
});
|
|
1367
|
+
const rows = [];
|
|
1368
|
+
for await (const row of this.scan(table)) rows.push(row);
|
|
1369
|
+
return (0, _src_core.filterRows)(rows, conditions).length;
|
|
1370
|
+
}
|
|
1371
|
+
async aggregate(table, operation, column, criteria) {
|
|
1372
|
+
const schema = this.#table(table);
|
|
1373
|
+
const conditions = criteria.conditions ?? [];
|
|
1374
|
+
const conditionsExact = conditions.every((condition) => isExactCondition(condition, schema));
|
|
1375
|
+
const columnExact = operation === "count" || (0, _orkestrel_contract.isString)(column) && schema.columns.some((candidate) => candidate.name === column && (candidate.type === "integer" || candidate.type === "real"));
|
|
1376
|
+
if (conditionsExact && columnExact) return this.#guard(() => {
|
|
1377
|
+
const { sql, params } = compileWhere(conditions, schema);
|
|
1378
|
+
const value = this.#require().prepare("SELECT " + aggregateSQL(operation, column) + " AS value FROM " + quote(table) + (sql === "" ? "" : " " + sql)).get(params)?.value;
|
|
1379
|
+
return value === null || value === void 0 ? void 0 : Number(value);
|
|
1380
|
+
});
|
|
1381
|
+
const rows = [];
|
|
1382
|
+
for await (const row of this.scan(table)) rows.push(row);
|
|
1383
|
+
return (0, _src_core.computeAggregate)((0, _src_core.filterRows)(rows, conditions), operation, column);
|
|
1384
|
+
}
|
|
1385
|
+
async *stream(table, criteria) {
|
|
1386
|
+
const schema = this.#table(table);
|
|
1387
|
+
const conditions = criteria.conditions ?? [];
|
|
1388
|
+
if (conditions.every((condition) => isExactCondition(condition, schema))) {
|
|
1389
|
+
const compiled = compileCriteria({
|
|
1390
|
+
conditions,
|
|
1391
|
+
limit: criteria.limit,
|
|
1392
|
+
offset: criteria.offset
|
|
1393
|
+
}, schema);
|
|
1394
|
+
for (const row of this.#require().prepare("SELECT * FROM " + quote(table) + (compiled.sql === "" ? "" : " " + compiled.sql)).iterate(compiled.params)) yield decodeRow(row, schema);
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
const offset = criteria.offset ?? 0;
|
|
1398
|
+
const limit = criteria.limit;
|
|
1399
|
+
let skipped = 0;
|
|
1400
|
+
let yielded = 0;
|
|
1401
|
+
for await (const row of this.scan(table)) {
|
|
1402
|
+
if (limit !== void 0 && yielded >= limit) return;
|
|
1403
|
+
if (conditions.length > 0 && !(0, _src_core.matchesCriteria)(row, conditions)) continue;
|
|
1404
|
+
if (skipped < offset) {
|
|
1405
|
+
skipped += 1;
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
yield row;
|
|
1409
|
+
yielded += 1;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Begin a native transaction — real `BEGIN`, `COMMIT`, `ROLLBACK`.
|
|
1414
|
+
*
|
|
1415
|
+
* @remarks
|
|
1416
|
+
* Calling `commit` or `rollback` a second time (on either method, in either
|
|
1417
|
+
* order) throws `DatabaseError` `CONFLICT`.
|
|
1418
|
+
*
|
|
1419
|
+
* @returns A {@link TransactionInterface} handle to `commit` or `rollback`
|
|
1420
|
+
*/
|
|
1421
|
+
async transaction() {
|
|
1422
|
+
const database = this.#require();
|
|
1423
|
+
this.#guard(() => database.exec("BEGIN"));
|
|
1424
|
+
let settled = false;
|
|
1425
|
+
this.#transacting = true;
|
|
1426
|
+
return {
|
|
1427
|
+
commit: async () => {
|
|
1428
|
+
if (settled) throw new _src_core.DatabaseError("CONFLICT", "Transaction already settled", {});
|
|
1429
|
+
settled = true;
|
|
1430
|
+
this.#transacting = false;
|
|
1431
|
+
this.#guard(() => database.exec("COMMIT"));
|
|
1432
|
+
},
|
|
1433
|
+
rollback: async () => {
|
|
1434
|
+
if (settled) throw new _src_core.DatabaseError("CONFLICT", "Transaction already settled", {});
|
|
1435
|
+
settled = true;
|
|
1436
|
+
this.#transacting = false;
|
|
1437
|
+
this.#guard(() => database.exec("ROLLBACK"));
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Apply a {@link Migration} plan by executing each step's projected DDL
|
|
1443
|
+
* ({@link import('../helpers.js').stepToSQL}).
|
|
1444
|
+
*
|
|
1445
|
+
* @remarks
|
|
1446
|
+
* Atomicity is provided by whichever native transaction is active: when
|
|
1447
|
+
* this driver's own `transaction()` hook already has a handle open (the
|
|
1448
|
+
* core's versioned reconcile / migrate path joins migrate + stamp under
|
|
1449
|
+
* one native `BEGIN`), the plan's DDL runs directly inside that enclosing
|
|
1450
|
+
* transaction — a mid-plan failure propagates out and the CALLER's
|
|
1451
|
+
* `commit`/`rollback` provides atomicity. node:sqlite (and SQLite
|
|
1452
|
+
* generally) rejects a nested `BEGIN`, so this driver must never open a
|
|
1453
|
+
* second native transaction while one is already open. Otherwise (no
|
|
1454
|
+
* enclosing transaction), `migrate` wraps the plan in its own native
|
|
1455
|
+
* `database.transaction` — atomic on its own: a mid-plan failure rolls
|
|
1456
|
+
* back every DDL statement already applied by the plan. A step
|
|
1457
|
+
* referencing a table not in this driver's declared schema (and that is
|
|
1458
|
+
* not itself a `table.add`) throws `DatabaseError` `MIGRATION` before any
|
|
1459
|
+
* DDL for that step runs, propagating out of whichever transaction is
|
|
1460
|
+
* active (which rolls back on a throw).
|
|
1461
|
+
*
|
|
1462
|
+
* @param plan - The migration plan to apply
|
|
1463
|
+
*/
|
|
1464
|
+
async migrate(plan) {
|
|
1465
|
+
const database = this.#require();
|
|
1466
|
+
const schema = new Map(this.#schema);
|
|
1467
|
+
this.#guard(() => {
|
|
1468
|
+
if (this.#transacting) this.#applyPlan(database, plan, schema);
|
|
1469
|
+
else database.transaction(() => this.#applyPlan(database, plan, schema));
|
|
1470
|
+
});
|
|
1471
|
+
this.#schema = schema;
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Read the persisted {@link DriverMeta} from the reserved `_meta` table.
|
|
1475
|
+
*
|
|
1476
|
+
* @returns The last-stamped `DriverMeta`, or `undefined` when never stamped
|
|
1477
|
+
* (or the stored row is malformed)
|
|
1478
|
+
*/
|
|
1479
|
+
async meta() {
|
|
1480
|
+
const row = this.#guard(() => this.#require().prepare("SELECT \"version\", \"schema\" FROM " + quote(META_TABLE) + " WHERE \"id\" = 1").get());
|
|
1481
|
+
if (row === void 0) return void 0;
|
|
1482
|
+
const version = row.version;
|
|
1483
|
+
const text = row.schema;
|
|
1484
|
+
if (typeof text !== "string") return void 0;
|
|
1485
|
+
if (typeof version !== "number" && typeof version !== "bigint") return void 0;
|
|
1486
|
+
let parsed;
|
|
1487
|
+
try {
|
|
1488
|
+
parsed = JSON.parse(text);
|
|
1489
|
+
} catch {
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
const candidate = {
|
|
1493
|
+
version: Number(version),
|
|
1494
|
+
schema: parsed
|
|
1495
|
+
};
|
|
1496
|
+
if (!(0, _src_core.isDriverMeta)(candidate)) return void 0;
|
|
1497
|
+
return candidate;
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Persist `meta` verbatim (as JSON) into the reserved `_meta` table's single
|
|
1501
|
+
* row.
|
|
1502
|
+
*
|
|
1503
|
+
* @param meta - The {@link DriverMeta} to persist
|
|
1504
|
+
*/
|
|
1505
|
+
async stamp(meta) {
|
|
1506
|
+
this.#guard(() => {
|
|
1507
|
+
this.#require().prepare("INSERT OR REPLACE INTO " + quote(META_TABLE) + " (\"id\", \"version\", \"schema\") VALUES (1, ?, ?)").run([meta.version, JSON.stringify(meta.schema)]);
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
async snapshot(tables) {
|
|
1511
|
+
const database = this.#require();
|
|
1512
|
+
const names = tables ?? [...this.#schema.keys()];
|
|
1513
|
+
const captured = /* @__PURE__ */ new Map();
|
|
1514
|
+
for (const name of names) {
|
|
1515
|
+
const schema = this.#schema.get(name);
|
|
1516
|
+
if (schema === void 0) continue;
|
|
1517
|
+
captured.set(name, {
|
|
1518
|
+
names: schema.columns.map((column) => column.name),
|
|
1519
|
+
rows: database.prepare("SELECT * FROM " + quote(name)).all()
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
return async () => {
|
|
1523
|
+
const current = this.#require();
|
|
1524
|
+
current.transaction(() => {
|
|
1525
|
+
for (const [name, snapshot] of captured) {
|
|
1526
|
+
current.exec("DELETE FROM " + quote(name));
|
|
1527
|
+
const statement = current.prepare("INSERT OR REPLACE INTO " + quote(name) + " (" + snapshot.names.map(quote).join(", ") + ") VALUES (" + snapshot.names.map(() => "?").join(", ") + ")");
|
|
1528
|
+
for (const row of snapshot.rows) statement.run(snapshot.names.map((column) => row[column]));
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
#guard(run) {
|
|
1534
|
+
try {
|
|
1535
|
+
return run();
|
|
1536
|
+
} catch (error) {
|
|
1537
|
+
if (error instanceof _src_core.DatabaseError) throw error;
|
|
1538
|
+
if ((0, _orkestrel_sqlite.isSQLiteError)(error)) {
|
|
1539
|
+
if (error.code === "CONSTRAINT") throw new _src_core.DatabaseError("CONFLICT", error.message, {
|
|
1540
|
+
cause: error,
|
|
1541
|
+
code: error.code
|
|
1542
|
+
});
|
|
1543
|
+
if (error.code === "CLOSED") throw new _src_core.DatabaseError("CLOSED", error.message, {
|
|
1544
|
+
cause: error,
|
|
1545
|
+
code: error.code
|
|
1546
|
+
});
|
|
1547
|
+
if (error.code === "BUSY") throw new _src_core.DatabaseError("DRIVER", error.message, {
|
|
1548
|
+
cause: error,
|
|
1549
|
+
code: error.code,
|
|
1550
|
+
retryable: true
|
|
1551
|
+
});
|
|
1552
|
+
throw new _src_core.DatabaseError("DRIVER", error.message, {
|
|
1553
|
+
cause: error,
|
|
1554
|
+
code: error.code
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
throw new _src_core.DatabaseError("DRIVER", error instanceof Error ? error.message : String(error), { cause: error });
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
#require() {
|
|
1561
|
+
if (this.#database === void 0) throw new _src_core.DatabaseError("CLOSED", `SQLite database '${this.#path}' is not open`, { path: this.#path });
|
|
1562
|
+
return this.#database;
|
|
1563
|
+
}
|
|
1564
|
+
#table(name) {
|
|
1565
|
+
this.#require();
|
|
1566
|
+
const schema = this.#schema.get(name);
|
|
1567
|
+
if (schema === void 0) throw new _src_core.DatabaseError("NOT_FOUND", `Table '${name}' is not in the schema`, { table: name });
|
|
1568
|
+
return schema;
|
|
1569
|
+
}
|
|
1570
|
+
#key(key, schema) {
|
|
1571
|
+
const primary = schema.columns.find((column) => column.name === schema.primary);
|
|
1572
|
+
return encodeValue(key, primary === void 0 ? "text" : primary.type);
|
|
1573
|
+
}
|
|
1574
|
+
#applyPlan(database, plan, schema) {
|
|
1575
|
+
for (const step of plan.steps) {
|
|
1576
|
+
const table = step.operation === "table.add" ? step.table.name : step.table;
|
|
1577
|
+
if (step.operation !== "table.add" && !schema.has(table)) throw new _src_core.DatabaseError("MIGRATION", `migrate: unknown table '${table}'`, { table });
|
|
1578
|
+
for (const sql of stepToSQL(step)) database.exec(sql);
|
|
1579
|
+
if (step.operation === "table.add") schema.set(step.table.name, step.table);
|
|
1580
|
+
else if (step.operation === "table.remove") schema.delete(step.table);
|
|
1581
|
+
else {
|
|
1582
|
+
const existing = schema.get(table);
|
|
1583
|
+
if (existing !== void 0) schema.set(table, stepToSchema(existing, step));
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
//#endregion
|
|
958
1589
|
//#region src/server/factories.ts
|
|
959
1590
|
/**
|
|
960
1591
|
* Create a persistent JSON-file {@link DriverInterface} for the core database layer.
|
|
961
1592
|
*
|
|
962
1593
|
* @remarks
|
|
963
|
-
* Pass it to `createDatabase` from `@
|
|
1594
|
+
* Pass it to `createDatabase` from `@orkestrel/database` to run the whole typed database +
|
|
964
1595
|
* relations stack against a single JSON file instead of memory — the `Database` /
|
|
965
1596
|
* `Table` / `Query` / relations API is unchanged; only where the bytes live changes.
|
|
966
1597
|
* The driver is the reference `MemoryDriver` plus JSON-file persistence: `open` loads
|
|
@@ -988,8 +1619,51 @@ var JSONDriver = class {
|
|
|
988
1619
|
function createJSONDriver(path) {
|
|
989
1620
|
return new JSONDriver(path);
|
|
990
1621
|
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Create a trusted-mode SQLite {@link DriverInterface} for the core database layer.
|
|
1624
|
+
*
|
|
1625
|
+
* @remarks
|
|
1626
|
+
* Pass it to `createDatabase` from `@orkestrel/database` to run the whole typed database +
|
|
1627
|
+
* relations stack against a real SQLite database — the `Database` / `Table` /
|
|
1628
|
+
* `Query` / relations API is unchanged; only where the bytes live changes. Built
|
|
1629
|
+
* on the published `@orkestrel/sqlite` synchronous wrapper: `open` issues real
|
|
1630
|
+
* typed `CREATE TABLE` / `CREATE INDEX` statements (reopen-safe) plus a reserved
|
|
1631
|
+
* `_meta` table for `meta()` / `stamp()` — avoid naming a table `_meta`.
|
|
1632
|
+
* Querying, paging, and aggregation run natively (`records` / `count` /
|
|
1633
|
+
* `aggregate` / `stream`); `transaction` and `migrate` use real `BEGIN` /
|
|
1634
|
+
* `COMMIT` / `ROLLBACK`, so `migrate` is atomic even mid-plan.
|
|
1635
|
+
*
|
|
1636
|
+
* @param options - A bare database file path (`':memory:'` by default, for
|
|
1637
|
+
* back-compat), or a full {@link SQLiteDriverOptions} bag (`path`,
|
|
1638
|
+
* `readonly`, `timeout`, `foreignKeys`, `pragmas`)
|
|
1639
|
+
* @returns A {@link DriverInterface} backed by SQLite
|
|
1640
|
+
*
|
|
1641
|
+
* @example
|
|
1642
|
+
* ```ts
|
|
1643
|
+
* import { createDatabase } from '@orkestrel/database'
|
|
1644
|
+
* import { stringShape } from '@orkestrel/contract'
|
|
1645
|
+
* import { createSQLiteDriver } from '@orkestrel/database/server'
|
|
1646
|
+
*
|
|
1647
|
+
* const db = createDatabase({
|
|
1648
|
+
* driver: createSQLiteDriver('data/app.sqlite'),
|
|
1649
|
+
* tables: { users: { id: stringShape(), name: stringShape() } },
|
|
1650
|
+
* })
|
|
1651
|
+
* await db.table('users').set({ id: 'u1', name: 'Ada' }) // persisted to app.sqlite
|
|
1652
|
+
*
|
|
1653
|
+
* // Or with options:
|
|
1654
|
+
* createSQLiteDriver({ path: 'data/app.sqlite', pragmas: { journal_mode: 'WAL' } })
|
|
1655
|
+
* ```
|
|
1656
|
+
*/
|
|
1657
|
+
function createSQLiteDriver(options = ":memory:") {
|
|
1658
|
+
const resolved = (0, _orkestrel_contract.isString)(options) ? { path: options } : options;
|
|
1659
|
+
return new SQLiteDriver(resolved.path ?? ":memory:", resolved);
|
|
1660
|
+
}
|
|
991
1661
|
//#endregion
|
|
1662
|
+
exports.EXACT_COLUMN_TYPES = EXACT_COLUMN_TYPES;
|
|
1663
|
+
exports.EXACT_RANGE_COLUMN_TYPES = EXACT_RANGE_COLUMN_TYPES;
|
|
992
1664
|
exports.JSONDriver = JSONDriver;
|
|
1665
|
+
exports.META_TABLE = META_TABLE;
|
|
1666
|
+
exports.SQLiteDriver = SQLiteDriver;
|
|
993
1667
|
exports.aggregateSQL = aggregateSQL;
|
|
994
1668
|
exports.columnSQL = columnSQL;
|
|
995
1669
|
exports.compileCriteria = compileCriteria;
|
|
@@ -997,6 +1671,7 @@ exports.compileOrder = compileOrder;
|
|
|
997
1671
|
exports.compilePage = compilePage;
|
|
998
1672
|
exports.compileWhere = compileWhere;
|
|
999
1673
|
exports.createJSONDriver = createJSONDriver;
|
|
1674
|
+
exports.createSQLiteDriver = createSQLiteDriver;
|
|
1000
1675
|
exports.declaredType = declaredType;
|
|
1001
1676
|
exports.decodeRow = decodeRow;
|
|
1002
1677
|
exports.decodeValue = decodeValue;
|
|
@@ -1006,9 +1681,17 @@ exports.escapeLike = escapeLike;
|
|
|
1006
1681
|
exports.fieldColumn = fieldColumn;
|
|
1007
1682
|
exports.fragment = fragment;
|
|
1008
1683
|
exports.generateKey = generateKey;
|
|
1684
|
+
exports.indexName = indexName;
|
|
1685
|
+
exports.isExactCondition = isExactCondition;
|
|
1686
|
+
exports.isExactCriteria = isExactCriteria;
|
|
1687
|
+
exports.isExactOrder = isExactOrder;
|
|
1688
|
+
exports.jsonTypeColumn = jsonTypeColumn;
|
|
1689
|
+
exports.matchesDeclaredType = matchesDeclaredType;
|
|
1009
1690
|
exports.quote = quote;
|
|
1010
1691
|
exports.schemaToIndexes = schemaToIndexes;
|
|
1011
1692
|
exports.schemaToTable = schemaToTable;
|
|
1693
|
+
exports.stepToSQL = stepToSQL;
|
|
1694
|
+
exports.stepToSchema = stepToSchema;
|
|
1012
1695
|
exports.valueType = valueType;
|
|
1013
1696
|
|
|
1014
1697
|
//# sourceMappingURL=index.cjs.map
|