flatsql 2.0.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +54 -64
  3. package/dist/artifacts/index.d.ts +1 -3
  4. package/dist/artifacts/index.d.ts.map +1 -1
  5. package/dist/artifacts/index.js +1 -3
  6. package/dist/artifacts/index.js.map +1 -1
  7. package/dist/response/artifact.d.ts +1 -2
  8. package/dist/response/artifact.d.ts.map +1 -1
  9. package/dist/response/artifact.js.map +1 -1
  10. package/dist/response/types.d.ts +6 -0
  11. package/dist/response/types.d.ts.map +1 -1
  12. package/dist/stress/index.d.ts +0 -1
  13. package/dist/stress/index.d.ts.map +1 -1
  14. package/dist/stress/index.js +0 -1
  15. package/dist/stress/index.js.map +1 -1
  16. package/dist/stress/native-publish-event-workload.d.ts.map +1 -1
  17. package/dist/stress/native-publish-event-workload.js +14 -4
  18. package/dist/stress/native-publish-event-workload.js.map +1 -1
  19. package/dist/stress/smoke-runner.d.ts.map +1 -1
  20. package/dist/stress/smoke-runner.js +6 -5
  21. package/dist/stress/smoke-runner.js.map +1 -1
  22. package/docs/PARTITION-STORE-WASM.md +241 -0
  23. package/docs/PARTITION-STORE.md +635 -0
  24. package/package.json +61 -22
  25. package/wasm/flatsql-io.js +4 -1
  26. package/wasm/flatsql-ps-threads.wasm +0 -0
  27. package/wasm/flatsql-wasi-noeh.wasm +0 -0
  28. package/wasm/flatsql-wasi.wasm +0 -0
  29. package/wasm/flatsql.js +1 -1
  30. package/wasm/flatsql.wasm +0 -0
  31. package/wasm/index.d.ts +2 -0
  32. package/wasm/index.js +16 -1
  33. package/wasm/integrity.json +28 -0
  34. package/wasm/ps-node-fault.d.mts +23 -0
  35. package/wasm/ps-node-fault.mjs +381 -0
  36. package/wasm/ps-node-guest.mjs +156 -0
  37. package/wasm/ps-node-host.d.mts +86 -0
  38. package/wasm/ps-node-host.mjs +431 -0
  39. package/wasm/ps-node-imports.mjs +847 -0
  40. package/wasm/ps.d.ts +21 -0
  41. package/wasm/ps.js +75 -0
  42. package/dist/artifacts/builder.d.ts +0 -31
  43. package/dist/artifacts/builder.d.ts.map +0 -1
  44. package/dist/artifacts/builder.js +0 -514
  45. package/dist/artifacts/builder.js.map +0 -1
  46. package/dist/artifacts/demo-extractors.d.ts +0 -15
  47. package/dist/artifacts/demo-extractors.d.ts.map +0 -1
  48. package/dist/artifacts/demo-extractors.js +0 -238
  49. package/dist/artifacts/demo-extractors.js.map +0 -1
  50. package/dist/artifacts/standalone-builder.d.ts +0 -62
  51. package/dist/artifacts/standalone-builder.d.ts.map +0 -1
  52. package/dist/artifacts/standalone-builder.js +0 -144
  53. package/dist/artifacts/standalone-builder.js.map +0 -1
  54. package/dist/artifacts/worker-client.d.ts +0 -32
  55. package/dist/artifacts/worker-client.d.ts.map +0 -1
  56. package/dist/artifacts/worker-client.js +0 -415
  57. package/dist/artifacts/worker-client.js.map +0 -1
  58. package/dist/core/database.d.ts +0 -86
  59. package/dist/core/database.d.ts.map +0 -1
  60. package/dist/core/database.js +0 -900
  61. package/dist/core/database.js.map +0 -1
  62. package/dist/core/index.d.ts +0 -3
  63. package/dist/core/index.d.ts.map +0 -1
  64. package/dist/core/index.js +0 -3
  65. package/dist/core/index.js.map +0 -1
  66. package/dist/core/table-store.d.ts +0 -30
  67. package/dist/core/table-store.d.ts.map +0 -1
  68. package/dist/core/table-store.js +0 -187
  69. package/dist/core/table-store.js.map +0 -1
  70. package/dist/flatbuffer-accessor.d.ts +0 -35
  71. package/dist/flatbuffer-accessor.d.ts.map +0 -1
  72. package/dist/flatbuffer-accessor.js +0 -114
  73. package/dist/flatbuffer-accessor.js.map +0 -1
  74. package/dist/index.d.ts +0 -15
  75. package/dist/index.d.ts.map +0 -1
  76. package/dist/index.js +0 -22
  77. package/dist/index.js.map +0 -1
  78. package/dist/stress/publish-event-workload.d.ts +0 -3
  79. package/dist/stress/publish-event-workload.d.ts.map +0 -1
  80. package/dist/stress/publish-event-workload.js +0 -467
  81. package/dist/stress/publish-event-workload.js.map +0 -1
  82. package/wasm/flatsql-artifact.worker.js +0 -847
  83. package/wasm/standalone.d.ts +0 -122
  84. package/wasm/standalone.js +0 -914
