2021-08-18 22:19:14 +00:00
|
|
|
import pytest
|
|
|
|
import re
|
2022-02-22 12:39:11 +00:00
|
|
|
import os.path
|
2021-08-18 22:19:14 +00:00
|
|
|
from helpers.cluster import ClickHouseCluster
|
|
|
|
|
|
|
|
cluster = ClickHouseCluster(__file__)
|
2022-03-22 16:39:58 +00:00
|
|
|
instance = cluster.add_instance(
|
|
|
|
"instance", main_configs=["configs/backups_disk.xml"], external_dirs=["/backups/"]
|
|
|
|
)
|
|
|
|
|
2021-08-18 22:19:14 +00:00
|
|
|
|
2021-10-26 09:48:31 +00:00
|
|
|
def create_and_fill_table(engine="MergeTree"):
|
|
|
|
if engine == "MergeTree":
|
|
|
|
engine = "MergeTree ORDER BY y PARTITION BY x%10"
|
2021-08-18 22:19:14 +00:00
|
|
|
instance.query("CREATE DATABASE test")
|
2021-10-26 09:48:31 +00:00
|
|
|
instance.query(f"CREATE TABLE test.table(x UInt32, y String) ENGINE={engine}")
|
2022-03-22 16:39:58 +00:00
|
|
|
instance.query(
|
|
|
|
"INSERT INTO test.table SELECT number, toString(number) FROM numbers(100)"
|
|
|
|
)
|
2021-08-18 22:19:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope="module", autouse=True)
|
|
|
|
def start_cluster():
|
|
|
|
try:
|
|
|
|
cluster.start()
|
|
|
|
yield cluster
|
|
|
|
finally:
|
|
|
|
cluster.shutdown()
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def cleanup_after_test():
|
|
|
|
try:
|
|
|
|
yield
|
|
|
|
finally:
|
|
|
|
instance.query("DROP DATABASE IF EXISTS test")
|
|
|
|
|
|
|
|
|
|
|
|
backup_id_counter = 0
|
2022-03-22 16:39:58 +00:00
|
|
|
|
|
|
|
|
2021-08-18 22:19:14 +00:00
|
|
|
def new_backup_name():
|
|
|
|
global backup_id_counter
|
|
|
|
backup_id_counter += 1
|
2021-11-05 14:18:23 +00:00
|
|
|
return f"Disk('backups', '{backup_id_counter}/')"
|
2021-08-18 22:19:14 +00:00
|
|
|
|
2021-10-26 09:48:31 +00:00
|
|
|
|
2022-02-22 12:39:11 +00:00
|
|
|
def get_backup_dir(backup_name):
|
2022-03-22 16:39:58 +00:00
|
|
|
counter = int(backup_name.split(",")[1].strip("')/ "))
|
|
|
|
return os.path.join(instance.path, f"backups/{counter}")
|
2022-02-22 12:39:11 +00:00
|
|
|
|
|
|
|
|
2022-03-22 16:39:58 +00:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
"engine", ["MergeTree", "Log", "TinyLog", "StripeLog", "Memory"]
|
|
|
|
)
|
2021-10-26 09:48:31 +00:00
|
|
|
def test_restore_table(engine):
|
2021-08-18 22:19:14 +00:00
|
|
|
backup_name = new_backup_name()
|
2021-10-26 09:48:31 +00:00
|
|
|
create_and_fill_table(engine=engine)
|
2021-08-18 22:19:14 +00:00
|
|
|
|
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "100\t4950\n"
|
2021-11-05 14:18:23 +00:00
|
|
|
instance.query(f"BACKUP TABLE test.table TO {backup_name}")
|
2021-08-18 22:19:14 +00:00
|
|
|
|
|
|
|
instance.query("DROP TABLE test.table")
|
|
|
|
assert instance.query("EXISTS test.table") == "0\n"
|
|
|
|
|
2021-11-05 14:18:23 +00:00
|
|
|
instance.query(f"RESTORE TABLE test.table FROM {backup_name}")
|
2021-08-18 22:19:14 +00:00
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "100\t4950\n"
|
|
|
|
|
|
|
|
|
2022-03-22 16:39:58 +00:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
"engine", ["MergeTree", "Log", "TinyLog", "StripeLog", "Memory"]
|
|
|
|
)
|
2021-10-26 09:48:31 +00:00
|
|
|
def test_restore_table_into_existing_table(engine):
|
2021-08-18 22:19:14 +00:00
|
|
|
backup_name = new_backup_name()
|
2021-10-26 09:48:31 +00:00
|
|
|
create_and_fill_table(engine=engine)
|
2021-08-18 22:19:14 +00:00
|
|
|
|
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "100\t4950\n"
|
2021-11-05 14:18:23 +00:00
|
|
|
instance.query(f"BACKUP TABLE test.table TO {backup_name}")
|
2021-08-18 22:19:14 +00:00
|
|
|
|
2022-03-22 16:39:58 +00:00
|
|
|
instance.query(
|
2022-04-12 20:38:21 +00:00
|
|
|
f"RESTORE TABLE test.table INTO test.table FROM {backup_name}"
|
2022-03-22 16:39:58 +00:00
|
|
|
)
|
2021-08-18 22:19:14 +00:00
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "200\t9900\n"
|
|
|
|
|
2022-03-22 16:39:58 +00:00
|
|
|
instance.query(
|
2022-04-12 20:38:21 +00:00
|
|
|
f"RESTORE TABLE test.table INTO test.table FROM {backup_name}"
|
2022-03-22 16:39:58 +00:00
|
|
|
)
|
2021-08-18 22:19:14 +00:00
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "300\t14850\n"
|
|
|
|
|
|
|
|
|
|
|
|
def test_restore_table_under_another_name():
|
|
|
|
backup_name = new_backup_name()
|
|
|
|
create_and_fill_table()
|
|
|
|
|
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "100\t4950\n"
|
2021-11-05 14:18:23 +00:00
|
|
|
instance.query(f"BACKUP TABLE test.table TO {backup_name}")
|
2021-08-18 22:19:14 +00:00
|
|
|
|
|
|
|
assert instance.query("EXISTS test.table2") == "0\n"
|
|
|
|
|
2021-11-05 14:18:23 +00:00
|
|
|
instance.query(f"RESTORE TABLE test.table INTO test.table2 FROM {backup_name}")
|
2021-08-18 22:19:14 +00:00
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table2") == "100\t4950\n"
|
|
|
|
|
|
|
|
|
|
|
|
def test_backup_table_under_another_name():
|
|
|
|
backup_name = new_backup_name()
|
|
|
|
create_and_fill_table()
|
|
|
|
|
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "100\t4950\n"
|
2021-11-05 14:18:23 +00:00
|
|
|
instance.query(f"BACKUP TABLE test.table AS test.table2 TO {backup_name}")
|
2021-08-18 22:19:14 +00:00
|
|
|
|
|
|
|
assert instance.query("EXISTS test.table2") == "0\n"
|
|
|
|
|
2021-11-05 14:18:23 +00:00
|
|
|
instance.query(f"RESTORE TABLE test.table2 FROM {backup_name}")
|
2021-08-18 22:19:14 +00:00
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table2") == "100\t4950\n"
|
|
|
|
|
|
|
|
|
2022-02-22 17:05:52 +00:00
|
|
|
def test_materialized_view():
|
|
|
|
backup_name = new_backup_name()
|
2022-03-22 16:39:58 +00:00
|
|
|
instance.query(
|
|
|
|
"CREATE MATERIALIZED VIEW mv_1(x UInt8) ENGINE=MergeTree ORDER BY tuple() POPULATE AS SELECT 1 AS x"
|
|
|
|
)
|
2022-02-22 17:05:52 +00:00
|
|
|
|
|
|
|
instance.query(f"BACKUP TABLE mv_1 TO {backup_name}")
|
|
|
|
instance.query("DROP TABLE mv_1")
|
|
|
|
instance.query(f"RESTORE TABLE mv_1 FROM {backup_name}")
|
|
|
|
|
|
|
|
assert instance.query("SELECT * FROM mv_1") == "1\n"
|
|
|
|
instance.query("DROP TABLE mv_1")
|
|
|
|
|
|
|
|
|
2021-08-18 22:19:14 +00:00
|
|
|
def test_incremental_backup():
|
|
|
|
backup_name = new_backup_name()
|
|
|
|
incremental_backup_name = new_backup_name()
|
|
|
|
create_and_fill_table()
|
|
|
|
|
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "100\t4950\n"
|
2021-11-05 14:18:23 +00:00
|
|
|
instance.query(f"BACKUP TABLE test.table TO {backup_name}")
|
2021-08-18 22:19:14 +00:00
|
|
|
|
|
|
|
instance.query("INSERT INTO test.table VALUES (65, 'a'), (66, 'b')")
|
|
|
|
|
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "102\t5081\n"
|
2022-03-22 16:39:58 +00:00
|
|
|
instance.query(
|
|
|
|
f"BACKUP TABLE test.table TO {incremental_backup_name} SETTINGS base_backup = {backup_name}"
|
|
|
|
)
|
2021-08-18 22:19:14 +00:00
|
|
|
|
2022-03-22 16:39:58 +00:00
|
|
|
instance.query(
|
|
|
|
f"RESTORE TABLE test.table AS test.table2 FROM {incremental_backup_name}"
|
|
|
|
)
|
2021-08-18 22:19:14 +00:00
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table2") == "102\t5081\n"
|
|
|
|
|
|
|
|
|
2022-02-22 12:39:11 +00:00
|
|
|
def test_incremental_backup_after_renaming_table():
|
|
|
|
backup_name = new_backup_name()
|
|
|
|
incremental_backup_name = new_backup_name()
|
|
|
|
create_and_fill_table()
|
|
|
|
|
|
|
|
instance.query(f"BACKUP TABLE test.table TO {backup_name}")
|
|
|
|
instance.query("RENAME TABLE test.table TO test.table2")
|
2022-03-22 16:39:58 +00:00
|
|
|
instance.query(
|
|
|
|
f"BACKUP TABLE test.table2 TO {incremental_backup_name} SETTINGS base_backup = {backup_name}"
|
|
|
|
)
|
2022-02-22 12:39:11 +00:00
|
|
|
|
|
|
|
# Files in a base backup can be searched by checksum, so an incremental backup with a renamed table actually
|
|
|
|
# contains only its changed metadata.
|
2022-04-16 20:18:42 +00:00
|
|
|
contents = os.listdir(get_backup_dir(incremental_backup_name))
|
|
|
|
assert '.backup' in contents
|
|
|
|
contents.remove('.backup')
|
|
|
|
assert len(contents) == 1
|
|
|
|
with open(os.path.join(get_backup_dir(incremental_backup_name), contents[0])) as table_def_in_backup:
|
|
|
|
assert table_def_in_backup.read().startswith('CREATE TABLE test.table2')
|
2022-02-22 12:39:11 +00:00
|
|
|
|
|
|
|
instance.query("DROP TABLE test.table2")
|
|
|
|
instance.query(f"RESTORE TABLE test.table2 FROM {incremental_backup_name}")
|
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table2") == "100\t4950\n"
|
|
|
|
|
|
|
|
|
2021-08-18 22:19:14 +00:00
|
|
|
def test_backup_not_found_or_already_exists():
|
|
|
|
backup_name = new_backup_name()
|
|
|
|
|
|
|
|
expected_error = "Backup .* not found"
|
2022-03-22 16:39:58 +00:00
|
|
|
assert re.search(
|
|
|
|
expected_error,
|
|
|
|
instance.query_and_get_error(
|
|
|
|
f"RESTORE TABLE test.table AS test.table2 FROM {backup_name}"
|
|
|
|
),
|
|
|
|
)
|
2021-08-18 22:19:14 +00:00
|
|
|
|
|
|
|
create_and_fill_table()
|
2021-11-05 14:18:23 +00:00
|
|
|
instance.query(f"BACKUP TABLE test.table TO {backup_name}")
|
2021-08-18 22:19:14 +00:00
|
|
|
|
|
|
|
expected_error = "Backup .* already exists"
|
2022-03-22 16:39:58 +00:00
|
|
|
assert re.search(
|
|
|
|
expected_error,
|
|
|
|
instance.query_and_get_error(f"BACKUP TABLE test.table TO {backup_name}"),
|
|
|
|
)
|
2021-11-05 14:18:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_file_engine():
|
2021-11-09 08:34:37 +00:00
|
|
|
backup_name = f"File('/backups/file/')"
|
2021-11-05 14:18:23 +00:00
|
|
|
create_and_fill_table()
|
|
|
|
|
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "100\t4950\n"
|
|
|
|
instance.query(f"BACKUP TABLE test.table TO {backup_name}")
|
|
|
|
|
|
|
|
instance.query("DROP TABLE test.table")
|
|
|
|
assert instance.query("EXISTS test.table") == "0\n"
|
|
|
|
|
|
|
|
instance.query(f"RESTORE TABLE test.table FROM {backup_name}")
|
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "100\t4950\n"
|
2021-11-09 08:33:28 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_database():
|
|
|
|
backup_name = new_backup_name()
|
|
|
|
create_and_fill_table()
|
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "100\t4950\n"
|
|
|
|
|
|
|
|
instance.query(f"BACKUP DATABASE test TO {backup_name}")
|
|
|
|
instance.query("DROP DATABASE test")
|
|
|
|
instance.query(f"RESTORE DATABASE test FROM {backup_name}")
|
|
|
|
|
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "100\t4950\n"
|
2022-01-28 17:33:35 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_zip_archive():
|
|
|
|
backup_name = f"File('/backups/archive.zip')"
|
|
|
|
create_and_fill_table()
|
|
|
|
|
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "100\t4950\n"
|
|
|
|
instance.query(f"BACKUP TABLE test.table TO {backup_name}")
|
2022-03-22 16:39:58 +00:00
|
|
|
assert os.path.isfile(
|
|
|
|
os.path.join(os.path.join(instance.path, "backups/archive.zip"))
|
|
|
|
)
|
2022-01-28 17:33:35 +00:00
|
|
|
|
|
|
|
instance.query("DROP TABLE test.table")
|
|
|
|
assert instance.query("EXISTS test.table") == "0\n"
|
|
|
|
|
|
|
|
instance.query(f"RESTORE TABLE test.table FROM {backup_name}")
|
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "100\t4950\n"
|
|
|
|
|
|
|
|
|
|
|
|
def test_zip_archive_with_settings():
|
|
|
|
backup_name = f"File('/backups/archive_with_settings.zip')"
|
|
|
|
create_and_fill_table()
|
|
|
|
|
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "100\t4950\n"
|
2022-03-22 16:39:58 +00:00
|
|
|
instance.query(
|
|
|
|
f"BACKUP TABLE test.table TO {backup_name} SETTINGS compression_method='lzma', compression_level=3, password='qwerty'"
|
|
|
|
)
|
2022-01-28 17:33:35 +00:00
|
|
|
|
|
|
|
instance.query("DROP TABLE test.table")
|
|
|
|
assert instance.query("EXISTS test.table") == "0\n"
|
|
|
|
|
2022-03-22 16:39:58 +00:00
|
|
|
instance.query(
|
|
|
|
f"RESTORE TABLE test.table FROM {backup_name} SETTINGS password='qwerty'"
|
|
|
|
)
|
2022-01-28 17:33:35 +00:00
|
|
|
assert instance.query("SELECT count(), sum(x) FROM test.table") == "100\t4950\n"
|