mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
Follow-up to #105171, which added `ruff` with a deliberately narrow, bug-class-only rule set and left the unused-name and star-import rules off pending a dedicated cleanup pass. This enables that second batch in `pyproject.toml` and fixes every violation across the tree (the bulk in integration tests): - `F401` unused-import and `F841` unused-variable: removed, preserving any side-effecting right-hand side (e.g. `result = node.query(...)` becomes `node.query(...)`). Two `try: import ...` feature probes are kept with `# noqa: F401`. - `F541` f-string-missing-placeholders: dropped the redundant `f` prefix. - `F403`/`F405` star imports: every `from module import *` is expanded into explicit imports of the names actually used, taken from each name's real origin, so stdlib/third-party names import from their real source rather than through a test "convenience bundle". The now-empty `tests/integration/helpers/kafka/common_direct.py` bundle is removed and its three remaining consumers import `_VarintBytes`/`KafkaAdminClient` directly. Eliminating the star imports surfaced a real bug the wildcard had masked from `F821`: `generate_presets.py` in `test_prometheus_protocols` called an undefined `execute_instant_query_with_http_api`; fixed to the existing `execute_query_via_http_api`. No `black`/`isort` reformatting was applied (neither is enforced in the style check), so the diff is limited to the lint fixes themselves. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
581 lines
18 KiB
Python
581 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import datetime
|
|
import decimal
|
|
import logging
|
|
import os
|
|
import random
|
|
import uuid
|
|
from io import StringIO
|
|
|
|
import psycopg
|
|
import psycopg2 as py_psql
|
|
import psycopg2.extras
|
|
import pytest
|
|
|
|
from helpers.cluster import ClickHouseCluster, get_docker_compose_path
|
|
|
|
psycopg2.extras.register_uuid()
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
DOCKER_COMPOSE_PATH = get_docker_compose_path()
|
|
|
|
cluster = ClickHouseCluster(__file__)
|
|
cluster.add_instance(
|
|
"node",
|
|
main_configs=[
|
|
"configs/postgresql.xml",
|
|
"configs/log.xml",
|
|
"configs/ssl_conf.xml",
|
|
"configs/dhparam.pem",
|
|
"configs/server.crt",
|
|
"configs/server.key",
|
|
],
|
|
user_configs=[
|
|
"configs/default_passwd.xml",
|
|
"configs/sync_inserts.xml"
|
|
],
|
|
with_postgres=True,
|
|
with_postgresql_java_client=True,
|
|
with_postgresql_dotnet_client=True,
|
|
)
|
|
|
|
cluster.add_instance(
|
|
"node_secure",
|
|
main_configs=[
|
|
"configs/postgresql_secure.xml",
|
|
"configs/log.xml",
|
|
"configs/ssl_conf.xml",
|
|
"configs/dhparam.pem",
|
|
"configs/server.crt",
|
|
"configs/server.key",
|
|
],
|
|
user_configs=[
|
|
"configs/default_passwd.xml",
|
|
"configs/sync_inserts.xml"
|
|
],
|
|
with_postgres=True,
|
|
with_postgresql_java_client=True,
|
|
)
|
|
|
|
server_port = 5433
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def started_cluster():
|
|
try:
|
|
cluster.start()
|
|
# Wait for the PostgreSQL handler to start.
|
|
# Cluster.start waits until port 9000 becomes accessible.
|
|
# Server opens the PostgreSQL compatibility port a bit later.
|
|
cluster.instances["node"].wait_for_log_line("PostgreSQL compatibility protocol")
|
|
yield cluster
|
|
except Exception as ex:
|
|
logging.exception(ex)
|
|
raise ex
|
|
finally:
|
|
cluster.shutdown()
|
|
|
|
|
|
def test_psql_client(started_cluster):
|
|
node = cluster.instances["node"]
|
|
|
|
for query_file in [
|
|
"query1.sql",
|
|
"query2.sql",
|
|
"query3.sql",
|
|
"query4.sql",
|
|
"query5.sql",
|
|
"query6.sql",
|
|
"query7.sql",
|
|
]:
|
|
started_cluster.copy_file_to_container(
|
|
started_cluster.postgres_id,
|
|
os.path.join(SCRIPT_DIR, "queries", query_file),
|
|
f"/{query_file}",
|
|
)
|
|
cmd_prefix = [
|
|
"/usr/bin/psql",
|
|
f"sslmode=require host={node.hostname} port={server_port} user=user_with_sha256 dbname=default password=abacaba",
|
|
]
|
|
# -F same as --field-separator
|
|
cmd_prefix += ["--no-align", "-F", " "]
|
|
|
|
res = started_cluster.exec_in_container(
|
|
started_cluster.postgres_id, cmd_prefix + ["-f", "/query1.sql"], shell=True
|
|
)
|
|
logging.debug(res)
|
|
assert res == "\n".join(["a", "1", "(1 row)", ""])
|
|
|
|
res = started_cluster.exec_in_container(
|
|
started_cluster.postgres_id, cmd_prefix + ["-f", "/query2.sql"], shell=True
|
|
)
|
|
logging.debug(res)
|
|
assert res == "\n".join(["a", "колонка", "(1 row)", ""])
|
|
|
|
res = started_cluster.exec_in_container(
|
|
started_cluster.postgres_id, cmd_prefix + ["-f", "/query3.sql"], shell=True
|
|
)
|
|
logging.debug(res)
|
|
assert res == "\n".join(
|
|
[
|
|
"CREATE DATABASE",
|
|
"USE",
|
|
"CREATE TABLE",
|
|
"INSERT 0 3",
|
|
"INSERT 0 3",
|
|
"column",
|
|
"0",
|
|
"0",
|
|
"1",
|
|
"1",
|
|
"5",
|
|
"5",
|
|
"(6 rows)",
|
|
"DROP DATABASE\n",
|
|
]
|
|
)
|
|
|
|
res = started_cluster.exec_in_container(
|
|
started_cluster.postgres_id, cmd_prefix + ["-f", "/query4.sql"], shell=True
|
|
)
|
|
logging.debug(res)
|
|
assert res == "\n".join(
|
|
["CREATE TABLE", "INSERT 0 2", "tmp_column", "0", "1", "(2 rows)", "DROP TABLE\n"]
|
|
)
|
|
|
|
res = started_cluster.exec_in_container(
|
|
started_cluster.postgres_id, cmd_prefix + ["-f", "/query5.sql"], shell=True
|
|
)
|
|
logging.debug(res)
|
|
assert res == "\n".join(
|
|
[
|
|
"CREATE DATABASE",
|
|
"USE",
|
|
"CREATE TABLE",
|
|
"INSERT 0 3",
|
|
"CREATE TABLE",
|
|
"INSERT 0 3",
|
|
"DROP DATABASE\n",
|
|
]
|
|
)
|
|
|
|
res = started_cluster.exec_in_container(
|
|
started_cluster.postgres_id, cmd_prefix + ["-f", "/query6.sql"], shell=True
|
|
)
|
|
logging.debug(res)
|
|
# PostgreSQL should return boolean values as 't' or 'f'
|
|
assert res == "\n".join(
|
|
["bool_true bool_false", "t f", "(1 row)", ""]
|
|
)
|
|
|
|
res = started_cluster.exec_in_container(
|
|
started_cluster.postgres_id, cmd_prefix + ["-f", "/query7.sql"], shell=True
|
|
)
|
|
logging.debug(res)
|
|
# Test all DDL command tags
|
|
assert res == "\n".join(
|
|
[
|
|
"CREATE DATABASE",
|
|
"USE",
|
|
"CREATE TABLE",
|
|
"CREATE TABLE",
|
|
"ALTER TABLE",
|
|
"INSERT 0 3",
|
|
"id name age",
|
|
"1 Alice 25",
|
|
"2 Bob 30",
|
|
"3 Charlie 35",
|
|
"(3 rows)",
|
|
"SET",
|
|
"TRUNCATE",
|
|
"DROP TABLE",
|
|
"DROP TABLE",
|
|
"DROP DATABASE\n",
|
|
]
|
|
)
|
|
|
|
def test_psql_client_secure(started_cluster):
|
|
node = cluster.instances["node_secure"]
|
|
|
|
started_cluster.copy_file_to_container(
|
|
started_cluster.postgres_id,
|
|
os.path.join(SCRIPT_DIR, "queries", "query1.sql"),
|
|
"/query1.sql",
|
|
)
|
|
|
|
cmd_prefix = [
|
|
"/usr/bin/psql",
|
|
f"sslmode=require host={node.hostname} port={server_port} user=user_with_sha256 dbname=default password=abacaba",
|
|
]
|
|
# -F same as --field-separator
|
|
cmd_prefix += ["--no-align", "-F", " "]
|
|
|
|
res = started_cluster.exec_in_container(
|
|
started_cluster.postgres_id, cmd_prefix + ["-f", "/query1.sql"], shell=True
|
|
)
|
|
logging.debug(res)
|
|
assert res == "\n".join(["a", "1", "(1 row)", ""])
|
|
|
|
|
|
postgres_container = started_cluster.get_docker_handle(started_cluster.postgres_id);
|
|
|
|
cmd_prefix = [
|
|
"/usr/bin/psql",
|
|
f"sslmode=disable host={node.hostname} port={server_port} user=user_with_sha256 dbname=default password=abacaba",
|
|
]
|
|
# -F same as --field-separator
|
|
cmd_prefix += ["--no-align", "-F", " "]
|
|
|
|
code, (stdout, stderr) = postgres_container.exec_run(cmd_prefix + ["-f", "/query1.sql"], demux=True,)
|
|
logging.debug(f"test_psql_client_secure code:{code} stdout:{stdout}, stderr:{stderr}")
|
|
assert (
|
|
"ERROR: SSL connection required.\n"
|
|
in stderr.decode()
|
|
)
|
|
|
|
assert node.contains_in_log(
|
|
"<Error> PostgreSQLHandler: DB::Exception: SSL connection required."
|
|
)
|
|
|
|
|
|
def test_new_user(started_cluster):
|
|
node = cluster.instances["node"]
|
|
|
|
db_id = f"x_{random.randint(0, 1000000)}"
|
|
|
|
ch = py_psql.connect(
|
|
host=node.ip_address,
|
|
port=server_port,
|
|
user="default",
|
|
password="123",
|
|
database="",
|
|
)
|
|
cur = ch.cursor()
|
|
cur.execute(f"CREATE DATABASE {db_id}")
|
|
cur.execute(f"USE {db_id}")
|
|
cur.execute("CREATE USER IF NOT EXISTS name7 IDENTIFIED WITH scram_sha256_password BY 'my_password'")
|
|
|
|
ch = py_psql.connect(
|
|
host=node.ip_address,
|
|
port=server_port,
|
|
user="name7",
|
|
password="my_password",
|
|
database=db_id,
|
|
)
|
|
cur = ch.cursor()
|
|
cur.execute("select 1;")
|
|
assert cur.fetchall() == [(1,)]
|
|
|
|
ch = py_psql.connect(
|
|
host=node.ip_address,
|
|
port=server_port,
|
|
user="default",
|
|
password="123",
|
|
database="",
|
|
)
|
|
cur = ch.cursor()
|
|
cur.execute(f"DROP DATABASE {db_id}")
|
|
|
|
|
|
def test_python_client(started_cluster):
|
|
node = cluster.instances["node"]
|
|
|
|
with pytest.raises(py_psql.OperationalError) as exc_info:
|
|
ch = py_psql.connect(
|
|
host=node.ip_address,
|
|
port=server_port,
|
|
user="default",
|
|
password="123",
|
|
database="",
|
|
)
|
|
cur = ch.cursor()
|
|
cur.execute("select name from tables;")
|
|
|
|
assert exc_info.value.args == ("SSL connection has been closed unexpectedly\n",)
|
|
|
|
ch = py_psql.connect(
|
|
host=node.ip_address,
|
|
port=server_port,
|
|
user="default",
|
|
password="123",
|
|
database="",
|
|
)
|
|
cur = ch.cursor()
|
|
|
|
cur.execute("select 1 as a, 2 as b")
|
|
assert (cur.description[0].name, cur.description[1].name) == ("a", "b")
|
|
assert cur.fetchall() == [(1, 2)]
|
|
|
|
cur.execute("CREATE DATABASE x")
|
|
cur.execute("USE x")
|
|
cur.execute(
|
|
"CREATE TEMPORARY TABLE tmp2 (ch Int8, i64 Int64, f64 Float64, str String, date Date, dec Decimal(19, 10), uuid UUID) ENGINE = Memory"
|
|
)
|
|
cur.execute(
|
|
"insert into tmp2 (ch, i64, f64, str, date, dec, uuid) values (44, 534324234, 0.32423423, 'hello', '2019-01-23', 0.333333, '61f0c404-5cb3-11e7-907b-a6006ad3dba0')"
|
|
)
|
|
cur.execute("select * from tmp2")
|
|
assert cur.fetchall()[0] == (
|
|
"44",
|
|
534324234,
|
|
0.32423423,
|
|
"hello",
|
|
datetime.date(2019, 1, 23),
|
|
decimal.Decimal("0.3333330000"),
|
|
uuid.UUID("61f0c404-5cb3-11e7-907b-a6006ad3dba0"),
|
|
)
|
|
cur.execute("DROP DATABASE x")
|
|
|
|
|
|
def test_prepared_statement(started_cluster):
|
|
node = started_cluster.instances["node"]
|
|
|
|
ch = psycopg.connect(
|
|
host=node.ip_address,
|
|
port=server_port,
|
|
user="default",
|
|
password="123",
|
|
)
|
|
cur = ch.cursor()
|
|
cur.execute("drop table if exists test;")
|
|
|
|
cur.execute(
|
|
"""CREATE TABLE test(
|
|
id INT
|
|
) ENGINE = Memory;"""
|
|
)
|
|
|
|
cur.execute("INSERT INTO test (id) VALUES (1), (2), (3);")
|
|
|
|
cur.execute("SELECT * FROM test WHERE id > %s;", ('2',), prepare=True)
|
|
assert cur.fetchall() == [(3,)]
|
|
|
|
cur.execute("PREPARE select_test AS SELECT * FROM test WHERE id = $1;")
|
|
cur.execute("EXECUTE select_test(1);")
|
|
assert cur.fetchall() == [(1,)]
|
|
|
|
cur.execute("DEALLOCATE select_test;")
|
|
with pytest.raises(Exception):
|
|
cur.execute("EXECUTE select_test(1);")
|
|
|
|
|
|
def test_copy_command(started_cluster):
|
|
node = cluster.instances["node"]
|
|
|
|
ch = py_psql.connect(
|
|
host=node.ip_address,
|
|
port=server_port,
|
|
user="default",
|
|
password="123",
|
|
database="",
|
|
)
|
|
cur = ch.cursor()
|
|
file_index = random.randint(0, 100000000)
|
|
|
|
cur.execute("CREATE DATABASE copy_x")
|
|
cur.execute("USE copy_x")
|
|
|
|
cur.execute("drop table if exists test;")
|
|
cur.execute("drop table if exists test_recreated;")
|
|
|
|
# test copy to -> copy from cycle for simple table
|
|
cur.execute("create table test (x UInt32) engine=Memory();")
|
|
cur.execute("insert into test values (42),(43),(44),(45);")
|
|
cur.execute("select * from test order by x;")
|
|
assert cur.fetchall() == [(42,), (43,), (44,), (45,)]
|
|
|
|
with open(f"out_{file_index}.tsv", "w") as f:
|
|
cur.copy_to(file=f, table="test")
|
|
with open(f"out_{file_index}.tsv", "r") as f:
|
|
assert f.read() == "42\n43\n44\n45\n"
|
|
|
|
cur.execute("create table test_recreated (x UInt32) engine=Memory();")
|
|
data_to_copy = "1\n2\n3\n4\n5\n"
|
|
cur.copy_from(StringIO(data_to_copy), "test_recreated", columns=("x",))
|
|
cur.execute("select * from test_recreated order by x;")
|
|
|
|
assert cur.fetchall() == [(1,), (2,), (3,), (4,), (5,)]
|
|
|
|
cur.execute("drop table if exists test;")
|
|
cur.execute("drop table if exists test_recreated;")
|
|
|
|
# test copy to -> copy from cycle for complex table
|
|
cur.execute("create table test (x UInt32, y String) engine=Memory();")
|
|
cur.execute("insert into test values (42,'a'),(43,'b'),(44,'c'),(45,'d');")
|
|
cur.execute("select * from test order by x;")
|
|
|
|
assert cur.fetchall() == [(42, "a"), (43, "b"), (44, "c"), (45, "d")]
|
|
|
|
with open(f"out_{file_index + 1}.tsv", "w") as f:
|
|
cur.copy_to(file=f, table="test")
|
|
with open(f"out_{file_index + 1}.tsv", "r") as f:
|
|
assert f.read() == '42\ta\n43\tb\n44\tc\n45\td\n'
|
|
|
|
cur.execute("create table test_recreated (x UInt32, y String) engine=Memory();")
|
|
data_to_copy = "1\ta\n2\tb\n3\tc\n"
|
|
cur.copy_from(StringIO(data_to_copy), "test_recreated", columns=("x","y"))
|
|
cur.execute("select * from test_recreated order by x;")
|
|
|
|
assert cur.fetchall() == [(1, "a"), (2, "b"), (3, "c")]
|
|
cur.execute("DROP DATABASE copy_x")
|
|
|
|
|
|
def test_boolean_type(started_cluster):
|
|
node = cluster.instances["node"]
|
|
|
|
ch = py_psql.connect(
|
|
host=node.ip_address,
|
|
port=server_port,
|
|
user="default",
|
|
password="123",
|
|
database="",
|
|
)
|
|
cur = ch.cursor()
|
|
|
|
# Test boolean literals
|
|
# PostgreSQL protocol MUST return boolean values as 't' or 'f' in text format
|
|
# psycopg2 will automatically convert 't'/'f' to Python True/False
|
|
# If server sends '1'/'0' or 'true'/'false', psycopg2 will NOT convert them to bool
|
|
cur.execute("SELECT true AS bool_true, false AS bool_false")
|
|
result = cur.fetchone()
|
|
logging.debug(f"Boolean literals result: {result}, types: {type(result[0])}, {type(result[1])}")
|
|
# psycopg2 should convert 't'/'f' to True/False automatically
|
|
# If we get strings or numbers, it means the server didn't send proper PostgreSQL boolean format
|
|
assert result == (True, False), \
|
|
f"Expected (True, False) from psycopg2 conversion of 't'/'f', but got {result} with types {type(result[0])}, {type(result[1])}"
|
|
|
|
# Test with table
|
|
cur.execute("CREATE DATABASE test_bool_db")
|
|
cur.execute("USE test_bool_db")
|
|
cur.execute("CREATE TEMPORARY TABLE bool_test (id Int32, flag Bool) ENGINE = Memory")
|
|
cur.execute("INSERT INTO bool_test VALUES (1, true), (2, false)")
|
|
cur.execute("SELECT id, flag FROM bool_test ORDER BY id")
|
|
results = cur.fetchall()
|
|
logging.debug(f"Table boolean results: {results}")
|
|
assert len(results) == 2
|
|
# Strict check for boolean values from table
|
|
assert results[0][1] is True, \
|
|
f"Expected True (psycopg2 conversion of 't'), but got {results[0][1]} with type {type(results[0][1])}"
|
|
assert results[1][1] is False, \
|
|
f"Expected False (psycopg2 conversion of 'f'), but got {results[1][1]} with type {type(results[1][1])}"
|
|
|
|
cur.execute("DROP TABLE bool_test")
|
|
cur.execute("DROP DATABASE test_bool_db")
|
|
cur.close()
|
|
ch.close()
|
|
|
|
|
|
def test_java_client(started_cluster):
|
|
node = cluster.instances["node"]
|
|
|
|
with open(os.path.join(SCRIPT_DIR, "java.reference")) as fp:
|
|
reference = fp.read()
|
|
|
|
# database not exists exception.
|
|
with pytest.raises(Exception) as exc:
|
|
res = started_cluster.exec_in_container(
|
|
started_cluster.postgresql_java_client_docker_id,
|
|
[
|
|
"bash",
|
|
"-c",
|
|
f"java JavaConnectorTest --host {node.hostname} --port {server_port} --user default --database abc",
|
|
],
|
|
)
|
|
assert (
|
|
"org.postgresql.util.PSQLException: ERROR: Invalid user or password"
|
|
in str(exc.value)
|
|
)
|
|
|
|
# non-empty password passed.
|
|
res = started_cluster.exec_in_container(
|
|
started_cluster.postgresql_java_client_docker_id,
|
|
[
|
|
"bash",
|
|
"-c",
|
|
f"java JavaConnectorTest --host {node.hostname} --port {server_port} --user default --password 123 --database default",
|
|
],
|
|
)
|
|
assert res == reference
|
|
|
|
|
|
def test_dotnet_client(started_cluster):
|
|
node = cluster.instances["node"]
|
|
|
|
with open(os.path.join(SCRIPT_DIR, "dotnet.reference")) as fp:
|
|
reference = fp.read()
|
|
|
|
res = started_cluster.exec_in_container(
|
|
started_cluster.postgresql_dotnet_client_docker_id,
|
|
[
|
|
"bash",
|
|
"-c",
|
|
f"cd /pg_testapp && dotnet run -- --host {node.hostname} --port {server_port} --username default --password 123",
|
|
],
|
|
)
|
|
# `dotnet run` builds first, so the .NET SDK can prepend build diagnostics to
|
|
# stdout. That noise only appears before the client output, so tolerate it
|
|
# with a directional suffix check while still catching any trailing or
|
|
# inserted protocol divergence.
|
|
assert res.endswith(reference)
|
|
|
|
|
|
def test_restricted_user_cannot_bypass_grants(started_cluster):
|
|
"""Verify that a user with limited grants can connect via PostgreSQL protocol
|
|
(pg_type and other system views are initialized internally), but cannot
|
|
perform operations beyond their granted privileges."""
|
|
node = started_cluster.instances["node"]
|
|
|
|
# Create a restricted user that can only SELECT from default database
|
|
ch = psycopg.connect(
|
|
host=node.ip_address,
|
|
port=server_port,
|
|
user="default",
|
|
password="123",
|
|
)
|
|
cur = ch.cursor()
|
|
cur.execute(
|
|
"CREATE USER IF NOT EXISTS pg_restricted IDENTIFIED WITH plaintext_password BY 'restricted123'"
|
|
)
|
|
cur.execute("GRANT SELECT ON default.* TO pg_restricted")
|
|
ch.close()
|
|
|
|
# Connect as the restricted user - should succeed
|
|
restricted = psycopg.connect(
|
|
host=node.ip_address,
|
|
port=server_port,
|
|
user="pg_restricted",
|
|
password="restricted123",
|
|
dbname="default",
|
|
)
|
|
cur = restricted.cursor()
|
|
|
|
# The internal pg_type view should be accessible.
|
|
# ClickHouse currently sends scalar values over the PostgreSQL protocol in
|
|
# text mode, so result[0] arrives as a string from psycopg.
|
|
cur.execute("SELECT count() FROM pg_type")
|
|
result = cur.fetchone()
|
|
assert int(result[0]) > 0
|
|
|
|
# SELECT should work
|
|
cur.execute("SELECT 1")
|
|
assert int(cur.fetchone()[0]) == 1
|
|
|
|
# CREATE TABLE should be denied
|
|
with pytest.raises(Exception) as exc:
|
|
cur.execute("CREATE TABLE default.test_restricted (id Int32) ENGINE = Memory")
|
|
assert "Not enough privileges" in str(exc.value)
|
|
|
|
restricted.close()
|
|
|
|
# Clean up
|
|
ch = psycopg.connect(
|
|
host=node.ip_address,
|
|
port=server_port,
|
|
user="default",
|
|
password="123",
|
|
)
|
|
cur = ch.cursor()
|
|
cur.execute("DROP USER IF EXISTS pg_restricted")
|
|
ch.close()
|