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 +39 -1
- package/index.d.ts +16 -0
- package/index.js +1 -1
- package/lib/plugins/index.js +5 -1
- package/lib/plugins/xql.js +163 -0
- package/lib/sql.js +90 -4
- package/lib/table.js +5 -0
- package/lib/wasm_client.js +12 -0
- package/lib/xql/NOTICE +16 -0
- package/lib/xql/index.js +0 -0
- package/lib/xql/src/compiler.js +497 -0
- package/lib/xql/src/index-store.js +186 -0
- package/lib/xql/src/jsql.js +46 -0
- package/lib/xql/src/parser.js +287 -0
- package/lib/xql/src/tokenizer.js +129 -0
- package/native/jsql-neo-native.node +0 -0
- package/package.json +2 -2
- package/test/bug-regression.test.js +87 -0
- package/test/plugin-xql.test.js +147 -0
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.
|
|
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
|

|
|
9
9
|

|
|
@@ -16,6 +16,17 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## Demo
|
|
20
|
+
|
|
21
|
+

|
|
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
package/lib/plugins/index.js
CHANGED
|
@@ -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;
|
package/lib/wasm_client.js
CHANGED
|
@@ -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.
|
package/lib/xql/index.js
ADDED
|
Binary file
|