jsql-neo 5.4.2 → 6.0.0-beta2

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.
Files changed (39) hide show
  1. package/README.md +198 -14
  2. package/index.d.ts +29 -0
  3. package/index.js +12 -2
  4. package/lib/ast.js +214 -0
  5. package/lib/database.js +26 -60
  6. package/lib/mongo_server.js +1 -1
  7. package/lib/native_client.js +43 -66
  8. package/lib/plugin.js +227 -3
  9. package/lib/plugins/audit.js +37 -0
  10. package/lib/plugins/index.js +72 -0
  11. package/lib/plugins/logger.js +35 -0
  12. package/lib/plugins/metrics.js +38 -0
  13. package/lib/plugins/timestamps.js +43 -0
  14. package/lib/plugins/validation.js +128 -0
  15. package/lib/sql.js +1212 -84
  16. package/lib/wasm_client.js +41 -57
  17. package/native/jsql-neo-native.node +0 -0
  18. package/nativesrc/jsql-neo-core/Cargo.lock +8 -1
  19. package/nativesrc/jsql-neo-core/Cargo.toml +3 -1
  20. package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +50 -13
  21. package/nativesrc/jsql-neo-core/src/engine/memory.rs +46 -23
  22. package/nativesrc/jsql-neo-core/src/engine/mod.rs +13 -1
  23. package/nativesrc/jsql-neo-core/src/engine/table.rs +338 -42
  24. package/nativesrc/jsql-neo-core/src/storage/wal.rs +245 -32
  25. package/nativesrc/jsql-neo-native/Cargo.lock +9 -2
  26. package/nativesrc/jsql-neo-native/Cargo.toml +9 -1
  27. package/nativesrc/jsql-neo-native/src/lib.rs +14 -2
  28. package/nativesrc/jsql-neo-wasm/Cargo.lock +9 -2
  29. package/nativesrc/jsql-neo-wasm/Cargo.toml +8 -1
  30. package/nativesrc/jsql-neo-wasm/src/lib.rs +18 -2
  31. package/package.json +4 -3
  32. package/test/sql-parser.test.js +450 -0
  33. package/wasm/browser.mjs +12 -0
  34. package/wasm/browser_bg.mjs +269 -248
  35. package/wasm/jsql_neo_wasm.d.ts +6 -0
  36. package/wasm/jsql_neo_wasm.js +248 -127
  37. package/wasm/jsql_neo_wasm_bg.wasm +0 -0
  38. package/wasm/jsql_neo_wasm_bg.wasm.d.ts +22 -22
  39. package/wasm/package.json +1 -1
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.4.0** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo)
6
+ > **v5.6.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.4.0
235
+ node -e "console.log(require('jsql-neo/package.json').version)" # 5.6.0
236
236
  ```
237
237
 
238
238
  ### 30-second demo
@@ -835,6 +835,7 @@ CREATE TABLE IF NOT EXISTS orders (
835
835
  **SELECT grammar**
836
836
 
837
837
  ```sql
838
+ WITH [RECURSIVE] cte_name [(col, ...)] AS (SELECT ...)[, ...] -- CTE,见下节
838
839
  SELECT [DISTINCT] select_list
839
840
  FROM table_reference
840
841
  [JOIN table_reference ON condition]
@@ -843,9 +844,69 @@ FROM table_reference
843
844
  [HAVING condition]
844
845
  [ORDER BY column [ASC|DESC] [, ...]]
845
846
  [LIMIT { count | offset, count | count OFFSET offset }]
847
+ [UNION [ALL|DISTINCT] SELECT ...]
848
+ [INTERSECT [ALL|DISTINCT] SELECT ...]
849
+ [EXCEPT [ALL|DISTINCT] SELECT ...]
846
850
  [RETURNING ...]
