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 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
- > **v5.6.0** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo)
6
+ > **v6.0.0** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo)
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)
@@ -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)" # 5.6.0
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 v5.6.0
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=5.6.0`.
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 v5.6.0
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
- *文档版本:v5.6.0 · 最后更新:2026-08-12*
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 v5.6.0
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
- find(key: unknown): unknown;
340
- range(min: unknown, max: unknown): unknown[];
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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.0.0-beta1 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.0.0 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
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 > 0 && node.keys[i - 1] === key) {
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 - 1].includes(rowIndex)) {
66
- node.values[i - 1].push(rowIndex);
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 = 0;
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.indexOf(key);
138
- return i === -1 ? [] : node.values[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
- let i = 0;
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.indexOf(key);
220
- if (i === -1) return false;
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 } 或函数 (作为 install)
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._applyFilter(table._rows, filter || {});
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);
@@ -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: '5.6.0-jsql-neo', gitVersion: 'jsql-neo', versionArray: [5, 6, 0, 0] };
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':
@@ -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
- return flushed && flushed[table] ? flushed[table] : null;
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 = '5.1.3';
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);
@@ -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 key = fr[join.foreignField];
538
- const matches = localRows.filter(lr => lr[join.localField] === key);
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
- const rows = this.find(query);
351
- return rows.length > 0 ? rows[0] : null;
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 > 0) {
866
- const subset = indices.map(i => this._rows[i]).filter(r => r);
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
- const indices = this._indexes[field].get(condition);
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.$gt !== undefined && condition.$lt !== undefined && this._btrees[field]) {
881
- const indices = this._btrees[field].range(condition.$gt, condition.$lt);
882
- const subset = indices.map(i => this._rows[i]).filter(r => r);
883
- return this._applyFilter(subset, query);
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.$gte !== undefined && condition.$lte !== undefined && this._btrees[field]) {
886
- const indices = this._btrees[field].range(condition.$gte, condition.$lte);
887
- const subset = indices.map(i => this._rows[i]).filter(r => r);
888
- return this._applyFilter(subset, query);
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
  }
@@ -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
- return flushed && flushed[table] ? flushed[table] : null;
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-beta2",
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",
@@ -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
- return flushed && flushed[table] ? flushed[table] : null;
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');
package/wasm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo-wasm",
3
- "version": "5.6.0",
3
+ "version": "6.0.0",
4
4
  "files": [
5
5
  "jsql_neo_wasm_bg.wasm",
6
6
  "jsql_neo_wasm.js",