com.clickhouse.jdbc.internal.ClickHouseConnectionImpl Maven / Gradle / Ivy
The newest version!
package com.clickhouse.jdbc.internal;
import java.io.Serializable;
import java.net.URI;
import java.sql.ClientInfoStatus;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Savepoint;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.TimeZone;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import com.clickhouse.client.ClickHouseClient;
import com.clickhouse.client.ClickHouseClientBuilder;
import com.clickhouse.client.ClickHouseConfig;
import com.clickhouse.client.ClickHouseException;
import com.clickhouse.client.ClickHouseNode;
import com.clickhouse.client.ClickHouseNodeSelector;
import com.clickhouse.client.ClickHouseNodes;
import com.clickhouse.client.ClickHouseParameterizedQuery;
import com.clickhouse.client.ClickHouseRequest;
import com.clickhouse.client.ClickHouseResponse;
import com.clickhouse.client.ClickHouseTransaction;
import com.clickhouse.client.ClickHouseRequest.Mutation;
import com.clickhouse.client.config.ClickHouseClientOption;
import com.clickhouse.client.http.config.ClickHouseHttpOption;
import com.clickhouse.config.ClickHouseDefaultOption;
import com.clickhouse.config.ClickHouseOption;
import com.clickhouse.config.ClickHouseRenameMethod;
import com.clickhouse.data.ClickHouseChecker;
import com.clickhouse.data.ClickHouseColumn;
import com.clickhouse.data.ClickHouseDataType;
import com.clickhouse.data.ClickHouseFormat;
import com.clickhouse.data.ClickHouseRecord;
import com.clickhouse.data.ClickHouseUtils;
import com.clickhouse.data.ClickHouseValues;
import com.clickhouse.data.ClickHouseVersion;
import com.clickhouse.logging.Logger;
import com.clickhouse.logging.LoggerFactory;
import com.clickhouse.jdbc.ClickHouseConnection;
import com.clickhouse.jdbc.ClickHouseDatabaseMetaData;
import com.clickhouse.jdbc.ClickHouseDriver;
import com.clickhouse.jdbc.ClickHouseStatement;
import com.clickhouse.jdbc.JdbcConfig;
import com.clickhouse.jdbc.JdbcParameterizedQuery;
import com.clickhouse.jdbc.JdbcParseHandler;
import com.clickhouse.jdbc.SqlExceptionUtils;
import com.clickhouse.jdbc.JdbcWrapper;
import com.clickhouse.jdbc.internal.ClickHouseJdbcUrlParser.ConnectionInfo;
import com.clickhouse.jdbc.parser.ClickHouseSqlParser;
import com.clickhouse.jdbc.parser.ClickHouseSqlStatement;
import com.clickhouse.jdbc.parser.ParseHandler;
import com.clickhouse.jdbc.parser.StatementType;
public class ClickHouseConnectionImpl extends JdbcWrapper implements ClickHouseConnection {
private static final Logger log = LoggerFactory.getLogger(ClickHouseConnectionImpl.class);
static final String SETTING_READONLY = "readonly";
static final String SETTING_MAX_INSERT_BLOCK = "max_insert_block_size";
static final String SETTING_LW_DELETE = "allow_experimental_lightweight_delete";
static final ClickHouseDefaultOption CUSTOM_CONFIG = new ClickHouseDefaultOption("custom_jdbc_config",
"custom_jdbc_config");
private static final String SQL_GET_SERVER_INFO = "select currentUser() user, timezone() timezone, version() version, "
+ getSetting(SETTING_READONLY, ClickHouseDataType.UInt8) + ", "
+ getSetting(ClickHouseTransaction.SETTING_THROW_ON_UNSUPPORTED_QUERY_INSIDE_TRANSACTION,
ClickHouseDataType.Int8)
+ ", "
+ getSetting(ClickHouseTransaction.SETTING_WAIT_CHANGES_BECOME_VISIBLE_AFTER_COMMIT_MODE,
ClickHouseDataType.String)
+ ","
+ getSetting(ClickHouseTransaction.SETTING_IMPLICIT_TRANSACTION, ClickHouseDataType.Int8) + ", "
+ getSetting(SETTING_MAX_INSERT_BLOCK, ClickHouseDataType.UInt64) + ", "
+ getSetting(SETTING_LW_DELETE, ClickHouseDataType.Int8) + ", "
+ getSetting((String) CUSTOM_CONFIG.getEffectiveDefaultValue(), ClickHouseDataType.String)
+ " FORMAT RowBinaryWithNamesAndTypes";
private static String getSetting(String setting, ClickHouseDataType type) {
return getSetting(setting, type, null);
}
private static String getSetting(String setting, ClickHouseDataType type, String defaultValue) {
StringBuilder builder = new StringBuilder();
if (type == ClickHouseDataType.String) {
builder.append("(ifnull((select value from system.settings where name = '").append(setting)
.append("'), ");
} else {
builder.append("to").append(type.name())
.append("(ifnull((select value from system.settings where name = '").append(setting)
.append("'), ");
}
if (ClickHouseChecker.isNullOrEmpty(defaultValue)) {
builder.append(type.getMaxPrecision() > 0 ? (type.isSigned() ? "'-1'" : "'0'") : "''");
} else {
builder.append('\'').append(defaultValue).append('\'');
}
return builder.append(")) as ").append(setting).toString();
}
protected static ClickHouseRecord getServerInfo(ClickHouseNode node, ClickHouseRequest> request,
boolean createDbIfNotExist) throws SQLException {
ClickHouseRequest> newReq = request.copy().option(ClickHouseClientOption.RENAME_RESPONSE_COLUMN,
ClickHouseRenameMethod.NONE);
if (!createDbIfNotExist) { // in case the database does not exist
newReq.option(ClickHouseClientOption.DATABASE, "");
}
try (ClickHouseResponse response = newReq.option(ClickHouseClientOption.ASYNC, false)
.option(ClickHouseClientOption.COMPRESS, false)
.option(ClickHouseClientOption.DECOMPRESS, false)
.option(ClickHouseClientOption.FORMAT, ClickHouseFormat.RowBinaryWithNamesAndTypes)
.query(SQL_GET_SERVER_INFO).executeAndWait()) {
return response.firstRecord();
} catch (Exception e) {
SQLException sqlExp = SqlExceptionUtils.handle(e);
if (createDbIfNotExist && sqlExp.getErrorCode() == 81) {
String db = node.getDatabase(request.getConfig());
try (ClickHouseResponse resp = newReq.use("")
.query(new StringBuilder("CREATE DATABASE IF NOT EXISTS `")
.append(ClickHouseUtils.escape(db, '`')).append('`').toString())
.executeAndWait()) {
return getServerInfo(node, request, false);
} catch (SQLException ex) {
throw ex;
} catch (Exception ex) {
throw SqlExceptionUtils.handle(ex);
}
} else {
throw sqlExp;
}
}
}
private final JdbcConfig jdbcConf;
private final ClickHouseClient client;
private final ClickHouseRequest> clientRequest;
private boolean autoCommit;
private boolean closed;
private String database;
private boolean readOnly;
private int networkTimeout;
private int rsHoldability;
private int txIsolation;
private final Optional clientTimeZone;
private final Calendar defaultCalendar;
private final TimeZone jvmTimeZone;
private final TimeZone serverTimeZone;
private final ClickHouseVersion serverVersion;
private final String user;
private final int initialReadOnly;
private final int initialNonTxQuerySupport;
private final String initialTxCommitWaitMode;
private final int initialImplicitTx;
private final long initialMaxInsertBlockSize;
// 0 - unsupported; 1 - experimental support; 2 - always support
private final int initialDeleteSupport;
private final Map> typeMap;
private final AtomicReference txRef;
protected JdbcTransaction createTransaction() throws SQLException {
if (!isTransactionSupported()) {
return new JdbcTransaction(null);
}
try {
ClickHouseTransaction tx = clientRequest.getManager().createTransaction(clientRequest);
tx.begin();
// if (txIsolation == Connection.TRANSACTION_READ_UNCOMMITTED) {
// tx.snapshot(ClickHouseTransaction.CSN_EVERYTHING_VISIBLE);
// }
clientRequest.transaction(tx);
return new JdbcTransaction(tx);
} catch (ClickHouseException e) {
throw SqlExceptionUtils.handle(e);
}
}
protected JdbcSavepoint createSavepoint() {
return new JdbcSavepoint(1, "name");
}
/**
* Checks if the connection is open or not.
*
* @throws SQLException when the connection is closed
*/
protected void ensureOpen() throws SQLException {
if (closed) {
throw SqlExceptionUtils.clientError("Cannot operate on a closed connection");
}
}
/**
* Checks if a feature can be supported or not.
*
* @param feature non-empty feature name
* @param silent whether to show warning in log or throw unsupported exception
* @throws SQLException when the feature is not supported and silent is
* {@code false}
*/
protected void ensureSupport(String feature, boolean silent) throws SQLException {
String msg = feature + " is not supported";
if (jdbcConf.isJdbcCompliant()) {
if (silent) {
log.debug("[JDBC Compliant Mode] %s. You may change %s to false to throw SQLException instead.", msg,
JdbcConfig.PROP_JDBC_COMPLIANT);
} else {
log.warn("[JDBC Compliant Mode] %s. You may change %s to false to throw SQLException instead.", msg,
JdbcConfig.PROP_JDBC_COMPLIANT);
}
} else if (!silent) {
throw SqlExceptionUtils.unsupportedError(msg);
}
}
protected void ensureTransactionSupport() throws SQLException {
if (!isTransactionSupported()) {
ensureSupport("Transaction", false);
}
}
protected List getTableColumns(String dbName, String tableName, String columns)
throws SQLException {
if (tableName == null || columns == null) {
throw SqlExceptionUtils.clientError("Failed to extract table and columns from the query");
}
if (columns.isEmpty()) {
columns = "*";
} else {
columns = columns.substring(1); // remove the leading bracket
}
StringBuilder builder = new StringBuilder();
builder.append("SELECT ").append(columns).append(" FROM ");
if (!ClickHouseChecker.isNullOrEmpty(dbName)) {
builder.append('`').append(ClickHouseUtils.escape(dbName, '`')).append('`').append('.');
}
builder.append('`').append(ClickHouseUtils.escape(tableName, '`')).append('`').append(" WHERE 0");
List list;
try (ClickHouseResponse resp = clientRequest.copy().format(ClickHouseFormat.RowBinaryWithNamesAndTypes)
.option(ClickHouseClientOption.RENAME_RESPONSE_COLUMN, ClickHouseRenameMethod.NONE)
.query(builder.toString()).executeAndWait()) {
list = resp.getColumns();
} catch (Exception e) {
throw SqlExceptionUtils.handle(e);
}
return list;
}
protected String getDatabase() throws SQLException {
ensureOpen();
return getCurrentDatabase();
}
// for testing purpose
final JdbcTransaction getJdbcTrasaction() {
return txRef.get();
}
public ClickHouseConnectionImpl(String url) throws SQLException {
this(url, new Properties());
}
public ClickHouseConnectionImpl(String url, Properties properties) throws SQLException {
this(ClickHouseJdbcUrlParser.parse(url, properties));
}
public ClickHouseConnectionImpl(ConnectionInfo connInfo) throws SQLException {
Properties props = connInfo.getProperties();
jvmTimeZone = TimeZone.getDefault();
if (props.get("disable_frameworks_detection") == null || !props.get("disable_frameworks_detection").toString().equalsIgnoreCase("true")) {
ClickHouseDriver.frameworksDetected = ClickHouseDriver.FrameworksDetection.getFrameworksDetected();
if (ClickHouseDriver.frameworksDetected != null)
props.setProperty(ClickHouseClientOption.PRODUCT_NAME.getKey(), props.getProperty(ClickHouseClientOption.PRODUCT_NAME.getKey()) + ClickHouseDriver.frameworksDetected);
}
ClickHouseClientBuilder clientBuilder = ClickHouseClient.builder()
.options(ClickHouseDriver.toClientOptions(props))
.defaultCredentials(connInfo.getDefaultCredentials());
ClickHouseNodes nodes = connInfo.getNodes();
final ClickHouseNode node;
final ClickHouseClient initialClient;
final ClickHouseRequest> initialRequest;
if (nodes.isSingleNode()) {
try {
node = nodes.apply(nodes.getNodeSelector());
} catch (Exception e) {
throw SqlExceptionUtils.clientError("Failed to get single-node", e);
}
initialClient = clientBuilder.nodeSelector(ClickHouseNodeSelector.of(node.getProtocol())).build();
initialRequest = initialClient.read(node);
} else {
log.debug("Selecting node from: %s", nodes);
initialClient = clientBuilder.build(); // use dummy client
initialRequest = initialClient.read(nodes);
try {
node = initialRequest.getServer();
} catch (Exception e) {
throw SqlExceptionUtils.clientError("No healthy node available", e);
}
}
log.debug("Connecting to: %s", node);
ClickHouseConfig config = initialRequest.getConfig();
String currentUser = null;
TimeZone timeZone = null;
ClickHouseVersion version = null;
ClickHouseRecord r = null;
if (config.hasServerInfo()) { // when both serverTimeZone and serverVersion are configured
timeZone = config.getServerTimeZone();
version = config.getServerVersion();
if (connInfo.getJdbcConfig().isCreateDbIfNotExist()) {
r = getServerInfo(node, initialRequest, true);
}
} else {
r = getServerInfo(node, initialRequest, connInfo.getJdbcConfig().isCreateDbIfNotExist());
currentUser = r.getValue(0).asString();
String tz = r.getValue(1).asString();
String ver = r.getValue(2).asString();
version = ClickHouseVersion.of(ver);
// https://github.com/ClickHouse/ClickHouse/commit/486d63864bcc6e15695cd3e9f9a3f83a84ec4009
if (version.check("(,20.7)")) {
throw SqlExceptionUtils.unsupportedError(
"We apologize, but this driver only works with ClickHouse servers 20.7 and above. "
+ "Please consider to upgrade your server to a more recent version.");
}
if (ClickHouseChecker.isNullOrBlank(tz)) {
tz = "UTC";
}
// tsTimeZone.hasSameRules(ClickHouseValues.UTC_TIMEZONE)
timeZone = "UTC".equals(tz) ? ClickHouseValues.UTC_TIMEZONE : TimeZone.getTimeZone(tz);
// update request and corresponding config
initialRequest.option(ClickHouseClientOption.SERVER_TIME_ZONE, tz)
.option(ClickHouseClientOption.SERVER_VERSION, ver);
}
final boolean useLightWeightDelete = version.check("[23.3,)");
if (r != null) {
initialReadOnly = r.getValue(3).asInteger();
initialNonTxQuerySupport = r.getValue(4).asInteger();
initialTxCommitWaitMode = r.getValue(5).asString().toLowerCase(Locale.ROOT);
initialImplicitTx = r.getValue(6).asInteger();
initialMaxInsertBlockSize = r.getValue(7).asLong();
initialDeleteSupport = useLightWeightDelete ? 2 : r.getValue(8).asInteger();
String customConf = ClickHouseUtils.unescape(r.getValue(9).asString());
if (ClickHouseChecker.isNullOrBlank(customConf)) {
jdbcConf = connInfo.getJdbcConfig();
client = initialClient;
clientRequest = initialRequest;
} else {
initialClient.close();
Properties newProps = ClickHouseJdbcUrlParser.newProperties();
Map options = ClickHouseUtils.extractParameters(customConf, null);
boolean resetAll = Boolean.parseBoolean(options.get("*"));
if (resetAll) {
clientBuilder.clearOptions();
} else {
newProps.putAll(connInfo.getJdbcConfig().getProperties());
newProps.putAll(props);
}
newProps.putAll(options);
jdbcConf = new JdbcConfig(newProps);
Map clientOpts = ClickHouseConfig.toClientOptions(newProps);
clientBuilder.options(clientOpts);
client = clientBuilder.build();
clientRequest = client.read(node);
if (resetAll && !initialRequest.getSettings().isEmpty()) {
clientRequest.clearSettings();
}
clientRequest.option(ClickHouseClientOption.SERVER_TIME_ZONE, timeZone.getID())
.option(ClickHouseClientOption.SERVER_VERSION, version.toString());
// two issues:
// 1) inefficient but definitely better than re-creating nodes in a cluster
// 2) client.read(node) won't work - you have to use clientRequest.copy()
for (Entry o : clientOpts.entrySet()) {
clientRequest.option(o.getKey(), o.getValue());
}
if (resetAll) {
clientRequest.freezeOptions().freezeSettings();
}
config = clientRequest.getConfig();
}
} else {
jdbcConf = connInfo.getJdbcConfig();
initialReadOnly = initialRequest.getSetting(SETTING_READONLY, 0);
initialNonTxQuerySupport = initialRequest
.getSetting(ClickHouseTransaction.SETTING_THROW_ON_UNSUPPORTED_QUERY_INSIDE_TRANSACTION, 1);
initialTxCommitWaitMode = initialRequest.getSetting(
ClickHouseTransaction.SETTING_WAIT_CHANGES_BECOME_VISIBLE_AFTER_COMMIT_MODE, "wait_unknown");
initialImplicitTx = initialRequest.getSetting(ClickHouseTransaction.SETTING_IMPLICIT_TRANSACTION, 0);
initialMaxInsertBlockSize = initialRequest.getSetting(SETTING_MAX_INSERT_BLOCK, 0L);
initialDeleteSupport = initialRequest.getSetting(SETTING_LW_DELETE, useLightWeightDelete ? 2 : 0);
client = initialClient;
clientRequest = initialRequest;
}
this.autoCommit = !jdbcConf.isJdbcCompliant() || jdbcConf.isAutoCommit();
this.closed = false;
this.database = config.getDatabase();
this.clientRequest.use(this.database);
this.readOnly = clientRequest.getSetting(SETTING_READONLY, initialReadOnly) != 0;
this.networkTimeout = 0;
this.rsHoldability = ResultSet.HOLD_CURSORS_OVER_COMMIT;
if (isTransactionSupported()) {
this.txIsolation = Connection.TRANSACTION_REPEATABLE_READ;
if (jdbcConf.isJdbcCompliant() && !this.readOnly) {
if (!this.clientRequest
.hasSetting(ClickHouseTransaction.SETTING_THROW_ON_UNSUPPORTED_QUERY_INSIDE_TRANSACTION)) {
this.clientRequest.set(ClickHouseTransaction.SETTING_THROW_ON_UNSUPPORTED_QUERY_INSIDE_TRANSACTION,
0);
}
// .set(ClickHouseTransaction.SETTING_WAIT_CHANGES_BECOME_VISIBLE_AFTER_COMMIT_MODE,
// "wait_unknown");
}
} else {
this.txIsolation = jdbcConf.isJdbcCompliant() ? Connection.TRANSACTION_READ_COMMITTED
: Connection.TRANSACTION_NONE;
}
this.user = currentUser != null ? currentUser : node.getCredentials(config).getUserName();
this.serverTimeZone = timeZone;
if (config.isUseServerTimeZone()) {
clientTimeZone = Optional.empty();
// with respect of default locale
defaultCalendar = new GregorianCalendar();
} else {
clientTimeZone = Optional.of(config.getUseTimeZone());
defaultCalendar = new GregorianCalendar(clientTimeZone.get());
}
this.serverVersion = version;
this.typeMap = new HashMap<>(jdbcConf.getTypeMap());
this.txRef = new AtomicReference<>(this.autoCommit ? null : createTransaction());
}
@Override
public String nativeSQL(String sql) throws SQLException {
ensureOpen();
// get rewritten query?
return sql;
}
@Override
public void setAutoCommit(boolean autoCommit) throws SQLException {
ensureOpen();
if (this.autoCommit == autoCommit) {
return;
}
ensureTransactionSupport();
if (this.autoCommit = autoCommit) { // commit
JdbcTransaction tx = txRef.getAndSet(null);
if (tx != null) {
tx.commit(log);
}
} else { // start new transaction
if (!txRef.compareAndSet(null, createTransaction())) {
log.warn("Not able to start a new transaction, reuse the exist one: %s", txRef.get());
}
}
}
@Override
public boolean getAutoCommit() throws SQLException {
ensureOpen();
return autoCommit;
}
@Override
public void begin() throws SQLException {
if (getAutoCommit()) {
throw SqlExceptionUtils.clientError("Cannot start new transaction in auto-commit mode");
}
ensureTransactionSupport();
JdbcTransaction tx = txRef.get();
if (tx == null || !tx.isNew()) {
// invalid transaction state
throw new SQLException(JdbcTransaction.ERROR_TX_STARTED, SqlExceptionUtils.SQL_STATE_INVALID_TX_STATE);
}
}
@Override
public void commit() throws SQLException {
if (getAutoCommit()) {
throw SqlExceptionUtils.clientError("Cannot commit in auto-commit mode");
}
ensureTransactionSupport();
JdbcTransaction tx = txRef.get();
if (tx == null) {
// invalid transaction state
throw new SQLException(JdbcTransaction.ERROR_TX_NOT_STARTED, SqlExceptionUtils.SQL_STATE_INVALID_TX_STATE);
} else {
try {
tx.commit(log);
} finally {
if (!txRef.compareAndSet(tx, createTransaction())) {
log.warn("Transaction was set to %s unexpectedly", txRef.get());
}
}
}
}
@Override
public void rollback() throws SQLException {
if (getAutoCommit()) {
throw SqlExceptionUtils.clientError("Cannot rollback in auto-commit mode");
}
ensureTransactionSupport();
JdbcTransaction tx = txRef.get();
if (tx == null) {
// invalid transaction state
throw new SQLException(JdbcTransaction.ERROR_TX_NOT_STARTED, SqlExceptionUtils.SQL_STATE_INVALID_TX_STATE);
} else {
try {
tx.rollback(log);
} finally {
if (!txRef.compareAndSet(tx, createTransaction())) {
log.warn("Transaction was set to %s unexpectedly", txRef.get());
}
}
}
}
@Override
public void close() throws SQLException {
try {
this.client.close();
} catch (Exception e) {
log.warn("Failed to close connection due to %s", e.getMessage());
throw SqlExceptionUtils.handle(e);
} finally {
this.closed = true;
}
JdbcTransaction tx = txRef.get();
if (tx != null) {
try {
tx.commit(log);
} finally {
if (!txRef.compareAndSet(tx, null)) {
log.warn("Transaction was set to %s unexpectedly", txRef.get());
}
}
}
}
@Override
public boolean isClosed() throws SQLException {
return closed;
}
@Override
public DatabaseMetaData getMetaData() throws SQLException {
return new ClickHouseDatabaseMetaData(this);
}
@Override
public void setReadOnly(boolean readOnly) throws SQLException {
ensureOpen();
if (initialReadOnly != 0) {
if (!readOnly) {
throw SqlExceptionUtils.clientError("Cannot change the setting on a read-only connection");
}
} else {
if (readOnly) {
clientRequest.set(SETTING_READONLY, 2);
} else {
clientRequest.removeSetting(SETTING_READONLY);
}
this.readOnly = readOnly;
}
}
@Override
public boolean isReadOnly() throws SQLException {
ensureOpen();
return readOnly;
}
@Override
public void setCatalog(String catalog) throws SQLException {
if (jdbcConf.useCatalog()) {
setCurrentDatabase(catalog, true);
} else {
log.warn(
"setCatalog method is no-op. Please either change databaseTerm to catalog or use setSchema method instead");
}
}
@Override
public String getCatalog() throws SQLException {
return jdbcConf.useCatalog() ? getDatabase() : null;
}
@Override
public void setTransactionIsolation(int level) throws SQLException {
ensureOpen();
if (Connection.TRANSACTION_NONE != level && Connection.TRANSACTION_READ_UNCOMMITTED != level
&& Connection.TRANSACTION_READ_COMMITTED != level && Connection.TRANSACTION_REPEATABLE_READ != level
&& Connection.TRANSACTION_SERIALIZABLE != level) {
throw new SQLException("Invalid transaction isolation level: " + level);
} else if (isTransactionSupported()) {
txIsolation = Connection.TRANSACTION_REPEATABLE_READ;
} else if (jdbcConf.isJdbcCompliant()) {
txIsolation = level;
} else {
txIsolation = Connection.TRANSACTION_NONE;
}
}
@Override
public int getTransactionIsolation() throws SQLException {
ensureOpen();
return txIsolation;
}
@Override
public SQLWarning getWarnings() throws SQLException {
ensureOpen();
return null;
}
@Override
public void clearWarnings() throws SQLException {
ensureOpen();
}
@Override
public Map> getTypeMap() throws SQLException {
ensureOpen();
return new HashMap<>(typeMap);
}
@Override
public void setTypeMap(Map> map) throws SQLException {
ensureOpen();
if (map != null) {
typeMap.putAll(map);
}
}
@Override
public void setHoldability(int holdability) throws SQLException {
ensureOpen();
if (holdability == ResultSet.CLOSE_CURSORS_AT_COMMIT || holdability == ResultSet.HOLD_CURSORS_OVER_COMMIT) {
rsHoldability = holdability;
} else {
throw new SQLException("Invalid holdability: " + holdability);
}
}
@Override
public int getHoldability() throws SQLException {
ensureOpen();
return rsHoldability;
}
@Override
public Savepoint setSavepoint() throws SQLException {
return setSavepoint(null);
}
@Override
public Savepoint setSavepoint(String name) throws SQLException {
ensureOpen();
if (getAutoCommit()) {
throw SqlExceptionUtils.clientError("Cannot set savepoint in auto-commit mode");
}
if (!jdbcConf.isJdbcCompliant()) {
throw SqlExceptionUtils.unsupportedError("setSavepoint not implemented");
}
JdbcTransaction tx = txRef.get();
if (tx == null) {
tx = createTransaction();
if (!txRef.compareAndSet(null, tx)) {
tx = txRef.get();
}
}
return tx.newSavepoint(name);
}
@Override
public void rollback(Savepoint savepoint) throws SQLException {
ensureOpen();
if (getAutoCommit()) {
throw SqlExceptionUtils.clientError("Cannot rollback to savepoint in auto-commit mode");
}
if (!jdbcConf.isJdbcCompliant()) {
throw SqlExceptionUtils.unsupportedError("rollback not implemented");
}
if (!(savepoint instanceof JdbcSavepoint)) {
throw SqlExceptionUtils.clientError("Unsupported type of savepoint: " + savepoint);
}
JdbcTransaction tx = txRef.get();
if (tx == null) {
// invalid transaction state
throw new SQLException(JdbcTransaction.ERROR_TX_NOT_STARTED, SqlExceptionUtils.SQL_STATE_INVALID_TX_STATE);
} else {
JdbcSavepoint s = (JdbcSavepoint) savepoint;
tx.logSavepointDetails(log, s, JdbcTransaction.ACTION_ROLLBACK);
tx.toSavepoint(s);
}
}
@Override
public void releaseSavepoint(Savepoint savepoint) throws SQLException {
ensureOpen();
if (getAutoCommit()) {
throw SqlExceptionUtils.clientError("Cannot release savepoint in auto-commit mode");
}
if (!jdbcConf.isJdbcCompliant()) {
throw SqlExceptionUtils.unsupportedError("rollback not implemented");
}
if (!(savepoint instanceof JdbcSavepoint)) {
throw SqlExceptionUtils.clientError("Unsupported type of savepoint: " + savepoint);
}
JdbcTransaction tx = txRef.get();
if (tx == null) {
// invalid transaction state
throw new SQLException(JdbcTransaction.ERROR_TX_NOT_STARTED, SqlExceptionUtils.SQL_STATE_INVALID_TX_STATE);
} else {
JdbcSavepoint s = (JdbcSavepoint) savepoint;
tx.logSavepointDetails(log, s, "released");
tx.toSavepoint(s);
}
}
@Override
public ClickHouseStatement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability)
throws SQLException {
ensureOpen();
return new ClickHouseStatementImpl(this, clientRequest.copy(), resultSetType, resultSetConcurrency,
resultSetHoldability);
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency,
int resultSetHoldability) throws SQLException {
ensureOpen();
ClickHouseConfig config = clientRequest.getConfig();
// TODO remove the extra parsing
ClickHouseSqlStatement[] stmts = parse(sql, config, clientRequest.getSettings());
if (stmts.length != 1) {
throw SqlExceptionUtils
.clientError("Prepared statement only supports one query but we got: " + stmts.length);
}
ClickHouseSqlStatement parsedStmt = stmts[0];
ClickHouseParameterizedQuery preparedQuery;
try {
preparedQuery = jdbcConf.useNamedParameter()
? ClickHouseParameterizedQuery.of(clientRequest.getConfig(), parsedStmt.getSQL())
: JdbcParameterizedQuery.of(config, parsedStmt.getSQL());
} catch (RuntimeException e) {
throw SqlExceptionUtils.clientError(e);
}
PreparedStatement ps = null;
if (preparedQuery.hasParameter()) {
if (parsedStmt.hasTempTable() || parsedStmt.hasInput()) {
throw SqlExceptionUtils
.clientError(
"External table, input function, and query parameter cannot be used together in PreparedStatement.");
} else if (parsedStmt.getStatementType() == StatementType.INSERT &&
!parsedStmt.containsKeyword("SELECT") && parsedStmt.hasValues() &&
(!parsedStmt.hasFormat() || clientRequest.getFormat().name().equals(parsedStmt.getFormat()))) {
String query = parsedStmt.getSQL();
boolean useStream = false;
Integer startIndex = parsedStmt.getPositions().get(ClickHouseSqlStatement.KEYWORD_VALUES_START);
if (startIndex != null) {
useStream = true;
int endIndex = parsedStmt.getPositions().get(ClickHouseSqlStatement.KEYWORD_VALUES_END);
for (int i = startIndex + 1; i < endIndex; i++) {
char ch = query.charAt(i);
if (ch != '?' && ch != ',' && !Character.isWhitespace(ch)) {
useStream = false;
break;
}
}
}
if (useStream) {
ps = new InputBasedPreparedStatement(this,
clientRequest.write().query(query.substring(0, parsedStmt.getStartPosition("VALUES")),
newQueryId()),
getTableColumns(parsedStmt.getDatabase(), parsedStmt.getTable(),
parsedStmt.getContentBetweenKeywords(
ClickHouseSqlStatement.KEYWORD_TABLE_COLUMNS_START,
ClickHouseSqlStatement.KEYWORD_TABLE_COLUMNS_END)),
resultSetType, resultSetConcurrency, resultSetHoldability);
}
}
} else {
if (parsedStmt.hasTempTable()) {
// queries using external/temporary table
ps = new TableBasedPreparedStatement(this,
clientRequest.copy().query(parsedStmt.getSQL(), newQueryId()), parsedStmt,
resultSetType, resultSetConcurrency, resultSetHoldability);
} else if (parsedStmt.getStatementType() == StatementType.INSERT) {
if (!ClickHouseChecker.isNullOrBlank(parsedStmt.getInput())) {
// an ugly workaround of https://github.com/ClickHouse/ClickHouse/issues/39866
// would be replace JSON and Object('json') types in the query to String
Mutation m = clientRequest.write();
if (parsedStmt.hasFormat()) {
m.format(ClickHouseFormat.valueOf(parsedStmt.getFormat()));
}
// insert query using input function
ps = new InputBasedPreparedStatement(this, m.query(parsedStmt.getSQL(), newQueryId()),
ClickHouseColumn.parse(parsedStmt.getInput()), resultSetType, resultSetConcurrency,
resultSetHoldability);
} else if (!parsedStmt.containsKeyword("SELECT") && !parsedStmt.hasValues()) {
ps = parsedStmt.hasFormat()
? new StreamBasedPreparedStatement(this,
clientRequest.write().query(parsedStmt.getSQL(), newQueryId()), parsedStmt,
resultSetType, resultSetConcurrency, resultSetHoldability)
: new InputBasedPreparedStatement(this,
clientRequest.write().query(parsedStmt.getSQL(), newQueryId()),
getTableColumns(parsedStmt.getDatabase(), parsedStmt.getTable(),
parsedStmt.getContentBetweenKeywords(
ClickHouseSqlStatement.KEYWORD_TABLE_COLUMNS_START,
ClickHouseSqlStatement.KEYWORD_TABLE_COLUMNS_END)),
resultSetType, resultSetConcurrency, resultSetHoldability);
}
}
}
return ps != null ? ps
: new SqlBasedPreparedStatement(this, clientRequest.copy().query(preparedQuery, newQueryId()),
stmts[0], resultSetType, resultSetConcurrency, resultSetHoldability);
}
@Override
public NClob createNClob() throws SQLException {
ensureOpen();
return createClob();
}
@Override
public boolean isValid(int timeout) throws SQLException {
if (timeout < 0) {
throw SqlExceptionUtils.clientError("Negative milliseconds is not allowed");
} else if (timeout == 0) {
timeout = clientRequest.getConfig().getConnectionTimeout();
} else {
timeout = (int) TimeUnit.SECONDS.toMillis(timeout);
}
if (isClosed()) {
return false;
}
return client.ping(clientRequest.getServer(), timeout);
}
@Override
public void setClientInfo(String name, String value) throws SQLClientInfoException {
try {
ensureOpen();
} catch (SQLException e) {
Map failedProps = new HashMap<>();
failedProps.put(PROP_APPLICATION_NAME, ClientInfoStatus.REASON_UNKNOWN_PROPERTY);
failedProps.put(PROP_CUSTOM_HTTP_HEADERS, ClientInfoStatus.REASON_UNKNOWN_PROPERTY);
failedProps.put(PROP_CUSTOM_HTTP_PARAMS, ClientInfoStatus.REASON_UNKNOWN_PROPERTY);
throw new SQLClientInfoException(e.getMessage(), failedProps);
}
if (PROP_APPLICATION_NAME.equals(name)) {
if (ClickHouseChecker.isNullOrBlank(value)) {
clientRequest.removeOption(ClickHouseClientOption.CLIENT_NAME);
} else {
clientRequest.option(ClickHouseClientOption.CLIENT_NAME, value);
}
} else if (PROP_CUSTOM_HTTP_HEADERS.equals(name)) {
if (ClickHouseChecker.isNullOrBlank(value)) {
clientRequest.removeOption(ClickHouseHttpOption.CUSTOM_HEADERS);
} else {
clientRequest.option(ClickHouseHttpOption.CUSTOM_HEADERS, value);
}
} else if (PROP_CUSTOM_HTTP_PARAMS.equals(name)) {
if (ClickHouseChecker.isNullOrBlank(value)) {
clientRequest.removeOption(ClickHouseHttpOption.CUSTOM_PARAMS);
} else {
clientRequest.option(ClickHouseHttpOption.CUSTOM_PARAMS, value);
}
}
}
@Override
public void setClientInfo(Properties properties) throws SQLClientInfoException {
try {
ensureOpen();
} catch (SQLException e) {
Map failedProps = new HashMap<>();
failedProps.put(PROP_APPLICATION_NAME, ClientInfoStatus.REASON_UNKNOWN_PROPERTY);
failedProps.put(PROP_CUSTOM_HTTP_HEADERS, ClientInfoStatus.REASON_UNKNOWN_PROPERTY);
failedProps.put(PROP_CUSTOM_HTTP_PARAMS, ClientInfoStatus.REASON_UNKNOWN_PROPERTY);
throw new SQLClientInfoException(e.getMessage(), failedProps);
}
if (properties != null) {
String value = properties.getProperty(PROP_APPLICATION_NAME);
if (ClickHouseChecker.isNullOrBlank(value)) {
clientRequest.removeOption(ClickHouseClientOption.CLIENT_NAME);
} else {
clientRequest.option(ClickHouseClientOption.CLIENT_NAME, value);
}
value = properties.getProperty(PROP_CUSTOM_HTTP_HEADERS);
if (ClickHouseChecker.isNullOrBlank(value)) {
clientRequest.removeOption(ClickHouseHttpOption.CUSTOM_HEADERS);
} else {
clientRequest.option(ClickHouseHttpOption.CUSTOM_HEADERS, value);
}
value = properties.getProperty(PROP_CUSTOM_HTTP_PARAMS);
if (ClickHouseChecker.isNullOrBlank(value)) {
clientRequest.removeOption(ClickHouseHttpOption.CUSTOM_PARAMS);
} else {
clientRequest.option(ClickHouseHttpOption.CUSTOM_PARAMS, value);
}
}
}
@Override
public String getClientInfo(String name) throws SQLException {
ensureOpen();
ClickHouseConfig config = clientRequest.getConfig();
String value = null;
if (PROP_APPLICATION_NAME.equals(name)) {
value = config.getClientName();
} else if (PROP_CUSTOM_HTTP_HEADERS.equals(name)) {
value = config.getStrOption(ClickHouseHttpOption.CUSTOM_HEADERS);
} else if (PROP_CUSTOM_HTTP_PARAMS.equals(name)) {
value = config.getStrOption(ClickHouseHttpOption.CUSTOM_PARAMS);
}
return value;
}
@Override
public Properties getClientInfo() throws SQLException {
ensureOpen();
ClickHouseConfig config = clientRequest.getConfig();
Properties props = new Properties();
props.setProperty(PROP_APPLICATION_NAME, config.getClientName());
props.setProperty(PROP_CUSTOM_HTTP_HEADERS, config.getStrOption(ClickHouseHttpOption.CUSTOM_HEADERS));
props.setProperty(PROP_CUSTOM_HTTP_PARAMS, config.getStrOption(ClickHouseHttpOption.CUSTOM_PARAMS));
return props;
}
@Override
public void setSchema(String schema) throws SQLException {
if (jdbcConf.useSchema()) {
setCurrentDatabase(schema, true);
} else {
log.warn(
"setSchema method is no-op. Please either change databaseTerm to schema or use setCatalog method instead");
}
}
@Override
public String getSchema() throws SQLException {
return jdbcConf.useSchema() ? getDatabase() : null;
}
@Override
public void abort(Executor executor) throws SQLException {
if (executor == null) {
throw SqlExceptionUtils.clientError("Non-null executor is required");
}
executor.execute(() -> {
try {
// try harder please
this.client.close();
} finally {
this.closed = true;
}
});
}
@Override
public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
ensureOpen();
if (executor == null) {
throw SqlExceptionUtils.clientError("Non-null executor is required");
}
if (milliseconds < 0) {
throw SqlExceptionUtils.clientError("Negative milliseconds is not allowed");
}
executor.execute(() -> {
// TODO close this connection when any statement timed out after this amount of
// time
networkTimeout = milliseconds;
});
}
@Override
public int getNetworkTimeout() throws SQLException {
ensureOpen();
return networkTimeout;
}
@Override
public ClickHouseConfig getConfig() {
return clientRequest.getConfig();
}
@Override
public boolean allowCustomSetting() {
return initialReadOnly != 1;
}
@Override
public String getCurrentDatabase() {
return database;
}
@Override
public void setCurrentDatabase(String db, boolean check) throws SQLException {
ensureOpen();
if (db == null || db.isEmpty()) {
throw new SQLException("Non-empty database name is required", SqlExceptionUtils.SQL_STATE_INVALID_SCHEMA);
} else {
clientRequest.use(db);
if (check) {
try (ClickHouseResponse response = clientRequest.query("select 1").executeAndWait()) {
database = db;
} catch (ClickHouseException e) {
throw SqlExceptionUtils.handle(e);
} finally {
if (!db.equals(database)) {
clientRequest.use(database);
}
}
} else {
database = db;
}
}
}
@Override
public String getCurrentUser() {
return user;
}
@Override
public Calendar getDefaultCalendar() {
return defaultCalendar;
}
@Override
public Optional getEffectiveTimeZone() {
return clientTimeZone;
}
@Override
public TimeZone getJvmTimeZone() {
return jvmTimeZone;
}
@Override
public TimeZone getServerTimeZone() {
return serverTimeZone;
}
@Override
public ClickHouseVersion getServerVersion() {
return serverVersion;
}
@Override
public ClickHouseTransaction getTransaction() {
return clientRequest.getTransaction();
}
@Override
public URI getUri() {
return clientRequest.getServer().toUri(ClickHouseJdbcUrlParser.JDBC_CLICKHOUSE_PREFIX);
}
@Override
public JdbcConfig getJdbcConfig() {
return jdbcConf;
}
@Override
public long getMaxInsertBlockSize() {
return initialMaxInsertBlockSize;
}
@Override
public boolean isTransactionSupported() {
return jdbcConf.isTransactionSupported() && initialNonTxQuerySupport >= 0
&& !ClickHouseChecker.isNullOrEmpty(initialTxCommitWaitMode);
}
@Override
public boolean isImplicitTransactionSupported() {
return jdbcConf.isTransactionSupported() && initialImplicitTx >= 0;
}
@Override
public String newQueryId() {
String queryId = clientRequest.getManager().createQueryId();
JdbcTransaction tx = txRef.get();
return tx != null ? tx.newQuery(queryId) : queryId;
}
@Override
public ClickHouseSqlStatement[] parse(String sql, ClickHouseConfig config, Map settings) {
ParseHandler handler = null;
if (jdbcConf.isJdbcCompliant()) {
boolean allowLwDelete = initialDeleteSupport > 1;
boolean allowLwUpdate = false;
if (settings != null) {
Serializable value = settings.get(SETTING_LW_DELETE);
if (!allowLwDelete && (value == null ? initialDeleteSupport == 1
: ClickHouseOption.fromString(value.toString(), Boolean.class))) {
allowLwDelete = true;
}
}
handler = JdbcParseHandler.getInstance(allowLwDelete, allowLwUpdate, jdbcConf.useLocalFile());
} else if (jdbcConf.useLocalFile()) {
handler = JdbcParseHandler.getInstance(false, false, true);
}
return ClickHouseSqlParser.parse(sql, config != null ? config : clientRequest.getConfig(), handler);
}
@Override
public boolean isWrapperFor(Class> iface) throws SQLException {
return iface == ClickHouseClient.class || iface == ClickHouseRequest.class
|| super.isWrapperFor(iface);
}
@Override
public T unwrap(Class iface) throws SQLException {
if (iface == ClickHouseClient.class) {
return iface.cast(client);
} else if (iface == ClickHouseRequest.class) {
return iface.cast(clientRequest);
}
return super.unwrap(iface);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy