jsql-neo 3.5.2 → 4.0.0
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/LICENSE +202 -0
- package/bin/jsql +97 -0
- package/index.js +20 -0
- package/lib/database.js +484 -28
- package/lib/mod.js +316 -0
- package/lib/mysql_compat.js +232 -0
- package/lib/mysql_server.js +514 -0
- package/lib/native_client.js +470 -0
- package/lib/nedb_compat.js +506 -0
- package/lib/plugin.js +35 -0
- package/lib/sql.js +1160 -0
- package/lib/table.js +42 -21
- package/lib/wasm_client.js +176 -8
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.toml +24 -0
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +449 -0
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +147 -0
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +41 -0
- package/nativesrc/jsql-neo-core/src/engine/table.rs +664 -0
- package/nativesrc/jsql-neo-core/src/lib.rs +3 -0
- package/nativesrc/jsql-neo-core/src/storage/mod.rs +1 -0
- package/nativesrc/jsql-neo-core/src/storage/persistent.rs +2 -0
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -0
- package/nativesrc/jsql-neo-core/src/types.rs +94 -0
- package/nativesrc/jsql-neo-native/Cargo.lock +606 -0
- package/nativesrc/jsql-neo-native/Cargo.toml +16 -0
- package/nativesrc/jsql-neo-native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-native/package.json +7 -0
- package/nativesrc/jsql-neo-native/src/lib.rs +281 -0
- package/package.json +9 -1
- package/wasm/jsql_neo_wasm.d.ts +8 -0
- package/wasm/jsql_neo_wasm.js +455 -6
- package/wasm/jsql_neo_wasm_bg.js +22 -0
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +4 -0
- package/wasm/package.json +1 -8
package/lib/sql.js
ADDED
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
const DANGEROUS_SQL = [
|
|
2
|
+
{ re: /^INTO$/, next: /^(OUTFILE|DUMPFILE)$/, name: 'INTO OUTFILE/DUMPFILE' },
|
|
3
|
+
{ re: /^LOAD$/, next: /^(FILE|DATA)$/, name: 'LOAD_FILE/LOAD DATA' },
|
|
4
|
+
{ re: /^SLEEP$/, name: 'SLEEP' },
|
|
5
|
+
{ re: /^BENCHMARK$/, name: 'BENCHMARK' },
|
|
6
|
+
{ re: /^GET_LOCK$/, name: 'GET_LOCK' },
|
|
7
|
+
{ re: /^RELEASE_LOCK$/, name: 'RELEASE_LOCK' },
|
|
8
|
+
{ re: /^SONAME$/, name: 'UDF SONAME' },
|
|
9
|
+
{ re: /^SYSEXEC$|^SYS_EXEC$/, name: 'sys_exec' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function findDangerousSQL(tokens) {
|
|
13
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
14
|
+
const t = tokens[i];
|
|
15
|
+
if (t.type !== 'keyword' && t.type !== 'ident') continue;
|
|
16
|
+
const upper = String(t.value).toUpperCase();
|
|
17
|
+
for (const d of DANGEROUS_SQL) {
|
|
18
|
+
if (d.re.test(upper)) {
|
|
19
|
+
if (d.next) {
|
|
20
|
+
const nxt = tokens[i + 1];
|
|
21
|
+
if (nxt && nxt.type === 'keyword' && d.next.test(String(nxt.value).toUpperCase())) {
|
|
22
|
+
return d.name;
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
return d.name;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class SQLToken {
|
|
34
|
+
constructor(type, value, pos) {
|
|
35
|
+
this.type = type; // 'keyword' | 'ident' | 'number' | 'string' | 'op' | 'eof'
|
|
36
|
+
this.value = value;
|
|
37
|
+
this.pos = pos;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const KEYWORDS = new Set([
|
|
42
|
+
'CREATE', 'TABLE', 'DROP', 'INSERT', 'INTO', 'VALUES', 'SELECT', 'FROM',
|
|
43
|
+
'WHERE', 'UPDATE', 'SET', 'DELETE', 'AND', 'OR', 'NOT', 'NULL', 'IS',
|
|
44
|
+
'LIKE', 'IN', 'LIMIT', 'OFFSET', 'ORDER', 'BY', 'ASC', 'DESC', 'PRIMARY',
|
|
45
|
+
'KEY', 'AUTO_INCREMENT', 'INTEGER', 'INT', 'BIGINT', 'STRING', 'TEXT',
|
|
46
|
+
'FLOAT', 'DOUBLE', 'REAL', 'BOOLEAN', 'BOOL', 'DATE', 'DATETIME',
|
|
47
|
+
'TIMESTAMP', 'ANY', 'OBJECT', 'ARRAY', 'BEGIN', 'COMMIT', 'ROLLBACK',
|
|
48
|
+
'TRANSACTION', 'WORK', 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'AS', 'UNIQUE',
|
|
49
|
+
'NOTNULL', 'DEFAULT', 'IF', 'EXISTS', 'DISTINCT', 'SHOW', 'USE', 'TABLES',
|
|
50
|
+
'DATABASES', 'DESCRIBE', 'DESC', 'ON', 'DUPLICATE'
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
function tokenize(sql) {
|
|
54
|
+
const tokens = [];
|
|
55
|
+
let i = 0;
|
|
56
|
+
const n = sql.length;
|
|
57
|
+
|
|
58
|
+
while (i < n) {
|
|
59
|
+
const c = sql[i];
|
|
60
|
+
|
|
61
|
+
if (c === ' ' || c === '\t' || c === '\n' || c === '\r') { i++; continue; }
|
|
62
|
+
|
|
63
|
+
if (c === '-' && sql[i + 1] === '-') {
|
|
64
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (c === '#' || (c === '/' && sql[i + 1] === '*')) {
|
|
68
|
+
if (c === '#') { while (i < n && sql[i] !== '\n') i++; continue; }
|
|
69
|
+
i += 2;
|
|
70
|
+
while (i + 1 < n && !(sql[i] === '*' && sql[i + 1] === '/')) i++;
|
|
71
|
+
i += 2;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (c === "'" || c === '"') {
|
|
76
|
+
const quote = c;
|
|
77
|
+
let j = i + 1;
|
|
78
|
+
let str = '';
|
|
79
|
+
while (j < n) {
|
|
80
|
+
if (sql[j] === '\\' && j + 1 < n) {
|
|
81
|
+
const esc = sql[j + 1];
|
|
82
|
+
const map = { n: '\n', t: '\t', r: '\r', '0': '\0', "'": "'", '"': '"', '\\': '\\' };
|
|
83
|
+
str += map[esc] !== undefined ? map[esc] : esc;
|
|
84
|
+
j += 2;
|
|
85
|
+
} else if (sql[j] === quote) {
|
|
86
|
+
break;
|
|
87
|
+
} else {
|
|
88
|
+
str += sql[j];
|
|
89
|
+
j++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
tokens.push(new SQLToken('string', str, i));
|
|
93
|
+
i = j + 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(sql[i + 1] || ''))) {
|
|
98
|
+
let j = i;
|
|
99
|
+
let isFloat = false;
|
|
100
|
+
while (j < n && /[0-9.]/.test(sql[j])) {
|
|
101
|
+
if (sql[j] === '.') isFloat = true;
|
|
102
|
+
j++;
|
|
103
|
+
}
|
|
104
|
+
if (sql[j] === 'e' || sql[j] === 'E') {
|
|
105
|
+
j++;
|
|
106
|
+
if (sql[j] === '+' || sql[j] === '-') j++;
|
|
107
|
+
while (j < n && /[0-9]/.test(sql[j])) j++;
|
|
108
|
+
isFloat = true;
|
|
109
|
+
}
|
|
110
|
+
const raw = sql.slice(i, j);
|
|
111
|
+
tokens.push(new SQLToken('number', isFloat ? parseFloat(raw) : parseInt(raw, 10), i));
|
|
112
|
+
i = j;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (/[a-zA-Z_$]/.test(c)) {
|
|
117
|
+
let j = i;
|
|
118
|
+
while (j < n && /[a-zA-Z0-9_$]/.test(sql[j])) j++;
|
|
119
|
+
const word = sql.slice(i, j);
|
|
120
|
+
const upper = word.toUpperCase();
|
|
121
|
+
tokens.push(new SQLToken(KEYWORDS.has(upper) ? 'keyword' : 'ident', upper === word ? word : word, i));
|
|
122
|
+
if (KEYWORDS.has(upper)) {
|
|
123
|
+
tokens[tokens.length - 1].value = upper;
|
|
124
|
+
tokens[tokens.length - 1].isKeyword = true;
|
|
125
|
+
}
|
|
126
|
+
i = j;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (c === '`') {
|
|
131
|
+
let j = i + 1;
|
|
132
|
+
while (j < n && sql[j] !== '`') j++;
|
|
133
|
+
tokens.push(new SQLToken('ident', sql.slice(i + 1, j), i));
|
|
134
|
+
i = j + 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const two = sql.slice(i, i + 2);
|
|
139
|
+
if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==') {
|
|
140
|
+
tokens.push(new SQLToken('op', two, i));
|
|
141
|
+
i += 2;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if ('=<>+-*/(),.;'.includes(c)) {
|
|
146
|
+
tokens.push(new SQLToken('op', c, i));
|
|
147
|
+
i++;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw new Error(`Unexpected character '${c}' at position ${i}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
tokens.push(new SQLToken('eof', null, n));
|
|
155
|
+
return tokens;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
class Parser {
|
|
159
|
+
constructor(tokens) {
|
|
160
|
+
this.tokens = tokens;
|
|
161
|
+
this.pos = 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
peek(offset = 0) { return this.tokens[this.pos + offset]; }
|
|
165
|
+
|
|
166
|
+
next() { return this.tokens[this.pos++]; }
|
|
167
|
+
|
|
168
|
+
expect(type, value) {
|
|
169
|
+
const t = this.next();
|
|
170
|
+
if (t.type !== type || (value !== undefined && t.value !== value)) {
|
|
171
|
+
throw new Error(`Expected ${value || type} but got '${t.value}' at position ${t.pos}`);
|
|
172
|
+
}
|
|
173
|
+
return t;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
expectKeyword(kw) {
|
|
177
|
+
const t = this.next();
|
|
178
|
+
if (t.type !== 'keyword' || t.value !== kw) {
|
|
179
|
+
throw new Error(`Expected ${kw} but got '${t.value}' at position ${t.pos}`);
|
|
180
|
+
}
|
|
181
|
+
return t;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
isKeyword(kw, offset = 0) {
|
|
185
|
+
const t = this.peek(offset);
|
|
186
|
+
return t.type === 'keyword' && t.value === kw;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
parseStatement() {
|
|
190
|
+
const t = this.peek();
|
|
191
|
+
if (t.type === 'eof') return null;
|
|
192
|
+
if (t.type !== 'keyword') throw new Error(`Expected SQL statement, got '${t.value}'`);
|
|
193
|
+
|
|
194
|
+
switch (t.value) {
|
|
195
|
+
case 'CREATE': return this.parseCreateTable();
|
|
196
|
+
case 'DROP': return this.parseDropTable();
|
|
197
|
+
case 'INSERT': return this.parseInsert();
|
|
198
|
+
case 'SELECT': return this.parseSelect();
|
|
199
|
+
case 'UPDATE': return this.parseUpdate();
|
|
200
|
+
case 'DELETE': return this.parseDelete();
|
|
201
|
+
case 'BEGIN': this.expectKeyword('BEGIN'); this.optionalTransaction(); return { type: 'begin' };
|
|
202
|
+
case 'COMMIT': this.expectKeyword('COMMIT'); this.optionalTransaction(); return { type: 'commit' };
|
|
203
|
+
case 'ROLLBACK': this.expectKeyword('ROLLBACK'); this.optionalTransaction(); return { type: 'rollback' };
|
|
204
|
+
case 'SHOW': return this.parseShow();
|
|
205
|
+
case 'DESCRIBE': case 'DESC': return this.parseDescribe();
|
|
206
|
+
case 'USE': return this.parseUse();
|
|
207
|
+
default: throw new Error(`Unsupported statement: ${t.value}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
optionalTransaction() {
|
|
212
|
+
if (this.isKeyword('TRANSACTION')) this.next();
|
|
213
|
+
if (this.isKeyword('WORK')) this.next();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
parseTableName() {
|
|
217
|
+
const t = this.next();
|
|
218
|
+
if (t.type !== 'ident') throw new Error(`Expected table name, got '${t.value}'`);
|
|
219
|
+
return t.value;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
parseCreateTable() {
|
|
223
|
+
this.expectKeyword('CREATE');
|
|
224
|
+
this.expectKeyword('TABLE');
|
|
225
|
+
let ifNotExists = false;
|
|
226
|
+
if (this.isKeyword('IF')) {
|
|
227
|
+
this.expectKeyword('IF'); this.expectKeyword('NOT'); this.expectKeyword('EXISTS');
|
|
228
|
+
ifNotExists = true;
|
|
229
|
+
}
|
|
230
|
+
const name = this.parseTableName();
|
|
231
|
+
this.expect('op', '(');
|
|
232
|
+
|
|
233
|
+
const schema = {};
|
|
234
|
+
let hasPk = false;
|
|
235
|
+
while (true) {
|
|
236
|
+
const t = this.peek();
|
|
237
|
+
if (t.type === 'keyword' && (t.value === 'PRIMARY' || t.value === 'UNIQUE')) {
|
|
238
|
+
if (t.value === 'PRIMARY') {
|
|
239
|
+
this.expectKeyword('PRIMARY'); this.expectKeyword('KEY');
|
|
240
|
+
this.expect('op', '(');
|
|
241
|
+
const pkCol = this.parseTableName();
|
|
242
|
+
this.expect('op', ')');
|
|
243
|
+
if (schema[pkCol]) schema[pkCol].primaryKey = true;
|
|
244
|
+
hasPk = true;
|
|
245
|
+
} else {
|
|
246
|
+
this.expectKeyword('UNIQUE');
|
|
247
|
+
this.expect('op', '(');
|
|
248
|
+
const uCol = this.parseTableName();
|
|
249
|
+
this.expect('op', ')');
|
|
250
|
+
if (schema[uCol]) schema[uCol].unique = true;
|
|
251
|
+
}
|
|
252
|
+
} else if (t.type === 'keyword' && t.value === 'CONSTRAINT') {
|
|
253
|
+
this.expectKeyword('CONSTRAINT');
|
|
254
|
+
this.next();
|
|
255
|
+
} else if (t.type === 'eof' || (t.type === 'op' && t.value === ')')) {
|
|
256
|
+
break;
|
|
257
|
+
} else {
|
|
258
|
+
const col = this.parseTableName();
|
|
259
|
+
const def = this.parseColumnDef();
|
|
260
|
+
schema[col] = def;
|
|
261
|
+
if (def.primaryKey) hasPk = true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const sep = this.peek();
|
|
265
|
+
if (sep.type === 'op' && sep.value === ',') { this.next(); continue; }
|
|
266
|
+
if (sep.type === 'op' && sep.value === ')') break;
|
|
267
|
+
throw new Error(`Expected ',' or ')' in CREATE TABLE, got '${sep.value}'`);
|
|
268
|
+
}
|
|
269
|
+
this.expect('op', ')');
|
|
270
|
+
this.optionalTailSemicolon();
|
|
271
|
+
|
|
272
|
+
if (!hasPk && schema.id === undefined) {
|
|
273
|
+
schema.id = { type: 'integer', primaryKey: true, autoIncrement: true };
|
|
274
|
+
}
|
|
275
|
+
return { type: 'createTable', name, schema, ifNotExists };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
parseColumnDef() {
|
|
279
|
+
const def = {};
|
|
280
|
+
const typeTok = this.next();
|
|
281
|
+
if (typeTok.type !== 'keyword') throw new Error(`Expected column type, got '${typeTok.value}'`);
|
|
282
|
+
const type = typeTok.value.toLowerCase();
|
|
283
|
+
const typeMap = {
|
|
284
|
+
integer: 'integer', int: 'integer', bigint: 'integer', tinyint: 'integer', smallint: 'integer',
|
|
285
|
+
string: 'string', text: 'string', varchar: 'string', char: 'string',
|
|
286
|
+
float: 'number', double: 'number', real: 'number', numeric: 'number', decimal: 'number',
|
|
287
|
+
boolean: 'boolean', bool: 'boolean',
|
|
288
|
+
date: 'date', datetime: 'datetime', timestamp: 'timestamp',
|
|
289
|
+
any: 'any', object: 'object', json: 'object', array: 'array'
|
|
290
|
+
};
|
|
291
|
+
const mapped = typeMap[type];
|
|
292
|
+
if (!mapped) throw new Error(`Unsupported column type: ${typeTok.value}`);
|
|
293
|
+
def.type = mapped;
|
|
294
|
+
|
|
295
|
+
while (true) {
|
|
296
|
+
const t = this.peek();
|
|
297
|
+
if (t.type === 'keyword') {
|
|
298
|
+
switch (t.value) {
|
|
299
|
+
case 'PRIMARY':
|
|
300
|
+
this.next(); this.expectKeyword('KEY'); def.primaryKey = true; def.unique = true; break;
|
|
301
|
+
case 'KEY':
|
|
302
|
+
this.next(); def.primaryKey = true; def.unique = true; break;
|
|
303
|
+
case 'AUTO_INCREMENT':
|
|
304
|
+
this.next(); def.autoIncrement = true; break;
|
|
305
|
+
case 'UNIQUE':
|
|
306
|
+
this.next(); def.unique = true; break;
|
|
307
|
+
case 'NOT':
|
|
308
|
+
this.next(); this.expectKeyword('NULL'); def.required = true; break;
|
|
309
|
+
case 'NULL':
|
|
310
|
+
this.next(); break;
|
|
311
|
+
case 'DEFAULT':
|
|
312
|
+
this.next(); def.default = this.parseValue(); break;
|
|
313
|
+
default:
|
|
314
|
+
return def;
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
return def;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
parseInsert() {
|
|
323
|
+
this.expectKeyword('INSERT');
|
|
324
|
+
this.expectKeyword('INTO');
|
|
325
|
+
const name = this.parseTableName();
|
|
326
|
+
let columns = null;
|
|
327
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
328
|
+
this.next();
|
|
329
|
+
columns = [];
|
|
330
|
+
while (true) {
|
|
331
|
+
columns.push(this.parseTableName());
|
|
332
|
+
if (this.peek().value === ',') { this.next(); continue; }
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
this.expect('op', ')');
|
|
336
|
+
}
|
|
337
|
+
this.expectKeyword('VALUES');
|
|
338
|
+
const rows = [];
|
|
339
|
+
while (true) {
|
|
340
|
+
this.expect('op', '(');
|
|
341
|
+
const values = [];
|
|
342
|
+
while (true) {
|
|
343
|
+
values.push(this.parseValue());
|
|
344
|
+
if (this.peek().value === ',') { this.next(); continue; }
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
this.expect('op', ')');
|
|
348
|
+
rows.push(values);
|
|
349
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
this.optionalTailSemicolon();
|
|
353
|
+
|
|
354
|
+
const dataRows = rows.map(vals => {
|
|
355
|
+
const row = {};
|
|
356
|
+
if (columns) {
|
|
357
|
+
columns.forEach((c, idx) => { row[c] = vals[idx] !== undefined ? vals[idx] : null; });
|
|
358
|
+
} else {
|
|
359
|
+
vals.forEach((v, idx) => { row['col' + (idx + 1)] = v; });
|
|
360
|
+
}
|
|
361
|
+
return row;
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
let onDuplicate = null;
|
|
365
|
+
if (this.isKeyword('ON')) {
|
|
366
|
+
this.expectKeyword('ON');
|
|
367
|
+
this.expectKeyword('DUPLICATE');
|
|
368
|
+
this.expectKeyword('KEY');
|
|
369
|
+
this.expectKeyword('UPDATE');
|
|
370
|
+
onDuplicate = [];
|
|
371
|
+
while (true) {
|
|
372
|
+
const col = this.parseTableName();
|
|
373
|
+
this.expect('op', '=');
|
|
374
|
+
const val = this.parseValue();
|
|
375
|
+
onDuplicate.push([col, val]);
|
|
376
|
+
if (this.peek().value === ',') { this.next(); continue; }
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
this.optionalTailSemicolon();
|
|
381
|
+
|
|
382
|
+
return { type: 'insert', name, columns, dataRows: columns ? dataRows : null, values: columns ? null : rows, onDuplicate };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
parseValue() {
|
|
386
|
+
const t = this.next();
|
|
387
|
+
if (t.type === 'number' || t.type === 'string') return t.value;
|
|
388
|
+
if (t.type === 'keyword' && t.value === 'NULL') return null;
|
|
389
|
+
if (t.type === 'op' && t.value === '-') {
|
|
390
|
+
const num = this.next();
|
|
391
|
+
if (num.type !== 'number') throw new Error('Expected number after -');
|
|
392
|
+
return -num.value;
|
|
393
|
+
}
|
|
394
|
+
if (t.type === 'op' && t.value === '+') {
|
|
395
|
+
const num = this.next();
|
|
396
|
+
if (num.type !== 'number') throw new Error('Expected number after +');
|
|
397
|
+
return num.value;
|
|
398
|
+
}
|
|
399
|
+
throw new Error(`Expected value, got '${t.value}'`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
parseSelect() {
|
|
403
|
+
this.expectKeyword('SELECT');
|
|
404
|
+
let distinct = false;
|
|
405
|
+
if (this.isKeyword('DISTINCT')) { this.next(); distinct = true; }
|
|
406
|
+
|
|
407
|
+
const columns = [];
|
|
408
|
+
let aggregate = null;
|
|
409
|
+
while (true) {
|
|
410
|
+
const t = this.peek();
|
|
411
|
+
if (t.type === 'keyword' && t.value === 'COUNT') {
|
|
412
|
+
this.next();
|
|
413
|
+
this.expect('op', '(');
|
|
414
|
+
this.expect('op', '*');
|
|
415
|
+
this.expect('op', ')');
|
|
416
|
+
aggregate = { type: 'COUNT' };
|
|
417
|
+
if (this.isKeyword('AS')) { this.next(); aggregate.alias = this.parseAlias(); }
|
|
418
|
+
columns.push({ expr: null, aggregate: true });
|
|
419
|
+
} else if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX'].includes(t.value)) {
|
|
420
|
+
this.next();
|
|
421
|
+
const fn = t.value;
|
|
422
|
+
this.expect('op', '(');
|
|
423
|
+
const col = this.parseTableName();
|
|
424
|
+
this.expect('op', ')');
|
|
425
|
+
aggregate = { type: fn, column: col };
|
|
426
|
+
if (this.isKeyword('AS')) { this.next(); aggregate.alias = this.parseAlias(); }
|
|
427
|
+
columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias });
|
|
428
|
+
} else if (t.type === 'op' && t.value === '*') {
|
|
429
|
+
this.next();
|
|
430
|
+
columns.push({ expr: '*' });
|
|
431
|
+
} else if (t.type === 'number' || t.type === 'string') {
|
|
432
|
+
this.next();
|
|
433
|
+
let alias = null;
|
|
434
|
+
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
435
|
+
columns.push({ expr: null, literal: t.value, alias });
|
|
436
|
+
} else {
|
|
437
|
+
const col = this.parseColumnRef();
|
|
438
|
+
let alias = null;
|
|
439
|
+
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
440
|
+
columns.push({ expr: col, alias });
|
|
441
|
+
}
|
|
442
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let table = null;
|
|
447
|
+
if (this.isKeyword('FROM')) {
|
|
448
|
+
this.next();
|
|
449
|
+
table = this.parseTableName();
|
|
450
|
+
}
|
|
451
|
+
let where = null;
|
|
452
|
+
if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
|
|
453
|
+
let orderBy = null;
|
|
454
|
+
if (this.isKeyword('ORDER')) {
|
|
455
|
+
this.expectKeyword('ORDER'); this.expectKeyword('BY');
|
|
456
|
+
orderBy = [];
|
|
457
|
+
while (true) {
|
|
458
|
+
const col = this.parseColumnRef();
|
|
459
|
+
let dir = 'asc';
|
|
460
|
+
if (this.isKeyword('ASC')) { this.next(); }
|
|
461
|
+
else if (this.isKeyword('DESC')) { this.next(); dir = 'desc'; }
|
|
462
|
+
orderBy.push({ column: col, dir });
|
|
463
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
let limit = null, offset = 0;
|
|
468
|
+
if (this.isKeyword('LIMIT')) {
|
|
469
|
+
this.next();
|
|
470
|
+
limit = this.parseValue();
|
|
471
|
+
if (this.isKeyword('OFFSET')) { this.next(); offset = this.parseValue(); }
|
|
472
|
+
else if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); offset = limit; limit = this.parseValue(); }
|
|
473
|
+
}
|
|
474
|
+
this.optionalTailSemicolon();
|
|
475
|
+
|
|
476
|
+
return { type: 'select', columns, aggregate, distinct, table, where, orderBy, limit, offset };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
parseAlias() {
|
|
480
|
+
const t = this.next();
|
|
481
|
+
if (t.type !== 'ident' && t.type !== 'keyword') throw new Error(`Expected alias, got '${t.value}'`);
|
|
482
|
+
return t.value;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
parseColumnRef() {
|
|
486
|
+
const t = this.next();
|
|
487
|
+
if (t.type !== 'ident') throw new Error(`Expected column name, got '${t.value}'`);
|
|
488
|
+
return t.value;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
parseUpdate() {
|
|
492
|
+
this.expectKeyword('UPDATE');
|
|
493
|
+
const table = this.parseTableName();
|
|
494
|
+
this.expectKeyword('SET');
|
|
495
|
+
const assignments = [];
|
|
496
|
+
while (true) {
|
|
497
|
+
const col = this.parseColumnRef();
|
|
498
|
+
this.expect('op', '=');
|
|
499
|
+
assignments.push([col, this.parseValue()]);
|
|
500
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
let where = null;
|
|
504
|
+
if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
|
|
505
|
+
this.optionalTailSemicolon();
|
|
506
|
+
return { type: 'update', table, assignments, where };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
parseDelete() {
|
|
510
|
+
this.expectKeyword('DELETE');
|
|
511
|
+
this.expectKeyword('FROM');
|
|
512
|
+
const table = this.parseTableName();
|
|
513
|
+
let where = null;
|
|
514
|
+
if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
|
|
515
|
+
this.optionalTailSemicolon();
|
|
516
|
+
return { type: 'delete', table, where };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
parseDropTable() {
|
|
520
|
+
this.expectKeyword('DROP');
|
|
521
|
+
this.expectKeyword('TABLE');
|
|
522
|
+
let ifExists = false;
|
|
523
|
+
if (this.isKeyword('IF')) {
|
|
524
|
+
this.expectKeyword('IF'); this.expectKeyword('EXISTS'); ifExists = true;
|
|
525
|
+
}
|
|
526
|
+
const table = this.parseTableName();
|
|
527
|
+
this.optionalTailSemicolon();
|
|
528
|
+
return { type: 'dropTable', table, ifExists };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
parseShow() {
|
|
532
|
+
this.expectKeyword('SHOW');
|
|
533
|
+
if (this.isKeyword('TABLES')) { this.next(); this.optionalTailSemicolon(); return { type: 'showTables' }; }
|
|
534
|
+
if (this.isKeyword('DATABASES')) { this.next(); this.optionalTailSemicolon(); return { type: 'showDatabases' }; }
|
|
535
|
+
throw new Error('Unsupported SHOW statement');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
parseDescribe() {
|
|
539
|
+
this.next();
|
|
540
|
+
const table = this.parseTableName();
|
|
541
|
+
this.optionalTailSemicolon();
|
|
542
|
+
return { type: 'describe', table };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
parseUse() {
|
|
546
|
+
this.expectKeyword('USE');
|
|
547
|
+
const db = this.parseTableName();
|
|
548
|
+
this.optionalTailSemicolon();
|
|
549
|
+
return { type: 'use', database: db };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
optionalTailSemicolon() {
|
|
553
|
+
if (this.peek().type === 'op' && this.peek().value === ';') this.next();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
parseExpr() {
|
|
557
|
+
return this.parseOr();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
parseOr() {
|
|
561
|
+
let left = this.parseAnd();
|
|
562
|
+
while (this.isKeyword('OR')) {
|
|
563
|
+
this.next();
|
|
564
|
+
const right = this.parseAnd();
|
|
565
|
+
left = { type: 'or', left, right };
|
|
566
|
+
}
|
|
567
|
+
return left;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
parseAnd() {
|
|
571
|
+
let left = this.parseNot();
|
|
572
|
+
while (this.isKeyword('AND')) {
|
|
573
|
+
this.next();
|
|
574
|
+
const right = this.parseNot();
|
|
575
|
+
left = { type: 'and', left, right };
|
|
576
|
+
}
|
|
577
|
+
return left;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
parseNot() {
|
|
581
|
+
if (this.isKeyword('NOT')) {
|
|
582
|
+
this.next();
|
|
583
|
+
return { type: 'not', expr: this.parseNot() };
|
|
584
|
+
}
|
|
585
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
586
|
+
this.next();
|
|
587
|
+
const e = this.parseExpr();
|
|
588
|
+
this.expect('op', ')');
|
|
589
|
+
return e;
|
|
590
|
+
}
|
|
591
|
+
return this.parseComparison();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
parseOperand() {
|
|
595
|
+
const t = this.next();
|
|
596
|
+
if (t.type === 'ident') return { type: 'column', name: t.value };
|
|
597
|
+
if (t.type === 'number' || t.type === 'string') return { type: 'value', value: t.value };
|
|
598
|
+
if (t.type === 'keyword' && t.value === 'NULL') return { type: 'value', value: null };
|
|
599
|
+
if (t.type === 'op' && (t.value === '-' || t.value === '+')) {
|
|
600
|
+
const num = this.next();
|
|
601
|
+
if (num.type !== 'number') throw new Error('Expected number after sign');
|
|
602
|
+
return { type: 'value', value: t.value === '-' ? -num.value : num.value };
|
|
603
|
+
}
|
|
604
|
+
throw new Error(`Expected value or column, got '${t.value}'`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
parseComparison() {
|
|
608
|
+
const left = this.parseOperand();
|
|
609
|
+
const t = this.peek();
|
|
610
|
+
|
|
611
|
+
if (t.type === 'keyword' && t.value === 'IS') {
|
|
612
|
+
this.next();
|
|
613
|
+
const not = this.isKeyword('NOT');
|
|
614
|
+
if (not) this.next();
|
|
615
|
+
this.expectKeyword('NULL');
|
|
616
|
+
return { type: 'isNull', operand: left, not: !!not };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (t.type === 'keyword' && t.value === 'IN') {
|
|
620
|
+
this.next();
|
|
621
|
+
this.expect('op', '(');
|
|
622
|
+
const list = [];
|
|
623
|
+
while (true) {
|
|
624
|
+
list.push(this.parseValue());
|
|
625
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
this.expect('op', ')');
|
|
629
|
+
return { type: 'in', operand: left, list };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (t.type === 'keyword' && t.value === 'LIKE') {
|
|
633
|
+
this.next();
|
|
634
|
+
const pattern = this.parseValue();
|
|
635
|
+
return { type: 'like', operand: left, pattern };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (t.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t.value)) {
|
|
639
|
+
this.next();
|
|
640
|
+
const right = this.parseOperand();
|
|
641
|
+
return { type: 'compare', op: t.value === '<>' ? '!=' : t.value, left, right };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
throw new Error(`Expected comparison operator, got '${t.value}'`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const OPERATORS = {
|
|
649
|
+
'=': (a, b) => a === b,
|
|
650
|
+
'==': (a, b) => a === b,
|
|
651
|
+
'!=': (a, b) => a !== b,
|
|
652
|
+
'<': (a, b) => a < b,
|
|
653
|
+
'<=': (a, b) => a <= b,
|
|
654
|
+
'>': (a, b) => a > b,
|
|
655
|
+
'>=': (a, b) => a >= b
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
function resolveOperand(operand, row) {
|
|
659
|
+
if (operand.type === 'value') return operand.value;
|
|
660
|
+
return row[operand.name];
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function likeMatch(value, pattern) {
|
|
664
|
+
if (typeof value !== 'string') return false;
|
|
665
|
+
const regex = pattern
|
|
666
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
667
|
+
.replace(/%/g, '.*')
|
|
668
|
+
.replace(/_/g, '.');
|
|
669
|
+
return new RegExp('^' + regex + '$', 'i').test(value);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function extractEqualPushdown(expr, schema) {
|
|
673
|
+
if (!expr || !schema) return null;
|
|
674
|
+
const filter = {};
|
|
675
|
+
const restParts = [];
|
|
676
|
+
const walk = (node) => {
|
|
677
|
+
if (!node) return;
|
|
678
|
+
if (node.type === 'and') { walk(node.left); walk(node.right); return; }
|
|
679
|
+
if (node.type === 'compare' && node.op === '=') {
|
|
680
|
+
const col = node.left && node.left.type === 'column' ? node.left.name : null;
|
|
681
|
+
const val = node.right && node.right.type === 'literal' ? node.right.value : undefined;
|
|
682
|
+
if (col && val !== undefined && val !== null && schema[col] && !(schema[col].primaryKey && schema[col].autoIncrement === false)) {
|
|
683
|
+
filter[col] = val;
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
restParts.push(node);
|
|
688
|
+
};
|
|
689
|
+
walk(expr);
|
|
690
|
+
if (Object.keys(filter).length === 0) return null;
|
|
691
|
+
let rest = null;
|
|
692
|
+
if (restParts.length === 1) rest = restParts[0];
|
|
693
|
+
else if (restParts.length > 1) rest = restParts.slice(1).reduce((a, b) => ({ type: 'and', left: a, right: b }), restParts[0]);
|
|
694
|
+
return { filter, rest };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function evaluateExpr(expr, row) {
|
|
698
|
+
if (expr === null || expr === undefined) return false;
|
|
699
|
+
switch (expr.type) {
|
|
700
|
+
case 'and': return evaluateExpr(expr.left, row) && evaluateExpr(expr.right, row);
|
|
701
|
+
case 'or': return evaluateExpr(expr.left, row) || evaluateExpr(expr.right, row);
|
|
702
|
+
case 'not': return !evaluateExpr(expr.expr, row);
|
|
703
|
+
case 'compare': {
|
|
704
|
+
const l = resolveOperand(expr.left, row);
|
|
705
|
+
const r = resolveOperand(expr.right, row);
|
|
706
|
+
if (expr.op === '=') return l === r || (l === null && r === null) || (l !== null && r !== null && String(l) === String(r));
|
|
707
|
+
if (l === null || r === null) return false;
|
|
708
|
+
const fn = OPERATORS[expr.op];
|
|
709
|
+
return typeof l === 'number' && typeof r === 'number' ? fn(l, r) : fn(String(l), String(r));
|
|
710
|
+
}
|
|
711
|
+
case 'isNull': {
|
|
712
|
+
const v = resolveOperand(expr.operand, row);
|
|
713
|
+
const isNull = v === null || v === undefined;
|
|
714
|
+
return expr.not ? !isNull : isNull;
|
|
715
|
+
}
|
|
716
|
+
case 'in': {
|
|
717
|
+
const v = resolveOperand(expr.operand, row);
|
|
718
|
+
return expr.list.some(x => x === v || String(x) === String(v));
|
|
719
|
+
}
|
|
720
|
+
case 'like': return likeMatch(resolveOperand(expr.operand, row), expr.pattern);
|
|
721
|
+
default: return false;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function normalizeRow(row, schema) {
|
|
726
|
+
if (row && typeof row === 'object' && row.fields && typeof row.fields === 'object') {
|
|
727
|
+
const flat = { ...row.fields };
|
|
728
|
+
if (schema) {
|
|
729
|
+
const pkCols = Object.keys(schema).filter(k => schema[k].primaryKey);
|
|
730
|
+
for (const c of pkCols) {
|
|
731
|
+
if ((flat[c] === undefined || flat[c] === null) && row.id !== undefined) flat[c] = row.id;
|
|
732
|
+
}
|
|
733
|
+
} else if (row.id !== undefined && flat.id === undefined) {
|
|
734
|
+
flat.id = row.id;
|
|
735
|
+
}
|
|
736
|
+
flat._rid = row.id;
|
|
737
|
+
return flat;
|
|
738
|
+
}
|
|
739
|
+
return row;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
class SQLExecutor {
|
|
743
|
+
constructor(engine) {
|
|
744
|
+
this.engine = engine;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async execute(statement) {
|
|
748
|
+
switch (statement.type) {
|
|
749
|
+
case 'createTable': {
|
|
750
|
+
const r = await this.engine.createTable(statement.name, statement.schema);
|
|
751
|
+
return { ok: true, type: 'createTable', table: statement.name, affectedRows: 0, result: r };
|
|
752
|
+
}
|
|
753
|
+
case 'dropTable': {
|
|
754
|
+
await this.engine.dropTable(statement.table);
|
|
755
|
+
return { ok: true, type: 'dropTable', table: statement.table, affectedRows: 0 };
|
|
756
|
+
}
|
|
757
|
+
case 'insert': {
|
|
758
|
+
let dataRows = statement.dataRows;
|
|
759
|
+
let schema = null;
|
|
760
|
+
if (dataRows === null && statement.values) {
|
|
761
|
+
schema = this.engine.getTableSchema
|
|
762
|
+
? await this.engine.getTableSchema(statement.name)
|
|
763
|
+
: (this.engine._schemas ? this.engine._schemas[statement.name] : null);
|
|
764
|
+
if (!schema) throw new Error(`Table '${statement.name}' does not exist`);
|
|
765
|
+
const colNames = Object.keys(schema);
|
|
766
|
+
const skipAuto = statement.values[0].length < colNames.length;
|
|
767
|
+
dataRows = statement.values.map(vals => {
|
|
768
|
+
const row = {};
|
|
769
|
+
let vi = 0;
|
|
770
|
+
colNames.forEach(c => {
|
|
771
|
+
if (schema[c].autoIncrement && (skipAuto || vals[vi] === undefined || vals[vi] === null)) {
|
|
772
|
+
if (!skipAuto && vi < vals.length) vi++;
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const v = vals[vi] !== undefined ? vals[vi] : null;
|
|
776
|
+
row[c] = v;
|
|
777
|
+
vi++;
|
|
778
|
+
});
|
|
779
|
+
return row;
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
if (!schema) {
|
|
783
|
+
schema = this.engine.getTableSchema
|
|
784
|
+
? await this.engine.getTableSchema(statement.name)
|
|
785
|
+
: (this.engine._schemas ? this.engine._schemas[statement.name] : null);
|
|
786
|
+
}
|
|
787
|
+
const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
|
|
788
|
+
let toInsert = dataRows;
|
|
789
|
+
let updated = 0;
|
|
790
|
+
if (pkCols.length > 0) {
|
|
791
|
+
const keyOf = (row) => pkCols.map(c => (row[c] !== undefined && row[c] !== null ? String(row[c]) : '')).join('|');
|
|
792
|
+
const hasExplicitPk = (row) => pkCols.some(c => row[c] !== undefined && row[c] !== null);
|
|
793
|
+
const explicit = dataRows.filter(hasExplicitPk);
|
|
794
|
+
if (explicit.length > 0) {
|
|
795
|
+
const all = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
796
|
+
const pkMap = new Map();
|
|
797
|
+
for (const row of all) pkMap.set(keyOf(row), row.id);
|
|
798
|
+
const conflicts = [];
|
|
799
|
+
const fresh = [];
|
|
800
|
+
for (const d of dataRows) {
|
|
801
|
+
const key = keyOf(d);
|
|
802
|
+
if (hasExplicitPk(d) && pkMap.has(key)) {
|
|
803
|
+
conflicts.push({ d, existingId: pkMap.get(key) });
|
|
804
|
+
} else {
|
|
805
|
+
fresh.push(d);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (conflicts.length > 0 && !statement.onDuplicate) {
|
|
809
|
+
throw new Error('ER_DUP_ENTRY: Duplicate entry for primary key');
|
|
810
|
+
}
|
|
811
|
+
if (statement.onDuplicate && conflicts.length > 0) {
|
|
812
|
+
for (const { d, existingId } of conflicts) {
|
|
813
|
+
const data = {};
|
|
814
|
+
for (const [col, val] of statement.onDuplicate) data[col] = val;
|
|
815
|
+
this.engine.updateById(statement.name, existingId, data);
|
|
816
|
+
updated++;
|
|
817
|
+
}
|
|
818
|
+
await this.engine.flush();
|
|
819
|
+
}
|
|
820
|
+
toInsert = fresh;
|
|
821
|
+
}
|
|
822
|
+
const seen = new Map();
|
|
823
|
+
let kept = [];
|
|
824
|
+
for (const d of toInsert) {
|
|
825
|
+
if (!hasExplicitPk(d)) { kept.push(d); continue; }
|
|
826
|
+
const key = keyOf(d);
|
|
827
|
+
if (seen.has(key)) {
|
|
828
|
+
if (!statement.onDuplicate) {
|
|
829
|
+
throw new Error('ER_DUP_ENTRY: Duplicate entry for primary key');
|
|
830
|
+
}
|
|
831
|
+
seen.get(key).row = d;
|
|
832
|
+
} else {
|
|
833
|
+
seen.set(key, { row: d });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (seen.size > 0) kept = kept.concat(Array.from(seen.values()).map(v => v.row));
|
|
837
|
+
toInsert = kept;
|
|
838
|
+
}
|
|
839
|
+
let ids = [];
|
|
840
|
+
if (toInsert.length > 0) {
|
|
841
|
+
ids = await this.engine.insert(statement.name, toInsert);
|
|
842
|
+
await this.engine.flush();
|
|
843
|
+
}
|
|
844
|
+
return {
|
|
845
|
+
ok: true, type: 'insert', table: statement.name,
|
|
846
|
+
affectedRows: toInsert.length + updated,
|
|
847
|
+
insertId: Array.isArray(ids) && ids.length > 0 ? ids[0] : null,
|
|
848
|
+
ids,
|
|
849
|
+
duplicateUpdated: updated,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
case 'select': {
|
|
853
|
+
let schema = null;
|
|
854
|
+
let all;
|
|
855
|
+
if (statement.table === null) {
|
|
856
|
+
all = [{ _virtual: true }];
|
|
857
|
+
} else {
|
|
858
|
+
if (this.engine.hasTable && !this.engine.hasTable(statement.table)) {
|
|
859
|
+
throw new Error(`Table '${statement.table}' does not exist`);
|
|
860
|
+
}
|
|
861
|
+
schema = this.engine.getTableSchema
|
|
862
|
+
? await this.engine.getTableSchema(statement.table)
|
|
863
|
+
: (this.engine._schemas ? this.engine._schemas[statement.table] : null);
|
|
864
|
+
if (!schema) {
|
|
865
|
+
throw new Error(`Table '${statement.table}' does not exist`);
|
|
866
|
+
}
|
|
867
|
+
const pushdown = extractEqualPushdown(statement.where, schema);
|
|
868
|
+
const engineFilter = pushdown ? pushdown.filter : {};
|
|
869
|
+
all = (await this.engine.find(statement.table, engineFilter, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
870
|
+
if (pushdown && pushdown.rest) {
|
|
871
|
+
all = all.filter(r => evaluateExpr(pushdown.rest, r));
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
let rows = all;
|
|
876
|
+
if (statement.where) {
|
|
877
|
+
rows = rows.filter(r => evaluateExpr(statement.where, r));
|
|
878
|
+
}
|
|
879
|
+
if (statement.distinct) {
|
|
880
|
+
const seen = new Set();
|
|
881
|
+
rows = rows.filter(r => {
|
|
882
|
+
const key = JSON.stringify(statement.columns.map(c => r[c.expr]));
|
|
883
|
+
if (seen.has(key)) return false;
|
|
884
|
+
seen.add(key);
|
|
885
|
+
return true;
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
if (statement.orderBy) {
|
|
889
|
+
const cmp = (a, b) => {
|
|
890
|
+
for (const o of statement.orderBy) {
|
|
891
|
+
const av = a[o.column], bv = b[o.column];
|
|
892
|
+
if (av === bv || (av === undefined && bv === undefined)) continue;
|
|
893
|
+
if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
|
|
894
|
+
if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
|
|
895
|
+
const r = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
|
|
896
|
+
if (r !== 0) return o.dir === 'asc' ? r : -r;
|
|
897
|
+
}
|
|
898
|
+
return 0;
|
|
899
|
+
};
|
|
900
|
+
rows = rows.slice().sort(cmp);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (statement.aggregate) {
|
|
904
|
+
const agg = statement.aggregate;
|
|
905
|
+
const rowsOnly = rows;
|
|
906
|
+
const aggName = agg.alias || (agg.type === 'COUNT' ? 'COUNT(*)' : agg.type + '(' + agg.column + ')');
|
|
907
|
+
if (agg.type === 'COUNT') {
|
|
908
|
+
return { ok: true, type: 'select', table: statement.table, columns: [aggName], rows: [[rowsOnly.length]], aggregate: agg };
|
|
909
|
+
}
|
|
910
|
+
const values = rowsOnly.map(r => r[agg.column]).filter(v => v !== null && v !== undefined);
|
|
911
|
+
let value;
|
|
912
|
+
if (agg.type === 'SUM') value = values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0);
|
|
913
|
+
else if (agg.type === 'AVG') value = values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
|
|
914
|
+
else if (agg.type === 'MIN') value = values.length ? Math.min(...values.map(v => Number(v))) : null;
|
|
915
|
+
else if (agg.type === 'MAX') value = values.length ? Math.max(...values.map(v => Number(v))) : null;
|
|
916
|
+
return { ok: true, type: 'select', table: statement.table, columns: [aggName], rows: [[value]], aggregate: agg };
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (statement.limit !== null) {
|
|
920
|
+
const start = statement.offset || 0;
|
|
921
|
+
rows = rows.slice(start, start + statement.limit);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (statement.columns.length === 1 && statement.columns[0].expr === '*') {
|
|
925
|
+
const schemaKeys = schema ? Object.keys(schema) : [];
|
|
926
|
+
const pkCols = schemaKeys.filter(k => schema[k].primaryKey);
|
|
927
|
+
const pk = pkCols.length > 0 ? pkCols[0] : (schemaKeys[0] || 'id');
|
|
928
|
+
const cols = schema ? [pk, ...schemaKeys.filter(k => k !== pk)] : Object.keys(all[0] || {});
|
|
929
|
+
return { ok: true, type: 'select', table: statement.table, columns: cols, rows: rows.map(r => cols.map(c => r[c])), raw: rows };
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const cols = statement.columns.map(c => c.alias || (c.literal !== undefined ? (c.expr === null ? String(c.literal) : c.expr) : c.expr));
|
|
933
|
+
const mapped = rows.map(r => statement.columns.map(c => {
|
|
934
|
+
if (c.expr === '*') return null;
|
|
935
|
+
if (c.literal !== undefined) return c.literal;
|
|
936
|
+
return r[c.expr];
|
|
937
|
+
}));
|
|
938
|
+
return { ok: true, type: 'select', table: statement.table, columns: cols, rows: mapped, raw: rows };
|
|
939
|
+
}
|
|
940
|
+
case 'update': {
|
|
941
|
+
const schema = this.engine.getTableSchema
|
|
942
|
+
? await this.engine.getTableSchema(statement.table)
|
|
943
|
+
: (this.engine._schemas ? this.engine._schemas[statement.table] : null);
|
|
944
|
+
const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
945
|
+
let count = 0;
|
|
946
|
+
for (const row of all) {
|
|
947
|
+
if (!statement.where || evaluateExpr(statement.where, row)) {
|
|
948
|
+
const id = row._rid !== undefined ? row._rid : row.id;
|
|
949
|
+
if (id !== undefined) {
|
|
950
|
+
const data = {};
|
|
951
|
+
for (const [col, val] of statement.assignments) data[col] = val;
|
|
952
|
+
this.engine.updateById(statement.table, id, data);
|
|
953
|
+
count++;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
await this.engine.flush();
|
|
958
|
+
return { ok: true, type: 'update', table: statement.table, affectedRows: count };
|
|
959
|
+
}
|
|
960
|
+
case 'delete': {
|
|
961
|
+
const schema = this.engine.getTableSchema
|
|
962
|
+
? await this.engine.getTableSchema(statement.table)
|
|
963
|
+
: (this.engine._schemas ? this.engine._schemas[statement.table] : null);
|
|
964
|
+
const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
965
|
+
const ids = [];
|
|
966
|
+
for (const row of all) {
|
|
967
|
+
if (!statement.where || evaluateExpr(statement.where, row)) {
|
|
968
|
+
const id = row._rid !== undefined ? row._rid : row.id;
|
|
969
|
+
if (id !== undefined) ids.push(id);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (ids.length > 0) {
|
|
973
|
+
if (this.engine.removeByIds) this.engine.removeByIds(statement.table, ids);
|
|
974
|
+
else for (const id of ids) this.engine.removeById(statement.table, id);
|
|
975
|
+
}
|
|
976
|
+
await this.engine.flush();
|
|
977
|
+
return { ok: true, type: 'delete', table: statement.table, affectedRows: ids.length };
|
|
978
|
+
}
|
|
979
|
+
case 'begin': return { ok: true, type: 'begin' };
|
|
980
|
+
case 'commit': return { ok: true, type: 'commit' };
|
|
981
|
+
case 'rollback': return { ok: true, type: 'rollback' };
|
|
982
|
+
case 'showTables': {
|
|
983
|
+
const tables = this.engine.getTables ? this.engine.getTables() : (this.engine.tables ? this.engine.tables() : []);
|
|
984
|
+
return { ok: true, type: 'showTables', columns: ['Tables'], rows: tables.map(t => [t]) };
|
|
985
|
+
}
|
|
986
|
+
case 'showDatabases':
|
|
987
|
+
return { ok: true, type: 'showDatabases', columns: ['Database'], rows: [['jsql']] };
|
|
988
|
+
case 'describe': {
|
|
989
|
+
const schema = this.engine.getTableSchema ? await this.engine.getTableSchema(statement.table) : null;
|
|
990
|
+
if (!schema) throw new Error(`Table '${statement.table}' does not exist`);
|
|
991
|
+
const rows = Object.entries(schema).map(([col, def]) => [col, def.type, def.primaryKey ? 'PRI' : '', def.autoIncrement ? 'auto_increment' : null, def.default !== undefined ? def.default : null]);
|
|
992
|
+
return { ok: true, type: 'describe', table: statement.table, columns: ['Field', 'Type', 'Key', 'Extra', 'Default'], rows };
|
|
993
|
+
}
|
|
994
|
+
case 'use':
|
|
995
|
+
return { ok: true, type: 'use', database: statement.database };
|
|
996
|
+
default:
|
|
997
|
+
throw new Error(`Unsupported statement type: ${statement.type}`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function splitStatements(sql) {
|
|
1003
|
+
const statements = [];
|
|
1004
|
+
let current = '';
|
|
1005
|
+
let inStr = null;
|
|
1006
|
+
let i = 0;
|
|
1007
|
+
while (i < sql.length) {
|
|
1008
|
+
const c = sql[i];
|
|
1009
|
+
if (inStr) {
|
|
1010
|
+
current += c;
|
|
1011
|
+
if (c === '\\' && i + 1 < sql.length) { current += sql[i + 1]; i += 2; continue; }
|
|
1012
|
+
if (c === inStr) inStr = null;
|
|
1013
|
+
i++;
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
if (c === "'" || c === '"') { inStr = c; current += c; i++; continue; }
|
|
1017
|
+
if (c === ';') {
|
|
1018
|
+
if (current.trim()) statements.push(current.trim());
|
|
1019
|
+
current = '';
|
|
1020
|
+
i++;
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
current += c;
|
|
1024
|
+
i++;
|
|
1025
|
+
}
|
|
1026
|
+
if (current.trim()) statements.push(current.trim());
|
|
1027
|
+
return statements;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function parseSQL(sql) {
|
|
1031
|
+
const tokens = tokenize(sql);
|
|
1032
|
+
const parser = new Parser(tokens);
|
|
1033
|
+
const stmt = parser.parseStatement();
|
|
1034
|
+
if (parser.peek().type !== 'eof') {
|
|
1035
|
+
throw new Error(`Unexpected token '${parser.peek().value}' after statement`);
|
|
1036
|
+
}
|
|
1037
|
+
return stmt;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function hasComments(sql) {
|
|
1041
|
+
let inStr = null;
|
|
1042
|
+
let i = 0;
|
|
1043
|
+
while (i < sql.length) {
|
|
1044
|
+
const c = sql[i];
|
|
1045
|
+
if (inStr) {
|
|
1046
|
+
if (c === '\\') i += 2;
|
|
1047
|
+
else { if (c === inStr) inStr = null; i++; }
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
if (c === "'" || c === '"') { inStr = c; i++; continue; }
|
|
1051
|
+
if (c === '`') { i++; while (i < sql.length && sql[i] !== '`') i++; i++; continue; }
|
|
1052
|
+
if (c === '-' && sql[i + 1] === '-') return true;
|
|
1053
|
+
if (c === '#') return true;
|
|
1054
|
+
if (c === '/' && sql[i + 1] === '*') return true;
|
|
1055
|
+
i++;
|
|
1056
|
+
}
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function escapeId(name) {
|
|
1061
|
+
return '`' + String(name).replace(/`/g, '``') + '`';
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function escapeValue(value) {
|
|
1065
|
+
if (value === null || value === undefined) return 'NULL';
|
|
1066
|
+
if (typeof value === 'number') return Number.isFinite(value) ? String(value) : 'NULL';
|
|
1067
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
1068
|
+
if (value instanceof Date) {
|
|
1069
|
+
const p = n => String(n).padStart(2, '0');
|
|
1070
|
+
return `'${value.getFullYear()}-${p(value.getMonth() + 1)}-${p(value.getDate())} ${p(value.getHours())}:${p(value.getMinutes())}:${p(value.getSeconds())}'`;
|
|
1071
|
+
}
|
|
1072
|
+
if (Buffer.isBuffer(value)) return "X'" + value.toString('hex') + "'";
|
|
1073
|
+
if (Array.isArray(value)) {
|
|
1074
|
+
if (value.some(Array.isArray)) {
|
|
1075
|
+
return value.map(row => '(' + row.map(escapeValue).join(', ') + ')').join(', ');
|
|
1076
|
+
}
|
|
1077
|
+
return value.map(escapeValue).join(', ');
|
|
1078
|
+
}
|
|
1079
|
+
if (typeof value === 'object') return "'" + JSON.stringify(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'";
|
|
1080
|
+
const str = String(value)
|
|
1081
|
+
.replace(/\\/g, '\\\\')
|
|
1082
|
+
.replace(/\0/g, '\\0')
|
|
1083
|
+
.replace(/'/g, "\\'")
|
|
1084
|
+
.replace(/\n/g, '\\n')
|
|
1085
|
+
.replace(/\r/g, '\\r')
|
|
1086
|
+
.replace(/\u001a/g, '\\Z');
|
|
1087
|
+
return "'" + str + "'";
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function applyParams(sql, values) {
|
|
1091
|
+
const args = values || [];
|
|
1092
|
+
let count = 0;
|
|
1093
|
+
let out = '';
|
|
1094
|
+
let idx = 0;
|
|
1095
|
+
let inStr = null;
|
|
1096
|
+
let i = 0;
|
|
1097
|
+
while (i < sql.length) {
|
|
1098
|
+
const c = sql[i];
|
|
1099
|
+
if (inStr) {
|
|
1100
|
+
out += c;
|
|
1101
|
+
if (c === '\\' && i + 1 < sql.length) { out += sql[i + 1]; i += 2; continue; }
|
|
1102
|
+
if (c === inStr) inStr = null;
|
|
1103
|
+
i++;
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
if (c === "'" || c === '"' || c === '`') { inStr = c; out += c; i++; continue; }
|
|
1107
|
+
if (c === '?' && sql[i + 1] === '?') {
|
|
1108
|
+
if (idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
|
|
1109
|
+
out += escapeId(args[idx++]);
|
|
1110
|
+
count++;
|
|
1111
|
+
i += 2;
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
if (c === '?') {
|
|
1115
|
+
if (idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
|
|
1116
|
+
out += escapeValue(args[idx++]);
|
|
1117
|
+
count++;
|
|
1118
|
+
i++;
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
out += c;
|
|
1122
|
+
i++;
|
|
1123
|
+
}
|
|
1124
|
+
if (idx !== args.length) {
|
|
1125
|
+
throw new Error(`Too many parameters for SQL: got ${args.length}, expected ${count}`);
|
|
1126
|
+
}
|
|
1127
|
+
return out;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
1131
|
+
if (Array.isArray(paramsOrOpts)) {
|
|
1132
|
+
sql = applyParams(sql, paramsOrOpts);
|
|
1133
|
+
} else if (paramsOrOpts && typeof paramsOrOpts === 'object') {
|
|
1134
|
+
opts = paramsOrOpts;
|
|
1135
|
+
}
|
|
1136
|
+
if (opts.safety !== false) {
|
|
1137
|
+
if (!opts.allowComments && hasComments(sql)) {
|
|
1138
|
+
throw new Error('SQL comments are disabled for security (--, #, /* */)');
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
let statements = splitStatements(sql);
|
|
1142
|
+
if (opts.maxStatements != null && statements.length > opts.maxStatements) {
|
|
1143
|
+
throw new Error(`too many statements (${statements.length} > ${opts.maxStatements})`);
|
|
1144
|
+
}
|
|
1145
|
+
if (opts.safety !== false) {
|
|
1146
|
+
for (const stmtSql of statements) {
|
|
1147
|
+
const dangerous = findDangerousSQL(tokenize(stmtSql));
|
|
1148
|
+
if (dangerous) throw new Error(`SQL statement blocked by security policy: ${dangerous}`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
const executor = new SQLExecutor(engine);
|
|
1152
|
+
const results = [];
|
|
1153
|
+
for (const stmtSql of statements) {
|
|
1154
|
+
const stmt = parseSQL(stmtSql);
|
|
1155
|
+
results.push(await executor.execute(stmt));
|
|
1156
|
+
}
|
|
1157
|
+
return results.length === 1 ? results[0] : results;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
module.exports = { tokenize, parseSQL, executeSQL, SQLExecutor, splitStatements, applyParams, escapeValue, escapeId };
|