mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
4 new test directories (generic settings-profile constraints, table data-drop size guards, startup-script dictionary creation, and Delta Lake concurrent shuffle reads) plus AWS STS assume-role coverage added to the existing test_storage_delta and test_storage_s3_queue suites, ported from ClickHouse's private/Cloud fork. None of these depend on SharedMergeTree, Shared Catalog, or any other Cloud-only feature. test_storage_s3_queue/test_sts_smoke.py needed a small, generic extension to the create_table() helper in helpers/s3_queue_common.py to pass through an extra_credentials() clause and support schema-less CREATE TABLE (empty format string) - matches the private fork's helper exactly. Every test here was run against a real local build via `python -m ci.praktika run "integration" --test ...` and confirmed passing before being added. A larger initial candidate list (17 directories, including test_packed_io, test_cache_warmer, test_prewarm_cache_merges, and additions to test_check_table, test_database_glue, test_default_compression_codec, test_replicated_database, and test_s3_cluster) was pared down after actually running them surfaced either Cloud-only dependencies invisible to code review, or real unexplained behavioral differences (S3 request counts, CHECK TABLE corruption detection on packed parts, a disks-CLI packed-io command mismatch) that need further investigation before they can be safely ported. Co-Authored-By: Claude Sonnet 5 <[email protected]>
317 lines
10 KiB
Python
317 lines
10 KiB
Python
import io
|
|
import logging
|
|
import random
|
|
import string
|
|
from minio.deleteobjects import DeleteObject
|
|
from helpers.config_cluster import minio_secret_key
|
|
|
|
DEFAULT_AUTH = ["'minio'", f"'{minio_secret_key}'"]
|
|
NO_AUTH = ["NOSIGN"]
|
|
|
|
|
|
def run_query(instance, query, stdin=None, settings=None):
|
|
# type: (ClickHouseInstance, str, object, dict) -> str
|
|
|
|
logging.info("Running query '{}'...".format(query))
|
|
result = instance.query(query, stdin=stdin, settings=settings)
|
|
logging.info("Query finished")
|
|
|
|
return result
|
|
|
|
|
|
def random_str(length=6):
|
|
alphabet = string.ascii_lowercase + string.digits
|
|
return "".join(random.SystemRandom().choice(alphabet) for _ in range(length))
|
|
|
|
|
|
def generate_random_files(
|
|
started_cluster,
|
|
files_path,
|
|
count,
|
|
storage="s3",
|
|
column_num=3,
|
|
row_num=10,
|
|
start_ind=0,
|
|
bucket=None,
|
|
use_prefix=None,
|
|
use_random_names=False,
|
|
files=None,
|
|
):
|
|
if files is not None:
|
|
pass
|
|
elif use_random_names:
|
|
files = [
|
|
(f"{files_path}/{random_str(10)}.csv", i)
|
|
for i in range(start_ind, start_ind + count)
|
|
]
|
|
elif use_prefix is not None:
|
|
files = [
|
|
(f"{files_path}/{use_prefix}_{i}.csv", i)
|
|
for i in range(start_ind, start_ind + count)
|
|
]
|
|
else:
|
|
files = [
|
|
(f"{files_path}/test_{i}.csv", i)
|
|
for i in range(start_ind, start_ind + count)
|
|
]
|
|
files.sort(key=lambda x: x[0])
|
|
|
|
print(f"Generating files: {files}")
|
|
|
|
total_values = []
|
|
for filename, i in files:
|
|
rand_values = [
|
|
[random.randint(0, 1000) for _ in range(column_num)] for _ in range(row_num)
|
|
]
|
|
total_values += rand_values
|
|
values_csv = (
|
|
"\n".join((",".join(map(str, row)) for row in rand_values)) + "\n"
|
|
).encode()
|
|
if storage == "s3":
|
|
put_s3_file_content(started_cluster, filename, values_csv, bucket)
|
|
else:
|
|
put_azure_file_content(started_cluster, filename, values_csv, bucket)
|
|
return total_values
|
|
|
|
|
|
def put_s3_file_content(started_cluster, filename, data, bucket=None):
|
|
bucket = started_cluster.minio_bucket if bucket is None else bucket
|
|
buf = io.BytesIO(data)
|
|
started_cluster.minio_client.put_object(bucket, filename, buf, len(data))
|
|
|
|
|
|
def put_azure_file_content(started_cluster, filename, data, bucket=None):
|
|
client = started_cluster.blob_service_client.get_blob_client(
|
|
started_cluster.azurite_container, filename
|
|
)
|
|
buf = io.BytesIO(data)
|
|
client.upload_blob(buf, "BlockBlob", len(data))
|
|
|
|
|
|
def count_minio_objects(started_cluster, bucket_name, prefix):
|
|
minio = started_cluster.minio_client
|
|
objects = list(minio.list_objects(
|
|
bucket_name,
|
|
prefix=prefix,
|
|
recursive=True))
|
|
return len(objects)
|
|
|
|
|
|
def count_azurite_blobs(started_cluster, container_name, prefix):
|
|
container_client = started_cluster.blob_service_client.get_container_client(
|
|
container_name
|
|
)
|
|
blob_names = list(container_client.list_blob_names(
|
|
name_starts_with=prefix))
|
|
return len(blob_names)
|
|
|
|
|
|
def recreate_minio_bucket(started_cluster, bucket_name):
|
|
minio_client = started_cluster.minio_client
|
|
if minio_client.bucket_exists(bucket_name):
|
|
logging.debug(f"minio bucket '{bucket_name}' exists, removing to recreate")
|
|
objects = minio_client.list_objects(bucket_name, recursive=True)
|
|
errors = list(minio_client.remove_objects(
|
|
bucket_name,
|
|
[DeleteObject(obj.object_name) for obj in objects],
|
|
))
|
|
assert not errors, f"failed to clear bucket '{bucket_name}': {errors}"
|
|
minio_client.remove_bucket(bucket_name)
|
|
minio_client.make_bucket(bucket_name)
|
|
|
|
|
|
def recreate_azurite_container(started_cluster, container_name):
|
|
container_client = started_cluster.blob_service_client.get_container_client(
|
|
container_name
|
|
)
|
|
if container_client.exists():
|
|
logging.debug(f"azurite container '{container_name}' exists, deleting to recreate")
|
|
container_client.delete_container()
|
|
container_client.create_container()
|
|
|
|
|
|
def create_table(
|
|
started_cluster,
|
|
node,
|
|
table_name,
|
|
mode,
|
|
files_path,
|
|
engine_name="S3Queue",
|
|
version=None,
|
|
format="column1 UInt32, column2 UInt32, column3 UInt32",
|
|
additional_settings={},
|
|
file_format="CSV",
|
|
auth=DEFAULT_AUTH,
|
|
bucket=None,
|
|
expect_error=False,
|
|
database_name="default",
|
|
replace=False,
|
|
no_settings=False,
|
|
hive_partitioning_path="",
|
|
hive_partitioning_columns="",
|
|
partitioning_mode="",
|
|
partition_regex="",
|
|
partition_component="",
|
|
after_processing="keep",
|
|
move_to_prefix=None,
|
|
move_to_bucket=None,
|
|
preserve_move_path=False,
|
|
extra_credentials="",
|
|
):
|
|
auth_params = ",".join(auth)
|
|
bucket = started_cluster.minio_bucket if bucket is None else bucket
|
|
|
|
settings = {
|
|
"s3queue_loading_retries": 0,
|
|
"after_processing": after_processing,
|
|
"keeper_path": f"/clickhouse/test_{table_name}",
|
|
"mode": f"{mode}",
|
|
}
|
|
if version is None:
|
|
settings["enable_hash_ring_filtering"] = 1
|
|
settings["use_persistent_processing_nodes"] = random.choice([True, False])
|
|
|
|
if after_processing == "move":
|
|
assert move_to_prefix or move_to_bucket
|
|
|
|
if preserve_move_path:
|
|
settings["after_processing_move_preserve_path"] = True
|
|
|
|
if move_to_prefix:
|
|
settings["after_processing_move_prefix"] = move_to_prefix
|
|
if move_to_bucket:
|
|
if engine_name == "S3Queue":
|
|
move_uri = f"http://{started_cluster.minio_host}:{started_cluster.minio_port}/{move_to_bucket}"
|
|
settings["after_processing_move_uri"] = move_uri
|
|
minio_access_key_id, minio_secret_access_key = [s.strip("'") for s in DEFAULT_AUTH]
|
|
settings["after_processing_move_access_key_id"] = minio_access_key_id
|
|
settings["after_processing_move_secret_access_key"] = minio_secret_access_key
|
|
else:
|
|
azurite_connection_string = started_cluster.env_variables['AZURITE_CONNECTION_STRING']
|
|
settings["after_processing_move_connection_string"] = azurite_connection_string
|
|
settings["after_processing_move_container"] = move_to_bucket
|
|
|
|
settings.update(additional_settings)
|
|
|
|
if hive_partitioning_columns:
|
|
hive_partitioning_columns = f", {hive_partitioning_columns}"
|
|
if not partitioning_mode: # Backward compatibility
|
|
settings["use_hive_partitioning"] = True
|
|
settings["allow_experimental_object_storage_queue_hive_partitioning"] = True
|
|
|
|
# Add regex partitioning settings
|
|
if partitioning_mode:
|
|
settings["partitioning_mode"] = partitioning_mode
|
|
if partition_regex:
|
|
settings["partition_regex"] = partition_regex
|
|
if partition_component:
|
|
settings["partition_component"] = partition_component
|
|
|
|
if len(extra_credentials) > 0:
|
|
extra_credentials = ", " + extra_credentials
|
|
if len(auth_params) > 0:
|
|
auth_params = ", " + auth_params
|
|
|
|
engine_def = None
|
|
if engine_name == "S3Queue":
|
|
url = f"http://{started_cluster.minio_host}:{started_cluster.minio_port}/{bucket}/{files_path}/{hive_partitioning_path}"
|
|
engine_def = (
|
|
f"{engine_name}('{url}' {auth_params}, {file_format} {extra_credentials})"
|
|
)
|
|
else:
|
|
azurite_connection_string = started_cluster.env_variables['AZURITE_CONNECTION_STRING']
|
|
engine_def = f"{engine_name}('{azurite_connection_string}', '{started_cluster.azurite_container}', '{files_path}/{hive_partitioning_path}', 'CSV')"
|
|
|
|
if format:
|
|
format = f'({format}{hive_partitioning_columns})'
|
|
|
|
create = "REPLACE" if replace else "CREATE"
|
|
if not replace:
|
|
node.query(f"DROP TABLE IF EXISTS {database_name}.{table_name}")
|
|
if no_settings:
|
|
create_query = f"""
|
|
{create} TABLE {database_name}.{table_name} {format}
|
|
ENGINE = {engine_def}
|
|
"""
|
|
else:
|
|
create_query = f"""
|
|
{create} TABLE {database_name}.{table_name} {format}
|
|
ENGINE = {engine_def}
|
|
SETTINGS {",".join((k+"="+repr(v) for k, v in settings.items()))}
|
|
"""
|
|
|
|
if expect_error:
|
|
return node.query_and_get_error(create_query)
|
|
|
|
node.query(create_query)
|
|
|
|
|
|
def create_mv(
|
|
node,
|
|
src_table_name,
|
|
dst_table_name,
|
|
mv_name=None,
|
|
create_dst_table_first=True,
|
|
format="column1 UInt32, column2 UInt32, column3 UInt32",
|
|
virtual_columns="_path String",
|
|
extra_dst_format=None,
|
|
dst_table_engine="MergeTree()",
|
|
dst_table_exists=False
|
|
):
|
|
if mv_name is None:
|
|
mv_name = f"{src_table_name}_mv"
|
|
if extra_dst_format is not None:
|
|
extra_dst_format = f", {extra_dst_format}"
|
|
else:
|
|
extra_dst_format = ""
|
|
|
|
if not dst_table_exists:
|
|
node.query(f"DROP TABLE IF EXISTS {dst_table_name};")
|
|
|
|
node.query(f"DROP TABLE IF EXISTS {mv_name};")
|
|
|
|
names = ""
|
|
for column in format.split(","):
|
|
name, _ = column.strip().rsplit(" ", 1)
|
|
if names == "":
|
|
names = name
|
|
else:
|
|
names += f", {name}"
|
|
|
|
virtual_format = ""
|
|
virtual_names = ""
|
|
virtual_columns_list = virtual_columns.split(",")
|
|
for column in virtual_columns_list:
|
|
virtual_format += f", {column}"
|
|
name, _ = column.strip().rsplit(" ", 1)
|
|
virtual_names += f", {name}"
|
|
|
|
if create_dst_table_first:
|
|
if not dst_table_exists:
|
|
node.query(f"""
|
|
CREATE TABLE {dst_table_name} ({format}{extra_dst_format}{virtual_format})
|
|
ENGINE = {dst_table_engine}
|
|
ORDER BY column1;
|
|
""")
|
|
node.query(
|
|
f"""
|
|
CREATE MATERIALIZED VIEW {mv_name} TO {dst_table_name} AS SELECT {names} {virtual_names} FROM {src_table_name};
|
|
"""
|
|
)
|
|
else:
|
|
node.query(
|
|
f"""
|
|
SET allow_materialized_view_with_bad_select=1;
|
|
CREATE MATERIALIZED VIEW {mv_name} TO {dst_table_name} AS SELECT {names} {virtual_names} FROM {src_table_name};
|
|
""")
|
|
if not dst_table_exists:
|
|
node.query(f"""
|
|
CREATE TABLE {dst_table_name} ({format}{extra_dst_format}{virtual_format})
|
|
ENGINE = {dst_table_engine}
|
|
ORDER BY column1;
|
|
""")
|
|
|
|
|
|
def generate_random_string(length=6):
|
|
return "".join(random.choice(string.ascii_lowercase) for i in range(length))
|