@@ -1,900 +0,0 @@
1
- // Main database class: FlatBuffers-SQLite
2
- // Provides SQL query interface over FlatBuffer storage
3
- import { TableStore } from './table-store.js';
4
- import { StackedFlatBufferStore } from '../storage/index.js';
5
- import { parseSchema } from '../schema/index.js';
6
- // Security: Maximum SQL query length to prevent ReDoS attacks
7
- const MAX_SQL_LENGTH = 10000;
8
- // Security: Maximum queries per second (0 = unlimited)
9
- const DEFAULT_RATE_LIMIT = 0;
10
- export class FlatSQLDatabase {
11
- schema;
12
- storage;
13
- tables = new Map();
14
- accessor;
15
- // Security: Configuration options
16
- maxSqlLength;
17
- rateLimit;
18
- queryTimestamps = [];
19
- constructor(schema, accessor, options = {}) {
20
- this.schema = schema;
21
- this.storage = new StackedFlatBufferStore(schema.name);
22
- this.accessor = accessor;
23
- this.maxSqlLength = options.maxSqlLength ?? MAX_SQL_LENGTH;
24
- this.rateLimit = options.rateLimit ?? DEFAULT_RATE_LIMIT;
25
- // Initialize table stores
26
- for (const tableDef of schema.tables) {
27
- const tableStore = new TableStore(tableDef, this.storage, (data, path) => this.accessor.getField(data, path));
28
- this.tables.set(tableDef.name, tableStore);
29
- }
30
- }
31
- // Create database from schema source (IDL or JSON Schema)
32
- static fromSchema(source, accessor, name = 'default', options = {}) {
33
- const schema = parseSchema(source, name);
34
- return new FlatSQLDatabase(schema, accessor, options);
35
- }
36
- // Insert a record (as JSON that will be converted to FlatBuffer)
37
- insert(tableName, data) {
38
- const tableStore = this.tables.get(tableName);
39
- if (!tableStore) {
40
- throw new Error(`Table not found: ${tableName}`);
41
- }
42
- const buffer = this.accessor.buildBuffer(tableName, data);
43
- return tableStore.insert(buffer);
44
- }
45
- // Insert raw FlatBuffer data
46
- insertRaw(tableName, flatbufferData) {
47
- const tableStore = this.tables.get(tableName);
48
- if (!tableStore) {
49
- throw new Error(`Table not found: ${tableName}`);
50
- }
51
- return tableStore.insert(flatbufferData);
52
- }
53
- // Stream in multiple FlatBuffers
54
- stream(tableName, flatbuffers) {
55
- const rowids = [];
56
- for (const fb of flatbuffers) {
57
- rowids.push(this.insertRaw(tableName, fb));
58
- }
59
- return rowids;
60
- }
61
- // Create a runtime index without requiring database-specific schema annotations.
62
- createIndex(tableName, columnName) {
63
- const tableStore = this.tables.get(tableName);
64
- if (!tableStore) {
65
- throw new Error(`Table not found: ${tableName}`);
66
- }
67
- tableStore.createIndex(columnName);
68
- }
69
- getStorageBytes() {
70
- return this.storage.getCurrentSize();
71
- }
72
- // Execute a simple SQL query.
73
- //
74
- // Supported grammar:
75
- // SELECT <columns> FROM <table> [WHERE <expr>] [ORDER BY <col> [ASC|DESC], ...] [LIMIT <n>]
76
- //
77
- // <expr> supports AND / OR / NOT / parentheses over these predicates:
78
- // col = != <> < > <= >= value
79
- // col [NOT] BETWEEN low AND high
80
- // col [NOT] IN (v1, v2, ...)
81
- // col [NOT] LIKE 'pattern' -- % and _ wildcards, ASCII case-insensitive (SQLite default)
82
- // col IS [NOT] NULL
83
- //
84
- // FAIL-CLOSED CONTRACT: a WHERE or ORDER BY clause that this grammar cannot
85
- // parse throws. It must NEVER degrade into an unfiltered full-table scan --
86
- // silently returning every row to a caller that asked for a subset is a
87
- // wrong answer that no consumer can detect.
88
- query(sql) {
89
- // Security: Check SQL length to prevent ReDoS
90
- if (sql.length > this.maxSqlLength) {
91
- throw new Error(`SQL query exceeds maximum length of ${this.maxSqlLength} characters`);
92
- }
93
- // Security: Rate limiting
94
- if (this.rateLimit > 0) {
95
- this.enforceRateLimit();
96
- }
97
- const parsed = this.parseSQL(sql);
98
- if (parsed.type !== 'SELECT') {
99
- throw new Error(`Unsupported query type: ${parsed.type}`);
100
- }
101
- const tableStore = this.tables.get(parsed.table);
102
- if (!tableStore) {
103
- throw new Error(`Table not found: ${parsed.table}`);
104
- }
105
- let records = selectRecords(tableStore, parsed.where);
106
- if (parsed.orderBy !== undefined) {
107
- records = sortRecords(records, parsed.orderBy);
108
- }
109
- if (parsed.limit !== undefined) {
110
- records = records.slice(0, parsed.limit);
111
- }
112
- // Determine columns
113
- const tableDef = tableStore.getTableDef();
114
- const columns = parsed.columns[0] === '*'
115
- ? tableDef.columns.map(c => c.name)
116
- : parsed.columns;
117
- // Build result rows
118
- const rows = records.map(record => {
119
- return columns.map(col => {
120
- if (col === '_rowid')
121
- return record.rowid;
122
- if (col === '_offset')
123
- return record.offset;
124
- if (col === '_data')
125
- return record.data;
126
- return record.fields.get(col) ?? null;
127
- });
128
- });
129
- return {
130
- columns,
131
- rows,
132
- rowCount: rows.length,
133
- };
134
- }
135
- parseSQL(sql) {
136
- const clauses = splitSelectStatement(sql.trim());
137
- if (!clauses) {
138
- // Security: Don't expose SQL in error message (could contain sensitive data)
139
- throw new Error('SQL parse error: unsupported query syntax');
140
- }
141
- const columns = splitTopLevel(clauses.columns, ',').map(c => c.trim());
142
- if (columns.length === 0 || columns.some(c => c.length === 0)) {
143
- throw new Error('SQL parse error: empty column in SELECT list');
144
- }
145
- if (!/^\w+$/.test(clauses.table)) {
146
- throw new Error('SQL parse error: unsupported FROM target');
147
- }
148
- let limit;
149
- if (clauses.limit !== undefined) {
150
- if (!/^\d+$/.test(clauses.limit.trim())) {
151
- // Fail closed: an unparsed LIMIT must not silently return every row.
152
- throw new Error('SQL parse error: LIMIT requires a non-negative integer');
153
- }
154
- limit = Number(clauses.limit.trim());
155
- }
156
- // Fail closed: parseWhereClause / parseOrderByClause throw on anything the
157
- // grammar does not understand. Neither may fall back to "no filter".
158
- const where = clauses.where === undefined ? undefined : parseWhereClause(clauses.where);
159
- const orderBy = clauses.orderBy === undefined
160
- ? undefined
161
- : parseOrderByClause(clauses.orderBy);
162
- return {
163
- type: 'SELECT',
164
- table: clauses.table,
165
- columns,
166
- where,
167
- orderBy,
168
- limit,
169
- };
170
- }
171
- // Security: Rate limiting enforcement
172
- enforceRateLimit() {
173
- const now = Date.now();
174
- const windowMs = 1000; // 1 second window
175
- // Remove timestamps older than the window
176
- this.queryTimestamps = this.queryTimestamps.filter(ts => now - ts < windowMs);
177
- if (this.queryTimestamps.length >= this.rateLimit) {
178
- throw new Error(`Rate limit exceeded: maximum ${this.rateLimit} queries per second`);
179
- }
180
- this.queryTimestamps.push(now);
181
- }
182
- // Get table definition
183
- getTableDef(tableName) {
184
- return this.tables.get(tableName)?.getTableDef();
185
- }
186
- // List all tables
187
- listTables() {
188
- return Array.from(this.tables.keys());
189
- }
190
- // Get raw storage data (for export)
191
- exportData() {
192
- return this.storage.getData();
193
- }
194
- // Get schema
195
- getSchema() {
196
- return this.schema;
197
- }
198
- // Get statistics
199
- getStats() {
200
- return Array.from(this.tables.entries()).map(([name, store]) => ({
201
- tableName: name,
202
- recordCount: store.getRecordCount(),
203
- indexes: store.getIndexNames(),
204
- }));
205
- }
206
- }
207
- /** Keywords that terminate the clause preceding them, in statement order. */
208
- const SELECT_CLAUSE_KEYWORDS = ['FROM', 'WHERE', 'ORDER BY', 'LIMIT'];
209
- /**
210
- * Walk `sql` and report every top-level (unquoted, paren-depth 0) occurrence of
211
- * a clause keyword. Returns undefined if the string has unbalanced quotes or
212
- * parentheses, which callers treat as a parse failure.
213
- */
214
- function scanTopLevel(sql, onKeyword, onDelimiter, delimiters) {
215
- let depth = 0;
216
- let i = 0;
217
- while (i < sql.length) {
218
- const ch = sql[i];
219
- if (ch === "'" || ch === '"') {
220
- const quote = ch;
221
- i++;
222
- let closed = false;
223
- while (i < sql.length) {
224
- if (sql[i] === quote) {
225
- // Doubled quote is an escaped quote, not a terminator.
226
- if (sql[i + 1] === quote) {
227
- i += 2;
228
- continue;
229
- }
230
- i++;
231
- closed = true;
232
- break;
233
- }
234
- i++;
235
- }
236
- if (!closed) {
237
- return false;
238
- }
239
- continue;
240
- }
241
- if (ch === '(') {
242
- depth++;
243
- i++;
244
- continue;
245
- }
246
- if (ch === ')') {
247
- depth--;
248
- if (depth < 0) {
249
- return false;
250
- }
251
- i++;
252
- continue;
253
- }
254
- if (depth === 0 && delimiters && onDelimiter && delimiters.includes(ch)) {
255
- onDelimiter(ch, i);
256
- i++;
257
- continue;
258
- }
259
- if (depth === 0 && onKeyword && isWordBoundary(sql, i)) {
260
- let matched;
261
- for (const keyword of SELECT_CLAUSE_KEYWORDS) {
262
- const end = matchKeywordAt(sql, i, keyword);
263
- if (end !== -1) {
264
- matched = keyword;
265
- onKeyword(keyword, i, end);
266
- i = end;
267
- break;
268
- }
269
- }
270
- if (matched) {
271
- continue;
272
- }
273
- }
274
- i++;
275
- }
276
- return depth === 0;
277
- }
278
- function isWordBoundary(sql, index) {
279
- if (index === 0) {
280
- return true;
281
- }
282
- return !/[A-Za-z0-9_$]/.test(sql[index - 1]);
283
- }
284
- /**
285
- * Match `keyword` at `index` case-insensitively, allowing any run of
286
- * whitespace between the words of a multi-word keyword ("ORDER BY").
287
- * Returns the index just past the keyword, or -1.
288
- */
289
- function matchKeywordAt(sql, index, keyword) {
290
- let i = index;
291
- const words = keyword.split(' ');
292
- for (let w = 0; w < words.length; w++) {
293
- if (w > 0) {
294
- let sawSpace = false;
295
- while (i < sql.length && /\s/.test(sql[i])) {
296
- i++;
297
- sawSpace = true;
298
- }
299
- if (!sawSpace) {
300
- return -1;
301
- }
302
- }
303
- const word = words[w];
304
- if (sql.slice(i, i + word.length).toUpperCase() !== word) {
305
- return -1;
306
- }
307
- i += word.length;
308
- }
309
- // The keyword must be followed by a boundary, so "FROMAGE" is not "FROM".
310
- if (i < sql.length && /[A-Za-z0-9_$]/.test(sql[i])) {
311
- return -1;
312
- }
313
- return i;
314
- }
315
- function splitSelectStatement(sql) {
316
- const selectEnd = matchKeywordAt(sql, 0, 'SELECT');
317
- if (selectEnd === -1) {
318
- return undefined;
319
- }
320
- const marks = [];
321
- const balanced = scanTopLevel(sql, (keyword, start, end) => {
322
- marks.push({ keyword, start, end });
323
- });
324
- if (!balanced) {
325
- return undefined;
326
- }
327
- // Each clause keyword may appear at most once, and only in statement order.
328
- let previousRank = -1;
329
- for (const mark of marks) {
330
- const rank = SELECT_CLAUSE_KEYWORDS.indexOf(mark.keyword);
331
- if (rank <= previousRank) {
332
- return undefined;
333
- }
334
- previousRank = rank;
335
- }
336
- if (marks.length === 0 || marks[0].keyword !== 'FROM') {
337
- return undefined;
338
- }
339
- const sliceUntilNext = (markIndex) => {
340
- const start = marks[markIndex].end;
341
- const end = markIndex + 1 < marks.length ? marks[markIndex + 1].start : sql.length;
342
- return sql.slice(start, end).trim();
343
- };
344
- const clauses = {
345
- columns: sql.slice(selectEnd, marks[0].start).trim(),
346
- table: sliceUntilNext(0),
347
- };
348
- if (clauses.columns.length === 0) {
349
- return undefined;
350
- }
351
- for (let m = 1; m < marks.length; m++) {
352
- const text = sliceUntilNext(m);
353
- if (marks[m].keyword === 'WHERE') {
354
- clauses.where = text;
355
- }
356
- else if (marks[m].keyword === 'ORDER BY') {
357
- clauses.orderBy = text;
358
- }
359
- else {
360
- clauses.limit = text;
361
- }
362
- }
363
- return clauses;
364
- }
365
- /** Split on a delimiter that is outside quotes and at paren depth 0. */
366
- function splitTopLevel(text, delimiter) {
367
- const parts = [];
368
- let last = 0;
369
- const balanced = scanTopLevel(text, undefined, (_delimiter, index) => {
370
- parts.push(text.slice(last, index));
371
- last = index + 1;
372
- }, [delimiter]);
373
- if (!balanced) {
374
- throw new Error('SQL parse error: unbalanced quotes or parentheses');
375
- }
376
- parts.push(text.slice(last));
377
- return parts;
378
- }
379
- function tokenizeWhere(input) {
380
- const tokens = [];
381
- let i = 0;
382
- while (i < input.length) {
383
- const ch = input[i];
384
- if (/\s/.test(ch)) {
385
- i++;
386
- continue;
387
- }
388
- if (ch === "'" || ch === '"') {
389
- const quote = ch;
390
- let value = '';
391
- let closed = false;
392
- i++;
393
- while (i < input.length) {
394
- if (input[i] === quote) {
395
- if (input[i + 1] === quote) {
396
- value += quote;
397
- i += 2;
398
- continue;
399
- }
400
- i++;
401
- closed = true;
402
- break;
403
- }
404
- value += input[i];
405
- i++;
406
- }
407
- if (!closed) {
408
- throw new Error('SQL parse error: unterminated string literal in WHERE');
409
- }
410
- tokens.push({ type: 'string', value });
411
- continue;
412
- }
413
- if (ch === '(' || ch === ')' || ch === ',') {
414
- tokens.push({ type: 'punc', value: ch });
415
- i++;
416
- continue;
417
- }
418
- const two = input.slice(i, i + 2);
419
- if (two === '<=' || two === '>=' || two === '<>' || two === '!=') {
420
- tokens.push({ type: 'op', value: two });
421
- i += 2;
422
- continue;
423
- }
424
- if (ch === '=' || ch === '<' || ch === '>') {
425
- tokens.push({ type: 'op', value: ch });
426
- i++;
427
- continue;
428
- }
429
- if (/[0-9]/.test(ch) || (ch === '-' && /[0-9]/.test(input[i + 1] ?? ''))) {
430
- let j = ch === '-' ? i + 1 : i;
431
- while (j < input.length && /[0-9]/.test(input[j])) {
432
- j++;
433
- }
434
- let isFloat = false;
435
- if (input[j] === '.' && /[0-9]/.test(input[j + 1] ?? '')) {
436
- isFloat = true;
437
- j++;
438
- while (j < input.length && /[0-9]/.test(input[j])) {
439
- j++;
440
- }
441
- }
442
- if ((input[j] === 'e' || input[j] === 'E') && /[0-9+-]/.test(input[j + 1] ?? '')) {
443
- isFloat = true;
444
- j += 2;
445
- while (j < input.length && /[0-9]/.test(input[j])) {
446
- j++;
447
- }
448
- }
449
- const raw = input.slice(i, j);
450
- if (isFloat) {
451
- tokens.push({ type: 'number', value: parseFloat(raw) });
452
- }
453
- else {
454
- // Security: preserve exactness beyond the safe integer range.
455
- const asBigInt = BigInt(raw);
456
- const exceedsSafe = asBigInt > BigInt(Number.MAX_SAFE_INTEGER) ||
457
- asBigInt < BigInt(Number.MIN_SAFE_INTEGER);
458
- tokens.push({ type: 'number', value: exceedsSafe ? asBigInt : Number(raw) });
459
- }
460
- i = j;
461
- continue;
462
- }
463
- if (/[A-Za-z_]/.test(ch)) {
464
- let j = i;
465
- while (j < input.length && /[A-Za-z0-9_$.]/.test(input[j])) {
466
- j++;
467
- }
468
- tokens.push({ type: 'word', value: input.slice(i, j) });
469
- i = j;
470
- continue;
471
- }
472
- // Fail closed. An unrecognised character is never ignored.
473
- throw new Error('SQL parse error: unexpected character in WHERE clause');
474
- }
475
- return tokens;
476
- }
477
- // ---------------------------------------------------------------------------
478
- // WHERE parser (recursive descent)
479
- // ---------------------------------------------------------------------------
480
- class WhereParser {
481
- tokens;
482
- pos = 0;
483
- constructor(tokens) {
484
- this.tokens = tokens;
485
- }
486
- parse() {
487
- const node = this.parseOr();
488
- if (this.pos < this.tokens.length) {
489
- throw new Error(`SQL parse error: unexpected token after WHERE expression: ${this.describe(this.peek())}`);
490
- }
491
- return node;
492
- }
493
- peek() {
494
- return this.tokens[this.pos];
495
- }
496
- describe(token) {
497
- if (!token) {
498
- return 'end of clause';
499
- }
500
- // Security: never echo literal values; identifiers and operators are
501
- // schema-level names, which the engine already surfaces in other errors.
502
- switch (token.type) {
503
- case 'word':
504
- return token.value;
505
- case 'op':
506
- case 'punc':
507
- return token.value;
508
- case 'string':
509
- return 'string literal';
510
- case 'number':
511
- return 'numeric literal';
512
- }
513
- }
514
- /** Consume the next token if it is the given keyword. */
515
- acceptKeyword(keyword) {
516
- const token = this.peek();
517
- if (token && token.type === 'word' && token.value.toUpperCase() === keyword) {
518
- this.pos++;
519
- return true;
520
- }
521
- return false;
522
- }
523
- expectKeyword(keyword) {
524
- if (!this.acceptKeyword(keyword)) {
525
- throw new Error(`SQL parse error: expected ${keyword}, found ${this.describe(this.peek())}`);
526
- }
527
- }
528
- acceptPunc(value) {
529
- const token = this.peek();
530
- if (token && token.type === 'punc' && token.value === value) {
531
- this.pos++;
532
- return true;
533
- }
534
- return false;
535
- }
536
- expectPunc(value) {
537
- if (!this.acceptPunc(value)) {
538
- throw new Error(`SQL parse error: expected '${value}', found ${this.describe(this.peek())}`);
539
- }
540
- }
541
- parseOr() {
542
- const children = [this.parseAnd()];
543
- while (this.acceptKeyword('OR')) {
544
- children.push(this.parseAnd());
545
- }
546
- return children.length === 1 ? children[0] : { kind: 'or', children };
547
- }
548
- parseAnd() {
549
- const children = [this.parseNot()];
550
- while (this.acceptKeyword('AND')) {
551
- children.push(this.parseNot());
552
- }
553
- return children.length === 1 ? children[0] : { kind: 'and', children };
554
- }
555
- parseNot() {
556
- if (this.acceptKeyword('NOT')) {
557
- return { kind: 'not', child: this.parseNot() };
558
- }
559
- return this.parsePrimary();
560
- }
561
- parsePrimary() {
562
- if (this.acceptPunc('(')) {
563
- const node = this.parseOr();
564
- this.expectPunc(')');
565
- return node;
566
- }
567
- return this.parsePredicate();
568
- }
569
- parsePredicate() {
570
- const token = this.peek();
571
- if (!token || token.type !== 'word') {
572
- throw new Error(`SQL parse error: expected a column name, found ${this.describe(token)}`);
573
- }
574
- const upper = token.value.toUpperCase();
575
- if (upper === 'AND' || upper === 'OR' || upper === 'NOT') {
576
- throw new Error(`SQL parse error: expected a column name, found ${upper}`);
577
- }
578
- const column = token.value;
579
- this.pos++;
580
- if (this.acceptKeyword('IS')) {
581
- const negated = this.acceptKeyword('NOT');
582
- this.expectKeyword('NULL');
583
- return { kind: 'isNull', column, negated };
584
- }
585
- const negated = this.acceptKeyword('NOT');
586
- if (this.acceptKeyword('BETWEEN')) {
587
- const minValue = this.parseLiteral();
588
- this.expectKeyword('AND');
589
- const maxValue = this.parseLiteral();
590
- return { kind: 'between', column, minValue, maxValue, negated };
591
- }
592
- if (this.acceptKeyword('IN')) {
593
- this.expectPunc('(');
594
- const values = [];
595
- if (!this.acceptPunc(')')) {
596
- values.push(this.parseLiteral());
597
- while (this.acceptPunc(',')) {
598
- values.push(this.parseLiteral());
599
- }
600
- this.expectPunc(')');
601
- }
602
- return { kind: 'in', column, values, negated };
603
- }
604
- if (this.acceptKeyword('LIKE')) {
605
- const pattern = this.parseLiteral();
606
- if (typeof pattern !== 'string') {
607
- throw new Error('SQL parse error: LIKE requires a string pattern');
608
- }
609
- return { kind: 'like', column, matcher: likeToRegExp(pattern), negated };
610
- }
611
- if (negated) {
612
- throw new Error(`SQL parse error: expected BETWEEN, IN or LIKE after NOT, found ${this.describe(this.peek())}`);
613
- }
614
- const opToken = this.peek();
615
- if (!opToken || opToken.type !== 'op') {
616
- throw new Error(`SQL parse error: expected a comparison operator, found ${this.describe(opToken)}`);
617
- }
618
- this.pos++;
619
- return { kind: 'cmp', column, operator: opToken.value, value: this.parseLiteral() };
620
- }
621
- parseLiteral() {
622
- const token = this.peek();
623
- if (!token) {
624
- throw new Error('SQL parse error: expected a value, found end of clause');
625
- }
626
- if (token.type === 'string' || token.type === 'number') {
627
- this.pos++;
628
- return token.value;
629
- }
630
- if (token.type === 'word') {
631
- const upper = token.value.toUpperCase();
632
- if (upper === 'TRUE') {
633
- this.pos++;
634
- return true;
635
- }
636
- if (upper === 'FALSE') {
637
- this.pos++;
638
- return false;
639
- }
640
- if (upper === 'NULL') {
641
- this.pos++;
642
- return null;
643
- }
644
- }
645
- // Bare identifiers are not values: column-to-column comparison is not
646
- // supported, and silently treating one as a string would be a wrong answer.
647
- throw new Error(`SQL parse error: expected a literal value, found ${this.describe(token)}`);
648
- }
649
- }
650
- function parseWhereClause(clause) {
651
- const trimmed = clause.trim();
652
- if (trimmed.length === 0) {
653
- throw new Error('SQL parse error: empty WHERE clause');
654
- }
655
- return new WhereParser(tokenizeWhere(trimmed)).parse();
656
- }
657
- function parseOrderByClause(clause) {
658
- const terms = splitTopLevel(clause, ',')
659
- .map(part => part.trim())
660
- .filter(part => part.length > 0);
661
- if (terms.length === 0) {
662
- throw new Error('SQL parse error: empty ORDER BY clause');
663
- }
664
- return terms.map(term => {
665
- const match = term.match(/^([A-Za-z_][A-Za-z0-9_$.]*)(?:\s+(ASC|DESC))?$/i);
666
- if (!match) {
667
- // Fail closed: an ORDER BY the engine cannot honour is an error, not an
668
- // unordered result set.
669
- throw new Error('SQL parse error: unsupported ORDER BY term');
670
- }
671
- return {
672
- column: match[1],
673
- descending: (match[2] ?? '').toUpperCase() === 'DESC',
674
- };
675
- });
676
- }
677
- /**
678
- * Translate a SQL LIKE pattern into an anchored RegExp.
679
- *
680
- * `%` matches any run of characters, `_` matches exactly one. Matching is ASCII
681
- * case-insensitive, the SQLite default. Every other character is escaped, so a
682
- * pattern can never inject regex syntax, and runs of `%` are collapsed so the
683
- * compiled expression cannot backtrack pathologically.
684
- */
685
- function likeToRegExp(pattern) {
686
- let source = '^';
687
- let previousWasWildcard = false;
688
- for (const ch of pattern) {
689
- if (ch === '%') {
690
- if (!previousWasWildcard) {
691
- source += '[\\s\\S]*';
692
- previousWasWildcard = true;
693
- }
694
- continue;
695
- }
696
- previousWasWildcard = false;
697
- if (ch === '_') {
698
- source += '[\\s\\S]';
699
- }
700
- else {
701
- source += ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
702
- }
703
- }
704
- return new RegExp(`${source}$`, 'i');
705
- }
706
- // ---------------------------------------------------------------------------
707
- // Evaluation
708
- // ---------------------------------------------------------------------------
709
- function getRecordValue(record, column) {
710
- if (column === '_rowid') {
711
- return record.rowid;
712
- }
713
- if (column === '_offset') {
714
- return record.offset;
715
- }
716
- return record.fields.get(column);
717
- }
718
- function isMissing(value) {
719
- return value === null || value === undefined;
720
- }
721
- /** Numeric-aware equality that bridges the BigInt/Number split in stored fields. */
722
- function valuesEqual(a, b) {
723
- if (typeof a === 'bigint' || typeof b === 'bigint') {
724
- if (typeof a === 'number' && !Number.isInteger(a)) {
725
- return false;
726
- }
727
- if (typeof b === 'number' && !Number.isInteger(b)) {
728
- return false;
729
- }
730
- if ((typeof a === 'bigint' || typeof a === 'number') &&
731
- (typeof b === 'bigint' || typeof b === 'number')) {
732
- return BigInt(a) === BigInt(b);
733
- }
734
- return false;
735
- }
736
- return a === b;
737
- }
738
- /** Ordering comparison; returns undefined when the values are not comparable. */
739
- function compareValues(a, b) {
740
- if (isMissing(a) || isMissing(b)) {
741
- return undefined;
742
- }
743
- if (typeof a === 'bigint' || typeof b === 'bigint') {
744
- if ((typeof a === 'bigint' || typeof a === 'number') &&
745
- (typeof b === 'bigint' || typeof b === 'number')) {
746
- const left = typeof a === 'bigint' ? a : BigInt(Math.trunc(a));
747
- const right = typeof b === 'bigint' ? b : BigInt(Math.trunc(b));
748
- return left < right ? -1 : left > right ? 1 : 0;
749
- }
750
- return undefined;
751
- }
752
- if (typeof a === 'number' && typeof b === 'number') {
753
- return a < b ? -1 : a > b ? 1 : 0;
754
- }
755
- if (typeof a === 'string' && typeof b === 'string') {
756
- return a < b ? -1 : a > b ? 1 : 0;
757
- }
758
- if (typeof a === 'boolean' && typeof b === 'boolean') {
759
- return a === b ? 0 : a ? 1 : -1;
760
- }
761
- return undefined;
762
- }
763
- function evaluateWhere(record, node) {
764
- switch (node.kind) {
765
- case 'and':
766
- return node.children.every(child => evaluateWhere(record, child));
767
- case 'or':
768
- return node.children.some(child => evaluateWhere(record, child));
769
- case 'not':
770
- return !evaluateWhere(record, node.child);
771
- case 'isNull': {
772
- const missing = isMissing(getRecordValue(record, node.column));
773
- return node.negated ? !missing : missing;
774
- }
775
- case 'cmp': {
776
- const value = getRecordValue(record, node.column);
777
- if (node.operator === '=') {
778
- return valuesEqual(value, node.value);
779
- }
780
- if (node.operator === '!=' || node.operator === '<>') {
781
- // NULL is not "not equal" to anything, matching SQL's NULL semantics
782
- // closely enough that a filtered result never includes unknowns.
783
- if (isMissing(value)) {
784
- return false;
785
- }
786
- return !valuesEqual(value, node.value);
787
- }
788
- const ordering = compareValues(value, node.value);
789
- if (ordering === undefined) {
790
- return false;
791
- }
792
- switch (node.operator) {
793
- case '<':
794
- return ordering < 0;
795
- case '>':
796
- return ordering > 0;
797
- case '<=':
798
- return ordering <= 0;
799
- case '>=':
800
- return ordering >= 0;
801
- }
802
- return false;
803
- }
804
- case 'between': {
805
- const value = getRecordValue(record, node.column);
806
- const low = compareValues(value, node.minValue);
807
- const high = compareValues(value, node.maxValue);
808
- if (low === undefined || high === undefined) {
809
- return false;
810
- }
811
- const inRange = low >= 0 && high <= 0;
812
- return node.negated ? !inRange : inRange;
813
- }
814
- case 'in': {
815
- const value = getRecordValue(record, node.column);
816
- if (isMissing(value)) {
817
- return false;
818
- }
819
- const found = node.values.some(candidate => valuesEqual(value, candidate));
820
- return node.negated ? !found : found;
821
- }
822
- case 'like': {
823
- const value = getRecordValue(record, node.column);
824
- if (isMissing(value)) {
825
- return false;
826
- }
827
- const matched = node.matcher.test(String(value));
828
- return node.negated ? !matched : matched;
829
- }
830
- }
831
- }
832
- // ---------------------------------------------------------------------------
833
- // Planning
834
- // ---------------------------------------------------------------------------
835
- /**
836
- * Pick an indexed seek that is guaranteed to be a superset of the rows the
837
- * predicate can match. Only conjunctive (AND) context is safe: under OR, a row
838
- * may match through the other branch, so the seek would lose rows.
839
- */
840
- function planIndexSeek(tableStore, node) {
841
- if (node.kind === 'cmp' && node.operator === '=' && tableStore.hasIndex(node.column)) {
842
- return { records: tableStore.findByIndex(node.column, node.value), exact: true };
843
- }
844
- if (node.kind === 'between' && !node.negated && tableStore.hasIndex(node.column)) {
845
- return {
846
- records: tableStore.findByRange(node.column, node.minValue, node.maxValue),
847
- exact: true,
848
- };
849
- }
850
- if (node.kind === 'and') {
851
- for (const child of node.children) {
852
- const seek = planIndexSeek(tableStore, child);
853
- if (seek) {
854
- // The remaining conjuncts still have to be checked.
855
- return { records: seek.records, exact: false };
856
- }
857
- }
858
- }
859
- return undefined;
860
- }
861
- function selectRecords(tableStore, where) {
862
- if (!where) {
863
- return tableStore.scanAll();
864
- }
865
- const seek = planIndexSeek(tableStore, where);
866
- if (seek?.exact) {
867
- return seek.records;
868
- }
869
- const candidates = seek ? seek.records : tableStore.scanAll();
870
- return candidates.filter(record => evaluateWhere(record, where));
871
- }
872
- function sortRecords(records, terms) {
873
- // Decorate with the original position so the sort is stable across engines.
874
- return records
875
- .map((record, index) => ({ record, index }))
876
- .sort((left, right) => {
877
- for (const term of terms) {
878
- const a = getRecordValue(left.record, term.column);
879
- const b = getRecordValue(right.record, term.column);
880
- // SQLite orders NULL first ascending; mirror that.
881
- const aMissing = isMissing(a);
882
- const bMissing = isMissing(b);
883
- if (aMissing || bMissing) {
884
- if (aMissing && bMissing) {
885
- continue;
886
- }
887
- const nullsFirst = aMissing ? -1 : 1;
888
- return term.descending ? -nullsFirst : nullsFirst;
889
- }
890
- const ordering = compareValues(a, b);
891
- if (ordering === undefined || ordering === 0) {
892
- continue;
893
- }
894
- return term.descending ? -ordering : ordering;
895
- }
896
- return left.index - right.index;
897
- })
898
- .map(entry => entry.record);
899
- }
900
- //# sourceMappingURL=database.js.map