jsql-neo 5.1.3 → 5.2.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/README.md CHANGED
@@ -13,8 +13,6 @@
13
13
 
14
14
  ## Why JSQL-NEO?
15
15
 
16
- My web : https://jsql.vexify.top/
17
-
18
16
  Most embedded databases make you choose: *native speed*, *portable WASM*, or *a familiar file format*.
19
17
  JSQL-NEO gives you **all three in one install** — plus drop-in compatibility with the **two most popular
20
18
  database protocols in the world**.
package/lib/query.js CHANGED
@@ -537,7 +537,7 @@ class Query {
537
537
  const key = fr[join.foreignField];
538
538
  const matches = localRows.filter(lr => lr[join.localField] === key);
539
539
  if (matches.length === 0) {
540
- result.push(this._rightNullRow(localRows, fr, join));
540
+ result.push({ ...fr, ...this._nullRow(join, true) });
541
541
  } else {
542
542
  for (const lr of matches) {
543
543
  result.push(this._mergeJoinRow(lr, fr, join));
@@ -582,7 +582,7 @@ class Query {
582
582
  for (const fr of foreignRows) {
583
583
  const matches = localRows.filter(lr => lr[join.localField] === fr[join.foreignField]);
584
584
  if (matches.length === 0) {
585
- result.push(this._rightNullRow(localRows, fr, join));
585
+ result.push({ ...fr, ...this._nullRow(join, true) });
586
586
  } else {
587
587
  for (const lr of matches) {
588
588
  result.push(this._mergeJoinRow(lr, fr, join));
@@ -621,24 +621,6 @@ class Query {
621
621
  return nulls;
622
622
  }
623
623
 
624
- /**
625
- * RIGHT JOIN 未匹配右表行:保留右表数据,本地表字段填 null。
626
- * 返回 { localNulls, merged },其中 merged = { ...nulls, ...fr }
627
- */
628
- _rightNullRow(localRows, foreignRow, join) {
629
- const localSchema = this._table._schema || {};
630
- const localNulls = {};
631
- for (const field of Object.keys(localSchema)) {
632
- if (field !== '_softDelete') localNulls[field] = null;
633
- }
634
- const prefix = join.as ? join.as + '_' : '';
635
- const foreignPrefixed = {};
636
- for (const [key, value] of Object.entries(foreignRow)) {
637
- foreignPrefixed[prefix + key] = value;
638
- }
639
- return { ...localNulls, ...foreignPrefixed };
640
- }
641
-
642
624
  // ============================================================
643
625
  // 内部: 排序
644
626
  // ============================================================
package/lib/sql.js CHANGED
@@ -261,18 +261,12 @@ class Parser {
261
261
  if (this.peek().type === 'op' && this.peek().value === '.') {
262
262
  this.next();
263
263
  const t2 = this.next();
264
- if (t2.type !== 'ident' && !(t2.type === 'keyword' && this._isSchemaView(t2.value))) {
265
- throw new Error(`Expected table name after '.', got '${t2.value}'`);
266
- }
264
+ if (t2.type !== 'ident') throw new Error(`Expected table name after '.', got '${t2.value}'`);
267
265
  return t.value + '.' + t2.value;
268
266
  }
269
267
  return t.value;
270
268
  }
271
269
 
272
- _isSchemaView(v) {
273
- return ['TABLES', 'COLUMNS', 'SCHEMATA', 'STATISTICS', 'KEY_COLUMN_USAGE', 'REFERENTIAL_CONSTRAINTS', 'TABLE_CONSTRAINTS', 'VIEWS'].includes(String(v).toUpperCase());
274
- }
275
-
276
270
  parseCreateTable() {
277
271
  this.expectKeyword('CREATE');
278
272
  this.expectKeyword('TABLE');
@@ -1876,12 +1870,11 @@ class SQLExecutor {
1876
1870
  const schema = this.engine.getTableSchema
1877
1871
  ? await this.engine.getTableSchema(statement.table)
1878
1872
  : (this.engine._schemas ? this.engine._schemas[statement.table] : null);
1879
- const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
1880
1873
  const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
1881
1874
  let count = 0;
1882
1875
  for (const row of all) {
1883
1876
  if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
1884
- const id = this._rowPkId(row, pkCols);
1877
+ const id = row._rid !== undefined ? row._rid : row.id;
1885
1878
  if (id !== undefined) {
1886
1879
  const data = {};
1887
1880
  for (const [col, val] of statement.assignments) {
@@ -1899,12 +1892,11 @@ class SQLExecutor {
1899
1892
  const schema = this.engine.getTableSchema
1900
1893
  ? await this.engine.getTableSchema(statement.table)
1901
1894
  : (this.engine._schemas ? this.engine._schemas[statement.table] : null);
1902
- const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
1903
1895
  const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
1904
1896
  const ids = [];
1905
1897
  for (const row of all) {
1906
1898
  if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
1907
- const id = this._rowPkId(row, pkCols);
1899
+ const id = row._rid !== undefined ? row._rid : row.id;
1908
1900
  if (id !== undefined) ids.push(id);
1909
1901
  }
1910
1902
  }
@@ -2174,19 +2166,6 @@ class SQLExecutor {
2174
2166
  return schema;
2175
2167
  }
2176
2168
 
2177
- /**
2178
- * 取行的行 ID:优先内部 _rid,否则用实际主键字段值(不再硬编码 id)。
2179
- */
2180
- _rowPkId(row, pkCols) {
2181
- if (row && row._rid !== undefined) return row._rid;
2182
- if (pkCols.length > 0) {
2183
- for (const c of pkCols) {
2184
- if (row[c] !== undefined && row[c] !== null) return row[c];
2185
- }
2186
- }
2187
- return row ? row.id : undefined;
2188
- }
2189
-
2190
2169
  async _readTable(table) {
2191
2170
  const schema = await this._getSchema(table);
2192
2171
  const rows = (await this.engine.find(table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
@@ -2387,19 +2366,6 @@ class SQLExecutor {
2387
2366
  return out;
2388
2367
  }
2389
2368
 
2390
- /**
2391
- * 生成对端表的前缀 null 行:仅含 `prefix.col` 键(值为 null),
2392
- * 用于 JOIN 未匹配行补齐限定列,避免回退到未前缀副本拿错值。
2393
- */
2394
- _nullPrefixedRow(schema, prefix) {
2395
- const out = {};
2396
- for (const k of Object.keys(schema)) {
2397
- if (k === '_softDelete') continue;
2398
- out[prefix + '.' + k] = null;
2399
- }
2400
- return out;
2401
- }
2402
-
2403
2369
  _aggValue(rows, fn, column) {
2404
2370
  if (fn === 'COUNT') return rows.length;
2405
2371
  const op = typeof column === 'string' ? { type: 'column', name: column } : column;
@@ -2479,7 +2445,6 @@ class SQLExecutor {
2479
2445
 
2480
2446
  let schema = null;
2481
2447
  let all;
2482
- let rowsAll;
2483
2448
  if (!statement.from) {
2484
2449
  // 无 FROM:虚拟行(SELECT 1, 'a')
2485
2450
  all = [{ _virtual: true }];
@@ -2504,6 +2469,7 @@ class SQLExecutor {
2504
2469
  const rows = filtered.map(r => outCols.map(c => (c in r ? r[c] : null)));
2505
2470
  return { ok: true, type: 'select', table: firstItem.table, columns: outCols, rows, raw: filtered };
2506
2471
  }
2472
+ let rowsAll;
2507
2473
  if (firstItem.subquery) {
2508
2474
  const res = await this.executeSelect(firstItem.subquery);
2509
2475
  rowsAll = { rows: this._subQueryRows(res), schema: null, columns: res.columns };
@@ -2529,10 +2495,6 @@ class SQLExecutor {
2529
2495
  }
2530
2496
  const matched = [];
2531
2497
  const unmatchedRight = new Set(rightRows.map((r, i) => i));
2532
- // 未匹配行补对端表的前缀 null 列:限定列名(如 a.id / b.id)按前缀解析,
2533
- // 避免回退到未前缀副本拿到错误值。
2534
- const rightNulls = (rightRes.schema) ? this._nullPrefixedRow(rightRes.schema, rightPrefix) : null;
2535
- const leftNulls = (rowsAll && rowsAll.schema) ? this._nullPrefixedRow(rowsAll.schema, firstPrefix) : null;
2536
2498
  rows.forEach(l => {
2537
2499
  let m = null;
2538
2500
  for (let ri = 0; ri < rightRows.length; ri++) {
@@ -2545,11 +2507,11 @@ class SQLExecutor {
2545
2507
  matched.push({ ...l, ...rightRows[m] });
2546
2508
  unmatchedRight.delete(m);
2547
2509
  } else if (j.type === 'left') {
2548
- matched.push(rightNulls ? { ...rightNulls, ...l } : { ...l });
2510
+ matched.push({ ...l });
2549
2511
  }
2550
2512
  });
2551
2513
  if (j.type === 'right') {
2552
- for (const ri of unmatchedRight) matched.push(leftNulls ? { ...leftNulls, ...rightRows[ri] } : { ...rightRows[ri] });
2514
+ for (const ri of unmatchedRight) matched.push({ ...rightRows[ri] });
2553
2515
  }
2554
2516
  rows = matched;
2555
2517
  }
package/lib/table.js CHANGED
@@ -242,23 +242,6 @@ class Table {
242
242
  const schema = this._schema;
243
243
  const checkConstraints = this._checkConstraints;
244
244
  const foreignKeys = this._foreignKeys;
245
- // 批量预分配自增 ID 范围:先扫描显式提供的最大值,一次性推进计数器,
246
- // 再在循环内用本地序号递增,避免批量内重复读取/写入同一计数器字段。
247
- const baseAutoInc = this._autoIncrement;
248
- if (autoIncField) {
249
- let maxExplicit = 0;
250
- for (const it of items) {
251
- const v = it[autoIncField];
252
- if (v !== undefined && v !== null) {
253
- const n = Number(v);
254
- if (!isNaN(n) && n > maxExplicit) maxExplicit = n;
255
- }
256
- }
257
- if (maxExplicit > baseAutoInc) {
258
- this._autoIncrement = maxExplicit;
259
- }
260
- }
261
- let autoIncSeq = 0;
262
245
  for (let i = 0; i < N; i++) {
263
246
  let data = items[i];
264
247
  for (const [f, dv] of Object.entries(defaults)) {
@@ -296,9 +279,8 @@ class Table {
296
279
  }
297
280
  }
298
281
  if (autoIncField && data[autoIncField] === undefined) {
299
- autoIncSeq++;
300
- data[autoIncField] = baseAutoInc + autoIncSeq;
301
- this._autoIncrement = data[autoIncField];
282
+ this._autoIncrement++;
283
+ data[autoIncField] = this._autoIncrement;
302
284
  } else if (autoIncField && data[autoIncField] > this._autoIncrement) {
303
285
  this._autoIncrement = data[autoIncField];
304
286
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "5.1.3",
3
+ "version": "5.2.0",
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",
@@ -49,8 +49,7 @@
49
49
  "test": "node test/smoke.js",
50
50
  "test:btree": "node test/btree.test.js",
51
51
  "test:regress": "node test/regress-5.1.0.js",
52
- "test:all": "node test/smoke.js && node test/coverage.js && node test/regress-5.1.0.js && node test/btree.test.js && node test/join.test.js",
53
- "test:join": "node test/join.test.js",
52
+ "test:all": "node test/smoke.js && node test/coverage.js && node test/regress-5.1.0.js && node test/btree.test.js",
54
53
  "test:coverage": "c8 --all --include='lib/**' --exclude='lib/mysql_server.js' --exclude='lib/native_client.js' --exclude='lib/wasm_client.js' --exclude='lib/mysql_compat.js' --exclude='lib/nedb_compat.js' --exclude='lib/plugin.js' --reporter=text --reporter=lcov node test/coverage.js",
55
54
  "test:orms": "node examples/orms/run-all.js"
56
55
  },
package/test/join.test.js DELETED
@@ -1,110 +0,0 @@
1
- /*
2
- * Regression tests for SQL JOIN null-fill correctness.
3
- * Zero runtime dependencies (uses Node builtins + in-repo libs only).
4
- *
5
- * node test/join.test.js
6
- *
7
- * Covers:
8
- * J1 LEFT JOIN unmatched rows null-fill right-table qualified columns
9
- * J2 RIGHT JOIN unmatched rows null-fill left-table qualified columns
10
- * J3 INNER JOIN unaffected
11
- * J4 unqualified column reference in JOIN
12
- * J5 WHERE filtering on the null-filled side
13
- * J6 self-join with table alias
14
- * J7 chained joins
15
- */
16
- const path = require('path');
17
-
18
- const ROOT = path.join(__dirname, '..');
19
- const Database = require(path.join(ROOT, 'lib/database'));
20
- const { executeSQL } = require(path.join(ROOT, 'lib/sql'));
21
-
22
- let passed = 0, failed = 0;
23
- function ok(name, cond, extra) {
24
- if (cond) { passed++; console.log('[OK]', name); }
25
- else { failed++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra) : ''); }
26
- }
27
-
28
- async function setup(db) {
29
- await executeSQL(db, 'CREATE TABLE a (id INT PRIMARY KEY, a_name STRING)');
30
- await executeSQL(db, 'CREATE TABLE b (id INT PRIMARY KEY, a_id INT, b_name STRING)');
31
- await executeSQL(db, "INSERT INTO a VALUES (1,'A1'),(2,'A2'),(3,'A3')");
32
- await executeSQL(db, "INSERT INTO b VALUES (10,2,'B2'),(20,99,'B99')");
33
- }
34
-
35
- (async () => {
36
- /* ============ J1 LEFT JOIN null-fill ============ */
37
- {
38
- const db = new Database(':memory:', { autoSave: false });
39
- await setup(db);
40
- const r = await executeSQL(db, 'SELECT a.id AS aid, a.a_name, b.id AS bid, b.b_name FROM a LEFT JOIN b ON a.id = b.a_id');
41
- const row1 = r.rows[0]; // a=1 unmatched
42
- ok('J1 left unmatched row', row1 && row1[0] === 1 && row1[1] === 'A1' && row1[2] === null && row1[3] === null, row1);
43
- const row3 = r.rows[2]; // a=3 unmatched
44
- ok('J1 second unmatched row', row3 && row3[0] === 3 && row3[2] === null, row3);
45
- const row2 = r.rows[1]; // matched
46
- ok('J1 matched row intact', row2 && row2[0] === 2 && row2[1] === 'A2' && row2[2] === 10 && row2[3] === 'B2', row2);
47
- }
48
-
49
- /* ============ J2 RIGHT JOIN null-fill ============ */
50
- {
51
- const db = new Database(':memory:', { autoSave: false });
52
- await setup(db);
53
- const r = await executeSQL(db, 'SELECT a.id AS aid, a.a_name, b.id AS bid, b.b_name FROM a RIGHT JOIN b ON a.id = b.a_id');
54
- const b99 = r.rows[1]; // b=20 unmatched (a_id=99)
55
- ok('J2 right unmatched row', b99 && b99[0] === null && b99[1] === null && b99[2] === 20 && b99[3] === 'B99', b99);
56
- const b2 = r.rows[0]; // matched
57
- ok('J2 matched row intact', b2 && b2[0] === 2 && b2[1] === 'A2' && b2[2] === 10 && b2[3] === 'B2', b2);
58
- }
59
-
60
- /* ============ J3 INNER JOIN ============ */
61
- {
62
- const db = new Database(':memory:', { autoSave: false });
63
- await setup(db);
64
- const r = await executeSQL(db, 'SELECT a.id AS aid, b.id AS bid FROM a INNER JOIN b ON a.id = b.a_id');
65
- ok('J3 inner join', r.rows.length === 1 && r.rows[0][0] === 2 && r.rows[0][1] === 10, r.rows);
66
- }
67
-
68
- /* ============ J4 unqualified reference ============ */
69
- {
70
- const db = new Database(':memory:', { autoSave: false });
71
- await setup(db);
72
- const r = await executeSQL(db, 'SELECT a.id AS aid, b.b_name FROM a LEFT JOIN b ON a.id = b.a_id');
73
- ok('J4 unprefixed id resolves to left table', r.rows[0][0] === 1 && r.rows[0][1] === null, r.rows[0]);
74
- }
75
-
76
- /* ============ J5 WHERE on null-filled side ============ */
77
- {
78
- const db = new Database(':memory:', { autoSave: false });
79
- await setup(db);
80
- const r = await executeSQL(db, "SELECT a.id AS aid, b.b_name FROM a LEFT JOIN b ON a.id = b.a_id WHERE b.id IS NULL");
81
- ok('J5 left where b.id IS NULL', r.rows.length === 2 && r.rows.every(x => x[1] === null), r.rows);
82
- const r2 = await executeSQL(db, "SELECT a.a_name, b.id AS bid FROM a RIGHT JOIN b ON a.id = b.a_id WHERE a.id IS NULL");
83
- ok('J5 right where a.id IS NULL', r2.rows.length === 1 && r2.rows[0][0] === null && r2.rows[0][1] === 20, r2.rows);
84
- }
85
-
86
- /* ============ J6 self-join with alias ============ */
87
- {
88
- const db = new Database(':memory:', { autoSave: false });
89
- await executeSQL(db, 'CREATE TABLE emp (id INT PRIMARY KEY, mgr_id INT, name STRING)');
90
- await executeSQL(db, "INSERT INTO emp VALUES (1,NULL,'boss'),(2,1,'alice'),(3,999,'orphan')");
91
- const r = await executeSQL(db, 'SELECT e.name AS ename, m.name AS mname FROM emp e LEFT JOIN emp m ON e.mgr_id = m.id');
92
- ok('J6 self left join', r.rows.length === 3 && r.rows[0][0] === 'boss' && r.rows[0][1] === null && r.rows[1][1] === 'boss' && r.rows[2][1] === null, r.rows);
93
- }
94
-
95
- /* ============ J7 chained joins ============ */
96
- {
97
- const db = new Database(':memory:', { autoSave: false });
98
- await executeSQL(db, 'CREATE TABLE a (id INT PRIMARY KEY, a_name STRING)');
99
- await executeSQL(db, 'CREATE TABLE b (id INT PRIMARY KEY, a_id INT, b_name STRING)');
100
- await executeSQL(db, 'CREATE TABLE c (id INT PRIMARY KEY, b_id INT, c_name STRING)');
101
- await executeSQL(db, "INSERT INTO a VALUES (1,'A1'),(2,'A2'),(3,'A3')");
102
- await executeSQL(db, "INSERT INTO b VALUES (10,2,'B2'),(20,99,'B99')");
103
- await executeSQL(db, "INSERT INTO c VALUES (100,10,'C10'),(200,77,'C77')");
104
- const r = await executeSQL(db, 'SELECT a.id AS aid, b.id AS bid, c.id AS cid, c.c_name FROM a LEFT JOIN b ON a.id = b.a_id LEFT JOIN c ON b.id = c.b_id');
105
- ok('J7 chained left joins', r.rows.length === 3 && r.rows[1][1] === 10 && r.rows[1][2] === 100 && r.rows[1][3] === 'C10' && r.rows[0][1] === null && r.rows[0][2] === null, r.rows);
106
- }
107
-
108
- console.log(failed === 0 ? `\nALL ${passed} JOIN TESTS PASSED` : `\n${failed} FAILURES (${passed} passed)`);
109
- process.exit(failed === 0 ? 0 : 1);
110
- })().catch((e) => { console.error('FATAL', e); process.exit(1); });