jsql-neo 6.5.11 → 6.5.13

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.
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+ /**
3
+ * XQL ↔ JSQL 适配器(浅集成)
4
+ *
5
+ * 把 JSQL 表加载成 XQL 集合,只读:加载后可用 XQL 查询与索引,
6
+ * 不做写回(XQL 目前没有 UPDATE / DELETE / 持久化)。
7
+ *
8
+ * 适配任何实现了 `find(table, filter, opts) -> Promise<rows[]>` 的数据库对象
9
+ * (JSQL 的 NativeJSQL / Database / WASM 均满足)。`find` 有默认 limit=100,
10
+ * 所以这里自动分页把整表读出来。
11
+ *
12
+ * @example
13
+ * const { xql } = require('@vexify/xql');
14
+ * const coll = await xql.fromJSQL(db, 'users', {
15
+ * indexes: ['id', ['score', 'range']],
16
+ * });
17
+ * coll.query("SELECT name FROM users WHERE age > 18 ORDER BY name");
18
+ */
19
+
20
+ async function fromJSQL(db, table, opts = {}) {
21
+ if (!db || typeof db.find !== 'function') {
22
+ throw new Error('fromJSQL: 需要一个实现了 find(table, filter, opts) 的数据库对象');
23
+ }
24
+ const { Collection } = require('../index.js');
25
+ const pageSize = opts.pageSize || 1000;
26
+ const rows = [];
27
+ let offset = 0;
28
+ for (;;) {
29
+ const batch = await db.find(table, opts.filter || {}, { limit: pageSize, offset });
30
+ if (!Array.isArray(batch) || batch.length === 0) break;
31
+ rows.push(...batch);
32
+ if (batch.length < pageSize) break;
33
+ offset += batch.length;
34
+ }
35
+ const coll = new Collection(table, rows);
36
+ if (opts.indexes) {
37
+ for (const spec of opts.indexes) {
38
+ const field = Array.isArray(spec) ? spec[0] : spec;
39
+ const type = Array.isArray(spec) ? spec[1] : null;
40
+ coll.index(field, type);
41
+ }
42
+ }
43
+ return coll;
44
+ }
45
+
46
+ module.exports = { fromJSQL };
@@ -0,0 +1,287 @@
1
+ 'use strict';
2
+ /**
3
+ * XQL 解析器(递归下降,产出扁平 AST)
4
+ *
5
+ * 支持两种写法,解析成**同一棵 AST**:
6
+ * 管道式:FROM users |> WHERE age > 18 |> SELECT name |> ORDER BY name |> LIMIT 10
7
+ * 标准式:SELECT name FROM users WHERE age > 18 ORDER BY name LIMIT 10
8
+ *
9
+ * AST 保持扁平(不建深层节点),编译期再决定怎么生成代码。
10
+ */
11
+
12
+ const { tokenize } = require('./tokenizer');
13
+
14
+ class Parser {
15
+ constructor(src) {
16
+ this.toks = tokenize(src);
17
+ this.i = 0;
18
+ }
19
+
20
+ peek(k = 0) { return this.toks[this.i + k] || this.toks[this.toks.length - 1]; }
21
+ next() { return this.toks[this.i++]; }
22
+ isKw(v) { const t = this.peek(); return t.t === 'kw' && t.v === v; }
23
+ isOp(v) { const t = this.peek(); return t.t === 'op' && t.v === v; }
24
+ eatKw(v) { if (!this.isKw(v)) this.err(`期望关键字 ${v}`); return this.next(); }
25
+ eatOp(v) { if (!this.isOp(v)) this.err(`期望 '${v}'`); return this.next(); }
26
+ err(msg) {
27
+ const t = this.peek();
28
+ throw new Error(`XQL: ${msg},位置 ${t.p}${t.v !== null && t.v !== undefined ? `(得到 '${t.v}')` : ''}`);
29
+ }
30
+
31
+ // ---- 顶层 ----
32
+ parse() {
33
+ if (this.isKw('FROM')) return this.parsePipeline();
34
+ if (this.isKw('SELECT')) return this.parseClassic();
35
+ this.err('期望 FROM 或 SELECT');
36
+ }
37
+
38
+ /** FROM coll [|> STAGE]... —— 首段 WHERE 允许省略 |>(写 SQL 的人常这么写) */
39
+ parsePipeline() {
40
+ this.eatKw('FROM');
41
+ const q = { source: this.parseSource(), where: null, select: null, orderBy: null, limit: null, offset: null };
42
+ if (this.isKw('WHERE')) { this.next(); q.where = this.parseExpr(); }
43
+ for (;;) {
44
+ if (!this.isOp('|>')) break;
45
+ this.next();
46
+ this.parseStage(q);
47
+ }
48
+ if (this.peek().t !== 'eof') this.err('语句结束后还有多余内容');
49
+ return q;
50
+ }
51
+
52
+ /** SELECT list FROM coll [WHERE e] [ORDER BY f] [LIMIT n] [OFFSET n] */
53
+ parseClassic() {
54
+ this.eatKw('SELECT');
55
+ const q = { source: null, where: null, select: this.parseSelectList(), orderBy: null, limit: null, offset: null };
56
+ this.eatKw('FROM');
57
+ q.source = this.parseSource();
58
+ if (this.isKw('WHERE')) { this.next(); q.where = this.parseExpr(); }
59
+ if (this.isKw('ORDER')) { this.next(); this.eatKw('BY'); q.orderBy = this.parseOrderBy(); }
60
+ if (this.isKw('LIMIT')) { this.next(); q.limit = this.parseCount(); }
61
+ if (this.isKw('OFFSET') || this.isKw('SKIP')) { this.next(); q.offset = this.parseCount(); }
62
+ if (this.peek().t !== 'eof') this.err('语句结束后还有多余内容');
63
+ return q;
64
+ }
65
+
66
+ parseStage(q) {
67
+ if (this.isKw('WHERE')) {
68
+ this.next();
69
+ const e = this.parseExpr();
70
+ // 多个 WHERE 用 AND 串起来 —— 管道式里每一段都能加,
71
+ // 重复出现不该是语法错误
72
+ q.where = q.where ? { k: 'log', op: 'and', l: q.where, r: e } : e;
73
+ return;
74
+ }
75
+ if (this.isKw('SELECT')) { this.next(); if (q.select) this.err('SELECT 只能出现一次'); q.select = this.parseSelectList(); return; }
76
+ if (this.isKw('ORDER')) { this.next(); this.eatKw('BY'); if (q.orderBy) this.err('ORDER BY 只能出现一次'); q.orderBy = this.parseOrderBy(); return; }
77
+ if (this.isKw('LIMIT')) { this.next(); if (q.limit !== null) this.err('LIMIT 只能出现一次'); q.limit = this.parseCount(); return; }
78
+ if (this.isKw('OFFSET') || this.isKw('SKIP')) { this.next(); q.offset = this.parseCount(); return; }
79
+ this.err('期望 WHERE / SELECT / ORDER BY / LIMIT / OFFSET');
80
+ }
81
+
82
+ parseSource() {
83
+ const t = this.peek();
84
+ if (t.t !== 'id') this.err('期望集合名');
85
+ this.next();
86
+ return t.v;
87
+ }
88
+
89
+ parseSelectList() {
90
+ const list = [];
91
+ for (;;) {
92
+ // SELECT *
93
+ if (this.isOp('*')) { this.next(); list.push({ expr: { k: 'star' }, alias: null }); }
94
+ else {
95
+ const expr = this.parseExpr();
96
+ let alias = null;
97
+ if (this.isKw('AS')) { this.next(); alias = this.parseIdent(); }
98
+ else if (this.peek().t === 'id') alias = this.next().v; // SELECT name n
99
+ list.push({ expr, alias });
100
+ }
101
+ if (!this.isOp(',')) break;
102
+ this.next();
103
+ }
104
+ return list;
105
+ }
106
+
107
+ parseOrderBy() {
108
+ const list = [];
109
+ for (;;) {
110
+ const expr = this.parseExpr();
111
+ let dir = 'asc';
112
+ if (this.isKw('ASC')) { this.next(); }
113
+ else if (this.isKw('DESC')) { this.next(); dir = 'desc'; }
114
+ list.push({ expr, dir });
115
+ if (!this.isOp(',')) break;
116
+ this.next();
117
+ }
118
+ return list;
119
+ }
120
+
121
+ parseCount() {
122
+ const t = this.peek();
123
+ if (t.t !== 'num') this.err('期望数字');
124
+ this.next();
125
+ return t.v;
126
+ }
127
+
128
+ parseIdent() {
129
+ const t = this.peek();
130
+ if (t.t !== 'id') this.err('期望标识符');
131
+ this.next();
132
+ return t.v;
133
+ }
134
+
135
+ // ---- 表达式(优先级递增)----
136
+ parseExpr() { return this.parseOr(); }
137
+
138
+ parseOr() {
139
+ let l = this.parseAnd();
140
+ while (this.isKw('OR')) { this.next(); l = { k: 'log', op: 'or', l, r: this.parseAnd() }; }
141
+ return l;
142
+ }
143
+
144
+ parseAnd() {
145
+ let l = this.parseNot();
146
+ while (this.isKw('AND')) { this.next(); l = { k: 'log', op: 'and', l, r: this.parseNot() }; }
147
+ return l;
148
+ }
149
+
150
+ parseNot() {
151
+ if (this.isKw('NOT')) { this.next(); return { k: 'not', e: this.parseNot() }; }
152
+ return this.parseCmp();
153
+ }
154
+
155
+ parseCmp() {
156
+ const l = this.parseAdd();
157
+ const t = this.peek();
158
+ if (t.t === 'op' && (t.v === '=' || t.v === '==' || t.v === '!=' || t.v === '<' || t.v === '>' || t.v === '<=' || t.v === '>=')) {
159
+ this.next();
160
+ return { k: 'bin', op: t.v === '==' ? '=' : t.v, l, r: this.parseAdd() };
161
+ }
162
+ if (this.isKw('IS')) {
163
+ this.next();
164
+ const neg = this.isKw('NOT');
165
+ if (neg) this.next();
166
+ this.eatKw('NULL');
167
+ return { k: 'isnull', e: l, neg };
168
+ }
169
+ if (this.isKw('LIKE')) { this.next(); return { k: 'like', e: l, p: this.parseAdd() }; }
170
+ // IN / NOT IN
171
+ if (this.isKw('IN') || (this.isKw('NOT') && this.peek(1).t === 'kw' && this.peek(1).v === 'IN')) {
172
+ const neg = this.isKw('NOT');
173
+ if (neg) this.next();
174
+ this.next(); // IN
175
+ this.eatOp('(');
176
+ const list = [this.parseExpr()];
177
+ while (this.isOp(',')) { this.next(); list.push(this.parseExpr()); }
178
+ this.eatOp(')');
179
+ return { k: 'in', e: l, list, neg };
180
+ }
181
+ if (this.isKw('BETWEEN')) {
182
+ this.next();
183
+ const lo = this.parseAdd();
184
+ this.eatKw('AND');
185
+ const hi = this.parseAdd();
186
+ return { k: 'between', e: l, lo, hi };
187
+ }
188
+ return l;
189
+ }
190
+
191
+ parseAdd() {
192
+ let l = this.parseMul();
193
+ for (;;) {
194
+ const t = this.peek();
195
+ if (t.t === 'op' && (t.v === '+' || t.v === '-')) { this.next(); l = { k: 'bin', op: t.v, l, r: this.parseMul() }; }
196
+ else break;
197
+ }
198
+ return l;
199
+ }
200
+
201
+ parseMul() {
202
+ let l = this.parseUnary();
203
+ for (;;) {
204
+ const t = this.peek();
205
+ if (t.t === 'op' && (t.v === '*' || t.v === '/' || t.v === '%')) { this.next(); l = { k: 'bin', op: t.v, l, r: this.parseUnary() }; }
206
+ else break;
207
+ }
208
+ return l;
209
+ }
210
+
211
+ parseUnary() {
212
+ if (this.isOp('-')) { this.next(); return { k: 'neg', e: this.parseUnary() }; }
213
+ if (this.isOp('+')) { this.next(); return this.parseUnary(); }
214
+ return this.parsePostfix();
215
+ }
216
+
217
+ /** 点号路径与数组下标:a.b.c、a[0]、a.b[1] */
218
+ parsePostfix() {
219
+ let e = this.parsePrimary();
220
+ for (;;) {
221
+ if (this.isOp('.')) {
222
+ this.next();
223
+ const t = this.peek();
224
+ if (t.t !== 'id' && t.t !== 'kw') this.err('点号后期望字段名');
225
+ this.next();
226
+ e = e.k === 'path' ? { k: 'path', segs: e.segs.concat(t.v) } : { k: 'path', segs: [t.v], base: e };
227
+ } else if (this.isOp('[')) {
228
+ this.next();
229
+ const idx = this.parseExpr();
230
+ this.eatOp(']');
231
+ e = { k: 'idx', obj: e, idx };
232
+ } else break;
233
+ }
234
+ return e;
235
+ }
236
+
237
+ parsePrimary() {
238
+ const t = this.peek();
239
+
240
+ if (t.t === 'num' || t.t === 'str') { this.next(); return { k: 'lit', v: t.v }; }
241
+ if (this.isKw('NULL')) { this.next(); return { k: 'lit', v: null }; }
242
+ if (this.isKw('TRUE')) { this.next(); return { k: 'lit', v: true }; }
243
+ if (this.isKw('FALSE')) { this.next(); return { k: 'lit', v: false }; }
244
+
245
+ if (this.isOp('(')) {
246
+ this.next();
247
+ const e = this.parseExpr();
248
+ this.eatOp(')');
249
+ return e;
250
+ }
251
+
252
+ // 聚合函数
253
+ if (t.t === 'kw' && (t.v === 'COUNT' || t.v === 'SUM' || t.v === 'AVG' || t.v === 'MIN' || t.v === 'MAX')) {
254
+ this.next();
255
+ this.eatOp('(');
256
+ let arg;
257
+ if (this.isOp('*')) { this.next(); arg = { k: 'star' }; }
258
+ else arg = this.parseExpr();
259
+ this.eatOp(')');
260
+ return { k: 'agg', fn: t.v, arg };
261
+ }
262
+
263
+ // 标量函数
264
+ if (t.t === 'id' && this.peek(1).t === 'op' && this.peek(1).v === '(') {
265
+ const name = this.next().v;
266
+ this.eatOp('(');
267
+ const args = [];
268
+ if (!this.isOp(')')) {
269
+ args.push(this.parseExpr());
270
+ while (this.isOp(',')) { this.next(); args.push(this.parseExpr()); }
271
+ }
272
+ this.eatOp(')');
273
+ return { k: 'call', fn: name.toLowerCase(), args };
274
+ }
275
+
276
+ if (t.t === 'id') { this.next(); return { k: 'path', segs: [t.v] }; }
277
+
278
+ this.err('期望表达式');
279
+ }
280
+ }
281
+
282
+ function parse(src) {
283
+ if (typeof src !== 'string' || src.trim() === '') throw new Error('XQL: 查询不能为空');
284
+ return new Parser(src).parse();
285
+ }
286
+
287
+ module.exports = { parse, Parser };
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+ /**
3
+ * XQL 词法分析器
4
+ *
5
+ * 设计约束:只做一次扫描,产出扁平 token 数组。
6
+ * 不分配嵌套结构、不做回溯 —— 词法层省下的每一纳秒都会在编译期还回来。
7
+ */
8
+
9
+ const KEYWORDS = new Set([
10
+ 'FROM', 'WHERE', 'SELECT', 'ORDER', 'BY', 'LIMIT', 'OFFSET', 'SKIP',
11
+ 'GROUP', 'HAVING', 'AS', 'ASC', 'DESC', 'AND', 'OR', 'NOT', 'NULL',
12
+ 'TRUE', 'FALSE', 'IN', 'LIKE', 'BETWEEN', 'IS', 'DISTINCT', 'COUNT',
13
+ 'SUM', 'AVG', 'MIN', 'MAX', 'PIPE',
14
+ ]);
15
+
16
+ const OPS = new Set([
17
+ '=', '==', '!=', '<>', '<', '>', '<=', '>=',
18
+ '+', '-', '*', '/', '%', '(', ')', ',', '.', '[', ']', '{', '}', ':',
19
+ '|>', '=>',
20
+ ]);
21
+
22
+ /**
23
+ * @param {string} src
24
+ * @returns {Array<{t:string, v:string, p:number}>} t=类型 v=值 p=起始位置
25
+ */
26
+ function tokenize(src) {
27
+ const out = [];
28
+ let i = 0;
29
+ const n = src.length;
30
+
31
+ while (i < n) {
32
+ const c = src[i];
33
+
34
+ // 空白
35
+ if (c === ' ' || c === '\t' || c === '\n' || c === '\r') { i++; continue; }
36
+
37
+ // 注释 -- 与 //
38
+ if (c === '-' && src[i + 1] === '-') { while (i < n && src[i] !== '\n') i++; continue; }
39
+ if (c === '/' && src[i + 1] === '/') { while (i < n && src[i] !== '\n') i++; continue; }
40
+
41
+ // 管道 |> 必须先于 | 与 > 匹配
42
+ if (c === '|' && src[i + 1] === '>') { out.push({ t: 'op', v: '|>', p: i }); i += 2; continue; }
43
+
44
+ // 字符串(单引号优先,双引号亦可)
45
+ if (c === "'" || c === '"') {
46
+ const q = c;
47
+ let j = i + 1;
48
+ let s = '';
49
+ while (j < n) {
50
+ if (src[j] === '\\' && j + 1 < n) {
51
+ const e = src[j + 1];
52
+ s += ({ n: '\n', t: '\t', r: '\r', '0': '\0' })[e] !== undefined ? ({ n: '\n', t: '\t', r: '\r', '0': '\0' })[e] : e;
53
+ j += 2;
54
+ } else if (src[j] === q) break;
55
+ else { s += src[j]; j++; }
56
+ }
57
+ if (j >= n) throw new Error(`XQL: 字符串未闭合,位置 ${i}`);
58
+ out.push({ t: 'str', v: s, p: i });
59
+ i = j + 1;
60
+ continue;
61
+ }
62
+
63
+ // 数字(含小数与负号)
64
+ if ((c >= '0' && c <= '9') || (c === '.' && src[i + 1] >= '0' && src[i + 1] <= '9')) {
65
+ let j = i;
66
+ while (j < n && ((src[j] >= '0' && src[j] <= '9') || src[j] === '.')) j++;
67
+ // 科学计数法
68
+ if (j < n && (src[j] === 'e' || src[j] === 'E')) {
69
+ let k = j + 1;
70
+ if (src[k] === '+' || src[k] === '-') k++;
71
+ if (src[k] >= '0' && src[k] <= '9') { while (k < n && src[k] >= '0' && src[k] <= '9') k++; j = k; }
72
+ }
73
+ out.push({ t: 'num', v: Number(src.slice(i, j)), p: i });
74
+ i = j;
75
+ continue;
76
+ }
77
+
78
+ // 负号:前面是操作符/开头时当作数字一部分
79
+ if (c === '-' && (src[i + 1] >= '0' && src[i + 1] <= '9')) {
80
+ const prev = out.length ? out[out.length - 1] : null;
81
+ if (!prev || prev.t === 'op' || (prev.t === 'kw' && prev.v !== 'TRUE' && prev.v !== 'FALSE' && prev.v !== 'NULL')) {
82
+ let j = i + 1;
83
+ while (j < n && ((src[j] >= '0' && src[j] <= '9') || src[j] === '.')) j++;
84
+ out.push({ t: 'num', v: -Number(src.slice(i + 1, j)), p: i });
85
+ i = j;
86
+ continue;
87
+ }
88
+ }
89
+
90
+ // 标识符 / 关键字(含点号路径由 parser 组装)
91
+ if (/[A-Za-z_$@]/.test(c)) {
92
+ let j = i;
93
+ while (j < n && /[A-Za-z0-9_$@]/.test(src[j])) j++;
94
+ const w = src.slice(i, j);
95
+ const up = w.toUpperCase();
96
+ out.push(KEYWORDS.has(up) ? { t: 'kw', v: up, p: i } : { t: 'id', v: w, p: i });
97
+ i = j;
98
+ continue;
99
+ }
100
+
101
+ // 反引号标识符
102
+ if (c === '`') {
103
+ let j = i + 1;
104
+ let s = '';
105
+ while (j < n && src[j] !== '`') { s += src[j]; j++; }
106
+ if (j >= n) throw new Error(`XQL: 反引号标识符未闭合,位置 ${i}`);
107
+ out.push({ t: 'id', v: s, p: i });
108
+ i = j + 1;
109
+ continue;
110
+ }
111
+
112
+ // 双字符操作符
113
+ const two = src.slice(i, i + 2);
114
+ if (two === '<=' || two === '>=' || two === '==' || two === '!=' || two === '<>') {
115
+ out.push({ t: 'op', v: two === '<>' ? '!=' : two, p: i });
116
+ i += 2;
117
+ continue;
118
+ }
119
+
120
+ if (OPS.has(c)) { out.push({ t: 'op', v: c, p: i }); i++; continue; }
121
+
122
+ throw new Error(`XQL: 无法识别的字符 '${c}',位置 ${i}`);
123
+ }
124
+
125
+ out.push({ t: 'eof', v: null, p: n });
126
+ return out;
127
+ }
128
+
129
+ module.exports = { tokenize, KEYWORDS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.5.11",
3
+ "version": "6.5.13",
4
4
  "description": "JSQL-NEO \u2014 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",
@@ -35,6 +35,7 @@
35
35
  "index.d.ts",
36
36
  "lib/",
37
37
  "bin/",
38
+ "src/",
38
39
  "wasm/",
39
40
  "native/",
40
41
  "nativesrc/",
@@ -46,7 +47,7 @@
46
47
  ],
47
48
  "scripts": {
48
49
  "postinstall": "node postinstall.js",
49
- "test": "node test/smoke.js && node test/native.test.js && node test/json-functions.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js && node test/star-nag.test.js",
50
+ "test": "node test/smoke.js && node test/native.test.js && node test/json-functions.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js && node test/star-nag.test.js && node test/plugin-xql.test.js",
50
51
  "test:parser": "node test/sql-parser.test.js",
51
52
  "test:btree": "node test/btree.test.js",
52
53
  "test:native": "node test/native.test.js",
@@ -0,0 +1,12 @@
1
+ /**
2
+ * JSQL CLI — 模块化入口
3
+ *
4
+ * 将各子命令拆分为独立模块,bin/jsql 通过此文件加载。
5
+ * 当前包含: plugin 命令
6
+ */
7
+
8
+ const plugin = require('./plugin');
9
+
10
+ module.exports = {
11
+ plugin,
12
+ };