diff --git a/pom.xml b/pom.xml index 47b67664d9562..927a76068c527 100644 --- a/pom.xml +++ b/pom.xml @@ -176,6 +176,7 @@ presto-node-ttl-fetchers presto-cluster-ttl-providers presto-google-sheets + presto-clickhouse presto-hive-function-namespace presto-delta presto-grpc-api diff --git a/presto-clickhouse/pom.xml b/presto-clickhouse/pom.xml new file mode 100755 index 0000000000000..2a8038ba57583 --- /dev/null +++ b/presto-clickhouse/pom.xml @@ -0,0 +1,263 @@ + + + 4.0.0 + + presto-root + com.facebook.presto + 0.273-SNAPSHOT + + + presto-clickhouse + Presto - Clickhouse Connector + presto-plugin + + + ${project.parent.basedir} + + + + + ru.yandex.clickhouse + clickhouse-jdbc + 0.2.4 + + + org.slf4j + jcl-over-slf4j + + + commons-logging + commons-logging + + + + + + com.facebook.airlift + configuration + + + + + com.facebook.airlift + log-manager + runtime + + + + com.google.code.findbugs + jsr305 + + + + com.google.guava + guava + + + + com.google.inject + guice + + + + javax.inject + javax.inject + + + + + com.facebook.presto + presto-spi + provided + + + + com.facebook.presto + presto-common + provided + + + + com.facebook.drift + drift-api + provided + + + + io.airlift + slice + provided + + + + io.airlift + units + provided + + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + + org.openjdk.jol + jol-core + provided + + + + com.facebook.airlift + bootstrap + + + joda-time + joda-time + + + com.facebook.airlift + log + + + javax.validation + validation-api + + + com.facebook.presto + presto-expressions + + + + + com.facebook.presto + presto-testng-services + test + + + + org.testng + testng + test + + + + com.facebook.presto + presto-main + test + + + + com.facebook.presto + presto-tpch + test + + + + com.facebook.airlift + testing + test + ${dep.airlift.version} + + + + com.facebook.airlift + json + test + + + + io.airlift.tpch + tpch + test + + + + org.assertj + assertj-core + test + + + + com.facebook.presto + presto-tests + test + + + + org.jetbrains + annotations + test + + + + org.testcontainers + clickhouse + test + + + + org.testcontainers + jdbc + test + + + + org.testcontainers + testcontainers + test + 1.14.3 + + + org.slf4j + slf4j-api + + + org.jetbrains + annotations + + + + + + + + ci + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + + + + + + default + + true + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + **/TestClickHouseIntegrationSmokeTest.java + **/TestClickHouseDistributedQueries.java + + + + + + + + \ No newline at end of file diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/BooleanReadFunction.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/BooleanReadFunction.java new file mode 100755 index 0000000000000..818241b4309cd --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/BooleanReadFunction.java @@ -0,0 +1,31 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@FunctionalInterface +public interface BooleanReadFunction + extends ReadFunction +{ + @Override + default Class getJavaType() + { + return boolean.class; + } + + boolean readBoolean(ResultSet resultSet, int columnIndex) + throws SQLException; +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/CaseSensitivity.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/CaseSensitivity.java new file mode 100755 index 0000000000000..61373350047c2 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/CaseSensitivity.java @@ -0,0 +1,20 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +public enum CaseSensitivity +{ + CASE_SENSITIVE, + CASE_INSENSITIVE; +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseClient.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseClient.java new file mode 100755 index 0000000000000..58af865a23c6b --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseClient.java @@ -0,0 +1,954 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.airlift.log.Logger; +import com.facebook.presto.common.predicate.TupleDomain; +import com.facebook.presto.common.type.CharType; +import com.facebook.presto.common.type.DecimalType; +import com.facebook.presto.common.type.Type; +import com.facebook.presto.common.type.VarbinaryType; +import com.facebook.presto.common.type.VarcharType; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.FixedSplitSource; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.TableNotFoundException; +import com.facebook.presto.spi.statistics.TableStatistics; +import com.google.common.base.CharMatcher; +import com.google.common.base.Joiner; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static com.facebook.presto.common.type.BigintType.BIGINT; +import static com.facebook.presto.common.type.BooleanType.BOOLEAN; +import static com.facebook.presto.common.type.DateType.DATE; +import static com.facebook.presto.common.type.DoubleType.DOUBLE; +import static com.facebook.presto.common.type.IntegerType.INTEGER; +import static com.facebook.presto.common.type.RealType.REAL; +import static com.facebook.presto.common.type.SmallintType.SMALLINT; +import static com.facebook.presto.common.type.TimeType.TIME; +import static com.facebook.presto.common.type.TimeWithTimeZoneType.TIME_WITH_TIME_ZONE; +import static com.facebook.presto.common.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.common.type.TimestampWithTimeZoneType.TIMESTAMP_WITH_TIME_ZONE; +import static com.facebook.presto.common.type.TinyintType.TINYINT; +import static com.facebook.presto.common.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.plugin.clickhouse.ClickHouseEngineType.MERGETREE; +import static com.facebook.presto.plugin.clickhouse.ClickHouseErrorCode.JDBC_ERROR; +import static com.facebook.presto.plugin.clickhouse.ClickhouseDXLKeyWords.ORDER_BY_PROPERTY; +import static com.facebook.presto.plugin.clickhouse.StandardReadMappings.jdbcTypeToPrestoType; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_TABLE_PROPERTY; +import static com.facebook.presto.spi.StandardErrorCode.NOT_FOUND; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.base.Verify.verify; +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.Iterables.getOnlyElement; +import static java.lang.String.format; +import static java.lang.String.join; +import static java.sql.ResultSetMetaData.columnNullable; +import static java.util.Collections.nCopies; +import static java.util.Locale.ENGLISH; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class ClickHouseClient +{ + private static final Logger log = Logger.get(ClickHouseClient.class); + private static final Map SQL_TYPES = ImmutableMap.builder() + .put(BOOLEAN, "boolean") + .put(BIGINT, "bigint") + .put(INTEGER, "integer") + .put(SMALLINT, "smallint") + .put(TINYINT, "tinyint") + .put(DOUBLE, "double precision") + .put(REAL, "real") + .put(VARBINARY, "varbinary") + .put(DATE, "Date") + .put(TIME, "time") + .put(TIME_WITH_TIME_ZONE, "time with timezone") + .put(TIMESTAMP, "timestamp") + .put(TIMESTAMP_WITH_TIME_ZONE, "timestamp with timezone") + .build(); + private static final String tempTableNamePrefix = "tmp_presto_"; + protected static final String identifierQuote = "\""; + protected final String connectorId; + protected final ConnectionFactory connectionFactory; + protected final boolean caseSensitiveEnabled; + protected final int commitBatchSize; + protected final Cache> remoteSchemaNames; + protected final Cache> remoteTableNames; + + private final boolean mapStringAsVarchar; + + @Inject + public ClickHouseClient(ClickHouseConnectorId connectorId, ClickHouseConfig config, ConnectionFactory connectionFactory) + { + this.connectorId = requireNonNull(connectorId, "connectorId is null").toString(); + this.connectionFactory = requireNonNull(connectionFactory, "connectionFactory is null"); + + this.commitBatchSize = config.getCommitBatchSize(); + this.mapStringAsVarchar = config.isMapStringAsVarchar(); + this.caseSensitiveEnabled = config.isCaseInsensitiveNameMatching(); + CacheBuilder remoteNamesCacheBuilder = CacheBuilder.newBuilder() + .expireAfterWrite(config.getCaseInsensitiveNameMatchingCacheTtl().toMillis(), MILLISECONDS); + this.remoteSchemaNames = remoteNamesCacheBuilder.build(); + this.remoteTableNames = remoteNamesCacheBuilder.build(); + } + + public int getCommitBatchSize() + { + return commitBatchSize; + } + + public List getTableNames(ClickHouseIdentity identity, Optional schema) + { + try (Connection connection = connectionFactory.openConnection(identity)) { + Optional remoteSchema = schema.map(schemaName -> toRemoteSchemaName(identity, connection, schemaName)); + try (ResultSet resultSet = getTables(connection, remoteSchema, Optional.empty())) { + ImmutableList.Builder list = ImmutableList.builder(); + while (resultSet.next()) { + String tableSchema = getTableSchemaName(resultSet); + String tableName = resultSet.getString("TABLE_NAME"); + list.add(new SchemaTableName(tableSchema.toLowerCase(ENGLISH), tableName.toLowerCase(ENGLISH))); + } + return list.build(); + } + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + protected String quoted(String name) + { + name = name.replace(identifierQuote, identifierQuote + identifierQuote); + return identifierQuote + name + identifierQuote; + } + + protected String getTableSchemaName(ResultSet resultSet) + throws SQLException + { + return resultSet.getString("TABLE_SCHEM"); + } + + public final Set getSchemaNames(ClickHouseIdentity identity) + { + try (Connection connection = connectionFactory.openConnection(identity)) { + return listSchemas(connection).stream() + .map(schemaName -> schemaName.toLowerCase(ENGLISH)) + .collect(toImmutableSet()); + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + public ConnectorSplitSource getSplits(ClickHouseIdentity identity, ClickHouseTableLayoutHandle layoutHandle) + { + ClickHouseTableHandle tableHandle = layoutHandle.getTable(); + ClickHouseSplit clickHouseSplit = new ClickHouseSplit( + connectorId, + tableHandle.getCatalogName(), + tableHandle.getSchemaName(), + tableHandle.getTableName(), + layoutHandle.getTupleDomain(), + layoutHandle.getAdditionalPredicate(), + layoutHandle.getSimpleExpression()); + return new FixedSplitSource(ImmutableList.of(clickHouseSplit)); + } + + public List getColumns(ConnectorSession session, ClickHouseTableHandle tableHandle) + { + try (Connection connection = connectionFactory.openConnection(ClickHouseIdentity.from(session))) { + try (ResultSet resultSet = getColumns(tableHandle, connection.getMetaData())) { + List columns = new ArrayList<>(); + while (resultSet.next()) { + ClickHouseTypeHandle typeHandle = new ClickHouseTypeHandle( + resultSet.getInt("DATA_TYPE"), + Optional.ofNullable(resultSet.getString("TYPE_NAME")), + resultSet.getInt("COLUMN_SIZE"), + resultSet.getInt("DECIMAL_DIGITS"), + Optional.empty(), + Optional.empty()); + Optional columnMapping = toPrestoType(session, typeHandle); + // skip unsupported column types + if (columnMapping.isPresent()) { + String columnName = resultSet.getString("COLUMN_NAME"); + boolean nullable = columnNullable == resultSet.getInt("NULLABLE"); + columns.add(new ClickHouseColumnHandle(connectorId, columnName, typeHandle, columnMapping.get().getType(), nullable)); + } + else { + log.info("The clickHouse datatype: " + typeHandle.getJdbcTypeName() + " unsupported."); + } + } + if (columns.isEmpty()) { + throw new TableNotFoundException(tableHandle.getSchemaTableName()); + } + return ImmutableList.copyOf(columns); + } + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + public Optional toPrestoType(ConnectorSession session, ClickHouseTypeHandle typeHandle) + { + return jdbcTypeToPrestoType(typeHandle, mapStringAsVarchar); + } + + public PreparedStatement getPreparedStatement(Connection connection, String sql) + throws SQLException + { + return connection.prepareStatement(sql); + } + + public PreparedStatement buildSql(ConnectorSession session, Connection connection, ClickHouseSplit split, List columnHandles) + throws SQLException + { + return new QueryBuilder(identifierQuote).buildSql( + this, + session, + connection, + split.getCatalogName(), + split.getSchemaName(), + split.getTableName(), + columnHandles, + split.getTupleDomain(), + split.getAdditionalPredicate(), + split.getSimpleExpression()); + } + + public String getIdentifierQuote() + { + return identifierQuote; + } + + public Connection getConnection(ClickHouseIdentity identity, ClickHouseSplit split) + throws SQLException + { + Connection connection = connectionFactory.openConnection(identity); + try { + connection.setReadOnly(true); + } + catch (SQLException e) { + connection.close(); + throw e; + } + return connection; + } + + public Connection getConnection(ClickHouseIdentity identity, ClickHouseOutputTableHandle handle) + throws SQLException + { + return connectionFactory.openConnection(identity); + } + + public String buildInsertSql(ClickHouseOutputTableHandle handle) + { + String columns = Joiner.on(',').join(nCopies(handle.getColumnNames().size(), "?")); + return new StringBuilder() + .append("INSERT INTO ") + .append(quoted(handle.getCatalogName(), handle.getSchemaName(), handle.getTemporaryTableName())) + .append(" VALUES (").append(columns).append(")") + .toString(); + } + + protected Collection listSchemas(Connection connection) + { + try (ResultSet resultSet = connection.getMetaData().getSchemas()) { + ImmutableSet.Builder schemaNames = ImmutableSet.builder(); + while (resultSet.next()) { + String schemaName = resultSet.getString("TABLE_SCHEM"); + // skip internal schemas + if (!schemaName.equalsIgnoreCase("information_schema")) { + schemaNames.add(schemaName); + } + } + return schemaNames.build(); + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + public ClickHouseTableHandle getTableHandle(ClickHouseIdentity identity, SchemaTableName schemaTableName) + { + try (Connection connection = connectionFactory.openConnection(identity)) { + String remoteSchema = toRemoteSchemaName(identity, connection, schemaTableName.getSchemaName()); + String remoteTable = toRemoteTableName(identity, connection, remoteSchema, schemaTableName.getTableName()); + try (ResultSet resultSet = getTables(connection, Optional.of(remoteSchema), Optional.of(remoteTable))) { + List tableHandles = new ArrayList<>(); + while (resultSet.next()) { + tableHandles.add(new ClickHouseTableHandle( + connectorId, + schemaTableName, + null, //"datasets", + resultSet.getString("TABLE_SCHEM"), + resultSet.getString("TABLE_NAME"))); + } + if (tableHandles.isEmpty()) { + return null; + } + if (tableHandles.size() > 1) { + throw new PrestoException(NOT_SUPPORTED, "Multiple tables matched: " + schemaTableName); + } + return getOnlyElement(tableHandles); + } + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + protected ResultSet getTables(Connection connection, Optional schemaName, Optional tableName) + throws SQLException + { + DatabaseMetaData metadata = connection.getMetaData(); + Optional escape = Optional.ofNullable(metadata.getSearchStringEscape()); + return metadata.getTables( + connection.getCatalog(), + escapeNamePattern(schemaName, escape).orElse(null), + escapeNamePattern(tableName, escape).orElse(null), + new String[] {"TABLE", "VIEW"}); + } + + private static ResultSet getColumns(ClickHouseTableHandle tableHandle, DatabaseMetaData metadata) + throws SQLException + { + Optional escape = Optional.ofNullable(metadata.getSearchStringEscape()); + return metadata.getColumns( + tableHandle.getCatalogName(), + escapeNamePattern(Optional.ofNullable(tableHandle.getSchemaName()), escape).orElse(null), + escapeNamePattern(Optional.ofNullable(tableHandle.getTableName()), escape).orElse(null), + null); + } + + protected static Optional escapeNamePattern(Optional name, Optional escape) + { + if (!name.isPresent() || !escape.isPresent()) { + return name; + } + return Optional.of(escapeNamePattern(name.get(), escape.get())); + } + + private static String escapeNamePattern(String name, String escape) + { + requireNonNull(name, "name is null"); + requireNonNull(escape, "escape is null"); + checkArgument(!escape.isEmpty(), "Escape string must not be empty"); + checkArgument(!escape.equals("_"), "Escape string must not be '_'"); + checkArgument(!escape.equals("%"), "Escape string must not be '%'"); + name = name.replace(escape, escape + escape); + name = name.replace("_", escape + "_"); + name = name.replace("%", escape + "%"); + return name; + } + + protected String quoted(@Nullable String catalog, @Nullable String schema, String table) + { + StringBuilder builder = new StringBuilder(); + if (!isNullOrEmpty(schema)) { + builder.append(quoted(schema)).append("."); + } + builder.append(quoted(table)); + return builder.toString(); + } + + public void addColumn(ClickHouseIdentity identity, ClickHouseTableHandle handle, ColumnMetadata column) + { + String schema = handle.getSchemaName(); + String table = handle.getTableName(); + String columnName = column.getName(); + String sql = format( + "ALTER TABLE %s ADD COLUMN %s", + quoted(handle.getCatalogName(), schema, table), + getColumnDefinitionSql(column, columnName)); + + try (Connection connection = connectionFactory.openConnection(identity)) { + DatabaseMetaData metadata = connection.getMetaData(); + if (metadata.storesUpperCaseIdentifiers()) { + schema = schema != null ? schema.toUpperCase(ENGLISH) : null; + table = table.toUpperCase(ENGLISH); + columnName = columnName.toUpperCase(ENGLISH); + } + execute(connection, sql); + } + catch (SQLException e) { + PrestoException exception = new PrestoException(JDBC_ERROR, e); + exception.addSuppressed(new RuntimeException("Query: " + sql)); + throw exception; + } + } + + public ClickHouseOutputTableHandle beginCreateTable(ConnectorSession session, ConnectorTableMetadata tableMetadata) + { + return createTemporaryTable(session, tableMetadata); + } + + public ClickHouseOutputTableHandle beginInsertTable(ConnectorSession session, ConnectorTableMetadata tableMetadata) + { + return beginWriteTable(session, tableMetadata); + } + + private ClickHouseOutputTableHandle beginWriteTable(ConnectorSession session, ConnectorTableMetadata tableMetadata) + { + try { + return beginInsertTable(tableMetadata, session, generateTemporaryTableName()); + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + public void dropColumn(ClickHouseIdentity identity, ClickHouseTableHandle handle, ClickHouseColumnHandle column) + { + try (Connection connection = connectionFactory.openConnection(identity)) { + String sql = format( + "ALTER TABLE %s DROP COLUMN %s", + quoted(handle.getCatalogName(), handle.getSchemaName(), handle.getTableName()), + column.getColumnName()); + execute(connection, sql); + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + public void finishInsertTable(ClickHouseIdentity identity, ClickHouseOutputTableHandle handle) + { + String temporaryTable = quoted(handle.getCatalogName(), handle.getSchemaName(), handle.getTemporaryTableName()); + String targetTable = quoted(handle.getCatalogName(), handle.getSchemaName(), handle.getTableName()); + String insertSql = format("INSERT INTO %s SELECT * FROM %s", targetTable, temporaryTable); + String cleanupSql = "DROP TABLE " + temporaryTable; + + try (Connection connection = getConnection(identity, handle)) { + execute(connection, insertSql); + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + + try (Connection connection = getConnection(identity, handle)) { + execute(connection, cleanupSql); + } + catch (SQLException e) { + log.warn(e, "Failed to cleanup temporary table: %s", temporaryTable); + } + } + + public void commitCreateTable(ClickHouseIdentity identity, ClickHouseOutputTableHandle handle) + { + renameTable( + identity, + handle.getCatalogName(), + new SchemaTableName(handle.getSchemaName(), handle.getTemporaryTableName()), + new SchemaTableName(handle.getSchemaName(), handle.getTableName())); + } + + public ClickHouseOutputTableHandle createTemporaryTable(ConnectorSession session, ConnectorTableMetadata tableMetadata) + { + try { + return createTable(tableMetadata, session, generateTemporaryTableName()); + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + public ClickHouseOutputTableHandle createTable(ConnectorSession session, ConnectorTableMetadata tableMetadata) + { + try { + return createTable(tableMetadata, session, tableMetadata.getTable().getTableName()); + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + protected String generateTemporaryTableName() + { + return tempTableNamePrefix + UUID.randomUUID().toString().replace("-", ""); + } + + public void abortReadConnection(Connection connection) + throws SQLException + { + } + + protected void execute(Connection connection, String query) + throws SQLException + { + try (Statement statement = connection.createStatement()) { + log.debug("Execute: %s", query); + statement.execute(query); + } + } + + public void renameColumn(ClickHouseIdentity identity, ClickHouseTableHandle handle, ClickHouseColumnHandle clickHouseColumn, String newColumnName) + { + String sql = format( + "ALTER TABLE %s RENAME COLUMN %s TO %s", + quoted(handle.getCatalogName(), handle.getSchemaName(), handle.getTableName()), + clickHouseColumn.getColumnName(), + newColumnName); + + try (Connection connection = connectionFactory.openConnection(identity)) { + DatabaseMetaData metadata = connection.getMetaData(); + if (metadata.storesUpperCaseIdentifiers()) { + newColumnName = newColumnName.toUpperCase(ENGLISH); + } + execute(connection, sql); + } + catch (SQLException e) { + PrestoException exception = new PrestoException(JDBC_ERROR, "Query: " + sql, e); + throw exception; + } + } + + public ClickHouseOutputTableHandle beginInsertTable(ConnectorTableMetadata tableMetadata, ConnectorSession session, String tableName) + throws SQLException + { + SchemaTableName schemaTableName = tableMetadata.getTable(); + ClickHouseIdentity identity = ClickHouseIdentity.from(session); + if (!getSchemaNames(identity).contains(schemaTableName.getSchemaName())) { + throw new PrestoException(NOT_FOUND, "Schema not found: " + schemaTableName.getSchemaName()); + } + + try (Connection connection = connectionFactory.openConnection(identity)) { + boolean uppercase = connection.getMetaData().storesUpperCaseIdentifiers(); + String remoteSchema = toRemoteSchemaName(identity, connection, schemaTableName.getSchemaName()); + String remoteTable = toRemoteTableName(identity, connection, remoteSchema, schemaTableName.getTableName()); + if (uppercase) { + tableName = tableName.toUpperCase(ENGLISH); + } + String catalog = connection.getCatalog(); + + ImmutableList.Builder columnNames = ImmutableList.builder(); + ImmutableList.Builder columnTypes = ImmutableList.builder(); + ImmutableList.Builder columnList = ImmutableList.builder(); + for (ColumnMetadata column : tableMetadata.getColumns()) { + String columnName = column.getName(); + if (uppercase) { + columnName = columnName.toUpperCase(ENGLISH); + } + columnNames.add(columnName); + columnTypes.add(column.getType()); + columnList.add(getColumnDefinitionSql(column, columnName)); + } + + SchemaTableName remoteTableName = new SchemaTableName(remoteSchema, tableName); + copyTableSchema(identity, catalog, remoteSchema, schemaTableName, remoteTableName); + + return new ClickHouseOutputTableHandle( + connectorId, + catalog, + remoteSchema, + remoteTable, + columnNames.build(), + columnTypes.build(), + tableName); + } + } + + public ClickHouseOutputTableHandle createTable(ConnectorTableMetadata tableMetadata, ConnectorSession session, String tableName) + throws SQLException + { + SchemaTableName schemaTableName = tableMetadata.getTable(); + ClickHouseIdentity identity = ClickHouseIdentity.from(session); + if (!getSchemaNames(identity).contains(schemaTableName.getSchemaName())) { + throw new PrestoException(NOT_FOUND, "Schema not found: " + schemaTableName.getSchemaName()); + } + + try (Connection connection = connectionFactory.openConnection(identity)) { + boolean uppercase = connection.getMetaData().storesUpperCaseIdentifiers(); + String remoteSchema = toRemoteSchemaName(identity, connection, schemaTableName.getSchemaName()); + String remoteTable = toRemoteTableName(identity, connection, remoteSchema, schemaTableName.getTableName()); + if (uppercase) { + tableName = tableName.toUpperCase(ENGLISH); + } + String catalog = connection.getCatalog(); + + ImmutableList.Builder columnNames = ImmutableList.builder(); + ImmutableList.Builder columnTypes = ImmutableList.builder(); + ImmutableList.Builder columnList = ImmutableList.builder(); + for (ColumnMetadata column : tableMetadata.getColumns()) { + String columnName = column.getName(); + if (uppercase) { + columnName = columnName.toUpperCase(ENGLISH); + } + columnNames.add(columnName); + columnTypes.add(column.getType()); + columnList.add(getColumnDefinitionSql(column, columnName)); + } + + RemoteTableName remoteTableName = new RemoteTableName(Optional.ofNullable(catalog), Optional.ofNullable(remoteSchema), tableName); + String sql = createTableSql(remoteTableName, columnList.build(), tableMetadata); + execute(connection, sql); + + return new ClickHouseOutputTableHandle( + connectorId, + catalog, + remoteSchema, + remoteTable, + columnNames.build(), + columnTypes.build(), + tableName); + } + } + + protected String toRemoteTableName(ClickHouseIdentity identity, Connection connection, String remoteSchema, String tableName) + { + requireNonNull(remoteSchema, "remoteSchema is null"); + requireNonNull(tableName, "tableName is null"); + verify(CharMatcher.forPredicate(Character::isUpperCase).matchesNoneOf(tableName), "Expected table name from internal metadata to be lowercase: %s", tableName); + + if (caseSensitiveEnabled) { + try { + com.facebook.presto.plugin.clickhouse.RemoteTableNameCacheKey cacheKey = new com.facebook.presto.plugin.clickhouse.RemoteTableNameCacheKey(identity, remoteSchema); + Map mapping = remoteTableNames.getIfPresent(cacheKey); + if (mapping != null && !mapping.containsKey(tableName)) { + // This might be a table that has just been created. Force reload. + mapping = null; + } + if (mapping == null) { + mapping = listTablesByLowerCase(connection, remoteSchema); + remoteTableNames.put(cacheKey, mapping); + } + String remoteTable = mapping.get(tableName); + if (remoteTable != null) { + return remoteTable; + } + } + catch (RuntimeException e) { + throw new PrestoException(JDBC_ERROR, "Failed to find remote table name: " + firstNonNull(e.getMessage(), e), e); + } + } + + try { + DatabaseMetaData metadata = connection.getMetaData(); + if (metadata.storesUpperCaseIdentifiers()) { + return tableName.toUpperCase(ENGLISH); + } + return tableName; + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + public void rollbackCreateTable(ClickHouseIdentity identity, ClickHouseOutputTableHandle handle) + { + dropTable(identity, new ClickHouseTableHandle( + handle.getConnectorId(), + new SchemaTableName(handle.getSchemaName(), handle.getTemporaryTableName()), + handle.getCatalogName(), + handle.getSchemaName(), + handle.getTemporaryTableName())); + } + + protected Map listTablesByLowerCase(Connection connection, String remoteSchema) + { + try (ResultSet resultSet = getTables(connection, Optional.of(remoteSchema), Optional.empty())) { + ImmutableMap.Builder map = ImmutableMap.builder(); + while (resultSet.next()) { + String tableName = resultSet.getString("TABLE_NAME"); + map.put(tableName.toLowerCase(ENGLISH), tableName); + } + return map.build(); + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + public void dropTable(ClickHouseIdentity identity, ClickHouseTableHandle handle) + { + StringBuilder sql = new StringBuilder() + .append("DROP TABLE ") + .append(quoted(handle.getCatalogName(), handle.getSchemaName(), handle.getTableName())); + + try (Connection connection = connectionFactory.openConnection(identity)) { + execute(connection, sql.toString()); + } + catch (SQLException e) { + PrestoException exception = new PrestoException(JDBC_ERROR, e); + exception.addSuppressed(new RuntimeException("Query: " + sql)); + throw exception; + } + } + + public TableStatistics getTableStatistics(ConnectorSession session, ClickHouseTableHandle handle, List columnHandles, TupleDomain tupleDomain) + { + return TableStatistics.empty(); + } + + public boolean schemaExists(ClickHouseIdentity identity, String schema) + { + return getSchemaNames(identity).contains(schema); + } + + public void renameTable(ClickHouseIdentity identity, ClickHouseTableHandle handle, SchemaTableName newTable) + { + renameTable(identity, handle.getCatalogName(), handle.getSchemaTableName(), newTable); + } + + public void createSchema(ClickHouseIdentity identity, String schemaName, Map properties) + { + try (Connection connection = connectionFactory.openConnection(identity)) { + execute(connection, "CREATE DATABASE " + quoted(schemaName)); + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + public void dropSchema(ClickHouseIdentity identity, String schemaName) + { + try (Connection connection = connectionFactory.openConnection(identity)) { + execute(connection, "DROP DATABASE " + quoted(schemaName)); + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + protected void renameTable(ClickHouseIdentity identity, String catalogName, SchemaTableName oldTable, SchemaTableName newTable) + { + String schemaName = oldTable.getSchemaName(); + String tableName = oldTable.getTableName(); + String newSchemaName = newTable.getSchemaName(); + String newTableName = newTable.getTableName(); + String sql = format("RENAME TABLE %s.%s TO %s.%s", + quoted(schemaName), + quoted(tableName), + quoted(newTable.getSchemaName()), + quoted(newTable.getTableName())); + + try (Connection connection = connectionFactory.openConnection(identity)) { + DatabaseMetaData metadata = connection.getMetaData(); + if (metadata.storesUpperCaseIdentifiers()) { + schemaName = schemaName.toUpperCase(ENGLISH); + tableName = tableName.toUpperCase(ENGLISH); + newSchemaName = newSchemaName.toUpperCase(ENGLISH); + newTableName = newTableName.toUpperCase(ENGLISH); + } + execute(connection, sql); + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + private String getColumnDefinitionSql(ColumnMetadata column, String columnName) + { + StringBuilder builder = new StringBuilder() + .append(quoted(columnName)) + .append(" "); + String columnTypeMapping = toWriteMapping(column.getType()); + if (column.isNullable()) { + builder.append("Nullable(").append(columnTypeMapping).append(")"); + } + else { + builder.append(columnTypeMapping); + } + return builder.toString(); + } + + protected String createTableSql(RemoteTableName remoteTableName, List columns, ConnectorTableMetadata tableMetadata) + { + ImmutableList.Builder tableOptions = ImmutableList.builder(); + Map tableProperties = tableMetadata.getProperties(); + ClickHouseEngineType engine = ClickHouseTableProperties.getEngine(tableProperties); + tableOptions.add("ENGINE = " + engine.getEngineType()); + if (engine == MERGETREE && formatProperty(ClickHouseTableProperties.getOrderBy(tableProperties)).equals(Optional.empty())) { + // order_by property is required + throw new PrestoException(INVALID_TABLE_PROPERTY, + format("The property of %s is required for table engine %s", ORDER_BY_PROPERTY, engine.getEngineType())); + } + formatProperty(ClickHouseTableProperties.getOrderBy(tableProperties)).ifPresent(value -> tableOptions.add("ORDER BY " + value)); + formatProperty(ClickHouseTableProperties.getPrimaryKey(tableProperties)).ifPresent(value -> tableOptions.add("PRIMARY KEY " + value)); + formatProperty(ClickHouseTableProperties.getPartitionBy(tableProperties)).ifPresent(value -> tableOptions.add("PARTITION BY " + value)); + ClickHouseTableProperties.getSampleBy(tableProperties).ifPresent(value -> tableOptions.add("SAMPLE BY " + value)); + + return format("CREATE TABLE %s (%s) %s", quoted(remoteTableName), join(", ", columns), join(" ", tableOptions.build())); + } + + /** + * format property to match ClickHouse create table statement + * + * @param properties property will be formatted + * @return formatted property + */ + private Optional formatProperty(List properties) + { + if (properties == null || properties.isEmpty()) { + return Optional.empty(); + } + else if (properties.size() == 1) { + // only one column + return Optional.of(properties.get(0)); + } + else { + // include more than one columns + return Optional.of("(" + String.join(",", properties) + ")"); + } + } + + private String toWriteMapping(Type type) + { + if (type == BOOLEAN) { + // ClickHouse uses UInt8 as boolean, restricted values to 0 and 1. + return "UInt8"; + } + if (type == TINYINT) { + return "Int8"; + } + if (type == SMALLINT) { + return "Int16"; + } + if (type == INTEGER) { + return "Int32"; + } + if (type == BIGINT) { + return "Int64"; + } + if (type == REAL) { + return "Float32"; + } + if (type == DOUBLE) { + return "Float64"; + } + if (type instanceof DecimalType) { + DecimalType decimalType = (DecimalType) type; + String dataType = format("Decimal(%s, %s)", decimalType.getPrecision(), decimalType.getScale()); + return dataType; + } + if (type instanceof CharType || type instanceof VarcharType) { + // The String type replaces the types VARCHAR, BLOB, CLOB, and others from other DBMSs. + return "String"; + } + if (type instanceof VarbinaryType) { + // Strings of arbitrary length. + return "String"; + } + if (type == DATE) { + return "Date"; + } + throw new PrestoException(NOT_SUPPORTED, "Unsupported column type: " + type); + } + + protected void copyTableSchema(ClickHouseIdentity identity, String catalogName, String schemaName, SchemaTableName tableName, SchemaTableName newTableName) + { + // ClickHouse does not support `create table tbl as select * from tbl2 where 0=1` + // ClickHouse support the following two methods to copy schema + // 1. create table tbl as tbl2 + // 2. create table tbl1 ENGINE= as select * from tbl2 + String oldCreateTableName = tableName.getTableName(); + String newCreateTableName = newTableName.getTableName(); + String sql = format( + "CREATE TABLE %s AS %s ", + quoted(null, schemaName, newCreateTableName), + quoted(null, schemaName, oldCreateTableName)); + + try (Connection connection = connectionFactory.openConnection(identity)) { + execute(connection, sql); + } + catch (SQLException e) { + PrestoException exception = new PrestoException(JDBC_ERROR, e); + exception.addSuppressed(new RuntimeException("Query: " + sql)); + throw exception; + } + } + + private String quoted(RemoteTableName remoteTableName) + { + return quoted( + remoteTableName.getCatalogName().orElse(null), + remoteTableName.getSchemaName().orElse(null), + remoteTableName.getTableName()); + } + + protected String toRemoteSchemaName(ClickHouseIdentity identity, Connection connection, String schemaName) + { + requireNonNull(schemaName, "schemaName is null"); + verify(CharMatcher.forPredicate(Character::isUpperCase).matchesNoneOf(schemaName), "Expected schema name from internal metadata to be lowercase: %s", schemaName); + + if (caseSensitiveEnabled) { + try { + Map mapping = remoteSchemaNames.getIfPresent(identity); + if (mapping != null && !mapping.containsKey(schemaName)) { + // This might be a schema that has just been created. Force reload. + mapping = null; + } + if (mapping == null) { + mapping = listSchemasByLowerCase(connection); + remoteSchemaNames.put(identity, mapping); + } + String remoteSchema = mapping.get(schemaName); + if (remoteSchema != null) { + return remoteSchema; + } + } + catch (RuntimeException e) { + throw new PrestoException(JDBC_ERROR, "Failed to find remote schema name: " + firstNonNull(e.getMessage(), e), e); + } + } + + try { + DatabaseMetaData metadata = connection.getMetaData(); + if (metadata.storesUpperCaseIdentifiers()) { + return schemaName.toUpperCase(ENGLISH); + } + return schemaName; + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + } + + protected Map listSchemasByLowerCase(Connection connection) + { + return listSchemas(connection).stream() + .collect(toImmutableMap(schemaName -> schemaName.toLowerCase(ENGLISH), schemaName -> schemaName)); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseColumnHandle.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseColumnHandle.java new file mode 100755 index 0000000000000..32070660485c0 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseColumnHandle.java @@ -0,0 +1,118 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.common.type.Type; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.Collections.emptyMap; +import static java.util.Objects.requireNonNull; + +public final class ClickHouseColumnHandle + implements ColumnHandle +{ + private final String connectorId; + private final String columnName; + private final ClickHouseTypeHandle clickHouseTypeHandle; + private final Type columnType; + private final boolean nullable; + + @JsonCreator + public ClickHouseColumnHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("columnName") String columnName, + @JsonProperty("clickHouseTypeHandle") ClickHouseTypeHandle clickHouseTypeHandle, + @JsonProperty("columnType") Type columnType, + @JsonProperty("nullable") boolean nullable) + { + this.connectorId = requireNonNull(connectorId, "connectorId is null"); + this.columnName = requireNonNull(columnName, "columnName is null"); + this.clickHouseTypeHandle = requireNonNull(clickHouseTypeHandle, "clickHouseTypeHandle is null"); + this.columnType = requireNonNull(columnType, "columnType is null"); + this.nullable = nullable; + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public String getColumnName() + { + return columnName; + } + + @JsonProperty + public ClickHouseTypeHandle getClickHouseTypeHandle() + { + return clickHouseTypeHandle; + } + + @JsonProperty + public Type getColumnType() + { + return columnType; + } + + @JsonProperty + public boolean isNullable() + { + return nullable; + } + + public ColumnMetadata getColumnMetadata() + { + return new ColumnMetadata(columnName, columnType, nullable, null, null, false, emptyMap()); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + ClickHouseColumnHandle o = (ClickHouseColumnHandle) obj; + return Objects.equals(this.connectorId, o.connectorId) && + Objects.equals(this.columnName, o.columnName); + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, columnName); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("connectorId", connectorId) + .add("columnName", columnName) + .add("clickHouseTypeHandle", clickHouseTypeHandle) + .add("columnType", columnType) + .add("nullable", nullable) + .toString(); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseConfig.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseConfig.java new file mode 100755 index 0000000000000..d8f867cf9674c --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseConfig.java @@ -0,0 +1,171 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.airlift.configuration.Config; +import com.facebook.airlift.configuration.ConfigDescription; +import com.facebook.airlift.configuration.ConfigSecuritySensitive; +import io.airlift.units.Duration; +import io.airlift.units.MinDuration; + +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; + +import static java.util.concurrent.TimeUnit.MINUTES; + +public class ClickHouseConfig +{ + private String connectionUrl; + private String connectionUser; + private String connectionPassword; + private String userCredential; + private String passwordCredential; + private boolean caseInsensitiveNameMatching; + private Duration caseInsensitiveNameMatchingCacheTtl = new Duration(1, MINUTES); + private boolean mapStringAsVarchar; + private boolean allowDropTable; + private int commitBatchSize; + + @NotNull + public String getConnectionUrl() + { + return connectionUrl; + } + + @Config("clickhouse.connection-url") + public ClickHouseConfig setConnectionUrl(String connectionUrl) + { + this.connectionUrl = connectionUrl; + return this; + } + + @Nullable + public String getConnectionUser() + { + return connectionUser; + } + + @Config("clickhouse.connection-user") + public ClickHouseConfig setConnectionUser(String connectionUser) + { + this.connectionUser = connectionUser; + return this; + } + + @Nullable + public String getConnectionPassword() + { + return connectionPassword; + } + + @Config("clickhouse.connection-password") + @ConfigSecuritySensitive + public ClickHouseConfig setConnectionPassword(String connectionPassword) + { + this.connectionPassword = connectionPassword; + return this; + } + + @Nullable + public String getUserCredential() + { + return userCredential; + } + + @Config("clickhouse.user-credential") + public ClickHouseConfig setUserCredential(String userCredential) + { + this.userCredential = userCredential; + return this; + } + + @Nullable + public String getPasswordCredential() + { + return passwordCredential; + } + + @Config("clickhouse.password-credential") + public ClickHouseConfig setPasswordCredential(String passwordCredential) + { + this.passwordCredential = passwordCredential; + return this; + } + + public boolean isCaseInsensitiveNameMatching() + { + return caseInsensitiveNameMatching; + } + + @Config("clickhouse.case-insensitive") + public ClickHouseConfig setCaseInsensitiveNameMatching(boolean caseInsensitiveNameMatching) + { + this.caseInsensitiveNameMatching = caseInsensitiveNameMatching; + return this; + } + + @NotNull + @MinDuration("0ms") + public Duration getCaseInsensitiveNameMatchingCacheTtl() + { + return caseInsensitiveNameMatchingCacheTtl; + } + + @Config("clickhouse.remote-name-cache-ttl") + public ClickHouseConfig setCaseInsensitiveNameMatchingCacheTtl(Duration caseInsensitiveNameMatchingCacheTtl) + { + this.caseInsensitiveNameMatchingCacheTtl = caseInsensitiveNameMatchingCacheTtl; + return this; + } + + public boolean isMapStringAsVarchar() + { + return mapStringAsVarchar; + } + + @Config("clickhouse.map-string-as-varchar") + @ConfigDescription("Map ClickHouse String and FixedString as varchar instead of varbinary") + public ClickHouseConfig setMapStringAsVarchar(boolean mapStringAsVarchar) + { + this.mapStringAsVarchar = mapStringAsVarchar; + return this; + } + + @Nullable + public boolean isAllowDropTable() + { + return allowDropTable; + } + + @Config("clickhouse.allow-drop-table") + @ConfigDescription("Allow connector to drop tables") + public ClickHouseConfig setAllowDropTable(boolean allowDropTable) + { + this.allowDropTable = allowDropTable; + return this; + } + + @Nullable + public int getCommitBatchSize() + { + return commitBatchSize; + } + + @Config("clickhouse.commitBatchSize") + public ClickHouseConfig setCommitBatchSize(int commitBatchSize) + { + this.commitBatchSize = commitBatchSize; + return this; + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseConnector.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseConnector.java new file mode 100755 index 0000000000000..f7665eba10882 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseConnector.java @@ -0,0 +1,206 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.airlift.bootstrap.LifeCycleManager; +import com.facebook.airlift.log.Logger; +import com.facebook.presto.plugin.clickhouse.optimization.ClickHousePlanOptimizerProvider; +import com.facebook.presto.spi.connector.Connector; +import com.facebook.presto.spi.connector.ConnectorAccessControl; +import com.facebook.presto.spi.connector.ConnectorCapabilities; +import com.facebook.presto.spi.connector.ConnectorCommitHandle; +import com.facebook.presto.spi.connector.ConnectorMetadata; +import com.facebook.presto.spi.connector.ConnectorPageSinkProvider; +import com.facebook.presto.spi.connector.ConnectorPlanOptimizerProvider; +import com.facebook.presto.spi.connector.ConnectorRecordSetProvider; +import com.facebook.presto.spi.connector.ConnectorSplitManager; +import com.facebook.presto.spi.connector.ConnectorTransactionHandle; +import com.facebook.presto.spi.function.FunctionMetadataManager; +import com.facebook.presto.spi.function.StandardFunctionResolution; +import com.facebook.presto.spi.procedure.Procedure; +import com.facebook.presto.spi.relation.RowExpressionService; +import com.facebook.presto.spi.session.PropertyMetadata; +import com.facebook.presto.spi.transaction.IsolationLevel; +import com.google.common.collect.ImmutableSet; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static com.facebook.presto.spi.connector.ConnectorCapabilities.NOT_NULL_COLUMN_CONSTRAINT; +import static com.facebook.presto.spi.connector.EmptyConnectorCommitHandle.INSTANCE; +import static com.facebook.presto.spi.transaction.IsolationLevel.READ_COMMITTED; +import static com.facebook.presto.spi.transaction.IsolationLevel.checkConnectorSupports; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.Sets.immutableEnumSet; +import static java.util.Objects.requireNonNull; + +public class ClickHouseConnector + implements Connector +{ + private static final Logger log = Logger.get(ClickHouseConnector.class); + + private final LifeCycleManager lifeCycleManager; + private final ClickHouseMetadataFactory clickHouseMetadataFactory; + private final ClickHouseSplitManager clickHouseSplitManager; + private final ClickHouseRecordSetProvider clickHouseRecordSetProvider; + private final ClickHousePageSinkProvider clickHousePageSinkProvider; + private final Optional accessControl; + private final Set procedures; + + private final ConcurrentMap transactions = new ConcurrentHashMap<>(); + private final FunctionMetadataManager functionManager; + private final StandardFunctionResolution functionResolution; + private final RowExpressionService rowExpressionService; + private final ClickHouseClient clickHouseClient; + private final List> tableProperties; + + @Inject + public ClickHouseConnector( + LifeCycleManager lifeCycleManager, + ClickHouseMetadataFactory clickHouseMetadataFactory, + ClickHouseSplitManager clickHouseSplitManager, + ClickHouseRecordSetProvider clickHouseRecordSetProvider, + ClickHousePageSinkProvider clickHousePageSinkProvider, + Optional accessControl, + Set procedures, + FunctionMetadataManager functionManager, + StandardFunctionResolution functionResolution, + RowExpressionService rowExpressionService, + Set tableProperties, + ClickHouseClient clickHouseClient) + { + this.lifeCycleManager = requireNonNull(lifeCycleManager, "lifeCycleManager is null"); + this.clickHouseMetadataFactory = requireNonNull(clickHouseMetadataFactory, "clickHouseMetadataFactory is null"); + this.clickHouseSplitManager = requireNonNull(clickHouseSplitManager, "clickHouseSplitManager is null"); + this.clickHouseRecordSetProvider = requireNonNull(clickHouseRecordSetProvider, "clickHouseRecordSetProvider is null"); + this.clickHousePageSinkProvider = requireNonNull(clickHousePageSinkProvider, "clickHousePageSinkProvider is null"); + this.accessControl = requireNonNull(accessControl, "accessControl is null"); + this.procedures = ImmutableSet.copyOf(requireNonNull(procedures, "procedures is null")); + this.functionManager = requireNonNull(functionManager, "functionManager is null"); + this.functionResolution = requireNonNull(functionResolution, "functionResolution is null"); + this.rowExpressionService = requireNonNull(rowExpressionService, "rowExpressionService is null"); + this.clickHouseClient = requireNonNull(clickHouseClient, "clickHouseClient is null"); + this.tableProperties = requireNonNull(tableProperties, "tableProperties is null").stream() + .flatMap(tablePropertiesProvider -> tablePropertiesProvider.getTableProperties().stream()) + .collect(toImmutableList()); + } + + @Override + public ConnectorPlanOptimizerProvider getConnectorPlanOptimizerProvider() + { + return new ClickHousePlanOptimizerProvider( + clickHouseClient, + functionManager, + functionResolution, + rowExpressionService.getDeterminismEvaluator(), + rowExpressionService.getExpressionOptimizer()); + } + + @Override + public boolean isSingleStatementWritesOnly() + { + return true; + } + + @Override + public ConnectorTransactionHandle beginTransaction(IsolationLevel isolationLevel, boolean readOnly) + { + checkConnectorSupports(READ_COMMITTED, isolationLevel); + ClickHouseTransactionHandle transaction = new ClickHouseTransactionHandle(); + transactions.put(transaction, clickHouseMetadataFactory.create()); + return transaction; + } + + @Override + public ConnectorMetadata getMetadata(ConnectorTransactionHandle transaction) + { + ClickHouseMetadata metadata = transactions.get(transaction); + checkArgument(metadata != null, "no such transaction: %s", transaction); + return metadata; + } + + @Override + public ConnectorCommitHandle commit(ConnectorTransactionHandle transaction) + { + checkArgument(transactions.remove(transaction) != null, "no such transaction: %s", transaction); + return INSTANCE; + } + + @Override + public void rollback(ConnectorTransactionHandle transaction) + { + ClickHouseMetadata metadata = transactions.remove(transaction); + checkArgument(metadata != null, "no such transaction: %s", transaction); + metadata.rollback(); + } + + @Override + public ConnectorSplitManager getSplitManager() + { + return clickHouseSplitManager; + } + + @Override + public ConnectorRecordSetProvider getRecordSetProvider() + { + return clickHouseRecordSetProvider; + } + + @Override + public ConnectorPageSinkProvider getPageSinkProvider() + { + return clickHousePageSinkProvider; + } + + @Override + public ConnectorAccessControl getAccessControl() + { + return accessControl.orElseThrow(UnsupportedOperationException::new); + } + + @Override + public Set getProcedures() + { + return procedures; + } + + @Override + public final void shutdown() + { + try { + lifeCycleManager.stop(); + } + catch (Exception e) { + log.error(e, "Error shutting down connector"); + } + } + + @Override + public List> getTableProperties() + { + return tableProperties; + } + + @Override + public Set getCapabilities() + { + return immutableEnumSet(NOT_NULL_COLUMN_CONSTRAINT); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseConnectorFactory.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseConnectorFactory.java new file mode 100755 index 0000000000000..0dc4c3d52647b --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseConnectorFactory.java @@ -0,0 +1,84 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.airlift.bootstrap.Bootstrap; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.classloader.ThreadContextClassLoader; +import com.facebook.presto.spi.connector.Connector; +import com.facebook.presto.spi.connector.ConnectorContext; +import com.facebook.presto.spi.connector.ConnectorFactory; +import com.facebook.presto.spi.function.FunctionMetadataManager; +import com.facebook.presto.spi.function.StandardFunctionResolution; +import com.facebook.presto.spi.relation.RowExpressionService; +import com.google.inject.Injector; + +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.base.Throwables.throwIfUnchecked; +import static java.util.Objects.requireNonNull; + +public class ClickHouseConnectorFactory + implements ConnectorFactory +{ + private final String name; + private final ClassLoader classLoader; + + public ClickHouseConnectorFactory(String name, ClassLoader classLoader) + { + checkArgument(!isNullOrEmpty(name), "name is null or empty"); + this.name = name; + this.classLoader = requireNonNull(classLoader, "classLoader is null"); + } + + @Override + public String getName() + { + return name; + } + + @Override + public ConnectorHandleResolver getHandleResolver() + { + return new ClickHouseHandleResolver(); + } + + @Override + public Connector create(String catalogName, Map requiredConfig, ConnectorContext context) + { + requireNonNull(requiredConfig, "requiredConfig is null"); + + try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(classLoader)) { + Bootstrap app = new Bootstrap( + binder -> { + binder.bind(FunctionMetadataManager.class).toInstance(context.getFunctionMetadataManager()); + binder.bind(StandardFunctionResolution.class).toInstance(context.getStandardFunctionResolution()); + binder.bind(RowExpressionService.class).toInstance(context.getRowExpressionService()); + }, + new ClickHouseModule(catalogName)); + Injector injector = app + .doNotInitializeLogging() + .setRequiredConfigurationProperties(requiredConfig) + .initialize(); + + return injector.getInstance(ClickHouseConnector.class); + } + catch (Exception e) { + throwIfUnchecked(e); + throw new RuntimeException(e); + } + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseConnectorId.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseConnectorId.java new file mode 100755 index 0000000000000..60f18807a8ec2 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseConnectorId.java @@ -0,0 +1,53 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +public final class ClickHouseConnectorId +{ + private final String id; + + public ClickHouseConnectorId(String id) + { + this.id = requireNonNull(id, "id is null"); + } + + @Override + public String toString() + { + return id; + } + + @Override + public int hashCode() + { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + ClickHouseConnectorId other = (ClickHouseConnectorId) obj; + return Objects.equals(this.id, other.id); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseEngineType.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseEngineType.java new file mode 100755 index 0000000000000..9ce34c25e0479 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseEngineType.java @@ -0,0 +1,47 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +public enum ClickHouseEngineType +{ + STRIPELOG("StripeLog()"), + LOG("Log()"), + TINYLOG("TinyLog()"), + MERGETREE("MergeTree()"), + VERSIONEDCOLLAPSINGMERGETREE("VersionedCollapsingMergeTree()"), + GRAPHITEMERGETREE("GraphiteMergeTree()"), + AGGREGATINGMERGETREE("AggregatingMergeTree()"), + COLLAPSINGMERGETREE("CollapsingMergeTree()"), + REPLACINGMERGETREE("ReplacingMergeTree()"), + SUMMINGMERGETREE("SummingMergeTree()"), + REPLICATEDMERGETREE("ReplicatedMergeTree()"), + REPLICATEDVERSIONEDCOLLAPSINGMERGETREE("ReplicatedVersionedCollapsingMergeTree()"), + REPLICATEDGRAPHITEMERGETREE("ReplicatedGraphiteMergeTree()"), + REPLICATEDAGGREGATINGMERGETREE("ReplicatedAggregatingMergeTree()"), + REPLICATEDCOLLAPSINGMERGETREE("ReplicatedCollapsingMergeTree()"), + REPLICATEDREPLACINGMERGETREE("ReplicatedReplacingMergeTree()"), + REPLICATEDSUMMINGMERGETREE("ReplicatedSummingMergeTree()"); + + private final String engineType; + + ClickHouseEngineType(String engineType) + { + this.engineType = engineType; + } + + public String getEngineType() + { + return this.engineType; + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseErrorCode.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseErrorCode.java new file mode 100755 index 0000000000000..2094f99db129f --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseErrorCode.java @@ -0,0 +1,40 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.spi.ErrorCode; +import com.facebook.presto.spi.ErrorCodeSupplier; +import com.facebook.presto.spi.ErrorType; + +import static com.facebook.presto.spi.ErrorType.EXTERNAL; + +public enum ClickHouseErrorCode + implements ErrorCodeSupplier +{ + JDBC_ERROR(0, EXTERNAL), + JDBC_NON_TRANSIENT_ERROR(1, EXTERNAL); + //TODO: Support more clickhouse error code + private final ErrorCode errorCode; + + ClickHouseErrorCode(int code, ErrorType type) + { + errorCode = new ErrorCode(code + 0x0400_0000, name(), type); + } + + @Override + public ErrorCode toErrorCode() + { + return errorCode; + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseHandleResolver.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseHandleResolver.java new file mode 100755 index 0000000000000..a7d7d8bd07ed4 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseHandleResolver.java @@ -0,0 +1,69 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableLayoutHandle; +import com.facebook.presto.spi.connector.ConnectorTransactionHandle; + +public class ClickHouseHandleResolver + implements ConnectorHandleResolver +{ + @Override + public Class getTransactionHandleClass() + { + return ClickHouseTransactionHandle.class; + } + + @Override + public Class getTableHandleClass() + { + return ClickHouseTableHandle.class; + } + + @Override + public Class getTableLayoutHandleClass() + { + return ClickHouseTableLayoutHandle.class; + } + + @Override + public Class getColumnHandleClass() + { + return ClickHouseColumnHandle.class; + } + + @Override + public Class getSplitClass() + { + return ClickHouseSplit.class; + } + + @Override + public Class getOutputTableHandleClass() + { + return ClickHouseOutputTableHandle.class; + } + + @Override + public Class getInsertTableHandleClass() + { + return ClickHouseOutputTableHandle.class; + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseIdentity.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseIdentity.java new file mode 100755 index 0000000000000..51fa02486293e --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseIdentity.java @@ -0,0 +1,79 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.spi.ConnectorSession; +import com.google.common.collect.ImmutableMap; + +import java.util.Map; +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.Objects.requireNonNull; + +public class ClickHouseIdentity +{ + public static ClickHouseIdentity from(ConnectorSession session) + { + return new ClickHouseIdentity(session.getIdentity().getUser(), session.getIdentity().getExtraCredentials()); + } + + private final String user; + private final Map extraCredentials; + + public ClickHouseIdentity(String user, Map extraCredentials) + { + this.user = requireNonNull(user, "user is null"); + this.extraCredentials = ImmutableMap.copyOf(requireNonNull(extraCredentials, "extraCredentials is null")); + } + + public String getUser() + { + return user; + } + + public Map getExtraCredentials() + { + return extraCredentials; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ClickHouseIdentity that = (ClickHouseIdentity) o; + return Objects.equals(user, that.user) && + Objects.equals(extraCredentials, that.extraCredentials); + } + + @Override + public int hashCode() + { + return Objects.hash(user, extraCredentials); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("user", user) + .add("extraCredentials", extraCredentials.keySet()) + .toString(); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseMetadata.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseMetadata.java new file mode 100755 index 0000000000000..7a5a4419eb138 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseMetadata.java @@ -0,0 +1,275 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.airlift.log.Logger; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorNewTableLayout; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableLayout; +import com.facebook.presto.spi.ConnectorTableLayoutHandle; +import com.facebook.presto.spi.ConnectorTableLayoutResult; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.Constraint; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SchemaTablePrefix; +import com.facebook.presto.spi.TableNotFoundException; +import com.facebook.presto.spi.connector.ConnectorMetadata; +import com.facebook.presto.spi.connector.ConnectorOutputMetadata; +import com.facebook.presto.spi.statistics.ComputedStatistics; +import com.facebook.presto.spi.statistics.TableStatistics; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.slice.Slice; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import static com.facebook.presto.spi.StandardErrorCode.PERMISSION_DENIED; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +public class ClickHouseMetadata + implements ConnectorMetadata +{ + private static final Logger log = Logger.get(ClickHouseMetadata.class); + private final ClickHouseClient clickHouseClient; + private final boolean allowDropTable; + + private final AtomicReference rollbackAction = new AtomicReference<>(); + + public ClickHouseMetadata(ClickHouseClient clickHouseClient, boolean allowDropTable) + { + this.clickHouseClient = requireNonNull(clickHouseClient, "client is null"); + this.allowDropTable = allowDropTable; + } + + @Override + public boolean schemaExists(ConnectorSession session, String schemaName) + { + return clickHouseClient.schemaExists(ClickHouseIdentity.from(session), schemaName); + } + + @Override + public List listSchemaNames(ConnectorSession session) + { + return ImmutableList.copyOf(clickHouseClient.getSchemaNames(ClickHouseIdentity.from(session))); + } + + @Override + public ClickHouseTableHandle getTableHandle(ConnectorSession session, SchemaTableName tableName) + { + return clickHouseClient.getTableHandle(ClickHouseIdentity.from(session), tableName); + } + + @Override + public List getTableLayouts(ConnectorSession session, ConnectorTableHandle table, Constraint constraint, Optional> desiredColumns) + { + ClickHouseTableHandle tableHandle = (ClickHouseTableHandle) table; + ConnectorTableLayout layout = new ConnectorTableLayout(new ClickHouseTableLayoutHandle(tableHandle, constraint.getSummary(), Optional.empty(), Optional.empty())); + return ImmutableList.of(new ConnectorTableLayoutResult(layout, constraint.getSummary())); + } + + @Override + public ConnectorTableLayout getTableLayout(ConnectorSession session, ConnectorTableLayoutHandle handle) + { + return new ConnectorTableLayout(handle); + } + + @Override + public ConnectorTableMetadata getTableMetadata(ConnectorSession session, ConnectorTableHandle table) + { + ClickHouseTableHandle handle = (ClickHouseTableHandle) table; + + ImmutableList.Builder columnMetadata = ImmutableList.builder(); + for (ClickHouseColumnHandle column : clickHouseClient.getColumns(session, handle)) { + columnMetadata.add(column.getColumnMetadata()); + } + return new ConnectorTableMetadata(handle.getSchemaTableName(), columnMetadata.build()); + } + + @Override + public List listTables(ConnectorSession session, Optional schemaName) + { + return clickHouseClient.getTableNames(ClickHouseIdentity.from(session), schemaName); + } + + @Override + public Map getColumnHandles(ConnectorSession session, ConnectorTableHandle tableHandle) + { + ClickHouseTableHandle clickHouseTableHandle = (ClickHouseTableHandle) tableHandle; + + ImmutableMap.Builder columnHandles = ImmutableMap.builder(); + for (ClickHouseColumnHandle column : clickHouseClient.getColumns(session, clickHouseTableHandle)) { + columnHandles.put(column.getColumnMetadata().getName(), column); + } + return columnHandles.build(); + } + + @Override + public Map> listTableColumns(ConnectorSession session, SchemaTablePrefix prefix) + { + ImmutableMap.Builder> columns = ImmutableMap.builder(); + List tables; + if (prefix.getTableName() != null) { + tables = ImmutableList.of(new SchemaTableName(prefix.getSchemaName(), prefix.getTableName())); + } + else { + tables = listTables(session, prefix.getSchemaName()); + } + for (SchemaTableName tableName : tables) { + try { + ClickHouseTableHandle tableHandle = clickHouseClient.getTableHandle(ClickHouseIdentity.from(session), tableName); + if (tableHandle == null) { + continue; + } + columns.put(tableName, getTableMetadata(session, tableHandle).getColumns()); + } + catch (TableNotFoundException e) { + log.info("table disappeared during listing operation"); + } + } + return columns.build(); + } + + @Override + public ColumnMetadata getColumnMetadata(ConnectorSession session, ConnectorTableHandle tableHandle, ColumnHandle columnHandle) + { + return ((ClickHouseColumnHandle) columnHandle).getColumnMetadata(); + } + + @Override + public void dropTable(ConnectorSession session, ConnectorTableHandle tableHandle) + { + if (!allowDropTable) { + throw new PrestoException(PERMISSION_DENIED, "DROP TABLE is disabled in this catalog"); + } + ClickHouseTableHandle handle = (ClickHouseTableHandle) tableHandle; + clickHouseClient.dropTable(ClickHouseIdentity.from(session), handle); + } + + @Override + public ConnectorOutputTableHandle beginCreateTable(ConnectorSession session, ConnectorTableMetadata tableMetadata, Optional layout) + { + ClickHouseOutputTableHandle handle = clickHouseClient.beginCreateTable(session, tableMetadata); + setRollback(() -> clickHouseClient.rollbackCreateTable(ClickHouseIdentity.from(session), handle)); + return handle; + } + + @Override + public void createTable(ConnectorSession session, ConnectorTableMetadata tableMetadata, boolean ignoreExisting) + { + clickHouseClient.createTable(session, tableMetadata); + } + + @Override + public Optional finishCreateTable(ConnectorSession session, ConnectorOutputTableHandle tableHandle, Collection fragments, Collection computedStatistics) + { + ClickHouseOutputTableHandle handle = (ClickHouseOutputTableHandle) tableHandle; + clickHouseClient.commitCreateTable(ClickHouseIdentity.from(session), handle); + clearRollback(); + return Optional.empty(); + } + + private void setRollback(Runnable action) + { + checkState(rollbackAction.compareAndSet(null, action), "rollback action is already set"); + } + + private void clearRollback() + { + rollbackAction.set(null); + } + + public void rollback() + { + Optional.ofNullable(rollbackAction.getAndSet(null)).ifPresent(Runnable::run); + } + + @Override + public ConnectorInsertTableHandle beginInsert(ConnectorSession session, ConnectorTableHandle tableHandle) + { + ClickHouseOutputTableHandle handle = clickHouseClient.beginInsertTable(session, getTableMetadata(session, tableHandle)); + setRollback(() -> clickHouseClient.rollbackCreateTable(ClickHouseIdentity.from(session), handle)); + return handle; + } + + @Override + public Optional finishInsert(ConnectorSession session, ConnectorInsertTableHandle tableHandle, Collection fragments, Collection computedStatistics) + { + ClickHouseOutputTableHandle clickHouseInsertHandle = (ClickHouseOutputTableHandle) tableHandle; + clickHouseClient.finishInsertTable(ClickHouseIdentity.from(session), clickHouseInsertHandle); + return Optional.empty(); + } + + @Override + public void addColumn(ConnectorSession session, ConnectorTableHandle table, ColumnMetadata columnMetadata) + { + ClickHouseTableHandle tableHandle = (ClickHouseTableHandle) table; + clickHouseClient.addColumn(ClickHouseIdentity.from(session), tableHandle, columnMetadata); + } + + @Override + public void dropColumn(ConnectorSession session, ConnectorTableHandle table, ColumnHandle column) + { + ClickHouseTableHandle tableHandle = (ClickHouseTableHandle) table; + ClickHouseColumnHandle columnHandle = (ClickHouseColumnHandle) column; + clickHouseClient.dropColumn(ClickHouseIdentity.from(session), tableHandle, columnHandle); + } + + @Override + public void renameColumn(ConnectorSession session, ConnectorTableHandle table, ColumnHandle column, String target) + { + ClickHouseTableHandle tableHandle = (ClickHouseTableHandle) table; + ClickHouseColumnHandle columnHandle = (ClickHouseColumnHandle) column; + clickHouseClient.renameColumn(ClickHouseIdentity.from(session), tableHandle, columnHandle, target); + } + + @Override + public void renameTable(ConnectorSession session, ConnectorTableHandle table, SchemaTableName newTableName) + { + ClickHouseTableHandle tableHandle = (ClickHouseTableHandle) table; + clickHouseClient.renameTable(ClickHouseIdentity.from(session), tableHandle, newTableName); + } + + @Override + public TableStatistics getTableStatistics(ConnectorSession session, ConnectorTableHandle tableHandle, Optional tableLayoutHandle, List columnHandles, Constraint constraint) + { + ClickHouseTableHandle handle = (ClickHouseTableHandle) tableHandle; + List columns = columnHandles.stream().map(ClickHouseColumnHandle.class::cast).collect(Collectors.toList()); + return clickHouseClient.getTableStatistics(session, handle, columns, constraint.getSummary()); + } + + @Override + public void createSchema(ConnectorSession session, String schemaName, Map properties) + { + clickHouseClient.createSchema(ClickHouseIdentity.from(session), schemaName, properties); + } + + @Override + public void dropSchema(ConnectorSession session, String schemaName) + { + clickHouseClient.dropSchema(ClickHouseIdentity.from(session), schemaName); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseMetadataFactory.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseMetadataFactory.java new file mode 100755 index 0000000000000..4c2d715398dc2 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseMetadataFactory.java @@ -0,0 +1,37 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import javax.inject.Inject; + +import static java.util.Objects.requireNonNull; + +public class ClickHouseMetadataFactory +{ + private final ClickHouseClient clickHouseClient; + private final boolean allowDropTable; + + @Inject + public ClickHouseMetadataFactory(ClickHouseClient clickHouseClient, ClickHouseConfig config) + { + this.clickHouseClient = requireNonNull(clickHouseClient, "clickHouseClient is null"); + requireNonNull(config, "config is null"); + this.allowDropTable = config.isAllowDropTable(); + } + + public ClickHouseMetadata create() + { + return new ClickHouseMetadata(clickHouseClient, allowDropTable); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseModule.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseModule.java new file mode 100755 index 0000000000000..d8b4ea6bf6c7e --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseModule.java @@ -0,0 +1,90 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.spi.connector.ConnectorAccessControl; +import com.facebook.presto.spi.procedure.Procedure; +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Provides; +import com.google.inject.Scopes; +import com.google.inject.Singleton; +import com.google.inject.multibindings.Multibinder; +import ru.yandex.clickhouse.ClickHouseDriver; + +import java.sql.SQLException; +import java.util.Optional; +import java.util.Properties; + +import static com.facebook.airlift.configuration.ConfigBinder.configBinder; +import static com.google.inject.multibindings.Multibinder.newSetBinder; +import static com.google.inject.multibindings.OptionalBinder.newOptionalBinder; +import static java.util.Objects.requireNonNull; + +public class ClickHouseModule + implements Module +{ + private final String connectorId; + + public ClickHouseModule(String connectorId) + { + this.connectorId = requireNonNull(connectorId, "connector id is null"); + } + + @Override + public void configure(Binder binder) + { + newOptionalBinder(binder, ConnectorAccessControl.class); + newSetBinder(binder, Procedure.class); + binder.bind(ClickHouseConnectorId.class).toInstance(new ClickHouseConnectorId(connectorId)); + binder.bind(ClickHouseMetadataFactory.class).in(Scopes.SINGLETON); + binder.bind(ClickHouseClient.class).in(Scopes.SINGLETON); + binder.bind(ClickHouseSplitManager.class).in(Scopes.SINGLETON); + binder.bind(ClickHouseRecordSetProvider.class).in(Scopes.SINGLETON); + binder.bind(ClickHousePageSinkProvider.class).in(Scopes.SINGLETON); + binder.bind(ClickHouseConnector.class).in(Scopes.SINGLETON); + bindTablePropertiesProvider(binder, ClickHouseTableProperties.class); + configBinder(binder).bindConfig(ClickHouseConfig.class); + } + + @Provides + @Singleton + public static ConnectionFactory connectionFactory(ClickHouseConfig clickhouseConfig) + throws SQLException + { + Properties connectionProperties = new Properties(); + connectionProperties.setProperty("useInformationSchema", "true"); + connectionProperties.setProperty("nullCatalogMeansCurrent", "false"); + connectionProperties.setProperty("useUnicode", "true"); + connectionProperties.setProperty("characterEncoding", "utf8"); + connectionProperties.setProperty("tinyInt1isBit", "false"); + + return new DriverConnectionFactory( + new ClickHouseDriver(), + clickhouseConfig.getConnectionUrl(), + Optional.ofNullable(clickhouseConfig.getUserCredential()), + Optional.ofNullable(clickhouseConfig.getPasswordCredential()), + connectionProperties); + } + + public static Multibinder tablePropertiesProviderBinder(Binder binder) + { + return newSetBinder(binder, TablePropertiesProvider.class); + } + + public static void bindTablePropertiesProvider(Binder binder, Class type) + { + tablePropertiesProviderBinder(binder).addBinding().to(type).in(Scopes.SINGLETON); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseOutputTableHandle.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseOutputTableHandle.java new file mode 100755 index 0000000000000..805745b1c2b03 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseOutputTableHandle.java @@ -0,0 +1,147 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.common.type.Type; +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.Nullable; + +import java.util.List; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +public class ClickHouseOutputTableHandle + implements ConnectorOutputTableHandle, ConnectorInsertTableHandle +{ + private final String connectorId; + private final String catalogName; + private final String schemaName; + private final String tableName; + private final List columnNames; + private final List columnTypes; + private final String temporaryTableName; + + @JsonCreator + public ClickHouseOutputTableHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("catalogName") @Nullable String catalogName, + @JsonProperty("schemaName") @Nullable String schemaName, + @JsonProperty("tableName") String tableName, + @JsonProperty("columnNames") List columnNames, + @JsonProperty("columnTypes") List columnTypes, + @JsonProperty("temporaryTableName") String temporaryTableName) + { + this.connectorId = requireNonNull(connectorId, "connectorId is null"); + this.catalogName = catalogName; + this.schemaName = schemaName; + this.tableName = requireNonNull(tableName, "tableName is null"); + this.temporaryTableName = requireNonNull(temporaryTableName, "temporaryTableName is null"); + + requireNonNull(columnNames, "columnNames is null"); + requireNonNull(columnTypes, "columnTypes is null"); + checkArgument(columnNames.size() == columnTypes.size(), "columnNames and columnTypes sizes don't match"); + this.columnNames = ImmutableList.copyOf(columnNames); + this.columnTypes = ImmutableList.copyOf(columnTypes); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + @Nullable + public String getCatalogName() + { + return catalogName; + } + + @JsonProperty + @Nullable + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + @JsonProperty + public List getColumnNames() + { + return columnNames; + } + + @JsonProperty + public List getColumnTypes() + { + return columnTypes; + } + + @JsonProperty + public String getTemporaryTableName() + { + return temporaryTableName; + } + + @Override + public String toString() + { + return format("jdbc:%s.%s.%s", catalogName, schemaName, tableName); + } + + @Override + public int hashCode() + { + return Objects.hash( + connectorId, + catalogName, + schemaName, + tableName, + columnNames, + columnTypes, + temporaryTableName); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ClickHouseOutputTableHandle other = (ClickHouseOutputTableHandle) obj; + return Objects.equals(this.connectorId, other.connectorId) && + Objects.equals(this.catalogName, other.catalogName) && + Objects.equals(this.schemaName, other.schemaName) && + Objects.equals(this.tableName, other.tableName) && + Objects.equals(this.columnNames, other.columnNames) && + Objects.equals(this.columnTypes, other.columnTypes) && + Objects.equals(this.temporaryTableName, other.temporaryTableName); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHousePageSink.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHousePageSink.java new file mode 100755 index 0000000000000..f810d0b7a3989 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHousePageSink.java @@ -0,0 +1,224 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.airlift.log.Logger; +import com.facebook.presto.common.Page; +import com.facebook.presto.common.block.Block; +import com.facebook.presto.common.type.DecimalType; +import com.facebook.presto.common.type.Type; +import com.facebook.presto.spi.ConnectorPageSink; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Shorts; +import com.google.common.primitives.SignedBytes; +import io.airlift.slice.Slice; +import org.joda.time.DateTimeZone; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.SQLNonTransientException; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static com.facebook.presto.common.type.BigintType.BIGINT; +import static com.facebook.presto.common.type.BooleanType.BOOLEAN; +import static com.facebook.presto.common.type.Chars.isCharType; +import static com.facebook.presto.common.type.DateType.DATE; +import static com.facebook.presto.common.type.Decimals.readBigDecimal; +import static com.facebook.presto.common.type.DoubleType.DOUBLE; +import static com.facebook.presto.common.type.IntegerType.INTEGER; +import static com.facebook.presto.common.type.RealType.REAL; +import static com.facebook.presto.common.type.SmallintType.SMALLINT; +import static com.facebook.presto.common.type.TinyintType.TINYINT; +import static com.facebook.presto.common.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.common.type.Varchars.isVarcharType; +import static com.facebook.presto.plugin.clickhouse.ClickHouseErrorCode.JDBC_ERROR; +import static com.facebook.presto.plugin.clickhouse.ClickHouseErrorCode.JDBC_NON_TRANSIENT_ERROR; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static java.lang.Float.intBitsToFloat; +import static java.lang.Math.toIntExact; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.concurrent.TimeUnit.DAYS; +import static org.joda.time.chrono.ISOChronology.getInstanceUTC; + +public class ClickHousePageSink + implements ConnectorPageSink +{ + private static final Logger log = Logger.get(ClickHousePageSink.class); + private final Connection connection; + private final PreparedStatement statement; + private final List columnTypes; + private int batchSize; + private int commitBatchSize; + + public ClickHousePageSink(ConnectorSession session, ClickHouseOutputTableHandle handle, ClickHouseClient clickHouseClient) + { + try { + connection = clickHouseClient.getConnection(ClickHouseIdentity.from(session), handle); + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + + try { + connection.setAutoCommit(false); + String insertSql = clickHouseClient.buildInsertSql(handle); + statement = connection.prepareStatement(clickHouseClient.buildInsertSql(handle)); + } + catch (SQLException e) { + closeWithSuppression(connection, e); + throw new PrestoException(JDBC_ERROR, e); + } + + this.commitBatchSize = clickHouseClient.getCommitBatchSize(); + if (commitBatchSize < 1000) { + this.commitBatchSize = 1000; + } + + columnTypes = handle.getColumnTypes(); + } + + @Override + public CompletableFuture appendPage(Page page) + { + try { + for (int position = 0; position < page.getPositionCount(); position++) { + for (int channel = 0; channel < page.getChannelCount(); channel++) { + appendColumn(page, position, channel); + } + + statement.addBatch(); + batchSize++; + + if (batchSize >= commitBatchSize) { + statement.executeBatch(); + connection.commit(); + connection.setAutoCommit(false); + batchSize = 0; + } + } + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + return NOT_BLOCKED; + } + + private void appendColumn(Page page, int position, int channel) + throws SQLException + { + Block block = page.getBlock(channel); + int parameter = channel + 1; + + if (block.isNull(position)) { + statement.setObject(parameter, null); + return; + } + + Type type = columnTypes.get(channel); + if (BOOLEAN.equals(type)) { + statement.setBoolean(parameter, type.getBoolean(block, position)); + } + else if (BIGINT.equals(type)) { + statement.setLong(parameter, type.getLong(block, position)); + } + else if (INTEGER.equals(type)) { + statement.setInt(parameter, toIntExact(type.getLong(block, position))); + } + else if (SMALLINT.equals(type)) { + statement.setShort(parameter, Shorts.checkedCast(type.getLong(block, position))); + } + else if (TINYINT.equals(type)) { + statement.setByte(parameter, SignedBytes.checkedCast(type.getLong(block, position))); + } + else if (DOUBLE.equals(type)) { + statement.setDouble(parameter, type.getDouble(block, position)); + } + else if (REAL.equals(type)) { + statement.setFloat(parameter, intBitsToFloat(toIntExact(type.getLong(block, position)))); + } + else if (type instanceof DecimalType) { + statement.setBigDecimal(parameter, readBigDecimal((DecimalType) type, block, position)); + } + else if (isVarcharType(type) || isCharType(type)) { + statement.setString(parameter, type.getSlice(block, position).toStringUtf8()); + } + else if (VARBINARY.equals(type)) { + statement.setBytes(parameter, type.getSlice(block, position).getBytes()); + } + else if (DATE.equals(type)) { + // convert to midnight in default time zone + long utcMillis = DAYS.toMillis(type.getLong(block, position)); + long localMillis = getInstanceUTC().getZone().getMillisKeepLocal(DateTimeZone.getDefault(), utcMillis); + statement.setDate(parameter, new Date(localMillis)); + } + else { + throw new PrestoException(NOT_SUPPORTED, "Unsupported column type: " + type.getDisplayName()); + } + } + + @Override + public CompletableFuture> finish() + { + // commit and close + try (Connection connection = this.connection; PreparedStatement statement = this.statement) { + if (batchSize > 0) { + statement.executeBatch(); + connection.commit(); + } + } + catch (SQLNonTransientException e) { + throw new PrestoException(JDBC_NON_TRANSIENT_ERROR, e); + } + catch (SQLException e) { + throw new PrestoException(JDBC_ERROR, e); + } + // the committer does not need any additional info + return completedFuture(ImmutableList.of()); + } + + @SuppressWarnings("unused") + @Override + public void abort() + { + // rollback and close + try (Connection connection = this.connection; + PreparedStatement statement = this.statement) { + connection.rollback(); + } + catch (SQLException e) { + // Exceptions happened during abort do not cause any real damage so ignore them + log.debug(e, "SQLException when abort"); + } + } + + @SuppressWarnings("ObjectEquality") + private static void closeWithSuppression(Connection connection, Throwable throwable) + { + try { + connection.close(); + } + catch (Throwable t) { + // Self-suppression not permitted + if (throwable != t) { + throwable.addSuppressed(t); + } + } + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHousePageSinkProvider.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHousePageSinkProvider.java new file mode 100755 index 0000000000000..8c4e9059567a6 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHousePageSinkProvider.java @@ -0,0 +1,53 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorPageSink; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PageSinkContext; +import com.facebook.presto.spi.connector.ConnectorPageSinkProvider; +import com.facebook.presto.spi.connector.ConnectorTransactionHandle; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +public class ClickHousePageSinkProvider + implements ConnectorPageSinkProvider +{ + private final ClickHouseClient clickHouseClient; + + @Inject + public ClickHousePageSinkProvider(ClickHouseClient clickHouseClient) + { + this.clickHouseClient = requireNonNull(clickHouseClient, "clickHouseClient is null"); + } + + @Override + public ConnectorPageSink createPageSink(ConnectorTransactionHandle transactionHandle, ConnectorSession session, ConnectorOutputTableHandle tableHandle, PageSinkContext pageSinkContext) + { + checkArgument(!pageSinkContext.isCommitRequired(), "ClickHouse connector does not support page sink commit"); + return new ClickHousePageSink(session, (ClickHouseOutputTableHandle) tableHandle, clickHouseClient); + } + + @Override + public ConnectorPageSink createPageSink(ConnectorTransactionHandle transactionHandle, ConnectorSession session, ConnectorInsertTableHandle tableHandle, PageSinkContext pageSinkContext) + { + checkArgument(!pageSinkContext.isCommitRequired(), "ClickHouse connector does not support page sink commit"); + return new ClickHousePageSink(session, (ClickHouseOutputTableHandle) tableHandle, clickHouseClient); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHousePlugin.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHousePlugin.java new file mode 100755 index 0000000000000..148c6b938153f --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHousePlugin.java @@ -0,0 +1,35 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.spi.Plugin; +import com.facebook.presto.spi.connector.ConnectorFactory; +import com.google.common.collect.ImmutableList; + +import static com.google.common.base.MoreObjects.firstNonNull; + +public class ClickHousePlugin + implements Plugin +{ + @Override + public Iterable getConnectorFactories() + { + return ImmutableList.of(new ClickHouseConnectorFactory("clickhouse", getClassLoader())); + } + + private static ClassLoader getClassLoader() + { + return firstNonNull(Thread.currentThread().getContextClassLoader(), ClickHousePlugin.class.getClassLoader()); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseRecordCursor.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseRecordCursor.java new file mode 100755 index 0000000000000..f41194cce8fc7 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseRecordCursor.java @@ -0,0 +1,238 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.airlift.log.Logger; +import com.facebook.presto.common.type.Type; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.RecordCursor; +import com.google.common.base.VerifyException; +import io.airlift.slice.Slice; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +import static com.facebook.presto.plugin.clickhouse.ClickHouseErrorCode.JDBC_ERROR; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +public class ClickHouseRecordCursor + implements RecordCursor +{ + private static final Logger log = Logger.get(ClickHouseRecordCursor.class); + + private final ClickHouseColumnHandle[] columnHandles; + private final BooleanReadFunction[] booleanReadFunctions; + private final DoubleReadFunction[] doubleReadFunctions; + private final LongReadFunction[] longReadFunctions; + private final SliceReadFunction[] sliceReadFunctions; + + private final ClickHouseClient clickHouseClient; + private final Connection connection; + private final PreparedStatement statement; + private final ResultSet resultSet; + private boolean closed; + + public ClickHouseRecordCursor(ClickHouseClient clickHouseClient, ConnectorSession session, ClickHouseSplit split, List columnHandles) + { + this.clickHouseClient = requireNonNull(clickHouseClient, "clickHouseClient is null"); + + this.columnHandles = columnHandles.toArray(new ClickHouseColumnHandle[0]); + + booleanReadFunctions = new BooleanReadFunction[columnHandles.size()]; + doubleReadFunctions = new DoubleReadFunction[columnHandles.size()]; + longReadFunctions = new LongReadFunction[columnHandles.size()]; + sliceReadFunctions = new SliceReadFunction[columnHandles.size()]; + + for (int i = 0; i < this.columnHandles.length; i++) { + ReadMapping readMapping = clickHouseClient.toPrestoType(session, columnHandles.get(i).getClickHouseTypeHandle()) + .orElseThrow(() -> new VerifyException("Unsupported column type")); + Class javaType = readMapping.getType().getJavaType(); + ReadFunction readFunction = readMapping.getReadFunction(); + + if (javaType == boolean.class) { + booleanReadFunctions[i] = (BooleanReadFunction) readFunction; + } + else if (javaType == double.class) { + doubleReadFunctions[i] = (DoubleReadFunction) readFunction; + } + else if (javaType == long.class) { + longReadFunctions[i] = (LongReadFunction) readFunction; + } + else if (javaType == Slice.class) { + sliceReadFunctions[i] = (SliceReadFunction) readFunction; + } + else { + throw new IllegalStateException(format("Unsupported java type %s", javaType)); + } + } + + try { + connection = clickHouseClient.getConnection(ClickHouseIdentity.from(session), split); + statement = clickHouseClient.buildSql(session, connection, split, columnHandles); + log.debug("Executing: %s", statement.toString()); + resultSet = statement.executeQuery(); + } + catch (SQLException | RuntimeException e) { + throw handleSqlException(e); + } + } + + @Override + public long getReadTimeNanos() + { + return 0; + } + + @Override + public long getCompletedBytes() + { + return 0; + } + + @Override + public Type getType(int field) + { + return columnHandles[field].getColumnType(); + } + + @Override + public boolean advanceNextPosition() + { + if (closed) { + return false; + } + + try { + return resultSet.next(); + } + catch (SQLException | RuntimeException e) { + throw handleSqlException(e); + } + } + + @Override + public boolean getBoolean(int field) + { + checkState(!closed, "cursor is closed"); + try { + return booleanReadFunctions[field].readBoolean(resultSet, field + 1); + } + catch (SQLException | RuntimeException e) { + throw handleSqlException(e); + } + } + + @Override + public long getLong(int field) + { + checkState(!closed, "cursor is closed"); + try { + return longReadFunctions[field].readLong(resultSet, field + 1); + } + catch (SQLException | RuntimeException e) { + throw handleSqlException(e); + } + } + + @Override + public double getDouble(int field) + { + checkState(!closed, "cursor is closed"); + try { + return doubleReadFunctions[field].readDouble(resultSet, field + 1); + } + catch (SQLException | RuntimeException e) { + throw handleSqlException(e); + } + } + + @Override + public Slice getSlice(int field) + { + checkState(!closed, "cursor is closed"); + try { + return sliceReadFunctions[field].readSlice(resultSet, field + 1); + } + catch (SQLException | RuntimeException e) { + throw handleSqlException(e); + } + } + + @Override + public Object getObject(int field) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isNull(int field) + { + checkState(!closed, "cursor is closed"); + checkArgument(field < columnHandles.length, "Invalid field index"); + + try { + // JDBC is kind of dumb: we need to read the field and then ask + // if it was null, which means we are wasting effort here. + // We could save the result of the field access if it matters. + resultSet.getObject(field + 1); + + return resultSet.wasNull(); + } + catch (SQLException | RuntimeException e) { + throw handleSqlException(e); + } + } + + @SuppressWarnings("UnusedDeclaration") + @Override + public void close() + { + if (closed) { + return; + } + closed = true; + + // use try with resources to close everything properly + try (Connection connection = this.connection; + Statement statement = this.statement; + ResultSet resultSet = this.resultSet) { + clickHouseClient.abortReadConnection(connection); + } + catch (SQLException e) { + // ignore exception from close + } + } + + private RuntimeException handleSqlException(Exception e) + { + try { + close(); + } + catch (Exception closeException) { + // Self-suppression not permitted + if (e != closeException) { + e.addSuppressed(closeException); + } + } + return new PrestoException(JDBC_ERROR, e); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseRecordSet.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseRecordSet.java new file mode 100755 index 0000000000000..e832cac4ab83b --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseRecordSet.java @@ -0,0 +1,61 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.common.type.Type; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordSet; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static java.util.Objects.requireNonNull; + +public class ClickHouseRecordSet + implements RecordSet +{ + private final ClickHouseClient clickHouseClient; + private final List columnHandles; + private final List columnTypes; + private final ClickHouseSplit split; + private final ConnectorSession session; + + public ClickHouseRecordSet(ClickHouseClient clickHouseClient, ConnectorSession session, ClickHouseSplit split, List columnHandles) + { + this.clickHouseClient = requireNonNull(clickHouseClient, "clickHouseClient is null"); + this.split = requireNonNull(split, "split is null"); + + requireNonNull(split, "split is null"); + this.columnHandles = requireNonNull(columnHandles, "column handles is null"); + ImmutableList.Builder types = ImmutableList.builder(); + for (ClickHouseColumnHandle column : columnHandles) { + types.add(column.getColumnType()); + } + this.columnTypes = types.build(); + this.session = requireNonNull(session, "session is null"); + } + + @Override + public List getColumnTypes() + { + return columnTypes; + } + + @Override + public RecordCursor cursor() + { + return new ClickHouseRecordCursor(clickHouseClient, session, split, columnHandles); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseRecordSetProvider.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseRecordSetProvider.java new file mode 100755 index 0000000000000..305657fc5eb1d --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseRecordSetProvider.java @@ -0,0 +1,53 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.connector.ConnectorRecordSetProvider; +import com.facebook.presto.spi.connector.ConnectorTransactionHandle; +import com.google.common.collect.ImmutableList; + +import javax.inject.Inject; + +import java.util.List; + +import static java.util.Objects.requireNonNull; + +public class ClickHouseRecordSetProvider + implements ConnectorRecordSetProvider +{ + private final ClickHouseClient clickHouseClient; + + @Inject + public ClickHouseRecordSetProvider(ClickHouseClient clickHouseClient) + { + this.clickHouseClient = requireNonNull(clickHouseClient, "clickHouseClient is null"); + } + + @Override + public RecordSet getRecordSet(ConnectorTransactionHandle transactionHandle, ConnectorSession session, ConnectorSplit split, List columns) + { + ClickHouseSplit clickHouseSplit = (ClickHouseSplit) split; + + ImmutableList.Builder handles = ImmutableList.builder(); + for (ColumnHandle handle : columns) { + handles.add((ClickHouseColumnHandle) handle); + } + + return new ClickHouseRecordSet(clickHouseClient, session, clickHouseSplit, handles.build()); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseSplit.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseSplit.java new file mode 100755 index 0000000000000..403797be90163 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseSplit.java @@ -0,0 +1,151 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.common.predicate.TupleDomain; +import com.facebook.presto.plugin.clickhouse.optimization.ClickHouseExpression; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.NodeProvider; +import com.facebook.presto.spi.SplitWeight; +import com.facebook.presto.spi.schedule.NodeSelectionStrategy; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.Nullable; + +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; + +import static com.facebook.presto.spi.schedule.NodeSelectionStrategy.NO_PREFERENCE; +import static java.util.Objects.requireNonNull; + +public class ClickHouseSplit + implements ConnectorSplit +{ + private final String connectorId; + private final String catalogName; + private final String schemaName; + private final String tableName; + private final TupleDomain tupleDomain; + private final Optional additionalPredicate; + private Optional simpleExpression; + + @JsonCreator + public ClickHouseSplit( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("catalogName") @Nullable String catalogName, + @JsonProperty("schemaName") @Nullable String schemaName, + @JsonProperty("tableName") String tableName, + @JsonProperty("tupleDomain") TupleDomain tupleDomain, + @JsonProperty("additionalProperty") Optional additionalPredicate, + @JsonProperty("simpleExpression") Optional simpleExpression) + { + this.connectorId = requireNonNull(connectorId, "connector id is null"); + this.catalogName = catalogName; + this.schemaName = schemaName; + this.tableName = requireNonNull(tableName, "table name is null"); + this.tupleDomain = requireNonNull(tupleDomain, "tupleDomain is null"); + this.additionalPredicate = requireNonNull(additionalPredicate, "additionalPredicate is null"); + this.simpleExpression = simpleExpression; + } + + @JsonProperty + public Optional getSimpleExpression() + { + return simpleExpression; + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + @Nullable + public String getCatalogName() + { + return catalogName; + } + + @JsonProperty + @Nullable + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + @JsonProperty + public TupleDomain getTupleDomain() + { + return tupleDomain; + } + + @JsonProperty + public Optional getAdditionalPredicate() + { + return additionalPredicate; + } + + @Override + public NodeSelectionStrategy getNodeSelectionStrategy() + { + return NO_PREFERENCE; + } + + @Override + public List getPreferredNodes(NodeProvider nodeProvider) + { + return null; + } + + public List getAddresses() + { + return ImmutableList.of(); + } + + @Override + public Object getInfo() + { + return this; + } + + @Override + public Object getSplitIdentifier() + { + return ConnectorSplit.super.getSplitIdentifier(); + } + + @Override + public OptionalLong getSplitSizeInBytes() + { + return ConnectorSplit.super.getSplitSizeInBytes(); + } + + @Override + public SplitWeight getSplitWeight() + { + return ConnectorSplit.super.getSplitWeight(); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseSplitManager.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseSplitManager.java new file mode 100755 index 0000000000000..9fc449b0353eb --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseSplitManager.java @@ -0,0 +1,47 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableLayoutHandle; +import com.facebook.presto.spi.connector.ConnectorSplitManager; +import com.facebook.presto.spi.connector.ConnectorTransactionHandle; + +import javax.inject.Inject; + +import static java.util.Objects.requireNonNull; + +public class ClickHouseSplitManager + implements ConnectorSplitManager +{ + private final ClickHouseClient clickHouseClient; + + @Inject + public ClickHouseSplitManager(ClickHouseClient clickHouseClient) + { + this.clickHouseClient = requireNonNull(clickHouseClient, "client is null"); + } + + @Override + public ConnectorSplitSource getSplits( + ConnectorTransactionHandle transactionHandle, + ConnectorSession session, + ConnectorTableLayoutHandle layout, + SplitSchedulingContext splitSchedulingContext) + { + ClickHouseTableLayoutHandle layoutHandle = (ClickHouseTableLayoutHandle) layout; + return clickHouseClient.getSplits(ClickHouseIdentity.from(session), layoutHandle); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTableHandle.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTableHandle.java new file mode 100755 index 0000000000000..eb761b55a64b7 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTableHandle.java @@ -0,0 +1,109 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.SchemaTableName; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Joiner; + +import javax.annotation.Nullable; + +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +public final class ClickHouseTableHandle + implements ConnectorTableHandle +{ + private final String connectorId; + private final SchemaTableName schemaTableName; + private final String catalogName; + private final String schemaName; + private final String tableName; + + @JsonCreator + public ClickHouseTableHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("schemaTableName") SchemaTableName schemaTableName, + @JsonProperty("catalogName") @Nullable String catalogName, + @JsonProperty("schemaName") @Nullable String schemaName, + @JsonProperty("tableName") String tableName) + { + this.connectorId = requireNonNull(connectorId, "connectorId is null"); + this.schemaTableName = requireNonNull(schemaTableName, "schemaTableName is null"); + this.catalogName = catalogName; + this.schemaName = schemaName; + this.tableName = requireNonNull(tableName, "tableName is null"); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public SchemaTableName getSchemaTableName() + { + return schemaTableName; + } + + @JsonProperty + @Nullable + public String getCatalogName() + { + return catalogName; + } + + @JsonProperty + @Nullable + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + ClickHouseTableHandle o = (ClickHouseTableHandle) obj; + return Objects.equals(this.connectorId, o.connectorId) && + Objects.equals(this.schemaTableName, o.schemaTableName); + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, schemaTableName); + } + + @Override + public String toString() + { + return Joiner.on(":").useForNull("null").join(connectorId, schemaTableName, catalogName, schemaName, tableName); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTableLayoutHandle.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTableLayoutHandle.java new file mode 100755 index 0000000000000..9563ee8d3d247 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTableLayoutHandle.java @@ -0,0 +1,99 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.common.predicate.TupleDomain; +import com.facebook.presto.plugin.clickhouse.optimization.ClickHouseExpression; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorTableLayoutHandle; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class ClickHouseTableLayoutHandle + implements ConnectorTableLayoutHandle +{ + private final ClickHouseTableHandle table; + private final TupleDomain tupleDomain; + private final Optional additionalPredicate; + private Optional simpleExpression; + + @JsonCreator + public ClickHouseTableLayoutHandle( + @JsonProperty("table") ClickHouseTableHandle table, + @JsonProperty("tupleDomain") TupleDomain domain, + @JsonProperty("additionalPredicate") Optional additionalPredicate, + @JsonProperty("simpleExpression") Optional simpleExpression) + { + this.table = requireNonNull(table, "table is null"); + this.tupleDomain = requireNonNull(domain, "tupleDomain is null"); + this.additionalPredicate = additionalPredicate; + this.simpleExpression = simpleExpression; + } + @JsonProperty + public Optional getSimpleExpression() + { + return simpleExpression; + } + + @JsonProperty + public Optional getAdditionalPredicate() + { + return additionalPredicate; + } + + @JsonProperty + public ClickHouseTableHandle getTable() + { + return table; + } + + @JsonProperty + public TupleDomain getTupleDomain() + { + return tupleDomain; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ClickHouseTableLayoutHandle that = (ClickHouseTableLayoutHandle) o; + return Objects.equals(table, that.table) && + Objects.equals(tupleDomain, that.tupleDomain) && + Objects.equals(additionalPredicate, that.additionalPredicate) && + Objects.equals(simpleExpression, that.simpleExpression); + } + + @Override + public int hashCode() + { + return Objects.hash(table, tupleDomain, additionalPredicate, simpleExpression); + } + + @Override + public String toString() + { + return table.toString(); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTableProperties.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTableProperties.java new file mode 100755 index 0000000000000..ebd873463da12 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTableProperties.java @@ -0,0 +1,180 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.common.type.ArrayType; +import com.facebook.presto.spi.session.PropertyMetadata; +import com.google.common.collect.ImmutableList; + +import javax.inject.Inject; + +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +import static com.facebook.presto.common.type.VarcharType.VARCHAR; +import static com.facebook.presto.plugin.clickhouse.ClickhouseDXLKeyWords.ORDER_BY_PROPERTY; +import static com.facebook.presto.plugin.clickhouse.ClickhouseDXLKeyWords.PARTITION_BY_PROPERTY; +import static com.facebook.presto.plugin.clickhouse.ClickhouseDXLKeyWords.PRIMARY_KEY_PROPERTY; +import static com.facebook.presto.plugin.clickhouse.ClickhouseDXLKeyWords.SAMPLE_BY_PROPERTY; +import static java.lang.String.format; +import static java.util.Locale.ENGLISH; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; + +public final class ClickHouseTableProperties + implements TablePropertiesProvider +{ + public static final String ENGINE_PROPERTY = "engine"; + + public static final ClickHouseEngineType DEFAULT_TABLE_ENGINE = ClickHouseEngineType.LOG; + + private final List> tableProperties; + + @Inject + public ClickHouseTableProperties() + { + tableProperties = ImmutableList.of( + enumProperty( + ENGINE_PROPERTY, + "ClickHouse Table Engine, defaults to Log", + ClickHouseEngineType.class, + DEFAULT_TABLE_ENGINE, + false), + new PropertyMetadata<>( + ORDER_BY_PROPERTY, + "columns to be the sorting key, it's required for table MergeTree engine family", + new ArrayType(VARCHAR), + List.class, + ImmutableList.of(), + false, + value -> (List) value, + value -> value), + new PropertyMetadata<>( + PARTITION_BY_PROPERTY, + "columns to be the partition key. it's optional for table MergeTree engine family", + new ArrayType(VARCHAR), + List.class, + ImmutableList.of(), + false, + value -> (List) value, + value -> value), + new PropertyMetadata<>( + PRIMARY_KEY_PROPERTY, + "columns to be the primary key. it's optional for table MergeTree engine family", + new ArrayType(VARCHAR), + List.class, + ImmutableList.of(), + false, + value -> (List) value, + value -> value), + stringProperty( + SAMPLE_BY_PROPERTY, + "An expression for sampling. it's optional for table MergeTree engine family", + null, + false)); + } + + public static ClickHouseEngineType getEngine(Map tableProperties) + { + requireNonNull(tableProperties, "tableProperties is null"); + return (ClickHouseEngineType) tableProperties.get(ENGINE_PROPERTY); + } + + @SuppressWarnings("unchecked") + public static List getOrderBy(Map tableProperties) + { + requireNonNull(tableProperties, "tableProperties is null"); + return (List) tableProperties.get(ORDER_BY_PROPERTY); + } + + @SuppressWarnings("unchecked") + public static List getPartitionBy(Map tableProperties) + { + requireNonNull(tableProperties, "tableProperties is null"); + return (List) tableProperties.get(PARTITION_BY_PROPERTY); + } + + @SuppressWarnings("unchecked") + public static List getPrimaryKey(Map tableProperties) + { + requireNonNull(tableProperties, "tableProperties is null"); + return (List) tableProperties.get(PRIMARY_KEY_PROPERTY); + } + + public static Optional getSampleBy(Map tableProperties) + { + requireNonNull(tableProperties, "tableProperties is null"); + + return Optional.ofNullable(tableProperties.get(SAMPLE_BY_PROPERTY)).map(String.class::cast); + } + + public List> getTableProperties() + { + return tableProperties; + } + public static > PropertyMetadata enumProperty(String name, String descriptionPrefix, Class type, T defaultValue, boolean hidden) + { + return enumProperty(name, descriptionPrefix, type, defaultValue, value -> {}, hidden); + } + + public static > PropertyMetadata enumProperty(String name, String descriptionPrefix, Class type, T defaultValue, Consumer validation, boolean hidden) + { + String allValues = EnumSet.allOf(type).stream() + .map(Enum::name) + .collect(joining(", ", "[", "]")); + return new PropertyMetadata<>( + name, + format("%s. Possible values: %s", descriptionPrefix, allValues), + VARCHAR, + type, + defaultValue, + hidden, + value -> { + T enumValue; + try { + enumValue = Enum.valueOf(type, ((String) value).toUpperCase(ENGLISH)); + } + catch (IllegalArgumentException e) { + throw new IllegalArgumentException(format("Invalid value [%s]. Valid values: %s", value, allValues), e); + } + validation.accept(enumValue); + return enumValue; + }, + Enum::name); + } + public static PropertyMetadata stringProperty(String name, String description, String defaultValue, boolean hidden) + { + return stringProperty(name, description, defaultValue, value -> {}, hidden); + } + + public static PropertyMetadata stringProperty(String name, String description, String defaultValue, Consumer validation, boolean hidden) + { + return new PropertyMetadata<>( + name, + description, + VARCHAR, + String.class, + defaultValue, + hidden, + object -> { + String value = (String) object; + validation.accept(value); + return value; + }, + object -> object); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTransactionHandle.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTransactionHandle.java new file mode 100755 index 0000000000000..7e668c8573653 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTransactionHandle.java @@ -0,0 +1,74 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.spi.connector.ConnectorTransactionHandle; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; +import java.util.UUID; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.Objects.requireNonNull; + +public class ClickHouseTransactionHandle + implements ConnectorTransactionHandle +{ + private final UUID uuid; + + public ClickHouseTransactionHandle() + { + this(UUID.randomUUID()); + } + + @JsonCreator + public ClickHouseTransactionHandle(@JsonProperty("uuid") UUID uuid) + { + this.uuid = requireNonNull(uuid, "uuid is null"); + } + + @JsonProperty + public UUID getUuid() + { + return uuid; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + ClickHouseTransactionHandle other = (ClickHouseTransactionHandle) obj; + return Objects.equals(uuid, other.uuid); + } + + @Override + public int hashCode() + { + return Objects.hash(uuid); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("uuid", uuid) + .toString(); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTypeHandle.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTypeHandle.java new file mode 100755 index 0000000000000..8fc6da55963bd --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickHouseTypeHandle.java @@ -0,0 +1,135 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; +import java.util.Optional; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.Objects.requireNonNull; + +public final class ClickHouseTypeHandle +{ + private final int jdbcType; + private final Optional jdbcTypeName; + private final int columnSize; + private final int decimalDigits; + private final Optional arrayDimensions; + private final Optional caseSensitivity; + + @JsonCreator + public ClickHouseTypeHandle( + @JsonProperty("jdbcType") int jdbcType, + @JsonProperty("jdbcTypeName") Optional jdbcTypeName, + @JsonProperty("columnSize") int columnSize, + @JsonProperty("decimalDigits") int decimalDigits, + @JsonProperty("arrayDimensions") Optional arrayDimensions, + @JsonProperty("caseSensitivity") Optional caseSensitivity) + { + this.jdbcType = jdbcType; + this.jdbcTypeName = requireNonNull(jdbcTypeName, "jdbcTypeName is null"); + this.columnSize = requireNonNull(columnSize, "columnSize is null"); + this.decimalDigits = requireNonNull(decimalDigits, "decimalDigits is null"); + this.arrayDimensions = requireNonNull(arrayDimensions, "arrayDimensions is null"); + this.caseSensitivity = requireNonNull(caseSensitivity, "caseSensitivity is null"); + } + + @JsonProperty + public int getJdbcType() + { + return jdbcType; + } + + @JsonProperty + public Optional getJdbcTypeName() + { + return jdbcTypeName; + } + + @JsonProperty + public int getColumnSize() + { + return columnSize; + } + + @JsonIgnore + public int getRequiredColumnSize() + { + return getColumnSize(); + } + + @JsonProperty + public int getDecimalDigits() + { + return decimalDigits; + } + + @JsonIgnore + public int getRequiredDecimalDigits() + { + return getDecimalDigits(); + } + + @JsonProperty + public Optional getArrayDimensions() + { + return arrayDimensions; + } + + @JsonProperty + public Optional getCaseSensitivity() + { + return caseSensitivity; + } + + @Override + public int hashCode() + { + return Objects.hash(jdbcType, jdbcTypeName, columnSize, decimalDigits, arrayDimensions); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ClickHouseTypeHandle that = (ClickHouseTypeHandle) o; + return jdbcType == that.jdbcType && + Objects.equals(columnSize, that.columnSize) && + Objects.equals(decimalDigits, that.decimalDigits) && + Objects.equals(jdbcTypeName, that.jdbcTypeName) && + Objects.equals(arrayDimensions, that.arrayDimensions); + } + + @Override + public String toString() + { + return toStringHelper(this) + .omitNullValues() + .add("jdbcType", jdbcType) + .add("jdbcTypeName", jdbcTypeName.orElse(null)) + .add("columnSize", columnSize) + .add("decimalDigits", decimalDigits) + .add("arrayDimensions", arrayDimensions.orElse(null)) + .toString(); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickhouseDXLKeyWords.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickhouseDXLKeyWords.java new file mode 100644 index 0000000000000..5f57705634306 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ClickhouseDXLKeyWords.java @@ -0,0 +1,26 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +public class ClickhouseDXLKeyWords +{ + private ClickhouseDXLKeyWords() + { + } + + public static final String ORDER_BY_PROPERTY = "order_by"; //required + public static final String PARTITION_BY_PROPERTY = "partition_by"; //optional + public static final String PRIMARY_KEY_PROPERTY = "primary_key"; //optional + public static final String SAMPLE_BY_PROPERTY = "sample_by"; //optional +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ConnectionFactory.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ConnectionFactory.java new file mode 100755 index 0000000000000..21e1469759ab8 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ConnectionFactory.java @@ -0,0 +1,30 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import java.sql.Connection; +import java.sql.SQLException; + +@FunctionalInterface +public interface ConnectionFactory + extends AutoCloseable +{ + Connection openConnection(ClickHouseIdentity identity) + throws SQLException; + + @Override + default void close() + throws SQLException + {} +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/DoubleReadFunction.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/DoubleReadFunction.java new file mode 100755 index 0000000000000..bbd51e5928366 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/DoubleReadFunction.java @@ -0,0 +1,31 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@FunctionalInterface +public interface DoubleReadFunction + extends ReadFunction +{ + @Override + default Class getJavaType() + { + return double.class; + } + + double readDouble(ResultSet resultSet, int columnIndex) + throws SQLException; +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/DriverConnectionFactory.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/DriverConnectionFactory.java new file mode 100755 index 0000000000000..0f01fbb38942e --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/DriverConnectionFactory.java @@ -0,0 +1,95 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import ru.yandex.clickhouse.ClickHouseDriver; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +public class DriverConnectionFactory + implements ConnectionFactory +{ + private final ClickHouseDriver driver; + private final String connectionUrl; + private final Properties connectionProperties; + private final Optional userCredential; + private final Optional passwordCredential; + + public DriverConnectionFactory(ClickHouseDriver driver, ClickHouseConfig config) + { + this( + driver, + config.getConnectionUrl(), + Optional.ofNullable(config.getUserCredential()), + Optional.ofNullable(config.getPasswordCredential()), + basicConnectionProperties(config)); + } + + public static Properties basicConnectionProperties(ClickHouseConfig config) + { + Properties connectionProperties = new Properties(); + if (config.getConnectionUser() != null) { + connectionProperties.setProperty("user", config.getConnectionUser()); + } + if (config.getConnectionPassword() != null) { + connectionProperties.setProperty("password", config.getConnectionPassword()); + } + return connectionProperties; + } + + public DriverConnectionFactory(ClickHouseDriver driver, String connectionUrl, Optional userCredential, Optional passwordCredential, Properties connectionProperties) + { + this.driver = requireNonNull(driver, "clickHouseDriver is null"); + this.connectionUrl = requireNonNull(connectionUrl, "connectionUrl is null"); + this.connectionProperties = new Properties(); + this.connectionProperties.putAll(requireNonNull(connectionProperties, "connectionProperties is null")); + this.userCredential = requireNonNull(userCredential, "userCredentialName is null"); + this.passwordCredential = requireNonNull(passwordCredential, "passwordCredentialName is null"); + } + + @Override + public Connection openConnection(ClickHouseIdentity identity) + throws SQLException + { + Properties updatedConnectionProperties; + if (userCredential.isPresent() || passwordCredential.isPresent()) { + updatedConnectionProperties = new Properties(); + updatedConnectionProperties.putAll(connectionProperties); + userCredential.ifPresent(credentialName -> setConnectionProperty(updatedConnectionProperties, identity.getExtraCredentials(), credentialName, "user")); + passwordCredential.ifPresent(credentialName -> setConnectionProperty(updatedConnectionProperties, identity.getExtraCredentials(), credentialName, "password")); + } + else { + updatedConnectionProperties = connectionProperties; + } + + Connection connection = driver.connect(connectionUrl, updatedConnectionProperties); + checkState(connection != null, "ClickHouseDriver returned null connection"); + return connection; + } + + private static void setConnectionProperty(Properties connectionProperties, Map extraCredentials, String credentialName, String propertyName) + { + String value = extraCredentials.get(credentialName); + if (value != null) { + connectionProperties.setProperty(propertyName, value); + } + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ForBaseJdbc.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ForBaseJdbc.java new file mode 100755 index 0000000000000..cf8259f07fbe0 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ForBaseJdbc.java @@ -0,0 +1,31 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import javax.inject.Qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({FIELD, PARAMETER, METHOD}) +@Qualifier +public @interface ForBaseJdbc +{ +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/LongReadFunction.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/LongReadFunction.java new file mode 100755 index 0000000000000..38a636ff0e23f --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/LongReadFunction.java @@ -0,0 +1,31 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@FunctionalInterface +public interface LongReadFunction + extends ReadFunction +{ + @Override + default Class getJavaType() + { + return long.class; + } + + long readLong(ResultSet resultSet, int columnIndex) + throws SQLException; +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/QueryBuilder.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/QueryBuilder.java new file mode 100755 index 0000000000000..3cbf2fbc432ac --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/QueryBuilder.java @@ -0,0 +1,316 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.common.predicate.Domain; +import com.facebook.presto.common.predicate.Range; +import com.facebook.presto.common.predicate.TupleDomain; +import com.facebook.presto.common.type.CharType; +import com.facebook.presto.common.type.Type; +import com.facebook.presto.common.type.VarcharType; +import com.facebook.presto.plugin.clickhouse.optimization.ClickHouseExpression; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorSession; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; +import org.joda.time.DateTimeZone; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.common.type.BigintType.BIGINT; +import static com.facebook.presto.common.type.BooleanType.BOOLEAN; +import static com.facebook.presto.common.type.DateTimeEncoding.unpackMillisUtc; +import static com.facebook.presto.common.type.DateType.DATE; +import static com.facebook.presto.common.type.DoubleType.DOUBLE; +import static com.facebook.presto.common.type.IntegerType.INTEGER; +import static com.facebook.presto.common.type.RealType.REAL; +import static com.facebook.presto.common.type.SmallintType.SMALLINT; +import static com.facebook.presto.common.type.TimeType.TIME; +import static com.facebook.presto.common.type.TimeWithTimeZoneType.TIME_WITH_TIME_ZONE; +import static com.facebook.presto.common.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.common.type.TimestampWithTimeZoneType.TIMESTAMP_WITH_TIME_ZONE; +import static com.facebook.presto.common.type.TinyintType.TINYINT; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.Iterables.getOnlyElement; +import static java.lang.Float.intBitsToFloat; +import static java.util.Collections.nCopies; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.stream.Collectors.joining; +import static org.joda.time.DateTimeZone.UTC; + +public class QueryBuilder +{ + private static final String ALWAYS_TRUE = "1=1"; + private static final String ALWAYS_FALSE = "1=0"; + + private final String quote; + + private static class TypeAndValue + { + private final Type type; + private final Object value; + + public TypeAndValue(Type type, Object value) + { + this.type = requireNonNull(type, "type is null"); + this.value = requireNonNull(value, "value is null"); + } + + public Type getType() + { + return type; + } + + public Object getValue() + { + return value; + } + } + + public QueryBuilder(String quote) + { + this.quote = requireNonNull(quote, "quote is null"); + } + + public PreparedStatement buildSql( + ClickHouseClient client, + ConnectorSession session, + Connection connection, + String catalog, + String schema, + String table, + List columns, + TupleDomain tupleDomain, + Optional additionalPredicate, + Optional simpleExpression) + throws SQLException + { + StringBuilder sql = new StringBuilder(); + + String columnNames = columns.stream() + .map(ClickHouseColumnHandle::getColumnName) + .map(this::quote) + .collect(joining(", ")); + + sql.append("SELECT "); + sql.append(columnNames); + if (columns.isEmpty()) { + sql.append("null"); + } + + sql.append(" FROM "); + if (!isNullOrEmpty(catalog)) { + sql.append(quote(catalog)).append('.'); + } + if (!isNullOrEmpty(schema)) { + sql.append(quote(schema)).append('.'); + } + sql.append(quote(table)); + + List accumulator = new ArrayList<>(); + + List clauses = toConjuncts(columns, tupleDomain, accumulator); + if (additionalPredicate.isPresent()) { + clauses = ImmutableList.builder() + .addAll(clauses) + .add(additionalPredicate.get().getExpression()) + .build(); + accumulator.addAll(additionalPredicate.get().getBoundConstantValues().stream() + .map(constantExpression -> new TypeAndValue(constantExpression.getType(), constantExpression.getValue())) + .collect(ImmutableList.toImmutableList())); + } + + if (!clauses.isEmpty()) { + sql.append(" WHERE ") + .append(Joiner.on(" AND ").join(clauses)); + } + + if (simpleExpression.isPresent()) { + sql.append(simpleExpression.get()); + } + + sql.append(String.format("/* %s : %s */", session.getUser(), session.getQueryId())); + PreparedStatement statement = client.getPreparedStatement(connection, sql.toString()); + + for (int i = 0; i < accumulator.size(); i++) { + TypeAndValue typeAndValue = accumulator.get(i); + if (typeAndValue.getType().equals(BIGINT)) { + statement.setLong(i + 1, (long) typeAndValue.getValue()); + } + else if (typeAndValue.getType().equals(INTEGER)) { + statement.setInt(i + 1, ((Number) typeAndValue.getValue()).intValue()); + } + else if (typeAndValue.getType().equals(SMALLINT)) { + statement.setShort(i + 1, ((Number) typeAndValue.getValue()).shortValue()); + } + else if (typeAndValue.getType().equals(TINYINT)) { + statement.setByte(i + 1, ((Number) typeAndValue.getValue()).byteValue()); + } + else if (typeAndValue.getType().equals(DOUBLE)) { + statement.setDouble(i + 1, (double) typeAndValue.getValue()); + } + else if (typeAndValue.getType().equals(REAL)) { + statement.setFloat(i + 1, intBitsToFloat(((Number) typeAndValue.getValue()).intValue())); + } + else if (typeAndValue.getType().equals(BOOLEAN)) { + statement.setBoolean(i + 1, (boolean) typeAndValue.getValue()); + } + else if (typeAndValue.getType().equals(DATE)) { + long millis = DAYS.toMillis((long) typeAndValue.getValue()); + statement.setDate(i + 1, new Date(UTC.getMillisKeepLocal(DateTimeZone.getDefault(), millis))); + } + else if (typeAndValue.getType().equals(TIME)) { + statement.setTime(i + 1, new Time((long) typeAndValue.getValue())); + } + else if (typeAndValue.getType().equals(TIME_WITH_TIME_ZONE)) { + statement.setTime(i + 1, new Time(unpackMillisUtc((long) typeAndValue.getValue()))); + } + else if (typeAndValue.getType().equals(TIMESTAMP)) { + statement.setTimestamp(i + 1, new Timestamp((long) typeAndValue.getValue())); + } + else if (typeAndValue.getType().equals(TIMESTAMP_WITH_TIME_ZONE)) { + statement.setTimestamp(i + 1, new Timestamp(unpackMillisUtc((long) typeAndValue.getValue()))); + } + else if (typeAndValue.getType() instanceof VarcharType) { + statement.setString(i + 1, ((Slice) typeAndValue.getValue()).toStringUtf8()); + } + else if (typeAndValue.getType() instanceof CharType) { + statement.setString(i + 1, ((Slice) typeAndValue.getValue()).toStringUtf8()); + } + else { + throw new UnsupportedOperationException("Can't handle type: " + typeAndValue.getType()); + } + } + + return statement; + } + + private static boolean isAcceptedType(Type type) + { + Type validType = requireNonNull(type, "type is null"); + return validType.equals(BIGINT) || + validType.equals(TINYINT) || + validType.equals(SMALLINT) || + validType.equals(INTEGER) || + validType.equals(DOUBLE) || + validType.equals(REAL) || + validType.equals(BOOLEAN) || + validType.equals(DATE) || + validType.equals(TIME) || + validType.equals(TIME_WITH_TIME_ZONE) || + validType.equals(TIMESTAMP) || + validType.equals(TIMESTAMP_WITH_TIME_ZONE) || + validType instanceof VarcharType || + validType instanceof CharType; + } + + private List toConjuncts(List columns, TupleDomain tupleDomain, List accumulator) + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (ClickHouseColumnHandle column : columns) { + Type type = column.getColumnType(); + if (isAcceptedType(type)) { + Domain domain = tupleDomain.getDomains().get().get(column); + if (domain != null) { + builder.add(toPredicate(column.getColumnName(), domain, type, accumulator)); + } + } + } + return builder.build(); + } + + private String toPredicate(String columnName, Domain domain, Type type, List accumulator) + { + checkArgument(domain.getType().isOrderable(), "Domain type must be orderable"); + + if (domain.getValues().isNone()) { + return domain.isNullAllowed() ? quote(columnName) + " IS NULL" : ALWAYS_FALSE; + } + + if (domain.getValues().isAll()) { + return domain.isNullAllowed() ? ALWAYS_TRUE : quote(columnName) + " IS NOT NULL"; + } + + List disjuncts = new ArrayList<>(); + List singleValues = new ArrayList<>(); + for (Range range : domain.getValues().getRanges().getOrderedRanges()) { + checkState(!range.isAll()); // Already checked + if (range.isSingleValue()) { + singleValues.add(range.getSingleValue()); + } + else { + List rangeConjuncts = new ArrayList<>(); + if (!range.isLowUnbounded()) { + rangeConjuncts.add(toPredicate(columnName, range.isLowInclusive() ? ">=" : ">", range.getLowBoundedValue(), type, accumulator)); + } + if (!range.isHighUnbounded()) { + rangeConjuncts.add(toPredicate(columnName, range.isHighInclusive() ? "<=" : "<", range.getHighBoundedValue(), type, accumulator)); + } + // If rangeConjuncts is null, then the range was ALL, which should already have been checked for + checkState(!rangeConjuncts.isEmpty()); + disjuncts.add("(" + Joiner.on(" AND ").join(rangeConjuncts) + ")"); + } + } + + // Add back all of the possible single values either as an equality or an IN predicate + if (singleValues.size() == 1) { + disjuncts.add(toPredicate(columnName, "=", getOnlyElement(singleValues), type, accumulator)); + } + else if (singleValues.size() > 1) { + for (Object value : singleValues) { + bindValue(value, type, accumulator); + } + String values = Joiner.on(",").join(nCopies(singleValues.size(), "?")); + disjuncts.add(quote(columnName) + " IN (" + values + ")"); + } + + // Add nullability disjuncts + checkState(!disjuncts.isEmpty()); + if (domain.isNullAllowed()) { + disjuncts.add(quote(columnName) + " IS NULL"); + } + + return "(" + Joiner.on(" OR ").join(disjuncts) + ")"; + } + + private String toPredicate(String columnName, String operator, Object value, Type type, List accumulator) + { + bindValue(value, type, accumulator); + return quote(columnName) + " " + operator + " ?"; + } + + private String quote(String name) + { + name = name.replace(quote, quote + quote); + return quote + name + quote; + } + + private static void bindValue(Object value, Type type, List accumulator) + { + checkArgument(isAcceptedType(type), "Can't handle type: %s", type); + accumulator.add(new TypeAndValue(type, value)); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ReadFunction.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ReadFunction.java new file mode 100755 index 0000000000000..746c282084f58 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ReadFunction.java @@ -0,0 +1,19 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +public interface ReadFunction +{ + Class getJavaType(); +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ReadMapping.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ReadMapping.java new file mode 100755 index 0000000000000..9f2d7e2f55b22 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/ReadMapping.java @@ -0,0 +1,76 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.common.type.Type; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +public final class ReadMapping +{ + public static ReadMapping booleanReadMapping(Type prestoType, BooleanReadFunction readFunction) + { + return new ReadMapping(prestoType, readFunction); + } + + public static ReadMapping longReadMapping(Type prestoType, LongReadFunction readFunction) + { + return new ReadMapping(prestoType, readFunction); + } + + public static ReadMapping doubleReadMapping(Type prestoType, DoubleReadFunction readFunction) + { + return new ReadMapping(prestoType, readFunction); + } + + public static ReadMapping sliceReadMapping(Type prestoType, SliceReadFunction readFunction) + { + return new ReadMapping(prestoType, readFunction); + } + + private final Type type; + private final ReadFunction readFunction; + + private ReadMapping(Type type, ReadFunction readFunction) + { + this.type = requireNonNull(type, "type is null"); + this.readFunction = requireNonNull(readFunction, "readFunction is null"); + checkArgument( + type.getJavaType() == readFunction.getJavaType(), + "Presto type %s is not compatible with read function %s returning %s", + type, + readFunction, + readFunction.getJavaType()); + } + + public Type getType() + { + return type; + } + + public ReadFunction getReadFunction() + { + return readFunction; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("type", type) + .toString(); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/RemoteTableName.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/RemoteTableName.java new file mode 100755 index 0000000000000..85bdf28018dd6 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/RemoteTableName.java @@ -0,0 +1,83 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Joiner; + +import java.util.Objects; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public final class RemoteTableName +{ + private final Optional catalogName; + private final Optional schemaName; + private final String tableName; + + @JsonCreator + public RemoteTableName(Optional catalogName, Optional schemaName, String tableName) + { + this.catalogName = requireNonNull(catalogName, "catalogName is null"); + this.schemaName = requireNonNull(schemaName, "schemaName is null"); + this.tableName = requireNonNull(tableName, "tableName is null"); + } + + @JsonProperty + public Optional getCatalogName() + { + return catalogName; + } + + @JsonProperty + public Optional getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RemoteTableName that = (RemoteTableName) o; + return catalogName.equals(that.catalogName) && + schemaName.equals(that.schemaName) && + tableName.equals(that.tableName); + } + + @Override + public int hashCode() + { + return Objects.hash(catalogName, schemaName, tableName); + } + + @Override + public String toString() + { + return Joiner.on(".").skipNulls().join(catalogName.orElse(null), schemaName.orElse(null), tableName); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/RemoteTableNameCacheKey.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/RemoteTableNameCacheKey.java new file mode 100755 index 0000000000000..60018254b5e67 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/RemoteTableNameCacheKey.java @@ -0,0 +1,70 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.Objects.requireNonNull; + +final class RemoteTableNameCacheKey +{ + private final ClickHouseIdentity identity; + private final String schema; + + RemoteTableNameCacheKey(ClickHouseIdentity identity, String schema) + { + this.identity = requireNonNull(identity, "identity is null"); + this.schema = requireNonNull(schema, "schema is null"); + } + + ClickHouseIdentity getIdentity() + { + return identity; + } + + String getSchema() + { + return schema; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RemoteTableNameCacheKey that = (RemoteTableNameCacheKey) o; + return Objects.equals(identity, that.identity) && + Objects.equals(schema, that.schema); + } + + @Override + public int hashCode() + { + return Objects.hash(identity, schema); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("identity", identity) + .add("schema", schema) + .toString(); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/SliceReadFunction.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/SliceReadFunction.java new file mode 100755 index 0000000000000..949024ee6d342 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/SliceReadFunction.java @@ -0,0 +1,33 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import io.airlift.slice.Slice; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@FunctionalInterface +public interface SliceReadFunction + extends ReadFunction +{ + @Override + default Class getJavaType() + { + return Slice.class; + } + + Slice readSlice(ResultSet resultSet, int columnIndex) + throws SQLException; +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/StandardReadMappings.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/StandardReadMappings.java new file mode 100755 index 0000000000000..3b41ab251d157 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/StandardReadMappings.java @@ -0,0 +1,235 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.common.type.CharType; +import com.facebook.presto.common.type.DecimalType; +import com.facebook.presto.common.type.Decimals; +import com.facebook.presto.common.type.VarcharType; +import com.facebook.presto.spi.PrestoException; +import com.google.common.base.CharMatcher; +import org.joda.time.chrono.ISOChronology; + +import java.sql.ResultSet; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.Optional; + +import static com.facebook.presto.common.type.BigintType.BIGINT; +import static com.facebook.presto.common.type.BooleanType.BOOLEAN; +import static com.facebook.presto.common.type.CharType.createCharType; +import static com.facebook.presto.common.type.DateType.DATE; +import static com.facebook.presto.common.type.DecimalType.createDecimalType; +import static com.facebook.presto.common.type.Decimals.encodeScaledValue; +import static com.facebook.presto.common.type.Decimals.encodeShortScaledValue; +import static com.facebook.presto.common.type.DoubleType.DOUBLE; +import static com.facebook.presto.common.type.IntegerType.INTEGER; +import static com.facebook.presto.common.type.RealType.REAL; +import static com.facebook.presto.common.type.SmallintType.SMALLINT; +import static com.facebook.presto.common.type.TimeType.TIME; +import static com.facebook.presto.common.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.common.type.TinyintType.TINYINT; +import static com.facebook.presto.common.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.common.type.VarcharType.createUnboundedVarcharType; +import static com.facebook.presto.common.type.VarcharType.createVarcharType; +import static com.facebook.presto.plugin.clickhouse.ClickHouseErrorCode.JDBC_ERROR; +import static com.facebook.presto.plugin.clickhouse.ReadMapping.longReadMapping; +import static com.facebook.presto.plugin.clickhouse.ReadMapping.sliceReadMapping; +import static io.airlift.slice.Slices.utf8Slice; +import static io.airlift.slice.Slices.wrappedBuffer; +import static java.lang.Float.floatToRawIntBits; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.joda.time.DateTimeZone.UTC; + +public final class StandardReadMappings +{ + private StandardReadMappings() {} + + private static final ISOChronology UTC_CHRONOLOGY = ISOChronology.getInstanceUTC(); + + public static ReadMapping booleanReadMapping() + { + return ReadMapping.booleanReadMapping(BOOLEAN, ResultSet::getBoolean); + } + + public static ReadMapping tinyintReadMapping() + { + return longReadMapping(TINYINT, ResultSet::getByte); + } + + public static ReadMapping smallintReadMapping() + { + return longReadMapping(SMALLINT, ResultSet::getShort); + } + + public static ReadMapping integerReadMapping() + { + return longReadMapping(INTEGER, ResultSet::getInt); + } + + public static ReadMapping bigintReadMapping() + { + return longReadMapping(BIGINT, ResultSet::getLong); + } + + public static ReadMapping realReadMapping() + { + return longReadMapping(REAL, (resultSet, columnIndex) -> floatToRawIntBits(resultSet.getFloat(columnIndex))); + } + + public static ReadMapping doubleReadMapping() + { + return ReadMapping.doubleReadMapping(DOUBLE, ResultSet::getDouble); + } + + public static ReadMapping decimalReadMapping(DecimalType decimalType) + { + // JDBC driver can return BigDecimal with lower scale than column's scale when there are trailing zeroes + int scale = decimalType.getScale(); + if (decimalType.isShort()) { + return longReadMapping(decimalType, (resultSet, columnIndex) -> encodeShortScaledValue(resultSet.getBigDecimal(columnIndex), scale)); + } + return sliceReadMapping(decimalType, (resultSet, columnIndex) -> encodeScaledValue(resultSet.getBigDecimal(columnIndex), scale)); + } + + public static ReadMapping charReadMapping(CharType charType) + { + requireNonNull(charType, "charType is null"); + return sliceReadMapping(charType, (resultSet, columnIndex) -> utf8Slice(CharMatcher.is(' ').trimTrailingFrom(resultSet.getString(columnIndex)))); + } + + public static ReadMapping varcharReadMapping(VarcharType varcharType) + { + return sliceReadMapping(varcharType, (resultSet, columnIndex) -> utf8Slice(resultSet.getString(columnIndex))); + } + + public static ReadMapping varbinaryReadMapping() + { + return sliceReadMapping(VARBINARY, (resultSet, columnIndex) -> wrappedBuffer(resultSet.getBytes(columnIndex))); + } + + public static ReadMapping dateReadMapping() + { + return longReadMapping(DATE, (resultSet, columnIndex) -> { + long localMillis = resultSet.getDate(columnIndex).getTime(); + long utcMillis = ISOChronology.getInstance().getZone().getMillisKeepLocal(UTC, localMillis); + return MILLISECONDS.toDays(utcMillis); + }); + } + + public static ReadMapping timeReadMapping() + { + return longReadMapping(TIME, (resultSet, columnIndex) -> { + Time time = resultSet.getTime(columnIndex); + return UTC_CHRONOLOGY.millisOfDay().get(time.getTime()); + }); + } + + public static ReadMapping timestampReadMapping() + { + return longReadMapping(TIMESTAMP, (resultSet, columnIndex) -> { + Timestamp timestamp = resultSet.getTimestamp(columnIndex); + return timestamp.getTime(); + }); + } + + public static Optional jdbcTypeToPrestoType(ClickHouseTypeHandle type, boolean mapStringAsVarchar) + { + String jdbcTypeName = type.getJdbcTypeName() + .orElseThrow(() -> new PrestoException(JDBC_ERROR, "Type name is missing: " + type)); + int columnSize = type.getColumnSize(); + + switch (jdbcTypeName.replaceAll("\\(.*\\)$", "")) { + case "IPv4": + case "IPv6": + case "Enum8": + case "Enum16": + return Optional.of(varcharReadMapping(createUnboundedVarcharType())); + + case "FixedString": // FixedString(n) + case "String": + if (mapStringAsVarchar) { + return Optional.of(varcharReadMapping(createUnboundedVarcharType())); + } + return Optional.of(varbinaryReadMapping()); + } + + switch (type.getJdbcType()) { + case Types.BIT: + case Types.BOOLEAN: + return Optional.of(booleanReadMapping()); + + case Types.TINYINT: + return Optional.of(tinyintReadMapping()); + + case Types.SMALLINT: + return Optional.of(smallintReadMapping()); + + case Types.INTEGER: + return Optional.of(integerReadMapping()); + + case Types.BIGINT: + return Optional.of(bigintReadMapping()); + + case Types.REAL: + return Optional.of(realReadMapping()); + + case Types.FLOAT: + case Types.DOUBLE: + return Optional.of(doubleReadMapping()); + + case Types.NUMERIC: + case Types.DECIMAL: + int decimalDigits = type.getDecimalDigits(); + int precision = columnSize + max(-decimalDigits, 0); + if (precision > Decimals.MAX_PRECISION) { + return Optional.empty(); + } + return Optional.of(decimalReadMapping(createDecimalType(precision, max(decimalDigits, 0)))); + + case Types.CHAR: + case Types.NCHAR: + int charLength = min(columnSize, CharType.MAX_LENGTH); + return Optional.of(charReadMapping(createCharType(charLength))); + + case Types.VARCHAR: + case Types.NVARCHAR: + case Types.LONGVARCHAR: + case Types.LONGNVARCHAR: + if (columnSize > VarcharType.MAX_LENGTH) { + return Optional.of(varcharReadMapping(createUnboundedVarcharType())); + } + return Optional.of(varcharReadMapping(createVarcharType(columnSize))); + + case Types.BINARY: + case Types.VARBINARY: + case Types.LONGVARBINARY: + return Optional.of(varbinaryReadMapping()); + + case Types.DATE: + return Optional.of(dateReadMapping()); + + case Types.TIME: + return Optional.of(timeReadMapping()); + + case Types.TIMESTAMP: + return Optional.of(timestampReadMapping()); + } + return Optional.empty(); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/TablePropertiesProvider.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/TablePropertiesProvider.java new file mode 100755 index 0000000000000..348dd7e821b25 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/TablePropertiesProvider.java @@ -0,0 +1,23 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.spi.session.PropertyMetadata; + +import java.util.List; + +public interface TablePropertiesProvider +{ + List> getTableProperties(); +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/ClickHouseComputePushdown.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/ClickHouseComputePushdown.java new file mode 100755 index 0000000000000..22efaa99cfd04 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/ClickHouseComputePushdown.java @@ -0,0 +1,240 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse.optimization; + +import com.facebook.presto.expressions.LogicalRowExpressions; +import com.facebook.presto.expressions.translator.TranslatedExpression; +import com.facebook.presto.plugin.clickhouse.ClickHouseTableHandle; +import com.facebook.presto.plugin.clickhouse.ClickHouseTableLayoutHandle; +import com.facebook.presto.spi.ConnectorPlanOptimizer; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.TableHandle; +import com.facebook.presto.spi.VariableAllocator; +import com.facebook.presto.spi.function.FunctionMetadataManager; +import com.facebook.presto.spi.function.StandardFunctionResolution; +import com.facebook.presto.spi.plan.FilterNode; +import com.facebook.presto.spi.plan.LimitNode; +import com.facebook.presto.spi.plan.PlanNode; +import com.facebook.presto.spi.plan.PlanNodeIdAllocator; +import com.facebook.presto.spi.plan.PlanVisitor; +import com.facebook.presto.spi.plan.TableScanNode; +import com.facebook.presto.spi.relation.DeterminismEvaluator; +import com.facebook.presto.spi.relation.ExpressionOptimizer; +import com.facebook.presto.spi.relation.RowExpression; +import com.google.common.collect.ImmutableList; + +import java.util.Optional; +import java.util.Set; + +import static com.facebook.presto.expressions.translator.FunctionTranslator.buildFunctionTranslator; +import static com.facebook.presto.expressions.translator.RowExpressionTreeTranslator.translateWith; +import static com.facebook.presto.spi.relation.ExpressionOptimizer.Level.OPTIMIZED; +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.Objects.requireNonNull; + +public class ClickHouseComputePushdown + implements ConnectorPlanOptimizer +{ + private final ExpressionOptimizer expressionOptimizer; + private final ClickHouseFilterToSqlTranslator clickHouseFilterToSqlTranslator; + private final LogicalRowExpressions logicalRowExpressions; + + public ClickHouseComputePushdown( + FunctionMetadataManager functionMetadataManager, + StandardFunctionResolution functionResolution, + DeterminismEvaluator determinismEvaluator, + ExpressionOptimizer expressionOptimizer, + String identifierQuote, + Set> functionTranslators) + { + requireNonNull(functionMetadataManager, "functionMetadataManager is null"); + requireNonNull(identifierQuote, "identifierQuote is null"); + requireNonNull(functionTranslators, "functionTranslators is null"); + requireNonNull(determinismEvaluator, "determinismEvaluator is null"); + requireNonNull(functionResolution, "functionResolution is null"); + + this.expressionOptimizer = requireNonNull(expressionOptimizer, "expressionOptimizer is null"); + this.clickHouseFilterToSqlTranslator = new ClickHouseFilterToSqlTranslator( + functionMetadataManager, + buildFunctionTranslator(functionTranslators), + identifierQuote); + this.logicalRowExpressions = new LogicalRowExpressions( + determinismEvaluator, + functionResolution, + functionMetadataManager); + } + + @Override + public PlanNode optimize( + PlanNode maxSubplan, + ConnectorSession session, + VariableAllocator variableAllocator, + PlanNodeIdAllocator idAllocator) + { + return maxSubplan.accept(new Visitor(session, idAllocator), null); + } + + private class Visitor + extends PlanVisitor + { + private final ConnectorSession session; + private final PlanNodeIdAllocator idAllocator; + + public Visitor(ConnectorSession session, PlanNodeIdAllocator idAllocator) + { + this.session = requireNonNull(session, "session is null"); + this.idAllocator = requireNonNull(idAllocator, "idAllocator is null"); + } + + @Override + public PlanNode visitPlan(PlanNode node, Void context) + { + ImmutableList.Builder children = ImmutableList.builder(); + boolean changed = false; + for (PlanNode child : node.getSources()) { + PlanNode newChild = child.accept(this, null); + if (newChild != child) { + changed = true; + } + children.add(newChild); + } + + if (!changed) { + return node; + } + return node.replaceChildren(children.build()); + } + + @Override + public PlanNode visitLimit(LimitNode node, Void context) + { + TableScanNode oldTableScanNode = null; + if (node.getSource() instanceof TableScanNode) { + oldTableScanNode = (TableScanNode) node.getSource(); + } + else if (node.getSource() instanceof FilterNode) { + FilterNode oldTableFilterNode = (FilterNode) node.getSource(); + oldTableScanNode = (TableScanNode) oldTableFilterNode.getSource(); + } + else { + return node; + } + TableHandle oldTableHandle = oldTableScanNode.getTable(); + ClickHouseTableHandle oldConnectorTable = (ClickHouseTableHandle) oldTableHandle.getConnectorHandle(); + + String simpleExpression = " limit " + node.getCount() + " "; + + ClickHouseTableLayoutHandle oldTableLayoutHandle = (ClickHouseTableLayoutHandle) oldTableHandle.getLayout().get(); + ClickHouseTableLayoutHandle newTableLayoutHandle = new ClickHouseTableLayoutHandle( + oldConnectorTable, + oldTableLayoutHandle.getTupleDomain(), + Optional.empty(), Optional.of(simpleExpression)); + + TableHandle tableHandle = new TableHandle( + oldTableHandle.getConnectorId(), + oldTableHandle.getConnectorHandle(), + oldTableHandle.getTransaction(), + Optional.of(newTableLayoutHandle)); + + TableScanNode newTableScanNode = new TableScanNode( + null, + idAllocator.getNextId(), + tableHandle, + oldTableScanNode.getOutputVariables(), + oldTableScanNode.getAssignments(), + oldTableScanNode.getCurrentConstraint(), + oldTableScanNode.getEnforcedConstraint()); + + return new LimitNode(null, idAllocator.getNextId(), newTableScanNode, node.getCount(), node.getStep()); + } + + @Override + public PlanNode visitFilter(FilterNode node, Void context) + { + if (!(node.getSource() instanceof TableScanNode)) { + return node; + } + + TableScanNode oldTableScanNode = (TableScanNode) node.getSource(); + TableHandle oldTableHandle = oldTableScanNode.getTable(); + ClickHouseTableHandle oldConnectorTable = (ClickHouseTableHandle) oldTableHandle.getConnectorHandle(); + + RowExpression predicate = expressionOptimizer.optimize(node.getPredicate(), OPTIMIZED, session); + predicate = logicalRowExpressions.convertToConjunctiveNormalForm(predicate); + TranslatedExpression clickHouseExpression = translateWith( + predicate, + clickHouseFilterToSqlTranslator, + oldTableScanNode.getAssignments()); + + // TODO if jdbcExpression is not present, walk through translated subtree to find out which parts can be pushed down + if (!oldTableHandle.getLayout().isPresent() || !clickHouseExpression.getTranslated().isPresent()) { + return node; + } + + ClickHouseTableLayoutHandle oldTableLayoutHandle = (ClickHouseTableLayoutHandle) oldTableHandle.getLayout().get(); + ClickHouseTableLayoutHandle newTableLayoutHandle = new ClickHouseTableLayoutHandle( + oldConnectorTable, + oldTableLayoutHandle.getTupleDomain(), + clickHouseExpression.getTranslated(), + Optional.empty()); + + TableHandle tableHandle = new TableHandle( + oldTableHandle.getConnectorId(), + oldTableHandle.getConnectorHandle(), + oldTableHandle.getTransaction(), + Optional.of(newTableLayoutHandle)); + + TableScanNode newTableScanNode = new TableScanNode( + null, + idAllocator.getNextId(), + tableHandle, + oldTableScanNode.getOutputVariables(), + oldTableScanNode.getAssignments(), + oldTableScanNode.getCurrentConstraint(), + oldTableScanNode.getEnforcedConstraint()); + + return new FilterNode(null, idAllocator.getNextId(), newTableScanNode, node.getPredicate()); + } + } + private static class LimitContext + { + private final long count; + private final LimitNode.Step step; + + public LimitContext(long count, LimitNode.Step step) + { + this.count = count; + this.step = step; + } + + public long getCount() + { + return count; + } + + public LimitNode.Step getStep() + { + return step; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("count", count) + .add("step", step) + .toString(); + } + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/ClickHouseExpression.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/ClickHouseExpression.java new file mode 100755 index 0000000000000..f920af82d63c6 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/ClickHouseExpression.java @@ -0,0 +1,83 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse.optimization; + +import com.facebook.presto.spi.relation.ConstantExpression; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +public class ClickHouseExpression +{ + private final String expression; + private final List boundConstantValues; + + public ClickHouseExpression(String expression) + { + this(expression, ImmutableList.of()); + } + + @JsonCreator + public ClickHouseExpression( + @JsonProperty("translatedString") String expression, + @JsonProperty("boundConstantValues") List constantBindValues) + { + this.expression = requireNonNull(expression, "expression is null"); + this.boundConstantValues = requireNonNull(constantBindValues, "boundConstantValues is null"); + } + + @JsonProperty + public String getExpression() + { + return expression; + } + + /** + * Constant expressions are not added to the expression String. Instead they appear as "?" in the query. + * This is because we would potentially lose precision on double values. Hence when we make a PreparedStatement + * out of the SQL string replacing every "?" by it's corresponding actual bindValue. + * + * @return List of constants to replace in the SQL string. + */ + @JsonProperty + public List getBoundConstantValues() + { + return boundConstantValues; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ClickHouseExpression that = (ClickHouseExpression) o; + return expression.equals(that.expression) && + boundConstantValues.equals(that.boundConstantValues); + } + + @Override + public int hashCode() + { + return Objects.hash(expression, boundConstantValues); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/ClickHouseFilterToSqlTranslator.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/ClickHouseFilterToSqlTranslator.java new file mode 100755 index 0000000000000..c1f65e8bff945 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/ClickHouseFilterToSqlTranslator.java @@ -0,0 +1,181 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse.optimization; + +import com.facebook.presto.common.type.BigintType; +import com.facebook.presto.common.type.BooleanType; +import com.facebook.presto.common.type.CharType; +import com.facebook.presto.common.type.DateType; +import com.facebook.presto.common.type.DoubleType; +import com.facebook.presto.common.type.IntegerType; +import com.facebook.presto.common.type.RealType; +import com.facebook.presto.common.type.SmallintType; +import com.facebook.presto.common.type.TimeType; +import com.facebook.presto.common.type.TimeWithTimeZoneType; +import com.facebook.presto.common.type.TimestampType; +import com.facebook.presto.common.type.TimestampWithTimeZoneType; +import com.facebook.presto.common.type.TinyintType; +import com.facebook.presto.common.type.Type; +import com.facebook.presto.common.type.VarcharType; +import com.facebook.presto.expressions.translator.FunctionTranslator; +import com.facebook.presto.expressions.translator.RowExpressionTranslator; +import com.facebook.presto.expressions.translator.RowExpressionTreeTranslator; +import com.facebook.presto.expressions.translator.TranslatedExpression; +import com.facebook.presto.plugin.clickhouse.ClickHouseColumnHandle; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.function.FunctionMetadata; +import com.facebook.presto.spi.function.FunctionMetadataManager; +import com.facebook.presto.spi.relation.CallExpression; +import com.facebook.presto.spi.relation.ConstantExpression; +import com.facebook.presto.spi.relation.LambdaDefinitionExpression; +import com.facebook.presto.spi.relation.SpecialFormExpression; +import com.facebook.presto.spi.relation.VariableReferenceExpression; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.facebook.presto.expressions.translator.TranslatedExpression.untranslated; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +public class ClickHouseFilterToSqlTranslator + extends RowExpressionTranslator> +{ + private final FunctionMetadataManager functionMetadataManager; + private final FunctionTranslator functionTranslator; + private final String quote; + + public ClickHouseFilterToSqlTranslator(FunctionMetadataManager functionMetadataManager, FunctionTranslator functionTranslator, String quote) + { + this.functionMetadataManager = requireNonNull(functionMetadataManager, "functionMetadataManager is null"); + this.functionTranslator = requireNonNull(functionTranslator, "functionTranslator is null"); + this.quote = requireNonNull(quote, "quote is null"); + } + + @Override + public TranslatedExpression translateConstant(ConstantExpression literal, Map context, RowExpressionTreeTranslator> rowExpressionTreeTranslator) + { + if (isSupportedType(literal.getType())) { + return new TranslatedExpression<>( + Optional.of(new ClickHouseExpression("?", ImmutableList.of(literal))), + literal, + ImmutableList.of()); + } + return untranslated(literal); + } + + @Override + public TranslatedExpression translateVariable(VariableReferenceExpression variable, Map context, RowExpressionTreeTranslator> rowExpressionTreeTranslator) + { + ClickHouseColumnHandle columnHandle = (ClickHouseColumnHandle) context.get(variable); + requireNonNull(columnHandle, format("Unrecognized variable %s", variable)); + return new TranslatedExpression<>( + Optional.of(new ClickHouseExpression(quote + columnHandle.getColumnName().replace(quote, quote + quote) + quote)), + variable, + ImmutableList.of()); + } + + @Override + public TranslatedExpression translateLambda(LambdaDefinitionExpression lambda, Map context, RowExpressionTreeTranslator> rowExpressionTreeTranslator) + { + return untranslated(lambda); + } + + @Override + public TranslatedExpression translateCall(CallExpression call, Map context, RowExpressionTreeTranslator> rowExpressionTreeTranslator) + { + List> translatedExpressions = call.getArguments().stream() + .map(expression -> rowExpressionTreeTranslator.rewrite(expression, context)) + .collect(toImmutableList()); + + FunctionMetadata functionMetadata = functionMetadataManager.getFunctionMetadata(call.getFunctionHandle()); + + try { + return functionTranslator.translate(functionMetadata, call, translatedExpressions); + } + catch (Throwable t) { + // no-op + } + return untranslated(call, translatedExpressions); + } + + @Override + public TranslatedExpression translateSpecialForm(SpecialFormExpression specialForm, Map context, RowExpressionTreeTranslator> rowExpressionTreeTranslator) + { + List> translatedExpressions = specialForm.getArguments().stream() + .map(expression -> rowExpressionTreeTranslator.rewrite(expression, context)) + .collect(toImmutableList()); + + List clickHouseExpressions = translatedExpressions.stream() + .map(TranslatedExpression::getTranslated) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toImmutableList()); + + if (clickHouseExpressions.size() < translatedExpressions.size()) { + return untranslated(specialForm, translatedExpressions); + } + + List sqlBodies = clickHouseExpressions.stream() + .map(ClickHouseExpression::getExpression) + .map(sql -> '(' + sql + ')') + .collect(toImmutableList()); + List variableBindings = clickHouseExpressions.stream() + .map(ClickHouseExpression::getBoundConstantValues) + .flatMap(List::stream) + .collect(toImmutableList()); + + switch (specialForm.getForm()) { + case AND: + return new TranslatedExpression<>( + Optional.of(new ClickHouseExpression(format("(%s)", Joiner.on(" AND ").join(sqlBodies)), variableBindings)), + specialForm, + translatedExpressions); + case OR: + return new TranslatedExpression<>( + Optional.of(new ClickHouseExpression(format("(%s)", Joiner.on(" OR ").join(sqlBodies)), variableBindings)), + specialForm, + translatedExpressions); + case IN: + return new TranslatedExpression<>( + Optional.of(new ClickHouseExpression(format("(%s IN (%s))", sqlBodies.get(0), Joiner.on(" , ").join(sqlBodies.subList(1, sqlBodies.size()))), variableBindings)), + specialForm, + translatedExpressions); + } + return untranslated(specialForm, translatedExpressions); + } + + private static boolean isSupportedType(Type type) + { + Type validType = requireNonNull(type, "type is null"); + return validType.equals(BigintType.BIGINT) || + validType.equals(TinyintType.TINYINT) || + validType.equals(SmallintType.SMALLINT) || + validType.equals(IntegerType.INTEGER) || + validType.equals(DoubleType.DOUBLE) || + validType.equals(RealType.REAL) || + validType.equals(BooleanType.BOOLEAN) || + validType.equals(DateType.DATE) || + validType.equals(TimeType.TIME) || + validType.equals(TimeWithTimeZoneType.TIME_WITH_TIME_ZONE) || + validType.equals(TimestampType.TIMESTAMP) || + validType.equals(TimestampWithTimeZoneType.TIMESTAMP_WITH_TIME_ZONE) || + validType instanceof VarcharType || + validType instanceof CharType; + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/ClickHousePlanOptimizerProvider.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/ClickHousePlanOptimizerProvider.java new file mode 100755 index 0000000000000..708d70c74c4ba --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/ClickHousePlanOptimizerProvider.java @@ -0,0 +1,77 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse.optimization; + +import com.facebook.presto.plugin.clickhouse.ClickHouseClient; +import com.facebook.presto.plugin.clickhouse.optimization.function.OperatorTranslators; +import com.facebook.presto.spi.ConnectorPlanOptimizer; +import com.facebook.presto.spi.connector.ConnectorPlanOptimizerProvider; +import com.facebook.presto.spi.function.FunctionMetadataManager; +import com.facebook.presto.spi.function.StandardFunctionResolution; +import com.facebook.presto.spi.relation.DeterminismEvaluator; +import com.facebook.presto.spi.relation.ExpressionOptimizer; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Inject; + +import java.util.Set; + +import static java.util.Objects.requireNonNull; + +public class ClickHousePlanOptimizerProvider + implements ConnectorPlanOptimizerProvider +{ + private final FunctionMetadataManager functionManager; + private final StandardFunctionResolution functionResolution; + private final DeterminismEvaluator determinismEvaluator; + private final ExpressionOptimizer expressionOptimizer; + private final String identifierQuote; + + @Inject + public ClickHousePlanOptimizerProvider( + ClickHouseClient clickHouseClient, + FunctionMetadataManager functionManager, + StandardFunctionResolution functionResolution, + DeterminismEvaluator determinismEvaluator, + ExpressionOptimizer expressionOptimizer) + { + this.functionManager = requireNonNull(functionManager, "functionManager is null"); + this.functionResolution = requireNonNull(functionResolution, "functionResolution is null"); + this.determinismEvaluator = requireNonNull(determinismEvaluator, "determinismEvaluator is null"); + this.expressionOptimizer = requireNonNull(expressionOptimizer, "expressionOptimizer is null"); + this.identifierQuote = clickHouseClient.getIdentifierQuote(); + } + + @Override + public Set getLogicalPlanOptimizers() + { + return ImmutableSet.of(); + } + + @Override + public Set getPhysicalPlanOptimizers() + { + return ImmutableSet.of(new ClickHouseComputePushdown( + functionManager, + functionResolution, + determinismEvaluator, + expressionOptimizer, + identifierQuote, + getFunctionTranslators())); + } + + private Set> getFunctionTranslators() + { + return ImmutableSet.of(OperatorTranslators.class); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/function/ClickHouseTranslationUtil.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/function/ClickHouseTranslationUtil.java new file mode 100755 index 0000000000000..bb061fccac8ee --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/function/ClickHouseTranslationUtil.java @@ -0,0 +1,41 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse.optimization.function; + +import com.facebook.presto.plugin.clickhouse.optimization.ClickHouseExpression; +import com.facebook.presto.spi.relation.ConstantExpression; + +import java.util.Arrays; +import java.util.List; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +public class ClickHouseTranslationUtil +{ + private ClickHouseTranslationUtil() + { + } + + public static String infixOperation(String operator, ClickHouseExpression left, ClickHouseExpression right) + { + return String.format("(%s %s %s)", left.getExpression(), operator, right.getExpression()); + } + + public static List forwardBindVariables(ClickHouseExpression... clickHouseExpressions) + { + return Arrays.stream(clickHouseExpressions).map(ClickHouseExpression::getBoundConstantValues) + .flatMap(List::stream) + .collect(toImmutableList()); + } +} diff --git a/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/function/OperatorTranslators.java b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/function/OperatorTranslators.java new file mode 100755 index 0000000000000..e357f1843bee0 --- /dev/null +++ b/presto-clickhouse/src/main/java/com/facebook/presto/plugin/clickhouse/optimization/function/OperatorTranslators.java @@ -0,0 +1,69 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse.optimization.function; + +import com.facebook.presto.common.type.StandardTypes; +import com.facebook.presto.plugin.clickhouse.optimization.ClickHouseExpression; +import com.facebook.presto.spi.function.ScalarFunction; +import com.facebook.presto.spi.function.ScalarOperator; +import com.facebook.presto.spi.function.SqlType; + +import static com.facebook.presto.common.function.OperatorType.ADD; +import static com.facebook.presto.common.function.OperatorType.EQUAL; +import static com.facebook.presto.common.function.OperatorType.NOT_EQUAL; +import static com.facebook.presto.common.function.OperatorType.SUBTRACT; +import static com.facebook.presto.plugin.clickhouse.optimization.function.ClickHouseTranslationUtil.forwardBindVariables; +import static com.facebook.presto.plugin.clickhouse.optimization.function.ClickHouseTranslationUtil.infixOperation; + +public class OperatorTranslators +{ + private OperatorTranslators() + { + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.BIGINT) + public static ClickHouseExpression add(@SqlType(StandardTypes.BIGINT) ClickHouseExpression left, @SqlType(StandardTypes.BIGINT) ClickHouseExpression right) + { + return new ClickHouseExpression(infixOperation("+", left, right), forwardBindVariables(left, right)); + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.BIGINT) + public static ClickHouseExpression subtract(@SqlType(StandardTypes.BIGINT) ClickHouseExpression left, @SqlType(StandardTypes.BIGINT) ClickHouseExpression right) + { + return new ClickHouseExpression(infixOperation("-", left, right), forwardBindVariables(left, right)); + } + + @ScalarOperator(EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static ClickHouseExpression equal(@SqlType(StandardTypes.BIGINT) ClickHouseExpression left, @SqlType(StandardTypes.BIGINT) ClickHouseExpression right) + { + return new ClickHouseExpression(infixOperation("=", left, right), forwardBindVariables(left, right)); + } + + @ScalarOperator(NOT_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static ClickHouseExpression notEqual(@SqlType(StandardTypes.BIGINT) ClickHouseExpression left, @SqlType(StandardTypes.BIGINT) ClickHouseExpression right) + { + return new ClickHouseExpression(infixOperation("<>", left, right), forwardBindVariables(left, right)); + } + + @ScalarFunction("not") + @SqlType(StandardTypes.BOOLEAN) + public static ClickHouseExpression not(@SqlType(StandardTypes.BOOLEAN) ClickHouseExpression expression) + { + return new ClickHouseExpression(String.format("(NOT(%s))", expression.getExpression()), expression.getBoundConstantValues()); + } +} diff --git a/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/ClickHouseQueryRunner.java b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/ClickHouseQueryRunner.java new file mode 100755 index 0000000000000..a0519c2f6855b --- /dev/null +++ b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/ClickHouseQueryRunner.java @@ -0,0 +1,101 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.airlift.log.Logger; +import com.facebook.airlift.log.Logging; +import com.facebook.presto.Session; +import com.facebook.presto.testing.QueryRunner; +import com.facebook.presto.tests.DistributedQueryRunner; +import com.facebook.presto.tpch.TpchPlugin; +import com.google.common.collect.ImmutableMap; +import io.airlift.tpch.TpchTable; + +import java.util.HashMap; +import java.util.Map; + +import static com.facebook.airlift.testing.Closeables.closeAllSuppress; +import static com.facebook.presto.testing.TestingSession.testSessionBuilder; +import static com.facebook.presto.tests.QueryAssertions.copyTpchTables; +import static com.facebook.presto.tpch.TpchMetadata.TINY_SCHEMA_NAME; + +public final class ClickHouseQueryRunner +{ + private static final String TPCH_SCHEMA = "tpch"; + + private ClickHouseQueryRunner() {} + + public static QueryRunner createClickHouseQueryRunner(TestingClickHouseServer server, Iterable> tables) + throws Exception + { + return createClickHouseQueryRunner(server, ImmutableMap.of(), ImmutableMap.of(), tables); + } + + public static DistributedQueryRunner createClickHouseQueryRunner( + TestingClickHouseServer server, + Map extraProperties, + Map connectorProperties, + Iterable> tables) + throws Exception + { + DistributedQueryRunner queryRunner = null; + try { + queryRunner = DistributedQueryRunner.builder(createSession()) + .setExtraProperties(extraProperties) + .build(); + + queryRunner.installPlugin(new TpchPlugin()); + queryRunner.createCatalog("tpch", "tpch"); + + connectorProperties = new HashMap<>(ImmutableMap.copyOf(connectorProperties)); + connectorProperties.putIfAbsent("clickhouse.connection-url", server.getJdbcUrl()); + connectorProperties.putIfAbsent("clickhouse.allow-drop-table", String.valueOf(true)); + connectorProperties.putIfAbsent("clickhouse.map-string-as-varchar", String.valueOf(true)); + + queryRunner.installPlugin(new ClickHousePlugin()); + queryRunner.createCatalog("clickhouse", "clickhouse", connectorProperties); + server.execute("CREATE DATABASE " + TPCH_SCHEMA); + copyTpchTables(queryRunner, "tpch", TINY_SCHEMA_NAME, createSession(), tables); + return queryRunner; + } + catch (Throwable e) { + closeAllSuppress(e, queryRunner); + throw e; + } + } + + public static Session createSession() + { + return testSessionBuilder() + .setCatalog("clickhouse") + .setSchema(TPCH_SCHEMA) + .build(); + } + + public static void main(String[] args) + throws Exception + { + Logging.initialize(); + + DistributedQueryRunner queryRunner = createClickHouseQueryRunner( + new TestingClickHouseServer(), + ImmutableMap.of("http-server.http.port", "8080"), + ImmutableMap.of(), + TpchTable.getTables()); + + Logger log = Logger.get(ClickHouseQueryRunner.class); + log.info("======== SERVER STARTED ========"); + log.info("\n====\n%s\n====", queryRunner.getCoordinator().getBaseUrl()); + } +} diff --git a/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/ClickHouseSqlExecutor.java b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/ClickHouseSqlExecutor.java new file mode 100755 index 0000000000000..53f96e40291e3 --- /dev/null +++ b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/ClickHouseSqlExecutor.java @@ -0,0 +1,49 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.Session; +import com.facebook.presto.testing.QueryRunner; +import com.facebook.presto.tests.sql.SqlExecutor; + +import static java.util.Objects.requireNonNull; + +public class ClickHouseSqlExecutor + implements SqlExecutor +{ + private final QueryRunner queryRunner; + private final Session session; + + public ClickHouseSqlExecutor(QueryRunner queryRunner) + { + this(queryRunner, queryRunner.getDefaultSession()); + } + + public ClickHouseSqlExecutor(QueryRunner queryRunner, Session session) + { + this.queryRunner = requireNonNull(queryRunner, "queryRunner is null"); + this.session = requireNonNull(session, "session is null"); + } + + @Override + public void execute(String sql) + { + try { + queryRunner.execute(session, sql); + } + catch (Throwable e) { + throw new RuntimeException("Error executing sql:\n" + sql, e); + } + } +} diff --git a/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestClickHouseConfig.java b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestClickHouseConfig.java new file mode 100755 index 0000000000000..8ed3bc685a04d --- /dev/null +++ b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestClickHouseConfig.java @@ -0,0 +1,85 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.airlift.configuration.testing.ConfigAssertions; +import com.google.common.collect.ImmutableMap; +import io.airlift.units.Duration; +import org.testng.annotations.Test; + +import java.util.Map; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; + +public class TestClickHouseConfig +{ + private static final String connectionUrl = "clickhouse.connection-url"; + private static final String connectionUser = "clickhouse.connection-user"; + private static final String connectionPassword = "clickhouse.connection-password"; + private static final String userCredential = "clickhouse.user-credential"; + private static final String passwordCredential = "clickhouse.password-credential"; + private static final String caseInsensitive = "clickhouse.case-insensitive"; + private static final String remoteNameCacheTtl = "clickhouse.remote-name-cache-ttl"; + private static final String mapStringAsVarchar = "clickhouse.map-string-as-varchar"; + private static final String allowDropTable = "clickhouse.allow-drop-table"; + private static final String commitBatchSize = "clickhouse.commitBatchSize"; + + @Test + public void testDefaults() + { + ConfigAssertions.assertRecordedDefaults(ConfigAssertions.recordDefaults(ClickHouseConfig.class) + .setConnectionUrl(null) + .setConnectionUser(null) + .setConnectionPassword(null) + .setUserCredential(null) + .setPasswordCredential(null) + .setCaseInsensitiveNameMatching(false) + .setAllowDropTable(false) + .setCaseInsensitiveNameMatchingCacheTtl(new Duration(1, MINUTES)) + .setMapStringAsVarchar(false) + .setCommitBatchSize(0)); + } + + @Test + public void testExplicitPropertyMappings() + { + Map properties = new ImmutableMap.Builder() + .put(connectionUrl, "jdbc:h2:mem:config") + .put(connectionUser, "user") + .put(connectionPassword, "password") + .put(userCredential, "foo") + .put(passwordCredential, "bar") + .put(caseInsensitive, "true") + .put(remoteNameCacheTtl, "1s") + .put(mapStringAsVarchar, "true") + .put(allowDropTable, "true") + .put(commitBatchSize, "1000") + .build(); + + ClickHouseConfig expected = new ClickHouseConfig() + .setConnectionUrl("jdbc:h2:mem:config") + .setConnectionUser("user") + .setConnectionPassword("password") + .setUserCredential("foo") + .setPasswordCredential("bar") + .setCaseInsensitiveNameMatching(true) + .setAllowDropTable(true) + .setCaseInsensitiveNameMatchingCacheTtl(new Duration(1, SECONDS)) + .setMapStringAsVarchar(true) + .setCommitBatchSize(1000); + + ConfigAssertions.assertFullMapping(properties, expected); + } +} diff --git a/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestClickHouseDistributedQueries.java b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestClickHouseDistributedQueries.java new file mode 100755 index 0000000000000..401f02bfba694 --- /dev/null +++ b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestClickHouseDistributedQueries.java @@ -0,0 +1,481 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.Session; +import com.facebook.presto.testing.MaterializedResult; +import com.facebook.presto.testing.QueryRunner; +import com.facebook.presto.tests.AbstractTestDistributedQueries; +import com.google.common.collect.ImmutableMap; +import io.airlift.tpch.TpchTable; +import org.intellij.lang.annotations.Language; +import org.testng.SkipException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +import java.security.SecureRandom; + +import static com.facebook.presto.common.type.BigintType.BIGINT; +import static com.facebook.presto.common.type.BooleanType.BOOLEAN; +import static com.facebook.presto.common.type.VarcharType.VARCHAR; +import static com.facebook.presto.plugin.clickhouse.ClickHouseQueryRunner.createClickHouseQueryRunner; +import static com.facebook.presto.testing.MaterializedResult.resultBuilder; +import static com.facebook.presto.testing.assertions.Assert.assertEquals; +import static com.facebook.presto.tests.QueryAssertions.assertEqualsIgnoreOrder; +import static java.lang.Character.MAX_RADIX; +import static java.lang.Math.abs; +import static java.lang.Math.min; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; +import static java.util.stream.IntStream.range; +import static org.assertj.core.api.Assertions.assertThat; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class TestClickHouseDistributedQueries + extends AbstractTestDistributedQueries +{ + private TestingClickHouseServer clickhouseServer; + + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + this.clickhouseServer = new TestingClickHouseServer(); + return createClickHouseQueryRunner(clickhouseServer, + ImmutableMap.of("http-server.http.port", "8080"), + ImmutableMap.of(), + TpchTable.getTables()); + } + + @AfterClass(alwaysRun = true) + public final void destroy() + { + if (clickhouseServer != null) { + clickhouseServer.close(); + } + } + + @Test + @Override + public void testLargeIn() + { + String longValues = range(0, 1000) + .mapToObj(Integer::toString) + .collect(joining(", ")); + assertQuery("SELECT orderkey FROM orders WHERE orderkey IN (" + longValues + ")"); + assertQuery("SELECT orderkey FROM orders WHERE orderkey NOT IN (" + longValues + ")"); + + assertQuery("SELECT orderkey FROM orders WHERE orderkey IN (mod(1000, orderkey), " + longValues + ")"); + assertQuery("SELECT orderkey FROM orders WHERE orderkey NOT IN (mod(1000, orderkey), " + longValues + ")"); + + String varcharValues = range(0, 1000) + .mapToObj(i -> "'" + i + "'") + .collect(joining(", ")); + assertQuery("SELECT orderkey FROM orders WHERE cast(orderkey AS VARCHAR) IN (" + varcharValues + ")"); + assertQuery("SELECT orderkey FROM orders WHERE cast(orderkey AS VARCHAR) NOT IN (" + varcharValues + ")"); + } + + @Override + protected boolean supportsViews() + { + return false; + } + + @Override + public void testRenameColumn() + { + // ClickHouse need resets all data in a column for specified column which to be renamed + throw new SkipException("TODO: test not implemented yet"); + } + + @Override + public void testDelete() + { + // ClickHouse need resets all data in a column for specified column which to be renamed + throw new SkipException("TODO: test not implemented yet"); + } + + @Test + @Override + public void testInsert() + { + @Language("SQL") String query = "SELECT orderdate, orderkey, totalprice FROM orders"; + + assertUpdate("CREATE TABLE test_insert AS " + query + " WITH NO DATA", 0); + assertQuery("SELECT count(*) FROM test_insert", "SELECT 0"); + + assertUpdate("INSERT INTO test_insert " + query, "SELECT count(*) FROM orders"); + + assertQuery("SELECT * FROM test_insert", query); + + assertUpdate("INSERT INTO test_insert (orderkey) VALUES (-1)", 1); + assertUpdate("INSERT INTO test_insert (orderkey) VALUES (null)", 1); + assertUpdate("INSERT INTO test_insert (orderdate) VALUES (DATE '2001-01-01')", 1); + assertUpdate("INSERT INTO test_insert (orderkey, orderdate) VALUES (-2, DATE '2001-01-02')", 1); + assertUpdate("INSERT INTO test_insert (orderdate, orderkey) VALUES (DATE '2001-01-03', -3)", 1); + assertUpdate("INSERT INTO test_insert (totalprice) VALUES (1234)", 1); + + assertQuery("SELECT * FROM test_insert", query + + " UNION ALL SELECT null, -1, null" + + " UNION ALL SELECT null, null, null" + + " UNION ALL SELECT DATE '2001-01-01', null, null" + + " UNION ALL SELECT DATE '2001-01-02', -2, null" + + " UNION ALL SELECT DATE '2001-01-03', -3, null" + + " UNION ALL SELECT null, null, 1234"); + + // UNION query produces columns in the opposite order + // of how they are declared in the table schema + assertUpdate( + "INSERT INTO test_insert (orderkey, orderdate, totalprice) " + + "SELECT orderkey, orderdate, totalprice FROM orders " + + "UNION ALL " + + "SELECT orderkey, orderdate, totalprice FROM orders", + "SELECT 2 * count(*) FROM orders"); + + assertUpdate("DROP TABLE test_insert"); + + assertUpdate("CREATE TABLE test_insert (a DOUBLE, b BIGINT)"); + + assertUpdate("INSERT INTO test_insert (a) VALUES (null)", 1); + assertUpdate("INSERT INTO test_insert (a) VALUES (1234)", 1); + assertQuery("SELECT a FROM test_insert", "VALUES (null), (1234)"); + + assertQueryFails("INSERT INTO test_insert (b) VALUES (1.23E1)", "line 1:37: Mismatch at column 1.*"); + + assertUpdate("DROP TABLE test_insert"); + } + + @Test + @Override + public void testDescribeOutputNamedAndUnnamed() + { + Session session = Session.builder(getSession()) + .addPreparedStatement("my_query", "SELECT 1, name, regionkey AS my_alias FROM nation") + .build(); + + MaterializedResult actual = computeActual(session, "DESCRIBE OUTPUT my_query"); + MaterializedResult expected = resultBuilder(session, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, BIGINT, BOOLEAN) + .row("_col0", "", "", "", "integer", 4, false) + .row("name", session.getCatalog().get(), session.getSchema().get(), "nation", "varchar", 0, false) + .row("my_alias", session.getCatalog().get(), session.getSchema().get(), "nation", "bigint", 8, true) + .build(); + assertEqualsIgnoreOrder(actual, expected); + } + + @Test + @Override + public void testInsertIntoNotNullColumn() + { + skipTestUnless(supportsNotNullColumns()); + + String catalog = getSession().getCatalog().get(); + String createTableFormat = "CREATE TABLE %s.tpch.test_not_null_with_insert (\n" + + " %s date,\n" + + " %s date NOT NULL,\n" + + " %s bigint NOT NULL\n" + + ")"; + @Language("SQL") String createTableSql = format( + createTableFormat, + getSession().getCatalog().get(), + "column_a", + "column_b", + "column_c"); + @Language("SQL") String expectedCreateTableSql = format( + createTableFormat, + getSession().getCatalog().get(), + "\"column_a\"", + "\"column_b\"", + "\"column_c\""); + assertUpdate(createTableSql); + assertEquals(computeScalar("SHOW CREATE TABLE test_not_null_with_insert"), expectedCreateTableSql); + + assertQueryFails("INSERT INTO test_not_null_with_insert (column_a) VALUES (date '2012-12-31')", "(?s).*NULL.*column_b.*"); + assertQueryFails("INSERT INTO test_not_null_with_insert (column_a, column_b) VALUES (date '2012-12-31', null)", "(?s).*NULL.*column_b.*"); + + assertQueryFails("INSERT INTO test_not_null_with_insert (column_b) VALUES (date '2012-12-31')", "(?s).*NULL.*column_c.*"); + assertQueryFails("INSERT INTO test_not_null_with_insert (column_b, column_c) VALUES (date '2012-12-31', null)", "(?s).*NULL.*column_c.*"); + + assertUpdate("INSERT INTO test_not_null_with_insert (column_b, column_c) VALUES (date '2012-12-31', 1)", 1); + assertUpdate("INSERT INTO test_not_null_with_insert (column_a, column_b, column_c) VALUES (date '2013-01-01', date '2013-01-02', 2)", 1); + assertQuery( + "SELECT * FROM test_not_null_with_insert", + "VALUES ( NULL, CAST ('2012-12-31' AS DATE), 1 ), ( CAST ('2013-01-01' AS DATE), CAST ('2013-01-02' AS DATE), 2 );"); + + assertUpdate("DROP TABLE test_not_null_with_insert"); + } + + @Override + public void testDropColumn() + { + String tableName = "test_drop_column_" + randomTableSuffix(); + + // only MergeTree engine table can drop column + assertUpdate("CREATE TABLE " + tableName + "(x int NOT NULL, y int, a int) WITH (engine = 'MergeTree', order_by = ARRAY['x'])"); + assertUpdate("INSERT INTO " + tableName + "(x,y,a) SELECT 123, 456, 111", 1); + + assertUpdate("ALTER TABLE " + tableName + " DROP COLUMN IF EXISTS y"); + assertUpdate("ALTER TABLE " + tableName + " DROP COLUMN IF EXISTS notExistColumn"); + assertQueryFails("SELECT y FROM " + tableName, ".* Column 'y' cannot be resolved"); + + assertUpdate("DROP TABLE " + tableName); + + assertFalse(getQueryRunner().tableExists(getSession(), tableName)); + assertUpdate("ALTER TABLE IF EXISTS " + tableName + " DROP COLUMN notExistColumn"); + assertUpdate("ALTER TABLE IF EXISTS " + tableName + " DROP COLUMN IF EXISTS notExistColumn"); + assertFalse(getQueryRunner().tableExists(getSession(), tableName)); + + // the columns are referenced by order_by/order_by property can not be dropped + assertUpdate("CREATE TABLE " + tableName + "(x int NOT NULL, y int, a int NOT NULL) WITH " + + "(engine = 'MergeTree', order_by = ARRAY['x'], partition_by = ARRAY['a'])"); + assertQueryFails("ALTER TABLE " + tableName + " DROP COLUMN x", "ClickHouse exception, code: 47,.*\\n"); + assertQueryFails("ALTER TABLE " + tableName + " DROP COLUMN a", "ClickHouse exception, code: 47,.*\\n"); + } + + @Override + public void testAddColumn() + { + String tableName = "test_add_column_" + randomTableSuffix(); + // Only MergeTree engine table can add column + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'MergeTree', order_by = ARRAY['id'])"); + assertUpdate("INSERT INTO " + tableName + " (id, x) VALUES(1, 'first')", 1); + + assertQueryFails("ALTER TABLE " + tableName + " ADD COLUMN X bigint", ".* Column 'X' already exists"); + assertQueryFails("ALTER TABLE " + tableName + " ADD COLUMN q bad_type", ".* Unknown type 'bad_type' for column 'q'"); + + assertUpdate("ALTER TABLE " + tableName + " ADD COLUMN a varchar"); + assertUpdate("INSERT INTO " + tableName + " SELECT 2, 'second', 'xxx'", 1); + assertQuery( + "SELECT x, a FROM " + tableName, + "VALUES ('first', NULL), ('second', 'xxx')"); + + assertUpdate("ALTER TABLE " + tableName + " ADD COLUMN b double"); + assertUpdate("INSERT INTO " + tableName + " SELECT 3, 'third', 'yyy', 33.3E0", 1); + assertQuery( + "SELECT x, a, b FROM " + tableName, + "VALUES ('first', NULL, NULL), ('second', 'xxx', NULL), ('third', 'yyy', 33.3)"); + + assertUpdate("ALTER TABLE " + tableName + " ADD COLUMN IF NOT EXISTS c varchar"); + assertUpdate("ALTER TABLE " + tableName + " ADD COLUMN IF NOT EXISTS c varchar"); + assertUpdate("INSERT INTO " + tableName + " SELECT 4, 'fourth', 'zzz', 55.3E0, 'newColumn'", 1); + assertQuery( + "SELECT x, a, b, c FROM " + tableName, + "VALUES ('first', NULL, NULL, NULL), ('second', 'xxx', NULL, NULL), ('third', 'yyy', 33.3, NULL), ('fourth', 'zzz', 55.3, 'newColumn')"); + assertUpdate("DROP TABLE " + tableName); + + assertFalse(getQueryRunner().tableExists(getSession(), tableName)); + assertUpdate("ALTER TABLE IF EXISTS " + tableName + " ADD COLUMN x bigint"); + assertUpdate("ALTER TABLE IF EXISTS " + tableName + " ADD COLUMN IF NOT EXISTS x bigint"); + assertFalse(getQueryRunner().tableExists(getSession(), tableName)); + } + + @Test + public void testShowCreateTable() + { + assertThat(computeActual("SHOW CREATE TABLE orders").getOnlyValue()) + .isEqualTo("CREATE TABLE clickhouse.tpch.orders (\n" + + " \"orderkey\" bigint,\n" + + " \"custkey\" bigint,\n" + + " \"orderstatus\" varchar,\n" + + " \"totalprice\" double,\n" + + " \"orderdate\" date,\n" + + " \"orderpriority\" varchar,\n" + + " \"clerk\" varchar,\n" + + " \"shippriority\" integer,\n" + + " \"comment\" varchar\n" + + ")"); + } + + @Override + public void testDescribeOutput() + { + MaterializedResult expectedColumns = resultBuilder(getSession(), VARCHAR, VARCHAR, VARCHAR, VARCHAR) + .row("orderkey", "bigint", "", "") + .row("custkey", "bigint", "", "") + .row("orderstatus", "varchar", "", "") + .row("totalprice", "double", "", "") + .row("orderdate", "date", "", "") + .row("orderpriority", "varchar", "", "") + .row("clerk", "varchar", "", "") + .row("shippriority", "integer", "", "") + .row("comment", "varchar", "", "") + .build(); + MaterializedResult actualColumns = computeActual("DESCRIBE orders"); + assertEquals(actualColumns, expectedColumns); + } + + @Test + public void testDifferentEngine() + { + String tableName = "test_add_column_" + randomTableSuffix(); + // MergeTree + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'MergeTree', order_by = ARRAY['id'])"); + assertTrue(getQueryRunner().tableExists(getSession(), tableName)); + assertUpdate("DROP TABLE " + tableName); + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'mergetree', order_by = ARRAY['id'])"); + assertTrue(getQueryRunner().tableExists(getSession(), tableName)); + assertUpdate("DROP TABLE " + tableName); + // MergeTree without order by + assertQueryFails("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'MergeTree')", "The property of order_by is required for table engine MergeTree\\(\\)"); + + // MergeTree with optional + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR, logdate DATE NOT NULL) WITH " + + "(engine = 'MergeTree', order_by = ARRAY['id'], partition_by = ARRAY['toYYYYMM(logdate)'])"); + assertTrue(getQueryRunner().tableExists(getSession(), tableName)); + assertUpdate("DROP TABLE " + tableName); + + //Log families + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'log')"); + assertUpdate("DROP TABLE " + tableName); + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'tinylog')"); + assertUpdate("DROP TABLE " + tableName); + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'stripelog')"); + assertUpdate("DROP TABLE " + tableName); + + //NOT support engine + assertQueryFails("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'bad_engine')", "Unable to set table property 'engine' to.*"); + } + + /** + * test clickhouse table properties + *

+ * Because the current connector does not support the `show create table` statement to display all the table properties, + * so we cannot use this statement to test whether the properties of the created table meet our expectations, + * and we will modify the test case after the `show create table` is full supported + */ + @Test + public void testTableProperty() + { + String tableName = "test_add_column_" + randomTableSuffix(); + // no table property, it should create a table with default Log engine table + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR)"); + assertTrue(getQueryRunner().tableExists(getSession(), tableName)); + assertUpdate("DROP TABLE " + tableName); + + // one required property + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'Log')"); + assertTrue(getQueryRunner().tableExists(getSession(), tableName)); + assertUpdate("DROP TABLE " + tableName); + + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'StripeLog')"); + assertTrue(getQueryRunner().tableExists(getSession(), tableName)); + assertUpdate("DROP TABLE " + tableName); + + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'TinyLog')"); + assertTrue(getQueryRunner().tableExists(getSession(), tableName)); + assertUpdate("DROP TABLE " + tableName); + + // Log engine DOES NOT any property + assertQueryFails("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'Log', order_by=ARRAY['id'])", ".* doesn't support PARTITION_BY, PRIMARY_KEY, ORDER_BY or SAMPLE_BY clauses.*\\n"); + assertQueryFails("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'Log', partition_by=ARRAY['id'])", ".* doesn't support PARTITION_BY, PRIMARY_KEY, ORDER_BY or SAMPLE_BY clauses.*\\n"); + assertQueryFails("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'Log', sample_by='id')", ".* doesn't support PARTITION_BY, PRIMARY_KEY, ORDER_BY or SAMPLE_BY clauses.*\\n"); + + // optional properties + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'MergeTree', order_by = ARRAY['id'])"); + assertTrue(getQueryRunner().tableExists(getSession(), tableName)); + assertUpdate("DROP TABLE " + tableName); + + // the column refers by order by must be not null + assertQueryFails("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'MergeTree', order_by = ARRAY['id', 'x'])", ".* Sorting key cannot contain nullable columns.*\\n"); + + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR) WITH (engine = 'MergeTree', order_by = ARRAY['id'], primary_key = ARRAY['id'])"); + assertTrue(getQueryRunner().tableExists(getSession(), tableName)); + assertUpdate("DROP TABLE " + tableName); + + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR NOT NULL, y VARCHAR NOT NULL) WITH (engine = 'MergeTree', order_by = ARRAY['id', 'x', 'y'], primary_key = ARRAY['id', 'x'])"); + assertTrue(getQueryRunner().tableExists(getSession(), tableName)); + assertUpdate("DROP TABLE " + tableName); + + assertUpdate("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR NOT NULL, y VARCHAR NOT NULL) WITH (engine = 'MergeTree', order_by = ARRAY['id', 'x'], primary_key = ARRAY['id','x'], sample_by = 'x' )"); + assertTrue(getQueryRunner().tableExists(getSession(), tableName)); + assertUpdate("DROP TABLE " + tableName); + + // Primary key must be a prefix of the sorting key, + assertQueryFails("CREATE TABLE " + tableName + " (id int NOT NULL, x VARCHAR NOT NULL, y VARCHAR NOT NULL) WITH (engine = 'MergeTree', order_by = ARRAY['id'], sample_by = ARRAY['x', 'y'])", + "Invalid value for table property 'sample_by': .*"); + + // wrong property type + assertQueryFails("CREATE TABLE " + tableName + " (id int NOT NULL) WITH (engine = 'MergeTree', order_by = 'id')", + "Invalid value for table property 'order_by': .*"); + assertQueryFails("CREATE TABLE " + tableName + " (id int NOT NULL) WITH (engine = 'MergeTree', order_by = ARRAY['id'], primary_key = 'id')", + "Invalid value for table property 'primary_key': .*"); + assertQueryFails("CREATE TABLE " + tableName + " (id int NOT NULL) WITH (engine = 'MergeTree', order_by = ARRAY['id'], primary_key = ARRAY['id'], partition_by = 'id')", + "Invalid value for table property 'partition_by': .*"); + } + + private static String randomTableSuffix() + { + SecureRandom random = new SecureRandom(); + String randomSuffix = Long.toString(abs(random.nextLong()), MAX_RADIX); + return randomSuffix.substring(0, min(5, randomSuffix.length())); + } + protected static final class DataMappingTestSetup + { + private final String trinoTypeName; + private final String sampleValueLiteral; + private final String highValueLiteral; + + private final boolean unsupportedType; + + public DataMappingTestSetup(String trinoTypeName, String sampleValueLiteral, String highValueLiteral) + { + this(trinoTypeName, sampleValueLiteral, highValueLiteral, false); + } + + private DataMappingTestSetup(String trinoTypeName, String sampleValueLiteral, String highValueLiteral, boolean unsupportedType) + { + this.trinoTypeName = requireNonNull(trinoTypeName, "trinoTypeName is null"); + this.sampleValueLiteral = requireNonNull(sampleValueLiteral, "sampleValueLiteral is null"); + this.highValueLiteral = requireNonNull(highValueLiteral, "highValueLiteral is null"); + this.unsupportedType = unsupportedType; + } + + public String getTrinoTypeName() + { + return trinoTypeName; + } + + public String getSampleValueLiteral() + { + return sampleValueLiteral; + } + + public String getHighValueLiteral() + { + return highValueLiteral; + } + + public boolean isUnsupportedType() + { + return unsupportedType; + } + + public DataMappingTestSetup asUnsupported() + { + return new DataMappingTestSetup( + trinoTypeName, + sampleValueLiteral, + highValueLiteral, + true); + } + + @Override + public String toString() + { + // toString is brief because it's used for test case labels in IDE + return trinoTypeName + (unsupportedType ? "!" : ""); + } + } +} diff --git a/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestClickHousePlugin.java b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestClickHousePlugin.java new file mode 100755 index 0000000000000..2081f18a5cd3c --- /dev/null +++ b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestClickHousePlugin.java @@ -0,0 +1,33 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.spi.Plugin; +import com.facebook.presto.spi.connector.ConnectorFactory; +import com.facebook.presto.testing.TestingConnectorContext; +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.Test; + +import static com.google.common.collect.Iterables.getOnlyElement; + +public class TestClickHousePlugin +{ + @Test + public void testCreateConnector() + { + Plugin plugin = new ClickHousePlugin(); + ConnectorFactory factory = getOnlyElement(plugin.getConnectorFactories()); + factory.create("test", ImmutableMap.of("clickhouse.connection-url", "jdbc:clickhouse://test"), new TestingConnectorContext()); + } +} diff --git a/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestingClickHouseModule.java b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestingClickHouseModule.java new file mode 100755 index 0000000000000..75375285fb429 --- /dev/null +++ b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestingClickHouseModule.java @@ -0,0 +1,55 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Provides; +import ru.yandex.clickhouse.ClickHouseDriver; + +import java.util.Map; +import java.util.Optional; +import java.util.Properties; + +import static com.facebook.airlift.configuration.ConfigBinder.configBinder; +import static java.lang.String.format; + +public class TestingClickHouseModule + implements Module +{ + @Override + public void configure(Binder binder) + { + configBinder(binder).bindConfig(ClickHouseConfig.class); + } + + @Provides + public ClickHouseClient provideJdbcClient(ClickHouseConnectorId id, ClickHouseConfig config) + { + Properties connectionProperties = new Properties(); + return new ClickHouseClient(id, config, new DriverConnectionFactory(new ClickHouseDriver(), + config.getConnectionUrl(), + Optional.ofNullable(config.getUserCredential()), + Optional.ofNullable(config.getPasswordCredential()), + connectionProperties)); + } + + public static Map createProperties() + { + return ImmutableMap.builder() + .put("clickhouse.connection-url", format("jdbc:clickhouse://localhost:8123/", System.nanoTime())) + .build(); + } +} diff --git a/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestingClickHouseServer.java b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestingClickHouseServer.java new file mode 100755 index 0000000000000..659c42cc40204 --- /dev/null +++ b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestingClickHouseServer.java @@ -0,0 +1,69 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import org.testcontainers.containers.ClickHouseContainer; + +import java.io.Closeable; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; + +import static java.lang.String.format; +import static org.testcontainers.containers.ClickHouseContainer.HTTP_PORT; + +public class TestingClickHouseServer + implements Closeable +{ + private static final String CLICKHOUSE_IMAGE = "yandex/clickhouse-server:20.8"; + private final ClickHouseContainer dockerContainer; + + public TestingClickHouseServer() + { + // Use 2nd stable version + dockerContainer = (ClickHouseContainer) new ClickHouseContainer(CLICKHOUSE_IMAGE) + .withStartupAttempts(10); + + dockerContainer.start(); + } + + public ClickHouseContainer getClickHouseContainer() + { + return dockerContainer; + } + public void execute(String sql) + { + try (Connection connection = DriverManager.getConnection(getJdbcUrl()); + Statement statement = connection.createStatement()) { + statement.execute(sql); + } + catch (Exception e) { + throw new RuntimeException("Failed to execute statement: " + sql, e); + } + } + + public String getJdbcUrl() + { + String s = format("jdbc:clickhouse://%s:%s/", dockerContainer.getContainerIpAddress(), + dockerContainer.getMappedPort(HTTP_PORT)); + return format("jdbc:clickhouse://%s:%s/", dockerContainer.getContainerIpAddress(), + dockerContainer.getMappedPort(HTTP_PORT)); + } + + @Override + public void close() + { + dockerContainer.stop(); + } +} diff --git a/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestingClickHouseTypeHandle.java b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestingClickHouseTypeHandle.java new file mode 100755 index 0000000000000..5cc8450748dec --- /dev/null +++ b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestingClickHouseTypeHandle.java @@ -0,0 +1,40 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import java.sql.Types; +import java.util.Optional; + +public final class TestingClickHouseTypeHandle +{ + private TestingClickHouseTypeHandle() {} + + public static final ClickHouseTypeHandle JDBC_BOOLEAN = new ClickHouseTypeHandle(Types.BOOLEAN, Optional.of("boolean"), 1, 0, Optional.empty(), Optional.empty()); + + public static final ClickHouseTypeHandle JDBC_SMALLINT = new ClickHouseTypeHandle(Types.SMALLINT, Optional.of("smallint"), 1, 0, Optional.empty(), Optional.empty()); + public static final ClickHouseTypeHandle JDBC_TINYINT = new ClickHouseTypeHandle(Types.TINYINT, Optional.of("tinyint"), 2, 0, Optional.empty(), Optional.empty()); + public static final ClickHouseTypeHandle JDBC_INTEGER = new ClickHouseTypeHandle(Types.INTEGER, Optional.of("integer"), 4, 0, Optional.empty(), Optional.empty()); + public static final ClickHouseTypeHandle JDBC_BIGINT = new ClickHouseTypeHandle(Types.BIGINT, Optional.of("bigint"), 8, 0, Optional.empty(), Optional.empty()); + + public static final ClickHouseTypeHandle JDBC_REAL = new ClickHouseTypeHandle(Types.REAL, Optional.of("real"), 8, 0, Optional.empty(), Optional.empty()); + public static final ClickHouseTypeHandle JDBC_DOUBLE = new ClickHouseTypeHandle(Types.DOUBLE, Optional.of("double precision"), 8, 0, Optional.empty(), Optional.empty()); + + public static final ClickHouseTypeHandle JDBC_CHAR = new ClickHouseTypeHandle(Types.CHAR, Optional.of("char"), 10, 0, Optional.empty(), Optional.empty()); + public static final ClickHouseTypeHandle JDBC_VARCHAR = new ClickHouseTypeHandle(Types.VARCHAR, Optional.of("varchar"), 10, 0, Optional.empty(), Optional.empty()); + public static final ClickHouseTypeHandle JDBC_STRING = new ClickHouseTypeHandle(Types.VARCHAR, Optional.of("String"), 10, 0, Optional.empty(), Optional.empty()); + + public static final ClickHouseTypeHandle JDBC_DATE = new ClickHouseTypeHandle(Types.DATE, Optional.of("date"), 8, 0, Optional.empty(), Optional.empty()); + public static final ClickHouseTypeHandle JDBC_TIME = new ClickHouseTypeHandle(Types.TIME, Optional.of("time"), 4, 0, Optional.empty(), Optional.empty()); + public static final ClickHouseTypeHandle JDBC_TIMESTAMP = new ClickHouseTypeHandle(Types.TIMESTAMP, Optional.of("timestamp"), 8, 0, Optional.empty(), Optional.empty()); +} diff --git a/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestingDatabase.java b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestingDatabase.java new file mode 100755 index 0000000000000..f2abe4bbd840a --- /dev/null +++ b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestingDatabase.java @@ -0,0 +1,92 @@ +/* + * Licensed 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 com.facebook.presto.plugin.clickhouse; + +import com.facebook.presto.spi.ConnectorSession; +import ru.yandex.clickhouse.ClickHouseDriver; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Optional; +import java.util.Properties; + +import static com.facebook.presto.testing.TestingSession.testSessionBuilder; + +final class TestingDatabase + implements AutoCloseable +{ + public static final String CONNECTOR_ID = "clickhouse"; + private static final ConnectorSession session = testSessionBuilder().build().toConnectorSession(); + + private final Connection connection; + private final ClickHouseClient clickHouseClient; + private final TestingClickHouseServer testingClickHouseServer; + + public TestingDatabase() + throws SQLException + { + testingClickHouseServer = new TestingClickHouseServer(); + Properties connectionProperties = new Properties(); + clickHouseClient = new ClickHouseClient(new ClickHouseConnectorId(CONNECTOR_ID), + new ClickHouseConfig(), + new DriverConnectionFactory(new ClickHouseDriver(), + testingClickHouseServer.getJdbcUrl(), + Optional.ofNullable(testingClickHouseServer.getClickHouseContainer().getUsername()), + Optional.ofNullable(testingClickHouseServer.getClickHouseContainer().getPassword()), + connectionProperties)); + connection = DriverManager.getConnection(testingClickHouseServer.getJdbcUrl()); + connection.createStatement().execute("CREATE DATABASE example"); + + connection.createStatement().execute("CREATE TABLE example.numbers(text varchar(10), text_short varchar(10), value bigint) ENGINE = TinyLog "); + connection.createStatement().execute("INSERT INTO example.numbers(text, text_short, value) VALUES " + + "('one', 'one', 1)," + + "('two', 'two', 2)," + + "('three', 'three', 3)," + + "('ten', 'ten', 10)," + + "('eleven', 'eleven', 11)," + + "('twelve', 'twelve', 12)" + + ""); + connection.createStatement().execute("CREATE TABLE example.view_source(id varchar(10)) ENGINE = TinyLog "); + //connection.createStatement().execute("CREATE TABLE example.view ENGINE = TinyLog AS SELECT id FROM example.view_source"); + connection.createStatement().execute("CREATE DATABASE tpch"); + connection.createStatement().execute("CREATE TABLE tpch.orders(orderkey bigint, custkey bigint) ENGINE = TinyLog "); + connection.createStatement().execute("CREATE TABLE tpch.lineitem(orderkey bigint , partkey bigint) ENGINE = TinyLog "); + + connection.createStatement().execute("CREATE DATABASE exa_ple"); + connection.createStatement().execute("CREATE TABLE exa_ple.num_ers(te_t varchar(10), \"VA%UE\" bigint) ENGINE = TinyLog "); + connection.createStatement().execute("CREATE TABLE exa_ple.table_with_float_col(col1 bigint, col2 double, col3 float, col4 real) ENGINE = TinyLog "); + + connection.createStatement().execute("CREATE DATABASE schema_for_create_table_tests"); + connection.commit(); + } + + @Override + public void close() + throws SQLException + { + connection.close(); + testingClickHouseServer.close(); + } + + public Connection getConnection() + { + return connection; + } + + public ClickHouseClient getClickHouseClient() + { + return clickHouseClient; + } +} diff --git a/presto-docs/src/main/sphinx/connector/clickhouse.rst b/presto-docs/src/main/sphinx/connector/clickhouse.rst new file mode 100644 index 0000000000000..505e5a6626d7e --- /dev/null +++ b/presto-docs/src/main/sphinx/connector/clickhouse.rst @@ -0,0 +1,156 @@ +==================== +ClickHouse connector +==================== + +The ClickHouse connector allows querying tables in an external +`ClickHouse `_ server. This can be used to +query data in the databases on that server, or combine it with other data +from different catalogs accessing ClickHouse or any other supported data source. + +Requirements +------------ + +To connect to a ClickHouse server, you need: + +* ClickHouse version 20.8 or higher. +* Network access from the Presto coordinator and workers to the ClickHouse + server. Port 8123 is the default port. + +Configuration +------------- + +The connector can query a ClickHouse server. Create a catalog properties file +that specifies the ClickHouse connector by setting the ``connector.name`` to +``clickhouse``. + +For example, to access a server as ``clickhouse``, create the file +``etc/catalog/clickhouse.properties``. Replace the connection properties as +appropriate for your setup: + +.. code-block:: none + + connector.name=clickhouse + clickhouse.connection-url=jdbc:clickhouse://host1:8123/ + clickhouse.connection-user=default + clickhouse.connection-password=secret + +Multiple ClickHouse servers +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you have multiple ClickHouse servers you need to configure one +catalog for each server. To add another catalog: + +* Add another properties file to ``etc/catalog`` +* Save it with a different name that ends in ``.properties`` + +For example, if you name the property file ``clickhouse.properties``, Prestodb uses the +configured connector to create a catalog named ``clickhouse``. + +General configuration properties +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following table describes general catalog configuration properties for the connector: + +========================================= ================ ============================================================================================================== +Property Name Default Value Description +========================================= ================ ============================================================================================================== +``clickhouse.map-string-as-varchar`` false When creating a table, support the clickhouse data type String. + +``clickhouse.allow-drop-table`` false Allow delete table operation. + +========================================= ================ ============================================================================================================== + + +Querying ClickHouse +------------------- + +The ClickHouse connector provides a schema for every ClickHouse *database*. +run ``SHOW SCHEMAS`` to see the available ClickHouse databases:: + + SHOW SCHEMAS FROM clickhouse; + +If you have a ClickHouse database named ``tpch``, run ``SHOW TABLES`` to view the +tables in this database:: + + SHOW TABLES FROM clickhouse.tpch; + +Run ``DESCRIBE`` or ``SHOW COLUMNS`` to list the columns in the ``cks`` table +in the ``tpch`` databases:: + + DESCRIBE clickhouse.tpch.cks; + SHOW COLUMNS FROM clickhouse.tpch.cks; + +Run ``SELECT`` to access the ``cks`` table in the ``tpch`` database:: + + SELECT * FROM clickhouse.tpch.cks; + +.. note:: + + If you used a different name for your catalog properties file, use + that catalog name instead of ``clickhouse`` in the above examples. + +Table properties +---------------- + +Table property usage example:: + + CREATE TABLE default.prestodb_ck ( + id int NOT NULL, + birthday DATE NOT NULL, + name VARCHAR, + age BIGINT, + logdate DATE NOT NULL + ) + WITH ( + engine = 'MergeTree', + order_by = ARRAY['id', 'birthday'], + partition_by = ARRAY['toYYYYMM(logdate)'], + primary_key = ARRAY['id'], + sample_by = 'id' + ); + +The following are supported ClickHouse table properties from ``_ + +=========================== ================ ============================================================================================================== +Property Name Default Value Description +=========================== ================ ============================================================================================================== +``engine`` ``Log`` Name and parameters of the engine. + +``order_by`` (none) Array of columns or expressions to concatenate to create the sorting key. Required if ``engine`` is ``MergeTree``. + +``partition_by`` (none) Array of columns or expressions to use as nested partition keys. Optional. + +``primary_key`` (none) Array of columns or expressions to concatenate to create the primary key. Optional. + +``sample_by`` (none) An expression to use for `sampling `_. + Optional. + +=========================== ================ ============================================================================================================== + +Currently the connector only supports ``Log`` and ``MergeTree`` table engines +in create table statement. ``ReplicatedMergeTree`` engine is not yet supported. + +Pushdown +-------- + +The connector supports pushdown for a number of operations: + +* :ref:`limit-pushdown` + +.. _clickhouse-sql-support: + +SQL support +----------- + +The connector provides read and write access to data and metadata in +a ClickHouse catalog. In addition to the :ref:`globally available +` and :ref:`read operation ` +statements, the connector supports the following features: + +* :doc:`/sql/insert` +* :doc:`/sql/truncate` +* :doc:`/sql/create-table` +* :doc:`/sql/create-table-as` +* :doc:`/sql/drop-table` +* :doc:`/sql/create-schema` +* :doc:`/sql/drop-schema`