jsql-neo 4.5.2 → 5.0.1
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 +1 -1
- package/lib/sql.js +240 -16
- package/lib/sqlite_compat.js +323 -0
- package/lib/sqlite_worker.js +260 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -284,6 +284,6 @@ CI (`.github/workflows/ci.yml`): engine smoke tests on Node 18/20/22 + a full OR
|
|
|
284
284
|
|
|
285
285
|
## License
|
|
286
286
|
|
|
287
|
-
|
|
287
|
+
[Apache-2.0](LICENSE) — free to use, modify, and distribute with attribution.
|
|
288
288
|
|
|
289
289
|
*JSQL-NEO: Rust-powered. Protocol-native. One package.*
|
package/lib/sql.js
CHANGED
|
@@ -56,7 +56,7 @@ const KEYWORDS = new Set([
|
|
|
56
56
|
'BETWEEN', 'USING', 'FULL', 'UNSIGNED', 'ZEROFILL', 'TRUNCATE', 'COLLATE', 'CHARACTER',
|
|
57
57
|
'ALTER', 'ADD', 'COLUMN', 'MODIFY', 'CHANGE', 'INDEX', 'FOREIGN', 'REFERENCES',
|
|
58
58
|
'CONSTRAINT', 'RENAME', 'TO', 'AFTER', 'FIRST', 'ENGINE', 'AUTO_INCREMENT', 'SPATIAL',
|
|
59
|
-
'REGEXP', 'TRUE', 'FALSE', 'RLIKE'
|
|
59
|
+
'REGEXP', 'TRUE', 'FALSE', 'RLIKE', 'PRAGMA', 'REPLACE', 'BLOB', 'RAISE', 'IGNORE'
|
|
60
60
|
]);
|
|
61
61
|
|
|
62
62
|
function tokenize(sql) {
|
|
@@ -245,6 +245,7 @@ class Parser {
|
|
|
245
245
|
case 'SET': return this.parseSet();
|
|
246
246
|
case 'DESCRIBE': case 'DESC': return this.parseDescribe();
|
|
247
247
|
case 'USE': return this.parseUse();
|
|
248
|
+
case 'PRAGMA': return this.parsePragma();
|
|
248
249
|
default: throw new Error(`Unsupported statement: ${t.value}`);
|
|
249
250
|
}
|
|
250
251
|
}
|
|
@@ -380,9 +381,14 @@ class Parser {
|
|
|
380
381
|
if (t.type === 'keyword') {
|
|
381
382
|
switch (t.value) {
|
|
382
383
|
case 'PRIMARY':
|
|
383
|
-
this.next(); this.expectKeyword('KEY'); def.primaryKey = true; def.unique = true;
|
|
384
|
+
this.next(); this.expectKeyword('KEY'); def.primaryKey = true; def.unique = true;
|
|
385
|
+
// SQLite 语义: INTEGER PRIMARY KEY 是 rowid 别名, 自动生成
|
|
386
|
+
if (mapped === 'integer') def.autoIncrement = true;
|
|
387
|
+
break;
|
|
384
388
|
case 'KEY':
|
|
385
|
-
this.next(); def.primaryKey = true; def.unique = true;
|
|
389
|
+
this.next(); def.primaryKey = true; def.unique = true;
|
|
390
|
+
if (mapped === 'integer') def.autoIncrement = true;
|
|
391
|
+
break;
|
|
386
392
|
case 'AUTO_INCREMENT':
|
|
387
393
|
case 'AUTOINCREMENT':
|
|
388
394
|
this.next(); def.autoIncrement = true; break;
|
|
@@ -952,6 +958,37 @@ class Parser {
|
|
|
952
958
|
return { type: 'use', database: db };
|
|
953
959
|
}
|
|
954
960
|
|
|
961
|
+
parsePragma() {
|
|
962
|
+
this.expectKeyword('PRAGMA');
|
|
963
|
+
let name = '';
|
|
964
|
+
const first = this.next();
|
|
965
|
+
if (first.type !== 'ident' && first.type !== 'keyword') throw new Error(`Expected pragma name, got '${first.value}'`);
|
|
966
|
+
name = first.value;
|
|
967
|
+
// pragma 名可能带 db. 前缀: PRAGMA main.table_info(users)
|
|
968
|
+
if (this.peek().type === 'op' && this.peek().value === '.') {
|
|
969
|
+
this.next();
|
|
970
|
+
const second = this.next();
|
|
971
|
+
if (second.type !== 'ident' && second.type !== 'keyword') throw new Error(`Expected pragma name after '.', got '${second.value}'`);
|
|
972
|
+
name = first.value + '.' + second.value;
|
|
973
|
+
}
|
|
974
|
+
let arg = null;
|
|
975
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
976
|
+
this.next();
|
|
977
|
+
const a = this.next();
|
|
978
|
+
if (a.type !== 'op' && a.type !== 'eof') arg = a.value;
|
|
979
|
+
if (this.peek().type === 'op' && this.peek().value === ')') this.next();
|
|
980
|
+
} else if (this.peek().type === 'op' && this.peek().value === '=') {
|
|
981
|
+
this.next();
|
|
982
|
+
const v = this.next();
|
|
983
|
+
if (v.type === 'number' || v.type === 'ident' || v.type === 'keyword' || v.type === 'string') arg = v.value;
|
|
984
|
+
} else if (this.peek().type !== 'eof' && !(this.peek().type === 'op' && this.peek().value === ';')) {
|
|
985
|
+
const v = this.next();
|
|
986
|
+
if (v.type === 'number' || v.type === 'ident' || v.type === 'keyword') arg = v.value;
|
|
987
|
+
}
|
|
988
|
+
this.optionalTailSemicolon();
|
|
989
|
+
return { type: 'pragma', name, arg };
|
|
990
|
+
}
|
|
991
|
+
|
|
955
992
|
optionalTailSemicolon() {
|
|
956
993
|
if (this.peek().type === 'op' && this.peek().value === ';') this.next();
|
|
957
994
|
}
|
|
@@ -1012,7 +1049,8 @@ class Parser {
|
|
|
1012
1049
|
const args = [];
|
|
1013
1050
|
if (!(this.peek().type === 'op' && this.peek().value === ')')) {
|
|
1014
1051
|
for (;;) {
|
|
1015
|
-
|
|
1052
|
+
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); args.push({ type: 'star' }); }
|
|
1053
|
+
else args.push(this.parseOperand());
|
|
1016
1054
|
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1017
1055
|
break;
|
|
1018
1056
|
}
|
|
@@ -1032,7 +1070,8 @@ class Parser {
|
|
|
1032
1070
|
const args = [];
|
|
1033
1071
|
if (!(this.peek().type === 'op' && this.peek().value === ')')) {
|
|
1034
1072
|
for (;;) {
|
|
1035
|
-
|
|
1073
|
+
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); args.push({ type: 'star' }); }
|
|
1074
|
+
else args.push(this.parseOperand());
|
|
1036
1075
|
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1037
1076
|
break;
|
|
1038
1077
|
}
|
|
@@ -1319,9 +1358,13 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
1319
1358
|
const name = (fnNode.name || '').toUpperCase();
|
|
1320
1359
|
const args = (fnNode.args || []).map(a => resolveOperand(a, row, ctx));
|
|
1321
1360
|
const session = ctx && ctx.session;
|
|
1361
|
+
if (ctx && ctx.functions && Object.prototype.hasOwnProperty.call(ctx.functions, name)) {
|
|
1362
|
+
return ctx.functions[name].apply(null, args);
|
|
1363
|
+
}
|
|
1322
1364
|
switch (name) {
|
|
1323
1365
|
case 'VERSION': return '8.0.0-jsql-neo';
|
|
1324
|
-
case 'LAST_INSERT_ID':
|
|
1366
|
+
case 'LAST_INSERT_ID':
|
|
1367
|
+
case 'LAST_INSERT_ROWID': {
|
|
1325
1368
|
if (args.length > 0) {
|
|
1326
1369
|
if (session) session.lastInsertId = args[0];
|
|
1327
1370
|
return args[0];
|
|
@@ -1564,7 +1607,15 @@ function scalarColumnName(c) {
|
|
|
1564
1607
|
}
|
|
1565
1608
|
|
|
1566
1609
|
function scalarColumnValue(c, r, ctx) {
|
|
1567
|
-
if (c.scalar)
|
|
1610
|
+
if (c.scalar) {
|
|
1611
|
+
const s = c.scalar;
|
|
1612
|
+
if (s && s.type === 'func' && ctx && ctx.ctxAggregates && Object.prototype.hasOwnProperty.call(ctx.ctxAggregates, String(s.name).toUpperCase())) {
|
|
1613
|
+
const fn = String(s.name).toUpperCase();
|
|
1614
|
+
const col = s.args && s.args[0];
|
|
1615
|
+
return ctx._aggValue(ctx.group, fn, col);
|
|
1616
|
+
}
|
|
1617
|
+
return resolveOperand(c.scalar, r, ctx);
|
|
1618
|
+
}
|
|
1568
1619
|
if (c.aggregate) return ctx._aggValue(ctx.group, c.aggregate || 'COUNT', c.column);
|
|
1569
1620
|
if (c.literal !== undefined) return c.literal;
|
|
1570
1621
|
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r, ctx);
|
|
@@ -1979,6 +2030,16 @@ class SQLExecutor {
|
|
|
1979
2030
|
await this.engine.useDatabase(statement.database);
|
|
1980
2031
|
}
|
|
1981
2032
|
return { ok: true, type: 'use', database: statement.database };
|
|
2033
|
+
case 'pragma': {
|
|
2034
|
+
const rows = this._runPragma(statement.name, statement.arg);
|
|
2035
|
+
const isSelect = ['table_info', 'table_list', 'index_list', 'index_info', 'collation_list', 'database_list', 'module_list', 'function_list', 'pragma_list'].includes(statement.name.toLowerCase());
|
|
2036
|
+
if (isSelect) {
|
|
2037
|
+
const cols = rows.length > 0 ? Object.keys(rows[0]) : [];
|
|
2038
|
+
return { ok: true, type: 'select', columns: cols, rows: rows.map(r => cols.map(c => r[c])), raw: rows };
|
|
2039
|
+
}
|
|
2040
|
+
const simple = rows.length > 0 && Object.keys(rows[0]).length === 1 ? rows[0][Object.keys(rows[0])[0]] : rows;
|
|
2041
|
+
return { ok: true, type: 'pragma', name: statement.name, value: simple };
|
|
2042
|
+
}
|
|
1982
2043
|
case 'describe': {
|
|
1983
2044
|
const schema = this.engine.getTableSchema ? await this.engine.getTableSchema(statement.table) : null;
|
|
1984
2045
|
if (!schema) throw new Error(`Table '${statement.table}' does not exist`);
|
|
@@ -1990,6 +2051,110 @@ class SQLExecutor {
|
|
|
1990
2051
|
}
|
|
1991
2052
|
}
|
|
1992
2053
|
|
|
2054
|
+
_runPragma(name, arg) {
|
|
2055
|
+
const engine = this.engine;
|
|
2056
|
+
const lname = String(name).toLowerCase().replace(/^.*\./, '');
|
|
2057
|
+
const rows = [];
|
|
2058
|
+
switch (lname) {
|
|
2059
|
+
case 'table_info': {
|
|
2060
|
+
const schema = engine.getTableSchema ? engine.getTableSchema(arg) : (engine._schemas ? engine._schemas[arg] : null);
|
|
2061
|
+
if (!schema) throw new Error(`table ${arg} may not be queried: no such table`);
|
|
2062
|
+
let cid = 0;
|
|
2063
|
+
for (const [col, def] of Object.entries(schema)) {
|
|
2064
|
+
let typeName = String(def.type || 'text').toUpperCase();
|
|
2065
|
+
if (typeName === 'NUMBER') typeName = 'REAL';
|
|
2066
|
+
if (typeName === 'OBJECT' || typeName === 'ARRAY') typeName = 'TEXT';
|
|
2067
|
+
rows.push({
|
|
2068
|
+
cid,
|
|
2069
|
+
name: col,
|
|
2070
|
+
type: typeName,
|
|
2071
|
+
notnull: def.required ? 1 : 0,
|
|
2072
|
+
dflt_value: def.default !== undefined ? def.default : null,
|
|
2073
|
+
pk: def.primaryKey ? 1 : 0,
|
|
2074
|
+
});
|
|
2075
|
+
cid++;
|
|
2076
|
+
}
|
|
2077
|
+
break;
|
|
2078
|
+
}
|
|
2079
|
+
case 'table_list': {
|
|
2080
|
+
const tables = engine.listTables ? engine.listTables() : Array.from(engine._tableNames || []);
|
|
2081
|
+
for (const t of tables) {
|
|
2082
|
+
rows.push({ schema: 'main', name: t, type: 'table', ncol: 0, wr: 1, strict: 0 });
|
|
2083
|
+
}
|
|
2084
|
+
break;
|
|
2085
|
+
}
|
|
2086
|
+
case 'index_list': {
|
|
2087
|
+
const schema = engine.getTableSchema ? engine.getTableSchema(arg) : (engine._schemas ? engine._schemas[arg] : null);
|
|
2088
|
+
if (!schema) throw new Error(`table ${arg} may not be queried: no such table`);
|
|
2089
|
+
let seq = 0;
|
|
2090
|
+
for (const [col, def] of Object.entries(schema)) {
|
|
2091
|
+
if (def.primaryKey || def.unique) {
|
|
2092
|
+
rows.push({ seq, name: 'sqlite_autoindex_' + arg + '_' + (seq + 1), unique: def.unique ? 1 : 0, origin: def.primaryKey ? 'pk' : 'u', partial: 0 });
|
|
2093
|
+
seq++;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
break;
|
|
2097
|
+
}
|
|
2098
|
+
case 'index_info': {
|
|
2099
|
+
const schema = engine.getTableSchema ? engine.getTableSchema(arg) : (engine._schemas ? engine._schemas[arg] : null);
|
|
2100
|
+
if (!schema) throw new Error(`no such index: ${arg}`);
|
|
2101
|
+
let seqno = 0;
|
|
2102
|
+
for (const [col, def] of Object.entries(schema)) {
|
|
2103
|
+
if (def.primaryKey || def.unique) {
|
|
2104
|
+
rows.push({ seqno, cid: seqno, name: col });
|
|
2105
|
+
seqno++;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
break;
|
|
2109
|
+
}
|
|
2110
|
+
case 'database_list': {
|
|
2111
|
+
rows.push({ seq: 0, name: 'main', file: '' });
|
|
2112
|
+
break;
|
|
2113
|
+
}
|
|
2114
|
+
case 'user_version': {
|
|
2115
|
+
const val = arg !== undefined && arg !== null ? arg : ((engine._pragmaValues && engine._pragmaValues.user_version) || 0);
|
|
2116
|
+
if (arg !== undefined && arg !== null) {
|
|
2117
|
+
engine._pragmaValues = engine._pragmaValues || {};
|
|
2118
|
+
engine._pragmaValues.user_version = Number(arg);
|
|
2119
|
+
}
|
|
2120
|
+
rows.push({ user_version: val });
|
|
2121
|
+
break;
|
|
2122
|
+
}
|
|
2123
|
+
case 'journal_mode': {
|
|
2124
|
+
rows.push({ journal_mode: arg !== undefined && arg !== null ? arg : 'memory' });
|
|
2125
|
+
break;
|
|
2126
|
+
}
|
|
2127
|
+
case 'foreign_keys': {
|
|
2128
|
+
rows.push({ foreign_keys: arg !== undefined && arg !== null ? arg : 0 });
|
|
2129
|
+
break;
|
|
2130
|
+
}
|
|
2131
|
+
case 'synchronous': {
|
|
2132
|
+
rows.push({ synchronous: arg !== undefined && arg !== null ? arg : 0 });
|
|
2133
|
+
break;
|
|
2134
|
+
}
|
|
2135
|
+
case 'cache_size': {
|
|
2136
|
+
rows.push({ cache_size: arg !== undefined && arg !== null ? arg : 0 });
|
|
2137
|
+
break;
|
|
2138
|
+
}
|
|
2139
|
+
case 'page_size':
|
|
2140
|
+
case 'encoding':
|
|
2141
|
+
case 'auto_vacuum':
|
|
2142
|
+
case 'temp_store':
|
|
2143
|
+
case 'locking_mode':
|
|
2144
|
+
case 'application_id':
|
|
2145
|
+
case 'integrity_check':
|
|
2146
|
+
case 'quick_check': {
|
|
2147
|
+
rows.push({ [lname]: lname === 'encoding' ? 'UTF-8' : (arg !== undefined && arg !== null ? arg : 0) });
|
|
2148
|
+
break;
|
|
2149
|
+
}
|
|
2150
|
+
default: {
|
|
2151
|
+
rows.push({ [lname]: arg !== undefined && arg !== null ? arg : 0 });
|
|
2152
|
+
break;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
return rows;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
1993
2158
|
async _getSchema(name) {
|
|
1994
2159
|
if (this.engine.hasTable && !this.engine.hasTable(name)) {
|
|
1995
2160
|
throw new Error(`Table '${name}' does not exist`);
|
|
@@ -2204,7 +2369,20 @@ class SQLExecutor {
|
|
|
2204
2369
|
_aggValue(rows, fn, column) {
|
|
2205
2370
|
if (fn === 'COUNT') return rows.length;
|
|
2206
2371
|
const op = typeof column === 'string' ? { type: 'column', name: column } : column;
|
|
2207
|
-
const values =
|
|
2372
|
+
const values = op && op.type === 'star'
|
|
2373
|
+
? rows.map(r => 1)
|
|
2374
|
+
: rows.map(r => resolveOperand(op, r, this.ctx)).filter(v => v !== null && v !== undefined);
|
|
2375
|
+
if (this.ctx && this.ctx.aggregates && Object.prototype.hasOwnProperty.call(this.ctx.aggregates, fn)) {
|
|
2376
|
+
const agg = this.ctx.aggregates[fn];
|
|
2377
|
+
if (typeof agg === 'function') {
|
|
2378
|
+
return agg(values);
|
|
2379
|
+
}
|
|
2380
|
+
if (agg && typeof agg.step === 'function') {
|
|
2381
|
+
let state = typeof agg.start === 'function' ? agg.start() : undefined;
|
|
2382
|
+
for (const v of values) state = agg.step(state, v);
|
|
2383
|
+
return typeof agg.result === 'function' ? agg.result(state) : state;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2208
2386
|
if (fn === 'SUM') return values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0);
|
|
2209
2387
|
if (fn === 'AVG') return values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
|
|
2210
2388
|
if (fn === 'MIN') return values.length ? Math.min(...values.map(v => Number(v))) : null;
|
|
@@ -2396,10 +2574,17 @@ class SQLExecutor {
|
|
|
2396
2574
|
}
|
|
2397
2575
|
|
|
2398
2576
|
// 聚合输出(无 GROUP BY 时)
|
|
2399
|
-
|
|
2577
|
+
const hasCustomAgg = this.ctx && this.ctx.aggregates && statement.columns.some(c => c.scalar && c.scalar.type === 'func' && Object.prototype.hasOwnProperty.call(this.ctx.aggregates, String(c.scalar.name).toUpperCase()));
|
|
2578
|
+
if (!statement.groupBy && (statement.columns.some(c => c.aggregate) || hasCustomAgg)) {
|
|
2400
2579
|
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
2401
2580
|
const valueOf = (c) => {
|
|
2402
2581
|
if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column);
|
|
2582
|
+
if (c.scalar && c.scalar.type === 'func' && hasCustomAgg) {
|
|
2583
|
+
const fn = String(c.scalar.name).toUpperCase();
|
|
2584
|
+
if (Object.prototype.hasOwnProperty.call(this.ctx.aggregates, fn)) {
|
|
2585
|
+
return this._aggValue(rows, fn, c.scalar.args && c.scalar.args[0]);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2403
2588
|
if (c.scalar) return resolveOperand(c.scalar, rows[0] || {}, this.ctx);
|
|
2404
2589
|
if (c.caseExpr) return resolveOperand(c.caseExpr, rows[0] || {}, this.ctx);
|
|
2405
2590
|
if (c.expr !== null && c.expr !== '*') return rows[0] ? resolveOperand({ type: 'column', name: c.expr }, rows[0]) : null;
|
|
@@ -2413,7 +2598,7 @@ class SQLExecutor {
|
|
|
2413
2598
|
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
2414
2599
|
const mapped = rows.map(r => {
|
|
2415
2600
|
const group = r._group || [r];
|
|
2416
|
-
return statement.columns.map(c => scalarColumnValue(c, r, { _aggValue: this._aggValue.bind(this), group }));
|
|
2601
|
+
return statement.columns.map(c => scalarColumnValue(c, r, { _aggValue: this._aggValue.bind(this), group, ctxAggregates: this.ctx ? this.ctx.aggregates : null }));
|
|
2417
2602
|
});
|
|
2418
2603
|
return { ok: true, type: 'select', table: statement.from ? (statement.from.tables[0].table || null) : null, columns: cols, rows: mapped, raw: rows };
|
|
2419
2604
|
}
|
|
@@ -2578,6 +2763,11 @@ function applyParams(sql, values) {
|
|
|
2578
2763
|
let idx = 0;
|
|
2579
2764
|
let inStr = null;
|
|
2580
2765
|
let i = 0;
|
|
2766
|
+
const named = {};
|
|
2767
|
+
let hasNamed = false;
|
|
2768
|
+
if (args && typeof args === 'object' && !Array.isArray(args)) {
|
|
2769
|
+
hasNamed = true;
|
|
2770
|
+
}
|
|
2581
2771
|
while (i < sql.length) {
|
|
2582
2772
|
const c = sql[i];
|
|
2583
2773
|
if (inStr) {
|
|
@@ -2589,23 +2779,53 @@ function applyParams(sql, values) {
|
|
|
2589
2779
|
}
|
|
2590
2780
|
if (c === "'" || c === '"' || c === '`') { inStr = c; out += c; i++; continue; }
|
|
2591
2781
|
if (c === '?' && sql[i + 1] === '?') {
|
|
2592
|
-
if (idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
|
|
2593
|
-
out += escapeId(args[idx++]);
|
|
2782
|
+
if (!hasNamed && idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
|
|
2783
|
+
out += escapeId(hasNamed ? args['@@'] : args[idx++]);
|
|
2594
2784
|
count++;
|
|
2595
2785
|
i += 2;
|
|
2596
2786
|
continue;
|
|
2597
2787
|
}
|
|
2598
2788
|
if (c === '?') {
|
|
2599
|
-
if (
|
|
2600
|
-
|
|
2789
|
+
if (sql[i + 1] >= '0' && sql[i + 1] <= '9') {
|
|
2790
|
+
// ?N 编号占位符
|
|
2791
|
+
let num = '';
|
|
2792
|
+
let j = i + 1;
|
|
2793
|
+
while (j < sql.length && sql[j] >= '0' && sql[j] <= '9') { num += sql[j]; j++; }
|
|
2794
|
+
const n = parseInt(num, 10);
|
|
2795
|
+
if (hasNamed) {
|
|
2796
|
+
if (!(n in args)) throw new Error(`No value for parameter ?${n}`);
|
|
2797
|
+
out += escapeValue(args[n]);
|
|
2798
|
+
} else {
|
|
2799
|
+
if (n - 1 >= args.length) throw new Error(`Not enough parameters for SQL: expected ?${n}`);
|
|
2800
|
+
out += escapeValue(args[n - 1]);
|
|
2801
|
+
if (n > idx) idx = n;
|
|
2802
|
+
}
|
|
2803
|
+
count++;
|
|
2804
|
+
i = j;
|
|
2805
|
+
continue;
|
|
2806
|
+
}
|
|
2807
|
+
if (!hasNamed && idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
|
|
2808
|
+
out += escapeValue(hasNamed ? args['?'] : args[idx++]);
|
|
2601
2809
|
count++;
|
|
2602
2810
|
i++;
|
|
2603
2811
|
continue;
|
|
2604
2812
|
}
|
|
2813
|
+
if ((c === ':' || c === '@' || c === '$') && i + 1 < sql.length && /[A-Za-z_]/.test(sql[i + 1])) {
|
|
2814
|
+
// 命名占位符 :name @name $name
|
|
2815
|
+
let name = '';
|
|
2816
|
+
let j = i + 1;
|
|
2817
|
+
while (j < sql.length && /[A-Za-z0-9_]/.test(sql[j])) { name += sql[j]; j++; }
|
|
2818
|
+
if (!hasNamed) throw new Error(`Named parameter ${c}${name} requires an object of parameters`);
|
|
2819
|
+
if (!(name in args)) throw new Error(`No value for parameter ${c}${name}`);
|
|
2820
|
+
out += escapeValue(args[name]);
|
|
2821
|
+
count++;
|
|
2822
|
+
i = j;
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2605
2825
|
out += c;
|
|
2606
2826
|
i++;
|
|
2607
2827
|
}
|
|
2608
|
-
if (idx !== args.length) {
|
|
2828
|
+
if (!hasNamed && idx !== args.length) {
|
|
2609
2829
|
throw new Error(`Too many parameters for SQL: got ${args.length}, expected ${count}`);
|
|
2610
2830
|
}
|
|
2611
2831
|
return out;
|
|
@@ -2632,7 +2852,11 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
|
2632
2852
|
if (dangerous) throw new Error(`SQL statement blocked by security policy: ${dangerous}`);
|
|
2633
2853
|
}
|
|
2634
2854
|
}
|
|
2635
|
-
const
|
|
2855
|
+
const ctx = {};
|
|
2856
|
+
if (opts.session) ctx.session = opts.session;
|
|
2857
|
+
if (opts.functions && typeof opts.functions === 'object') ctx.functions = opts.functions;
|
|
2858
|
+
if (opts.aggregates && typeof opts.aggregates === 'object') ctx.aggregates = opts.aggregates;
|
|
2859
|
+
const executor = new SQLExecutor(engine, Object.keys(ctx).length > 0 ? ctx : null);
|
|
2636
2860
|
const results = [];
|
|
2637
2861
|
for (const stmtSql of statements) {
|
|
2638
2862
|
const stmt = parseSQL(stmtSql);
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// © Vexify 2026 All Rights Reserved.
|
|
2
|
+
/**
|
|
3
|
+
* better-sqlite3 兼容层 — 主线程
|
|
4
|
+
*
|
|
5
|
+
* 提供与 better-sqlite3 完全一致的同步 API:
|
|
6
|
+
* const Database = require('jsql-neo/sqlite');
|
|
7
|
+
* const db = new Database('file.db');
|
|
8
|
+
* const row = db.prepare('SELECT * FROM users WHERE id = ?').get(1);
|
|
9
|
+
*
|
|
10
|
+
* 底层通过 worker_threads + SharedArrayBuffer(Atomics) 同步桥调用 JSQL 引擎。
|
|
11
|
+
* 每个操作:主线程 postMessage 请求 → Atomics.wait 阻塞 → worker 完成后
|
|
12
|
+
* postMessage 结果 + Atomics.notify → 主线程 receiveMessageOnPort 同步取回。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { Worker, MessageChannel, receiveMessageOnPort } = require('worker_threads');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const WORKER_PATH = path.join(__dirname, 'sqlite_worker.js');
|
|
19
|
+
|
|
20
|
+
class SqliteError extends Error {
|
|
21
|
+
constructor(message, code) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'SqliteError';
|
|
24
|
+
this.code = code || 'SQLITE_ERROR';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class Database {
|
|
29
|
+
constructor(filename = ':memory:', options = {}) {
|
|
30
|
+
if (filename !== null && filename !== undefined && typeof filename !== 'string') {
|
|
31
|
+
throw new TypeError('The first argument must be a string or undefined');
|
|
32
|
+
}
|
|
33
|
+
this.name = filename || ':memory:';
|
|
34
|
+
this.memory = this.name === ':memory:';
|
|
35
|
+
this.readonly = !!options.readonly;
|
|
36
|
+
this.open = true;
|
|
37
|
+
this._options = options;
|
|
38
|
+
this._sab = new SharedArrayBuffer(4);
|
|
39
|
+
this._ctrl = new Int32Array(this._sab);
|
|
40
|
+
this._seq = 0;
|
|
41
|
+
const channel = new MessageChannel();
|
|
42
|
+
this._port = channel.port2;
|
|
43
|
+
this._worker = new Worker(WORKER_PATH, {
|
|
44
|
+
workerData: { filename: this.name, options, port: channel.port1 },
|
|
45
|
+
transferList: [channel.port1],
|
|
46
|
+
});
|
|
47
|
+
this._sync('start', [this.name, this._options]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_sync(op, args) {
|
|
51
|
+
if (!this.open && op !== 'close') throw new SqliteError('The database connection is not open', 'SQLITE_MISUSE');
|
|
52
|
+
const id = ++this._seq;
|
|
53
|
+
// 预先清空控制槽,避免读到旧值
|
|
54
|
+
Atomics.store(this._ctrl, 0, 0);
|
|
55
|
+
this._worker.postMessage({ id, op, args, sab: this._sab });
|
|
56
|
+
// 阻塞直到 worker 完成(ctrl[0] === id)
|
|
57
|
+
const wait = Atomics.wait(this._ctrl, 0, 0);
|
|
58
|
+
if (wait === 'not-equal') {
|
|
59
|
+
// 已经完成,直接取
|
|
60
|
+
} else if (wait === 'timed-out' || wait === 'not-equal') {
|
|
61
|
+
// 重新等待
|
|
62
|
+
}
|
|
63
|
+
// 取结果(同步从端口队列读取)
|
|
64
|
+
let result;
|
|
65
|
+
let guard = 0;
|
|
66
|
+
while (true) {
|
|
67
|
+
const msg = receiveMessageOnPort(this._port);
|
|
68
|
+
if (msg && msg.message && msg.message.id === id) {
|
|
69
|
+
result = msg.message;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
// 消息可能尚未到达(竞态),再等一次
|
|
73
|
+
const w2 = Atomics.wait(this._ctrl, 0, id - 1);
|
|
74
|
+
if (w2 !== 'ok' && ++guard > 1000) break;
|
|
75
|
+
}
|
|
76
|
+
if (!result) throw new SqliteError('Worker did not respond', 'SQLITE_INTERNAL');
|
|
77
|
+
if (!result.ok) throw new SqliteError(result.error.message, result.error.code);
|
|
78
|
+
return result.result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
close() {
|
|
82
|
+
if (!this.open) return;
|
|
83
|
+
try {
|
|
84
|
+
this._sync('close', []);
|
|
85
|
+
} finally {
|
|
86
|
+
this.open = false;
|
|
87
|
+
this._worker.terminate();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
prepare(sql) {
|
|
92
|
+
if (typeof sql !== 'string') throw new TypeError('The "sql" argument must be a string');
|
|
93
|
+
return new Statement(this, sql);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
exec(sql) {
|
|
97
|
+
if (typeof sql !== 'string') throw new TypeError('The "sql" argument must be a string');
|
|
98
|
+
this._sync('exec', [sql, null]);
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
pragma(pragma, options = {}) {
|
|
103
|
+
if (typeof pragma !== 'string') throw new TypeError('The "pragma" argument must be a string');
|
|
104
|
+
const { simple } = options;
|
|
105
|
+
const sql = /^\s*pragma\b/i.test(pragma) ? pragma : 'PRAGMA ' + pragma;
|
|
106
|
+
const name = (pragma.replace(/^\s*pragma\b\s*/i, '').split(/[=(]/)[0] || '').trim();
|
|
107
|
+
const r = this._sync('all', [sql, null]);
|
|
108
|
+
if (simple) {
|
|
109
|
+
if (Array.isArray(r)) {
|
|
110
|
+
const first = r.length > 0 ? r[0] : null;
|
|
111
|
+
if (first && typeof first === 'object') {
|
|
112
|
+
const keys = Object.keys(first);
|
|
113
|
+
return keys.length === 1 ? first[keys[0]] : first;
|
|
114
|
+
}
|
|
115
|
+
return first;
|
|
116
|
+
}
|
|
117
|
+
return r;
|
|
118
|
+
}
|
|
119
|
+
if (!Array.isArray(r)) return [{ [name || 'value']: r }];
|
|
120
|
+
if (r.length > 0 && (typeof r[0] !== 'object' || r[0] === null)) {
|
|
121
|
+
return r.map(v => ({ [name || 'value']: v }));
|
|
122
|
+
}
|
|
123
|
+
return r;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
transaction(fn) {
|
|
127
|
+
if (typeof fn !== 'function') throw new TypeError('Expected a function');
|
|
128
|
+
const wrapped = (...params) => {
|
|
129
|
+
if (!this.open) throw new SqliteError('The database connection is not open', 'SQLITE_MISUSE');
|
|
130
|
+
this.exec('BEGIN');
|
|
131
|
+
try {
|
|
132
|
+
const result = fn.apply(this, params);
|
|
133
|
+
if (result && typeof result.then === 'function') {
|
|
134
|
+
this.exec('ROLLBACK');
|
|
135
|
+
throw new SqliteError('Transaction functions cannot be asynchronous', 'SQLITE_MISUSE');
|
|
136
|
+
}
|
|
137
|
+
this.exec('COMMIT');
|
|
138
|
+
return result;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
try { this.exec('ROLLBACK'); } catch (e) {}
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
wrapped.deferred = () => wrapped;
|
|
145
|
+
wrapped.immediate = () => wrapped;
|
|
146
|
+
wrapped.exclusive = () => wrapped;
|
|
147
|
+
wrapped.default = wrapped;
|
|
148
|
+
return wrapped;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function(name, options, fn) {
|
|
152
|
+
if (typeof options === 'function') { fn = options; options = {}; }
|
|
153
|
+
if (typeof fn !== 'function') throw new TypeError('Expected a function');
|
|
154
|
+
const fnStr = fn.toString();
|
|
155
|
+
if (/\[native code\]/.test(fnStr)) {
|
|
156
|
+
throw new SqliteError(`Cannot register native function '${name}'. Only source-available functions work over the worker bridge.`, 'SQLITE_FEATURE_NOT_SUPPORTED');
|
|
157
|
+
}
|
|
158
|
+
this._sync('registerFunction', [name, fnStr, !!(options && options.deterministic)]);
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
aggregate(name, options, factory) {
|
|
163
|
+
if (typeof options === 'function') { factory = options; options = {}; }
|
|
164
|
+
if (typeof options === 'object' && options !== null && factory === undefined) { factory = options; options = {}; }
|
|
165
|
+
if (typeof factory !== 'function' && (typeof factory !== 'object' || factory === null)) {
|
|
166
|
+
throw new TypeError('Expected a function or aggregate object');
|
|
167
|
+
}
|
|
168
|
+
// 单函数形式: (values) => result;对象形式: { start, step, result }
|
|
169
|
+
let specStr;
|
|
170
|
+
if (typeof factory === 'function') {
|
|
171
|
+
specStr = factory.toString();
|
|
172
|
+
if (/\[native code\]/.test(specStr)) {
|
|
173
|
+
throw new SqliteError(`Cannot register native aggregate '${name}'`, 'SQLITE_FEATURE_NOT_SUPPORTED');
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
const parts = [];
|
|
177
|
+
for (const key of ['start', 'step', 'result']) {
|
|
178
|
+
const f = factory[key];
|
|
179
|
+
if (typeof f === 'function') parts.push(`${key}: ${f.toString()}`);
|
|
180
|
+
}
|
|
181
|
+
specStr = '{ ' + parts.join(', ') + ' }';
|
|
182
|
+
}
|
|
183
|
+
this._sync('registerAggregate', [name, specStr, !!(options && options.deterministic)]);
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
table(name, options, fn) {
|
|
188
|
+
throw new SqliteError('db.table() is not supported by the worker bridge', 'SQLITE_FEATURE_NOT_SUPPORTED');
|
|
189
|
+
}
|
|
190
|
+
serialize() {
|
|
191
|
+
return Buffer.from(this._sync('serialize', []));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
deserialize(buffer) {
|
|
195
|
+
const buf = Buffer.from(buffer);
|
|
196
|
+
this._sync('deserialize', [buf]);
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
loadExtension() {
|
|
201
|
+
throw new SqliteError('db.loadExtension() is not supported: JSQL-NEO does not support loading native SQLite extensions', 'SQLITE_FEATURE_NOT_SUPPORTED');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
backup(destination, options) {
|
|
205
|
+
const dump = this._sync('backup', []);
|
|
206
|
+
const target = new Database(destination, {});
|
|
207
|
+
try {
|
|
208
|
+
target.deserialize(dump);
|
|
209
|
+
return Promise.resolve({ totalPages: 1, remainingPages: 0, idle: true });
|
|
210
|
+
} catch (e) {
|
|
211
|
+
target.close();
|
|
212
|
+
throw e;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
unsafeMode() {
|
|
217
|
+
this._unsafeMode = true;
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
get defaultSafeIntegers() { return false; }
|
|
222
|
+
set defaultSafeIntegers(v) {}
|
|
223
|
+
|
|
224
|
+
get db() { return this; }
|
|
225
|
+
|
|
226
|
+
[Symbol.for('nodejs.util.inspect.custom')]() {
|
|
227
|
+
return `Database { name: '${this.name}', open: ${this.open} }`;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
class Statement {
|
|
232
|
+
constructor(database, sql) {
|
|
233
|
+
this.database = database;
|
|
234
|
+
this._src = sql;
|
|
235
|
+
this._params = [];
|
|
236
|
+
this._rawMode = false;
|
|
237
|
+
this._pluck = false;
|
|
238
|
+
this._safeIntegers = false;
|
|
239
|
+
const trimmed = sql.trim().toLowerCase();
|
|
240
|
+
this._type = trimmed.startsWith('select') || trimmed.startsWith('pragma') || trimmed.startsWith('with') ? 'select'
|
|
241
|
+
: trimmed.startsWith('insert') ? 'insert'
|
|
242
|
+
: trimmed.startsWith('update') ? 'update'
|
|
243
|
+
: trimmed.startsWith('delete') ? 'delete'
|
|
244
|
+
: trimmed.startsWith('create') || trimmed.startsWith('drop') || trimmed.startsWith('alter') ? 'ddl'
|
|
245
|
+
: 'other';
|
|
246
|
+
this._reader = this._type === 'select' || /^\s*(select|pragma|with)\b/i.test(sql);
|
|
247
|
+
this._tableName = null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
get reader() { return this._reader; }
|
|
251
|
+
get safeIntegers() { return this._safeIntegers; }
|
|
252
|
+
set safeIntegers(v) { this._safeIntegers = !!v; }
|
|
253
|
+
get source() { return this._src; }
|
|
254
|
+
|
|
255
|
+
bind(...params) {
|
|
256
|
+
if (params.length === 1 && Array.isArray(params[0])) params = params[0];
|
|
257
|
+
this._params = params;
|
|
258
|
+
return this;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
raw(enabled = true) {
|
|
262
|
+
this._rawMode = !!enabled;
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
pluck(enabled = true) {
|
|
267
|
+
this._pluck = !!enabled;
|
|
268
|
+
return this;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
expand() { return this; }
|
|
272
|
+
returning() { return this; }
|
|
273
|
+
|
|
274
|
+
_call(mode, params) {
|
|
275
|
+
let p = params;
|
|
276
|
+
if (p === undefined || p === null) p = this._params;
|
|
277
|
+
if (Array.isArray(p) && p.length === 1 && typeof p[0] === 'object' && p[0] !== null && !Array.isArray(p[0]) && !(p[0] instanceof Date) && !Buffer.isBuffer(p[0])) {
|
|
278
|
+
const hasNamed = /[@:$][A-Za-z_][A-Za-z0-9_]*/.test(this.source);
|
|
279
|
+
p = hasNamed ? p[0] : p;
|
|
280
|
+
} else if (!Array.isArray(p) && (typeof p !== 'object' || p === null)) {
|
|
281
|
+
p = [p];
|
|
282
|
+
}
|
|
283
|
+
return this.database._sync(mode, [this.source, p]);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
run(...params) {
|
|
287
|
+
const r = this._call('run', params.length > 0 ? params : undefined);
|
|
288
|
+
return r;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
get(...params) {
|
|
292
|
+
return this._call('get', params.length > 0 ? params : undefined);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
all(...params) {
|
|
296
|
+
const r = this._call('all', params.length > 0 ? params : undefined);
|
|
297
|
+
if (this._rawMode) return this._call('raw', params.length > 0 ? params : undefined);
|
|
298
|
+
if (this._pluck) {
|
|
299
|
+
const cols = r && r.length > 0 ? Object.keys(r[0]) : [];
|
|
300
|
+
return cols.length > 0 ? r.map(row => row[cols[0]]) : [];
|
|
301
|
+
}
|
|
302
|
+
return r;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
iterate(...params) {
|
|
306
|
+
const rows = this.all(...params);
|
|
307
|
+
let i = 0;
|
|
308
|
+
const it = {
|
|
309
|
+
next: () => i < rows.length ? { value: rows[i++], done: false } : { value: undefined, done: true },
|
|
310
|
+
[Symbol.iterator]: () => it,
|
|
311
|
+
};
|
|
312
|
+
return it;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
columns() {
|
|
316
|
+
return this._call('columns', []);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = Database;
|
|
321
|
+
module.exports.default = Database;
|
|
322
|
+
module.exports.Database = Database;
|
|
323
|
+
module.exports.SqliteError = SqliteError;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// © Vexify 2026 All Rights Reserved.
|
|
2
|
+
/**
|
|
3
|
+
* better-sqlite3 兼容层 — worker 线程
|
|
4
|
+
* 在 worker 中持有一个持久 Database,主线程通过 postMessage + Atomics 同步桥调用。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { workerData } = require('worker_threads');
|
|
8
|
+
const Database = require('./database');
|
|
9
|
+
const { executeSQL, parseSQL, applyParams } = require('./sql');
|
|
10
|
+
|
|
11
|
+
const PARENT = require('worker_threads').parentPort;
|
|
12
|
+
const PORT = workerData.port || PARENT;
|
|
13
|
+
let db = null;
|
|
14
|
+
let sab = null;
|
|
15
|
+
let ctrl = null;
|
|
16
|
+
const userFunctions = {};
|
|
17
|
+
const userAggregates = {};
|
|
18
|
+
|
|
19
|
+
function startEngine(filename, options) {
|
|
20
|
+
const opts = options || {};
|
|
21
|
+
const isMemory = filename === ':memory:' || !filename;
|
|
22
|
+
if (opts.engine === 'native') {
|
|
23
|
+
const { NativeJSQL } = require('./native_client');
|
|
24
|
+
const engine = new NativeJSQL({ mode: 'memory' });
|
|
25
|
+
db = {
|
|
26
|
+
start: async () => { await engine.start(); },
|
|
27
|
+
stop: async () => { await engine.stop(); },
|
|
28
|
+
hasTable: name => engine._tableNames.has(name),
|
|
29
|
+
createTable: (n, s) => engine.createTable(n, s),
|
|
30
|
+
dropTable: n => engine.dropTable(n),
|
|
31
|
+
insert: (n, rows) => engine.insert(n, rows),
|
|
32
|
+
find: (n, f, o) => engine.find(n, f, o),
|
|
33
|
+
updateByIds: (n, e) => engine.updateByIds(n, e),
|
|
34
|
+
removeByIds: (n, ids) => engine.removeByIds(n, ids),
|
|
35
|
+
count: (n, f) => engine.count ? engine.count(n, f) : engine.find(n, f || {}, {}).then(rs => rs.length),
|
|
36
|
+
getTableSchema: n => engine._schemas[n] || null,
|
|
37
|
+
truncate: async () => { throw new Error('truncate not supported on native engine'); },
|
|
38
|
+
flush: async () => { if (engine.flush) await engine.flush(); },
|
|
39
|
+
listTables: () => Array.from(engine._tableNames || []),
|
|
40
|
+
_schemas: engine._schemas,
|
|
41
|
+
};
|
|
42
|
+
return db;
|
|
43
|
+
}
|
|
44
|
+
// 纯 JS 引擎
|
|
45
|
+
db = new Database(isMemory ? ':memory:' : filename, opts.mode === 'hybrid' ? { mode: 'hybrid' } : {});
|
|
46
|
+
return db;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toRows(result) {
|
|
50
|
+
// 把 executeSQL 的 select 结果转成 better-sqlite3 风格的对象行数组
|
|
51
|
+
if (result && result.type === 'select' && Array.isArray(result.rows)) {
|
|
52
|
+
const cols = Array.isArray(result.columns) ? result.columns : [];
|
|
53
|
+
return result.rows.map(r => {
|
|
54
|
+
const o = {};
|
|
55
|
+
for (let i = 0; i < cols.length; i++) o[cols[i]] = r[i];
|
|
56
|
+
return o;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (result && result.type === 'pragma' && result.value !== undefined) {
|
|
60
|
+
return result.value;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let _lastInsertId = 0;
|
|
66
|
+
|
|
67
|
+
async function executeStatement(sql, params) {
|
|
68
|
+
if (params && typeof params === 'object' && !Array.isArray(params)) {
|
|
69
|
+
sql = applyParams(sql, params);
|
|
70
|
+
params = null;
|
|
71
|
+
}
|
|
72
|
+
const result = await executeSQL(db, sql, params, {
|
|
73
|
+
session: {
|
|
74
|
+
get lastInsertId() { return _lastInsertId; },
|
|
75
|
+
set lastInsertId(v) { _lastInsertId = Number(v); },
|
|
76
|
+
},
|
|
77
|
+
functions: userFunctions,
|
|
78
|
+
aggregates: userAggregates,
|
|
79
|
+
});
|
|
80
|
+
if (result && result.type === 'insert' && result.insertId !== null && result.insertId !== undefined) {
|
|
81
|
+
_lastInsertId = Number(result.insertId);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function cleanResult(r) {
|
|
87
|
+
if (!r || typeof r !== 'object') return r;
|
|
88
|
+
if (Array.isArray(r)) return r.map(cleanResult);
|
|
89
|
+
if (Buffer.isBuffer(r) || r instanceof Uint8Array || r instanceof ArrayBuffer || ArrayBuffer.isView(r)) return r;
|
|
90
|
+
const out = {};
|
|
91
|
+
for (const [k, v] of Object.entries(r)) {
|
|
92
|
+
if (k === 'result' || k === 'engine') continue;
|
|
93
|
+
if (v instanceof Date) out[k] = v.toISOString();
|
|
94
|
+
else if (v === undefined) out[k] = null;
|
|
95
|
+
else if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
|
|
96
|
+
// 仅保留可 JSON 化的值(防止 Table/内部对象循环引用)
|
|
97
|
+
try { JSON.stringify(v); out[k] = v; } catch (e) { out[k] = null; }
|
|
98
|
+
} else out[k] = v;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function handleOp(op, args) {
|
|
104
|
+
let result;
|
|
105
|
+
switch (op) {
|
|
106
|
+
case 'start': {
|
|
107
|
+
await startEngine(args[0], args[1]).start();
|
|
108
|
+
result = { ok: true };
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case 'close': {
|
|
112
|
+
if (db && typeof db.stop === 'function') await db.stop();
|
|
113
|
+
result = { ok: true };
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case 'exec': {
|
|
117
|
+
result = await executeStatement(args[0], args[1]);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case 'run': {
|
|
121
|
+
const r = await executeStatement(args[0], args[1]);
|
|
122
|
+
result = {
|
|
123
|
+
changes: r ? (r.affectedRows || 0) : 0,
|
|
124
|
+
lastInsertRowid: r && r.type === 'insert' ? Number(r.insertId || 0) : _lastInsertId,
|
|
125
|
+
};
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'get': {
|
|
129
|
+
const r = await executeStatement(args[0], args[1]);
|
|
130
|
+
const rows = toRows(r);
|
|
131
|
+
result = Array.isArray(rows) ? (rows.length > 0 ? rows[0] : undefined) : rows;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case 'all': {
|
|
135
|
+
const r = await executeStatement(args[0], args[1]);
|
|
136
|
+
const rows = toRows(r);
|
|
137
|
+
result = rows === undefined || rows === null ? [] : rows;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'raw': {
|
|
141
|
+
const r = await executeStatement(args[0], args[1]);
|
|
142
|
+
let rows = r && r.type === 'select' ? (r.rows || []) : (toRows(r) || []);
|
|
143
|
+
const cols = r && Array.isArray(r.columns) ? r.columns : (rows.length > 0 ? Object.keys(rows[0]) : []);
|
|
144
|
+
if (Array.isArray(rows) && rows.length > 0 && typeof rows[0] === 'object' && !Array.isArray(rows[0])) {
|
|
145
|
+
rows = rows.map(row => cols.map(c => row[c]));
|
|
146
|
+
}
|
|
147
|
+
result = rows;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case 'columns': {
|
|
151
|
+
const r = await executeStatement(args[0], args[1]);
|
|
152
|
+
const cols = r && r.type === 'select' && Array.isArray(r.columns) ? r.columns : [];
|
|
153
|
+
result = cols.map((name, i) => ({
|
|
154
|
+
name,
|
|
155
|
+
type: 'TEXT',
|
|
156
|
+
column: name,
|
|
157
|
+
table: r && r.table ? r.table : null,
|
|
158
|
+
database: null,
|
|
159
|
+
}));
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'parse': {
|
|
163
|
+
const stmt = parseSQL(args[0]);
|
|
164
|
+
result = { type: stmt.type, name: stmt.name || stmt.table || null };
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'hasTable': {
|
|
168
|
+
result = typeof db.hasTable === 'function' ? db.hasTable(args[0]) : false;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case 'registerFunction': {
|
|
172
|
+
const [name, fnStr, deterministic] = args;
|
|
173
|
+
// 用函数源码在 worker 中重建(无闭包依赖的纯函数)
|
|
174
|
+
try {
|
|
175
|
+
// eslint-disable-next-line no-new-func
|
|
176
|
+
const fn = new Function('return (' + fnStr + ')')();
|
|
177
|
+
userFunctions[String(name).toUpperCase()] = fn;
|
|
178
|
+
result = { ok: true };
|
|
179
|
+
} catch (e) {
|
|
180
|
+
throw new Error('Failed to register function ' + name + ': ' + e.message);
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case 'clearFunctions': {
|
|
185
|
+
for (const k of Object.keys(userFunctions)) delete userFunctions[k];
|
|
186
|
+
result = { ok: true };
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case 'registerAggregate': {
|
|
190
|
+
const [name, specStr, deterministic] = args;
|
|
191
|
+
try {
|
|
192
|
+
// eslint-disable-next-line no-new-func
|
|
193
|
+
const spec = new Function('return (' + specStr + ')')();
|
|
194
|
+
userAggregates[String(name).toUpperCase()] = spec;
|
|
195
|
+
result = { ok: true };
|
|
196
|
+
} catch (e) {
|
|
197
|
+
throw new Error('Failed to register aggregate ' + name + ': ' + e.message);
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case 'serialize': {
|
|
202
|
+
const { exportAllToJSON } = require('./migrate');
|
|
203
|
+
const tables = db.listTables ? db.listTables() : Array.from(db._tableNames || []);
|
|
204
|
+
const dump = await exportAllToJSON(db, tables);
|
|
205
|
+
result = Buffer.from(JSON.stringify(dump), 'utf8');
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
case 'deserialize': {
|
|
209
|
+
const { importFromJSON } = require('./migrate');
|
|
210
|
+
const dump = JSON.parse(Buffer.from(args[0]).toString('utf8'));
|
|
211
|
+
await importFromJSON(db, dump);
|
|
212
|
+
if (typeof db.save === 'function') db.save();
|
|
213
|
+
result = { ok: true };
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case 'backup': {
|
|
217
|
+
// 复制到另一个 sqlite 兼容库
|
|
218
|
+
const { exportAllToJSON } = require('./migrate');
|
|
219
|
+
const target = args[0];
|
|
220
|
+
const tables = db.listTables ? db.listTables() : Array.from(db._tableNames || []);
|
|
221
|
+
const dump = await exportAllToJSON(db, tables);
|
|
222
|
+
// 目标由主线程用另一个 Database 加载,此处返回 dump
|
|
223
|
+
result = Buffer.from(JSON.stringify(dump), 'utf8');
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case 'clearAggregates': {
|
|
227
|
+
for (const k of Object.keys(userAggregates)) delete userAggregates[k];
|
|
228
|
+
result = { ok: true };
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
default:
|
|
232
|
+
throw new Error(`Unknown op: ${op}`);
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
PARENT.on('message', async (msg) => {
|
|
238
|
+
const { id, op, args, sab: sabArr } = msg;
|
|
239
|
+
if (sabArr) {
|
|
240
|
+
sab = sabArr;
|
|
241
|
+
ctrl = new Int32Array(sab);
|
|
242
|
+
}
|
|
243
|
+
let payload;
|
|
244
|
+
try {
|
|
245
|
+
payload = { ok: true, result: cleanResult(await handleOp(op, args)) };
|
|
246
|
+
} catch (e) {
|
|
247
|
+
payload = { ok: false, error: { message: e.message, code: e.code || 'SQLITE_ERROR' } };
|
|
248
|
+
}
|
|
249
|
+
PORT.postMessage({ id, ...payload });
|
|
250
|
+
if (ctrl) {
|
|
251
|
+
Atomics.store(ctrl, 0, id);
|
|
252
|
+
Atomics.notify(ctrl, 0);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
PARENT.on('close', async () => {
|
|
257
|
+
try {
|
|
258
|
+
if (db && typeof db.stop === 'function') await db.stop();
|
|
259
|
+
} catch (e) { /* ignore */ }
|
|
260
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "JSQL-NEO — 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",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"./wasm/*": "./wasm/*",
|
|
25
25
|
"./lib/*.js": "./lib/*.js",
|
|
26
26
|
"./lib/*": "./lib/*.js",
|
|
27
|
-
"./package.json": "./package.json"
|
|
27
|
+
"./package.json": "./package.json",
|
|
28
|
+
"./sqlite": "./lib/sqlite_compat.js"
|
|
28
29
|
},
|
|
29
30
|
"bin": {
|
|
30
31
|
"jsql": "./bin/jsql"
|