jsql-neo 6.5.16 → 6.5.17
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 +1 -1
- package/index.js +1 -1
- package/lib/sql.js +59 -17
- package/package.json +1 -1
- package/test/extra-coverage.test.js +5 -4
- package/test/v6.5.14-regression.test.js +26 -0
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.17** — 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
|

|
package/index.js
CHANGED
package/lib/sql.js
CHANGED
|
@@ -2885,8 +2885,21 @@ function applyCast(value, dataType) {
|
|
|
2885
2885
|
if (value === null || value === undefined) return null;
|
|
2886
2886
|
// SIGNED / UNSIGNED 是 MySQL 的整型别名(CAST(x AS SIGNED)),
|
|
2887
2887
|
// 此前没列入整型分支,导致 CAST('12abc' AS SIGNED) 原样返回字符串。
|
|
2888
|
+
// CAST(x AS UNSIGNED):MySQL 先取整,再按 64 位无符号回绕(负数变成补码),
|
|
2889
|
+
// CAST(-5 AS UNSIGNED) = 18446744073709551611。用 BigInt 保精度,
|
|
2890
|
+
// 序列化层会把 BigInt 转成字符串(见 6.5.11 的超大整数处理)。
|
|
2891
|
+
if (t === 'UNSIGNED') {
|
|
2892
|
+
let n;
|
|
2893
|
+
if (typeof value === 'boolean') n = value ? 1n : 0n;
|
|
2894
|
+
else if (typeof value === 'number') n = Number.isFinite(value) ? BigInt(Math.trunc(value)) : 0n;
|
|
2895
|
+
else {
|
|
2896
|
+
const m = String(value).trim().match(/^[+-]?\d+(\.\d+)?/);
|
|
2897
|
+
n = m ? BigInt(Math.trunc(Number(m[0]))) : 0n;
|
|
2898
|
+
}
|
|
2899
|
+
return BigInt.asUintN(64, n);
|
|
2900
|
+
}
|
|
2888
2901
|
if (t === 'INTEGER' || t === 'INT' || t === 'BIGINT' || t === 'SMALLINT' ||
|
|
2889
|
-
t === 'TINYINT' || t === 'SIGNED' || t === '
|
|
2902
|
+
t === 'TINYINT' || t === 'SIGNED' || t === 'MEDIUMINT') {
|
|
2890
2903
|
// MySQL CAST(... AS SIGNED/INT):取数字前缀('12abc'→12),无法解析时返回 0(而非 null)。
|
|
2891
2904
|
if (typeof value === 'boolean') return value ? 1 : 0;
|
|
2892
2905
|
if (typeof value === 'number') return Number.isFinite(value) ? Math.trunc(value) : 0;
|
|
@@ -3498,7 +3511,13 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
3498
3511
|
}
|
|
3499
3512
|
case 'UPPER': case 'UCASE': return args[0] == null ? null : String(args[0]).toUpperCase();
|
|
3500
3513
|
case 'LOWER': case 'LCASE': return args[0] == null ? null : String(args[0]).toLowerCase();
|
|
3501
|
-
|
|
3514
|
+
// MySQL:LENGTH 是「字节数」(utf8mb4 下 'héllo' = 6),
|
|
3515
|
+
// CHAR_LENGTH / CHARACTER_LENGTH 才是「字符数」。此前三个全按字符数,
|
|
3516
|
+
// 非 ASCII 文本的字节长度全部偏小。
|
|
3517
|
+
case 'LENGTH': case 'OCTET_LENGTH':
|
|
3518
|
+
return args[0] == null ? null : Buffer.byteLength(String(args[0]), 'utf8');
|
|
3519
|
+
case 'CHAR_LENGTH': case 'CHARACTER_LENGTH':
|
|
3520
|
+
return args[0] == null ? null : String(args[0]).length;
|
|
3502
3521
|
// TRIM(s) / TRIM([LEADING|TRAILING|BOTH] chars FROM s) / TRIM(s, chars)
|
|
3503
3522
|
case 'TRIM': {
|
|
3504
3523
|
if (args[0] == null) return null;
|
|
@@ -3785,7 +3804,10 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
3785
3804
|
case 'FLOOR': return args[0] == null ? null : Math.floor(args[0]);
|
|
3786
3805
|
case 'CEIL': case 'CEILING': return args[0] == null ? null : Math.ceil(args[0]);
|
|
3787
3806
|
case 'MOD': return (args[0] == null || args[1] === 0) ? null : args[0] % args[1];
|
|
3788
|
-
case 'POWER': case 'POW':
|
|
3807
|
+
case 'POWER': case 'POW':
|
|
3808
|
+
// MySQL:任一参数为 NULL 就返回 NULL。Math.pow(2, null) 会按 0 算出 1。
|
|
3809
|
+
if (args[0] == null || args[1] == null) return null;
|
|
3810
|
+
return Math.pow(args[0], args[1]);
|
|
3789
3811
|
case 'SQRT': return args[0] == null ? null : Math.sqrt(args[0]);
|
|
3790
3812
|
case 'IFNULL': case 'NVL': return args[0] != null ? args[0] : args[1];
|
|
3791
3813
|
case 'COALESCE': {
|
|
@@ -3820,8 +3842,20 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
3820
3842
|
}
|
|
3821
3843
|
return s.substr(start - 1);
|
|
3822
3844
|
}
|
|
3823
|
-
|
|
3824
|
-
case '
|
|
3845
|
+
// MySQL:负长度返回空串(不是「从末尾切」或丢参数后的怪异子串)。
|
|
3846
|
+
case 'LEFT': {
|
|
3847
|
+
if (args[0] == null) return null;
|
|
3848
|
+
const n = Number(args[1]);
|
|
3849
|
+
if (!Number.isFinite(n) || n < 0) return '';
|
|
3850
|
+
return String(args[0]).slice(0, n);
|
|
3851
|
+
}
|
|
3852
|
+
case 'RIGHT': {
|
|
3853
|
+
if (args[0] == null) return null;
|
|
3854
|
+
const n = Number(args[1]);
|
|
3855
|
+
if (!Number.isFinite(n) || n < 0) return '';
|
|
3856
|
+
if (n === 0) return '';
|
|
3857
|
+
return String(args[0]).slice(-n);
|
|
3858
|
+
}
|
|
3825
3859
|
case 'LOCATE': {
|
|
3826
3860
|
if (args[0] == null || args[1] == null) return null;
|
|
3827
3861
|
const idx = String(args[1]).indexOf(String(args[0]));
|
|
@@ -3884,23 +3918,31 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
3884
3918
|
}
|
|
3885
3919
|
return Array.isArray(v) ? v.length : null;
|
|
3886
3920
|
}
|
|
3921
|
+
// MySQL:填充串按「需要多少补多少」截断,不能整段循环拼接——
|
|
3922
|
+
// LPAD('hi',5,'xy') = 'xyxhi'(此前每轮都加完整的 'xy',得到 'xyxyhi')。
|
|
3887
3923
|
case 'LPAD': {
|
|
3888
|
-
if (args[0] == null) return null;
|
|
3889
|
-
|
|
3890
|
-
const
|
|
3891
|
-
const
|
|
3924
|
+
if (args[0] == null || args[1] == null) return null;
|
|
3925
|
+
if (args.length >= 3 && args[2] == null) return null;
|
|
3926
|
+
const s = String(args[0]);
|
|
3927
|
+
const n = Math.trunc(Number(args[1]));
|
|
3928
|
+
if (!Number.isFinite(n)) return null;
|
|
3892
3929
|
if (n <= s.length) return s.slice(0, n);
|
|
3893
|
-
|
|
3894
|
-
return s;
|
|
3930
|
+
const pad = args[2] === undefined ? ' ' : String(args[2]);
|
|
3931
|
+
if (pad === '') return s;
|
|
3932
|
+
const need = n - s.length;
|
|
3933
|
+
return pad.repeat(Math.ceil(need / pad.length)).slice(0, need) + s;
|
|
3895
3934
|
}
|
|
3896
3935
|
case 'RPAD': {
|
|
3897
|
-
if (args[0] == null) return null;
|
|
3898
|
-
|
|
3899
|
-
const
|
|
3900
|
-
const
|
|
3936
|
+
if (args[0] == null || args[1] == null) return null;
|
|
3937
|
+
if (args.length >= 3 && args[2] == null) return null;
|
|
3938
|
+
const s = String(args[0]);
|
|
3939
|
+
const n = Math.trunc(Number(args[1]));
|
|
3940
|
+
if (!Number.isFinite(n)) return null;
|
|
3901
3941
|
if (n <= s.length) return s.slice(0, n);
|
|
3902
|
-
|
|
3903
|
-
return s;
|
|
3942
|
+
const pad = args[2] === undefined ? ' ' : String(args[2]);
|
|
3943
|
+
if (pad === '') return s;
|
|
3944
|
+
const need = n - s.length;
|
|
3945
|
+
return s + pad.repeat(Math.ceil(need / pad.length)).slice(0, need);
|
|
3904
3946
|
}
|
|
3905
3947
|
case 'RAND': return args.length > 0 && args[0] != null ? seedRand(Number(args[0]))() : Math.random();
|
|
3906
3948
|
case 'UNIX_TIMESTAMP': {
|
package/package.json
CHANGED
|
@@ -159,15 +159,16 @@ test('超长字符串 100000 字符正常处理', async () => {
|
|
|
159
159
|
assert.strictEqual(await one(q, `SELECT LENGTH('${long}')`), 100000);
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
-
test('Unicode é 正确处理(LENGTH=5, UPPER 含重音)', async () => {
|
|
162
|
+
test('Unicode é 正确处理(LENGTH=6 字节 / CHAR_LENGTH=5, UPPER 含重音)', async () => {
|
|
163
163
|
const { q } = makeDb();
|
|
164
|
-
assert.strictEqual(await one(q, "SELECT LENGTH('héllo')"),
|
|
164
|
+
assert.strictEqual(await one(q, "SELECT LENGTH('héllo')"), 6);
|
|
165
|
+
assert.strictEqual(await one(q, "SELECT CHAR_LENGTH('héllo')"), 5);
|
|
165
166
|
assert.strictEqual(await one(q, "SELECT UPPER('café')"), 'CAFÉ');
|
|
166
167
|
});
|
|
167
168
|
|
|
168
|
-
test('Emoji 按 UTF-
|
|
169
|
+
test('Emoji 按 UTF-8 字节计数(LENGTH=8)', async () => {
|
|
169
170
|
const { q } = makeDb();
|
|
170
|
-
assert.strictEqual(await one(q, "SELECT LENGTH('🚀🌟')"),
|
|
171
|
+
assert.strictEqual(await one(q, "SELECT LENGTH('🚀🌟')"), 8);
|
|
171
172
|
});
|
|
172
173
|
|
|
173
174
|
test('Emoji UPPER 无变化', async () => {
|
|
@@ -272,6 +272,32 @@ function makeDb() { return new Database(':memory:'); }
|
|
|
272
272
|
ok('RLIKE BINARY 同样生效', eq((await q("SELECT id FROM r6 WHERE s RLIKE BINARY 'abc'")).rows, [[1]]));
|
|
273
273
|
ok('非法正则安全返回不匹配', eq((await q("SELECT id FROM r6 WHERE s REGEXP BINARY '('")).rows, []));
|
|
274
274
|
|
|
275
|
+
// ── B1–B6. 标量函数语义(v6.5.17) ─────────────────────────
|
|
276
|
+
console.log('\n--- B1–B6. 标量函数 ---');
|
|
277
|
+
ok('B1 LENGTH 按字节(héllo=6)', (await one("SELECT LENGTH('héllo')")) === 6, await one("SELECT LENGTH('héllo')"));
|
|
278
|
+
ok('B1 CHAR_LENGTH 按字符(héllo=5)', (await one("SELECT CHAR_LENGTH('héllo')")) === 5);
|
|
279
|
+
ok('B1 emoji 字节数(🚀🌟=8)', (await one("SELECT LENGTH('🚀🌟')")) === 8);
|
|
280
|
+
ok('B2 OCTET_LENGTH 已实现(héllo=6)', (await one("SELECT OCTET_LENGTH('héllo')")) === 6);
|
|
281
|
+
ok('B2 OCTET_LENGTH NULL → NULL', (await one('SELECT OCTET_LENGTH(NULL)')) === null);
|
|
282
|
+
ok("B3 LEFT('hello',-3) → ''", (await one("SELECT LEFT('hello',-3)")) === '');
|
|
283
|
+
ok("B3 RIGHT('hello',-3) → ''", (await one("SELECT RIGHT('hello',-3)")) === '');
|
|
284
|
+
ok("B3 LEFT('hello',0) → ''", (await one("SELECT LEFT('hello',0)")) === '');
|
|
285
|
+
ok("B3 LEFT('hello',3) → 'hel'", (await one("SELECT LEFT('hello',3)")) === 'hel');
|
|
286
|
+
ok("B3 RIGHT('hello',3) → 'llo'", (await one("SELECT RIGHT('hello',3)")) === 'llo');
|
|
287
|
+
ok("B4 LPAD('hi',5,'xy') → 'xyxhi'", (await one("SELECT LPAD('hi',5,'xy')")) === 'xyxhi',
|
|
288
|
+
await one("SELECT LPAD('hi',5,'xy')"));
|
|
289
|
+
ok("B4 RPAD('hi',5,'xy') → 'hixyx'", (await one("SELECT RPAD('hi',5,'xy')")) === 'hixyx',
|
|
290
|
+
await one("SELECT RPAD('hi',5,'xy')"));
|
|
291
|
+
ok("B4 LPAD 目标更短时截断", (await one("SELECT LPAD('hello',3,'xy')")) === 'hel');
|
|
292
|
+
ok('B4 LPAD 填充串为 NULL → NULL', (await one("SELECT LPAD('hi',5,NULL)")) === null);
|
|
293
|
+
ok('B5 POWER(2,NULL) → NULL', (await one('SELECT POWER(2,NULL)')) === null);
|
|
294
|
+
ok('B5 POW(NULL,2) → NULL', (await one('SELECT POW(NULL,2)')) === null);
|
|
295
|
+
ok('B5 POWER 正常仍为 8', (await one('SELECT POWER(2,3)')) === 8);
|
|
296
|
+
ok('B6 CAST(-5 AS UNSIGNED) 64 位回绕', String(await one('SELECT CAST(-5 AS UNSIGNED)')) === '18446744073709551611',
|
|
297
|
+
String(await one('SELECT CAST(-5 AS UNSIGNED)')));
|
|
298
|
+
ok('B6 CAST(5 AS UNSIGNED) 保持原值', String(await one('SELECT CAST(5 AS UNSIGNED)')) === '5');
|
|
299
|
+
ok('B6 CAST(-5 AS SIGNED) 不回绕', (await one('SELECT CAST(-5 AS SIGNED)')) === -5);
|
|
300
|
+
|
|
275
301
|
// ── M9. 插件钩子优先级 ─────────────────────────────────────
|
|
276
302
|
console.log('\n--- M9. 钩子优先级 ---');
|
|
277
303
|
{
|