jsql-neo 6.0.0-beta2 → 6.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.
- package/README.md +7 -7
- 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 +1 -1
- 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/table.js +68 -21
- package/lib/wasm_client.js +14 -1
- package/package.json +2 -2
- package/test/btree.test.js +16 -0
- package/test/query-opt.test.js +109 -0
- package/wasm/browser.mjs +12 -2
- package/wasm/package.json +1 -1
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. The browser. **And it fits in one npm package.**
|
|
5
5
|
|
|
6
|
-
> **
|
|
6
|
+
> **v6.0.0** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
|
@@ -232,7 +232,7 @@ npm install && npm run build # option 3: from source
|
|
|
232
232
|
Verify:
|
|
233
233
|
|
|
234
234
|
```bash
|
|
235
|
-
node -e "console.log(require('jsql-neo/package.json').version)" #
|
|
235
|
+
node -e "console.log(require('jsql-neo/package.json').version)" # 6.0.0
|
|
236
236
|
```
|
|
237
237
|
|
|
238
238
|
### 30-second demo
|
|
@@ -1457,7 +1457,7 @@ jsql mod --engine wasm # switch engine (restart required)
|
|
|
1457
1457
|
|
|
1458
1458
|
```bash
|
|
1459
1459
|
$ jsql version
|
|
1460
|
-
jsql-neo
|
|
1460
|
+
jsql-neo v6.0.0
|
|
1461
1461
|
engine: native (napi) | wasm | js
|
|
1462
1462
|
node: v22.0.0 platform: linux x64
|
|
1463
1463
|
```
|
|
@@ -1478,7 +1478,7 @@ jsql tui --memory -q # memory mode, quiet
|
|
|
1478
1478
|
jsql tui --prompt 'db> ' --no-color
|
|
1479
1479
|
```
|
|
1480
1480
|
|
|
1481
|
-
The status bar shows: `db=<name> dialect=<d> mode=<tui|batch> ver=
|
|
1481
|
+
The status bar shows: `db=<name> dialect=<d> mode=<tui|batch> ver=6.0.0`.
|
|
1482
1482
|
|
|
1483
1483
|
### Keyboard shortcuts
|
|
1484
1484
|
|
|
@@ -4042,7 +4042,7 @@ Data dir: /root/.jsql-neo/data
|
|
|
4042
4042
|
|
|
4043
4043
|
```bash
|
|
4044
4044
|
$ jsql version
|
|
4045
|
-
jsql-neo
|
|
4045
|
+
jsql-neo v6.0.0
|
|
4046
4046
|
engine: native (napi) | wasm | js
|
|
4047
4047
|
node: v22.0.0
|
|
4048
4048
|
platform: linux x64
|
|
@@ -5119,7 +5119,7 @@ Apache License
|
|
|
5119
5119
|
|
|
5120
5120
|
*JSQL-NEO — One engine to rule them all. MySQL. PostgreSQL. MongoDB. Redis. SQL. TypeScript. The browser.*
|
|
5121
5121
|
|
|
5122
|
-
*文档版本:
|
|
5122
|
+
*文档版本:v6.0.0 · 最后更新:2026-09-26*
|
|
5123
5123
|
|
|
5124
5124
|
---
|
|
5125
5125
|
|
|
@@ -6889,7 +6889,7 @@ Usage: jsql version
|
|
|
6889
6889
|
|
|
6890
6890
|
输出版本与环境信息:
|
|
6891
6891
|
|
|
6892
|
-
jsql-neo
|
|
6892
|
+
jsql-neo v6.0.0
|
|
6893
6893
|
engine: native (napi) | wasm | js
|
|
6894
6894
|
node: v22.0.0
|
|
6895
6895
|
platform: linux x64
|
package/index.d.ts
CHANGED
|
@@ -85,7 +85,7 @@ declare namespace JSQLNeo {
|
|
|
85
85
|
|
|
86
86
|
export class JSQL {
|
|
87
87
|
constructor(opts?: JSQLOptions);
|
|
88
|
-
use(plugin: JSQLPlugin | ((db: JSQL) => void)): this;
|
|
88
|
+
use(plugin: JSQLPlugin | string | ((db: JSQL) => void), opts?: Record<string, unknown>): this;
|
|
89
89
|
on(event: JSQLHook, fn: (...args: any[]) => unknown): this;
|
|
90
90
|
onEvent(fn: (event: string, data: unknown) => void): this;
|
|
91
91
|
start(): Promise<void>;
|
|
@@ -113,6 +113,15 @@ declare namespace JSQLNeo {
|
|
|
113
113
|
* @returns 单条语句返回单个结果对象(含 columns/rows),多条返回数组。
|
|
114
114
|
*/
|
|
115
115
|
query(sql: string, paramsOrOpts?: unknown[] | Record<string, unknown>, opts?: Record<string, unknown>): Promise<any>;
|
|
116
|
+
exists(table: string, filter?: Record<string, unknown>): Promise<boolean>;
|
|
117
|
+
prepare(sql: string): PreparedStatement;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface PreparedStatement {
|
|
121
|
+
sql: string;
|
|
122
|
+
all(...params: unknown[]): Promise<any>;
|
|
123
|
+
get(...params: unknown[]): Promise<Record<string, unknown> | null>;
|
|
124
|
+
run(...params: unknown[]): Promise<any>;
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
export class NativeJSQL extends JSQL {}
|
|
@@ -309,6 +318,9 @@ declare namespace JSQLNeo {
|
|
|
309
318
|
createTable(name: string, schema: Schema): Promise<unknown>;
|
|
310
319
|
insert(table: string, data: Record<string, unknown>): Promise<unknown>;
|
|
311
320
|
find(table: string, filter?: Record<string, unknown>): Promise<Row[]>;
|
|
321
|
+
exists(table: string, filter?: Record<string, unknown>): boolean;
|
|
322
|
+
prepare(sql: string): PreparedStatement;
|
|
323
|
+
query(sql: string, paramsOrOpts?: unknown[] | SQLOptions, opts?: SQLOptions): Promise<SQLResult>;
|
|
312
324
|
update(table: string, filter: Record<string, unknown>, data: Record<string, unknown>): Promise<number>;
|
|
313
325
|
remove(table: string, filter: Record<string, unknown>): Promise<number>;
|
|
314
326
|
findOne(table: string, filter?: Record<string, unknown>): Promise<Row | null>;
|
|
@@ -316,12 +328,14 @@ declare namespace JSQLNeo {
|
|
|
316
328
|
getTables(): Promise<string[]>;
|
|
317
329
|
dropTable(name: string): Promise<void>;
|
|
318
330
|
getTableSchema(name: string): Promise<Schema | null>;
|
|
331
|
+
use(plugin: JSQLPlugin | string | ((db: Database) => void), opts?: Record<string, unknown>): this;
|
|
319
332
|
}
|
|
320
333
|
|
|
321
334
|
export class Table {
|
|
322
335
|
constructor(name: string, schema: Schema, db?: unknown);
|
|
323
336
|
insert(data: Record<string, unknown>): Promise<unknown>;
|
|
324
337
|
find(filter?: Record<string, unknown>): Promise<Row[]>;
|
|
338
|
+
exists(filter?: Record<string, unknown>): boolean;
|
|
325
339
|
updateById(id: number | string, data: Record<string, unknown>): Promise<unknown>;
|
|
326
340
|
removeById(id: number | string): Promise<unknown>;
|
|
327
341
|
count(): Promise<number>;
|
|
@@ -329,15 +343,27 @@ declare namespace JSQLNeo {
|
|
|
329
343
|
|
|
330
344
|
export class Query {
|
|
331
345
|
constructor(table: unknown);
|
|
346
|
+
where(conditions: Record<string, unknown>): this;
|
|
347
|
+
orderBy(field: string, dir?: 'asc' | 'desc'): this;
|
|
348
|
+
limit(n: number): this;
|
|
349
|
+
offset(n: number): this;
|
|
350
|
+
get(): Row[];
|
|
351
|
+
first(): Row | null;
|
|
352
|
+
exists(): boolean;
|
|
353
|
+
count(): number;
|
|
332
354
|
exec(): Promise<Row[]>;
|
|
333
355
|
then(resolve: (rows: Row[]) => unknown, reject?: (err: Error) => unknown): Promise<unknown>;
|
|
334
356
|
}
|
|
335
357
|
|
|
336
358
|
export class BTree {
|
|
337
|
-
constructor(order?: number);
|
|
359
|
+
constructor(order?: number, unique?: boolean);
|
|
338
360
|
insert(key: unknown, value: unknown): void;
|
|
339
|
-
|
|
340
|
-
|
|
361
|
+
search(key: unknown): number[];
|
|
362
|
+
searchMany(keys: unknown[]): number[];
|
|
363
|
+
prefix(prefix: string): number[];
|
|
364
|
+
range(min: unknown, max: unknown): number[];
|
|
365
|
+
greaterThan(min: unknown): number[];
|
|
366
|
+
lessThan(max: unknown): number[];
|
|
341
367
|
}
|
|
342
368
|
|
|
343
369
|
export class Cache {
|
package/index.js
CHANGED
package/lib/btree.js
CHANGED
|
@@ -50,20 +50,27 @@ class BTree {
|
|
|
50
50
|
this._size++;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
_lowerBound(keys, key) {
|
|
54
|
+
let lo = 0, hi = keys.length;
|
|
55
|
+
while (lo < hi) {
|
|
56
|
+
const mid = (lo + hi) >> 1;
|
|
57
|
+
if (keys[mid] < key) lo = mid + 1;
|
|
58
|
+
else hi = mid;
|
|
59
|
+
}
|
|
60
|
+
return lo;
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
_insertNonFull(node, key, rowIndex) {
|
|
54
64
|
if (node.leaf) {
|
|
55
|
-
|
|
56
|
-
let i = node.keys.length - 1;
|
|
57
|
-
while (i >= 0 && key < node.keys[i]) i--;
|
|
58
|
-
i++;
|
|
65
|
+
const i = this._lowerBound(node.keys, key);
|
|
59
66
|
|
|
60
|
-
if (i
|
|
67
|
+
if (i < node.keys.length && node.keys[i] === key) {
|
|
61
68
|
if (this._unique) {
|
|
62
69
|
this._size--;
|
|
63
70
|
return;
|
|
64
71
|
}
|
|
65
|
-
if (!node.values[i
|
|
66
|
-
node.values[i
|
|
72
|
+
if (!node.values[i].includes(rowIndex)) {
|
|
73
|
+
node.values[i].push(rowIndex);
|
|
67
74
|
}
|
|
68
75
|
this._size--;
|
|
69
76
|
return;
|
|
@@ -119,8 +126,7 @@ class BTree {
|
|
|
119
126
|
* (分隔键副本可能在左子树作为最大值,或在右子树作为最小值)。
|
|
120
127
|
*/
|
|
121
128
|
_route(node, key) {
|
|
122
|
-
let i =
|
|
123
|
-
while (i < node.keys.length && key > node.keys[i]) i++;
|
|
129
|
+
let i = this._lowerBound(node.keys, key);
|
|
124
130
|
if (i < node.keys.length && node.keys[i] === key) {
|
|
125
131
|
return this._getMax(node.children[i]).key === key ? i : i + 1;
|
|
126
132
|
}
|
|
@@ -134,8 +140,43 @@ class BTree {
|
|
|
134
140
|
search(key) {
|
|
135
141
|
let node = this._root;
|
|
136
142
|
while (!node.leaf) node = node.children[this._route(node, key)];
|
|
137
|
-
const i = node.keys
|
|
138
|
-
return i
|
|
143
|
+
const i = this._lowerBound(node.keys, key);
|
|
144
|
+
return (i < node.keys.length && node.keys[i] === key) ? node.values[i] : [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
searchMany(keys) {
|
|
148
|
+
if (!keys || keys.length === 0) return [];
|
|
149
|
+
const out = [];
|
|
150
|
+
const seen = new Set();
|
|
151
|
+
for (let i = 0; i < keys.length; i++) {
|
|
152
|
+
const hits = this.search(keys[i]);
|
|
153
|
+
for (let j = 0; j < hits.length; j++) {
|
|
154
|
+
const idx = hits[j];
|
|
155
|
+
if (!seen.has(idx)) {
|
|
156
|
+
seen.add(idx);
|
|
157
|
+
out.push(idx);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
prefix(prefix) {
|
|
165
|
+
if (prefix === undefined || prefix === null) return [];
|
|
166
|
+
const p = String(prefix);
|
|
167
|
+
const result = [];
|
|
168
|
+
let node = this._findLeaf(p);
|
|
169
|
+
while (node) {
|
|
170
|
+
for (let i = 0; i < node.keys.length; i++) {
|
|
171
|
+
const k = node.keys[i];
|
|
172
|
+
const s = k === undefined || k === null ? '' : String(k);
|
|
173
|
+
if (s < p) continue;
|
|
174
|
+
if (!s.startsWith(p)) return result;
|
|
175
|
+
result.push(...node.values[i]);
|
|
176
|
+
}
|
|
177
|
+
node = node.next;
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
139
180
|
}
|
|
140
181
|
|
|
141
182
|
/**
|
|
@@ -173,6 +214,18 @@ class BTree {
|
|
|
173
214
|
return result;
|
|
174
215
|
}
|
|
175
216
|
|
|
217
|
+
greaterThanEqual(min) {
|
|
218
|
+
const result = [];
|
|
219
|
+
let node = this._findLeaf(min);
|
|
220
|
+
while (node) {
|
|
221
|
+
for (let i = 0; i < node.keys.length; i++) {
|
|
222
|
+
if (node.keys[i] >= min) result.push(...node.values[i]);
|
|
223
|
+
}
|
|
224
|
+
node = node.next;
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
|
|
176
229
|
/**
|
|
177
230
|
* 小于查询(严格开区间,不含 max 本身)
|
|
178
231
|
*/
|
|
@@ -190,12 +243,24 @@ class BTree {
|
|
|
190
243
|
return result;
|
|
191
244
|
}
|
|
192
245
|
|
|
246
|
+
lessThanEqual(max) {
|
|
247
|
+
const result = [];
|
|
248
|
+
let node = this._root;
|
|
249
|
+
while (!node.leaf) node = node.children[0];
|
|
250
|
+
while (node) {
|
|
251
|
+
for (let i = 0; i < node.keys.length; i++) {
|
|
252
|
+
if (node.keys[i] <= max) result.push(...node.values[i]);
|
|
253
|
+
else return result;
|
|
254
|
+
}
|
|
255
|
+
node = node.next;
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
|
|
193
260
|
_findLeaf(key) {
|
|
194
261
|
let node = this._root;
|
|
195
262
|
while (!node.leaf) {
|
|
196
|
-
|
|
197
|
-
while (i < node.keys.length && key > node.keys[i]) i++;
|
|
198
|
-
node = node.children[i];
|
|
263
|
+
node = node.children[this._lowerBound(node.keys, key)];
|
|
199
264
|
}
|
|
200
265
|
return node;
|
|
201
266
|
}
|
|
@@ -216,8 +281,8 @@ class BTree {
|
|
|
216
281
|
|
|
217
282
|
_removeFromNode(node, key, rowIndex) {
|
|
218
283
|
if (node.leaf) {
|
|
219
|
-
const i = node.keys
|
|
220
|
-
if (i
|
|
284
|
+
const i = this._lowerBound(node.keys, key);
|
|
285
|
+
if (i >= node.keys.length || node.keys[i] !== key) return false;
|
|
221
286
|
if (!node.values[i].includes(rowIndex)) return false;
|
|
222
287
|
const vals = node.values[i].filter(v => v !== rowIndex);
|
|
223
288
|
if (vals.length === 0) {
|
package/lib/database.js
CHANGED
|
@@ -435,6 +435,17 @@ class Database {
|
|
|
435
435
|
if (!name || name.length > 64 || name === '.' || name === '..' || /[\x00-\x1f/\\]|\.\./.test(name)) {
|
|
436
436
|
throw createError('ER_BAD_TABLE_NAME', name);
|
|
437
437
|
}
|
|
438
|
+
if (typeof schema === 'string') {
|
|
439
|
+
const parsed = {};
|
|
440
|
+
for (const seg of schema.split(',')) {
|
|
441
|
+
const trimmed = seg.trim();
|
|
442
|
+
if (!trimmed) continue;
|
|
443
|
+
const parts = trimmed.split(/\s+/);
|
|
444
|
+
const field = parts.shift();
|
|
445
|
+
parsed[field] = parts.join(' ').toLowerCase().trim() || 'string';
|
|
446
|
+
}
|
|
447
|
+
schema = parsed;
|
|
448
|
+
}
|
|
438
449
|
if (!this._runHooks('beforeCreateTable', [name, schema])) throw createError('ER_PLUGIN_ABORT', 'createTable aborted by plugin');
|
|
439
450
|
if (this._tables[name] || (this._dirMode && this._meta.tables[name])) {
|
|
440
451
|
throw createError('ER_TABLE_EXISTS_ERROR', name);
|
|
@@ -1076,10 +1087,11 @@ class Database {
|
|
|
1076
1087
|
|
|
1077
1088
|
/**
|
|
1078
1089
|
* 注册插件
|
|
1079
|
-
* @param {object|function} plugin - { name, version, install(db, ctx), hooks, api, onEvent }
|
|
1090
|
+
* @param {object|function|string} plugin - { name, version, install(db, ctx), hooks, api, onEvent }、函数或内置插件名
|
|
1091
|
+
* @param {object} [opts] - 传给内置插件工厂的配置,例如 db.use('timestamps', { createdField: 'created_at' })
|
|
1080
1092
|
*/
|
|
1081
|
-
use(plugin) {
|
|
1082
|
-
return applyPlugin(this, plugin);
|
|
1093
|
+
use(plugin, opts) {
|
|
1094
|
+
return applyPlugin(this, plugin, opts);
|
|
1083
1095
|
}
|
|
1084
1096
|
|
|
1085
1097
|
_buildCtx(plugin) {
|
|
@@ -1212,7 +1224,7 @@ class Database {
|
|
|
1212
1224
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1213
1225
|
if (!this._runHooks('beforeFind', [tableName, { filter, opts }])) return [];
|
|
1214
1226
|
const { limit, offset = 0 } = opts;
|
|
1215
|
-
let rows = table.
|
|
1227
|
+
let rows = table._applyFilterOptimized(table._rows, filter || {});
|
|
1216
1228
|
if (limit > 0) rows = rows.slice(offset, offset + limit);
|
|
1217
1229
|
else if (offset > 0) rows = rows.slice(offset);
|
|
1218
1230
|
this._touchTable(tableName);
|
|
@@ -1220,6 +1232,25 @@ class Database {
|
|
|
1220
1232
|
return rows;
|
|
1221
1233
|
}
|
|
1222
1234
|
|
|
1235
|
+
exists(tableName, filter = {}) {
|
|
1236
|
+
const table = this._ensureTable(tableName);
|
|
1237
|
+
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1238
|
+
this._touchTable(tableName);
|
|
1239
|
+
return table.exists(filter);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
async query(sql, paramsOrOpts, opts) {
|
|
1243
|
+
const { executeSQL } = require('./sql');
|
|
1244
|
+
if (!this._runHooks('beforeQuery', [sql, paramsOrOpts, opts])) return null;
|
|
1245
|
+
const result = await executeSQL(this, sql, paramsOrOpts, opts);
|
|
1246
|
+
this._runHooks('afterQuery', [sql, paramsOrOpts, opts, result]);
|
|
1247
|
+
return result;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
prepare(sql) {
|
|
1251
|
+
return require('./prepared').createPrepared(this, sql);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1223
1254
|
findById(tableName, id) {
|
|
1224
1255
|
const table = this._ensureTable(tableName);
|
|
1225
1256
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
package/lib/mongo_server.js
CHANGED
|
@@ -322,7 +322,7 @@ class MongoServer {
|
|
|
322
322
|
localTime: new Date(), logicalSessionTimeoutMinutes: 30, connectionId: 1,
|
|
323
323
|
};
|
|
324
324
|
case 'ping': return { ok: 1 };
|
|
325
|
-
case 'buildInfo': return { ok: 1, version: '
|
|
325
|
+
case 'buildInfo': return { ok: 1, version: '6.0.0-jsql-neo', gitVersion: 'jsql-neo', versionArray: [6, 0, 0, 0] };
|
|
326
326
|
case 'getParameter': return { ok: 1 };
|
|
327
327
|
case 'endSessions': return { ok: 1 };
|
|
328
328
|
case 'listDatabases':
|
package/lib/native_client.js
CHANGED
|
@@ -499,11 +499,15 @@ class JSQL {
|
|
|
499
499
|
this._buffer[table].push(arr[0]);
|
|
500
500
|
this._bufferSize++;
|
|
501
501
|
const flushed = await this._flush();
|
|
502
|
-
|
|
502
|
+
const result = flushed && flushed[table] ? flushed[table] : null;
|
|
503
|
+
this._emit('insert', { table, count: arr.length, ids: result });
|
|
504
|
+
this._runHooks('afterInsert', [table, filtered, result]);
|
|
505
|
+
return result;
|
|
503
506
|
}
|
|
504
507
|
|
|
505
508
|
async createTable(name, schema) {
|
|
506
509
|
await this._flush();
|
|
510
|
+
if (typeof schema === 'string') schema = parseSchemaString(schema);
|
|
507
511
|
if (!this._runHooks('beforeCreateTable', [name, schema])) return null;
|
|
508
512
|
const r = safeParse(native.jsqlCreateTable(name, JSON.stringify(mapNativeSchema(schema))));
|
|
509
513
|
if (r && r.ok === false) throw new Error(r.error || 'create table failed');
|
|
@@ -656,6 +660,15 @@ class JSQL {
|
|
|
656
660
|
return result;
|
|
657
661
|
}
|
|
658
662
|
|
|
663
|
+
async exists(table, filter = {}) {
|
|
664
|
+
const rows = await this.find(table, filter, { limit: 1 });
|
|
665
|
+
return Array.isArray(rows) && rows.length > 0;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
prepare(sql) {
|
|
669
|
+
return require('./prepared').createPrepared(this, sql);
|
|
670
|
+
}
|
|
671
|
+
|
|
659
672
|
async stop() {
|
|
660
673
|
this._flushOpsNow();
|
|
661
674
|
await this._flush();
|
package/lib/pg_server.js
CHANGED
|
@@ -762,7 +762,7 @@ class PgServer {
|
|
|
762
762
|
this.auth = options.auth || null; // { user: { password, databases: [...] } }
|
|
763
763
|
this.noAuth = options.noAuth === true;
|
|
764
764
|
this.dataDir = options.dataDir || null;
|
|
765
|
-
this.version = '
|
|
765
|
+
this.version = require('../package.json').version;
|
|
766
766
|
this._server = null;
|
|
767
767
|
this._sockets = new Set();
|
|
768
768
|
this._connSeq = 0;
|
package/lib/plugin.js
CHANGED
|
@@ -159,7 +159,7 @@ function buildPluginContext(engine, plugin) {
|
|
|
159
159
|
/** 主动触发一个钩子(含自定义钩子) */
|
|
160
160
|
hook(hookName, ...args) { return engine.runHook(hookName, ...args); },
|
|
161
161
|
/** 组合:加载另一个插件 */
|
|
162
|
-
use(other) { return engine.use(other); },
|
|
162
|
+
use(other, opts) { return engine.use(other, opts); },
|
|
163
163
|
/** 表清单(同步) */
|
|
164
164
|
tables() {
|
|
165
165
|
if (engine._tableNames) return Array.from(engine._tableNames);
|
|
@@ -191,7 +191,7 @@ function buildPluginContext(engine, plugin) {
|
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
193
|
* 把一个插件应用到引擎:登记钩子、调用 install、挂载事件与 API。
|
|
194
|
-
* 引擎的 `use()` 只需 `return applyPlugin(this, plugin)`。
|
|
194
|
+
* 引擎的 `use()` 只需 `return applyPlugin(this, plugin, opts)`。
|
|
195
195
|
* @returns {object} engine(便于链式调用)
|
|
196
196
|
*/
|
|
197
197
|
function applyPlugin(engine, plugin, opts) {
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* timestamps —— 自动维护创建/更新时间戳。
|
|
3
3
|
*
|
|
4
4
|
* 在插入时补全 createdAt / updatedAt(已存在则不覆盖 createdAt),
|
|
5
|
-
* 更新时刷新 updatedAt
|
|
5
|
+
* 更新时刷新 updatedAt。建表时若 schema 未声明这两个字段,会原地补上,
|
|
6
|
+
* 以便 native / WASM 引擎按 schema 列持久化。钩子内只原地修改数据、不回调引擎。
|
|
6
7
|
*
|
|
7
8
|
* @example
|
|
8
9
|
* db.use(require('jsql-neo/lib/plugins').timestamps());
|
|
@@ -22,9 +23,22 @@ function createTimestamps(opts = {}) {
|
|
|
22
23
|
if (row[updatedField] === undefined || opts.overwriteUpdated) row[updatedField] = t;
|
|
23
24
|
};
|
|
24
25
|
|
|
26
|
+
const stampDef = { type: 'string' };
|
|
27
|
+
|
|
28
|
+
const ensureStampFields = (schema) => {
|
|
29
|
+
if (!schema) return;
|
|
30
|
+
if (typeof schema === 'string') return;
|
|
31
|
+
if (typeof schema !== 'object') return;
|
|
32
|
+
if (schema[createdField] === undefined) schema[createdField] = { ...stampDef };
|
|
33
|
+
if (schema[updatedField] === undefined) schema[updatedField] = { ...stampDef };
|
|
34
|
+
};
|
|
35
|
+
|
|
25
36
|
return definePlugin('timestamps', {
|
|
26
37
|
config: { createdField, updatedField },
|
|
27
38
|
hooks: {
|
|
39
|
+
beforeCreateTable(_name, schema) {
|
|
40
|
+
ensureStampFields(schema);
|
|
41
|
+
},
|
|
28
42
|
beforeInsert(_table, rows) {
|
|
29
43
|
if (Array.isArray(rows)) rows.forEach(r => stampRow(r, true));
|
|
30
44
|
else stampRow(rows, true);
|
package/lib/prepared.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared prepared-statement helper for JS / native / WASM engines.
|
|
3
|
+
* parse once, bind on each run. Compatible with executeSQL placeholder rules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function createPrepared(engine, sql) {
|
|
7
|
+
if (typeof sql !== 'string' || sql.length === 0) {
|
|
8
|
+
throw new Error('prepare() requires a SQL string');
|
|
9
|
+
}
|
|
10
|
+
const bound = {
|
|
11
|
+
sql,
|
|
12
|
+
async all(...params) {
|
|
13
|
+
const values = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
14
|
+
return engine.query(sql, values);
|
|
15
|
+
},
|
|
16
|
+
async get(...params) {
|
|
17
|
+
const r = await bound.all(...params);
|
|
18
|
+
if (!r) return null;
|
|
19
|
+
if (Array.isArray(r.raw) && r.raw.length > 0) return r.raw[0];
|
|
20
|
+
if (Array.isArray(r.rows) && r.rows.length > 0) {
|
|
21
|
+
if (r.columns && Array.isArray(r.rows[0])) {
|
|
22
|
+
const obj = {};
|
|
23
|
+
for (let i = 0; i < r.columns.length; i++) obj[r.columns[i]] = r.rows[0][i];
|
|
24
|
+
return obj;
|
|
25
|
+
}
|
|
26
|
+
return r.rows[0];
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
},
|
|
30
|
+
async run(...params) {
|
|
31
|
+
return bound.all(...params);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
return bound;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { createPrepared };
|
package/lib/query.js
CHANGED
|
@@ -25,6 +25,7 @@ class Query {
|
|
|
25
25
|
this._cache = null; // { ttl, result, timestamp }
|
|
26
26
|
this._caseExpr = null; // [{ when, then }, ...] + else
|
|
27
27
|
this._optimizerHints = {}; // { useHashJoin: true }
|
|
28
|
+
this._forUpdate = false;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
// ============================================================
|
|
@@ -302,8 +303,6 @@ class Query {
|
|
|
302
303
|
});
|
|
303
304
|
}
|
|
304
305
|
|
|
305
|
-
// 字段选择
|
|
306
|
-
rows = rows.map(r => ({ ...r }));
|
|
307
306
|
if (this._selectFields) {
|
|
308
307
|
rows = rows.map(r => {
|
|
309
308
|
const obj = {};
|
|
@@ -312,6 +311,12 @@ class Query {
|
|
|
312
311
|
}
|
|
313
312
|
return obj;
|
|
314
313
|
});
|
|
314
|
+
} else if (this._caseExpr) {
|
|
315
|
+
// CASE already cloned
|
|
316
|
+
} else {
|
|
317
|
+
const copy = new Array(rows.length);
|
|
318
|
+
for (let i = 0; i < rows.length; i++) copy[i] = { ...rows[i] };
|
|
319
|
+
rows = copy;
|
|
315
320
|
}
|
|
316
321
|
|
|
317
322
|
// 缓存存储
|
|
@@ -329,6 +334,14 @@ class Query {
|
|
|
329
334
|
return rows.length > 0 ? rows[0] : null;
|
|
330
335
|
}
|
|
331
336
|
|
|
337
|
+
exists() {
|
|
338
|
+
const prev = this._limitCount;
|
|
339
|
+
this._limitCount = 1;
|
|
340
|
+
const rows = this.get();
|
|
341
|
+
this._limitCount = prev;
|
|
342
|
+
return rows.length > 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
332
345
|
count() {
|
|
333
346
|
return this._countRaw();
|
|
334
347
|
}
|
|
@@ -533,10 +546,16 @@ class Query {
|
|
|
533
546
|
}
|
|
534
547
|
}
|
|
535
548
|
} else if (join.type === 'right') {
|
|
549
|
+
const localMap = new Map();
|
|
550
|
+
for (const lr of localRows) {
|
|
551
|
+
const key = lr[join.localField];
|
|
552
|
+
if (key === undefined || key === null) continue;
|
|
553
|
+
if (!localMap.has(key)) localMap.set(key, []);
|
|
554
|
+
localMap.get(key).push(lr);
|
|
555
|
+
}
|
|
536
556
|
for (const fr of foreignTable._rows) {
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
if (matches.length === 0) {
|
|
557
|
+
const matches = localMap.get(fr[join.foreignField]);
|
|
558
|
+
if (!matches || matches.length === 0) {
|
|
540
559
|
result.push(this._rightNullRow(localRows, fr, join));
|
|
541
560
|
} else {
|
|
542
561
|
for (const lr of matches) {
|
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
|
@@ -484,7 +484,10 @@ class JSQL {
|
|
|
484
484
|
this._buffer[table].push(arr[0]);
|
|
485
485
|
this._bufferSize++;
|
|
486
486
|
const flushed = await this._flush();
|
|
487
|
-
|
|
487
|
+
const result = flushed && flushed[table] ? flushed[table] : null;
|
|
488
|
+
this._emit('insert', { table, count: arr.length, ids: result });
|
|
489
|
+
this._runHooks('afterInsert', [table, arr, result]);
|
|
490
|
+
return result;
|
|
488
491
|
}
|
|
489
492
|
|
|
490
493
|
async insertMany(table, data) {
|
|
@@ -493,6 +496,7 @@ class JSQL {
|
|
|
493
496
|
|
|
494
497
|
async createTable(name, schema) {
|
|
495
498
|
await this._flush();
|
|
499
|
+
if (typeof schema === 'string') schema = parseSchemaString(schema);
|
|
496
500
|
if (!this._runHooks('beforeCreateTable', [name, schema])) return null;
|
|
497
501
|
const r = safeJsonParse(wasmBindings.jsql_create_table(name, JSON.stringify(mapNativeSchema(schema))));
|
|
498
502
|
if (r && r.ok === false) throw new Error(r.error || 'create table failed');
|
|
@@ -692,6 +696,15 @@ class JSQL {
|
|
|
692
696
|
return result;
|
|
693
697
|
}
|
|
694
698
|
|
|
699
|
+
async exists(table, filter = {}) {
|
|
700
|
+
const rows = await this.find(table, filter, { limit: 1 });
|
|
701
|
+
return Array.isArray(rows) && rows.length > 0;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
prepare(sql) {
|
|
705
|
+
return require('./prepared').createPrepared(this, sql);
|
|
706
|
+
}
|
|
707
|
+
|
|
695
708
|
async stop() {
|
|
696
709
|
await this._flush();
|
|
697
710
|
this._runHooks('onStop', []);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "6.0.0
|
|
3
|
+
"version": "6.0.0",
|
|
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",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
],
|
|
46
46
|
"scripts": {
|
|
47
47
|
"postinstall": "node postinstall.js",
|
|
48
|
-
"test": "node test/smoke.js && node test/sql-parser.test.js",
|
|
48
|
+
"test": "node test/smoke.js && node test/sql-parser.test.js && node test/query-opt.test.js",
|
|
49
49
|
"test:parser": "node test/sql-parser.test.js",
|
|
50
50
|
"test:btree": "node test/btree.test.js",
|
|
51
51
|
"test:native": "node test/native.test.js",
|
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); });
|
package/wasm/browser.mjs
CHANGED
|
@@ -174,10 +174,16 @@ export class JSQL {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
use(plugin) {
|
|
177
|
+
use(plugin, opts) {
|
|
178
|
+
if (typeof plugin === 'string') {
|
|
179
|
+
throw new Error('use: builtin plugin names require the Node plugin runtime; pass a plugin object');
|
|
180
|
+
}
|
|
178
181
|
if (typeof plugin === 'function') {
|
|
179
182
|
plugin = { install: plugin };
|
|
180
183
|
}
|
|
184
|
+
if (opts && plugin && typeof plugin === 'object') {
|
|
185
|
+
plugin.config = Object.assign({}, plugin.config || {}, opts);
|
|
186
|
+
}
|
|
181
187
|
if (plugin.hooks) {
|
|
182
188
|
for (const [event, fn] of Object.entries(plugin.hooks)) {
|
|
183
189
|
if (this._hooks[event]) {
|
|
@@ -377,7 +383,10 @@ export class JSQL {
|
|
|
377
383
|
this._buffer[table].push(arr[0]);
|
|
378
384
|
this._bufferSize++;
|
|
379
385
|
const flushed = await this._flush();
|
|
380
|
-
|
|
386
|
+
const result = flushed && flushed[table] ? flushed[table] : null;
|
|
387
|
+
this._emit('insert', { table, count: arr.length, ids: result });
|
|
388
|
+
this._runHooks('afterInsert', [table, arr, result]);
|
|
389
|
+
return result;
|
|
381
390
|
}
|
|
382
391
|
|
|
383
392
|
async insertMany(table, data) {
|
|
@@ -386,6 +395,7 @@ export class JSQL {
|
|
|
386
395
|
|
|
387
396
|
async createTable(name, schema) {
|
|
388
397
|
await this._flush();
|
|
398
|
+
if (typeof schema === 'string') schema = parseSchemaString(schema);
|
|
389
399
|
if (!this._runHooks('beforeCreateTable', [name, schema])) return null;
|
|
390
400
|
const r = safeJsonParse(jsql_create_table(name, JSON.stringify(mapNativeSchema(schema))));
|
|
391
401
|
if (r && r.ok === false) throw new Error(r.error || 'create table failed');
|