jsql-neo 4.0.2 → 4.1.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/bin/jsql +69 -0
- package/bin/jsql-server +90 -0
- package/bin/jsql-server-control.js +177 -0
- package/lib/database.js +98 -9
- package/lib/errors.js +6 -0
- package/lib/jsql_format.js +33 -14
- package/lib/mod.js +43 -67
- package/lib/mysql_compat.js +134 -17
- package/lib/mysql_server.js +315 -22
- package/lib/native_client.js +95 -32
- package/lib/sql.js +646 -122
- package/lib/table.js +114 -12
- package/lib/wasm_client.js +59 -6
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.lock +455 -0
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +12 -6
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +28 -7
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +22 -1
- package/nativesrc/jsql-neo-core/src/engine/table.rs +53 -23
- package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -1
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +1 -0
- package/nativesrc/jsql-neo-native/build.rs +3 -0
- package/nativesrc/jsql-neo-native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-native/src/lib.rs +75 -87
- package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -0
- package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -0
- package/nativesrc/jsql-neo-wasm/src/lib.rs +423 -0
- package/package.json +8 -1
- package/wasm/jsql_neo_wasm.d.ts +2 -0
- package/wasm/jsql_neo_wasm.js +5 -0
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +1 -0
package/bin/jsql
CHANGED
|
@@ -15,6 +15,8 @@ function printModules(list) {
|
|
|
15
15
|
|
|
16
16
|
const manager = new ModuleManager();
|
|
17
17
|
|
|
18
|
+
const RAW_ARGV = process.argv.slice(2);
|
|
19
|
+
|
|
18
20
|
const cli = yaggs()
|
|
19
21
|
.usage('jsql <command> [options]')
|
|
20
22
|
.option('help', { alias: 'h', type: 'boolean', description: 'Show help' })
|
|
@@ -50,6 +52,10 @@ const cli = yaggs()
|
|
|
50
52
|
switch (action) {
|
|
51
53
|
case 'add': {
|
|
52
54
|
if (!argv.address) return fail(new Error("'mod add' requires --address <path>"));
|
|
55
|
+
const ext = path.extname(argv.address).toLowerCase();
|
|
56
|
+
if (ext === '.js' || ext === '.cjs' || ext === '.mjs') {
|
|
57
|
+
console.warn("Warning: JS modules run without sandbox isolation (trust model). Use .json files for data-only plugins.");
|
|
58
|
+
}
|
|
53
59
|
const entry = manager.add(argv.address);
|
|
54
60
|
out(useJson ? entry : `Module '${entry.name}' added (${entry.path}). Use 'jsql mod enable ${entry.name}' to enable.`);
|
|
55
61
|
break;
|
|
@@ -89,6 +95,69 @@ const cli = yaggs()
|
|
|
89
95
|
})
|
|
90
96
|
.command('version', 'Show version', null, () => {
|
|
91
97
|
console.log(require('../package.json').version);
|
|
98
|
+
})
|
|
99
|
+
.command('server', 'Run the MySQL-compatible server in background', (sub) => {
|
|
100
|
+
sub.option('port', { alias: 'p', type: 'number', description: 'Listen port (default 3306)' });
|
|
101
|
+
sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
|
|
102
|
+
sub.option('data-dir', { type: 'string', description: 'Directory to store databases (default in-memory)' });
|
|
103
|
+
sub.option('auth', { alias: 'a', type: 'string', description: 'User credential: user:password[:db1,db2] (repeatable)' });
|
|
104
|
+
sub.option('config', { type: 'string', description: 'Path to config file (JSON)' });
|
|
105
|
+
sub.option('no-auth', { type: 'boolean', description: 'Allow connections without authentication (local dev only)' });
|
|
106
|
+
}, (argv) => {
|
|
107
|
+
const { serverControl } = require('./jsql-server-control');
|
|
108
|
+
const action = argv._[0];
|
|
109
|
+
const fail = (err) => {
|
|
110
|
+
console.error(`Error: ${err.message}`);
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
};
|
|
113
|
+
const options = {};
|
|
114
|
+
if (argv.config) options.config = argv.config;
|
|
115
|
+
if (argv.port != null) options.port = argv.port;
|
|
116
|
+
if (argv.host != null) options.host = argv.host;
|
|
117
|
+
if (argv['data-dir'] != null) options.dataDir = argv['data-dir'];
|
|
118
|
+
if (argv['no-auth'] === true) options.noAuth = true;
|
|
119
|
+
const authSpecs = [];
|
|
120
|
+
for (let i = 0; i < RAW_ARGV.length; i++) {
|
|
121
|
+
const a = RAW_ARGV[i];
|
|
122
|
+
if (a === '--auth' || a === '-a') {
|
|
123
|
+
if (RAW_ARGV[i + 1]) authSpecs.push(RAW_ARGV[++i]);
|
|
124
|
+
} else if (a.startsWith('--auth=')) {
|
|
125
|
+
authSpecs.push(a.slice(7));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (authSpecs.length > 0) options.auth = authSpecs;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
switch (action) {
|
|
132
|
+
case 'start': {
|
|
133
|
+
serverControl.start(options).then(
|
|
134
|
+
(msg) => console.log(msg),
|
|
135
|
+
(err) => fail(err)
|
|
136
|
+
);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'stop': {
|
|
140
|
+
serverControl.stop(options).then(
|
|
141
|
+
(msg) => console.log(msg),
|
|
142
|
+
(err) => fail(err)
|
|
143
|
+
);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case 'status': {
|
|
147
|
+
serverControl.status(options).then(
|
|
148
|
+
(msg) => console.log(msg),
|
|
149
|
+
(err) => fail(err)
|
|
150
|
+
);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case undefined:
|
|
154
|
+
return fail(new Error("Missing action. Usage: jsql server <start|stop|status>"));
|
|
155
|
+
default:
|
|
156
|
+
return fail(new Error(`Unknown server action: ${action}`));
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {
|
|
159
|
+
fail(e);
|
|
160
|
+
}
|
|
92
161
|
});
|
|
93
162
|
|
|
94
163
|
cli.run(process.argv.slice(2)).catch(e => {
|
package/bin/jsql-server
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// © Vexify 2026 All Rights Reserved.
|
|
3
|
+
// jsql-neo 后台常驻服务入口 — 由 `jsql start` 以 detached 子进程方式拉起。
|
|
4
|
+
// 用法: node bin/jsql-server --config <config.json>
|
|
5
|
+
// 支持 SIGTERM/SIGINT 优雅关闭。
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const { createMysqlServer } = require('../lib/mysql_server');
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const args = { _: [] };
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
const a = argv[i];
|
|
15
|
+
if (a === '--config') { args.config = argv[++i]; }
|
|
16
|
+
else if (a.startsWith('--config=')) { args.config = a.slice(9); }
|
|
17
|
+
else args._.push(a);
|
|
18
|
+
}
|
|
19
|
+
return args;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function loadConfig(configPath) {
|
|
23
|
+
const cfg = { port: 3306, host: '127.0.0.1' };
|
|
24
|
+
if (configPath && fs.existsSync(configPath)) {
|
|
25
|
+
Object.assign(cfg, JSON.parse(fs.readFileSync(configPath, 'utf8')));
|
|
26
|
+
}
|
|
27
|
+
return cfg;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildAuth(cfg) {
|
|
31
|
+
if (cfg.auth) {
|
|
32
|
+
if (typeof cfg.auth !== 'object' || Array.isArray(cfg.auth)) {
|
|
33
|
+
throw new Error('config.auth must be an object map { user: "password" | { password, databases } }');
|
|
34
|
+
}
|
|
35
|
+
return cfg.auth;
|
|
36
|
+
}
|
|
37
|
+
if (cfg.user != null || cfg.password != null) {
|
|
38
|
+
const auth = {};
|
|
39
|
+
auth[cfg.user || 'root'] = cfg.password || '';
|
|
40
|
+
return auth;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function main() {
|
|
46
|
+
const args = parseArgs(process.argv.slice(2));
|
|
47
|
+
const cfg = loadConfig(args.config);
|
|
48
|
+
const auth = buildAuth(cfg);
|
|
49
|
+
const options = {
|
|
50
|
+
port: cfg.port || 3306,
|
|
51
|
+
host: cfg.host || '127.0.0.1',
|
|
52
|
+
dataDir: cfg.dataDir || null,
|
|
53
|
+
maxConnections: cfg.maxConnections,
|
|
54
|
+
idleTimeout: cfg.idleTimeout,
|
|
55
|
+
handshakeTimeout: cfg.handshakeTimeout,
|
|
56
|
+
safety: cfg.safety !== false,
|
|
57
|
+
allowComments: cfg.allowComments === true,
|
|
58
|
+
noAuth: cfg.noAuth === true,
|
|
59
|
+
};
|
|
60
|
+
if (auth) options.auth = auth;
|
|
61
|
+
if (cfg.onSecurityEvent) options.onSecurityEvent = cfg.onSecurityEvent;
|
|
62
|
+
|
|
63
|
+
const server = createMysqlServer(options);
|
|
64
|
+
|
|
65
|
+
let closing = false;
|
|
66
|
+
const shutdown = (code) => {
|
|
67
|
+
if (closing) return;
|
|
68
|
+
closing = true;
|
|
69
|
+
server.close(() => process.exit(code));
|
|
70
|
+
setTimeout(() => process.exit(code), 2000).unref();
|
|
71
|
+
};
|
|
72
|
+
process.on('SIGTERM', () => shutdown(0));
|
|
73
|
+
process.on('SIGINT', () => shutdown(0));
|
|
74
|
+
|
|
75
|
+
server.listen(err => {
|
|
76
|
+
if (err) {
|
|
77
|
+
console.error(`[jsql-server] failed to start: ${err.message}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const addr = server.address;
|
|
82
|
+
console.log(`[jsql-server] listening on ${options.host}:${addr ? addr.port : options.port} (dataDir: ${options.dataDir || 'in-memory'})`);
|
|
83
|
+
console.log(`[jsql-server] pid ${process.pid}`);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main().catch(e => {
|
|
88
|
+
console.error(`[jsql-server] ${e.message}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// © Vexify 2026 All Rights Reserved.
|
|
2
|
+
// jsql server start/stop/status — 后台常驻服务控制(对标 `net mysql80 start`)。
|
|
3
|
+
// 状态文件放在 ~/.jsql/ 下:server.pid / server.log / server.json(保存启动配置)。
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const STATE_DIR = path.join(os.homedir(), '.jsql');
|
|
11
|
+
const PID_FILE = path.join(STATE_DIR, 'server.pid');
|
|
12
|
+
const LOG_FILE = path.join(STATE_DIR, 'server.log');
|
|
13
|
+
const CONF_FILE = path.join(STATE_DIR, 'server.json');
|
|
14
|
+
|
|
15
|
+
function ensureStateDir() {
|
|
16
|
+
fs.mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readPid() {
|
|
20
|
+
try {
|
|
21
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
|
|
22
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isAlive(pid) {
|
|
29
|
+
if (!pid) return false;
|
|
30
|
+
try {
|
|
31
|
+
process.kill(pid, 0);
|
|
32
|
+
return true;
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return e.code === 'EPERM';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isPortOpen(port, host) {
|
|
39
|
+
return new Promise(resolve => {
|
|
40
|
+
const net = require('net');
|
|
41
|
+
const sock = net.connect({ port, host: host || '127.0.0.1' });
|
|
42
|
+
sock.once('connect', () => { sock.destroy(); resolve(true); });
|
|
43
|
+
sock.once('error', () => resolve(false));
|
|
44
|
+
setTimeout(() => { sock.destroy(); resolve(false); }, 800);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseAuthSpecs(list) {
|
|
49
|
+
const auth = {};
|
|
50
|
+
for (const spec of list) {
|
|
51
|
+
const idx = spec.indexOf(':');
|
|
52
|
+
if (idx <= 0) {
|
|
53
|
+
auth[spec] = '';
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const user = spec.slice(0, idx);
|
|
57
|
+
const rest = spec.slice(idx + 1);
|
|
58
|
+
const idx2 = rest.indexOf(':');
|
|
59
|
+
const password = idx2 === -1 ? rest : rest.slice(0, idx2);
|
|
60
|
+
const dbPart = idx2 === -1 ? '' : rest.slice(idx2 + 1);
|
|
61
|
+
const dbs = dbPart
|
|
62
|
+
? dbPart.split(',').map(s => s.trim()).filter(Boolean)
|
|
63
|
+
: null;
|
|
64
|
+
auth[user] = dbs ? { password, databases: dbs } : password;
|
|
65
|
+
}
|
|
66
|
+
return auth;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function start(opts = {}) {
|
|
70
|
+
ensureStateDir();
|
|
71
|
+
const existing = readPid();
|
|
72
|
+
if (existing && isAlive(existing)) {
|
|
73
|
+
return `jsql server is already running (pid ${existing}).`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const configPath = opts.config || CONF_FILE;
|
|
77
|
+
let cfg = { port: 3306, host: '127.0.0.1' };
|
|
78
|
+
if (fs.existsSync(configPath)) {
|
|
79
|
+
try { cfg = Object.assign(cfg, JSON.parse(fs.readFileSync(configPath, 'utf8'))); } catch (e) {}
|
|
80
|
+
}
|
|
81
|
+
if (opts.port != null) cfg.port = opts.port;
|
|
82
|
+
if (opts.host != null) cfg.host = opts.host;
|
|
83
|
+
if (opts.dataDir != null) cfg.dataDir = path.resolve(opts.dataDir);
|
|
84
|
+
if (opts.noAuth) {
|
|
85
|
+
cfg.noAuth = true;
|
|
86
|
+
delete cfg.auth;
|
|
87
|
+
delete cfg.user;
|
|
88
|
+
delete cfg.password;
|
|
89
|
+
} else {
|
|
90
|
+
cfg.noAuth = false;
|
|
91
|
+
}
|
|
92
|
+
if (opts.auth) cfg.auth = parseAuthSpecs(Array.isArray(opts.auth) ? opts.auth : [opts.auth]);
|
|
93
|
+
fs.writeFileSync(CONF_FILE, JSON.stringify(cfg, null, 2), 'utf8');
|
|
94
|
+
|
|
95
|
+
const entry = path.join(__dirname, 'jsql-server');
|
|
96
|
+
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
97
|
+
const child = spawn(process.execPath, [entry, '--config', CONF_FILE], {
|
|
98
|
+
detached: true,
|
|
99
|
+
stdio: ['ignore', logFd, logFd],
|
|
100
|
+
env: Object.assign({}, process.env, { JSQL_SERVER_MAIN: '1' }),
|
|
101
|
+
});
|
|
102
|
+
child.once('error', err => {
|
|
103
|
+
fs.closeSync(logFd);
|
|
104
|
+
throw err;
|
|
105
|
+
});
|
|
106
|
+
child.unref();
|
|
107
|
+
|
|
108
|
+
fs.writeFileSync(PID_FILE, String(child.pid), 'utf8');
|
|
109
|
+
|
|
110
|
+
const ready = await waitReady(cfg.port, cfg.host, 5000);
|
|
111
|
+
if (!ready) {
|
|
112
|
+
if (isAlive(child.pid)) {
|
|
113
|
+
try { process.kill(child.pid, 'SIGTERM'); } catch (e) {}
|
|
114
|
+
fs.unlinkSync(PID_FILE);
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`server failed to start (see ${LOG_FILE})`);
|
|
117
|
+
}
|
|
118
|
+
return `jsql server started (pid ${child.pid}, ${cfg.host}:${cfg.port}, dataDir: ${cfg.dataDir || 'in-memory'}). Log: ${LOG_FILE}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function waitReady(port, host, timeoutMs) {
|
|
122
|
+
const deadline = Date.now() + timeoutMs;
|
|
123
|
+
return new Promise(resolve => {
|
|
124
|
+
const tick = async () => {
|
|
125
|
+
if (await isPortOpen(port, host)) return resolve(true);
|
|
126
|
+
if (Date.now() > deadline) return resolve(false);
|
|
127
|
+
setTimeout(tick, 200);
|
|
128
|
+
};
|
|
129
|
+
tick();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function stop(opts = {}) {
|
|
134
|
+
const pid = readPid();
|
|
135
|
+
if (!pid) return 'jsql server is not running.';
|
|
136
|
+
if (!isAlive(pid)) {
|
|
137
|
+
fs.unlinkSync(PID_FILE);
|
|
138
|
+
return 'jsql server is not running.';
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
process.kill(pid, 'SIGTERM');
|
|
142
|
+
} catch (e) {
|
|
143
|
+
throw new Error(`failed to stop server (pid ${pid}): ${e.message}`);
|
|
144
|
+
}
|
|
145
|
+
const deadline = Date.now() + 5000;
|
|
146
|
+
while (Date.now() < deadline) {
|
|
147
|
+
if (!isAlive(pid)) break;
|
|
148
|
+
await new Promise(r => setTimeout(r, 150));
|
|
149
|
+
}
|
|
150
|
+
if (isAlive(pid)) {
|
|
151
|
+
try { process.kill(pid, 'SIGKILL'); } catch (e) {}
|
|
152
|
+
}
|
|
153
|
+
fs.unlinkSync(PID_FILE);
|
|
154
|
+
return `jsql server stopped (pid ${pid}).`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function status(opts = {}) {
|
|
158
|
+
const pid = readPid();
|
|
159
|
+
if (!pid || !isAlive(pid)) {
|
|
160
|
+
return 'jsql server is not running.';
|
|
161
|
+
}
|
|
162
|
+
let cfg = {};
|
|
163
|
+
try { cfg = JSON.parse(fs.readFileSync(CONF_FILE, 'utf8')); } catch (e) {}
|
|
164
|
+
const port = (opts.port != null ? opts.port : cfg.port) || 3306;
|
|
165
|
+
const host = (opts.host != null ? opts.host : cfg.host) || '127.0.0.1';
|
|
166
|
+
const up = await isPortOpen(port, host);
|
|
167
|
+
const lines = [
|
|
168
|
+
`jsql server is running (pid ${pid}).`,
|
|
169
|
+
` ${host}:${port} ${up ? 'listening' : 'NOT responding'}`,
|
|
170
|
+
` dataDir: ${cfg.dataDir || 'in-memory'}`,
|
|
171
|
+
` log: ${LOG_FILE}`,
|
|
172
|
+
];
|
|
173
|
+
if (cfg.auth) lines.push(` users: ${Object.keys(cfg.auth).join(', ')}`);
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = { serverControl: { start, stop, status }, STATE_DIR, PID_FILE, LOG_FILE, CONF_FILE };
|
package/lib/database.js
CHANGED
|
@@ -134,20 +134,53 @@ class Database {
|
|
|
134
134
|
try {
|
|
135
135
|
if (fs.existsSync(this._metaPath)) {
|
|
136
136
|
const parsed = JSON.parse(fs.readFileSync(this._metaPath, 'utf8'));
|
|
137
|
-
if (parsed && parsed.tables)
|
|
137
|
+
if (parsed && parsed.tables) {
|
|
138
|
+
this._meta = parsed;
|
|
139
|
+
// 清洗 meta 中的 file 字段:拒绝绝对路径/.. 穿越,防恶意 meta.json 写任意位置
|
|
140
|
+
for (const name of Object.keys(this._meta.tables)) {
|
|
141
|
+
const meta = this._meta.tables[name];
|
|
142
|
+
if (meta && meta.file !== undefined) {
|
|
143
|
+
if (this._safeTableFile(meta.file) === null) {
|
|
144
|
+
delete this._meta.tables[name];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
138
149
|
}
|
|
139
150
|
} catch (e) {
|
|
140
151
|
this._meta = { version: 1, tables: {} };
|
|
141
152
|
}
|
|
153
|
+
for (const name of Object.keys(this._meta.tables)) {
|
|
154
|
+
if (this[name] === undefined && !(name in this)) {
|
|
155
|
+
Object.defineProperty(this, name, {
|
|
156
|
+
get: () => this._ensureTable(name) || this._tables[name],
|
|
157
|
+
enumerable: true,
|
|
158
|
+
configurable: true
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
142
162
|
this._startMonitor();
|
|
143
163
|
}
|
|
144
164
|
|
|
145
165
|
_tableFile(name) {
|
|
146
166
|
const meta = this._meta.tables[name];
|
|
147
|
-
if (meta && meta.file)
|
|
167
|
+
if (meta && meta.file) {
|
|
168
|
+
const safe = this._safeTableFile(meta.file);
|
|
169
|
+
if (safe) return path.join(this._filePath, safe);
|
|
170
|
+
}
|
|
148
171
|
return path.join(this._filePath, encodeURIComponent(name) + '.jsql');
|
|
149
172
|
}
|
|
150
173
|
|
|
174
|
+
_safeTableFile(file) {
|
|
175
|
+
if (typeof file !== 'string' || file.length === 0) return null;
|
|
176
|
+
if (path.isAbsolute(file)) return null;
|
|
177
|
+
if (file.includes('/') || file.includes('\\')) return null;
|
|
178
|
+
const base = path.basename(file);
|
|
179
|
+
if (base !== file || base === '.' || base === '..') return null;
|
|
180
|
+
if (file.includes('\0')) return null;
|
|
181
|
+
return file;
|
|
182
|
+
}
|
|
183
|
+
|
|
151
184
|
_saveMeta() {
|
|
152
185
|
if (!this._dirMode) return;
|
|
153
186
|
try {
|
|
@@ -166,7 +199,7 @@ class Database {
|
|
|
166
199
|
if (!this._dirMode) return null;
|
|
167
200
|
const meta = this._meta.tables[name];
|
|
168
201
|
if (!meta) return null;
|
|
169
|
-
const file =
|
|
202
|
+
const file = this._tableFile(name);
|
|
170
203
|
if (!fs.existsSync(file)) return null;
|
|
171
204
|
try {
|
|
172
205
|
const fmt = new JSQLFormat(file);
|
|
@@ -197,13 +230,13 @@ class Database {
|
|
|
197
230
|
if (this._flushInterval <= 0) {
|
|
198
231
|
this._flushTimer = setImmediate(() => {
|
|
199
232
|
this._flushTimer = null;
|
|
200
|
-
try { this._flushDirty(); } catch (e) {}
|
|
233
|
+
try { this._flushDirty(); } catch (e) { this._emit('error', e); }
|
|
201
234
|
});
|
|
202
235
|
return;
|
|
203
236
|
}
|
|
204
237
|
this._flushTimer = setTimeout(() => {
|
|
205
238
|
this._flushTimer = null;
|
|
206
|
-
try { this._flushDirty(); } catch (e) {}
|
|
239
|
+
try { this._flushDirty(); } catch (e) { this._emit('error', e); }
|
|
207
240
|
}, this._flushInterval);
|
|
208
241
|
if (this._flushTimer.unref) this._flushTimer.unref();
|
|
209
242
|
}
|
|
@@ -211,8 +244,15 @@ class Database {
|
|
|
211
244
|
_flushDirty() {
|
|
212
245
|
if (!this._dirMode || this._dirtyTables.size === 0) return;
|
|
213
246
|
const names = [...this._dirtyTables];
|
|
214
|
-
|
|
215
|
-
|
|
247
|
+
for (const name of names) {
|
|
248
|
+
try {
|
|
249
|
+
this._flushTable(name);
|
|
250
|
+
this._dirtyTables.delete(name);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
// 保留脏标记,下次 flush 重试;错误对外传播而非静默
|
|
253
|
+
this._emit('error', e);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
216
256
|
this._saveMeta();
|
|
217
257
|
}
|
|
218
258
|
|
|
@@ -374,6 +414,9 @@ class Database {
|
|
|
374
414
|
// ============================================================
|
|
375
415
|
|
|
376
416
|
createTable(name, schema) {
|
|
417
|
+
if (!name || name.length > 64 || name === '.' || name === '..' || /[\x00-\x1f/\\]|\.\./.test(name)) {
|
|
418
|
+
throw createError('ER_BAD_TABLE_NAME', name);
|
|
419
|
+
}
|
|
377
420
|
if (!this._runHooks('beforeCreateTable', [name, schema])) throw createError('ER_PLUGIN_ABORT', 'createTable aborted by plugin');
|
|
378
421
|
if (this._tables[name] || (this._dirMode && this._meta.tables[name])) {
|
|
379
422
|
throw createError('ER_TABLE_EXISTS_ERROR', name);
|
|
@@ -1287,7 +1330,7 @@ class Database {
|
|
|
1287
1330
|
}
|
|
1288
1331
|
}
|
|
1289
1332
|
} else if (targetVersion < current) {
|
|
1290
|
-
for (const m of this._migrations.reverse()) {
|
|
1333
|
+
for (const m of [...this._migrations].reverse()) {
|
|
1291
1334
|
if (m.version <= current && m.version > targetVersion) {
|
|
1292
1335
|
m.down(this);
|
|
1293
1336
|
this._setCurrentVersion(m.version - 1);
|
|
@@ -1448,7 +1491,14 @@ class Database {
|
|
|
1448
1491
|
clearTimeout(this._autoSaveTimer);
|
|
1449
1492
|
this._autoSaveTimer = null;
|
|
1450
1493
|
}
|
|
1451
|
-
if (this.
|
|
1494
|
+
if (this._dirMode) {
|
|
1495
|
+
try {
|
|
1496
|
+
this._flushDirty();
|
|
1497
|
+
this._saveMeta();
|
|
1498
|
+
} catch (e) {
|
|
1499
|
+
this._emit('error', e);
|
|
1500
|
+
}
|
|
1501
|
+
} else if (this._dirty && !this._memoryMode) {
|
|
1452
1502
|
this.save();
|
|
1453
1503
|
}
|
|
1454
1504
|
if (this._jsqlFormat) this._jsqlFormat._close();
|
|
@@ -1475,6 +1525,45 @@ class Database {
|
|
|
1475
1525
|
this._markDirtyLegacy();
|
|
1476
1526
|
}
|
|
1477
1527
|
|
|
1528
|
+
async beginTx() {
|
|
1529
|
+
this._txSnapshot = {};
|
|
1530
|
+
for (const [name, table] of Object.entries(this._tables)) {
|
|
1531
|
+
this._txSnapshot[name] = { rows: JSON.parse(JSON.stringify(table.toJSON())), schema: table._schema };
|
|
1532
|
+
}
|
|
1533
|
+
this._txId = (this._txId || 0) + 1;
|
|
1534
|
+
return this._txId;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
async commitTx() {
|
|
1538
|
+
this._txSnapshot = null;
|
|
1539
|
+
this._txId = undefined;
|
|
1540
|
+
if (this._dirMode) {
|
|
1541
|
+
try { this._flushDirty(); } catch (e) {}
|
|
1542
|
+
} else if (!this._memoryMode) {
|
|
1543
|
+
this.save();
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
async rollbackTx() {
|
|
1548
|
+
const snap = this._txSnapshot;
|
|
1549
|
+
this._txSnapshot = null;
|
|
1550
|
+
this._txId = undefined;
|
|
1551
|
+
if (!snap) return;
|
|
1552
|
+
for (const name of Object.keys(this._tables)) {
|
|
1553
|
+
if (!snap[name]) delete this._tables[name];
|
|
1554
|
+
}
|
|
1555
|
+
for (const [name, entry] of Object.entries(snap)) {
|
|
1556
|
+
if (this._tables[name]) {
|
|
1557
|
+
this._tables[name]._loadRows(entry.rows);
|
|
1558
|
+
} else {
|
|
1559
|
+
const table = new Table(name, entry.schema, this);
|
|
1560
|
+
table._loadRows(entry.rows);
|
|
1561
|
+
this._tables[name] = table;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
this._dirty = true;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1478
1567
|
// ============================================================
|
|
1479
1568
|
// 加密(内部)
|
|
1480
1569
|
// ============================================================
|
package/lib/errors.js
CHANGED
|
@@ -62,6 +62,12 @@ const ErrorCodes = {
|
|
|
62
62
|
ER_TRIGGER_EXISTS: { code: 1359, msg: "Trigger '%s' already exists" },
|
|
63
63
|
ER_TRIGGER_NOT_FOUND: { code: 1360, msg: "Trigger '%s' does not exist" },
|
|
64
64
|
ER_PLUGIN_ERR: { code: 1125, msg: "Plugin error: %s" },
|
|
65
|
+
ER_PLUGIN_ABORT: { code: 1125, msg: "Operation aborted by plugin: %s" },
|
|
66
|
+
ER_TABLE_EXISTS_ERROR: { code: 1050, msg: "Table '%s' already exists" },
|
|
67
|
+
ER_BAD_TABLE_NAME: { code: 1058, msg: "Invalid table name '%s'" },
|
|
68
|
+
ER_BAD_REGEX: { code: 1258, msg: "Unsafe regular expression rejected: %s" },
|
|
69
|
+
ER_DBATTACH_EXISTS: { code: 1007, msg: "Database '%s' already attached" },
|
|
70
|
+
ER_DBATTACH_NOT_FOUND: { code: 1049, msg: "Attached database '%s' not found" },
|
|
65
71
|
};
|
|
66
72
|
|
|
67
73
|
/**
|
package/lib/jsql_format.js
CHANGED
|
@@ -305,19 +305,25 @@ class JSQLFormat {
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
_reset() {
|
|
308
|
-
fs.ftruncateSync(this.fd, SZ.SUPER);
|
|
309
|
-
const sb = Buffer.alloc(SZ.SUPER); sb.write('JSQL', 0, 'utf8');
|
|
310
|
-
fs.writeSync(this.fd, sb, 0, SZ.SUPER, 0);
|
|
311
308
|
this.nBlocks = 1;
|
|
312
309
|
this._nextTableId = 1;
|
|
313
310
|
this.tables.clear();
|
|
314
311
|
this.pkIdx.clear();
|
|
315
312
|
}
|
|
316
313
|
|
|
314
|
+
_truncateToSuper() {
|
|
315
|
+
fs.ftruncateSync(this.fd, SZ.SUPER);
|
|
316
|
+
const sb = Buffer.alloc(SZ.SUPER); sb.write('JSQL', 0, 'utf8');
|
|
317
|
+
fs.writeSync(this.fd, sb, 0, SZ.SUPER, 0);
|
|
318
|
+
this.nBlocks = 1;
|
|
319
|
+
}
|
|
320
|
+
|
|
317
321
|
saveAll(allTables) {
|
|
318
322
|
this._open();
|
|
319
323
|
this._reset();
|
|
320
324
|
|
|
325
|
+
const pending = [];
|
|
326
|
+
|
|
321
327
|
for (const [name, info] of Object.entries(allTables)) {
|
|
322
328
|
const { schema, rows, pkField } = info;
|
|
323
329
|
if (!rows || rows.length === 0) continue;
|
|
@@ -338,8 +344,16 @@ class JSQLFormat {
|
|
|
338
344
|
// Scratch buffer for row encoding (max row size fits in one block)
|
|
339
345
|
const scratch = Buffer.allocUnsafe(SZ.DATA);
|
|
340
346
|
|
|
347
|
+
// Phase 1: encode every row into memory first. Any error thrown here
|
|
348
|
+
// (oversized row, bad value) aborts before the file is touched.
|
|
341
349
|
for (const row of rows) {
|
|
342
350
|
const end = encRowInPlace(row, encoders, scratch, 0);
|
|
351
|
+
if (end > SZ.DATA) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`row in table '${name}' is too large (${end} bytes > max row size ${SZ.DATA} bytes); ` +
|
|
354
|
+
'split the value or reduce field sizes before saving'
|
|
355
|
+
);
|
|
356
|
+
}
|
|
343
357
|
if (bp + end > SZ.BLOCK) {
|
|
344
358
|
const raw = Buffer.from(buf.subarray(SZ.HEADER, bp));
|
|
345
359
|
rawBlocks.push(raw);
|
|
@@ -368,13 +382,19 @@ class JSQLFormat {
|
|
|
368
382
|
buf.writeUInt32LE(bp - SZ.HEADER, 40);
|
|
369
383
|
}
|
|
370
384
|
|
|
385
|
+
pkEntries.sort((a, b) => a.hash - b.hash);
|
|
386
|
+
pending.push({ name, tableId, rawBlocks, blockIds, pkEntries });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Phase 2: all rows encoded successfully — now write to disk.
|
|
390
|
+
for (const p of pending) {
|
|
371
391
|
// Compress and write blocks
|
|
372
|
-
for (let i = 0; i < rawBlocks.length; i++) {
|
|
373
|
-
const bid = blockIds[i];
|
|
374
|
-
const compBuf = zlib.gzipSync(rawBlocks[i], { level: 1 });
|
|
392
|
+
for (let i = 0; i < p.rawBlocks.length; i++) {
|
|
393
|
+
const bid = p.blockIds[i];
|
|
394
|
+
const compBuf = zlib.gzipSync(p.rawBlocks[i], { level: 1 });
|
|
375
395
|
const block = Buffer.alloc(SZ.BLOCK);
|
|
376
|
-
mkHeader(BT.DATA, bid, tableId).copy(block);
|
|
377
|
-
block.writeUInt32LE(rawBlocks[i].length, 40);
|
|
396
|
+
mkHeader(BT.DATA, bid, p.tableId).copy(block);
|
|
397
|
+
block.writeUInt32LE(p.rawBlocks[i].length, 40);
|
|
378
398
|
block[48] = 1;
|
|
379
399
|
block.writeUInt32LE(compBuf.length, 44);
|
|
380
400
|
compBuf.copy(block, SZ.HEADER);
|
|
@@ -382,13 +402,12 @@ class JSQLFormat {
|
|
|
382
402
|
fs.writeSync(this.fd, block, 0, SZ.BLOCK, bid * SZ.BLOCK);
|
|
383
403
|
}
|
|
384
404
|
|
|
385
|
-
const t = this.tables.get(name);
|
|
386
|
-
t.rowCount =
|
|
387
|
-
t.dataBlock = blockIds.length > 0 ? blockIds[blockIds.length - 1] : 0;
|
|
405
|
+
const t = this.tables.get(p.name);
|
|
406
|
+
t.rowCount = p.pkEntries.length;
|
|
407
|
+
t.dataBlock = p.blockIds.length > 0 ? p.blockIds[p.blockIds.length - 1] : 0;
|
|
388
408
|
|
|
389
|
-
|
|
390
|
-
this.
|
|
391
|
-
this._saveIdx(name);
|
|
409
|
+
this.pkIdx.set(p.name, p.pkEntries);
|
|
410
|
+
this._saveIdx(p.name);
|
|
392
411
|
}
|
|
393
412
|
|
|
394
413
|
this._saveSuper();
|