Repository navigation
Antalya 26.6 - Read clustering columns from delta lake and use it for data skipping #2395
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: antalya-26.6
Are you sure you want to change the base?
Changes from all commits
a9097cf
9d30c69
32268bc
35afbc2
1aceb29
376279f
dd65e97
84a4e10
ae296bb
34cc849
edadc97
2c647b0
148d5f8
15b23f3
3ddd5d5
16770f6
47cd546
1ae4e19
8a5d194
31a65ce
35197a8
c9dbbc8
e8453b9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| +7 −7 | ffi/examples/read-table/read_table.c | |
| +55 −0 | ffi/src/domain_metadata.rs | |
| +10 −3 | ffi/src/lib.rs | |
| +0 −1 | ffi/src/scan.rs | |
| +7 −4 | ffi/src/table_changes.rs | |
| +6 −1 | kernel/src/actions/visitors.rs | |
| +1 −0 | kernel/src/engine/parquet_row_group_skipping.rs | |
| +4 −1 | kernel/src/scan/mod.rs | |
| +1 −0 | kernel/src/schema/mod.rs | |
| +31 −0 | kernel/src/snapshot.rs | |
| +15 −15 | kernel/tests/read.rs | |
| +7 −7 | test-utils/src/lib.rs |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,11 @@ | |
| #include <IO/WriteBufferFromString.h> | ||
| #include <IO/WriteHelpers.h> | ||
| #include <Interpreters/Context.h> | ||
| #include <Interpreters/ActionsDAG.h> | ||
|
|
||
| #include <Poco/JSON/Parser.h> | ||
| #include <Poco/JSON/Object.h> | ||
| #include <Poco/JSON/Array.h> | ||
|
|
||
| #include <Storages/ObjectStorage/DataLakes/DeltaLake/getSchemaFromSnapshot.h> | ||
| #include <Storages/ObjectStorage/DataLakes/DeltaLake/PartitionPruner.h> | ||
|
|
@@ -41,6 +46,7 @@ namespace DB::ErrorCodes | |
| { | ||
| extern const int BAD_ARGUMENTS; | ||
| extern const int DELTA_KERNEL_ERROR; | ||
| extern const int INCORRECT_DATA; | ||
| } | ||
|
|
||
| namespace DB::Setting | ||
|
|
@@ -49,13 +55,15 @@ namespace DB::Setting | |
| extern const SettingsInt64 delta_lake_snapshot_version; | ||
| extern const SettingsBool delta_lake_throw_on_engine_predicate_error; | ||
| extern const SettingsBool delta_lake_enable_engine_predicate; | ||
| extern const SettingsBool delta_lake_clustering_predicate_fallback; | ||
| } | ||
|
|
||
| namespace ProfileEvents | ||
| { | ||
| extern const Event DeltaLakePartitionPrunedFiles; | ||
| extern const Event DeltaLakeSnapshotInitializations; | ||
| extern const Event DeltaLakeScannedFiles; | ||
| extern const Event DeltaLakeClusteringPredicateFallbacks; | ||
| } | ||
|
|
||
| namespace DB::FailPoints | ||
|
|
@@ -96,13 +104,15 @@ class TableSnapshot::Iterator final : public DB::IObjectIterator | |
| const TableSchema & table_schema_, | ||
| const DB::NameToNameMap & physical_names_map_, | ||
| const DB::Names & partition_columns_, | ||
| const DB::Names & clustering_columns_, | ||
| DB::ObjectStoragePtr object_storage_, | ||
| const DB::ActionsDAG * filter_, | ||
| DB::IDataLakeMetadata::FileProgressCallback callback_, | ||
| size_t list_batch_size_, | ||
| bool enable_expression_visitor_logging_, | ||
| bool throw_on_engine_predicate_error_, | ||
| bool enable_engine_predicate_, | ||
| bool enable_clustering_predicate_fallback_, | ||
| UpdateStatsFunc update_stats_func_, | ||
| LoggerPtr log_) | ||
| : kernel_snapshot_state(kernel_snapshot_state_) | ||
|
|
@@ -111,13 +121,15 @@ class TableSnapshot::Iterator final : public DB::IObjectIterator | |
| , read_schema(read_schema_) | ||
| , expression_schema(table_schema_) | ||
| , partition_columns(partition_columns_) | ||
| , clustering_columns(clustering_columns_) | ||
| , object_storage(object_storage_) | ||
| , callback(callback_) | ||
| , list_batch_size(list_batch_size_) | ||
| , log(log_) | ||
| , enable_expression_visitor_logging(enable_expression_visitor_logging_) | ||
| , throw_on_engine_predicate_error(throw_on_engine_predicate_error_) | ||
| , enable_engine_predicate(enable_engine_predicate_) | ||
| , enable_clustering_predicate_fallback(enable_clustering_predicate_fallback_) | ||
| , update_stats_func(update_stats_func_) | ||
| { | ||
| if (filter_) | ||
|
|
@@ -189,6 +201,57 @@ class TableSnapshot::Iterator final : public DB::IObjectIterator | |
| predicate.get(), | ||
| /* schema */nullptr), | ||
| "scan"); | ||
|
|
||
| if (engine_predicate_exception && enable_clustering_predicate_fallback && !clustering_columns.empty()) | ||
| { | ||
| /// Full predicate translation failed. Try a fallback: restrict the filter | ||
| /// to only reference clustering columns (which always have per-file min/max stats). | ||
| /// `restrictFilterDAGToInputs` replaces non-computable AND branches with `true`, | ||
| /// giving us a weaker but valid predicate for file-level skipping. | ||
| DB::NameSet clustering_inputs; | ||
| for (const auto & col : clustering_columns) | ||
| clustering_inputs.insert(col); | ||
|
|
||
| const auto * filter_output = filter->getOutputs().at(0); | ||
| auto restricted_dag = filter->restrictFilterDAGToInputs(filter_output, clustering_inputs); | ||
|
|
||
| /// Check if the restricted filter is non-trivial (has at least one function node). | ||
| /// If restrictFilterDAGToInputs replaced everything with constant `true`, | ||
| /// the DAG will have no function nodes. | ||
| bool has_function = false; | ||
| for (const auto & node : restricted_dag.getNodes()) | ||
| { | ||
| if (node.type == DB::ActionsDAG::ActionType::FUNCTION) | ||
| { | ||
| has_function = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (has_function) | ||
| { | ||
| std::exception_ptr fallback_exception; | ||
| auto fallback_predicate = getEnginePredicate(restricted_dag, fallback_exception, nullptr); | ||
| if (!fallback_exception) | ||
| { | ||
| LOG_INFO( | ||
| log, | ||
| "Full predicate translation failed; falling back to clustering-column-only " | ||
| "predicate for file-level data skipping (clustering columns: {})", | ||
| fmt::join(clustering_columns, ", ")); | ||
|
|
||
| ProfileEvents::increment(ProfileEvents::DeltaLakeClusteringPredicateFallbacks); | ||
|
|
||
| scan = KernelUtils::unwrapResult( | ||
| ffi::scan( | ||
| kernel_snapshot_state->snapshot.get(), | ||
| kernel_snapshot_state->engine.get(), | ||
| fallback_predicate.get(), | ||
| /* schema */nullptr), | ||
| "scan"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| else | ||
| { | ||
|
|
@@ -617,13 +680,15 @@ class TableSnapshot::Iterator final : public DB::IObjectIterator | |
| DB::NamesAndTypesList read_schema; | ||
| DB::NamesAndTypesList expression_schema; | ||
| DB::Names partition_columns; | ||
| DB::Names clustering_columns; | ||
| const DB::ObjectStoragePtr object_storage; | ||
| const DB::IDataLakeMetadata::FileProgressCallback callback; | ||
| const size_t list_batch_size; | ||
| const LoggerPtr log; | ||
| const bool enable_expression_visitor_logging; | ||
| const bool throw_on_engine_predicate_error; | ||
| const bool enable_engine_predicate; | ||
| const bool enable_clustering_predicate_fallback; | ||
| const UpdateStatsFunc update_stats_func; | ||
|
|
||
| std::exception_ptr scan_exception; | ||
|
|
@@ -919,6 +984,12 @@ DB::ObjectIterator TableSnapshot::iterate( | |
| const auto & settings = context->getSettingsRef(); | ||
| std::lock_guard lock(mutex); | ||
| initOrUpdateSchemaIfChanged(); | ||
| /// Clustering columns are only used by the clustering-predicate fallback in `Iterator::initScanState`. | ||
| const bool need_clustering_columns = filter_dag | ||
| && settings[DB::Setting::delta_lake_enable_engine_predicate] | ||
| && settings[DB::Setting::delta_lake_clustering_predicate_fallback]; | ||
| if (need_clustering_columns) | ||
| initOrUpdateClusteringColumns(); | ||
| auto state = getKernelSnapshotState(); | ||
| auto update_stats_func = [self = shared_from_this(), version = state->snapshot_version, this] | ||
| (SnapshotStats && stats) | ||
|
|
@@ -942,13 +1013,15 @@ DB::ObjectIterator TableSnapshot::iterate( | |
| schema->table_schema, | ||
| schema->physical_names_map, | ||
| schema->partition_columns, | ||
| need_clustering_columns ? *clustering_columns : DB::Names{}, | ||
| object_storage, | ||
| filter_dag, | ||
| callback, | ||
| list_batch_size, | ||
| settings[DB::Setting::delta_lake_enable_expression_visitor_logging], | ||
| settings[DB::Setting::delta_lake_throw_on_engine_predicate_error], | ||
| settings[DB::Setting::delta_lake_enable_engine_predicate], | ||
| settings[DB::Setting::delta_lake_clustering_predicate_fallback], | ||
| std::move(update_stats_func), | ||
| log); | ||
| } | ||
|
|
@@ -1011,6 +1084,113 @@ const DB::NameToNameMap & TableSnapshot::getPhysicalNamesMap() const | |
| return schema->physical_names_map; | ||
| } | ||
|
|
||
| void TableSnapshot::initOrUpdateClusteringColumns() const | ||
| { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I understand this method called in any case, but
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added logic to run |
||
| if (clustering_columns.has_value()) | ||
| return; | ||
|
|
||
| auto read_domain_metadata = [](const KernelSnapshotState & state) | ||
| { | ||
| return KernelUtils::unwrapResult( | ||
| ffi::get_domain_metadata_internal( | ||
| state.snapshot.get(), | ||
| KernelUtils::toDeltaString("delta.clustering"), | ||
| state.engine.get(), | ||
| KernelUtils::allocateString), | ||
| "get_domain_metadata_internal(delta.clustering)"); | ||
| }; | ||
|
|
||
| auto state = getKernelSnapshotState(); | ||
| const auto pre_fingerprint = kernel_state_credentials_fingerprint; | ||
| decltype(read_domain_metadata(*state)) result = nullptr; | ||
| try | ||
| { | ||
| result = read_domain_metadata(*state); | ||
| } | ||
| catch (const DB::Exception & e) | ||
| { | ||
| if (!tryRefreshAfterStaleTokenError(e, pre_fingerprint, "clustering metadata read")) | ||
| throw; | ||
| /// Invalidate the cached engine so `initOrUpdateSnapshot` rebuilds with the | ||
| /// freshened credentials, then re-read the domain metadata once. | ||
| kernel_snapshot_state.reset(); | ||
| initOrUpdateSnapshot(); | ||
| result = read_domain_metadata(*kernel_snapshot_state); | ||
| } | ||
|
|
||
| if (!result) | ||
| { | ||
| LOG_TRACE(log, "No delta.clustering domain metadata found for table {} — table is not liquid-clustered", helper->getTableLocation()); | ||
| clustering_columns.emplace(); | ||
| return; | ||
| } | ||
|
|
||
| std::unique_ptr<std::string> json_str(static_cast<std::string *>(result)); | ||
|
|
||
| DB::Names physical_columns; | ||
| try | ||
| { | ||
| Poco::JSON::Parser parser; | ||
| auto json = parser.parse(*json_str); | ||
| auto obj = json.extract<Poco::JSON::Object::Ptr>(); | ||
|
|
||
| auto arr = obj->getArray("clusteringColumns"); | ||
| if (!arr) | ||
| throw DB::Exception(DB::ErrorCodes::INCORRECT_DATA, "delta.clustering: missing clusteringColumns array"); | ||
|
|
||
| for (size_t i = 0; i < arr->size(); ++i) | ||
| { | ||
| auto path_arr = arr->getArray(static_cast<unsigned int>(i)); | ||
| if (!path_arr || path_arr->size() == 0) | ||
| continue; | ||
|
|
||
| std::string col_name; | ||
| for (size_t j = 0; j < path_arr->size(); ++j) | ||
| { | ||
| if (!col_name.empty()) | ||
| col_name += '.'; | ||
| col_name += path_arr->getElement<std::string>(static_cast<unsigned int>(j)); | ||
| } | ||
| physical_columns.push_back(std::move(col_name)); | ||
| } | ||
| } | ||
| catch (const Poco::Exception & e) | ||
| { | ||
| throw DB::Exception( | ||
| DB::ErrorCodes::INCORRECT_DATA, | ||
| "Failed to parse delta.clustering domain metadata of table {}: {}. " | ||
| "Set delta_lake_clustering_predicate_fallback = 0 to read the table without clustering-based file skipping", | ||
| helper->getTableLocation(), e.displayText()); | ||
| } | ||
|
|
||
| /// Convert physical column names to logical names using the inverse of physical_names_map. | ||
| /// The filter DAG uses logical names, so the clustering column names must match. | ||
| DB::Names logical_columns; | ||
| if (schema.has_value() && !schema->physical_names_map.empty()) | ||
| { | ||
| /// Build reverse map: physical → logical | ||
| DB::NameToNameMap reverse_map; | ||
| for (const auto & [logical, physical] : schema->physical_names_map) | ||
| reverse_map[physical] = logical; | ||
|
|
||
| for (const auto & phys : physical_columns) | ||
| { | ||
| auto it = reverse_map.find(phys); | ||
| if (it != reverse_map.end()) | ||
| logical_columns.push_back(it->second); | ||
| else | ||
| logical_columns.push_back(phys); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| logical_columns = std::move(physical_columns); | ||
| } | ||
|
|
||
| LOG_TRACE(log, "Clustering columns for table {}: {}", helper->getTableLocation(), fmt::join(logical_columns, ", ")); | ||
| clustering_columns.emplace(std::move(logical_columns)); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| #endif | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the intended
a = 42 AND cityHash64(b) != 0case, unsupported functions returnVISITOR_FAILED_OR_UNSUPPORTEDfromEngineIterator::getNextImplwithout settingengine_predicate_exception; only thrown exceptions populate that pointer. Consequently this condition is false, the clustering fallback and its profile event never run, and the newly added fallback test observes zero hits. Detect unsupported translation explicitly rather than using only the exception pointer.Useful? React with 👍 / 👎.