847
851
  ```
848
852
 
853
+ **表达式与函数** — `CAST(expr AS type)`(INTEGER / FLOAT / DECIMAL / BOOLEAN / TEXT 等,
854
+ 支持 `VARCHAR(10)` 带长度写法);`LIKE` / `ILIKE`(大小写不敏感)/ `REGEXP` / `RLIKE`;
855
+ `= ANY (SELECT ...)` / `> ALL (SELECT ...)`(含 `SOME`);
856
+ `ORDER BY <表达式>`(如 `ORDER BY sal * 12 DESC`、`ORDER BY UPPER(name)`)。
857
+
858
+ ```sql
859
+ SELECT CAST(sal AS TEXT) AS s, CAST(ratio AS FLOAT) AS r FROM emp;
860
+ SELECT name FROM users WHERE name ILIKE 'a%'; -- 大小写不敏感
861
+ SELECT name FROM emp WHERE sal > ALL (SELECT sal FROM emp WHERE dept = 'ops');
862
+ SELECT name FROM emp ORDER BY sal * 12 DESC;
863
+ SELECT name FROM emp FETCH FIRST 10 ROWS ONLY; -- 等价于 LIMIT 10
864
+ ```
865
+
866
+ **分组汇总** — `GROUP BY ... WITH ROLLUP` 会在结果末尾追加一行汇总
867
+ (分组列置 `NULL`,聚合列覆盖全部行):
868
+
869
+ ```sql
870
+ SELECT dept, SUM(sal) AS total FROM emp GROUP BY dept WITH ROLLUP;
871
+ -- [['eng', 300], ['ops', 300], [null, 600]]
872
+ ```
873
+
874
+ **写入冲突处理** — 四种写法统一处理主键/唯一键冲突:
875
+
876
+ | 写法 | 冲突时行为 | 返回字段 |
877
+ |---|---|---|
878
+ | 默认 | 抛 `ER_DUP_ENTRY` | — |
879
+ | `INSERT IGNORE` | 跳过冲突行 | `duplicateSkipped` |
880
+ | `ON CONFLICT (col) DO NOTHING` | 跳过冲突行 | `duplicateSkipped` |
881
+ | `ON DUPLICATE KEY UPDATE ...` / `ON CONFLICT (col) DO UPDATE SET ...` | 更新已有行 | `duplicateUpdated` |
882
+ | `REPLACE INTO` | 删除旧行后插入新行 | — |
883
+
884
+ **`RETURNING`** — `INSERT` / `UPDATE` / `DELETE` 均可带 `RETURNING *` 或 `RETURNING col, ...`,
885
+ 结果在返回对象的 `returning: { columns, rows }` 上(同时填充 `columns` / `rows`):
886
+
887
+ ```sql
888
+ INSERT INTO emp (name, sal) VALUES ('z', 100) RETURNING id, name;
889
+ UPDATE emp SET sal = 555 WHERE id = 1 RETURNING id, sal;
890
+ DELETE FROM emp WHERE id = 1 RETURNING *;
891
+ ```
892
+
893
+ **视图** — `CREATE [OR REPLACE] VIEW name [(cols)] AS SELECT ...` / `DROP VIEW [IF EXISTS] name`。
894
+ 视图定义保存在 `engine._views`,查询时内联为 FROM 子查询(支持视图引用视图,嵌套上限 16 层):
895
+
896
+ ```sql
897
+ CREATE VIEW v_eng AS SELECT * FROM emp WHERE dept = 'eng';
898
+ SELECT name FROM v_eng;
899
+ CREATE OR REPLACE VIEW v_eng AS SELECT * FROM emp;
900
+ DROP VIEW v_eng;
901
+ ```
902
+
903
+ **`EXPLAIN`** — `EXPLAIN [ANALYZE] <statement>` 输出 `step` / `detail` 两列的计划结构
904
+ (扫描 / 连接 / 过滤 / 聚合 / 排序 / 限制)。**没有代价模型**,只展示计划形态,不预测行数或耗时。
905
+
906
+ ```sql
907
+ EXPLAIN SELECT name FROM emp WHERE sal > 50;
908
+ ```
909
+
849
910
  **JOINs** — `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN`, `FULL OUTER JOIN`, `CROSS JOIN`,
850
911
  implicit `FROM a, b WHERE`, self-joins, multi-table chains.
851
912
 
@@ -866,21 +927,94 @@ FROM (SELECT dept FROM emp WHERE salary > 5000) t
866
927
  GROUP BY t.dept;
867
928
  ```
