jsql-neo 5.3.1 → 5.4.1
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 +0 -0
- package/README.md +240 -33
- package/bin/jsql +0 -0
- package/bin/jsql-server +0 -0
- package/bin/jsql-server-control.js +0 -0
- package/index.d.ts +0 -0
- package/index.js +0 -0
- package/lib/btree.js +0 -0
- package/lib/cache.js +0 -0
- package/lib/client.js +0 -0
- package/lib/database.js +0 -0
- package/lib/date-types.js +0 -0
- package/lib/errors.js +0 -0
- package/lib/jsql_format.js +0 -0
- package/lib/migrate.js +0 -0
- package/lib/mod.js +0 -0
- package/lib/mongo_server.js +1 -1
- package/lib/multiserver.js +0 -0
- package/lib/mysql_compat.js +0 -0
- package/lib/mysql_server.js +0 -0
- package/lib/native_client.js +182 -23
- package/lib/nedb_compat.js +0 -0
- package/lib/pg_server.js +0 -0
- package/lib/plugin.js +0 -0
- package/lib/query.js +0 -0
- package/lib/redis_server.js +0 -0
- package/lib/sql.js +0 -0
- package/lib/sqlite_compat.js +0 -0
- package/lib/sqlite_worker.js +5 -1
- package/lib/table.js +0 -0
- package/lib/tui.js +0 -0
- package/lib/wasm.js +0 -0
- package/lib/wasm_client.js +200 -23
- package/lib/web_ui.js +1 -1
- package/nativesrc/jsql-neo-core/Cargo.lock +0 -0
- package/nativesrc/jsql-neo-core/Cargo.toml +1 -1
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +0 -0
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +0 -0
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +0 -0
- package/nativesrc/jsql-neo-core/src/engine/table.rs +0 -0
- package/nativesrc/jsql-neo-core/src/lib.rs +0 -0
- package/nativesrc/jsql-neo-core/src/storage/mod.rs +0 -0
- package/nativesrc/jsql-neo-core/src/storage/persistent.rs +0 -0
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +0 -0
- package/nativesrc/jsql-neo-core/src/types.rs +0 -0
- package/nativesrc/jsql-neo-native/Cargo.lock +0 -0
- package/nativesrc/jsql-neo-native/Cargo.toml +1 -1
- package/nativesrc/jsql-neo-native/build.rs +0 -0
- package/nativesrc/jsql-neo-native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-native/package.json +0 -0
- package/nativesrc/jsql-neo-native/src/lib.rs +0 -0
- package/nativesrc/jsql-neo-wasm/Cargo.lock +2 -2
- package/nativesrc/jsql-neo-wasm/Cargo.toml +1 -1
- package/nativesrc/jsql-neo-wasm/src/lib.rs +24 -0
- package/package.json +3 -3
- package/postinstall.js +146 -25
- package/test/btree.test.js +0 -0
- package/test/coverage.js +0 -0
- package/test/join.test.js +110 -0
- package/test/native.test.js +159 -0
- package/test/regress-5.1.0.js +0 -0
- package/test/smoke.js +0 -0
- package/test/wasm.test.js +111 -0
- package/wasm/browser.d.ts +81 -80
- package/wasm/browser.mjs +407 -242
- package/wasm/browser_bg.mjs +166 -31
- package/wasm/jsql_neo_wasm.d.ts +6 -0
- package/wasm/jsql_neo_wasm.js +57 -0
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +3 -0
- package/wasm/package.json +1 -1
- package/bin/jsql-neo-server +0 -0
- package/lib/compress_pool.js +0 -122
- package/lib/compress_worker.js +0 -32
- package/wasm/jsql_neo_wasm_bg.js +0 -373
package/lib/web_ui.js
CHANGED
|
@@ -111,7 +111,7 @@ load();
|
|
|
111
111
|
|
|
112
112
|
class WebUI {
|
|
113
113
|
constructor(opts = {}) {
|
|
114
|
-
this.port = opts.port
|
|
114
|
+
this.port = opts.port !== undefined ? opts.port : 8080;
|
|
115
115
|
this.dataDir = opts.dataDir || '.';
|
|
116
116
|
this.readonly = !!opts.readonly;
|
|
117
117
|
this.host = opts.host || '127.0.0.1';
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -171,7 +171,7 @@ dependencies = [
|
|
|
171
171
|
|
|
172
172
|
[[package]]
|
|
173
173
|
name = "jsql-neo-core"
|
|
174
|
-
version = "
|
|
174
|
+
version = "5.4.0"
|
|
175
175
|
dependencies = [
|
|
176
176
|
"bincode",
|
|
177
177
|
"chrono",
|
|
@@ -187,7 +187,7 @@ dependencies = [
|
|
|
187
187
|
|
|
188
188
|
[[package]]
|
|
189
189
|
name = "jsql-neo-wasm"
|
|
190
|
-
version = "
|
|
190
|
+
version = "5.4.0"
|
|
191
191
|
dependencies = [
|
|
192
192
|
"js-sys",
|
|
193
193
|
"jsql-neo-core",
|
|
@@ -421,3 +421,27 @@ pub fn jsql_find_by_ids(table: &str, ids_json: &str) -> String {
|
|
|
421
421
|
}
|
|
422
422
|
})
|
|
423
423
|
}
|
|
424
|
+
|
|
425
|
+
#[wasm_bindgen]
|
|
426
|
+
pub fn jsql_begin_tx() -> String {
|
|
427
|
+
ENGINE.with(|eng| match eng.borrow_mut().begin_tx() {
|
|
428
|
+
Ok(tx_id) => serde_json::json!({ "ok": true, "txId": tx_id }).to_string(),
|
|
429
|
+
Err(e) => format!(r#"{{"ok":false,"error":"{}"}}"#, e),
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
#[wasm_bindgen]
|
|
434
|
+
pub fn jsql_commit_tx(tx_id: &str) -> String {
|
|
435
|
+
ENGINE.with(|eng| match eng.borrow_mut().commit_tx(tx_id) {
|
|
436
|
+
Ok(()) => r#"{"ok":true}"#.to_string(),
|
|
437
|
+
Err(e) => format!(r#"{{"ok":false,"error":"{}"}}"#, e),
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#[wasm_bindgen]
|
|
442
|
+
pub fn jsql_rollback_tx(tx_id: &str) -> String {
|
|
443
|
+
ENGINE.with(|eng| match eng.borrow_mut().rollback_tx(tx_id) {
|
|
444
|
+
Ok(()) => r#"{"ok":true}"#.to_string(),
|
|
445
|
+
Err(e) => format!(r#"{{"ok":false,"error":"{}"}}"#, e),
|
|
446
|
+
})
|
|
447
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.1",
|
|
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",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"types": "./wasm/browser.d.ts",
|
|
20
20
|
"default": "./wasm/browser.mjs"
|
|
21
21
|
},
|
|
22
|
-
"./wasm/browser_bg.mjs": "./wasm/browser_bg.mjs",
|
|
23
22
|
"./wasm/browser.d.ts": "./wasm/browser.d.ts",
|
|
24
23
|
"./wasm/*": "./wasm/*",
|
|
25
24
|
"./lib/*.js": "./lib/*.js",
|
|
@@ -48,8 +47,9 @@
|
|
|
48
47
|
"postinstall": "node postinstall.js",
|
|
49
48
|
"test": "node test/smoke.js",
|
|
50
49
|
"test:btree": "node test/btree.test.js",
|
|
50
|
+
"test:native": "node test/native.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",
|
|
52
|
+
"test:all": "node test/smoke.js && node test/native.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js",
|
|
53
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",
|
|
54
54
|
"test:orms": "node examples/orms/run-all.js"
|
|
55
55
|
},
|
package/postinstall.js
CHANGED
|
@@ -1,38 +1,159 @@
|
|
|
1
1
|
const { execSync } = require('child_process');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const https = require('https');
|
|
4
6
|
|
|
7
|
+
const ROOT = __dirname;
|
|
8
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
9
|
+
|
|
10
|
+
// Server CLI binary (bin/jsql-neo-server)
|
|
5
11
|
const BIN_NAME = process.platform === 'win32' ? 'jsql-neo-server.exe' : 'jsql-neo-server';
|
|
6
|
-
const BP = path.join(
|
|
12
|
+
const BP = path.join(ROOT, 'bin', BIN_NAME);
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
// Native N-API module (native/jsql-neo-native.node)
|
|
15
|
+
const NATIVE_PATH = path.join(ROOT, 'native', 'jsql-neo-native.node');
|
|
16
|
+
|
|
17
|
+
// GitHub 加速代理前缀;把原始 github 链接直接拼在后面即可
|
|
18
|
+
const GHPROXY = 'https://gh-proxy.com/';
|
|
19
|
+
const TAG = `v${pkg.version}`;
|
|
20
|
+
|
|
21
|
+
// 从 repository 字段提取 owner/repo,例如 vexify-org/JSQL-neo
|
|
22
|
+
const REPO = (pkg.repository && pkg.repository.url)
|
|
23
|
+
? pkg.repository.url.replace(/\.git$/, '').replace(/^.*github\.com\//i, '')
|
|
24
|
+
: 'vexify-org/JSQL-neo';
|
|
25
|
+
|
|
26
|
+
// 与 .github/workflows/build-native.yml 中发布的资产命名保持一致
|
|
27
|
+
function nativeTargetSuffix(platform = process.platform, arch = process.arch) {
|
|
28
|
+
const p = platform === 'win32' ? 'win32' : platform === 'darwin' ? 'darwin' : 'linux';
|
|
29
|
+
const a = arch === 'x64' ? 'x64' : arch === 'arm64' ? 'arm64' : arch;
|
|
30
|
+
if (p === 'linux') return `${p}-${a}-gnu`;
|
|
31
|
+
if (p === 'darwin') return `${p}-${a}`;
|
|
32
|
+
return `${p}-${a}-msvc`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function download(url, dest) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const mod = url.startsWith('https:') ? https : http;
|
|
38
|
+
const req = mod.get(url, (res) => {
|
|
39
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
40
|
+
res.resume();
|
|
41
|
+
download(res.headers.location, dest).then(resolve, reject);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (res.statusCode !== 200) {
|
|
45
|
+
res.resume();
|
|
46
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
12
47
|
return;
|
|
48
|
+
}
|
|
49
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
50
|
+
const ws = fs.createWriteStream(dest);
|
|
51
|
+
ws.on('error', reject);
|
|
52
|
+
res.pipe(ws);
|
|
53
|
+
res.on('end', () => ws.end(() => resolve()));
|
|
54
|
+
});
|
|
55
|
+
req.on('error', reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildNativeFromSource() {
|
|
60
|
+
const crate = path.join(ROOT, 'nativesrc', 'jsql-neo-native');
|
|
61
|
+
if (!fs.existsSync(path.join(crate, 'Cargo.toml'))) return;
|
|
62
|
+
console.log('[jsql-neo] building native module from source...');
|
|
63
|
+
try {
|
|
64
|
+
execSync('cargo build --release', { cwd: crate, stdio: 'inherit' });
|
|
65
|
+
const rel = path.join(crate, 'target', 'release');
|
|
66
|
+
const src = process.platform === 'win32'
|
|
67
|
+
? path.join(rel, 'jsql_neo_native.dll')
|
|
68
|
+
: process.platform === 'darwin'
|
|
69
|
+
? path.join(rel, 'libjsql_neo_native.dylib')
|
|
70
|
+
: path.join(rel, 'libjsql_neo_native.so');
|
|
71
|
+
if (fs.existsSync(src)) {
|
|
72
|
+
fs.mkdirSync(path.dirname(NATIVE_PATH), { recursive: true });
|
|
73
|
+
fs.copyFileSync(src, NATIVE_PATH);
|
|
74
|
+
fs.chmodSync(NATIVE_PATH, 0o755);
|
|
75
|
+
console.log(`[jsql-neo] native module built: native/jsql-neo-native.node`);
|
|
13
76
|
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.warn('[jsql-neo] native build failed:', e.message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
14
81
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
82
|
+
// 尝试加载 native 模块;能成功 require 才算「可用」,否则视为不可用
|
|
83
|
+
function tryLoadNative() {
|
|
84
|
+
if (!fs.existsSync(NATIVE_PATH)) return false;
|
|
85
|
+
try {
|
|
86
|
+
require(NATIVE_PATH);
|
|
87
|
+
fs.chmodSync(NATIVE_PATH, 0o755);
|
|
88
|
+
return true;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.warn(`[jsql-neo] existing native module cannot be loaded (${e.code || e.message}), will re-acquire.`);
|
|
91
|
+
try { fs.unlinkSync(NATIVE_PATH); } catch (_) { /* ignore */ }
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function downloadAndVerify() {
|
|
97
|
+
const asset = `jsql-neo-native.${nativeTargetSuffix()}.node`;
|
|
98
|
+
const url = `${GHPROXY}https://github.com/${REPO}/releases/download/${TAG}/${asset}`;
|
|
99
|
+
console.log(`[jsql-neo] downloading native module: ${url}`);
|
|
100
|
+
try {
|
|
101
|
+
await download(url, NATIVE_PATH);
|
|
102
|
+
if (!tryLoadNative()) {
|
|
103
|
+
console.warn('[jsql-neo] downloaded module also invalid, removed.');
|
|
104
|
+
return false;
|
|
32
105
|
}
|
|
106
|
+
console.log('[jsql-neo] native module downloaded: native/jsql-neo-native.node');
|
|
107
|
+
return true;
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.warn('[jsql-neo] download failed, falling back to source build:', e.message);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function ensureNativeNode() {
|
|
115
|
+
// 已存在且能加载 → 直接用;否则删掉并从 gh-proxy.com 下载,找不到才回退源码编译
|
|
116
|
+
if (tryLoadNative()) {
|
|
117
|
+
console.log('[jsql-neo] native module ready: native/jsql-neo-native.node');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (await downloadAndVerify()) return;
|
|
121
|
+
buildNativeFromSource();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function ensureServerBinary() {
|
|
125
|
+
if (fs.existsSync(BP)) {
|
|
126
|
+
fs.chmodSync(BP, 0o755);
|
|
127
|
+
console.log(`[jsql-neo] binary ready: ${BP}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Build from source
|
|
132
|
+
const serverDir = path.join(ROOT, '..', 'jsql-neo-server');
|
|
133
|
+
if (fs.existsSync(path.join(serverDir, 'Cargo.toml'))) {
|
|
134
|
+
console.log('[jsql-neo] building Rust server from source...');
|
|
135
|
+
try {
|
|
136
|
+
execSync('cargo build --release', { cwd: serverDir, stdio: 'inherit' });
|
|
137
|
+
const src = path.join(serverDir, 'target', 'release', BIN_NAME);
|
|
138
|
+
if (fs.existsSync(src)) {
|
|
139
|
+
fs.mkdirSync(path.join(ROOT, 'bin'), { recursive: true });
|
|
140
|
+
fs.copyFileSync(src, BP);
|
|
141
|
+
fs.chmodSync(BP, 0o755);
|
|
142
|
+
console.log(`[jsql-neo] binary built: ${BP}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.warn('[jsql-neo] cargo build failed:', e.message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.warn(`[jsql-neo] server binary not found at ${BP}`);
|
|
151
|
+
console.warn('[jsql-neo] install Rust from https://rustup.rs and run: npm run build');
|
|
152
|
+
}
|
|
33
153
|
|
|
34
|
-
|
|
35
|
-
|
|
154
|
+
async function main() {
|
|
155
|
+
await ensureNativeNode();
|
|
156
|
+
ensureServerBinary();
|
|
36
157
|
}
|
|
37
158
|
|
|
38
|
-
main();
|
|
159
|
+
main();
|
package/test/btree.test.js
CHANGED
|
File without changes
|
package/test/coverage.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,110 @@
|
|
|
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); });
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Native (.node N-API) regression tests
|
|
3
|
+
* node test/native.test.js
|
|
4
|
+
* Covers: NativeJSQL CRUD + flat row shape, shorthand/snake_case schemas,
|
|
5
|
+
* disk persistence, and the better-sqlite3 compat layer on the native engine.
|
|
6
|
+
*/
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const ROOT = path.join(__dirname, '..');
|
|
11
|
+
|
|
12
|
+
let passed = 0, failed = 0;
|
|
13
|
+
function ok(name, cond, extra) {
|
|
14
|
+
if (cond) { passed++; console.log('[OK]', name); }
|
|
15
|
+
else { failed++; console.log('[FAIL]', name, extra !== undefined ? JSON.stringify(extra) : ''); }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
(async () => {
|
|
19
|
+
const { JSQL } = require(path.join(ROOT, 'lib', 'native_client'));
|
|
20
|
+
|
|
21
|
+
/* ---------- basic CRUD + flat row shape ---------- */
|
|
22
|
+
{
|
|
23
|
+
const db = new JSQL();
|
|
24
|
+
await db.start();
|
|
25
|
+
await db.createTable('users', { id: 'integer primary key auto_increment', name: 'string', age: 'integer' });
|
|
26
|
+
const ids = await db.insert('users', [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]);
|
|
27
|
+
ok('batch insert returns ids', Array.isArray(ids) && ids.length === 2, ids);
|
|
28
|
+
|
|
29
|
+
const row = db.findById('users', 1);
|
|
30
|
+
ok('findById returns flat row', row && row.id === 1 && row.name === 'Alice' && row.age === 30 && !('fields' in row), row);
|
|
31
|
+
|
|
32
|
+
const rows = await db.find('users', {});
|
|
33
|
+
ok('find returns flat rows', rows.length === 2 && rows[1].name === 'Bob' && rows[1].id === 2, rows);
|
|
34
|
+
ok('filter match', (await db.find('users', { age: 25 })).length === 1);
|
|
35
|
+
ok('filter no match', (await db.find('users', { age: 99 })).length === 0);
|
|
36
|
+
ok('count', (await db.count('users')) === 2);
|
|
37
|
+
|
|
38
|
+
db.updateByIds('users', [[1, { age: 99 }]]);
|
|
39
|
+
await new Promise(r => setTimeout(r, 150));
|
|
40
|
+
ok('updateByIds applied', db.findById('users', 1).age === 99, db.findById('users', 1));
|
|
41
|
+
|
|
42
|
+
db.removeByIds('users', [2]);
|
|
43
|
+
await new Promise(r => setTimeout(r, 150));
|
|
44
|
+
ok('removeByIds applied', (await db.count('users')) === 1, await db.count('users'));
|
|
45
|
+
await db.stop();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ---------- shorthand and snake_case schemas ---------- */
|
|
49
|
+
{
|
|
50
|
+
const db = new JSQL();
|
|
51
|
+
await db.start();
|
|
52
|
+
await db.createTable('t2', { name: 'string unique', n: 'number', ok: 'boolean' });
|
|
53
|
+
await db.insert('t2', { name: 'x', n: 1.5, ok: true });
|
|
54
|
+
const r2 = (await db.find('t2', {}))[0];
|
|
55
|
+
ok('shorthand schema fields', r2.name === 'x' && r2.n === 1.5 && r2.ok === true, r2);
|
|
56
|
+
|
|
57
|
+
await db.createTable('t3', { id: { type: 'integer', primary_key: true, auto_increment: true }, name: { type: 'string' } });
|
|
58
|
+
await db.insert('t3', { name: 'a' });
|
|
59
|
+
await db.insert('t3', { id: 7, name: 'b' });
|
|
60
|
+
ok('snake_case pk respected', db.findById('t3', 7).name === 'b', db.findById('t3', 7));
|
|
61
|
+
ok('explicit pk bumps next id', db.findById('t3', 1).name === 'a');
|
|
62
|
+
|
|
63
|
+
await db.createTable('cfg', 'key string primary key, value string');
|
|
64
|
+
const cfgIds = await db.insert('cfg', { key: 'a', value: '1' });
|
|
65
|
+
ok('full shorthand string schema', cfgIds[0] === 'a' && db.findById('cfg', 'a').value === '1', cfgIds);
|
|
66
|
+
await db.stop();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ---------- disk persistence roundtrip ---------- */
|
|
70
|
+
{
|
|
71
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-native-'));
|
|
72
|
+
const db = new JSQL({ path: dir, mode: 'disk' });
|
|
73
|
+
await db.start();
|
|
74
|
+
await db.createTable('p', { id: { type: 'integer', primaryKey: true, autoIncrement: true }, v: { type: 'string' } });
|
|
75
|
+
await db.insert('p', { v: 'persisted' });
|
|
76
|
+
await db.stop();
|
|
77
|
+
const db2 = new JSQL({ path: dir, mode: 'disk' });
|
|
78
|
+
await db2.start();
|
|
79
|
+
ok('tables restored', db2.getTables().includes('p'));
|
|
80
|
+
ok('rows persisted', (await db2.find('p', {}))[0].v === 'persisted', await db2.find('p', {}));
|
|
81
|
+
await db2.stop();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ---------- better-sqlite3 compat on native engine ---------- */
|
|
85
|
+
{
|
|
86
|
+
const Database = require(path.join(ROOT, 'lib', 'sqlite_compat'));
|
|
87
|
+
const db = new Database(':memory:', { engine: 'native' });
|
|
88
|
+
db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING, age INTEGER)');
|
|
89
|
+
const r = db.prepare('INSERT INTO t (name, age) VALUES (?, ?)').run('a', 1);
|
|
90
|
+
ok('compat run returns changes', r.changes === 1 && r.lastInsertRowid === 1, r);
|
|
91
|
+
db.prepare('INSERT INTO t (name, age) VALUES (?, ?)').run('b', 2);
|
|
92
|
+
ok('compat select', db.prepare('SELECT * FROM t WHERE age = ?').get(1).name === 'a');
|
|
93
|
+
db.prepare('UPDATE t SET age = 99 WHERE name = ?').run('b');
|
|
94
|
+
ok('compat update', db.prepare('SELECT age FROM t WHERE name = ?').get('b').age === 99);
|
|
95
|
+
db.exec('BEGIN');
|
|
96
|
+
db.prepare('INSERT INTO t (name) VALUES (?)').run('tx');
|
|
97
|
+
db.exec('ROLLBACK');
|
|
98
|
+
ok('compat rollback', db.prepare('SELECT COUNT(*) AS n FROM t').get().n === 2);
|
|
99
|
+
const buf = db.serialize();
|
|
100
|
+
db.close();
|
|
101
|
+
const db2 = new Database(':memory:', { engine: 'native' });
|
|
102
|
+
db2.deserialize(buf);
|
|
103
|
+
ok('compat serialize roundtrip', db2.prepare('SELECT COUNT(*) AS n FROM t').get().n === 2);
|
|
104
|
+
db2.close();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* ---------- non-auto pk: id resolution by pk value ---------- */
|
|
108
|
+
{
|
|
109
|
+
const db = new JSQL();
|
|
110
|
+
await db.start();
|
|
111
|
+
await db.createTable('config', { key: { type: 'string', primaryKey: true }, value: 'string' });
|
|
112
|
+
const ids = await db.insert('config', [{ key: 'a', value: '1' }, { key: 'b', value: '2' }]);
|
|
113
|
+
ok('non-auto pk insert returns pk values', JSON.stringify(ids) === JSON.stringify(['a', 'b']), ids);
|
|
114
|
+
|
|
115
|
+
const a = db.findById('config', 'a');
|
|
116
|
+
ok('findById resolves string pk', a && a.value === '1' && !('fields' in a), a);
|
|
117
|
+
ok('findById pk miss returns null', db.findById('config', 'nope') === null);
|
|
118
|
+
|
|
119
|
+
db.updateById('config', 'b', { value: '22' });
|
|
120
|
+
await new Promise(r => setTimeout(r, 150));
|
|
121
|
+
ok('updateById resolves string pk', db.findById('config', 'b').value === '22', db.findById('config', 'b'));
|
|
122
|
+
|
|
123
|
+
db.removeById('config', 'a');
|
|
124
|
+
await new Promise(r => setTimeout(r, 150));
|
|
125
|
+
ok('removeById resolves string pk', (await db.count('config')) === 1, await db.count('config'));
|
|
126
|
+
|
|
127
|
+
db.updateByIds('config', [['b', { value: '222' }]]);
|
|
128
|
+
await new Promise(r => setTimeout(r, 150));
|
|
129
|
+
ok('updateByIds resolves string pk', db.findById('config', 'b').value === '222', db.findById('config', 'b'));
|
|
130
|
+
|
|
131
|
+
db.removeByIds('config', ['b']);
|
|
132
|
+
await new Promise(r => setTimeout(r, 150));
|
|
133
|
+
ok('removeByIds resolves string pk', (await db.count('config')) === 0, await db.count('config'));
|
|
134
|
+
await db.stop();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ---------- transaction aliases (beginTransaction / rollback / commit) ---------- */
|
|
138
|
+
{
|
|
139
|
+
const db = new JSQL();
|
|
140
|
+
await db.start();
|
|
141
|
+
await db.createTable('tx', { id: 'integer primary key auto_increment', name: 'string' });
|
|
142
|
+
await db.insert('tx', { name: 'n1' });
|
|
143
|
+
await db.beginTransaction();
|
|
144
|
+
await db.insert('tx', { name: 'n2' });
|
|
145
|
+
await db.rollback();
|
|
146
|
+
ok('rollback reverts insert', (await db.count('tx')) === 1, await db.count('tx'));
|
|
147
|
+
await db.beginTransaction();
|
|
148
|
+
await db.insert('tx', { name: 'n3' });
|
|
149
|
+
await db.commit();
|
|
150
|
+
ok('commit keeps insert', (await db.count('tx')) === 2, await db.count('tx'));
|
|
151
|
+
await db.stop();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
155
|
+
process.exit(failed ? 1 : 0);
|
|
156
|
+
})().catch(e => {
|
|
157
|
+
console.error('[FATAL]', e.stack.split('\n').slice(0, 3).join('\n'));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
});
|
package/test/regress-5.1.0.js
CHANGED
|
File without changes
|
package/test/smoke.js
CHANGED
|
File without changes
|