jsql-neo 6.5.23 → 6.5.25
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 +1 -1
- package/README.md +4 -3
- package/bin/jsql +120 -1
- package/index.js +1 -1
- package/lib/native_client.js +91 -34
- package/lib/uninstaller.js +216 -0
- package/lib/updater.js +365 -0
- package/lib/wasm_client.js +88 -30
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +3 -3
- package/nativesrc/jsql-neo-core/src/engine/table.rs +263 -42
- package/nativesrc/jsql-neo-core/src/types.rs +2 -0
- package/package.json +1 -1
- package/postinstall.js +179 -44
- package/test/cli-maintenance.test.js +112 -0
- package/test/issue-38.test.js +56 -0
- package/wasm/browser.d.ts +34 -22
- package/wasm/browser.mjs +101 -29
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
package/LICENSE
CHANGED
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
same "printed page" as the copyright notice for easier
|
|
188
188
|
identification within third-party archives.
|
|
189
189
|
|
|
190
|
-
Copyright
|
|
190
|
+
Copyright 2026 Vexify
|
|
191
191
|
|
|
192
192
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
193
|
you may not use this file except in compliance with the License.
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
|
|
4
4
|
> MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
|
|
5
5
|
|
|
6
|
-
> **v6.5.
|
|
6
|
+
> **v6.5.25** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|