868
929
 
869
- **Transactions** — `BEGIN` / `START TRANSACTION`, `COMMIT`, `ROLLBACK`,
870
- `SAVEPOINT sp` / `ROLLBACK TO SAVEPOINT` / `RELEASE SAVEPOINT`. Read-committed-equivalent
871
- isolation; per-statement atomicity without explicit transactions; cross-table transactions;
872
- rolled-back changes are invisible to other connections until commit.
930
+ **CTE(公用表表达式)** — `WITH [RECURSIVE] name [(cols)] AS (SELECT ...)`,
931
+ 可声明多个,后面的 CTE 可以引用前面的。执行层把 CTE 内联为 `FROM` 子查询,
932
+ 不需要引擎侧临时表。
933
+
934
+ ```sql
935
+ WITH engineers AS (
936
+ SELECT * FROM emp WHERE dept = 'eng'
937
+ )
938
+ SELECT name, salary FROM engineers WHERE salary > 8000;
939
+
940
+ -- 多个 CTE,后者引用前者
941
+ WITH all_emp AS (SELECT * FROM emp),
942
+ high AS (SELECT * FROM all_emp WHERE salary > 8000)
943
+ SELECT dept, COUNT(*) AS n FROM high GROUP BY dept;
944
+
945
+ -- 显式列名清单
946
+ WITH t (who, howmuch) AS (SELECT name, salary FROM emp)
947
+ SELECT who FROM t;
948
+
949
+ -- CTE 也可接 INSERT / UPDATE / DELETE
950
+ WITH src AS (SELECT * FROM emp WHERE dept = 'eng')
951
+ INSERT INTO eng_backup SELECT * FROM src;
952
+ ```
953
+
954
+ **窗口函数** — `function() OVER ( [PARTITION BY ...] [ORDER BY ...] [frame] )`。
955
+ 在 `WHERE` / `GROUP BY` / `HAVING` 之后、`DISTINCT` / `ORDER BY` / `LIMIT` 之前求值,
956
+ 符合 SQL 标准求值顺序。窗口聚合**不会**把结果塌缩成一行。
957
+
958
+ | 函数 | 说明 |
959
+ |---|---|
960
+ | `ROW_NUMBER()` | 分区内连续行号 |
961
+ | `RANK()` / `DENSE_RANK()` | 排名(并列时 `RANK` 跳号、`DENSE_RANK` 不跳号) |
962
+ | `NTILE(n)` | 把分区切成 n 个桶 |
963
+ | `LAG(expr[, n[, default]])` / `LEAD(...)` | 取分区内前/后第 n 行的值 |
964
+ | `FIRST_VALUE(expr)` / `LAST_VALUE(expr)` | 帧内首/末行的值 |
965
+ | `SUM/AVG/MIN/MAX/COUNT(...) OVER (...)` | 窗口聚合 |
966
+
967
+ ```sql
968
+ SELECT name, dept, salary,
969
+ ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn,
970
+ RANK() OVER (ORDER BY salary DESC) AS rk,
971
+ SUM(salary) OVER (PARTITION BY dept) AS dept_total,
972
+ LAG(salary) OVER (PARTITION BY dept ORDER BY salary) AS prev
973
+ FROM emp;
974
+
975
+ -- 窗口帧
976
+ SELECT name,
977
+ AVG(salary) OVER (ORDER BY salary
978
+ ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS moving_avg
979
+ FROM emp;
980
+ ```
981
+
982
+ **集合运算符** — `UNION [ALL|DISTINCT]`、`INTERSECT [ALL|DISTINCT]`、`EXCEPT [ALL|DISTINCT]`。
983
+ 三者默认都是 `DISTINCT`;`UNION ALL` 保留重复行。
984
+
985
+ ```sql
986
+ SELECT dept FROM emp WHERE salary > 5000
987
+ INTERSECT
988
+ SELECT dept FROM emp WHERE salary < 20000;
989
+
990
+ SELECT dept FROM emp
991
+ EXCEPT
992
+ SELECT dept FROM emp WHERE salary < 5000;
993
+ ```
994
+
995
+ **`EXISTS` / `NOT EXISTS`** — 支持**相关子查询**(逐行求值,外层行的列可在子查询中引用):
996
+
997
+ ```sql
998
+ SELECT name FROM emp e
999
+ WHERE EXISTS (SELECT 1 FROM orders o WHERE o.emp_id = e.id AND o.amount > 1000);
1000
+ ```
1001
+
1002
+ **Transactions** — `BEGIN` / `START TRANSACTION`, `COMMIT`, `ROLLBACK`.
1003
+ Read-committed-equivalent isolation; per-statement atomicity without explicit transactions;
1004
+ cross-table transactions; rolled-back changes are invisible to other connections until commit.
873
1005
 
