jsql-neo 6.5.10 → 6.5.12

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.5.10** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
6
+ > **v6.5.12** — stable release · [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)
@@ -16,6 +16,17 @@
16
16
 
17
17
  ---
18
18
 
19
+ ## Demo
20
+
21
+ ![JSQL-NEO terminal demo](doc/demo.gif)
22
+
23
+ A real `jsql tui` session: joins, NULL semantics, exact 64-bit integers, and
24
+ savepoint rollback.
25
+
26
+ [MP4 version](doc/demo.mp4) · [asciinema recording](doc/demo.cast)
27
+
28
+ ---
29
+
19
30
  ## 🚀 Quick Start
20
31
 
21
32
  ### One-line install
@@ -160,6 +171,33 @@ await db.start();
160
171
 
161
172
  ---
162
173
 
174
+ ## ✨ XQL 集成
175
+
176
+ 用 [XQL](https://github.com/vexify-org/XQL) 查询 JSQL 表:把 SQL 直接接到文档上,
177
+ 支持等值 hash 索引与范围 range 索引,速度不低于手写 JS 循环,并支持管道式语法。
178
+
179
+ ```js
180
+ const { NativeJSQL } = require('jsql-neo');
181
+ const db = new NativeJSQL();
182
+ await db.start();
183
+
184
+ db.use(require('jsql-neo/lib/plugins').xql());
185
+
186
+ // 标准 SQL
187
+ const rows = await db.xql('SELECT name FROM users WHERE age > 18 ORDER BY name');
188
+ // 管道式(同样的语义)
189
+ const pipe = await db.xql('FROM users |> WHERE age > 18 |> SELECT name |> ORDER BY name');
190
+
191
+ // 索引:等值 → hash 索引 O(1),范围 → 有序索引 O(log n + k)
192
+ db.xqlIndex('users', 'id');
193
+ db.xqlIndex('users', 'score', 'range');
194
+ ```
195
+
196
+ 写入(INSERT / UPDATE / DELETE)后 XQL 缓存自动失效、下次查询重建;XQL 只读,
197
+ 不做写回——数据的唯一权威仍是 JSQL 引擎。详见 [PLUGINS.md](PLUGINS.md#xql-插件)。
198
+
199
+ ---
200
+
163
201
  ## 📖 Documentation
164
202
 
165
203
  Detailed docs live in `doc/` — click to read.
package/index.d.ts CHANGED
@@ -115,6 +115,22 @@ declare namespace JSQLNeo {
115
115
  query(sql: string, paramsOrOpts?: unknown[] | Record<string, unknown>, opts?: Record<string, unknown>): Promise<any>;
116
116
  exists(table: string, filter?: Record<string, unknown>): Promise<boolean>;
117
117
  prepare(sql: string): PreparedStatement;
118
+
119
+ // ── xql 插件(db.use('xql') 后可用)─────────────────────
120
+ /** 用 XQL 查询(自动加载源表)。opts.table 可显式指定数据源。 */
121
+ xql(sql: string, opts?: { table?: string }): Promise<Row[]>;
122
+ /** 显式加载一张表成 XQL 集合 */
123
+ xqlLoad(table: string): Promise<unknown>;
124
+ /** 强制重建某表的 XQL 集合 */
125
+ xqlRefresh(table: string): Promise<unknown>;
126
+ /** 运行时建索引(type 缺省按字段类型猜,'range' 为有序索引) */
127
+ xqlIndex(table: string, field: string, type?: 'hash' | 'range'): this;
128
+ /** 已加载的 XQL 集合名 */
129
+ xqlTables(): string[];
130
+ /** 取某表的 XQL 集合(未加载返回 null) */
131
+ xqlCollection(table: string): unknown | null;
132
+ /** 清缓存(不传清全部) */
133
+ xqlClear(table?: string): this;
118
134
  }
119
135
 
120
136
  export interface PreparedStatement {
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.5.10 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.5.12 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
@@ -16,6 +16,7 @@ const { createLogger } = require('./logger');
16
16
  const { createMetrics } = require('./metrics');
17
17
  const { createAudit } = require('./audit');
18
18
  const { createValidation } = require('./validation');
19
+ const { createXql } = require('./xql');
19
20
 
20
21
  /** 名称 → 插件工厂 */
21
22
  const builtins = {
@@ -23,7 +24,8 @@ const builtins = {
23
24
  logger: createLogger,
24
25
  metrics: createMetrics,
25
26
  audit: createAudit,
26
- validation: createValidation
27
+ validation: createValidation,
28
+ xql: createXql
27
29
  };
28
30
 
29
31
  /** 内置插件名清单 */
@@ -60,11 +62,13 @@ module.exports = {
60
62
  createMetrics,
61
63
  createAudit,
62
64
  createValidation,
65
+ createXql,
63
66
  timestamps: createTimestamps,
64
67
  logger: createLogger,
65
68
  metrics: createMetrics,
66
69
  audit: createAudit,
67
70
  validation: createValidation,
71
+ xql: createXql,
68
72
  builtins,
69
73
  list,
70
74
  create,
@@ -0,0 +1,163 @@
1
+ /**
2
+ * xql —— 用 XQL(eXtend Query Language)查询 JSQL 表
3
+ *
4
+ * XQL 把 SQL 直接接到 JS 文档上:支持等值 hash 索引与范围 range 索引,
5
+ * 查询速度不低于手写 JS 循环,并支持管道式语法(FROM t |> WHERE … |> LIMIT n)。
6
+ *
7
+ * 本插件把 JSQL 表加载成 XQL 集合(自动分页读全表),写入时标记失效、
8
+ * 下次查询重建,从而在 JSQL 之上获得 XQL 的管道语法与索引。XQL 只读,
9
+ * 不做写回——数据的唯一权威仍然是 JSQL 引擎本身。
10
+ *
11
+ * @example
12
+ * db.use(require('jsql-neo/lib/plugins').xql());
13
+ *
14
+ * await db.xql('SELECT name FROM users WHERE age > 18 ORDER BY name');
15
+ * await db.xql('FROM users |> WHERE age > 18 |> SELECT name |> ORDER BY name');
16
+ * db.xqlIndex('users', 'id'); // 等值 → hash 索引,O(1)
17
+ * db.xqlIndex('users', 'score', 'range'); // 范围 → range 索引
18
+ *
19
+ * @example 自动建索引 + 显式加载
20
+ * db.use(require('jsql-neo/lib/plugins').xql({
21
+ * indexes: { users: ['id', ['score', 'range']] },
22
+ * pageSize: 2000,
23
+ * }));
24
+ * await db.xqlLoad('users');
25
+ */
26
+ const { definePlugin } = require('../plugin');
27
+
28
+ /** 规整索引配置:'id' | ['score','range'] → ['id', null] | ['score','range'] */
29
+ function normalizeIndexSpecs(specs) {
30
+ return (specs || []).map(s => Array.isArray(s) ? [s[0], s[1] || null] : [s, null]);
31
+ }
32
+
33
+ /** 记录索引规格(去重,按字段名) */
34
+ function recordIndexSpec(state, table, field, type) {
35
+ const list = state.indexSpecs[table] || (state.indexSpecs[table] = []);
36
+ const exists = list.some(s => (Array.isArray(s) ? s[0] : s) === field);
37
+ if (!exists) list.push(type ? [field, type] : field);
38
+ }
39
+
40
+ function createXql(opts = {}) {
41
+ const pageSize = opts.pageSize || 1000;
42
+ const autoLoad = opts.autoLoad !== false;
43
+ const getXql = opts.xql || (() => require('../xql'));
44
+
45
+ return definePlugin('xql', {
46
+ config: { pageSize, autoLoad, indexes: opts.indexes || {} },
47
+ install(engine, ctx) {
48
+ const state = {
49
+ collections: new Map(), // table -> Collection
50
+ dirty: new Set(), // table 需要重建
51
+ indexSpecs: Object.assign({}, opts.indexes || {}),
52
+ };
53
+ ctx.store.set('xql', state);
54
+
55
+ const load = async (table) => {
56
+ const rows = [];
57
+ let offset = 0;
58
+ for (;;) {
59
+ const batch = await engine.find(table, {}, { limit: pageSize, offset });
60
+ if (!Array.isArray(batch) || batch.length === 0) break;
61
+ rows.push(...batch);
62
+ if (batch.length < pageSize) break;
63
+ offset += batch.length;
64
+ }
65
+ const xql = getXql();
66
+ const coll = new xql.Collection(table, rows);
67
+ for (const [field, type] of normalizeIndexSpecs(state.indexSpecs[table])) {
68
+ coll.index(field, type);
69
+ }
70
+ state.collections.set(table, coll);
71
+ state.dirty.delete(table);
72
+ return coll;
73
+ };
74
+
75
+ const ensure = async (table) => {
76
+ const has = state.collections.has(table);
77
+ if (!has && !autoLoad) {
78
+ throw new Error(`xql: 表 '${table}' 未加载 —— 先 db.xqlLoad('${table}')`);
79
+ }
80
+ if (!has || state.dirty.has(table)) return load(table);
81
+ return state.collections.get(table);
82
+ };
83
+
84
+ const invalidate = (table) => {
85
+ if (state.collections.has(table)) state.dirty.add(table);
86
+ };
87
+
88
+ // 写入 → 缓存失效(native 只有 beforeUpdate/beforeDelete 会触发,
89
+ // afterInsert 全引擎都有;用 before* 让三种引擎行为一致)
90
+ ctx.on('afterInsert', (t) => invalidate(t));
91
+ ctx.on('beforeUpdate', (t) => invalidate(t));
92
+ ctx.on('beforeDelete', (t) => invalidate(t));
93
+ ctx.on('afterDropTable', (t) => {
94
+ state.collections.delete(t);
95
+ state.dirty.delete(t);
96
+ });
97
+ ctx.on('onStop', () => {
98
+ state.collections.clear();
99
+ state.dirty.clear();
100
+ });
101
+
102
+ const api = {
103
+ version: getXql().version,
104
+
105
+ /** 用 XQL 查询(自动加载源表)。opts.table 可显式指定数据源。 */
106
+ async query(sql, opts = {}) {
107
+ const xql = getXql();
108
+ const src = opts.table || xql.compile(sql).__source;
109
+ const coll = await ensure(src);
110
+ return coll.query(sql);
111
+ },
112
+
113
+ /** 显式加载一张表成 XQL 集合(含配置的索引) */
114
+ load(table) { return load(table); },
115
+
116
+ /** 强制重建某张表的 XQL 集合 */
117
+ refresh(table) {
118
+ state.dirty.add(table);
119
+ return load(table);
120
+ },
121
+
122
+ /** 给表建索引(运行时)。等值不传 type → hash,传 'range' → 有序索引。 */
123
+ index(table, field, type) {
124
+ recordIndexSpec(state, table, field, type);
125
+ const c = state.collections.get(table);
126
+ if (c) c.index(field, type);
127
+ return engine;
128
+ },
129
+
130
+ /** 已加载的 XQL 集合名 */
131
+ tables() { return Array.from(state.collections.keys()); },
132
+
133
+ /** 取某表的 XQL 集合(未加载返回 null) */
134
+ collection(table) { return state.collections.get(table) || null; },
135
+
136
+ /** 清空缓存(可指定表;不传清全部) */
137
+ clear(table) {
138
+ if (table) {
139
+ state.collections.delete(table);
140
+ state.dirty.delete(table);
141
+ } else {
142
+ state.collections.clear();
143
+ state.dirty.clear();
144
+ }
145
+ return engine;
146
+ },
147
+ };
148
+
149
+ ctx.expose(api);
150
+
151
+ // 便捷方法直接挂到引擎上:db.xql / db.xqlIndex / …
152
+ engine.xql = api.query;
153
+ engine.xqlLoad = api.load;
154
+ engine.xqlRefresh = api.refresh;
155
+ engine.xqlIndex = api.index;
156
+ engine.xqlTables = api.tables;
157
+ engine.xqlCollection = api.collection;
158
+ engine.xqlClear = api.clear;
159
+ },
160
+ });
161
+ }
162
+
163
+ module.exports = { createXql, xql: createXql };
package/lib/sql.js CHANGED
@@ -708,6 +708,11 @@ class Parser {
708
708
  break;
709
709
  } else {
710
710
  const col = this.parseTableName();
711
+ // 重复列名必须报错:对象键覆盖会静默丢列(schema[col] = def 直接覆盖),
712
+ // 用户 CREATE TABLE d(a INT, a INT) 会悄悄变成单列 a。
713
+ if (schema[col] !== undefined) {
714
+ throw new Error(`Duplicate column name '${col}' in table '${name}'`);
715
+ }
711
716
  const def = this.parseColumnDef();
712
717
  schema[col] = def;
713
718
  if (def.primaryKey) hasPk = true;
@@ -755,6 +760,8 @@ class Parser {
755
760
  const mapped = typeMap[type];
756
761
  if (!mapped) throw new Error(`Unsupported column type: ${typeTok.value}`);
757
762
  def.type = mapped;
763
+ // JSON / JSONB 列保留原始标记,供数据层校验非法 JSON({bad json 这类字符串会被 JSON.parse 拒绝)
764
+ if (type === 'json' || type === 'jsonb') def.jsonType = true;
758
765
  if (/^(BIG)?SERIAL$/i.test(typeRaw)) { def.autoIncrement = true; def.required = true; }
759
766
 
760
767
  // 长度限制: TEXT(255) / VARCHAR(100) / INTEGER(11) ...
@@ -3110,10 +3117,6 @@ function resolveOperand(operand, row, ctx) {
3110
3117
  // 布尔类节点出现在"取值"位置时(如 IF(status = 'paid', ...)、SUM(IF(a > b,1,0))),
3111
3118
  // 求值成 true/false 供上层按真值判断
3112
3119
  case 'compare':
3113
- case 'and':
3114
- case 'or':
3115
- case 'xor':
3116
- case 'not':
3117
3120
  case 'in':
3118
3121
  case 'isNull':
3119
3122
  case 'isTruth':
@@ -3122,6 +3125,25 @@ function resolveOperand(operand, row, ctx) {
3122
3125
  case 'like':
3123
3126
  case 'between':
3124
3127
  return evaluateExpr(operand, row, ctx);
3128
+ case 'and': {
3129
+ const a = sqlTruthy(resolveOperand(operand.left, row, ctx));
3130
+ const b = sqlTruthy(resolveOperand(operand.right, row, ctx));
3131
+ return sqlAnd(a, b);
3132
+ }
3133
+ case 'or': {
3134
+ const a = sqlTruthy(resolveOperand(operand.left, row, ctx));
3135
+ const b = sqlTruthy(resolveOperand(operand.right, row, ctx));
3136
+ return sqlOr(a, b);
3137
+ }
3138
+ case 'xor': {
3139
+ const a = sqlTruthy(resolveOperand(operand.left, row, ctx));
3140
+ const b = sqlTruthy(resolveOperand(operand.right, row, ctx));
3141
+ return sqlXor(a, b);
3142
+ }
3143
+ case 'not': {
3144
+ const v = sqlTruthy(resolveOperand(operand.expr, row, ctx));
3145
+ return v === null ? null : (v ? 0 : 1);
3146
+ }
3125
3147
  case 'unit': {
3126
3148
  // 裸时间单位(MONTH / DAY …):优先当列取,取不到就返回单位名本身
3127
3149
  const lower = String(operand.name).toLowerCase();
@@ -4422,6 +4444,33 @@ function ciCompare(a, b, cs) {
4422
4444
  return as === bs ? 0 : (as < bs ? -1 : 1);
4423
4445
  }
4424
4446
 
4447
+ // ── 三值逻辑(SQL 标准:TRUE / FALSE / UNKNOWN)──────────────────────
4448
+ // NULL 参与布尔运算时结果不是 0/1,而是 UNKNOWN(用 null 表示)。
4449
+ // 此前 resolveOperand 没有 and/or/xor/not 分支,SELECT TRUE AND TRUE 落到
4450
+ // default 返回 undefined → 数值化成 0,所有布尔表达式全部失效。
4451
+ function sqlTruthy(v) {
4452
+ if (v === null || v === undefined) return null;
4453
+ if (typeof v === 'boolean') return v;
4454
+ if (typeof v === 'number') return v !== 0;
4455
+ if (typeof v === 'string') return v !== '' && !(Number(v) === 0 && String(v).trim() !== '');
4456
+ return Boolean(v);
4457
+ }
4458
+ // 三值真值表,入参已归一化为 true / false / null,返回 1 / 0 / null
4459
+ function sqlAnd(a, b) {
4460
+ if (a === false || b === false) return 0;
4461
+ if (a === null || b === null) return null;
4462
+ return 1;
4463
+ }
4464
+ function sqlOr(a, b) {
4465
+ if (a === true || b === true) return 1;
4466
+ if (a === null || b === null) return null;
4467
+ return 0;
4468
+ }
4469
+ function sqlXor(a, b) {
4470
+ if (a === null || b === null) return null;
4471
+ return a !== b ? 1 : 0;
4472
+ }
4473
+
4425
4474
  function evaluateExpr(expr, row, ctx) {
4426
4475
  if (expr === null || expr === undefined) return false;
4427
4476
  switch (expr.type) {
@@ -6347,6 +6396,18 @@ class SQLExecutor {
6347
6396
  walk(statement.where);
6348
6397
  walk(statement.having);
6349
6398
  walk(statement.orderBy);
6399
+ // ORDER BY 的列在解析器里是裸字符串(非 {type:'column'} 对象),walk 检不到。
6400
+ // 字符串可能是输出别名/位置序号(已在 known 里豁免),也可能是真实列;
6401
+ // 两者都不是 → 必须报错(此前静默忽略,用户 ORDER BY nonexistent_col 拿到原序)。
6402
+ if (Array.isArray(statement.orderBy)) {
6403
+ for (const o of statement.orderBy) {
6404
+ const c = o && o.column;
6405
+ if (typeof c !== 'string') continue; // 位置序号(数字)跳过
6406
+ const n = c.toLowerCase();
6407
+ const base = n.indexOf('.') >= 0 ? n.slice(n.indexOf('.') + 1) : n;
6408
+ if (!known.has(n) && !known.has(base)) throw new Error(`no such column: ${c}`);
6409
+ }
6410
+ }
6350
6411
  if (Array.isArray(statement.groupBy)) {
6351
6412
  // groupBy 是列名字符串数组
6352
6413
  for (const g of statement.groupBy) {
@@ -7876,6 +7937,30 @@ async function inferColumnTypes(engine, stmt, r) {
7876
7937
  });
7877
7938
  }
7878
7939
 
7940
+ // 结果里的大整数(> MAX_SAFE_INTEGER 的 BigInt)在 JSON.stringify / wasm 绑定下
7941
+ // 无法序列化("Do not know how to serialize a BigInt")。输出前递归转成字符串,
7942
+ // 保留精度且可序列化;内部运算仍走 BigInt,精度不丢。
7943
+ function finalizeBigInts(value, seen) {
7944
+ if (typeof value === 'bigint') return value.toString();
7945
+ if (Array.isArray(value)) {
7946
+ if (!seen) seen = new Set();
7947
+ if (seen.has(value)) return value;
7948
+ seen.add(value);
7949
+ for (let i = 0; i < value.length; i++) value[i] = finalizeBigInts(value[i], seen);
7950
+ return value;
7951
+ }
7952
+ if (value && typeof value === 'object') {
7953
+ // Buffer / Date / RegExp 等特殊对象不深入(Buffer 是 Uint8Array,逐字节递归会爆栈)
7954
+ if (value instanceof Buffer || value instanceof Date || value instanceof RegExp) return value;
7955
+ if (!seen) seen = new Set();
7956
+ if (seen.has(value)) return value;
7957
+ seen.add(value);
7958
+ for (const k of Object.keys(value)) value[k] = finalizeBigInts(value[k], seen);
7959
+ return value;
7960
+ }
7961
+ return value;
7962
+ }
7963
+
7879
7964
  async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
7880
7965
  // 参数守卫:engine 必须是引擎对象,sql 必须是字符串。误用时报清晰错误,
7881
7966
  // 而不是在后续像 `reading 'length'` 那样抛出令人费解的 TypeError。
@@ -7934,6 +8019,7 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
7934
8019
  let r = await executor.execute(stmt);
7935
8020
  const durationMs = Number(process.hrtime.bigint() - t0) / 1e6;
7936
8021
  r = buildResultEnvelope(stmt, r, Math.round(durationMs * 1000) / 1000);
8022
+ r = finalizeBigInts(r);
7937
8023
  if (r && r.columns && r.columnTypes === undefined) {
7938
8024
  r.columnTypes = await inferColumnTypes(engine, stmt, r);
7939
8025
  }
package/lib/table.js CHANGED
@@ -1543,6 +1543,11 @@ class Table {
1543
1543
  else if (typeof v === 'number' || typeof v === 'boolean' || v instanceof Date) result[field] = String(v);
1544
1544
  else if (Buffer.isBuffer(v)) result[field] = v.toString('utf8');
1545
1545
  else throw createError('ER_TRUNCATED_WRONG_VALUE', t, v);
1546
+ } else if ((t === 'object' || t === 'array' || t === 'json') && (def.jsonType || t === 'json')) {
1547
+ // JSON / JSONB 列:字符串形式必须是合法 JSON({bad json 这类会被 JSON.parse 拒绝)
1548
+ if (typeof v === 'string') {
1549
+ try { JSON.parse(v); } catch (e) { throw createError('ER_TRUNCATED_WRONG_VALUE', t, v); }
1550
+ }
1546
1551
  }
1547
1552
  }
1548
1553
  return result;
@@ -561,6 +561,18 @@ class JSQL {
561
561
  // 难以定位的 "Cannot convert undefined or null to object",这里直接给明确 TypeError
562
562
  const arr = Array.isArray(data) ? data : [data];
563
563
  arr.forEach((row, i) => assertRowObject(row, i, arr.length > 1 || Array.isArray(data)));
564
+ // JSON / JSONB 列:字符串形式必须是合法 JSON({bad json 这类会被 JSON.parse 拒绝)
565
+ const schema = this._schemas[table];
566
+ if (schema) {
567
+ for (const row of arr) {
568
+ for (const [f, def] of Object.entries(schema)) {
569
+ if (def && def.jsonType && typeof row[f] === 'string') {
570
+ try { JSON.parse(row[f]); }
571
+ catch (e) { throw new Error(`Truncated incorrect object value: '${row[f]}'`); }
572
+ }
573
+ }
574
+ }
575
+ }
564
576
  if (!(await this._runHooks('beforeInsert', [table, arr]))) return [];
565
577
  if (arr.length > 1) {
566
578
  await this._flush();
package/lib/xql/NOTICE ADDED
@@ -0,0 +1,16 @@
1
+ XQL — eXtend Query Language
2
+ Copyright 2026 Vexify
3
+
4
+ This product includes software developed at Vexify.
5
+
6
+ Licensed under the Apache License, Version 2.0 (the "License");
7
+ you may not use this file except in compliance with the License.
8
+ You may obtain a copy of the License at
9
+
10
+ http://www.apache.org/licenses/LICENSE-2.0
11
+
12
+ Unless required by applicable law or agreed to in writing, software
13
+ distributed under the License is distributed on an "AS IS" BASIS,
14
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ See the License for the specific language governing permissions and
16
+ limitations under the License.
Binary file