Files
Alexey MilovidovandClaude Opus 4.8 f9118c6600 Align system.users fast path with full scan for duplicate users_xml names
Resolve the matched id through `AccessControl` (`tryRead` and `findStorage`),
the same way the full-scan fallback does, instead of reading from the matched
`storage` directly.

Two `users_xml` storages defining the same user name derive the same generated
`UUID` from `UsersConfigParser::generateID`, and `findStorage` always resolves
that `UUID` to the first storage. The previous fast path read from the specific
storage and emitted its name, so `WHERE name = 'dup_user'` could attribute a row
to `storage_b` while the full scan (`WHERE name LIKE 'dup\\_user'`) attributed
both rows to the first storage. The fast path is now a transparent optimization
that produces identical rows to the full scan.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-07 18:01:59 +00:00

58 lines
2.1 KiB
Python

import pytest
from helpers.cluster import ClickHouseCluster
cluster = ClickHouseCluster(__file__)
# `dup_user` is defined in two separate `users_xml` access storages (`storage_a` and
# `storage_b`). Both storages derive the same generated `UUID` from the user name, so a
# full scan over `findAll<User>` emits a row for each storage but resolves every row to
# the first storage. The fast path that `system.users` takes for `name = '...'` and
# `name IN (...)` predicates has to keep the same semantics instead of attributing the
# rows to different storages or collapsing the duplicates.
node = cluster.add_instance(
"node",
main_configs=["configs/access_storages.xml"],
extra_configs=["configs/users_a.xml", "configs/users_b.xml"],
)
@pytest.fixture(scope="module", autouse=True)
def started_cluster():
try:
cluster.start()
yield cluster
finally:
cluster.shutdown()
def rows(predicate):
# Sort so the comparison does not depend on row order between the fast path and
# the full scan.
return node.query(
f"SELECT id, name, storage FROM system.users WHERE {predicate} ORDER BY id, name, storage"
)
def test_fast_path_matches_full_scan_for_duplicate_names():
# The fallback (full-scan) path: `LIKE` is not extracted as a literal name predicate.
full_scan = rows("name LIKE 'dup\\_user'")
# `dup_user` exists in two storages, so we expect two rows.
assert full_scan.count("dup_user") == 2
# Fast path with equality must return exactly the same rows as the full scan.
assert rows("name = 'dup_user'") == full_scan
# Fast path with `IN` must return exactly the same rows as the full scan.
assert rows("name IN ('dup_user')") == full_scan
def test_fast_path_matches_full_scan_for_mixed_in():
# `only_a` is unique to one storage, `dup_user` is in both: three rows total.
full_scan = rows("name LIKE 'dup\\_user' OR name LIKE 'only\\_a'")
assert rows("name IN ('dup_user', 'only_a')") == full_scan
assert full_scan.count("dup_user") == 2
assert full_scan.count("only_a") == 1