diff --git a/APIJSONORM/README.md b/APIJSONORM/README.md index 745733b4..3ad56245 100644 --- a/APIJSONORM/README.md +++ b/APIJSONORM/README.md @@ -1,6 +1,6 @@ -# APIJSONORM [![](https://jitpack.io/v/Tencent/APIJSON.svg)](https://jitpack.io/#Tencent/APIJSON) [Ask DeepWiki.com](https://deepwiki.com/Tencent/APIJSON) -腾讯 [APIJSON](https://github.com/Tencent/APIJSON) ORM 库,可通过 Maven, Gradle 等远程依赖。
-Tencent [APIJSON](https://github.com/Tencent/APIJSON) ORM library for remote dependencies with Maven, Gradle, etc. +# APIJSONORM [![](https://jitpack.io/v/APIJSON/APIJSON.svg)](https://jitpack.io/#APIJSON/APIJSON) [Ask DeepWiki.com](https://deepwiki.com/APIJSON/APIJSON) +[APIJSON](https://github.com/APIJSON/APIJSON) ORM 库,可通过 Maven, Gradle 等远程依赖。
+[APIJSON](https://github.com/APIJSON/APIJSON) ORM library for remote dependencies with Maven, Gradle, etc. ### Maven #### 1. 在 pom.xml 中添加 JitPack 仓库 @@ -19,9 +19,9 @@ Tencent [APIJSON](https://github.com/Tencent/APIJSON) ORM library for remote dep #### 2. Add the APIJSON dependency to pom.xml ```xml - com.github.Tencent + com.github.APIJSON APIJSON - 8.1.8 + latest ``` @@ -45,7 +45,7 @@ Tencent [APIJSON](https://github.com/Tencent/APIJSON) ORM library for remote dep #### 2. Add the APIJSON dependency in one of your modules(such as `app`) ```gradle dependencies { - implementation 'com.github.Tencent:APIJSON:8.1.8' + implementation 'com.github.APIJSON:APIJSON:latest' } ``` @@ -54,7 +54,7 @@ Tencent [APIJSON](https://github.com/Tencent/APIJSON) ORM library for remote dep ### FASTJSON 2 #### Code -https://github.com/Tencent/APIJSON/tree/fastjson2 +https://github.com/APIJSON/APIJSON/tree/fastjson2 #### Maven https://mvnrepository.com/artifact/com.github.linushp/zikai-apijson/1.0 diff --git a/APIJSONORM/pom.xml b/APIJSONORM/pom.xml index 7a35eb01..41ccd10b 100644 --- a/APIJSONORM/pom.xml +++ b/APIJSONORM/pom.xml @@ -3,9 +3,9 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - com.github.Tencent + com.github.APIJSON APIJSON - 8.1.8 + 10.0.0 jar APIJSONORM @@ -21,6 +21,12 @@ + + junit + junit + 4.13.2 + test + diff --git a/APIJSONORM/src/main/java/apijson/JSON.java b/APIJSONORM/src/main/java/apijson/JSON.java index 0a14e042..3f13dc8b 100755 --- a/APIJSONORM/src/main/java/apijson/JSON.java +++ b/APIJSONORM/src/main/java/apijson/JSON.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/JSONCreator.java b/APIJSONORM/src/main/java/apijson/JSONCreator.java index df0d9066..79395a86 100755 --- a/APIJSONORM/src/main/java/apijson/JSONCreator.java +++ b/APIJSONORM/src/main/java/apijson/JSONCreator.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/JSONList.java b/APIJSONORM/src/main/java/apijson/JSONList.java index 092bf9f3..8130f63a 100644 --- a/APIJSONORM/src/main/java/apijson/JSONList.java +++ b/APIJSONORM/src/main/java/apijson/JSONList.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/JSONMap.java b/APIJSONORM/src/main/java/apijson/JSONMap.java index c2913055..03c9642c 100755 --- a/APIJSONORM/src/main/java/apijson/JSONMap.java +++ b/APIJSONORM/src/main/java/apijson/JSONMap.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/JSONParser.java b/APIJSONORM/src/main/java/apijson/JSONParser.java index 7c38a39d..feb64528 100755 --- a/APIJSONORM/src/main/java/apijson/JSONParser.java +++ b/APIJSONORM/src/main/java/apijson/JSONParser.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/JSONRequest.java b/APIJSONORM/src/main/java/apijson/JSONRequest.java index c74dfe34..4d7f4f9b 100755 --- a/APIJSONORM/src/main/java/apijson/JSONRequest.java +++ b/APIJSONORM/src/main/java/apijson/JSONRequest.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/JSONResponse.java b/APIJSONORM/src/main/java/apijson/JSONResponse.java index 7527adff..4e76f59e 100755 --- a/APIJSONORM/src/main/java/apijson/JSONResponse.java +++ b/APIJSONORM/src/main/java/apijson/JSONResponse.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/Log.java b/APIJSONORM/src/main/java/apijson/Log.java index f00e495a..6b1c3b65 100755 --- a/APIJSONORM/src/main/java/apijson/Log.java +++ b/APIJSONORM/src/main/java/apijson/Log.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ @@ -12,7 +12,7 @@ */ public class Log { public static boolean DEBUG = false; - public static final String VERSION = "8.1.8"; + public static final String VERSION = "10.0.0"; public static final String LEVEL_VERBOSE = "VERBOSE"; public static final String LEVEL_INFO = "INFO"; diff --git a/APIJSONORM/src/main/java/apijson/MethodAccess.java b/APIJSONORM/src/main/java/apijson/MethodAccess.java index 1804f7a7..ac9f9d0e 100755 --- a/APIJSONORM/src/main/java/apijson/MethodAccess.java +++ b/APIJSONORM/src/main/java/apijson/MethodAccess.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/NotNull.java b/APIJSONORM/src/main/java/apijson/NotNull.java index 1265ccac..61145216 100755 --- a/APIJSONORM/src/main/java/apijson/NotNull.java +++ b/APIJSONORM/src/main/java/apijson/NotNull.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/RequestMethod.java b/APIJSONORM/src/main/java/apijson/RequestMethod.java index 27e4cab6..cc2a3814 100755 --- a/APIJSONORM/src/main/java/apijson/RequestMethod.java +++ b/APIJSONORM/src/main/java/apijson/RequestMethod.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/SQL.java b/APIJSONORM/src/main/java/apijson/SQL.java index 868f0d2a..17288135 100755 --- a/APIJSONORM/src/main/java/apijson/SQL.java +++ b/APIJSONORM/src/main/java/apijson/SQL.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/StringUtil.java b/APIJSONORM/src/main/java/apijson/StringUtil.java index 17916100..180cdfbe 100755 --- a/APIJSONORM/src/main/java/apijson/StringUtil.java +++ b/APIJSONORM/src/main/java/apijson/StringUtil.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ @@ -114,7 +114,7 @@ public static String get(Object[] arr, boolean ignoreEmptyItem) { public static String get(Object[] arr, String split) { return get(arr, split, false); } - //CS304 Issue link: https://github.com/Tencent/APIJSON/issues/182 + //CS304 Issue link: https://github.com/APIJSON/APIJSON/issues/182 /**获取string,为null则返回"" * @param arr -the str arr given * @param split -the token used to split @@ -192,7 +192,7 @@ public static String getString(Object[] array, boolean ignoreEmptyItem) { public static String getString(Object[] array, String split) { return get(array, split, false); } - //CS304 Issue link: https://github.com/Tencent/APIJSON/issues/182 + //CS304 Issue link: https://github.com/APIJSON/APIJSON/issues/182 /**FIXME 用 get 替代 * @param array -the str array given * @param split -the token used to split @@ -755,7 +755,7 @@ public static String getNumber(String s) { return getNumber(s, false); } - //CS304 Issue link: https://github.com/Tencent/APIJSON/issues/182 + //CS304 Issue link: https://github.com/APIJSON/APIJSON/issues/182 /**去掉string内所有非数字类型字符 * @param s -string passed in * @param onlyStart 中间有非数字时只获取前面的数字 @@ -858,7 +858,7 @@ public static String getCorrectEmail(String email) { public static String getPrice(String price) { return getPrice(price, PRICE_FORMAT_DEFAULT); } - //CS304 Issue link: https://github.com/Tencent/APIJSON/issues/182 + //CS304 Issue link: https://github.com/APIJSON/APIJSON/issues/182 /**获取价格,保留两位小数 * @param price -price passed in * @param formatType 添加单位(元) diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractFunctionParser.java b/APIJSONORM/src/main/java/apijson/orm/AbstractFunctionParser.java index 7ed7fc4d..6c4e248e 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractFunctionParser.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractFunctionParser.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractObjectParser.java b/APIJSONORM/src/main/java/apijson/orm/AbstractObjectParser.java index 5fd00c17..79ed5738 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractObjectParser.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractObjectParser.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ @@ -486,6 +486,11 @@ else if (value instanceof String) { // //key{}@ getRealKey, 引用赋值路径 if (target == null) { // String#equals(null)会出错 Log.d(TAG, "onParse target == null >> return true;"); + if (method == POST || method == PUT) { + throw new UnsupportedOperationException("引用赋值 " + path + "/" + key + ": " + targetPath + + " 解析失败,POST 和 PUT 接口必须保证每个对象执行都成功!"); + } + if (Log.DEBUG) { parser.putWarnIfNeed(AbstractParser.KEY_REF, path + "/" + key + ": " + targetPath + " 引用赋值获取路径对应的值为 null!请检查路径是否错误!"); } diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java b/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java index defccc93..159536fd 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ @@ -646,7 +646,7 @@ public M parseResponse(M request) { onClose(); - // CS304 Issue link: https://github.com/Tencent/APIJSON/issues/232 + // CS304 Issue link: https://github.com/APIJSON/APIJSON/issues/232 if (IS_PRINT_REQUEST_STRING_LOG || Log.DEBUG || error != null) { Log.sl("\n\n\n", '<', ""); Log.fd(TAG, requestMethod + "/parseResponse request = \n" + requestString + "\n\n"); @@ -829,8 +829,8 @@ public M extendResult(M object, int code, String msg, String warn, boolean isRoo + " \n数据库: DEFAULT_DATABASE = " + AbstractSQLConfig.DEFAULT_DATABASE + " \nJDK: " + Log.JAVA_VERSION + " " + Log.OS_ARCH + " \nAPIJSON: " + Log.VERSION - + " \n \n【常见问题】:https://github.com/Tencent/APIJSON/issues/36" - + " \n【通用文档】:https://github.com/Tencent/APIJSON/blob/master/Document.md" + + " \n \n【常见问题】:https://github.com/APIJSON/APIJSON/issues/36" + + " \n【通用文档】:https://github.com/APIJSON/APIJSON/blob/master/Document.md" + " \n【视频教程】:https://search.bilibili.com/all?keyword=APIJSON"); msg = index >= 0 ? msg.substring(0, index) : msg; @@ -985,11 +985,11 @@ public M extendErrorResult(M object, Throwable e, RequestMethod requestMethod, S msg += Log.KEY_SYSTEM_INFO_DIVIDER + " 浏览器打开以下链接查看解答" + (isSQLException ? "" : apiatuoAndGitHubLink) - // GitHub Issue 搜索貌似是精准包含,不易找到答案 + " \n\nGitHub: \n https://github.com/Tencent/APIJSON/issues?q=is%3Aissue+" + encodedMsg + // GitHub Issue 搜索貌似是精准包含,不易找到答案 + " \n\nGitHub: \n https://github.com/APIJSON/APIJSON/issues?q=is%3Aissue+" + encodedMsg + " \n\n【Google】:\n https://www.google.com/search?q=" + encodedMsg + " \n\n【百度】:\n https://www.baidu.com/s?ie=UTF-8&wd=" + encodedMsg + (isSQLException ? apiatuoAndGitHubLink : "") - + " \n\n都没找到答案?打开这个链接 \n https://github.com/Tencent/APIJSON/issues/new?assignees=&labels=&template=--bug.md " + + " \n\n都没找到答案?打开这个链接 \n https://github.com/APIJSON/APIJSON/issues/new?assignees=&labels=&template=--bug.md " + " \n然后提交问题,推荐用以下模板修改,注意要换行保持清晰可读。" + " \n【标题】:" + msg + " \n【内容】:" + env + "\n\n**问题描述**\n" + msg @@ -2012,7 +2012,7 @@ public void putQueryResult(String path, Object result) { queryResultMap.put(path, result); // } } - //CS304 Issue link: https://github.com/Tencent/APIJSON/issues/48 + //CS304 Issue link: https://github.com/APIJSON/APIJSON/issues/48 /**根据路径获取值 * @param valuePath -the path need to get value * @return parent == null ? valuePath : parent.get(keys[keys.length - 1]) @@ -2303,6 +2303,86 @@ protected M getRequestStructure(RequestMethod method, String tag, int version) t KEY_METHOD_ENUM_MAP.put(KEY_DELETE, RequestMethod.DELETE); } + private void parseMethodDirective(String key, RequestMethod keyMethod, @NotNull M request) throws Exception { + boolean isPost = KEY_POST.equals(key); + Object val = request.get(key); + Map obj = val instanceof Map ? JSON.get(request, key) : null; + if (obj == null) { + if (val instanceof String) { + String[] tbls = StringUtil.split((String) val); + if (tbls != null && tbls.length > 0) { + obj = new LinkedHashMap(); + for (String tbl : tbls) { + if (obj.containsKey(tbl)) { + throw new ConflictException(key + ": value 中 " + tbl + " 已经存在,不能重复!"); + } + + obj.put(tbl, isPost && isTableArray(tbl) + ? tbl.substring(0, tbl.length() - 2) + ":[]" : ""); + } + } + } + else { + throw new IllegalArgumentException(key + ": value 中 value 类型错误,只能是 String 或 Map {} !"); + } + } + + Set> set = obj == null ? new HashSet<>() : obj.entrySet(); + for (Entry objEntry : set) { + String objKey = objEntry == null ? null : objEntry.getKey(); + if (objKey == null) { + continue; + } + + Map objAttrMap = new HashMap<>(); + objAttrMap.put(KEY_METHOD, keyMethod); + keyObjectAttributesMap.put(objKey, objAttrMap); + + Object objVal = objEntry.getValue(); + Map objAttrJson = objVal instanceof Map ? JSON.getMap(obj, objKey) : null; + if (objAttrJson == null) { + if (objVal instanceof String) { + objAttrMap.put(KEY_TAG, "".equals(objVal) ? objKey : objVal); + } + else { + throw new IllegalArgumentException(key + ": { " + objKey + ": value 中 value 类型错误,只能是 String 或 Map {} !"); + } + } + else { + boolean hasTag = false; + for (Entry entry : objAttrJson.entrySet()) { + String objAttrKey = entry == null ? null : entry.getKey(); + if (objAttrKey == null) { + continue; + } + + switch (objAttrKey) { + case KEY_DATABASE: + case KEY_DATASOURCE: + case KEY_NAMESPACE: + case KEY_CATALOG: + case KEY_SCHEMA: + case KEY_VERSION: + case KEY_ROLE: + objAttrMap.put(objAttrKey, entry.getValue()); + break; + case KEY_TAG: + hasTag = true; + objAttrMap.put(objAttrKey, entry.getValue()); + break; + default: + break; + } + } + + if (hasTag == false) { + objAttrMap.put(KEY_TAG, isPost && isTableArray(objKey) + ? objKey.substring(0, objKey.length() - 2) + ":[]" : objKey); + } + } + } + } + protected M batchVerify(RequestMethod method, String tag, int version, String name, @NotNull M request, int maxUpdateCount, SQLCreator creator) throws Exception { M correctRequest = JSON.createJSONObject(); List removeTmpKeys = new ArrayList<>(); // 请求json里面的临时变量,不需要带入后面的业务中,比如 @post、@get等 @@ -2312,103 +2392,34 @@ protected M batchVerify(RequestMethod method, String tag, int version, String na throw new IllegalArgumentException("JSON 对象格式不正确 !正确示例例如 \"User\": {}"); } + // 先收集所有显式方法,避免同一请求中方法指令的字段顺序影响对象解析结果。 for (String key : reqSet) { - // key 重复直接抛错(xxx:alias, xxx:alias[]) - if (correctRequest.containsKey(key) || correctRequest.containsKey(key + KEY_ARRAY)) { - throw new IllegalArgumentException("对象名重复,请添加别名区分 ! 重复对象名为: " + key); + RequestMethod keyMethod = KEY_POST.equals(key) ? RequestMethod.POST : KEY_METHOD_ENUM_MAP.get(key); + if (keyMethod == null) { + continue; } - boolean isPost = KEY_POST.equals(key); - // @post、@get 等 RequestMethod + removeTmpKeys.add(key); try { - RequestMethod keyMethod = isPost ? RequestMethod.POST : KEY_METHOD_ENUM_MAP.get(key); - if (keyMethod != null) { - // 如果不匹配,异常不处理即可 - removeTmpKeys.add(key); - - Object val = request.get(key); - Map obj = val instanceof Map ? JSON.get(request, key) : null; - if (obj == null) { - if (val instanceof String) { - String[] tbls = StringUtil.split((String) val); - if (tbls != null && tbls.length > 0) { - obj = new LinkedHashMap(); - for (int i = 0; i < tbls.length; i++) { - String tbl = tbls[i]; - if (obj.containsKey(tbl)) { - throw new ConflictException(key + ": value 中 " + tbl + " 已经存在,不能重复!"); - } - - obj.put(tbl, isPost && isTableArray(tbl) - ? tbl.substring(0, tbl.length() - 2) + ":[]" : ""); - } - } - } - else { - throw new IllegalArgumentException(key + ": value 中 value 类型错误,只能是 String 或 Map {} !"); - } - } - - Set> set = obj == null ? new HashSet<>() : obj.entrySet(); - - for (Entry objEntry : set) { - String objKey = objEntry == null ? null : objEntry.getKey(); - if (objKey == null) { - continue; - } - - Map objAttrMap = new HashMap<>(); - objAttrMap.put(KEY_METHOD, keyMethod); - keyObjectAttributesMap.put(objKey, objAttrMap); - - Object objVal = objEntry.getValue(); - Map objAttrJson = objVal instanceof Map ? JSON.getMap(obj, objKey) : null; - if (objAttrJson == null) { - if (objVal instanceof String) { - objAttrMap.put(KEY_TAG, "".equals(objVal) ? objKey : objVal); - } - else { - throw new IllegalArgumentException(key + ": { " + objKey + ": value 中 value 类型错误,只能是 String 或 Map {} !"); - } - } - else { - Set> objSet = objAttrJson.entrySet(); - - boolean hasTag = false; - for (Entry entry : objSet) { - String objAttrKey = entry == null ? null : entry.getKey(); - if (objAttrKey == null) { - continue; - } + parseMethodDirective(key, keyMethod, request); + } + catch (Exception e) { + Log.e(TAG, "parse method directive failed", e); + throw e; + } + } - switch (objAttrKey) { - case KEY_DATABASE: - case KEY_DATASOURCE: - case KEY_NAMESPACE: - case KEY_CATALOG: - case KEY_SCHEMA: - case KEY_VERSION: - case KEY_ROLE: - objAttrMap.put(objAttrKey, entry.getValue()); - break; - case KEY_TAG: - hasTag = true; - objAttrMap.put(objAttrKey, entry.getValue()); - break; - default: - break; - } - } + for (String key : reqSet) { + // key 重复直接抛错(xxx:alias, xxx:alias[]) + if (correctRequest.containsKey(key) || correctRequest.containsKey(key + KEY_ARRAY)) { + throw new IllegalArgumentException("对象名重复,请添加别名区分 ! 重复对象名为: " + key); + } - if (hasTag == false) { - objAttrMap.put(KEY_TAG, isPost && isTableArray(objKey) - ? objKey.substring(0, objKey.length() - 2) + ":[]" : objKey); - } - } - } - continue; - } + if (KEY_POST.equals(key) || KEY_METHOD_ENUM_MAP.containsKey(key)) { + continue; + } + try { // 1、非crud,对于没有显式声明操作方法的,直接用 URL(/get, /post 等) 对应的默认操作方法 // 2、crud, 没有声明就用 GET // 3、兼容 sql@ Map,设置 GET方法 diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java index 05e9f04e..ca47bdf2 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ @@ -86,6 +86,7 @@ public abstract class AbstractSQLConfig, L exte * 表名映射,隐藏真实表名,对安全要求很高的表可以这么做 */ public static Map TABLE_KEY_MAP; + public static Map UPSERT_TABLE_MAP; /** * 字段名映射,隐藏真实字段名,对安全要求很高的表可以这么做,另外可以配置 name_tag:(name,tag) 来实现多字段 IN,length_tag:length(tag) 来实现 SQL 函数复杂条件 */ @@ -135,6 +136,8 @@ public abstract class AbstractSQLConfig, L exte TABLE_KEY_MAP.put(AllTableComment.class.getSimpleName(), AllTableComment.TABLE_NAME); TABLE_KEY_MAP.put(AllColumnComment.class.getSimpleName(), AllColumnComment.TABLE_NAME); + UPSERT_TABLE_MAP = new HashMap<>(); + ALLOW_PARTIAL_UPDATE_FAIL_TABLE_MAP = new HashMap<>(); CONFIG_TABLE_LIST = new ArrayList<>(); // Table, Column 等是系统表 AbstractVerifier.SYSTEM_ACCESS_MAP.keySet()); @@ -154,6 +157,9 @@ public abstract class AbstractSQLConfig, L exte DATABASE_LIST.add(DATABASE_COCKROACHDB); DATABASE_LIST.add(DATABASE_DAMENG); DATABASE_LIST.add(DATABASE_KINGBASE); + DATABASE_LIST.add(DATABASE_KINGBASE_MYSQL); + DATABASE_LIST.add(DATABASE_KINGBASE_ORACLE); + DATABASE_LIST.add(DATABASE_KINGBASE_SQLSERVER); DATABASE_LIST.add(DATABASE_ELASTICSEARCH); DATABASE_LIST.add(DATABASE_MANTICORE); DATABASE_LIST.add(DATABASE_CLICKHOUSE); @@ -178,6 +184,7 @@ public abstract class AbstractSQLConfig, L exte DATABASE_LIST.add(DATABASE_SURREALDB); DATABASE_LIST.add(DATABASE_OPENGAUSS); DATABASE_LIST.add(DATABASE_DORIS); + DATABASE_LIST.add(DATABASE_STARROCKS); RAW_MAP = new LinkedHashMap<>(); // 保证顺序,避免配置冲突等意外情况 @@ -893,6 +900,7 @@ public String getUserIdKey() { private boolean main = true; private Object id; // Table 的 id + private boolean idGeneratedByAPIJSON; private Object idIn; // User Table 的 id IN private Object userId; // Table 的 userId private Object userIdIn; // Table 的 userId IN @@ -1010,6 +1018,15 @@ public AbstractSQLConfig setId(Object id) { this.id = id; return this; } + @Override + public boolean isIdGeneratedByAPIJSON() { + return idGeneratedByAPIJSON; + } + @Override + public AbstractSQLConfig setIdGeneratedByAPIJSON(boolean generated) { + this.idGeneratedByAPIJSON = generated; + return this; + } @Override public Object getIdIn() { @@ -1083,11 +1100,11 @@ public String gainSQLDatabase() { @Override public boolean isTSQL() { // 兼容 TSQL 语法 - return isOracle() || isSQLServer() || isDb2(); + return isOracle() || isSQLServer() || isDb2() || isKingBaseOracle() || isKingBaseSQLServer(); } @Override public boolean isMSQL() { // 兼容 MySQL 语法,但不一定可以使用它的 JDBC/ODBC - return isMySQL() || isTiDB() || isMariaDB() || isSQLite() || isTDengine(); + return isMySQL() || isTiDB() || isMariaDB() || isSQLite() || isTDengine() || isKingBaseMySQL(); } @Override public boolean isPSQL() { // 兼容 PostgreSQL 语法,但不一定可以使用它的 JDBC/ODBC @@ -1171,7 +1188,31 @@ public boolean isKingBase() { return isKingBase(gainSQLDatabase()); } public static boolean isKingBase(String db) { - return DATABASE_KINGBASE.equals(db); + return KingbaseSQLDialect.from(db).isKingbase(); + } + + @Override + public boolean isKingBaseMySQL() { + return isKingBaseMySQL(gainSQLDatabase()); + } + public static boolean isKingBaseMySQL(String db) { + return KingbaseSQLDialect.from(db).isMySQL(); + } + + @Override + public boolean isKingBaseOracle() { + return isKingBaseOracle(gainSQLDatabase()); + } + public static boolean isKingBaseOracle(String db) { + return KingbaseSQLDialect.from(db).isOracle(); + } + + @Override + public boolean isKingBaseSQLServer() { + return isKingBaseSQLServer(gainSQLDatabase()); + } + public static boolean isKingBaseSQLServer(String db) { + return KingbaseSQLDialect.from(db).isSQLServer(); } @Override @@ -1375,12 +1416,24 @@ public static boolean isDoris(String db) { return DATABASE_DORIS.equals(db); } + @Override + public boolean isStarRocks() { + return isStarRocks(gainSQLDatabase()); + } + public static boolean isStarRocks(String db) { + return DATABASE_STARROCKS.equals(db); + } + @Override public String getQuote() { // MongoDB 同时支持 `tbl` 反引号 和 "col" 双引号 if(isElasticsearch() || isManticore() || isIoTDB() || isSurrealDB()) { return ""; } - return isMySQL() || isMariaDB() || isTiDB() || isClickHouse() || isTDengine() || isMilvus() || isDoris() ? "`" : "\""; + KingbaseSQLDialect kingbaseDialect = KingbaseSQLDialect.from(gainSQLDatabase()); + if (kingbaseDialect.isKingbase()) { + return kingbaseDialect.getIdentifierQuote(); + } + return isMySQL() || isMariaDB() || isTiDB() || isClickHouse() || isTDengine() || isMilvus() || isDoris() || isStarRocks() ? "`" : "\""; } public String quote(String s) { @@ -1516,7 +1569,7 @@ public AbstractSQLConfig setTable(String table) { //Table已经在Parse } public String gainAs() { - return isOracle() || isManticore() ? " " : " AS "; + return isOracle() || isKingBaseOracle() || isManticore() ? " " : " AS "; } @Override @@ -1716,7 +1769,8 @@ protected String gainHavingItem(String quote, String table, String alias + "预编译模式下 @having:\"column?value;function(arg0,arg1,...)?value...\"" + " 中 column?value 必须符合正则表达式 " + PATTERN_FUNCTION + " 且不包含连续减号 -- !不允许空格!"); } - return expression; + + return parseSQLExpression(KEY_HAVING, expression, containRaw, false, null); } int end = expression.lastIndexOf(")"); @@ -2065,7 +2119,7 @@ public String gainOrderString(boolean hasPrefix) { // return (hasPrefix ? " ORDER BY " : "") + StringUtil.concat(order, joinOrder, ", "); // } - if (getCount() > 0 && (isSQLServer() || isDb2())) { + if (getCount() > 0 && (isSQLServer() || isKingBaseSQLServer() || isDb2())) { // Oracle, SQL Server, DB2 的 OFFSET 必须加 ORDER BY.去掉Oracle,Oracle里面没有offset关键字 // String[] ss = StringUtil.split(order); @@ -2575,7 +2629,7 @@ else if (SQL_FUNCTION_MAP.containsKey(fun) == false) { } // 获取前半部分函数的参数解析 fun(arg0,agr1) - String agrsString1[] = parseArgsSplitWithComma( + String[] args = parseArgsSplitWithComma( s1.substring(index1 + 1, s1.lastIndexOf(")")), false, containRaw, allowAlias ); @@ -2599,9 +2653,9 @@ else if (SQL_FUNCTION_MAP.containsKey(fun) == false) { } // 获取后半部分的参数解析 (agr0 agr1 ...) - String[] argsString2 = parseArgsSplitWithComma(argString2, false, containRaw, allowAlias); - expression = fun + "(" + StringUtil.get(agrsString1) + (containOver ? ") OVER (" : ") AGAINST (") - + StringUtil.get(argsString2) + ")" + suffix // 传参不传空格,拼接带空格 + String[] args2 = parseArgsSplitWithComma(argString2, false, containRaw, allowAlias); + expression = fun + "(" + StringUtil.get(args) + (containOver ? ") OVER (" : ") AGAINST (") + + StringUtil.get(args2) + ")" + suffix // 传参不传空格,拼接带空格 + (StringUtil.isEmpty(alias, true) ? "" : gainAs() + quote + alias + quote); } } @@ -2845,7 +2899,7 @@ else if (ck.contains("`") || ck.contains("'") || origin.startsWith("_") || origi public List> getValues() { return values; } - public String getValuesString() { + public String gainValuesString() { String s = ""; if (values != null && values.size() > 0) { Object[] items = new Object[values.size()]; @@ -3064,8 +3118,14 @@ else if (isSurrealDB()) { } } + KingbaseSQLDialect kingbaseDialect = KingbaseSQLDialect.from(gainSQLDatabase()); + String kingbaseLimit = kingbaseDialect.getSelectLimit(getOffset(page, count), count); + if (kingbaseLimit != null) { + return kingbaseLimit; + } + boolean isOracle = isOracle(); - return gainLimitString(page, count, isTSQL(), isOracle || isDameng() || isKingBase(), isPresto() || isTrino()); + return gainLimitString(page, count, isTSQL(), isOracle || isDameng() || isKingBaseOracle(), isPresto() || isTrino()); } /**获取限制数量及偏移量 * @param page @@ -3193,7 +3253,7 @@ public AbstractSQLConfig setWhere(Map where) { public Object getWhere(String key) { return getWhere(key, false); } - //CS304 Issue link: https://github.com/Tencent/APIJSON/issues/48 + //CS304 Issue link: https://github.com/APIJSON/APIJSON/issues/48 /** * @param key - the key passed in * @param exactMatch - whether it is exact match @@ -4005,7 +4065,7 @@ public String gainCompareString(String key, String column, Object value, String public String gainKey(@NotNull String key) { String lenFun = ""; if (key.endsWith("[")) { - lenFun = isSQLServer() ? "datalength" : "length"; + lenFun = isSQLServer() || isKingBaseSQLServer() ? "datalength" : "length"; key = key.substring(0, key.length() - 1); } else if (key.endsWith("{")) { @@ -4293,10 +4353,11 @@ public String gainRegExpString(String key, String column, Object[] values, int t * @return key REGEXP 'value' */ public String gainRegExpString(String key, String column, String value, boolean ignoreCase) { - if (isPSQL()) { + if (isPSQL() || isKingBaseSQLServer()) { return gainKey(column) + " ~" + (ignoreCase ? "* " : " ") + gainValue(key, column, value); } - if (isOracle() || isDameng() || isKingBase() || (isMySQL() && gainDBVersionNums()[0] >= 8)) { + if (isOracle() || isDameng() || DATABASE_KINGBASE.equals(gainSQLDatabase()) + || isKingBaseOracle() || isKingBaseMySQL() || (isMySQL() && gainDBVersionNums()[0] >= 8)) { return "regexp_like(" + gainKey(column) + ", " + gainValue(key, column, value) + (ignoreCase ? ", 'i'" : ", 'c'") + ")"; } if (isPresto() || isTrino()) { @@ -4615,7 +4676,10 @@ public String gainContainString(String key, String column, Object[] childs, int condition += (gainKey(column) + " @> " + gainValue(key, column, newJSONArray(c))); // operator does not exist: jsonb @> character varying "[" + c + "]"); } - else if (isOracle() || isDameng() || isKingBase()) { + else if (isKingBase() && isKingBaseOracle() == false) { + condition += (gainKey(column) + "::jsonb @> " + gainValue(key, column, newJSONArray(c)) + "::jsonb"); + } + else if (isOracle() || isDameng() || isKingBaseOracle()) { condition += ("json_textcontains(" + gainKey(column) + ", " + (StringUtil.isEmpty(path, true) ? "'$'" : gainValue(key, column, path)) + ", " + gainValue(key, column, c == null ? null : c.toString()) + ")"); } @@ -4662,7 +4726,7 @@ public List getWithAsExprSQLList() { } private void clearWithAsExprListIfNeed() { // mysql8版本以上,子查询支持with as表达式 - if(this.isMySQL() && this.gainDBVersionNums()[0] >= 8) { + if((this.isMySQL() || this.isKingBaseMySQL()) && this.gainDBVersionNums()[0] >= 8) { this.withAsExprSQLList = new ArrayList<>(); } } @@ -4716,7 +4780,7 @@ private String withAsExprSubqueryString(SQLConfig cfg, Subquery> L newJSONArray(Object obj) { public String gainSetString() throws Exception { return gainSetString(getMethod(), getContent(), ! isTest()); } - //CS304 Issue link: https://github.com/Tencent/APIJSON/issues/48 + //CS304 Issue link: https://github.com/APIJSON/APIJSON/issues/48 /**获取SET * @param method -the method used * @param content -the content map @@ -4944,13 +5008,13 @@ public static , L extends List> String // TODO procedure 改为 List procedureList; behind : true; function: callFunction(); String key; ... // for (...) { Call procedure1();\n SQL \n; Call procedure2(); ... } // 貌似不需要,因为 ObjectParser 里就已经处理的顺序等,只是这里要解决下 Schema 问题。 - + String table = config.getTable(); String procedure = config.getProcedure(); if (StringUtil.isNotEmpty(procedure, true)) { int ind = procedure.indexOf("."); boolean hasPrefix = ind >= 0 && ind < procedure.indexOf("("); String sch = hasPrefix ? AbstractFunctionParser.extractSchema( - procedure.substring(0, ind), config.getTable() + procedure.substring(0, ind), table ) : config.gainSQLSchema(); String q = config.getQuote(); @@ -4970,58 +5034,201 @@ public static , L extends List> String method = GET; } - String cSql = null; + String cSql; switch (method) { case POST: - return "INSERT INTO " + tablePath + config.gainColumnString() + " VALUES" + config.getValuesString(); + return config.gainUpsertSQL(); case PUT: - if(config.isClickHouse()){ - return "ALTER TABLE " + tablePath + " UPDATE" + config.gainSetString() + config.gainWhereString(true); - } - cSql = "UPDATE " + tablePath + config.gainSetString() + config.gainWhereString(true) - + (config.isMySQL() ? config.gainLimitString() : ""); - cSql = buildWithAsExprSql(config, cSql); - return cSql; + return config.gainUpdateSQL(); case DELETE: - if(config.isClickHouse()){ - return "ALTER TABLE " + tablePath + " DELETE" + config.gainWhereString(true); - } - cSql = "DELETE FROM " + tablePath + config.gainWhereString(true) - + (config.isMySQL() ? config.gainLimitString() : ""); // PostgreSQL 不允许 LIMIT - cSql = buildWithAsExprSql(config, cSql); - return cSql; + return config.gainDeleteSQL(); default: - String explain = config.isExplain() ? (config.isSQLServer() ? "SET STATISTICS PROFILE ON " - : (config.isOracle() || config.isDameng() || config.isKingBase() ? "EXPLAIN PLAN FOR " : "EXPLAIN ")) : ""; - if (config.isTest() && RequestMethod.isGetMethod(config.getMethod(), true)) { // FIXME 为啥是 code 而不是 count ? - String q = config.getQuote(); // 生成 SELECT ( (24 >=0 AND 24 <3) ) AS `code` LIMIT 1 OFFSET 0 - return explain + "SELECT " + config.gainWhereString(false) - + config.gainAs() + q + JSONResponse.KEY_COUNT + q + config.gainLimitString(); - } - - config.setPreparedValueList(new ArrayList()); - String column = config.gainColumnString(); - if (config.isOracle() || config.isDameng() || config.isKingBase()) { - //When config's database is oracle,Using subquery since Oracle12 below does not support OFFSET FETCH paging syntax. - //针对oracle分组后条数的统计 - if (StringUtil.isNotEmpty(config.getGroup(),true) && RequestMethod.isHeadMethod(config.getMethod(), true)){ - return explain + "SELECT count(*) FROM (SELECT " + (config.getCache() == JSONMap.CACHE_RAM - ? "SQL_NO_CACHE " : "") + column + " FROM " + gainConditionString(tablePath, config) + ") " + config.gainLimitString(); + return config.gainSelectSQL(); + } + } + + public static String gainInsertSQL(String tablePath, String columns, String values) { + return "INSERT INTO " + tablePath + columns + " VALUES" + values; + } + + public String gainUpsertSQL() throws Exception { + String table = getTable(); + Object obj = UPSERT_TABLE_MAP.get(table); + String tablePath = gainTablePath(); + if (obj == null) { + return gainInsertSQL(tablePath, gainColumnString(), gainValuesString()); + } + + String database = gainSQLDatabase(); + String q = getQuote(); + + String key = ""; + String setStr = ""; + if (obj instanceof String) { // MySQL/PostgreSQL 简单写 version = version + 1 + setStr = (String) obj; + } else if (obj instanceof Map) { + // PostgreSQL 写 { "": "excluded.version = Method.version" } + // 或 自定义 CONFLICT key 写 { "package,method": "excluded.version = Method.version + 1" } + // 或多数据库写 { "Oracle": { "method,tag": "excluded.version = Request.version + 1" }, "DB2": {...} } 等 + + Map setObj = (Map) obj; + Object dbSetObj = setObj.get(database); + if (dbSetObj instanceof String) { + setStr = (String) dbSetObj; + } else { + boolean isDBSet = dbSetObj instanceof Map; + if (isDBSet) { + setObj = (Map) dbSetObj; + } else if (dbSetObj != null) { + throw new IllegalArgumentException("UPSERT_TABLE_MAP 中 " + table + ": {" + database + ": value} 中 value 不合法,必须是 String 或 Map 类型!"); + } + + Set> set = setObj.entrySet(); + boolean first = true; + for (Entry entry : set) { + String k = entry == null ? null : entry.getKey(); + Object v = k == null ? null : entry.getValue(); + if (v == null || (isDBSet == false && DATABASE_LIST.contains(k))) { + continue; + } + + if (StringUtil.isEmpty(k)) { + k = q + getIdKey() + q; + } else if (!(k.contains(",") || (k.startsWith("`") && k.endsWith("`")) || (k.startsWith("\"") && k.endsWith("\"")))) { + k = q + k + q; } - String sql = "SELECT " + (config.getCache() == JSONMap.CACHE_RAM - ? "SQL_NO_CACHE " : "") + column + " FROM " + gainConditionString(tablePath, config); - return explain + config.gainOraclePageSQL(sql); + key += (first ? "" : ", ") + k; + setStr += (first ? "" : ", ") + v; // 自定义表达式不好自动处理,后端配置正确即可 gainValue(k, k, v); + first = false; } + } + } else { + throw new IllegalArgumentException("UPSERT_TABLE_MAP 中 " + table + ": value 中 value 不合法,必须是 String 或 Map 类型!"); + } + + if (StringUtil.isEmpty(setStr)) { + return gainInsertSQL(tablePath, gainColumnString(), gainValuesString()); + } + + String columns = gainColumnString(); - cSql = "SELECT " + (config.getCache() == JSONMap.CACHE_RAM ? "SQL_NO_CACHE " : "") - + column + " FROM " + gainConditionString(tablePath, config) + config.gainLimitString(); - cSql = buildWithAsExprSql(config, cSql); - if(config.isElasticsearch()) { // elasticSearch 不支持 explain - return cSql; + if (isPostgreSQL(database) || isSQLite(database) || isCockroachDB(database) || isTimescaleDB(database)) { + if (StringUtil.isEmpty(key)) { + key = q + getIdKey() + q; + } + return gainInsertSQL(tablePath, columns, gainValuesString()) + " ON CONFLICT(" + key + ") DO UPDATE SET " + setStr; + } + + boolean isDB2 = isDb2(database); + boolean isOracle = isDB2 == false && isOracle(database); + boolean isDameng = isDB2 == false && isOracle == false && isDameng(database); + if (isDB2 || isOracle || isDameng || isSQLServer(database)) { + String alias = gainSQLAlias(); + String idKey = getIdKey(); + + String vs; + String cvs = ""; + + vs = ""; + List cols = getColumn(); + List> valss = cols == null || cols.isEmpty() ? null : getValues(); + if (valss == null || valss.size() != 1) { + throw new IllegalArgumentException("POST " + table + " 不合法,UPSERT 模式下 VALUES 必须是 1 条记录!"); + } + + List vals = valss.get(0); + boolean first = true; + for (int i = 0; i < cols.size(); i++) { + String c = cols.get(i); + if (c == null) { // FIXME 带上 id 是否有问题? || idKey.equals(c)) { + continue; } - return explain + cSql; + + Object v = vals.get(i); + String s = gainValue(c, c, v) + (isDB2 ? "" : " " + q + c + q); + + vs += (first ? "" : ", ") + s; + cvs += (first ? "" : ", ") + "excluded." + q + c + q; + first = false; + } + + if (StringUtil.isEmpty(vs)) { + throw new IllegalArgumentException("POST " + table + " 不合法,UPSERT 模式下 VALUES 必须是 1 条记录!"); + } + + vs = isDB2 ? "VALUES(" + vs + ")" : "SELECT " + vs + (isOracle || isDameng ? " FROM dual" : ""); + + idKey = q + idKey + q; + return "MERGE INTO " + tablePath + " " + alias + " USING (\n " + vs + "\n) excluded" + + (isDB2 ? columns : "") + " ON " + alias + "." + idKey + " = excluded." + idKey + " \nWHEN MATCHED THEN \n UPDATE SET " + + setStr + " \nWHEN NOT MATCHED THEN \n INSERT" + columns + " VALUES(" + cvs + ")"; } + + return gainInsertSQL(tablePath, columns, gainValuesString()) + " ON DUPLICATE KEY UPDATE " + setStr; + } + + public String gainUpdateSQL() throws Exception { + String tablePath = gainTablePath(); + if (isClickHouse()){ + return "ALTER TABLE " + tablePath + " UPDATE" + gainSetString() + gainWhereString(true); + } + + String cSql = "UPDATE " + tablePath + gainSetString() + gainWhereString(true) + + (isMySQL() || KingbaseSQLDialect.from(gainSQLDatabase()).supportsDmlLimit() ? gainLimitString() : ""); + cSql = buildWithAsExprSql(this, cSql); + return cSql; + } + + public String gainDeleteSQL() throws Exception { + String tablePath = gainTablePath(); + if(isClickHouse()){ + return "ALTER TABLE " + tablePath + " DELETE" + gainWhereString(true); + } + + String cSql = "DELETE FROM " + tablePath + gainWhereString(true) + + (isMySQL() || KingbaseSQLDialect.from(gainSQLDatabase()).supportsDmlLimit() ? gainLimitString() : ""); // PostgreSQL 不允许 LIMIT + cSql = buildWithAsExprSql(this, cSql); + return cSql; + } + + public String gainSelectSQL() throws Exception { + KingbaseSQLDialect kingbaseDialect = KingbaseSQLDialect.from(gainSQLDatabase()); + String kingbaseExplain = kingbaseDialect.getExplainPrefix(); + String explain = isExplain() ? (kingbaseExplain != null ? kingbaseExplain + : (isSQLServer() ? "SET STATISTICS PROFILE ON " + : (isOracle() || isDameng() ? "EXPLAIN PLAN FOR " : "EXPLAIN "))) : ""; + if (isTest() && RequestMethod.isGetMethod(getMethod(), true)) { // FIXME 为啥是 code 而不是 count ? + String q = getQuote(); // 生成 SELECT ( (24 >=0 AND 24 <3) ) AS `code` LIMIT 1 OFFSET 0 + return explain + "SELECT " + gainWhereString(false) + + gainAs() + q + JSONResponse.KEY_COUNT + q + gainLimitString(); + } + + setPreparedValueList(new ArrayList<>()); + + String tablePath = gainTablePath(); + String column = gainColumnString(); + if (isOracle() || isDameng() || isKingBaseOracle()) { + //When config's database is oracle,Using subquery since Oracle12 below does not support OFFSET FETCH paging syntax. + //针对oracle分组后条数的统计 + if (StringUtil.isNotEmpty(getGroup(),true) && RequestMethod.isHeadMethod(getMethod(), true)){ + return explain + "SELECT count(*) FROM (SELECT " + (getCache() == JSONMap.CACHE_RAM + ? "SQL_NO_CACHE " : "") + column + " FROM " + gainConditionString(tablePath, this) + ") " + gainLimitString(); + } + + String sql = "SELECT " + (getCache() == JSONMap.CACHE_RAM + ? "SQL_NO_CACHE " : "") + column + " FROM " + gainConditionString(tablePath, this); + return explain + gainOraclePageSQL(sql); + } + + String cSql = "SELECT " + (getCache() == JSONMap.CACHE_RAM ? "SQL_NO_CACHE " : "") + + column + " FROM " + gainConditionString(tablePath, this) + gainLimitString(); + cSql = buildWithAsExprSql(this, cSql); + if(isElasticsearch()) { // elasticSearch 不支持 explain + return cSql; + } + + return explain + cSql; } private static , L extends List> String buildWithAsExprSql(@NotNull AbstractSQLConfig config, String cSql) throws Exception { @@ -5044,7 +5251,7 @@ private static , L extends List> String @Override public boolean isWithAsEnable() { - return ENABLE_WITH_AS && (isMySQL() == false || gainDBVersionNums()[0] >= 8); + return ENABLE_WITH_AS && ((isMySQL() || isKingBaseMySQL()) == false || gainDBVersionNums()[0] >= 8); } /**Oracle的分页获取 @@ -5081,20 +5288,21 @@ private static , L extends List> String String where = config.gainWhereString(true); //根据方法不同,聚合语句不同。GROUP BY 和 HAVING 可以加在 HEAD 上, HAVING 可以加在 PUT, DELETE 上,GET 全加,POST 全都不加 + RequestMethod method = config.getMethod(); String aggregation; - if (RequestMethod.isGetMethod(config.getMethod(), true)) { + if (RequestMethod.isGetMethod(method, true)) { aggregation = config.gainGroupString(true) + config.gainHavingString(true) + config.gainSampleString(true) + config.gainLatestString(true) + config.gainPartitionString(true) + config.gainFillString(true) + config.gainOrderString(true); } - else if (RequestMethod.isHeadMethod(config.getMethod(), true)) { - // TODO 加参数 isPagenation 判断是 GET 内分页 query:2 查总数,不用加这些条件 + else if (RequestMethod.isHeadMethod(method, true)) { + // TODO 加参数 isPagination 判断是 GET 内分页 query:2 查总数,不用加这些条件 aggregation = config.gainGroupString(true) + config.gainHavingString(true) + config.gainSampleString(true) + config.gainLatestString(true) + config.gainPartitionString(true) + config.gainFillString(true); } - else if (config.getMethod() == PUT || config.getMethod() == DELETE) { + else if (method == PUT || method == DELETE) { aggregation = config.gainHavingString(true) ; } else { @@ -5349,7 +5557,11 @@ else if (rt.endsWith("~")) { if (isPSQL()) { sql += (first ? ON : AND) + lk + (isNot ? NOT : "") + " ~" + (ignoreCase ? "* " : " ") + rk; } - else if (isOracle() || isDameng() || isKingBase()) { + else if (isKingBaseSQLServer()) { + sql += (first ? ON : AND) + lk + (isNot ? NOT : "") + " ~" + (ignoreCase ? "* " : " ") + rk; + } + else if (isOracle() || isDameng() || DATABASE_KINGBASE.equals(gainSQLDatabase()) + || isKingBaseOracle() || isKingBaseMySQL()) { sql += (first ? ON : AND) + "regexp_like(" + lk + ", " + rk + (ignoreCase ? ", 'i'" : ", 'c'") + ")"; } else if (isPresto() || isTrino()) { @@ -5405,7 +5617,11 @@ else if ("{}".equals(rt) || "<>".equals(rt)) { sql += (first ? ON : AND) + (isNot ? "( " : "") + gainCondition(isNot, arrKeyPath + " IS NOT NULL AND " + arrKeyPath + " @> " + itemKeyPath) + (isNot ? ") " : ""); } - else if (isOracle() || isDameng() || isKingBase()) { + else if (isKingBase() && isKingBaseOracle() == false) { + sql += (first ? ON : AND) + (isNot ? "( " : "") + gainCondition(isNot, arrKeyPath + + " IS NOT NULL AND " + arrKeyPath + "::jsonb @> " + itemKeyPath + "::jsonb") + (isNot ? ") " : ""); + } + else if (isOracle() || isDameng() || isKingBaseOracle()) { sql += (first ? ON : AND) + (isNot ? "( " : "") + gainCondition(isNot, arrKeyPath + " IS NOT NULL AND json_textcontains(" + arrKeyPath + ", '$', " + itemKeyPath + ")") + (isNot ? ") " : ""); @@ -5536,8 +5752,10 @@ public static , L extends List> SQLConf } Object id = request.get(idKey); + boolean idGeneratedByAPIJSON = false; if (id == null && method == POST) { id = callback.newId(method, database, datasource, namespace, catalog, schema, table); // null 表示数据库自增 id + idGeneratedByAPIJSON = id != null; } if (id != null) { // null 无效 @@ -5977,25 +6195,30 @@ else if (w.startsWith("!")) { if (enableFakeDelete && method == DELETE) { // 查询 Access 假删除 - Map accessFakeDeleteMap = AbstractVerifier.ACCESS_FAKE_DELETE_MAP.get(config.getTable()); - - Object deletedKey = accessFakeDeleteMap.get(KEY_DELETED_KEY); - if (StringUtil.isNotEmpty(deletedKey, true)) { - // 假删除需要更新的其他字段,比如:删除时间 deletedTime 之类的 - Map fakeDeleteMap = new HashMap<>(); - fakeDeleteMap.put(deletedKey.toString(), accessFakeDeleteMap.get(KEY_DELETED_VALUE)); - fakeDeleteMap = config.onFakeDelete(fakeDeleteMap); - - Map content = config.getContent(); - if (content == null || content.isEmpty()) { - content = fakeDeleteMap; - } else { - content.putAll(fakeDeleteMap); - } + Map> fakeDeleteConfigMap = AbstractVerifier.ACCESS_FAKE_DELETE_MAP; + Map accessFakeDeleteMap = fakeDeleteConfigMap == null + ? null : fakeDeleteConfigMap.get(config.getTable()); + + Object deletedKey = accessFakeDeleteMap == null ? null : accessFakeDeleteMap.get(KEY_DELETED_KEY); + if ((deletedKey instanceof String) == false || StringUtil.isEmpty(deletedKey, true)) { + throw new IllegalArgumentException(config.getTable() + " 对应的假删除配置错误!" + + KEY_DELETED_KEY + ":value 中 value 必须为非空 String!当前值为 " + deletedKey); + } - config.setMethod(PUT); - config.setContent(content); + // 假删除需要更新的其他字段,比如:删除时间 deletedTime 之类的 + Map fakeDeleteMap = new HashMap<>(); + fakeDeleteMap.put(deletedKey.toString(), accessFakeDeleteMap.get(KEY_DELETED_VALUE)); + fakeDeleteMap = config.onFakeDelete(fakeDeleteMap); + + Map content = config.getContent(); + if (content == null || content.isEmpty()) { + content = fakeDeleteMap; + } else { + content.putAll(fakeDeleteMap); } + + config.setMethod(PUT); + config.setContent(content); } List cs = new ArrayList<>(); @@ -6168,6 +6391,7 @@ else if (keyMap != null) { config.setRole(role); config.setId(id); + config.setIdGeneratedByAPIJSON(idGeneratedByAPIJSON); config.setIdIn(idIn); config.setUserId(userId); config.setUserIdIn(userIdIn); diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLExecutor.java b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLExecutor.java index 3107b905..0cb9f676 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLExecutor.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLExecutor.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ @@ -9,14 +9,17 @@ import apijson.orm.Join.On; import apijson.orm.exception.NotExistException; -import java.io.BufferedReader; -import java.math.BigDecimal; -import java.math.BigInteger; +import java.io.Reader; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.sql.*; -import java.time.DayOfWeek; -import java.time.LocalDateTime; -import java.time.Month; -import java.time.Year; +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.time.Month; +import java.time.Year; +import java.time.temporal.TemporalAccessor; import java.util.Date; import java.util.*; import java.util.Map.Entry; @@ -1012,11 +1015,18 @@ protected M onPutColumn(@NotNull SQLConfig config, @NotNull ResultSet r * @return * @throws SQLException */ - protected boolean isHideColumn(@NotNull SQLConfig config, @NotNull ResultSet rs, @NotNull ResultSetMetaData rsmd - , final int row, @NotNull M table, final int columnIndex, Map childMap - , Map keyMap) throws SQLException { - return rsmd.getColumnName(columnIndex).startsWith("_"); - } + protected boolean isHideColumn(@NotNull SQLConfig config, @NotNull ResultSet rs, @NotNull ResultSetMetaData rsmd + , final int row, @NotNull M table, final int columnIndex, Map childMap + , Map keyMap) throws SQLException { + String columnName = rsmd.getColumnName(columnIndex); + // Kingbase Oracle pagination appends ROWNUM as the final RN column. It is + // an implementation detail and must not leak into the API response. + if (config.isKingBaseOracle() && config.getCount() > 0 + && columnIndex == rsmd.getColumnCount() && "RN".equalsIgnoreCase(columnName)) { + return true; + } + return columnName.startsWith("_"); + } /**resultList.put(position, table); * @param config @@ -1078,74 +1088,394 @@ protected Object getValue(@NotNull SQLConfig config, @NotNull ResultSet // Log.i(TAG, "select while (rs.next()) { >> for (int i = 0; i < length; i++) {" // + "\n >>> value = " + value); - boolean castToJson = false; - - //数据库查出来的null和empty值都有意义,去掉会导致 Moment:{ @column:"content" } 部分无结果及中断数组查询! - if (value instanceof Boolean) { - //加快判断速度 - } - else if (value instanceof Number) { - value = getNumVal((Number) value); - } - else if (value instanceof Timestamp) { - value = ((Timestamp) value).toString(); - } - else if (value instanceof Date) { // java.sql.Date 和 java.sql.Time 都继承 java.util.Date - value = ((Date) value).toString(); - } - else if (value instanceof LocalDateTime) { - value = ((LocalDateTime) value).toString(); - } - else if (value instanceof Year) { - value = ((Year) value).getValue(); - } - else if (value instanceof Month) { - value = ((Month) value).getValue(); - } - else if (value instanceof DayOfWeek) { - value = ((DayOfWeek) value).getValue(); - } - else if (value instanceof String && isJSONType(config, rsmd, columnIndex, label)) { //json String - castToJson = true; - } - else if (value instanceof Blob) { //FIXME 存的是 abcde,取出来直接就是 [97, 98, 99, 100, 101] 这种 byte[] 类型,没有经过以下处理,但最终序列化后又变成了字符串 YWJjZGU= - castToJson = true; - value = new String(((Blob) value).getBytes(1, (int) ((Blob) value).length()), "UTF-8"); - } - else if (value instanceof Clob) { //SQL Server TEXT 类型 居然走这个 - castToJson = true; - - StringBuffer sb = new StringBuffer(); - BufferedReader br = new BufferedReader(((Clob) value).getCharacterStream()); - String s = br.readLine(); - while (s != null) { - sb.append(s); - s = br.readLine(); - } - value = sb.toString(); - - try { - br.close(); - } - catch (Exception e) { - Log.e(TAG, "close BufferedReader failed", e); - } - } - - if (castToJson == false) { - List json = config.getJson(); - castToJson = json != null && json.contains(label); - } - if (castToJson) { - try { - value = JSON.parse(value); - } catch (Exception e) { - Log.e(TAG, "getValue try { value = parseJSON((String) value); } catch (Exception e) { \n" + e.getMessage()); - } - } - - return value; - } + int jdbcType = Types.OTHER; + String typeName = null; + try { + jdbcType = rsmd.getColumnType(columnIndex); + typeName = rsmd.getColumnTypeName(columnIndex); + } + catch (SQLException e) { + // Some JDBC bridges do not expose complete metadata. Value-class mapping + // still provides a safe fallback in that case. + Log.e(TAG, "getValue failed to read JDBC metadata for column " + label + ": " + e.getMessage()); + } + return mapResultValue(config, value, jdbcType, typeName, label); + } + + @Override + public Object mapResultValue(@NotNull SQLConfig config, Object value, int jdbcType + , String typeName, String label) throws Exception { + if (config.isKingBaseMySQL()) { + return mapKingbaseMySQLResultValue(config, value, jdbcType, typeName, label); + } + if (config.isKingBaseOracle()) { + return mapKingbaseOracleResultValue(config, value, jdbcType, typeName, label); + } + if (config.isKingBaseSQLServer()) { + return mapKingbaseSQLServerResultValue(config, value, jdbcType, typeName, label); + } + if (value == null || value instanceof Boolean) { + return value; + } + if (value instanceof Number) { + return getNumVal((Number) value); + } + if (value instanceof Timestamp || value instanceof Date || value instanceof LocalDateTime) { + return value.toString(); + } + if (value instanceof Year) { + return ((Year) value).getValue(); + } + if (value instanceof Month) { + return ((Month) value).getValue(); + } + if (value instanceof DayOfWeek) { + return ((DayOfWeek) value).getValue(); + } + if (value instanceof TemporalAccessor || value instanceof UUID) { + return value.toString(); + } + + boolean castToJson = isJSONTypeName(typeName); + List jsonColumns = config.getJson(); + castToJson = castToJson || jsonColumns != null && jsonColumns.contains(label); + + if (value instanceof Blob) { + value = new String(((Blob) value).getBytes(1, (int) ((Blob) value).length()), "UTF-8"); + castToJson = true; + } + else if (value instanceof Clob) { + value = readClob((Clob) value); + castToJson = true; + } + else if (value instanceof SQLXML) { + value = ((SQLXML) value).getString(); + } + else if (value instanceof java.sql.Array) { + java.sql.Array sqlArray = (java.sql.Array) value; + try { + value = mapArrayValue(config, sqlArray.getArray(), jdbcType, typeName, label); + } + finally { + sqlArray.free(); + } + } + else if (value.getClass().isArray() && value instanceof byte[] == false) { + value = mapArrayValue(config, value, jdbcType, typeName, label); + } + else if (config.isKingBase() && isKingBaseJdbcObject(value)) { + // Kingbase represents json/jsonb and several extension types as vendor objects. + // JSON values are parsed; other extension values are exposed as strings. + value = value.toString(); + } + + if (castToJson && value instanceof String) { + try { + return JSON.parse(value); + } + catch (Exception e) { + Log.e(TAG, "mapResultValue failed to parse JSON column " + label + ": " + e.getMessage()); + } + } + return value; + } + + /** + * Default KingbaseES Oracle-mode mapping. Kingbase maps Oracle NUMBER to + * numeric, RAW/LONG RAW to bytea, and Oracle LOB/date/JSON types to the + * corresponding Kingbase JDBC values. Keep precision and binary data JSON-safe. + */ + protected Object mapKingbaseOracleResultValue(@NotNull SQLConfig config, Object value + , int jdbcType, String typeName, String label) throws Exception { + if (value == null) { + return null; + } + + String normalizedType = normalizeJdbcTypeName(typeName); + List jsonColumns = config.getJson(); + boolean json = isJSONTypeName(normalizedType) || jsonColumns != null && jsonColumns.contains(label); + + if (value instanceof java.sql.Array) { + java.sql.Array sqlArray = (java.sql.Array) value; + try { + return mapArrayValue(config, sqlArray.getArray(), jdbcType, normalizedType, label); + } + finally { + sqlArray.free(); + } + } + if (value.getClass().isArray() && !(value instanceof byte[])) { + return mapArrayValue(config, value, jdbcType, normalizedType, label); + } + + if (value instanceof Blob) { + Blob blob = (Blob) value; + byte[] bytes = blob.getBytes(1, (int) blob.length()); + value = json ? new String(bytes, StandardCharsets.UTF_8) + : Base64.getEncoder().encodeToString(bytes); + } + else if (value instanceof byte[]) { + value = json ? new String((byte[]) value, StandardCharsets.UTF_8) + : Base64.getEncoder().encodeToString((byte[]) value); + } + else if (value instanceof Clob) { + value = readClob((Clob) value); + } + else if (value instanceof SQLXML) { + value = ((SQLXML) value).getString(); + } + else if (isKingBaseJdbcObject(value)) { + value = getKingbaseJdbcValue(value); + } + + if (json && value instanceof String) { + try { + return JSON.parse(value); + } + catch (Exception e) { + Log.e(TAG, "mapKingbaseOracleResultValue failed to parse JSON column " + label + ": " + e.getMessage()); + return value; + } + } + if (jdbcType == Types.BOOLEAN || "bool".equals(normalizedType) || "boolean".equals(normalizedType)) { + return mapBooleanValue(value); + } + if (value instanceof Boolean) { + return value; + } + if (value instanceof Number) { + return getNumVal((Number) value); + } + if (value instanceof Timestamp || value instanceof Date || value instanceof TemporalAccessor || value instanceof UUID) { + return value.toString(); + } + return value; + } + + protected Object mapBooleanValue(Object value) { + if (value instanceof Number) { + return ((Number) value).intValue() != 0; + } + if (value instanceof String) { + String booleanValue = ((String) value).trim(); + if ("1".equals(booleanValue) || "t".equalsIgnoreCase(booleanValue) || "true".equalsIgnoreCase(booleanValue)) { + return true; + } + if ("0".equals(booleanValue) || "f".equalsIgnoreCase(booleanValue) || "false".equalsIgnoreCase(booleanValue)) { + return false; + } + } + return value; + } + + /** Default KingbaseES MySQL-mode DB type to JSON-safe Java type mapping. */ + protected Object mapKingbaseMySQLResultValue(@NotNull SQLConfig config, Object value + , int jdbcType, String typeName, String label) throws Exception { + if (value == null) { + return null; + } + + String normalizedType = normalizeJdbcTypeName(typeName); + List jsonColumns = config.getJson(); + boolean json = isJSONTypeName(normalizedType) || jsonColumns != null && jsonColumns.contains(label); + + if (value instanceof java.sql.Array) { + java.sql.Array sqlArray = (java.sql.Array) value; + try { + return mapArrayValue(config, sqlArray.getArray(), jdbcType, normalizedType, label); + } + finally { + sqlArray.free(); + } + } + if (value.getClass().isArray() && !(value instanceof byte[])) { + return mapArrayValue(config, value, jdbcType, normalizedType, label); + } + + if (value instanceof Blob) { + Blob blob = (Blob) value; + byte[] bytes = blob.getBytes(1, (int) blob.length()); + value = json ? new String(bytes, StandardCharsets.UTF_8) + : Base64.getEncoder().encodeToString(bytes); + } + else if (value instanceof byte[]) { + value = json ? new String((byte[]) value, StandardCharsets.UTF_8) + : Base64.getEncoder().encodeToString((byte[]) value); + } + else if (value instanceof Clob) { + value = readClob((Clob) value); + } + else if (value instanceof SQLXML) { + value = ((SQLXML) value).getString(); + } + else if (isKingBaseJdbcObject(value)) { + value = getKingbaseJdbcValue(value); + } + + if (json && value instanceof String) { + try { + return JSON.parse(value); + } + catch (Exception e) { + Log.e(TAG, "mapKingbaseMySQLResultValue failed to parse JSON column " + label + ": " + e.getMessage()); + return value; + } + } + if (jdbcType == Types.BOOLEAN || "bool".equals(normalizedType) || "boolean".equals(normalizedType)) { + return mapBooleanValue(value); + } + if (value instanceof Boolean) { + return value; + } + if (value instanceof Number) { + return getNumVal((Number) value); + } + if (value instanceof Year) { + return ((Year) value).getValue(); + } + if (value instanceof Timestamp || value instanceof Date || value instanceof TemporalAccessor || value instanceof UUID) { + return value.toString(); + } + return value; + } + + /** + * Default mapping for KingbaseES SQL Server mode. The mode exposes SQL + * Server-compatible numeric, binary, date/time, JSON/JSONB, XML, + * UNIQUEIDENTIFIER, ROWVERSION and SQL_VARIANT types through the Kingbase + * JDBC driver. Preserve exact decimals, make binary values JSON-safe and + * unwrap vendor objects without adding a compile-time driver dependency. + */ + protected Object mapKingbaseSQLServerResultValue(@NotNull SQLConfig config, Object value + , int jdbcType, String typeName, String label) throws Exception { + if (value == null) { + return null; + } + + String normalizedType = normalizeJdbcTypeName(typeName); + List jsonColumns = config.getJson(); + boolean json = isJSONTypeName(normalizedType) || jsonColumns != null && jsonColumns.contains(label); + + if (value instanceof java.sql.Array) { + java.sql.Array sqlArray = (java.sql.Array) value; + try { + return mapArrayValue(config, sqlArray.getArray(), jdbcType, normalizedType, label); + } + finally { + sqlArray.free(); + } + } + if (value.getClass().isArray() && !(value instanceof byte[])) { + return mapArrayValue(config, value, jdbcType, normalizedType, label); + } + + if (value instanceof Blob) { + Blob blob = (Blob) value; + byte[] bytes = blob.getBytes(1, (int) blob.length()); + value = json ? new String(bytes, StandardCharsets.UTF_8) + : Base64.getEncoder().encodeToString(bytes); + } + else if (value instanceof byte[]) { + value = json ? new String((byte[]) value, StandardCharsets.UTF_8) + : Base64.getEncoder().encodeToString((byte[]) value); + } + else if (value instanceof Clob) { + value = readClob((Clob) value); + } + else if (value instanceof SQLXML) { + value = ((SQLXML) value).getString(); + } + else if (isKingBaseJdbcObject(value)) { + Object unwrapped = getKingbaseJdbcValue(value); + if (unwrapped != value) { + return mapKingbaseSQLServerResultValue(config, unwrapped, jdbcType, normalizedType, label); + } + } + + if (json && value instanceof String) { + try { + return JSON.parse(value); + } + catch (Exception e) { + Log.e(TAG, "mapKingbaseSQLServerResultValue failed to parse JSON column " + label + ": " + e.getMessage()); + return value; + } + } + if (jdbcType == Types.BOOLEAN || jdbcType == Types.BIT + || "bool".equals(normalizedType) || "boolean".equals(normalizedType) || "bit".equals(normalizedType)) { + return mapBooleanValue(value); + } + if (value instanceof Boolean) { + return value; + } + if (value instanceof Number) { + return getNumVal((Number) value); + } + if (value instanceof Timestamp || value instanceof Date || value instanceof Time + || value instanceof TemporalAccessor || value instanceof UUID) { + return value.toString(); + } + return value; + } + + protected String normalizeJdbcTypeName(String typeName) { + if (typeName == null) { + return ""; + } + String normalized = typeName.trim().toLowerCase(Locale.ROOT).replace("`", "").replace("\"", ""); + int schemaSeparator = normalized.lastIndexOf('.'); + return schemaSeparator < 0 ? normalized : normalized.substring(schemaSeparator + 1); + } + + protected Object getKingbaseJdbcValue(Object value) { + try { + Method method = value.getClass().getMethod("getValue"); + return method.invoke(value); + } + catch (Exception ignored) { + return value.toString(); + } + } + + protected boolean isJSONTypeName(String typeName) { + String normalized = normalizeJdbcTypeName(typeName); + return "json".equals(normalized) || "jsonb".equals(normalized) + || "_json".equals(normalized) || "_jsonb".equals(normalized) + || "json[]".equals(normalized) || "jsonb[]".equals(normalized); + } + + protected boolean isKingBaseJdbcObject(Object value) { + Package pkg = value == null ? null : value.getClass().getPackage(); + String packageName = pkg == null ? value == null ? "" : value.getClass().getName() : pkg.getName(); + return packageName.startsWith("com.kingbase8"); + } + + protected Object mapArrayValue(@NotNull SQLConfig config, Object array, int jdbcType + , String typeName, String label) throws Exception { + if (array == null || array.getClass().isArray() == false) { + return array; + } + int length = java.lang.reflect.Array.getLength(array); + List result = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + result.add(mapResultValue(config, java.lang.reflect.Array.get(array, i), jdbcType, typeName, label)); + } + return result; + } + + protected String readClob(Clob clob) throws Exception { + StringBuilder sb = new StringBuilder(); + try (Reader reader = clob.getCharacterStream()) { + char[] buffer = new char[4096]; + int length; + while ((length = reader.read(buffer)) >= 0) { + if (length > 0) { + sb.append(buffer, 0, length); + } + } + } + return sb.toString(); + } public Object getNumVal(Number value) { if (value == null) { @@ -1156,9 +1486,13 @@ public Object getNumVal(Number value) { return ((BigInteger) value).toString(); } - if (value instanceof BigDecimal) { - return ((BigDecimal) value).toString(); - } + if (value instanceof BigDecimal) { + return ((BigDecimal) value).toString(); + } + if (value instanceof Double && !Double.isFinite((Double) value) + || value instanceof Float && !Float.isFinite((Float) value)) { + return value.toString(); + } double v = value.doubleValue(); // if (v > Integer.MAX_VALUE || v < Integer.MIN_VALUE) { // 避免前端/客户端拿到精度丢失甚至严重失真的值 @@ -1193,9 +1527,7 @@ public boolean isJSONType(@NotNull SQLConfig config, ResultSetMetaData //TODO CHAR和JSON类型的字段,getColumnType返回值都是1 ,如果不用CHAR,改用VARCHAR,则可以用上面这行来提高性能。 //return rsmd.getColumnType(position) == 1; - if (column.toLowerCase().contains("json")) { - return true; - } + return isJSONTypeName(column); } catch (SQLException e) { Log.e(TAG, "isJsonColumn failed", e); } @@ -1210,19 +1542,24 @@ public PreparedStatement getStatement(@NotNull SQLConfig config) throws return getStatement(config, null); } - @Override - public PreparedStatement getStatement(@NotNull SQLConfig config, String sql) throws Exception { - if (StringUtil.isEmpty(sql)) { - sql = config.gainSQL(config.isPrepared()); - } - - Connection conn = getConnection(config); - PreparedStatement statement; //创建Statement对象 - if (config.getMethod() == RequestMethod.POST && config.getId() == null) { //自增id - if (config.isOracle()) { - // 解决 oracle 使用自增主键 插入获取不到id问题 - String[] generatedColumns = {config.getIdKey()}; - statement = conn.prepareStatement(sql, generatedColumns); + @Override + public PreparedStatement getStatement(@NotNull SQLConfig config, String sql) throws Exception { + Connection conn = getConnection(config); + if (prepareDatabaseGeneratedId(config, conn)) { + // The caller may have generated SQL before metadata inspection. Rebuild it + // after removing APIJSON's synthetic id from the INSERT. + sql = null; + } + if (StringUtil.isEmpty(sql)) { + sql = config.gainSQL(config.isPrepared()); + } + PreparedStatement statement; //创建Statement对象 + if (config.getMethod() == RequestMethod.POST && config.getId() == null) { //自增id + if (config.isOracle() || config.isKingBase()) { + // Oracle and Kingbase JDBC require the generated column name; the + // RETURN_GENERATED_KEYS flag alone may leave generatedKeys null. + String[] generatedColumns = {config.getIdKey()}; + statement = conn.prepareStatement(sql, generatedColumns); } else { statement = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); } @@ -1235,9 +1572,19 @@ else if (RequestMethod.isGetMethod(config.getMethod(), true)) { // } // TODO 补充各种支持 TYPE_SCROLL_SENSITIVE 和 CONCUR_UPDATABLE 的数据库 - if (config.isMySQL() || config.isTiDB() || config.isMariaDB() || config.isOracle() || config.isSQLServer() || config.isDb2() - || config.isPostgreSQL() || config.isCockroachDB() || config.isOpenGauss() || config.isTimescaleDB() || config.isQuestDB() - ) { + if (config.isKingBase()) { + try { + statement = conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); + } + catch (SQLException e) { + // Some Kingbase JDBC/server combinations do not support scrollable cursors. + Log.e(TAG, "Kingbase scrollable ResultSet unavailable, falling back to forward-only: " + e.getMessage()); + statement = conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + } + } + else if (config.isMySQL() || config.isTiDB() || config.isMariaDB() || config.isOracle() || config.isSQLServer() || config.isDb2() + || config.isPostgreSQL() || config.isCockroachDB() || config.isOpenGauss() || config.isTimescaleDB() || config.isQuestDB() + ) { statement = conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); } else { statement = conn.prepareStatement(sql); @@ -1265,10 +1612,93 @@ else if (RequestMethod.isGetMethod(config.getMethod(), true)) { } // statement.close(); - return statement; - } - - public PreparedStatement setArgument(@NotNull SQLConfig config, @NotNull PreparedStatement statement, int index, Object value) throws SQLException { + return statement; + } + + /** + * Kingbase SQL Server compatibility mode supports true IDENTITY columns. If + * APIJSON supplied its fallback id, inspect the actual column metadata and + * omit that id only when the database declares the column auto-generated. + */ + protected boolean prepareDatabaseGeneratedId(@NotNull SQLConfig config, @NotNull Connection conn) throws Exception { + if (config.getMethod() != RequestMethod.POST || !config.isKingBaseSQLServer() + || !config.isIdGeneratedByAPIJSON() || config.getId() == null + || !isAutoGeneratedIdColumn(config, conn)) { + return false; + } + + List columns = config.getColumn(); + int idIndex = columns == null ? -1 : columns.indexOf(config.getIdKey()); + if (idIndex < 0) { + return false; + } + + List newColumns = new ArrayList<>(columns); + newColumns.remove(idIndex); + config.setColumn(newColumns); + + List> values = config.getValues(); + if (values != null) { + List> newValues = new ArrayList<>(values.size()); + for (List row : values) { + List newRow = row == null ? null : new ArrayList<>(row); + if (newRow != null && idIndex < newRow.size()) { + newRow.remove(idIndex); + } + newValues.add(newRow); + } + config.setValues(newValues); + } + + config.setId(null); + config.setIdGeneratedByAPIJSON(false); + return true; + } + + protected boolean isAutoGeneratedIdColumn(@NotNull SQLConfig config, @NotNull Connection conn) throws SQLException { + String idKey = config.getIdKey(); + DatabaseMetaData metadata = conn.getMetaData(); + try (ResultSet columns = metadata.getColumns(config.gainSQLCatalog(), config.gainSQLSchema(), config.gainSQLTable(), idKey)) { + while (columns.next()) { + String columnName = columns.getString("COLUMN_NAME"); + if (idKey.equalsIgnoreCase(columnName) + && ("YES".equalsIgnoreCase(columns.getString("IS_AUTOINCREMENT")) + || "YES".equalsIgnoreCase(columns.getString("IS_GENERATEDCOLUMN")))) { + return true; + } + } + } + + // Some Kingbase JDBC versions leave IS_AUTOINCREMENT blank in + // DatabaseMetaData but expose it correctly through result metadata. + String q = config.getQuote(); + String schema = config.gainSQLSchema(); + String tablePath = (StringUtil.isEmpty(schema, true) ? "" : q + schema + q + ".") + + q + config.gainSQLTable() + q; + String probeSql = "SELECT " + q + idKey + q + " FROM " + tablePath + " WHERE 1 = 0"; + try (PreparedStatement probe = conn.prepareStatement(probeSql); ResultSet rs = probe.executeQuery()) { + return rs.getMetaData().isAutoIncrement(1); + } + } + + public PreparedStatement setArgument(@NotNull SQLConfig config, @NotNull PreparedStatement statement, int index, Object value) throws SQLException { + if (config.isKingBaseMySQL()) { + setKingbaseMySQLArgument(statement, index + 1, value); + return statement; + } + if (config.isKingBaseOracle()) { + setKingbaseOracleArgument(statement, index + 1, value); + return statement; + } + if (config.isKingBaseSQLServer()) { + setKingbaseSQLServerArgument(statement, index + 1, value); + return statement; + } + if (config.isKingBase() && value != null + && (value instanceof Map || value instanceof Collection || value.getClass().isArray())) { + statement.setObject(index + 1, JSON.toJSONString(value), Types.OTHER); + return statement; + } //JSON.isBooleanOrNumberOrString(v) 解决 PostgreSQL: Can't infer the SQL type to use for an instance of com.alibaba.fastjson.JSONList if (apijson.JSON.isBoolOrNumOrStr(value)) { statement.setObject(index + 1, value); //PostgreSQL JDBC 不支持隐式类型转换 tinyint = varchar 报错 @@ -1276,8 +1706,188 @@ public PreparedStatement setArgument(@NotNull SQLConfig config, @NotNul else { statement.setString(index + 1, value == null ? null : value.toString()); //MySQL setObject 不支持 JSON 类型 } - return statement; - } + return statement; + } + + protected void setKingbaseMySQLArgument(@NotNull PreparedStatement statement, int parameterIndex, Object value) throws SQLException { + int jdbcType = Types.OTHER; + String typeName = ""; + try { + ParameterMetaData metadata = statement.getParameterMetaData(); + if (metadata != null) { + jdbcType = metadata.getParameterType(parameterIndex); + typeName = normalizeJdbcTypeName(metadata.getParameterTypeName(parameterIndex)); + } + } + catch (SQLException ignored) { + // The driver may not expose parameter metadata until execution. + } + + if (value == null) { + if (jdbcType == Types.NULL || jdbcType == 0) { + statement.setObject(parameterIndex, null); + } + else { + statement.setNull(parameterIndex, jdbcType); + } + return; + } + if (value instanceof byte[]) { + statement.setBytes(parameterIndex, (byte[]) value); + return; + } + if (value instanceof java.sql.Array) { + statement.setArray(parameterIndex, (java.sql.Array) value); + return; + } + if (value instanceof Map || value instanceof Collection || value.getClass().isArray()) { + String json = JSON.toJSONString(value); + if (isJSONTypeName(typeName) || jdbcType == Types.OTHER) { + statement.setObject(parameterIndex, json, Types.OTHER); + } + else { + statement.setString(parameterIndex, json); + } + return; + } + if (value instanceof UUID && jdbcType == Types.OTHER) { + statement.setObject(parameterIndex, value.toString(), Types.OTHER); + return; + } + statement.setObject(parameterIndex, value); + } + + protected void setKingbaseOracleArgument(@NotNull PreparedStatement statement, int parameterIndex, Object value) throws SQLException { + int jdbcType = Types.OTHER; + String typeName = ""; + try { + ParameterMetaData metadata = statement.getParameterMetaData(); + if (metadata != null) { + jdbcType = metadata.getParameterType(parameterIndex); + typeName = normalizeJdbcTypeName(metadata.getParameterTypeName(parameterIndex)); + } + } + catch (SQLException ignored) { + // Kingbase JDBC may defer parameter metadata until the statement executes. + } + + if (value == null) { + if (jdbcType == Types.NULL || jdbcType == 0 || jdbcType == Types.OTHER && typeName.isEmpty()) { + statement.setObject(parameterIndex, null); + } + else { + statement.setNull(parameterIndex, jdbcType); + } + return; + } + if (value instanceof byte[]) { + statement.setBytes(parameterIndex, (byte[]) value); + return; + } + if (value instanceof Blob) { + statement.setBlob(parameterIndex, (Blob) value); + return; + } + if (value instanceof Clob) { + statement.setClob(parameterIndex, (Clob) value); + return; + } + if (value instanceof java.sql.Array) { + statement.setArray(parameterIndex, (java.sql.Array) value); + return; + } + if (value instanceof Map || value instanceof Collection || value.getClass().isArray()) { + String json = JSON.toJSONString(value); + if (isJSONTypeName(typeName) || jdbcType == Types.OTHER) { + statement.setObject(parameterIndex, json, Types.OTHER); + } + else if (jdbcType == Types.CLOB || jdbcType == Types.NCLOB) { + statement.setString(parameterIndex, json); + } + else { + statement.setString(parameterIndex, json); + } + return; + } + if (value instanceof java.util.Date && !(value instanceof java.sql.Date) + && !(value instanceof Time) && !(value instanceof Timestamp)) { + statement.setTimestamp(parameterIndex, new Timestamp(((java.util.Date) value).getTime())); + return; + } + if (value instanceof UUID) { + statement.setString(parameterIndex, value.toString()); + return; + } + statement.setObject(parameterIndex, value); + } + + protected void setKingbaseSQLServerArgument(@NotNull PreparedStatement statement, int parameterIndex, Object value) throws SQLException { + int jdbcType = Types.OTHER; + String typeName = ""; + try { + ParameterMetaData metadata = statement.getParameterMetaData(); + if (metadata != null) { + jdbcType = metadata.getParameterType(parameterIndex); + typeName = normalizeJdbcTypeName(metadata.getParameterTypeName(parameterIndex)); + } + } + catch (SQLException ignored) { + // Kingbase JDBC may defer parameter metadata until execution. + } + + if (value == null) { + if (jdbcType == Types.NULL || jdbcType == 0 || jdbcType == Types.OTHER && typeName.isEmpty()) { + statement.setObject(parameterIndex, null); + } + else { + statement.setNull(parameterIndex, jdbcType); + } + return; + } + if (value instanceof byte[]) { + statement.setBytes(parameterIndex, (byte[]) value); + return; + } + if (value instanceof Blob) { + statement.setBlob(parameterIndex, (Blob) value); + return; + } + if (value instanceof Clob) { + statement.setClob(parameterIndex, (Clob) value); + return; + } + if (value instanceof SQLXML) { + statement.setSQLXML(parameterIndex, (SQLXML) value); + return; + } + if (value instanceof java.sql.Array) { + statement.setArray(parameterIndex, (java.sql.Array) value); + return; + } + if (value instanceof Map || value instanceof Collection || value.getClass().isArray()) { + String serialized = JSON.toJSONString(value); + if (isJSONTypeName(typeName) || jdbcType == Types.OTHER) { + statement.setObject(parameterIndex, serialized, Types.OTHER); + } + else if (jdbcType == Types.NCHAR || jdbcType == Types.NVARCHAR || jdbcType == Types.LONGNVARCHAR) { + statement.setNString(parameterIndex, serialized); + } + else { + statement.setString(parameterIndex, serialized); + } + return; + } + if (value instanceof java.util.Date && !(value instanceof java.sql.Date) + && !(value instanceof Time) && !(value instanceof Timestamp)) { + statement.setTimestamp(parameterIndex, new Timestamp(((java.util.Date) value).getTime())); + return; + } + if (value instanceof UUID) { + statement.setObject(parameterIndex, value); + return; + } + statement.setObject(parameterIndex, value); + } protected Map connectionMap = new HashMap<>(); public Map getConnectionMap() { @@ -1523,21 +2133,41 @@ public int executeUpdate(@NotNull SQLConfig config, String sql) throws count = stt.executeUpdate(StringUtil.isEmpty(sql) ? config.gainSQL(false) : sql); } - else { - stt = getStatement(config); - count = ((PreparedStatement) stt).executeUpdate(); // PreparedStatement 不用传 SQL - } + else { + stt = getStatement(config); + PreparedStatement preparedStatement = (PreparedStatement) stt; + if (config.isKingBaseSQLServer() && config.getMethod() == RequestMethod.POST + && config.getId() == null) { + // Kingbase SQL Server mode returns IDENTITY as a regular result set named + // GENERATED_KEYS. Its executeUpdate() returns -1 and getGeneratedKeys() + // raises SQLState 02000 with the V009R001C010 driver. + boolean hasResultSet = preparedStatement.execute(); + count = preparedStatement.getUpdateCount(); + if (hasResultSet) { + try (ResultSet rs = preparedStatement.getResultSet()) { + if (rs != null && rs.next()) { + config.setId(rs.getLong(1)); + count = 1; + } + } + } + } + else { + count = preparedStatement.executeUpdate(); // PreparedStatement 不用传 SQL + } + } if (count <= 0 && config.isHive()) { count = 1; } - if (config.getId() == null && config.getMethod() == RequestMethod.POST) { // 自增id - ResultSet rs = stt.getGeneratedKeys(); - if (rs != null && rs.next()) { - config.setId(rs.getLong(1)); - } - } + if (config.getId() == null && config.getMethod() == RequestMethod.POST) { // 自增id + try (ResultSet rs = stt.getGeneratedKeys()) { + if (rs != null && rs.next()) { + config.setId(rs.getLong(1)); + } + } + } return count; } diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java b/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java index 81bef3cd..cb15e25c 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ @@ -177,6 +177,8 @@ public abstract class AbstractVerifier, L exten ACCESS_MAP = new HashMap<>(SYSTEM_ACCESS_MAP); + ACCESS_FAKE_DELETE_MAP = new LinkedHashMap<>(); + REQUEST_MAP = new HashMap<>(ACCESS_MAP.size()*7); // 单个与批量增删改 COMPILE_MAP = new HashMap(); @@ -1814,7 +1816,8 @@ public static , L extends List> void ve config.setTable(table); param.forEach((key,value) -> config.putWhere(key, value, false)); - SQLExecutor executor = parser.getSQLExecutor(); + SQLExecutor executor = parser.createSQLExecutor(); + executor.setParser(parser); try { M result = executor.execute(config, false); if (result == null) { @@ -1902,7 +1905,8 @@ public static , L extends List> void ve } param.forEach((key,value) -> config.putWhere(key,value, false)); - SQLExecutor executor = parser.getSQLExecutor(); + SQLExecutor executor = parser.createSQLExecutor(); + executor.setParser(parser); try { M result = executor.execute(config, false); if (result == null) { diff --git a/APIJSONORM/src/main/java/apijson/orm/ConfigMethodAccess.java b/APIJSONORM/src/main/java/apijson/orm/ConfigMethodAccess.java index f8935516..8772512d 100755 --- a/APIJSONORM/src/main/java/apijson/orm/ConfigMethodAccess.java +++ b/APIJSONORM/src/main/java/apijson/orm/ConfigMethodAccess.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/Entry.java b/APIJSONORM/src/main/java/apijson/orm/Entry.java index 3e010435..41705cb6 100755 --- a/APIJSONORM/src/main/java/apijson/orm/Entry.java +++ b/APIJSONORM/src/main/java/apijson/orm/Entry.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/FunctionParser.java b/APIJSONORM/src/main/java/apijson/orm/FunctionParser.java index 8af02923..29c8f3ad 100644 --- a/APIJSONORM/src/main/java/apijson/orm/FunctionParser.java +++ b/APIJSONORM/src/main/java/apijson/orm/FunctionParser.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/JSONRequest.java b/APIJSONORM/src/main/java/apijson/orm/JSONRequest.java index 5ac92429..c9c9d12a 100755 --- a/APIJSONORM/src/main/java/apijson/orm/JSONRequest.java +++ b/APIJSONORM/src/main/java/apijson/orm/JSONRequest.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/Join.java b/APIJSONORM/src/main/java/apijson/orm/Join.java index 96d5e2ec..3695638a 100644 --- a/APIJSONORM/src/main/java/apijson/orm/Join.java +++ b/APIJSONORM/src/main/java/apijson/orm/Join.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/KingbaseSQLDialect.java b/APIJSONORM/src/main/java/apijson/orm/KingbaseSQLDialect.java new file mode 100644 index 00000000..6bed9f08 --- /dev/null +++ b/APIJSONORM/src/main/java/apijson/orm/KingbaseSQLDialect.java @@ -0,0 +1,86 @@ +/*Copyright (C) 2026 the APIJSON group. All rights reserved. + +This source code is licensed under the Apache License Version 2.0.*/ + +package apijson.orm; + +/** Centralized SQL capabilities for KingbaseES compatibility modes. */ +public enum KingbaseSQLDialect { + NONE(null, null, false, false), + LEGACY(SQLConfig.DATABASE_KINGBASE, "\"", false, false), + MYSQL(SQLConfig.DATABASE_KINGBASE_MYSQL, "`", true, true), + ORACLE(SQLConfig.DATABASE_KINGBASE_ORACLE, "\"", false, false), + SQLSERVER(SQLConfig.DATABASE_KINGBASE_SQLSERVER, "\"", false, false); + + private final String database; + private final String identifierQuote; + private final boolean mysqlSyntax; + private final boolean dmlLimit; + + KingbaseSQLDialect(String database, String identifierQuote, boolean mysqlSyntax, boolean dmlLimit) { + this.database = database; + this.identifierQuote = identifierQuote; + this.mysqlSyntax = mysqlSyntax; + this.dmlLimit = dmlLimit; + } + + public static KingbaseSQLDialect from(String database) { + if (database != null) { + for (KingbaseSQLDialect dialect : values()) { + if (database.equals(dialect.database)) { + return dialect; + } + } + } + return NONE; + } + + public boolean isKingbase() { + return this != NONE; + } + + public boolean isMySQL() { + return mysqlSyntax; + } + + public boolean isOracle() { + return this == ORACLE; + } + + public boolean isSQLServer() { + return this == SQLSERVER; + } + + public boolean supportsDmlLimit() { + return dmlLimit; + } + + /** + * SQL Server compatibility mode follows the SQL Server spelling of the + * OFFSET/FETCH clause. Other T-SQL-family databases in APIJSON retain their + * existing FETCH FIRST spelling. + */ + public String getSelectLimit(int offset, int count) { + if (isSQLServer()) { + return " OFFSET " + offset + " ROWS FETCH NEXT " + count + " ROWS ONLY"; + } + return null; + } + + /** + * KingbaseES exposes an explain statement returning the plan result set. + * SQL Server's session-level STATISTICS PROFILE switch is therefore neither + * required nor suitable for APIJSON's one-shot explain request. Oracle mode + * retains its compatible EXPLAIN PLAN FOR spelling. + */ + public String getExplainPrefix() { + if (isOracle()) { + return "EXPLAIN PLAN FOR "; + } + return isKingbase() ? "EXPLAIN " : null; + } + + public String getIdentifierQuote() { + return identifierQuote; + } +} diff --git a/APIJSONORM/src/main/java/apijson/orm/Logic.java b/APIJSONORM/src/main/java/apijson/orm/Logic.java index f860aa9a..f857b060 100755 --- a/APIJSONORM/src/main/java/apijson/orm/Logic.java +++ b/APIJSONORM/src/main/java/apijson/orm/Logic.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/ObjectParser.java b/APIJSONORM/src/main/java/apijson/orm/ObjectParser.java index 5d207f41..743e5a87 100755 --- a/APIJSONORM/src/main/java/apijson/orm/ObjectParser.java +++ b/APIJSONORM/src/main/java/apijson/orm/ObjectParser.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/OnParseCallback.java b/APIJSONORM/src/main/java/apijson/orm/OnParseCallback.java index e02fd90f..68544cfd 100755 --- a/APIJSONORM/src/main/java/apijson/orm/OnParseCallback.java +++ b/APIJSONORM/src/main/java/apijson/orm/OnParseCallback.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/Operation.java b/APIJSONORM/src/main/java/apijson/orm/Operation.java index 45fcbf47..cf14a9eb 100755 --- a/APIJSONORM/src/main/java/apijson/orm/Operation.java +++ b/APIJSONORM/src/main/java/apijson/orm/Operation.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/Pair.java b/APIJSONORM/src/main/java/apijson/orm/Pair.java index 7661d07b..d060687a 100755 --- a/APIJSONORM/src/main/java/apijson/orm/Pair.java +++ b/APIJSONORM/src/main/java/apijson/orm/Pair.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/Parser.java b/APIJSONORM/src/main/java/apijson/orm/Parser.java index 1492dfc1..b47e9f8f 100755 --- a/APIJSONORM/src/main/java/apijson/orm/Parser.java +++ b/APIJSONORM/src/main/java/apijson/orm/Parser.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/ParserCreator.java b/APIJSONORM/src/main/java/apijson/orm/ParserCreator.java index f3f8d375..b7dc4572 100755 --- a/APIJSONORM/src/main/java/apijson/orm/ParserCreator.java +++ b/APIJSONORM/src/main/java/apijson/orm/ParserCreator.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/SQLConfig.java b/APIJSONORM/src/main/java/apijson/orm/SQLConfig.java index 5386d160..8e6bbff6 100755 --- a/APIJSONORM/src/main/java/apijson/orm/SQLConfig.java +++ b/APIJSONORM/src/main/java/apijson/orm/SQLConfig.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ @@ -27,6 +27,9 @@ public interface SQLConfig, L extends List, L extends List, L extends List, L extends List setId(Object id); + /**Whether the current id was supplied by {@link AbstractSQLConfig.IdCallback}, rather than by the request.*/ + default boolean isIdGeneratedByAPIJSON() { + return false; + } + + default SQLConfig setIdGeneratedByAPIJSON(boolean generated) { + return this; + } + Object getIdIn(); SQLConfig setIdIn(Object idIn); diff --git a/APIJSONORM/src/main/java/apijson/orm/SQLCreator.java b/APIJSONORM/src/main/java/apijson/orm/SQLCreator.java index 77625cfa..58c3bc5d 100755 --- a/APIJSONORM/src/main/java/apijson/orm/SQLCreator.java +++ b/APIJSONORM/src/main/java/apijson/orm/SQLCreator.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/SQLExecutor.java b/APIJSONORM/src/main/java/apijson/orm/SQLExecutor.java index 6540771d..ab9defac 100755 --- a/APIJSONORM/src/main/java/apijson/orm/SQLExecutor.java +++ b/APIJSONORM/src/main/java/apijson/orm/SQLExecutor.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ @@ -88,7 +88,16 @@ default int executeUpdate(@NotNull SQLConfig config) throws Exception { * @param label * @return */ - boolean isJSONType(@NotNull SQLConfig config, ResultSetMetaData rsmd, int position, String label); + boolean isJSONType(@NotNull SQLConfig config, ResultSetMetaData rsmd, int position, String label); + + /** + * Convert a JDBC value to a JSON-serializable Java value. Database adapters can + * override this hook without replacing the complete result-set traversal. + */ + default Object mapResultValue(@NotNull SQLConfig config, Object value, int jdbcType + , String typeName, String label) throws Exception { + return value; + } Connection getConnection(@NotNull SQLConfig config) throws Exception; diff --git a/APIJSONORM/src/main/java/apijson/orm/Subquery.java b/APIJSONORM/src/main/java/apijson/orm/Subquery.java index ce6e7244..5246abcb 100644 --- a/APIJSONORM/src/main/java/apijson/orm/Subquery.java +++ b/APIJSONORM/src/main/java/apijson/orm/Subquery.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/Verifier.java b/APIJSONORM/src/main/java/apijson/orm/Verifier.java index 97db58a9..3503af69 100755 --- a/APIJSONORM/src/main/java/apijson/orm/Verifier.java +++ b/APIJSONORM/src/main/java/apijson/orm/Verifier.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/VerifierCreator.java b/APIJSONORM/src/main/java/apijson/orm/VerifierCreator.java index 37429098..6fa1939c 100644 --- a/APIJSONORM/src/main/java/apijson/orm/VerifierCreator.java +++ b/APIJSONORM/src/main/java/apijson/orm/VerifierCreator.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/Visitor.java b/APIJSONORM/src/main/java/apijson/orm/Visitor.java index 5d1b6063..031f0bf2 100755 --- a/APIJSONORM/src/main/java/apijson/orm/Visitor.java +++ b/APIJSONORM/src/main/java/apijson/orm/Visitor.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/exception/CommonException.java b/APIJSONORM/src/main/java/apijson/orm/exception/CommonException.java index 7e2884ef..f63446f7 100755 --- a/APIJSONORM/src/main/java/apijson/orm/exception/CommonException.java +++ b/APIJSONORM/src/main/java/apijson/orm/exception/CommonException.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ @@ -205,6 +205,15 @@ else if (config.isOpenGauss()) { else if (config.isDameng()) { db = SQLConfig.DATABASE_DAMENG + " " + dbVersion; } + else if (config.isKingBaseMySQL()) { + db = SQLConfig.DATABASE_KINGBASE_MYSQL + " " + dbVersion; + } + else if (config.isKingBaseOracle()) { + db = SQLConfig.DATABASE_KINGBASE_ORACLE + " " + dbVersion; + } + else if (config.isKingBaseSQLServer()) { + db = SQLConfig.DATABASE_KINGBASE_SQLSERVER + " " + dbVersion; + } else if (config.isKingBase()) { db = SQLConfig.DATABASE_KINGBASE + " " + dbVersion; } @@ -250,6 +259,9 @@ else if (config.isTrino()) { else if (config.isDoris()) { db = SQLConfig.DATABASE_DORIS + " " + dbVersion; } + else if (config.isStarRocks()) { + db = SQLConfig.DATABASE_STARROCKS + " " + dbVersion; + } else if (config.isSnowflake()) { db = SQLConfig.DATABASE_SNOWFLAKE + " " + dbVersion; } diff --git a/APIJSONORM/src/main/java/apijson/orm/exception/ConditionErrorException.java b/APIJSONORM/src/main/java/apijson/orm/exception/ConditionErrorException.java index 44c43a29..1aa7429f 100755 --- a/APIJSONORM/src/main/java/apijson/orm/exception/ConditionErrorException.java +++ b/APIJSONORM/src/main/java/apijson/orm/exception/ConditionErrorException.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/exception/ConflictException.java b/APIJSONORM/src/main/java/apijson/orm/exception/ConflictException.java index a39d88d3..1ae87394 100755 --- a/APIJSONORM/src/main/java/apijson/orm/exception/ConflictException.java +++ b/APIJSONORM/src/main/java/apijson/orm/exception/ConflictException.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/exception/NotExistException.java b/APIJSONORM/src/main/java/apijson/orm/exception/NotExistException.java index 69e34a80..70f9112b 100755 --- a/APIJSONORM/src/main/java/apijson/orm/exception/NotExistException.java +++ b/APIJSONORM/src/main/java/apijson/orm/exception/NotExistException.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/exception/NotLoggedInException.java b/APIJSONORM/src/main/java/apijson/orm/exception/NotLoggedInException.java index 1e16b579..d71030ca 100755 --- a/APIJSONORM/src/main/java/apijson/orm/exception/NotLoggedInException.java +++ b/APIJSONORM/src/main/java/apijson/orm/exception/NotLoggedInException.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/exception/OutOfRangeException.java b/APIJSONORM/src/main/java/apijson/orm/exception/OutOfRangeException.java index 043ba5a6..0e254e86 100755 --- a/APIJSONORM/src/main/java/apijson/orm/exception/OutOfRangeException.java +++ b/APIJSONORM/src/main/java/apijson/orm/exception/OutOfRangeException.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/exception/UnsupportedDataTypeException.java b/APIJSONORM/src/main/java/apijson/orm/exception/UnsupportedDataTypeException.java index 1c7d7c27..84f0a6aa 100644 --- a/APIJSONORM/src/main/java/apijson/orm/exception/UnsupportedDataTypeException.java +++ b/APIJSONORM/src/main/java/apijson/orm/exception/UnsupportedDataTypeException.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/exception/package-info.java b/APIJSONORM/src/main/java/apijson/orm/exception/package-info.java index 073c6bda..61306205 100755 --- a/APIJSONORM/src/main/java/apijson/orm/exception/package-info.java +++ b/APIJSONORM/src/main/java/apijson/orm/exception/package-info.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/Access.java b/APIJSONORM/src/main/java/apijson/orm/model/Access.java index db934ec9..8f0b332f 100644 --- a/APIJSONORM/src/main/java/apijson/orm/model/Access.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/Access.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/AllColumn.java b/APIJSONORM/src/main/java/apijson/orm/model/AllColumn.java index f4d93962..49110e98 100644 --- a/APIJSONORM/src/main/java/apijson/orm/model/AllColumn.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/AllColumn.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/AllColumnComment.java b/APIJSONORM/src/main/java/apijson/orm/model/AllColumnComment.java index cf1480f7..495d9ad7 100644 --- a/APIJSONORM/src/main/java/apijson/orm/model/AllColumnComment.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/AllColumnComment.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/AllTable.java b/APIJSONORM/src/main/java/apijson/orm/model/AllTable.java index dcc3268c..3c92e32d 100644 --- a/APIJSONORM/src/main/java/apijson/orm/model/AllTable.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/AllTable.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/AllTableComment.java b/APIJSONORM/src/main/java/apijson/orm/model/AllTableComment.java index ed61a0b4..9491641a 100644 --- a/APIJSONORM/src/main/java/apijson/orm/model/AllTableComment.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/AllTableComment.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/Column.java b/APIJSONORM/src/main/java/apijson/orm/model/Column.java index 800b60eb..f9bdae3a 100755 --- a/APIJSONORM/src/main/java/apijson/orm/model/Column.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/Column.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/Document.java b/APIJSONORM/src/main/java/apijson/orm/model/Document.java index 1b2bc5d1..d800c147 100755 --- a/APIJSONORM/src/main/java/apijson/orm/model/Document.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/Document.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/ExtendedProperty.java b/APIJSONORM/src/main/java/apijson/orm/model/ExtendedProperty.java index cc6e04ee..00343db9 100644 --- a/APIJSONORM/src/main/java/apijson/orm/model/ExtendedProperty.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/ExtendedProperty.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/Function.java b/APIJSONORM/src/main/java/apijson/orm/model/Function.java index 841e6424..d35a4bc9 100644 --- a/APIJSONORM/src/main/java/apijson/orm/model/Function.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/Function.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/PgAttribute.java b/APIJSONORM/src/main/java/apijson/orm/model/PgAttribute.java index 174b3db0..e18aaf8c 100644 --- a/APIJSONORM/src/main/java/apijson/orm/model/PgAttribute.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/PgAttribute.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/PgClass.java b/APIJSONORM/src/main/java/apijson/orm/model/PgClass.java index e58feb69..46933708 100644 --- a/APIJSONORM/src/main/java/apijson/orm/model/PgClass.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/PgClass.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/Request.java b/APIJSONORM/src/main/java/apijson/orm/model/Request.java index 46be3d30..0c5700ea 100755 --- a/APIJSONORM/src/main/java/apijson/orm/model/Request.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/Request.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/Script.java b/APIJSONORM/src/main/java/apijson/orm/model/Script.java index 51205db0..6b9d48fb 100644 --- a/APIJSONORM/src/main/java/apijson/orm/model/Script.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/Script.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/SysColumn.java b/APIJSONORM/src/main/java/apijson/orm/model/SysColumn.java index 534446de..54e30143 100644 --- a/APIJSONORM/src/main/java/apijson/orm/model/SysColumn.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/SysColumn.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/SysTable.java b/APIJSONORM/src/main/java/apijson/orm/model/SysTable.java index 7b1eb74c..2066a164 100644 --- a/APIJSONORM/src/main/java/apijson/orm/model/SysTable.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/SysTable.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/Table.java b/APIJSONORM/src/main/java/apijson/orm/model/Table.java index 45df12fd..ff806f64 100755 --- a/APIJSONORM/src/main/java/apijson/orm/model/Table.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/Table.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/model/package-info.java b/APIJSONORM/src/main/java/apijson/orm/model/package-info.java index a0b5641c..24a487fe 100755 --- a/APIJSONORM/src/main/java/apijson/orm/model/package-info.java +++ b/APIJSONORM/src/main/java/apijson/orm/model/package-info.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/orm/package-info.java b/APIJSONORM/src/main/java/apijson/orm/package-info.java index 76569c93..1e7c79b2 100755 --- a/APIJSONORM/src/main/java/apijson/orm/package-info.java +++ b/APIJSONORM/src/main/java/apijson/orm/package-info.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/main/java/apijson/package-info.java b/APIJSONORM/src/main/java/apijson/package-info.java index a4fcbeb2..0cc67076 100755 --- a/APIJSONORM/src/main/java/apijson/package-info.java +++ b/APIJSONORM/src/main/java/apijson/package-info.java @@ -1,4 +1,4 @@ -/*Copyright (C) 2020 Tencent. All rights reserved. +/*Copyright (C) 2026 the APIJSON group. All rights reserved. This source code is licensed under the Apache License Version 2.0.*/ diff --git a/APIJSONORM/src/test/java/apijson/orm/AbstractObjectParserReferenceTest.java b/APIJSONORM/src/test/java/apijson/orm/AbstractObjectParserReferenceTest.java new file mode 100644 index 00000000..78b3b161 --- /dev/null +++ b/APIJSONORM/src/test/java/apijson/orm/AbstractObjectParserReferenceTest.java @@ -0,0 +1,309 @@ +package apijson.orm; + +import apijson.JSON; +import apijson.JSONParser; +import apijson.RequestMethod; +import apijson.orm.exception.CommonException; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class AbstractObjectParserReferenceTest { + private static final String REFERENCE_KEY = "id@"; + private static final String TARGET_PATH = "Privacy/id"; + private static final String TRAILING_KEY = "afterReference"; + private static final long RESOLVED_VALUE = 42L; + + private static JSONParser, ? extends List> previousJSONParser; + + @BeforeClass + public static void installJSONParser() { + previousJSONParser = JSON.DEFAULT_JSON_PARSER; + JSON.DEFAULT_JSON_PARSER = new JSONParser, List>() { + @Override + public Map createJSONObject() { + return new LinkedHashMap<>(); + } + + @Override + public List createJSONArray() { + return new ArrayList<>(); + } + + @Override + public Object parse(Object json) { + return json; + } + + @Override + @SuppressWarnings("unchecked") + public Map parseObject(Object json) { + return (Map) json; + } + + @Override + public T parseObject(Object json, Class clazz) { + return clazz.cast(json); + } + + @Override + @SuppressWarnings("unchecked") + public List parseArray(Object json) { + return (List) json; + } + + @Override + @SuppressWarnings("unchecked") + public List parseArray(Object json, Class clazz) { + return (List) json; + } + + @Override + public String toJSONString(Object obj, boolean format) { + return String.valueOf(obj); + } + }; + } + + @AfterClass + public static void restoreJSONParser() { + JSON.DEFAULT_JSON_PARSER = previousJSONParser; + } + + @Test + public void rejectsUnresolvedPostTableReference() throws Exception { + Fixture fixture = newFixture(RequestMethod.POST, true, null); + + assertUnsupportedReference(fixture); + } + + @Test + public void rejectsUnresolvedPutTableReference() throws Exception { + Fixture fixture = newFixture(RequestMethod.PUT, true, null); + + assertUnsupportedReference(fixture); + } + + @Test + public void rejectsUnresolvedPostNonTableReference() throws Exception { + Fixture fixture = newFixture(RequestMethod.POST, false, null); + + assertUnsupportedReference(fixture); + } + + @Test + public void keepsGetTableInvalidationForUnresolvedReference() throws Exception { + Fixture fixture = newFixture(RequestMethod.GET, true, null); + + assertNull(fixture.objectParser.parse(fixture.name, false)); + assertEquals(TARGET_PATH, fixture.parser.lastPath); + } + + @Test + public void keepsDeleteTableInvalidationForUnresolvedReference() throws Exception { + Fixture fixture = newFixture(RequestMethod.DELETE, true, null); + + assertNull(fixture.objectParser.parse(fixture.name, false)); + assertEquals(TARGET_PATH, fixture.parser.lastPath); + } + + @Test + public void keepsGetNonTableSkipForUnresolvedReference() throws Exception { + Fixture fixture = newFixture(RequestMethod.GET, false, null); + + assertSame(fixture.objectParser, fixture.objectParser.parse(fixture.name, false)); + assertEquals(TARGET_PATH, fixture.parser.lastPath); + assertFalse(fixture.objectParser.getSQLRequest().containsKey(REFERENCE_KEY)); + assertEquals(Boolean.TRUE, fixture.objectParser.getSQLRequest().get(TRAILING_KEY)); + } + + @Test + public void resolvesPostReferenceAndContinuesParsing() throws Exception { + Fixture fixture = newFixture(RequestMethod.POST, true, RESOLVED_VALUE); + + assertSame(fixture.objectParser, fixture.objectParser.parse(fixture.name, false)); + assertEquals(TARGET_PATH, fixture.parser.lastPath); + assertFalse(fixture.objectParser.getSQLRequest().containsKey(REFERENCE_KEY)); + assertEquals(Long.valueOf(RESOLVED_VALUE), fixture.objectParser.getSQLRequest().get("id")); + assertEquals(Boolean.TRUE, fixture.objectParser.getSQLRequest().get(TRAILING_KEY)); + } + + @Test + public void rejectsUnresolvedCrudReferenceWithPostDirective() throws Exception { + Map user = new LinkedHashMap<>(); + user.put(REFERENCE_KEY, TARGET_PATH); + user.put(TRAILING_KEY, Boolean.TRUE); + + Map request = new LinkedHashMap<>(); + request.put("User", user); + request.put("@post", "User"); + + ReferenceParser parser = new ReferenceParser(null); + Map verifiedRequest = parser.verifyCrud(request); + Map verifiedUser = JSON.get(verifiedRequest, "User"); + assertSame(RequestMethod.POST, verifiedUser.get("@method")); + + try { + parser.onObjectParse(verifiedUser, null, "User", null, false, null); + fail("The effective POST method must reject an unresolved reference"); + } + catch (Exception e) { + assertUnsupportedOperation(e); + } + + assertNotNull(parser.lastObjectParser); + assertSame(RequestMethod.POST, parser.lastObjectParser.getMethod()); + assertEquals(TARGET_PATH, parser.lastPath); + assertNotNull(parser.lastObjectParser.getSQLRequest()); + assertFalse(parser.lastObjectParser.getSQLRequest().containsKey(TRAILING_KEY)); + } + + private static void assertUnsupportedReference(Fixture fixture) throws Exception { + try { + fixture.objectParser.parse(fixture.name, false); + fail("Unresolved POST/PUT references must be rejected"); + } + catch (Exception e) { + assertUnsupportedOperation(e); + } + + assertEquals(TARGET_PATH, fixture.parser.lastPath); + assertNotNull(fixture.objectParser.getSQLRequest()); + assertFalse("Entries after the unresolved reference must not be parsed", + fixture.objectParser.getSQLRequest().containsKey(TRAILING_KEY)); + } + + private static void assertUnsupportedOperation(Exception e) { + Throwable cause = e instanceof CommonException && e.getCause() != null ? e.getCause() : e; + assertTrue("Expected UnsupportedOperationException but got " + cause.getClass().getName(), + cause instanceof UnsupportedOperationException); + } + + private static Fixture newFixture(RequestMethod method, boolean isTable, Object target) throws Exception { + Map request = new LinkedHashMap<>(); + request.put(REFERENCE_KEY, TARGET_PATH); + request.put(TRAILING_KEY, Boolean.TRUE); + + ReferenceParser parser = new ReferenceParser(target); + TestObjectParser objectParser = new TestObjectParser(request, isTable); + objectParser.setParser(parser); + objectParser.setMethod(method); + + return new Fixture(parser, objectParser, isTable ? "User" : "metadata"); + } + + private static final class Fixture { + private final ReferenceParser parser; + private final TestObjectParser objectParser; + private final String name; + + private Fixture(ReferenceParser parser, TestObjectParser objectParser, String name) { + this.parser = parser; + this.objectParser = objectParser; + this.name = name; + } + } + + private static final class TestObjectParser + extends AbstractObjectParser, List> { + private TestObjectParser(Map request, boolean isTable) throws Exception { + super(request, null, null, false, isTable, false); + } + + @Override + public SQLConfig, List> newSQLConfig(RequestMethod method, + String table, String alias, Map request, + List, List>> joinList, boolean isProcedure) { + return null; + } + } + + private static final class ReferenceParser + extends AbstractParser, List> { + private final Object target; + private String lastPath; + private TestObjectParser lastObjectParser; + + private ReferenceParser(Object target) { + super(RequestMethod.CRUD, false); + this.target = target; + } + + private Map verifyCrud(Map request) throws Exception { + return batchVerify(RequestMethod.CRUD, null, 0, null, request, 10, this); + } + + @Override + public Object getValueByPath(String valuePath) { + lastPath = valuePath; + return target; + } + + @Override + protected Map getRequestStructure(RequestMethod method, String tag, int version) { + return new LinkedHashMap<>(); + } + + @Override + protected Map objectVerify(RequestMethod method, String tag, int version, String name, + Map request, int maxUpdateCount, + SQLCreator, List> creator, Map object) { + return request; + } + + @Override + public Object onFunctionParse(String key, String function, String parentPath, String currentName, + Map currentObject, boolean containRaw) { + return null; + } + + @Override + public ObjectParser, List> createObjectParser( + Map request, String parentPath, + SQLConfig, List> arrayConfig, + boolean isSubquery, boolean isTable, boolean isArrayMainTable) throws Exception { + TestObjectParser objectParser = new TestObjectParser(request, isTable); + objectParser.setParser(this); + lastObjectParser = objectParser; + return objectParser; + } + + @Override + public Parser, List> createParser() { + return new ReferenceParser(target); + } + + @Override + public FunctionParser, List> createFunctionParser() { + return null; + } + + @Override + public Verifier, List> createVerifier() { + return null; + } + + @Override + public SQLConfig, List> createSQLConfig() { + return null; + } + + @Override + public SQLExecutor, List> createSQLExecutor() { + return null; + } + } +} diff --git a/APIJSONORM/src/test/java/apijson/orm/AbstractParserMethodDirectiveTest.java b/APIJSONORM/src/test/java/apijson/orm/AbstractParserMethodDirectiveTest.java new file mode 100644 index 00000000..dc96bfb0 --- /dev/null +++ b/APIJSONORM/src/test/java/apijson/orm/AbstractParserMethodDirectiveTest.java @@ -0,0 +1,193 @@ +package apijson.orm; + +import apijson.JSON; +import apijson.JSONParser; +import apijson.RequestMethod; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class AbstractParserMethodDirectiveTest { + private static JSONParser, ? extends List> previousJSONParser; + + @BeforeClass + public static void setUpJSONParser() { + previousJSONParser = JSON.DEFAULT_JSON_PARSER; + JSON.DEFAULT_JSON_PARSER = new JSONParser, List>() { + @Override + public Map createJSONObject() { + return new LinkedHashMap<>(); + } + + @Override + public List createJSONArray() { + return new ArrayList<>(); + } + + @Override + public Object parse(Object json) { + return json; + } + + @Override + @SuppressWarnings("unchecked") + public Map parseObject(Object json) { + return (Map) json; + } + + @Override + public T parseObject(Object json, Class clazz) { + return clazz.cast(json); + } + + @Override + @SuppressWarnings("unchecked") + public List parseArray(Object json) { + return (List) json; + } + + @Override + @SuppressWarnings("unchecked") + public List parseArray(Object json, Class clazz) { + return (List) json; + } + + @Override + public String toJSONString(Object obj, boolean format) { + return String.valueOf(obj); + } + }; + } + + @AfterClass + public static void restoreJSONParser() { + JSON.DEFAULT_JSON_PARSER = previousJSONParser; + } + + @Test + public void dispatchesMethodDirectivesBeforeBusinessObjectsRegardlessOfOrder() throws Exception { + TestParser parser = new TestParser(); + Map request = new LinkedHashMap<>(); + + Map delete = new LinkedHashMap<>(); + delete.put("id", 1L); + request.put("Moment", delete); + request.put("@delete", "Moment"); + + List comments = new ArrayList<>(); + comments.add(new LinkedHashMap()); + request.put("Comment:new[]", comments); + request.put("@post", "Comment:new[]"); + + parser.run(request); + + assertSame(RequestMethod.DELETE, parser.methods.get("Moment")); + assertSame(RequestMethod.POST, parser.methods.get("Comment:new[]")); + } + + @Test + public void keepsObjectMethodPriorityOverGlobalDirective() throws Exception { + TestParser parser = new TestParser(); + Map request = new LinkedHashMap<>(); + Map moment = new LinkedHashMap<>(); + moment.put("@method", RequestMethod.PUT.name()); + moment.put("id", 1L); + request.put("Moment", moment); + request.put("@delete", "Moment"); + + parser.run(request); + + assertSame(RequestMethod.PUT, parser.methods.get("Moment")); + } + + @Test + public void rejectsArrayMethodDirectiveValues() throws Exception { + Map request = new LinkedHashMap<>(); + List directive = new ArrayList<>(); + directive.add("Moment"); + request.put("@delete", directive); + request.put("Moment", new LinkedHashMap()); + + try { + new TestParser().run(request); + fail("List-valued method directive should be rejected"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("String")); + assertTrue(e.getMessage().contains("Map")); + } + } + + private static final class TestParser extends AbstractParser, List> { + private final Map methods = new LinkedHashMap<>(); + + private TestParser() { + super(RequestMethod.CRUD, false); + } + + private Map run(Map request) throws Exception { + return batchVerify(RequestMethod.CRUD, null, 0, null, request, 10, this); + } + + @Override + protected Map getRequestStructure(RequestMethod method, String tag, int version) { + return new LinkedHashMap<>(); + } + + @Override + protected Map objectVerify(RequestMethod method, String tag, int version, String name, + Map request, int maxUpdateCount, + SQLCreator, List> creator, Map object) { + methods.put(request.keySet().iterator().next(), method); + return request; + } + + @Override + public Object onFunctionParse(String key, String function, String parentPath, String currentName, + Map currentObject, boolean containRaw) { + return null; + } + + @Override + public ObjectParser, List> createObjectParser( + Map request, String parentPath, + SQLConfig, List> arrayConfig, + boolean isSubquery, boolean isTable, boolean isArrayMainTable) { + return null; + } + + @Override + public Parser, List> createParser() { + return new TestParser(); + } + + @Override + public FunctionParser, List> createFunctionParser() { + return null; + } + + @Override + public Verifier, List> createVerifier() { + return null; + } + + @Override + public SQLConfig, List> createSQLConfig() { + return null; + } + + @Override + public SQLExecutor, List> createSQLExecutor() { + return null; + } + } +} diff --git a/APIJSONORM/src/test/java/apijson/orm/AbstractSQLConfigFakeDeleteTest.java b/APIJSONORM/src/test/java/apijson/orm/AbstractSQLConfigFakeDeleteTest.java new file mode 100644 index 00000000..cd31e28b --- /dev/null +++ b/APIJSONORM/src/test/java/apijson/orm/AbstractSQLConfigFakeDeleteTest.java @@ -0,0 +1,214 @@ +package apijson.orm; + +import apijson.JSON; +import apijson.JSONParser; +import apijson.RequestMethod; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class AbstractSQLConfigFakeDeleteTest { + private static final String TABLE = "User"; + private static final Map> EAGERLY_INITIALIZED_MAP = + AbstractVerifier.ACCESS_FAKE_DELETE_MAP; + + private static JSONParser, ? extends List> previousJSONParser; + private Map> previousFakeDeleteMap; + + @BeforeClass + public static void installJSONParser() { + previousJSONParser = JSON.DEFAULT_JSON_PARSER; + JSON.DEFAULT_JSON_PARSER = new JSONParser, List>() { + @Override + public Map createJSONObject() { + return new LinkedHashMap<>(); + } + + @Override + public List createJSONArray() { + return new ArrayList<>(); + } + + @Override + public Object parse(Object json) { + return json; + } + + @Override + @SuppressWarnings("unchecked") + public Map parseObject(Object json) { + return (Map) json; + } + + @Override + public T parseObject(Object json, Class clazz) { + return clazz.cast(json); + } + + @Override + @SuppressWarnings("unchecked") + public List parseArray(Object json) { + return (List) json; + } + + @Override + @SuppressWarnings("unchecked") + public List parseArray(Object json, Class clazz) { + return (List) json; + } + + @Override + public String toJSONString(Object obj, boolean format) { + return String.valueOf(obj); + } + }; + } + + @AfterClass + public static void restoreJSONParser() { + JSON.DEFAULT_JSON_PARSER = previousJSONParser; + } + + @Before + public void resetFakeDeleteMap() { + previousFakeDeleteMap = AbstractVerifier.ACCESS_FAKE_DELETE_MAP; + AbstractVerifier.ACCESS_FAKE_DELETE_MAP = new LinkedHashMap<>(); + } + + @After + public void restoreFakeDeleteMap() { + AbstractVerifier.ACCESS_FAKE_DELETE_MAP = previousFakeDeleteMap; + } + + @Test + public void eagerlyInitializesFakeDeleteMap() { + assertNotNull(EAGERLY_INITIALIZED_MAP); + } + + @Test + public void keepsGetWhenTableHasNoFakeDeleteConfig() throws Exception { + SQLConfig, List> config = newConfig(RequestMethod.GET, true); + + assertEquals(RequestMethod.GET, config.getMethod()); + } + + @Test + public void rejectsDeleteWhenTableConfigIsMissing() throws Exception { + assertInvalidConfig(null); + } + + @Test + public void rejectsDeleteWhenTableConfigIsEmpty() throws Exception { + assertInvalidConfig(new LinkedHashMap()); + } + + @Test + public void rejectsDeleteWhenDeletedKeyIsBlank() throws Exception { + Map tableConfig = new LinkedHashMap<>(); + tableConfig.put(AbstractSQLConfig.KEY_DELETED_KEY, " "); + + assertInvalidConfig(tableConfig); + } + + @Test + public void rejectsDeleteWhenDeletedKeyIsNotAString() throws Exception { + Map tableConfig = new LinkedHashMap<>(); + tableConfig.put(AbstractSQLConfig.KEY_DELETED_KEY, 123); + + assertInvalidConfig(tableConfig); + } + + @Test + public void rewritesConfiguredDeleteAsPut() throws Exception { + Map tableConfig = new LinkedHashMap<>(); + tableConfig.put(AbstractSQLConfig.KEY_DELETED_KEY, "deletedFlag"); + tableConfig.put(AbstractSQLConfig.KEY_DELETED_VALUE, 1); + AbstractVerifier.ACCESS_FAKE_DELETE_MAP.put(TABLE, tableConfig); + + SQLConfig, List> config = newConfig(RequestMethod.DELETE, true); + + assertEquals(RequestMethod.PUT, config.getMethod()); + assertNotNull(config.getContent()); + assertEquals(Integer.valueOf(1), config.getContent().get("deletedFlag")); + } + + @Test + public void keepsDeleteWhenFakeDeleteIsDisabled() throws Exception { + SQLConfig, List> config = newConfig(RequestMethod.DELETE, false); + + assertEquals(RequestMethod.DELETE, config.getMethod()); + } + + private static void assertInvalidConfig(Map tableConfig) throws Exception { + if (tableConfig != null) { + AbstractVerifier.ACCESS_FAKE_DELETE_MAP.put(TABLE, tableConfig); + } + + try { + SQLConfig, List> config = + newConfig(RequestMethod.DELETE, true); + fail("Invalid fake-delete config must throw instead of returning " + config.getMethod()); + } + catch (IllegalArgumentException e) { + assertNotNull(e.getMessage()); + assertTrue(e.getMessage().contains(TABLE)); + assertTrue(e.getMessage().contains(AbstractSQLConfig.KEY_DELETED_KEY)); + assertTrue(e.getMessage().contains("String")); + } + } + + private static SQLConfig, List> newConfig( + RequestMethod method, final boolean fakeDelete) throws Exception { + Map request = new LinkedHashMap<>(); + request.put("id", 1L); + + AbstractSQLConfig.Callback, List> callback = + new AbstractSQLConfig.SimpleCallback, List>() { + @Override + public SQLConfig, List> getSQLConfig(RequestMethod method, + String database, String datasource, String namespace, String catalog, String schema, + String table) { + return new AbstractSQLConfig, List>(method, table) { + @Override + public boolean isFakeDelete() { + return fakeDelete; + } + + @Override + public String gainDBVersion() { + return "8.0.0"; + } + + @Override + public String gainDBUri() { + return "jdbc:mysql://localhost/test"; + } + + @Override + public String gainDBAccount() { + return "test"; + } + + @Override + public String gainDBPassword() { + return "test"; + } + }; + } + }; + + return AbstractSQLConfig.newSQLConfig(method, TABLE, null, request, null, false, callback); + } +} diff --git a/APIJSONORM/src/test/java/apijson/orm/KingbaseCompatibilityTest.java b/APIJSONORM/src/test/java/apijson/orm/KingbaseCompatibilityTest.java new file mode 100644 index 00000000..308e6162 --- /dev/null +++ b/APIJSONORM/src/test/java/apijson/orm/KingbaseCompatibilityTest.java @@ -0,0 +1,616 @@ +package apijson.orm; + +import apijson.RequestMethod; +import apijson.JSON; +import apijson.JSONParser; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.math.BigDecimal; +import java.lang.reflect.Proxy; +import java.sql.*; +import java.time.LocalDateTime; +import java.time.Year; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import javax.sql.rowset.serial.SerialBlob; +import javax.sql.rowset.serial.SerialClob; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class KingbaseCompatibilityTest { + @BeforeClass + public static void installJsonParser() { + JSON.DEFAULT_JSON_PARSER = new JSONParser, List>() { + @Override + public Map createJSONObject() { + return new LinkedHashMap<>(); + } + + @Override + public List createJSONArray() { + return new ArrayList<>(); + } + + @Override + public Object parse(Object json) { + if ("{\"enabled\":true}".equals(json)) { + Map result = createJSONObject(); + result.put("enabled", true); + return result; + } + return json; + } + + @Override + public Map parseObject(Object json) { + return (Map) parse(json); + } + + @Override + public T parseObject(Object json, Class clazz) { + return clazz.cast(parse(json)); + } + + @Override + public List parseArray(Object json) { + return (List) parse(json); + } + + @Override + public List parseArray(Object json, Class clazz) { + return (List) parse(json); + } + + @Override + public String toJSONString(Object obj, boolean format) { + return String.valueOf(obj); + } + }; + } + + private AbstractSQLConfig, List> config(String database) { + AbstractSQLConfig, List> config = + new AbstractSQLConfig, List>(RequestMethod.GET, "User") { + @Override + public String gainDBVersion() { + return "9.0.0"; + } + + @Override + public String gainDBUri() { + return "jdbc:kingbase8://localhost/test"; + } + + @Override + public String gainDBAccount() { + return "test"; + } + + @Override + public String gainDBPassword() { + return "test"; + } + }; + config.setDatabase(database); + return config; + } + + @Test + public void recognizesAllCompatibilityModes() { + AbstractSQLConfig, List> mysql = config(SQLConfig.DATABASE_KINGBASE_MYSQL); + AbstractSQLConfig, List> oracle = config(SQLConfig.DATABASE_KINGBASE_ORACLE); + AbstractSQLConfig, List> sqlServer = config(SQLConfig.DATABASE_KINGBASE_SQLSERVER); + + assertTrue(mysql.isKingBase() && mysql.isKingBaseMySQL() && mysql.isMSQL()); + assertTrue(oracle.isKingBase() && oracle.isKingBaseOracle() && oracle.isTSQL()); + assertTrue(sqlServer.isKingBase() && sqlServer.isKingBaseSQLServer() && sqlServer.isTSQL()); + assertEquals("`", mysql.getQuote()); + assertEquals(" ", oracle.gainAs()); + assertEquals("\"", sqlServer.getQuote()); + assertEquals(KingbaseSQLDialect.MYSQL, KingbaseSQLDialect.from(SQLConfig.DATABASE_KINGBASE_MYSQL)); + assertTrue(KingbaseSQLDialect.MYSQL.supportsDmlLimit()); + assertEquals(KingbaseSQLDialect.ORACLE, KingbaseSQLDialect.from(SQLConfig.DATABASE_KINGBASE_ORACLE)); + assertTrue(KingbaseSQLDialect.ORACLE.isOracle()); + assertFalse(KingbaseSQLDialect.ORACLE.supportsDmlLimit()); + } + + @Test + public void generatesKingbaseMySQLCrudDialect() throws Exception { + AbstractSQLConfig, List> select = config(SQLConfig.DATABASE_KINGBASE_MYSQL); + select.setColumn(Arrays.asList("id", "name")); + select.putWhere("id", 7L, false); + select.setCount(10).setPage(2); + String selectSql = select.gainSQL(true); + assertTrue(selectSql, selectSql.startsWith("SELECT `id`,`name` FROM `sys`.`User`")); + assertTrue(selectSql, selectSql.endsWith(" LIMIT 10 OFFSET 20")); + assertEquals(Arrays.asList(7L), select.getPreparedValueList()); + + AbstractSQLConfig, List> insert = config(SQLConfig.DATABASE_KINGBASE_MYSQL); + insert.setMethod(RequestMethod.POST).setColumn(Arrays.asList("id", "name")); + insert.setValues(Arrays.asList(Arrays.asList(7L, "Kingbase"))); + String insertSql = insert.gainSQL(true); + assertTrue(insertSql, insertSql.startsWith("INSERT INTO `sys`.`User`(`id`,`name`) VALUES")); + assertEquals(Arrays.asList(7L, "Kingbase"), insert.getPreparedValueList()); + + AbstractSQLConfig, List> update = config(SQLConfig.DATABASE_KINGBASE_MYSQL); + Map content = new LinkedHashMap<>(); + content.put("name", "KES"); + update.setMethod(RequestMethod.PUT).setContent(content).putWhere("id", 7L, false); + update.setCount(1); + String updateSql = update.gainSQL(true); + assertTrue(updateSql, updateSql.startsWith("UPDATE `sys`.`User` SET `name` = ? WHERE")); + assertTrue(updateSql, updateSql.endsWith(" LIMIT 1")); + + AbstractSQLConfig, List> delete = config(SQLConfig.DATABASE_KINGBASE_MYSQL); + delete.setMethod(RequestMethod.DELETE).putWhere("id", 7L, false); + delete.setCount(1); + String deleteSql = delete.gainSQL(true); + assertTrue(deleteSql, deleteSql.startsWith("DELETE FROM `sys`.`User` WHERE")); + assertTrue(deleteSql, deleteSql.endsWith(" LIMIT 1")); + } + + @Test + public void generatesModeSpecificRegularExpressions() { + String mysql = config(SQLConfig.DATABASE_KINGBASE_MYSQL).gainRegExpString("name", "name", "A.*", true); + String oracle = config(SQLConfig.DATABASE_KINGBASE_ORACLE).gainRegExpString("name", "name", "A.*", true); + String sqlServer = config(SQLConfig.DATABASE_KINGBASE_SQLSERVER).gainRegExpString("name", "name", "A.*", true); + + assertTrue(mysql.startsWith("regexp_like(")); + assertTrue(oracle.startsWith("regexp_like(")); + assertTrue(sqlServer.contains(" ~* ")); + } + + @Test + public void generatesKingbaseOracleCrudDialect() throws Exception { + AbstractSQLConfig, List> select = config(SQLConfig.DATABASE_KINGBASE_ORACLE); + select.setAlias("u").setColumn(Arrays.asList("id", "name")); + select.putWhere("id", 7L, false).setCount(10).setPage(2); + String selectSql = select.gainSQL(true); + assertTrue(selectSql, selectSql.startsWith("SELECT * FROM (SELECT \"User__u\".*, ROWNUM \"RN\" FROM (SELECT")); + assertTrue(selectSql, selectSql.contains("FROM \"sys\".\"User\" WHERE")); + assertTrue(selectSql, selectSql.endsWith("WHERE \"RN\" > 20")); + assertFalse(selectSql, selectSql.contains(" AS \"u\"")); + assertEquals(Arrays.asList(7L), select.getPreparedValueList()); + + AbstractSQLConfig, List> insert = config(SQLConfig.DATABASE_KINGBASE_ORACLE); + insert.setMethod(RequestMethod.POST).setColumn(Arrays.asList("id", "name")); + insert.setValues(Arrays.asList(Arrays.asList(7L, "Kingbase"))); + String insertSql = insert.gainSQL(true); + assertTrue(insertSql, insertSql.startsWith("INSERT INTO \"sys\".\"User\"(\"id\",\"name\") VALUES")); + assertEquals(Arrays.asList(7L, "Kingbase"), insert.getPreparedValueList()); + + AbstractSQLConfig, List> update = config(SQLConfig.DATABASE_KINGBASE_ORACLE); + Map content = new LinkedHashMap<>(); + content.put("name", "KES"); + update.setMethod(RequestMethod.PUT).setContent(content).putWhere("id", 7L, false).setCount(1); + String updateSql = update.gainSQL(true); + assertTrue(updateSql, updateSql.startsWith("UPDATE \"sys\".\"User\" SET \"name\" = ? WHERE")); + assertFalse(updateSql, updateSql.contains(" LIMIT ")); + + AbstractSQLConfig, List> delete = config(SQLConfig.DATABASE_KINGBASE_ORACLE); + delete.setMethod(RequestMethod.DELETE).putWhere("id", 7L, false).setCount(1); + String deleteSql = delete.gainSQL(true); + assertTrue(deleteSql, deleteSql.startsWith("DELETE FROM \"sys\".\"User\" WHERE")); + assertFalse(deleteSql, deleteSql.contains(" LIMIT ")); + + AbstractSQLConfig, List> explain = config(SQLConfig.DATABASE_KINGBASE_ORACLE); + explain.setExplain(true); + assertTrue(explain.gainSQL(true).startsWith("EXPLAIN PLAN FOR SELECT")); + } + + @Test + public void generatesKingbaseSQLServerCrudDialect() throws Exception { + AbstractSQLConfig, List> select = config(SQLConfig.DATABASE_KINGBASE_SQLSERVER); + select.setAlias("u").setColumn(Arrays.asList("id", "name")); + select.putWhere("id", 7L, false).setCount(10).setPage(2); + String selectSql = select.gainSQL(true); + assertTrue(selectSql, selectSql.startsWith("SELECT \"id\",\"name\" FROM \"sys\".\"User\"")); + assertTrue(selectSql, selectSql.contains(" ORDER BY \"id\"")); + assertTrue(selectSql, selectSql.endsWith(" OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY")); + assertEquals(Arrays.asList(7L), select.getPreparedValueList()); + + AbstractSQLConfig, List> insert = config(SQLConfig.DATABASE_KINGBASE_SQLSERVER); + insert.setMethod(RequestMethod.POST).setColumn(Arrays.asList("id", "name")); + insert.setValues(Arrays.asList(Arrays.asList(7L, "Kingbase"))); + String insertSql = insert.gainSQL(true); + assertTrue(insertSql, insertSql.startsWith("INSERT INTO \"sys\".\"User\"(\"id\",\"name\") VALUES")); + assertFalse(insertSql, insertSql.contains(" RETURNING ")); + assertEquals(Arrays.asList(7L, "Kingbase"), insert.getPreparedValueList()); + + AbstractSQLConfig, List> update = config(SQLConfig.DATABASE_KINGBASE_SQLSERVER); + Map content = new LinkedHashMap<>(); + content.put("name", "KES"); + update.setMethod(RequestMethod.PUT).setContent(content).putWhere("id", 7L, false).setCount(1); + String updateSql = update.gainSQL(true); + assertTrue(updateSql, updateSql.startsWith("UPDATE \"sys\".\"User\" SET \"name\" = ? WHERE")); + assertFalse(updateSql, updateSql.contains(" LIMIT ")); + + AbstractSQLConfig, List> delete = config(SQLConfig.DATABASE_KINGBASE_SQLSERVER); + delete.setMethod(RequestMethod.DELETE).putWhere("id", 7L, false).setCount(1); + String deleteSql = delete.gainSQL(true); + assertTrue(deleteSql, deleteSql.startsWith("DELETE FROM \"sys\".\"User\" WHERE")); + assertFalse(deleteSql, deleteSql.contains(" LIMIT ")); + + AbstractSQLConfig, List> explain = config(SQLConfig.DATABASE_KINGBASE_SQLSERVER); + explain.setExplain(true); + assertTrue(explain.gainSQL(true).startsWith("EXPLAIN SELECT")); + } + + @Test + public void generatesModeSpecificJsonContainment() { + for (String database : new String[] { + SQLConfig.DATABASE_KINGBASE_MYSQL, + SQLConfig.DATABASE_KINGBASE_SQLSERVER + }) { + String sql = config(database).gainContainString("get<>", "get", new Object[] {"UNKNOWN"}, Logic.TYPE_OR); + assertTrue(sql, sql.contains("::jsonb @>")); + assertTrue(sql, !sql.contains("json_contains(")); + assertTrue(sql, !sql.contains("json_textcontains(")); + } + + String oracleSql = config(SQLConfig.DATABASE_KINGBASE_ORACLE) + .gainContainString("get<>", "get", new Object[] {"UNKNOWN"}, Logic.TYPE_OR); + assertTrue(oracleSql, oracleSql.contains("json_textcontains(")); + assertFalse(oracleSql, oracleSql.contains("::jsonb")); + } + + @Test + public void mapsJdbcValuesToJsonSafeValues() throws Exception { + AbstractSQLExecutor, List> executor = + new AbstractSQLExecutor, List>() { }; + SQLConfig, List> config = config(SQLConfig.DATABASE_KINGBASE_MYSQL); + + Object json = executor.mapResultValue(config, "{\"enabled\":true}", Types.OTHER, "jsonb", "settings"); + Object array = executor.mapResultValue(config, new Object[] {1, "two"}, Types.ARRAY, "varchar[]", "tags"); + + assertTrue(json instanceof Map); + assertEquals("true", String.valueOf(((Map) json).get("enabled"))); + assertEquals("1234567890.123456789", executor.mapResultValue(config, + new BigDecimal("1234567890.123456789"), Types.NUMERIC, "numeric", "amount")); + assertTrue(array instanceof List); + assertEquals(2, ((List) array).size()); + assertEquals(Base64.getEncoder().encodeToString("binary".getBytes("UTF-8")), + executor.mapResultValue(config, "binary".getBytes("UTF-8"), Types.VARBINARY, "varbinary", "payload")); + assertEquals(Base64.getEncoder().encodeToString("blob".getBytes("UTF-8")), + executor.mapResultValue(config, new SerialBlob("blob".getBytes("UTF-8")), Types.BLOB, "blob", "payload")); + assertEquals("ordinary text", executor.mapResultValue(config, + new SerialClob("ordinary text".toCharArray()), Types.CLOB, "longtext", "description")); + assertEquals("2026-07-19T10:20", executor.mapResultValue(config, + LocalDateTime.of(2026, 7, 19, 10, 20), Types.TIMESTAMP, "datetime", "createdAt")); + assertEquals(2026, executor.mapResultValue(config, Year.of(2026), Types.SMALLINT, "year", "year")); + assertEquals("not json", executor.mapResultValue(config, "not json", Types.OTHER, + "business_json_status", "status")); + assertEquals(true, executor.mapResultValue(config, "1", Types.BOOLEAN, "bool", "enabled")); + assertEquals("NaN", executor.mapResultValue(config, Double.NaN, Types.DOUBLE, "double", "score")); + } + + @Test + public void mapsKingbaseOracleValuesToJsonSafeValues() throws Exception { + AbstractSQLExecutor, List> executor = + new AbstractSQLExecutor, List>() { }; + SQLConfig, List> config = config(SQLConfig.DATABASE_KINGBASE_ORACLE); + + Object json = executor.mapResultValue(config, "{\"enabled\":true}", Types.OTHER, "JSON", "settings"); + assertTrue(json instanceof Map); + assertEquals("1234567890.123456789", executor.mapResultValue(config, + new BigDecimal("1234567890.123456789"), Types.NUMERIC, "NUMBER", "amount")); + assertEquals(Base64.getEncoder().encodeToString("raw".getBytes("UTF-8")), + executor.mapResultValue(config, "raw".getBytes("UTF-8"), Types.VARBINARY, "RAW", "payload")); + assertEquals(Base64.getEncoder().encodeToString("blob".getBytes("UTF-8")), + executor.mapResultValue(config, new SerialBlob("blob".getBytes("UTF-8")), Types.BLOB, "BLOB", "payload")); + assertEquals("ordinary text", executor.mapResultValue(config, + new SerialClob("ordinary text".toCharArray()), Types.CLOB, "CLOB", "description")); + assertEquals("2026-07-19 10:20:00.0", executor.mapResultValue(config, + Timestamp.valueOf("2026-07-19 10:20:00"), Types.TIMESTAMP, "TIMESTAMP", "createdAt")); + assertEquals(true, executor.mapResultValue(config, "1", Types.BOOLEAN, "BOOLEAN", "enabled")); + } + + @Test + public void mapsKingbaseSQLServerValuesToJsonSafeValues() throws Exception { + AbstractSQLExecutor, List> executor = + new AbstractSQLExecutor, List>() { }; + SQLConfig, List> config = config(SQLConfig.DATABASE_KINGBASE_SQLSERVER); + + Object json = executor.mapResultValue(config, "{\"enabled\":true}", Types.OTHER, "JSONB", "settings"); + assertTrue(json instanceof Map); + assertEquals("1234567890.123456789", executor.mapResultValue(config, + new BigDecimal("1234567890.123456789"), Types.DECIMAL, "MONEY", "amount")); + assertEquals(Short.valueOf((short) 255), executor.mapResultValue(config, + Short.valueOf((short) 255), Types.TINYINT, "TINYINT", "level")); + assertEquals("9007199254740992", executor.mapResultValue(config, + 9007199254740992L, Types.BIGINT, "BIGINT", "externalId")); + assertEquals(Base64.getEncoder().encodeToString("row-version".getBytes("UTF-8")), + executor.mapResultValue(config, "row-version".getBytes("UTF-8"), Types.BINARY, "ROWVERSION", "version")); + assertEquals(Base64.getEncoder().encodeToString("image".getBytes("UTF-8")), + executor.mapResultValue(config, "image".getBytes("UTF-8"), Types.LONGVARBINARY, "IMAGE", "picture")); + assertEquals("ordinary text", executor.mapResultValue(config, + new SerialClob("ordinary text".toCharArray()), Types.CLOB, "NTEXT", "description")); + assertEquals("line 1\nline 2", executor.mapResultValue(config, + new SerialClob("line 1\nline 2".toCharArray()), Types.CLOB, "NTEXT", "multiline")); + assertEquals("2026-07-19 10:20:00.0", executor.mapResultValue(config, + Timestamp.valueOf("2026-07-19 10:20:00"), Types.TIMESTAMP, "DATETIME2", "createdAt")); + assertEquals(true, executor.mapResultValue(config, "1", Types.BIT, "BIT", "enabled")); + assertEquals("NaN", executor.mapResultValue(config, Double.NaN, Types.DOUBLE, "FLOAT", "score")); + assertEquals("123e4567-e89b-12d3-a456-426614174000", executor.mapResultValue(config, + java.util.UUID.fromString("123e4567-e89b-12d3-a456-426614174000"), Types.OTHER, + "UNIQUEIDENTIFIER", "requestId")); + } + + @Test + public void bindsKingbaseMySQLValuesUsingParameterMetadata() throws Exception { + AbstractSQLExecutor, List> executor = + new AbstractSQLExecutor, List>() { }; + SQLConfig, List> config = config(SQLConfig.DATABASE_KINGBASE_MYSQL); + AtomicReference call = new AtomicReference<>(); + AtomicReference arguments = new AtomicReference<>(); + + ParameterMetaData metadata = (ParameterMetaData) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {ParameterMetaData.class}, (proxy, method, args) -> { + if ("getParameterType".equals(method.getName())) return Types.OTHER; + if ("getParameterTypeName".equals(method.getName())) return "jsonb"; + return primitiveDefault(method.getReturnType()); + }); + PreparedStatement statement = (PreparedStatement) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {PreparedStatement.class}, (proxy, method, args) -> { + if ("getParameterMetaData".equals(method.getName())) return metadata; + if (method.getName().startsWith("set")) { + call.set(method.getName()); + arguments.set(args); + } + return primitiveDefault(method.getReturnType()); + }); + + executor.setArgument(config, statement, 0, new LinkedHashMap()); + assertEquals("setObject", call.get()); + assertEquals(Types.OTHER, arguments.get()[2]); + + executor.setArgument(config, statement, 0, new byte[] {1, 2}); + assertEquals("setBytes", call.get()); + } + + @Test + public void bindsKingbaseOracleValuesUsingParameterMetadata() throws Exception { + AbstractSQLExecutor, List> executor = + new AbstractSQLExecutor, List>() { }; + SQLConfig, List> config = config(SQLConfig.DATABASE_KINGBASE_ORACLE); + AtomicReference call = new AtomicReference<>(); + AtomicReference arguments = new AtomicReference<>(); + AtomicInteger jdbcType = new AtomicInteger(Types.OTHER); + AtomicReference typeName = new AtomicReference<>("JSON"); + + ParameterMetaData metadata = (ParameterMetaData) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {ParameterMetaData.class}, (proxy, method, args) -> { + if ("getParameterType".equals(method.getName())) return jdbcType.get(); + if ("getParameterTypeName".equals(method.getName())) return typeName.get(); + return primitiveDefault(method.getReturnType()); + }); + PreparedStatement statement = (PreparedStatement) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {PreparedStatement.class}, (proxy, method, args) -> { + if ("getParameterMetaData".equals(method.getName())) return metadata; + if (method.getName().startsWith("set")) { + call.set(method.getName()); + arguments.set(args); + } + return primitiveDefault(method.getReturnType()); + }); + + executor.setArgument(config, statement, 0, new LinkedHashMap()); + assertEquals("setObject", call.get()); + assertEquals(Types.OTHER, arguments.get()[2]); + + executor.setArgument(config, statement, 0, new byte[] {1, 2}); + assertEquals("setBytes", call.get()); + + jdbcType.set(Types.NUMERIC); + typeName.set("NUMBER"); + executor.setArgument(config, statement, 0, null); + assertEquals("setNull", call.get()); + assertEquals(Types.NUMERIC, arguments.get()[1]); + + executor.setArgument(config, statement, 0, new java.util.Date(0)); + assertEquals("setTimestamp", call.get()); + } + + @Test + public void bindsKingbaseSQLServerValuesUsingParameterMetadata() throws Exception { + AbstractSQLExecutor, List> executor = + new AbstractSQLExecutor, List>() { }; + SQLConfig, List> config = config(SQLConfig.DATABASE_KINGBASE_SQLSERVER); + AtomicReference call = new AtomicReference<>(); + AtomicReference arguments = new AtomicReference<>(); + AtomicInteger jdbcType = new AtomicInteger(Types.OTHER); + AtomicReference typeName = new AtomicReference<>("JSONB"); + + ParameterMetaData metadata = (ParameterMetaData) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {ParameterMetaData.class}, (proxy, method, args) -> { + if ("getParameterType".equals(method.getName())) return jdbcType.get(); + if ("getParameterTypeName".equals(method.getName())) return typeName.get(); + return primitiveDefault(method.getReturnType()); + }); + PreparedStatement statement = (PreparedStatement) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {PreparedStatement.class}, (proxy, method, args) -> { + if ("getParameterMetaData".equals(method.getName())) return metadata; + if (method.getName().startsWith("set")) { + call.set(method.getName()); + arguments.set(args); + } + return primitiveDefault(method.getReturnType()); + }); + + executor.setArgument(config, statement, 0, new LinkedHashMap()); + assertEquals("setObject", call.get()); + assertEquals(Types.OTHER, arguments.get()[2]); + + executor.setArgument(config, statement, 0, new byte[] {1, 2}); + assertEquals("setBytes", call.get()); + + jdbcType.set(Types.NVARCHAR); + typeName.set("NVARCHAR"); + executor.setArgument(config, statement, 0, null); + assertEquals("setNull", call.get()); + assertEquals(Types.NVARCHAR, arguments.get()[1]); + + executor.setArgument(config, statement, 0, new java.util.Date(0)); + assertEquals("setTimestamp", call.get()); + + executor.setArgument(config, statement, 0, Arrays.asList("中文", "text")); + assertEquals("setNString", call.get()); + + executor.setArgument(config, statement, 0, java.util.UUID.fromString("123e4567-e89b-12d3-a456-426614174000")); + assertEquals("setObject", call.get()); + } + + @Test + public void readsKingbaseSQLServerGeneratedKey() throws Exception { + ResultSet generatedKeys = (ResultSet) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {ResultSet.class}, (proxy, method, args) -> { + if ("next".equals(method.getName())) return true; + if ("getLong".equals(method.getName())) return 42L; + return primitiveDefault(method.getReturnType()); + }); + PreparedStatement statement = (PreparedStatement) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {PreparedStatement.class}, (proxy, method, args) -> { + if ("execute".equals(method.getName())) return true; + if ("getUpdateCount".equals(method.getName())) return -1; + if ("getResultSet".equals(method.getName())) return generatedKeys; + return primitiveDefault(method.getReturnType()); + }); + Connection mockConnection = (Connection) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {Connection.class}, (proxy, method, args) -> { + if ("prepareStatement".equals(method.getName())) return statement; + return primitiveDefault(method.getReturnType()); + }); + AbstractSQLExecutor, List> executor = + new AbstractSQLExecutor, List>() { + @Override + public Connection getConnection(SQLConfig, List> config) { + return mockConnection; + } + }; + AbstractSQLConfig, List> insert = config(SQLConfig.DATABASE_KINGBASE_SQLSERVER); + insert.setMethod(RequestMethod.POST).setPrepared(false).setId(null); + insert.setColumn(Arrays.asList("name")).setValues(Arrays.asList(Arrays.asList("Kingbase"))); + + assertEquals(1, executor.executeUpdate(insert, null)); + assertEquals(Long.valueOf(42L), insert.getId()); + } + + @Test + public void requestsKingbaseGeneratedKeyByColumnName() throws Exception { + AtomicReference prepareArguments = new AtomicReference<>(); + PreparedStatement expected = (PreparedStatement) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {PreparedStatement.class}, (proxy, method, args) -> primitiveDefault(method.getReturnType())); + Connection mockConnection = (Connection) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {Connection.class}, (proxy, method, args) -> { + if ("prepareStatement".equals(method.getName())) { + prepareArguments.set(args); + return expected; + } + return primitiveDefault(method.getReturnType()); + }); + AbstractSQLExecutor, List> executor = + new AbstractSQLExecutor, List>() { + @Override + public Connection getConnection(SQLConfig, List> config) { + return mockConnection; + } + }; + for (String database : Arrays.asList(SQLConfig.DATABASE_KINGBASE_MYSQL, + SQLConfig.DATABASE_KINGBASE_ORACLE, SQLConfig.DATABASE_KINGBASE_SQLSERVER)) { + AbstractSQLConfig, List> insert = config(database); + insert.setMethod(RequestMethod.POST).setId(null); + + assertTrue(expected == executor.getStatement(insert, "INSERT INTO User(name) VALUES(?)")); + assertFalse(String.valueOf(prepareArguments.get()[0]).contains(" RETURNING ")); + assertTrue(prepareArguments.get()[1] instanceof String[]); + assertEquals("id", ((String[]) prepareArguments.get()[1])[0]); + } + } + + @Test + public void fallsBackToForwardOnlyKingbaseCursor() throws Exception { + AtomicInteger calls = new AtomicInteger(); + PreparedStatement expected = (PreparedStatement) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {PreparedStatement.class}, (proxy, method, args) -> primitiveDefault(method.getReturnType())); + Connection mockConnection = (Connection) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {Connection.class}, (proxy, method, args) -> { + if ("prepareStatement".equals(method.getName()) && args != null && args.length == 3) { + if (calls.getAndIncrement() == 0) throw new SQLFeatureNotSupportedException("scroll cursor"); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, args[1]); + assertEquals(ResultSet.CONCUR_READ_ONLY, args[2]); + return expected; + } + return primitiveDefault(method.getReturnType()); + }); + AbstractSQLExecutor, List> executor = + new AbstractSQLExecutor, List>() { + @Override + public Connection getConnection(SQLConfig, List> config) { + return mockConnection; + } + }; + + assertTrue(expected == executor.getStatement(config(SQLConfig.DATABASE_KINGBASE_ORACLE), "SELECT 1")); + assertEquals(2, calls.get()); + } + + private static Object primitiveDefault(Class type) { + if (!type.isPrimitive()) return null; + if (type == boolean.class) return false; + if (type == byte.class) return (byte) 0; + if (type == short.class) return (short) 0; + if (type == int.class) return 0; + if (type == long.class) return 0L; + if (type == float.class) return 0F; + if (type == double.class) return 0D; + if (type == char.class) return '\0'; + return null; + } + + @Test + public void removesOnlyApiGeneratedIdForIdentityColumn() throws Exception { + AbstractSQLExecutor, List> executor = + new AbstractSQLExecutor, List>() { + @Override + protected boolean isAutoGeneratedIdColumn(SQLConfig, List> config, + Connection conn) { + return true; + } + }; + AbstractSQLConfig, List> config = config(SQLConfig.DATABASE_KINGBASE_SQLSERVER); + config.setMethod(RequestMethod.POST).setId(123L).setIdGeneratedByAPIJSON(true); + config.setColumn(new ArrayList<>(Arrays.asList("id", "name"))); + config.setValues(new ArrayList<>(Arrays.asList(new ArrayList(Arrays.asList(123L, "test"))))); + + assertTrue(executor.prepareDatabaseGeneratedId(config, null)); + assertEquals(Arrays.asList("name"), config.getColumn()); + assertEquals(Arrays.asList("test"), config.getValues().get(0)); + assertEquals(null, config.getId()); + } + + @Test + public void hidesOnlyFinalOraclePaginationRowNumber() throws Exception { + AbstractSQLExecutor, List> executor = + new AbstractSQLExecutor, List>() { }; + SQLConfig, List> config = config(SQLConfig.DATABASE_KINGBASE_ORACLE).setCount(10); + ResultSetMetaData metadata = (ResultSetMetaData) Proxy.newProxyInstance( + getClass().getClassLoader(), new Class[] {ResultSetMetaData.class}, (proxy, method, args) -> { + if ("getColumnCount".equals(method.getName())) return 3; + if ("getColumnName".equals(method.getName())) return ((Integer) args[0]) == 3 ? "rn" : "name"; + return null; + }); + + assertFalse(executor.isHideColumn(config, (ResultSet) null, metadata, 0, new LinkedHashMap<>(), 2, null, null)); + assertTrue(executor.isHideColumn(config, (ResultSet) null, metadata, 0, new LinkedHashMap<>(), 3, null, null)); + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f9fab294..be3ffc4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ - [qiujunlin](https://github.com/qiujunlin)(字节跳动工程师,还开源了 APIJSONDemo) - [HANXU2018](https://github.com/HANXU2018)(网易工程师,还开源了 APIJSON-DOC) - [hclown9804](https://github.com/hclown9804)(Datawhale) -- [chenyanlann](https://github.com/chenyanlann)(还开源了 APIJSONDemo_ClickHouse) +- [chenyanlann](https://github.com/chenyanlann)(还开源了 APIJSONDemo_ClickHouse, APIJSONBoot_Hive) - [haolingzhang1](https://github.com/haolingzhang1)(腾讯工程师,还开源了 APIJson--demo) - [jerrylususu](https://github.com/jerrylususu)(还开源了 apijson_todo_demo 和 apijson_role_extend) - [Dalezee](https://github.com/Dalezee)(还开源了 apijson_camp) @@ -75,6 +75,16 @@ - [GeXin97](https://github.com/GeXin97) - [yunjiao-source](https://github.com/yunjiao-source)(还开源了 [apijson-spring-boot](https://gitee.com/yunjiao-source/apijson-spring-boot)) - [moxixi527](https://github.com/moxixi527)(热门技术博主) +- [wu-sv](https://github.com/wu-sv) +- [Qiaochu Hu](https://github.com/hobostay?tab=overview&from=2026-06-01&to=2026-06-30) +- [quan](https://github.com/yuanq20)(腾讯工程师) +- [Nguyen Van Nam](https://github.com/Nam0101)(HUST) +- [Zhengcy05](https://github.com/Zhengcy05) +- [014-code](https://github.com/014-code)(东信时代、用友、福州市榕智 工程师) +- [Ziran Fu](https://github.com/fuziran) +- [xiaoaozz](https://github.com/xiaoaozz) +- [Henrique](https://github.com/henriquejsza) + #### 其中特别致谢:
cloudAndMonkey 提交的 11 个 Commits, 对 APIJSON 做出了 1,496 增加和 845 处删减(截止 2022/12/15 日);
@@ -113,7 +123,7 @@ APIJSON 作为腾讯开源的知名热门项目,贡献代码除了可以给简 ### 如果有比较大的改动 -以下是具体步骤:(如果使用本步骤,GitHub 可能不会把贡献者添加到 Contributors 中,推荐用以下 [详细的图文步骤](https://github.com/Tencent/APIJSON/blob/master/CONTRIBUTING.md#%E8%AF%A6%E7%BB%86%E7%9A%84%E5%9B%BE%E6%96%87%E6%AD%A5%E9%AA%A4%E5%8F%AF%E5%8F%82%E8%80%83%E4%BB%A5%E4%B8%8B%E4%BB%BB%E6%84%8F%E4%B8%80%E7%AF%87)) +以下是具体步骤:(如果使用本步骤,GitHub 可能不会把贡献者添加到 Contributors 中,推荐用以下 [详细的图文步骤](/CONTRIBUTING.md#%E8%AF%A6%E7%BB%86%E7%9A%84%E5%9B%BE%E6%96%87%E6%AD%A5%E9%AA%A4%E5%8F%AF%E5%8F%82%E8%80%83%E4%BB%A5%E4%B8%8B%E4%BB%BB%E6%84%8F%E4%B8%80%E7%AF%87)) #### Fork 仓库 @@ -134,7 +144,7 @@ $ git clone git@github.com:/APIJSON.git ```bash $ git remote add # 例如: -$ git remote add APIJSON git@github.com:Tencent/APIJSON.git +$ git remote add APIJSON git@github.com:APIJSON/APIJSON.git ``` #### 保持与 APIJSON 仓库的同步 diff --git a/Document-Chinese.md b/Document-Chinese.md index ffe8e975..c88f0d98 100644 --- a/Document-Chinese.md +++ b/Document-Chinese.md @@ -1,11 +1,9 @@ -## 中文 | [English](https://github.com/Tencent/APIJSON/blob/master/Document.md) +## 中文 | [English](/Document.md) # APIJSON 通用文档 本文是通用文档,只和 APIJSON 协议有关,和 C#, Go, Java, JavaScript, PHP, Python, TypeScript 等开发语言无关。
-具体开发语言相关的 配置、运行、部署 等文档见各个相关项目的文档,可以在首页点击对应语言的入口来查看。
-https://github.com/Tencent/APIJSON -![image](https://user-images.githubusercontent.com/5738175/134520081-a63d3817-321c-4e7b-9e03-73c6827a19c1.png) - +具体开发语言相关的 配置、运行、部署 等文档见各个相关项目的文档,可以在[首页](/README.md)点击对应语言的入口来查看。
+![](https://github.com/user-attachments/assets/cc085b42-8678-4fa0-8ccc-b59f8322f28c) 后端开发者可以先看 [图文入门教程1](http://apijson.cn/doc/zh/) 或 [图文入门教程2](https://hanxu2018.github.io/APIJSON-DOC/) (和本文档有出入的点以本文档为准。例如正则匹配 key? 已废弃,用 key~ 替代;例如 "@column":"store_id,sum(amt):totAmt" 中逗号 , 有误,应该用分号 ; 隔开 SQL 函数,改为 "@column":"store_id;sum(amt):totAmt") diff --git a/Document.md b/Document.md index d3f18d4a..f0833f26 100644 --- a/Document.md +++ b/Document.md @@ -1,4 +1,4 @@ -## English | [中文](https://github.com/Tencent/APIJSON/blob/master/Document-Chinese.md) +## English | [中文](/Document-Chinese.md) #### A better online document is available at https://apijsondocs.readthedocs.io diff --git a/LICENSE b/LICENSE index 9743dd5a..b53efcfc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,14 +1,14 @@ -Tencent is pleased to support the open source community by making APIJSON available. +Copyright (C) 2026 the APIJSON group. All rights reserved. -Copyright (C) 2020 Tencent. All rights reserved. +## 💖 Acknowledgments + +This project has traveled a remarkable path of growth, shaped by individual passion and corporate recognition: +* **Corporate Sponsorship (2020–2026):** We extend our deepest gratitude to the project's former corporate sponsor and its open-source program office. During this period, the organization provided invaluable resources, internal and external advocacy, and official recognition—including a prestigious **Open Source Contribution Award** and grants awarded to the core maintainers. Their structured backing laid a rock-solid foundation for the project’s maturity. +* **The Open Source Community:** To all individual contributors, issue-reporters, and stagers—thank you for carrying the torch forward as we transition back to an independent, community-driven governance model. APIJSON is licensed under the Apache License Version 2.0. A copy of the Apache License Version 2.0 is included in this file. -The copyright notice pertaining to the Tencent code in this repo was previously in the name of “THL A29 Limited.” -That entity has now been de-registered. -You should treat all previously distributed copies of the code as if the copyright notice was in the name of “Tencent.” - Other dependencies and licenses: Open Source Software Licensed under the Apache License Version 2.0: diff --git a/README-Chinese.md b/README-Chinese.md index c13f82e9..35866784 100644 --- a/README-Chinese.md +++ b/README-Chinese.md @@ -1,7 +1,3 @@ -Tencent is pleased to support the open source community by making APIJSON available.
-Copyright (C) 2020 Tencent. All rights reserved.
-This source code is licensed under the Apache License Version 2.0
-

APIJSON

@@ -9,14 +5,14 @@ This source code is licensed under the Apache License Version 2.0

🏆 实时 零代码、全功能、强安全 ORM 库 🚀
后端接口和文档零代码,前端(客户端) 定制返回 JSON 的数据和结构

- English - 通用文档 + English + 通用文档 视频教程 测试用例 AI 问答 Skills - MCP - A2A + MCP + A2API

@@ -54,6 +50,11 @@ This source code is licensed under the Apache License Version 2.0
+ + + + +

@@ -63,7 +64,7 @@ This source code is licensed under the Apache License Version 2.0
- +

@@ -76,7 +77,7 @@ This source code is licensed under the Apache License Version 2.0

- +

@@ -191,15 +192,13 @@ https://www.bilibili.com/video/BV1yv411p7Y4
### 为什么选择 APIJSON? -前后端 关于接口的 开发、文档、联调 等 10 大痛点解析
-https://github.com/Tencent/APIJSON/wiki +前后端 关于接口的 开发、文档、联调 等 10 大痛点解析 [Wiki](https://github.com/APIJSON/APIJSON/wiki)
* **解决十大痛点** (可帮前后端开发大幅提振开发效率、强力杜绝联调扯皮、巧妙规避文档缺陷、非常节省流量带宽) * **开发提速很大** (CRUD 零代码热更新全自动,APIJSONBoot 对比 SSM、SSH 等保守估计可提速 20 倍以上) -* **腾讯官方开源** (使用 GitHub、Gitee、工蜂 等平台的官方账号开源,微信公众号、腾讯云+社区 等官方公告) * **社区影响力大** (GitHub 18K+ Star 在 400W Java 项目排名前 100,远超 FLAG, BAT 等国内外绝大部分开源项目) -* **各项荣誉成就** (腾讯内外 5 个奖项、腾讯开源前五、腾讯后端 Star 第一、Trending 日周月榜大满贯 等) -* **多样用户案例** (腾讯内有互娱、音乐、微信、云与智慧,外部有华为、华能、百度、快手、中兴、圆通、传音等) +* **各项荣誉成就** (腾讯内外 5 个奖项、QECon 大会和 Gitee Meetup 演讲、GitHub Trending 日周月榜大满贯 等) +* **多样用户案例** (腾讯内有互娱、音乐、微信、云与智慧等,外部有华为、华能、百度、快手、中兴、圆通、传音等) * **适用场景广泛** (社交聊天、阅读资讯、影音娱乐、办公学习 等各种 App、网站、小程序、公众号 等非金融类项目) * **周边生态丰富** (Android, iOS, Web 等各种 Demo、继承 JSON 的海量生态、零代码 接口测试 和 单元测试 工具等) * **文档视频齐全** (项目介绍、快速上手、安装部署 等后端、前端、客户端的 图文解说、视频教程、代码注释 等) @@ -209,7 +208,7 @@ https://github.com/Tencent/APIJSON/wiki * **高质可靠代码** (代码严谨规范,蚂蚁集团源伞 Pinpoint 代码扫描分析报告平均每行代码 Bug 率低至 0.15%) * **兼容各种项目** (协议不限 HTTP,与其它库无冲突,对各类 Web 框架集成友好且提供 SpringBoot, JFinal 的示例) * **工程轻量小巧** (无第三方依赖,Jar 仅 263KB,Java 文件仅 68 个共 14864 行代码,例如 APIJSONORM 8.1.0) -* **多年持续迭代** (自 2016 年起已连续维护 8 年多,70+ 贡献者、100+ 发版、3000+ 提交,不断更新迭代中...) +* **十年持续迭代** (自 2016 年起已连续维护 9 年多,80+ 贡献者、100+ 发版、3000+ 提交,不断更新迭代中...) **按照一般互联网中小型项目情况可得出以下对比表格:** @@ -223,7 +222,7 @@ https://github.com/Tencent/APIJSON/wiki ### 用户反馈 **腾讯 IEG 数据产品开发组负责人 xinlin:** -“腾讯的 APIJSON 开源方案,它可以做到零代码生成接口和文档,并且整个生成过程是自动化。当企业有元数据的时候,马上就可以获得接口” +“APIJSON 开源方案,它可以做到零代码生成接口和文档,并且整个生成过程是自动化。当企业有元数据的时候,马上就可以获得接口” **腾讯科技 后台开发高级工程师 雷大锤:** “可以抽出时间来看apijson了,这个可以为T10做准备,也是业界很火的东西,可以提升个人影响力!” @@ -243,31 +242,28 @@ https://github.com/Tencent/APIJSON/wiki **中兴 duyijiang:** “感谢腾讯大大提供的框架,很好用” -https://github.com/Tencent/APIJSON/issues/132#issuecomment-1106669540 +https://github.com/APIJSON/APIJSON/issues/132#issuecomment-1106669540
### 常见问题 #### 1.如何定制业务逻辑? -在后端编写 远程函数,可以拿到 session、version、当前 JSON 对象、参数名称 等,然后对查到的数据自定义处理
-https://github.com/Tencent/APIJSON/issues/101 +在后端编写 远程函数,可以拿到 session、version、当前 JSON 对象、参数名称 等,然后对查到的数据自定义处理 [issues#101](https://github.com/APIJSON/APIJSON/issues/101)
#### 2.如何控制权限? -在 Access 表配置校验规则,默认不允许访问,需要对 每张表、每种角色、每种操作 做相应的配置,粒度细分到行级
-https://github.com/Tencent/APIJSON/issues/12 +在 Access 表配置校验规则,默认不允许访问,需要对 每张表、每种角色、每种操作 做相应的配置,粒度细分到行级 [issues#12](https://github.com/APIJSON/APIJSON/issues/12)
#### 3.如何校验参数? -在 Request 表配置校验规则 structure,提供 MUST、TYPE、VERIFY 等通用方法,可通过 远程函数 来完全自定义
-https://github.com/Tencent/APIJSON/wiki#%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86 +在 Request 表配置校验规则 structure,提供 MUST、TYPE、VERIFY 等通用方法,可通过 远程函数 来完全自定义 [Wiki](/wiki#%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86)
+ +更多常见问题及提问前必看 [issues#36](https://github.com/APIJSON/APIJSON/issues/36)
-更多常见问题及提问前必看
-https://github.com/Tencent/APIJSON/issues/36
### 注意事项 **请求参数 JSON 中表名、字段名、关键词及对应的值都是大小写敏感、逗号敏感、分号敏感、空格敏感、换行敏感,
-大部分情况都不允许空格和换行,表名以大写字母开头,不要想当然,请严格按照 [设计规范](https://github.com/Tencent/APIJSON/blob/master/Document.md#3) 来调用 API !** -[#181](https://github.com/Tencent/APIJSON/issues/181) +大部分情况都不允许空格和换行,表名以大写字母开头,不要想当然,请严格按照 [设计规范](/Document.md#3) 来调用 API !** +[issues#181](https://github.com/APIJSON/APIJSON/issues/181)


@@ -299,7 +295,7 @@ https://github.com/Tencent/APIJSON/issues/36 ### 使用登记 如果您在使用 APIJSON,请让我们知道,您的使用对我们非常重要(新的按登记顺序排列、专群优先答疑解惑):
-https://github.com/Tencent/APIJSON/issues/187 +https://github.com/APIJSON/APIJSON/issues/187

@@ -337,7 +333,7 @@ https://github.com/Tencent/APIJSON/issues/187 - + @@ -348,14 +344,14 @@ https://github.com/Tencent/APIJSON/issues/187 - + - - - + + +
@@ -377,8 +373,7 @@ https://github.com/Tencent/APIJSON/issues/187 * [上海钰亿环保科技有限公司](#) ### 贡献者们 -主项目 APIJSON 的贡献者们(6 个腾讯工程师、1 个微软工程师、1 个阿里云工程师、1 个字节跳动工程师、1 个网易工程师、1 个 Zoom 工程师、1 个圆通工程师、1 个知乎基础研发架构师、1 个智联招聘工程师、gorm-plus 作者、1 个美国加州大学学生、3 个 SUSTech 学生等):
-https://github.com/Tencent/APIJSON/blob/master/CONTRIBUTING.md
+[主项目 APIJSON 的贡献者们](/CONTRIBUTING.md)(7 个腾讯工程师、1 个微软工程师、1 个阿里云工程师、1 个字节跳动工程师、1 个网易工程师、1 个 Zoom 工程师、1 个圆通工程师、1 个知乎基础研发架构师、1 个智联招聘工程师、gorm-plus 作者、1 个美国加州大学学生、3 个 SUSTech 学生等):

Screenshot 2026-04-18 at 05 28 48 @@ -388,8 +383,9 @@ https://search.gitee.com/?skin=rec&type=repository&q=apijson&sort=stars_count Screenshot 2026-04-18 at 05 29 36 -还有为 APIJSON 扫描代码贡献 Issue 的 [蚂蚁集团源伞](https://www.sourcebrella.com) 和 [奇安信代码卫士](https://github.com/QiAnXinCodeSafe) +还有 [腾讯安全](https://security.tencent.com)、[蚂蚁集团源伞](https://www.sourcebrella.com)、[奇安信代码卫士](https://github.com/QiAnXinCodeSafe) 提交安全报告(Issue)等
+ image
@@ -400,24 +396,23 @@ https://search.gitee.com/?skin=rec&type=repository&q=apijson&sort=stars_count 数百名知名大厂员工点了 Star,也有腾讯、华为、字节跳动、Microsoft、Zoom 等不少知名大厂员工提了 PR/Issue,感谢大家的支持~
-[![Stargazers over time](https://starchart.cc/Tencent/APIJSON.svg)](https://starchart.cc/Tencent/APIJSON) -image + +image image image -根据开源指南针报告,APIJSON Java 版已经是国内顶级、国际一流的 Java 开源项目了 [#518](https://github.com/Tencent/APIJSON/issues/518)
+根据开源指南针报告,APIJSON Java 版已经是国内顶级、国际一流的 Java 开源项目了 [#518](https://github.com/APIJSON/APIJSON/issues/518)
image ### 规划及路线图 -新增功能、强化安全、提高性能、增强稳定、完善文档、丰富周边、推广使用
-https://github.com/Tencent/APIJSON/blob/master/Roadmap.md +[新增功能、强化安全、提高性能、增强稳定、完善文档、丰富周边、推广使用](/Roadmap.md)
理论上所有支持 SQL 与 JDBC/ODBC 的软件,都可以用本项目对接 CRUD,待测试:
[OceanBase](https://www.oceanbase.com/docs/oceanbase/V2.2.50/ss-sr-select_daur3l), [Spark](https://spark.apache.org/docs/3.3.0/sql-ref-syntax-qry-select.html)(可用 Hive 对接), [Phoenix](http://phoenix.apache.org/language/index.html#select)(延伸支持 HBase) ### 我要赞赏 创作不易,坚持更难,右上角点亮 ⭐ Star 来收藏/支持下吧,谢谢 ^_^
-https://github.com/Tencent/APIJSON +https://github.com/APIJSON/APIJSON

@@ -426,9 +421,8 @@ https://github.com/Tencent/APIJSON 导航目录: [项目简介](#--apijson) [上手使用](#%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B) 社区生态      完整详细的导航目录 [点这里查看](/Navigation.md)
### 技术交流 -如果有什么问题或建议可以 [填问卷](https://wj.qq.com/s2/10971431/2a09) 或 [提 Issue](https://github.com/Tencent/APIJSON/issues/36),交流技术,分享经验。
-如果你解决了某些 bug,或者新增了一些功能,欢迎 [贡献代码](https://github.com/Tencent/APIJSON/pulls),感激不尽~
-https://github.com/Tencent/APIJSON/blob/master/CONTRIBUTING.md +如果有什么问题或建议可以 [填问卷](https://wj.qq.com/s2/10971431/2a09) 或 [提 Issue](https://github.com/APIJSON/APIJSON/issues/36),交流技术,分享经验。
+如果你解决了某些 bug,或者新增了一些功能,欢迎 [贡献代码](/CONTRIBUTING.md),感激不尽~
**开发者也是人,也需要工作、休息、恋爱、陪伴家人、走亲会友等,也有心情不好和身体病痛,**
**往往没有额外的时间精力顾及一些小问题,请理解和支持,开源要大家参与贡献才会更美好~**
@@ -439,13 +433,13 @@ https://github.com/Tencent/APIJSON/blob/master/CONTRIBUTING.md **如果你已经多次得到帮助,却仍然只索取不贡献,那就别指望大家再帮你!**
**私聊作者请教技术问题 或者 频繁在互助群 @ 作者 可能会被拉黑/禁言/踢群,请尊重和理解,谢谢!**
-如果你 [提 PR 登记了自己使用 APIJSON 的公司](https://github.com/Tencent/APIJSON/issues/187),可以加 **企业用户支持专群**,作者亲自且优先答疑,
+如果你 [提 PR 登记了自己使用 APIJSON 的公司](/issues/187),可以加 **企业用户支持专群**,作者亲自且优先答疑,
作者只有解答完了这个专群里的全部问题,才看情况解答 Issue/问卷 里的问题(对 Issue/问卷 不保证解答、更不保证及时);
之前的几个互助群,由于大多数问题 在文档/Issue 已有答案却反复提 或者 缺少必要信息要来来回回沟通问清细节 已浪费太多时间,
-甚至有白嫖还把自己当大爷的自私自利伸手党输出情绪,我们不再支持,建议未登记企业的用户 [填问卷](https://wj.qq.com/s2/10971431/2a09) 或 [提 Issue](https://github.com/Tencent/APIJSON/issues/36)。
+甚至有白嫖还把自己当大爷的自私自利伸手党输出情绪,我们不再支持,建议未登记企业的用户 [填问卷](https://wj.qq.com/s2/10971431/2a09) 或 [提 Issue](https://github.com/APIJSON/APIJSON/issues/36)。
如果你为 APIJSON 做出了以下任何一个贡献,我们将优先为你答疑解惑:
-[提交了 PR 且被合并](https://github.com/Tencent/APIJSON/pull/92)、[提交了优质 Issue](https://github.com/Tencent/APIJSON/issues/189)、[发表了优质文章](https://blog.csdn.net/qq_41829492/article/details/88670940)、[开发了可用的生态项目](https://github.com/zhangchunlin/uliweb-apijson),
+[提交了 PR 且被合并](https://github.com/APIJSON/APIJSON/pull/92)、[提交了优质 Issue](https://github.com/APIJSON/APIJSON/issues/189)、[发表了优质文章](https://blog.csdn.net/qq_41829492/article/details/88670940)、[开发了可用的生态项目](https://github.com/zhangchunlin/uliweb-apijson),
Issue/问卷 一般解答顺序:贡献者 > 帮助他人的用户 > 提供任职企业的用户 > 其他用户。 @@ -474,7 +468,7 @@ Issue/问卷 一般解答顺序:贡献者 > 帮助他人的用户 > 提供任 [学习自动化接口APIJSON](https://www.jianshu.com/p/981a2a630c7b) -[APIJSON 接口调试实践](https://github.com/Tencent/APIJSON/issues/189) +[APIJSON 接口调试实践](https://github.com/APIJSON/APIJSON/issues/189) [APIJSON-零代码接口和文档 JSON 协议 与 ORM 库](https://cloud.tencent.com/developer/article/2077042) @@ -520,15 +514,15 @@ Issue/问卷 一般解答顺序:贡献者 > 帮助他人的用户 > 提供任 [APIJSON – The No-Code API Revolution That Puts Developers in the Fast Lane](https://medevel.com/apijson) -[APIJSON:17.4k Star!腾讯开源的零代码接口与文档协议及ORM库](https://mp.weixin.qq.com/s/gr84DmWKs4O6lcoT-iaV5w) +[APIJSON:17.4k Star!零代码接口与文档协议及ORM库](https://mp.weixin.qq.com/s/gr84DmWKs4O6lcoT-iaV5w) -[APIJSON腾讯开源的后端开发神器!!!](https://cloud.tencent.com/developer/article/2372220) +[APIJSON-后端开发神器!!!](https://cloud.tencent.com/developer/article/2372220) [apijson 快速上手](https://blog.csdn.net/qq_16381291/article/details/147110737) [APIJSON快速入门-零后端代码,接口所见即所得](https://www.toutiao.com/article/7503844050689376783) -[腾讯开源!零代码,全自动万能API接口](https://mp.weixin.qq.com/s/WWndAa68BqBfflWgL5592A) +[零代码,全自动万能API接口](https://mp.weixin.qq.com/s/WWndAa68BqBfflWgL5592A) [APIJSON项目实战教程:零代码实现高效JSON接口开发](https://blog.csdn.net/gitblog_00682/article/details/148375065) @@ -536,9 +530,9 @@ Issue/问卷 一般解答顺序:贡献者 > 帮助他人的用户 > 提供任 [APIJSON:重新定义后端开发体验的零代码ORM框架](https://blog.csdn.net/gitblog_01177/article/details/155216163) -[API自动生成这么爽?实测腾讯APIJSON,零代码就能玩转后端接口!](https://mp.weixin.qq.com/s/DmMIGHHcZ783KobGecMxGg) +[API自动生成这么爽?实测 APIJSON,零代码就能玩转后端接口!](https://mp.weixin.qq.com/s/DmMIGHHcZ783KobGecMxGg) -[腾讯开源的 APIJSON:后端接口不用写了?](https://mp.weixin.qq.com/s/zhkfG4AQEsg0N87lhStwvw) +[APIJSON:后端接口不用写了?](https://mp.weixin.qq.com/s/zhkfG4AQEsg0N87lhStwvw) [3分钟掌握APIJSON搜索黑科技:从模糊匹配到智能检索](https://blog.csdn.net/gitblog_00009/article/details/152403741) @@ -547,6 +541,8 @@ Issue/问卷 一般解答顺序:贡献者 > 帮助他人的用户 > 提供任 ### 生态项目 [APIJSON-Demo](https://github.com/APIJSON/APIJSON-Demo) APIJSON 各种语言、各种框架 的 使用示例项目、上手文档、测试数据 SQL 文件 等 +[A2API](https://github.com/TommyLemon/A2API) AI 生成一次 UI,API 每次都安全、快速、稳定操作数据。聊天生成 UI 来安全、快速、稳定增删改查表格、表单、图表等数据。 + [apijson-orm](https://github.com/APIJSON/apijson-orm) APIJSON ORM 库,可通过 Maven, Gradle 等远程依赖 [apijson-framework](https://github.com/APIJSON/apijson-framework) APIJSON 服务端框架,通过数据库表配置角色权限、参数校验等,简化使用 @@ -673,14 +669,16 @@ Issue/问卷 一般解答顺序:贡献者 > 帮助他人的用户 > 提供任 [APIJSONServer](https://github.com/cyber2jie/APIJSONServer) 基于APIJSON实现的数据服务端 +[BankTransferNetwork](https://gitee.com/guxurui2024/BankTransferNetwork) 基于 APIJSON + Spark + Kafka 构建的金融交易网络风险图谱分析系统 + 感谢热心的作者们的贡献,点亮 ⭐ Star 收藏/支持下他们吧~ -### 腾讯犀牛鸟开源人才培养计划 -https://github.com/Tencent/APIJSON/issues/229 +### 犀牛鸟开源人才培养计划 +https://github.com/APIJSON/APIJSON/issues/229 -#### qiujunlin **2.接入 presto/hive/clickhouse/db2 任意一个** +#### qiujunlin **2.接入 presto/hive/clickhouse/db2 任意一个** APIJSON 接入 clickhouse 使用demo
https://github.com/qiujunlin/APIJSONDemo @@ -756,9 +754,3 @@ APIJSON 上手教程:https://www.bilibili.com/video/BV1Pq4y1n7rJ ### 持续更新 https://github.com/Tencent/APIJSON/commits/master - -### 工蜂主页 -https://git.code.tencent.com/Tencent_Open_Source/APIJSON - -### 码云主页 -https://gitee.com/Tencent/APIJSON diff --git a/README-extend.md b/README-extend.md index 98b60ac2..022a84a0 100644 --- a/README-extend.md +++ b/README-extend.md @@ -52,6 +52,29 @@ key= Moment[] } } +同一事务中对同一张表执行删除和批量新增时,需要通过别名保证 key 唯一。`@post`、`@delete` 等方法指令的 value 只能是 String 或 JSONObject,不能使用 JSONArray: + +```json +{ + "@delete": { + "MobilizeTaskInfo:remove": "MobilizeTaskInfo" + }, + "MobilizeTaskInfo:remove": { + "id{}": ["09a7a740-1935-469a-ab76-ea1ee1f94ddc"] + }, + "@post": { + "MobilizeTaskInfo:add[]": "MobilizeTaskInfo:[]" + }, + "MobilizeTaskInfo:add[]": [ + { + "leading_unit_name": "市交通局", + "serial_number": "GD2025-064", + "status": 1 + } + ] +} +``` + 对于没有显式声明操作方法的,直接用 URL(/get, /post 等) 对应的默认操作方法 ``` diff --git a/README.md b/README.md index 0bcb1841..04698ceb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ -Tencent is pleased to support the open source community by making APIJSON available.
-Copyright (C) 2020 Tencent. All rights reserved.
-This source code is licensed under the Apache License Version 2.0
-

APIJSON

@@ -10,14 +6,14 @@ This source code is licensed under the Apache License Version 2.0

🏆 Real-Time no-code, powerful and secure ORM 🚀
providing APIs and Docs without coding by Backend, and Frontend can customize response JSONs

-  中文版  -  Document  +  中文版  +  Document   Video   Test  Ask AI Skills - MCP - A2A + MCP + A2API

@@ -56,6 +52,11 @@ This source code is licensed under the Apache License Version 2.0
+ + + + +

@@ -65,7 +66,7 @@ This source code is licensed under the Apache License Version 2.0
- +

@@ -214,7 +215,7 @@ Complex production App like Twitter tweets
##

4. Contributing

We are always looking for more developers to help implementing new features, fix bugs, etc.
-Please have a look at the [open issues](https://github.com/Tencent/APIJSON/issues) before opening a new one.
+Please have a look at the [open issues](https://github.com/APIJSON/APIJSON/issues) before opening a new one.
Fork the project and send a pull request.
@@ -223,7 +224,7 @@ Fork the project and send a pull request.
##

5. Releases

-See the latest release [here](https://github.com/Tencent/APIJSON/releases) +See the latest release [here](https://github.com/APIJSON/APIJSON/releases)
@@ -232,14 +233,14 @@ See the latest release [here](https://github.com/Tencent/APIJSON/releases) https://github.com/TommyLemon
![](https://github.com/user-attachments/assets/cef2bd45-b20d-469e-8781-1d647cf0477f) -If you have any questions or suggestions, you can [create an issue](https://github.com/Tencent/APIJSON/issues) or [send me an e-mail](mailto:tommylemon@qq.com). +If you have any questions or suggestions, you can [create an issue](https://github.com/APIJSON/APIJSON/issues) or [send me an e-mail](mailto:tommylemon@qq.com).

### Users of APIJSON: -https://github.com/Tencent/APIJSON/issues/187 +[issues/187](https://github.com/APIJSON/APIJSON/issues/187)
@@ -277,7 +278,7 @@ https://github.com/Tencent/APIJSON/issues/187 - + @@ -288,22 +289,21 @@ https://github.com/Tencent/APIJSON/issues/187 - + - - - + + +
-[More APIJSON Users](https://github.com/Tencent/APIJSON/issues/73) +[More APIJSON Users](https://github.com/APIJSON/APIJSON/issues/73) ### Contributers of APIJSON: -Contributers for the APIJSON core project(6 Tencent engineers, 1 Microsoft engineer, 1 Zhihu architect, 1 Bytedance(TikTok) engineer, 1 NetEase engineer, 1 Zoom engineer, 1 YTO Express engineer, 1 Zhilian engineer, 1 UC student、3 SUSTech students, etc.):
-https://github.com/Tencent/APIJSON/blob/master/CONTRIBUTING.md
+[Contributers](/CONTRIBUTING.md) for the APIJSON core project(7 Tencent engineers, 1 Microsoft engineer, 1 Zhihu architect, 1 Bytedance(TikTok) engineer, 1 NetEase engineer, 1 Zoom engineer, 1 YTO Express engineer, 1 Zhilian engineer, 1 UC student、3 SUSTech students, etc.):

Screenshot 2026-04-18 at 05 28 48 @@ -313,6 +313,14 @@ https://search.gitee.com/?skin=rec&type=repository&q=apijson&sort=stars_count Screenshot 2026-04-18 at 05 29 36 +[Tencent Security](https://security.tencent.com), [Ant Group Sourcebrella](https://www.sourcebrella.com), [QAX CodeSafe](https://github.com/QiAnXinCodeSafe) submitted security reports(Issue) +
+ image + + +
+
+ Thanks to all contributers of APIJSON!
@@ -320,21 +328,26 @@ Thanks to all contributers of APIJSON! ### Statistics Hundreds of employees from big famous companies(Tencent, Google, Apple, Microsoft, Amazon, Huawei, Alibaba, Paypal, Meituan, Bytedance(TikTok), IBM, Baidu, JD, NetEase, Kuaishou, Shopee, etc.) starred,
a lot of employees from big famous companies(Tencent, Huawei, Microsoft, Zoom, etc.) created PR/Issue, thank you all~
-[![Stargazers over time](https://starchart.cc/Tencent/APIJSON.svg)](https://starchart.cc/Tencent/APIJSON) -image + +image image image +APIJSON was shown as a top open source Java projects on OSS Compass [#518](https://github.com/APIJSON/APIJSON/issues/518)
+image + ### Ecosystem [APIJSON-Demo](https://github.com/APIJSON/APIJSON-Demo) Demo projects with document and SQL files for APIJSON with different programming languages and different frameworks +[A2API](https://github.com/TommyLemon/A2API) AI generate UI once, API view/edit data everytime safely, quickly and stably. Chat agent to HTTP API to safely, quickly and stably add, view, edit or remove data in tables, forms or charts together with A2UI. + [apijson-orm](https://github.com/APIJSON/apijson-orm) APIJSON ORM library, Maven, Gradle, etc can be used for dependencies [apijson-framework](https://github.com/APIJSON/apijson-framework) APIJSON Server Framework for configuring access of roles and validation of arguments in database tables, then using APIJSON easier [apijson-router](https://github.com/APIJSON/apijson-router) A router plugin for APIJSON, expose undercontrolled RESTful-like HTTP API to public network, transfer to APIJSON request and execute -[apijson-column](https://github.com/APIJSON/apijson-column) A column plugin for Tencent APIJSON, supports Column Inverse and Column Mapping +[apijson-column](https://github.com/APIJSON/apijson-column) A column plugin for APIJSON, supports Column Inverse and Column Mapping [apijson-jackson](https://github.com/APIJSON/apijson-jackson) A jackson plugin for APIJSON @@ -454,6 +467,8 @@ a lot of employees from big famous companies(Tencent, Huawei, Microsoft, Zoom, e [APIJSONServer](https://github.com/cyber2jie/APIJSONServer) Data server based on APIJSON +[BankTransferNetwork](https://gitee.com/guxurui2024/BankTransferNetwork) Financial trading risk analyzer, based on APIJSON + Spark + Kafka + Thank you to all the enthusiastic authors for the contributions~
### Please give them a ⭐ Star(on the top right) to support their hard works!