jsql-neo 6.4.3 → 6.4.5

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  > **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
4
4
  > MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
5
5
 
6
- > **v6.4.3** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
6
+ > **v6.4.5** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
7
7
 
8
8
  ![Engines](https://img.shields.io/badge/engines-Native%20%7C%20WASM%20%7C%20Pure%20JS-7ee787)
9
9
  ![MySQL](https://img.shields.io/badge/protocol-MySQL%20compatible-1f6feb)
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.4.3 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.4.5 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
package/lib/sql.js CHANGED
@@ -2813,6 +2813,40 @@ function rowHasPrefixedKeys(row) {
2813
2813
  return false;
2814
2814
  }
2815
2815
 
2816
+ /**
2817
+ * 整棵语句树里有没有出现过「表.列」形式的限定引用。
2818
+ *
2819
+ * executeSelect 会给每一行调用 _prefixRow,把键复制成「表.列」形式。
2820
+ * 单表查询里这些键根本没被引用,10 万行就是 10 万个白造对象
2821
+ * (node --prof 显示占整个查询 30% 的 GC)。
2822
+ *
2823
+ * 但限定引用不一定长在 {type:/column/} 节点里 —— ORDER BY 的 o.column
2824
+ * 就是纯字符串 "t.age",HAVING / pushdown 的键也是。只扫对象节点会漏,
2825
+ * 漏了就少加前缀,相关子查询和限定列全部解析错。两种形态都要查。
2826
+ *
2827
+ * 消费方清单(摸清这些才敢跳过前缀):
2828
+ * 1. resolveOperand 的 column 分支 —— 限定引用解析
2829
+ * 2. rowHasPrefixedKeys —— 未知限定符的防自匹配守卫
2830
+ * 3. SELECT * 的 starKeys —— 单表时取 Object.keys(schema),与行前缀无关
2831
+ * 4. JOIN 右表 —— 多表同名列消歧,本来就强制前缀
2832
+ */
2833
+ const QUALIFIED_REF_RE = /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/;
2834
+ function hasQualifiedRef(node, depth) {
2835
+ if (!node || typeof node !== "object" || depth > 200) return false;
2836
+ if (Array.isArray(node)) {
2837
+ for (const n of node) if (hasQualifiedRef(n, depth + 1)) return true;
2838
+ return false;
2839
+ }
2840
+ if (node.type === "column" && typeof node.name === "string" && node.name.indexOf(".") !== -1) return true;
2841
+ for (const k in node) {
2842
+ if (k === "ctx" || k === "engine" || k === "session") continue;
2843
+ const v = node[k];
2844
+ if (typeof v === "string") { if (QUALIFIED_REF_RE.test(v)) return true; }
2845
+ else if (v && typeof v === "object" && hasQualifiedRef(v, depth + 1)) return true;
2846
+ }
2847
+ return false;
2848
+ }
2849
+
2816
2850
  /**
2817
2851
  * 字符串能否当作数值参与算术运算。
2818
2852
  * MySQL 里 '5' + 5 = 10、'5' + '5' = 10、'abc' + 1 = 1(非数字按 0)。
@@ -6843,10 +6877,25 @@ class SQLExecutor {
6843
6877
  return (a && String(a).includes('.') ? String(a) : `${firstPrefix}.${a}`);
6844
6878
  }).filter(Boolean)
6845
6879
  : null;
6880
+ /*
6881
+ * 曾经试着在「单表 + 无 JOIN」时跳过 _prefixRow 以省掉 10 万次对象分配
6882
+ * (node --prof 显示它占 30% GC)。实测确实快 2~3 倍,但相关子查询
6883
+ * (EXISTS / 标量 / IN)靠 `外层表.列` 引用外层行,没前缀就解析不到,
6884
+ * 而 _collectSubqueries 在这个位置抓不到它们(前面已被改写)。
6885
+ * 收益真实但边界没摸清,先回退,留待摸清后再上。
6886
+ */
6887
+ /*
6888
+ * 单表 + 无 JOIN + 无派生表 + 整棵树没有限定引用时,跳过前缀。
6889
+ * 不缓存到 statement 上:语句对象可能被 prepare() 复用,挂属性会串味。
6890
+ */
6891
+ const needPrefix = isVirtualSource
6892
+ || !!(statement.from.joins && statement.from.joins.length)
6893
+ || hasQualifiedRef(statement, 0);
6846
6894
  let rows = isVirtualSource
6847
6895
  ? rowsAll.rows.map(r => this._prefixRow(r, firstPrefix))
6848
- : (firstItem._virtualRows ? rowsAll.rows : rowsAll.rows.map(r => this._prefixRow(r, firstPrefix)));
6849
-
6896
+ : (needPrefix || firstItem._virtualRows
6897
+ ? rowsAll.rows.map(r => this._prefixRow(r, firstPrefix))
6898
+ : rowsAll.rows);
6850
6899
  for (const j of statement.from.joins) {
6851
6900
  // 虚拟行(递归 CTE 物化的派生表)要直接取行,不能当普通子查询执行
6852
6901
  const jVirtual = j.item._virtualRows ||
@@ -7132,28 +7181,54 @@ class SQLExecutor {
7132
7181
  if (statement.orderBy) {
7133
7182
  // ORDER BY 项可能是列名字符串、表达式节点,或列的位置序号(ORDER BY 1,1-based)
7134
7183
  const ordNames = statement.columns.map(c => scalarColumnName(c));
7135
- const ordVal = (row, o) => {
7184
+ /*
7185
+ * 把排序键的解析**预先编译**,比较器里只做数组下标访问。
7186
+ *
7187
+ * 此前每次比较都调 `resolveOperand({type:'column', name}, row, ctx)` ——
7188
+ * 那个对象字面量是每次新建的。6.8 万行排序约 230 万次比较,
7189
+ * 就是 230 万次白白分配,实测占掉整个 ORDER BY 的大半时间
7190
+ * (10 万行 WHERE+ORDER+LIMIT 从 133ms 量级大幅下降)。
7191
+ *
7192
+ * 同时按「投影列名 → 下标」解析;命中就直接 `row[idx]`,
7193
+ * 不再走 resolveOperand 的通用路径。
7194
+ */
7195
+ const ordKeys = statement.orderBy.map(o => {
7196
+ const dir = o.dir === 'asc' ? 1 : -1;
7197
+ // 位置序号:ORDER BY 1
7136
7198
  const pos = (o.column && typeof o.column === 'object' && o.column.type === 'value' && typeof o.column.value === 'number')
7137
7199
  ? o.column.value
7138
7200
  : (typeof o.column === 'number' ? o.column : null);
7139
7201
  if (pos !== null && pos !== undefined) {
7140
7202
  const name = ordNames[pos - 1];
7141
- if (name !== undefined && name !== null) return resolveOperand({ type: 'column', name }, row, this.ctx);
7203
+ return { dir, at: (name === undefined || name === null) ? () => null : (r) => (r == null ? null : r[name]) };
7142
7204
  }
7143
- return typeof o.column === 'string'
7144
- ? resolveOperand({ type: 'column', name: o.column }, row, this.ctx)
7145
- : resolveOperand(o.column, row, this.ctx);
7146
- };
7205
+ if (typeof o.column === 'string') {
7206
+ const f = o.column;
7207
+ // 节点对象提到循环外复用 —— 此前每次比较都新建一个 {type:'column',name}
7208
+ const node = { type: 'column', name: f };
7209
+ return {
7210
+ dir,
7211
+ at: (r) => {
7212
+ // 先按字段名直取;取不到再走 resolveOperand(点号路径、表达式等)
7213
+ const v = (r == null) ? undefined : r[f];
7214
+ return v !== undefined ? v : resolveOperand(node, r, this.ctx);
7215
+ },
7216
+ };
7217
+ }
7218
+ return { dir, at: (r) => resolveOperand(o.column, r, this.ctx) };
7219
+ });
7220
+
7147
7221
  const cmp = (a, b) => {
7148
- for (const o of statement.orderBy) {
7149
- const av = ordVal(a, o);
7150
- const bv = ordVal(b, o);
7222
+ for (let i = 0; i < ordKeys.length; i++) {
7223
+ const k = ordKeys[i];
7224
+ const av = k.at(a);
7225
+ const bv = k.at(b);
7151
7226
  if (av === bv || (av === undefined && bv === undefined)) continue;
7152
- if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
7153
- if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
7227
+ if (av === undefined || av === null) return -1 * k.dir;
7228
+ if (bv === undefined || bv === null) return 1 * k.dir;
7154
7229
  // P2-1: localeCompare 对字符串排序受 locale 影响,应使用简单字符串比较
7155
7230
  const r = typeof av === 'number' && typeof bv === 'number' ? av - bv : (String(av) < String(bv) ? -1 : String(av) > String(bv) ? 1 : 0);
7156
- if (r !== 0) return o.dir === 'asc' ? r : -r;
7231
+ if (r !== 0) return r * k.dir;
7157
7232
  }
7158
7233
  return 0;
7159
7234
  };
@@ -7252,16 +7327,42 @@ class SQLExecutor {
7252
7327
  if (c.expr === '*') { for (const k of starKeys) cols.push(k.includes('.') ? k.slice(k.indexOf('.') + 1) : k); }
7253
7328
  else cols.push(scalarColumnName(c));
7254
7329
  }
7330
+ /*
7331
+ * 预先编译每列的取值函数。
7332
+ *
7333
+ * 此前循环里每次调用都新建 `{type:'column', name}` 对象字面量,
7334
+ * 10 万行 × N 列就是 10 万 × N 次白分配 —— 实测光 `SELECT id FROM t`
7335
+ * 就要 94ms,其中绝大部分是这个。
7336
+ *
7337
+ * 最常见的「普通列名」直接走属性访问,不再过 resolveOperand 的通用路径;
7338
+ * 取不到值才退回(兼容点号路径、限定引用等)。
7339
+ */
7340
+ const starIdx = [];
7341
+ const colGetters = statement.columns.map((c, ci) => {
7342
+ if (c.expr === '*') { starIdx.push(ci); return null; }
7343
+ if (c.over || (c.scalar && c.scalar.over)) return (r) => windowColumnValue(c, r);
7344
+ if (c.scalar) return (r) => projectScalar(resolveOperand(c.scalar, r, this.ctx));
7345
+ if (c.literal !== undefined) return () => c.literal;
7346
+ if (c.caseExpr) return (r) => projectScalar(evaluateCaseVal(c.caseExpr, r));
7347
+ if (typeof c.expr === 'string') {
7348
+ const f = c.expr;
7349
+ const node = { type: 'column', name: f };
7350
+ return (r) => {
7351
+ const v = (r == null) ? undefined : r[f];
7352
+ return v === undefined ? projectScalar(resolveOperand(node, r, this.ctx)) : projectScalar(v);
7353
+ };
7354
+ }
7355
+ const node2 = { type: 'column', name: c.expr };
7356
+ return (r) => projectScalar(resolveOperand(node2, r, this.ctx));
7357
+ });
7358
+
7255
7359
  const mapped = await this._projectRows(statement, rows, (r) => {
7256
7360
  const out = [];
7257
- for (const c of statement.columns) {
7258
- if (c.expr === '*') {
7361
+ for (let ci = 0; ci < colGetters.length; ci++) {
7362
+ const g = colGetters[ci];
7363
+ if (g === null) {
7259
7364
  for (const k of starKeys) out.push(r[k] !== undefined ? r[k] : r[String(k).slice(String(k).indexOf('.') + 1)]);
7260
- } else if (c.over || (c.scalar && c.scalar.over)) out.push(windowColumnValue(c, r));
7261
- else if (c.scalar) out.push(projectScalar(resolveOperand(c.scalar, r, this.ctx)));
7262
- else if (c.literal !== undefined) out.push(c.literal);
7263
- else if (c.caseExpr) out.push(projectScalar(evaluateCaseVal(c.caseExpr, r)));
7264
- else out.push(projectScalar(resolveOperand({ type: 'column', name: c.expr }, r, this.ctx)));
7365
+ } else out.push(g(r));
7265
7366
  }
7266
7367
  return out;
7267
7368
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.4.3",
3
+ "version": "6.4.5",
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",
@@ -46,7 +46,7 @@
46
46
  ],
47
47
  "scripts": {
48
48
  "postinstall": "node postinstall.js",
49
- "test": "node test/smoke.js && node test/json-functions.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js",
49
+ "test": "node test/smoke.js && node test/native.test.js && node test/json-functions.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js",
50
50
  "test:parser": "node test/sql-parser.test.js",
51
51
  "test:btree": "node test/btree.test.js",
52
52
  "test:native": "node test/native.test.js",
@@ -1,3 +1,17 @@
1
+ /* ⚠️ 已知损坏,未纳入任何 CI 套件(test:all / npm test 都不含本文件)
2
+ *
3
+ * 现状:本文件会崩在 `rows is not iterable`(lib/mongo_server.js:541)。
4
+ * 另外 MongoDB 协议栈与**官方现代 driver 的握手尚未打通** ——
5
+ * 用官方 mongodb driver 连不上。
6
+ *
7
+ * 因此「四协议」目前实际是「一个半」:MySQL 可用;PG / Redis 以库形式
8
+ * 提供、需自行装配;MongoDB 仅供实验。
9
+ * 文档已按此更正(doc/why.md、doc/cli.md)。
10
+ *
11
+ * 修好前请勿把本文件加进 CI —— 它一崩就会像之前那样掐断 `&&` 链。
12
+ * 修 MongoDB 需要单独排期,不属于性能/打包类改动。
13
+ */
14
+
1
15
  // Test: MongoDB $group aggregation operators
2
16
  // Directly tests the _dispatch method of createMongoServer
3
17
  const { createMongoServer } = require('../lib/mongo_server.js');
@@ -26,7 +26,7 @@ function ok(name, cond, extra) {
26
26
  const ids = await db.insert('users', [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]);
27
27
  ok('batch insert returns ids', Array.isArray(ids) && ids.length === 2, ids);
28
28
 
29
- const row = db.findById('users', 1);
29
+ const row = await db.findById('users', 1);
30
30
  ok('findById returns flat row', row && row.id === 1 && row.name === 'Alice' && row.age === 30 && !('fields' in row), row);
31
31
 
32
32
  const rows = await db.find('users', {});
@@ -35,11 +35,11 @@ function ok(name, cond, extra) {
35
35
  ok('filter no match', (await db.find('users', { age: 99 })).length === 0);
36
36
  ok('count', (await db.count('users')) === 2);
37
37
 
38
- db.updateByIds('users', [[1, { age: 99 }]]);
38
+ await db.updateByIds('users', [[1, { age: 99 }]]);
39
39
  await new Promise(r => setTimeout(r, 150));
40
- ok('updateByIds applied', db.findById('users', 1).age === 99, db.findById('users', 1));
40
+ ok('updateByIds applied', (await db.findById('users', 1)).age === 99, await db.findById('users', 1));
41
41
 
42
- db.removeByIds('users', [2]);
42
+ await db.removeByIds('users', [2]);
43
43
  await new Promise(r => setTimeout(r, 150));
44
44
  ok('removeByIds applied', (await db.count('users')) === 1, await db.count('users'));
45
45
  await db.stop();
@@ -57,12 +57,12 @@ function ok(name, cond, extra) {
57
57
  await db.createTable('t3', { id: { type: 'integer', primary_key: true, auto_increment: true }, name: { type: 'string' } });
58
58
  await db.insert('t3', { name: 'a' });
59
59
  await db.insert('t3', { id: 7, name: 'b' });
60
- ok('snake_case pk respected', db.findById('t3', 7).name === 'b', db.findById('t3', 7));
61
- ok('explicit pk bumps next id', db.findById('t3', 1).name === 'a');
60
+ ok('snake_case pk respected', (await db.findById('t3', 7)).name === 'b', await db.findById('t3', 7));
61
+ ok('explicit pk bumps next id', (await db.findById('t3', 1)).name === 'a');
62
62
 
63
63
  await db.createTable('cfg', 'key string primary key, value string');
64
64
  const cfgIds = await db.insert('cfg', { key: 'a', value: '1' });
65
- ok('full shorthand string schema', cfgIds[0] === 'a' && db.findById('cfg', 'a').value === '1', cfgIds);
65
+ ok('full shorthand string schema', cfgIds[0] === 'a' && (await db.findById('cfg', 'a')).value === '1', cfgIds);
66
66
  await db.stop();
67
67
  }
68
68
 
@@ -112,23 +112,23 @@ function ok(name, cond, extra) {
112
112
  const ids = await db.insert('config', [{ key: 'a', value: '1' }, { key: 'b', value: '2' }]);
113
113
  ok('non-auto pk insert returns pk values', JSON.stringify(ids) === JSON.stringify(['a', 'b']), ids);
114
114
 
115
- const a = db.findById('config', 'a');
115
+ const a = await db.findById('config', 'a');
116
116
  ok('findById resolves string pk', a && a.value === '1' && !('fields' in a), a);
117
- ok('findById pk miss returns null', db.findById('config', 'nope') === null);
117
+ ok('findById pk miss returns null', await db.findById('config', 'nope') === null);
118
118
 
119
- db.updateById('config', 'b', { value: '22' });
119
+ await db.updateById('config', 'b', { value: '22' });
120
120
  await new Promise(r => setTimeout(r, 150));
121
- ok('updateById resolves string pk', db.findById('config', 'b').value === '22', db.findById('config', 'b'));
121
+ ok('updateById resolves string pk', (await db.findById('config', 'b')).value === '22', await db.findById('config', 'b'));
122
122
 
123
- db.removeById('config', 'a');
123
+ await db.removeById('config', 'a');
124
124
  await new Promise(r => setTimeout(r, 150));
125
125
  ok('removeById resolves string pk', (await db.count('config')) === 1, await db.count('config'));
126
126
 
127
- db.updateByIds('config', [['b', { value: '222' }]]);
127
+ await db.updateByIds('config', [['b', { value: '222' }]]);
128
128
  await new Promise(r => setTimeout(r, 150));
129
- ok('updateByIds resolves string pk', db.findById('config', 'b').value === '222', db.findById('config', 'b'));
129
+ ok('updateByIds resolves string pk', (await db.findById('config', 'b')).value === '222', await db.findById('config', 'b'));
130
130
 
131
- db.removeByIds('config', ['b']);
131
+ await db.removeByIds('config', ['b']);
132
132
  await new Promise(r => setTimeout(r, 150));
133
133
  ok('removeByIds resolves string pk', (await db.count('config')) === 0, await db.count('config'));
134
134
  await db.stop();
@@ -90,50 +90,50 @@ function respClient(port) {
90
90
  /* ============ Core engine ============ */
91
91
  const db = new Database(':memory:', { autoSave: false });
92
92
  await db.start();
93
- db.createTable('users', { id: 'integer primary key auto_increment', name: 'string', email: 'string unique', age: 'integer' });
94
- for (const [n, a] of [['A', 10], ['B', 20], ['C', 30]]) db.insert('users', { name: n, email: n.toLowerCase() + '@x', age: a });
93
+ await db.createTable('users', { id: 'integer primary key auto_increment', name: 'string', email: 'string unique', age: 'integer' });
94
+ for (const [n, a] of [['A', 10], ['B', 20], ['C', 30]]) await db.insert('users', { name: n, email: n.toLowerCase() + '@x', age: a });
95
95
 
96
96
  {
97
- const gt = db.find('users', { age: { $gt: 20 } }).map(r => r.name);
97
+ const gt = (await db.find('users', { age: { $gt: 20 } })).map(r => r.name);
98
98
  ok('find $gt excludes boundary', JSON.stringify(gt) === '["C"]', gt);
99
- const lt = db.find('users', { age: { $lt: 20 } }).map(r => r.name);
99
+ const lt = (await db.find('users', { age: { $lt: 20 } })).map(r => r.name);
100
100
  ok('find $lt excludes boundary', JSON.stringify(lt) === '["A"]', lt);
101
101
  }
102
102
 
103
103
  {
104
- db.removeById('users', 2); // B (id=2) removed, C still id=3
105
- ok('removeById with PK count', db.count('users') === 2, db.count('users'));
106
- ok('removeById PK lookup null', db.findById('users', 2) === null);
107
- ok('removeById B-Tree still consistent', db.find('users', { age: { $gt: 15 } }).length === 1);
104
+ await db.removeById('users', 2); // B (id=2) removed, C still id=3
105
+ ok('removeById with PK count', await db.count('users') === 2, await db.count('users'));
106
+ ok('removeById PK lookup null', await db.findById('users', 2) === null);
107
+ ok('removeById B-Tree still consistent', (await db.find('users', { age: { $gt: 15 } })).length === 1);
108
108
  }
109
109
 
110
110
  {
111
- db.createTable('np', { v: 'string', n: 'integer' });
112
- for (let i = 0; i < 10; i++) db.insert('np', { v: 'v' + i, n: i });
113
- db.createTable('h', { id: 'integer primary key auto_increment', grp: 'string', val: 'integer' });
114
- for (let i = 0; i < 12; i++) db.insert('h', { grp: 'g' + (i % 4), val: i });
111
+ await db.createTable('np', { v: 'string', n: 'integer' });
112
+ for (let i = 0; i < 10; i++) await db.insert('np', { v: 'v' + i, n: i });
113
+ await db.createTable('h', { id: 'integer primary key auto_increment', grp: 'string', val: 'integer' });
114
+ for (let i = 0; i < 12; i++) await db.insert('h', { grp: 'g' + (i % 4), val: i });
115
115
  db._tables.h.createIndex('grp');
116
116
 
117
- db.removeByIds('np', [1, 2]);
118
- ok('removeByIds no-PK rebuilds indexes', db.find('np').length === 8, db.find('np').length);
117
+ await db.removeByIds('np', [1, 2]);
118
+ ok('removeByIds no-PK rebuilds indexes', (await db.find('np')).length === 8, (await db.find('np')).length);
119
119
 
120
- const pre = db.find('h', { grp: 'g1' }).map(r => r.id).sort((a, b) => a - b);
121
- db.removeById('h', 1); db.removeById('h', 5); db.removeById('h', 9); // the g0 rows
122
- const post = db.find('h', { grp: 'g1' }).map(r => r.id).sort((a, b) => a - b);
120
+ const pre = (await db.find('h', { grp: 'g1' })).map(r => r.id).sort((a, b) => a - b);
121
+ await db.removeById('h', 1); await db.removeById('h', 5); await db.removeById('h', 9); // the g0 rows
122
+ const post = (await db.find('h', { grp: 'g1' })).map(r => r.id).sort((a, b) => a - b);
123
123
  ok('hash index survives swap-pop (g1 intact)', JSON.stringify(post) === JSON.stringify(pre), { pre, post });
124
- ok('hash index has no stale g0 rows', db.find('h', { grp: 'g0' }).length === 0);
124
+ ok('hash index has no stale g0 rows', (await db.find('h', { grp: 'g0' })).length === 0);
125
125
  let stale = 0;
126
126
  for (const list of db._tables.h._indexes.grp.values()) for (const i of list) if (!db._tables.h._rows[i]) stale++;
127
127
  ok('hash index zero stale entries', stale === 0, stale);
128
128
  }
129
129
 
130
130
  {
131
- db.createTable('docs', { id: 'integer primary key', data: 'string' });
132
- db.insert('docs', { id: 1, data: JSON.stringify({ a: { b: 1 }, list: [1, 2, 3] }) });
131
+ await db.createTable('docs', { id: 'integer primary key', data: 'string' });
132
+ await db.insert('docs', { id: 1, data: JSON.stringify({ a: { b: 1 }, list: [1, 2, 3] }) });
133
133
  db.begin();
134
- db.updateById('docs', 1, { data: JSON.stringify({ a: { b: 2 }, list: [9] }) });
134
+ await db.updateById('docs', 1, { data: JSON.stringify({ a: { b: 2 }, list: [9] }) });
135
135
  db.rollback();
136
- const d = JSON.parse(db.findById('docs', 1).data);
136
+ const d = JSON.parse((await db.findById('docs', 1)).data);
137
137
  ok('transaction snapshot deep-copies nested object', d.a.b === 1, d);
138
138
  ok('transaction snapshot deep-copies arrays', JSON.stringify(d.list) === '[1,2,3]', d.list);
139
139
  }
@@ -148,31 +148,31 @@ function respClient(port) {
148
148
  ok('parseFieldShorthand default', s3.default === 'x' && s3.type === 'string', s3);
149
149
 
150
150
  let e = null;
151
- try { db.insert('users', { name: 'X', email: 'a@x', age: 1 }); } catch (err) { e = err; }
151
+ try { await db.insert('users', { name: 'X', email: 'a@x', age: 1 }); } catch (err) { e = err; }
152
152
  ok('duplicate unique throws ER_DUP_ENTRY', e && (e.code === 1062 || String(e).includes('1062')), e && (e.code || e.message));
153
- db.createTable('pk2', { a: 'integer primary key', b: 'string' });
154
- db.insert('pk2', { a: 1, b: 'x' });
153
+ await db.createTable('pk2', { a: 'integer primary key', b: 'string' });
154
+ await db.insert('pk2', { a: 1, b: 'x' });
155
155
  e = null;
156
- try { db.insert('pk2', { a: 1, b: 'y' }); } catch (err) { e = err; }
156
+ try { await db.insert('pk2', { a: 1, b: 'y' }); } catch (err) { e = err; }
157
157
  ok('duplicate primary key throws ER_DUP_ENTRY', e && (e.code === 1062 || String(e).includes('1062')), e && (e.code || e.message));
158
158
  }
159
159
 
160
160
  {
161
161
  const r = await migrate.importFromJSON(db, { table: 't1', schema: { id: 'integer primary key' }, rows: [{ id: 1 }, { id: 2 }] });
162
- ok('importFromJSON single-table shape', r && db.count('t1') === 2);
162
+ ok('importFromJSON single-table shape', r && await db.count('t1') === 2);
163
163
  let threw = null;
164
164
  try { await migrate.importFromJSON(db, { table: 't1', schema: { id: 'integer primary key' }, rows: [{ id: 3 }] }); } catch (err) { threw = err; }
165
165
  ok('importFromJSON refuses existing table without overwrite', !!threw, threw && (threw.message || String(threw)));
166
166
  await migrate.importFromJSON(db, { table: 't1', schema: { id: 'integer primary key' }, rows: [{ id: 3 }] }, { overwrite: true });
167
- ok('importFromJSON overwrite replaces table', db.count('t1') === 1, db.count('t1'));
167
+ ok('importFromJSON overwrite replaces table', await db.count('t1') === 1, await db.count('t1'));
168
168
  }
169
169
 
170
170
  {
171
- db.createTable('lk', { s: 'string' });
172
- for (const s of ['a.b', 'axb', 'aXb', 'abc']) db.insert('lk', { s });
173
- const m = db.find('lk', { s: { $like: 'a.b' } }).map(r => r.s);
171
+ await db.createTable('lk', { s: 'string' });
172
+ for (const s of ['a.b', 'axb', 'aXb', 'abc']) await db.insert('lk', { s });
173
+ const m = (await db.find('lk', { s: { $like: 'a.b' } })).map(r => r.s);
174
174
  ok('$like escapes regex metacharacters', JSON.stringify(m) === '["a.b"]', m);
175
- const m2 = db.find('lk', { s: { $like: 'a%b' } }).map(r => r.s);
175
+ const m2 = (await db.find('lk', { s: { $like: 'a%b' } })).map(r => r.s);
176
176
  ok('$like % wildcard still works', JSON.stringify(m2) === '["a.b","axb","aXb"]', m2);
177
177
  }
178
178
  await db.stop();