diff --git a/APIJSONORM/README.md b/APIJSONORM/README.md index 0cb431e2..745733b4 100644 --- a/APIJSONORM/README.md +++ b/APIJSONORM/README.md @@ -21,7 +21,7 @@ Tencent [APIJSON](https://github.com/Tencent/APIJSON) ORM library for remote dep com.github.Tencent APIJSON - LATEST + 8.1.8 ``` @@ -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:latest' + implementation 'com.github.Tencent:APIJSON:8.1.8' } ``` diff --git a/APIJSONORM/pom.xml b/APIJSONORM/pom.xml index 7a35eb01..00ff59dc 100644 --- a/APIJSONORM/pom.xml +++ b/APIJSONORM/pom.xml @@ -5,7 +5,7 @@ com.github.Tencent APIJSON - 8.1.8 + 8.2.0 jar APIJSONORM @@ -21,6 +21,12 @@ + + junit + junit + 4.13.2 + test + diff --git a/APIJSONORM/src/main/java/apijson/Log.java b/APIJSONORM/src/main/java/apijson/Log.java index f00e495a..736877f9 100755 --- a/APIJSONORM/src/main/java/apijson/Log.java +++ b/APIJSONORM/src/main/java/apijson/Log.java @@ -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 = "8.2.0"; public static final String LEVEL_VERBOSE = "VERBOSE"; public static final String LEVEL_INFO = "INFO"; diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractObjectParser.java b/APIJSONORM/src/main/java/apijson/orm/AbstractObjectParser.java index c4f7c5ff..5fd00c17 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractObjectParser.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractObjectParser.java @@ -375,7 +375,7 @@ else if (_method == PUT && value instanceof List && (whereList == null || whe if (isSubquery == false) { // 解决 SQL 语法报错,子查询不能 EXPLAIN Boolean exp = parser.getGlobalExplain(); - if (sch != null) { + if (exp != null) { sqlRequest.putIfAbsent(JSONMap.KEY_EXPLAIN, exp); } diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java b/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java index defccc93..26e63a9c 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java @@ -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..66cf0214 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java @@ -154,6 +154,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 +181,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 +897,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 +1015,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 +1097,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 +1185,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 +1413,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 +1566,7 @@ public AbstractSQLConfig setTable(String table) { //Table已经在Parse } public String gainAs() { - return isOracle() || isManticore() ? " " : " AS "; + return isOracle() || isKingBaseOracle() || isManticore() ? " " : " AS "; } @Override @@ -2065,7 +2115,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); @@ -3064,8 +3114,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 @@ -4005,7 +4061,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 +4349,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 +4672,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 +4722,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 +4776,7 @@ private String withAsExprSubqueryString(SQLConfig cfg, Subquery, L extends List> String return "ALTER TABLE " + tablePath + " UPDATE" + config.gainSetString() + config.gainWhereString(true); } cSql = "UPDATE " + tablePath + config.gainSetString() + config.gainWhereString(true) - + (config.isMySQL() ? config.gainLimitString() : ""); + + (config.isMySQL() || KingbaseSQLDialect.from(config.gainSQLDatabase()).supportsDmlLimit() ? config.gainLimitString() : ""); cSql = buildWithAsExprSql(config, cSql); return cSql; case DELETE: @@ -4987,12 +5047,15 @@ public static , L extends List> String return "ALTER TABLE " + tablePath + " DELETE" + config.gainWhereString(true); } cSql = "DELETE FROM " + tablePath + config.gainWhereString(true) - + (config.isMySQL() ? config.gainLimitString() : ""); // PostgreSQL 不允许 LIMIT + + (config.isMySQL() || KingbaseSQLDialect.from(config.gainSQLDatabase()).supportsDmlLimit() ? config.gainLimitString() : ""); // PostgreSQL 不允许 LIMIT cSql = buildWithAsExprSql(config, cSql); return cSql; default: - String explain = config.isExplain() ? (config.isSQLServer() ? "SET STATISTICS PROFILE ON " - : (config.isOracle() || config.isDameng() || config.isKingBase() ? "EXPLAIN PLAN FOR " : "EXPLAIN ")) : ""; + KingbaseSQLDialect kingbaseDialect = KingbaseSQLDialect.from(config.gainSQLDatabase()); + String kingbaseExplain = kingbaseDialect.getExplainPrefix(); + String explain = config.isExplain() ? (kingbaseExplain != null ? kingbaseExplain + : (config.isSQLServer() ? "SET STATISTICS PROFILE ON " + : (config.isOracle() || config.isDameng() ? "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) @@ -5001,7 +5064,7 @@ public static , L extends List> String config.setPreparedValueList(new ArrayList()); String column = config.gainColumnString(); - if (config.isOracle() || config.isDameng() || config.isKingBase()) { + if (config.isOracle() || config.isDameng() || config.isKingBaseOracle()) { //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)){ @@ -5044,7 +5107,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的分页获取 @@ -5349,7 +5412,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 +5472,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 +5607,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 无效 @@ -6168,6 +6241,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..c4979e17 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLExecutor.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLExecutor.java @@ -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..6a59e0ef 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java @@ -1814,7 +1814,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 +1903,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/KingbaseSQLDialect.java b/APIJSONORM/src/main/java/apijson/orm/KingbaseSQLDialect.java new file mode 100644 index 00000000..113f4c4f --- /dev/null +++ b/APIJSONORM/src/main/java/apijson/orm/KingbaseSQLDialect.java @@ -0,0 +1,86 @@ +/*Copyright (C) 2020 Tencent. 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/SQLConfig.java b/APIJSONORM/src/main/java/apijson/orm/SQLConfig.java index 5386d160..0b044594 100755 --- a/APIJSONORM/src/main/java/apijson/orm/SQLConfig.java +++ b/APIJSONORM/src/main/java/apijson/orm/SQLConfig.java @@ -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/SQLExecutor.java b/APIJSONORM/src/main/java/apijson/orm/SQLExecutor.java index 6540771d..436fadce 100755 --- a/APIJSONORM/src/main/java/apijson/orm/SQLExecutor.java +++ b/APIJSONORM/src/main/java/apijson/orm/SQLExecutor.java @@ -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/exception/CommonException.java b/APIJSONORM/src/main/java/apijson/orm/exception/CommonException.java index 7e2884ef..e5f11274 100755 --- a/APIJSONORM/src/main/java/apijson/orm/exception/CommonException.java +++ b/APIJSONORM/src/main/java/apijson/orm/exception/CommonException.java @@ -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/script/JSR223ScriptExecutor.java b/APIJSONORM/src/main/java/apijson/orm/script/JSR223ScriptExecutor.java index 9c2c9baf..7e08a945 100644 --- a/APIJSONORM/src/main/java/apijson/orm/script/JSR223ScriptExecutor.java +++ b/APIJSONORM/src/main/java/apijson/orm/script/JSR223ScriptExecutor.java @@ -27,11 +27,34 @@ public abstract class JSR223ScriptExecutor, L e @Override public ScriptExecutor init() { - ScriptEngineManager scriptEngineManager = new ScriptEngineManager(); - scriptEngine = scriptEngineManager.getEngineByName(scriptEngineName()); + scriptEngine = createScriptEngine(); return this; } + protected ScriptEngine createScriptEngine() { + String name = scriptEngineName(); + if ("nashorn".equalsIgnoreCase(name) || "javascript".equalsIgnoreCase(name) + || "js".equalsIgnoreCase(name) || "ecmascript".equalsIgnoreCase(name)) { + try { + Class factoryClass = Class.forName("jdk.nashorn.api.scripting.NashornScriptEngineFactory"); + Class filterClass = Class.forName("jdk.nashorn.api.scripting.ClassFilter"); + Object filter = java.lang.reflect.Proxy.newProxyInstance( + filterClass.getClassLoader(), + new Class[]{filterClass}, + (proxy, method, methodArgs) -> isClassExposureAllowed((String) methodArgs[0])); + Object factory = factoryClass.getDeclaredConstructor().newInstance(); + return (ScriptEngine) factoryClass.getMethod("getScriptEngine", filterClass).invoke(factory, filter); + } catch (Throwable e) { + Log.e(TAG, "create sandboxed Nashorn engine failed, falling back: " + e); + } + } + return new ScriptEngineManager().getEngineByName(name); + } + + protected boolean isClassExposureAllowed(String className) { + return false; + } + protected abstract String scriptEngineName(); protected abstract Object extendParameter(AbstractFunctionParser parser, Map currentObject, String methodName, Object[] args); 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/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/README-Chinese.md b/README-Chinese.md index f866e0bc..f68107dd 100644 --- a/README-Chinese.md +++ b/README-Chinese.md @@ -14,6 +14,10 @@ This source code is licensed under the Apache License Version 2.0
视频教程 测试用例 AI 问答 + Skills + MCP + A2A + A2API

@@ -413,7 +417,7 @@ https://github.com/Tencent/APIJSON/blob/master/Roadmap.md [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 来支持/收藏下吧,谢谢 ^_^
+创作不易,坚持更难,右上角点亮 ⭐ Star 来收藏/支持下吧,谢谢 ^_^
https://github.com/Tencent/APIJSON
@@ -544,6 +548,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 服务端框架,通过数据库表配置角色权限、参数校验等,简化使用 @@ -596,9 +602,11 @@ Issue/问卷 一般解答顺序:贡献者 > 帮助他人的用户 > 提供任 [apijson-node](https://github.com/kevinaskin/apijson-node) 字节跳动工程师开源的 Node.ts 版 APIJSON,提供 nestjs 和 typeorm 的 Demo 及后台管理 +[nestjs-apijson](https://github.com/yangzhouQS/nestjs-apijson) APIJSON NestJS 版,支持 CRUD、JOIN、各种条件,适配 MySQL, PostgreSQL, SQLite (AI 辅助) + [uliweb-apijson](https://github.com/zhangchunlin/uliweb-apijson) Python 版 APIJSON,支持 MySQL, PostgreSQL, SQL Server, Oracle, SQLite 等 -[apijson-rust](https://github.com/APIJSON/apijson-rust) APIJSON 的 Rust 版,一个优雅、高性能的 Rust 多数据源管理系统,支持 MySQL 和 PostgreSQL +[apijson-rust](https://github.com/APIJSON/apijson-rust) APIJSON 的 Rust 版,一个优雅、高性能的 Rust 多数据源管理系统,支持 MySQL 和 PostgreSQL (AI 辅助) [APIJSONParser](https://github.com/Zerounary/APIJSONParser) 第三方 APIJSON 解析器,将 JSON 动态解析成 SQL @@ -634,7 +642,9 @@ Issue/问卷 一般解答顺序:贡献者 > 帮助他人的用户 > 提供任 [ApiJsonByJFinal](https://gitee.com/zhiyuexin/ApiJsonByJFinal) 整合 APIJSON 和 JFinal 的 Demo -[apijson-go-demo](https://github.com/glennliao/apijson-go-demo) apijson-go demos,提供 3 个从简单到复杂的不同场景 Demo +[bookmark](https://github.com/glennliao/bookmark) goframe + apijson-go + vue3 + antd vue 4 的在线书签 + +[apijson-go-demo](https://github.com/APIJSON/apijson-go-demo) apijson-go demos,提供 3 个从简单到复杂的不同场景 Demo [apijson-builder](https://github.com/pengxianggui/apijson-builder) 一个方便为 APIJSON 构建 RESTful 请求的 JavaScript 库 @@ -650,7 +660,7 @@ Issue/问卷 一般解答顺序:贡献者 > 帮助他人的用户 > 提供任 [xyerp](https://gitee.com/yinjg1997/xyerp) 基于ApiJson的低代码ERP -[quick-boot](https://github.com/csx-bill/quick-boot) 基于 Spring Cloud 2022、Spring Boot 3、AMIS 和 APIJSON 的低代码系统。 +[quick-boot](https://github.com/csx-bill/quick-boot/tree/master) 基于 Spring Cloud 2022、Spring Boot 3、AMIS 和 APIJSON 的低代码系统。 [apijson-query-spring-boot-starter](https://gitee.com/mingbaobaba/apijson-query-spring-boot-starter) 一个快速构建 APIJSON 查询条件的插件 @@ -666,7 +676,7 @@ Issue/问卷 一般解答顺序:贡献者 > 帮助他人的用户 > 提供任 [APIJSONServer](https://github.com/cyber2jie/APIJSONServer) 基于APIJSON实现的数据服务端 -感谢热心的作者们的贡献,点 ⭐Star 支持下他们吧~ +感谢热心的作者们的贡献,点亮 ⭐ Star 收藏/支持下他们吧~ ### 腾讯犀牛鸟开源人才培养计划 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 f6c637fd..f45b100e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ This source code is licensed under the Apache License Version 2.0
 Video   Test  Ask AI + Skills + MCP + A2A + A2API

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

- +

@@ -141,7 +145,7 @@ You're gonna leave 'em all in awe, awe, awe.
**Tired with endless arguments about HTTP API dev or use?**
**Use APIJSON-the ORM for providing infinity codeless CRUD APIs that fit almost all your needs.**
-**Unfold the Power(In Your Soul) with ⭐Star & Clone.** +**Unfold the Power(In Your Soul) with ⭐ Star & Clone.** ### APIJSON Show #### Postman test APIJSON @@ -215,7 +219,7 @@ Please have a look at the [open issues](https://github.com/Tencent/APIJSON/issue Fork the project and send a pull request.
-Please also ⭐Star the project! +### Please also ⭐ Star(on the top right) this project!
##

5. Releases

@@ -322,5 +326,138 @@ a lot of employees from big famous companies(Tencent, Huawei, Microsoft, Zoom, e image 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-jackson](https://github.com/APIJSON/apijson-jackson) A jackson plugin for APIJSON + +[apijson-fastjson2](https://github.com/APIJSON/apijson-fastjson2) A fastjson2 plugin for APIJSON + +[apijson-gson](https://github.com/APIJSON/apijson-gson) A gson plugin for APIJSON + +[apijson-milvus](https://github.com/APIJSON/apijson-milvus) An APIJSON plugin for Milvus - An AI vector database + +[apijson-influxdb](https://github.com/APIJSON/apijson-influxdb) An APIJSON plugin for InfluxDB - An IoT time-series database + +[apijson-mongodb](https://github.com/APIJSON/apijson-mongodb) An APIJSON plugin for MongoDB - A NoSQL database + +[apijson-cassandra](https://github.com/APIJSON/apijson-cassandra) An APIJSON plugin for Cassandra - A NoSQL database + +[APIAuto](https://github.com/TommyLemon/APIAuto) ☔ The most advanced tool for HTTP API. Machine learning no-code testing and AI assistant, generating codes and static analysis, generating comments and floating hints. Used by Tencent, SHEIN, TRANSSION, etc + +[CVAuto](https://github.com/TommyLemon/CVAuto) 👁 No-code, zero-annotation CV(Computer Vision) AI automated testing tool 🚀 Eliminates the need for extensive manual tasks such as drawing bounding boxes and labeling for image recognition algorithms + +[UnitAuto](https://github.com/TommyLemon/UnitAuto) ☀️ The most advanced unit testing way powered by machine learning. Coding-free, comprehensive and automatic testing for methods/functions. Used by Tencent, Kwai, a Fortune 500 company, etc + +[SQLAuto](https://github.com/TommyLemon/SQLAuto) 🔍 A smart SQL testing automation tool for databases, supports any CRUD, any template variables, generating argument combinations, generating lots of data rows + +[UIGO](https://github.com/TommyLemon/UIGO) 📱 Coding-free, fast, accurate and stable UI replayer 🚀 Incredible ±3px auto locating and ±2ms auto waiting. Used by Tencent, invited by WeChat team to share + +[APIJSONdocs](https://github.com/ruoranw/APIJSONdocs) APIJSON English documentation, provided a website to view + +[apijson-doc](https://github.com/vincentCheng/apijson-doc) APIJSON Chinese documentation, provided a website to view + +[apijson.org](https://github.com/APIJSON/apijson.org) APIJSON official website + +[APIJSON.NET](https://github.com/liaozb/APIJSON.NET) APIJSON for C#, supports CRUD for MySQL, PostgreSQL, SQL Server, Oracle, SQLite + +[apijson-go](https://github.com/glennliao/apijson-go) APIJSON for Go, based on Go(>=1.18) + GoFrame2, support CRUD + +[apijson-go](https://gitee.com/tiangao/apijson-go) APIJSON for Go, support CRUD, can directly run in Docker + +[apijson-hyperf](https://github.com/kvnZero/hyperf-APIJSON.git) APIJSON for PHP, based on Hyperf, supports MySQL + +[APIJSON-php](https://github.com/xianglong111/APIJSON-php) APIJSON for PHP, based on ThinkPHP,supports MySQL, PostgreSQL, SQL Server, Oracle, etc + +[apijson-php](https://github.com/qq547057827/apijson-php) APIJSON for PHP, based on ThinkPHP,supports MySQL, PostgreSQL, SQL Server, Oracle, etc + +[apijson-node](https://github.com/kevinaskin/apijson-node) APIJSON for Node.js, developed by a ByteDance engineer, provides demos for NestJS and Typeorm, as well as backend management + +[nestjs-apijson](https://github.com/yangzhouQS/nestjs-apijson) APIJSON for NestJS, supports CRUD, Joins for MySQL, PostgreSQL, SQLite (AI assisted) + +[uliweb-apijson](https://github.com/zhangchunlin/uliweb-apijson) APIJSON for Python, supports CRUD for MySQL, PostgreSQL, SQL Server, Oracle, SQLite, etc + +[apijson-rust](https://github.com/APIJSON/apijson-rust) APIJSON for Rust, supports CRUD for MySQL and PostgreSQL (AI assisted) + +[APIJSONParser](https://github.com/Zerounary/APIJSONParser) An APIJSON Parser, dynamically parses JSON to SQL + +[FfApiJson](https://gitee.com/own_3_0/ff-api-json) An APIJSON Parser, parses JSON to SQL, supports multiple data sources + +[APIJSON-ToDo-Demo](https://github.com/jerrylususu/apijson_todo_demo) A simple TODO demo for APIJSON with customized authorization and authentication + +[apijson-learn](https://github.com/rainboy-learn/apijson-learn) APIJSON study note and analysis for source code + +[apijson-practice](https://github.com/vcoolwind/apijson-practice) A library for APIJSON parameter validation annotations and related demos, open-sourced by a BAT expert + +[apijson-db2](https://github.com/andream7/apijson-db2) Demo of APIJSON + IBM DB2 database, open-sourced by a Microsoft engineer + +[APIJSONDemo](https://github.com/qiujunlin/APIJSONDemo) Demo of APIJSON + ClickHouse database, open-sourced by a ByteDance engineer + +[APIJSONDemo_ClickHouse](https://github.com/chenyanlann/APIJSONDemo_ClickHouse) Demo for APIJSON + SpringBoot + ClickHouse + +[APIJSONBoot_Hive](https://github.com/chenyanlann/APIJSONBoot_Hive) Demo for APIJSON + SpringBoot + Hive + +[apijson-sample](https://gitee.com/greyzeng/apijson-sample) Simple demo and tutorial for using APIJSON + +[apijson-examples](https://gitee.com/drone/apijson-examples) Demo for APIJSON with frontend web page, backend server and management system + +[apijson-ruoyi](https://github.com/daodol/apijson-ruoyi) APIJSON + RuoYi, provides online maintenance for database configuration, etc + +[light4j](https://github.com/xlongwei/light4j) Demo for APIJSON + Redis + light-4j - a microservices framework + +[SpringServer1.2-APIJSON](https://github.com/Airforce-1/SpringServer1.2-APIJSON) The smart Party building server provides interfaces for uploading and downloading files + +[apijson_template](https://github.com/abliger/apijson_template) APIJSON Java template, using Gradle to manage dependencie and building apps + +[api-json-demo](https://gitee.com/hxdwd/api-json-demo) Demo for APIJSON to replace traditional ORM, compated Oracle transactions + +[ApiJsonByJFinal](https://gitee.com/zhiyuexin/ApiJsonByJFinal) Demo for APIJSON + JFinal - a popular web framework + +[bookmark](https://github.com/glennliao/bookmark) Online bookmark using goframe + apijson-go + vue3 + antd vue 4 + +[apijson-go-demo](https://github.com/APIJSON/apijson-go-demo) Demo for apijson-go + +[apijson-go-ui](https://github.com/glennliao/apijson-go-ui) apijson-go UI config, supports access control, request rule configuration, etc + +[apijson-builder](https://github.com/pengxianggui/apijson-builder) A JavaScript client library providing RESTful-like functions for APIJSON + +[AbsGrade](https://github.com/APIJSON/AbsGrade) List cascading algorithm supports single-level comments in WeChat Moments, double-level comments in QQ Space, and multi-level (unlimited-level) folders in Baidu Cloud + +[APIJSON-Android-RxJava](https://github.com/TommyLemon/APIJSON-Android-RxJava) Practical project mimicking WeChat Moments updates, based on ZBLibrary(UI) + APIJSON(HTTP) + RxJava(Data) + +[Android-ZBLibrary](https://github.com/TommyLemon/Android-ZBLibrary) 🔥 An Android MVP Framework with many demos, detailed documents, simple usages and strict codes + +[apijson-dynamic-datasource](https://github.com/wb04307201/apijson-dynamic-datasource) Based on APIJSON, this demo demonstrates dynamic data source switching and batch operations on the same data source to ensure transaction consistency + +[xyerp](https://gitee.com/yinjg1997/xyerp) Low-code ERP based on APIJSON + +[quick-boot](https://github.com/csx-bill/quick-boot/tree/master) Low-code system based on Spring Cloud 2022 + Spring Boot 3 + AMIS + APIJSON + +[apijson-query-spring-boot-starter](https://gitee.com/mingbaobaba/apijson-query-spring-boot-starter) A plugin for quickly building APIJSON query conditions + +[apijson-builder](https://github.com/yeli19950109/apijson-builder) A simplified TypeScript wrapper for APIJSON, easier to remember than directly constructing query JSON + +[lanmuc](https://gitee.com/element-admin/lanmuc) A platform for producing low-code backend APIs, compatible with both configuration-based and code-based APIs, enabling rapid API production and project deployment + +[review_plan](https://gitee.com/PPXcodeTry/review_plan) Review Reminder Web Version (Java Technology Practice Project) + +[apijson-nutz](https://github.com/vincent109/apijson-nutz) Demo for APIJSON + Nutz + NutzBoot + +[apijson-spring-boot](https://gitee.com/yunjiao-source/apijson-spring-boot) Springboot3 for APIJSON, using YAML to simplify configuration + +[APIJSONServer](https://github.com/cyber2jie/APIJSONServer) Data server based on APIJSON + +Thank you to all the enthusiastic authors for the contributions~
+### Please give them a ⭐ Star(on the top right) to support their hard works!
diff --git a/context7.json b/context7.json new file mode 100644 index 00000000..7059d93f --- /dev/null +++ b/context7.json @@ -0,0 +1,4 @@ +{ + "url": "https://context7.com/tencent/apijson", + "public_key": "pk_hqcWKqm5UmWzQouumDz3F" +}