jsql-neo 6.0.2 → 6.0.4

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.
Binary file
@@ -205,7 +205,7 @@ dependencies = [
205
205
 
206
206
  [[package]]
207
207
  name = "jsql-neo-core"
208
- version = "5.6.0"
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 = "5.6.0"
223
+ version = "6.0.0"
224
224
  dependencies = [
225
225
  "jsql-neo-core",
226
226
  "napi",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.0.2",
3
+ "version": "6.0.4",
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",
@@ -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
- const { JSQL } = require('../lib/wasm_client.js');
218
- let db;
219
- try {
220
- db = new JSQL();
221
- await db.start();
222
- } catch (e) {
223
- console.log('[SKIP] WASM 客户端不可用:' + e.message.slice(0, 60));
224
- }
225
- if (db) {
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(`并发 insert 200 行 → count 恰为 200(实际 ${cnt})`, cnt === 200);
339
+ ok(`${tag} 并发 insert 200 行 → count 恰为 200(实际 ${cnt})`, cnt === 200);
231
340
 
232
341
  // flush 失败不得把坏行留在 buffer 里二次爆炸
233
- ok('insert(null) 抛 TypeError', await (async () => {
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('insert(undefined) 抛 TypeError', await (async () => {
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('insert(非对象) 抛 TypeError', await (async () => {
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');