|
|
22
22
|
|
|
23
|
-
A real `jsql tui` session: joins, NULL semantics,
|
|
24
|
-
savepoint rollback.
|
|
23
|
+
A real `jsql tui` session (v6.5.25): 64-bit integer precision, joins, NULL semantics,
|
|
24
|
+
and savepoint rollback — all in one in-memory engine, rendered on a dark background.
|
|
25
25
|
|
|
26
26
|
[MP4 version](doc/demo.mp4) · [asciinema recording](doc/demo.cast)
|
|
27
27
|
|
|
@@ -227,5 +227,6 @@ Apache-2.0 License — contributions welcome!
|
|
|
227
227
|
- **Issues:** [Open an issue](https://github.com/vexify-org/JSQL-neo/issues)
|
|
228
228
|
- **PRs:** Fork → branch → PR — thank you!
|
|
229
229
|
- **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
230
|
+
- **Dev container:** [jsql-neo/devbox](https://github.com/jsql-neo/devbox) — reproducible dev environment (Docker/devcontainer)
|
|
230
231
|
- **Code of Conduct:** [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
231
232
|
- **Security:** [SECURITY.md](SECURITY.md) — please report vulnerabilities privately
|
package/bin/jsql
CHANGED
|
@@ -13,7 +13,7 @@ const { nag, setStarred, readConfig } = require('../lib/star-nag');
|
|
|
13
13
|
|
|
14
14
|
// star 提示:任何异常都不该挡住命令本身
|
|
15
15
|
// version / --version 只回版本号、star 本身就是干这件事的 —— 这些不提示,其余全部提示
|
|
16
|
-
const NO_NAG_ARGS = new Set(['version', '--version', '-v', 'star']);
|
|
16
|
+
const NO_NAG_ARGS = new Set(['version', '--version', '-v', 'star', 'update', 'uninstall', 'remove']);
|
|
17
17
|
if (!process.env.JSQL_NO_STAR_NAG && !NO_NAG_ARGS.has(process.argv[2])) {
|
|
18
18
|
try { nag(); } catch (e) { /* 提示失败与命令无关 */ }
|
|
19
19
|
}
|
|
@@ -34,6 +34,122 @@ const manager = new ModuleManager();
|
|
|
34
34
|
const RAW_ARGV = process.argv.slice(2);
|
|
35
35
|
|
|
36
36
|
const _pkg = require('../package.json');
|
|
37
|
+
|
|
38
|
+
// ── update / uninstall 命令的实现 ─────────────────────────────
|
|
39
|
+
|
|
40
|
+
function updateOptions(sub) {
|
|
41
|
+
sub.option('check', { type: 'boolean', description: 'Only check for a newer version' });
|
|
42
|
+
sub.option('force', { type: 'boolean', description: 'Reinstall even if already up to date' });
|
|
43
|
+
sub.option('dry-run', { type: 'boolean', description: 'Print the install command without running it' });
|
|
44
|
+
sub.option('json', { type: 'boolean', description: 'Output as JSON' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function updateHandler(argv) {
|
|
48
|
+
const updater = require('../lib/updater');
|
|
49
|
+
try {
|
|
50
|
+
const result = await updater.runUpdate({
|
|
51
|
+
checkOnly: argv.check === true,
|
|
52
|
+
force: argv.force === true,
|
|
53
|
+
dryRun: argv['dry-run'] === true,
|
|
54
|
+
});
|
|
55
|
+
if (argv.json) console.log(JSON.stringify(result, null, 2));
|
|
56
|
+
else console.log(updater.formatResult(result));
|
|
57
|
+
if (!result.ok) process.exitCode = 1;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error(`Error: ${e.message}`);
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function uninstallOptions(sub) {
|
|
65
|
+
sub.option('yes', { alias: ['y'], type: 'boolean', description: 'Skip the confirmation prompt' });
|
|
66
|
+
sub.option('keep-data', { type: 'boolean', description: 'Keep user data (plugins/config/cache)' });
|
|
67
|
+
sub.option('global', { type: 'boolean', description: 'Only uninstall the global npm package' });
|
|
68
|
+
sub.option('all', { type: 'boolean', description: 'Remove user data and the global npm package' });
|
|
69
|
+
sub.option('dry-run', { type: 'boolean', description: 'Show what would be removed' });
|
|
70
|
+
sub.option('json', { type: 'boolean', description: 'Output as JSON' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function uninstallHandler(argv) {
|
|
74
|
+
const un = require('../lib/uninstaller');
|
|
75
|
+
const fail = (e) => { console.error(`Error: ${e.message}`); process.exitCode = 1; };
|
|
76
|
+
try {
|
|
77
|
+
const yes = argv.yes === true || argv.y === true;
|
|
78
|
+
const dryRun = argv['dry-run'] === true;
|
|
79
|
+
const doGlobal = argv.global === true || argv.all === true;
|
|
80
|
+
const keepData = argv['keep-data'] === true || (argv.global === true && argv.all !== true);
|
|
81
|
+
const doData = !keepData;
|
|
82
|
+
|
|
83
|
+
if (!doData && !doGlobal) {
|
|
84
|
+
console.log('Nothing to do (data kept, global package not requested).');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const targets = doData ? un.inspectTargets() : [];
|
|
89
|
+
const existing = targets.filter((t) => t.exists);
|
|
90
|
+
const globalInstalled = doGlobal ? un.isGloballyInstalled() : false;
|
|
91
|
+
|
|
92
|
+
if (!dryRun) {
|
|
93
|
+
if (doData && existing.length === 0 && !doGlobal) {
|
|
94
|
+
console.log('No jsql user data found. Nothing to remove.');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!yes) {
|
|
98
|
+
if (!process.stdin.isTTY) {
|
|
99
|
+
console.error('Refusing to delete user data without confirmation. Re-run with --yes (or --dry-run to preview).');
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (doData && existing.length) {
|
|
104
|
+
console.log('The following user data will be removed:');
|
|
105
|
+
for (const t of existing) console.log(` - ${t.path} (${t.label})`);
|
|
106
|
+
}
|
|
107
|
+
if (doGlobal) console.log('Will also run: npm uninstall -g ' + un.PKG_NAME);
|
|
108
|
+
const ok = await un.confirm('Proceed? [y/N] ');
|
|
109
|
+
if (!ok) {
|
|
110
|
+
console.log('Aborted. No changes were made.');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const dataResult = doData ? un.removeData({ dryRun }) : { removed: [], skipped: [], failed: [] };
|
|
117
|
+
let globalResult = null;
|
|
118
|
+
if (doGlobal) {
|
|
119
|
+
if (dryRun) globalResult = { ok: true, dryRun: true, command: `npm uninstall -g ${un.PKG_NAME}`, error: null };
|
|
120
|
+
else if (!globalInstalled) globalResult = { ok: true, skipped: true, command: `npm uninstall -g ${un.PKG_NAME}`, error: null };
|
|
121
|
+
else globalResult = un.uninstallGlobal(false);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (argv.json) {
|
|
125
|
+
console.log(JSON.stringify({ data: dataResult, global: globalResult, dryRun }, null, 2));
|
|
126
|
+
} else {
|
|
127
|
+
const verb = dryRun ? 'Would remove' : 'Removed';
|
|
128
|
+
if (dataResult.removed.length) {
|
|
129
|
+
console.log(`${verb} ${dataResult.removed.length} path(s):`);
|
|
130
|
+
for (const p of dataResult.removed) console.log(` - ${p}`);
|
|
131
|
+
} else if (doData) {
|
|
132
|
+
console.log('No jsql user data to remove.');
|
|
133
|
+
}
|
|
134
|
+
for (const f of dataResult.failed) console.error(`Failed to remove ${f.path}: ${f.error}`);
|
|
135
|
+
if (doGlobal) {
|
|
136
|
+
if (globalResult.dryRun) console.log(`Would run: ${globalResult.command}`);
|
|
137
|
+
else if (globalResult.skipped) console.log('Global jsql-neo package is not installed; skipped npm uninstall.');
|
|
138
|
+
else if (globalResult.ok) console.log('Uninstalled global package (npm uninstall -g jsql-neo).');
|
|
139
|
+
else console.error(`Global uninstall failed: ${globalResult.error}`);
|
|
140
|
+
}
|
|
141
|
+
if (!doGlobal) {
|
|
142
|
+
console.log('To remove the package itself, run: npm uninstall -g jsql-neo');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (dataResult.failed.length || (globalResult && !globalResult.ok && !globalResult.skipped)) {
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
}
|
|
148
|
+
} catch (e) {
|
|
149
|
+
fail(e);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
37
153
|
const cli = (yaggs || require('./yaggs-fallback').buildYaggsFallback(_pkg))({
|
|
38
154
|
pkg: _pkg,
|
|
39
155
|
})
|
|
@@ -412,6 +528,9 @@ const cli = (yaggs || require('./yaggs-fallback').buildYaggsFallback(_pkg))({
|
|
|
412
528
|
fail(e);
|
|
413
529
|
}
|
|
414
530
|
})
|
|
531
|
+
.command('update', 'Update jsql-neo to the latest version', updateOptions, updateHandler)
|
|
532
|
+
.command('uninstall', 'Remove jsql user data and/or the global package', uninstallOptions, uninstallHandler)
|
|
533
|
+
.command('remove', 'Alias of uninstall', uninstallOptions, uninstallHandler)
|
|
415
534
|
.command('version', 'Show version', null, () => {
|
|
416
535
|
console.log(require('../package.json').version);
|
|
417
536
|
})
|
package/index.js
CHANGED
package/lib/native_client.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const { executeSQL } = require('./sql');
|
|
4
|
-
const { JSQL_Error, ErrorCodes } = require('./errors');
|
|
4
|
+
const { JSQL_Error, ErrorCodes, createError } = require('./errors');
|
|
5
|
+
const bigintJson = require('./bigint_json');
|
|
5
6
|
const {
|
|
6
7
|
createHookRegistry, applyPlugin, buildPluginContext, registerHook,
|
|
7
8
|
runHooks, getPluginApis
|
|
@@ -155,6 +156,69 @@ function assertRowObject(row, index, isBatch) {
|
|
|
155
156
|
throw new TypeError(`insert(): expected a row object${where}, got ${got}`);
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
const INT64_MIN = -9223372036854775808n;
|
|
160
|
+
const INT64_MAX = 9223372036854775807n;
|
|
161
|
+
const SAFE_INT_MIN = BigInt(Number.MIN_SAFE_INTEGER);
|
|
162
|
+
const SAFE_INT_MAX = BigInt(Number.MAX_SAFE_INTEGER);
|
|
163
|
+
const DECIMAL_INT_RE = /^[+-]?\d+$/;
|
|
164
|
+
|
|
165
|
+
function isIntegerType(t) {
|
|
166
|
+
return t === 'integer' || t === 'int' || t === 'bigint';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 与 lib/table.js 的 normalizeInteger 一致(Issue #38)。 */
|
|
170
|
+
function normalizeInteger(v, type) {
|
|
171
|
+
let b;
|
|
172
|
+
if (typeof v === 'bigint') b = v;
|
|
173
|
+
else {
|
|
174
|
+
try { b = BigInt(String(v).trim()); } catch (_) { throw createError('ER_TRUNCATED_WRONG_VALUE', type, v); }
|
|
175
|
+
}
|
|
176
|
+
if (b < INT64_MIN || b > INT64_MAX) throw createError('ER_TRUNCATED_WRONG_VALUE', type, b.toString());
|
|
177
|
+
return (b >= SAFE_INT_MIN && b <= SAFE_INT_MAX) ? Number(b) : b;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Rust 绑定层把 >2^53 的整数以十进制字符串返回,这里还原为 BigInt。 */
|
|
181
|
+
function restoreInteger(v) {
|
|
182
|
+
if (typeof v === 'bigint') return v;
|
|
183
|
+
if (typeof v === 'string' && DECIMAL_INT_RE.test(v)) {
|
|
184
|
+
const b = BigInt(v);
|
|
185
|
+
return (b >= SAFE_INT_MIN && b <= SAFE_INT_MAX) ? Number(b) : b;
|
|
186
|
+
}
|
|
187
|
+
if (typeof v === 'number' && Number.isInteger(v) && !Number.isSafeInteger(v)) return BigInt(v);
|
|
188
|
+
return v;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeIntegerInput(v, type) {
|
|
192
|
+
if (typeof v === 'bigint') return normalizeInteger(v, type);
|
|
193
|
+
if (typeof v === 'number') {
|
|
194
|
+
if (!Number.isInteger(v) || !Number.isSafeInteger(v)) throw createError('ER_TRUNCATED_WRONG_VALUE', type, v);
|
|
195
|
+
return v;
|
|
196
|
+
}
|
|
197
|
+
if (typeof v === 'string' && DECIMAL_INT_RE.test(v.trim())) return normalizeInteger(v.trim(), type);
|
|
198
|
+
throw createError('ER_TRUNCATED_WRONG_VALUE', type, v);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 按 schema 做插入/更新前的值归一(JSON 列序列化、整数列精确化)。 */
|
|
202
|
+
function normalizeRowForRust(table, schema, row) {
|
|
203
|
+
const out = {};
|
|
204
|
+
for (const [k, v] of Object.entries(row)) {
|
|
205
|
+
const def = schema[k];
|
|
206
|
+
let t = null;
|
|
207
|
+
if (typeof def === 'string') t = def.toLowerCase();
|
|
208
|
+
else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
|
|
209
|
+
if (v !== null && v !== undefined && ['json', 'array', 'object'].includes(t) && typeof v !== 'string') {
|
|
210
|
+
out[k] = safeStringify(v, table, k);
|
|
211
|
+
} else if (v !== null && v !== undefined && isIntegerType(t)) {
|
|
212
|
+
out[k] = normalizeIntegerInput(v, t);
|
|
213
|
+
} else if (v instanceof Date) {
|
|
214
|
+
out[k] = v.toISOString();
|
|
215
|
+
} else {
|
|
216
|
+
out[k] = v;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
158
222
|
const NATIVE_TYPE_MAP = {
|
|
159
223
|
text: 'string',
|
|
160
224
|
varchar: 'string',
|
|
@@ -169,7 +233,7 @@ const NATIVE_TYPE_MAP = {
|
|
|
169
233
|
array: 'string',
|
|
170
234
|
object: 'string',
|
|
171
235
|
bool: 'boolean',
|
|
172
|
-
bigint: '
|
|
236
|
+
bigint: 'bigint',
|
|
173
237
|
int: 'integer',
|
|
174
238
|
};
|
|
175
239
|
|
|
@@ -260,9 +324,9 @@ function restoreRow(row, schema) {
|
|
|
260
324
|
if (typeof colDef === 'string') t = colDef.toLowerCase();
|
|
261
325
|
else if (colDef && typeof colDef === 'object' && colDef.type) t = String(colDef.type).toLowerCase();
|
|
262
326
|
if (['json', 'array', 'object'].includes(t) && typeof value === 'string') {
|
|
263
|
-
try { out[field] =
|
|
264
|
-
} else if ((t
|
|
265
|
-
out[field] =
|
|
327
|
+
try { out[field] = bigintJson.parse(value); } catch (e) { out[field] = value; }
|
|
328
|
+
} else if (isIntegerType(t)) {
|
|
329
|
+
out[field] = restoreInteger(value);
|
|
266
330
|
} else if ((t === 'float' || t === 'double' || t === 'number') && typeof value === 'string' && !Number.isNaN(Number(value))) {
|
|
267
331
|
out[field] = Number(value);
|
|
268
332
|
} else {
|
|
@@ -274,6 +338,7 @@ function restoreRow(row, schema) {
|
|
|
274
338
|
|
|
275
339
|
function _encodedFieldSize(v) {
|
|
276
340
|
if (v === null || v === undefined) return 1;
|
|
341
|
+
if (typeof v === 'bigint') return 1 + 8;
|
|
277
342
|
if (typeof v === 'number') {
|
|
278
343
|
if (Number.isInteger(v)) return (v >= -2147483648 && v <= 2147483647) ? 1 + 4 : 1 + 8;
|
|
279
344
|
return 1 + 8;
|
|
@@ -287,6 +352,9 @@ function encodeField(buf, off, v) {
|
|
|
287
352
|
if (v === null || v === undefined) {
|
|
288
353
|
buf.writeUInt8(0, off); return off + 1;
|
|
289
354
|
}
|
|
355
|
+
if (typeof v === 'bigint') {
|
|
356
|
+
buf.writeUInt8(INT64_TAG, off); buf.writeBigInt64LE(v, off + 1); return off + 9;
|
|
357
|
+
}
|
|
290
358
|
if (typeof v === 'number') {
|
|
291
359
|
if (Number.isInteger(v)) {
|
|
292
360
|
if (v >= -2147483648 && v <= 2147483647) {
|
|
@@ -554,7 +622,9 @@ class JSQL {
|
|
|
554
622
|
let t = null;
|
|
555
623
|
if (typeof def === 'string') t = def.trim().split(/\s+/)[0].toLowerCase();
|
|
556
624
|
else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
|
|
557
|
-
if (
|
|
625
|
+
if (isIntegerType(t)) {
|
|
626
|
+
try { return normalizeIntegerInput(id, t); } catch (_) { return id; }
|
|
627
|
+
}
|
|
558
628
|
return id;
|
|
559
629
|
}
|
|
560
630
|
|
|
@@ -578,7 +648,7 @@ class JSQL {
|
|
|
578
648
|
if (pk) {
|
|
579
649
|
if (this._isAutoIntPk(table)) {
|
|
580
650
|
const n = toNonNeg('id', id, NaN);
|
|
581
|
-
if (Number.
|
|
651
|
+
if (typeof id !== 'bigint' && Number.isSafeInteger(n)) {
|
|
582
652
|
// 负 id 会被原生层 `i64 -> u64` 静默回绕成 usize::MAX,导致
|
|
583
653
|
// findById 误报 not found(行明明存在却取不到)。在此显式拒绝。
|
|
584
654
|
if (n < 0) throw new Error('id must be a non-negative number');
|
|
@@ -589,7 +659,7 @@ class JSQL {
|
|
|
589
659
|
}
|
|
590
660
|
const filter = {};
|
|
591
661
|
filter[pk] = this._coercePkId(table, id);
|
|
592
|
-
const r = safeParse(this._n('jsqlFind', table,
|
|
662
|
+
const r = safeParse(this._n('jsqlFind', table, bigintJson.stringify(filter), 1, 0));
|
|
593
663
|
if (r && r.error) return r;
|
|
594
664
|
return Array.isArray(r) && r.length > 0 ? r[0] : null;
|
|
595
665
|
}
|
|
@@ -601,7 +671,7 @@ class JSQL {
|
|
|
601
671
|
if (pk) {
|
|
602
672
|
if (this._isAutoIntPk(table)) {
|
|
603
673
|
const n = toNonNeg('id', id, NaN);
|
|
604
|
-
if (Number.
|
|
674
|
+
if (typeof id !== 'bigint' && Number.isSafeInteger(n)) {
|
|
605
675
|
// 负 id 在原生层回绕为 usize::MAX 后静默 not found,update/remove
|
|
606
676
|
// 会无提示地什么都不做 —— 这里显式拒绝(见 issue #35)。
|
|
607
677
|
if (n < 0) throw new Error('id must be a non-negative number');
|
|
@@ -612,7 +682,7 @@ class JSQL {
|
|
|
612
682
|
}
|
|
613
683
|
const filter = {};
|
|
614
684
|
filter[pk] = this._coercePkId(table, id);
|
|
615
|
-
const r = safeParse(this._n('jsqlFind', table,
|
|
685
|
+
const r = safeParse(this._n('jsqlFind', table, bigintJson.stringify(filter), 1, 0));
|
|
616
686
|
if (Array.isArray(r) && r.length > 0) return r[0].id;
|
|
617
687
|
return null;
|
|
618
688
|
}
|
|
@@ -639,23 +709,7 @@ class JSQL {
|
|
|
639
709
|
const schema = this._schemas[table];
|
|
640
710
|
// 对 JSON/array/object 列保留 safeStringify 以给出可读错误(环形对象等)
|
|
641
711
|
if (schema) {
|
|
642
|
-
rows = rows.map(row =>
|
|
643
|
-
const out = {};
|
|
644
|
-
for (const [k, v] of Object.entries(row)) {
|
|
645
|
-
const def = schema[k];
|
|
646
|
-
let t = null;
|
|
647
|
-
if (typeof def === 'string') t = def.toLowerCase();
|
|
648
|
-
else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
|
|
649
|
-
if (v !== null && v !== undefined && ['json', 'array', 'object'].includes(t) && typeof v !== 'string') {
|
|
650
|
-
out[k] = safeStringify(v, table, k);
|
|
651
|
-
} else if (v instanceof Date) {
|
|
652
|
-
out[k] = v.toISOString();
|
|
653
|
-
} else {
|
|
654
|
-
out[k] = v;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return out;
|
|
658
|
-
});
|
|
712
|
+
rows = rows.map(row => normalizeRowForRust(table, schema, row));
|
|
659
713
|
}
|
|
660
714
|
// 批量插入优先走二进制格式:整批只在 JS 侧 encode 一次,Rust 直接按
|
|
661
715
|
// tag 解析成 FieldValue,省掉「JS JSON.stringify → Rust serde 解析 →
|
|
@@ -666,7 +720,7 @@ class JSQL {
|
|
|
666
720
|
if (typeof native.jsqlInsertBuf === 'function' && _canEncodeBatch(rows)) {
|
|
667
721
|
r = safeParse(this._n('jsqlInsertBuf', table, encodeBatch(rows)));
|
|
668
722
|
} else if (typeof native.jsqlInsertRaw === 'function') {
|
|
669
|
-
r = safeParse(this._n('jsqlInsertRaw', table,
|
|
723
|
+
r = safeParse(this._n('jsqlInsertRaw', table, bigintJson.stringify(rows)));
|
|
670
724
|
} else {
|
|
671
725
|
r = safeParse(this._n('jsqlInsertBuf', table, encodeBatch(rows)));
|
|
672
726
|
}
|
|
@@ -704,7 +758,7 @@ class JSQL {
|
|
|
704
758
|
for (var table in update) {
|
|
705
759
|
var entries = update[table];
|
|
706
760
|
if (entries.length === 0) continue;
|
|
707
|
-
var r = safeParse(this._n('jsqlUpdateByIds', table,
|
|
761
|
+
var r = safeParse(this._n('jsqlUpdateByIds', table, bigintJson.stringify(entries)));
|
|
708
762
|
this._emit('update', { table, entries, result: r });
|
|
709
763
|
}
|
|
710
764
|
this._opBuffer = { remove: {}, update: {} };
|
|
@@ -844,7 +898,7 @@ class JSQL {
|
|
|
844
898
|
this._flushOpsNow();
|
|
845
899
|
if (!(await this._runHooks('beforeFind', [table, { ids }]))) return null;
|
|
846
900
|
let r;
|
|
847
|
-
if (this._isAutoIntPk(table) && ids.every(id => Number.
|
|
901
|
+
if (this._isAutoIntPk(table) && ids.every(id => typeof id !== 'bigint' && Number.isSafeInteger(Number(id)))) {
|
|
848
902
|
// 自增主键:内部 id 即主键值,一次批量调用即可,避免 N 次往返。
|
|
849
903
|
// #35:负 id 在原生层 `i64 -> u64` 回绕成 usize::MAX,静默 not found;
|
|
850
904
|
// NaN/Infinity 同样不允许。绑定层先校验,绝不把非法值传给原生模块。
|
|
@@ -869,7 +923,7 @@ class JSQL {
|
|
|
869
923
|
async find(table, filter, opts = {}) {
|
|
870
924
|
this._flushOpsNow();
|
|
871
925
|
if (!(await this._runHooks('beforeFind', [table, { filter, opts }]))) return [];
|
|
872
|
-
const filterStr = filter ?
|
|
926
|
+
const filterStr = filter ? bigintJson.stringify(filter) : '';
|
|
873
927
|
const { limit = 100, offset = 0 } = opts;
|
|
874
928
|
// #35:原生层把 limit/offset 按 `as usize` 无符号转换,负数会静默回绕
|
|
875
929
|
// (release 下空结果 / debug 下 usize::MAX+1 溢出 panic)。绑定层先校验,
|
|
@@ -894,10 +948,11 @@ class JSQL {
|
|
|
894
948
|
}
|
|
895
949
|
|
|
896
950
|
async updateByIds(table, entries) {
|
|
951
|
+
const schema = this._schemas[table];
|
|
897
952
|
const pairs = [];
|
|
898
953
|
for (const [id, data] of entries) {
|
|
899
954
|
const seq = this._seqIdByPk(table, id);
|
|
900
|
-
if (seq !== null) pairs.push([seq, data]);
|
|
955
|
+
if (seq !== null) pairs.push([seq, schema ? normalizeRowForRust(table, schema, data) : data]);
|
|
901
956
|
}
|
|
902
957
|
if (!(await this._runHooks('beforeUpdate', [table, pairs]))) return;
|
|
903
958
|
if (!this._opBuffer.update[table]) this._opBuffer.update[table] = [];
|
|
@@ -922,8 +977,10 @@ class JSQL {
|
|
|
922
977
|
// 改主键 / UNIQUE 列前先查冲突(同步版,与 lib/database.js 的修复对齐)。
|
|
923
978
|
// 此前直接写入,导致 UPDATE t SET id = 1 WHERE id = 2 静默产生两行同主键。
|
|
924
979
|
this._assertNoKeyConflictSync(table, id, data);
|
|
980
|
+
const schema = this._schemas[table];
|
|
981
|
+
const payload = schema ? normalizeRowForRust(table, schema, data) : data;
|
|
925
982
|
if (!this._opBuffer.update[table]) this._opBuffer.update[table] = [];
|
|
926
|
-
this._opBuffer.update[table].push([seq,
|
|
983
|
+
this._opBuffer.update[table].push([seq, payload]);
|
|
927
984
|
this._scheduleOpFlush();
|
|
928
985
|
}
|
|
929
986
|
|
|
@@ -953,7 +1010,7 @@ class JSQL {
|
|
|
953
1010
|
if (valOf(self, f) === nv) continue; // 值没变
|
|
954
1011
|
// 只需知道「是否还存在另一行占用了这个键值」:limit 2 让原生层命中即停,
|
|
955
1012
|
// 输出量恒定,不随表规模增长。
|
|
956
|
-
const probe = safeParse(this._n('jsqlFind', table,
|
|
1013
|
+
const probe = safeParse(this._n('jsqlFind', table, bigintJson.stringify({ [f]: nv }), 2, 0));
|
|
957
1014
|
if (!Array.isArray(probe)) continue;
|
|
958
1015
|
if (probe.some(r => String(idOf(r)) !== String(id))) {
|
|
959
1016
|
const e = new Error(`Duplicate entry '${nv}' for key '${f}'`);
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* jsql uninstall —— 清理 jsql-neo 的用户级数据(可选卸载全局包)。
|
|
4
|
+
*
|
|
5
|
+
* 红线:
|
|
6
|
+
* - 只允许删除用户 home 目录下、且属于 jsql 的固定路径;
|
|
7
|
+
* - 拒绝删除 home 本身、文件系统根、当前工作目录、包目录(仓库)及其祖先/子目录;
|
|
8
|
+
* - 绝不静默删除:非 TTY 且没有 --yes 时直接拒绝;TTY 下要求交互确认。
|
|
9
|
+
*
|
|
10
|
+
* 覆盖的数据位置(与代码里的实际路径保持一致):
|
|
11
|
+
* ~/.jsql/ 插件注册表安装目录(lib/database.js / src/cli/plugin.js)
|
|
12
|
+
* ~/.config/jsql/ 模块配置 mod.config(lib/mod.js)
|
|
13
|
+
* ~/.jsqlmr star 提示状态(lib/star-nag.js)
|
|
14
|
+
* ~/.cache/jsql/ 可能的缓存(存在才删)
|
|
15
|
+
* 支持 JSQL_PLUGIN_DIR / JSQL_MOD_CONFIG / JSQL_MR_PATH / JSQL_CACHE_DIR 覆盖。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const readline = require('readline');
|
|
22
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
const PKG_NAME = 'jsql-neo';
|
|
25
|
+
|
|
26
|
+
function packageDir() {
|
|
27
|
+
return path.resolve(__dirname, '..');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function npmCommand() {
|
|
31
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isInside(child, parent) {
|
|
35
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
36
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 列出所有清理目标。
|
|
41
|
+
* @param {{keepData?: boolean}} [opts]
|
|
42
|
+
* @returns {Array<{path: string, kind: 'dir'|'file', label: string}>}
|
|
43
|
+
*/
|
|
44
|
+
function collectTargets(opts = {}) {
|
|
45
|
+
if (opts.keepData) return [];
|
|
46
|
+
const home = os.homedir();
|
|
47
|
+
const targets = [];
|
|
48
|
+
const add = (p, kind, label) => {
|
|
49
|
+
if (!p) return;
|
|
50
|
+
const resolved = path.resolve(p);
|
|
51
|
+
if (targets.some((t) => t.path === resolved)) return;
|
|
52
|
+
targets.push({ path: resolved, kind, label });
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
add(process.env.JSQL_PLUGIN_DIR || path.join(home, '.jsql'), 'dir', '插件/用户数据 (~/.jsql)');
|
|
56
|
+
|
|
57
|
+
if (process.env.JSQL_MOD_CONFIG) {
|
|
58
|
+
add(process.env.JSQL_MOD_CONFIG, 'file', '模块配置 (JSQL_MOD_CONFIG)');
|
|
59
|
+
} else {
|
|
60
|
+
add(path.join(home, '.config', 'jsql'), 'dir', '模块配置 (~/.config/jsql)');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
add(process.env.JSQL_MR_PATH || path.join(home, '.jsqlmr'), 'file', 'star 状态 (~/.jsqlmr)');
|
|
64
|
+
add(process.env.JSQL_CACHE_DIR || path.join(home, '.cache', 'jsql'), 'dir', '缓存 (~/.cache/jsql)');
|
|
65
|
+
|
|
66
|
+
return targets;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 安全校验:任何目标都必须落在 home 下、且不能碰到仓库/工作目录。
|
|
71
|
+
* @throws {Error} 校验不通过
|
|
72
|
+
*/
|
|
73
|
+
function assertSafeTarget(target) {
|
|
74
|
+
const resolved = path.resolve(target.path || target);
|
|
75
|
+
const home = path.resolve(os.homedir());
|
|
76
|
+
const pkg = packageDir();
|
|
77
|
+
const cwd = path.resolve(process.cwd());
|
|
78
|
+
const root = path.parse(resolved).root;
|
|
79
|
+
|
|
80
|
+
if (resolved === root) {
|
|
81
|
+
throw new Error(`refusing to remove filesystem root: ${resolved}`);
|
|
82
|
+
}
|
|
83
|
+
if (resolved === home) {
|
|
84
|
+
throw new Error(`refusing to remove home directory: ${resolved}`);
|
|
85
|
+
}
|
|
86
|
+
if (!isInside(resolved, home)) {
|
|
87
|
+
throw new Error(`refusing to remove path outside home (${home}): ${resolved}`);
|
|
88
|
+
}
|
|
89
|
+
if (isInside(pkg, resolved) || isInside(resolved, pkg)) {
|
|
90
|
+
throw new Error(`refusing to remove path overlapping the jsql-neo package: ${resolved}`);
|
|
91
|
+
}
|
|
92
|
+
// 目标不能是当前工作目录的祖先(否则会连工作目录一起删掉)
|
|
93
|
+
if (isInside(cwd, resolved)) {
|
|
94
|
+
throw new Error(`refusing to remove an ancestor of the current working directory: ${resolved}`);
|
|
95
|
+
}
|
|
96
|
+
// 目标必须是 jsql 命名的目录/文件,杜绝误删
|
|
97
|
+
const base = path.basename(resolved).toLowerCase();
|
|
98
|
+
const looksLikeJsql = base === '.jsqlmr' || base.startsWith('jsql') || base === '.jsql';
|
|
99
|
+
const parentIsConfig = path.basename(path.dirname(resolved)) === '.config'
|
|
100
|
+
|| path.basename(path.dirname(resolved)) === '.cache';
|
|
101
|
+
if (!looksLikeJsql && !parentIsConfig) {
|
|
102
|
+
throw new Error(`refusing to remove unexpected path: ${resolved}`);
|
|
103
|
+
}
|
|
104
|
+
return resolved;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 检查目标是否存在,并给出可删除列表(不实际删除)。
|
|
109
|
+
* @param {{keepData?: boolean}} [opts]
|
|
110
|
+
*/
|
|
111
|
+
function inspectTargets(opts = {}) {
|
|
112
|
+
return collectTargets(opts).map((t) => {
|
|
113
|
+
let exists = false;
|
|
114
|
+
let isSymlink = false;
|
|
115
|
+
try {
|
|
116
|
+
const st = fs.lstatSync(t.path);
|
|
117
|
+
exists = true;
|
|
118
|
+
isSymlink = st.isSymbolicLink();
|
|
119
|
+
} catch (_) { /* 不存在 */ }
|
|
120
|
+
return { ...t, exists, isSymlink };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 删除数据目标。
|
|
126
|
+
* @param {{keepData?: boolean, dryRun?: boolean}} [opts]
|
|
127
|
+
* @returns {{removed: string[], skipped: string[], failed: Array<{path: string, error: string}>}}
|
|
128
|
+
*/
|
|
129
|
+
function removeData(opts = {}) {
|
|
130
|
+
const result = { removed: [], skipped: [], failed: [] };
|
|
131
|
+
const targets = inspectTargets(opts);
|
|
132
|
+
for (const t of targets) {
|
|
133
|
+
if (!t.exists) {
|
|
134
|
+
result.skipped.push(t.path);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
let safe;
|
|
138
|
+
try {
|
|
139
|
+
safe = assertSafeTarget(t);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
result.failed.push({ path: t.path, error: e.message });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (opts.dryRun) {
|
|
145
|
+
result.removed.push(safe);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
// 符号链接只删链接本身,绝不跟随
|
|
150
|
+
if (t.isSymlink) fs.unlinkSync(safe);
|
|
151
|
+
else fs.rmSync(safe, { recursive: true, force: true });
|
|
152
|
+
result.removed.push(safe);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
result.failed.push({ path: safe, error: e.message });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 卸载全局 npm 包;失败时返回错误信息而不是抛异常 */
|
|
161
|
+
function uninstallGlobal(dryRun) {
|
|
162
|
+
const args = ['uninstall', '-g', PKG_NAME];
|
|
163
|
+
const command = `${npmCommand()} ${args.join(' ')}`;
|
|
164
|
+
if (dryRun) return { ok: true, dryRun: true, command, error: null };
|
|
165
|
+
|
|
166
|
+
const r = spawnSync(npmCommand(), args, {
|
|
167
|
+
stdio: 'inherit',
|
|
168
|
+
timeout: 5 * 60 * 1000,
|
|
169
|
+
});
|
|
170
|
+
if (r.error) return { ok: false, command, error: r.error.message };
|
|
171
|
+
if (r.status !== 0) return { ok: false, command, error: `npm uninstall exited with code ${r.status}` };
|
|
172
|
+
return { ok: true, command, error: null };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 全局安装是否还存在(用 npm root -g 判断) */
|
|
176
|
+
function isGloballyInstalled() {
|
|
177
|
+
try {
|
|
178
|
+
const root = execFileSync(npmCommand(), ['root', '-g'], {
|
|
179
|
+
encoding: 'utf8',
|
|
180
|
+
timeout: 15000,
|
|
181
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
182
|
+
}).trim();
|
|
183
|
+
if (!root) return false;
|
|
184
|
+
return fs.existsSync(path.join(root, PKG_NAME, 'package.json'));
|
|
185
|
+
} catch (_) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 交互确认;非 TTY 返回 false(调用方应要求 --yes) */
|
|
191
|
+
function confirm(question) {
|
|
192
|
+
return new Promise((resolve) => {
|
|
193
|
+
if (!process.stdin.isTTY) {
|
|
194
|
+
resolve(false);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
198
|
+
rl.question(question, (answer) => {
|
|
199
|
+
rl.close();
|
|
200
|
+
resolve(/^y(es)?$/i.test(String(answer || '').trim()));
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
PKG_NAME,
|
|
207
|
+
collectTargets,
|
|
208
|
+
assertSafeTarget,
|
|
209
|
+
inspectTargets,
|
|
210
|
+
removeData,
|
|
211
|
+
uninstallGlobal,
|
|
212
|
+
isGloballyInstalled,
|
|
213
|
+
confirm,
|
|
214
|
+
packageDir,
|
|
215
|
+
isInside,
|
|
216
|
+
};
|