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 +0 -2
- package/lib/query.js +2 -20
- package/lib/sql.js +6 -44
- package/lib/table.js +2 -20
- package/package.json +2 -3
- package/test/join.test.js +0 -110
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.
|
|
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.
|
|
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'
|
|
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 =
|
|
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 =
|
|
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(
|
|
2510
|
+
matched.push({ ...l });
|
|
2549
2511
|
}
|
|
2550
2512
|
});
|
|
2551
2513
|
if (j.type === 'right') {
|
|
2552
|
-
for (const ri of unmatchedRight) matched.push(
|
|
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
|
-
|
|
300
|
-
data[autoIncField] =
|
|
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.
|
|
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
|
|
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); });
|