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.
- package/README.md +39 -1
- package/index.d.ts +16 -0
- package/index.js +1 -1
- package/lib/database.js +8 -1
- package/lib/plugins/index.js +5 -1
- package/lib/plugins/xql.js +163 -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/package.json +3 -2
- package/src/cli/index.js +12 -0
- package/src/cli/plugin.js +346 -0
- package/test/plugin-xql.test.js +170 -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.13** — 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/database.js
CHANGED
|
@@ -1668,12 +1668,19 @@ class Database {
|
|
|
1668
1668
|
const pluginName = manifest.name || entry.name;
|
|
1669
1669
|
// 查找入口文件:manifest.main || manifest.entry || 'index.js'
|
|
1670
1670
|
const entryFile = manifest.main || manifest.entry || 'index.js';
|
|
1671
|
-
//
|
|
1671
|
+
// 兼容三种布局:
|
|
1672
|
+
// 1. entry 是仓库相对路径(如 "plugins/xql/index.js")且文件在根级 → basename
|
|
1673
|
+
// 2. entry 是根级相对路径(如 "index.js")且文件在根级
|
|
1674
|
+
// 3. plugins/ 子目录(旧布局)
|
|
1672
1675
|
const candidates = [
|
|
1673
1676
|
path.join(dir, entryFile),
|
|
1674
1677
|
path.join(dir, entryFile + '.js'),
|
|
1678
|
+
path.join(dir, path.basename(entryFile)),
|
|
1679
|
+
path.join(dir, path.basename(entryFile) + '.js'),
|
|
1675
1680
|
path.join(dir, 'plugins', entryFile),
|
|
1676
1681
|
path.join(dir, 'plugins', entryFile + '.js'),
|
|
1682
|
+
path.join(dir, 'plugins', path.basename(entryFile)),
|
|
1683
|
+
path.join(dir, 'plugins', path.basename(entryFile) + '.js'),
|
|
1677
1684
|
];
|
|
1678
1685
|
let entryPath = null;
|
|
1679
1686
|
for (const c of candidates) {
|
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/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
|