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

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.15** — 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.15 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
package/lib/sql.js CHANGED
@@ -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];
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.15",
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,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
+ })();