()
+ .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`