jsql-neo 6.0.0-beta2 → 6.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 +134 -26
- package/index.d.ts +30 -4
- package/index.js +1 -1
- package/lib/btree.js +81 -16
- package/lib/database.js +35 -4
- package/lib/mongo_server.js +1 -1
- package/lib/native_client.js +14 -1
- package/lib/pg_server.js +8 -2
- package/lib/plugin.js +2 -2
- package/lib/plugins/timestamps.js +15 -1
- package/lib/prepared.js +37 -0
- package/lib/query.js +24 -5
- package/lib/redis_server.js +58 -28
- package/lib/sql.js +749 -57
- package/lib/table.js +68 -21
- package/lib/wasm_client.js +72 -9
- package/package.json +4 -3
- package/test/btree.test.js +16 -0
- package/test/query-opt.test.js +109 -0
- package/test/readme-audit.test.js +281 -0
- package/wasm/browser.mjs +12 -2
- package/wasm/package.json +1 -1
package/lib/table.js
CHANGED
|
@@ -347,8 +347,17 @@ class Table {
|
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
findOne(query = {}) {
|
|
350
|
-
|
|
351
|
-
|
|
350
|
+
let rows = this._getVisibleRows();
|
|
351
|
+
if (Object.keys(query).length > 0) {
|
|
352
|
+
rows = this._applyFilterOptimized(rows, query);
|
|
353
|
+
}
|
|
354
|
+
return rows.length > 0 ? { ...rows[0] } : null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
exists(query = {}) {
|
|
358
|
+
if (Object.keys(query).length === 0) return this._getVisibleRows().length > 0;
|
|
359
|
+
const rows = this._applyFilterOptimized(this._getVisibleRows(), query);
|
|
360
|
+
return rows.length > 0;
|
|
352
361
|
}
|
|
353
362
|
|
|
354
363
|
findById(id) {
|
|
@@ -853,39 +862,77 @@ class Table {
|
|
|
853
862
|
// 内部: 过滤器(B-Tree 加速)
|
|
854
863
|
// ============================================================
|
|
855
864
|
|
|
865
|
+
_rowsFromIndices(indices) {
|
|
866
|
+
const subset = [];
|
|
867
|
+
for (let i = 0; i < indices.length; i++) {
|
|
868
|
+
const row = this._rows[indices[i]];
|
|
869
|
+
if (row) subset.push(row);
|
|
870
|
+
}
|
|
871
|
+
return subset;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
_likePrefix(pattern) {
|
|
875
|
+
if (typeof pattern !== 'string' || pattern.length === 0) return null;
|
|
876
|
+
if (pattern[0] === '%' || pattern[0] === '_') return null;
|
|
877
|
+
let end = pattern.length;
|
|
878
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
879
|
+
const c = pattern[i];
|
|
880
|
+
if (c === '%' || c === '_') { end = i; break; }
|
|
881
|
+
}
|
|
882
|
+
return end > 0 ? pattern.slice(0, end) : null;
|
|
883
|
+
}
|
|
884
|
+
|
|
856
885
|
_applyFilterOptimized(rows, query) {
|
|
857
886
|
this._ensureBTrees();
|
|
858
|
-
// 尝试用 B-Tree 加速
|
|
859
887
|
for (const [field, condition] of Object.entries(query)) {
|
|
860
888
|
if (field === '$or' || field === '$and' || field === '$not') continue;
|
|
861
|
-
// 等值查询用 B-Tree
|
|
862
889
|
if (typeof condition !== 'object' || condition === null || Array.isArray(condition) || condition instanceof RegExp) {
|
|
863
890
|
if (this._btrees[field]) {
|
|
864
891
|
const indices = this._btrees[field].search(condition);
|
|
865
|
-
if (indices.length
|
|
866
|
-
|
|
867
|
-
return this._applyFilter(subset, query);
|
|
868
|
-
}
|
|
869
|
-
return [];
|
|
892
|
+
if (indices.length === 0) return [];
|
|
893
|
+
return this._applyFilter(this._rowsFromIndices(indices), query);
|
|
870
894
|
}
|
|
871
895
|
if (this._indexes[field] && this._indexes[field].has(condition)) {
|
|
872
|
-
|
|
873
|
-
const subset = indices.map(i => this._rows[i]).filter(r => r);
|
|
874
|
-
return this._applyFilter(subset, query);
|
|
896
|
+
return this._applyFilter(this._rowsFromIndices(this._indexes[field].get(condition)), query);
|
|
875
897
|
}
|
|
876
898
|
if (this._indexes[field]) return [];
|
|
877
899
|
}
|
|
878
|
-
// 范围查询用 B-Tree
|
|
879
900
|
if (condition && typeof condition === 'object') {
|
|
880
|
-
if (condition.$
|
|
881
|
-
const indices = this._btrees[field].
|
|
882
|
-
|
|
883
|
-
return this._applyFilter(
|
|
901
|
+
if (this._btrees[field] && Array.isArray(condition.$in) && condition.$in.length > 0) {
|
|
902
|
+
const indices = this._btrees[field].searchMany(condition.$in);
|
|
903
|
+
if (indices.length === 0) return [];
|
|
904
|
+
return this._applyFilter(this._rowsFromIndices(indices), query);
|
|
905
|
+
}
|
|
906
|
+
if (this._btrees[field] && typeof condition.$like === 'string') {
|
|
907
|
+
const prefix = this._likePrefix(condition.$like);
|
|
908
|
+
if (prefix) {
|
|
909
|
+
const indices = this._btrees[field].prefix(prefix);
|
|
910
|
+
if (indices.length === 0) return [];
|
|
911
|
+
return this._applyFilter(this._rowsFromIndices(indices), query);
|
|
912
|
+
}
|
|
884
913
|
}
|
|
885
|
-
if (condition.$
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
914
|
+
if (this._btrees[field] && (condition.$gt !== undefined || condition.$gte !== undefined ||
|
|
915
|
+
condition.$lt !== undefined || condition.$lte !== undefined || condition.$between)) {
|
|
916
|
+
let min, max, exclusiveMin = false, exclusiveMax = false;
|
|
917
|
+
if (condition.$between && Array.isArray(condition.$between) && condition.$between.length === 2) {
|
|
918
|
+
min = condition.$between[0];
|
|
919
|
+
max = condition.$between[1];
|
|
920
|
+
} else {
|
|
921
|
+
if (condition.$gte !== undefined) min = condition.$gte;
|
|
922
|
+
else if (condition.$gt !== undefined) { min = condition.$gt; exclusiveMin = true; }
|
|
923
|
+
if (condition.$lte !== undefined) max = condition.$lte;
|
|
924
|
+
else if (condition.$lt !== undefined) { max = condition.$lt; exclusiveMax = true; }
|
|
925
|
+
}
|
|
926
|
+
if (min === undefined && max === undefined) continue;
|
|
927
|
+
let indices;
|
|
928
|
+
if (min !== undefined && max !== undefined) {
|
|
929
|
+
indices = this._btrees[field].range(min, max);
|
|
930
|
+
} else if (min !== undefined) {
|
|
931
|
+
indices = exclusiveMin ? this._btrees[field].greaterThan(min) : this._btrees[field].greaterThanEqual(min);
|
|
932
|
+
} else {
|
|
933
|
+
indices = exclusiveMax ? this._btrees[field].lessThan(max) : this._btrees[field].lessThanEqual(max);
|
|
934
|
+
}
|
|
935
|
+
return this._applyFilter(this._rowsFromIndices(indices), query);
|
|
889
936
|
}
|
|
890
937
|
}
|
|
891
938
|
}
|
package/lib/wasm_client.js
CHANGED
|
@@ -10,6 +10,32 @@ function safeJsonParse(str) {
|
|
|
10
10
|
try { return JSON.parse(str); } catch { return str; }
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* 序列化并给出可读错误。直接 JSON.stringify 遇到循环引用会抛
|
|
15
|
+
* "Converting circular structure to JSON",指向不到具体列,这里补上表/列信息。
|
|
16
|
+
*/
|
|
17
|
+
function safeStringify(value, table, column) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
const msg = (e && e.message) ? e.message : String(e);
|
|
22
|
+
throw new Error(`Cannot serialize column '${column}' of table '${table}': ${msg}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 插入行必须是普通对象;null/undefined/数组/基本类型在这里就被拦下 */
|
|
27
|
+
function assertRowObject(row, index, isBatch) {
|
|
28
|
+
const ok = row !== null && row !== undefined && typeof row === 'object' &&
|
|
29
|
+
!Array.isArray(row) && !(row instanceof Date);
|
|
30
|
+
if (ok) return;
|
|
31
|
+
const got = row === null ? 'null'
|
|
32
|
+
: Array.isArray(row) ? 'array'
|
|
33
|
+
: row instanceof Date ? 'Date'
|
|
34
|
+
: typeof row;
|
|
35
|
+
const where = isBatch ? ` at index ${index}` : '';
|
|
36
|
+
throw new TypeError(`insert(): expected a row object${where}, got ${got}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
const NATIVE_TYPE_MAP = {
|
|
14
40
|
text: 'string',
|
|
15
41
|
varchar: 'string',
|
|
@@ -422,7 +448,7 @@ class JSQL {
|
|
|
422
448
|
if (typeof def === 'string') t = def.toLowerCase();
|
|
423
449
|
else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
|
|
424
450
|
if (v !== null && v !== undefined && ['json', 'array', 'object'].includes(t) && typeof v !== 'string') {
|
|
425
|
-
out[k] =
|
|
451
|
+
out[k] = safeStringify(v, table, k);
|
|
426
452
|
} else if (v instanceof Date) {
|
|
427
453
|
out[k] = v.toISOString();
|
|
428
454
|
} else {
|
|
@@ -432,7 +458,7 @@ class JSQL {
|
|
|
432
458
|
return out;
|
|
433
459
|
});
|
|
434
460
|
}
|
|
435
|
-
const r = safeJsonParse(wasmBindings.jsql_insert_json(table,
|
|
461
|
+
const r = safeJsonParse(wasmBindings.jsql_insert_json(table, safeStringify(rows, table, '<rows>')));
|
|
436
462
|
if (r && r.error) throw new Error(r.error);
|
|
437
463
|
if (r && Array.isArray(r) && !this._pkAutoIncrement(table)) {
|
|
438
464
|
const pk = this._pkOf(table);
|
|
@@ -448,24 +474,48 @@ class JSQL {
|
|
|
448
474
|
|
|
449
475
|
async _flush() {
|
|
450
476
|
if (!this._runHooks('beforeFlush', [])) return null;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
flushed[table] = r;
|
|
456
|
-
}
|
|
477
|
+
// 先把整个 buffer 原子取出再写入。
|
|
478
|
+
// 旧写法是遍历 this._buffer 然后在 await 之后才清空 —— 这期间其他协程会继续
|
|
479
|
+
// 往同一个数组 push,导致同一批行被多个 flush 重复写(并发 insert 行数暴涨)。
|
|
480
|
+
const pending = this._buffer;
|
|
457
481
|
this._buffer = {};
|
|
458
482
|
this._bufferSize = 0;
|
|
483
|
+
const flushed = {};
|
|
484
|
+
try {
|
|
485
|
+
for (const [table, rows] of Object.entries(pending)) {
|
|
486
|
+
if (rows.length === 0) continue;
|
|
487
|
+
flushed[table] = await this._insertBatch(table, rows);
|
|
488
|
+
}
|
|
489
|
+
} catch (e) {
|
|
490
|
+
// 坏行绝不回写 buffer:否则 stop() 二次 flush 会再抛同一个错,进程 exit 1。
|
|
491
|
+
// 但也不能静默丢数据 —— 记到 _flushErrors 供排查(见 lastFlushErrors())。
|
|
492
|
+
if (!this._flushErrors) this._flushErrors = [];
|
|
493
|
+
this._flushErrors.push({
|
|
494
|
+
at: Date.now(),
|
|
495
|
+
message: (e && e.message) ? e.message : String(e),
|
|
496
|
+
tables: Object.keys(pending),
|
|
497
|
+
rowCounts: Object.fromEntries(Object.entries(pending).map(([t, r]) => [t, r.length])),
|
|
498
|
+
});
|
|
499
|
+
throw e;
|
|
500
|
+
}
|
|
459
501
|
this._runHooks('afterFlush', []);
|
|
460
502
|
return flushed;
|
|
461
503
|
}
|
|
462
504
|
|
|
505
|
+
/** 返回历次 flush 失败的记录(坏行不会回写 buffer,只能从这里查) */
|
|
506
|
+
lastFlushErrors() {
|
|
507
|
+
return (this._flushErrors || []).slice();
|
|
508
|
+
}
|
|
509
|
+
|
|
463
510
|
async flush() {
|
|
464
511
|
await this._flush();
|
|
465
512
|
}
|
|
466
513
|
|
|
467
514
|
async insert(table, data) {
|
|
515
|
+
// 入参校验:null / undefined / 非对象会在下游 Object.entries() 处炸成
|
|
516
|
+
// 难以定位的 "Cannot convert undefined or null to object",这里直接给明确 TypeError
|
|
468
517
|
const arr = Array.isArray(data) ? data : [data];
|
|
518
|
+
arr.forEach((row, i) => assertRowObject(row, i, arr.length > 1 || Array.isArray(data)));
|
|
469
519
|
if (!this._runHooks('beforeInsert', [table, arr])) return [];
|
|
470
520
|
if (arr.length > 1) {
|
|
471
521
|
await this._flush();
|
|
@@ -484,7 +534,10 @@ class JSQL {
|
|
|
484
534
|
this._buffer[table].push(arr[0]);
|
|
485
535
|
this._bufferSize++;
|
|
486
536
|
const flushed = await this._flush();
|
|
487
|
-
|
|
537
|
+
const result = flushed && flushed[table] ? flushed[table] : null;
|
|
538
|
+
this._emit('insert', { table, count: arr.length, ids: result });
|
|
539
|
+
this._runHooks('afterInsert', [table, arr, result]);
|
|
540
|
+
return result;
|
|
488
541
|
}
|
|
489
542
|
|
|
490
543
|
async insertMany(table, data) {
|
|
@@ -493,6 +546,7 @@ class JSQL {
|
|
|
493
546
|
|
|
494
547
|
async createTable(name, schema) {
|
|
495
548
|
await this._flush();
|
|
549
|
+
if (typeof schema === 'string') schema = parseSchemaString(schema);
|
|
496
550
|
if (!this._runHooks('beforeCreateTable', [name, schema])) return null;
|
|
497
551
|
const r = safeJsonParse(wasmBindings.jsql_create_table(name, JSON.stringify(mapNativeSchema(schema))));
|
|
498
552
|
if (r && r.ok === false) throw new Error(r.error || 'create table failed');
|
|
@@ -692,6 +746,15 @@ class JSQL {
|
|
|
692
746
|
return result;
|
|
693
747
|
}
|
|
694
748
|
|
|
749
|
+
async exists(table, filter = {}) {
|
|
750
|
+
const rows = await this.find(table, filter, { limit: 1 });
|
|
751
|
+
return Array.isArray(rows) && rows.length > 0;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
prepare(sql) {
|
|
755
|
+
return require('./prepared').createPrepared(this, sql);
|
|
756
|
+
}
|
|
757
|
+
|
|
695
758
|
async stop() {
|
|
696
759
|
await this._flush();
|
|
697
760
|
this._runHooks('onStop', []);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.2",
|
|
4
4
|
"description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"./wasm/browser.d.ts": "./wasm/browser.d.ts",
|
|
23
23
|
"./wasm/*": "./wasm/*",
|
|
24
|
+
"./lib/plugins": "./lib/plugins/index.js",
|
|
24
25
|
"./lib/*.js": "./lib/*.js",
|
|
25
26
|
"./lib/*": "./lib/*.js",
|
|
26
27
|
"./package.json": "./package.json",
|
|
@@ -45,12 +46,12 @@
|
|
|
45
46
|
],
|
|
46
47
|
"scripts": {
|
|
47
48
|
"postinstall": "node postinstall.js",
|
|
48
|
-
"test": "node test/smoke.js && node test/sql-parser.test.js",
|
|
49
|
+
"test": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js",
|
|
49
50
|
"test:parser": "node test/sql-parser.test.js",
|
|
50
51
|
"test:btree": "node test/btree.test.js",
|
|
51
52
|
"test:native": "node test/native.test.js",
|
|
52
53
|
"test:regress": "node test/regress-5.1.0.js",
|
|
53
|
-
"test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/native.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js",
|
|
54
|
+
"test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js",
|
|
54
55
|
"test:coverage": "c8 --all --include='lib/**' --exclude='lib/mysql_server.js' --exclude='lib/native_client.js' --exclude='lib/wasm_client.js' --exclude='lib/mysql_compat.js' --exclude='lib/nedb_compat.js' --exclude='lib/plugin.js' --reporter=text --reporter=lcov node test/coverage.js",
|
|
55
56
|
"test:orms": "node examples/orms/run-all.js"
|
|
56
57
|
},
|
package/test/btree.test.js
CHANGED
|
@@ -58,6 +58,22 @@ function validate(bt) {
|
|
|
58
58
|
ok('non-unique remove value keeps key until last', n.search('a').length === 0 && n.size === 1);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/* ---- 二分查找 / IN / 前缀 ---- */
|
|
62
|
+
{
|
|
63
|
+
const bt = new BTree(16);
|
|
64
|
+
for (let i = 0; i < 200; i++) bt.insert(i, i);
|
|
65
|
+
ok('binary search mid', JSON.stringify(bt.search(123)) === '[123]');
|
|
66
|
+
ok('searchMany unique', JSON.stringify(bt.searchMany([0, 199, 50, 50]).sort((a, b) => a - b)) === '[0,50,199]');
|
|
67
|
+
ok('greaterThanEqual last two', JSON.stringify(bt.greaterThanEqual(198).sort((a, b) => a - b)) === '[198,199]');
|
|
68
|
+
ok('lessThanEqual first two', JSON.stringify(bt.lessThanEqual(1).sort((a, b) => a - b)) === '[0,1]');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
const bt = new BTree(8);
|
|
73
|
+
['alpha', 'alpine', 'beta', 'alpine'].forEach((k, i) => bt.insert(k, i));
|
|
74
|
+
ok('prefix alp', JSON.stringify(bt.prefix('alp').sort((a, b) => a - b)) === '[0,1,3]');
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
/* ---- 多值键删除只影响目标 rowIndex(size 对照 distinct keys) ---- */
|
|
62
78
|
{
|
|
63
79
|
const bt = new BTree(8);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Query / B-Tree / exists / prepare 回归
|
|
3
|
+
* node test/query-opt.test.js
|
|
4
|
+
*/
|
|
5
|
+
const Database = require('../lib/database');
|
|
6
|
+
const BTree = require('../lib/btree');
|
|
7
|
+
|
|
8
|
+
let passed = 0, failed = 0;
|
|
9
|
+
function ok(name, cond, extra) {
|
|
10
|
+
if (cond) { passed++; }
|
|
11
|
+
else { failed++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra) : ''); }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
const bt = new BTree(8);
|
|
16
|
+
for (let i = 0; i < 80; i++) bt.insert(i, i);
|
|
17
|
+
ok('search hit', JSON.stringify(bt.search(42)) === '[42]');
|
|
18
|
+
ok('search miss', bt.search(999).length === 0);
|
|
19
|
+
ok('searchMany', JSON.stringify(bt.searchMany([1, 5, 999, 5]).sort((a, b) => a - b)) === '[1,5]');
|
|
20
|
+
ok('prefix numeric-as-string', bt.prefix('7').length > 0);
|
|
21
|
+
ok('gte/gt', bt.greaterThanEqual(78).length === 2 && bt.greaterThan(78).length === 1);
|
|
22
|
+
ok('lte/lt', bt.lessThanEqual(1).length === 2 && bt.lessThan(1).length === 1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
const bt = new BTree(8);
|
|
27
|
+
['alice', 'albert', 'bob', 'carol', 'art'].forEach((n, i) => bt.insert(n, i));
|
|
28
|
+
const hits = bt.prefix('al').sort((a, b) => a - b);
|
|
29
|
+
ok('string prefix al', JSON.stringify(hits) === '[0,1]');
|
|
30
|
+
ok('string prefix z', bt.prefix('z').length === 0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
const db = new Database(':memory:');
|
|
35
|
+
db.createTable('users', {
|
|
36
|
+
id: { type: 'integer', primaryKey: true, autoIncrement: true },
|
|
37
|
+
name: { type: 'string', unique: true },
|
|
38
|
+
age: { type: 'integer' }
|
|
39
|
+
});
|
|
40
|
+
db.users.insertMany([
|
|
41
|
+
{ name: 'alice', age: 20 },
|
|
42
|
+
{ name: 'albert', age: 30 },
|
|
43
|
+
{ name: 'bob', age: 40 },
|
|
44
|
+
{ name: 'carol', age: 50 }
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
ok('table.exists true', db.users.exists({ name: 'bob' }) === true);
|
|
48
|
+
ok('table.exists false', db.users.exists({ name: 'zack' }) === false);
|
|
49
|
+
ok('db.exists true', db.exists('users', { age: { $gte: 50 } }) === true);
|
|
50
|
+
ok('db.exists false', db.exists('users', { age: { $gt: 90 } }) === false);
|
|
51
|
+
ok('query.exists', db.users.where({ age: { $lt: 25 } }).exists() === true);
|
|
52
|
+
|
|
53
|
+
const inRows = db.users.find({ name: { $in: ['alice', 'carol', 'nope'] } });
|
|
54
|
+
ok('$in via btree', inRows.length === 2 && inRows.every(r => r.name === 'alice' || r.name === 'carol'));
|
|
55
|
+
|
|
56
|
+
const likeRows = db.users.find({ name: { $like: 'al%' } });
|
|
57
|
+
ok('$like prefix via btree', likeRows.length === 2 && likeRows.every(r => r.name.startsWith('al')));
|
|
58
|
+
|
|
59
|
+
const rangeRows = db.users.find({ age: { $gte: 30, $lte: 40 } });
|
|
60
|
+
ok('range via btree', rangeRows.length === 2);
|
|
61
|
+
|
|
62
|
+
const one = db.users.findOne({ name: 'bob' });
|
|
63
|
+
ok('findOne no full clone', one && one.name === 'bob' && one.age === 40);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
(async () => {
|
|
67
|
+
const db = new Database(':memory:');
|
|
68
|
+
db.createTable('users', {
|
|
69
|
+
id: { type: 'integer', primaryKey: true, autoIncrement: true },
|
|
70
|
+
name: { type: 'string' },
|
|
71
|
+
age: { type: 'integer' }
|
|
72
|
+
});
|
|
73
|
+
db.users.insertMany([
|
|
74
|
+
{ name: 'alice', age: 20 },
|
|
75
|
+
{ name: 'bob', age: 40 }
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const stmt = db.prepare('SELECT name, age FROM users WHERE age > ? ORDER BY age ASC');
|
|
79
|
+
const r = await stmt.all([25]);
|
|
80
|
+
ok('prepare.all columns', r && Array.isArray(r.columns) && r.columns.includes('name'));
|
|
81
|
+
ok('prepare.all rows', r && r.rows && r.rows.length === 1 && (r.rows[0][0] === 'bob' || r.rows[0].name === 'bob'), r && r.rows);
|
|
82
|
+
|
|
83
|
+
const first = await stmt.get(10);
|
|
84
|
+
ok('prepare.get', first && (first.name === 'alice' || first[0] === 'alice'), first);
|
|
85
|
+
|
|
86
|
+
const empty = await db.prepare('SELECT name FROM users WHERE name = ?').get('nope');
|
|
87
|
+
ok('prepare.get empty', empty === null);
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
const pdb = new Database(':memory:');
|
|
91
|
+
pdb.use('timestamps', { createdField: 'created_at', updatedField: 'updated_at' });
|
|
92
|
+
pdb.createTable('notes', { id: { type: 'integer', primaryKey: true, autoIncrement: true }, title: { type: 'string' } });
|
|
93
|
+
const schema = pdb.getTableSchema('notes');
|
|
94
|
+
ok('use opts timestamps fields', !!(schema && schema.created_at && schema.updated_at), schema);
|
|
95
|
+
pdb.insert('notes', { title: 'hello' });
|
|
96
|
+
const row = pdb.notes.findOne({ title: 'hello' });
|
|
97
|
+
ok('timestamps stamped', !!(row && row.created_at && row.updated_at), row);
|
|
98
|
+
|
|
99
|
+
let afterInsertHits = 0;
|
|
100
|
+
pdb.on('afterInsert', () => { afterInsertHits++; });
|
|
101
|
+
pdb.insert('notes', { title: 'one' });
|
|
102
|
+
ok('single-row afterInsert', afterInsertHits === 1, afterInsertHits);
|
|
103
|
+
pdb.insert('notes', [{ title: 'a' }, { title: 'b' }]);
|
|
104
|
+
ok('batch afterInsert', afterInsertHits === 2, afterInsertHits);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(failed === 0 ? `\nALL ${passed} QUERY-OPT TESTS PASSED` : `\n${failed} FAILURES (${passed} passed)`);
|
|
108
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
109
|
+
})().catch(e => { console.error('FATAL', e); process.exit(1); });
|