874
1006
  ```sql
875
1007
  BEGIN;
876
1008
  UPDATE accounts SET balance = balance - 100 WHERE id = 1;
877
1009
  UPDATE accounts SET balance = balance + 100 WHERE id = 2;
878
- SAVEPOINT sp1;
879
- UPDATE accounts SET balance = balance - 100 WHERE id = 3;
880
- ROLLBACK TO SAVEPOINT sp1;
881
1010
  COMMIT;
882
1011
  ```
883
1012
 
1013
+ > **SAVEPOINT — 部分支持。** `SAVEPOINT name` 与 `RELEASE SAVEPOINT name` 可以解析并登记,
1014
+ > 但 **`ROLLBACK TO SAVEPOINT` 会抛出明确错误**:引擎没有保存点/快照能力,
1015
+ > 无法只回退到某个保存点。用 `ROLLBACK` 回滚整个事务。
1016
+ > 这里刻意选择"报错"而不是"静默成功"——假装回滚成功会让调用方误以为数据已还原。
1017
+
884
1018
  **Other** — `SHOW DATABASES/TABLES/COLUMNS/CREATE TABLE`, `DESCRIBE t`, `USE db`,
885
1019
  `EXPLAIN SELECT`, `SET SESSION x = y`, no-table queries (`SELECT VERSION()`), SQLite-style `PRAGMA`.
886
1020
 
@@ -1141,6 +1275,56 @@ Result matrix: SELECT → `rows`; INSERT → `affectedRows` + `insertId`; UPDATE
1141
1275
  `affectedRows`; DDL/txn → `message`. Params: positional `?`, named `:name`, or object maps.
1142
1276
  Multi-statement supported; `SQLExecutor` class runs batched SQL from a string/stream.
1143
1277
 
1278
+ #### 参数绑定:两种方式
1279
+
1280
+ ```js
1281
+ // 1) 数组入参 —— 执行前用 applyParams 内联替换(转义安全,默认路径)
1282
+ await executeSQL(db, 'SELECT * FROM emp WHERE salary > ?', [5000]);
1283
+
1284
+ // 2) 原生 ? 占位符 —— 解析成 AST 节点,在执行期绑定(5.6.0+)
1285
+ await executeSQL(db, 'SELECT * FROM emp WHERE salary > ?', { params: [5000] });
1286
+
1287
+ // 支持顺序 ? 、编号 ?1 与标识符占位 ??
1288
+ await executeSQL(db, 'SELECT * FROM emp WHERE dept = ?1 OR salary > ?2', { params: ['eng', 5000] });
1289
+ ```
1290
+
1291
+ ### AST access & rewrite
1292
+
1293
+ `parseSQL()` 返回普通对象树。5.6.0 起提供 `lib/ast.js`(同时从 `lib/sql.js` 导出 `AST`、`walk`、
1294
+ `transform`、`visit`)用于遍历、查找与改写这棵树,无需了解每种节点类型。
1295
+
1296
+ ```js
1297
+ const { parseSQL, AST } = require('jsql-neo');
1298
+
1299
+ const ast = parseSQL('SELECT name, salary FROM emp WHERE salary > 100 ORDER BY salary DESC');
1300
+
1301
+ AST.tables(ast); // ['emp']
1302
+ AST.columns(ast); // ['name', 'salary']
1303
+
1304
+ // 遍历(返回 false 可跳过子树)
1305
+ AST.walk(ast, (node, parent, key) => { console.log(node.type); });
1306
+
1307
+ // 查找 / 收集
1308
+ AST.find(ast, n => n.type === 'compare');
1309
+ AST.collect(ast, n => n.type === 'column');
1310
+
1311
+ // 不可变改写(返回新树,原 AST 不变)
1312
+ const renamed = AST.transform(ast, (node) => {
1313
+ if (node && node.type === 'column' && node.name === 'name') return { ...node, name: 'NAME' };
1314
+ });
1315
+
1316
+ // 原地改写
1317
+ AST.rewrite(ast, (node) => {
1318
+ if (node && node.type === 'value' && node.value === 100) return { ...node, value: 200 };
1319
+ });
1320
+
1321
+ // 按节点类型分派的访问器(visitSelect / visitColumn / visitCompare …)
1322
+ class Upper extends AST.StatementVisitor {
1323
+ visitColumn(node) { node.name = String(node.name).toUpperCase(); }
1324
+ }
1325
+ new Upper().run(ast);
1326
+ ```
1327
+
1144
1328
  ### Events & hooks
