Merge remote-tracking branch 'origin/master' into add-pcodec-rust-codec

Resolve the `contrib/rust_vendor` conflict with a vendor commit merging
`chdig v26.10.1` from master and `rand_xoshiro` 0.6.0 of this PR.
This commit is contained in:
Alexey Milovidov committed 2026-10-06 04:46:16 +00:00
commit 7ff41f4842
437 files changed
+15118 -4742

No files matched your search

+5 -5
View File
@@ -1069,11 +1069,11 @@ jobs:
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Build (wasm64)' --workflow "MasterCI" --ci --timestamp
build_amd_fuzzers:
build_arm_fuzzers:
runs-on: [self-hosted, arm-large]
needs: [build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_msan, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_debug, build_arm_msan, build_arm_tidy, build_arm_tsan, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'QnVpbGQgKGFtZF9mdXp6ZXJzKQ==') }}
name: "Build (amd_fuzzers)"
if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'QnVpbGQgKGFybV9mdXp6ZXJzKQ==') }}
name: "Build (arm_fuzzers)"
outputs:
data: ${{ steps.run.outputs.DATA }}
pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
@@ -1101,7 +1101,7 @@ jobs:
id: run
run: |
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Build (amd_fuzzers)' --workflow "MasterCI" --ci --timestamp
PYTHONUNBUFFERED=1 python3 -m praktika run 'Build (arm_fuzzers)' --workflow "MasterCI" --ci --timestamp
build_wasm_parser:
runs-on: [self-hosted, arm-large]
@@ -5525,7 +5525,7 @@ jobs:
finish_workflow:
runs-on: [self-hosted, arm-tiny]
needs: [ast_fuzzer_amd_debug, ast_fuzzer_amd_msan, ast_fuzzer_amd_release_oracle, ast_fuzzer_amd_tsan, ast_fuzzer_arm_asan_ubsan, build_amd_asan_ubsan, build_amd_binary, build_amd_compat, build_amd_darwin, build_amd_debug, build_amd_freebsd, build_amd_fuzzers, build_amd_msan, build_amd_musl, build_amd_release, build_amd_release_pr_cache_warmup, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_darwin, build_arm_debug, build_arm_msan, build_arm_release, build_arm_release_pr_cache_warmup, build_arm_tidy, build_arm_tsan, build_arm_v80compat, build_llvm_coverage_build, build_loongarch64, build_ppc64le, build_riscv64, build_s390x, build_wasm64, build_wasm_parser, buzzhouse_amd_debug, buzzhouse_amd_msan, buzzhouse_amd_tsan, buzzhouse_arm_asan_ubsan, clickbench_amd_release, clickbench_arm_release, compatibility_check_amd_release, compatibility_check_arm_release, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, docs_examples, install_packages_amd_release, install_packages_arm_release, integration_tests_amd_asan_ubsan_db_disk_1_8, integration_tests_amd_asan_ubsan_db_disk_2_8, integration_tests_amd_asan_ubsan_db_disk_3_8, integration_tests_amd_asan_ubsan_db_disk_4_8, integration_tests_amd_asan_ubsan_db_disk_5_8, integration_tests_amd_asan_ubsan_db_disk_6_8, integration_tests_amd_asan_ubsan_db_disk_7_8, integration_tests_amd_asan_ubsan_db_disk_8_8, integration_tests_amd_binary_excluded_from_llvm, integration_tests_amd_llvm_coverage_1_8, integration_tests_amd_llvm_coverage_2_8, integration_tests_amd_llvm_coverage_3_8, integration_tests_amd_llvm_coverage_4_8, integration_tests_amd_llvm_coverage_5_8, integration_tests_amd_llvm_coverage_6_8, integration_tests_amd_llvm_coverage_7_8, integration_tests_amd_llvm_coverage_8_8, integration_tests_amd_msan_1_8, integration_tests_amd_msan_2_8, integration_tests_amd_msan_3_8, integration_tests_amd_msan_4_8, integration_tests_amd_msan_5_8, integration_tests_amd_msan_6_8, integration_tests_amd_msan_7_8, integration_tests_amd_msan_8_8, integration_tests_amd_tsan_1_6, integration_tests_amd_tsan_2_6, integration_tests_amd_tsan_3_6, integration_tests_amd_tsan_4_6, integration_tests_amd_tsan_5_6, integration_tests_amd_tsan_6_6, integration_tests_arm_binary_distributed_plan_1_4, integration_tests_arm_binary_distributed_plan_2_4, integration_tests_arm_binary_distributed_plan_3_4, integration_tests_arm_binary_distributed_plan_4_4, llvm_coverage, performance_comparison_amd_release_master_head_1_6, performance_comparison_amd_release_master_head_2_6, performance_comparison_amd_release_master_head_3_6, performance_comparison_amd_release_master_head_4_6, performance_comparison_amd_release_master_head_5_6, performance_comparison_amd_release_master_head_6_6, performance_comparison_arm_release_master_head_1_6, performance_comparison_arm_release_master_head_2_6, performance_comparison_arm_release_master_head_3_6, performance_comparison_arm_release_master_head_4_6, performance_comparison_arm_release_master_head_5_6, performance_comparison_arm_release_master_head_6_6, sqllogic_test, sqlstorm_test, sqltest, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_1_3, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_2_3, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_3_3, stateless_tests_amd_asan_ubsan_distributed_plan_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_sequential, stateless_tests_amd_debug_parallel, stateless_tests_amd_debug_sequential, stateless_tests_amd_llvm_coverage_1_3, stateless_tests_amd_llvm_coverage_2_3, stateless_tests_amd_llvm_coverage_3_3, stateless_tests_amd_llvm_coverage_asyncinsert_s3_storage_parallel, stateless_tests_amd_llvm_coverage_asyncinsert_s3_storage_sequential, stateless_tests_amd_llvm_coverage_parallelreplicas_s3_storage_parallel, stateless_tests_amd_llvm_coverage_parallelreplicas_s3_storage_sequential, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_parallel_1_3, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_parallel_2_3, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_parallel_3_3, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_sequential_1_2, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_sequential_2_2, stateless_tests_amd_msan_parallel_1_3, stateless_tests_amd_msan_parallel_2_3, stateless_tests_amd_msan_parallel_3_3, stateless_tests_amd_msan_sequential_1_2, stateless_tests_amd_msan_sequential_2_2, stateless_tests_amd_release_parallel_1_2, stateless_tests_amd_release_parallel_2_2, stateless_tests_amd_release_sequential_1_2, stateless_tests_amd_release_sequential_2_2, stateless_tests_amd_tsan_parallel, stateless_tests_amd_tsan_s3_storage_parallel_1_3, stateless_tests_amd_tsan_s3_storage_parallel_2_3, stateless_tests_amd_tsan_s3_storage_parallel_3_3, stateless_tests_amd_tsan_s3_sLine truncated
needs: [ast_fuzzer_amd_debug, ast_fuzzer_amd_msan, ast_fuzzer_amd_release_oracle, ast_fuzzer_amd_tsan, ast_fuzzer_arm_asan_ubsan, build_amd_asan_ubsan, build_amd_binary, build_amd_compat, build_amd_darwin, build_amd_debug, build_amd_freebsd, build_amd_msan, build_amd_musl, build_amd_release, build_amd_release_pr_cache_warmup, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_darwin, build_arm_debug, build_arm_fuzzers, build_arm_msan, build_arm_release, build_arm_release_pr_cache_warmup, build_arm_tidy, build_arm_tsan, build_arm_v80compat, build_llvm_coverage_build, build_loongarch64, build_ppc64le, build_riscv64, build_s390x, build_wasm64, build_wasm_parser, buzzhouse_amd_debug, buzzhouse_amd_msan, buzzhouse_amd_tsan, buzzhouse_arm_asan_ubsan, clickbench_amd_release, clickbench_arm_release, compatibility_check_amd_release, compatibility_check_arm_release, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, docs_examples, install_packages_amd_release, install_packages_arm_release, integration_tests_amd_asan_ubsan_db_disk_1_8, integration_tests_amd_asan_ubsan_db_disk_2_8, integration_tests_amd_asan_ubsan_db_disk_3_8, integration_tests_amd_asan_ubsan_db_disk_4_8, integration_tests_amd_asan_ubsan_db_disk_5_8, integration_tests_amd_asan_ubsan_db_disk_6_8, integration_tests_amd_asan_ubsan_db_disk_7_8, integration_tests_amd_asan_ubsan_db_disk_8_8, integration_tests_amd_binary_excluded_from_llvm, integration_tests_amd_llvm_coverage_1_8, integration_tests_amd_llvm_coverage_2_8, integration_tests_amd_llvm_coverage_3_8, integration_tests_amd_llvm_coverage_4_8, integration_tests_amd_llvm_coverage_5_8, integration_tests_amd_llvm_coverage_6_8, integration_tests_amd_llvm_coverage_7_8, integration_tests_amd_llvm_coverage_8_8, integration_tests_amd_msan_1_8, integration_tests_amd_msan_2_8, integration_tests_amd_msan_3_8, integration_tests_amd_msan_4_8, integration_tests_amd_msan_5_8, integration_tests_amd_msan_6_8, integration_tests_amd_msan_7_8, integration_tests_amd_msan_8_8, integration_tests_amd_tsan_1_6, integration_tests_amd_tsan_2_6, integration_tests_amd_tsan_3_6, integration_tests_amd_tsan_4_6, integration_tests_amd_tsan_5_6, integration_tests_amd_tsan_6_6, integration_tests_arm_binary_distributed_plan_1_4, integration_tests_arm_binary_distributed_plan_2_4, integration_tests_arm_binary_distributed_plan_3_4, integration_tests_arm_binary_distributed_plan_4_4, llvm_coverage, performance_comparison_amd_release_master_head_1_6, performance_comparison_amd_release_master_head_2_6, performance_comparison_amd_release_master_head_3_6, performance_comparison_amd_release_master_head_4_6, performance_comparison_amd_release_master_head_5_6, performance_comparison_amd_release_master_head_6_6, performance_comparison_arm_release_master_head_1_6, performance_comparison_arm_release_master_head_2_6, performance_comparison_arm_release_master_head_3_6, performance_comparison_arm_release_master_head_4_6, performance_comparison_arm_release_master_head_5_6, performance_comparison_arm_release_master_head_6_6, sqllogic_test, sqlstorm_test, sqltest, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_1_3, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_2_3, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_3_3, stateless_tests_amd_asan_ubsan_distributed_plan_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_sequential, stateless_tests_amd_debug_parallel, stateless_tests_amd_debug_sequential, stateless_tests_amd_llvm_coverage_1_3, stateless_tests_amd_llvm_coverage_2_3, stateless_tests_amd_llvm_coverage_3_3, stateless_tests_amd_llvm_coverage_asyncinsert_s3_storage_parallel, stateless_tests_amd_llvm_coverage_asyncinsert_s3_storage_sequential, stateless_tests_amd_llvm_coverage_parallelreplicas_s3_storage_parallel, stateless_tests_amd_llvm_coverage_parallelreplicas_s3_storage_sequential, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_parallel_1_3, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_parallel_2_3, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_parallel_3_3, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_sequential_1_2, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_sequential_2_2, stateless_tests_amd_msan_parallel_1_3, stateless_tests_amd_msan_parallel_2_3, stateless_tests_amd_msan_parallel_3_3, stateless_tests_amd_msan_sequential_1_2, stateless_tests_amd_msan_sequential_2_2, stateless_tests_amd_release_parallel_1_2, stateless_tests_amd_release_parallel_2_2, stateless_tests_amd_release_sequential_1_2, stateless_tests_amd_release_sequential_2_2, stateless_tests_amd_tsan_parallel, stateless_tests_amd_tsan_s3_storage_parallel_1_3, stateless_tests_amd_tsan_s3_storage_parallel_2_3, stateless_tests_amd_tsan_s3_storage_parallel_3_3, stateless_tests_amd_tsan_s3_sLine truncated
if: ${{ always() }}
name: "Finish Workflow"
outputs:
+11 -11
View File
@@ -151,11 +151,11 @@ jobs:
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Dockers Build (multiplatform manifest)' --workflow "NightlyFuzzers" --ci --timestamp
build_amd_fuzzers:
build_arm_fuzzers:
runs-on: [self-hosted, arm-large]
needs: [config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'QnVpbGQgKGFtZF9mdXp6ZXJzKQ==') }}
name: "Build (amd_fuzzers)"
if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'QnVpbGQgKGFybV9mdXp6ZXJzKQ==') }}
name: "Build (arm_fuzzers)"
outputs:
data: ${{ steps.run.outputs.DATA }}
pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
@@ -183,13 +183,13 @@ jobs:
id: run
run: |
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Build (amd_fuzzers)' --workflow "NightlyFuzzers" --ci --timestamp
PYTHONUNBUFFERED=1 python3 -m praktika run 'Build (arm_fuzzers)' --workflow "NightlyFuzzers" --ci --timestamp
build_amd_release:
build_arm_release:
runs-on: [self-hosted, arm-large]
needs: [config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'QnVpbGQgKGFtZF9yZWxlYXNlKQ==') }}
name: "Build (amd_release)"
if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'QnVpbGQgKGFybV9yZWxlYXNlKQ==') }}
name: "Build (arm_release)"
outputs:
data: ${{ steps.run.outputs.DATA }}
pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
@@ -217,11 +217,11 @@ jobs:
id: run
run: |
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Build (amd_release)' --workflow "NightlyFuzzers" --ci --timestamp
PYTHONUNBUFFERED=1 python3 -m praktika run 'Build (arm_release)' --workflow "NightlyFuzzers" --ci --timestamp
libfuzzer_tests:
runs-on: [self-hosted, amd-medium]
needs: [build_amd_fuzzers, build_amd_release, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
runs-on: [self-hosted, arm-medium]
needs: [build_arm_fuzzers, build_arm_release, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'bGliRnV6emVyIHRlc3Rz') }}
name: "libFuzzer tests"
outputs:
@@ -255,7 +255,7 @@ jobs:
finish_workflow:
runs-on: [self-hosted, arm-tiny]
needs: [build_amd_fuzzers, build_amd_release, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, libfuzzer_tests]
needs: [build_arm_fuzzers, build_arm_release, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, libfuzzer_tests]
if: ${{ always() }}
name: "Finish Workflow"
outputs:
+5 -5
View File
@@ -1143,11 +1143,11 @@ jobs:
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Build (wasm64)' --workflow "PR" --ci --timestamp
build_amd_fuzzers:
build_arm_fuzzers:
runs-on: [self-hosted, pr-arm-large]
needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_msan, build_amd_tsan, build_arm_binary, build_arm_tidy, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, integration_tests_amd_asan_ubsan_db_disk_targeted, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_targeted, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential, style_check, unit_tests_asan_ubsan, unit_tests_asan_ubsan_function_prop_fuzzer, unit_tests_msan, unit_tests_msan_function_prop_fuzzer, unit_tests_tsan, unit_tests_tsan_function_prop_fuzzer]
if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'QnVpbGQgKGFtZF9mdXp6ZXJzKQ==') }}
name: "Build (amd_fuzzers)"
if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'QnVpbGQgKGFybV9mdXp6ZXJzKQ==') }}
name: "Build (arm_fuzzers)"
outputs:
data: ${{ steps.run.outputs.DATA }}
pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
@@ -1175,7 +1175,7 @@ jobs:
id: run
run: |
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Build (amd_fuzzers)' --workflow "PR" --ci --timestamp
PYTHONUNBUFFERED=1 python3 -m praktika run 'Build (arm_fuzzers)' --workflow "PR" --ci --timestamp
build_wasm_parser:
runs-on: [self-hosted, pr-arm-large]
@@ -4955,7 +4955,7 @@ jobs:
finish_workflow:
runs-on: [self-hosted, pr-arm-tiny]
needs: [ast_fuzzer_amd_debug, ast_fuzzer_amd_debug_targeted, ast_fuzzer_amd_debug_targeted_old_compatibility, ast_fuzzer_amd_msan, ast_fuzzer_amd_release_oracle, ast_fuzzer_amd_tsan, ast_fuzzer_arm_asan_ubsan, bugfix_validation_functional_tests_aarch64, bugfix_validation_functional_tests_amd64, bugfix_validation_integration_tests_aarch64, bugfix_validation_integration_tests_amd64, bugfix_validation_unit_tests, build_amd_asan_ubsan, build_amd_binary, build_amd_compat, build_amd_darwin, build_amd_debug, build_amd_freebsd, build_amd_fuzzers, build_amd_msan, build_amd_musl, build_amd_release, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_darwin, build_arm_debug, build_arm_msan, build_arm_release, build_arm_tidy, build_arm_tsan, build_arm_v80compat, build_llvm_coverage_build, build_loongarch64, build_ppc64le, build_profile_diff, build_riscv64, build_s390x, build_toolchain_pgo_bolt_aarch64, build_toolchain_pgo_bolt_amd64, build_wasm64, build_wasm_parser, buzzhouse_amd_debug, buzzhouse_amd_msan, buzzhouse_amd_tsan, buzzhouse_arm_asan_ubsan, clickbench_amd_release, clickbench_arm_release, code_review, compatibility_check_amd_release, compatibility_check_arm_release, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, docs_check_mintlify, docs_examples, fast_test, fast_test_arm_darwin, install_packages_amd_release, install_packages_arm_release, integration_tests_amd_asan_ubsan_db_disk_targeted, integration_tests_amd_asan_ubsan_flaky, integration_tests_amd_binary_excluded_from_llvm, integration_tests_amd_llvm_coverage_1_8, integration_tests_amd_llvm_coverage_2_8, integration_tests_amd_llvm_coverage_3_8, integration_tests_amd_llvm_coverage_4_8, integration_tests_amd_llvm_coverage_5_8, integration_tests_amd_llvm_coverage_6_8, integration_tests_amd_llvm_coverage_7_8, integration_tests_amd_llvm_coverage_8_8, integration_tests_amd_msan_targeted, integration_tests_amd_tsan_targeted, integration_tests_arm_binary_distributed_plan_targeted, keeper_stress_tests_pr, llvm_coverage, parser_memory_check, performance_comparison_amd_release_master_head_1_6, performance_comparison_amd_release_master_head_2_6, performance_comparison_amd_release_master_head_3_6, performance_comparison_amd_release_master_head_4_6, performance_comparison_amd_release_master_head_5_6, performance_comparison_amd_release_master_head_6_6, performance_comparison_arm_release_master_head_1_6, performance_comparison_arm_release_master_head_2_6, performance_comparison_arm_release_master_head_3_6, performance_comparison_arm_release_master_head_4_6, performance_comparison_arm_release_master_head_5_6, performance_comparison_arm_release_master_head_6_6, promql_compliance, quick_functional_tests, sqllogic_test, sqlstorm_test, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_targeted, stateless_tests_amd_asan_ubsan_flaky_check, stateless_tests_amd_binary_excluded_from_llvm, stateless_tests_amd_binary_flaky_check, stateless_tests_amd_debug_distributed_plan_s3_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_sequential, stateless_tests_amd_debug_flaky_check, stateless_tests_amd_debug_parallel, stateless_tests_amd_debug_sequential, stateless_tests_amd_llvm_coverage_1_3, stateless_tests_amd_llvm_coverage_2_3, stateless_tests_amd_llvm_coverage_3_3, stateless_tests_amd_llvm_coverage_asyncinsert_s3_storage_parallel, stateless_tests_amd_llvm_coverage_asyncinsert_s3_storage_sequential, stateless_tests_amd_llvm_coverage_parallelreplicas_s3_storage_parallel, stateless_tests_amd_llvm_coverage_parallelreplicas_s3_storage_sequential, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_parallel_1_3, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_parallel_2_3, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_parallel_3_3, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_sequential_1_2, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_sequential_2_2, stateless_tests_amd_msan_flaky_check, stateless_tests_amd_tsan_flaky_check, stateless_tests_amd_tsan_s3_storage_targeted, stateless_tests_arm_asan_ubsan_azure_parallel, stateless_tests_arm_asan_ubsan_azure_sequential, stateless_tests_arm_asan_ubsan_targeted, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential, storage_memory_check, stress_test_amd_asan_ubsan, stress_test_amd_debug, stress_test_amd_msan, stress_test_amd_tsan, stress_test_arm_asan_ubsan, stress_test_arm_asan_ubsan_s3, stress_test_arm_debug, stress_test_arm_msan, stress_test_arm_release, stress_test_arm_tsan, style_check, unit_tests_amd_llvm_coverage, unit_tests_asan_ubsan, unit_tests_asan_ubsan_function_prop_fuzzer, unit_tests_msan, unit_tests_msan_function_prop_fuzzer, unit_tests_tsan, unit_tests_tsan_function_prop_fuzzer, upgrade_check_amd_release]
needs: [ast_fuzzer_amd_debug, ast_fuzzer_amd_debug_targeted, ast_fuzzer_amd_debug_targeted_old_compatibility, ast_fuzzer_amd_msan, ast_fuzzer_amd_release_oracle, ast_fuzzer_amd_tsan, ast_fuzzer_arm_asan_ubsan, bugfix_validation_functional_tests_aarch64, bugfix_validation_functional_tests_amd64, bugfix_validation_integration_tests_aarch64, bugfix_validation_integration_tests_amd64, bugfix_validation_unit_tests, build_amd_asan_ubsan, build_amd_binary, build_amd_compat, build_amd_darwin, build_amd_debug, build_amd_freebsd, build_amd_msan, build_amd_musl, build_amd_release, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_darwin, build_arm_debug, build_arm_fuzzers, build_arm_msan, build_arm_release, build_arm_tidy, build_arm_tsan, build_arm_v80compat, build_llvm_coverage_build, build_loongarch64, build_ppc64le, build_profile_diff, build_riscv64, build_s390x, build_toolchain_pgo_bolt_aarch64, build_toolchain_pgo_bolt_amd64, build_wasm64, build_wasm_parser, buzzhouse_amd_debug, buzzhouse_amd_msan, buzzhouse_amd_tsan, buzzhouse_arm_asan_ubsan, clickbench_amd_release, clickbench_arm_release, code_review, compatibility_check_amd_release, compatibility_check_arm_release, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, docs_check_mintlify, docs_examples, fast_test, fast_test_arm_darwin, install_packages_amd_release, install_packages_arm_release, integration_tests_amd_asan_ubsan_db_disk_targeted, integration_tests_amd_asan_ubsan_flaky, integration_tests_amd_binary_excluded_from_llvm, integration_tests_amd_llvm_coverage_1_8, integration_tests_amd_llvm_coverage_2_8, integration_tests_amd_llvm_coverage_3_8, integration_tests_amd_llvm_coverage_4_8, integration_tests_amd_llvm_coverage_5_8, integration_tests_amd_llvm_coverage_6_8, integration_tests_amd_llvm_coverage_7_8, integration_tests_amd_llvm_coverage_8_8, integration_tests_amd_msan_targeted, integration_tests_amd_tsan_targeted, integration_tests_arm_binary_distributed_plan_targeted, keeper_stress_tests_pr, llvm_coverage, parser_memory_check, performance_comparison_amd_release_master_head_1_6, performance_comparison_amd_release_master_head_2_6, performance_comparison_amd_release_master_head_3_6, performance_comparison_amd_release_master_head_4_6, performance_comparison_amd_release_master_head_5_6, performance_comparison_amd_release_master_head_6_6, performance_comparison_arm_release_master_head_1_6, performance_comparison_arm_release_master_head_2_6, performance_comparison_arm_release_master_head_3_6, performance_comparison_arm_release_master_head_4_6, performance_comparison_arm_release_master_head_5_6, performance_comparison_arm_release_master_head_6_6, promql_compliance, quick_functional_tests, sqllogic_test, sqlstorm_test, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_targeted, stateless_tests_amd_asan_ubsan_flaky_check, stateless_tests_amd_binary_excluded_from_llvm, stateless_tests_amd_binary_flaky_check, stateless_tests_amd_debug_distributed_plan_s3_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_sequential, stateless_tests_amd_debug_flaky_check, stateless_tests_amd_debug_parallel, stateless_tests_amd_debug_sequential, stateless_tests_amd_llvm_coverage_1_3, stateless_tests_amd_llvm_coverage_2_3, stateless_tests_amd_llvm_coverage_3_3, stateless_tests_amd_llvm_coverage_asyncinsert_s3_storage_parallel, stateless_tests_amd_llvm_coverage_asyncinsert_s3_storage_sequential, stateless_tests_amd_llvm_coverage_parallelreplicas_s3_storage_parallel, stateless_tests_amd_llvm_coverage_parallelreplicas_s3_storage_sequential, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_parallel_1_3, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_parallel_2_3, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_parallel_3_3, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_sequential_1_2, stateless_tests_amd_llvm_coverage_s3_storage_dbreplicated_sequential_2_2, stateless_tests_amd_msan_flaky_check, stateless_tests_amd_tsan_flaky_check, stateless_tests_amd_tsan_s3_storage_targeted, stateless_tests_arm_asan_ubsan_azure_parallel, stateless_tests_arm_asan_ubsan_azure_sequential, stateless_tests_arm_asan_ubsan_targeted, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential, storage_memory_check, stress_test_amd_asan_ubsan, stress_test_amd_debug, stress_test_amd_msan, stress_test_amd_tsan, stress_test_arm_asan_ubsan, stress_test_arm_asan_ubsan_s3, stress_test_arm_debug, stress_test_arm_msan, stress_test_arm_release, stress_test_arm_tsan, style_check, unit_tests_amd_llvm_coverage, unit_tests_asan_ubsan, unit_tests_asan_ubsan_function_prop_fuzzer, unit_tests_msan, unit_tests_msan_function_prop_fuzzer, unit_tests_tsan, unit_tests_tsan_function_prop_fuzzer, upgrade_check_amd_release]
if: ${{ always() }}
name: "Finish Workflow"
outputs:
+7 -7
View File
@@ -117,11 +117,11 @@ jobs:
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Dockers Build (arm)' --workflow "WeeklyFuzzersCorpus" --ci --timestamp
build_amd_fuzzers:
build_arm_fuzzers:
runs-on: [self-hosted, arm-large]
needs: [config_workflow, dockers_build_amd, dockers_build_arm]
if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'QnVpbGQgKGFtZF9mdXp6ZXJzKQ==') }}
name: "Build (amd_fuzzers)"
if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'QnVpbGQgKGFybV9mdXp6ZXJzKQ==') }}
name: "Build (arm_fuzzers)"
outputs:
data: ${{ steps.run.outputs.DATA }}
pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
@@ -149,11 +149,11 @@ jobs:
id: run
run: |
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Build (amd_fuzzers)' --workflow "WeeklyFuzzersCorpus" --ci --timestamp
PYTHONUNBUFFERED=1 python3 -m praktika run 'Build (arm_fuzzers)' --workflow "WeeklyFuzzersCorpus" --ci --timestamp
libfuzzer_corpus_minimization:
runs-on: [self-hosted, amd-medium]
needs: [build_amd_fuzzers, config_workflow, dockers_build_amd, dockers_build_arm]
runs-on: [self-hosted, arm-medium]
needs: [build_arm_fuzzers, config_workflow, dockers_build_amd, dockers_build_arm]
if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'bGliRnV6emVyIGNvcnB1cyBtaW5pbWl6YXRpb24=') }}
name: "libFuzzer corpus minimization"
outputs:
@@ -187,7 +187,7 @@ jobs:
finish_workflow:
runs-on: [self-hosted, arm-tiny]
needs: [build_amd_fuzzers, config_workflow, dockers_build_amd, dockers_build_arm, libfuzzer_corpus_minimization]
needs: [build_arm_fuzzers, config_workflow, dockers_build_amd, dockers_build_arm, libfuzzer_corpus_minimization]
if: ${{ always() }}
name: "Finish Workflow"
outputs:
+1 -1
View File
@@ -93,7 +93,7 @@
url = https://github.com/ClickHouse/icu
[submodule "contrib/flatbuffers"]
path = contrib/flatbuffers
url = https://github.com/ClickHouse/flatbuffers
url = https://github.com/google/flatbuffers
[submodule "contrib/replxx"]
path = contrib/replxx
url = https://github.com/ClickHouse/replxx
-357
View File
@@ -43,52 +43,10 @@
* Count the number of symbols of the set in a string.
*/
struct SearchSymbols
{
static constexpr auto BUFFER_SIZE = 16;
SearchSymbols() = default;
explicit SearchSymbols(std::string in)
: str(std::move(in))
{
if (str.size() > BUFFER_SIZE)
{
throw std::runtime_error("SearchSymbols may contain at most " + std::to_string(BUFFER_SIZE) + " symbols but " + std::to_string(str.size()) + " symbols were provided");
}
#if defined(__SSE4_2__)
char tmp_safety_buffer[BUFFER_SIZE] = {0};
memcpy(tmp_safety_buffer, str.data(), str.size());
simd_vector = _mm_loadu_si128(reinterpret_cast<const __m128i *>(tmp_safety_buffer));
#endif
}
#if defined(__SSE4_2__)
__m128i simd_vector{};
#endif
std::string str;
};
namespace detail
{
template <char ...chars> constexpr bool is_in(char x) { return ((x == chars) || ...); } // NOLINT(misc-redundant-expression)
static bool is_in(char c, const char * symbols, size_t num_chars)
{
for (size_t i = 0u; i < num_chars; ++i)
{
if (c == symbols[i])
{
return true;
}
}
return false;
}
#if defined(__SSE2__)
template <char s0>
inline __m128i mm_is_in(__m128i bytes)
@@ -105,50 +63,6 @@ inline __m128i mm_is_in(__m128i bytes)
return _mm_or_si128(eq0, eq);
}
inline __m128i mm_is_in(__m128i bytes, const char * symbols, size_t num_chars)
{
__m128i accumulator = _mm_setzero_si128();
for (size_t i = 0; i < num_chars; ++i)
{
__m128i eq = _mm_cmpeq_epi8(bytes, _mm_set1_epi8(symbols[i]));
accumulator = _mm_or_si128(accumulator, eq);
}
return accumulator;
}
inline std::array<__m128i, 16u> mm_is_in_prepare(const char * symbols, size_t num_chars)
{
std::array<__m128i, 16u> result {};
for (size_t i = 0; i < num_chars; ++i)
{
result[i] = _mm_set1_epi8(symbols[i]);
}
/// Pad unused slots with a repeat of an actual needle byte so `mm_is_in_execute`
/// does not spuriously match `\0` bytes in the haystack via the zero-initialised
/// slots. Callers ensure `num_chars >= 1` before reaching the SIMD body.
for (size_t i = num_chars; i < 16u; ++i)
{
result[i] = result[0];
}
return result;
}
inline __m128i mm_is_in_execute(__m128i bytes, const std::array<__m128i, 16u> & needles)
{
__m128i accumulator = _mm_setzero_si128();
for (const auto & needle : needles)
{
__m128i eq = _mm_cmpeq_epi8(bytes, needle);
accumulator = _mm_or_si128(accumulator, eq);
}
return accumulator;
}
#endif
#if defined(__aarch64__)
@@ -171,18 +85,6 @@ inline uint8x16_t neon_is_in(uint8x16_t bytes)
return vorrq_u8(eq0, eq);
}
inline uint8x16_t neon_is_in(uint8x16_t bytes, const char * symbols, size_t num_chars)
{
uint8x16_t accumulator = vdupq_n_u8(0);
for (size_t i = 0; i < num_chars; ++i)
{
uint8x16_t eq = vceqq_u8(bytes, vdupq_n_u8(static_cast<uint8_t>(symbols[i])));
accumulator = vorrq_u8(accumulator, eq);
}
return accumulator;
}
/// Compresses a 16-byte all-0/all-1 vector into a 64-bit value where each input
/// byte occupies a 4-bit nibble (all bits set if matched, all clear otherwise).
inline uint64_t neon_to_bitmask(uint8x16_t eq)
@@ -297,86 +199,6 @@ inline const char * find_first_symbols_sse2(const char * const begin, const char
return return_mode == ReturnMode::End ? end : nullptr;
}
#if defined(__aarch64__)
/// Runtime-needle NEON body for long haystacks. Always returns either a
/// matching pointer or `end` (treated by the caller as "no match"). Kept
/// out-of-line so short-haystack callers stay free of the SIMD prologue
/// (no stack frame, no extra branches).
template <bool positive>
[[gnu::noinline]] const char * find_first_symbols_neon_long_rt(const char * pos, const char * const end, const char * symbols, size_t num_chars)
{
if (maybe_negate<positive>(is_in(pos[0], symbols, num_chars))) return pos;
if (maybe_negate<positive>(is_in(pos[1], symbols, num_chars))) return pos + 1;
if (maybe_negate<positive>(is_in(pos[2], symbols, num_chars))) return pos + 2;
if (maybe_negate<positive>(is_in(pos[3], symbols, num_chars))) return pos + 3;
if (maybe_negate<positive>(is_in(pos[4], symbols, num_chars))) return pos + 4;
if (maybe_negate<positive>(is_in(pos[5], symbols, num_chars))) return pos + 5;
if (maybe_negate<positive>(is_in(pos[6], symbols, num_chars))) return pos + 6;
if (maybe_negate<positive>(is_in(pos[7], symbols, num_chars))) return pos + 7;
pos += 8;
for (; pos + 15 < end; pos += 16)
{
uint8x16_t bytes = vld1q_u8(reinterpret_cast<const uint8_t *>(pos));
uint8x16_t eq = maybe_negate<positive>(neon_is_in(bytes, symbols, num_chars));
uint64_t bit_mask = neon_to_bitmask(eq);
if (bit_mask)
return pos + (__builtin_ctzll(bit_mask) >> 2);
}
for (; pos < end; ++pos)
if (maybe_negate<positive>(is_in(*pos, symbols, num_chars)))
return pos;
return end;
}
#endif
template <bool positive, ReturnMode return_mode>
inline const char * find_first_symbols_sse2(const char * const begin, const char * const end, const char * symbols, size_t num_chars)
{
const char * pos = begin;
#if defined(__SSE2__)
if (pos + 15 < end)
{
const auto needles = mm_is_in_prepare(symbols, num_chars);
for (; pos + 15 < end; pos += 16)
{
__m128i bytes = _mm_loadu_si128(reinterpret_cast<const __m128i *>(pos));
__m128i eq = mm_is_in_execute(bytes, needles);
uint16_t bit_mask = maybe_negate<positive>(uint16_t(_mm_movemask_epi8(eq)));
if (bit_mask)
return pos + __builtin_ctz(bit_mask);
}
}
#elif defined(__aarch64__)
/// See the compile-time variant above for why the SIMD body is out of line.
/// `neon_is_in` (rather than the prepare/execute pair) avoids forcing the
/// compiler to reserve stack space for a `std::array<uint8x16_t, 16>` in
/// every call to this function.
if (end - begin >= 16) [[unlikely]]
{
const char * found = find_first_symbols_neon_long_rt<positive>(begin, end, symbols, num_chars);
if (found != end)
return found;
return return_mode == ReturnMode::End ? end : nullptr;
}
#endif
for (; pos < end; ++pos)
if (maybe_negate<positive>(is_in(*pos, symbols, num_chars)))
return pos;
return return_mode == ReturnMode::End ? end : nullptr;
}
#if defined(__aarch64__)
/// See the forward variant above. Out-of-lined for the same reason.
/// Always returns either a matching pointer or `nullptr`; the inline caller
@@ -449,83 +271,6 @@ inline const char * find_last_symbols_sse2(const char * const begin, const char
return return_mode == ReturnMode::End ? end : nullptr;
}
#if defined(__aarch64__)
/// Runtime-needle NEON body for long haystacks (find_last). Returns either a
/// matching pointer or `nullptr`. Kept out-of-line for the same reasons as
/// the forward variant.
template <bool positive>
[[gnu::noinline]] const char * find_last_symbols_neon_long_rt(const char * const begin, const char * pos, const char * symbols, size_t num_chars)
{
if (maybe_negate<positive>(is_in(pos[-1], symbols, num_chars))) return pos - 1;
if (maybe_negate<positive>(is_in(pos[-2], symbols, num_chars))) return pos - 2;
if (maybe_negate<positive>(is_in(pos[-3], symbols, num_chars))) return pos - 3;
if (maybe_negate<positive>(is_in(pos[-4], symbols, num_chars))) return pos - 4;
if (maybe_negate<positive>(is_in(pos[-5], symbols, num_chars))) return pos - 5;
if (maybe_negate<positive>(is_in(pos[-6], symbols, num_chars))) return pos - 6;
if (maybe_negate<positive>(is_in(pos[-7], symbols, num_chars))) return pos - 7;
if (maybe_negate<positive>(is_in(pos[-8], symbols, num_chars))) return pos - 8;
pos -= 8;
for (; pos - 16 >= begin; pos -= 16)
{
uint8x16_t bytes = vld1q_u8(reinterpret_cast<const uint8_t *>(pos - 16));
uint8x16_t eq = maybe_negate<positive>(neon_is_in(bytes, symbols, num_chars));
uint64_t bit_mask = neon_to_bitmask(eq);
if (bit_mask)
return pos - 1 - (__builtin_clzll(bit_mask) >> 2);
}
--pos;
for (; pos >= begin; --pos)
if (maybe_negate<positive>(is_in(*pos, symbols, num_chars)))
return pos;
return nullptr;
}
#endif
template <bool positive, ReturnMode return_mode>
inline const char * find_last_symbols_sse2(const char * const begin, const char * const end, const char * symbols, size_t num_chars)
{
const char * pos = end;
#if defined(__SSE2__)
if (pos - 16 >= begin)
{
const auto needles = mm_is_in_prepare(symbols, num_chars);
for (; pos - 16 >= begin; pos -= 16)
{
__m128i bytes = _mm_loadu_si128(reinterpret_cast<const __m128i *>(pos - 16));
__m128i eq = mm_is_in_execute(bytes, needles);
uint16_t bit_mask = maybe_negate<positive>(uint16_t(_mm_movemask_epi8(eq)));
if (bit_mask)
return pos - 1 - (__builtin_clz(bit_mask) - 16); /// because __builtin_clz works with mask as uint32.
}
}
#elif defined(__aarch64__)
/// See the forward variant above. Out-of-lined for the same reason.
if (end - begin >= 16) [[unlikely]]
{
const char * found = find_last_symbols_neon_long_rt<positive>(begin, end, symbols, num_chars);
if (found)
return found;
return return_mode == ReturnMode::End ? end : nullptr;
}
#endif
--pos;
for (; pos >= begin; --pos)
if (maybe_negate<positive>(is_in(*pos, symbols, num_chars)))
return pos;
return return_mode == ReturnMode::End ? end : nullptr;
}
template <bool positive, ReturnMode return_mode, size_t num_chars,
char c01, char c02 = 0, char c03 = 0, char c04 = 0,
char c05 = 0, char c06 = 0, char c07 = 0, char c08 = 0,
@@ -578,42 +323,6 @@ inline const char * find_first_symbols_sse42(const char * const begin, const cha
return return_mode == ReturnMode::End ? end : nullptr;
}
template <bool positive, ReturnMode return_mode>
inline const char * find_first_symbols_sse42(const char * const begin, const char * const end, const SearchSymbols & symbols)
{
const char * pos = begin;
const auto num_chars = symbols.str.size();
#if defined(__SSE4_2__)
constexpr int mode = _SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY | _SIDD_LEAST_SIGNIFICANT;
const __m128i set = symbols.simd_vector;
for (; pos + 15 < end; pos += 16)
{
__m128i bytes = _mm_loadu_si128(reinterpret_cast<const __m128i *>(pos));
if constexpr (positive)
{
if (_mm_cmpestrc(set, num_chars, bytes, 16, mode))
return pos + _mm_cmpestri(set, num_chars, bytes, 16, mode);
}
else
{
if (_mm_cmpestrc(set, num_chars, bytes, 16, mode | _SIDD_NEGATIVE_POLARITY))
return pos + _mm_cmpestri(set, num_chars, bytes, 16, mode | _SIDD_NEGATIVE_POLARITY);
}
}
#endif
for (; pos < end; ++pos)
if (maybe_negate<positive>(is_in(*pos, symbols.str.data(), num_chars)))
return pos;
return return_mode == ReturnMode::End ? end : nullptr;
}
/// NOTE No SSE 4.2 implementation for find_last_symbols_or_null. Not worth to do.
template <bool positive, ReturnMode return_mode, char... symbols>
@@ -627,28 +336,6 @@ inline const char * find_first_symbols_dispatch(const char * begin, const char *
return find_first_symbols_sse2<positive, return_mode, symbols...>(begin, end);
}
template <bool positive, ReturnMode return_mode>
inline const char * find_first_symbols_dispatch(const std::string_view haystack, const SearchSymbols & symbols)
{
/// Empty needle: no byte is in the (empty) symbol set. For positive search
/// nothing is found; for the negative variant every byte qualifies and we
/// return the first one. Bypassing the SIMD body here keeps `mm_is_in_prepare`
/// and `neon_is_in` free of an extra branch and guards against the zero-pad
/// degenerate case (a zero-filled needle vector would otherwise match `\0`).
if (symbols.str.empty())
{
if constexpr (!positive)
if (!haystack.empty())
return haystack.data();
return return_mode == ReturnMode::End ? haystack.data() + haystack.size() : nullptr;
}
#if defined(__SSE4_2__)
if (symbols.str.size() >= 5)
return find_first_symbols_sse42<positive, return_mode>(haystack.data(), haystack.data() + haystack.size(), symbols);
#endif
return find_first_symbols_sse2<positive, return_mode>(haystack.data(), haystack.data() + haystack.size(), symbols.str.data(), symbols.str.size());
}
template <bool positive, ReturnMode return_mode, char... symbols>
inline const char * find_last_symbols_dispatch(const char * begin, const char * end)
requires(0 <= sizeof...(symbols) && sizeof...(symbols) <= 16)
@@ -656,20 +343,6 @@ inline const char * find_last_symbols_dispatch(const char * begin, const char *
return find_last_symbols_sse2<positive, return_mode, symbols...>(begin, end);
}
template <bool positive, ReturnMode return_mode>
inline const char * find_last_symbols_dispatch(const std::string_view haystack, const SearchSymbols & symbols)
{
/// See the forward dispatcher above.
if (symbols.str.empty())
{
if constexpr (!positive)
if (!haystack.empty())
return haystack.data() + haystack.size() - 1;
return return_mode == ReturnMode::End ? haystack.data() + haystack.size() : nullptr;
}
return find_last_symbols_sse2<positive, return_mode>(haystack.data(), haystack.data() + haystack.size(), symbols.str.data(), symbols.str.size());
}
}
@@ -687,11 +360,6 @@ inline char * find_first_symbols(char * begin, char * end)
return const_cast<char *>(detail::find_first_symbols_dispatch<true, detail::ReturnMode::End, symbols...>(begin, end));
}
inline const char * find_first_symbols(std::string_view haystack, const SearchSymbols & symbols)
{
return detail::find_first_symbols_dispatch<true, detail::ReturnMode::End>(haystack, symbols);
}
template <char... symbols>
inline const char * find_first_not_symbols(const char * begin, const char * end)
{
@@ -704,11 +372,6 @@ inline char * find_first_not_symbols(char * begin, char * end)
return const_cast<char *>(detail::find_first_symbols_dispatch<false, detail::ReturnMode::End, symbols...>(begin, end));
}
inline const char * find_first_not_symbols(std::string_view haystack, const SearchSymbols & symbols)
{
return detail::find_first_symbols_dispatch<false, detail::ReturnMode::End>(haystack, symbols);
}
template <char... symbols>
inline const char * find_first_symbols_or_null(const char * begin, const char * end)
{
@@ -721,11 +384,6 @@ inline char * find_first_symbols_or_null(char * begin, char * end)
return const_cast<char *>(detail::find_first_symbols_dispatch<true, detail::ReturnMode::Nullptr, symbols...>(begin, end));
}
inline const char * find_first_symbols_or_null(std::string_view haystack, const SearchSymbols & symbols)
{
return detail::find_first_symbols_dispatch<true, detail::ReturnMode::Nullptr>(haystack, symbols);
}
template <char... symbols>
inline const char * find_first_not_symbols_or_null(const char * begin, const char * end)
{
@@ -738,11 +396,6 @@ inline char * find_first_not_symbols_or_null(char * begin, char * end)
return const_cast<char *>(detail::find_first_symbols_dispatch<false, detail::ReturnMode::Nullptr, symbols...>(begin, end));
}
inline const char * find_first_not_symbols_or_null(std::string_view haystack, const SearchSymbols & symbols)
{
return detail::find_first_symbols_dispatch<false, detail::ReturnMode::Nullptr>(haystack, symbols);
}
template <char... symbols>
inline const char * find_last_symbols_or_null(const char * begin, const char * end)
{
@@ -755,11 +408,6 @@ inline char * find_last_symbols_or_null(char * begin, char * end)
return const_cast<char *>(detail::find_last_symbols_dispatch<true, detail::ReturnMode::Nullptr, symbols...>(begin, end));
}
inline const char * find_last_symbols_or_null(std::string_view haystack, const SearchSymbols & symbols)
{
return detail::find_last_symbols_dispatch<true, detail::ReturnMode::Nullptr>(haystack, symbols);
}
template <char... symbols>
inline const char * find_last_not_symbols_or_null(const char * begin, const char * end)
{
@@ -772,11 +420,6 @@ inline char * find_last_not_symbols_or_null(char * begin, char * end)
return const_cast<char *>(detail::find_last_symbols_dispatch<false, detail::ReturnMode::Nullptr, symbols...>(begin, end));
}
inline const char * find_last_not_symbols_or_null(std::string_view haystack, const SearchSymbols & symbols)
{
return detail::find_last_symbols_dispatch<false, detail::ReturnMode::Nullptr>(haystack, symbols);
}
template <char... symbols>
inline size_t count_symbols(const char * begin, const char * end)
{
+3 -3
View File
@@ -398,7 +398,7 @@ class BuildTypes(metaclass=MetaClasses.WithIter):
# browser. A CMake project of its own rather than a target of this tree, with its own
# toolchain and its own job script - see `build_wasm_parser.py`.
WASM_PARSER = "wasm_parser"
AMD_FUZZERS = "amd_fuzzers"
ARM_FUZZERS = "arm_fuzzers"
AMD_CFI = "amd_cfi"
@@ -543,7 +543,7 @@ class ArtifactNames:
TGZ_AMD_RELEASE = "TGZ_AMD_RELEASE"
TGZ_ARM_RELEASE = "TGZ_ARM_RELEASE"
AMD_FUZZERS = "AMD_FUZZERS"
ARM_FUZZERS = "ARM_FUZZERS"
FUZZERS_CORPUS = "FUZZERS_CORPUS"
CLICKHOUSE_EXAMPLES = "CLICKHOUSE_EXAMPLES"
@@ -806,7 +806,7 @@ class ArtifactConfigs:
],
)
fuzzers = Artifact.Config(
name=ArtifactNames.AMD_FUZZERS,
name=ArtifactNames.ARM_FUZZERS,
type=Artifact.Type.S3,
path=[
f"{TEMP_DIR}/build/programs/*_fuzzer",
+13 -13
View File
@@ -622,14 +622,14 @@ class JobConfigs:
runs_on=RunnerLabels.ARM_LARGE,
),
Job.ParamSet(
parameter=BuildTypes.AMD_FUZZERS,
parameter=BuildTypes.ARM_FUZZERS,
provides=[],
# The target arch comes from the toolchain file, not from the host, so this
# cross-compiles on arm like every other Linux `amd_*` build. It has to: the
# ~18 fuzzers each statically link the whole of ClickHouse with its own copy
# of the ASan+debug DWARF, ~94 GiB of build output, which does not fit in the
# ~135 GiB free on `amd-large` (`m7i.8xlarge`) and dies linking one of the
# last targets. Only the job that *runs* the binaries needs an amd64 host.
# Targets aarch64: each fuzzer statically links all of ClickHouse with ASan
# and SanitizerCoverage, and that image's allocated sections already exceed
# 2 GiB - out of reach of x86-64's 32-bit displacements, which lld cannot
# repair with thunks, while aarch64 addresses +-4 GiB and does thunk calls.
# The ~94 GiB of build output also does not fit the ~135 GiB free on
# `amd-large` (`m7i.8xlarge`).
runs_on=RunnerLabels.ARM_LARGE,
),
)
@@ -650,7 +650,7 @@ class JobConfigs:
with_git_submodules=True,
)
)
if job.parameter == BuildTypes.AMD_FUZZERS
if job.parameter == BuildTypes.ARM_FUZZERS
else job
)
for job in special_build_jobs
@@ -1962,7 +1962,7 @@ class JobConfigs:
)
libfuzzer_job = Job.Config(
name=JobNames.LIBFUZZER_TEST,
runs_on=RunnerLabels.AMD_MEDIUM,
runs_on=RunnerLabels.ARM_MEDIUM,
command="python3 ./ci/jobs/libfuzzer_test_check.py 'libFuzzer tests'",
# Five hours of fuzzing per target, all targets in parallel, plus
# artifact download and corpus upload. Praktika's default is exactly
@@ -1972,9 +1972,9 @@ class JobConfigs:
# from the actual set of functions, data types and keywords. It has to be the
# binary for the arch this job runs the fuzzers on.
requires=[
ArtifactNames.AMD_FUZZERS,
ArtifactNames.ARM_FUZZERS,
ArtifactNames.FUZZERS_CORPUS,
ArtifactNames.CH_AMD_RELEASE,
ArtifactNames.CH_ARM_RELEASE,
],
digest_config=Job.CacheDigestConfig(
include_paths=[
@@ -1991,12 +1991,12 @@ class JobConfigs:
)
libfuzzer_corpus_minimization_job = Job.Config(
name=JobNames.LIBFUZZER_CORPUS_MINIMIZATION,
runs_on=RunnerLabels.AMD_MEDIUM,
runs_on=RunnerLabels.ARM_MEDIUM,
command=(
"python3 ./ci/jobs/libfuzzer_test_check.py --minimize-only "
"'libFuzzer corpus minimization'"
),
requires=[ArtifactNames.AMD_FUZZERS, ArtifactNames.FUZZERS_CORPUS],
requires=[ArtifactNames.ARM_FUZZERS, ArtifactNames.FUZZERS_CORPUS],
digest_config=Job.CacheDigestConfig(
include_paths=[
"./ci/jobs/libfuzzer_test_check.py",
+2 -2
View File
@@ -49,7 +49,7 @@ BUILD_TYPE_TO_CMAKE = {
# Emscripten's own toolchain file, which supplies `emcc`/`em++`, so no compiler is passed
# here. `emcc` is a Python driver that sccache cannot cache, hence the cache is disabled.
BuildTypes.WASM64: f" emcmake cmake --debug-trycompile -DCMAKE_VERBOSE_MAKEFILE=1 -LA -DCMAKE_BUILD_TYPE=None -DENABLE_THINLTO=0 -DSANITIZE= -DCOMPILER_CACHE=disabled -DENABLE_BUILD_PROFILING=0 -DENABLE_TESTS=0 -DENABLE_LEXER_TEST=0 -DENABLE_UTILS=0 -DCMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY=ON",
BuildTypes.AMD_FUZZERS: f" cmake --debug-trycompile -DCMAKE_VERBOSE_MAKEFILE=1 -LA -DCMAKE_BUILD_TYPE=None -DENABLE_THINLTO=0 -DSANITIZE=address -DENABLE_CHECK_HEAVY_BUILDS=1 -DBUILD_STRIPPED_BINARY=1 -DENABLE_CLICKHOUSE_SELF_EXTRACTING=1 -DCMAKE_C_COMPILER={ToolSet.COMPILER_C} -DCMAKE_CXX_COMPILER={ToolSet.COMPILER_CPP} -DCOMPILER_CACHE={ToolSet.COMPILER_CACHE} -DCMAKE_TOOLCHAIN_FILE={repo_path_normalized}/cmake/linux/toolchain-x86_64.cmake -DENABLE_BUILD_PROFILING=0 -DENABLE_TESTS=0 -DENABLE_UTILS=0 -DCMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY=ON -DENABLE_FUZZING=1 -DENABLE_PROTOBUF=1 -DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=ON -DENABLE_BUZZHOUSE=0 -DPARALLEL_LINK_JOBS=1", # TODO: fix build with -DSANITIZE_COVERAGE=1
BuildTypes.ARM_FUZZERS: f" cmake --debug-trycompile -DCMAKE_VERBOSE_MAKEFILE=1 -LA -DCMAKE_BUILD_TYPE=None -DENABLE_THINLTO=0 -DSANITIZE=address -DENABLE_CHECK_HEAVY_BUILDS=1 -DBUILD_STRIPPED_BINARY=1 -DENABLE_CLICKHOUSE_SELF_EXTRACTING=1 -DCMAKE_C_COMPILER={ToolSet.COMPILER_C} -DCMAKE_CXX_COMPILER={ToolSet.COMPILER_CPP} -DCOMPILER_CACHE={ToolSet.COMPILER_CACHE} -DCMAKE_TOOLCHAIN_FILE={repo_path_normalized}/cmake/linux/toolchain-aarch64.cmake -DENABLE_BUILD_PROFILING=0 -DENABLE_TESTS=0 -DENABLE_UTILS=0 -DCMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY=ON -DENABLE_FUZZING=1 -DENABLE_PROTOBUF=1 -DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=ON -DENABLE_BUZZHOUSE=0 -DPARALLEL_LINK_JOBS=1", # TODO: fix build with -DSANITIZE_COVERAGE=1
}
# sccache-warmup builds (MasterCI only) reuse the cmake configuration of the
@@ -417,7 +417,7 @@ def main():
files = []
if res and JobStages.BUILD in stages:
if build_type == BuildTypes.AMD_FUZZERS:
if build_type == BuildTypes.ARM_FUZZERS:
targets = "fuzzers"
elif build_type == BuildTypes.WASM64:
# The multicall `clickhouse` binary builds and links for WebAssembly, and
@@ -170,11 +170,7 @@ function setup_logs_replication()
# (~1500 columns, each with a long description) expand into a `CREATE`
# larger than the remote server's default `max_query_size` (256 KiB)
# and the parse fails with `Code: 62. Max query size exceeded`.
# Even without comments, the `bucketed` schema of `metric_log` (the default)
# has a `Map` keyed by an `Enum16` of every metric plus an `ALIAS` column per
# metric, which is still larger than 256 KiB, so raise the limit as well.
echo "$statement" | clickhouse-client --max_query_size=$((16 * 1024 * 1024)) \
--database_replicated_initial_query_timeout_sec=10 \
echo "$statement" | clickhouse-client --database_replicated_initial_query_timeout_sec=10 \
--distributed_ddl_task_timeout=30 --distributed_ddl_output_mode=throw_only_active \
"${CONNECTION_ARGS[@]:?}" || continue
+2 -2
View File
@@ -352,7 +352,7 @@ def _has_arch_sensitive_changes(changed_files):
)
# `Build (amd_fuzzers)` builds the libFuzzer targets. No job of the PR workflow uses its output
# `Build (arm_fuzzers)` builds the libFuzzer targets. No job of the PR workflow uses its output
# (the targets run in `NightlyFuzzers`), so in a PR it only checks that they still compile. A PR
# below `SMALL_PR_CHANGED_LINES` skips it with the stress tests, unless it touches the fuzz
# targets or their build: over 2026-08-20 to 2026-10-02 the build failed alone (all other builds
@@ -360,7 +360,7 @@ def _has_arch_sensitive_changes(changed_files):
# in the linker). Master builds it on every commit. `Build (wasm64)` has no consumer either, but
# keeps running: it compiles with the Emscripten clang, and in the same period it alone found
# compile errors in 4 PRs below the threshold.
SMALL_PR_SKIPPED_BUILDS = (f"{JobNames.BUILD} ({BuildTypes.AMD_FUZZERS})",)
SMALL_PR_SKIPPED_BUILDS = (f"{JobNames.BUILD} ({BuildTypes.ARM_FUZZERS})",)
assert set(SMALL_PR_SKIPPED_BUILDS) <= {
j.name for j in JobConfigs.special_build_jobs
@@ -366,13 +366,13 @@ class TestGeneratorProducesAUsableDictionary:
class TestFuzzersBuildDigestCoversItsOwnInputs:
"""`Build (amd_fuzzers)` stages inputs the shared build digest does not cover.
"""`Build (arm_fuzzers)` stages inputs the shared build digest does not cover.
Its POST_BUILD step runs tests/fuzz/build.sh, which packs the .options files,
the dictionary and seed corpora repacked from tests/queries/0_stateless into
the artifact. Without those paths in the digest, a commit changing the
dictionary wiring or the corpus takes a cache hit and every consumer -
NightlyFuzzers included - reuses a stale AMD_FUZZERS artifact.
NightlyFuzzers included - reuses a stale ARM_FUZZERS artifact.
"""
_REQUIRED = ["./tests/fuzz/", "./tests/queries/0_stateless/"]
@@ -385,7 +385,7 @@ class TestFuzzersBuildDigestCoversItsOwnInputs:
return next(
j
for j in JobConfigs.special_build_jobs
if j.parameter == BuildTypes.AMD_FUZZERS
if j.parameter == BuildTypes.ARM_FUZZERS
)
def test_staged_inputs_are_in_the_digest(self):
@@ -393,7 +393,7 @@ class TestFuzzersBuildDigestCoversItsOwnInputs:
missing = [p for p in self._REQUIRED if p not in include_paths]
assert (
missing == []
), f"Build (amd_fuzzers) stages these but does not hash them: {missing}"
), f"Build (arm_fuzzers) stages these but does not hash them: {missing}"
def test_the_shared_build_digest_does_not_already_cover_them(self):
# Negative control: the assertion above holds trivially if the shared
@@ -422,4 +422,4 @@ class TestFuzzersBuildDigestCoversItsOwnInputs:
for j in JobConfigs.special_build_jobs
if any(p in j.digest_config.include_paths for p in self._REQUIRED)
]
assert widened == [BuildTypes.AMD_FUZZERS], widened
assert widened == [BuildTypes.ARM_FUZZERS], widened
+18 -10
View File
@@ -61,7 +61,7 @@ from ci.workflows.weekly_fuzzers_corpus import ( # noqa: E402
workflow as corpus_workflow,
)
_SHARED_BUILD = "Build (amd_release)"
_SHARED_BUILD = "Build (arm_release)"
# ci/praktika/digest.py's drop list, mirrored so a change there surfaces here.
_DROPPED_FROM_DIGEST = [
@@ -176,16 +176,24 @@ class TestBuildIsSharedWithMasterCI:
)
assert _config_digest(job, untagged) != _config_digest(job, nightly_workflow)
def test_the_examples_wiring_does_not_reach_the_shared_build(self):
# It is appended to the ARM release job only, which is why the plain and
# the with-examples list agree on the AMD job the two workflows share.
def test_the_examples_wiring_reaches_both_sides_or_neither(self):
# The suffix is appended to the ARM release job only, and that is the job
# the two workflows share, so both must take it from the same list. A
# workflow switched back to the plain list would keep the name and lose
# the cache entry.
with_examples = [
j.name
for j in JobConfigs.release_build_jobs_with_examples
if "--build-examples" in j.command
]
assert with_examples, "no job takes the examples: this arm is vacuous"
assert _SHARED_BUILD not in with_examples
nightly, master = _mangled("NightlyFuzzers"), _mangled("MasterCI")
assert (
_job(nightly, _SHARED_BUILD).command == _job(master, _SHARED_BUILD).command
)
assert sorted(_job(nightly, _SHARED_BUILD).provides) == sorted(
_job(master, _SHARED_BUILD).provides
)
def test_what_the_shared_build_provides_is_declared(self):
# An artifact a job provides but the workflow does not declare has
@@ -311,13 +319,13 @@ class TestConsumerRequiresTheReleaseBinary:
_CONSUMER = "libFuzzer tests"
def test_job_config_requires_the_release_binary(self):
assert ArtifactNames.CH_AMD_RELEASE in JobConfigs.libfuzzer_job.requires
assert ArtifactNames.CH_ARM_RELEASE in JobConfigs.libfuzzer_job.requires
def test_the_workflow_job_requires_it_too(self):
# The workflow takes the shared config, but a workflow is free to
# substitute a job, so assert the instance the workflow will run.
assert (
ArtifactNames.CH_AMD_RELEASE
ArtifactNames.CH_ARM_RELEASE
in _job(nightly_workflow, self._CONSUMER).requires
)
@@ -326,7 +334,7 @@ class TestConsumerRequiresTheReleaseBinary:
# never resolve.
provided = {a for job in nightly_workflow.jobs for a in job.provides}
required = set(_job(nightly_workflow, self._CONSUMER).requires)
assert ArtifactNames.CH_AMD_RELEASE in provided
assert ArtifactNames.CH_ARM_RELEASE in provided
assert required <= provided, sorted(required - provided)
def test_dictionary_inputs_are_in_the_consumer_digest(self):
@@ -381,7 +389,7 @@ class TestOnlyTheGeneratingJobNeedsTheBinary:
job.name
for job in self._script_jobs()
if ("--minimize-only" in job.command)
== (ArtifactNames.CH_AMD_RELEASE in job.requires)
== (ArtifactNames.CH_ARM_RELEASE in job.requires)
]
assert mismatched == [], mismatched
@@ -395,7 +403,7 @@ class TestOnlyTheGeneratingJobNeedsTheBinary:
# The other half: a job cannot be given an artifact its workflow never
# produces, so this is what makes the requirement above unsatisfiable.
provided = {a for job in corpus_workflow.jobs for a in job.provides}
assert ArtifactNames.CH_AMD_RELEASE not in provided
assert ArtifactNames.CH_ARM_RELEASE not in provided
class TestLongRetentionTags:
+2 -2
View File
@@ -19,7 +19,7 @@ workflow = Workflow.Config(
engine=Workflow.Engine.GH_ACTIONS,
jobs=[
*[
j.set_provides([ArtifactNames.AMD_FUZZERS, ArtifactNames.FUZZERS_CORPUS])
j.set_provides([ArtifactNames.ARM_FUZZERS, ArtifactNames.FUZZERS_CORPUS])
for j in JobConfigs.special_build_jobs
if "fuzzers" in j.name
],
@@ -31,7 +31,7 @@ workflow = Workflow.Config(
*[
j
for j in JobConfigs.release_build_jobs_with_examples
if "amd_release" in j.name
if "arm_release" in j.name
],
JobConfigs.libfuzzer_job,
],
+1 -1
View File
@@ -23,7 +23,7 @@ workflow = Workflow.Config(
branches=[BASE_BRANCH],
jobs=[
*[
j.set_provides([ArtifactNames.AMD_FUZZERS, ArtifactNames.FUZZERS_CORPUS])
j.set_provides([ArtifactNames.ARM_FUZZERS, ArtifactNames.FUZZERS_CORPUS])
for j in JobConfigs.special_build_jobs
if "fuzzers" in j.name
],
+11 -2
View File
@@ -43,7 +43,7 @@ function(add_contrib cmake_folder)
endfunction()
# Use the original launchers for the projects that are needed for source code generation:
# protoc and llvm-tlbgen.
# protoc, flatc and llvm-tlbgen.
disable_heavy_build_check_if_needed()
disable_dummy_launchers_if_needed()
@@ -58,6 +58,15 @@ add_contrib (abseil-cpp-cmake abseil-cpp)
add_contrib (zlib-ng-cmake zlib-ng)
add_contrib (google-protobuf-cmake google-protobuf)
add_contrib (grpc-cmake grpc)
# Declared here rather than in `arrow-cmake`, because `flatbuffers-cmake` below builds `flatc` only
# when Arrow needs it, and has to see the final value on the first configure.
set (ENABLE_PARQUET_DEFAULT ${ENABLE_LIBRARIES})
if (OS_FREEBSD)
set (ENABLE_PARQUET_DEFAULT OFF)
endif()
option (ENABLE_PARQUET "Enable parquet" ${ENABLE_PARQUET_DEFAULT})
add_contrib (flatbuffers-cmake flatbuffers) # builds `flatc`, which generates the Arrow IPC bindings
add_contrib (compiler-rt-cmake llvm-project)
add_contrib (libllvmlibc-cmake llvm-project)
add_contrib (llvm-project-cmake llvm-project)
@@ -105,7 +114,7 @@ add_contrib (thrift-cmake thrift)
add_contrib (StringZilla-cmake StringZilla)
# parquet/arrow/orc
add_contrib (xsimd-cmake xsimd) # required by arrow's SIMD codepaths
add_contrib (arrow-cmake arrow) # requires: snappy, thrift, double-conversion, xsimd
add_contrib (arrow-cmake arrow) # requires: snappy, thrift, double-conversion, xsimd, flatbuffers
add_contrib (avro-cmake avro) # requires: snappy
add_contrib (openldap-cmake openldap)
add_contrib (msgpack-c-cmake msgpack-c)
+53 -23
View File
@@ -13,12 +13,7 @@
# make -j8
# {BUILD_DIR}/contrib/arrow/cpp/build/release/parquet-reader some_file.parquet
set (ENABLE_PARQUET_DEFAULT ${ENABLE_LIBRARIES})
if (OS_FREEBSD)
set (ENABLE_PARQUET_DEFAULT OFF)
endif()
option (ENABLE_PARQUET "Enable parquet" ${ENABLE_PARQUET_DEFAULT})
# `ENABLE_PARQUET` is declared in `contrib/CMakeLists.txt`, before `flatbuffers-cmake`.
if (NOT ENABLE_PARQUET)
message(STATUS "Not using parquet")
return()
@@ -97,22 +92,50 @@ if (ENABLE_ORC)
DEPENDS "${PROTO_DIR}/orc_proto.proto" ${PROTOBUF_EXECUTABLE})
endif()
# === flatbuffers
set(FLATBUFFERS_SRC_DIR "${ClickHouse_SOURCE_DIR}/contrib/flatbuffers")
set(FLATBUFFERS_BINARY_DIR "${ClickHouse_BINARY_DIR}/contrib/flatbuffers")
set(FLATBUFFERS_INCLUDE_DIR "${FLATBUFFERS_SRC_DIR}/include")
# === Arrow IPC metadata bindings
# Arrow's pre-generated `generated/*_generated.h` are pinned by a `static_assert` to the FlatBuffers
# version they were produced with, so regenerate them with our `flatc`. The flags are from Arrow's
# `cpp/build-support/update-flatbuffers.sh`, minus `--scoped-enums`, which Arrow's code does not use.
set(FLATBUFFERS_SRCS
${FLATBUFFERS_SRC_DIR}/src/idl_parser.cpp
${FLATBUFFERS_SRC_DIR}/src/idl_gen_text.cpp
${FLATBUFFERS_SRC_DIR}/src/reflection.cpp
${FLATBUFFERS_SRC_DIR}/src/util.cpp)
if (NOT CMAKE_HOST_SYSTEM_NAME STREQUAL CMAKE_SYSTEM_NAME OR NOT CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL CMAKE_SYSTEM_PROCESSOR OR USE_MUSL)
set(FLATC_EXECUTABLE "${PROJECT_BINARY_DIR}/native/contrib/flatbuffers-cmake/flatc")
else ()
set(FLATC_EXECUTABLE $<TARGET_FILE:flatc>)
endif()
add_library(_flatbuffers STATIC ${FLATBUFFERS_SRCS})
# SYSTEM so the FlatBuffers headers don't trip ClickHouse's -Werror -Weverything (e.g. -Wdocumentation)
# in consumers that include them directly, such as the native Arrow IPC reader/writer.
target_include_directories(_flatbuffers SYSTEM PUBLIC ${FLATBUFFERS_INCLUDE_DIR})
target_compile_definitions(_flatbuffers PRIVATE -DFLATBUFFERS_LOCALE_INDEPENDENT=0)
set(ARROW_FORMAT_DIR "${ClickHouse_SOURCE_DIR}/contrib/arrow/format")
# The bindings live in a "generated" subdirectory of the include directory, because that is how
# Arrow's sources spell them: #include "generated/Message_generated.h".
set(ARROW_IPC_GENERATED_INCLUDE_DIR "${ClickHouse_BINARY_DIR}/contrib/flatbuffers")
set(ARROW_IPC_GENERATED_DIR "${ARROW_IPC_GENERATED_INCLUDE_DIR}/generated")
set(ARROW_FBS
${ARROW_FORMAT_DIR}/File.fbs
${ARROW_FORMAT_DIR}/Message.fbs
${ARROW_FORMAT_DIR}/Schema.fbs
${ARROW_FORMAT_DIR}/SparseTensor.fbs
${ARROW_FORMAT_DIR}/Tensor.fbs
${ARROW_SRC_DIR}/arrow/ipc/feather.fbs)
set(ARROW_IPC_GENERATED_HEADERS
${ARROW_IPC_GENERATED_DIR}/File_generated.h
${ARROW_IPC_GENERATED_DIR}/Message_generated.h
${ARROW_IPC_GENERATED_DIR}/Schema_generated.h
${ARROW_IPC_GENERATED_DIR}/SparseTensor_generated.h
${ARROW_IPC_GENERATED_DIR}/Tensor_generated.h
${ARROW_IPC_GENERATED_DIR}/feather_generated.h)
add_custom_command(OUTPUT ${ARROW_IPC_GENERATED_HEADERS}
COMMAND ${CMAKE_COMMAND} -E make_directory "${ARROW_IPC_GENERATED_DIR}"
COMMAND ${FLATC_EXECUTABLE}
--cpp
--cpp-std c++11
--no-warnings
-o "${ARROW_IPC_GENERATED_DIR}"
${ARROW_FBS}
DEPENDS ${ARROW_FBS} ${FLATC_EXECUTABLE})
add_custom_target(_arrow_ipc_generated DEPENDS ${ARROW_IPC_GENERATED_HEADERS})
# Interface target exposing only the FlatBuffers-generated Arrow IPC metadata headers
# (Message_generated.h, Schema_generated.h, File_generated.h) plus the FlatBuffers library.
@@ -121,8 +144,9 @@ target_compile_definitions(_flatbuffers PRIVATE -DFLATBUFFERS_LOCALE_INDEPENDENT
# the buffers directly to/from ClickHouse columns. The include base ".../cpp/src" lets the code
# use <generated/Message_generated.h> while the generated headers' own relative includes resolve.
add_library(_arrow_ipc_metadata INTERFACE)
target_include_directories(_arrow_ipc_metadata SYSTEM INTERFACE "${ClickHouse_SOURCE_DIR}/contrib/arrow/cpp/src")
target_link_libraries(_arrow_ipc_metadata INTERFACE _flatbuffers)
target_include_directories(_arrow_ipc_metadata SYSTEM INTERFACE "${ARROW_IPC_GENERATED_INCLUDE_DIR}" "${ARROW_SRC_DIR}")
target_link_libraries(_arrow_ipc_metadata INTERFACE ch_contrib::flatbuffers)
add_dependencies(_arrow_ipc_metadata _arrow_ipc_generated)
add_library(ch_contrib::arrow_ipc_metadata ALIAS _arrow_ipc_metadata)
@@ -561,7 +585,7 @@ add_library(ch_contrib::arrow ALIAS _arrow)
target_link_libraries(_arrow PRIVATE
boost::filesystem
_flatbuffers
ch_contrib::flatbuffers
ch_contrib::double_conversion
@@ -583,6 +607,9 @@ endif()
target_include_directories(_arrow SYSTEM BEFORE PUBLIC ${ARROW_SRC_DIR})
target_include_directories(_arrow SYSTEM BEFORE PUBLIC "${ClickHouse_SOURCE_DIR}/contrib/arrow-cmake/cpp/src")
# Must come before ${ARROW_SRC_DIR}, so that our generated bindings shadow the ones Arrow ships.
target_include_directories(_arrow SYSTEM BEFORE PUBLIC ${ARROW_IPC_GENERATED_INCLUDE_DIR})
add_dependencies(_arrow _arrow_ipc_generated)
target_include_directories(_arrow SYSTEM PRIVATE ${HDFS_INCLUDE_DIR})
@@ -666,8 +693,11 @@ add_library(_parquet ${PARQUET_SRCS})
add_library(ch_contrib::parquet ALIAS _parquet)
target_include_directories(_parquet SYSTEM BEFORE
PUBLIC
# Same as for `_arrow`: must come before the Arrow source directory.
"${ARROW_IPC_GENERATED_INCLUDE_DIR}"
"${ClickHouse_SOURCE_DIR}/contrib/arrow/cpp/src"
"${CMAKE_CURRENT_SOURCE_DIR}/cpp/src")
add_dependencies(_parquet _arrow_ipc_generated)
target_link_libraries(_parquet
PUBLIC
_arrow
+1 -1
+34 -29
View File
@@ -9,35 +9,41 @@ set(BROTLI_SOURCE_DIR "${ClickHouse_SOURCE_DIR}/contrib/brotli/c")
set(BROTLI_BINARY_DIR "${ClickHouse_BINARY_DIR}/contrib/brotli/c")
set(SRCS
"${BROTLI_SOURCE_DIR}/enc/command.c"
"${BROTLI_SOURCE_DIR}/enc/fast_log.c"
"${BROTLI_SOURCE_DIR}/dec/bit_reader.c"
"${BROTLI_SOURCE_DIR}/dec/state.c"
"${BROTLI_SOURCE_DIR}/dec/huffman.c"
"${BROTLI_SOURCE_DIR}/dec/decode.c"
"${BROTLI_SOURCE_DIR}/enc/encode.c"
"${BROTLI_SOURCE_DIR}/enc/dictionary_hash.c"
"${BROTLI_SOURCE_DIR}/enc/cluster.c"
"${BROTLI_SOURCE_DIR}/enc/entropy_encode.c"
"${BROTLI_SOURCE_DIR}/enc/literal_cost.c"
"${BROTLI_SOURCE_DIR}/enc/compress_fragment_two_pass.c"
"${BROTLI_SOURCE_DIR}/enc/static_dict.c"
"${BROTLI_SOURCE_DIR}/enc/compress_fragment.c"
"${BROTLI_SOURCE_DIR}/enc/block_splitter.c"
"${BROTLI_SOURCE_DIR}/enc/backward_references_hq.c"
"${BROTLI_SOURCE_DIR}/enc/histogram.c"
"${BROTLI_SOURCE_DIR}/enc/brotli_bit_stream.c"
"${BROTLI_SOURCE_DIR}/enc/utf8_util.c"
"${BROTLI_SOURCE_DIR}/enc/encoder_dict.c"
"${BROTLI_SOURCE_DIR}/enc/backward_references.c"
"${BROTLI_SOURCE_DIR}/enc/bit_cost.c"
"${BROTLI_SOURCE_DIR}/enc/metablock.c"
"${BROTLI_SOURCE_DIR}/enc/memory.c"
"${BROTLI_SOURCE_DIR}/common/dictionary.c"
"${BROTLI_SOURCE_DIR}/common/transform.c"
"${BROTLI_SOURCE_DIR}/common/platform.c"
"${BROTLI_SOURCE_DIR}/common/context.c"
"${BROTLI_SOURCE_DIR}/common/constants.c"
"${BROTLI_SOURCE_DIR}/common/context.c"
"${BROTLI_SOURCE_DIR}/common/dictionary.c"
"${BROTLI_SOURCE_DIR}/common/platform.c"
"${BROTLI_SOURCE_DIR}/common/shared_dictionary.c"
"${BROTLI_SOURCE_DIR}/common/transform.c"
"${BROTLI_SOURCE_DIR}/dec/bit_reader.c"
"${BROTLI_SOURCE_DIR}/dec/decode.c"
"${BROTLI_SOURCE_DIR}/dec/huffman.c"
"${BROTLI_SOURCE_DIR}/dec/prefix.c"
"${BROTLI_SOURCE_DIR}/dec/state.c"
"${BROTLI_SOURCE_DIR}/dec/static_init.c"
"${BROTLI_SOURCE_DIR}/enc/backward_references.c"
"${BROTLI_SOURCE_DIR}/enc/backward_references_hq.c"
"${BROTLI_SOURCE_DIR}/enc/bit_cost.c"
"${BROTLI_SOURCE_DIR}/enc/block_splitter.c"
"${BROTLI_SOURCE_DIR}/enc/brotli_bit_stream.c"
"${BROTLI_SOURCE_DIR}/enc/cluster.c"
"${BROTLI_SOURCE_DIR}/enc/command.c"
"${BROTLI_SOURCE_DIR}/enc/compound_dictionary.c"
"${BROTLI_SOURCE_DIR}/enc/compress_fragment.c"
"${BROTLI_SOURCE_DIR}/enc/compress_fragment_two_pass.c"
"${BROTLI_SOURCE_DIR}/enc/dictionary_hash.c"
"${BROTLI_SOURCE_DIR}/enc/encode.c"
"${BROTLI_SOURCE_DIR}/enc/encoder_dict.c"
"${BROTLI_SOURCE_DIR}/enc/entropy_encode.c"
"${BROTLI_SOURCE_DIR}/enc/fast_log.c"
"${BROTLI_SOURCE_DIR}/enc/histogram.c"
"${BROTLI_SOURCE_DIR}/enc/literal_cost.c"
"${BROTLI_SOURCE_DIR}/enc/memory.c"
"${BROTLI_SOURCE_DIR}/enc/metablock.c"
"${BROTLI_SOURCE_DIR}/enc/static_dict.c"
"${BROTLI_SOURCE_DIR}/enc/static_dict_lut.c"
"${BROTLI_SOURCE_DIR}/enc/static_init.c"
"${BROTLI_SOURCE_DIR}/enc/utf8_util.c"
)
add_library(_brotli ${SRCS})
@@ -45,4 +51,3 @@ add_library(ch_contrib::brotli ALIAS _brotli)
target_include_directories(_brotli SYSTEM BEFORE PUBLIC "${BROTLI_SOURCE_DIR}/include")
target_compile_definitions(_brotli PRIVATE BROTLI_BUILD_PORTABLE=1)
+1 -1
+89
View File
@@ -0,0 +1,89 @@
# FlatBuffers: the runtime library, used by the native Arrow IPC reader/writer and by Arrow itself,
# and the `flatc` schema compiler, which generates the Arrow IPC metadata bindings at build time
# (see `contrib/arrow-cmake`).
#
# Because `flatc` runs during the build, this directory is added from the window in
# `contrib/CMakeLists.txt` where the real compiler and linker are used even in clang-tidy builds,
# next to `protoc`. Elsewhere the dummy launchers would leave `flatc` as an empty file. The
# launchers are switched per directory (`CMAKE_AR` and `CMAKE_EXE_LINKER_FLAGS` are read when a
# directory is done), so the whole directory has to sit inside that window; wrapping just the
# targets does not work.
set(LIBRARY_DIR "${ClickHouse_SOURCE_DIR}/contrib/flatbuffers")
set(FLATBUFFERS_SRCS
${LIBRARY_DIR}/src/idl_parser.cpp
${LIBRARY_DIR}/src/file_manager.cpp
${LIBRARY_DIR}/src/file_name_manager.cpp
${LIBRARY_DIR}/src/idl_gen_text.cpp
${LIBRARY_DIR}/src/reflection.cpp
${LIBRARY_DIR}/src/util.cpp)
add_library(_flatbuffers STATIC ${FLATBUFFERS_SRCS})
# SYSTEM so the FlatBuffers headers don't trip ClickHouse's -Werror -Weverything (e.g. -Wdocumentation)
# in consumers that include them directly, such as the native Arrow IPC reader/writer.
target_include_directories(_flatbuffers SYSTEM PUBLIC "${LIBRARY_DIR}/include")
target_compile_definitions(_flatbuffers PRIVATE -DFLATBUFFERS_LOCALE_INDEPENDENT=0)
add_library(ch_contrib::flatbuffers ALIAS _flatbuffers)
# Only Arrow needs `flatc`. Skip it otherwise: when cross-compiling it is a native target, and the
# native sub-build cannot work on every platform (for WebAssembly it would get `emcc` as its host
# compiler).
if (NOT ENABLE_PARQUET)
return()
endif()
# === flatc
#
# Generated FlatBuffers code contains a `static_assert` on `FLATBUFFERS_VERSION_MAJOR`/`MINOR`/
# `REVISION` that pins it to the exact FlatBuffers release it was produced with, so pre-generated
# bindings (such as the ones Arrow ships) cannot be compiled against any other version of the
# library. Build the compiler and regenerate the bindings from the schemas instead.
set(FLATC_SRCS
${LIBRARY_DIR}/src/annotated_binary_text_gen.cpp
${LIBRARY_DIR}/src/bfbs_gen_lua.cpp
${LIBRARY_DIR}/src/bfbs_gen_nim.cpp
${LIBRARY_DIR}/src/binary_annotator.cpp
${LIBRARY_DIR}/src/code_generators.cpp
${LIBRARY_DIR}/src/flatc.cpp
${LIBRARY_DIR}/src/flatc_main.cpp
${LIBRARY_DIR}/src/idl_gen_binary.cpp
${LIBRARY_DIR}/src/idl_gen_cpp.cpp
${LIBRARY_DIR}/src/idl_gen_csharp.cpp
${LIBRARY_DIR}/src/idl_gen_dart.cpp
${LIBRARY_DIR}/src/idl_gen_fbs.cpp
${LIBRARY_DIR}/src/idl_gen_go.cpp
${LIBRARY_DIR}/src/idl_gen_grpc.cpp
${LIBRARY_DIR}/src/idl_gen_java.cpp
${LIBRARY_DIR}/src/idl_gen_json_schema.cpp
${LIBRARY_DIR}/src/idl_gen_kotlin.cpp
${LIBRARY_DIR}/src/idl_gen_kotlin_kmp.cpp
${LIBRARY_DIR}/src/idl_gen_lobster.cpp
${LIBRARY_DIR}/src/idl_gen_php.cpp
${LIBRARY_DIR}/src/idl_gen_python.cpp
${LIBRARY_DIR}/src/idl_gen_rust.cpp
${LIBRARY_DIR}/src/idl_gen_swift.cpp
${LIBRARY_DIR}/src/idl_gen_ts.cpp
${LIBRARY_DIR}/include/codegen/python.cc
${LIBRARY_DIR}/grpc/src/compiler/cpp_generator.cc
${LIBRARY_DIR}/grpc/src/compiler/go_generator.cc
${LIBRARY_DIR}/grpc/src/compiler/java_generator.cc
${LIBRARY_DIR}/grpc/src/compiler/python_generator.cc
${LIBRARY_DIR}/grpc/src/compiler/swift_generator.cc
${LIBRARY_DIR}/grpc/src/compiler/ts_generator.cc)
add_executable(flatc ${FLATC_SRCS})
target_link_libraries(flatc PRIVATE _flatbuffers)
target_include_directories(flatc SYSTEM PRIVATE
"${LIBRARY_DIR}/include"
"${LIBRARY_DIR}"
"${LIBRARY_DIR}/grpc")
target_compile_definitions(flatc PRIVATE -DFLATBUFFERS_LOCALE_INDEPENDENT=0)
# When cross-compiling, `flatc` has to run on the host, so it is built by the native sub-build
# (see `add_native_target` in the top-level `CMakeLists.txt`), same as `protoc`.
add_native_target (flatc)
if (NOT CMAKE_HOST_SYSTEM_NAME STREQUAL CMAKE_SYSTEM_NAME OR NOT CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL CMAKE_SYSTEM_PROCESSOR OR USE_MUSL)
set_target_properties (flatc PROPERTIES IMPORTED_LOCATION "${PROJECT_BINARY_DIR}/native/contrib/flatbuffers-cmake/flatc")
endif ()
@@ -16,7 +16,6 @@ set (SRCS
"${SRC_DIR}/json_reporter.cc"
"${SRC_DIR}/perf_counters.cc"
"${SRC_DIR}/reporter.cc"
"${SRC_DIR}/sleep.cc"
"${SRC_DIR}/statistics.cc"
"${SRC_DIR}/string_util.cc"
"${SRC_DIR}/sysinfo.cc"
+1 -1
+1 -1
View File
@@ -38,7 +38,7 @@ RUN arch=${TARGETARCH:-amd64} \
# lts / testing / prestable / etc
ARG REPO_CHANNEL="stable"
ARG REPOSITORY="https://packages.clickhouse.com/tgz/${REPO_CHANNEL}"
ARG VERSION="26.9.10.4"
ARG VERSION="26.9.11.2"
ARG PACKAGES="clickhouse-keeper"
ARG DIRECT_DOWNLOAD_URLS=""
+1 -1
View File
@@ -35,7 +35,7 @@ RUN sed -i -e "s|http://archive.ubuntu.com|${apt_archive}|g" -e "s|http://ports.
ARG REPO_CHANNEL="stable"
ARG REPOSITORY="deb [signed-by=/usr/share/keyrings/clickhouse-keyring.gpg] https://packages.clickhouse.com/deb ${REPO_CHANNEL} main"
ARG VERSION="26.9.10.4"
ARG VERSION="26.9.11.2"
ARG PACKAGES="clickhouse-common-static clickhouse-keeper"
ARG deb_location_url=""
+1 -1
View File
@@ -35,7 +35,7 @@ RUN arch=${TARGETARCH:-amd64} \
# lts / testing / prestable / etc
ARG REPO_CHANNEL="stable"
ARG REPOSITORY="https://packages.clickhouse.com/tgz/${REPO_CHANNEL}"
ARG VERSION="26.9.10.4"
ARG VERSION="26.9.11.2"
ARG PACKAGES="clickhouse-client clickhouse-server clickhouse-common-static"
ARG DIRECT_DOWNLOAD_URLS=""
+1 -1
View File
@@ -41,7 +41,7 @@ RUN sed -i -e "s|http://archive.ubuntu.com|${apt_archive}|g" -e "s|http://ports.
ARG REPO_CHANNEL="stable"
ARG REPOSITORY="deb [signed-by=/usr/share/keyrings/clickhouse-keyring.gpg] https://packages.clickhouse.com/deb ${REPO_CHANNEL} main"
ARG VERSION="26.9.10.4"
ARG VERSION="26.9.11.2"
ARG PACKAGES="clickhouse-client clickhouse-server clickhouse-common-static"
#docker-official-library:off
+1 -1
View File
@@ -34,7 +34,7 @@ RUN sed -i -e "s|http://archive.ubuntu.com|${apt_archive}|g" -e "s|http://ports.
ARG REPO_CHANNEL="stable"
ARG REPOSITORY="deb [signed-by=/usr/share/keyrings/clickhouse-keyring.gpg] https://packages.clickhouse.com/deb ${REPO_CHANNEL} main"
ARG VERSION="26.9.10.4"
ARG VERSION="26.9.11.2"
ARG PACKAGES="clickhouse-client clickhouse-server clickhouse-common-static"
#docker-official-library:off
+764
View File
@@ -42191,6 +42191,50 @@
"destination": "/clickstack/migration",
"source": "/use-cases/observability/clickstack/migration"
},
{
"destination": "/ar/clickstack/migration/datadog",
"source": "/ar/use-cases/observability/clickstack/migration/datadog"
},
{
"destination": "/es/clickstack/migration/datadog",
"source": "/es/use-cases/observability/clickstack/migration/datadog"
},
{
"destination": "/fr/clickstack/migration/datadog",
"source": "/fr/use-cases/observability/clickstack/migration/datadog"
},
{
"destination": "/ja/clickstack/migration/datadog",
"source": "/ja/use-cases/observability/clickstack/migration/datadog"
},
{
"destination": "/ko/clickstack/migration/datadog",
"source": "/ko/use-cases/observability/clickstack/migration/datadog"
},
{
"destination": "/pt-BR/clickstack/migration/datadog",
"source": "/pt-BR/use-cases/observability/clickstack/migration/datadog"
},
{
"destination": "/ru/clickstack/migration/datadog",
"source": "/ru/use-cases/observability/clickstack/migration/datadog"
},
{
"destination": "/zh/clickstack/migration/datadog",
"source": "/zh/use-cases/observability/clickstack/migration/datadog"
},
{
"destination": "/ja/clickstack/migration/datadog",
"source": "/jp/use-cases/observability/clickstack/migration/datadog"
},
{
"destination": "/clickstack/migration/datadog",
"source": "/en/use-cases/observability/clickstack/migration/datadog"
},
{
"destination": "/clickstack/migration/datadog",
"source": "/use-cases/observability/clickstack/migration/datadog"
},
{
"destination": "/clickstack/migration/elastic",
"source": "/use-cases/observability/clickstack/migration/elastic"
@@ -52787,6 +52831,726 @@
"destination": "https://clickhouse.com/company/our-story",
"source": "/zh/resources/about/history"
},
{
"source": "/ar/products/cloud/api-reference/udf/udf-attach",
"destination": "/ar/products/cloud/api-reference/udf/attach-udf-to-service"
},
{
"source": "/ar/products/cloud/api-reference/udf/udf-attachment-get",
"destination": "/ar/products/cloud/api-reference/udf/get-udf-attachment"
},
{
"source": "/ar/products/cloud/api-reference/udf/udf-attachment-list",
"destination": "/ar/products/cloud/api-reference/udf/list-udf-attachments"
},
{
"source": "/ar/products/cloud/api-reference/udf/udf-create",
"destination": "/ar/products/cloud/api-reference/udf/create-udf"
},
{
"source": "/ar/products/cloud/api-reference/udf/udf-delete",
"destination": "/ar/products/cloud/api-reference/udf/delete-udf"
},
{
"source": "/ar/products/cloud/api-reference/udf/udf-detach",
"destination": "/ar/products/cloud/api-reference/udf/detach-udf-from-service"
},
{
"source": "/ar/products/cloud/api-reference/udf/udf-get",
"destination": "/ar/products/cloud/api-reference/udf/get-udf"
},
{
"source": "/ar/products/cloud/api-reference/udf/udf-list",
"destination": "/ar/products/cloud/api-reference/udf/list-udfs"
},
{
"source": "/ar/products/cloud/api-reference/udf/udf-upload-session-create",
"destination": "/ar/products/cloud/api-reference/udf/create-udf-upload-url"
},
{
"source": "/ar/products/cloud/api-reference/udf/udf-version-create",
"destination": "/ar/products/cloud/api-reference/udf/create-udf-version"
},
{
"source": "/ar/products/cloud/api-reference/udf/udf-version-delete",
"destination": "/ar/products/cloud/api-reference/udf/delete-udf-version"
},
{
"source": "/ar/products/cloud/api-reference/udf/udf-version-list",
"destination": "/ar/products/cloud/api-reference/udf/list-udf-versions"
},
{
"source": "/ar/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-create",
"destination": "/ar/products/cloud/api-reference/query-api-endpoints/create-a-query-api-endpoint"
},
{
"source": "/ar/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-delete",
"destination": "/ar/products/cloud/api-reference/query-api-endpoints/delete-a-query-api-endpoint"
},
{
"source": "/ar/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-get",
"destination": "/ar/products/cloud/api-reference/query-api-endpoints/get-a-query-api-endpoint"
},
{
"source": "/ar/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-list",
"destination": "/ar/products/cloud/api-reference/query-api-endpoints/list-query-api-endpoints"
},
{
"source": "/ar/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-update",
"destination": "/ar/products/cloud/api-reference/query-api-endpoints/update-a-query-api-endpoint"
},
{
"source": "/es/products/cloud/api-reference/udf/udf-attach",
"destination": "/es/products/cloud/api-reference/udf/attach-udf-to-service"
},
{
"source": "/es/products/cloud/api-reference/udf/udf-attachment-get",
"destination": "/es/products/cloud/api-reference/udf/get-udf-attachment"
},
{
"source": "/es/products/cloud/api-reference/udf/udf-attachment-list",
"destination": "/es/products/cloud/api-reference/udf/list-udf-attachments"
},
{
"source": "/es/products/cloud/api-reference/udf/udf-create",
"destination": "/es/products/cloud/api-reference/udf/create-udf"
},
{
"source": "/es/products/cloud/api-reference/udf/udf-delete",
"destination": "/es/products/cloud/api-reference/udf/delete-udf"
},
{
"source": "/es/products/cloud/api-reference/udf/udf-detach",
"destination": "/es/products/cloud/api-reference/udf/detach-udf-from-service"
},
{
"source": "/es/products/cloud/api-reference/udf/udf-get",
"destination": "/es/products/cloud/api-reference/udf/get-udf"
},
{
"source": "/es/products/cloud/api-reference/udf/udf-list",
"destination": "/es/products/cloud/api-reference/udf/list-udfs"
},
{
"source": "/es/products/cloud/api-reference/udf/udf-upload-session-create",
"destination": "/es/products/cloud/api-reference/udf/create-udf-upload-url"
},
{
"source": "/es/products/cloud/api-reference/udf/udf-version-create",
"destination": "/es/products/cloud/api-reference/udf/create-udf-version"
},
{
"source": "/es/products/cloud/api-reference/udf/udf-version-delete",
"destination": "/es/products/cloud/api-reference/udf/delete-udf-version"
},
{
"source": "/es/products/cloud/api-reference/udf/udf-version-list",
"destination": "/es/products/cloud/api-reference/udf/list-udf-versions"
},
{
"source": "/es/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-create",
"destination": "/es/products/cloud/api-reference/query-api-endpoints/create-a-query-api-endpoint"
},
{
"source": "/es/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-delete",
"destination": "/es/products/cloud/api-reference/query-api-endpoints/delete-a-query-api-endpoint"
},
{
"source": "/es/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-get",
"destination": "/es/products/cloud/api-reference/query-api-endpoints/get-a-query-api-endpoint"
},
{
"source": "/es/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-list",
"destination": "/es/products/cloud/api-reference/query-api-endpoints/list-query-api-endpoints"
},
{
"source": "/es/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-update",
"destination": "/es/products/cloud/api-reference/query-api-endpoints/update-a-query-api-endpoint"
},
{
"source": "/fr/products/cloud/api-reference/udf/udf-attach",
"destination": "/fr/products/cloud/api-reference/udf/attach-udf-to-service"
},
{
"source": "/fr/products/cloud/api-reference/udf/udf-attachment-get",
"destination": "/fr/products/cloud/api-reference/udf/get-udf-attachment"
},
{
"source": "/fr/products/cloud/api-reference/udf/udf-attachment-list",
"destination": "/fr/products/cloud/api-reference/udf/list-udf-attachments"
},
{
"source": "/fr/products/cloud/api-reference/udf/udf-create",
"destination": "/fr/products/cloud/api-reference/udf/create-udf"
},
{
"source": "/fr/products/cloud/api-reference/udf/udf-delete",
"destination": "/fr/products/cloud/api-reference/udf/delete-udf"
},
{
"source": "/fr/products/cloud/api-reference/udf/udf-detach",
"destination": "/fr/products/cloud/api-reference/udf/detach-udf-from-service"
},
{
"source": "/fr/products/cloud/api-reference/udf/udf-get",
"destination": "/fr/products/cloud/api-reference/udf/get-udf"
},
{
"source": "/fr/products/cloud/api-reference/udf/udf-list",
"destination": "/fr/products/cloud/api-reference/udf/list-udfs"
},
{
"source": "/fr/products/cloud/api-reference/udf/udf-upload-session-create",
"destination": "/fr/products/cloud/api-reference/udf/create-udf-upload-url"
},
{
"source": "/fr/products/cloud/api-reference/udf/udf-version-create",
"destination": "/fr/products/cloud/api-reference/udf/create-udf-version"
},
{
"source": "/fr/products/cloud/api-reference/udf/udf-version-delete",
"destination": "/fr/products/cloud/api-reference/udf/delete-udf-version"
},
{
"source": "/fr/products/cloud/api-reference/udf/udf-version-list",
"destination": "/fr/products/cloud/api-reference/udf/list-udf-versions"
},
{
"source": "/fr/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-create",
"destination": "/fr/products/cloud/api-reference/query-api-endpoints/create-a-query-api-endpoint"
},
{
"source": "/fr/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-delete",
"destination": "/fr/products/cloud/api-reference/query-api-endpoints/delete-a-query-api-endpoint"
},
{
"source": "/fr/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-get",
"destination": "/fr/products/cloud/api-reference/query-api-endpoints/get-a-query-api-endpoint"
},
{
"source": "/fr/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-list",
"destination": "/fr/products/cloud/api-reference/query-api-endpoints/list-query-api-endpoints"
},
{
"source": "/fr/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-update",
"destination": "/fr/products/cloud/api-reference/query-api-endpoints/update-a-query-api-endpoint"
},
{
"source": "/ja/products/cloud/api-reference/udf/udf-attach",
"destination": "/ja/products/cloud/api-reference/udf/attach-udf-to-service"
},
{
"source": "/ja/products/cloud/api-reference/udf/udf-attachment-get",
"destination": "/ja/products/cloud/api-reference/udf/get-udf-attachment"
},
{
"source": "/ja/products/cloud/api-reference/udf/udf-attachment-list",
"destination": "/ja/products/cloud/api-reference/udf/list-udf-attachments"
},
{
"source": "/ja/products/cloud/api-reference/udf/udf-create",
"destination": "/ja/products/cloud/api-reference/udf/create-udf"
},
{
"source": "/ja/products/cloud/api-reference/udf/udf-delete",
"destination": "/ja/products/cloud/api-reference/udf/delete-udf"
},
{
"source": "/ja/products/cloud/api-reference/udf/udf-detach",
"destination": "/ja/products/cloud/api-reference/udf/detach-udf-from-service"
},
{
"source": "/ja/products/cloud/api-reference/udf/udf-get",
"destination": "/ja/products/cloud/api-reference/udf/get-udf"
},
{
"source": "/ja/products/cloud/api-reference/udf/udf-list",
"destination": "/ja/products/cloud/api-reference/udf/list-udfs"
},
{
"source": "/ja/products/cloud/api-reference/udf/udf-upload-session-create",
"destination": "/ja/products/cloud/api-reference/udf/create-udf-upload-url"
},
{
"source": "/ja/products/cloud/api-reference/udf/udf-version-create",
"destination": "/ja/products/cloud/api-reference/udf/create-udf-version"
},
{
"source": "/ja/products/cloud/api-reference/udf/udf-version-delete",
"destination": "/ja/products/cloud/api-reference/udf/delete-udf-version"
},
{
"source": "/ja/products/cloud/api-reference/udf/udf-version-list",
"destination": "/ja/products/cloud/api-reference/udf/list-udf-versions"
},
{
"source": "/ja/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-create",
"destination": "/ja/products/cloud/api-reference/query-api-endpoints/create-a-query-api-endpoint"
},
{
"source": "/ja/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-delete",
"destination": "/ja/products/cloud/api-reference/query-api-endpoints/delete-a-query-api-endpoint"
},
{
"source": "/ja/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-get",
"destination": "/ja/products/cloud/api-reference/query-api-endpoints/get-a-query-api-endpoint"
},
{
"source": "/ja/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-list",
"destination": "/ja/products/cloud/api-reference/query-api-endpoints/list-query-api-endpoints"
},
{
"source": "/ja/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-update",
"destination": "/ja/products/cloud/api-reference/query-api-endpoints/update-a-query-api-endpoint"
},
{
"source": "/ko/products/cloud/api-reference/udf/udf-attach",
"destination": "/ko/products/cloud/api-reference/udf/attach-udf-to-service"
},
{
"source": "/ko/products/cloud/api-reference/udf/udf-attachment-get",
"destination": "/ko/products/cloud/api-reference/udf/get-udf-attachment"
},
{
"source": "/ko/products/cloud/api-reference/udf/udf-attachment-list",
"destination": "/ko/products/cloud/api-reference/udf/list-udf-attachments"
},
{
"source": "/ko/products/cloud/api-reference/udf/udf-create",
"destination": "/ko/products/cloud/api-reference/udf/create-udf"
},
{
"source": "/ko/products/cloud/api-reference/udf/udf-delete",
"destination": "/ko/products/cloud/api-reference/udf/delete-udf"
},
{
"source": "/ko/products/cloud/api-reference/udf/udf-detach",
"destination": "/ko/products/cloud/api-reference/udf/detach-udf-from-service"
},
{
"source": "/ko/products/cloud/api-reference/udf/udf-get",
"destination": "/ko/products/cloud/api-reference/udf/get-udf"
},
{
"source": "/ko/products/cloud/api-reference/udf/udf-list",
"destination": "/ko/products/cloud/api-reference/udf/list-udfs"
},
{
"source": "/ko/products/cloud/api-reference/udf/udf-upload-session-create",
"destination": "/ko/products/cloud/api-reference/udf/create-udf-upload-url"
},
{
"source": "/ko/products/cloud/api-reference/udf/udf-version-create",
"destination": "/ko/products/cloud/api-reference/udf/create-udf-version"
},
{
"source": "/ko/products/cloud/api-reference/udf/udf-version-delete",
"destination": "/ko/products/cloud/api-reference/udf/delete-udf-version"
},
{
"source": "/ko/products/cloud/api-reference/udf/udf-version-list",
"destination": "/ko/products/cloud/api-reference/udf/list-udf-versions"
},
{
"source": "/ko/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-create",
"destination": "/ko/products/cloud/api-reference/query-api-endpoints/create-a-query-api-endpoint"
},
{
"source": "/ko/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-delete",
"destination": "/ko/products/cloud/api-reference/query-api-endpoints/delete-a-query-api-endpoint"
},
{
"source": "/ko/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-get",
"destination": "/ko/products/cloud/api-reference/query-api-endpoints/get-a-query-api-endpoint"
},
{
"source": "/ko/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-list",
"destination": "/ko/products/cloud/api-reference/query-api-endpoints/list-query-api-endpoints"
},
{
"source": "/ko/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-update",
"destination": "/ko/products/cloud/api-reference/query-api-endpoints/update-a-query-api-endpoint"
},
{
"source": "/pt-br/products/cloud/api-reference/udf/udf-attach",
"destination": "/pt-br/products/cloud/api-reference/udf/attach-udf-to-service"
},
{
"source": "/pt-br/products/cloud/api-reference/udf/udf-attachment-get",
"destination": "/pt-br/products/cloud/api-reference/udf/get-udf-attachment"
},
{
"source": "/pt-br/products/cloud/api-reference/udf/udf-attachment-list",
"destination": "/pt-br/products/cloud/api-reference/udf/list-udf-attachments"
},
{
"source": "/pt-br/products/cloud/api-reference/udf/udf-create",
"destination": "/pt-br/products/cloud/api-reference/udf/create-udf"
},
{
"source": "/pt-br/products/cloud/api-reference/udf/udf-delete",
"destination": "/pt-br/products/cloud/api-reference/udf/delete-udf"
},
{
"source": "/pt-br/products/cloud/api-reference/udf/udf-detach",
"destination": "/pt-br/products/cloud/api-reference/udf/detach-udf-from-service"
},
{
"source": "/pt-br/products/cloud/api-reference/udf/udf-get",
"destination": "/pt-br/products/cloud/api-reference/udf/get-udf"
},
{
"source": "/pt-br/products/cloud/api-reference/udf/udf-list",
"destination": "/pt-br/products/cloud/api-reference/udf/list-udfs"
},
{
"source": "/pt-br/products/cloud/api-reference/udf/udf-upload-session-create",
"destination": "/pt-br/products/cloud/api-reference/udf/create-udf-upload-url"
},
{
"source": "/pt-br/products/cloud/api-reference/udf/udf-version-create",
"destination": "/pt-br/products/cloud/api-reference/udf/create-udf-version"
},
{
"source": "/pt-br/products/cloud/api-reference/udf/udf-version-delete",
"destination": "/pt-br/products/cloud/api-reference/udf/delete-udf-version"
},
{
"source": "/pt-br/products/cloud/api-reference/udf/udf-version-list",
"destination": "/pt-br/products/cloud/api-reference/udf/list-udf-versions"
},
{
"source": "/pt-br/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-create",
"destination": "/pt-br/products/cloud/api-reference/query-api-endpoints/create-a-query-api-endpoint"
},
{
"source": "/pt-br/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-delete",
"destination": "/pt-br/products/cloud/api-reference/query-api-endpoints/delete-a-query-api-endpoint"
},
{
"source": "/pt-br/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-get",
"destination": "/pt-br/products/cloud/api-reference/query-api-endpoints/get-a-query-api-endpoint"
},
{
"source": "/pt-br/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-list",
"destination": "/pt-br/products/cloud/api-reference/query-api-endpoints/list-query-api-endpoints"
},
{
"source": "/pt-br/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-update",
"destination": "/pt-br/products/cloud/api-reference/query-api-endpoints/update-a-query-api-endpoint"
},
{
"source": "/pt-BR/products/cloud/api-reference/udf/udf-attach",
"destination": "/pt-br/products/cloud/api-reference/udf/attach-udf-to-service"
},
{
"source": "/pt-BR/products/cloud/api-reference/udf/udf-attachment-get",
"destination": "/pt-br/products/cloud/api-reference/udf/get-udf-attachment"
},
{
"source": "/pt-BR/products/cloud/api-reference/udf/udf-attachment-list",
"destination": "/pt-br/products/cloud/api-reference/udf/list-udf-attachments"
},
{
"source": "/pt-BR/products/cloud/api-reference/udf/udf-create",
"destination": "/pt-br/products/cloud/api-reference/udf/create-udf"
},
{
"source": "/pt-BR/products/cloud/api-reference/udf/udf-delete",
"destination": "/pt-br/products/cloud/api-reference/udf/delete-udf"
},
{
"source": "/pt-BR/products/cloud/api-reference/udf/udf-detach",
"destination": "/pt-br/products/cloud/api-reference/udf/detach-udf-from-service"
},
{
"source": "/pt-BR/products/cloud/api-reference/udf/udf-get",
"destination": "/pt-br/products/cloud/api-reference/udf/get-udf"
},
{
"source": "/pt-BR/products/cloud/api-reference/udf/udf-list",
"destination": "/pt-br/products/cloud/api-reference/udf/list-udfs"
},
{
"source": "/pt-BR/products/cloud/api-reference/udf/udf-upload-session-create",
"destination": "/pt-br/products/cloud/api-reference/udf/create-udf-upload-url"
},
{
"source": "/pt-BR/products/cloud/api-reference/udf/udf-version-create",
"destination": "/pt-br/products/cloud/api-reference/udf/create-udf-version"
},
{
"source": "/pt-BR/products/cloud/api-reference/udf/udf-version-delete",
"destination": "/pt-br/products/cloud/api-reference/udf/delete-udf-version"
},
{
"source": "/pt-BR/products/cloud/api-reference/udf/udf-version-list",
"destination": "/pt-br/products/cloud/api-reference/udf/list-udf-versions"
},
{
"source": "/pt-BR/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-create",
"destination": "/pt-br/products/cloud/api-reference/query-api-endpoints/create-a-query-api-endpoint"
},
{
"source": "/pt-BR/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-delete",
"destination": "/pt-br/products/cloud/api-reference/query-api-endpoints/delete-a-query-api-endpoint"
},
{
"source": "/pt-BR/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-get",
"destination": "/pt-br/products/cloud/api-reference/query-api-endpoints/get-a-query-api-endpoint"
},
{
"source": "/pt-BR/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-list",
"destination": "/pt-br/products/cloud/api-reference/query-api-endpoints/list-query-api-endpoints"
},
{
"source": "/pt-BR/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-update",
"destination": "/pt-br/products/cloud/api-reference/query-api-endpoints/update-a-query-api-endpoint"
},
{
"source": "/ru/products/cloud/api-reference/udf/udf-attach",
"destination": "/ru/products/cloud/api-reference/udf/attach-udf-to-service"
},
{
"source": "/ru/products/cloud/api-reference/udf/udf-attachment-get",
"destination": "/ru/products/cloud/api-reference/udf/get-udf-attachment"
},
{
"source": "/ru/products/cloud/api-reference/udf/udf-attachment-list",
"destination": "/ru/products/cloud/api-reference/udf/list-udf-attachments"
},
{
"source": "/ru/products/cloud/api-reference/udf/udf-create",
"destination": "/ru/products/cloud/api-reference/udf/create-udf"
},
{
"source": "/ru/products/cloud/api-reference/udf/udf-delete",
"destination": "/ru/products/cloud/api-reference/udf/delete-udf"
},
{
"source": "/ru/products/cloud/api-reference/udf/udf-detach",
"destination": "/ru/products/cloud/api-reference/udf/detach-udf-from-service"
},
{
"source": "/ru/products/cloud/api-reference/udf/udf-get",
"destination": "/ru/products/cloud/api-reference/udf/get-udf"
},
{
"source": "/ru/products/cloud/api-reference/udf/udf-list",
"destination": "/ru/products/cloud/api-reference/udf/list-udfs"
},
{
"source": "/ru/products/cloud/api-reference/udf/udf-upload-session-create",
"destination": "/ru/products/cloud/api-reference/udf/create-udf-upload-url"
},
{
"source": "/ru/products/cloud/api-reference/udf/udf-version-create",
"destination": "/ru/products/cloud/api-reference/udf/create-udf-version"
},
{
"source": "/ru/products/cloud/api-reference/udf/udf-version-delete",
"destination": "/ru/products/cloud/api-reference/udf/delete-udf-version"
},
{
"source": "/ru/products/cloud/api-reference/udf/udf-version-list",
"destination": "/ru/products/cloud/api-reference/udf/list-udf-versions"
},
{
"source": "/ru/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-create",
"destination": "/ru/products/cloud/api-reference/query-api-endpoints/create-a-query-api-endpoint"
},
{
"source": "/ru/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-delete",
"destination": "/ru/products/cloud/api-reference/query-api-endpoints/delete-a-query-api-endpoint"
},
{
"source": "/ru/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-get",
"destination": "/ru/products/cloud/api-reference/query-api-endpoints/get-a-query-api-endpoint"
},
{
"source": "/ru/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-list",
"destination": "/ru/products/cloud/api-reference/query-api-endpoints/list-query-api-endpoints"
},
{
"source": "/ru/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-update",
"destination": "/ru/products/cloud/api-reference/query-api-endpoints/update-a-query-api-endpoint"
},
{
"source": "/zh/products/cloud/api-reference/udf/udf-attach",
"destination": "/zh/products/cloud/api-reference/udf/attach-udf-to-service"
},
{
"source": "/zh/products/cloud/api-reference/udf/udf-attachment-get",
"destination": "/zh/products/cloud/api-reference/udf/get-udf-attachment"
},
{
"source": "/zh/products/cloud/api-reference/udf/udf-attachment-list",
"destination": "/zh/products/cloud/api-reference/udf/list-udf-attachments"
},
{
"source": "/zh/products/cloud/api-reference/udf/udf-create",
"destination": "/zh/products/cloud/api-reference/udf/create-udf"
},
{
"source": "/zh/products/cloud/api-reference/udf/udf-delete",
"destination": "/zh/products/cloud/api-reference/udf/delete-udf"
},
{
"source": "/zh/products/cloud/api-reference/udf/udf-detach",
"destination": "/zh/products/cloud/api-reference/udf/detach-udf-from-service"
},
{
"source": "/zh/products/cloud/api-reference/udf/udf-get",
"destination": "/zh/products/cloud/api-reference/udf/get-udf"
},
{
"source": "/zh/products/cloud/api-reference/udf/udf-list",
"destination": "/zh/products/cloud/api-reference/udf/list-udfs"
},
{
"source": "/zh/products/cloud/api-reference/udf/udf-upload-session-create",
"destination": "/zh/products/cloud/api-reference/udf/create-udf-upload-url"
},
{
"source": "/zh/products/cloud/api-reference/udf/udf-version-create",
"destination": "/zh/products/cloud/api-reference/udf/create-udf-version"
},
{
"source": "/zh/products/cloud/api-reference/udf/udf-version-delete",
"destination": "/zh/products/cloud/api-reference/udf/delete-udf-version"
},
{
"source": "/zh/products/cloud/api-reference/udf/udf-version-list",
"destination": "/zh/products/cloud/api-reference/udf/list-udf-versions"
},
{
"source": "/zh/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-create",
"destination": "/zh/products/cloud/api-reference/query-api-endpoints/create-a-query-api-endpoint"
},
{
"source": "/zh/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-delete",
"destination": "/zh/products/cloud/api-reference/query-api-endpoints/delete-a-query-api-endpoint"
},
{
"source": "/zh/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-get",
"destination": "/zh/products/cloud/api-reference/query-api-endpoints/get-a-query-api-endpoint"
},
{
"source": "/zh/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-list",
"destination": "/zh/products/cloud/api-reference/query-api-endpoints/list-query-api-endpoints"
},
{
"source": "/zh/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-update",
"destination": "/zh/products/cloud/api-reference/query-api-endpoints/update-a-query-api-endpoint"
},
{
"source": "/products/cloud/api-reference/udf/udf-attach",
"destination": "/products/cloud/api-reference/udf/attach-udf-to-service"
},
{
"source": "/products/cloud/api-reference/udf/udf-attachment-get",
"destination": "/products/cloud/api-reference/udf/get-udf-attachment"
},
{
"source": "/products/cloud/api-reference/udf/udf-attachment-list",
"destination": "/products/cloud/api-reference/udf/list-udf-attachments"
},
{
"source": "/products/cloud/api-reference/udf/udf-create",
"destination": "/products/cloud/api-reference/udf/create-udf"
},
{
"source": "/products/cloud/api-reference/udf/udf-delete",
"destination": "/products/cloud/api-reference/udf/delete-udf"
},
{
"source": "/products/cloud/api-reference/udf/udf-detach",
"destination": "/products/cloud/api-reference/udf/detach-udf-from-service"
},
{
"source": "/products/cloud/api-reference/udf/udf-get",
"destination": "/products/cloud/api-reference/udf/get-udf"
},
{
"source": "/products/cloud/api-reference/udf/udf-list",
"destination": "/products/cloud/api-reference/udf/list-udfs"
},
{
"source": "/products/cloud/api-reference/udf/udf-upload-session-create",
"destination": "/products/cloud/api-reference/udf/create-udf-upload-url"
},
{
"source": "/products/cloud/api-reference/udf/udf-version-create",
"destination": "/products/cloud/api-reference/udf/create-udf-version"
},
{
"source": "/products/cloud/api-reference/udf/udf-version-delete",
"destination": "/products/cloud/api-reference/udf/delete-udf-version"
},
{
"source": "/products/cloud/api-reference/udf/udf-version-list",
"destination": "/products/cloud/api-reference/udf/list-udf-versions"
},
{
"source": "/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-create",
"destination": "/products/cloud/api-reference/query-api-endpoints/create-a-query-api-endpoint"
},
{
"source": "/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-delete",
"destination": "/products/cloud/api-reference/query-api-endpoints/delete-a-query-api-endpoint"
},
{
"source": "/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-get",
"destination": "/products/cloud/api-reference/query-api-endpoints/get-a-query-api-endpoint"
},
{
"source": "/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-list",
"destination": "/products/cloud/api-reference/query-api-endpoints/list-query-api-endpoints"
},
{
"source": "/products/cloud/api-reference/query-api-endpoints/query-api-endpoint-update",
"destination": "/products/cloud/api-reference/query-api-endpoints/update-a-query-api-endpoint"
},
{
"source": "/products/cloud/api-reference/prometheus/organization-prometheus-discovery-get",
"destination": "/products/cloud/api-reference/prometheus/list-prometheus-scrape-targets"
},
{
"source": "/ar/products/cloud/api-reference/prometheus/organization-prometheus-discovery-get",
"destination": "/ar/products/cloud/api-reference/prometheus/list-prometheus-scrape-targets"
},
{
"source": "/es/products/cloud/api-reference/prometheus/organization-prometheus-discovery-get",
"destination": "/es/products/cloud/api-reference/prometheus/list-prometheus-scrape-targets"
},
{
"source": "/fr/products/cloud/api-reference/prometheus/organization-prometheus-discovery-get",
"destination": "/fr/products/cloud/api-reference/prometheus/list-prometheus-scrape-targets"
},
{
"source": "/ja/products/cloud/api-reference/prometheus/organization-prometheus-discovery-get",
"destination": "/ja/products/cloud/api-reference/prometheus/list-prometheus-scrape-targets"
},
{
"source": "/ko/products/cloud/api-reference/prometheus/organization-prometheus-discovery-get",
"destination": "/ko/products/cloud/api-reference/prometheus/list-prometheus-scrape-targets"
},
{
"source": "/pt-BR/products/cloud/api-reference/prometheus/organization-prometheus-discovery-get",
"destination": "/pt-br/products/cloud/api-reference/prometheus/list-prometheus-scrape-targets"
},
{
"source": "/pt-br/products/cloud/api-reference/prometheus/organization-prometheus-discovery-get",
"destination": "/pt-br/products/cloud/api-reference/prometheus/list-prometheus-scrape-targets"
},
{
"source": "/ru/products/cloud/api-reference/prometheus/organization-prometheus-discovery-get",
"destination": "/ru/products/cloud/api-reference/prometheus/list-prometheus-scrape-targets"
},
{
"source": "/zh/products/cloud/api-reference/prometheus/organization-prometheus-discovery-get",
"destination": "/zh/products/cloud/api-reference/prometheus/list-prometheus-scrape-targets"
},
{
"destination": "/products/cloud/api-reference/:path*",
"source": "/api-reference/:path*"
+13
View File
@@ -0,0 +1,13 @@
---
sidebar_position: 1
sidebar_label: 2026
---
# 2026 Changelog
### ClickHouse release v26.3.42.3-lts (d8ab7375308) FIXME as compared to v26.3.41.4-lts (bc541fda593)
#### Bug Fix (user-visible misbehavior in an official stable release)
* Backported in [#123773](https://github.com/ClickHouse/ClickHouse/issues/123773): Fix an out-of-bounds read when deserializing a `uniqTheta` state shorter than 24 bytes, for instance from `CAST(<string> AS AggregateFunction(uniqTheta, ...))`. [#119598](https://github.com/ClickHouse/ClickHouse/pull/119598) ([Alexey Milovidov](https://github.com/alexey-milovidov)).
* Backported in [#123819](https://github.com/ClickHouse/ClickHouse/issues/123819): Fix a buffer over-read on the sequential read path from Azure Blob Storage: an endpoint that answers a ranged request with more data than was requested could deliver bytes from outside the requested range to the caller. The end of a read is now decided by the locally set right bound, not by the `Content-Length` of the response, and a read is pinned to the listed generation of the blob with `If-Match`, so an object replaced during a read fails with `AZURE_OBJECT_CHANGED_DURING_READ` instead of being truncated (new setting `azure_validate_etag_on_read`, enabled by default). [#120910](https://github.com/ClickHouse/ClickHouse/pull/120910) ([Alexey Milovidov](https://github.com/alexey-milovidov)).
+14
View File
@@ -0,0 +1,14 @@
---
sidebar_position: 1
sidebar_label: 2026
---
# 2026 Changelog
### ClickHouse release v26.7.22.4-stable (b1273b7dc8d) FIXME as compared to v26.7.21.2-stable (530e32101b1)
#### Bug Fix (user-visible misbehavior in an official stable release)
* Backported in [#123522](https://github.com/ClickHouse/ClickHouse/issues/123522): Fix a `text` or `ngrambf_v1` index silently dropping matching rows when the indexed column is compared for equality against a `FixedString` value, or searched with `hasAny` or `hasAll` over `FixedString` needles. Happens at default settings, since `use_skip_indexes` and `enable_full_text_index` are both on. [#117314](https://github.com/ClickHouse/ClickHouse/pull/117314) ([Alexey Milovidov](https://github.com/alexey-milovidov)).
* Backported in [#123731](https://github.com/ClickHouse/ClickHouse/issues/123731): Fix an out-of-bounds read when deserializing a `uniqTheta` state shorter than 24 bytes, for instance from `CAST(<string> AS AggregateFunction(uniqTheta, ...))`. [#119598](https://github.com/ClickHouse/ClickHouse/pull/119598) ([Alexey Milovidov](https://github.com/alexey-milovidov)).
* Backported in [#123821](https://github.com/ClickHouse/ClickHouse/issues/123821): Fix a buffer over-read on the sequential read path from Azure Blob Storage: an endpoint that answers a ranged request with more data than was requested could deliver bytes from outside the requested range to the caller. The end of a read is now decided by the locally set right bound, not by the `Content-Length` of the response, and a read is pinned to the listed generation of the blob with `If-Match`, so an object replaced during a read fails with `AZURE_OBJECT_CHANGED_DURING_READ` instead of being truncated (new setting `azure_validate_etag_on_read`, enabled by default). [#120910](https://github.com/ClickHouse/ClickHouse/pull/120910) ([Alexey Milovidov](https://github.com/alexey-milovidov)).
+13
View File
@@ -0,0 +1,13 @@
---
sidebar_position: 1
sidebar_label: 2026
---
# 2026 Changelog
### ClickHouse release v26.8.18.2-lts (9f84259255b) FIXME as compared to v26.8.17.4-lts (20fcd0f7eb7)
#### NOT FOR CHANGELOG / INSIGNIFICANT
* Backported in [#123721](https://github.com/ClickHouse/ClickHouse/issues/123721): Warn when CLEAR COLUMN leaves an EPHEMERAL-derived MATERIALIZED column stale, and cover it with a test. [#115371](https://github.com/ClickHouse/ClickHouse/pull/115371) ([Shaohua Wang](https://github.com/tiandiwonder)).
+12
View File
@@ -0,0 +1,12 @@
---
sidebar_position: 1
sidebar_label: 2026
---
# 2026 Changelog
### ClickHouse release v26.9.11.2-stable (a83cd7baa96) FIXME as compared to v26.9.10.4-stable (01c3dd9dfbb)
#### Bug Fix (user-visible misbehavior in an official stable release)
* Backported in [#123989](https://github.com/ClickHouse/ClickHouse/issues/123989): Fixed a segmentation fault or `std::length_error` when reading `TSV` formats with `input_format_tsv_crlf_end_of_line = 1` if an escape sequence ended exactly at the end of a read buffer. [#123861](https://github.com/ClickHouse/ClickHouse/pull/123861) ([Groene AI](https://github.com/groeneai)).
@@ -22,16 +22,15 @@ The keys in a named collection must match the parameter names of the correspondi
function, table engine, database, etc. In the examples below the parameter list is
linked to for each type.
Parameters set in a named collection can be overridden in SQL, this is shown in the examples
below. This ability can be limited using `[NOT] OVERRIDABLE` keywords and XML attributes
and/or the configuration option `allow_named_collection_override_by_default`.
Overriding a parameter stored in a named collection requires the `SHOW NAMED COLLECTIONS SECRETS` privilege on that collection, including when the parameter is marked `OVERRIDABLE`. Keys marked `NOT OVERRIDABLE` in SQL or `overridable="false"` in XML cannot be overridden. Adding a parameter that the collection does not store does not require this additional privilege. Aliases, such as `addresses_expr` for `host`, count as overrides of the stored parameter.
<Warning>
If override is allowed, it may be possible for users without administrative access to
figure out the credentials that you are trying to hide.
If you are using named collections with that purpose, you should disable
`allow_named_collection_override_by_default` (which is enabled by default).
</Warning>
```sql
GRANT SHOW NAMED COLLECTIONS SECRETS ON my_collection TO my_user;
```
This check requires the privilege alone; it does not depend on the settings that control displaying secrets. The `allow_named_collection_override_by_default` setting is obsolete and has no effect.
Dictionary sources follow the same rule. The privilege is checked when the dictionary is created, attached, or restored. When the dictionary is loaded, only keys marked `NOT OVERRIDABLE` are enforced.
## Storing named collections in the system database {#storing-named-collections-in-the-system-database}
@@ -46,9 +45,9 @@ url = 'https://connection.url/'
In the above example:
* `key_1` can always be overridden.
* `key_1` can be overridden by users with `SHOW NAMED COLLECTIONS SECRETS` on the collection.
* `key_2` can never be overridden.
* `url` can be overridden or not depending on the value of `allow_named_collection_override_by_default`.
* `url` can be overridden by users with `SHOW NAMED COLLECTIONS SECRETS` on the collection.
### Permissions to create named collections with DDL {#permissions-to-create-named-collections-with-ddl}
@@ -127,9 +126,9 @@ Changing the storage type requires a server restart; `SYSTEM RELOAD CONFIG` does
In the above example:
* `key_1` can always be overridden.
* `key_1` can be overridden by users with `SHOW NAMED COLLECTIONS SECRETS` on the collection.
* `key_2` can never be overridden.
* `url` can be overridden or not depending on the value of `allow_named_collection_override_by_default`.
* `url` can be overridden by users with `SHOW NAMED COLLECTIONS SECRETS` on the collection.
## Modifying named collections {#modifying-named-collections}
@@ -492,9 +491,16 @@ SELECT * FROM remote(remote1, database = default, table = test);
### Example of using named collections with a dictionary with source ClickHouse {#example-of-using-named-collections-with-a-dictionary-with-source-clickhouse}
```sql
CREATE NAMED COLLECTION remote_dict AS
host = 'localhost',
port = 9000,
database = 'default',
user = 'default',
password = '';
CREATE DICTIONARY dict(a Int64, b String)
PRIMARY KEY a
SOURCE(CLICKHOUSE(NAME remote1 TABLE test DB default))
SOURCE(CLICKHOUSE(NAME remote_dict TABLE test))
LIFETIME(MIN 1 MAX 2)
LAYOUT(HASHED());
@@ -677,7 +683,7 @@ CREATE DICTIONARY dict
`b` String
)
PRIMARY KEY a
SOURCE(MONGODB(NAME mymongo COLLECTION my_dict))
SOURCE(MONGODB(NAME mymongo))
LIFETIME(MIN 1 MAX 2)
LAYOUT(HASHED())
@@ -689,5 +695,5 @@ SELECT dictGet('dict', 'b', 2);
```
<Note>
The named collection specifies `my_collection` for the collection name. In the function call it is overwritten by `collection = 'my_dict'` to select another collection.
The dictionary uses `my_collection`, as specified in the named collection. To use another MongoDB collection, define a separate named collection with that value.
</Note>
@@ -412,15 +412,16 @@ Configuration:
A new disk type `s3_plain_rewritable` was introduced in `24.4`.
Similar to the `s3_plain` disk type, it does not require additional storage for
metadata files. Instead, metadata is stored in S3.
Unlike the `s3_plain` disk type, `s3_plain_rewritable` allows executing merges
Unlike the `s3_plain` disk type, `s3_plain_rewritable` allows executing merges
and supports `INSERT` operations.
[Mutations](/reference/statements/alter/index#mutations) and replication of tables are not supported.
Replication of tables is not supported.
[Mutations](/reference/statements/alter/index#mutations) and `ALTER` operations
are supported only if the disk is configured with `enable_hard_links`, see below.
A use case for this disk type is for non-replicated `MergeTree` tables. Although
the `s3` disk type is suitable for non-replicated `MergeTree` tables, you may opt
for the `s3_plain_rewritable` disk type if you do not require local metadata
for the table and are willing to accept a limited set of operations. This could
be useful, for example, for system tables.
for the table. This could be useful, for example, for system tables.
Configuration:
@@ -447,6 +448,43 @@ is equal to
Starting from `24.5` it is possible to configure any object storage disk
(`s3`, `azure`, `local`) using the `plain_rewritable` metadata type.
#### Hard links on plain rewritable disks {#plain-rewritable-hard-links}
`ALTER` operations and mutations of a `MergeTree` table share the unchanged files
between a part and its mutated successor with hard links, instead of copying
them. The `enable_hard_links` setting (`0` by default) allows a disk with the
`plain_rewritable` metadata to create real hard links, which unlocks these
operations:
```xml
<s3_plain_rewritable>
<type>s3_plain_rewritable</type>
<endpoint>https://s3.eu-west-1.amazonaws.com/clickhouse-eu-west-1.clickhouse.com/data/</endpoint>
<use_environment_credentials>1</use_environment_credentials>
<enable_hard_links>1</enable_hard_links>
</s3_plain_rewritable>
```
A hard link makes the metadata of a directory list its files explicitly, together
with the keys of the objects storing them, so that a file can refer to an object
stored under the prefix of another directory. Consider the following before
enabling it:
- A server that does not have the `enable_hard_links` setting cannot read a
directory in this form - it reads the whole metadata of the directory as its
logical path. A disk that has such directories is therefore not readable after
a downgrade to such a version, and cannot be shared with it. Directories that
never had hard links keep the previous layout, so a disk stays readable by
older servers until the first operation that creates a hard link - a mutation,
an `ALTER`, a `FREEZE` or a backup. Turning the setting off again does not
convert the existing directories back.
- The setting is read only when the disk is created: a change takes effect after
a restart of the server, `SYSTEM RELOAD CONFIG` does not apply it.
- An object whose last link is removed is deleted in two steps: the metadata of
the directory is rewritten first, and the object is deleted after that. If the
server is killed in between, the object is left in the bucket and is not
referenced by anything, so it has to be deleted manually.
### Using Azure Blob Storage {#azure-blob-storage}
`MergeTree` family table engines can store data to [Azure Blob Storage](https://azure.microsoft.com/en-us/services/storage/blobs/)
@@ -316,6 +316,8 @@ Default value: 0.
This setting enables or disables the `SHOW NAMED COLLECTIONS SECRETS` privilege for the user, which allows the user to see the values stored in [named collections](/concepts/features/configuration/server-config/named-collections), for example in the [`system.named_collections`](/reference/system-tables/named_collections) table.
It is also required to override a key that is stored in a named collection when the collection is used, for example `url` in `s3(my_collection, url = '...')`, a `SETTINGS` clause of a table engine, or a key in a dictionary source. Adding a key that is not stored in the collection does not require it.
It is disabled by default: being able to manage named collections does not mean being able to read back the credentials that are already stored in them.
This setting only has an effect together with `named_collection_control`, because with `named_collection_control` disabled all named collection privileges, including `SHOW NAMED COLLECTIONS SECRETS`, are revoked anyway.
@@ -348,7 +348,7 @@ SETTINGS allow_introspection_functions=1
### Example: generating a fragmentation flame graph {#example-fragmentation-flamegraph}
The same query can split the waste of each size class between its backtraces, in proportion to their number of old allocations, and print the result in the collapsed format for `flamegraph.pl`. Size classes with waste but without old enough samples go under an `[unattributed]` frame, so the total of the flame graph is the total waste. Read `system.jemalloc_sampled_allocations` once per query: every read flushes a new profile, so two subqueries would see different data.
The same query can split the waste of each size class between its backtraces, in proportion to their number of old allocations, and print the result in the collapsed format for `flamegraph.pl`. Size classes with waste but without old enough samples go under an `[unattributed]` frame, so the total of the flame graph is the total waste. Samples whose stack jemalloc could not unwind have an empty `trace` and go under `[unknown]`. Read `system.jemalloc_sampled_allocations` once per query: every read flushes a new profile, so two subqueries would see different data.
```sql
WITH 60e9 AS min_age_ns
@@ -362,7 +362,7 @@ LEFT JOIN
arena,
size_class,
sum(weight) AS est_old_objects,
arrayStringConcat(arrayMap(a -> demangle(addressToSymbol(a)), arrayReverse(trace)), ';') AS stack
if(empty(trace), '[unknown]', arrayStringConcat(arrayMap(a -> demangle(addressToSymbol(a)), arrayReverse(trace)), ';')) AS stack
FROM system.jemalloc_sampled_allocations
WHERE age_ns > min_age_ns
GROUP BY arena, size_class, trace
@@ -12,17 +12,19 @@ integration:
import { Image } from "/snippets/components/Image.jsx";
ClickPipes supports integrating with a schema registry to decode Avro- and Protobuf-encoded record values and [structured Kafka keys](/integrations/clickpipes/kafka/reference#structured-message-keys).
ClickPipes supports integrating with a schema registry to decode Avro- and Protobuf-encoded record values. Kafka ClickPipes also support [structured Kafka keys](/integrations/clickpipes/kafka/reference#structured-message-keys).
For Protobuf record values, you can also [upload a schema directly](/integrations/clickpipes/kafka/create-kafka-clickpipe#7-select-your-topic) without configuring a schema registry.
## Supported registries for Kafka ClickPipes {#supported-schema-registries}
## Supported registries for streaming ClickPipes {#supported-schema-registries}
Kafka ClickPipes supports two families of schema registry:
Kafka ClickPipes support two families of schema registry:
- [Confluent-compatible registries](#confluent-compatible-registries): any registry that is API-compatible with the Confluent Schema Registry, such as the Confluent Schema Registry itself and the Redpanda Schema Registry. Supports Avro and Protobuf.
- [AWS Glue Schema Registry](#aws-glue-schema-registry): for Avro data serialized with the AWS Glue SerDe, typically from Amazon MSK.
Kinesis ClickPipes support the AWS Glue Schema Registry for Avro and Protobuf data.
ClickPipes doesn't support Azure Schema Registry yet. If you require support for it, [reach out to our team](https://clickhouse.com/company/contact?loc=clickpipes).
## Confluent-compatible registries {#confluent-compatible-registries}
@@ -62,15 +64,15 @@ When mappings for fields such as `_key.id` are configured, ClickPipes resolves t
If your producers serialize Avro with the AWS Glue SerDe (for example `AWSKafkaAvroSerializer` against an Amazon MSK topic), ClickPipes can resolve those schemas directly from the AWS Glue Schema Registry. Glue uses a different wire format and a different API from Confluent-compatible registries, so it is configured separately.
AWS Glue Schema Registry configuration is currently available only through the ClickHouse Cloud console. It isn't supported through the ClickPipes API or Terraform provider.
AWS Glue Schema Registry configuration is available through the ClickHouse Cloud console, the ClickPipes API, and the Terraform provider.
<Note>
**Avro only.** AWS Glue registries are supported for the Avro format only. The Glue SerDe can also frame JSON and Protobuf, but these are not supported by ClickPipes and are rejected when the pipe is created.
**Kafka:** AWS Glue registries support the Avro format only. **Kinesis:** AWS Glue registries support `AvroConfluent` and `Protobuf`. JSON is not supported through the Glue SerDe.
</Note>
### Configuration {#glue-configuration}
In the ClickPipe creation wizard, enable **Schema registry** on the Kafka connection step and set **Registry type** to **AWS Glue**:
In the ClickPipe creation wizard, enable **Schema registry** on the Kafka or Kinesis connection step and set **Registry type** to **AWS Glue**:
<Image img="/images/integrations/data-ingestion/clickpipes/cp_glue_schema_registry.png" alt="Schema registry panel with AWS Glue selected" size="lg" border/>
@@ -81,7 +83,7 @@ In the ClickPipe creation wizard, enable **Schema registry** on the Kafka connec
| Registry name | Yes | Name of the Glue registry. Schemas that resolve to a different registry are rejected, so a typo surfaces when ClickPipes resolves a schema version. | `my-glue-registry` |
| IAM role ARN | Conditional | A dedicated role for registry access. Optional when your broker uses IAM authentication; required otherwise. | `arn:aws:iam::123456789012:role/ClickHouseAccessRole-glue` |
There is no registry URL to configure. Every record produced by the Glue SerDe carries the ID of its own schema version, which ClickPipes resolves with `glue:GetSchemaVersion` and caches, one API call per distinct schema version. Schema evolution is handled automatically: when records switch to a new schema version mid-stream, it is resolved on first sight.
There is no registry URL to configure. Every record produced by the Glue SerDe carries the ID of its own schema version, which ClickPipes resolves with `glue:GetSchemaVersion` and caches, one API call per distinct schema version. Schema evolution is handled automatically: when records switch to a new schema version mid-stream, it is resolved on first sight. For Kinesis, configure the same fields through [OpenAPI](/integrations/clickpipes/programmatic-access/openapi) or [Terraform](/integrations/clickpipes/programmatic-access/terraform).
### IAM setup {#glue-iam-setup}
@@ -186,8 +188,8 @@ Paste the new role's ARN into the **IAM role ARN** field in the wizard.
### Limitations {#glue-limitations}
- Avro only. JSON Schema and Protobuf via the Glue SerDe are not supported.
- Kafka sources only. Kinesis ClickPipes cannot use a Glue registry.
- Kafka supports Avro only. JSON Schema and Protobuf via the Glue SerDe are not supported.
- Kinesis supports `AvroConfluent` and `Protobuf` through AWS Glue Schema Registry. An uploaded Protobuf schema cannot be combined with a schema registry.
## Schema mapping {#schema-mapping}
@@ -35,7 +35,7 @@ You have familiarized yourself with the [ClickPipes intro](/integrations/clickpi
<Image img="/images/integrations/data-ingestion/clickpipes/cp_step2_kinesis.webp" alt="Fill out connection details" size="lg" border/>
5. Select the Kinesis stream, **Data format**, and starting offset. For `Protobuf`, upload a `.proto` file or serialized `FileDescriptorSet` (maximum 768 KiB). No schema registry is required.
5. Select the Kinesis stream, **Data format**, and starting offset. Kinesis ClickPipes support JSON, Avro, and Protobuf. For `Protobuf`, upload a `.proto` file or serialized `FileDescriptorSet` (maximum 768 KiB), or configure an AWS Glue Schema Registry. For Avro, configure an AWS Glue Schema Registry. Do not combine an uploaded Protobuf schema with a schema registry.
The UI displays sample data from the selected stream. **Use Kinesis Enhanced Fan-Out** is enabled by default. See the [Amazon Kinesis documentation](https://docs.aws.amazon.com/streams/latest/dev/enhanced-consumers.html) for details.
@@ -81,8 +81,15 @@ You have familiarized yourself with the [ClickPipes intro](/integrations/clickpi
The supported formats are:
- [JSON](/reference/formats/JSON/JSON)
- [Avro](/reference/formats/Avro/AvroConfluent) (with AWS Glue Schema Registry)
- [Protobuf](/reference/formats/Protobuf/Protobuf)
## AWS Glue Schema Registry {#aws-glue-schema-registry}
Kinesis ClickPipes can resolve Avro and Protobuf schemas from AWS Glue Schema Registry. Select **AWS Glue** in the ClickPipe creation flow, then provide the registry's AWS region and name. Optionally provide an IAM role for registry access; otherwise, ClickPipes uses the Kinesis source's IAM identity. That identity needs `glue:GetSchemaVersion` permission.
You can configure the same fields through [OpenAPI](/integrations/clickpipes/programmatic-access/openapi) or [Terraform](/integrations/clickpipes/programmatic-access/terraform). The [schema registry guide](/integrations/clickpipes/kafka/schema-registries#aws-glue-schema-registry) includes the required IAM policy and configuration details.
## Compression {#compression}
ClickPipes for Kinesis automatically detects and decompresses compressed records. Unlike Kafka, where the client library handles decompression transparently, Kinesis delivers raw bytes — ClickPipes handles this for you with no configuration required.
@@ -151,11 +151,16 @@ curl -u "$KEY_ID:$KEY_SECRET" \
"name": "My Kinesis ClickPipe",
"source": {
"kinesis": {
"format": "JSONEachRow",
"format": "AvroConfluent",
"streamName": "my-stream",
"region": "us-east-1",
"iteratorType": "TRIM_HORIZON",
"authentication": "IAM_USER",
"schemaRegistry": {
"type": "glue",
"glueRegion": "us-east-1",
"glueRegistryName": "my-registry"
},
"accessKey": {
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
"secretKey": "<secret_key>"
@@ -177,6 +182,8 @@ curl -u "$KEY_ID:$KEY_SECRET" \
"https://api.clickhouse.cloud/v1/organizations/$ORG_ID/services/$SERVICE_ID/clickpipes"
```
This example configures Avro with AWS Glue Schema Registry. To decode Protobuf using Glue, set `format` to `Protobuf` and retain `schemaRegistry`. To upload a Protobuf schema instead, set `format` to `Protobuf` and provide the base64-encoded `.proto` file or serialized `FileDescriptorSet` in `protobufSchema`; do not include `schemaRegistry` in that request.
### Amazon S3 {#create-s3}
```bash
@@ -216,12 +216,18 @@ resource "clickhouse_clickpipe" "kinesis_clickpipe" {
source {
kinesis {
format = "JSONEachRow"
format = "AvroConfluent"
stream_name = "my-stream"
region = "us-east-1"
iterator_type = "TRIM_HORIZON"
authentication = "IAM_USER"
schema_registry {
type = "glue"
glue_region = "us-east-1"
glue_registry_name = "my-registry"
}
access_key {
access_key_id = var.aws_access_key_id
secret_key = var.aws_secret_key
@@ -252,6 +258,49 @@ resource "clickhouse_clickpipe" "kinesis_clickpipe" {
}
```
#### Kinesis with an uploaded Protobuf schema {#kinesis-protobuf-schema}
Set `format` to `Protobuf` and use `protobuf_schema` for a base64-encoded `.proto` file or serialized `FileDescriptorSet`. Do not configure `schema_registry` when uploading a schema.
```hcl
resource "clickhouse_clickpipe" "kinesis_protobuf_clickpipe" {
name = "My Kinesis Protobuf ClickPipe"
service_id = var.service_id
source {
kinesis {
format = "Protobuf"
stream_name = "my-stream"
region = "us-east-1"
iterator_type = "TRIM_HORIZON"
authentication = "IAM_USER"
protobuf_schema = filebase64("${path.module}/event.proto")
access_key {
access_key_id = var.aws_access_key_id
secret_key = var.aws_secret_key
}
}
}
destination {
table = "my_table"
managed_table = true
table_definition {
engine {
type = "MergeTree"
}
}
columns {
name = "id"
type = "UInt64"
}
}
}
```
#### Kinesis with IAM role {#kinesis-iam-role}
IAM role authentication requires a ClickHouse service running on AWS.
+35
View File
@@ -12,6 +12,41 @@ import { Image } from "/snippets/components/Image.jsx";
In addition to this ClickHouse Cloud changelog, please see the [Cloud Compatibility](/products/cloud/guides/cloud-compatibility) page.
<Update label="Oct 5, 2026">
This week's release simplifies billing for customers with multiple ClickHouse Cloud organizations, extends ClickPipes serialization support for Kafka and Kinesis, and introduces an experimental native TCP option for .NET developers. You can now manage prepaid credits across organizations in the console or retrieve balances programmatically; streaming ClickPipes support common Avro and Protobuf workflows; and the .NET driver offers a faster, lower-allocation native client.
## New features {#2026-10-05-new-features}
### Transfer prepaid credits between organizations {#transfer-prepaid-credits-between-organizations}
Customers with multiple organizations can now move prepaid credits between them in the console, without opening a support ticket. A single view shows prepaid credits across all of your organizations, so you can track consumption and plan renewals. This requires Org Admin, Billing Admin, or a custom role with billing permissions on each organization involved. Credits can be split across tiers but not between Cloud and BYOC organizations or across different cloud marketplaces. See the [self-service prepaid credit management documentation](/products/cloud/reference/billing/self-service-prepaid-credit-management).
## API {#2026-10-05-api}
### New API endpoint for organization credit balances {#new-api-endpoint-for-organization-credit-balances}
The new `GET /v1/organizations/{organizationId}/creditBalances` endpoint in the ClickHouse Cloud OpenAPI returns an organization's active credit balances, including prepaid and trial credits. Use it to monitor credit consumption from your own tooling. See the [self-service prepaid credit management documentation](/products/cloud/reference/billing/self-service-prepaid-credit-management).
## ClickPipes {#2026-10-05-clickpipes}
### Avro and Protobuf support for Kinesis ClickPipes {#avro-and-protobuf-support-for-kinesis-clickpipes}
Kinesis ClickPipes now support **Avro** and **Protobuf** in addition to JSON. Schemas can come from the **AWS Glue Schema Registry** or from an uploaded schema file. Kafka and Kinesis ClickPipes now support the same common serialization formats, and the feature is available in the UI, OpenAPI, and Terraform. See the [Kinesis ClickPipes documentation](/integrations/clickpipes/kinesis/).
### Protobuf schema file uploads for Kafka ClickPipes {#protobuf-schema-file-uploads-for-kafka-clickpipes}
Kafka ClickPipes now accept Protobuf schemas uploaded as `.proto` files or file descriptor sets (`.desc`). If you don't use a schema registry, you can now provide a standalone schema for deserialization. See the [Kafka ClickPipes documentation](/integrations/clickpipes/kafka/create-kafka-clickpipe#7-select-your-topic).
## Integrations {#2026-10-05-integrations}
### .NET driver v1.5.0 adds a native TCP client {#net-driver-v150-adds-a-native-tcp-client}
The ClickHouse .NET driver v1.5.0 adds a new native TCP client alongside the existing HTTP client. It supports connection pooling, sessions, TLS, LZ4/ZSTD compression, query parameters, and OpenTelemetry tracing. In benchmarks on ClickHouse Cloud, reads were up to 2x faster, concurrent inserts up to 2x faster, and memory allocation dropped by orders of magnitude. The TCP API is experimental and may change; continue using the HTTP client for `ADO.NET`, ORMs, and raw formats such as CSV or Parquet. See the [TCP client documentation](/integrations/language-clients/csharp/tcp) and [NuGet package](https://www.nuget.org/packages/ClickHouse.Driver/1.5.0).
</Update>
<Update label="Sep 22, 2026">
This week's updates expand ClickHouse Cloud's compliance footprint into new APAC regions, add public-beta replica-aware routing, and introduce new capabilities for metrics, compute, and dbt. HIPAA- and PCI-regulated cells are now available in AWS Seoul and Sydney; eligible customers can evaluate Prometheus-compatible metrics workloads and On-Demand Compute; and ClickHouse is available through dbt Core v2 and dbt Platform.
@@ -13,6 +13,7 @@ keywords: ['changelog', 'release notes', 'updates', 'new features', 'cloud chang
## 2026 {#2026}
<div className="flex flex-wrap gap-2">
<Card title="26.8" href="/resources/changelogs/cloud/release-notes/26_8" className="w-fit" />
<Card title="26.6" href="/resources/changelogs/cloud/release-notes/26_6" className="w-fit" />
<Card title="26.4" href="/resources/changelogs/cloud/release-notes/26_4" className="w-fit" />
<Card title="26.2" href="/resources/changelogs/cloud/release-notes/26_02" className="w-fit" />
File diff suppressed because it is too large. Load diff
@@ -45,6 +45,19 @@ For advance testing before production upgrades, use the Fast or Regular channel
</Warning>
<ReleaseSchedule releases={[
{
changelog_link: 'https://clickhouse.com/docs/changelogs/26.8',
version: '26.8',
fast_start_date: '2026-10-12',
fast_end_date: 'TBD',
regular_start_date: 'TBD',
regular_end_date: 'TBD',
slow_start_date: 'TBD',
slow_end_date: 'TBD',
fast_progress: 'green',
regular_progress: 'green',
slow_progress: 'green',
},
{
changelog_link: 'https://clickhouse.com/docs/changelogs/26.6',
version: '26.6',
@@ -23,6 +23,7 @@
{
"group": "2026",
"pages": [
"resources/changelogs/cloud/release-notes/26_8",
"resources/changelogs/cloud/release-notes/26_6",
"resources/changelogs/cloud/release-notes/26_4",
"resources/changelogs/cloud/release-notes/26_02"
@@ -322,6 +322,7 @@ Each feature sits behind a `DBMS_MIN_REVISION_WITH_*` threshold. The writer emit
| Offsets `String` serialization | `DBMS_MIN_REVISION_WITH_STRING_WITH_SIZE_STREAM_SERIALIZATION` | `54492` | Below the threshold `String` column data uses the per-value layout (`VarUInt` length prefix + raw bytes per row) instead of the [offsets layout](#string-type) (cumulative byte offsets as `UInt64`, then all data concatenated). Gated purely on the revision, with no per-column wire marker. At or above the threshold the offsets layout also applies to `String` nested inside composite types (`Array`, `Nullable`, `Map`, `Tuple`, `Variant`, `Dynamic`, `JSON`), but **not** to the dictionary of a `LowCardinality` column: a `LowCardinality` dictionary is always written with the default nested serialization, so its `String` values keep the per-value layout (this includes `LowCardinality(String)` and `LowCardinality(Nullable(String))`). The `Buffers` format is not part of the protocol and always uses the per-value layout. |
| Aggregate-function versioning | `DBMS_MIN_REVISION_WITH_AGGREGATE_FUNCTIONS_VERSIONING` | `54452` | `AggregateFunction` state is written without an embedded version. |
| `skip_degree` in `quantileDeterministic` state | `DBMS_MIN_REVISION_WITH_QUANTILE_DETERMINISTIC_SKIP_DEGREE` | `54491` | `AggregateFunction(quantileDeterministic, ...)` states (and the `quantiles`/`median` spellings of the same function) are written at state version `0`, without the trailing `UInt8 skip_degree`. See the note under [type aliases](#type-aliases). |
| 64-bit hash sample in `uniq` state | `DBMS_MIN_REVISION_WITH_UNIQ_STATE_VERSION_1` | `54493` | `AggregateFunction(uniq, ...)` states are written at state version `0`: no leading flags byte and no trailing sample of 64-bit hashes. See the note under [type aliases](#type-aliases). |
| `out_of_order_buckets` in `BlockInfo` | `DBMS_MIN_REVISION_WITH_OUT_OF_ORDER_BUCKETS_IN_AGGREGATION` | `54480` | `BlockInfo` field ID `3` is not written (see [BlockInfo](#blockinfo)). |
| Parallel block marshalling (`DETACHED`) | `DBMS_MIN_REVISON_WITH_PARALLEL_BLOCK_MARSHALLING` | `54478` | Columns are never wrapped in a `ColumnBLOB`; no `DETACHED` / `DETACHED_OVER_SPARSE` kinds appear (see [kind_stack](#kind-stack-and-sparse-encoding)). |
| `DateTime(tz)` type parameter | `DBMS_MIN_REVISION_WITH_TIME_ZONE_PARAMETER_IN_DATETIME_DATA_TYPE` | `54337` | The timezone parameter is dropped from the `type` string — `DateTime('UTC')` is announced as bare `DateTime`. |
@@ -1067,7 +1068,7 @@ So a `Point` column is decoded exactly as `Tuple(Float64, Float64)` (rendering a
<Note>
Two related types are **not** aliases. They are valid `Native` column types — a client can receive an `AggregateFunction` column from a `-State` combinator or distributed aggregation, for instance — but each carries its own specialized payload that is outside the scope of this page:
- `AggregateFunction(func, ...)` holds an *intermediate* aggregation state (not a finalized value); its binary layout is specific to the aggregate function and version. The function name may carry parameters, each printed as a SQL literal, as in `AggregateFunction(topK(3), UInt64)`. Some aggregate functions print an explicit `::Type` suffix on a parameter, as in `AggregateFunction(topK('3'::Decimal32(0)), UInt64)`, because a `Decimal` or wide-integer value has no bare literal form to print; whether a function does that is a property of that function rather than of the format, so a decoder that inspects parameters must accept both spellings. A decoder that only needs the argument types is unaffected. A versioned aggregate function spells a non-zero state version out as the first argument of the type string — `AggregateFunction(1, quantileDeterministic, UInt64, UInt64)` — while version `0` is not printed, so the type string stays `AggregateFunction(quantileDeterministic, UInt64, UInt64)`. On a negotiated connection (revision above `0`) the writer picks the version from the protocol revision **alone** — never from a version that happens to be pinned on its local type, because version `0` is indistinguishable on the wire from "no version announced" — so the same function can announce different versions on different connections, and a reader must take the version from the type string rather than assume the newest one. At revision `0` there is no peer to derive a version for — the stream is self-describing and read back by whoever wrote it — so a version pinned on the type survives into the stream (and into the announced type string) rather than being downgraded to `0`. `quantileDeterministic` (with the `quantilesDeterministic` and `medianDeterministic` spellings) is versioned from revision `54491`: version `1` appends a trailing `UInt8 skip_degree` to the state, and its prefix is byte-identical to version `0`. Below that revision the writer emits version `0` states, so an older peer is unaffected. All of this applies equally to a state nested inside a container type — `Array`, `Tuple`, `Map`, `Nullable`, `Variant` (where the alternative's spelled-out version changes the type string without changing the discriminator order), or a `Nested` / `SimpleAggregateFunction` wrapper. A wrapper's custom name contains a second copy of its argument type, so the writer must rebuild that copy with the same negotiated aggregate-state version; otherwise its header can announce version `1` for a version-`0` payload. One `Variant` corner case has no wire representation at all: when two alternatives differ only in a spelled-out state version (legal as a column type, since the type strings differ), re-versioning both for the negotiated revision would collapse them into the same alternative, so the sender must fail the transfer rather than announce versions the payload is not written with. `Dynamic` is the deliberate exception to revision-derived versioning: a `Dynamic` column announces its nested types itself — through its own type lists and through the [binary type encoding](/reference/data-types/data-types-binary-encoding), whose explicit version field carries a spelled-out state version (the values of its shared variant embed their type that way even at rest) — and those announcements are part of the column's own data, identical for every peer and every medium, so they are never rewritten for a negotiated revision. A state nested inside a `Dynamic` value is therefore written at the version spelled in its type (which a fresh state carries, since a `-State` query result spells its version out), and at version `0` when the type spells none, on every connection and medium alike.
- `AggregateFunction(func, ...)` holds an *intermediate* aggregation state (not a finalized value); its binary layout is specific to the aggregate function and version. The function name may carry parameters, each printed as a SQL literal, as in `AggregateFunction(topK(3), UInt64)`. Some aggregate functions print an explicit `::Type` suffix on a parameter, as in `AggregateFunction(topK('3'::Decimal32(0)), UInt64)`, because a `Decimal` or wide-integer value has no bare literal form to print; whether a function does that is a property of that function rather than of the format, so a decoder that inspects parameters must accept both spellings. A decoder that only needs the argument types is unaffected. A versioned aggregate function spells a non-zero state version out as the first argument of the type string — `AggregateFunction(1, quantileDeterministic, UInt64, UInt64)` — while version `0` is not printed, so the type string stays `AggregateFunction(quantileDeterministic, UInt64, UInt64)`. On a negotiated connection (revision above `0`) the writer picks the version from the protocol revision **alone** — never from a version that happens to be pinned on its local type, because version `0` is indistinguishable on the wire from "no version announced" — so the same function can announce different versions on different connections, and a reader must take the version from the type string rather than assume the newest one. At revision `0` there is no peer to derive a version for — the stream is self-describing and read back by whoever wrote it — so a version pinned on the type survives into the stream (and into the announced type string) rather than being downgraded to `0`. `quantileDeterministic` (with the `quantilesDeterministic` and `medianDeterministic` spellings) is versioned from revision `54491`: version `1` appends a trailing `UInt8 skip_degree` to the state, and its prefix is byte-identical to version `0`. Below that revision the writer emits version `0` states, so an older peer is unaffected. `uniq` is versioned from revision `54493`: version `1` prepends a flags byte to the version-`0` layout and, when bit `0` of the flags is set, appends a small additional sample of full 64-bit hashes, each XOR-ed with the constant `0x9E3779B97F4A0000` (`UInt8 skip_degree`, `VarUInt count`, then the 8-byte values), which keeps the estimate accurate at cardinalities above two billion; a version `0` state carries the 32-bit main sample only. Bit `1` of the flags says that the 64-bit sample is *incomplete* — the state has absorbed a version `0` state, whose elements were never offered to a 64-bit sample — in which case no 64-bit sample is written and a reader must estimate from the 32-bit main sample alone (and keep saying so in the states it goes on to write); the two bits are therefore mutually exclusive, and a reader must reject a state that sets both. No other bit is defined, and a reader must reject a state that sets one. All of this applies equally to a state nested inside a container type — `Array`, `Tuple`, `Map`, `Nullable`, `Variant` (where the alternative's spelled-out version changes the type string without changing the discriminator order), or a `Nested` / `SimpleAggregateFunction` wrapper. It also applies under aggregate-function combinators: a pass-through combinator over a versioned function — `uniqIf`, `uniqArray`, `uniqForEach`, `uniqMap`, `uniqArgMin` / `uniqArgMax`, `uniqOrNull` / `uniqOrDefault`, `uniqResample`, and the implicit `Nullable`-argument adaptor — forwards the version to the nested states its payload contains, spells the nested function's current version out in a fresh state's type string exactly like the plain function (`AggregateFunction(1, uniqIf, UInt64, UInt8)`), and derives the version from the protocol revision on a negotiated connection the same way. A wrapper's custom name contains a second copy of its argument type, so the writer must rebuild that copy with the same negotiated aggregate-state version; otherwise its header can announce version `1` for a version-`0` payload. One `Variant` corner case has no wire representation at all: when two alternatives differ only in a spelled-out state version (legal as a column type, since the type strings differ), re-versioning both for the negotiated revision would collapse them into the same alternative, so the sender must fail the transfer rather than announce versions the payload is not written with. `Dynamic` is the deliberate exception to revision-derived versioning: a `Dynamic` column announces its nested types itself — through its own type lists and through the [binary type encoding](/reference/data-types/data-types-binary-encoding), whose explicit version field carries a spelled-out state version (the values of its shared variant embed their type that way even at rest) — and those announcements are part of the column's own data, identical for every peer and every medium, so they are never rewritten for a negotLine truncated
- `QBit(T, N[, stride])` stores a vector with its bit planes transposed for vector-search workloads; its on-wire stream layout (group-major `FixedString` bit-plane streams, `element_size * (N / stride)` of them with an explicit `stride`) and its binary type encoding (tag `0x36`, or `0x37` `QBitWithStride` when `stride != N`) are documented on the [`QBit` data type page](/reference/data-types/qbit) and in the [binary type encoding](/reference/data-types/data-types-binary-encoding) reference, so a `Native` reader does not have to recover them from the C++ source.
</Note>
@@ -159,6 +159,7 @@ When a feature is active, its fields **must** be present on the wire. The protoc
| HTTP_HANDLER_IN_CLIENT_INFO | 54490 | ClientInfo | Adds `http_handler_name` and `http_request_url` (both `String`) to the **HTTP** branch of ClientInfo, immediately after `http_referer`. They carry the name of the matched SQL-defined HTTP handler and the request URL, so that `currentHandler`/`currentRequestURL` and the `http_handler_name`/`http_request_url` `system.query_log` columns stay populated on remote shards for distributed handler queries. Written only when `query_interface = HTTP` (empty strings for a non-handler HTTP query); required once negotiated version ≥ 54490 — omitting it desynchronizes the rest of the Query packet. |
| QUANTILE_DETERMINISTIC_SKIP_DEGREE | 54491 | Block (Column) | Bumps the state version of `AggregateFunction(quantileDeterministic, ...)` (and the `quantiles`/`median` spellings of it) from `0` to `1`, which appends a trailing `UInt8 skip_degree` to each serialized state. Only the state payload of that one aggregate function changes; the column framing is untouched, and every other aggregate function keeps its own version. Below this revision the writer emits version `0` states, so an older peer is unaffected. |
| STRING_WITH_SIZE_STREAM_SERIALIZATION | 54492 | Block (Column) | `String` columns switch from the per-value length-prefixed layout to a separate stream of cumulative byte offsets (the same offsets layout an `Array` uses), sent as-is: `[UInt64 × num_rows offsets][data blob]`. Gated on the negotiated revision; below 54492 the writer uses the per-value length prefixes. The wire carries no per-column marker for this — both peers derive it from the revision alone. See [String column layout](/resources/develop-contribute/native-protocol/format#string-type). |
| UNIQ_STATE_VERSION_1 | 54493 | Block (Column) | Bumps the state version of `AggregateFunction(uniq, ...)` from `0` to `1`. A version `1` state starts with a flags byte, followed by the main sample in the version-`0` layout; when bit `0` of the flags is set, an additional small sample of full 64-bit hashes, each XOR-ed with the constant `0x9E3779B97F4A0000`, follows (`UInt8 skip_degree`, `VarUInt count`, then `count` 8-byte values) — it is thinned out to at most 16384 values and keeps the estimate accurate at cardinalities above two billion, where the 32-bit main sample degrades and used to overflow into garbage. Bit `1` marks the 64-bit sample as incomplete (the state has absorbed a version `0` state, whose elements the 64-bit sample never saw): no such sample is written, and the estimate comes from the 32-bit main sample alone. The two bits are mutually exclusive and no other bit is defined; a reader must reject a flags byte that sets both or any other bit. Only the state payload of that one aggregate function changes; the column framing is untouched, and every other aggregate function keeps its own version. Pass-through combinators over `uniq` (`uniqIf`, `uniqArray`, `uniqForEach`, `uniqMap`, `uniqArgMin` / `uniqArgMax`, `uniqOrNull` / `uniqOrDefault`, `uniqResample`, and the implicit `Nullable`-argument adaptor) follow the same gate and forward the version to the nested `uniq` states inside their payloads. Below this revision the writer emits version `0` states (the main sample only), so an older peer is unaffected. |
## Packet envelope {#packet-envelope}
@@ -276,6 +276,17 @@ level as two lines: one with the total number of removed nodes and the roots of
subtrees, and one with the paths missing from the snapshot that bound the damaged region. Descendants
below those roots are removed as well but are not listed individually.
After checking the local log tail, Keeper writes the repaired tree to a new snapshot file at the same
log index before starting Raft. Completed repairs take precedence over older copies at that index,
so an interrupted recovery can resume without selecting the damaged snapshot. New followers receive
the repaired snapshot, and subsequent restarts
no longer need orphan removal enabled. The original snapshot and any same-index copies are preserved
with an `orphaned_` filename prefix; these files are excluded from snapshot loading and automatic
retention. Keep them for diagnosis, then remove them manually when they are no longer needed. The
recovery restart needs additional disk space for the repaired snapshot and the backup of the original.
If writing the repaired snapshot, synchronizing local directory changes, or preserving the originals
fails, Keeper refuses to start. A subsequent startup finishes any interrupted archival.
<Warning>
**Repairs local state only**
+1 -14
View File
@@ -1404,19 +1404,7 @@
<level>trace</level>
</text_log>
<!-- Metric log contains rows with current values of ProfileEvents, CurrentMetrics collected with "collect_interval_milliseconds" interval.
The schema of the table is selected by "schema_type":
- "bucketed" (default) - all metrics in a single Map column with the bucketed serialization,
plus an ALIAS column per metric, so the table has a few columns instead of thousands;
- "wide" - a separate column per metric;
- "transposed" - metrics as rows, similar to system.asynchronous_metric_log.
Note that "bucketed" gets the settings of the bucketed Map serialization from the
default table definition, so specifying "engine" below replaces them; for that reason,
and because every metric is an ALIAS column of that schema, the default stays "wide"
when "engine" is specified or when alias columns are disabled with
"default_system_log_flush_policy.skip_alias_columns".
-->
<!-- Metric log contains rows with current values of ProfileEvents, CurrentMetrics collected with "collect_interval_milliseconds" interval. -->
<metric_log>
<database>system</database>
<table>metric_log</table>
@@ -1426,7 +1414,6 @@
<buffer_size_rows_flush_threshold>524288</buffer_size_rows_flush_threshold>
<collect_interval_milliseconds>1000</collect_interval_milliseconds>
<flush_on_crash>false</flush_on_crash>
<!-- <schema_type>bucketed</schema_type> -->
</metric_log>
<!-- Error log contains rows with current values of errors collected with "collect_interval_milliseconds" interval. -->
+1 -1
View File
@@ -2707,7 +2707,7 @@ SETTINGS jemalloc_enable_profiler = 1,
/// Estimated live sampled allocations per (arena, size class, stack), unbiased by `weight`.
function fragmentationPinnersSubquery(minAgeNs) {
return `SELECT arena, size_class, sum(weight) AS est_old_objects, arrayStringConcat(arrayMap(a -> demangle(addressToSymbol(a)), arrayReverse(trace)), ';') AS stack FROM system.jemalloc_sampled_allocations WHERE age_ns > ${minAgeNs} GROUP BY arena, size_class, trace`;
return `SELECT arena, size_class, sum(weight) AS est_old_objects, if(empty(trace), '[unknown]', arrayStringConcat(arrayMap(a -> demangle(addressToSymbol(a)), arrayReverse(trace)), ';')) AS stack FROM system.jemalloc_sampled_allocations WHERE age_ns > ${minAgeNs} GROUP BY arena, size_class, trace`;
}
/// (arena, size class) cells that have at least one old-enough sample; waste in other
+149 -6
View File
@@ -8930,6 +8930,9 @@ async function auxiliaryQuery(server_address, user, password, query, url_params
if (on_error) on_error("Network error");
return false;
}
/// Recorded from the values the request was sent with, whether or not it still belongs to the
/// active connection: the outcome is about these credentials, not the live fields.
rememberAuthenticationOutcome(response, server_address, user, password);
if (!response.ok) {
let error_text = await response.text();
if (!quiet && belongs_to_active_connection()) {
@@ -10155,6 +10158,8 @@ function closeDatabases() {
function toggleDatabases() {
if (!databases_toggle.dataset['opened']) {
/// The databases list is a request with the live credentials (see `dropAutofilledStalePassword`).
dropAutofilledStalePassword();
loadDatabases(url_elem.value, user_elem.value, password_elem.value).then(ok => {
if (ok !== false) {
databases_toggle.dataset['opened'] = 1;
@@ -10331,6 +10336,119 @@ if (main_container && typeof ResizeObserver !== 'undefined') {
new ResizeObserver(refreshPinnedLayout).observe(main_container);
}
/// Logins whose last successful authentication used an empty password, as `[server, user]` keys (see
/// `passwordlessLoginKey`), persisted in `localStorage`.
///
/// The browser's password manager is offered the login after each Run (`storeCredentials`), but the
/// Credential Management API cannot express an empty password: `PasswordCredential` rejects it with a
/// `TypeError`, and there is no API to delete a saved login. So once a password was saved for a
/// server/user and that user later authenticates with an empty password (the account was switched to
/// `no_password`, or its password was removed), the browser keeps the stale entry and autofills it on
/// every visit — and the page would send the stale secret with every request. The page cannot fix the
/// browser's store, but it can remember on its own that the last successful login of this server/user
/// was passwordless, and drop the autofilled password the next time one shows up
/// (`dropAutofilledStalePassword`). Only an autofilled value is dropped (recognised by the `:autofill`
/// pseudo-class): a password typed by the user is always kept, and a successful login with it forgets
/// the entry again, so giving the account a password back is a single ordinary login. Picking the saved
/// login from the browser's suggestions while the entry exists drops it too; the authentication then
/// fails visibly and typing the password once resolves it.
const PASSWORDLESS_LOGINS_KEY = 'passwordless_logins';
const MAX_PASSWORDLESS_LOGINS = 100;
/// The identity of a login for this purpose: the endpoint as `sameServerAddress` compares it (the
/// root-path spelling and the URL userinfo do not make a different server) and the effective user
/// (`effectiveConnectionUser`, so a user given in the userinfo and the same user typed in the field
/// are one login).
function passwordlessLoginKey(server_address, user) {
let endpoint = server_address;
try {
const u = new URL(server_address, location.href);
endpoint = u.protocol + '//' + u.host + ((u.pathname === '' || u.pathname === '/') ? '/' : u.pathname) + u.search;
} catch (e) {
/// Not a parsable URL: keyed by its raw text, it can only ever match itself.
}
return JSON.stringify([endpoint, effectiveConnectionUser(server_address, user)]);
}
function loadPasswordlessLogins() {
let parsed = null;
/// Storage access can throw too (a storage policy denying it), not only the parse of a malformed value.
try { parsed = JSON.parse(window.localStorage.getItem(PASSWORDLESS_LOGINS_KEY) ?? '[]'); } catch (e) { /* malformed or inaccessible: start over */ }
return new Set(Array.isArray(parsed) ? parsed.filter(k => typeof k === 'string') : []);
}
/// Record a successful authentication as `server_address`/`user` with `password`: an empty password
/// remembers the login as passwordless, a non-empty one forgets it.
///
/// Best-effort: it runs on the success path of an authentication check and of a query, and a storage
/// failure (`QuotaExceededError`, a storage policy exception) must not turn that success into a
/// client-side error or lose the green indication. Without the entry the page merely does not drop a
/// stale autofilled password next time, which is the pre-existing behaviour.
function rememberLoginPassword(server_address, user, password) {
const key = passwordlessLoginKey(server_address, user);
const logins = loadPasswordlessLogins();
if (password) {
if (!logins.delete(key)) return;
} else {
if (logins.has(key)) return;
logins.add(key);
/// A `Set` iterates in insertion order, so the oldest entries go first.
while (logins.size > MAX_PASSWORDLESS_LOGINS) logins.delete(logins.values().next().value);
}
try { window.localStorage.setItem(PASSWORDLESS_LOGINS_KEY, JSON.stringify([...logins])); } catch (e) { /* best-effort, see above */ }
}
/// Record what the HTTP `response` to a request sent as `server_address`/`user` with `password` tells
/// about these credentials (see `rememberLoginPassword`). Called from every request that authenticates
/// with the connection fields, not only from `checkCredentials`: the ping in `checkURL` probes the
/// origin rather than the configured path, so on some deployments the credentials check never runs,
/// and a query, the database panel or the completions can be the first request to authenticate.
///
/// ClickHouse authenticates before it executes the query, so the query's own error (a syntax error, an
/// unknown table) follows a successful authentication just like a success does. A rejected login is
/// answered with `401` or `403`. `403` also covers `ACCESS_DENIED` (a missing privilege), which does
/// follow a successful authentication: the `X-ClickHouse-Exception-Code` header tells it apart, so such
/// a response proves the authentication like any other error of the query. Any other error proves the
/// authentication only when the server itself produced it, which the same header shows: a proxy in
/// front of the server can answer on its own. The header is not exposed to a cross-origin page, where
/// only a successful response proves it, and a `403` without it proves nothing either way.
///
/// Without the proof a non-empty password still forgets the passwordless entry: at worst the page then
/// keeps an autofilled password it would have dropped, which is the pre-existing behaviour. An empty
/// password is remembered only with the proof, since the entry makes the page drop a saved password.
const ACCESS_DENIED_ERROR_CODE = '497';
function rememberAuthenticationOutcome(response, server_address, user, password) {
const exception_code = response.headers.get('X-ClickHouse-Exception-Code');
if (response.status === 401) return;
if (response.status === 403 && exception_code?.trim() !== ACCESS_DENIED_ERROR_CODE) return;
if (response.ok || exception_code !== null || password) {
rememberLoginPassword(server_address, user, password);
}
}
/// Whether the browser filled the field itself (its password manager), as opposed to the user typing
/// or the page setting it. Reported by the standard `:autofill` pseudo-class, or its `-webkit-` form in
/// older engines; a browser supporting neither (or an environment without `CSS.supports`, such as the
/// headless harnesses of the integration tests) never reports a field as autofilled, which leaves the
/// stale-password handling inert there.
const AUTOFILL_SELECTOR = (typeof CSS !== 'undefined' && typeof CSS.supports === 'function')
? ([':autofill', ':-webkit-autofill'].find(s => CSS.supports(`selector(${s})`)) ?? null)
: null;
function isAutofilled(elem) {
return AUTOFILL_SELECTOR !== null && elem.matches(AUTOFILL_SELECTOR);
}
/// Drop a browser-autofilled password for a login remembered as passwordless (see
/// `PASSWORDLESS_LOGINS_KEY`). Called wherever an autofilled value can first show up or first be
/// sent: from the `input` event autofill raises, and from the points that read the field to build a
/// request, since autofill does not always raise `input`. Returns whether the field was cleared.
function dropAutofilledStalePassword() {
if (!password_elem.value || !isAutofilled(password_elem)) return false;
if (!loadPasswordlessLogins().has(passwordlessLoginKey(url_elem.value, user_elem.value))) return false;
password_elem.value = '';
return true;
}
let check_credentials_request_num = 0;
/// The connection tuple the credential indication was last checked against, or `null` while nothing
/// has been checked yet. The commit on leaving the connection surface is licensed by the
@@ -10355,11 +10473,18 @@ function connectionNeedsCheck() {
function checkCredentials() {
++check_credentials_request_num;
const current_check_credentials_request_num = check_credentials_request_num;
/// An autofilled password for a login remembered as passwordless must not be sent, or shown as
/// checked (see `dropAutofilledStalePassword`).
dropAutofilledStalePassword();
/// The indication this request is about to produce belongs to the tuple it is sent with.
checked_connection = connectionTuple();
getServerStatus(url_elem.value, user_elem.value, password_elem.value).then(json => {
const [server_address, user, password] = [url_elem.value, user_elem.value, password_elem.value];
getServerStatus(server_address, user, password).then(json => {
if (current_check_credentials_request_num == check_credentials_request_num) {
if (json) {
/// These credentials were accepted: recorded from the values the request was sent with,
/// not the live fields (an autofill raising no `input` could have changed them since).
rememberLoginPassword(server_address, user, password);
[user_elem, password_elem].forEach(e => { e.classList.remove('fail'); e.classList.add('ok'); });
document.getElementById('server_info').innerText = `v${json.v}, uptime ${formatUptime(json.t)} `;
updateServerInfoVisibility();
@@ -10468,6 +10593,9 @@ let connection_edited = false;
/// as the replay intends; they just do not arm a commit. The restored connection's credentials are
/// reported once the user re-enters the password, which is a real edit.
function onCredentialInput(event) {
/// Browser autofill raises `input` too (when it raises anything): a filled-in password for a login
/// remembered as passwordless is dropped right here, before it can be shown or sent.
dropAutofilledStalePassword();
if (event.isTrusted) { connection_edited = true; }
++connection_generation;
resetCredentialsStatus();
@@ -11736,6 +11864,10 @@ async function postImpl(cell, posted_request_num, query, targetResultEl, abortCo
}
}
/// The response the retries above settled on tells whether the snapshotted credentials were
/// accepted, a failure of the query itself included (see `rememberAuthenticationOutcome`).
rememberAuthenticationOutcome(response, connection.url, connection.user, connection.password);
format = response.headers.get('X-ClickHouse-Format') ?? default_format;
/// An image output format (`PNG`) is detected by the `X-ClickHouse-Format` header as well as
@@ -17526,6 +17658,7 @@ async function postOne()
/// reason: these inputs are shared, and re-reading them after the preflight await (as
/// `postImpl` used to) would let a tab switch + connection edit during it send this run's
/// snapshotted query to the newly entered server or identity.
dropAutofilledStalePassword();
const connection = { url: url_elem.value, user: user_elem.value, password: password_elem.value };
/// Snapshot the selected database at launch too (see the connection above): the panel selection
/// is shared, so re-reading it after the preflight await would let a mid-await database change
@@ -17646,6 +17779,7 @@ async function postAll()
const param_sources = Object.assign({}, tab.params || {}, param_values_at_launch);
/// See `postOne`: the connection context and selected database are snapshotted at launch so a tab
/// switch + connection/database edit during the preflight await cannot reroute this run.
dropAutofilledStalePassword();
const connection = { url: url_elem.value, user: user_elem.value, password: password_elem.value };
const query_database = selected_database;
++activation_num;
@@ -17729,6 +17863,8 @@ async function cancel()
}
function storeCredentials() {
/// A stale autofilled password must not be re-offered to the browser's store either.
dropAutofilledStalePassword();
if (password_elem.value) {
const cred = new PasswordCredential({
id: user_elem.value,
@@ -17830,6 +17966,8 @@ format_button.addEventListener('click', async (e) => {
/// An error (e.g. a syntax error in the query) is shown in the result pane, as for the other queries.
format_button.classList.add('busy');
/// The formatting query is a request with the live credentials (see `dropAutofilledStalePassword`).
dropAutofilledStalePassword();
const json = await auxiliaryQuery(url_elem.value, user_elem.value, password_elem.value, body,
'', false, generation);
format_button.classList.remove('busy');
@@ -20141,10 +20279,7 @@ function wasmAvailable() {
}
/// Build the request URL the same way `postImpl` does, but for the completion query.
function buildCompletionUrl() {
const user = user_elem.value;
const password = password_elem.value;
const server_address = url_elem.value;
function buildCompletionUrl(server_address, user, password) {
let url = server_address + (server_address.indexOf('?') >= 0 ? '&' : '?') + 'add_http_cors_header=1';
if (user) url += '&user=' + encodeURIComponent(user);
if (password) url += '&password=' + encodeURIComponent(password);
@@ -20180,12 +20315,17 @@ async function loadCompletions() {
completionsLoadStarted = true;
const generation = connectionGeneration;
/// The completion query is a request with the live credentials (see `dropAutofilledStalePassword`).
dropAutofilledStalePassword();
const [server_address, user, password] = [url_elem.value, user_elem.value, password_elem.value];
try {
const response = await fetch(buildCompletionUrl(), {
const response = await fetch(buildCompletionUrl(server_address, user, password), {
method: "POST",
body: COMPLETION_QUERY,
headers: { 'Authorization': 'never' },
});
rememberAuthenticationOutcome(response, server_address, user, password);
/// The connection settings changed while loading — discard this stale result.
if (generation !== connectionGeneration) {
return;
@@ -22098,6 +22238,7 @@ document.getElementById('download-button').addEventListener('click', async funct
// connection then surfaces as a visible authentication error rather than silently exporting
// data from the wrong server.
const user = last_user_for_download;
dropAutofilledStalePassword();
const password = password_elem.value;
const server_address = last_url_for_download || url_elem.value;
const query = last_query_for_download;
@@ -22244,6 +22385,8 @@ document.getElementById('download-button').addEventListener('click', async funct
function sendCredentialsToTerminal() {
if (terminalIframe && terminalIframe.contentWindow && terminalOrigin) {
/// The terminal authenticates with these credentials (see `dropAutofilledStalePassword`).
dropAutofilledStalePassword();
terminalIframe.contentWindow.postMessage({
type: 'webterminal-credentials',
user: user_elem.value,
+2
View File
@@ -115,6 +115,8 @@
<named_collection_control>1</named_collection_control>
<!-- User can see the values stored in named collections, e.g. in the `system.named_collections` table.
It is also required to override a key stored in a named collection when the collection is used
(adding a missing key is allowed without it).
It is disabled by default: being able to manage named collections does not mean
being able to read back the credentials that are already stored in them.
+131 -130
View File
@@ -187,7 +187,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
"syn 3.0.6",
]
[[package]]
@@ -214,9 +214,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.13.1"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "block-buffer"
@@ -249,9 +249,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.4.3"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630"
dependencies = [
"find-msvc-tools",
"shlex 2.0.1",
@@ -259,9 +259,9 @@ dependencies = [
[[package]]
name = "cfg-if"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "cfg_aliases"
@@ -297,9 +297,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.6.6"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
dependencies = [
"clap_builder",
"clap_derive",
@@ -307,9 +307,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.6.6"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
dependencies = [
"anstream 1.0.0",
"anstyle",
@@ -319,32 +319,32 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.6.9"
version = "4.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be2ad0423bdbbb0e25bc89add796f3559706d4a95e1bc98e4d9662a957b6a19"
checksum = "037e2a1a92236d0aff7e845093f64661d6df4c02c9fcc61a60e9e1d736fa392f"
dependencies = [
"clap",
]
[[package]]
name = "clap_derive"
version = "4.6.4"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
dependencies = [
"anstyle",
"heck",
"proc-macro2",
"pulldown-cmark",
"quote",
"syn 3.0.3",
"syn 3.0.6",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
[[package]]
name = "cobs"
@@ -352,7 +352,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
dependencies = [
"thiserror 2.0.20",
"thiserror 2.0.21",
]
[[package]]
@@ -514,18 +514,18 @@ version = "0.132.1"
[[package]]
name = "crc32fast"
version = "1.5.0"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
checksum = "e71406cd8807725f7ac2f999a4cdd32e98f829fdf65f528343cebf945e41df1e"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
@@ -536,18 +536,18 @@ dependencies = [
[[package]]
name = "crossbeam-channel"
version = "0.5.16"
version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.7"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
@@ -555,27 +555,27 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.20"
version = "0.9.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.13"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26"
checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.22"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
[[package]]
name = "crunchy"
@@ -616,9 +616,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.199"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824894a4a85dca76d4c95c2b9098c036f5a29f627b30c12780774f6654e60974"
checksum = "13f6de320895f42e6e081abb5c7983bedcf0b6d0ff9323de0d33f620c8ac1199"
dependencies = [
"cc",
"cxx-build",
@@ -631,9 +631,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.199"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1ae0b651ea5b0000b19513aef5a03f194d7e3486f2d9258b658da8677fe9036"
checksum = "4fde53ca86b9704a943fef0f1e1d836239a6aedca5de3c65fa9f97ec0bd46d39"
dependencies = [
"cc",
"codespan-reporting",
@@ -641,39 +641,39 @@ dependencies = [
"proc-macro2",
"quote",
"scratch",
"syn 3.0.3",
"syn 3.0.6",
]
[[package]]
name = "cxxbridge-cmd"
version = "1.0.199"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb05f91d3fb8435d9bab6ac5ce6ac1868be774325fb7fb2a91be39393b21388e"
checksum = "07bae89236c811fd4d08ed3441759ac4ac98d752cbfd3de341315ba16ad20ec3"
dependencies = [
"clap",
"codespan-reporting",
"indexmap",
"proc-macro2",
"quote",
"syn 3.0.3",
"syn 3.0.6",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.199"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf293202e0e3e98495785745389e8d0755b217e66f19194a5c695c25e03282ef"
checksum = "49045042e5fced01b80742aba5508de82aa4f13677ed3fa2b4cda709c40c5918"
[[package]]
name = "cxxbridge-macro"
version = "1.0.199"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca001d746947c7249ed9d332a10f7a59daedbafeb0ec68c5c18a7db7a93f6ccc"
checksum = "b181252e2e3d3b5d183afbdc033a3958b445eb3e1a0ae65fc3d4f5259f5da6fd"
dependencies = [
"indexmap",
"proc-macro2",
"quote",
"syn 3.0.3",
"syn 3.0.6",
]
[[package]]
@@ -749,7 +749,7 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
dependencies = [
"thiserror 2.0.20",
"thiserror 2.0.21",
]
[[package]]
@@ -832,9 +832,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
version = "1.17.0"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
[[package]]
name = "embedded-io"
@@ -907,9 +907,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.11"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "fnv"
@@ -1084,9 +1084,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "2.14.0"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
@@ -1126,9 +1126,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
version = "0.2.35"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb"
dependencies = [
"defmt",
"jiff-core",
@@ -1141,18 +1141,19 @@ dependencies = [
[[package]]
name = "jiff-core"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c"
dependencies = [
"defmt",
"log",
]
[[package]]
name = "jiff-static"
version = "0.2.35"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212"
dependencies = [
"jiff-core",
"proc-macro2",
@@ -1162,9 +1163,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.104"
version = "0.3.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
dependencies = [
"cfg-if",
"futures-util",
@@ -1173,9 +1174,9 @@ dependencies = [
[[package]]
name = "lazy_static"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
[[package]]
name = "leb128fmt"
@@ -1185,9 +1186,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.189"
version = "0.2.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
[[package]]
name = "libm"
@@ -1197,9 +1198,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libredox"
version = "0.1.20"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a"
checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d"
dependencies = [
"libc",
]
@@ -1221,9 +1222,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "log"
version = "0.4.33"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "mach2"
@@ -1242,9 +1243,9 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "memfd"
version = "0.6.5"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227"
checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90"
dependencies = [
"rustix",
]
@@ -1255,7 +1256,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.13.1",
"bitflags 2.13.2",
"cfg-if",
"cfg_aliases",
"libc",
@@ -1345,9 +1346,9 @@ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "portable-atomic-util"
version = "0.2.7"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
dependencies = [
"portable-atomic",
]
@@ -1366,9 +1367,9 @@ dependencies = [
[[package]]
name = "powerfmt"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
[[package]]
name = "ppv-lite86"
@@ -1436,7 +1437,7 @@ version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
dependencies = [
"bitflags 2.13.1",
"bitflags 2.13.2",
"memchr",
"unicase",
]
@@ -1552,22 +1553,22 @@ dependencies = [
[[package]]
name = "ref-cast"
version = "1.0.26"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d"
checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.26"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c"
checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
"syn 3.0.6",
]
[[package]]
@@ -1627,11 +1628,11 @@ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
[[package]]
name = "rustix"
version = "1.1.4"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
dependencies = [
"bitflags 2.13.1",
"bitflags 2.13.2",
"errno",
"libc",
"linux-raw-sys",
@@ -1672,7 +1673,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 3.0.3",
"syn 3.0.6",
]
[[package]]
@@ -1718,7 +1719,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
"syn 3.0.6",
]
[[package]]
@@ -1729,7 +1730,7 @@ checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
"syn 3.0.6",
]
[[package]]
@@ -1839,9 +1840,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.2"
version = "1.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
dependencies = [
"serde",
]
@@ -1912,9 +1913,9 @@ dependencies = [
[[package]]
name = "syn"
version = "3.0.3"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
@@ -1967,11 +1968,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.20"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
"thiserror-impl 2.0.20",
"thiserror-impl 2.0.21",
]
[[package]]
@@ -1987,13 +1988,13 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.20"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
"syn 3.0.6",
]
[[package]]
@@ -2070,15 +2071,15 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicase"
version = "2.9.0"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f"
[[package]]
name = "unicode-ident"
version = "1.0.24"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "unicode-segmentation"
@@ -2192,9 +2193,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.127"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
dependencies = [
"cfg-if",
"once_cell",
@@ -2205,9 +2206,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.127"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -2215,22 +2216,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.127"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.119",
"syn 3.0.6",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.127"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
dependencies = [
"unicode-ident",
]
@@ -2247,12 +2248,12 @@ dependencies = [
[[package]]
name = "wasm-encoder"
version = "0.256.0"
version = "0.261.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec1492381bfd5ea51c2a99a919b676662559925cb8d7490547ec2e14c1ad3eb1"
checksum = "2608e8bb6d67fd68f5a8d0eb1363d6e7bcbc1f8ded5a0bd3a1e382462b876b22"
dependencies = [
"leb128fmt",
"wasmparser 0.256.0",
"wasmparser 0.261.0",
]
[[package]]
@@ -2261,7 +2262,7 @@ version = "0.248.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa4439c5eee9df71ee0c6efb37f63b1fcb1fec38f85f5142c54e7ed05d33091a"
dependencies = [
"bitflags 2.13.1",
"bitflags 2.13.2",
"hashbrown 0.17.1",
"indexmap",
"semver",
@@ -2270,11 +2271,11 @@ dependencies = [
[[package]]
name = "wasmparser"
version = "0.256.0"
version = "0.261.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60bd825ffedc6cba8a642924ba7ae424afbc47811cffbcb7b92031ec24e59b4c"
checksum = "4f20f20e44f7e8aeb6744823ea9d869ede51e51be4fdaedede2852282e54d2d8"
dependencies = [
"bitflags 2.13.1",
"bitflags 2.13.2",
"indexmap",
"semver",
]
@@ -2296,7 +2297,7 @@ version = "45.0.1"
dependencies = [
"addr2line",
"async-trait",
"bitflags 2.13.1",
"bitflags 2.13.2",
"bumpalo",
"cc",
"cfg-if",
@@ -2401,7 +2402,7 @@ dependencies = [
"pulley-interpreter",
"smallvec",
"target-lexicon",
"thiserror 2.0.20",
"thiserror 2.0.21",
"wasmparser 0.248.0",
"wasmtime-environ",
"wasmtime-internal-core",
@@ -2477,22 +2478,22 @@ dependencies = [
[[package]]
name = "wast"
version = "256.0.0"
version = "261.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3ad42723fc9222da007f05812a3249c9a4ee7af79d497fc2a2079579ad7efe6"
checksum = "776443145731a4062e5b0d392892a2005909b6ab72d9fdc3cad53dd1a714e44a"
dependencies = [
"bumpalo",
"leb128fmt",
"memchr",
"unicode-width 0.2.2",
"wasm-encoder 0.256.0",
"wasm-encoder 0.261.0",
]
[[package]]
name = "wat"
version = "1.256.0"
version = "1.261.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37cc86c54d8011b3202e265bfebada440733ea3a788ffaf46d395f7d2fdeacfb"
checksum = "a7b4d1a49ea73a8f3326e74e3a05db667001b16bd1035ed3356fc1a0ed05ca7f"
dependencies = [
"wast",
]
@@ -2550,7 +2551,7 @@ dependencies = [
"regalloc2",
"smallvec",
"target-lexicon",
"thiserror 2.0.20",
"thiserror 2.0.21",
"wasmparser 0.248.0",
"wasmtime-environ",
"wasmtime-internal-core",
@@ -2654,18 +2655,18 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "zerocopy"
version = "0.8.56"
version = "0.8.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.56"
version = "0.8.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
dependencies = [
"proc-macro2",
"quote",
+14
View File
@@ -2,6 +2,8 @@
#include <Common/Exception.h>
#include <Common/quoteString.h>
#include <Interpreters/ExpressionContainsArrayJoin.h>
#include <Interpreters/ExpressionContainsColumnMatcher.h>
#include <Parsers/IAST.h>
#include <boost/range/algorithm/equal.hpp>
@@ -11,6 +13,7 @@ namespace ErrorCodes
{
extern const int NOT_IMPLEMENTED;
extern const int ILLEGAL_PREWHERE;
extern const int BAD_ARGUMENTS;
}
void checkRowPolicyFilterExpression(const ASTPtr & expression)
@@ -18,6 +21,17 @@ void checkRowPolicyFilterExpression(const ASTPtr & expression)
/// `arrayJoin` changes the number of rows, while a row policy filter must yield one verdict per row.
if (expressionContainsArrayJoin(expression))
throw Exception(ErrorCodes::ILLEGAL_PREWHERE, "arrayJoin is not allowed in a row policy filter expression");
/// A row policy filter is a predicate over the rows of one table, so a column matcher has no meaning in it.
/// Reject it here, when the policy is created or altered, rather than only on the next read of the table.
if (expression)
{
if (const auto * matcher = findColumnMatcherInExpression(*expression))
throw Exception(ErrorCodes::BAD_ARGUMENTS,
"Column matcher {} is not allowed in a row policy filter expression; list the columns explicitly. In filter {}",
matcher->formatForErrorMessage(),
expression->formatForErrorMessage());
}
}
+1
View File
@@ -73,6 +73,7 @@ using RowPolicyPtr = std::shared_ptr<const RowPolicy>;
/// A row policy filter is applied as a per-row predicate at the storage read stage, so a
/// row-count-changing function like `arrayJoin` breaks the reader's invariants. Reject it.
/// A column matcher (`*`, `COLUMNS(...)`) has no meaning in a per-row predicate either. Reject it too.
void checkRowPolicyFilterExpression(const ASTPtr & expression);
}
@@ -237,6 +237,13 @@ For the calculation state, the function uses a sample of element hash values up
This algorithm is very accurate and very efficient on the CPU.
When the query contains several of these functions, using `uniq` is almost as fast as using other aggregate functions.
The sample keeps 32-bit hashes, which cannot distinguish more than a few billion values, so in addition
a small heavily-thinned-out sample of full 64-bit hashes is maintained (about one hash per 262144 values,
bounded by 16384 hashes), and the estimate switches to it at around two billion. This keeps the estimate
accurate (within about 1%) for cardinalities of hundreds of billions and more, where servers older
than 26.8 returned garbage (issue #6078). States written by those servers carry no 64-bit sample,
so merging with them keeps only the 32-bit precision for the merged-in values.
</details>
<Tip>
+85 -9
View File
@@ -10,6 +10,7 @@
#include <base/bit_cast.h>
#include <DataTypes/DataTypeAggregateFunction.h>
#include <DataTypes/DataTypesNumber.h>
#include <DataTypes/DataTypeTuple.h>
@@ -27,6 +28,7 @@
#include <AggregateFunctions/UniquesHashSet.h>
#include <Columns/ColumnFixedString.h>
#include <Columns/ColumnString.h>
#include <Core/ProtocolDefines.h>
#include <Common/VectorWithMemoryTracking.h>
namespace DB
@@ -256,6 +258,16 @@ struct IsUniqExactSet<UniqExactSet<T1, T2>> : std::true_type
{
};
template <typename T>
struct IsUniquesHashSet : std::false_type
{
};
template <typename Hash>
struct IsUniquesHashSet<UniquesHashSet<Hash>> : std::true_type
{
};
/** Hash function for uniq.
*/
template <typename T, typename ColumnType>
@@ -549,6 +561,7 @@ private:
static constexpr size_t num_args = 1;
static constexpr bool is_able_to_parallelize_merge = Data::is_able_to_parallelize_merge;
static constexpr bool is_parallelize_merge_prepare_needed = Data::is_parallelize_merge_prepare_needed;
static constexpr bool is_uniques_hash_set = detail::IsUniquesHashSet<DataSet>::value;
public:
explicit AggregateFunctionUniq(const DataTypes & argument_types_)
@@ -560,6 +573,34 @@ public:
bool allocatesMemoryInArena() const override { return false; }
/// Version 1 of the `uniq` state carries an additional small sample of 64-bit hashes,
/// which fixes the overflow of the estimate at cardinalities above ten billion (issue #6078).
bool isVersioned() const override { return is_uniques_hash_set; }
/// The default version - the one used when the type carries no explicit version - must stay 0,
/// so that unversioned `AggregateFunction(uniq, ...)` types in persisted data keep the byte
/// layout unversioned data always had. Version 1 applies only where it is spelled out
/// in the type name (see `getStateType`) or derived from the negotiated revision on the `Native` wire.
/// See the analogous comment for `quantileDeterministic` in AggregateFunctionQuantile.h for the full reasoning.
size_t getDefaultVersion() const override { return 0; }
/// The state type spells the version out, so that the type name and the payload stay consistent
/// on every medium the state can travel through.
DataTypePtr getStateType() const override
{
if constexpr (is_uniques_hash_set)
return std::make_shared<DataTypeAggregateFunction>(this->shared_from_this(), this->argument_types, this->parameters, 1);
else
return IAggregateFunction::getStateType();
}
size_t getVersionFromRevision(size_t revision) const override
{
if (is_uniques_hash_set && revision >= DBMS_MIN_REVISION_WITH_UNIQ_STATE_VERSION_1)
return 1;
return 0;
}
/// ALWAYS_INLINE is required to have better code layout for uniqHLL12 function
void ALWAYS_INLINE add(AggregateDataPtr __restrict place, const IColumn ** columns, size_t row_num, Arena *) const override
{
@@ -573,7 +614,7 @@ public:
{
using Traits = detail::AggregateFunctionUniqTraits<T, ColumnType>;
addBatchUniq<&DataSet::insertHash>(row_begin, row_end, places, place_offset, columns, if_argument_pos,
[&](size_t row) { return DataSet::hash(Traits::hash(Traits::value(*columns[0], row))); },
[&](size_t row) { return DataSet::fullHash(Traits::hash(Traits::value(*columns[0], row))); },
[&](AggregateDataPtr place) { return &this->data(place).set; });
}
else
@@ -672,14 +713,20 @@ public:
}
}
void serialize(ConstAggregateDataPtr __restrict place, WriteBuffer & buf, std::optional<size_t> /* version */) const override
void serialize(ConstAggregateDataPtr __restrict place, WriteBuffer & buf, std::optional<size_t> version) const override
{
this->data(place).set.write(buf);
if constexpr (is_uniques_hash_set)
this->data(place).set.write(buf, version.value_or(getDefaultVersion()) < 1);
else
this->data(place).set.write(buf);
}
void deserialize(AggregateDataPtr __restrict place, ReadBuffer & buf, std::optional<size_t> /* version */, Arena *) const override
void deserialize(AggregateDataPtr __restrict place, ReadBuffer & buf, std::optional<size_t> version, Arena *) const override
{
this->data(place).set.read(buf);
if constexpr (is_uniques_hash_set)
this->data(place).set.read(buf, version.value_or(getDefaultVersion()) < 1);
else
this->data(place).set.read(buf);
}
void insertResultInto(AggregateDataPtr __restrict place, IColumn & to, Arena *) const override
@@ -701,6 +748,7 @@ private:
static constexpr size_t is_able_to_parallelize_merge = Data::is_able_to_parallelize_merge;
static constexpr size_t argument_is_tuple = Data::argument_is_tuple;
static constexpr bool is_uniques_hash_set = detail::IsUniquesHashSet<typename Data::Set>::value;
size_t num_args = 0;
@@ -718,6 +766,28 @@ public:
bool allocatesMemoryInArena() const override { return false; }
/// Version 1 of the `uniq` state carries an additional small sample of 64-bit hashes,
/// which fixes the overflow of the estimate at cardinalities above ten billion (issue #6078).
/// The versioning follows the same scheme as the single-argument `uniq` above, see the comments there.
bool isVersioned() const override { return is_uniques_hash_set; }
size_t getDefaultVersion() const override { return 0; }
DataTypePtr getStateType() const override
{
if constexpr (is_uniques_hash_set)
return std::make_shared<DataTypeAggregateFunction>(this->shared_from_this(), this->argument_types, this->parameters, 1);
else
return IAggregateFunction::getStateType();
}
size_t getVersionFromRevision(size_t revision) const override
{
if (is_uniques_hash_set && revision >= DBMS_MIN_REVISION_WITH_UNIQ_STATE_VERSION_1)
return 1;
return 0;
}
void add(AggregateDataPtr __restrict place, const IColumn ** columns, size_t row_num, Arena *) const override
{
detail::Adder<T, ColumnTuple, Data>::add(this->data(place), columns, num_args, row_num);
@@ -779,14 +849,20 @@ public:
}
}
void serialize(ConstAggregateDataPtr __restrict place, WriteBuffer & buf, std::optional<size_t> /* version */) const override
void serialize(ConstAggregateDataPtr __restrict place, WriteBuffer & buf, std::optional<size_t> version) const override
{
this->data(place).set.write(buf);
if constexpr (is_uniques_hash_set)
this->data(place).set.write(buf, version.value_or(getDefaultVersion()) < 1);
else
this->data(place).set.write(buf);
}
void deserialize(AggregateDataPtr __restrict place, ReadBuffer & buf, std::optional<size_t> /* version */, Arena *) const override
void deserialize(AggregateDataPtr __restrict place, ReadBuffer & buf, std::optional<size_t> version, Arena *) const override
{
this->data(place).set.read(buf);
if constexpr (is_uniques_hash_set)
this->data(place).set.read(buf, version.value_or(getDefaultVersion()) < 1);
else
this->data(place).set.read(buf);
}
void insertResultInto(AggregateDataPtr __restrict place, IColumn & to, Arena *) const override
@@ -118,6 +118,11 @@ public:
return nested_func->getDefaultVersion();
}
DataTypePtr getStateType() const override
{
return this->getStateTypeWithVersionOf(*nested_func);
}
void create(AggregateDataPtr __restrict place) const override
{
nested_func->create(place);
@@ -138,6 +138,8 @@ public:
size_t getDefaultVersion() const override { return nested_function->getDefaultVersion(); }
DataTypePtr getStateType() const override { return this->getStateTypeWithVersionOf(*nested_function); }
bool allocatesMemoryInArena() const override
{
return nested_function->allocatesMemoryInArena() || singleValueTypeAllocatesMemoryInArena(key_type->getTypeId());
@@ -359,6 +359,11 @@ public:
return nested_func->getDefaultVersion();
}
DataTypePtr getStateType() const override
{
return this->getStateTypeWithVersionOf(*nested_func);
}
bool canMergeStateFromDifferentVariant(const IAggregateFunction & rhs) const override
{
/// Distinct state contains a history of unique values and can be merged across
@@ -223,6 +223,11 @@ public:
return nested_func->getDefaultVersion();
}
DataTypePtr getStateType() const override
{
return this->getStateTypeWithVersionOf(*nested_func);
}
template <bool up_to_state>
void destroyImpl(AggregateDataPtr __restrict place) const noexcept
{
@@ -473,7 +478,7 @@ public:
}
}
void serialize(ConstAggregateDataPtr __restrict place, WriteBuffer & buf, std::optional<size_t> /* version */) const override
void serialize(ConstAggregateDataPtr __restrict place, WriteBuffer & buf, std::optional<size_t> version) const override
{
const AggregateFunctionForEachData & state = data(place);
writeBinaryLittleEndian(state.dynamic_array_size, buf);
@@ -481,7 +486,10 @@ public:
const char * nested_state = state.array_of_aggregate_datas;
for (size_t i = 0; i < state.dynamic_array_size; ++i)
{
nested_func->serialize(nested_state, buf);
/// `isVersioned` and `getDefaultVersion` forward to the nested function, so the version
/// is in the nested function's terms already. `deserialize` forwards it, so dropping it
/// here would desynchronize the written payload from the later read.
nested_func->serialize(nested_state, buf, version);
nested_state += nested_size_of_data;
}
}
@@ -97,6 +97,11 @@ public:
return nested_func->getDefaultVersion();
}
DataTypePtr getStateType() const override
{
return this->getStateTypeWithVersionOf(*nested_func);
}
void create(AggregateDataPtr __restrict place) const override
{
nested_func->create(place);
@@ -132,6 +132,11 @@ public:
return nested_func->getDefaultVersion();
}
DataTypePtr getStateType() const override
{
return this->getStateTypeWithVersionOf(*nested_func);
}
AggregateFunctionMap(AggregateFunctionPtr nested, const DataTypes & types)
: Base(types, nested->getParameters(), std::make_shared<DataTypeMap>(DataTypes{getKeyType(types, nested), nested->getResultType()}))
, nested_func(nested)
@@ -290,7 +295,7 @@ public:
destroyImpl<true>(place);
}
void serialize(ConstAggregateDataPtr __restrict place, WriteBuffer & buf, std::optional<size_t> /* version */) const override
void serialize(ConstAggregateDataPtr __restrict place, WriteBuffer & buf, std::optional<size_t> version) const override
{
auto & merged_maps = this->data(place).merged_maps;
writeVarUInt(merged_maps.size(), buf);
@@ -298,11 +303,14 @@ public:
for (const auto & elem : merged_maps)
{
this->data(place).writeKey(elem.first, buf);
nested_func->serialize(elem.second, buf);
/// `isVersioned` and `getDefaultVersion` forward to the nested function, so the version
/// is in the nested function's terms already; dropping it would silently downgrade
/// every per-key state to the format of the nested function's default version.
nested_func->serialize(elem.second, buf, version);
}
}
void deserialize(AggregateDataPtr __restrict place, ReadBuffer & buf, std::optional<size_t> /* version */, Arena * arena) const override
void deserialize(AggregateDataPtr __restrict place, ReadBuffer & buf, std::optional<size_t> version, Arena * arena) const override
{
auto & merged_maps = this->data(place).merged_maps;
UInt64 size = 0;
@@ -313,7 +321,6 @@ public:
KeyType key{};
this->data(place).readKey(key, buf);
/// Take the slot before creating the state. `emplace` on a key that is already present
/// does not insert, so creating first would abandon the fresh state: nothing would
/// reference it and `destroyImpl` walks only `merged_maps`. `serialize` writes each key
@@ -336,7 +343,7 @@ public:
throw;
}
nested_func->deserialize(it->second, buf, std::nullopt, arena);
nested_func->deserialize(it->second, buf, version, arena);
}
}
@@ -47,6 +47,19 @@ public:
return nested_func->isVersioned();
}
/** `Merge` was the one combinator that forwarded `isVersioned` and `getDefaultVersion` but not
* this, so it answered the base class's `0` for every revision while claiming to be versioned.
* No shape of `...MergeState...` was found where that is observable - the state type carries
* the version through the chain on its own, and a round trip of `uniqMergeState` /
* `uniqMergeStateIf` over `remote()` was verified to announce `AggregateFunction(1, uniq, ...)`
* and to write a byte-identical payload either way - but a function that reports a version
* independent of the revision it is asked about is a trap for the next caller of it.
*/
size_t getVersionFromRevision(size_t revision) const override
{
return nested_func->getVersionFromRevision(revision);
}
size_t getDefaultVersion() const override
{
return nested_func->getDefaultVersion();
@@ -356,6 +356,11 @@ public:
return nested_function->getDefaultVersion();
}
DataTypePtr getStateType() const override
{
return this->getStateTypeWithVersionOf(*nested_function);
}
AggregateFunctionPtr getNestedFunction() const override { return nested_function; }
#if USE_EMBEDDED_COMPILER
@@ -80,11 +80,21 @@ public:
return nested_function->isVersioned();
}
size_t getVersionFromRevision(size_t revision) const override
{
return nested_function->getVersionFromRevision(revision);
}
size_t getDefaultVersion() const override
{
return nested_function->getDefaultVersion();
}
DataTypePtr getStateType() const override
{
return this->getStateTypeWithVersionOf(*nested_function);
}
bool isState() const override
{
return nested_function->isState();
@@ -146,6 +146,11 @@ public:
return nested_function->getDefaultVersion();
}
DataTypePtr getStateType() const override
{
return this->getStateTypeWithVersionOf(*nested_function);
}
bool allocatesMemoryInArena() const override
{
return nested_function->allocatesMemoryInArena();
@@ -92,6 +92,30 @@ size_t AggregateFunctionTuple::getVersionFromRevision(size_t revision) const
return version;
}
/// `-Tuple` is a pass-through combinator: it stores the nested states inside its own state and
/// forwards the version to every one of them in `serialize` / `deserialize`. So, like the other
/// pass-through combinators, its state type has to spell the version out - otherwise a fresh
/// state column is created at the default version and every local round trip of the column
/// (`groupArray` over the states, sorting, views) writes the nested states in the legacy layout.
/// The version is the same maximum over the nested functions that `getDefaultVersion` takes,
/// but computed from the versions the nested functions spell out in their own state types.
DataTypePtr AggregateFunctionTuple::getStateType() const
{
std::optional<size_t> version;
for (const auto & func : nested_functions)
{
/// `getStateType` returns a fresh `DataTypePtr` by value, so it has to be held in a named
/// variable - a temporary would be destroyed at the end of the full expression.
const DataTypePtr nested_state_type = func->getStateType();
const auto * nested_state = typeid_cast<const DataTypeAggregateFunction *>(nested_state_type.get());
if (!nested_state)
continue;
if (auto nested_version = nested_state->getVersionIfExplicit())
version = std::max(version.value_or(0), *nested_version);
}
return std::make_shared<DataTypeAggregateFunction>(shared_from_this(), argument_types, parameters, version);
}
void AggregateFunctionTuple::create(AggregateDataPtr __restrict place) const
{
size_t i = 0;
@@ -71,6 +71,7 @@ public:
bool isVersioned() const override;
size_t getDefaultVersion() const override;
size_t getVersionFromRevision(size_t revision) const override;
DataTypePtr getStateType() const override;
size_t sizeOfData() const override { return total_state_size; }
size_t alignOfData() const override { return max_state_align; }
@@ -2,6 +2,7 @@
#include <AggregateFunctions/IAggregateFunction.h>
#include <DataTypes/DataTypeAggregateFunction.h>
#include <Common/Exception.h>
#include <Common/typeid_cast.h>
namespace DB
@@ -17,6 +18,18 @@ DataTypePtr IAggregateFunction::getStateType() const
return std::make_shared<DataTypeAggregateFunction>(shared_from_this(), argument_types, parameters);
}
DataTypePtr IAggregateFunction::getStateTypeWithVersionOf(const IAggregateFunction & nested) const
{
std::optional<size_t> version;
/// The state type has to be held in a named variable: `getStateType` returns a fresh
/// `DataTypePtr` by value, and a temporary would be destroyed at the end of the full
/// expression, leaving `nested_state` dangling.
const DataTypePtr nested_state_type = nested.getStateType();
if (const auto * nested_state = typeid_cast<const DataTypeAggregateFunction *>(nested_state_type.get()))
version = nested_state->getVersionIfExplicit();
return std::make_shared<DataTypeAggregateFunction>(shared_from_this(), argument_types, parameters, version);
}
DataTypePtr IAggregateFunction::getNormalizedStateType() const
{
DataTypes normalized_argument_types;
@@ -97,6 +97,16 @@ public:
/// Same as the above but normalize state types so that variants with the same binary representation will use the same type.
virtual DataTypePtr getNormalizedStateType() const;
/// State type for a pass-through combinator - one that stores nested states inside its own state
/// and forwards `isVersioned` / `getDefaultVersion` to the nested function (`-If`, `-Array`,
/// `-ForEach`, `-Map`, `-ArgMin` / `-ArgMax`, `-OrNull` / `-OrDefault`, `-Resample`, `-Distinct`,
/// and the implicit adaptor for `Nullable` arguments).
/// If the nested function spells its current state version out in its state type, the combinator's
/// state type must spell the same version: a fresh state column otherwise falls back to the legacy
/// default version on local serialization round trips of the column (`groupArray` over the states,
/// sorting, views), losing the information the newer version carries.
DataTypePtr getStateTypeWithVersionOf(const IAggregateFunction & nested) const;
/// Identifies the state representation variant used by this function.
/// The default is Aggregation (normal GROUP BY implementation).
virtual AggregateFunctionStateVariant getStateVariant() const
-61
View File
@@ -170,45 +170,6 @@ public:
}
}
/// The number of leading bytes `compact_theta_sketch_parser::parse` reads before it checks that the
/// buffer holds them, derived from the first 8 bytes - the only ones it does check for up front. The
/// offsets are the ones of `compact_theta_sketch_parser`; every serial version and preamble size not
/// listed here is either checked by the parser itself before each read or rejected by it from the
/// first 8 bytes.
static size_t headerBytesReadBeforeSizeCheck(const uint8_t * data, size_t size)
{
static constexpr size_t header_size = 8;
if (size < header_size)
return header_size;
const uint8_t preamble_longs = data[0];
const uint8_t serial_version = data[1];
const uint8_t flags = data[5];
static constexpr uint8_t is_empty_flag = 1 << 2;
switch (serial_version)
{
/// `num_entries` at offset 8 and `theta` at offset 16, unconditionally.
case 1:
return 24;
/// `num_entries` at offset 8 for two preamble longs, `theta` at offset 16 as well for three.
case 2:
if (preamble_longs == 2)
return 16;
if (preamble_longs == 3)
return 24;
return header_size;
/// `num_entries` at offset 8 unless the sketch is flagged empty; a single-entry sketch
/// (one preamble long) and `theta` (more than two) are checked by the parser itself.
case 3:
if ((flags & is_empty_flag) || preamble_longs == 1 || preamble_longs > 2)
return header_size;
return 16;
default:
return header_size;
}
}
/// You can only call for an empty object.
void read(DB::ReadBuffer & in)
{
@@ -217,28 +178,6 @@ public:
if (bytes.empty())
return;
/** `compact_theta_sketch_parser::parse` verifies that the buffer holds 8 bytes and then reads
* header fields that lie beyond them before it checks the size again: `num_entries` at offset
* 8 for serial versions 1, 2 and 3, and `theta` at offset 16 for serial versions 1 and 2. A
* state shorter than that - which any `CAST` from a string can produce - is therefore read
* past its end, and the out-of-bounds value decides the size the parser then demands.
*
* Require up front the bytes the parser reads before it validates anything, exactly as the
* `std::istream` deserializer of `datasketches` has to consume them before it can decide
* anything about the sketch. A state that stops short of them is refused the same way the
* parser refuses one that stops short of its entries, and the parser then never reads past
* the buffer. The upstream fix is to move each `check_memory_size` before the field it
* guards; this keeps the read in bounds for every version of `datasketches-cpp` this
* repository pulls in.
*/
const size_t required_size = headerBytesReadBeforeSizeCheck(bytes.data(), bytes.size());
if (bytes.size() < required_size)
throw Exception(
ErrorCodes::CORRUPTED_DATA,
"Cannot deserialize Theta sketch state: at least {} bytes expected, actual {}",
required_size,
bytes.size());
try
{
auto sk = datasketches::compact_theta_sketch::deserialize(bytes.data(), bytes.size());
+567 -18
View File
@@ -1,5 +1,6 @@
#pragma once
#include <bit>
#include <math.h>
#include <base/defines.h>
@@ -50,6 +51,25 @@ namespace ErrorCodes
* - if the situation repeats, then only elements dividing by 4, etc., are taken.
* - the size() method returns an approximate number of elements that have been inserted into the set;
* - there are methods for quick reading and writing in binary and text form.
*
* A 32-bit hash cannot distinguish more than a few billion elements: the estimate degrades quickly
* beyond a billion, and above ~30 billion the correction formula even resulted in undefined behavior
* (issue #6078). To keep the estimate accurate up to trillions, in addition to the main set a small
* "wide" set of full 64-bit hashes (XOR-ed with a constant, see `wideHash`) is maintained (state version 1).
* It is thinned out aggressively from the very beginning - only values with UNIQUES_HASH_SET_WIDE_INITIAL_SKIP_DEGREE
* zero low-order bits enter it - so it stays empty or tiny for the states where the main set is accurate
* (about 4 values per million distinct elements), and it is bounded by UNIQUES_HASH_SET_WIDE_MAX_SIZE
* values thinned out further. Once the wide set has enough values for its own estimate to be reliable
* (which happens at about two billion elements), size() switches to it.
*
* The wide set has to be maintained from the first insertion: the high bits of the hashes
* of the already consumed elements cannot be recovered later. In particular, a state written
* in the legacy format (state version 0) carries no wide set, and its elements were never
* offered to one. Such a state is marked as having an incomplete wide sample: it drops its own
* wide set, never starts a new one, and propagates the mark through merges, so that size()
* of anything it is merged into falls back to the legacy 32-bit estimator. Estimating from
* a wide set that covers only a part of the elements would undercount the rest entirely,
* which is much worse than the degrading precision of the 32-bit estimate.
*/
/// The maximum degree of buffer size before the values are discarded
@@ -66,6 +86,24 @@ namespace ErrorCodes
/// Initial buffer size degree
#define UNIQUES_HASH_SET_INITIAL_SIZE_DEGREE 4
/** Only hashes with this many zero low-order bits enter the wide set, so that it stays
* negligibly small while the main set is accurate: one hash in 262144 passes.
*/
#define UNIQUES_HASH_SET_WIDE_INITIAL_SKIP_DEGREE 18
/// The maximum degree of the wide set buffer size before the values are thinned out further.
#define UNIQUES_HASH_SET_WIDE_MAX_SIZE_DEGREE 15
/// The maximum number of elements of the wide set: 16384 values take 256 KiB.
#define UNIQUES_HASH_SET_WIDE_MAX_SIZE (1ULL << (UNIQUES_HASH_SET_WIDE_MAX_SIZE_DEGREE - 1))
/** The estimate is taken from the wide set when it has at least this many values, i.e. starting
* from about UNIQUES_HASH_SET_WIDE_MIN_SIZE_FOR_ESTIMATE * 2^UNIQUES_HASH_SET_WIDE_INITIAL_SKIP_DEGREE,
* about two billion elements: at that point the statistical error of the wide estimate (about 1%)
* meets the degrading precision of the 32-bit one.
*/
#define UNIQUES_HASH_SET_WIDE_MIN_SIZE_FOR_ESTIMATE 8192
/** This hash function is not the most optimal, but UniquesHashSet states counted with it,
* stored in many places on disks (in many companies), so it continues to be used.
@@ -87,11 +125,33 @@ private:
using HashValue = UInt32;
using Allocator = HashTableAllocatorWithStackMemory<(1ULL << UNIQUES_HASH_SET_INITIAL_SIZE_DEGREE) * sizeof(UInt32)>;
/** The set of full 64-bit hashes with the same open addressing scheme as the main set.
* It is small (it starts with much stronger thinning), so it does not need stack memory,
* and the whole set, including this header, lives in a single lazy heap allocation.
*/
struct WideSet
{
UInt32 m_size = 0; /// Number of elements
UInt8 size_degree = 0; /// The size of the table as a power of 2
UInt8 skip_degree = UNIQUES_HASH_SET_WIDE_INITIAL_SKIP_DEGREE;
bool has_zero = false;
UInt64 * buf = nullptr;
size_t bufSize() const { return 1ULL << size_degree; }
size_t maxFill() const { return 1ULL << (size_degree - 1); }
size_t mask() const { return bufSize() - 1; }
size_t place(UInt64 x) const { return (x >> (64 - UNIQUES_HASH_MAX_SIZE_DEGREE)) & mask(); }
bool good(UInt64 hash) const { return hash == ((hash >> skip_degree) << skip_degree); }
};
/// Thinning leaves at most 2 ^ (32 - skip_degree) hashes, and `shrinkIfNeed()` raises the
/// degree only while more than UNIQUES_HASH_MAX_SIZE of them remain, so one degree past the
/// thinning budget is the last it can produce.
static constexpr UInt8 max_skip_degree = UNIQUES_HASH_BITS_FOR_SKIP + 1;
/// The analogous bound for the wide set, whose hashes are 64-bit.
static constexpr UInt8 max_wide_skip_degree = (64 - UNIQUES_HASH_MAX_SIZE_DEGREE) + 1;
UInt32 m_size; /// Number of elements
UInt8 size_degree{}; /// The size of the table as a power of 2
UInt8 skip_degree; /// Skip elements not divisible by 2 ^ skip_degree
@@ -99,6 +159,15 @@ private:
HashValue * buf{};
/// nullptr while no hash has passed the wide thinning (i.e. for most of the states).
WideSet * wide = nullptr;
/** Some of the elements of this set were never offered to the wide set, so a wide estimate
* would miss them. This happens when a state serialized in the legacy format (state version 0)
* is read or merged in. The mark is sticky: the missing high bits cannot be recovered.
*/
bool wide_incomplete = false;
#ifdef UNIQUES_HASH_SET_COUNT_COLLISIONS
/// For profiling.
mutable size_t collisions;
@@ -294,6 +363,204 @@ private:
}
}
UInt8 wideSkipDegree() const { return wide ? wide->skip_degree : UNIQUES_HASH_SET_WIDE_INITIAL_SKIP_DEGREE; }
static WideSet * allocWideSet(UInt8 new_size_degree)
{
/// The header and the table are placed in a single allocation.
size_t bytes = sizeof(WideSet) + (1ULL << new_size_degree) * sizeof(UInt64);
WideSet * res = new (HashTableAllocator().alloc(bytes)) WideSet;
res->size_degree = new_size_degree;
res->buf = reinterpret_cast<UInt64 *>(reinterpret_cast<char *>(res) + sizeof(WideSet));
return res;
}
static void freeWideSet(WideSet * set)
{
if (set)
HashTableAllocator().free(set, sizeof(WideSet) + set->bufSize() * sizeof(UInt64));
}
void freeWide()
{
freeWideSet(wide);
wide = nullptr;
}
/// The wide set no longer represents all the elements - drop it and never start a new one.
void markWideIncomplete()
{
wide_incomplete = true;
freeWide();
}
/// Copy the wide set with a table of the given size (the values are reinserted, not memcpy'd).
static WideSet * resizeWideSet(const WideSet * old, UInt8 new_size_degree)
{
WideSet * res = allocWideSet(new_size_degree);
res->m_size = old->m_size;
res->skip_degree = old->skip_degree;
res->has_zero = old->has_zero;
for (size_t i = 0; i < old->bufSize(); ++i)
if (old->buf[i])
wideReinsertImpl(*res, old->buf[i]);
return res;
}
static void wideReinsertImpl(WideSet & set, UInt64 x)
{
size_t place_value = set.place(x);
while (set.buf[place_value])
{
++place_value;
place_value &= set.mask();
}
set.buf[place_value] = x;
}
/// Delete all values of the wide set whose hashes do not divide by 2 ^ skip_degree.
static void wideRehash(WideSet & set)
{
for (size_t i = 0; i < set.bufSize(); ++i)
{
if (set.buf[i])
{
if (!set.good(set.buf[i]))
{
set.buf[i] = 0;
--set.m_size;
}
else if (i != set.place(set.buf[i]))
{
UInt64 x = set.buf[i];
set.buf[i] = 0;
wideReinsertImpl(set, x);
}
}
}
for (size_t i = 0; i < set.bufSize() && set.buf[i]; ++i)
{
if (i != set.place(set.buf[i]))
{
UInt64 x = set.buf[i];
set.buf[i] = 0;
wideReinsertImpl(set, x);
}
}
}
/// Insert a value of the wide set, i.e. a full hash already passed through `wideHash`.
void wideInsert(UInt64 x)
{
if (unlikely(wide_incomplete))
return;
/// The caller may have checked the value against a stale (smaller) thinning mask
/// for the sake of the hot loop, so the check is repeated here.
if (x & ((1ULL << wideSkipDegree()) - 1))
return;
if (!wide)
wide = allocWideSet(UNIQUES_HASH_SET_INITIAL_SIZE_DEGREE);
if (x == 0)
{
wide->m_size += !wide->has_zero;
wide->has_zero = true;
return;
}
size_t place_value = wide->place(x);
while (wide->buf[place_value] && wide->buf[place_value] != x)
{
++place_value;
place_value &= wide->mask();
}
if (wide->buf[place_value] == x)
return;
wide->buf[place_value] = x;
++wide->m_size;
if (unlikely(wide->m_size > wide->maxFill()))
{
if (wide->m_size > UNIQUES_HASH_SET_WIDE_MAX_SIZE)
{
while (wide->m_size > UNIQUES_HASH_SET_WIDE_MAX_SIZE)
{
if (unlikely(wide->skip_degree >= max_wide_skip_degree))
throw DB::Exception(DB::ErrorCodes::INCORRECT_DATA,
"Cannot thin out the wide set of UniquesHashSet: {} elements remain at the maximum skip degree of {}",
static_cast<size_t>(wide->m_size), static_cast<size_t>(max_wide_skip_degree));
++wide->skip_degree;
wideRehash(*wide);
}
}
else
{
WideSet * new_set = resizeWideSet(wide, wide->size_degree + 1);
freeWide();
wide = new_set;
}
}
}
UInt64 wideSkipMask() const { return (1ULL << wideSkipDegree()) - 1; }
/** The wide set samples the full hashes XOR-ed with this constant rather than the hashes themselves.
* The sample stays just as uniform, but the hash of a frequent value never becomes a candidate
* systematically: the default hash of the integer 0 is 0, and zeros are frequent in real data -
* without the offset every such row would be a candidate for the wide set and would cost
* an extra pass over its batch (this doubled the time of `uniq` over such columns).
* The low bits up to the maximum thinning of the main set are zero, so that every candidate
* still passes good() of the main set (see `widePrefilterMask`), and the next ones are not,
* so that the hash 0 is never a candidate.
*/
static constexpr UInt64 wide_hash_offset = 0x9E3779B97F4A0000ULL;
static_assert((wide_hash_offset & ((1ULL << max_skip_degree) - 1)) == 0);
static_assert((wide_hash_offset & ((1ULL << UNIQUES_HASH_SET_WIDE_INITIAL_SKIP_DEGREE) - 1)) != 0);
/// The value of the wide set for a full hash.
static UInt64 wideHash(UInt64 full_hash) { return full_hash ^ wide_hash_offset; }
/** Wide candidates (the values of `wideHash` with at least UNIQUES_HASH_SET_WIDE_INITIAL_SKIP_DEGREE
* zero low-order bits) have the low max_skip_degree bits of the hash itself zero (see `wide_hash_offset`),
* and the thinning of the main set never exceeds them (at the degree of 16 the number
* of possible sampled 32-bit hashes is already down to the maximum size of the set),
* so every wide candidate passes good() of the main set, and it is enough to check
* the candidates on that path.
* The check is a pre-filter on the truncated hash: `((hash ^ wide_hash_offset) & widePrefilterMask()) == 0`.
* It passes a superset of the candidates (an exact one while the wide thinning fits 32 bits),
* and `wideInsert` checks the full value against the current mask itself - which also makes
* a stale pre-filter mask harmless.
*/
HashValue widePrefilterMask() const { return static_cast<HashValue>(wideSkipMask()); }
static constexpr HashValue wide_prefilter_offset = static_cast<HashValue>(wide_hash_offset);
/** With how many different elements, when randomly scattered across 2^32 buckets,
* the given number of non-empty buckets is obtained on average.
* This is the correction of the systematic error due to collisions of the 32-bit hashes.
*/
static size_t correctFor32BitCollisions(size_t res)
{
size_t p32 = 1ULL << 32;
/** When the number of distinct elements is much larger than 2^32, almost all buckets are filled
* and `res` may reach (or, due to the pseudo-random remainder, exceed) 2^32,
* where the formula below is not defined (previously this resulted in undefined behavior
* when casting infinity or NaN to an integer). Saturate the estimate instead:
* 32 bits of a hash contain no information about larger cardinalities anyway
* (and the wide set takes over long before this point).
*/
if (res >= p32)
res = p32 - 1;
return static_cast<size_t>(round(static_cast<double>(p32) * (log(static_cast<double>(p32)) - log(static_cast<double>(p32 - res)))));
}
public:
using value_type = Value;
@@ -310,10 +577,19 @@ public:
}
UniquesHashSet(const UniquesHashSet & rhs) // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init) - base class is an allocator with stack memory, initialized in alloc()
: m_size(rhs.m_size), skip_degree(rhs.skip_degree), has_zero(rhs.has_zero)
: m_size(rhs.m_size), skip_degree(rhs.skip_degree), has_zero(rhs.has_zero), wide_incomplete(rhs.wide_incomplete)
{
alloc(rhs.size_degree);
memcpy(buf, rhs.buf, buf_size() * sizeof(buf[0]));
if (rhs.wide)
{
wide = allocWideSet(rhs.wide->size_degree);
wide->m_size = rhs.wide->m_size;
wide->skip_degree = rhs.wide->skip_degree;
wide->has_zero = rhs.wide->has_zero;
memcpy(wide->buf, rhs.wide->buf, rhs.wide->bufSize() * sizeof(UInt64));
}
}
UniquesHashSet & operator=(const UniquesHashSet & rhs)
@@ -330,34 +606,53 @@ public:
m_size = rhs.m_size;
skip_degree = rhs.skip_degree;
has_zero = rhs.has_zero;
wide_incomplete = rhs.wide_incomplete;
memcpy(buf, rhs.buf, buf_size() * sizeof(buf[0]));
freeWide();
if (rhs.wide)
{
wide = allocWideSet(rhs.wide->size_degree);
wide->m_size = rhs.wide->m_size;
wide->skip_degree = rhs.wide->skip_degree;
wide->has_zero = rhs.wide->has_zero;
memcpy(wide->buf, rhs.wide->buf, rhs.wide->bufSize() * sizeof(UInt64));
}
return *this;
}
~UniquesHashSet()
{
free();
freeWide();
}
static HashValue hash(Value key) { return static_cast<HashValue>(Hash()(key)); }
/// The full 64-bit hash: its low 32 bits feed the main set, and the whole value the wide set (see `wideHash`).
static UInt64 fullHash(Value key) { return static_cast<UInt64>(Hash()(key)); }
static HashValue hash(Value key) { return static_cast<HashValue>(fullHash(key)); }
void ALWAYS_INLINE prefetch(HashValue hash_value) const
void ALWAYS_INLINE prefetch(UInt64 full_hash) const
{
__builtin_prefetch(&buf[place(hash_value)]);
__builtin_prefetch(&buf[place(static_cast<HashValue>(full_hash))]);
}
void ALWAYS_INLINE insert(Value x)
{
insertHash(hash(x));
insertHash(fullHash(x));
}
void ALWAYS_INLINE insertHash(HashValue hash_value)
/// Insert an element by its full 64-bit hash (see `fullHash`), computed by the caller in advance.
void ALWAYS_INLINE insertHash(UInt64 full_hash)
{
const HashValue hash_value = static_cast<HashValue>(full_hash);
if (!good(hash_value))
return;
if (unlikely(((hash_value ^ wide_prefilter_offset) & widePrefilterMask()) == 0))
wideInsert(wideHash(full_hash));
insertImpl(hash_value);
shrinkIfNeed();
}
@@ -367,10 +662,85 @@ public:
/// We choose a value that is big enough to provide sufficient instruction level parallelism but not too big to bloat the code size.
static constexpr size_t insert_many_batch_size = 8;
private:
/** The candidates for the wide set met by `insertMany`. A candidate is expected once per 262144 values,
* but a frequent value can be a candidate too (e.g. the hash of zero is zero), so a run
* of the same value is recorded only once.
*/
/// `values` is intentionally left uninitialized: only the first `size` entries are read.
struct WideCandidates // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init)
{
std::array<Value, 16> values; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init)
size_t size = 0;
/// The position to rescan from, if `values` has overflowed (pathological input).
size_t overflow_position = 0;
bool overflow = false;
};
template <typename SourceType, auto Transform>
static void ALWAYS_INLINE addWideCandidate(WideCandidates & candidates, const SourceType * data, size_t position)
{
if (candidates.overflow)
return;
const Value value = Transform(data[position]);
if (candidates.size && candidates.values[candidates.size - 1] == value)
return;
if (candidates.size == candidates.values.size())
{
candidates.overflow = true;
candidates.overflow_position = position;
return;
}
candidates.values[candidates.size++] = value;
}
template <typename SourceType, auto Transform>
static void ALWAYS_INLINE addWideCandidates(WideCandidates & candidates, const SourceType * data, size_t batch_begin, UInt32 candidate_mask)
{
while (candidate_mask)
{
addWideCandidate<SourceType, Transform>(candidates, data, batch_begin + std::countr_zero(candidate_mask));
candidate_mask &= candidate_mask - 1;
}
}
template <typename SourceType, auto Transform>
void NO_INLINE insertWideCandidates(const WideCandidates & candidates, const SourceType * data, size_t size)
{
for (size_t k = 0; k < candidates.size; ++k)
wideInsert(wideHash(fullHash(candidates.values[k])));
/// Insertion into the wide set is idempotent, so on overflow the tail is simply rescanned.
if (candidates.overflow)
{
for (size_t k = candidates.overflow_position; k < size; ++k)
{
const UInt64 wide_hash = wideHash(fullHash(Transform(data[k])));
if (unlikely((wide_hash & wideSkipMask()) == 0))
wideInsert(wide_hash);
}
}
}
public:
template <typename SourceType, auto Transform>
requires std::is_invocable_r_v<Value, decltype(Transform), const SourceType &>
void insertMany(const SourceType * data, size_t size)
{
/** The candidates for the wide set (see the comment at `widePrefilterMask`) are detected
* behind the `good()` test, which every one of them passes and which already rejects all
* but one value in 2^skip_degree, and they are only collected into a local buffer there.
* They are inserted after the loop rather than inside it: a call inside would make
* the compiler cautiously reload the fields of the set from memory all the time.
* This loop is very sensitive to the code generation: the batch loops below must be
* fully unrolled as they are without the wide set, otherwise `uniq` gets up to 50% slower.
*/
WideCandidates wide_candidates;
const HashValue wide_prefilter = widePrefilterMask();
size_t i = 0;
while (i < size)
{
@@ -386,6 +756,8 @@ public:
{
hash_value[j] = hash(Transform(data[i + j]));
}
const size_t batch_begin = i;
i += insert_many_batch_size;
std::array<size_t, insert_many_batch_size> place_value_batch; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init) - filled by the loop below before read
@@ -394,12 +766,17 @@ public:
place_value_batch[j] = place(hash_value[j]);
}
UInt32 wide_candidate_mask = 0;
#pragma clang loop unroll(full)
for (size_t j = 0; j < insert_many_batch_size; ++j)
{
const HashValue & x = hash_value[j];
if (!good(x))
continue;
if (unlikely(((x ^ wide_prefilter_offset) & wide_prefilter) == 0))
wide_candidate_mask |= 1U << j;
if (x == 0)
{
m_size += !has_zero;
@@ -424,24 +801,54 @@ public:
buf[place_value] = x;
++m_size;
}
if (unlikely(wide_candidate_mask))
addWideCandidates<SourceType, Transform>(wide_candidates, data, batch_begin, wide_candidate_mask);
}
else
{
const HashValue hash_value = hash(Transform(data[i]));
const size_t position = i;
i++;
if (!good(hash_value))
continue;
if (unlikely(((hash_value ^ wide_prefilter_offset) & wide_prefilter) == 0))
addWideCandidates<SourceType, Transform>(wide_candidates, data, position, 1);
insertImpl(hash_value);
}
/// We need to check shrink condition after each batch or single insert
shrinkIfNeed();
}
if (unlikely(wide_candidates.size))
insertWideCandidates<SourceType, Transform>(wide_candidates, data, size);
}
size_t size() const
{
/** The wide set gives a reliable estimate once it has enough values;
* below that the 32-bit main set is more precise. If the wide sample does not cover
* all the elements (see `wide_incomplete`), it is not there at all and the legacy
* estimator is used, because a partial wide estimate would silently lose the rest.
*/
if (wide && wide->m_size >= UNIQUES_HASH_SET_WIDE_MIN_SIZE_FOR_ESTIMATE)
{
size_t res = static_cast<size_t>(wide->m_size) * (1ULL << wide->skip_degree);
/** Pseudo-random remainder - in order to be not visible,
* that the number is divided by the power of two.
*/
res += (intHashCRC32(wide->m_size) & ((1ULL << wide->skip_degree) - 1));
/** Collisions of 64-bit hashes are negligible: their systematic error would reach
* just 0.003% at 10^15 distinct elements, far below the statistical error of the estimate.
*/
return res;
}
if (0 == skip_degree)
return m_size;
@@ -452,25 +859,25 @@ public:
*/
res += (intHashCRC32(m_size) & ((1ULL << skip_degree) - 1));
size_t p32 = 1ULL << 32;
/// The correction below is undefined once the thinned count reaches the hash space:
/// report the largest count it can express.
if (unlikely(res >= p32))
res = p32 - 1;
/** Correction of a systematic error due to collisions during hashing in UInt32.
* `fixed_res(res)` formula
* - with how many different elements of fixed_res,
* when randomly scattered across 2^32 buckets,
* filled buckets with average of res is obtained.
*/
size_t fixed_res = static_cast<size_t>(round(static_cast<double>(p32) * (log(p32) - log(p32 - res))));
return fixed_res;
return correctFor32BitCollisions(res);
}
/// For tests.
size_t wideSetSize() const { return wide ? wide->m_size : 0; }
bool isWideIncomplete() const { return wide_incomplete; }
void merge(const UniquesHashSet & rhs)
{
/// The elements of `rhs` that are missing from its wide set are missing from ours as well.
if (rhs.wide_incomplete)
markWideIncomplete();
if (rhs.skip_degree > skip_degree)
{
skip_degree = rhs.skip_degree;
@@ -492,9 +899,42 @@ public:
shrinkIfNeed();
}
}
/// An incomplete state must not carry a wide set at all (see `markWideIncomplete`), so the
/// elements of `rhs.wide` are not merged - otherwise an empty wide set would be allocated
/// here (`wideInsert` refuses to fill it) and the state would serialize with both flag bits
/// set, which the format forbids.
if (rhs.wide && !wide_incomplete)
{
if (rhs.wide->skip_degree > wideSkipDegree())
{
if (!wide)
wide = allocWideSet(UNIQUES_HASH_SET_INITIAL_SIZE_DEGREE);
wide->skip_degree = rhs.wide->skip_degree;
wideRehash(*wide);
}
if (rhs.wide->has_zero && (!wide || !wide->has_zero))
wideInsert(0);
for (size_t i = 0; i < rhs.wide->bufSize(); ++i)
{
UInt64 x = rhs.wide->buf[i];
if (x && (x & ((1ULL << wideSkipDegree()) - 1)) == 0)
wideInsert(x);
}
}
}
void write(DB::WriteBuffer & wb) const
/** Two serialization formats exist.
* The legacy format (state version 0): skip_degree, then the number of values, then the 32-bit values
* of the main set.
* The new format (state version 1): a flags byte (bit 0 - a wide set follows the main set,
* bit 1 - the wide sample is incomplete, see `wide_incomplete`; the two are mutually exclusive),
* then the main set in the legacy layout, then, if present, the wide set: its skip_degree,
* the number of values, and the 64-bit values.
*/
void write(DB::WriteBuffer & wb, bool use_legacy_format) const
{
if (m_size > UNIQUES_HASH_MAX_SIZE)
throw Poco::Exception("Cannot write UniquesHashSet: too large size_degree.");
@@ -502,6 +942,12 @@ public:
/// A null `buf` here would indicate upstream state corruption (e.g. a double-destroyed state).
chassert(buf);
if (!use_legacy_format)
{
UInt8 flags = (wide ? 1 : 0) | (wide_incomplete ? 2 : 0);
DB::writeBinaryLittleEndian(flags, wb);
}
DB::writeBinaryLittleEndian(skip_degree, wb);
DB::writeVarUInt(m_size, wb);
@@ -514,11 +960,42 @@ public:
for (size_t i = 0; i < buf_size(); ++i)
if (buf[i])
DB::writeBinaryLittleEndian(buf[i], wb);
if (use_legacy_format || !wide)
return;
DB::writeBinaryLittleEndian(wide->skip_degree, wb);
DB::writeVarUInt(wide->m_size, wb);
if (wide->has_zero)
{
UInt64 x = 0;
DB::writeBinaryLittleEndian(x, wb);
}
for (size_t i = 0; i < wide->bufSize(); ++i)
if (wide->buf[i])
DB::writeBinaryLittleEndian(wide->buf[i], wb);
}
void read(DB::ReadBuffer & rb)
void read(DB::ReadBuffer & rb, bool use_legacy_format)
{
has_zero = false;
wide_incomplete = false;
freeWide();
bool has_wide = false;
if (!use_legacy_format)
{
UInt8 flags = 0;
DB::readBinaryLittleEndian(flags, rb);
if ((flags & ~3) || (flags & 3) == 3)
throw DB::Exception(DB::ErrorCodes::INCORRECT_DATA,
"Cannot read UniquesHashSet: invalid flags {}: the defined bits are mutually exclusive and no other bit is allowed",
static_cast<size_t>(flags));
has_wide = flags & 1;
wide_incomplete = flags & 2;
}
DB::readBinaryLittleEndian(skip_degree, rb);
@@ -534,6 +1011,13 @@ public:
"Cannot read UniquesHashSet: the element count is {}, which exceeds the maximum value of {}",
static_cast<size_t>(m_size), static_cast<size_t>(UNIQUES_HASH_MAX_SIZE));
/** A non-empty state written in the legacy format carries elements that were never offered
* to a wide set, so no wide estimate of this state (or of anything it is merged into)
* can be trusted. An empty legacy state contributes nothing and does not spoil a merge.
*/
if (use_legacy_format && m_size > 0)
wide_incomplete = true;
free();
UInt8 new_size_degree = m_size <= 1
@@ -568,12 +1052,60 @@ public:
reinsertImpl(hs[i]);
}
}
if (!has_wide)
return;
UInt8 wide_skip_degree = 0;
DB::readBinaryLittleEndian(wide_skip_degree, rb);
size_t wide_size = 0;
DB::readVarUInt(wide_size, rb);
if (unlikely(wide_size > UNIQUES_HASH_SET_WIDE_MAX_SIZE))
throw DB::Exception(DB::ErrorCodes::INCORRECT_DATA,
"Cannot read UniquesHashSet: the wide set size is {}, which exceeds the maximum value of {}",
wide_size, static_cast<size_t>(UNIQUES_HASH_SET_WIDE_MAX_SIZE));
if (unlikely(wide_skip_degree > max_wide_skip_degree))
throw DB::Exception(DB::ErrorCodes::INCORRECT_DATA,
"Cannot read UniquesHashSet: the wide set skip degree is {}, which exceeds the maximum value of {}",
static_cast<size_t>(wide_skip_degree), static_cast<size_t>(max_wide_skip_degree));
UInt8 wide_size_degree = wide_size <= 1
? UNIQUES_HASH_SET_INITIAL_SIZE_DEGREE
: static_cast<UInt8>(std::max(UNIQUES_HASH_SET_INITIAL_SIZE_DEGREE, static_cast<int>(log2(wide_size - 1)) + 2));
wide = allocWideSet(wide_size_degree);
wide->m_size = static_cast<UInt32>(wide_size);
wide->skip_degree = wide_skip_degree;
for (size_t i = 0; i < wide_size; ++i)
{
UInt64 x = 0;
DB::readBinaryLittleEndian(x, rb);
if (x == 0)
wide->has_zero = true;
else
wideReinsertImpl(*wide, x);
}
}
static void skip(DB::ReadBuffer & rb)
static void skip(DB::ReadBuffer & rb, bool use_legacy_format)
{
size_t size = 0;
bool has_wide = false;
if (!use_legacy_format)
{
UInt8 flags = 0;
DB::readBinaryLittleEndian(flags, rb);
if ((flags & ~3) || (flags & 3) == 3)
throw DB::Exception(DB::ErrorCodes::INCORRECT_DATA,
"Cannot read UniquesHashSet: invalid flags {}: the defined bits are mutually exclusive and no other bit is allowed",
static_cast<size_t>(flags));
has_wide = flags & 1;
}
rb.ignore();
DB::readVarUInt(size, rb);
@@ -581,6 +1113,19 @@ public:
throw Poco::Exception("Cannot read UniquesHashSet: too large size_degree.");
rb.ignore(sizeof(HashValue) * size);
if (!has_wide)
return;
rb.ignore();
DB::readVarUInt(size, rb);
if (unlikely(size > UNIQUES_HASH_SET_WIDE_MAX_SIZE))
throw DB::Exception(DB::ErrorCodes::INCORRECT_DATA,
"Cannot read UniquesHashSet: the wide set size is {}, which exceeds the maximum value of {}",
size, static_cast<size_t>(UNIQUES_HASH_SET_WIDE_MAX_SIZE));
rb.ignore(sizeof(UInt64) * size);
}
#ifdef UNIQUES_HASH_SET_COUNT_COLLISIONS
@@ -596,3 +1141,7 @@ public:
#undef UNIQUES_HASH_MAX_SIZE
#undef UNIQUES_HASH_BITS_FOR_SKIP
#undef UNIQUES_HASH_SET_INITIAL_SIZE_DEGREE
#undef UNIQUES_HASH_SET_WIDE_INITIAL_SKIP_DEGREE
#undef UNIQUES_HASH_SET_WIDE_MAX_SIZE_DEGREE
#undef UNIQUES_HASH_SET_WIDE_MAX_SIZE
#undef UNIQUES_HASH_SET_WIDE_MIN_SIZE_FOR_ESTIMATE
@@ -0,0 +1,410 @@
#include <AggregateFunctions/UniquesHashSet.h>
#include <Common/HashTable/Hash.h>
#include <IO/ReadBufferFromString.h>
#include <IO/WriteBufferFromString.h>
#include <gtest/gtest.h>
#include <random>
#include <unordered_set>
using namespace DB;
namespace
{
/// The same concrete type that the `uniq` aggregate function uses.
using TestSet = UniquesHashSet<DefaultHash<UInt64>>;
std::string serialize(const TestSet & set, bool use_legacy_format)
{
WriteBufferFromOwnString out;
set.write(out, use_legacy_format);
return out.str();
}
TestSet deserialize(const std::string & data, bool use_legacy_format)
{
ReadBufferFromString in(data);
TestSet set;
set.read(in, use_legacy_format);
return set;
}
/// Craft a serialized state with the given raw (already hashed and thinned out) values,
/// to test the states of cardinalities that would be too slow to reach by actual insertion.
/// The wide set values are appended when present.
std::string craftState(
UInt8 skip_degree,
const std::vector<UInt32> & values,
bool use_legacy_format,
UInt8 wide_skip_degree = 0,
const std::vector<UInt64> & wide_values = {})
{
WriteBufferFromOwnString out;
if (!use_legacy_format)
{
UInt8 flags = wide_values.empty() ? 0 : 1;
writeBinaryLittleEndian(flags, out);
}
writeBinaryLittleEndian(skip_degree, out);
writeVarUInt(values.size(), out);
for (UInt32 value : values)
writeBinaryLittleEndian(value, out);
if (!use_legacy_format && !wide_values.empty())
{
writeBinaryLittleEndian(wide_skip_degree, out);
writeVarUInt(wide_values.size(), out);
for (UInt64 value : wide_values)
writeBinaryLittleEndian(value, out);
}
return out.str();
}
/// Distinct pseudo-random values with the given number of zero low-order bits.
template <typename StoredValue>
std::vector<StoredValue> randomSampledValues(size_t count, UInt8 skip_degree, UInt64 seed)
{
std::mt19937_64 rng(seed);
std::unordered_set<StoredValue> values;
while (values.size() < count)
{
StoredValue value = static_cast<StoredValue>(rng() << skip_degree);
if (value != 0)
values.insert(value);
}
return std::vector<StoredValue>(values.begin(), values.end());
}
}
TEST(UniquesHashSet, ExactAtSmallCardinality)
{
TestSet set;
for (UInt64 i = 0; i < 50000; ++i)
set.insert(i);
/// Below the thinning threshold the set stores all distinct 32-bit hashes.
/// Among 50000 of them, an occasional collision is possible.
EXPECT_GE(set.size(), 49998u);
EXPECT_LE(set.size(), 50000u);
}
TEST(UniquesHashSet, EstimateAtModerateCardinality)
{
TestSet set;
for (UInt64 i = 0; i < 10000000; ++i)
set.insert(i);
/// The typical error of the estimate is about 0.6%; allow 3%.
EXPECT_NEAR(static_cast<double>(set.size()), 10000000.0, 300000.0);
/// The wide set holds about one value per 262144 distinct elements.
EXPECT_GE(set.wideSetSize(), 20u);
EXPECT_LE(set.wideSetSize(), 60u);
}
TEST(UniquesHashSet, MergeDeduplicates)
{
TestSet a;
TestSet b;
for (UInt64 i = 0; i < 40000; ++i)
a.insert(i);
for (UInt64 i = 20000; i < 60000; ++i)
b.insert(i);
a.merge(b);
EXPECT_GE(a.size(), 59997u);
EXPECT_LE(a.size(), 60000u);
}
TEST(UniquesHashSet, RoundTripNewFormat)
{
TestSet set;
for (UInt64 i = 0; i < 1000000; ++i)
set.insert(i);
std::string serialized = serialize(set, /*use_legacy_format=*/ false);
TestSet restored = deserialize(serialized, /*use_legacy_format=*/ false);
EXPECT_EQ(set.size(), restored.size());
EXPECT_EQ(set.wideSetSize(), restored.wideSetSize());
/// The state must remain usable after the round trip.
for (UInt64 i = 1000000; i < 1100000; ++i)
restored.insert(i);
EXPECT_NEAR(static_cast<double>(restored.size()), 1100000.0, 33000.0);
}
TEST(UniquesHashSet, LegacyFormatIsThePlainMainSet)
{
TestSet set;
for (UInt64 i = 0; i < 1000000; ++i)
set.insert(i);
/// The new format is a flags byte, the main set in the legacy layout, then the wide set.
std::string legacy = serialize(set, /*use_legacy_format=*/ true);
std::string versioned = serialize(set, /*use_legacy_format=*/ false);
EXPECT_EQ(versioned[0], 1);
EXPECT_EQ(versioned.substr(1, legacy.size()), legacy);
EXPECT_GT(versioned.size(), 1 + legacy.size());
/// A state without the wide set differs only by the flags byte.
TestSet small;
for (UInt64 i = 0; i < 100; ++i)
small.insert(i);
if (small.wideSetSize() == 0)
{
std::string small_legacy = serialize(small, /*use_legacy_format=*/ true);
std::string small_versioned = serialize(small, /*use_legacy_format=*/ false);
EXPECT_EQ(small_versioned[0], 0);
EXPECT_EQ(small_versioned.substr(1), small_legacy);
}
/// The legacy round trip drops the wide set but keeps the main one.
TestSet restored = deserialize(legacy, /*use_legacy_format=*/ true);
EXPECT_EQ(restored.wideSetSize(), 0u);
EXPECT_EQ(restored.size(), set.size());
}
TEST(UniquesHashSet, HugeCardinalityViaWideSet)
{
/// Simulate the state left by 10^11 distinct elements: the wide set thinned out to the degree 23
/// keeps about 10^11 / 2^23 values; the main set is saturated and unusable.
constexpr UInt64 cardinality = 100000000000ULL;
constexpr UInt8 wide_skip_degree = 23;
constexpr size_t num_wide = cardinality >> wide_skip_degree;
/// (There are only 65535 nonzero multiples of 2^16 among the 32-bit values, so 65000 is close
/// to the saturation of the main sample.)
auto main_values = randomSampledValues<UInt32>(65000, 16, 1);
auto wide_values = randomSampledValues<UInt64>(num_wide, wide_skip_degree, 2);
TestSet set = deserialize(craftState(16, main_values, false, wide_skip_degree, wide_values), false);
/// Before the wide set was introduced, the estimate overflowed to values around 1.8 * 10^19 here.
EXPECT_NEAR(static_cast<double>(set.size()), static_cast<double>(cardinality), 0.001 * cardinality);
}
TEST(UniquesHashSet, LegacyEstimateSaturatesInsteadOfOverflow)
{
/// A fully saturated legacy state: at the thinning degree of 16 every possible sampled
/// 32-bit hash (all multiples of 2^16) is present. The corrected estimate used to be
/// computed as round(2^32 * log(2^32 / 0)) = inf, and casting it to an integer
/// was undefined behavior that produced values like 18446743978444128518.
std::vector<UInt32> values(65536);
for (size_t i = 1; i < values.size(); ++i)
values[i] = static_cast<UInt32>(i << 16);
values[0] = 0;
TestSet set = deserialize(craftState(16, values, /*use_legacy_format=*/ true), /*use_legacy_format=*/ true);
/// The saturated estimate is 2^32 * ln(2^32), about 9.5 * 10^10.
size_t estimate = set.size();
EXPECT_GE(estimate, 90000000000ULL);
EXPECT_LE(estimate, 100000000000ULL);
}
TEST(UniquesHashSet, WideSetsMergeAndThinOut)
{
constexpr UInt8 wide_skip_degree = 20;
auto main_values = randomSampledValues<UInt32>(60000, 16, 3);
/// Each state alone is below the cap of the wide set; their union is above it,
/// so the merge has to thin the wide set out.
auto a_wide = randomSampledValues<UInt64>(12000, wide_skip_degree, 4);
auto b_wide = randomSampledValues<UInt64>(12000, wide_skip_degree, 5);
TestSet a = deserialize(craftState(16, main_values, false, wide_skip_degree, a_wide), false);
TestSet b = deserialize(craftState(16, main_values, false, wide_skip_degree, b_wide), false);
double a_estimate = static_cast<double>(a.size());
EXPECT_NEAR(a_estimate, 12000.0 * exp2(wide_skip_degree), 0.001 * a_estimate);
a.merge(b);
/// Random 64-bit values practically do not overlap, so the union holds about 24000 values.
double expected = 24000.0 * exp2(wide_skip_degree);
EXPECT_NEAR(static_cast<double>(a.size()), expected, 0.03 * expected);
/// The other merge direction gives the same estimate.
TestSet a2 = deserialize(craftState(16, main_values, false, wide_skip_degree, a_wide), false);
TestSet b2 = deserialize(craftState(16, main_values, false, wide_skip_degree, b_wide), false);
b2.merge(a2);
EXPECT_EQ(b2.size(), a.size());
}
TEST(UniquesHashSet, MergingLegacyStateDropsTheWideSet)
{
constexpr UInt8 wide_skip_degree = 20;
auto main_values = randomSampledValues<UInt32>(50000, 16, 6);
auto wide_values = randomSampledValues<UInt64>(10000, wide_skip_degree, 7);
TestSet with_wide = deserialize(craftState(16, main_values, false, wide_skip_degree, wide_values), false);
TestSet legacy = deserialize(craftState(16, randomSampledValues<UInt32>(30000, 16, 8), true), true);
EXPECT_TRUE(legacy.isWideIncomplete());
EXPECT_FALSE(with_wide.isWideIncomplete());
/// A legacy state carries no wide set: its elements are known by 32 bits of the hash only,
/// so they cannot contribute to the wide set of the merged state. An estimate from a wide set
/// that covers only a part of the elements would lose the rest, so the merged state drops
/// the wide set altogether and falls back to the 32-bit estimator.
with_wide.merge(legacy);
EXPECT_TRUE(with_wide.isWideIncomplete());
EXPECT_EQ(with_wide.wideSetSize(), 0u);
/// And it stays incomplete: the elements consumed later are not enough to make the sample whole.
for (UInt64 i = 0; i < 1000000; ++i)
with_wide.insert(i);
EXPECT_EQ(with_wide.wideSetSize(), 0u);
/// The mark travels with the state: a version-1 state whose wide sample is incomplete
/// says so in bit 1 of its flags byte, so a reader does not start estimating from a wide set
/// that it would build after the incomplete elements were already consumed.
std::string serialized = serialize(with_wide, /*use_legacy_format=*/ false);
EXPECT_EQ(serialized[0], 2);
EXPECT_TRUE(deserialize(serialized, /*use_legacy_format=*/ false).isWideIncomplete());
/// An empty legacy state contributes nothing, so it does not spoil the wide set.
TestSet clean = deserialize(craftState(16, main_values, false, wide_skip_degree, wide_values), false);
clean.merge(deserialize(craftState(0, {}, true), true));
EXPECT_FALSE(clean.isWideIncomplete());
EXPECT_EQ(clean.wideSetSize(), wide_values.size());
/// Nor does a later merge with a clean wide state resurrect the sample: an incomplete state
/// stays without a wide set even when the other side's thinning is coarser than the initial
/// one (this used to allocate an empty wide set that nothing could fill, making the state
/// serialize with both flag bits - which the format defines as mutually exclusive).
with_wide.merge(clean);
EXPECT_TRUE(with_wide.isWideIncomplete());
EXPECT_EQ(with_wide.wideSetSize(), 0u);
serialized = serialize(with_wide, /*use_legacy_format=*/ false);
EXPECT_EQ(serialized[0], 2);
}
TEST(UniquesHashSet, MergingLegacyStateDoesNotLoseItsElements)
{
/** The rolling-upgrade case: one shard is new and reports a version-1 state for about
* three billion distinct values, the other is old and reports a version-0 state for another
* disjoint three billion. The merged estimate has to account for both: taking it from
* the wide set - which only ever saw the new shard's half - would report three billion.
* The 32-bit sample saw both halves (that is what the random overlap of the crafted
* values models), so it is the one to use.
*/
constexpr UInt8 wide_skip_degree = 18;
constexpr size_t sampled_main_values = 32950; /// 3e9 distinct values sampled at 2 ^ 16.
constexpr size_t sampled_wide_values = 11444; /// The same 3e9 sampled at 2 ^ 18.
TestSet versioned = deserialize(
craftState(
16, randomSampledValues<UInt32>(sampled_main_values, 16, 11), false,
wide_skip_degree, randomSampledValues<UInt64>(sampled_wide_values, wide_skip_degree, 12)),
false);
TestSet legacy = deserialize(craftState(16, randomSampledValues<UInt32>(sampled_main_values, 16, 13), true), true);
/// Above the switch threshold the version-1 state alone is estimated from the wide set.
EXPECT_GE(versioned.wideSetSize(), 8192u);
EXPECT_GT(versioned.size(), 2500000000ULL);
EXPECT_LT(versioned.size(), 3500000000ULL);
versioned.merge(legacy);
EXPECT_GT(versioned.size(), 5000000000ULL);
EXPECT_LT(versioned.size(), 7000000000ULL);
}
TEST(UniquesHashSet, InsertPathFeedsTheWideSet)
{
/// Find values whose 64-bit hashes pass the wide thinning. The wide set samples
/// the hashes XOR-ed with a constant (see `UniquesHashSet::wideHash`).
std::vector<UInt64> passing;
for (UInt64 candidate = 0; passing.size() < 5; ++candidate)
if (((DefaultHash<UInt64>()(candidate) ^ 0x9E3779B97F4A0000ULL) & ((1 << 18) - 1)) == 0)
passing.push_back(candidate);
TestSet set;
for (UInt64 i = 0; i < 100; ++i)
set.insert(i);
for (UInt64 candidate : passing)
{
set.insert(candidate);
set.insert(candidate); /// The wide set deduplicates.
}
EXPECT_GE(set.wideSetSize(), 5u);
std::string serialized = serialize(set, false);
EXPECT_EQ(serialized[0], 1);
TestSet restored = deserialize(serialized, false);
EXPECT_EQ(restored.wideSetSize(), set.wideSetSize());
EXPECT_EQ(restored.size(), set.size());
/// The batched insertion collects the same candidates, also when they repeat.
std::vector<UInt64> values;
for (UInt64 i = 0; i < 100; ++i)
values.push_back(i);
for (UInt64 candidate : passing)
for (size_t i = 0; i < 20; ++i)
values.push_back(candidate);
TestSet batched;
batched.insertMany<UInt64, std::identity{}>(values.data(), values.size());
EXPECT_EQ(batched.wideSetSize(), set.wideSetSize());
EXPECT_EQ(batched.size(), set.size());
}
TEST(UniquesHashSet, FrequentZeroIsNotAWideCandidate)
{
/// The hash of the integer 0 is 0. Zeros are frequent in real data, so the wide set
/// samples the hashes XOR-ed with a constant that makes 0 never pass its thinning.
std::vector<UInt64> zeros(10000, 0);
TestSet set;
set.insertMany<UInt64, std::identity{}>(zeros.data(), zeros.size());
set.insert(0);
EXPECT_EQ(set.size(), 1u);
EXPECT_EQ(set.wideSetSize(), 0u);
}
TEST(UniquesHashSet, RejectsCorruptedStates)
{
{
/// Unknown flags.
WriteBufferFromOwnString out;
writeBinaryLittleEndian(static_cast<UInt8>(4), out);
std::string data = out.str();
ReadBufferFromString in(data);
TestSet set;
EXPECT_THROW(set.read(in, /*use_legacy_format=*/ false), Poco::Exception);
}
{
/// The two defined flag bits are mutually exclusive: a state either carries a wide set
/// or says that its wide sample is incomplete, never both.
WriteBufferFromOwnString out;
writeBinaryLittleEndian(static_cast<UInt8>(3), out);
std::string data = out.str();
ReadBufferFromString in(data);
TestSet set;
EXPECT_THROW(set.read(in, /*use_legacy_format=*/ false), Poco::Exception);
}
{
/// Too large wide set skip degree.
std::string data = craftState(0, {}, false, 64, {1ULL << 40});
ReadBufferFromString in(data);
TestSet set;
EXPECT_THROW(set.read(in, /*use_legacy_format=*/ false), Poco::Exception);
}
{
/// Too large wide set size.
std::vector<UInt64> too_many(20000, 0);
for (size_t i = 0; i < too_many.size(); ++i)
too_many[i] = (i + 1) << 20;
std::string data = craftState(0, {}, false, 20, too_many);
ReadBufferFromString in(data);
TestSet set;
EXPECT_THROW(set.read(in, /*use_legacy_format=*/ false), Poco::Exception);
}
}
@@ -10,6 +10,7 @@
#include <Common/NaNUtils.h>
#include <Core/AccurateComparison.h>
#include <Core/Settings.h>
#include <DataTypes/DataTypeDateTime64.h>
#include <DataTypes/DataTypeLowCardinality.h>
#include <DataTypes/DataTypeString.h>
#include <DataTypes/DataTypeTuple.h>
@@ -457,6 +458,18 @@ static std::optional<Field> tryConvertToColumnType(const ConstantNode * constant
return converted;
}
/// A comparison of `DateTime` or a lower-scale `DateTime64` with a `DateTime64` widens the former to the latter's scale.
static bool isLowerScaleTimePoint(const DataTypePtr & from, const DataTypePtr & to)
{
const auto * to_date_time64 = typeid_cast<const DataTypeDateTime64 *>(to.get());
if (!to_date_time64)
return false;
if (isDateTime(from))
return true;
const auto * from_date_time64 = typeid_cast<const DataTypeDateTime64 *>(from.get());
return from_date_time64 && from_date_time64->getScale() < to_date_time64->getScale();
}
enum class BoundaryCheckResult : uint8_t
{
ABOVE_MAX,
@@ -2123,9 +2136,10 @@ private:
function_node.resolveAsFunction(and_function_resolver);
}
/// If one side is a constant `String`/`FixedString` compared with a non-string type, convert
/// the literal to that type as `executeWithConstString` does and return the edge's domain.
ComparisonOrderDomain tryNormalizeConstStringEdge(const String & function_name, QueryTreeNodePtr & lhs, QueryTreeNodePtr & rhs) const
/// If one side is a constant the comparison converts once to the other side's type (a `String`/`FixedString`
/// vs a non-string, as `executeWithConstString` does, or a `DateTime`/lower-scale `DateTime64` vs a `DateTime64`,
/// widened exactly), convert the literal to that type and return the edge's domain.
ComparisonOrderDomain tryNormalizeConstEdge(const String & function_name, QueryTreeNodePtr & lhs, QueryTreeNodePtr & rhs) const
{
const auto * lhs_constant = lhs->as<ConstantNode>();
const auto * rhs_constant = rhs->as<ConstantNode>();
@@ -2136,12 +2150,16 @@ private:
const auto & other_side = lhs_constant ? rhs : lhs;
const auto constant_type = removeLowCardinality(constant_side->getResultType());
const auto other_type = removeLowCardinality(other_side->getResultType());
if (!isStringOrFixedString(constant_type) || isStringOrFixedString(other_type) || other_type->isNullable())
if (other_type->isNullable())
return {};
const auto * constant_node = lhs_constant ? lhs_constant : rhs_constant;
auto converted = tryConvertFieldToType(
constant_node->getValue(), *other_type, constant_type.get(), getFormatSettings(getContext()));
Field converted;
if (isStringOrFixedString(constant_type) && !isStringOrFixedString(other_type))
converted = tryConvertFieldToType(
constant_node->getValue(), *other_type, constant_type.get(), getFormatSettings(getContext()));
else if (isLowerScaleTimePoint(constant_type, other_type))
converted = tryConvertFieldToTypeExact(constant_node->getValue(), *other_type, constant_type.get());
if (converted.isNull())
return {};
@@ -2277,10 +2295,10 @@ private:
/// Only comparisons within one order domain join transitive chains; the seeding above
/// is domain-agnostic (it converts constants to the expression's own type).
auto comparison_domain = argument_function->getFunctionOrThrow()->getComparisonOrderDomain();
/// A const string compares after a one-time conversion to the other side's type (see
/// `executeWithConstString`); mirror it so the edge carries a typed constant instead.
/// A constant of another type may compare after a one-time conversion to the other side's
/// type; mirror it so the edge carries a typed constant instead.
if (!comparison_domain.isValid())
comparison_domain = tryNormalizeConstStringEdge(function_name, lhs, rhs);
comparison_domain = tryNormalizeConstEdge(function_name, lhs, rhs);
if (!comparison_domain.isValid())
continue;
@@ -31,6 +31,7 @@ IdentifierResolveScope::IdentifierResolveScope(QueryTreeNodePtr scope_node_, Ide
projection_mask_map = parent_scope->projection_mask_map;
global_with_aliases = parent_scope->global_with_aliases;
in_prewhere = parent_scope->in_prewhere;
resolving_join_on_expression = parent_scope->resolving_join_on_expression;
if (parent_scope->identifier_resolve_cache_force_disabled)
disableIdentifierCachePermanently();
@@ -217,6 +217,10 @@ struct IdentifierResolveScope
/// Join retutns NULLs instead of default values
bool join_use_nulls = false;
bool allow_resolve_from_using = true;
/// Points to the JOIN node whose ON expression is currently being resolved (where both sides should be accessible),
/// or to the ARRAY JOIN node whose expressions are being resolved (where its input should be accessible).
/// nullptr when not resolving any JOIN ON or ARRAY JOIN expression
const IQueryTreeNode * resolving_join_on_expression = nullptr;
/** True while the `PREWHERE` expression of this query is being resolved.
* `PREWHERE` is evaluated by the reading step and cannot contain a correlated subquery,
@@ -231,9 +235,6 @@ struct IdentifierResolveScope
/// JOIN USING count (joins whose keys can retype a matched column)
size_t using_joins_count = 0;
/// True while resolving a JOIN ON expression.
bool resolving_join_on_expression = false;
/// Subquery depth
size_t subquery_depth = 0;
+241 -15
View File
@@ -48,6 +48,8 @@ namespace Setting
extern const SettingsBool single_join_prefer_left_table;
extern const SettingsBool analyzer_compatibility_allow_compound_identifiers_in_unflatten_nested;
extern const SettingsBool analyzer_compatibility_prefer_alias_over_subcolumn;
extern const SettingsBool semi_join_include_columns_from_both_sides;
extern const SettingsBool anti_join_include_columns_from_both_sides;
}
namespace ErrorCodes
@@ -59,6 +61,7 @@ namespace ErrorCodes
extern const int LOGICAL_ERROR;
extern const int UNKNOWN_TABLE;
extern const int TABLE_UUID_MISMATCH;
extern const int SEMI_ANTI_JOIN_COLUMN_ACCESS_DENIED;
}
QueryTreeNodePtr IdentifierResolver::convertJoinedColumnTypeToNullIfNeeded(
@@ -713,6 +716,13 @@ bool IdentifierResolver::tryBindIdentifierToTableExpressions(const IdentifierLoo
if (table_expression_node.get() == table_expression_node_to_ignore.get())
continue;
/// The columns of a hidden SEMI/ANTI JOIN side are not visible here, so they must not make a
/// name of the preserved side ambiguous: otherwise `SELECT *` over `t1 LEFT SEMI JOIN t2` with
/// a column `a` on both sides would keep the only surviving column named `t1.a`, and an outer
/// query could no longer refer to it as `a` although `t2` is not part of the result at all.
if (isTableExpressionHiddenBySemiAntiJoin(table_expression_node.get(), scope))
continue;
can_bind_identifier_to_table_expression = tryBindIdentifierToTableExpression(identifier_lookup, table_expression_node, scope);
if (can_bind_identifier_to_table_expression)
break;
@@ -731,6 +741,11 @@ bool IdentifierResolver::tryBindIdentifierToArrayJoinExpressions(const Identifie
if (!array_join_node)
continue;
/// The aliases of an ARRAY JOIN on a hidden SEMI/ANTI JOIN side are not visible here either,
/// so they must not make a name of the preserved side ambiguous (see `tryBindIdentifierToTableExpressions`).
if (isTableExpressionHiddenBySemiAntiJoin(table_expression.get(), scope))
continue;
for (const auto & array_join_expression : array_join_node->getJoinExpressions())
{
auto array_join_expression_alias = array_join_expression->getAlias();
@@ -1418,9 +1433,9 @@ static bool innerJoinKeyColumnsAreEquated(
* Example, for "SELECT id FROM t1 FULL JOIN t2 USING (id)"
* this creates "SELECT firstNonDefault(t1.id, t2.id) AS id FROM ..." to coalesce the values appropriately.
*/
QueryTreeNodePtr createProjectionForUsing(const ColumnNode & using_column_node, JoinKind join_kind, IdentifierResolveScope & scope);
QueryTreeNodePtr createProjectionForUsing(const ColumnNode & using_column_node, JoinKind join_kind, IdentifierResolveScope & scope, std::optional<JoinTableSide> preserved_side);
QueryTreeNodePtr createProjectionForUsing(const ColumnNode & using_column_node, JoinKind join_kind, IdentifierResolveScope & scope)
QueryTreeNodePtr createProjectionForUsing(const ColumnNode & using_column_node, JoinKind join_kind, IdentifierResolveScope & scope, std::optional<JoinTableSide> preserved_side)
{
const auto & using_expression = using_column_node.getExpression();
if (!using_expression)
@@ -1431,14 +1446,26 @@ QueryTreeNodePtr createProjectionForUsing(const ColumnNode & using_column_node,
if (arguments.size() < 2)
throw Exception(ErrorCodes::LOGICAL_ERROR, "Expected at least 2 arguments for USING projection, but got {}", arguments.size());
for (size_t i = 0; i < arguments.size(); ++i)
/** When a disabled `semi_join_include_columns_from_both_sides` / `anti_join_include_columns_from_both_sides` hides one side of the JOIN, the
* visible `USING` key must keep the preserved side's own type: widening it to the `USING`
* supertype would expose the type of a table that is not part of the result at all.
* The preserved side never needs `join_use_nulls` either - a `SEMI`/`ANTI` JOIN only preserves
* rows of that side, so nothing of it can become NULL - so the conversion is skipped entirely.
*/
if (!preserved_side)
{
auto resolved_side = (i + 1 == arguments.size()) ? JoinTableSide::Right : JoinTableSide::Left;
auto converted_argument = IdentifierResolver::convertJoinedColumnTypeToNullIfNeeded(arguments[i], using_column_node.getResultType(), join_kind, resolved_side, scope);
if (converted_argument)
arguments[i] = converted_argument;
for (size_t i = 0; i < arguments.size(); ++i)
{
auto resolved_side = (i + 1 == arguments.size()) ? JoinTableSide::Right : JoinTableSide::Left;
auto converted_argument = IdentifierResolver::convertJoinedColumnTypeToNullIfNeeded(arguments[i], using_column_node.getResultType(), join_kind, resolved_side, scope);
if (converted_argument)
arguments[i] = converted_argument;
}
}
if (preserved_side == JoinTableSide::Right)
return arguments.back();
if (join_kind == JoinKind::Right)
return arguments.back();
@@ -1460,6 +1487,165 @@ QueryTreeNodePtr createProjectionForUsing(const ColumnNode & using_column_node,
return function_node;
}
/// Returns true if `target` is `root` itself or is nested below `root` in a join tree.
static bool joinSubtreeContains(const IQueryTreeNode * root, const IQueryTreeNode * target)
{
if (!root)
return false;
if (root == target)
return true;
if (const auto * join = root->as<JoinNode>())
return joinSubtreeContains(join->getLeftTableExpressionNode().get(), target)
|| joinSubtreeContains(join->getRightTableExpressionNode().get(), target);
if (const auto * cross_join = root->as<CrossJoinNode>())
{
for (const auto & table_expression : cross_join->getTableExpressions())
if (joinSubtreeContains(table_expression.get(), target))
return true;
return false;
}
if (const auto * array_join = root->as<ArrayJoinNode>())
return joinSubtreeContains(array_join->getTableExpressionNode().get(), target);
return false;
}
SemiAntiJoinSideChecker::SemiAntiJoinSideChecker(
const JoinNode & join_node,
JoinStrictness strictness,
JoinKind kind,
const ContextPtr & context,
const IQueryTreeNode * resolving_join_on_expression)
{
is_semi = strictness == JoinStrictness::Semi;
is_anti = strictness == JoinStrictness::Anti;
if (!is_semi && !is_anti)
return;
const auto & settings = context->getSettingsRef();
const bool skip_non_preserved_side = (is_semi && !settings[Setting::semi_join_include_columns_from_both_sides])
|| (is_anti && !settings[Setting::anti_join_include_columns_from_both_sides]);
skip_left = skip_non_preserved_side && isRight(kind);
skip_right = skip_non_preserved_side && isLeft(kind);
/// An inner JOIN's ON expression needs both sides of that inner join. An ancestor may only
/// relax the restriction for the ancestor side which contains that inner JOIN; relaxing the
/// other side would expose a sibling table outside the current ON expression.
if (resolving_join_on_expression)
{
if (resolving_join_on_expression == &join_node)
{
skip_left = false;
skip_right = false;
return;
}
if (skip_left && joinSubtreeContains(join_node.getLeftTableExpressionNode().get(), resolving_join_on_expression))
skip_left = false;
if (skip_right && joinSubtreeContains(join_node.getRightTableExpressionNode().get(), resolving_join_on_expression))
skip_right = false;
}
}
bool SemiAntiJoinSideChecker::shouldSkipSide(JoinTableSide side) const
{
return (skip_left && side == JoinTableSide::Left) || (skip_right && side == JoinTableSide::Right);
}
std::optional<JoinTableSide> SemiAntiJoinSideChecker::preservedSideOrNone() const
{
if (skip_left)
return JoinTableSide::Right;
if (skip_right)
return JoinTableSide::Left;
return {};
}
bool IdentifierResolver::isTableExpressionHiddenBySemiAntiJoin(
const IQueryTreeNode * table_expression_node,
const IdentifierResolveScope & scope)
{
const auto & settings = scope.context->getSettingsRef();
if (settings[Setting::semi_join_include_columns_from_both_sides] && settings[Setting::anti_join_include_columns_from_both_sides])
return false;
const auto * nearest_query_scope = scope.getNearestQueryScope();
if (!nearest_query_scope)
return false;
const auto * query_node = nearest_query_scope->scope_node->as<QueryNode>();
if (!query_node || !query_node->getJoinTreeNode())
return false;
/// Follow the path from the root of the join tree down to the table expression. Every SEMI/ANTI
/// JOIN on that path hides the side which contains the table expression if the compatibility
/// setting says so, unless its own ON expression is being resolved (`SemiAntiJoinSideChecker`).
const IQueryTreeNode * current = query_node->getJoinTreeNode().get();
while (current && current != table_expression_node)
{
if (const auto * join_node = current->as<JoinNode>())
{
const auto * left = join_node->getLeftTableExpressionNode().get();
const auto * right = join_node->getRightTableExpressionNode().get();
const bool is_from_left = joinSubtreeContains(left, table_expression_node);
const bool is_from_right = !is_from_left && joinSubtreeContains(right, table_expression_node);
if (!is_from_left && !is_from_right)
return false;
SemiAntiJoinSideChecker checker(*join_node, join_node->getStrictness(), join_node->getKind(), scope.context, scope.resolving_join_on_expression);
if (checker.shouldSkipSide(is_from_left ? JoinTableSide::Left : JoinTableSide::Right))
return true;
current = is_from_left ? left : right;
}
else if (const auto * cross_join_node = current->as<CrossJoinNode>())
{
const IQueryTreeNode * next = nullptr;
for (const auto & table_expression : cross_join_node->getTableExpressions())
{
if (joinSubtreeContains(table_expression.get(), table_expression_node))
{
next = table_expression.get();
break;
}
}
current = next;
}
else if (const auto * array_join_node = current->as<ArrayJoinNode>())
{
current = array_join_node->getTableExpressionNode().get();
}
else
{
return false;
}
}
return false;
}
void SemiAntiJoinSideChecker::throwIfTableAccessDenied(
JoinTableSide side,
const IQueryTreeNode & node_for_error_message,
const IQueryTreeNode & scope_node) const
{
if (!shouldSkipSide(side))
return;
const char * join_type_str = is_semi ? "SEMI" : "ANTI";
const char * side_str = side == JoinTableSide::Left ? "left" : "right";
throw Exception(ErrorCodes::SEMI_ANTI_JOIN_COLUMN_ACCESS_DENIED,
"Cannot access columns from the {} side of {} JOIN in this context. Expression {} is not available. In scope {}",
side_str,
join_type_str,
node_for_error_message.formatASTForErrorMessage(),
scope_node.formatASTForErrorMessage());
}
/// With `database_qualified = false`, the qualifier is the first identifier part matched against
/// aliases, table names and materialized CTE names. With `database_qualified = true`, the first two identifier parts are
/// matched against the database and table names of the leaf table expressions (`db.table.column`);
@@ -1536,6 +1722,7 @@ IdentifierResolveResult IdentifierResolver::tryResolveIdentifierFromJoin(const I
{
const auto & from_join_node = table_expression_node->as<const JoinNode &>();
JoinKind join_kind = from_join_node.getKind();
JoinStrictness join_strictness = from_join_node.getStrictness();
bool join_node_in_resolve_process = scope.table_expressions_in_resolve_process.contains(table_expression_node.get());
std::unordered_map<std::string, ColumnNodePtr> join_using_column_name_to_column_node;
@@ -1550,9 +1737,34 @@ IdentifierResolveResult IdentifierResolver::tryResolveIdentifierFromJoin(const I
}
}
SemiAntiJoinSideChecker side_checker(from_join_node, join_strictness, join_kind, scope.context, scope.resolving_join_on_expression);
/// Set when one side of this JOIN is hidden because `semi_join_include_columns_from_both_sides` / `anti_join_include_columns_from_both_sides` is disabled.
const auto using_preserved_side = side_checker.preservedSideOrNone();
std::optional<JoinTableSide> denied_qualified_access;
bool ambiguous_in_join_tree = false;
auto try_resolve_identifier_from_join_tree_node = [&](const TableExpressionNodePtr & join_tree_node, bool may_be_override_by_using_column)
auto try_resolve_identifier_from_join_tree_node = [&](const TableExpressionNodePtr & join_tree_node, bool may_be_override_by_using_column, JoinTableSide side)
{
if (side_checker.shouldSkipSide(side))
{
const bool is_table_lookup = identifier_lookup.isTableExpressionLookup();
const bool is_qualified_expr = identifier_lookup.isExpressionLookup()
&& identifier_lookup.identifier.getPartsSize() > 1;
/// A fully qualified reference names the skipped table just as well as an alias or a bare
/// table name does, so it must be attributed to the skipped side too. Otherwise the
/// reference degrades to `UNKNOWN_IDENTIFIER`, which a statically-dead `if(false, ...)`
/// branch silently folds away instead of reporting the access violation.
/// The `db.table` prefix takes two parts, so it is the whole identifier of a table
/// expression lookup (`db.table.*` is looked up by its qualifier `db.table`), while an
/// expression lookup needs a third part for the column name (`db.table.column`).
const size_t min_database_qualified_parts = is_table_lookup ? 2 : 3;
const bool binds_qualifier = qualifierBindsToJoinSubtree(join_tree_node, identifier_lookup.identifier, scope, /*database_qualified=*/false)
|| (identifier_lookup.identifier.getPartsSize() >= min_database_qualified_parts
&& qualifierBindsToJoinSubtree(join_tree_node, identifier_lookup.identifier, scope, /*database_qualified=*/true));
if ((is_table_lookup || is_qualified_expr) && binds_qualifier)
denied_qualified_access = side;
return QueryTreeNodePtr{};
}
/// scope.join_using_columns holds raw pointers to this stack-local map. The pop must run
/// even if tryResolveIdentifierFromJoinTreeNode throws: an UNKNOWN_IDENTIFIER from a
/// statically-dead if/multiIf branch is caught and swallowed during resolution, and a
@@ -1597,9 +1809,9 @@ IdentifierResolveResult IdentifierResolver::tryResolveIdentifierFromJoin(const I
QueryTreeNodePtr left_resolved_identifier = nullptr;
QueryTreeNodePtr right_resolved_identifier = nullptr;
if (binds_left || !binds_right)
left_resolved_identifier = try_resolve_identifier_from_join_tree_node(from_join_node.getLeftTableExpressionNodeTyped(), join_kind == JoinKind::Right);
left_resolved_identifier = try_resolve_identifier_from_join_tree_node(from_join_node.getLeftTableExpressionNodeTyped(), join_kind == JoinKind::Right, JoinTableSide::Left);
if (!binds_left || binds_right)
right_resolved_identifier = try_resolve_identifier_from_join_tree_node(from_join_node.getRightTableExpressionNodeTyped(), join_kind != JoinKind::Right);
right_resolved_identifier = try_resolve_identifier_from_join_tree_node(from_join_node.getRightTableExpressionNodeTyped(), join_kind != JoinKind::Right, JoinTableSide::Right);
if (ambiguous_in_join_tree)
return IdentifierResolveResult::ambiguousInJoinTree();
@@ -1614,14 +1826,23 @@ IdentifierResolveResult IdentifierResolver::tryResolveIdentifierFromJoin(const I
if (binds_left != binds_right && !left_resolved_identifier && !right_resolved_identifier)
{
if (binds_left && qualifierBindsToJoinSubtree(from_join_node.getRightTableExpressionNodeTyped(), identifier_lookup.identifier, scope, /*database_qualified=*/ true))
right_resolved_identifier = try_resolve_identifier_from_join_tree_node(from_join_node.getRightTableExpressionNodeTyped(), join_kind != JoinKind::Right);
right_resolved_identifier = try_resolve_identifier_from_join_tree_node(from_join_node.getRightTableExpressionNodeTyped(), join_kind != JoinKind::Right, JoinTableSide::Right);
else if (binds_right && qualifierBindsToJoinSubtree(from_join_node.getLeftTableExpressionNodeTyped(), identifier_lookup.identifier, scope, /*database_qualified=*/ true))
left_resolved_identifier = try_resolve_identifier_from_join_tree_node(from_join_node.getLeftTableExpressionNodeTyped(), join_kind == JoinKind::Right);
left_resolved_identifier = try_resolve_identifier_from_join_tree_node(from_join_node.getLeftTableExpressionNodeTyped(), join_kind == JoinKind::Right, JoinTableSide::Left);
if (ambiguous_in_join_tree)
return IdentifierResolveResult::ambiguousInJoinTree();
}
/// Report the access violation only after every remaining preserved-side interpretation has
/// failed: a skipped-side alias or table name equal to the database part of a fully qualified
/// `db.table.column` reference must not deny the reference while the database-qualified
/// fallback above can still resolve it from the preserved side.
if (!left_resolved_identifier && !right_resolved_identifier && denied_qualified_access)
{
side_checker.throwIfTableAccessDenied(*denied_qualified_access, *table_expression_node, *scope.scope_node);
}
if (!identifier_lookup.isExpressionLookup())
{
if (left_resolved_identifier && right_resolved_identifier)
@@ -1785,7 +2006,7 @@ IdentifierResolveResult IdentifierResolver::tryResolveIdentifierFromJoin(const I
if (using_column_node_it != join_using_column_name_to_column_node.end())
{
const auto & using_column_node = using_column_node_it->second->as<const ColumnNode &>();
resolved_identifier = createProjectionForUsing(using_column_node, join_kind, scope);
resolved_identifier = createProjectionForUsing(using_column_node, join_kind, scope, using_preserved_side);
}
else if (resolvedIdenfiersFromJoinAreEquals(left_resolved_identifier, right_resolved_identifier, scope))
{
@@ -1855,7 +2076,10 @@ IdentifierResolveResult IdentifierResolver::tryResolveIdentifierFromJoin(const I
}
else
{
convert_resolved_result_type_if_needed(left_resolved_identifier, join_using_column_name_to_column_node, resolved_identifier, scope, node_to_projection_name);
/// The right side is hidden by the compatibility settings, so the `USING` key must keep
/// the preserved left side's type instead of the `USING` supertype.
if (using_preserved_side != JoinTableSide::Left)
convert_resolved_result_type_if_needed(left_resolved_identifier, join_using_column_name_to_column_node, resolved_identifier, scope, node_to_projection_name);
}
}
else if (right_resolved_identifier)
@@ -1869,7 +2093,9 @@ IdentifierResolveResult IdentifierResolver::tryResolveIdentifierFromJoin(const I
}
else
{
convert_resolved_result_type_if_needed(right_resolved_identifier, join_using_column_name_to_column_node, resolved_identifier, scope, node_to_projection_name);
/// Same as above, with the left side hidden and the right side preserved.
if (using_preserved_side != JoinTableSide::Right)
convert_resolved_result_type_if_needed(right_resolved_identifier, join_using_column_name_to_column_node, resolved_identifier, scope, node_to_projection_name);
}
}
+42
View File
@@ -24,6 +24,38 @@ using ProjectionNames = std::vector<ProjectionName>;
struct Settings;
/// Checks whether access to a specific side of a SEMI/ANTI JOIN is allowed.
/// Used both during identifier resolution and qualified matcher resolution.
struct SemiAntiJoinSideChecker
{
bool is_semi = false;
bool is_anti = false;
bool skip_left = false;
bool skip_right = false;
SemiAntiJoinSideChecker() = default;
SemiAntiJoinSideChecker(
const JoinNode & join_node,
JoinStrictness strictness,
JoinKind kind,
const ContextPtr & context,
const IQueryTreeNode * resolving_join_on_expression);
bool shouldSkipSide(JoinTableSide side) const;
/// The side that stays visible when the compatibility settings hide the other one.
/// Returns nullopt when both sides remain visible.
std::optional<JoinTableSide> preservedSideOrNone() const;
/// Throw if access to the given side of a SEMI/ANTI JOIN is denied.
/// The caller is responsible for determining the correct side (e.g. via isFromJoinTree).
void throwIfTableAccessDenied(
JoinTableSide side,
const IQueryTreeNode & node_for_error_message,
const IQueryTreeNode & scope_node) const;
};
class IdentifierResolver
{
public:
@@ -58,11 +90,21 @@ public:
const TableExpressionNodePtr & table_expression_node,
const IdentifierResolveScope & scope);
/// Check whether the identifier binds to any table expression of the scope other than `table_expression_node`.
/// A table expression which a SEMI/ANTI JOIN hides from the current context (see `isTableExpressionHiddenBySemiAntiJoin`)
/// is not a competing binder: its columns are not visible, so they cannot make another name ambiguous.
static bool tryBindIdentifierToTableExpressions(
const IdentifierLookup & identifier_lookup,
const TableExpressionNodePtr & table_expression_node,
const IdentifierResolveScope & scope);
/// Returns true if `table_expression_node` is on the non-preserved side of a SEMI/ANTI JOIN of the nearest
/// query scope's join tree and a disabled `semi_join_include_columns_from_both_sides` / `anti_join_include_columns_from_both_sides` hides that side from
/// everything outside that join's own ON expression (see `SemiAntiJoinSideChecker`).
static bool isTableExpressionHiddenBySemiAntiJoin(
const IQueryTreeNode * table_expression_node,
const IdentifierResolveScope & scope);
static bool tryBindIdentifierToArrayJoinExpressions(
const IdentifierLookup & identifier_lookup,
const IdentifierResolveScope & scope);
+202 -43
View File
@@ -29,6 +29,7 @@
#include <Analyzer/WindowNode.h>
#include <Analyzer/Resolve/CorrelatedColumnsCollector.h>
#include <Analyzer/Resolve/IdentifierResolver.h>
#include <Analyzer/Resolve/IdentifierResolveScope.h>
#include <Analyzer/Resolve/QueryAnalyzer.h>
#include <Analyzer/Resolve/QueryExpressionsAliasVisitor.h>
@@ -89,6 +90,7 @@ namespace Setting
{
extern const SettingsBool aggregate_functions_null_for_empty;
extern const SettingsBool analyzer_compatibility_allow_non_aggregate_in_having;
extern const SettingsBool analyzer_compatibility_prefer_alias_over_subcolumn;
extern const SettingsBool analyzer_compatibility_allow_cte_redefinition;
extern const SettingsBool enable_streaming_queries;
extern const SettingsBool analyzer_compatibility_join_using_top_level_identifier;
@@ -543,6 +545,31 @@ void QueryAnalyzer::resolveConstantExpression(QueryTreeNodePtr & node, const Tab
validateCorrelatedSubqueries(node, scope.context);
}
/** Append the immediate child table expressions of a join-tree internal node to `children`.
* Internal nodes are `JOIN` (left, right), `CROSS JOIN` (all operands) and `ARRAY JOIN` (its
* wrapped table expression). Leaf table expressions (table, table function, subquery, ...) add
* nothing. This is the single place that enumerates join-tree node types, so that every traversal
* (subtree membership, SEMI/ANTI side checks, ...) stays consistent when a new node type appears.
* Keep in sync with the join-tree node types handled by `initializeQueryJoinTreeNode`.
*/
static void collectJoinTreeChildTableExpressions(const IQueryTreeNode * node, std::vector<const IQueryTreeNode *> & children)
{
if (const auto * join_node = node->as<JoinNode>())
{
children.push_back(join_node->getLeftTableExpressionNode().get());
children.push_back(join_node->getRightTableExpressionNode().get());
}
else if (const auto * cross_join_node = node->as<CrossJoinNode>())
{
for (const auto & table_expression : cross_join_node->getTableExpressions())
children.push_back(table_expression.get());
}
else if (const auto * array_join_node = node->as<ArrayJoinNode>())
{
children.push_back(array_join_node->getTableExpressionNode().get());
}
}
static bool isFromJoinTree(const IQueryTreeNode * node_source, const IQueryTreeNode * tree_node)
{
if (node_source == tree_node)
@@ -559,16 +586,10 @@ static bool isFromJoinTree(const IQueryTreeNode * node_source, const IQueryTreeN
if (node_source == current)
return true;
if (const auto * child_join_node = current->as<JoinNode>())
{
stack.push(child_join_node->getLeftTableExpressionNode().get());
stack.push(child_join_node->getRightTableExpressionNode().get());
}
if (const auto * child_join_node = current->as<CrossJoinNode>())
{
stack.push_range(child_join_node->getTableExpressions() | std::views::transform(&QueryTreeNodePtr::get));
}
std::vector<const IQueryTreeNode *> children;
collectJoinTreeChildTableExpressions(current, children);
for (const auto * child : children)
stack.push(child);
}
return false;
}
@@ -2240,6 +2261,20 @@ void QueryAnalyzer::updateMatchedColumnsFromJoinUsing(
bool join_node_in_resolve_process = nearest_query_scope->table_expressions_in_resolve_process.contains(join_node);
if (!join_node_in_resolve_process && join_node && join_node->isUsingJoinExpression())
{
/** When a disabled `semi_join_include_columns_from_both_sides` / `anti_join_include_columns_from_both_sides` hides one side of the JOIN, only the
* preserved side is part of the result, so its `USING` key must keep its own type rather than be
* widened to the `USING` supertype - which is derived from the hidden side as well.
*/
SemiAntiJoinSideChecker semi_anti_using_checker(
*join_node,
join_node->getStrictness(),
join_node->getKind(),
scope.context,
scope.resolving_join_on_expression);
if (semi_anti_using_checker.preservedSideOrNone())
return;
const auto & join_using_list = join_node->getJoinExpression()->as<ListNode &>();
const auto & join_using_nodes = join_using_list.getNodes();
@@ -2287,6 +2322,76 @@ void QueryAnalyzer::updateMatchedColumnsFromJoinUsing(
}
}
/** Check if a table expression is from the non-preserved side of a SEMI or ANTI JOIN.
* Throws an exception if access is not allowed.
*
* We must check ALL SEMI/ANTI JOIN nodes on the path from the root to the table expression,
* not just the first one found. Consider: (t1 LEFT SEMI JOIN t2) LEFT SEMI JOIN t3
* For t2.*, the outer join sees t2 on its left side (preserved) and would allow access,
* but the inner join sees t2 on its right side (non-preserved) and must deny access.
* Stopping at the first match would incorrectly allow t2.*.
*/
static void checkSemiAntiJoinTableAccess(
const QueryTreeNodePtr & table_expression_node,
const IdentifierResolveScope & scope,
const QueryTreeNodePtr & node_for_error_message)
{
const auto * nearest_query_scope = scope.getNearestQueryScope();
if (!nearest_query_scope)
return;
auto * query_node = nearest_query_scope->scope_node->as<QueryNode>();
if (!query_node || !query_node->getJoinTreeNode())
return;
/// Follow the path from the root join down to the table expression, checking every
/// SEMI/ANTI JOIN node along the way. Access is denied if any containing join denies it.
std::stack<const IQueryTreeNode *> stack;
stack.push(query_node->getJoinTreeNode().get());
while (!stack.empty())
{
const auto * current = stack.top();
stack.pop();
/// A JOIN node imposes a per-side restriction: determine which side contains the table
/// expression, check access if it is a SEMI/ANTI JOIN, then descend only into that side.
if (const auto * join_node = current->as<JoinNode>())
{
bool is_from_left = isFromJoinTree(table_expression_node.get(), join_node->getLeftTableExpressionNode().get());
bool is_from_right = !is_from_left && isFromJoinTree(table_expression_node.get(), join_node->getRightTableExpressionNode().get());
if (!is_from_left && !is_from_right)
continue;
if (join_node->getStrictness() == JoinStrictness::Semi || join_node->getStrictness() == JoinStrictness::Anti)
{
SemiAntiJoinSideChecker checker(
*join_node,
join_node->getStrictness(),
join_node->getKind(),
scope.context,
scope.resolving_join_on_expression);
JoinTableSide side = is_from_left ? JoinTableSide::Left : JoinTableSide::Right;
checker.throwIfTableAccessDenied(side, *node_for_error_message, *scope.scope_node);
}
stack.push(is_from_left ? join_node->getLeftTableExpressionNode().get() : join_node->getRightTableExpressionNode().get());
continue;
}
/// Other join-tree nodes (CROSS JOIN, ARRAY JOIN, ...) impose no per-side restriction on
/// their own; descend into whichever children contain the table expression so that any
/// SEMI/ANTI JOIN nested below (e.g. wrapped by ARRAY JOIN) is still reached and checked.
std::vector<const IQueryTreeNode *> children;
collectJoinTreeChildTableExpressions(current, children);
for (const auto * child : children)
{
if (isFromJoinTree(table_expression_node.get(), child))
stack.push(child);
}
}
}
/** Resolve qualified tree matcher.
*
* First try to match qualified identifier to expression. If qualified identifier matched a compound expression node then
@@ -2333,30 +2438,48 @@ QueryAnalyzer::QueryTreeNodesWithNames QueryAnalyzer::resolveQualifiedMatcher(Qu
}
else
{
const auto & element_names = tuple_data_type->getElementNames();
QueryTreeNodesWithNames matched_expression_nodes_with_column_names;
auto qualified_matcher_element_identifier = matcher_node_typed.getQualifiedIdentifier();
for (const auto & element_name : element_names)
/// `analyzer_compatibility_prefer_alias_over_subcolumn` also applies to a qualified
/// matcher. If its qualifier names a table, prefer that table over a same-named Tuple
/// column: skip the Tuple expansion and fall through to table-expression resolution,
/// which also runs the SEMI/ANTI access check.
bool prefer_table_expression = false;
if (scope.context->getSettingsRef()[Setting::analyzer_compatibility_prefer_alias_over_subcolumn])
{
if (!matcher_node_typed.isMatchingColumn(element_name))
continue;
IdentifierResolveContext identifier_resolve_settings;
identifier_resolve_settings.allow_to_check_cte = false;
identifier_resolve_settings.allow_to_check_database_catalog = false;
auto get_subcolumn_function = std::make_shared<FunctionNode>("getSubcolumn");
get_subcolumn_function->getArguments().getNodes().push_back(expression_query_tree_node);
get_subcolumn_function->getArguments().getNodes().push_back(std::make_shared<ConstantNode>(element_name));
QueryTreeNodePtr function_query_node = get_subcolumn_function;
resolveFunction(function_query_node, scope);
qualified_matcher_element_identifier.push_back(element_name);
node_to_projection_name.emplace(function_query_node, qualified_matcher_element_identifier.getFullName());
qualified_matcher_element_identifier.pop_back();
matched_expression_nodes_with_column_names.emplace_back(std::move(function_query_node), element_name);
auto table_identifier_lookup = IdentifierLookup{matcher_node_typed.getQualifiedIdentifier(), IdentifierLookupContext::TABLE_EXPRESSION};
prefer_table_expression = tryResolveIdentifier(table_identifier_lookup, scope, identifier_resolve_settings).resolved_identifier != nullptr;
}
return matched_expression_nodes_with_column_names;
if (!prefer_table_expression)
{
const auto & element_names = tuple_data_type->getElementNames();
QueryTreeNodesWithNames matched_expression_nodes_with_column_names;
auto qualified_matcher_element_identifier = matcher_node_typed.getQualifiedIdentifier();
for (const auto & element_name : element_names)
{
if (!matcher_node_typed.isMatchingColumn(element_name))
continue;
auto get_subcolumn_function = std::make_shared<FunctionNode>("getSubcolumn");
get_subcolumn_function->getArguments().getNodes().push_back(expression_query_tree_node);
get_subcolumn_function->getArguments().getNodes().push_back(std::make_shared<ConstantNode>(element_name));
QueryTreeNodePtr function_query_node = get_subcolumn_function;
resolveFunction(function_query_node, scope);
qualified_matcher_element_identifier.push_back(element_name);
node_to_projection_name.emplace(function_query_node, qualified_matcher_element_identifier.getFullName());
qualified_matcher_element_identifier.pop_back();
matched_expression_nodes_with_column_names.emplace_back(std::move(function_query_node), element_name);
}
return matched_expression_nodes_with_column_names;
}
}
}
@@ -2396,6 +2519,11 @@ QueryAnalyzer::QueryTreeNodesWithNames QueryAnalyzer::resolveQualifiedMatcher(Qu
/// join-tree table expression (the `FROM x` clone). Remap the matcher to that initialized join-tree node so
/// `x.*` expands the same columns, matching the column/unqualified-matcher behavior and avoiding the
/// uninitialized-data path.
///
/// The remap must happen BEFORE checkSemiAntiJoinTableAccess: that check uses raw pointer identity
/// via isFromJoinTree, so a synthetic self-reference node - which never appears in the join tree -
/// would silently bypass the SEMI/ANTI side restriction. Running the remap first lets the check
/// inspect the actual join-tree node.
auto * nearest_query_scope = scope.getNearestQueryScope();
if (const auto * table_expression_typed = table_expression_node->asTableExpression();
nearest_query_scope && table_expression_typed
@@ -2426,6 +2554,9 @@ QueryAnalyzer::QueryTreeNodesWithNames QueryAnalyzer::resolveQualifiedMatcher(Qu
table_expression_node = std::move(remapped_table_expression_node);
}
/// Check if the table is from the non-preserved side of a SEMI or ANTI JOIN
checkSemiAntiJoinTableAccess(table_expression_node, scope, matcher_node);
NamesAndTypes matched_columns;
auto * table_expression_query_node = table_expression_node->as<QueryNode>();
@@ -2465,7 +2596,7 @@ QueryAnalyzer::QueryTreeNodesWithNames QueryAnalyzer::resolveQualifiedMatcher(Qu
return result_matched_column_nodes_with_names;
}
QueryTreeNodePtr createProjectionForUsing(const ColumnNode & using_column_node, JoinKind join_kind, IdentifierResolveScope & scope);
QueryTreeNodePtr createProjectionForUsing(const ColumnNode & using_column_node, JoinKind join_kind, IdentifierResolveScope & scope, std::optional<JoinTableSide> preserved_side);
/// Resolve non qualified matcher, using scope join tree node.
QueryAnalyzer::QueryTreeNodesWithNames QueryAnalyzer::resolveUnqualifiedMatcher(QueryTreeNodePtr & matcher_node, IdentifierResolveScope & scope)
@@ -2615,6 +2746,18 @@ QueryAnalyzer::QueryTreeNodesWithNames QueryAnalyzer::resolveUnqualifiedMatcher(
QueryTreeNodesWithNames matched_expression_nodes_with_column_names;
/** For SEMI/ANTI JOIN, SELECT * should only return columns from one side per SQL standard:
* - LEFT SEMI/ANTI JOIN: only left table columns
* - RIGHT SEMI/ANTI JOIN: only right table columns
* Controlled by `semi_join_include_columns_from_both_sides` and `anti_join_include_columns_from_both_sides` (see `SemiAntiJoinSideChecker`).
*/
SemiAntiJoinSideChecker semi_anti_star_checker(
*join_node,
join_node->getStrictness(),
join_node->getKind(),
scope.context,
scope.resolving_join_on_expression);
/** If there is JOIN with USING we need to match only single USING column and do not use left table expression
* and right table expression column with same name.
*
@@ -2662,7 +2805,8 @@ QueryAnalyzer::QueryTreeNodesWithNames QueryAnalyzer::resolveUnqualifiedMatcher(
is_column_from_parent_scope(join_using_column_nodes.at(1)))
continue;
QueryTreeNodePtr matched_column_node = createProjectionForUsing(join_using_column_node, join_node->getKind(), scope);
QueryTreeNodePtr matched_column_node = createProjectionForUsing(
join_using_column_node, join_node->getKind(), scope, semi_anti_star_checker.preservedSideOrNone());
matched_column_node->setAlias(join_using_column_name);
table_expression_column_names_to_skip.insert(join_using_column_name);
@@ -2670,20 +2814,26 @@ QueryAnalyzer::QueryTreeNodesWithNames QueryAnalyzer::resolveUnqualifiedMatcher(
}
}
for (auto && left_table_column_with_name : left_table_expression_columns)
if (!semi_anti_star_checker.shouldSkipSide(JoinTableSide::Left))
{
if (table_expression_column_names_to_skip.contains(left_table_column_with_name.second))
continue;
for (auto && left_table_column_with_name : left_table_expression_columns)
{
if (table_expression_column_names_to_skip.contains(left_table_column_with_name.second))
continue;
matched_expression_nodes_with_column_names.push_back(std::move(left_table_column_with_name));
matched_expression_nodes_with_column_names.push_back(std::move(left_table_column_with_name));
}
}
for (auto && right_table_column_with_name : right_table_expression_columns)
if (!semi_anti_star_checker.shouldSkipSide(JoinTableSide::Right))
{
if (table_expression_column_names_to_skip.contains(right_table_column_with_name.second))
continue;
for (auto && right_table_column_with_name : right_table_expression_columns)
{
if (table_expression_column_names_to_skip.contains(right_table_column_with_name.second))
continue;
matched_expression_nodes_with_column_names.push_back(std::move(right_table_column_with_name));
matched_expression_nodes_with_column_names.push_back(std::move(right_table_column_with_name));
}
}
table_expressions_column_nodes_with_names_stack.push_back(std::move(matched_expression_nodes_with_column_names));
@@ -5592,6 +5742,12 @@ void QueryAnalyzer::resolveArrayJoin(QueryTreeNodePtr & array_join_node, Identif
std::vector<QueryTreeNodePtr> array_join_column_expressions;
array_join_column_expressions.reserve(array_join_nodes_size);
/// ARRAY JOIN expressions read the columns of their own input, even if that input is on the hidden side of
/// an enclosing SEMI/ANTI JOIN, so treat them as an ON expression of this node (see `SemiAntiJoinSideChecker`).
const auto * previous_resolving_join_on_expression = scope.resolving_join_on_expression;
scope.resolving_join_on_expression = array_join_node.get();
SCOPE_EXIT(scope.resolving_join_on_expression = previous_resolving_join_on_expression);
for (auto & array_join_expression : array_join_nodes)
{
auto array_join_expression_alias = array_join_expression->getAlias();
@@ -6032,10 +6188,13 @@ void QueryAnalyzer::resolveJoin(QueryTreeNodePtr & join_node, IdentifierResolveS
{
expressions_visitor.visit(join_node_typed.getJoinExpression());
auto join_expression = join_node_typed.getJoinExpression();
const bool previous_resolving_join_on_expression = scope.resolving_join_on_expression;
scope.resolving_join_on_expression = true;
/// Set pointer to current JOIN node to allow access to both sides in its ON expression
const auto * previous_resolving_join_on_expression = scope.resolving_join_on_expression;
scope.resolving_join_on_expression = join_node.get();
SCOPE_EXIT(scope.resolving_join_on_expression = previous_resolving_join_on_expression);
resolveExpressionNode(join_expression, scope, false /*allow_lambda_expression*/, false /*allow_table_expression*/);
scope.resolving_join_on_expression = previous_resolving_join_on_expression;
join_node_typed.getJoinExpression() = std::move(join_expression);
}
else if (join_node_typed.isUsingJoinExpression())
+14 -3
View File
@@ -80,6 +80,7 @@ namespace ErrorCodes
extern const int LOGICAL_ERROR;
extern const int UNSUPPORTED_METHOD;
extern const int SUPPORT_IS_DISABLED;
extern const int SEMI_ANTI_JOIN_COLUMN_ACCESS_DENIED;
}
namespace Setting
@@ -1346,7 +1347,7 @@ ProjectionNames QueryAnalyzer::resolveFunction(QueryTreeNodePtr & node, Identifi
&& !function_node_ptr->isWindowFunction()
/// JOIN planning unwraps root constant source expressions. Keep JOIN ON expressions on
/// the regular path so a preserved scalar-subquery source is never sent to the planner.
&& !scope.resolving_join_on_expression
&& !(scope.resolving_join_on_expression && scope.resolving_join_on_expression->getNodeType() == QueryTreeNodeType::JOIN)
&& !lambda_expression_untyped
&& !UserDefinedSQLFunctionFactory::instance().tryGet(function_name)
&& !UserDefinedExecutableFunctionFactory::instance().tryGet(function_name, scope.context, parameters)) /// NOLINT(readability-static-accessed-through-instance)
@@ -1586,8 +1587,14 @@ ProjectionNames QueryAnalyzer::resolveFunction(QueryTreeNodePtr & node, Identifi
false /*allow_table_expression*/,
allow_niladic_functions);
}
catch (const Exception &)
catch (const Exception & e)
{
/// SEMI/ANTI JOIN column access violations must not be masked by dead-branch
/// folding: they are compile-time access-control errors, not "unknown column"
/// lookups. Rethrow so the query is rejected even when the offending reference
/// sits in a statically unreachable branch of `if`.
if (e.code() == ErrorCodes::SEMI_ANTI_JOIN_COLUMN_ACCESS_DENIED)
throw;
apply_constant_if_optimization = true;
}
@@ -1719,8 +1726,12 @@ ProjectionNames QueryAnalyzer::resolveFunction(QueryTreeNodePtr & node, Identifi
false /*allow_table_expression*/,
allow_niladic_functions);
}
catch (const Exception &)
catch (const Exception & e)
{
/// See the `if` special case above: SEMI/ANTI JOIN access violations
/// must not be swallowed by dead-branch folding.
if (e.code() == ErrorCodes::SEMI_ANTI_JOIN_COLUMN_ACCESS_DENIED)
throw;
apply_constant_multi_if_optimization = true;
}
}
+1 -13
View File
@@ -3,7 +3,6 @@
#include <Access/ContextAccess.h>
#include <Common/NamedCollections/NamedCollections.h>
#include <Common/NamedCollections/NamedCollectionsFactory.h>
#include <Core/Settings.h>
#include <Interpreters/Context.h>
#include <Interpreters/evaluateConstantExpression.h>
#include <Parsers/ASTExpressionList.h>
@@ -22,11 +21,6 @@
namespace DB
{
namespace Setting
{
extern const SettingsBool allow_named_collection_override_by_default;
}
namespace ErrorCodes
{
extern const int BAD_ARGUMENTS;
@@ -515,15 +509,9 @@ NamedCollectionPtr BackupInfo::getNamedCollection(ContextPtr context) const
{
auto mutable_collection = collection->duplicate();
auto params_from_query = getParamsMapFromAST(kv_args, context);
const auto allow_override_by_default = context->getSettingsRef()[Setting::allow_named_collection_override_by_default];
for (const auto & [key, value] : params_from_query)
{
/// Enforce the same override permission as the table-function/storage paths
/// (`tryGetNamedCollectionWithOverrides`): a non-overridable key (e.g. an operator-static endpoint or
/// credentials) must not be redirected from the query, otherwise the collection's static credentials
/// could be reused against a user-chosen endpoint under the S3 credential restriction.
if (!mutable_collection->isOverridable(key, allow_override_by_default))
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Override not allowed for '{}'", key);
checkNamedCollectionOverride(*collection, key, context);
/// Marked before the value is written so the mark remembers the replaced stored value.
mutable_collection->markQueryOverridden(key);
mutable_collection->setOrUpdate<String>(key, fieldToString(value), {});
+5
View File
@@ -160,6 +160,11 @@ void LocalConnection::sendProfileEvents()
void LocalConnection::captureCurrentException()
{
/// Stop the executors before `onException`, as `finishQuery` does before `onFinish`:
/// `onException` logs the processors' profile counters, which the executor threads write.
state->executor.reset();
state->pushing_async_executor.reset();
state->pushing_executor.reset();
state->io.onException();
try
{
+6 -1
View File
@@ -815,13 +815,18 @@ ColumnAggregateFunction::MutablePtr ColumnAggregateFunction::createView() const
{
auto res = create(func, concatArenas(foreign_arenas, my_arena));
res->src = getPtr();
/// The view refers to the same states, so it must keep the state version:
/// the version affects how the states are serialized (e.g. in `groupArray` over a state column).
res->version = version;
res->type_string = type_string;
return res;
}
ColumnAggregateFunction::ColumnAggregateFunction(const ColumnAggregateFunction & src_)
: COWHelper<IColumnHelper<ColumnAggregateFunction>, ColumnAggregateFunction>(src_),
foreign_arenas(concatArenas(src_.foreign_arenas, src_.my_arena)),
func(src_.func), src(src_.getPtr()), data(src_.data.begin(), src_.data.end())
func(src_.func), src(src_.getPtr()), data(src_.data.begin(), src_.data.end()),
type_string(src_.type_string), version(src_.version)
{
}
+10
View File
@@ -6,6 +6,8 @@
#include <Compression/ICompressionCodec.h>
#include <Core/ColumnWithTypeAndName.h>
#include <Core/Field.h>
#include <Core/ProtocolDefines.h>
#include <DataTypes/DataTypeAggregateFunction.h>
#include <DataTypes/Serializations/ISerialization.h>
#include <Formats/NativeReader.h>
#include <Formats/NativeWriter.h>
@@ -124,6 +126,14 @@ public:
{
WriteBufferFromVector<BLOB> wbuf(blob);
CompressedWriteBuffer compressed_buffer(wbuf, codec);
/// The type announced on the wire gets the state version of a versioned aggregate function
/// derived from the negotiated revision (see the comment in `NativeWriter::write`), and the
/// reader parses the payload according to the announced type. Derive the version the same way
/// here, or the payload would be written with the version resolved from the local type
/// and lose sync with the announcement.
bool include_version = client_revision >= DBMS_MIN_REVISION_WITH_AGGREGATE_FUNCTIONS_VERSIONING;
setVersionToAggregateFunctions(
wrapped_column.type, /* if_empty= */ client_revision == 0, include_version ? std::optional<size_t>(client_revision) : std::nullopt);
auto [serialization, _, column_to_write] = NativeWriter::getSerializationAndColumn(client_revision, wrapped_column);
NativeWriter::writeData(
*serialization, column_to_write, compressed_buffer, format_settings, 0, column_to_write->size(), client_revision);
@@ -231,6 +231,7 @@ int main(int argc, char ** argv)
REGISTER_E2E(UInt32, makeUInt32Columns, 4, 16384, 16);
REGISTER_E2E(UInt32, makeUInt32Columns, 4, 16384, 256);
benchmark::MaybeReenterWithoutASLR(argc, argv);
benchmark::Initialize(&argc, argv);
benchmark::RunSpecifiedBenchmarks();
return 0;
+6 -3
View File
@@ -133,10 +133,10 @@ std::optional<ConfigProcessor::LoadedConfig> ConfigReloader::reloadIfNewer(bool
loaded_config = config_processor.loadConfigWithZooKeeperIncludes(
zk_node_cache.get(), zk_changed_event, fallback_to_preprocessed, is_config_changed);
}
catch (const Coordination::Exception & e)
catch (const Coordination::Exception &)
{
if (Coordination::isHardwareError(e.code))
need_reload_from_zk = true;
// Always reload from ZK in case of an exception to ensure we don't miss any changes.
need_reload_from_zk = true;
const auto message = getCurrentExceptionMessageAndPattern(/*with_stacktrace=*/true);
auto exc = std::make_unique<Exception>(message, ErrorCodes::CANNOT_LOAD_CONFIG);
@@ -149,6 +149,9 @@ std::optional<ConfigProcessor::LoadedConfig> ConfigReloader::reloadIfNewer(bool
}
catch (...)
{
// Always reload from ZK in case of an exception to ensure we don't miss any changes.
need_reload_from_zk = true;
const auto message = getCurrentExceptionMessageAndPattern(/*with_stacktrace=*/true);
auto exc = std::make_unique<Exception>(message, ErrorCodes::CANNOT_LOAD_CONFIG);
+1
View File
@@ -712,6 +712,7 @@
M(1019, QUERY_SLOT_ACQUISITION_TIMEOUT) \
M(1020, MEMORY_RESERVATION_ACQUISITION_TIMEOUT) \
M(1021, AZURE_OBJECT_CHANGED_DURING_READ) \
M(1022, SEMI_ANTI_JOIN_COLUMN_ACCESS_DENIED) \
/* Error codes do not have to be contiguous, they only have to be unique - this is checked by a `static_assert` below. */
#ifdef APPLY_FOR_EXTERNAL_ERROR_CODES
+5
View File
@@ -244,6 +244,8 @@ static struct InitFiu
REGULAR(zero_copy_unlock_zk_fail_after_op) \
REGULAR(plain_rewritable_object_storage_azure_not_found_on_init) \
PAUSEABLE(storage_merge_tree_background_clear_old_parts_pause) \
PAUSEABLE(storage_merge_tree_load_mutations_pause_before_read) \
PAUSEABLE(merge_tree_drop_all_data_pause_before_removing_parts) \
PAUSEABLE(storage_merge_create_children_plans_pause) \
PAUSEABLE_ONCE(storage_shared_merge_tree_mutate_pause_before_wait) \
PAUSEABLE(database_replicated_startup_pause) \
@@ -322,6 +324,7 @@ static struct InitFiu
ONCE(mt_alter_settings_throw_before_metadata_commit) \
PAUSEABLE_ONCE(mt_alter_settings_pause_before_metadata_commit) \
PAUSEABLE_ONCE(mt_alter_readonly_pause_after_metadata_commit) \
PAUSEABLE_ONCE(mt_move_partition_pause_before_commit) \
PAUSEABLE_ONCE(mt_pause_before_loading_outdated_part) \
PAUSEABLE(mt_pause_before_loading_queued_outdated_part) \
ONCE(mt_alter_readonly_throw_in_start_background_workers) \
@@ -414,6 +417,8 @@ static struct InitFiu
ONCE(distributed_plan_delay_root_cause_report) \
ONCE(zk_send_thread_request_window_throw) \
ONCE(zk_send_thread_operations_insert_throw) \
PAUSEABLE_ONCE(context_zookeeper_lock_acquired_pause) \
PAUSEABLE_ONCE(context_auxiliary_zookeeper_lock_acquired_pause) \
REGULAR(replicated_database_status_finished_node_missing) \
PAUSEABLE_ONCE(rmt_cancel_removed_parts_check_pause_in_gap) \
PAUSEABLE_ONCE(limit_by_sorted_stream_transform_pause) \
@@ -38,6 +38,11 @@ public:
bool has(const Key & key) const;
const std::string & getName() const
{
return collection_name;
}
bool hasAny(const std::initializer_list<Key> & keys) const;
template <typename T> T get(const Key & key) const;
-242
View File
@@ -13,13 +13,6 @@ void test_find_first_not(const std::string & haystack, std::size_t expected_pos)
ASSERT_EQ(begin + expected_pos, find_first_not_symbols<symbols...>(begin, end));
}
static void test_find_first_not(const std::string & haystack, const std::string & symbols, const std::size_t expected_pos)
{
const char * begin = haystack.data();
ASSERT_EQ(begin + expected_pos, find_first_not_symbols(haystack, SearchSymbols(symbols)));
}
TEST(FindSymbols, SimpleTest)
{
@@ -57,92 +50,6 @@ TEST(FindSymbols, SimpleTest)
}
}
TEST(FindSymbols, RunTimeNeedle)
{
auto test_haystack = [](const auto & haystack, const auto & unfindable_needle) {
#define TEST_HAYSTACK_AND_NEEDLE(haystack_, needle_) \
do { \
const auto & h = haystack_; \
const auto & n = needle_; \
EXPECT_EQ( \
std::find_first_of(h.data(), h.data() + h.size(), n.data(), n.data() + n.size()), \
find_first_symbols(h, SearchSymbols(n)) \
) << "haystack: \"" << h << "\" (" << static_cast<const void*>(h.data()) << ")" \
<< ", needle: \"" << n << "\""; \
} \
while (false)
// can't find needle
TEST_HAYSTACK_AND_NEEDLE(haystack, unfindable_needle);
#define TEST_WITH_MODIFIED_NEEDLE(haystack, in_needle, needle_update_statement) \
do \
{ \
std::string needle = (in_needle); \
(needle_update_statement); \
TEST_HAYSTACK_AND_NEEDLE(haystack, needle); \
} \
while (false)
// findable symbol is at beginning of the needle
// Can find at first pos of haystack
TEST_WITH_MODIFIED_NEEDLE(haystack, unfindable_needle, needle.front() = haystack.front());
// Can find at first pos of haystack
TEST_WITH_MODIFIED_NEEDLE(haystack, unfindable_needle, needle.front() = haystack.back());
// Can find in the middle of haystack
TEST_WITH_MODIFIED_NEEDLE(haystack, unfindable_needle, needle.front() = haystack[haystack.size() / 2]);
// findable symbol is at end of the needle
// Can find at first pos of haystack
TEST_WITH_MODIFIED_NEEDLE(haystack, unfindable_needle, needle.back() = haystack.front());
// Can find at first pos of haystack
TEST_WITH_MODIFIED_NEEDLE(haystack, unfindable_needle, needle.back() = haystack.back());
// Can find in the middle of haystack
TEST_WITH_MODIFIED_NEEDLE(haystack, unfindable_needle, needle.back() = haystack[haystack.size() / 2]);
// findable symbol is in the middle of the needle
// Can find at first pos of haystack
TEST_WITH_MODIFIED_NEEDLE(haystack, unfindable_needle, needle[needle.size() / 2] = haystack.front());
// Can find at first pos of haystack
TEST_WITH_MODIFIED_NEEDLE(haystack, unfindable_needle, needle[needle.size() / 2] = haystack.back());
// Can find in the middle of haystack
TEST_WITH_MODIFIED_NEEDLE(haystack, unfindable_needle, needle[needle.size() / 2] = haystack[haystack.size() / 2]);
#undef TEST_WITH_MODIFIED_NEEDLE
#undef TEST_HAYSTACK_AND_NEEDLE
};
// there are 4 major groups of cases:
// haystack < 16 bytes, haystack > 16 bytes
// needle < 5 bytes, needle >= 5 bytes
// First and last symbols of haystack should be unique
const std::string long_haystack = "Hello, world! Goodbye...?";
const std::string short_haystack = "Hello, world!";
// In sync with find_first_symbols_dispatch code: long needles receive special treatment.
// as of now "long" means >= 5
const std::string unfindable_long_needle = "0123456789ABCDEF";
const std::string unfindable_short_needle = "0123";
{
SCOPED_TRACE("Long haystack");
test_haystack(long_haystack, unfindable_long_needle);
test_haystack(long_haystack, unfindable_short_needle);
}
{
SCOPED_TRACE("Short haystack");
test_haystack(short_haystack, unfindable_long_needle);
test_haystack(short_haystack, unfindable_short_needle);
}
// SearchSymbols rejects needles longer than SearchSymbols::BUFFER_SIZE on every platform,
// not only where the SSE4.2 path is compiled in.
const std::string excessively_long_needle = "ABCDEFIJKLMNOPQRSTUVWXYZacfghijkmnpqstuvxz";
ASSERT_ANY_THROW(SearchSymbols{excessively_long_needle});
}
TEST(FindNotSymbols, AllSymbolsPresent)
{
std::string str_with_17_bytes = "hello world hello";
@@ -161,12 +68,6 @@ TEST(FindNotSymbols, AllSymbolsPresent)
test_find_first_not<'h', 'e', 'l', 'o', 'w', 'r', 'd', ' '>(str_with_15_bytes, str_with_15_bytes.size());
test_find_first_not<'h', 'e', 'l', 'o', 'w', 'r', 'd', ' '>(str_with_16_bytes, str_with_16_bytes.size());
test_find_first_not<'h', 'e', 'l', 'o', 'w', 'r', 'd', ' '>(str_with_17_bytes, str_with_17_bytes.size());
const auto * symbols = "helowrd ";
test_find_first_not(str_with_15_bytes, symbols, str_with_15_bytes.size());
test_find_first_not(str_with_16_bytes, symbols, str_with_16_bytes.size());
test_find_first_not(str_with_17_bytes, symbols, str_with_17_bytes.size());
}
TEST(FindNotSymbols, NoSymbolsMatch)
@@ -175,28 +76,24 @@ TEST(FindNotSymbols, NoSymbolsMatch)
// begin should be returned since the first character of the string does not match any of the below symbols
test_find_first_not<'h', 'i', 'j'>(s, 0u);
test_find_first_not(s, "hij", 0u);
}
TEST(FindNotSymbols, ExtraSymbols)
{
std::string s = "hello_world_hello";
test_find_first_not<'h', 'e', 'l', 'o', ' '>(s, 5u);
test_find_first_not(s, "helo ", 5u);
}
TEST(FindNotSymbols, EmptyString)
{
std::string s;
test_find_first_not<'h', 'e', 'l', 'o', 'w', 'r', 'd', ' '>(s, s.size());
test_find_first_not(s, "helowrd ", s.size());
}
TEST(FindNotSymbols, SingleChar)
{
std::string s = "a";
test_find_first_not<'a'>(s, s.size());
test_find_first_not(s, "a", s.size());
}
TEST(FindNotSymbols, NullCharacter)
@@ -206,148 +103,9 @@ TEST(FindNotSymbols, NullCharacter)
// to \0.
std::string s("abcdefg\0x", 9u);
test_find_first_not<'a', 'b', 'c', 'd', 'e', 'f', 'g'>(s, 7u);
test_find_first_not(s, "abcdefg", 7u);
// Same check with a haystack long enough to exercise the SIMD body — guards against
// implementations that pad unused needle slots with \0 and falsely match it.
std::string long_s("abcdefgabcdefgab\0", 17u);
test_find_first_not<'a', 'b', 'c', 'd', 'e', 'f', 'g'>(long_s, 16u);
test_find_first_not(long_s, "abcdefg", 16u);
}
TEST(FindSymbols, EmptyRunTimeNeedle)
{
// Empty SearchSymbols. With `positive=true` the result must be end/nullptr
// (no byte is in the empty symbol set); with `positive=false` the first/last
// byte qualifies. Long haystacks (>= 16 bytes) and embedded `\0` bytes also
// guard against the SIMD body matching `\0` via a zero-padded needle vector.
const SearchSymbols empty{};
auto end_of = [](const std::string & h) { return h.data() + h.size(); };
// Short haystack, no `\0`.
{
const std::string h = "abc";
EXPECT_EQ(find_first_symbols(h, empty), end_of(h));
EXPECT_EQ(find_first_not_symbols(h, empty), h.data());
EXPECT_EQ(find_first_symbols_or_null(h, empty), nullptr);
EXPECT_EQ(find_first_not_symbols_or_null(h, empty), h.data());
EXPECT_EQ(find_last_symbols_or_null(h, empty), nullptr);
EXPECT_EQ(find_last_not_symbols_or_null(h, empty), end_of(h) - 1);
}
// Empty haystack.
{
const std::string h;
EXPECT_EQ(find_first_symbols(h, empty), end_of(h));
EXPECT_EQ(find_first_not_symbols(h, empty), end_of(h));
EXPECT_EQ(find_first_symbols_or_null(h, empty), nullptr);
EXPECT_EQ(find_first_not_symbols_or_null(h, empty), nullptr);
EXPECT_EQ(find_last_symbols_or_null(h, empty), nullptr);
EXPECT_EQ(find_last_not_symbols_or_null(h, empty), nullptr);
}
// Long haystack (exercises SIMD body) containing a `\0`. A zero-padded needle
// vector would falsely match the `\0`; the empty-needle fast path must skip
// SIMD entirely.
{
const std::string h("aaaaaaaaaaaaaaaa\0aaaaaaaaaaaaaaa", 32u);
ASSERT_EQ(h.size(), 32u);
EXPECT_EQ(find_first_symbols(h, empty), end_of(h));
EXPECT_EQ(find_first_symbols_or_null(h, empty), nullptr);
EXPECT_EQ(find_last_symbols_or_null(h, empty), nullptr);
EXPECT_EQ(find_first_not_symbols(h, empty), h.data());
EXPECT_EQ(find_first_not_symbols_or_null(h, empty), h.data());
EXPECT_EQ(find_last_not_symbols_or_null(h, empty), end_of(h) - 1);
}
}
TEST(FindLastSymbols, RunTimeNeedleLongHaystack)
{
// These exercise find_last_symbols_or_null(string_view, SearchSymbols) and
// find_last_not_symbols_or_null(string_view, SearchSymbols) on haystacks
// long enough to enter the SIMD reverse-search body (end - begin >= 16).
// The reverse path computes the match index as `__builtin_clzll(bit_mask) >> 2`
// on AArch64, so we cover the boundary positions and the \0 case.
auto offset = [](const char * p, const std::string & h) -> ssize_t
{
return p == nullptr ? -1 : p - h.data();
};
// 32 bytes — two full SIMD chunks, no scalar tail.
{
const std::string haystack = "abcdefghijklmnop0123456789ABCDEF";
ASSERT_EQ(haystack.size(), 32u);
// Match in the second (most-recent, scanned-first) SIMD chunk.
EXPECT_EQ(offset(find_last_symbols_or_null(haystack, SearchSymbols("F")), haystack), 31);
// Match only in the first SIMD chunk (must skip the empty second chunk).
EXPECT_EQ(offset(find_last_symbols_or_null(haystack, SearchSymbols("a")), haystack), 0);
// Match in the middle of the haystack (first byte of the second chunk).
EXPECT_EQ(offset(find_last_symbols_or_null(haystack, SearchSymbols("0")), haystack), 16);
// Last byte of the first chunk (boundary).
EXPECT_EQ(offset(find_last_symbols_or_null(haystack, SearchSymbols("p")), haystack), 15);
// Multiple matches: must return the rightmost one (second chunk, position 22).
EXPECT_EQ(offset(find_last_symbols_or_null(haystack, SearchSymbols("ag6")), haystack), 22);
// No match — must return nullptr.
EXPECT_EQ(find_last_symbols_or_null(haystack, SearchSymbols("xyz")), nullptr);
}
// 17 bytes — minimal length that takes the SIMD branch (one chunk + 1-byte scalar tail).
{
const std::string haystack = "0123456789abcdefX";
ASSERT_EQ(haystack.size(), 17u);
// Tail byte (handled by scalar prologue of the reverse helper, pos[-1]).
EXPECT_EQ(offset(find_last_symbols_or_null(haystack, SearchSymbols("X")), haystack), 16);
// First byte of the haystack.
EXPECT_EQ(offset(find_last_symbols_or_null(haystack, SearchSymbols("0")), haystack), 0);
// Match inside the SIMD chunk, not in the prologue.
EXPECT_EQ(offset(find_last_symbols_or_null(haystack, SearchSymbols("5")), haystack), 5);
}
// \0 byte in the haystack: a runtime needle of "\0" must find it; an unrelated
// needle must not falsely match the embedded \0. This guards against any padding
// of unused needle slots with \0.
{
const std::string haystack("abcdefghijklmnop\0qrstuvwxyz", 27u);
ASSERT_EQ(haystack.size(), 27u);
EXPECT_EQ(offset(find_last_symbols_or_null(haystack, SearchSymbols(std::string("\0", 1u))), haystack), 16);
EXPECT_EQ(find_last_symbols_or_null(haystack, SearchSymbols("0123")), nullptr);
}
// Negative variant (find_last_not_symbols_or_null) with a runtime needle.
{
// 17 bytes, last byte differs from the needle — that byte should be returned
// by the scalar prologue (pos[-1]).
const std::string haystack = "aaaaaaaaaaaaaaaab";
ASSERT_EQ(haystack.size(), 17u);
EXPECT_EQ(offset(find_last_not_symbols_or_null(haystack, SearchSymbols("a")), haystack), 16);
// First byte differs from the needle, rest matches the needle — must be found
// in the SIMD body of the reverse pass.
const std::string haystack2 = "Xaaaaaaaaaaaaaaaa";
ASSERT_EQ(haystack2.size(), 17u);
EXPECT_EQ(offset(find_last_not_symbols_or_null(haystack2, SearchSymbols("a")), haystack2), 0);
// 32 bytes with a single non-needle byte exactly at the SIMD chunk boundary
// (position 15 — last byte of the first reverse-scanned chunk).
const std::string haystack3 = "aaaaaaaaaaaaaaaXaaaaaaaaaaaaaaaa";
ASSERT_EQ(haystack3.size(), 32u);
EXPECT_EQ(offset(find_last_not_symbols_or_null(haystack3, SearchSymbols("a")), haystack3), 15);
// All bytes match the needle — must return nullptr.
const std::string haystack4(32u, 'a');
EXPECT_EQ(find_last_not_symbols_or_null(haystack4, SearchSymbols("a")), nullptr);
// \0 byte: with needle "a", an embedded \0 is a non-needle byte and must be
// found by find_last_not.
const std::string haystack5("aaaaaaaaaaaaaaaa\0aaaaaaaaaaaaaaa", 32u);
ASSERT_EQ(haystack5.size(), 32u);
EXPECT_EQ(offset(find_last_not_symbols_or_null(haystack5, SearchSymbols("a")), haystack5), 16);
}
}
+2
View File
@@ -787,6 +787,8 @@ void KeeperServer::startup(const Poco::Util::AbstractConfiguration & config, boo
/// digest checking disabled, which orphan removal requires, an entry referencing a removed path
/// would silently resolve differently instead of failing.
///
/// A successful check also persists the repaired snapshot, so Raft can only serve the repaired
/// tree and a restart no longer needs orphan removal enabled.
/// This must stay between `setLogStore` above and `launchRaftServer` below: throwing here is a
/// clean startup failure, whereas failing later inside `KeeperStateMachine::preprocess` would
/// `abort()` the process.
+121 -11
View File
@@ -1,4 +1,5 @@
#include <filesystem>
#include <limits>
#include <memory>
#include <optional>
#include <Compression/CompressedReadBuffer.h>
@@ -13,7 +14,6 @@
#include <Coordination/ReadBufferFromNuraftBuffer.h>
#include <Coordination/WriteBufferFromNuraftBuffer.h>
#include <Core/Field.h>
#include <Common/thread_local_rng.h>
#include <Disks/IDisk.h>
#include <IO/CompressionMethod.h>
#include <IO/ReadBufferFromFile.h>
@@ -22,12 +22,13 @@
#include <IO/WriteHelpers.h>
#include <IO/copyData.h>
#include <base/sort.h>
#include <Common/ZooKeeper/ZooKeeperCommon.h>
#include <Common/ZooKeeper/ZooKeeperIO.h>
#include <Common/logger_useful.h>
#include <Common/ProfileEvents.h>
#include <Common/SharedLockGuard.h>
#include <Common/Stopwatch.h>
#include <Common/ZooKeeper/ZooKeeperCommon.h>
#include <Common/ZooKeeper/ZooKeeperIO.h>
#include <Common/logger_useful.h>
#include <Common/thread_local_rng.h>
#include <zstd.h>
@@ -66,15 +67,30 @@ namespace
return static_cast<int>(level);
}
std::string getSnapshotFileName(uint64_t up_to_log_idx, bool compress_zstd)
std::string getSnapshotFileName(uint64_t up_to_log_idx, bool compress_zstd, uint64_t recovery_generation = 0)
{
/// Unique-from-birth name avoids collisions between concurrent same-index writes.
auto base = fmt::format("snapshot_{}_{:016x}.bin", up_to_log_idx, thread_local_rng());
auto base = recovery_generation
? fmt::format("snapshot_{}_recovered_{}_{:016x}.bin", up_to_log_idx, recovery_generation, thread_local_rng())
: fmt::format("snapshot_{}_{:016x}.bin", up_to_log_idx, thread_local_rng());
if (compress_zstd)
base += ".zstd";
return base;
}
uint64_t getSnapshotRecoveryGeneration(const std::string & path)
{
const auto name = fs::path(path).filename().string();
const auto index_end = name.find('_', std::string_view("snapshot_").size());
constexpr std::string_view recovered_prefix = "_recovered_";
if (index_end == std::string::npos || !std::string_view(name).substr(index_end).starts_with(recovered_prefix))
return 0;
const auto generation_begin = index_end + recovered_prefix.size();
const auto generation_end = name.find('_', generation_begin);
return parse<uint64_t>(name.substr(generation_begin, generation_end - generation_begin));
}
void cancelAndResetWriteBuffer(std::unique_ptr<WriteBuffer> & buffer)
{
if (buffer && !buffer->isFinalized() && !buffer->isCanceled())
@@ -124,10 +140,12 @@ namespace
/// A file fsync does not persist a directory-entry change, so the marker unlink needs the
/// directory fsynced too, with the fd opened before the unlink. Snapshots live at the disk
/// root; getDirectorySyncGuard("") returns nullptr (no-op) on non-local disks.
void removeSnapshotMarker(const DiskPtr & disk, const std::string & tmp_snapshot_file_name)
void removeSnapshotMarker(const DiskPtr & disk, const std::string & tmp_snapshot_file_name, bool checked_directory_sync = false)
{
SyncGuardPtr dir_sync_guard = disk->getDirectorySyncGuard("");
disk->removeFile(tmp_snapshot_file_name);
if (checked_directory_sync && dir_sync_guard)
dir_sync_guard->sync();
}
void writeNode(std::string_view data, const KeeperNodeStats & stats, SnapshotVersion version, WriteBuffer & out)
@@ -434,8 +452,17 @@ KeeperSnapshotManager::KeeperSnapshotManager(
LOG_TRACE(log, "Found {} on {}", snapshot_file, disk->getName());
size_t snapshot_up_to = getLogIdxFromSnapshotPath(snapshot_file);
if (existing_snapshots.contains(snapshot_up_to))
if (auto registered = existing_snapshots.find(snapshot_up_to); registered != existing_snapshots.end())
{
/// A completed recovery is authoritative for this index, even if archiving its old
/// copies was interrupted. A later recovery of that file has a higher generation.
if (getSnapshotRecoveryGeneration(snapshot_file) > getSnapshotRecoveryGeneration(registered->second->path))
{
duplicate_snapshot_files.push_back(
DuplicateSnapshotFile{registered->second->disk, registered->second->path, snapshot_up_to});
registered->second = makeManagedSnapshotFileInfo(snapshot_file, disk, snapshot_up_to);
continue;
}
/// Equivalent snapshots for the same committed index (upgrade races, crashed loser
/// cleanup, interrupted moves). First-scanned copy stays registered; the duplicate
/// is handled after the scan.
@@ -477,6 +504,12 @@ KeeperSnapshotManager::KeeperSnapshotManager(
for (auto & duplicate : duplicate_snapshot_files)
{
const auto & registered = existing_snapshots.at(duplicate.up_to_log_idx);
if (getSnapshotRecoveryGeneration(registered->path) > getSnapshotRecoveryGeneration(duplicate.path))
{
/// Finish an interrupted recovery before ordinary maintenance can move or retire files.
archiveSnapshotFile(makeManagedSnapshotFileInfo(duplicate.path, duplicate.disk, duplicate.up_to_log_idx));
continue;
}
if (!oldest_retained_idx || duplicate.up_to_log_idx < *oldest_retained_idx)
{
LOG_WARNING(
@@ -912,12 +945,17 @@ void KeeperSnapshotManager::removeSnapshot(uint64_t log_idx)
}
SnapshotFileInfoPtr KeeperSnapshotManager::writeSnapshotFile(const KeeperStorageSnapshot & snapshot)
{
return writeSnapshotFile(
snapshot, getLatestSnapshotDisk(), getSnapshotFileName(snapshot.snapshot_meta->get_last_log_idx(), compress_snapshots_zstd));
}
SnapshotFileInfoPtr KeeperSnapshotManager::writeSnapshotFile(
const KeeperStorageSnapshot & snapshot, const DiskPtr & disk, const std::string & snapshot_file_name, bool checked_directory_sync)
{
auto up_to_log_idx = snapshot.snapshot_meta->get_last_log_idx();
auto snapshot_file_name = getSnapshotFileName(up_to_log_idx, compress_snapshots_zstd);
auto tmp_snapshot_file_name = "tmp_" + snapshot_file_name;
auto disk = getLatestSnapshotDisk();
std::unique_ptr<WriteBuffer> writer;
std::unique_ptr<WriteBuffer> compressed_writer;
try
@@ -925,8 +963,15 @@ SnapshotFileInfoPtr KeeperSnapshotManager::writeSnapshotFile(const KeeperStorage
/// Create empty marker: if both tmp_<name> and <name> exist on restart, the snapshot
/// is treated as incomplete and both are removed (see KeeperSnapshotManager constructor).
{
auto dir_sync_guard = checked_directory_sync ? disk->getDirectorySyncGuard("") : nullptr;
auto buf = disk->writeFile(tmp_snapshot_file_name);
buf->finalize();
if (checked_directory_sync)
{
buf->sync();
if (dir_sync_guard)
dir_sync_guard->sync();
}
}
writer = disk->writeFile(snapshot_file_name);
@@ -962,7 +1007,7 @@ SnapshotFileInfoPtr KeeperSnapshotManager::writeSnapshotFile(const KeeperStorage
compressed_writer.reset();
writer.reset();
removeSnapshotMarker(disk, tmp_snapshot_file_name);
removeSnapshotMarker(disk, tmp_snapshot_file_name, checked_directory_sync);
}
catch (...)
{
@@ -975,6 +1020,71 @@ SnapshotFileInfoPtr KeeperSnapshotManager::writeSnapshotFile(const KeeperStorage
return makeManagedSnapshotFileInfo(snapshot_file_name, disk, up_to_log_idx);
}
SnapshotFileInfoPtr KeeperSnapshotManager::rewriteSnapshotAfterRecovery(const KeeperStorageSnapshot & snapshot)
{
if (keeper_context->getServerState() != KeeperContext::Phase::INIT)
throw Exception(ErrorCodes::LOGICAL_ERROR, "Snapshot recovery is only allowed during startup");
const auto log_idx = snapshot.snapshot_meta->get_last_log_idx();
auto original = existing_snapshots.at(log_idx);
const auto generation = getSnapshotRecoveryGeneration(original->path);
if (generation == std::numeric_limits<uint64_t>::max())
throw Exception(ErrorCodes::LOGICAL_ERROR, "Snapshot recovery generation overflow for index {}", log_idx);
/// Never replace an existing file: plain object storage cannot rename, and replacement on
/// other object disks can unlink the destination before installing the source. The marker
/// protects an incomplete write; removing it commits the higher recovery generation on disk.
auto repaired = writeSnapshotFile(
snapshot,
getLatestSnapshotDisk(),
getSnapshotFileName(log_idx, compress_snapshots_zstd, generation + 1),
/*checked_directory_sync=*/true);
/// From this point the repaired file must survive any subsequent failure. Restart will prefer
/// it and finish archiving old copies, so never retire it as an unpublished write.
auto & old_copies = retained_duplicate_snapshots[log_idx];
old_copies.push_back(original);
existing_snapshots.at(log_idx) = repaired;
while (!old_copies.empty())
{
archiveSnapshotFile(old_copies.back());
old_copies.pop_back();
}
retained_duplicate_snapshots.erase(log_idx);
LOG_WARNING(log, "Persisted recovered snapshot {} at {} on disk {}", log_idx, repaired->path, repaired->disk->getName());
return repaired;
}
void KeeperSnapshotManager::archiveSnapshotFile(const SnapshotFileInfoPtr & file_info)
{
const auto & disk = file_info->disk;
if (!file_info->path.starts_with("orphaned_"))
{
auto backup_path = "orphaned_" + file_info->path;
{
auto dir_sync_guard = disk->getDirectorySyncGuard("");
auto reader = disk->readFile(file_info->path, getReadSettings());
auto writer = disk->writeFile(backup_path);
copyData(*reader, *writer);
writer->finalize();
writer->sync();
if (dir_sync_guard)
dir_sync_guard->sync();
}
auto dir_sync_guard = disk->getDirectorySyncGuard("");
disk->removeFile(file_info->path);
/// A failed directory sync can be retried without trying to read an already removed file.
file_info->path.swap(backup_path);
LOG_WARNING(
log, "Removed superseded snapshot {} after preserving it at {} on disk {}", backup_path, file_info->path, disk->getName());
if (dir_sync_guard)
dir_sync_guard->sync();
}
else if (auto dir_sync_guard = disk->getDirectorySyncGuard(""))
dir_sync_guard->sync();
}
SnapshotFileInfoPtr KeeperSnapshotManager::serializeSnapshotToDisk(const KeeperStorageSnapshot & snapshot)
{
auto up_to_log_idx = snapshot.snapshot_meta->get_last_log_idx();
Loaded 100 of 437 files, more files were not shown because too many files have changed in this diff. Show more