mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
1494 lines
53 KiB
Python
1494 lines
53 KiB
Python
import threading
|
|
import time
|
|
from contextlib import contextmanager
|
|
|
|
## sudo -H pip install PyMySQL
|
|
import pymysql.cursors
|
|
import pytest
|
|
|
|
from helpers.client import QueryRuntimeException
|
|
from helpers.cluster import ClickHouseCluster
|
|
from helpers.config_cluster import mysql_pass
|
|
|
|
cluster = ClickHouseCluster(__file__)
|
|
|
|
node1 = cluster.add_instance(
|
|
"node1",
|
|
main_configs=[
|
|
"configs/remote_servers.xml",
|
|
"configs/named_collections.xml",
|
|
"certs/ca.pem",
|
|
"certs/client-key.pem",
|
|
"certs/client-cert.pem",
|
|
],
|
|
user_configs=["configs/users.xml"],
|
|
with_mysql8=True,
|
|
)
|
|
node2 = cluster.add_instance(
|
|
"node2", main_configs=["configs/remote_servers.xml"], with_mysql_cluster=True
|
|
)
|
|
node3 = cluster.add_instance(
|
|
"node3",
|
|
main_configs=["configs/remote_servers.xml"],
|
|
user_configs=["configs/users.xml"],
|
|
with_mysql8=True,
|
|
)
|
|
|
|
create_table_sql_template = """
|
|
CREATE TABLE `clickhouse`.`{}` (
|
|
`id` int(11) NOT NULL,
|
|
`name` varchar(50) NOT NULL,
|
|
`age` int NOT NULL default 0,
|
|
`money` int NOT NULL default 0,
|
|
`source` enum('IP','URL') DEFAULT 'IP',
|
|
PRIMARY KEY (`id`)) ENGINE=InnoDB;
|
|
"""
|
|
|
|
drop_table_sql_template = """
|
|
DROP TABLE IF EXISTS `clickhouse`.`{}`;
|
|
"""
|
|
|
|
|
|
def get_mysql_conn(started_cluster, host):
|
|
conn = pymysql.connect(
|
|
user="root", password=mysql_pass, host=host, port=started_cluster.mysql8_port
|
|
)
|
|
return conn
|
|
|
|
|
|
def create_mysql_table(conn, tableName):
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(create_table_sql_template.format(tableName))
|
|
|
|
|
|
def drop_mysql_table(conn, tableName):
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(drop_table_sql_template.format(tableName))
|
|
|
|
|
|
def create_mysql_db(conn, name):
|
|
with conn.cursor() as cursor:
|
|
cursor.execute("DROP DATABASE IF EXISTS {}".format(name))
|
|
cursor.execute("CREATE DATABASE {} DEFAULT CHARACTER SET 'utf8'".format(name))
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def started_cluster():
|
|
try:
|
|
cluster.start()
|
|
|
|
conn = get_mysql_conn(cluster, cluster.mysql8_ip)
|
|
create_mysql_db(conn, "clickhouse")
|
|
|
|
## create mysql db and table
|
|
conn1 = get_mysql_conn(cluster, cluster.mysql2_ip)
|
|
create_mysql_db(conn1, "clickhouse")
|
|
yield cluster
|
|
|
|
finally:
|
|
cluster.shutdown()
|
|
|
|
|
|
def test_many_connections(started_cluster):
|
|
table_name = "test_many_connections"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE {table_name}(id UInt32, name String, age UInt32, money UInt32) ENGINE = MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}');
|
|
"""
|
|
)
|
|
|
|
node1.query(
|
|
"INSERT INTO {} (id, name) SELECT number, concat('name_', toString(number)) from numbers(10) ".format(
|
|
table_name
|
|
)
|
|
)
|
|
|
|
query = "SELECT count() FROM ("
|
|
for i in range(24):
|
|
query += "SELECT id FROM {t} UNION ALL "
|
|
query += "SELECT id FROM {t})"
|
|
|
|
assert node1.query(query.format(t=table_name)) == "250\n"
|
|
drop_mysql_table(conn, table_name)
|
|
conn.close()
|
|
|
|
|
|
def test_insert_select(started_cluster):
|
|
table_name = "test_insert_select"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE {table_name}(id UInt32, name String, age UInt32, money UInt32) ENGINE = MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}');
|
|
"""
|
|
)
|
|
node1.query(
|
|
"INSERT INTO {}(id, name, money) select number, concat('name_', toString(number)), 3 from numbers(10000) ".format(
|
|
table_name
|
|
)
|
|
)
|
|
assert node1.query("SELECT count() FROM {}".format(table_name)).rstrip() == "10000"
|
|
assert (
|
|
node1.query("SELECT sum(money) FROM {}".format(table_name)).rstrip() == "30000"
|
|
)
|
|
conn.close()
|
|
|
|
|
|
def test_replace_select(started_cluster):
|
|
table_name = "test_replace_select"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE {table_name}(id UInt32, name String, age UInt32, money UInt32) ENGINE = MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}', 1);
|
|
"""
|
|
)
|
|
node1.query(
|
|
"INSERT INTO {}(id, name, money) select number, concat('name_', toString(number)), 3 from numbers(10000) ".format(
|
|
table_name
|
|
)
|
|
)
|
|
node1.query(
|
|
"INSERT INTO {}(id, name, money) select number, concat('name_', toString(number)), 3 from numbers(10000) ".format(
|
|
table_name
|
|
)
|
|
)
|
|
assert node1.query("SELECT count() FROM {}".format(table_name)).rstrip() == "10000"
|
|
assert (
|
|
node1.query("SELECT sum(money) FROM {}".format(table_name)).rstrip() == "30000"
|
|
)
|
|
conn.close()
|
|
|
|
|
|
def test_insert_on_duplicate_select(started_cluster):
|
|
table_name = "test_insert_on_duplicate_select"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE {table_name}(id UInt32, name String, age UInt32, money UInt32) ENGINE = MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}', 0, 'update money = money + values(money)');
|
|
"""
|
|
)
|
|
node1.query(
|
|
"INSERT INTO {}(id, name, money) select number, concat('name_', toString(number)), 3 from numbers(10000) ".format(
|
|
table_name
|
|
)
|
|
)
|
|
node1.query(
|
|
"INSERT INTO {}(id, name, money) select number, concat('name_', toString(number)), 3 from numbers(10000) ".format(
|
|
table_name
|
|
)
|
|
)
|
|
assert node1.query("SELECT count() FROM {}".format(table_name)).rstrip() == "10000"
|
|
assert (
|
|
node1.query("SELECT sum(money) FROM {}".format(table_name)).rstrip() == "60000"
|
|
)
|
|
conn.close()
|
|
|
|
|
|
def test_where(started_cluster):
|
|
table_name = "test_where"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE {table_name}(id UInt32, name String, age UInt32, money UInt32) ENGINE = MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}');
|
|
"""
|
|
)
|
|
node1.query(
|
|
"INSERT INTO {}(id, name, money) select number, concat('name_', toString(number)), 3 from numbers(10000) ".format(
|
|
table_name
|
|
)
|
|
)
|
|
assert (
|
|
node1.query(
|
|
"SELECT count() FROM {} WHERE name LIKE '%name_%'".format(table_name)
|
|
).rstrip()
|
|
== "10000"
|
|
)
|
|
assert (
|
|
node1.query(
|
|
"SELECT count() FROM {} WHERE name NOT LIKE '%tmp_%'".format(table_name)
|
|
).rstrip()
|
|
== "10000"
|
|
)
|
|
assert (
|
|
node1.query(
|
|
"SELECT count() FROM {} WHERE money IN (1, 2, 3)".format(table_name)
|
|
).rstrip()
|
|
== "10000"
|
|
)
|
|
assert (
|
|
node1.query(
|
|
"SELECT count() FROM {} WHERE money IN (1, 2, 4, 5, 6)".format(table_name)
|
|
).rstrip()
|
|
== "0"
|
|
)
|
|
assert (
|
|
node1.query(
|
|
"SELECT count() FROM {} WHERE money NOT IN (1, 2, 4, 5, 6)".format(
|
|
table_name
|
|
)
|
|
).rstrip()
|
|
== "10000"
|
|
)
|
|
assert (
|
|
node1.query(
|
|
"SELECT count() FROM {} WHERE name LIKE concat('name_', toString(1))".format(
|
|
table_name
|
|
)
|
|
).rstrip()
|
|
== "1"
|
|
)
|
|
conn.close()
|
|
|
|
|
|
def test_table_function(started_cluster):
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, "table_function")
|
|
create_mysql_table(conn, "table_function")
|
|
table_function = (
|
|
f"mysql('mysql80:3306', 'clickhouse', '{{}}', 'root', '{mysql_pass}')".format(
|
|
"table_function"
|
|
)
|
|
)
|
|
assert node1.query("SELECT count() FROM {}".format(table_function)).rstrip() == "0"
|
|
node1.query(
|
|
"INSERT INTO {} (id, name, money) select number, concat('name_', toString(number)), 3 from numbers(10000)".format(
|
|
"TABLE FUNCTION " + table_function
|
|
)
|
|
)
|
|
assert (
|
|
node1.query("SELECT count() FROM {}".format(table_function)).rstrip() == "10000"
|
|
)
|
|
assert (
|
|
node1.query(
|
|
"SELECT sum(c) FROM ("
|
|
"SELECT count() as c FROM {} WHERE id % 3 == 0"
|
|
" UNION ALL SELECT count() as c FROM {} WHERE id % 3 == 1"
|
|
" UNION ALL SELECT count() as c FROM {} WHERE id % 3 == 2)".format(
|
|
table_function, table_function, table_function
|
|
)
|
|
).rstrip()
|
|
== "10000"
|
|
)
|
|
assert (
|
|
node1.query("SELECT sum(`money`) FROM {}".format(table_function)).rstrip()
|
|
== "30000"
|
|
)
|
|
node1.query(
|
|
"INSERT INTO {} (id, name, age, money) SELECT id + 100000, name, age, money FROM {}".format(
|
|
"TABLE FUNCTION " + table_function, table_function
|
|
)
|
|
)
|
|
assert (
|
|
node1.query("SELECT sum(`money`) FROM {}".format(table_function)).rstrip()
|
|
== "60000"
|
|
)
|
|
conn.close()
|
|
|
|
|
|
def test_schema_inference(started_cluster):
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, "inference_table")
|
|
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
"CREATE TABLE clickhouse.inference_table (id INT PRIMARY KEY, data BINARY(16) NOT NULL)"
|
|
)
|
|
|
|
parameters = f"'mysql80:3306', 'clickhouse', 'inference_table', 'root', '{mysql_pass}'"
|
|
|
|
node1.query(
|
|
f"CREATE TABLE mysql_schema_inference_engine ENGINE=MySQL({parameters})"
|
|
)
|
|
node1.query(f"CREATE TABLE mysql_schema_inference_function AS mysql({parameters})")
|
|
|
|
expected = "id\tInt32\t\t\t\t\t\ndata\tFixedString(16)\t\t\t\t\t\n"
|
|
assert node1.query("DESCRIBE TABLE mysql_schema_inference_engine") == expected
|
|
assert node1.query("DESCRIBE TABLE mysql_schema_inference_function") == expected
|
|
|
|
node1.query("DROP TABLE mysql_schema_inference_engine")
|
|
node1.query("DROP TABLE mysql_schema_inference_function")
|
|
|
|
drop_mysql_table(conn, "inference_table")
|
|
|
|
|
|
def test_binary_type(started_cluster):
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, "binary_type")
|
|
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
"CREATE TABLE clickhouse.binary_type (id INT PRIMARY KEY, data BINARY(16) NOT NULL)"
|
|
)
|
|
table_function = (
|
|
f"mysql('mysql80:3306', 'clickhouse', '{{}}', 'root', '{mysql_pass}')".format(
|
|
"binary_type"
|
|
)
|
|
)
|
|
node1.query(
|
|
"INSERT INTO {} VALUES (42, 'clickhouse')".format(
|
|
"TABLE FUNCTION " + table_function
|
|
)
|
|
)
|
|
assert (
|
|
node1.query("SELECT * FROM {}".format(table_function))
|
|
== "42\tclickhouse\\0\\0\\0\\0\\0\\0\n"
|
|
)
|
|
drop_mysql_table(conn, "binary_type")
|
|
|
|
|
|
def test_enum_type(started_cluster):
|
|
table_name = "test_enum_type"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE {table_name}(id UInt32, name String, age UInt32, money UInt32, source Enum8('IP' = 1, 'URL' = 2)) ENGINE = MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}', 1);
|
|
"""
|
|
)
|
|
node1.query(
|
|
"INSERT INTO {} (id, name, age, money, source) VALUES (1, 'name', 0, 0, 'URL')".format(
|
|
table_name
|
|
)
|
|
)
|
|
assert (
|
|
node1.query("SELECT source FROM {} LIMIT 1".format(table_name)).rstrip()
|
|
== "URL"
|
|
)
|
|
conn.close()
|
|
|
|
|
|
def test_external_settings(started_cluster):
|
|
table_name = "test_external_settings"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
conn = get_mysql_conn(started_cluster, started_cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
|
|
node3.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
node3.query(
|
|
f"""
|
|
CREATE TABLE {table_name}(id UInt32, name String, age UInt32, money UInt32) ENGINE = MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}');
|
|
"""
|
|
)
|
|
node3.query(
|
|
"INSERT INTO {}(id, name, money) select number, concat('name_', toString(number)), 3 from numbers(100) ".format(
|
|
table_name
|
|
)
|
|
)
|
|
assert node3.query("SELECT count() FROM {}".format(table_name)).rstrip() == "100"
|
|
assert node3.query("SELECT sum(money) FROM {}".format(table_name)).rstrip() == "300"
|
|
node3.query(
|
|
"select value from system.settings where name = 'max_block_size' FORMAT TSV"
|
|
) == "2\n"
|
|
node3.query(
|
|
"select value from system.settings where name = 'external_storage_max_read_rows' FORMAT TSV"
|
|
) == "0\n"
|
|
assert (
|
|
node3.query(
|
|
"SELECT COUNT(DISTINCT blockNumber()) FROM {} FORMAT TSV".format(table_name)
|
|
)
|
|
== "50\n"
|
|
)
|
|
conn.close()
|
|
|
|
|
|
def test_settings_connection_wait_timeout(started_cluster):
|
|
table_name = "test_settings_connection_wait_timeout"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
wait_timeout = 2
|
|
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE {table_name}
|
|
(
|
|
id UInt32,
|
|
name String,
|
|
age UInt32,
|
|
money UInt32
|
|
)
|
|
ENGINE = MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}')
|
|
SETTINGS connection_wait_timeout={wait_timeout}, connection_pool_size=1
|
|
"""
|
|
)
|
|
|
|
node1.query(
|
|
"INSERT INTO {} (id, name) SELECT number, concat('name_', toString(number)) from numbers(10) ".format(
|
|
table_name
|
|
)
|
|
)
|
|
|
|
worker_started_event = threading.Event()
|
|
|
|
def worker():
|
|
worker_started_event.set()
|
|
node1.query(
|
|
"SELECT 1, sleepEachRow(1) FROM {} SETTINGS max_threads=1".format(
|
|
table_name
|
|
)
|
|
)
|
|
|
|
worker_thread = threading.Thread(target=worker)
|
|
worker_thread.start()
|
|
|
|
# ensure that first query started in worker_thread
|
|
assert worker_started_event.wait(10)
|
|
time.sleep(1)
|
|
|
|
started = time.time()
|
|
with pytest.raises(
|
|
QueryRuntimeException,
|
|
match=r"Exception: mysqlxx::Pool is full \(connection_wait_timeout is exceeded\)",
|
|
):
|
|
node1.query(
|
|
"SELECT 2, sleepEachRow(1) FROM {} SETTINGS max_threads=1".format(
|
|
table_name
|
|
)
|
|
)
|
|
ended = time.time()
|
|
assert (ended - started) >= wait_timeout
|
|
|
|
worker_thread.join()
|
|
|
|
drop_mysql_table(conn, table_name)
|
|
conn.close()
|
|
|
|
|
|
def test_predefined_connection_configuration(started_cluster):
|
|
conn = get_mysql_conn(started_cluster, started_cluster.mysql8_ip)
|
|
table_name = "test_table"
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
|
|
node1.query(
|
|
"""
|
|
DROP TABLE IF EXISTS test_table;
|
|
CREATE TABLE test_table (id UInt32, name String, age UInt32, money UInt32)
|
|
ENGINE MySQL(mysql1);
|
|
"""
|
|
)
|
|
node1.query(
|
|
"INSERT INTO test_table (id, name, money) select number, toString(number), number from numbers(100)"
|
|
)
|
|
assert node1.query("SELECT count() FROM test_table").rstrip() == "100"
|
|
|
|
node1.query(
|
|
"""
|
|
DROP TABLE IF EXISTS test_table;
|
|
CREATE TABLE test_table (id UInt32, name String, age UInt32, money UInt32)
|
|
ENGINE MySQL(mysql1, replace_query=1);
|
|
"""
|
|
)
|
|
node1.query(
|
|
"INSERT INTO test_table (id, name, money) select number, toString(number), number from numbers(100)"
|
|
)
|
|
node1.query(
|
|
"INSERT INTO test_table (id, name, money) select number, toString(number), number from numbers(100)"
|
|
)
|
|
assert node1.query("SELECT count() FROM test_table").rstrip() == "100"
|
|
|
|
node1.query_and_get_error(
|
|
"""
|
|
DROP TABLE IF EXISTS test_table;
|
|
CREATE TABLE test_table (id UInt32, name String, age UInt32, money UInt32)
|
|
ENGINE MySQL(mysql1, query=1);
|
|
"""
|
|
)
|
|
node1.query_and_get_error(
|
|
"""
|
|
DROP TABLE IF EXISTS test_table;
|
|
CREATE TABLE test_table (id UInt32, name String, age UInt32, money UInt32)
|
|
ENGINE MySQL(mysql1, replace_query=1, on_duplicate_clause='kek');
|
|
"""
|
|
)
|
|
node1.query_and_get_error(
|
|
"""
|
|
DROP TABLE IF EXISTS test_table;
|
|
CREATE TABLE test_table (id UInt32, name String, age UInt32, money UInt32)
|
|
ENGINE MySQL(fff);
|
|
"""
|
|
)
|
|
node1.query_and_get_error(
|
|
"""
|
|
DROP TABLE IF EXISTS test_table;
|
|
CREATE TABLE test_table (id UInt32, name String, age UInt32, money UInt32)
|
|
ENGINE MySQL(mysql2);
|
|
"""
|
|
)
|
|
|
|
node1.query(
|
|
"""
|
|
DROP TABLE IF EXISTS test_table;
|
|
CREATE TABLE test_table (id UInt32, name String, age UInt32, money UInt32)
|
|
ENGINE MySQL(mysql3, port=3306);
|
|
"""
|
|
)
|
|
assert node1.query("SELECT count() FROM test_table").rstrip() == "100"
|
|
|
|
assert "Connection pool cannot have zero size" in node1.query_and_get_error(
|
|
"SELECT count() FROM mysql(mysql1, `table`='test_table', connection_pool_size=0)"
|
|
)
|
|
assert "Connection pool cannot have zero size" in node1.query_and_get_error(
|
|
"SELECT count() FROM mysql(mysql4)"
|
|
)
|
|
assert (
|
|
int(node1.query("SELECT count() FROM mysql(mysql4, connection_pool_size=1)"))
|
|
== 100
|
|
)
|
|
|
|
|
|
# Regression for (k, v) IN ((k, v))
|
|
def test_mysql_in(started_cluster):
|
|
table_name = "test_mysql_in"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE {table_name}
|
|
(
|
|
id UInt32,
|
|
name String,
|
|
age UInt32,
|
|
money UInt32
|
|
)
|
|
ENGINE = MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}')
|
|
"""
|
|
)
|
|
|
|
node1.query(
|
|
"INSERT INTO {} (id, name) SELECT number, concat('name_', toString(number)) from numbers(10) ".format(
|
|
table_name
|
|
)
|
|
)
|
|
node1.query("SELECT * FROM {} WHERE (id) IN (1)".format(table_name))
|
|
node1.query("SELECT * FROM {} WHERE (id) IN (1, 2)".format(table_name))
|
|
node1.query(
|
|
"SELECT * FROM {} WHERE (id, name) IN ((1, 'name_1'))".format(table_name)
|
|
)
|
|
node1.query(
|
|
"SELECT * FROM {} WHERE (id, name) IN ((1, 'name_1'),(1, 'name_1'))".format(
|
|
table_name
|
|
)
|
|
)
|
|
|
|
drop_mysql_table(conn, table_name)
|
|
conn.close()
|
|
|
|
|
|
def test_mysql_null(started_cluster):
|
|
table_name = "test_mysql_in"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
"""
|
|
CREATE TABLE `clickhouse`.`{}` (
|
|
`id` int(11) NOT NULL,
|
|
`money` int NULL default NULL,
|
|
PRIMARY KEY (`id`)) ENGINE=InnoDB;
|
|
""".format(
|
|
table_name
|
|
)
|
|
)
|
|
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE {table_name}
|
|
(
|
|
id UInt32,
|
|
money Nullable(UInt32)
|
|
)
|
|
ENGINE = MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}')
|
|
"""
|
|
)
|
|
|
|
node1.query(
|
|
"INSERT INTO {} (id, money) SELECT number, if(number%2, NULL, 1) from numbers(10) ".format(
|
|
table_name
|
|
)
|
|
)
|
|
|
|
assert (
|
|
int(
|
|
node1.query(
|
|
"SELECT count() FROM {} WHERE money IS NULL SETTINGS external_table_strict_query=1".format(
|
|
table_name
|
|
)
|
|
)
|
|
)
|
|
== 5
|
|
)
|
|
assert (
|
|
int(
|
|
node1.query(
|
|
"SELECT count() FROM {} WHERE money IS NOT NULL SETTINGS external_table_strict_query=1".format(
|
|
table_name
|
|
)
|
|
)
|
|
)
|
|
== 5
|
|
)
|
|
|
|
drop_mysql_table(conn, table_name)
|
|
conn.close()
|
|
|
|
|
|
def test_settings(started_cluster):
|
|
table_name = "test_settings"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
wait_timeout = 123
|
|
rw_timeout = 10123001
|
|
connect_timeout = 10123002
|
|
connection_pool_size = 1
|
|
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE {table_name}
|
|
(
|
|
id UInt32,
|
|
name String,
|
|
age UInt32,
|
|
money UInt32
|
|
)
|
|
ENGINE = MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}')
|
|
SETTINGS connection_wait_timeout={wait_timeout}, connect_timeout={connect_timeout}, read_write_timeout={rw_timeout}, connection_pool_size={connection_pool_size}
|
|
"""
|
|
)
|
|
|
|
node1.query(f"SELECT * FROM {table_name}")
|
|
assert node1.contains_in_log(
|
|
f"with settings: connect_timeout={connect_timeout}, read_write_timeout={rw_timeout}"
|
|
)
|
|
|
|
rw_timeout = 20123001
|
|
connect_timeout = 20123002
|
|
node1.query("SELECT * FROM mysql(mysql_with_settings, table='test_settings')")
|
|
assert node1.contains_in_log(
|
|
f"with settings: connect_timeout={connect_timeout}, read_write_timeout={rw_timeout}"
|
|
)
|
|
|
|
rw_timeout = 30123001
|
|
connect_timeout = 30123002
|
|
node1.query(
|
|
f"""
|
|
SELECT *
|
|
FROM mysql('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}',
|
|
SETTINGS
|
|
connection_wait_timeout={wait_timeout},
|
|
connect_timeout={connect_timeout},
|
|
read_write_timeout={rw_timeout},
|
|
connection_pool_size={connection_pool_size})
|
|
"""
|
|
)
|
|
assert node1.contains_in_log(
|
|
f"with settings: connect_timeout={connect_timeout}, read_write_timeout={rw_timeout}"
|
|
)
|
|
|
|
node1.query("DROP DATABASE IF EXISTS m")
|
|
node1.query("DROP DATABASE IF EXISTS mm")
|
|
|
|
rw_timeout = 40123001
|
|
connect_timeout = 40123002
|
|
node1.query(
|
|
f"""
|
|
CREATE DATABASE m
|
|
ENGINE = MySQL(mysql_with_settings, connection_wait_timeout={wait_timeout}, connect_timeout={connect_timeout}, read_write_timeout={rw_timeout}, connection_pool_size={connection_pool_size})
|
|
"""
|
|
)
|
|
assert node1.contains_in_log(
|
|
f"with settings: connect_timeout={connect_timeout}, read_write_timeout={rw_timeout}"
|
|
)
|
|
|
|
rw_timeout = 50123001
|
|
connect_timeout = 50123002
|
|
node1.query(
|
|
f"""
|
|
CREATE DATABASE mm ENGINE = MySQL('mysql80:3306', 'clickhouse', 'root', '{mysql_pass}')
|
|
SETTINGS
|
|
connection_wait_timeout={wait_timeout},
|
|
connect_timeout={connect_timeout},
|
|
read_write_timeout={rw_timeout},
|
|
connection_pool_size={connection_pool_size}
|
|
"""
|
|
)
|
|
assert node1.contains_in_log(
|
|
f"with settings: connect_timeout={connect_timeout}, read_write_timeout={rw_timeout}"
|
|
)
|
|
|
|
node1.query("DROP DATABASE m")
|
|
node1.query("DROP DATABASE mm")
|
|
|
|
drop_mysql_table(conn, table_name)
|
|
conn.close()
|
|
|
|
|
|
def test_enable_compression(started_cluster):
|
|
table_name = "test_enable_compression"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
node1.query("DROP NAMED COLLECTION IF EXISTS mysql_compression_creds")
|
|
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
f"INSERT INTO `clickhouse`.`{table_name}` (id, name, age, money) VALUES (1, 'name_1', 10, 20)"
|
|
)
|
|
conn.commit()
|
|
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE {table_name}
|
|
(
|
|
id UInt32,
|
|
name String,
|
|
age UInt32,
|
|
money UInt32
|
|
)
|
|
ENGINE = MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}')
|
|
SETTINGS enable_compression=1
|
|
"""
|
|
)
|
|
|
|
assert node1.query(f"SELECT * FROM {table_name} FORMAT TSV").strip() == "1\tname_1\t10\t20"
|
|
|
|
# Wire-level assertion for the MySQL table engine path (SETTINGS clause).
|
|
# Create a second engine table pointing at performance_schema.session_status to
|
|
# confirm that ClickHouse actually negotiates CLIENT_COMPRESS for that connection.
|
|
node1.query("DROP TABLE IF EXISTS compression_status_engine")
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE compression_status_engine
|
|
(
|
|
VARIABLE_NAME String,
|
|
VARIABLE_VALUE String
|
|
)
|
|
ENGINE = MySQL('mysql80:3306', 'performance_schema', 'session_status', 'root', '{mysql_pass}')
|
|
SETTINGS enable_compression = 1
|
|
"""
|
|
)
|
|
engine_compression_status = ""
|
|
for _ in range(10):
|
|
engine_compression_status = node1.query(
|
|
"SELECT VARIABLE_VALUE FROM compression_status_engine WHERE VARIABLE_NAME = 'Compression' FORMAT TSV"
|
|
).strip()
|
|
if engine_compression_status == "ON":
|
|
break
|
|
time.sleep(0.5)
|
|
node1.query("DROP TABLE IF EXISTS compression_status_engine")
|
|
assert engine_compression_status == "ON", (
|
|
f"Expected Compression=ON via MySQL engine SETTINGS path, got: {engine_compression_status!r}"
|
|
)
|
|
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
|
|
assert (
|
|
node1.query(
|
|
f"""
|
|
SELECT id, name, age, money
|
|
FROM mysql('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}',
|
|
SETTINGS enable_compression = 1)
|
|
FORMAT TSV
|
|
"""
|
|
).strip()
|
|
== "1\tname_1\t10\t20"
|
|
)
|
|
|
|
node1.query(
|
|
f"""
|
|
CREATE NAMED COLLECTION mysql_compression_creds AS
|
|
host = 'mysql80',
|
|
port = 3306,
|
|
database = 'clickhouse',
|
|
user = 'root',
|
|
password = '{mysql_pass}',
|
|
enable_compression = 1
|
|
"""
|
|
)
|
|
try:
|
|
assert (
|
|
node1.query(
|
|
f"SELECT id, name, age, money FROM mysql(mysql_compression_creds, table='{table_name}') FORMAT TSV"
|
|
).strip()
|
|
== "1\tname_1\t10\t20"
|
|
)
|
|
|
|
# Verify wire-level compression is actually negotiated via the named-collection path.
|
|
# Named collections use a different settings carrier than the literal SETTINGS clause,
|
|
# so this is a distinct code path. Override database+table to query performance_schema
|
|
# so MySQL reports the compression state of the ClickHouse-opened connection.
|
|
nc_compression_status = ""
|
|
for _ in range(10):
|
|
nc_compression_status = node1.query(
|
|
"""
|
|
SELECT VARIABLE_VALUE
|
|
FROM mysql(mysql_compression_creds, database='performance_schema', table='session_status')
|
|
WHERE VARIABLE_NAME = 'Compression'
|
|
FORMAT TSV
|
|
"""
|
|
).strip()
|
|
if nc_compression_status == "ON":
|
|
break
|
|
time.sleep(0.5)
|
|
|
|
assert nc_compression_status == "ON", (
|
|
f"Expected Compression=ON via named collection, got: {nc_compression_status!r}"
|
|
)
|
|
finally:
|
|
node1.query("DROP NAMED COLLECTION IF EXISTS mysql_compression_creds")
|
|
|
|
# Separately verify wire-level compression via the literal table-function path (SETTINGS clause).
|
|
# This is a distinct code path from the named-collection path above.
|
|
compression_status = ""
|
|
for _ in range(10):
|
|
compression_status = node1.query(
|
|
f"""
|
|
SELECT VARIABLE_VALUE
|
|
FROM mysql('mysql80:3306', 'performance_schema', 'session_status', 'root', '{mysql_pass}',
|
|
SETTINGS enable_compression = 1)
|
|
WHERE VARIABLE_NAME = 'Compression'
|
|
FORMAT TSV
|
|
"""
|
|
).strip()
|
|
if compression_status == "ON":
|
|
break
|
|
time.sleep(0.5)
|
|
|
|
assert compression_status == "ON", (
|
|
f"Expected MySQL compression to be ON, got: {compression_status!r}"
|
|
)
|
|
|
|
# Wire-level assertion for the MySQL database engine path.
|
|
# Create a DATABASE pointing at performance_schema, then read session_status through
|
|
# it to confirm CLIENT_COMPRESS is negotiated on the database-engine connection.
|
|
node1.query("DROP DATABASE IF EXISTS compression_test_db")
|
|
node1.query(
|
|
f"""
|
|
CREATE DATABASE compression_test_db
|
|
ENGINE = MySQL('mysql80:3306', 'performance_schema', 'root', '{mysql_pass}')
|
|
SETTINGS enable_compression = 1
|
|
"""
|
|
)
|
|
db_compression_status = ""
|
|
for _ in range(10):
|
|
db_compression_status = node1.query(
|
|
"SELECT VARIABLE_VALUE FROM compression_test_db.session_status WHERE VARIABLE_NAME = 'Compression' FORMAT TSV"
|
|
).strip()
|
|
if db_compression_status == "ON":
|
|
break
|
|
time.sleep(0.5)
|
|
node1.query("DROP DATABASE IF EXISTS compression_test_db")
|
|
assert db_compression_status == "ON", (
|
|
f"Expected Compression=ON via MySQL database engine path, got: {db_compression_status!r}"
|
|
)
|
|
|
|
drop_mysql_table(conn, table_name)
|
|
conn.close()
|
|
|
|
|
|
def test_mysql_point(started_cluster):
|
|
table_name = "test_mysql_point"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
f"""
|
|
CREATE TABLE `clickhouse`.`{table_name}` (
|
|
`id` int NOT NULL,
|
|
`point` Point NOT NULL,
|
|
PRIMARY KEY (`id`)) ENGINE=InnoDB;
|
|
"""
|
|
)
|
|
cursor.execute(
|
|
f"INSERT INTO `clickhouse`.`{table_name}` SELECT 1, Point(15, 20)"
|
|
)
|
|
assert 1 == cursor.execute(f"SELECT count(*) FROM `clickhouse`.`{table_name}`")
|
|
|
|
conn.commit()
|
|
|
|
result = node1.query(
|
|
f"DESCRIBE mysql('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}')"
|
|
)
|
|
assert result.strip() == "id\tInt32\t\t\t\t\t\npoint\tPoint"
|
|
|
|
assert 1 == int(
|
|
node1.query(
|
|
f"SELECT count() FROM mysql('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}')"
|
|
)
|
|
)
|
|
assert (
|
|
"(15,20)"
|
|
== node1.query(
|
|
f"SELECT point FROM mysql('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}')"
|
|
).strip()
|
|
)
|
|
|
|
node1.query("DROP TABLE IF EXISTS test")
|
|
node1.query(
|
|
f"CREATE TABLE test (id Int32, point Point) Engine=MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}')"
|
|
)
|
|
assert "(15,20)" == node1.query("SELECT point FROM test").strip()
|
|
|
|
drop_mysql_table(conn, table_name)
|
|
conn.close()
|
|
|
|
|
|
def test_mysql_geometry(started_cluster):
|
|
table_name = "test_mysql_geometry"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
f"""
|
|
CREATE TABLE `clickhouse`.`{table_name}` (
|
|
`id` int NOT NULL,
|
|
`ls` linestring NOT NULL,
|
|
`pg` polygon NOT NULL,
|
|
`mls` multilinestring NOT NULL,
|
|
`mpg` multipolygon NOT NULL,
|
|
`geo` geometry NOT NULL,
|
|
`geo_mp` geometry NOT NULL,
|
|
PRIMARY KEY (`id`)) ENGINE=InnoDB;
|
|
"""
|
|
)
|
|
cursor.execute(
|
|
f"""
|
|
INSERT INTO `clickhouse`.`{table_name}` SET
|
|
id = 1,
|
|
ls = ST_GeomFromText('LINESTRING(0 0, 1 1, 2 2)'),
|
|
pg = ST_GeomFromText('POLYGON((0 0, 4 0, 4 4, 0 4, 0 0))'),
|
|
mls = ST_GeomFromText('MULTILINESTRING((0 0, 1 1), (2 2, 3 3))'),
|
|
mpg = ST_GeomFromText('MULTIPOLYGON(((0 0, 2 0, 2 2, 0 2, 0 0)))'),
|
|
geo = ST_GeomFromText('LINESTRING(5 5, 6 6)'),
|
|
geo_mp = ST_GeomFromText('MULTIPOINT(0 0, 1 1)')
|
|
"""
|
|
)
|
|
assert 1 == cursor.execute(f"SELECT count(*) FROM `clickhouse`.`{table_name}`")
|
|
|
|
conn.commit()
|
|
|
|
table_function = (
|
|
f"mysql('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}')"
|
|
)
|
|
|
|
# The concrete spatial column types are mapped to the corresponding ClickHouse geometric types,
|
|
# because their subtype is fixed by the column type. The generic `geometry` column type can hold
|
|
# a value of any subtype, so it maps to the umbrella `Geometry` type (a `Variant` over the
|
|
# concrete geometric types). A value whose subtype has no ClickHouse counterpart (`MULTIPOINT`,
|
|
# `GEOMETRYCOLLECTION`) then throws at read time (checked below); this incompatibility is accepted
|
|
# in exchange for a proper geometric type.
|
|
assert (
|
|
node1.query(
|
|
"SELECT toTypeName(ls), toTypeName(pg), toTypeName(mls), toTypeName(mpg), toTypeName(geo), toTypeName(geo_mp) "
|
|
f"FROM {table_function} LIMIT 1"
|
|
).strip()
|
|
== "LineString\tPolygon\tMultiLineString\tMultiPolygon\tGeometry\tGeometry"
|
|
)
|
|
|
|
assert (
|
|
node1.query(f"SELECT ls FROM {table_function}").strip()
|
|
== "[(0,0),(1,1),(2,2)]"
|
|
)
|
|
assert (
|
|
node1.query(f"SELECT pg FROM {table_function}").strip()
|
|
== "[[(0,0),(4,0),(4,4),(0,4),(0,0)]]"
|
|
)
|
|
assert (
|
|
node1.query(f"SELECT mls FROM {table_function}").strip()
|
|
== "[[(0,0),(1,1)],[(2,2),(3,3)]]"
|
|
)
|
|
assert (
|
|
node1.query(f"SELECT mpg FROM {table_function}").strip()
|
|
== "[[[(0,0),(2,0),(2,2),(0,2),(0,0)]]]"
|
|
)
|
|
# A generic `geometry` column holding a representable subtype reads back as the geometric value:
|
|
# `geo` stores a LINESTRING, so it is read through the `Geometry` Variant's `LineString` alternative.
|
|
assert (
|
|
node1.query(f"SELECT geo FROM {table_function}").strip()
|
|
== "[(5,5),(6,6)]"
|
|
)
|
|
# Regression test for the accepted incompatibility: a generic `geometry` column holding a subtype
|
|
# with no ClickHouse counterpart (`MULTIPOINT` here) throws at read time, because `MULTIPOINT` is
|
|
# not representable by the `Geometry` Variant.
|
|
assert "Incorrect geometry type" in node1.query_and_get_error(
|
|
f"SELECT geo_mp FROM {table_function}"
|
|
)
|
|
|
|
# Regression test: the WKB parsing of spatial values read from MySQL honors the
|
|
# `max_wkb_geometry_elements` setting (which bounds point/ring/polygon counts before the parser
|
|
# reserves memory). `ls` is a LINESTRING of 3 points, so reading it with the limit set to 1 must
|
|
# be rejected instead of parsing an oversized element count. Before the fix the MySQL read path
|
|
# called `parseWKBFormat` with the default `0`, so only the hard-coded 100M cap applied.
|
|
assert "TOO_LARGE_ARRAY_SIZE" in node1.query_and_get_error(
|
|
f"SELECT ls FROM {table_function} SETTINGS max_wkb_geometry_elements = 1"
|
|
)
|
|
# A limit large enough for this value reads it back normally.
|
|
assert (
|
|
node1.query(
|
|
f"SELECT ls FROM {table_function} SETTINGS max_wkb_geometry_elements = 1000000"
|
|
).strip()
|
|
== "[(0,0),(1,1),(2,2)]"
|
|
)
|
|
|
|
# When the `geometry` mapping is disabled, the concrete spatial types (except `Point`) also fall
|
|
# back to String.
|
|
assert (
|
|
node1.query(
|
|
f"SELECT toTypeName(ls) FROM {table_function} LIMIT 1",
|
|
settings={"mysql_datatypes_support_level": "decimal,datetime64,date2Date32"},
|
|
).strip()
|
|
== "String"
|
|
)
|
|
|
|
# Regression test: disabling `geometry` through the table function's own `SETTINGS` must be
|
|
# honored during schema inference (the per-call opt-out), falling a concrete spatial type back to
|
|
# `String`. Before the fix the table function inferred the structure with the default engine
|
|
# settings, so both the per-call `SETTINGS` and a query-level value were silently ignored.
|
|
table_function_no_geo = (
|
|
f"mysql('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}', "
|
|
"SETTINGS mysql_datatypes_support_level = 'decimal,datetime64,date2Date32')"
|
|
)
|
|
assert (
|
|
node1.query(f"SELECT toTypeName(ls) FROM {table_function_no_geo} LIMIT 1").strip()
|
|
== "String"
|
|
)
|
|
|
|
# The per-call `SETTINGS` override the query-context value: here `geometry` is enabled through the
|
|
# function-local `SETTINGS` even though the query context disables it.
|
|
table_function_geo = (
|
|
f"mysql('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}', "
|
|
"SETTINGS mysql_datatypes_support_level = 'decimal,datetime64,date2Date32,geometry')"
|
|
)
|
|
assert (
|
|
node1.query(
|
|
f"SELECT toTypeName(ls) FROM {table_function_geo} LIMIT 1",
|
|
settings={"mysql_datatypes_support_level": "decimal,datetime64,date2Date32"},
|
|
).strip()
|
|
== "LineString"
|
|
)
|
|
|
|
# A user can still read a generic `geometry` column as a geometric value by declaring the column
|
|
# as `Geometry` explicitly through the MySQL table engine (when its values are representable).
|
|
node1.query("DROP TABLE IF EXISTS test_geometry")
|
|
node1.query(
|
|
"CREATE TABLE test_geometry (id Int32, ls LineString, pg Polygon, mls MultiLineString, mpg MultiPolygon, geo Geometry) "
|
|
f"Engine=MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}')"
|
|
)
|
|
assert node1.query("SELECT ls FROM test_geometry").strip() == "[(0,0),(1,1),(2,2)]"
|
|
assert node1.query("SELECT geo FROM test_geometry").strip() == "[(5,5),(6,6)]"
|
|
node1.query("DROP TABLE IF EXISTS test_geometry")
|
|
|
|
# The MySQL table engine infers the columns from MySQL when they are omitted. By default the
|
|
# `geometry` mapping is on, so a concrete spatial column is inferred as its geometric type.
|
|
node1.query("DROP TABLE IF EXISTS test_geometry_inferred")
|
|
node1.query(
|
|
f"CREATE TABLE test_geometry_inferred Engine=MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}')"
|
|
)
|
|
assert (
|
|
node1.query("SELECT toTypeName(ls) FROM test_geometry_inferred LIMIT 1").strip()
|
|
== "LineString"
|
|
)
|
|
node1.query("DROP TABLE IF EXISTS test_geometry_inferred")
|
|
|
|
# Regression test: disabling `geometry` through the table engine's own SETTINGS must be honored
|
|
# during schema inference, falling the concrete spatial types back to `String` (raw WKB). This is
|
|
# the per-engine opt-out; before the fix it was ignored because schema inference read the global
|
|
# query-context setting instead of the engine setting.
|
|
node1.query("DROP TABLE IF EXISTS test_geometry_no_geo")
|
|
node1.query(
|
|
f"CREATE TABLE test_geometry_no_geo Engine=MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}') "
|
|
"SETTINGS mysql_datatypes_support_level = 'decimal,datetime64,date2Date32'"
|
|
)
|
|
assert (
|
|
node1.query("SELECT toTypeName(ls) FROM test_geometry_no_geo LIMIT 1").strip()
|
|
== "String"
|
|
)
|
|
node1.query("DROP TABLE IF EXISTS test_geometry_no_geo")
|
|
|
|
# Regression test (query-backed inference): a MySQL source defined over a *query* rather than a
|
|
# table name infers its columns from the query result-set metadata, which reports every spatial
|
|
# value as the generic `MYSQL_TYPE_GEOMETRY` without exposing the concrete subtype. It must not be
|
|
# guessed as `Point` - that made reading a non-`Point` value such as this `LINESTRING` throw
|
|
# `Only Point data type is supported` at read time - so it falls back to the raw WKB `String`.
|
|
# Before the fix the query-backed path also dropped the effective `mysql_datatypes_support_level`
|
|
# entirely (it read the global query context), so the per-call/per-engine opt-out was ignored.
|
|
table_function_query = f"mysql('mysql80:3306', 'clickhouse', query('SELECT ls FROM {table_name}'), 'root', '{mysql_pass}')"
|
|
# The concrete subtype is unknowable from query result metadata, so it is inferred as `String` (raw
|
|
# WKB), never a geometric type such as `Point`/`LineString` (nullability follows the query metadata).
|
|
ls_query_type = node1.query(
|
|
f"SELECT toTypeName(ls) FROM {table_function_query} LIMIT 1"
|
|
).strip()
|
|
assert "String" in ls_query_type and "Point" not in ls_query_type
|
|
# The value reads back as raw WKB `String` without an exception (before the fix this threw).
|
|
assert len(node1.query(f"SELECT ls FROM {table_function_query}").strip()) > 0
|
|
|
|
# Regression test (named-collection precedence): a named collection carrying an explicit
|
|
# `mysql_datatypes_support_level` opt-out keeps precedence over a conflicting session
|
|
# `SET mysql_datatypes_support_level`. The `mysql_no_geo_types` collection disables `geometry`, so a
|
|
# `LINESTRING` column is inferred as `String` even though the session enables `geometry`, and the
|
|
# session value is not frozen into `SHOW CREATE`. Before the fix the query-context bridge overwrote
|
|
# the named-collection value with the session value (and persisted it into the table definition).
|
|
node1.query("DROP TABLE IF EXISTS test_geometry_nc")
|
|
node1.query(
|
|
"CREATE TABLE test_geometry_nc Engine=MySQL(mysql_no_geo_types)",
|
|
settings={
|
|
"mysql_datatypes_support_level": "decimal,datetime64,date2Date32,geometry"
|
|
},
|
|
)
|
|
assert (
|
|
node1.query("SELECT toTypeName(ls) FROM test_geometry_nc LIMIT 1").strip()
|
|
== "String"
|
|
)
|
|
assert "mysql_datatypes_support_level" not in node1.query(
|
|
"SHOW CREATE TABLE test_geometry_nc"
|
|
)
|
|
node1.query("DROP TABLE IF EXISTS test_geometry_nc")
|
|
|
|
drop_mysql_table(conn, table_name)
|
|
conn.close()
|
|
|
|
|
|
def test_joins(started_cluster):
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, "test_joins_mysql_users")
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
"CREATE TABLE clickhouse.test_joins_mysql_users (id INT NOT NULL, name varchar(50) NOT NULL, created TIMESTAMP, PRIMARY KEY (`id`)) ENGINE=InnoDB;"
|
|
)
|
|
cursor.execute(
|
|
"INSERT INTO clickhouse.test_joins_mysql_users VALUES (469722, '[email protected]', '2019-08-30 07:55:01')"
|
|
)
|
|
|
|
drop_mysql_table(conn, "test_joins_mysql_tickets")
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
"CREATE TABLE clickhouse.test_joins_mysql_tickets (id INT NOT NULL, subject varchar(50), created TIMESTAMP, creator INT NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;"
|
|
)
|
|
cursor.execute(
|
|
"INSERT INTO clickhouse.test_joins_mysql_tickets VALUES (281607, 'Feedback', '2024-06-25 12:09:41', 469722)"
|
|
)
|
|
|
|
conn.commit()
|
|
|
|
node1.query("DROP TABLE IF EXISTS test_joins_table_users")
|
|
node1.query("DROP TABLE IF EXISTS test_joins_table_tickets")
|
|
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE test_joins_table_users
|
|
(
|
|
`id` Int32,
|
|
`Name` String,
|
|
`Created` Nullable(DateTime)
|
|
)
|
|
ENGINE = MySQL('mysql80:3306', 'clickhouse', 'test_joins_mysql_users', 'root', '{mysql_pass}');
|
|
"""
|
|
)
|
|
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE test_joins_table_tickets
|
|
(
|
|
`id` Int32,
|
|
`Subject` Nullable(String),
|
|
`Created` Nullable(DateTime),
|
|
`Creator` Int32
|
|
)
|
|
ENGINE = MySQL('mysql80:3306', 'clickhouse', 'test_joins_mysql_tickets', 'root', '{mysql_pass}');
|
|
"""
|
|
)
|
|
|
|
node1.query(
|
|
"""
|
|
SELECT test_joins_table_tickets.id, Subject, test_joins_table_tickets.Created, Name
|
|
FROM test_joins_table_tickets
|
|
LEFT JOIN test_joins_table_users ON test_joins_table_tickets.Creator = test_joins_table_users.id
|
|
WHERE test_joins_table_tickets.Created = '2024-06-25 12:09:41'
|
|
"""
|
|
) == "281607\tFeedback\t2024-06-25 12:09:41\t[email protected]\n"
|
|
|
|
node1.query("DROP TABLE test_joins_table_users")
|
|
node1.query("DROP TABLE test_joins_table_tickets")
|
|
|
|
|
|
def test_mysql_ssl_auth(started_cluster):
|
|
conn = get_mysql_conn(started_cluster, started_cluster.mysql8_ip)
|
|
table_name = "test_table"
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
"""
|
|
CREATE USER 'ssl_user'@'{}' REQUIRE X509;
|
|
""".format(
|
|
node1.ip_address
|
|
)
|
|
)
|
|
cursor.execute(
|
|
"""
|
|
GRANT ALL PRIVILEGES ON *.* TO 'ssl_user'@'{}' WITH GRANT OPTION;
|
|
""".format(
|
|
node1.ip_address
|
|
)
|
|
)
|
|
|
|
node1.query(
|
|
"""
|
|
DROP TABLE IF EXISTS test_table;
|
|
CREATE TABLE test_table (id UInt32, name String, age UInt32, money UInt32) ENGINE MySQL(mysql_with_ssl);
|
|
SELECT * FROM test_table;
|
|
DROP TABLE test_table;
|
|
"""
|
|
)
|
|
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
"""
|
|
DROP USER 'ssl_user'@'{}';
|
|
""".format(
|
|
node1.ip_address
|
|
)
|
|
)
|
|
cursor.execute(
|
|
"""
|
|
FLUSH PRIVILEGES;
|
|
"""
|
|
)
|
|
|
|
|
|
def test_mysql_reading_clone(started_cluster):
|
|
table_name = "test_mysql_reading_clone"
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE {table_name}
|
|
(
|
|
id UInt32,
|
|
name String,
|
|
age UInt32,
|
|
money UInt32
|
|
)
|
|
ENGINE = MySQL('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}')
|
|
"""
|
|
)
|
|
|
|
node1.query(
|
|
"INSERT INTO {} (id, name) SELECT number, concat('name_', toString(number)) from numbers(10) ".format(
|
|
table_name
|
|
)
|
|
)
|
|
|
|
assert node1.query(f"SELECT count() FROM {table_name}").rstrip() == "10"
|
|
|
|
result = node1.query(f"SELECT count() FROM (SELECT (SELECT tx.id) = 1 as x FROM {table_name} AS tx) WHERE x SETTINGS correlated_subqueries_substitute_equivalent_expressions = 0")
|
|
|
|
assert result.rstrip() == "1"
|
|
|
|
drop_mysql_table(conn, table_name)
|
|
conn.close()
|
|
node1.query(f"DROP TABLE IF EXISTS {table_name}")
|
|
|
|
|
|
def test_query_passing_table_function(started_cluster):
|
|
table_name = "table_function_query"
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
create_mysql_table(conn, table_name)
|
|
|
|
# Populate via the plain (table-name) form of the table function.
|
|
plain = f"mysql('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}')"
|
|
node1.query(
|
|
f"INSERT INTO TABLE FUNCTION {plain} (id, name, money) "
|
|
"SELECT number, concat('name_', toString(number)), 3 FROM numbers(100)"
|
|
)
|
|
|
|
# query('...') form: the WHERE is executed on the MySQL side.
|
|
q_func = f"mysql('mysql80:3306', 'clickhouse', query('SELECT * FROM {table_name} WHERE id % 2 = 0'), 'root', '{mysql_pass}')"
|
|
assert node1.query(f"SELECT count() FROM {q_func}").rstrip() == "50"
|
|
assert node1.query(f"SELECT sum(money) FROM {q_func}").rstrip() == "150"
|
|
|
|
# subquery form.
|
|
q_subq = f"mysql('mysql80:3306', 'clickhouse', (SELECT id, name FROM {table_name} WHERE id < 10), 'root', '{mysql_pass}')"
|
|
assert node1.query(f"SELECT count() FROM {q_subq}").rstrip() == "10"
|
|
assert node1.query(f"SELECT name FROM {q_subq} ORDER BY id LIMIT 1").rstrip() == "name_0"
|
|
# Only the columns produced by the query are visible (id, name).
|
|
row = node1.query(f"SELECT * FROM {q_subq} ORDER BY id LIMIT 1").rstrip()
|
|
assert len(row.split("\t")) == 2
|
|
|
|
# The external_table_functions_use_nulls setting is honoured by the query-backed schema inference,
|
|
# exactly as it is by the table-name path: a nullable MySQL column is exposed as Nullable(T) by
|
|
# default and as plain T when the setting is disabled.
|
|
null_table = "table_function_query_null"
|
|
drop_mysql_table(conn, null_table)
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
f"CREATE TABLE clickhouse.{null_table} "
|
|
f"(id INT NOT NULL, money INT NULL DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB;"
|
|
)
|
|
cursor.execute(f"INSERT INTO clickhouse.{null_table} VALUES (1, 10), (2, NULL)")
|
|
conn.commit()
|
|
|
|
q_null = f"mysql('mysql80:3306', 'clickhouse', query('SELECT id, money FROM {null_table}'), 'root', '{mysql_pass}')"
|
|
assert (
|
|
node1.query(
|
|
f"SELECT toTypeName(money) FROM {q_null} LIMIT 1 SETTINGS external_table_functions_use_nulls = 1"
|
|
).rstrip()
|
|
== "Nullable(Int32)"
|
|
)
|
|
assert (
|
|
node1.query(
|
|
f"SELECT toTypeName(money) FROM {q_null} LIMIT 1 SETTINGS external_table_functions_use_nulls = 0"
|
|
).rstrip()
|
|
== "Int32"
|
|
)
|
|
drop_mysql_table(conn, null_table)
|
|
|
|
# external_table_strict_query: an outer filter that cannot be pushed down into the passed query is
|
|
# applied locally by default, but rejected with INCORRECT_QUERY under external_table_strict_query = 1.
|
|
q_strict = f"mysql('mysql80:3306', 'clickhouse', query('SELECT id, name FROM {table_name}'), 'root', '{mysql_pass}')"
|
|
assert node1.query(f"SELECT count() FROM {q_strict} WHERE id = 1").rstrip() == "1"
|
|
assert "INCORRECT_QUERY" in node1.query_and_get_error(
|
|
f"SELECT count() FROM {q_strict} WHERE id = 1 SETTINGS external_table_strict_query = 1"
|
|
)
|
|
|
|
# INSERT into a query-backed table function is rejected with INCORRECT_QUERY before the storage is
|
|
# constructed, so the remote schema-inference query is never run against MySQL.
|
|
assert "INCORRECT_QUERY" in node1.query_and_get_error(
|
|
f"INSERT INTO TABLE FUNCTION {q_strict} (id, name) VALUES (1, 'x')"
|
|
)
|
|
|
|
drop_mysql_table(conn, table_name)
|
|
conn.close()
|
|
|
|
|
|
def test_query_passing_engine(started_cluster):
|
|
table_name = "engine_query"
|
|
second_table = "engine_query_dim"
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
drop_mysql_table(conn, second_table)
|
|
create_mysql_table(conn, table_name)
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
f"CREATE TABLE clickhouse.{second_table} (id INT PRIMARY KEY, factor INT NOT NULL) ENGINE=InnoDB;"
|
|
)
|
|
conn.commit()
|
|
|
|
node1.query(
|
|
f"INSERT INTO TABLE FUNCTION mysql('mysql80:3306', 'clickhouse', '{table_name}', 'root', '{mysql_pass}') "
|
|
"(id, name, money) SELECT number, concat('name_', toString(number)), number FROM numbers(10)"
|
|
)
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(f"INSERT INTO clickhouse.{second_table} VALUES (1, 10), (2, 100)")
|
|
conn.commit()
|
|
|
|
node1.query("DROP TABLE IF EXISTS mysql_engine_query")
|
|
node1.query(
|
|
f"""
|
|
CREATE TABLE mysql_engine_query (id UInt32, name String, calculated Int64)
|
|
ENGINE = MySQL('mysql80:3306', 'clickhouse',
|
|
query('SELECT a.id, a.name, a.money * b.factor AS calculated
|
|
FROM {table_name} AS a JOIN {second_table} AS b ON a.id = b.id'),
|
|
'root', '{mysql_pass}')
|
|
"""
|
|
)
|
|
assert node1.query("SELECT count() FROM mysql_engine_query").rstrip() == "2"
|
|
assert (
|
|
node1.query("SELECT sum(calculated) FROM mysql_engine_query").rstrip()
|
|
== str(1 * 10 + 2 * 100)
|
|
)
|
|
|
|
# Writing into a query-backed table is not allowed.
|
|
assert "INCORRECT_QUERY" in node1.query_and_get_error(
|
|
"INSERT INTO mysql_engine_query VALUES (5, 'x', 0)"
|
|
)
|
|
|
|
node1.query("DROP TABLE mysql_engine_query")
|
|
drop_mysql_table(conn, table_name)
|
|
drop_mysql_table(conn, second_table)
|
|
conn.close()
|
|
|
|
|
|
def test_query_passing_type_mismatch(started_cluster):
|
|
# A declared structure whose types disagree with the passed query must surface as a query error, never
|
|
# abort the server.
|
|
table_name = "query_passing_mismatch"
|
|
conn = get_mysql_conn(started_cluster, cluster.mysql8_ip)
|
|
drop_mysql_table(conn, table_name)
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
f"CREATE TABLE clickhouse.{table_name} (a INT NOT NULL, b VARCHAR(50) NOT NULL, PRIMARY KEY (a)) ENGINE=InnoDB;"
|
|
)
|
|
cursor.execute(
|
|
f"INSERT INTO clickhouse.{table_name} VALUES (1, 'name_1'), (2, 'name_2')"
|
|
)
|
|
conn.commit()
|
|
|
|
# Column b holds text but is declared Int32. MySQL's typed accessor reports a query error instead of
|
|
# crashing.
|
|
node1.query("DROP TABLE IF EXISTS mysql_type_mismatch")
|
|
node1.query(
|
|
f"CREATE TABLE mysql_type_mismatch (a Int32, b Int32) "
|
|
f"ENGINE = MySQL('mysql80:3306', 'clickhouse', query('SELECT a, b FROM {table_name}'), 'root', '{mysql_pass}')"
|
|
)
|
|
assert node1.query_and_get_error("SELECT * FROM mysql_type_mismatch") != ""
|
|
node1.query("DROP TABLE mysql_type_mismatch")
|
|
|
|
drop_mysql_table(conn, table_name)
|
|
conn.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
with contextmanager(started_cluster)() as cluster:
|
|
for name, instance in list(cluster.instances.items()):
|
|
print(name, instance.ip_address)
|
|
input("Cluster created, press any key to destroy...")
|