|
22 | 22 | #include <algorithm> |
23 | 23 | #include <cstdint> |
24 | 24 | #include <iterator> |
| 25 | +#include <mutex> |
25 | 26 | #include <ranges> |
| 27 | +#include <shared_mutex> |
26 | 28 | #include <vector> |
27 | 29 |
|
28 | 30 | #include "iceberg/expression/expression.h" |
|
37 | 39 | #include "iceberg/schema.h" |
38 | 40 | #include "iceberg/util/checked_cast.h" |
39 | 41 | #include "iceberg/util/content_file_util.h" |
| 42 | +#include "iceberg/util/executor_util_internal.h" |
40 | 43 | #include "iceberg/util/macros.h" |
41 | 44 |
|
42 | 45 | namespace iceberg { |
@@ -528,107 +531,153 @@ DeleteFileIndex::Builder& DeleteFileIndex::Builder::IgnoreResiduals() { |
528 | 531 | return *this; |
529 | 532 | } |
530 | 533 |
|
| 534 | +DeleteFileIndex::Builder& DeleteFileIndex::Builder::PlanWith(OptionalExecutor executor) { |
| 535 | + executor_ = executor; |
| 536 | + return *this; |
| 537 | +} |
| 538 | + |
531 | 539 | Result<std::vector<ManifestEntry>> DeleteFileIndex::Builder::LoadDeleteFiles() { |
532 | | - // Build expression caches per spec ID |
533 | | - std::unordered_map<int32_t, std::shared_ptr<Expression>> part_expr_cache; |
| 540 | + // TODO(zehua): Replace with a thread-safe LRU cache. |
| 541 | + std::shared_mutex projected_expr_cache_mutex; |
| 542 | + std::unordered_map<int32_t, std::shared_ptr<Expression>> projected_expr_cache; |
| 543 | + std::shared_mutex eval_cache_mutex; |
534 | 544 | std::unordered_map<int32_t, std::unique_ptr<ManifestEvaluator>> eval_cache; |
535 | 545 |
|
536 | 546 | auto data_filter = ignore_residuals_ ? True::Instance() : data_filter_; |
537 | 547 |
|
538 | | - // Filter and read manifests into manifest entries |
539 | | - std::vector<ManifestEntry> files; |
540 | | - for (const auto& manifest : delete_manifests_) { |
541 | | - if (manifest.content != ManifestContent::kDeletes) { |
542 | | - continue; |
| 548 | + auto and_filters = |
| 549 | + [](std::shared_ptr<Expression> left, |
| 550 | + std::shared_ptr<Expression> right) -> Result<std::shared_ptr<Expression>> { |
| 551 | + if (left && right) { |
| 552 | + return And::MakeFolded(std::move(left), std::move(right)); |
543 | 553 | } |
544 | | - if (!manifest.has_added_files() && !manifest.has_existing_files()) { |
545 | | - continue; |
| 554 | + return right ? std::move(right) : std::move(left); |
| 555 | + }; |
| 556 | + |
| 557 | + auto get_projected_expr = [&](int32_t spec_id, |
| 558 | + const std::shared_ptr<PartitionSpec>& spec) |
| 559 | + -> Result<std::shared_ptr<Expression>> { |
| 560 | + if (!data_filter_) { |
| 561 | + return std::shared_ptr<Expression>(); |
546 | 562 | } |
547 | 563 |
|
548 | | - const int32_t spec_id = manifest.partition_spec_id; |
549 | | - auto spec_iter = specs_by_id_.find(spec_id); |
550 | | - ICEBERG_CHECK(spec_iter != specs_by_id_.cend(), |
551 | | - "Partition spec ID {} not found when loading delete files", spec_id); |
552 | | - |
553 | | - const auto& spec = spec_iter->second; |
554 | | - |
555 | | - // Get or compute projected partition expression |
556 | | - if (!part_expr_cache.contains(spec_id) && data_filter_) { |
557 | | - auto projector = Projections::Inclusive(*spec, *schema_, case_sensitive_); |
558 | | - ICEBERG_ASSIGN_OR_RAISE(auto projected, projector->Project(data_filter_)); |
559 | | - part_expr_cache[spec_id] = std::move(projected); |
| 564 | + { |
| 565 | + std::shared_lock lock(projected_expr_cache_mutex); |
| 566 | + auto iter = projected_expr_cache.find(spec_id); |
| 567 | + if (iter != projected_expr_cache.end()) { |
| 568 | + return iter->second; |
| 569 | + } |
560 | 570 | } |
561 | 571 |
|
562 | | - // Get or create manifest evaluator |
563 | | - if (!eval_cache.contains(spec_id)) { |
564 | | - auto filter = partition_filter_; |
565 | | - if (auto it = part_expr_cache.find(spec_id); it != part_expr_cache.cend()) { |
566 | | - if (filter) { |
567 | | - ICEBERG_ASSIGN_OR_RAISE(filter, And::Make(filter, it->second)); |
568 | | - } else { |
569 | | - filter = it->second; |
570 | | - } |
571 | | - } |
572 | | - if (filter) { |
573 | | - ICEBERG_ASSIGN_OR_RAISE(auto evaluator, |
574 | | - ManifestEvaluator::MakePartitionFilter( |
575 | | - std::move(filter), spec, *schema_, case_sensitive_)); |
576 | | - eval_cache[spec_id] = std::move(evaluator); |
577 | | - } |
| 572 | + std::lock_guard lock(projected_expr_cache_mutex); |
| 573 | + auto iter = projected_expr_cache.find(spec_id); |
| 574 | + if (iter != projected_expr_cache.end()) { |
| 575 | + return iter->second; |
578 | 576 | } |
579 | 577 |
|
580 | | - // Evaluate manifest against filter |
581 | | - if (auto it = eval_cache.find(spec_id); it != eval_cache.end()) { |
582 | | - ICEBERG_ASSIGN_OR_RAISE(auto should_match, it->second->Evaluate(manifest)); |
583 | | - if (!should_match) { |
584 | | - continue; // Manifest doesn't match filter |
585 | | - } |
| 578 | + auto projector = Projections::Inclusive(*spec, *schema_, case_sensitive_); |
| 579 | + ICEBERG_ASSIGN_OR_RAISE(auto projected, projector->Project(data_filter_)); |
| 580 | + auto [inserted_iter, _] = projected_expr_cache.emplace(spec_id, std::move(projected)); |
| 581 | + return inserted_iter->second; |
| 582 | + }; |
| 583 | + |
| 584 | + auto get_manifest_evaluator = |
| 585 | + [&](int32_t spec_id, const std::shared_ptr<PartitionSpec>& spec, |
| 586 | + const std::shared_ptr<Expression>& filter) -> Result<ManifestEvaluator*> { |
| 587 | + if (!filter) { |
| 588 | + return nullptr; |
586 | 589 | } |
587 | 590 |
|
588 | | - // Read manifest entries |
589 | | - ICEBERG_ASSIGN_OR_RAISE(auto reader, |
590 | | - ManifestReader::Make(manifest, io_, schema_, spec)); |
591 | | - |
592 | | - auto partition_filter = partition_filter_; |
593 | | - if (auto it = part_expr_cache.find(spec_id); it != part_expr_cache.cend()) { |
594 | | - if (partition_filter) { |
595 | | - ICEBERG_ASSIGN_OR_RAISE(partition_filter, |
596 | | - And::Make(partition_filter, it->second)); |
597 | | - } else { |
598 | | - partition_filter = it->second; |
| 591 | + { |
| 592 | + std::shared_lock lock(eval_cache_mutex); |
| 593 | + auto iter = eval_cache.find(spec_id); |
| 594 | + if (iter != eval_cache.end()) { |
| 595 | + return iter->second.get(); |
599 | 596 | } |
600 | 597 | } |
601 | | - if (partition_filter) { |
602 | | - reader->FilterPartitions(std::move(partition_filter)); |
603 | | - } |
604 | | - if (partition_set_) { |
605 | | - reader->FilterPartitions(partition_set_); |
606 | | - } |
607 | | - reader->FilterRows(data_filter).CaseSensitive(case_sensitive_).TryDropStats(); |
608 | | - |
609 | | - ICEBERG_ASSIGN_OR_RAISE(auto entries, reader->LiveEntries()); |
610 | | - files.reserve(files.size() + entries.size()); |
611 | | - |
612 | | - for (auto& entry : entries) { |
613 | | - ICEBERG_CHECK(entry.data_file != nullptr, "ManifestEntry must have a data file"); |
614 | | - ICEBERG_CHECK(entry.sequence_number.has_value(), |
615 | | - "Missing sequence number from delete file: {}", |
616 | | - entry.data_file->file_path); |
617 | | - if (entry.sequence_number.value() > min_sequence_number_) { |
618 | | - auto& file = *entry.data_file; |
619 | | - // keep minimum stats to avoid memory pressure |
620 | | - std::unordered_set<int32_t> columns = |
621 | | - file.content == DataFile::Content::kPositionDeletes |
622 | | - ? std::unordered_set<int32_t>{MetadataColumns::kDeleteFilePathColumnId} |
623 | | - : std::unordered_set<int32_t>(file.equality_ids.begin(), |
624 | | - file.equality_ids.end()); |
625 | | - ContentFileUtil::DropUnselectedStats(*entry.data_file, columns); |
626 | | - files.emplace_back(std::move(entry)); |
627 | | - } |
| 598 | + |
| 599 | + std::lock_guard lock(eval_cache_mutex); |
| 600 | + auto iter = eval_cache.find(spec_id); |
| 601 | + if (iter != eval_cache.end()) { |
| 602 | + return iter->second.get(); |
628 | 603 | } |
629 | | - } |
630 | 604 |
|
631 | | - return files; |
| 605 | + ICEBERG_ASSIGN_OR_RAISE(auto evaluator, ManifestEvaluator::MakePartitionFilter( |
| 606 | + filter, spec, *schema_, case_sensitive_)); |
| 607 | + auto [inserted_iter, _] = eval_cache.emplace(spec_id, std::move(evaluator)); |
| 608 | + return inserted_iter->second.get(); |
| 609 | + }; |
| 610 | + |
| 611 | + return ParallelCollect( |
| 612 | + executor_, delete_manifests_, |
| 613 | + [&](const ManifestFile& manifest) -> Result<std::vector<ManifestEntry>> { |
| 614 | + std::vector<ManifestEntry> manifest_result; |
| 615 | + if (manifest.content != ManifestContent::kDeletes) { |
| 616 | + return manifest_result; |
| 617 | + } |
| 618 | + if (!manifest.has_added_files() && !manifest.has_existing_files()) { |
| 619 | + return manifest_result; |
| 620 | + } |
| 621 | + |
| 622 | + const int32_t spec_id = manifest.partition_spec_id; |
| 623 | + auto spec_iter = specs_by_id_.find(spec_id); |
| 624 | + ICEBERG_CHECK(spec_iter != specs_by_id_.cend(), |
| 625 | + "Partition spec ID {} not found when loading delete files", |
| 626 | + spec_id); |
| 627 | + |
| 628 | + const auto& spec = spec_iter->second; |
| 629 | + |
| 630 | + ICEBERG_ASSIGN_OR_RAISE(auto projected_data_filter, |
| 631 | + get_projected_expr(spec_id, spec)); |
| 632 | + ICEBERG_ASSIGN_OR_RAISE(auto delete_partition_filter, |
| 633 | + and_filters(partition_filter_, projected_data_filter)); |
| 634 | + ICEBERG_ASSIGN_OR_RAISE( |
| 635 | + auto manifest_evaluator, |
| 636 | + get_manifest_evaluator(spec_id, spec, delete_partition_filter)); |
| 637 | + if (manifest_evaluator != nullptr) { |
| 638 | + ICEBERG_ASSIGN_OR_RAISE(auto should_match, |
| 639 | + manifest_evaluator->Evaluate(manifest)); |
| 640 | + if (!should_match) { |
| 641 | + return manifest_result; |
| 642 | + } |
| 643 | + } |
| 644 | + |
| 645 | + // Read manifest entries |
| 646 | + ICEBERG_ASSIGN_OR_RAISE(auto reader, |
| 647 | + ManifestReader::Make(manifest, io_, schema_, spec)); |
| 648 | + |
| 649 | + if (delete_partition_filter) { |
| 650 | + reader->FilterPartitions(std::move(delete_partition_filter)); |
| 651 | + } |
| 652 | + if (partition_set_) { |
| 653 | + reader->FilterPartitions(partition_set_); |
| 654 | + } |
| 655 | + reader->FilterRows(data_filter).CaseSensitive(case_sensitive_).TryDropStats(); |
| 656 | + |
| 657 | + ICEBERG_ASSIGN_OR_RAISE(auto entries, reader->LiveEntries()); |
| 658 | + manifest_result.reserve(entries.size()); |
| 659 | + |
| 660 | + for (auto& entry : entries) { |
| 661 | + ICEBERG_CHECK(entry.data_file != nullptr, |
| 662 | + "ManifestEntry must have a data file"); |
| 663 | + ICEBERG_CHECK(entry.sequence_number.has_value(), |
| 664 | + "Missing sequence number from delete file: {}", |
| 665 | + entry.data_file->file_path); |
| 666 | + if (entry.sequence_number.value() > min_sequence_number_) { |
| 667 | + auto& file = *entry.data_file; |
| 668 | + // keep minimum stats to avoid memory pressure |
| 669 | + std::unordered_set<int32_t> columns = |
| 670 | + file.content == DataFile::Content::kPositionDeletes |
| 671 | + ? std::unordered_set< |
| 672 | + int32_t>{MetadataColumns::kDeleteFilePathColumnId} |
| 673 | + : std::unordered_set<int32_t>(file.equality_ids.begin(), |
| 674 | + file.equality_ids.end()); |
| 675 | + ContentFileUtil::DropUnselectedStats(*entry.data_file, columns); |
| 676 | + manifest_result.emplace_back(std::move(entry)); |
| 677 | + } |
| 678 | + } |
| 679 | + return manifest_result; |
| 680 | + }); |
632 | 681 | } |
633 | 682 |
|
634 | 683 | Status DeleteFileIndex::Builder::AddDV( |
|
0 commit comments