jsql-neo 6.4.0-beta.14 → 6.4.0-beta.16

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 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.4.0-beta.14** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
6
+ > **v6.4.0-beta.16** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
7
7
 
8
8
  ![Engines](https://img.shields.io/badge/engines-Native%20%7C%20WASM%20%7C%20Pure%20JS-7ee787)
9
9
  ![MySQL](https://img.shields.io/badge/protocol-MySQL%20compatible-1f6feb)
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.4.0-beta.14 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.4.0-beta.16 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
package/lib/btree.js CHANGED
@@ -171,9 +171,10 @@ class BTree {
171
171
  for (let i = 0; i < node.keys.length; i++) {
172
172
  const k = node.keys[i];
173
173
  const s = k === undefined || k === null ? '' : String(k);
174
- if (s < p) continue;
175
- if (!s.startsWith(p)) return result;
176
- result.push(...node.values[i]);
174
+ // 不能假设键按字典序排序(数字键按数值序),必须检查所有键
175
+ if (s.startsWith(p)) {
176
+ result.push(...node.values[i]);
177
+ }
177
178
  }
178
179
  node = node.next;
179
180
  }
@@ -326,13 +327,14 @@ class BTree {
326
327
  const left = parent.children[i - 1];
327
328
  const right = parent.children[i];
328
329
  if (right.leaf) {
329
- // 叶子节点:分隔键是叶子的副本,用被移动的键而非旧分隔键,避免重复
330
+ // 叶子节点:移动左叶最大键到右叶最小位置
330
331
  const k = left.keys.pop();
331
332
  const v = left.values.pop();
332
333
  right.keys.unshift(k);
333
334
  right.values.unshift(v);
334
- parent.keys[i - 1] = k;
335
- parent.values[i - 1] = v;
335
+ // 父节点分隔键应为左叶新的最大值(pop 后的最后一个键)
336
+ parent.keys[i - 1] = left.keys[left.keys.length - 1];
337
+ parent.values[i - 1] = left.values[left.values.length - 1];
336
338
  } else {
337
339
  right.keys.unshift(parent.keys[i - 1]);
338
340
  right.values.unshift(parent.values[i - 1]);
package/lib/query.js CHANGED
@@ -675,6 +675,12 @@ class Query {
675
675
  for (const { field, dir } of this._orderBy) {
676
676
  const va = a[field];
677
677
  const vb = b[field];
678
+ // MySQL 语义:NULL 在 ASC 时排在最前,DESC 时排在最后
679
+ const aNull = va === null || va === undefined;
680
+ const bNull = vb === null || vb === undefined;
681
+ if (aNull && bNull) continue;
682
+ if (aNull) return dir === 'asc' ? -1 : 1;
683
+ if (bNull) return dir === 'asc' ? 1 : -1;
678
684
  if (va < vb) return dir === 'asc' ? -1 : 1;
679
685
  if (va > vb) return dir === 'asc' ? 1 : -1;
680
686
  }
package/lib/sql.js CHANGED
@@ -2908,7 +2908,7 @@ function resolveOperand(operand, row, ctx) {
2908
2908
  }
2909
2909
  // P1-19: ON DUPLICATE KEY UPDATE 表达式赋值,如 count = count + 1
2910
2910
  case 'binary': {
2911
- return evaluateExpr({ type: 'arith', op: operand.op, left: operand.left, right: operand.right }, row, ctx);
2911
+ return evaluateExpr(operand, row, ctx);
2912
2912
  }
2913
2913
  case 'func':
2914
2914
  return applyScalarFunction(operand, row, ctx);
@@ -3622,6 +3622,16 @@ function applyScalarFunction(fnNode, row, ctx) {
3622
3622
  });
3623
3623
  case 'DATABASE': case 'SCHEMA': return row && row.__db !== undefined ? row.__db : 'default';
3624
3624
  // ===== JSON 函数(B2 修复:此前未实现,静默返回 NULL)=====
3625
+ // 辅助函数:获取 JSON 值的类型
3626
+ function getJsonType(val) {
3627
+ if (val === null) return 'null';
3628
+ if (Array.isArray(val)) return 'array';
3629
+ if (typeof val === 'object') return 'object';
3630
+ if (typeof val === 'string') return 'string';
3631
+ if (typeof val === 'number') return Number.isInteger(val) ? 'integer' : 'number';
3632
+ if (typeof val === 'boolean') return 'boolean';
3633
+ return 'unknown';
3634
+ }
3625
3635
  case 'JSON_OBJECT': {
3626
3636
  // JSON_OBJECT(k1, v1, k2, v2, ...) → { k1: v1, k2: v2 }
3627
3637
  const obj = {};
@@ -3657,7 +3667,7 @@ function applyScalarFunction(fnNode, row, ctx) {
3657
3667
  if (firstKey === '') return data;
3658
3668
  let current = data;
3659
3669
  // 逐段解析路径
3660
- const segments = path.replace(/^\$/, '').split('.');
3670
+ const segments = path.replace(/^\$\.?/, '').split('.');
3661
3671
  for (const seg of segments) {
3662
3672
  if (current == null) return null;
3663
3673
  // 处理数组索引: key[0] 或 [0]
@@ -3675,6 +3685,353 @@ function applyScalarFunction(fnNode, row, ctx) {
3675
3685
  }
3676
3686
  return current === undefined ? null : current;
3677
3687
  }
3688
+ case 'JSON_KEYS': {
3689
+ // JSON_KEYS(json) → [key1, key2, ...]
3690
+ const json = args[0];
3691
+ if (json == null) return null;
3692
+ let data = json;
3693
+ if (typeof json === 'string') {
3694
+ try { data = JSON.parse(json); } catch (e) { return null; }
3695
+ }
3696
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) return null;
3697
+ return Object.keys(data);
3698
+ }
3699
+ case 'JSON_LENGTH': {
3700
+ // JSON_LENGTH(json[, path]) → 元素数/长度
3701
+ const json = args[0];
3702
+ const path = args[1];
3703
+ if (json == null) return null;
3704
+ let data = json;
3705
+ if (typeof json === 'string') {
3706
+ try { data = JSON.parse(json); } catch (e) { return null; }
3707
+ }
3708
+ if (path != null) {
3709
+ // 按路径提取后取长度
3710
+ const p = String(path).replace(/^\$\.?/, '');
3711
+ if (p === '') return Array.isArray(data) ? data.length : (typeof data === 'object' && data !== null ? Object.keys(data).length : 0);
3712
+ let current = data;
3713
+ for (const seg of p.split('.')) {
3714
+ if (current == null) return null;
3715
+ const arrMatch = seg.match(/^([^\[]*)\[(\d+)\]$/);
3716
+ if (arrMatch) {
3717
+ const key = arrMatch[1];
3718
+ const idx = parseInt(arrMatch[2], 10);
3719
+ if (key) current = current[key];
3720
+ if (Array.isArray(current)) current = current[idx];
3721
+ else return null;
3722
+ } else {
3723
+ if (typeof current !== 'object' || current === null) return null;
3724
+ current = current[seg];
3725
+ }
3726
+ }
3727
+ if (current == null) return null;
3728
+ if (Array.isArray(current)) return current.length;
3729
+ if (typeof current === 'object') return Object.keys(current).length;
3730
+ if (typeof current === 'string') return current.length;
3731
+ return 1;
3732
+ }
3733
+ if (Array.isArray(data)) return data.length;
3734
+ if (typeof data === 'object' && data !== null) return Object.keys(data).length;
3735
+ if (typeof data === 'string') return data.length;
3736
+ return 1;
3737
+ }
3738
+ case 'JSON_MERGE': case 'JSON_MERGE_PRESERVE': {
3739
+ // JSON_MERGE_PRESERVE(json1, json2, ...) → 合并后的 JSON
3740
+ const items = args.filter(a => a != null);
3741
+ if (!items.length) return null;
3742
+ const parsed = items.map(item => {
3743
+ if (typeof item === 'string') { try { return JSON.parse(item); } catch (e) { return null; } }
3744
+ return item;
3745
+ }).filter(a => a != null);
3746
+ if (!parsed.length) return null;
3747
+ // 递归合并
3748
+ function mergePreserve(target, source) {
3749
+ if (Array.isArray(target) && Array.isArray(source)) {
3750
+ return [...target, ...source];
3751
+ }
3752
+ if (typeof target === 'object' && target !== null && typeof source === 'object' && source !== null && !Array.isArray(target) && !Array.isArray(source)) {
3753
+ const result = { ...target };
3754
+ for (const k of Object.keys(source)) {
3755
+ if (k in result) result[k] = mergePreserve(result[k], source[k]);
3756
+ else result[k] = source[k];
3757
+ }
3758
+ return result;
3759
+ }
3760
+ return source;
3761
+ }
3762
+ return parsed.reduce((acc, cur) => mergePreserve(acc, cur));
3763
+ }
3764
+ case 'JSON_MERGE_PATCH': {
3765
+ // JSON_MERGE_PATCH(json1, json2, ...) → RFC 7396 合并
3766
+ const items = args.filter(a => a != null);
3767
+ if (!items.length) return null;
3768
+ const parsed = items.map(item => {
3769
+ if (typeof item === 'string') { try { return JSON.parse(item); } catch (e) { return null; } }
3770
+ return item;
3771
+ }).filter(a => a != null);
3772
+ if (!parsed.length) return null;
3773
+ function mergePatch(target, source) {
3774
+ if (typeof source !== 'object' || source === null || Array.isArray(source)) return source;
3775
+ if (typeof target !== 'object' || target === null || Array.isArray(target)) return { ...source };
3776
+ const result = { ...target };
3777
+ for (const k of Object.keys(source)) {
3778
+ if (source[k] === null) delete result[k];
3779
+ else result[k] = mergePatch(result[k], source[k]);
3780
+ }
3781
+ return result;
3782
+ }
3783
+ return parsed.reduce((acc, cur) => mergePatch(acc, cur));
3784
+ }
3785
+ case 'JSON_OBJECTAGG': {
3786
+ // JSON_OBJECTAGG(key, value) → { key: value } (聚合函数)
3787
+ // 在 JSQL 中作为普通函数调用,接收 key 和 value
3788
+ const key = args[0];
3789
+ const value = args[1];
3790
+ if (key == null) return null;
3791
+ const obj = {};
3792
+ obj[String(key)] = value === undefined ? null : value;
3793
+ return obj;
3794
+ }
3795
+ case 'JSON_ARRAYAGG': {
3796
+ // JSON_ARRAYAGG(expr) → [expr] (聚合函数)
3797
+ // 在 JSQL 中作为普通函数调用,接收单个值
3798
+ const val = args[0];
3799
+ return [val === undefined ? null : val];
3800
+ }
3801
+ case 'JSON_SET': {
3802
+ // JSON_SET(json, path, val, ...) → 设置值(路径存在则更新,不存在则创建)
3803
+ const json = args[0];
3804
+ if (json == null) return null;
3805
+ let data = json;
3806
+ if (typeof json === 'string') {
3807
+ try { data = JSON.parse(json); } catch (e) { return null; }
3808
+ }
3809
+ // 深拷贝
3810
+ data = JSON.parse(JSON.stringify(data));
3811
+ for (let i = 1; i + 1 < args.length; i += 2) {
3812
+ const path = String(args[i]);
3813
+ const val = args[i + 1];
3814
+ const p = path.replace(/^\$\.?/, '');
3815
+ if (p === '') continue;
3816
+ const segments = p.split('.');
3817
+ let current = data;
3818
+ for (let j = 0; j < segments.length - 1; j++) {
3819
+ const seg = segments[j];
3820
+ const arrMatch = seg.match(/^([^\[]*)\[(\d+)\]$/);
3821
+ if (arrMatch) {
3822
+ const key = arrMatch[1];
3823
+ const idx = parseInt(arrMatch[2], 10);
3824
+ if (key) current = current[key];
3825
+ if (Array.isArray(current)) current = current[idx];
3826
+ else { current = null; break; }
3827
+ } else {
3828
+ if (typeof current !== 'object' || current === null) { current = null; break; }
3829
+ if (!(seg in current)) current[seg] = {};
3830
+ current = current[seg];
3831
+ }
3832
+ }
3833
+ if (current == null) continue;
3834
+ const last = segments[segments.length - 1];
3835
+ const arrMatch = last.match(/^([^\[]*)\[(\d+)\]$/);
3836
+ if (arrMatch) {
3837
+ const key = arrMatch[1];
3838
+ const idx = parseInt(arrMatch[2], 10);
3839
+ if (key) current = current[key];
3840
+ if (Array.isArray(current)) current[idx] = val;
3841
+ } else {
3842
+ current[last] = val;
3843
+ }
3844
+ }
3845
+ return data;
3846
+ }
3847
+ case 'JSON_INSERT': {
3848
+ // JSON_INSERT(json, path, val, ...) → 仅路径不存在时插入
3849
+ const json = args[0];
3850
+ if (json == null) return null;
3851
+ let data = json;
3852
+ if (typeof json === 'string') {
3853
+ try { data = JSON.parse(json); } catch (e) { return null; }
3854
+ }
3855
+ data = JSON.parse(JSON.stringify(data));
3856
+ for (let i = 1; i + 1 < args.length; i += 2) {
3857
+ const path = String(args[i]);
3858
+ const val = args[i + 1];
3859
+ const p = path.replace(/^\$\.?/, '');
3860
+ if (p === '') continue;
3861
+ const segments = p.split('.');
3862
+ let current = data;
3863
+ let exists = true;
3864
+ for (let j = 0; j < segments.length - 1; j++) {
3865
+ const seg = segments[j];
3866
+ const arrMatch = seg.match(/^([^\[]*)\[(\d+)\]$/);
3867
+ if (arrMatch) {
3868
+ const key = arrMatch[1];
3869
+ const idx = parseInt(arrMatch[2], 10);
3870
+ if (key) current = current[key];
3871
+ if (Array.isArray(current)) current = current[idx];
3872
+ else { exists = false; break; }
3873
+ } else {
3874
+ if (typeof current !== 'object' || current === null || !(seg in current)) { exists = false; break; }
3875
+ current = current[seg];
3876
+ }
3877
+ }
3878
+ if (!exists) continue;
3879
+ const last = segments[segments.length - 1];
3880
+ if (typeof current === 'object' && current !== null && !(last in current)) {
3881
+ current[last] = val;
3882
+ }
3883
+ }
3884
+ return data;
3885
+ }
3886
+ case 'JSON_REMOVE': {
3887
+ // JSON_REMOVE(json, path, ...) → 删除路径
3888
+ const json = args[0];
3889
+ if (json == null) return null;
3890
+ let data = json;
3891
+ if (typeof json === 'string') {
3892
+ try { data = JSON.parse(json); } catch (e) { return null; }
3893
+ }
3894
+ data = JSON.parse(JSON.stringify(data));
3895
+ for (let i = 1; i < args.length; i++) {
3896
+ const path = String(args[i]);
3897
+ const p = path.replace(/^\$\.?/, '');
3898
+ if (p === '') continue;
3899
+ const segments = p.split('.');
3900
+ let current = data;
3901
+ for (let j = 0; j < segments.length - 1; j++) {
3902
+ const seg = segments[j];
3903
+ const arrMatch = seg.match(/^([^\[]*)\[(\d+)\]$/);
3904
+ if (arrMatch) {
3905
+ const key = arrMatch[1];
3906
+ const idx = parseInt(arrMatch[2], 10);
3907
+ if (key) current = current[key];
3908
+ if (Array.isArray(current)) current = current[idx];
3909
+ else { current = null; break; }
3910
+ } else {
3911
+ if (typeof current !== 'object' || current === null) { current = null; break; }
3912
+ current = current[seg];
3913
+ }
3914
+ }
3915
+ if (current == null) continue;
3916
+ const last = segments[segments.length - 1];
3917
+ const arrMatch = last.match(/^([^\[]*)\[(\d+)\]$/);
3918
+ if (arrMatch) {
3919
+ const key = arrMatch[1];
3920
+ const idx = parseInt(arrMatch[2], 10);
3921
+ if (key) current = current[key];
3922
+ if (Array.isArray(current)) current.splice(idx, 1);
3923
+ } else {
3924
+ delete current[last];
3925
+ }
3926
+ }
3927
+ return data;
3928
+ }
3929
+ case 'JSON_UNQUOTE': {
3930
+ // JSON_UNQUOTE(json_val) → 去掉 JSON 字符串的引号
3931
+ const val = args[0];
3932
+ if (val == null) return null;
3933
+ if (typeof val === 'string') {
3934
+ // 如果是 JSON 字符串(带引号),解析它
3935
+ const trimmed = val.trim();
3936
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
3937
+ try { return JSON.parse(trimmed); } catch (e) { return trimmed.slice(1, -1); }
3938
+ }
3939
+ return val;
3940
+ }
3941
+ return val;
3942
+ }
3943
+ case 'JSON_QUOTE': {
3944
+ // JSON_QUOTE(str) → 给字符串加引号
3945
+ const val = args[0];
3946
+ if (val == null) return null;
3947
+ return JSON.stringify(String(val));
3948
+ }
3949
+ case 'JSON_TYPE': {
3950
+ // JSON_TYPE(json[, path]) → 返回类型
3951
+ const json = args[0];
3952
+ const path = args[1];
3953
+ if (json == null) return null;
3954
+ let data = json;
3955
+ if (typeof json === 'string') {
3956
+ try { data = JSON.parse(json); } catch (e) { return null; }
3957
+ }
3958
+ if (path != null) {
3959
+ const p = String(path).replace(/^\$\.?/, '');
3960
+ if (p === '') return getJsonType(data);
3961
+ let current = data;
3962
+ for (const seg of p.split('.')) {
3963
+ if (current == null) return null;
3964
+ const arrMatch = seg.match(/^([^\[]*)\[(\d+)\]$/);
3965
+ if (arrMatch) {
3966
+ const key = arrMatch[1];
3967
+ const idx = parseInt(arrMatch[2], 10);
3968
+ if (key) current = current[key];
3969
+ if (Array.isArray(current)) current = current[idx];
3970
+ else return null;
3971
+ } else {
3972
+ if (typeof current !== 'object' || current === null) return null;
3973
+ current = current[seg];
3974
+ }
3975
+ }
3976
+ return getJsonType(current);
3977
+ }
3978
+ return getJsonType(data);
3979
+ }
3980
+ case 'JSON_DEPTH': {
3981
+ // JSON_DEPTH(json) → 返回深度
3982
+ const json = args[0];
3983
+ if (json == null) return null;
3984
+ let data = json;
3985
+ if (typeof json === 'string') {
3986
+ try { data = JSON.parse(json); } catch (e) { return null; }
3987
+ }
3988
+ function depth(obj) {
3989
+ if (typeof obj !== 'object' || obj === null) return 0;
3990
+ if (Array.isArray(obj)) {
3991
+ if (obj.length === 0) return 1;
3992
+ return 1 + Math.max(...obj.map(depth));
3993
+ }
3994
+ const keys = Object.keys(obj);
3995
+ if (keys.length === 0) return 1;
3996
+ return 1 + Math.max(...keys.map(k => depth(obj[k])));
3997
+ }
3998
+ return depth(data);
3999
+ }
4000
+ case 'JSON_VALID': {
4001
+ // JSON_VALID(json) → 1 if valid JSON, 0 otherwise
4002
+ const val = args[0];
4003
+ if (val == null) return 0;
4004
+ if (typeof val !== 'string') return 1; // 非字符串视为有效
4005
+ try { JSON.parse(val); return 1; } catch (e) { return 0; }
4006
+ }
4007
+ case 'JSON_SEARCH': {
4008
+ // JSON_SEARCH(json, one_or_all, search_str) → 搜索路径
4009
+ const json = args[0];
4010
+ const mode = args[1];
4011
+ const search = args[2];
4012
+ if (json == null || search == null) return null;
4013
+ let data = json;
4014
+ if (typeof json === 'string') {
4015
+ try { data = JSON.parse(json); } catch (e) { return null; }
4016
+ }
4017
+ const searchStr = String(search).toLowerCase();
4018
+ const results = [];
4019
+ function searchObj(obj, currentPath) {
4020
+ if (typeof obj === 'string' && obj.toLowerCase().includes(searchStr)) {
4021
+ results.push(currentPath);
4022
+ } else if (Array.isArray(obj)) {
4023
+ obj.forEach((item, i) => searchObj(item, currentPath + '[' + i + ']'));
4024
+ } else if (typeof obj === 'object' && obj !== null) {
4025
+ for (const k of Object.keys(obj)) {
4026
+ searchObj(obj[k], currentPath ? currentPath + '.' + k : '$.' + k);
4027
+ }
4028
+ }
4029
+ }
4030
+ searchObj(data, '$');
4031
+ if (results.length === 0) return null;
4032
+ if (mode === 'one') return results[0];
4033
+ return results;
4034
+ }
3678
4035
  case 'JSON_CONTAINS': {
3679
4036
  // JSON_CONTAINS(json, value) → 1 or 0
3680
4037
  const json = args[0];
@@ -3866,10 +4223,14 @@ function evaluateExpr(expr, row, ctx) {
3866
4223
  }
3867
4224
  case 'in': {
3868
4225
  const v = resolveOperand(expr.operand, row, ctx);
4226
+ if (v === null || v === undefined) return null; // NULL IN (...) → NULL(三值逻辑)
3869
4227
  if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
3870
- if (!expr.list) return false;
4228
+ if (!expr.list) return expr.not ? true : false;
4229
+ // 列表中出现 NULL 时:三值逻辑 → NULL IN (1,2,NULL) 为 NULL(UNKNOWN)
4230
+ const hasNull = expr.list.some(x => x === null || x === undefined);
4231
+ if (hasNull) return null;
3871
4232
  const r = expr.list.some(x => x === v || String(x) === String(v));
3872
- return expr.not ? !r : r; // NOT IN:此前漏了取反,导致 NOT IN 与 IN 结果相同
4233
+ return expr.not ? !r : r;
3873
4234
  }
3874
4235
  case 'like': {
3875
4236
  const v = resolveOperand(expr.operand, row, ctx);
@@ -6394,9 +6755,25 @@ class SQLExecutor {
6394
6755
 
6395
6756
  // 分组聚合
6396
6757
  if (statement.groupBy) {
6758
+ // MySQL 支持 GROUP BY alias,而 alias 指向 SELECT 列表的输出列名。
6759
+ // 解析时 groupBy[] 存的是原始字符串(如 "b"),必须先在 SELECT 别名里查找:
6760
+ // - 找到 → 取输出行里的 alias 值(因为 alias 已求出值了)
6761
+ // - 找不到 → 当列名取输入行里对应列的值(标准 SQL 行为)
6762
+ // 此前全用 resolveOperand({type:'column', name:g}) 按列名取,导致 GROUP BY b
6763
+ // 取 row['b'],而原始行里没有 b 列(b 是别名),全返回 undefined 并成一组。
6764
+ const aliases = {};
6765
+ if (statement.columns) {
6766
+ for (const c of statement.columns) {
6767
+ if (c.alias) aliases[c.alias.toLowerCase()] = true;
6768
+ }
6769
+ }
6397
6770
  const groups = new Map();
6398
6771
  for (const row of rows) {
6399
- const key = JSON.stringify(statement.groupBy.map(g => resolveOperand({ type: 'column', name: g }, row, this.ctx)));
6772
+ const key = JSON.stringify(statement.groupBy.map(g => {
6773
+ const key = String(g).toLowerCase();
6774
+ if (aliases[key] && row[key] !== undefined) return row[key];
6775
+ return resolveOperand({ type: 'column', name: g }, row, this.ctx);
6776
+ }));
6400
6777
  if (!groups.has(key)) groups.set(key, []);
6401
6778
  groups.get(key).push(row);
6402
6779
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.4.0-beta.14",
3
+ "version": "6.4.0-beta.16",
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",
@@ -46,7 +46,7 @@
46
46
  ],
47
47
  "scripts": {
48
48
  "postinstall": "node postinstall.js",
49
- "test": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js",
49
+ "test": "node test/smoke.js && node test/json-functions.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js",
50
50
  "test:parser": "node test/sql-parser.test.js",
51
51
  "test:btree": "node test/btree.test.js",
52
52
  "test:native": "node test/native.test.js",
@@ -0,0 +1,344 @@
1
+ // © Vexify 2026 All Rights Reserved.
2
+ /**
3
+ * Bug Hunt Tests — v6.4.0-beta.16
4
+ * Tests for bugs found during code review:
5
+ * 1. BTree _borrowFromLeft wrong parent key (leaf nodes)
6
+ * 2. Query _applyOrderBy missing NULL handling
7
+ * 3. BTree prefix incorrect early return for numeric keys
8
+ *
9
+ * node test/bug-hunt.test.js
10
+ */
11
+ const BTree = require('../lib/btree');
12
+ const Query = require('../lib/query');
13
+
14
+ let passed = 0, failed = 0;
15
+ function ok(name, cond, extra) {
16
+ if (cond) { passed++; }
17
+ else { failed++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra) : ''); }
18
+ }
19
+
20
+ // ============================================================
21
+ // Helper: Mock Table for Query tests
22
+ // ============================================================
23
+ function createMockTable(name, rows, schema = {}) {
24
+ const table = {
25
+ _name: name,
26
+ _rows: rows,
27
+ _schema: schema,
28
+ _btrees: {},
29
+ _indexes: {},
30
+ _applyFilterOptimized(rows, query) {
31
+ return rows.filter(row => {
32
+ for (const [field, condition] of Object.entries(query)) {
33
+ const value = row[field];
34
+ if (condition !== null && typeof condition === 'object' && !Array.isArray(condition)) {
35
+ for (const [op, target] of Object.entries(condition)) {
36
+ if (op === '$gt' && !(value > target)) return false;
37
+ if (op === '$gte' && !(value >= target)) return false;
38
+ if (op === '$lt' && !(value < target)) return false;
39
+ if (op === '$lte' && !(value <= target)) return false;
40
+ if (op === '$eq' && value !== target) return false;
41
+ if (op === '$ne' && value === target) return false;
42
+ if (op === '$in' && !target.includes(value)) return false;
43
+ }
44
+ } else {
45
+ if (value !== condition) return false;
46
+ }
47
+ }
48
+ return true;
49
+ });
50
+ },
51
+ _applyFilter(rows, query) { return this._applyFilterOptimized(rows, query); },
52
+ _matchRow(row, query) {
53
+ for (const [field, condition] of Object.entries(query)) {
54
+ const value = row[field];
55
+ if (condition !== null && typeof condition === 'object' && !Array.isArray(condition)) {
56
+ for (const [op, target] of Object.entries(condition)) {
57
+ if (op === '$gt' && !(value > target)) return false;
58
+ if (op === '$gte' && !(value >= target)) return false;
59
+ if (op === '$lt' && !(value < target)) return false;
60
+ if (op === '$lte' && !(value <= target)) return false;
61
+ if (op === '$eq' && value !== target) return false;
62
+ if (op === '$ne' && value === target) return false;
63
+ if (op === '$in' && !target.includes(value)) return false;
64
+ }
65
+ } else {
66
+ if (value !== condition) return false;
67
+ }
68
+ }
69
+ return true;
70
+ },
71
+ _getVisibleRows() { return this._rows; },
72
+ _db: {
73
+ _logSlowQuery() {},
74
+ _tables: {},
75
+ },
76
+ };
77
+ return table;
78
+ }
79
+
80
+ // ============================================================
81
+ // Bug 1: BTree _borrowFromLeft wrong parent key
82
+ // ============================================================
83
+ console.log('\n=== Bug 1: BTree _borrowFromLeft parent key ===');
84
+
85
+ {
86
+ const tree = new BTree(4, false);
87
+ for (let i = 0; i < 20; i++) tree.insert(i, i);
88
+
89
+ // Delete from the right side to trigger left borrows
90
+ for (let i = 19; i >= 10; i--) tree.remove(i, i);
91
+
92
+ // After deletions, search for remaining keys should work
93
+ let allFound = true;
94
+ for (let i = 0; i < 10; i++) {
95
+ const result = tree.search(i);
96
+ if (result.length === 0 || !result.includes(i)) { allFound = false; break; }
97
+ }
98
+ ok('Bug1: search after left borrows', allFound);
99
+
100
+ // Range query should return correct results
101
+ const rangeResult = tree.range(0, 9);
102
+ ok('Bug1: range [0,9] returns 10 items', rangeResult.length === 10, { len: rangeResult.length });
103
+ }
104
+
105
+ {
106
+ const tree = new BTree(4, false);
107
+ for (let i = 0; i < 50; i++) tree.insert(i, i);
108
+
109
+ // Delete every other key to force rebalancing
110
+ for (let i = 1; i < 50; i += 2) tree.remove(i, i);
111
+
112
+ // Verify all remaining keys are searchable
113
+ let allEvenFound = true;
114
+ for (let i = 0; i < 50; i += 2) {
115
+ const result = tree.search(i);
116
+ if (result.length === 0 || !result.includes(i)) { allEvenFound = false; break; }
117
+ }
118
+ ok('Bug1: even keys after heavy deletions', allEvenFound);
119
+
120
+ // Verify deleted keys are gone
121
+ let allOddGone = true;
122
+ for (let i = 1; i < 50; i += 2) {
123
+ const result = tree.search(i);
124
+ if (result.length !== 0) { allOddGone = false; break; }
125
+ }
126
+ ok('Bug1: odd keys deleted', allOddGone);
127
+
128
+ // Greater than query
129
+ const gtResult = tree.greaterThan(25);
130
+ ok('Bug1: greaterThan(25) finds keys', gtResult.length > 0);
131
+
132
+ // Less than query
133
+ const ltResult = tree.lessThan(25);
134
+ ok('Bug1: lessThan(25) finds keys', ltResult.length > 0);
135
+ }
136
+
137
+ {
138
+ const tree = new BTree(4, true);
139
+ for (let i = 0; i < 30; i++) tree.insert(i * 10, i);
140
+
141
+ // Delete from the right half to trigger left borrows
142
+ for (let i = 29; i >= 15; i--) tree.remove(i * 10, i);
143
+
144
+ // Verify remaining keys
145
+ let allFound = true;
146
+ for (let i = 0; i < 15; i++) {
147
+ const result = tree.search(i * 10);
148
+ if (result.length !== 1 || result[0] !== i) { allFound = false; break; }
149
+ }
150
+ ok('Bug1: unique index with left borrows', allFound);
151
+ }
152
+
153
+ // ============================================================
154
+ // Bug 2: Query _applyOrderBy missing NULL handling
155
+ // ============================================================
156
+ console.log('\n=== Bug 2: Query _applyOrderBy NULL handling ===');
157
+
158
+ {
159
+ const rows = [
160
+ { id: 1, name: 'Alice', age: 30 },
161
+ { id: 2, name: 'Bob', age: null },
162
+ { id: 3, name: 'Charlie', age: 25 },
163
+ { id: 4, name: 'Dave', age: null },
164
+ ];
165
+ const table = createMockTable('users', rows);
166
+ const query = new Query(table);
167
+ query.orderBy('age', 'asc');
168
+ const result = query.get();
169
+
170
+ ok('Bug2: ASC nulls first (1st)', result[0].age === null);
171
+ ok('Bug2: ASC nulls first (2nd)', result[1].age === null);
172
+ ok('Bug2: ASC 25 before 30', result[2].age === 25);
173
+ ok('Bug2: ASC 30 last', result[3].age === 30);
174
+ }
175
+
176
+ {
177
+ const rows = [
178
+ { id: 1, name: 'Alice', age: 30 },
179
+ { id: 2, name: 'Bob', age: null },
180
+ { id: 3, name: 'Charlie', age: 25 },
181
+ { id: 4, name: 'Dave', age: null },
182
+ ];
183
+ const table = createMockTable('users', rows);
184
+ const query = new Query(table);
185
+ query.orderBy('age', 'desc');
186
+ const result = query.get();
187
+
188
+ ok('Bug2: DESC nulls last (1st=30)', result[0].age === 30);
189
+ ok('Bug2: DESC nulls last (2nd=25)', result[1].age === 25);
190
+ ok('Bug2: DESC nulls last (3rd=null)', result[2].age === null);
191
+ ok('Bug2: DESC nulls last (4th=null)', result[3].age === null);
192
+ }
193
+
194
+ {
195
+ const rows = [
196
+ { id: 1, name: 'Alice', age: null },
197
+ { id: 2, name: 'Bob', age: null },
198
+ { id: 3, name: 'Charlie', age: null },
199
+ ];
200
+ const table = createMockTable('users', rows);
201
+ const query = new Query(table);
202
+ query.orderBy('age', 'asc');
203
+ const result = query.get();
204
+
205
+ ok('Bug2: all nulls ASC', result.length === 3 && result.every(r => r.age === null));
206
+ }
207
+
208
+ {
209
+ const rows = [
210
+ { id: 1, name: 'Alice', score: 85 },
211
+ { id: 2, name: 'Bob' }, // score is undefined
212
+ { id: 3, name: 'Charlie', score: null },
213
+ { id: 4, name: 'Dave', score: 90 },
214
+ ];
215
+ const table = createMockTable('users', rows);
216
+ const query = new Query(table);
217
+ query.orderBy('score', 'asc');
218
+ const result = query.get();
219
+
220
+ // Both null and undefined sort as null; stable sort preserves original order
221
+ // Bob (undefined) was at index 1, Charlie (null) at index 2
222
+ ok('Bug2: mixed null/undefined ASC (1st=null/undefined)', result[0].score == null);
223
+ ok('Bug2: mixed null/undefined ASC (2nd=null/undefined)', result[1].score == null);
224
+ ok('Bug2: mixed null/undefined ASC (3rd=85)', result[2].score === 85);
225
+ ok('Bug2: mixed null/undefined ASC (4th=90)', result[3].score === 90);
226
+ }
227
+
228
+ {
229
+ const rows = [
230
+ { id: 1, department: 'Engineering', salary: 5000 },
231
+ { id: 2, department: 'Engineering', salary: null },
232
+ { id: 3, department: 'Sales', salary: 4000 },
233
+ { id: 4, department: 'Sales', salary: null },
234
+ { id: 5, department: 'Engineering', salary: 6000 },
235
+ ];
236
+ const table = createMockTable('employees', rows);
237
+ const query = new Query(table);
238
+ query.orderBy('department', 'asc').orderBy('salary', 'asc');
239
+ const result = query.get();
240
+
241
+ ok('Bug2: multi-field orderBy', result.length === 5);
242
+ ok('Bug2: Eng null salary first', result[0].department === 'Engineering' && result[0].salary === null);
243
+ ok('Bug2: Eng 5000 second', result[1].department === 'Engineering' && result[1].salary === 5000);
244
+ ok('Bug2: Eng 6000 third', result[2].department === 'Engineering' && result[2].salary === 6000);
245
+ ok('Bug2: Sales null salary fourth', result[3].department === 'Sales' && result[3].salary === null);
246
+ ok('Bug2: Sales 4000 fifth', result[4].department === 'Sales' && result[4].salary === 4000);
247
+ }
248
+
249
+ // ============================================================
250
+ // Bug 3: BTree prefix incorrect early return for numeric keys
251
+ // ============================================================
252
+ console.log('\n=== Bug 3: BTree prefix with numeric keys ===');
253
+
254
+ {
255
+ const tree = new BTree(64, false);
256
+ const keys = [1, 2, 10, 20, 100, 200, 1000, 2000];
257
+ for (const key of keys) tree.insert(key, key);
258
+
259
+ const result = tree.prefix('1');
260
+ ok('Bug3: prefix "1" finds 1', result.includes(1));
261
+ ok('Bug3: prefix "1" finds 10', result.includes(10));
262
+ ok('Bug3: prefix "1" finds 100', result.includes(100));
263
+ ok('Bug3: prefix "1" finds 1000', result.includes(1000));
264
+ ok('Bug3: prefix "1" excludes 2', !result.includes(2));
265
+ ok('Bug3: prefix "1" excludes 20', !result.includes(20));
266
+ ok('Bug3: prefix "1" excludes 200', !result.includes(200));
267
+ ok('Bug3: prefix "1" excludes 2000', !result.includes(2000));
268
+ }
269
+
270
+ {
271
+ const tree = new BTree(64, false);
272
+ const keys = [2, 20, 200, 2000, 3, 30, 300];
273
+ for (const key of keys) tree.insert(key, key);
274
+
275
+ const result = tree.prefix('2');
276
+ ok('Bug3: prefix "2" finds 2', result.includes(2));
277
+ ok('Bug3: prefix "2" finds 20', result.includes(20));
278
+ ok('Bug3: prefix "2" finds 200', result.includes(200));
279
+ ok('Bug3: prefix "2" finds 2000', result.includes(2000));
280
+ ok('Bug3: prefix "2" excludes 3', !result.includes(3));
281
+ }
282
+
283
+ {
284
+ const tree = new BTree(64, false);
285
+ const keys = ['apple', 'application', 'apply', 'banana', 'cherry'];
286
+ for (const key of keys) tree.insert(key, key);
287
+
288
+ const result = tree.prefix('app');
289
+ ok('Bug3: string prefix finds apple', result.includes('apple'));
290
+ ok('Bug3: string prefix finds application', result.includes('application'));
291
+ ok('Bug3: string prefix finds apply', result.includes('apply'));
292
+ ok('Bug3: string prefix excludes banana', !result.includes('banana'));
293
+ }
294
+
295
+ {
296
+ const tree = new BTree(4, false);
297
+ for (let i = 1; i <= 100; i++) tree.insert(i, i);
298
+
299
+ // Prefix '1' should match: 1, 10-19, 100
300
+ const result = tree.prefix('1');
301
+ ok('Bug3: multi-leaf prefix "1" finds 1', result.includes(1));
302
+ ok('Bug3: multi-leaf prefix "1" finds 10', result.includes(10));
303
+ ok('Bug3: multi-leaf prefix "1" finds 11', result.includes(11));
304
+ ok('Bug3: multi-leaf prefix "1" finds 19', result.includes(19));
305
+ ok('Bug3: multi-leaf prefix "1" finds 100', result.includes(100));
306
+
307
+ // Prefix '9' should match: 9, 90-99
308
+ const result9 = tree.prefix('9');
309
+ ok('Bug3: multi-leaf prefix "9" finds 9', result9.includes(9));
310
+ ok('Bug3: multi-leaf prefix "9" finds 90', result9.includes(90));
311
+ ok('Bug3: multi-leaf prefix "9" finds 99', result9.includes(99));
312
+ }
313
+
314
+ {
315
+ const tree = new BTree(64, false);
316
+ tree.insert(1, 1);
317
+ tree.insert(2, 2);
318
+ tree.insert(3, 3);
319
+
320
+ const result = tree.prefix('9');
321
+ ok('Bug3: non-matching prefix returns empty', result.length === 0);
322
+ }
323
+
324
+ {
325
+ const tree = new BTree(64, false);
326
+ tree.insert('a.b', 1);
327
+ tree.insert('a*b', 2);
328
+ tree.insert('a+b', 3);
329
+ tree.insert('abc', 4);
330
+
331
+ const result = tree.prefix('a');
332
+ ok('Bug3: special chars prefix finds a.b', result.includes(1));
333
+ ok('Bug3: special chars prefix finds a*b', result.includes(2));
334
+ ok('Bug3: special chars prefix finds a+b', result.includes(3));
335
+ ok('Bug3: special chars prefix finds abc', result.includes(4));
336
+ }
337
+
338
+ // ============================================================
339
+ // Summary
340
+ // ============================================================
341
+ console.log('\n========================================');
342
+ console.log(`Results: ${passed} passed, ${failed} failed`);
343
+ if (failed > 0) process.exit(1);
344
+ else console.log('All tests passed!');
@@ -0,0 +1,172 @@
1
+ /*
2
+ * JSON function tests for JSQL-neo
3
+ * node test/json-functions.test.js
4
+ * Covers: JSON_EXTRACT path fix, JSON_KEYS, JSON_LENGTH, JSON_MERGE,
5
+ * JSON_MERGE_PATCH, JSON_MERGE_PRESERVE, JSON_OBJECTAGG, JSON_ARRAYAGG,
6
+ * JSON_SET, JSON_INSERT, JSON_REMOVE, JSON_UNQUOTE, JSON_QUOTE,
7
+ * JSON_TYPE, JSON_DEPTH, JSON_VALID, JSON_SEARCH
8
+ */
9
+ const { executeSQL } = require('../lib/sql.js');
10
+
11
+ let pass = 0, fail = 0;
12
+ const ok = (name, cond, extra) => {
13
+ if (cond) { pass++; console.log('[OK]', name); }
14
+ else { fail++; console.log('[FAIL]', name, extra !== undefined ? '→ ' + JSON.stringify(extra) : ''); }
15
+ };
16
+
17
+ const engine = {
18
+ hasTable: () => true,
19
+ truncate: async () => {},
20
+ flush: async () => {},
21
+ find: async () => [],
22
+ getTableSchema: async () => ({}),
23
+ };
24
+
25
+ // Helper: execute SQL and return first row as object
26
+ async function query(sql) {
27
+ const r = await executeSQL(engine, sql);
28
+ if (!r.rows || r.rows.length === 0) return null;
29
+ const row = {};
30
+ r.columns.forEach((col, i) => { row[col] = r.rows[0][i]; });
31
+ return row;
32
+ }
33
+
34
+ (async () => {
35
+ // ===== Bug 1: JSON_EXTRACT path parsing =====
36
+ {
37
+ const r1 = await query('SELECT JSON_EXTRACT(\'{"name":"test"}\', \'$.name\') AS val');
38
+ ok('JSON_EXTRACT $.name returns "test"', r1 && r1.val === 'test', r1 && r1.val);
39
+
40
+ const r2 = await query('SELECT JSON_EXTRACT(\'{"a":{"b":42}}\', \'$.a.b\') AS val');
41
+ ok('JSON_EXTRACT $.a.b returns 42', r2 && r2.val === 42, r2 && r2.val);
42
+
43
+ const r3 = await query('SELECT JSON_EXTRACT(\'{"items":[10,20,30]}\', \'$.items[1]\') AS val');
44
+ ok('JSON_EXTRACT $.items[1] returns 20', r3 && r3.val === 20, r3 && r3.val);
45
+
46
+ const r4 = await query('SELECT JSON_EXTRACT(\'{"name":"test"}\', \'$\') AS val');
47
+ ok('JSON_EXTRACT $ returns whole object', r4 && typeof r4.val === 'object' && r4.val.name === 'test', r4 && r4.val);
48
+
49
+ const r5 = await query('SELECT JSON_EXTRACT(\'{"name":"test"}\', \'$.nonexistent\') AS val');
50
+ ok('JSON_EXTRACT $.nonexistent returns null', r5 && (r5.val === null || r5.val === undefined), r5 && r5.val);
51
+ }
52
+
53
+ // ===== JSON_KEYS =====
54
+ {
55
+ const r = await query('SELECT JSON_KEYS(\'{"a":1,"b":2,"c":3}\') AS val');
56
+ ok('JSON_KEYS returns all keys', r && Array.isArray(r.val) && r.val.length === 3 && r.val.includes('a') && r.val.includes('b') && r.val.includes('c'), r && r.val);
57
+ }
58
+
59
+ // ===== JSON_LENGTH =====
60
+ {
61
+ const r1 = await query('SELECT JSON_LENGTH(\'{"a":1,"b":2,"c":3}\') AS val');
62
+ ok('JSON_LENGTH object returns 3', r1 && r1.val === 3, r1 && r1.val);
63
+
64
+ const r2 = await query('SELECT JSON_LENGTH(\'[1,2,3,4,5]\') AS val');
65
+ ok('JSON_LENGTH array returns 5', r2 && r2.val === 5, r2 && r2.val);
66
+
67
+ const r3 = await query('SELECT JSON_LENGTH(\'{"a":{"b":1,"c":2}}\', \'$.a\') AS val');
68
+ ok('JSON_LENGTH with path returns 2', r3 && r3.val === 2, r3 && r3.val);
69
+ }
70
+
71
+ // ===== JSON_MERGE_PRESERVE =====
72
+ {
73
+ const r = await query('SELECT JSON_MERGE_PRESERVE(\'{"a":1}\', \'{"b":2}\') AS val');
74
+ ok('JSON_MERGE_PRESERVE merges objects', r && typeof r.val === 'object' && r.val.a === 1 && r.val.b === 2, r && r.val);
75
+ }
76
+
77
+ // ===== JSON_MERGE_PATCH =====
78
+ {
79
+ const r = await query('SELECT JSON_MERGE_PATCH(\'{"a":1,"b":2}\', \'{"b":99,"c":3}\') AS val');
80
+ ok('JSON_MERGE_PATCH patches objects', r && typeof r.val === 'object' && r.val.a === 1 && r.val.b === 99 && r.val.c === 3, r && r.val);
81
+ }
82
+
83
+ // ===== JSON_OBJECTAGG =====
84
+ {
85
+ const r = await query('SELECT JSON_OBJECTAGG(\'key\', \'value\') AS val');
86
+ ok('JSON_OBJECTAGG creates object', r && typeof r.val === 'object' && r.val.key === 'value', r && r.val);
87
+ }
88
+
89
+ // ===== JSON_ARRAYAGG =====
90
+ {
91
+ const r = await query('SELECT JSON_ARRAYAGG(42) AS val');
92
+ ok('JSON_ARRAYAGG creates array', r && Array.isArray(r.val) && r.val.length === 1 && r.val[0] === 42, r && r.val);
93
+ }
94
+
95
+ // ===== JSON_SET =====
96
+ {
97
+ const r = await query('SELECT JSON_SET(\'{"a":1}\', \'$.b\', 2) AS val');
98
+ ok('JSON_SET adds new key', r && typeof r.val === 'object' && r.val.a === 1 && r.val.b === 2, r && r.val);
99
+
100
+ const r2 = await query('SELECT JSON_SET(\'{"a":1}\', \'$.a\', 99) AS val');
101
+ ok('JSON_SET updates existing key', r2 && typeof r2.val === 'object' && r2.val.a === 99, r2 && r2.val);
102
+ }
103
+
104
+ // ===== JSON_INSERT =====
105
+ {
106
+ const r = await query('SELECT JSON_INSERT(\'{"a":1}\', \'$.b\', 2) AS val');
107
+ ok('JSON_INSERT adds new key', r && typeof r.val === 'object' && r.val.a === 1 && r.val.b === 2, r && r.val);
108
+
109
+ const r2 = await query('SELECT JSON_INSERT(\'{"a":1}\', \'$.a\', 99) AS val');
110
+ ok('JSON_INSERT does not overwrite existing', r2 && typeof r2.val === 'object' && r2.val.a === 1, r2 && r2.val);
111
+ }
112
+
113
+ // ===== JSON_REMOVE =====
114
+ {
115
+ const r = await query('SELECT JSON_REMOVE(\'{"a":1,"b":2}\', \'$.b\') AS val');
116
+ ok('JSON_REMOVE deletes key', r && typeof r.val === 'object' && r.val.a === 1 && !('b' in r.val), r && r.val);
117
+ }
118
+
119
+ // ===== JSON_UNQUOTE =====
120
+ {
121
+ const r = await query('SELECT JSON_UNQUOTE(\'"hello"\') AS val');
122
+ ok('JSON_UNQUOTE removes quotes', r && r.val === 'hello', r && r.val);
123
+ }
124
+
125
+ // ===== JSON_QUOTE =====
126
+ {
127
+ const r = await query('SELECT JSON_QUOTE(\'hello\') AS val');
128
+ ok('JSON_QUOTE adds quotes', r && r.val === '"hello"', r && r.val);
129
+ }
130
+
131
+ // ===== JSON_TYPE =====
132
+ {
133
+ const r1 = await query('SELECT JSON_TYPE(\'{"a":1}\') AS val');
134
+ ok('JSON_TYPE object returns "object"', r1 && r1.val === 'object', r1 && r1.val);
135
+
136
+ const r2 = await query('SELECT JSON_TYPE(\'[1,2,3]\') AS val');
137
+ ok('JSON_TYPE array returns "array"', r2 && r2.val === 'array', r2 && r2.val);
138
+
139
+ const r3 = await query('SELECT JSON_TYPE(\'"hello"\') AS val');
140
+ ok('JSON_TYPE string returns "string"', r3 && r3.val === 'string', r3 && r3.val);
141
+
142
+ const r4 = await query('SELECT JSON_TYPE(\'42\') AS val');
143
+ ok('JSON_TYPE integer returns "integer"', r4 && r4.val === 'integer', r4 && r4.val);
144
+ }
145
+
146
+ // ===== JSON_DEPTH =====
147
+ {
148
+ const r1 = await query('SELECT JSON_DEPTH(\'{"a":1}\') AS val');
149
+ ok('JSON_DEPTH flat object returns 1', r1 && r1.val === 1, r1 && r1.val);
150
+
151
+ const r2 = await query('SELECT JSON_DEPTH(\'{"a":{"b":1}}\') AS val');
152
+ ok('JSON_DEPTH nested object returns 2', r2 && r2.val === 2, r2 && r2.val);
153
+ }
154
+
155
+ // ===== JSON_VALID =====
156
+ {
157
+ const r1 = await query('SELECT JSON_VALID(\'{"a":1}\') AS val');
158
+ ok('JSON_VALID valid JSON returns 1', r1 && r1.val === 1, r1 && r1.val);
159
+
160
+ const r2 = await query('SELECT JSON_VALID(\'not json\') AS val');
161
+ ok('JSON_VALID invalid JSON returns 0', r2 && r2.val === 0, r2 && r2.val);
162
+ }
163
+
164
+ // ===== JSON_SEARCH =====
165
+ {
166
+ const r = await query('SELECT JSON_SEARCH(\'{"name":"test","desc":"hello world"}\', \'all\', \'test\') AS val');
167
+ ok('JSON_SEARCH finds matching value', r && Array.isArray(r.val) && r.val.length > 0, r && r.val);
168
+ }
169
+
170
+ console.log(`\n${pass} passed, ${fail} failed`);
171
+ process.exit(fail > 0 ? 1 : 0);
172
+ })();