jsql-neo 6.0.2 → 6.0.5
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 +40 -35
- package/lib/migrate.js +126 -30
- package/lib/native_client.js +69 -8
- package/lib/sql.js +523 -65
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-native/Cargo.lock +2 -2
- package/package.json +1 -1
- package/test/readme-audit.test.js +153 -14
|
Binary file
|
|
@@ -205,7 +205,7 @@ dependencies = [
|
|
|
205
205
|
|
|
206
206
|
[[package]]
|
|
207
207
|
name = "jsql-neo-core"
|
|
208
|
-
version = "
|
|
208
|
+
version = "6.0.0"
|
|
209
209
|
dependencies = [
|
|
210
210
|
"bincode",
|
|
211
211
|
"chrono",
|
|
@@ -220,7 +220,7 @@ dependencies = [
|
|
|
220
220
|
|
|
221
221
|
[[package]]
|
|
222
222
|
name = "jsql-neo-native"
|
|
223
|
-
version = "
|
|
223
|
+
version = "6.0.0"
|
|
224
224
|
dependencies = [
|
|
225
225
|
"jsql-neo-core",
|
|
226
226
|
"napi",
|
package/package.json
CHANGED
|
@@ -139,6 +139,88 @@ async function main() {
|
|
|
139
139
|
JSON.stringify(await rowsOf('SELECT name FROM (SELECT name, sal FROM emp WHERE sal > 150)')) === '[["b"],["c"],["d"]]');
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
console.log('\n--- 方言特性:DIV / XOR / && / || / <=> / :: / -> ->> / LIKE ESCAPE ---');
|
|
143
|
+
{
|
|
144
|
+
const data = [
|
|
145
|
+
{ id: 1, n: 10, flag: 1, s: '100%', nm: 'Alice', meta: '{"name":"Alice","addr":{"city":"SH"},"tags":["x","y"]}' },
|
|
146
|
+
{ id: 2, n: 20, flag: 0, s: '1000', nm: 'Bob', meta: '{"name":"Bob","addr":{"city":"BJ"},"tags":[]}' },
|
|
147
|
+
{ id: 3, n: 30, flag: 1, s: '100', nm: 'Cara', meta: null },
|
|
148
|
+
];
|
|
149
|
+
const engine = {
|
|
150
|
+
hasTable: () => true, truncate: async () => {}, flush: async () => {},
|
|
151
|
+
find: async () => data.map(r => ({ ...r })),
|
|
152
|
+
getTableSchema: async () => ({
|
|
153
|
+
id: { type: 'integer', primaryKey: true }, n: { type: 'integer' }, flag: { type: 'integer' },
|
|
154
|
+
s: { type: 'string' }, nm: { type: 'string' }, meta: { type: 'json' },
|
|
155
|
+
}),
|
|
156
|
+
};
|
|
157
|
+
const rowsOf = async (sql) => {
|
|
158
|
+
const r = await executeSQL(engine, sql, OPTS);
|
|
159
|
+
return (Array.isArray(r) ? r[0] : r).rows;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
ok('DIV 整数除法', JSON.stringify(await rowsOf('SELECT 7 DIV 2 AS r')) === '[[3]]');
|
|
163
|
+
ok('DIV 负数向零取整', JSON.stringify(await rowsOf('SELECT -7 DIV 2 AS r')) === '[[-3]]');
|
|
164
|
+
ok("'5'::INT 类型转换", JSON.stringify(await rowsOf("SELECT '5'::INT AS r")) === '[[5]]');
|
|
165
|
+
ok('XOR 逻辑异或(WHERE)',
|
|
166
|
+
JSON.stringify(await rowsOf('SELECT id FROM tbl WHERE flag = 1 XOR n = 20')) === '[[1],[2],[3]]');
|
|
167
|
+
ok('&& 是 AND 别名',
|
|
168
|
+
JSON.stringify(await rowsOf('SELECT id FROM tbl WHERE flag = 1 && n > 10')) === '[[3]]');
|
|
169
|
+
ok('|| 是 OR 别名',
|
|
170
|
+
JSON.stringify(await rowsOf('SELECT id FROM tbl WHERE flag = 0 || n > 20')) === '[[2],[3]]');
|
|
171
|
+
ok('<=> 非空相等', JSON.stringify(await rowsOf('SELECT id FROM tbl WHERE n <=> 20')) === '[[2]]');
|
|
172
|
+
ok('<=> NULL 安全(NULL<=>NULL 为真)',
|
|
173
|
+
JSON.stringify(await rowsOf('SELECT id FROM tbl WHERE meta <=> NULL')) === '[[3]]');
|
|
174
|
+
ok("->>'name'", JSON.stringify(await rowsOf("SELECT meta->>'name' AS x FROM tbl WHERE id = 1")) === '[["Alice"]]');
|
|
175
|
+
ok("->'addr'->>'city' 链式",
|
|
176
|
+
JSON.stringify(await rowsOf("SELECT meta->'addr'->>'city' AS x FROM tbl WHERE id = 1")) === '[["SH"]]');
|
|
177
|
+
ok("->'tags'->>0 数组下标",
|
|
178
|
+
JSON.stringify(await rowsOf("SELECT meta->'tags'->>0 AS x FROM tbl WHERE id = 1")) === '[["x"]]');
|
|
179
|
+
// PG JSON 包含 / 路径操作符
|
|
180
|
+
ok("meta @> '{...}' 包含",
|
|
181
|
+
JSON.stringify(await rowsOf(`SELECT id FROM tbl WHERE meta @> '{"addr":{"city":"SH"}}'`)) === '[[1]]');
|
|
182
|
+
ok("'{...}' <@ meta 被包含",
|
|
183
|
+
JSON.stringify(await rowsOf(`SELECT id FROM tbl WHERE '{"name":"Bob"}' <@ meta`)) === '[[2]]');
|
|
184
|
+
ok("meta#>'{addr,city}' 路径",
|
|
185
|
+
JSON.stringify(await rowsOf("SELECT meta#>'{addr,city}' AS x FROM tbl WHERE id = 1")) === '[["SH"]]');
|
|
186
|
+
ok("meta#>>'{addr,city}' 路径文本",
|
|
187
|
+
JSON.stringify(await rowsOf("SELECT meta#>>'{addr,city}' AS x FROM tbl WHERE id = 1")) === '[["SH"]]');
|
|
188
|
+
ok("meta#>'{tags,0}' 路径含下标",
|
|
189
|
+
JSON.stringify(await rowsOf("SELECT meta#>'{tags,0}' AS x FROM tbl WHERE id = 1")) === '[["x"]]');
|
|
190
|
+
// JSON 键存在 ? / ?| / ?&
|
|
191
|
+
ok("meta ? 'name'", JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ? 'name'")) === '[[1],[2]]');
|
|
192
|
+
ok("meta ? 'nope'(不存在)",
|
|
193
|
+
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ? 'nope'")) === '[]');
|
|
194
|
+
ok("meta ?| '{name,nope}'(任一)",
|
|
195
|
+
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ?| '{name,nope}'")) === '[[1],[2]]');
|
|
196
|
+
ok("meta ?& '{name,addr}'(全部)",
|
|
197
|
+
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ?& '{name,addr}'")) === '[[1],[2]]');
|
|
198
|
+
ok("meta ?& '{name,nope}'(缺一个)",
|
|
199
|
+
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ?& '{name,nope}'")) === '[]');
|
|
200
|
+
// PG 正则:~ 敏感 / ~* 不敏感 / !~ 取反
|
|
201
|
+
ok("nm ~ '^A' 大小写敏感", JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm ~ '^A'")) === '[[1]]');
|
|
202
|
+
ok("nm ~* '^a' 不敏感", JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm ~* '^a'")) === '[[1]]');
|
|
203
|
+
ok("nm !~ '^A' 取反", JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm !~ '^A'")) === '[[2],[3]]');
|
|
204
|
+
ok('REGEXP 仍可用', JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm REGEXP '^A'")) === '[[1]]');
|
|
205
|
+
ok("LIKE ... ESCAPE '!' 匹配字面量 %",
|
|
206
|
+
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE s LIKE '100!%' ESCAPE '!'")) === '[[1]]');
|
|
207
|
+
ok('LIKE 反斜杠转义匹配字面量 %',
|
|
208
|
+
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE s LIKE '100\\%'")) === '[[1]]');
|
|
209
|
+
ok('LIKE 普通 % 仍为通配',
|
|
210
|
+
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE s LIKE '100%'")) === '[[1],[2],[3]]');
|
|
211
|
+
|
|
212
|
+
// 参数绑定:位置 ? / 命名 :name / 编号 $1(README「Params」一节)
|
|
213
|
+
const rowsOfP = async (sql, params) => {
|
|
214
|
+
const r = await executeSQL(engine, sql, params, OPTS);
|
|
215
|
+
return (Array.isArray(r) ? r[0] : r).rows;
|
|
216
|
+
};
|
|
217
|
+
ok('位置参数 ?', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = ?', [1])) === '[[1]]');
|
|
218
|
+
ok('命名参数 :id', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = :id', { id: 2 })) === '[[2]]');
|
|
219
|
+
ok('命名参数多占位', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = :a OR id = :b', { a: 1, b: 3 })) === '[[1],[3]]');
|
|
220
|
+
ok('编号参数 $1', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = $1', [3])) === '[[3]]');
|
|
221
|
+
ok('编号参数 $2/$1', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = $2 OR id = $1', [1, 2])) === '[[1],[2]]');
|
|
222
|
+
}
|
|
223
|
+
|
|
142
224
|
console.log('\n--- DDL / 解析器节点覆盖 ---');
|
|
143
225
|
{
|
|
144
226
|
ok('CREATE INDEX 可解析', parseSQL('CREATE INDEX i ON t (a)').type === 'createIndex');
|
|
@@ -156,6 +238,30 @@ async function main() {
|
|
|
156
238
|
const st = parseSQL('CREATE TABLE t (id INT PRIMARY KEY, c DATETIME DEFAULT CURRENT_TIMESTAMP)');
|
|
157
239
|
return st.schema.c.default === 'CURRENT_TIMESTAMP';
|
|
158
240
|
})());
|
|
241
|
+
ok('SERIAL 折叠为整型+自增', (() => {
|
|
242
|
+
const st = parseSQL('CREATE TABLE t (id SERIAL PRIMARY KEY, n TEXT)');
|
|
243
|
+
return st.schema.id.type === 'integer' && st.schema.id.autoIncrement === true;
|
|
244
|
+
})());
|
|
245
|
+
ok('BIGSERIAL 折叠为整型+自增', (() => {
|
|
246
|
+
const st = parseSQL('CREATE TABLE t (id BIGSERIAL PRIMARY KEY)');
|
|
247
|
+
return st.schema.id.type === 'integer' && st.schema.id.autoIncrement === true;
|
|
248
|
+
})());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log('\n--- executeSQL 结果信封(README 承诺的字段)---');
|
|
252
|
+
{
|
|
253
|
+
const engine = {
|
|
254
|
+
hasTable: () => true, truncate: async () => {}, flush: async () => {},
|
|
255
|
+
find: async () => [{ id: 1, name: 'a' }, { id: 2, name: 'b' }],
|
|
256
|
+
getTableSchema: async () => ({ id: { type: 'integer', primaryKey: true }, name: { type: 'string' } }),
|
|
257
|
+
};
|
|
258
|
+
const r = await executeSQL(engine, 'SELECT * FROM t', OPTS);
|
|
259
|
+
for (const f of ['columns', 'columnTypes', 'rows', 'rowCount', 'affectedRows', 'message', 'command', 'durationMs', 'warnings']) {
|
|
260
|
+
ok(`信封含 ${f}`, r[f] !== undefined, Object.keys(r));
|
|
261
|
+
}
|
|
262
|
+
ok('rowCount = 2', r.rowCount === 2);
|
|
263
|
+
ok('command = SELECT', r.command === 'SELECT');
|
|
264
|
+
ok('columnTypes 可推断(id → INT)', Array.isArray(r.columnTypes) && r.columnTypes[0] === 'INT');
|
|
159
265
|
}
|
|
160
266
|
|
|
161
267
|
console.log('\n--- 顶层工厂导出不得为 undefined ---');
|
|
@@ -214,29 +320,32 @@ async function main() {
|
|
|
214
320
|
|
|
215
321
|
console.log('\n--- 并发 insert 不得重复写(曾行数暴涨 / 丢失)---');
|
|
216
322
|
{
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
323
|
+
// 两个引擎都要验收:曾只测 wasm,导致 native(默认引擎)的同款竞态长期漏网。
|
|
324
|
+
for (const engineName of ['wasm_client', 'native_client']) {
|
|
325
|
+
let JSQL, db;
|
|
326
|
+
try {
|
|
327
|
+
({ JSQL } = require('../lib/' + engineName + '.js'));
|
|
328
|
+
db = new JSQL();
|
|
329
|
+
await db.start();
|
|
330
|
+
} catch (e) {
|
|
331
|
+
console.log(`[SKIP] ${engineName} 不可用:` + e.message.slice(0, 60));
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const tag = `[${engineName}]`;
|
|
226
335
|
let id = 0;
|
|
227
336
|
await db.createTable('conc', { id: { type: 'INT', primaryKey: true }, b: { type: 'INT' } });
|
|
228
337
|
await Promise.all(Array.from({ length: 200 }, () => db.insert('conc', { id: ++id, b: 5 })));
|
|
229
338
|
const cnt = await db.count('conc');
|
|
230
|
-
ok(
|
|
339
|
+
ok(`${tag} 并发 insert 200 行 → count 恰为 200(实际 ${cnt})`, cnt === 200);
|
|
231
340
|
|
|
232
341
|
// flush 失败不得把坏行留在 buffer 里二次爆炸
|
|
233
|
-
ok(
|
|
342
|
+
ok(`${tag} insert(null) 抛 TypeError`, await (async () => {
|
|
234
343
|
try { await db.insert('conc', null); return false; } catch (e) { return e instanceof TypeError; }
|
|
235
344
|
})());
|
|
236
|
-
ok(
|
|
345
|
+
ok(`${tag} insert(undefined) 抛 TypeError`, await (async () => {
|
|
237
346
|
try { await db.insert('conc', undefined); return false; } catch (e) { return e instanceof TypeError; }
|
|
238
347
|
})());
|
|
239
|
-
ok(
|
|
348
|
+
ok(`${tag} insert(非对象) 抛 TypeError`, await (async () => {
|
|
240
349
|
try { await db.insert('conc', 42); return false; } catch (e) { return e instanceof TypeError; }
|
|
241
350
|
})());
|
|
242
351
|
|
|
@@ -246,13 +355,43 @@ async function main() {
|
|
|
246
355
|
cyc.self = cyc;
|
|
247
356
|
let cycErr = null;
|
|
248
357
|
try { await db.insert('cyc', { id: 1, meta: cyc }); } catch (e) { cycErr = e; }
|
|
249
|
-
ok(
|
|
358
|
+
ok(`${tag} 环形对象给出带表/列名的错误`,
|
|
250
359
|
!!cycErr && /Cannot serialize column 'meta'/.test(cycErr.message),
|
|
251
360
|
cycErr && cycErr.message);
|
|
252
361
|
await db.stop();
|
|
253
362
|
}
|
|
254
363
|
}
|
|
255
364
|
|
|
365
|
+
console.log('\n--- 唯一约束:非主键 UNIQUE 必须报 ER_DUP_ENTRY ---');
|
|
366
|
+
{
|
|
367
|
+
let JSQL, db;
|
|
368
|
+
try {
|
|
369
|
+
({ JSQL } = require('../lib/native_client.js'));
|
|
370
|
+
db = new JSQL();
|
|
371
|
+
await db.start();
|
|
372
|
+
} catch (e) {
|
|
373
|
+
console.log('[SKIP] native 不可用:' + e.message.slice(0, 60));
|
|
374
|
+
}
|
|
375
|
+
if (db) {
|
|
376
|
+
const run = (sql) => executeSQL(db, sql, OPTS);
|
|
377
|
+
const dupErrMsg = async (sql) => {
|
|
378
|
+
try { await run(sql); return null; } catch (e) { return e.message; }
|
|
379
|
+
};
|
|
380
|
+
await run('CREATE TABLE u (id SERIAL PRIMARY KEY, email TEXT UNIQUE, nick TEXT UNIQUE)');
|
|
381
|
+
await run("INSERT INTO u (email, nick) VALUES ('a@x.com', 'a')");
|
|
382
|
+
ok('唯一列重复 → ER_DUP_ENTRY',
|
|
383
|
+
/ER_DUP_ENTRY/.test(await dupErrMsg("INSERT INTO u (email, nick) VALUES ('a@x.com', 'z')") || ''));
|
|
384
|
+
ok('批内唯一重复 → ER_DUP_ENTRY',
|
|
385
|
+
/ER_DUP_ENTRY/.test(await dupErrMsg("INSERT INTO u (email, nick) VALUES ('b@x.com', 'n1'), ('c@x.com', 'n1')") || ''));
|
|
386
|
+
const r1 = await run('SELECT id FROM u');
|
|
387
|
+
ok('冲突后仍只有 1 行', r1.rows.length === 1, r1.rows);
|
|
388
|
+
await run("INSERT INTO u (email, nick) VALUES (NULL, 'x'), (NULL, 'y')");
|
|
389
|
+
const r2 = await run('SELECT id FROM u');
|
|
390
|
+
ok('UNIQUE 允许多个 NULL', r2.rows.length === 3, r2.rows);
|
|
391
|
+
await db.stop();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
256
395
|
console.log('\n--- Redis:RESP2 帧解析(曾 inline 命令恒为 unknown command)---');
|
|
257
396
|
{
|
|
258
397
|
const { RedisServer } = require('../lib/redis_server.js');
|