mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
229 lines
7.5 KiB
Python
229 lines
7.5 KiB
Python
import pytest
|
|
|
|
from helpers.cluster import ClickHouseCluster
|
|
|
|
# 26.4 writes the pre-WithCodec header format; the new reader recovers the
|
|
# posting list codec from the index DDL. We test both the default codec and
|
|
# 'bitpacking', where DDL recovery is the only way to decode old segments.
|
|
OLD_VERSION_TAG = "26.4"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def started_cluster():
|
|
cluster = ClickHouseCluster(__file__)
|
|
try:
|
|
cluster.add_instance(
|
|
"node",
|
|
image="clickhouse/clickhouse-server",
|
|
tag=OLD_VERSION_TAG,
|
|
with_installed_binary=True,
|
|
stay_alive=True,
|
|
)
|
|
cluster.start()
|
|
yield cluster
|
|
finally:
|
|
cluster.shutdown()
|
|
|
|
|
|
# Queries with known answers covering single-token lookups, AND intersection,
|
|
# OR union, and a missing token.
|
|
SEARCH_QUERIES = [
|
|
("SELECT count() FROM {table} WHERE hasToken(s, 'common')", "2500"),
|
|
("SELECT count() FROM {table} WHERE hasToken(s, 'rare')", "2500"),
|
|
("SELECT count() FROM {table} WHERE hasToken(s, 'unique42')", "1"),
|
|
("SELECT count() FROM {table} WHERE hasToken(s, 'absent')", "0"),
|
|
(
|
|
"SELECT count() FROM {table} WHERE hasAllTokens(s, ['common', 'shared'])",
|
|
"2500",
|
|
),
|
|
(
|
|
"SELECT count() FROM {table} WHERE hasAnyTokens(s, ['rare', 'unique42'])",
|
|
"2501",
|
|
),
|
|
(
|
|
"SELECT arraySort(groupArray(k)) FROM {table} "
|
|
"WHERE hasToken(s, 'unique42')",
|
|
"[42]",
|
|
),
|
|
]
|
|
|
|
|
|
def create_and_populate(node, table, posting_list_codec):
|
|
node.query(f"DROP TABLE IF EXISTS {table} SYNC")
|
|
|
|
codec_clause = ""
|
|
if posting_list_codec is not None:
|
|
codec_clause = f", posting_list_codec = '{posting_list_codec}'"
|
|
|
|
# Small posting_list_block_size makes each posting list span many packed
|
|
# blocks, exercising the on-disk layout walked by the new reader.
|
|
node.query(
|
|
f"""
|
|
CREATE TABLE {table} (
|
|
k UInt64,
|
|
s String,
|
|
INDEX idx s TYPE text(
|
|
tokenizer = 'splitByNonAlpha',
|
|
posting_list_block_size = 64
|
|
{codec_clause}
|
|
)
|
|
)
|
|
ENGINE = MergeTree
|
|
ORDER BY k
|
|
SETTINGS index_granularity = 128
|
|
"""
|
|
)
|
|
|
|
# 5000 rows: every row carries 'shared'; even k -> 'common', odd k -> 'rare';
|
|
# row 42 carries 'unique42'. Inserted as two parts then merged so the
|
|
# resulting posting lists are stitched from multiple segments.
|
|
node.query(
|
|
f"""
|
|
INSERT INTO {table}
|
|
SELECT
|
|
number,
|
|
concat(
|
|
'shared ',
|
|
if(number % 2 = 0, 'common', 'rare'),
|
|
if(number = 42, ' unique42', '')
|
|
)
|
|
FROM numbers(2500)
|
|
"""
|
|
)
|
|
node.query(
|
|
f"""
|
|
INSERT INTO {table}
|
|
SELECT
|
|
number + 2500,
|
|
concat('shared ', if(number % 2 = 0, 'common', 'rare'))
|
|
FROM numbers(2500)
|
|
"""
|
|
)
|
|
|
|
node.query(f"OPTIMIZE TABLE {table} FINAL")
|
|
|
|
|
|
# Exercise the lazy posting list apply mode against the upgraded binary:
|
|
# pre-WithCodec granules silently fall back to eager mode, while the new-format
|
|
# part inserted after the upgrade actually uses the cursor-based reader.
|
|
LAZY_APPLY_SETTINGS = {
|
|
"text_index_posting_list_apply_mode": "lazy",
|
|
}
|
|
|
|
|
|
def run_search_queries(node, table, settings=None):
|
|
return [
|
|
node.query(q.format(table=table), settings=settings).strip()
|
|
for q, _ in SEARCH_QUERIES
|
|
]
|
|
|
|
|
|
def expected_results():
|
|
return [expected for _, expected in SEARCH_QUERIES]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"posting_list_codec",
|
|
[
|
|
pytest.param(None, id="default_codec"),
|
|
pytest.param("bitpacking", id="bitpacking_codec"),
|
|
],
|
|
)
|
|
def test_text_index_upgrade(started_cluster, posting_list_codec):
|
|
node = started_cluster.instances["node"]
|
|
table = f"text_index_upgrade_{posting_list_codec or 'default'}"
|
|
|
|
create_and_populate(node, table, posting_list_codec)
|
|
|
|
# Ground truth from the old server: pre-WithCodec layout on disk, old reader.
|
|
assert run_search_queries(node, table) == expected_results()
|
|
|
|
# Swap the binary but keep the data dir; the new reader must load the
|
|
# old-format index segments produced above.
|
|
node.restart_with_latest_version()
|
|
|
|
# Same data, same queries, same answers under the upgraded binary.
|
|
# Lazy mode falls back to materialize for these pre-WithCodec granules.
|
|
assert run_search_queries(node, table, settings=LAZY_APPLY_SETTINGS) == expected_results()
|
|
|
|
# Confirm the text index is engaged after upgrade; without this check a
|
|
# silent fallback to full scan would still pass the queries above.
|
|
explain = node.query(
|
|
f"EXPLAIN indexes = 1 "
|
|
f"SELECT count() FROM {table} WHERE hasToken(s, 'unique42')",
|
|
settings=LAZY_APPLY_SETTINGS,
|
|
)
|
|
assert "Name: idx" in explain, (
|
|
f"text index `idx` not picked up after upgrade:\n{explain}"
|
|
)
|
|
|
|
# Insert a third part via the upgraded binary so the table has both old-
|
|
# and new-format index segments side-by-side.
|
|
node.query(
|
|
f"""
|
|
INSERT INTO {table}
|
|
SELECT
|
|
number + 5000,
|
|
concat(
|
|
'shared ',
|
|
if(number % 2 = 0, 'common', 'rare'),
|
|
if(number = 42, ' unique5042', '')
|
|
)
|
|
FROM numbers(2500)
|
|
"""
|
|
)
|
|
|
|
# After the new insert 'common' and 'rare' each gain 1250 rows; 'unique42'
|
|
# stays at row 42; 'unique5042' is new on row 5042 in the new-format part.
|
|
mixed_expected = [
|
|
"3750", # hasToken 'common'
|
|
"3750", # hasToken 'rare'
|
|
"1", # hasToken 'unique42'
|
|
"0", # hasToken 'absent'
|
|
"3750", # hasAllTokens ['common', 'shared']
|
|
"3751", # hasAnyTokens ['rare', 'unique42']
|
|
"[42]", # arraySort(groupArray(k)) for 'unique42'
|
|
]
|
|
# Mixed run: old-format parts take the materialize fallback; the new-format
|
|
# part can satisfy the lazy-mode preconditions.
|
|
assert run_search_queries(node, table, settings=LAZY_APPLY_SETTINGS) == mixed_expected
|
|
|
|
# Confirm the new-format part is indexed for the new token.
|
|
assert (
|
|
node.query(
|
|
f"SELECT count() FROM {table} WHERE hasToken(s, 'unique5042')",
|
|
settings=LAZY_APPLY_SETTINGS,
|
|
).strip()
|
|
== "1"
|
|
)
|
|
|
|
# Merge across mixed-format parts: posting lists from the old pre-WithCodec
|
|
# layout and the new layout are read back and re-emitted as one new-format
|
|
# part. Fails if the new reader cannot decode old segments end-to-end.
|
|
node.query(f"OPTIMIZE TABLE {table} FINAL")
|
|
|
|
# Exactly one active part proves the merge actually ran (a silent skip
|
|
# would still answer the queries below correctly).
|
|
active_parts = node.query(
|
|
f"SELECT count() FROM system.parts "
|
|
f"WHERE table = '{table}' AND active"
|
|
).strip()
|
|
assert active_parts == "1", (
|
|
f"expected a single active part after OPTIMIZE FINAL, got {active_parts}"
|
|
)
|
|
|
|
# Same queries against the merged part: checks mixed-version index data
|
|
# was correctly merged, not just readable. The merged part is new-format,
|
|
# so lazy mode is now actually engaged for every query.
|
|
assert run_search_queries(node, table, settings=LAZY_APPLY_SETTINGS) == mixed_expected
|
|
assert (
|
|
node.query(
|
|
f"SELECT count() FROM {table} WHERE hasToken(s, 'unique5042')",
|
|
settings=LAZY_APPLY_SETTINGS,
|
|
).strip()
|
|
== "1"
|
|
)
|
|
|
|
node.query(f"DROP TABLE {table} SYNC")
|
|
node.restart_with_original_version()
|