diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java index a4c01bdb..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); @@ -894,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 @@ -1011,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() { @@ -1084,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 @@ -1172,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 @@ -1389,6 +1426,10 @@ public String getQuote() { // MongoDB 同时支持 `tbl` 反引号 和 "col" if(isElasticsearch() || isManticore() || isIoTDB() || isSurrealDB()) { return ""; } + KingbaseSQLDialect kingbaseDialect = KingbaseSQLDialect.from(gainSQLDatabase()); + if (kingbaseDialect.isKingbase()) { + return kingbaseDialect.getIdentifierQuote(); + } return isMySQL() || isMariaDB() || isTiDB() || isClickHouse() || isTDengine() || isMilvus() || isDoris() || isStarRocks() ? "`" : "\""; } @@ -1525,7 +1566,7 @@ public AbstractSQLConfig setTable(String table) { //Table已经在Parse } public String gainAs() { - return isOracle() || isManticore() ? " " : " AS "; + return isOracle() || isKingBaseOracle() || isManticore() ? " " : " AS "; } @Override @@ -2074,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); @@ -3073,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 @@ -4014,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("{")) { @@ -4302,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()) { @@ -4624,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()) + ")"); } @@ -4671,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<>(); } } @@ -4725,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: @@ -4996,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) @@ -5010,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)){ @@ -5053,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的分页获取 @@ -5358,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()) { @@ -5414,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 ? ") " : ""); @@ -5545,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 无效 @@ -6177,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/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 89180b86..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 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 e9ae6bb4..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; } 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)); + } +}