1145
1329
 
1146
1330
  ```js
@@ -1273,7 +1457,7 @@ jsql mod --engine wasm # switch engine (restart required)
1273
1457
 
1274
1458
  ```bash
1275
1459
  $ jsql version
1276
- jsql-neo v5.4.0
1460
+ jsql-neo v5.6.0
1277
1461
  engine: native (napi) | wasm | js
1278
1462
  node: v22.0.0 platform: linux x64
1279
1463
  ```
@@ -1294,7 +1478,7 @@ jsql tui --memory -q # memory mode, quiet
1294
1478
  jsql tui --prompt 'db> ' --no-color
1295
1479
  ```
1296
1480
 
1297
- The status bar shows: `db=<name> dialect=<d> mode=<tui|batch> ver=5.4.0`.
1481
+ The status bar shows: `db=<name> dialect=<d> mode=<tui|batch> ver=5.6.0`.
1298
1482
 
1299
1483
  ### Keyboard shortcuts
1300
1484
 
@@ -3858,7 +4042,7 @@ Data dir: /root/.jsql-neo/data
3858
4042
 
3859
4043
  ```bash
3860
4044
  $ jsql version
3861
- jsql-neo v5.4.0
4045
+ jsql-neo v5.6.0
3862
4046
  engine: native (napi) | wasm | js
3863
4047
  node: v22.0.0
3864
4048
  platform: linux x64
@@ -4935,7 +5119,7 @@ Apache License
4935
5119
 
4936
5120
  *JSQL-NEO — One engine to rule them all. MySQL. PostgreSQL. MongoDB. Redis. SQL. TypeScript. The browser.*
4937
5121
 
4938
- *文档版本:v5.4.0 · 最后更新:2026-08-12*
5122
+ *文档版本:v5.6.0 · 最后更新:2026-08-12*
4939
5123
 
4940
5124
  ---
4941
5125
 
@@ -6705,7 +6889,7 @@ Usage: jsql version
6705
6889
 
6706
6890
  输出版本与环境信息:
6707
6891
 
6708
- jsql-neo v5.4.0
6892
+ jsql-neo v5.6.0
6709
6893
  engine: native (napi) | wasm | js
6710
6894
  node: v22.0.0
6711
6895
  platform: linux x64
package/index.d.ts CHANGED
@@ -92,6 +92,7 @@ declare namespace JSQLNeo {
92
92
  stop(): Promise<void>;
93
93
  flush(): Promise<void>;
94
94
  createTable(name: string, schema: Schema): Promise<unknown>;
95
+ addColumn(table: string, name: string, def: string | FieldDef): Promise<unknown>;
95
96
  dropTable(name: string): Promise<unknown>;
96
97
  insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<unknown>;
97
98
  insertMany(table: string, data: Record<string, unknown>[]): Promise<unknown>;
@@ -260,6 +261,32 @@ declare namespace JSQLNeo {
260
261
  safety?: boolean;
261
262
  maxStatements?: number;
262
263
  session?: Record<string, unknown>;
264
+ /** 5.5.0+:原生 `?` 占位符绑定值(与 applyParams 的数组入参二选一) */
265
+ params?: unknown[];
266
+ }
267
+
268
+ /** AST 节点:带字符串 type 的普通对象树 */
269
+ export interface ASTNode {
270
+ type: string;
271
+ [key: string]: unknown;
272
+ }
273
+
274
+ /** 5.5.0+:AST 访问与改写 */
275
+ export namespace AST {
276
+ function walk(ast: unknown, visitor: (node: ASTNode, parent?: unknown, key?: string) => void | false, opts?: { post?: (node: ASTNode) => void }): unknown;
277
+ function collect(ast: unknown, predicate: (node: ASTNode) => boolean): ASTNode[];
278
+ function find(ast: unknown, predicate: (node: ASTNode) => boolean): ASTNode | null;
279
+ function transform(ast: unknown, fn: (node: unknown) => unknown): unknown;
280
+ function rewrite(ast: unknown, fn: (node: unknown) => unknown): unknown;
281
+ function tables(ast: unknown): string[];
282
+ function columns(ast: unknown): string[];
283
+ function isNode(value: unknown): boolean;
284
+ function cloneNode(node: unknown): unknown;
285
+ class StatementVisitor {
286
+ run(ast: unknown): unknown;
287
+ visitDefault?(node: ASTNode): void;
288
+ [key: string]: unknown;
289
+ }
263
290
  }
264
291
 
265
292
  export function executeSQL(engine: unknown, sql: string, paramsOrOpts?: unknown[] | SQLOptions, opts?: SQLOptions): Promise<SQLResult>;
@@ -268,6 +295,8 @@ declare namespace JSQLNeo {
268
295
  export function applyParams(sql: string, values: unknown[]): string;
269
296
  export function escapeValue(value: unknown): string;
270
297
  export function escapeId(value: string): string;
298
+ export function walk(ast: unknown, visitor: (node: ASTNode) => void | false): unknown;
299
+ export function transform(ast: unknown, fn: (node: unknown) => unknown): unknown;
271
300
 
272
301
  export interface DatabaseOptions {
273
302
  dataDir?: string;
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v5.1.2 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.0.0-beta1 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
@@ -13,7 +13,8 @@
13
13
 
14
14
  const WasmClient = require('./lib/wasm_client');
15
15
  const NativeClient = require('./lib/native_client');
16
- const { Plugin } = require('./lib/plugin');
16
+ const { Plugin, definePlugin, HOOKS } = require('./lib/plugin');
17
+ const plugins = require('./lib/plugins');
17
18
  const { ModuleManager } = require('./lib/mod');
18
19
  const sql = require('./lib/sql');
19
20
  const { Datastore } = require('./lib/nedb_compat');
@@ -71,6 +72,9 @@ module.exports = {
71
72
  BTree: require('./lib/btree'),
72
73
  Cache: require('./lib/cache'),
73
74
  Plugin,
75
+ definePlugin,
76
+ HOOKS,
77
+ plugins,
74
78
  ModuleManager,
75
79
  JSQL_Error: require('./lib/errors').JSQL_Error,
76
80
  ErrorCodes: require('./lib/errors').ErrorCodes,
@@ -80,6 +84,12 @@ module.exports = {
80
84
  SQL: sql,
81
85
  executeSQL: sql.executeSQL,
82
86
  parseSQL: sql.parseSQL,
87
+ // AST 访问与改写(5.5.0+)
88
+ AST: sql.AST,
89
+ walk: sql.walk,
90
+ transform: sql.transform,
91
+ tokenize: sql.tokenize,
92
+ splitStatements: sql.splitStatements,
83
93
  Datastore,
84
94
  createConnection: mysqlCompat.createConnection,
85
95
  createPool: mysqlCompat.createPool,
package/lib/ast.js ADDED
@@ -0,0 +1,214 @@
1
+ /**
2
+ * lib/ast.js — JSQL-NEO AST 访问与改写
3
+ *
4
+ * parseSQL() 返回的是普通 JS 对象树。本模块提供在不了解每种节点类型的
5
+ * 前提下遍历 / 查找 / 改写这棵树的能力:
6
+ *
7
+ * walk(ast, visitor[, opts]) 深度优先遍历(前序 + 可选后序)
8
+ * collect(ast, predicate) 收集所有满足条件的节点
9
+ * find(ast, predicate) 返回第一个满足条件的节点
10
+ * transform(ast, fn) 不可变改写,返回新树
11
+ * rewrite(ast, fn) 原地改写
12
+ * tables(ast) 列出语句引用的所有表名
13
+ * columns(ast) 列出语句引用的所有列名
14
+ * StatementVisitor 基类,按节点类型分派 visitXxx 方法
15
+ *
16
+ * 设计约定:
17
+ * - 「节点」指带字符串 type 字段的对象;但遍历会穿透所有普通对象与数组,
18
+ * 因为 SELECT 列描述符({ expr, scalar, alias })这类中间结构没有 type。
19
+ * - seen 集合防止共享子树被重复访问或死循环。
20
+ * - transform 返回新树、不改入参;rewrite 原地修改。
21
+ */
22
+
23
+ const SKIP_KEYS = new Set(['pos']);
24
+
25
+ function isNode(v) {
26
+ return v !== null && typeof v === 'object' && !Array.isArray(v) && typeof v.type === 'string';
27
+ }
28
+
29
+ function childKeys(obj) {
30
+ const out = [];
31
+ for (const k of Object.keys(obj)) if (!SKIP_KEYS.has(k)) out.push(k);
32
+ return out;
33
+ }
34
+
35
+ /** 深度优先遍历。visitor(node, parent, key) 返回 false 可跳过该子树。 */
36
+ function walk(ast, visitor, opts = {}) {
37
+ const seen = new Set();
38
+ const post = opts.post || null;
39
+ const fn = typeof visitor === 'function' ? visitor : null;
40
+
41
+ const visit = (value, parent, key) => {
42
+ if (value === null || typeof value !== 'object') return;
43
+ if (seen.has(value)) return;
44
+ seen.add(value);
45
+
46
+ if (Array.isArray(value)) {
47
+ for (const item of value) visit(item, parent, key);
48
+ return;
49
+ }
50
+
51
+ const node = isNode(value);
52
+ let descend = true;
53
+ if (node && fn) {
54
+ const r = fn(value, parent, key);
55
+ if (r === false) descend = false;
56
+ }
57
+ if (descend) {
58
+ for (const k of childKeys(value)) visit(value[k], value, k);
59
+ }
60
+ if (node && post) post(value, parent, key);
61
+ };
62
+
63
+ if (Array.isArray(ast)) { for (const n of ast) visit(n, null, null); }
64
+ else visit(ast, null, null);
65
+ return ast;
66
+ }
67
+
68
+ /** 收集所有满足 predicate 的节点。 */
69
+ function collect(ast, predicate) {
70
+ const out = [];
71
+ walk(ast, (node) => { if (predicate(node)) out.push(node); });
72
+ return out;
73
+ }
74
+
75
+ /** 返回第一个满足 predicate 的节点,找不到返回 null。 */
76
+ function find(ast, predicate) {
77
+ let hit = null;
78
+ walk(ast, (node) => {
79
+ if (hit) return false;
80
+ if (predicate(node)) { hit = node; return false; }
81
+ });
82
+ return hit;
83
+ }
84
+
85
+ function cloneValue(v, seen) {
86
+ if (v === null || typeof v !== 'object') return v;
87
+ if (seen.has(v)) return v;
88
+ seen.add(v);
89
+ if (Array.isArray(v)) return v.map(x => cloneValue(x, seen));
90
+ const out = {};
91
+ for (const k of Object.keys(v)) out[k] = cloneValue(v[k], seen);
92
+ return out;
93
+ }
94
+
95
+ function cloneNode(node) { return cloneValue(node, new Set()); }
96
+
97
+ /**
98
+ * 不可变改写:对每个节点调用 fn(node),返回值非 undefined 时替换该节点。
99
+ * 返回新树,入参保持不变。
100
+ */
101
+ function transform(ast, fn) {
102
+ const seen = new Set();
103
+
104
+ const visit = (value) => {
105
+ if (value === null || typeof value !== 'object') {
106
+ const r = fn(value);
107
+ return r === undefined ? value : r;
108
+ }
109
+ if (seen.has(value)) return value;
110
+ seen.add(value);
111
+
112
+ if (Array.isArray(value)) {
113
+ const arr = value.map(visit);
114
+ const rep = fn(arr);
115
+ return rep === undefined ? arr : rep;
116
+ }
117
+
118
+ const out = {};
119
+ for (const k of Object.keys(value)) out[k] = visit(value[k]);
120
+
121
+ if (isNode(value)) {
122
+ const rep = fn(out);
123
+ return rep === undefined ? out : rep;
124
+ }
125
+ return out;
126
+ };
127
+
128
+ return visit(ast);
129
+ }
130
+
131
+ /** 原地改写:fn(node) 返回值非 undefined 时替换原节点。 */
132
+ function rewrite(ast, fn) {
133
+ const seen = new Set();
134
+
135
+ const visit = (value, parent, key, index) => {
136
+ if (value === null || typeof value !== 'object') {
137
+ if (parent && key) {
138
+ const r = fn(value);
139
+ if (r !== undefined) {
140
+ if (index === undefined) parent[key] = r; else parent[key][index] = r;
141
+ }
142
+ }
143
+ return;
144
+ }
145
+ if (seen.has(value)) return;
146
+ seen.add(value);
147
+
148
+ if (Array.isArray(value)) {
149
+ for (let i = 0; i < value.length; i++) visit(value[i], parent, key, i);
150
+ return;
151
+ }
152
+
153
+ for (const k of childKeys(value)) visit(value[k], value, k);
154
+
155
+ if (isNode(value) && parent && key) {
156
+ const r = fn(value);
157
+ if (r !== undefined) {
158
+ if (index === undefined) parent[key] = r; else parent[key][index] = r;
159
+ }
160
+ }
161
+ };
162
+
163
+ visit(ast, null, null);
164
+ return ast;
165
+ }
166
+
167
+ /** 语句引用的所有表名(含 JOIN / 子查询)。 */
168
+ function tables(ast) {
169
+ const out = new Set();
170
+ walk(ast, (node) => {
171
+ if (node.type === 'select' && node.from) {
172
+ for (const t of (node.from.tables || [])) if (t && t.table) out.add(t.table);
173
+ for (const j of (node.from.joins || [])) if (j.item && j.item.table) out.add(j.item.table);
174
+ }
175
+ if (node.type === 'insert' || node.type === 'update' || node.type === 'delete' ||
176
+ node.type === 'createTable' || node.type === 'dropTable' || node.type === 'truncate') {
177
+ if (node.name) out.add(node.name);
178
+ if (node.table) out.add(node.table);
179
+ }
180
+ });
181
+ return [...out];
182
+ }
183
+
184
+ /** 语句引用的所有列名(去重)。 */
185
+ function columns(ast) {
186
+ const out = new Set();
187
+ walk(ast, (node) => {
188
+ if (node.type === 'column' && node.name) out.add(node.name);
189
+ });
190
+ return [...out];
191
+ }
192
+
193
+ /**
194
+ * 按节点类型分派的访问器基类。
195
+ * 子类定义 visitSelect(node) / visitCompare(node) … 即可;未定义类型走 visitDefault。
196
+ *
197
+ * class MyVisitor extends StatementVisitor {
198
+ * visitColumn(node) { node.name = String(node.name).toLowerCase(); }
199
+ * }
200
+ * new MyVisitor().run(ast);
201
+ */
202
+ class StatementVisitor {
203
+ run(ast) {
204
+ walk(ast, (node) => {
205
+ const camel = String(node.type).replace(/(^|[-_])([a-z])/g, (_, __, ch) => ch.toUpperCase());
206
+ const method = this['visit' + camel];
207
+ if (typeof method === 'function') method.call(this, node);
208
+ else if (typeof this.visitDefault === 'function') this.visitDefault(node);
209
+ });
210
+ return ast;
211
+ }
212
+ }
213
+
214
+ module.exports = { walk, collect, find, transform, rewrite, tables, columns, StatementVisitor, isNode, cloneNode };