BaseJdbcExecutor.java
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.jdbc;
import org.apache.doris.cloud.security.SecurityChecker;
import org.apache.doris.common.exception.InternalException;
import org.apache.doris.common.jni.vec.ColumnType;
import org.apache.doris.common.jni.vec.ColumnValueConverter;
import org.apache.doris.common.jni.vec.VectorColumn;
import org.apache.doris.common.jni.vec.VectorTable;
import org.apache.doris.thrift.TJdbcExecutorCtorParams;
import org.apache.doris.thrift.TJdbcOperation;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.codec.binary.Hex;
import org.apache.log4j.Logger;
import org.apache.thrift.TDeserializer;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.semver4j.Semver;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public abstract class BaseJdbcExecutor implements JdbcExecutor {
private static final Logger LOG = Logger.getLogger(BaseJdbcExecutor.class);
private static final TBinaryProtocol.Factory PROTOCOL_FACTORY = new TBinaryProtocol.Factory();
private HikariDataSource hikariDataSource = null;
private final byte[] hikariDataSourceLock = new byte[0];
private ClassLoader classLoader = null;
private Connection conn = null;
protected JdbcDataSourceConfig config;
protected PreparedStatement preparedStatement = null;
protected Statement stmt = null;
protected ResultSet resultSet = null;
protected ResultSetMetaData resultSetMetaData = null;
protected List<Object[]> block = null;
protected VectorTable outputTable = null;
protected int batchSizeNum = 0;
protected int curBlockRows = 0;
protected String jdbcDriverVersion;
private static final Map<URL, ClassLoader> classLoaderMap = Maps.newConcurrentMap();
// col name(lowercase) -> index in resultSetMetaData
// this map is only used for "query()" tvf, so only valid if isTvf is true.
// Because for "query()" tvf, the sql string is written by user, so the column name in resultSetMetaData
// maybe larger than the column name in outputTable.
// For example, if the sql is "select a from query('select a,b from tbl')",
// the column num in resultSetMetaData is 2, but the outputTable only has 1 column "a".
// But if the sql is "select a from (select a,b from tbl)x",
// the column num in resultSetMetaData is 1, and the outputTable also has 1 column "a".
// Because the planner will do the column pruning before generating the sql string.
// So, for query() tvf, we need to map the column name in outputTable to the column index in resultSetMetaData.
private Map<String, Integer> resultSetColumnMap = null;
private boolean isTvf = false;
public BaseJdbcExecutor(byte[] thriftParams) throws Exception {
setJdbcDriverSystemProperties();
TJdbcExecutorCtorParams request = new TJdbcExecutorCtorParams();
TDeserializer deserializer = new TDeserializer(PROTOCOL_FACTORY);
try {
deserializer.deserialize(request, thriftParams);
} catch (TException e) {
throw new InternalException(e.getMessage());
}
this.config = new JdbcDataSourceConfig()
.setCatalogId(request.catalog_id)
.setJdbcUser(request.jdbc_user)
.setJdbcPassword(request.jdbc_password)
.setJdbcUrl(request.jdbc_url)
.setJdbcDriverUrl(request.driver_path)
.setJdbcDriverClass(request.jdbc_driver_class)
.setJdbcDriverChecksum(request.jdbc_driver_checksum)
.setBatchSize(request.batch_size)
.setOp(request.op)
.setTableType(request.table_type)
.setConnectionPoolMinSize(request.connection_pool_min_size)
.setConnectionPoolMaxSize(request.connection_pool_max_size)
.setConnectionPoolMaxWaitTime(request.connection_pool_max_wait_time)
.setConnectionPoolMaxLifeTime(request.connection_pool_max_life_time)
.setConnectionPoolKeepAlive(request.connection_pool_keep_alive);
JdbcDataSource.getDataSource().setCleanupInterval(request.connection_pool_cache_clear_time);
init(config, request.statement);
this.jdbcDriverVersion = getJdbcDriverVersion();
this.isTvf = request.isSetIsTvf() ? request.is_tvf : false;
}
public void close() throws Exception {
if (outputTable != null) {
outputTable.close();
}
try {
if (stmt != null && !stmt.isClosed()) {
try {
stmt.cancel();
} catch (SQLException e) {
LOG.warn("Cannot cancelling statement: ", e);
}
}
if (conn != null && resultSet != null) {
abortReadConnection(conn, resultSet);
}
} finally {
closeResources(resultSet, stmt, conn);
if (config.getConnectionPoolMinSize() == 0 && hikariDataSource != null) {
hikariDataSource.close();
JdbcDataSource.getDataSource().getSourcesMap().remove(config.createCacheKey());
hikariDataSource = null;
}
}
}
private void closeResources(Object... resources) {
for (Object resource : resources) {
if (resource != null) {
try {
if (resource instanceof ResultSet) {
((ResultSet) resource).close();
} else if (resource instanceof Statement) {
((Statement) resource).close();
} else if (resource instanceof Connection) {
((Connection) resource).close();
}
} catch (Exception e) {
LOG.warn("Cannot close resource: ", e);
}
}
}
}
protected void abortReadConnection(Connection connection, ResultSet resultSet)
throws SQLException {
}
protected void setJdbcDriverSystemProperties() {
System.setProperty("com.zaxxer.hikari.useWeakReferences", "true");
}
public void cleanDataSource() {
if (hikariDataSource != null) {
hikariDataSource.close();
JdbcDataSource.getDataSource().getSourcesMap().remove(config.createCacheKey());
hikariDataSource = null;
}
}
public void testConnection() throws JdbcExecutorException {
try {
resultSet = ((PreparedStatement) stmt).executeQuery();
if (!resultSet.next()) {
throw new JdbcExecutorException(
"Failed to test connection in BE: query executed but returned no results.");
}
} catch (SQLException e) {
throw new JdbcExecutorException("Failed to test connection in BE: ", e);
}
}
public int read() throws JdbcExecutorException {
try {
resultSet = ((PreparedStatement) stmt).executeQuery();
resultSetMetaData = resultSet.getMetaData();
int columnCount = resultSetMetaData.getColumnCount();
block = new ArrayList<>(columnCount);
return columnCount;
} catch (SQLException e) {
throw new JdbcExecutorException("JDBC executor sql has error: ", e);
}
}
public long getBlockAddress(int batchSize, Map<String, String> outputParams) throws JdbcExecutorException {
try {
if (outputTable != null) {
outputTable.close();
}
outputTable = VectorTable.createWritableTable(outputParams, 0);
String isNullableString = outputParams.get("is_nullable");
String replaceString = outputParams.get("replace_string");
if (isNullableString == null || replaceString == null) {
throw new IllegalArgumentException(
"Output parameters 'is_nullable' and 'replace_string' are required.");
}
String[] nullableList = isNullableString.split(",");
String[] replaceStringList = replaceString.split(",");
curBlockRows = 0;
int outputColumnCount = outputTable.getColumns().length;
initializeBlock(outputColumnCount, replaceStringList, batchSize, outputTable);
// the resultSetColumnMap is only for "query()" tvf
if (this.isTvf && this.resultSetColumnMap == null) {
this.resultSetColumnMap = new HashMap<>();
int resultSetColumnCount = resultSetMetaData.getColumnCount();
for (int i = 1; i <= resultSetColumnCount; i++) {
String columnName = resultSetMetaData.getColumnName(i).trim().toLowerCase();
resultSetColumnMap.put(columnName, i);
}
}
do {
for (int i = 0; i < outputColumnCount; ++i) {
String outputColumnName = outputTable.getFields()[i];
int columnIndex = getRealColumnIndex(outputColumnName, i);
if (columnIndex > -1) {
ColumnType type = convertTypeIfNecessary(i, outputTable.getColumnType(i), replaceStringList);
block.get(i)[curBlockRows] = getColumnValue(columnIndex, type, replaceStringList);
} else {
throw new RuntimeException("Column not found in resultSetColumnMap: " + outputColumnName);
}
}
curBlockRows++;
} while (curBlockRows < batchSize && resultSet.next());
for (int i = 0; i < outputColumnCount; ++i) {
String outputColumnName = outputTable.getFields()[i];
int columnIndex = getRealColumnIndex(outputColumnName, i);
if (columnIndex > -1) {
ColumnType type = outputTable.getColumnType(i);
Object[] columnData = block.get(i);
Class<?> componentType = columnData.getClass().getComponentType();
Object[] newColumn = (Object[]) Array.newInstance(componentType, curBlockRows);
System.arraycopy(columnData, 0, newColumn, 0, curBlockRows);
boolean isNullable = Boolean.parseBoolean(nullableList[i]);
outputTable.appendData(
i,
newColumn,
getOutputConverter(type, replaceStringList[i]),
isNullable
);
} else {
throw new RuntimeException("Column not found in resultSetColumnMap: " + outputColumnName);
}
}
} catch (Exception e) {
LOG.warn("jdbc get block address exception: ", e);
throw new JdbcExecutorException("jdbc get block address: ", e);
} finally {
block.clear();
}
return outputTable.getMetaAddress();
}
private int getRealColumnIndex(String outputColumnName, int indexInOutputTable) {
// -1 because ResultSetMetaData column index starts from 1, but index in outputTable starts from 0.
int columnIndex = this.isTvf
? resultSetColumnMap.getOrDefault(outputColumnName.toLowerCase(), 0) - 1 : indexInOutputTable;
return columnIndex;
}
protected void initializeBlock(int columnCount, String[] replaceStringList, int batchSizeNum,
VectorTable outputTable) {
for (int i = 0; i < columnCount; ++i) {
block.add(outputTable.getColumn(i).newObjectContainerArray(batchSizeNum));
}
}
public int write(Map<String, String> params) throws JdbcExecutorException {
VectorTable batchTable = VectorTable.createReadableTable(params);
// Can't release or close batchTable, it's released by c++
try {
insert(batchTable);
} catch (SQLException e) {
throw new JdbcExecutorException("JDBC executor sql has error: ", e);
}
return batchTable.getNumRows();
}
public void openTrans() throws JdbcExecutorException {
try {
if (conn != null) {
conn.setAutoCommit(false);
}
} catch (SQLException e) {
throw new JdbcExecutorException("JDBC executor open transaction has error: ", e);
}
}
public void commitTrans() throws JdbcExecutorException {
try {
if (conn != null) {
conn.commit();
}
} catch (SQLException e) {
throw new JdbcExecutorException("JDBC executor commit transaction has error: ", e);
}
}
public void rollbackTrans() throws JdbcExecutorException {
try {
if (conn != null) {
conn.rollback();
}
} catch (SQLException e) {
throw new JdbcExecutorException("JDBC executor rollback transaction has error: ", e);
}
}
public int getCurBlockRows() {
return curBlockRows;
}
public boolean hasNext() throws JdbcExecutorException {
try {
if (resultSet == null) {
return false;
}
return resultSet.next();
} catch (SQLException e) {
throw new JdbcExecutorException("resultSet to get next error: ", e);
}
}
private void init(JdbcDataSourceConfig config, String sql) throws JdbcExecutorException {
ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
String hikariDataSourceKey = config.createCacheKey();
try {
initializeClassLoader(config);
Thread.currentThread().setContextClassLoader(classLoader);
hikariDataSource = JdbcDataSource.getDataSource().getSource(hikariDataSourceKey);
if (hikariDataSource == null) {
synchronized (hikariDataSourceLock) {
hikariDataSource = JdbcDataSource.getDataSource().getSource(hikariDataSourceKey);
if (hikariDataSource == null) {
long start = System.currentTimeMillis();
HikariDataSource ds = new HikariDataSource();
ds.setDriverClassName(config.getJdbcDriverClass());
ds.setJdbcUrl(SecurityChecker.getInstance().getSafeJdbcUrl(config.getJdbcUrl()));
ds.setUsername(config.getJdbcUser());
ds.setPassword(config.getJdbcPassword());
ds.setMinimumIdle(config.getConnectionPoolMinSize()); // default 1
ds.setMaximumPoolSize(config.getConnectionPoolMaxSize()); // default 10
ds.setConnectionTimeout(config.getConnectionPoolMaxWaitTime()); // default 5000
ds.setMaxLifetime(config.getConnectionPoolMaxLifeTime()); // default 30 min
ds.setIdleTimeout(config.getConnectionPoolMaxLifeTime() / 2L); // default 15 min
setValidationQuery(ds);
if (config.isConnectionPoolKeepAlive()) {
ds.setKeepaliveTime(config.getConnectionPoolMaxLifeTime() / 5L); // default 6 min
}
hikariDataSource = ds;
JdbcDataSource.getDataSource().putSource(hikariDataSourceKey, hikariDataSource);
LOG.info("JdbcClient set"
+ " ConnectionPoolMinSize = " + config.getConnectionPoolMinSize()
+ ", ConnectionPoolMaxSize = " + config.getConnectionPoolMaxSize()
+ ", ConnectionPoolMaxWaitTime = " + config.getConnectionPoolMaxWaitTime()
+ ", ConnectionPoolMaxLifeTime = " + config.getConnectionPoolMaxLifeTime()
+ ", ConnectionPoolKeepAlive = " + config.isConnectionPoolKeepAlive());
LOG.info("init datasource [" + (config.getJdbcUrl() + config.getJdbcUser()) + "] cost: " + (
System.currentTimeMillis() - start) + " ms");
}
}
}
long start = System.currentTimeMillis();
conn = hikariDataSource.getConnection();
LOG.info("get connection [" + (config.getJdbcUrl() + config.getJdbcUser()) + "] cost: " + (
System.currentTimeMillis() - start)
+ " ms");
initializeStatement(conn, config, sql);
} catch (MalformedURLException e) {
throw new JdbcExecutorException("MalformedURLException to load class about "
+ config.getJdbcDriverUrl(), e);
} catch (SQLException e) {
throw new JdbcExecutorException("Initialize datasource failed: ", e);
} catch (FileNotFoundException e) {
throw new JdbcExecutorException("FileNotFoundException failed: ", e);
} catch (Exception e) {
String msg = e.getMessage();
// If driver class loading failed (Hikari wraps it), clear stale cache and prompt retry
if (msg != null && msg.contains("Failed to load driver class")) {
try {
URL url = new URL(config.getJdbcDriverUrl());
classLoaderMap.remove(url);
// Prompt user to verify driver validity and retry
throw new JdbcExecutorException(
String.format("Failed to load driver class `%s`. "
+ "Please check that the driver JAR is valid and retry.",
config.getJdbcDriverClass()), e);
} catch (MalformedURLException ignore) {
// ignore invalid URL when cleaning cache
}
}
throw new JdbcExecutorException("Initialize datasource failed: ", e);
} finally {
Thread.currentThread().setContextClassLoader(oldClassLoader);
}
}
private synchronized void initializeClassLoader(JdbcDataSourceConfig config) {
try {
URL[] urls = {new URL(config.getJdbcDriverUrl())};
if (classLoaderMap.containsKey(urls[0]) && classLoaderMap.get(urls[0]) != null) {
this.classLoader = classLoaderMap.get(urls[0]);
} else {
String expectedChecksum = config.getJdbcDriverChecksum();
String actualChecksum = computeObjectChecksum(urls[0].toString(), null);
if (!expectedChecksum.equals(actualChecksum)) {
throw new RuntimeException("Checksum mismatch for JDBC driver.");
}
ClassLoader parent = getClass().getClassLoader();
this.classLoader = URLClassLoader.newInstance(urls, parent);
classLoaderMap.put(urls[0], this.classLoader);
}
} catch (MalformedURLException e) {
throw new RuntimeException("Failed to load JDBC driver from path: "
+ config.getJdbcDriverUrl(), e);
}
}
public static String computeObjectChecksum(String urlStr, String encodedAuthInfo) {
try (InputStream inputStream = getInputStreamFromUrl(urlStr, encodedAuthInfo, 10000, 10000)) {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] buf = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buf)) != -1) {
digest.update(buf, 0, bytesRead);
}
return Hex.encodeHexString(digest.digest());
} catch (IOException | NoSuchAlgorithmException e) {
throw new RuntimeException("Compute driver checksum from url: " + urlStr
+ " encountered an error: " + e.getMessage());
}
}
public static InputStream getInputStreamFromUrl(String urlStr, String encodedAuthInfo, int connectTimeoutMs,
int readTimeoutMs) throws IOException {
try {
URL url = new URL(urlStr);
URLConnection conn = url.openConnection();
if (encodedAuthInfo != null) {
conn.setRequestProperty("Authorization", "Basic " + encodedAuthInfo);
}
conn.setConnectTimeout(connectTimeoutMs);
conn.setReadTimeout(readTimeoutMs);
return conn.getInputStream();
} catch (Exception e) {
throw new IOException("Failed to open URL connection: " + urlStr, e);
}
}
protected void setValidationQuery(HikariDataSource ds) {
ds.setConnectionTestQuery("SELECT 1");
}
protected void initializeStatement(Connection conn, JdbcDataSourceConfig config, String sql) throws SQLException {
if (config.getOp() == TJdbcOperation.READ) {
conn.setAutoCommit(false);
Preconditions.checkArgument(sql != null, "SQL statement cannot be null for READ operation.");
stmt = conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(config.getBatchSize()); // set fetch size to batch size
batchSizeNum = config.getBatchSize();
} else {
Preconditions.checkArgument(sql != null, "SQL statement cannot be null for WRITE operation.");
LOG.info("Insert SQL: " + sql);
preparedStatement = conn.prepareStatement(sql);
}
}
protected abstract Object getColumnValue(int columnIndex, ColumnType type, String[] replaceStringList)
throws SQLException;
/**
* Some special column types (like bitmap/hll in Doris) may need to be converted to string.
* Subclass can override this method to handle such conversions.
*
* @param outputIdx
* @param origType
* @param replaceStringList
* @return
*/
protected ColumnType convertTypeIfNecessary(int outputIdx, ColumnType origType, String[] replaceStringList) {
return origType;
}
/*
| Type | Java Array Type |
|---------------------------------------------|----------------------------|
| BOOLEAN | Boolean[] |
| TINYINT | Byte[] |
| SMALLINT | Short[] |
| INT | Integer[] |
| BIGINT | Long[] |
| LARGEINT | BigInteger[] |
| FLOAT | Float[] |
| DOUBLE | Double[] |
| DECIMALV2, DECIMAL32, DECIMAL64, DECIMAL128 | BigDecimal[] |
| DATE, DATEV2 | LocalDate[] |
| DATETIME, DATETIMEV2 | LocalDateTime[] |
| CHAR, VARCHAR, STRING | String[] |
| ARRAY | List<Object>[] |
| MAP | Map<Object, Object>[] |
| STRUCT | Map<String, Object>[] |
*/
protected abstract ColumnValueConverter getOutputConverter(ColumnType columnType, String replaceString);
protected ColumnValueConverter createConverter(
Function<Object, ?> converterFunction, Class<?> type) {
return (Object[] columnData) -> {
Object[] result = (Object[]) Array.newInstance(type, columnData.length);
for (int i = 0; i < columnData.length; i++) {
result[i] = columnData[i] != null ? converterFunction.apply(columnData[i]) : null;
}
return result;
};
}
private int insert(VectorTable data) throws SQLException {
for (int i = 0; i < data.getNumRows(); ++i) {
for (int j = 0; j < data.getColumns().length; ++j) {
insertColumn(i, j, data.getColumns()[j]);
}
preparedStatement.addBatch();
}
preparedStatement.executeBatch();
preparedStatement.clearBatch();
return data.getNumRows();
}
private void insertColumn(int rowIdx, int colIdx, VectorColumn column) throws SQLException {
int parameterIndex = colIdx + 1;
ColumnType.Type dorisType = column.getColumnPrimitiveType();
if (column.isNullAt(rowIdx)) {
insertNullColumn(parameterIndex, dorisType);
return;
}
switch (dorisType) {
case BOOLEAN:
preparedStatement.setBoolean(parameterIndex, column.getBoolean(rowIdx));
break;
case TINYINT:
preparedStatement.setByte(parameterIndex, column.getByte(rowIdx));
break;
case SMALLINT:
preparedStatement.setShort(parameterIndex, column.getShort(rowIdx));
break;
case INT:
preparedStatement.setInt(parameterIndex, column.getInt(rowIdx));
break;
case BIGINT:
preparedStatement.setLong(parameterIndex, column.getLong(rowIdx));
break;
case LARGEINT:
preparedStatement.setObject(parameterIndex, column.getBigInteger(rowIdx));
break;
case FLOAT:
preparedStatement.setFloat(parameterIndex, column.getFloat(rowIdx));
break;
case DOUBLE:
preparedStatement.setDouble(parameterIndex, column.getDouble(rowIdx));
break;
case DECIMALV2:
case DECIMAL32:
case DECIMAL64:
case DECIMAL128:
preparedStatement.setBigDecimal(parameterIndex, column.getDecimal(rowIdx));
break;
case DATEV2:
preparedStatement.setDate(parameterIndex, Date.valueOf(column.getDate(rowIdx)));
break;
case DATETIMEV2:
preparedStatement.setTimestamp(
parameterIndex, Timestamp.valueOf(column.getDateTime(rowIdx)));
break;
case CHAR:
case VARCHAR:
case STRING:
case BINARY:
preparedStatement.setString(parameterIndex, column.getStringWithOffset(rowIdx));
break;
default:
throw new RuntimeException("Unknown type value: " + dorisType);
}
}
private void insertNullColumn(int parameterIndex, ColumnType.Type dorisType)
throws SQLException {
switch (dorisType) {
case BOOLEAN:
preparedStatement.setNull(parameterIndex, Types.BOOLEAN);
break;
case TINYINT:
preparedStatement.setNull(parameterIndex, Types.TINYINT);
break;
case SMALLINT:
preparedStatement.setNull(parameterIndex, Types.SMALLINT);
break;
case INT:
preparedStatement.setNull(parameterIndex, Types.INTEGER);
break;
case BIGINT:
preparedStatement.setNull(parameterIndex, Types.BIGINT);
break;
case LARGEINT:
preparedStatement.setNull(parameterIndex, Types.JAVA_OBJECT);
break;
case FLOAT:
preparedStatement.setNull(parameterIndex, Types.FLOAT);
break;
case DOUBLE:
preparedStatement.setNull(parameterIndex, Types.DOUBLE);
break;
case DECIMALV2:
case DECIMAL32:
case DECIMAL64:
case DECIMAL128:
preparedStatement.setNull(parameterIndex, Types.DECIMAL);
break;
case DATEV2:
preparedStatement.setNull(parameterIndex, Types.DATE);
break;
case DATETIMEV2:
preparedStatement.setNull(parameterIndex, Types.TIMESTAMP);
break;
case CHAR:
case VARCHAR:
case STRING:
case BINARY:
preparedStatement.setNull(parameterIndex, Types.VARCHAR);
break;
default:
throw new RuntimeException("Unknown type value: " + dorisType);
}
}
private String getJdbcDriverVersion() {
try {
if (conn != null) {
DatabaseMetaData metaData = conn.getMetaData();
return metaData.getDriverVersion();
} else {
return null;
}
} catch (SQLException e) {
LOG.warn("Failed to retrieve JDBC Driver version", e);
return null;
}
}
protected boolean isJdbcVersionGreaterThanOrEqualTo(String version) {
Semver currentVersion = Semver.coerce(jdbcDriverVersion);
Semver targetVersion = Semver.coerce(version);
if (currentVersion != null && targetVersion != null) {
return currentVersion.isGreaterThanOrEqualTo(targetVersion);
} else {
return false;
}
}
protected String trimSpaces(String str) {
int end = str.length() - 1;
while (end >= 0 && str.charAt(end) == ' ') {
end--;
}
return str.substring(0, end + 1);
}
protected String timeToString(java.sql.Time time) {
if (time == null) {
return null;
} else {
long milliseconds = time.getTime() % 1000L;
if (milliseconds > 0) {
return String.format("%s.%03d", time, milliseconds);
} else {
return time.toString();
}
}
}
protected String defaultByteArrayToHexString(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex.toUpperCase());
}
return hexString.toString();
}
}