3131#include " duckdb/common/types/value_map.hpp"
3232#include " duckdb/main/settings.hpp"
3333#include " duckdb/transaction/duck_transaction_manager.hpp"
34+ #include < limits>
35+ #include < list>
36+ #include < utility>
3437
3538namespace duckdb {
3639
@@ -576,70 +579,127 @@ vector<unique_ptr<Expression>> ExtractFilterExpressions(const ColumnDefinition &
576579
577580bool TryScanIndex (ART &art, IndexEntry &entry, const ColumnList &column_list, TableFunctionInitInput &input,
578581 TableFilterSet &filter_set, idx_t max_count, set<row_t > &row_ids) {
579- // FIXME: No support for index scans on compound ARTs.
580- // See note above on multi-filter support.
581- if (art.unbound_expressions .size () > 1 ) {
582- return false ;
582+ vector<unique_ptr<Expression>> index_exprs;
583+ for (const auto &expr : art.unbound_expressions ) {
584+ index_exprs.push_back (expr->Copy ());
583585 }
584586
585- auto index_expr = art. unbound_expressions [ 0 ]-> Copy ();
587+ // If this is a view, the column IDs are (may be?) relative to the view projection
586588 auto &indexed_columns = art.GetColumnIds ();
587589
588- // NOTE: We do not push down multi-column filters, e.g., 42 = a + b.
589- if (indexed_columns.size () != 1 ) {
590+ // Allow composite ART scans
591+ if (indexed_columns.size () != index_exprs. size () ) {
590592 return false ;
591593 }
592594
593595 // Resolve bound column references in the index_expr against the current input projection
594- column_t updated_index_column;
595- bool found_index_column_in_input = false ;
596-
597- // Find the indexed column amongst the input columns
598- for (idx_t i = 0 ; i < input.column_ids .size (); ++i) {
599- if (input.column_ids [i] == indexed_columns[0 ]) {
600- updated_index_column = i;
601- found_index_column_in_input = true ;
602- break ;
596+ bool rewrite_index_exprs = false ;
597+ vector<column_t > index_column_to_input_pos;
598+ index_column_to_input_pos.resize (indexed_columns.size (), std::numeric_limits<idx_t >::max ());
599+
600+ // Associate indexed columns to input columns
601+ for (idx_t i = 0 ; i < indexed_columns.size (); ++i) {
602+ for (idx_t j = 0 ; j < input.column_ids .size (); ++j) {
603+ if (indexed_columns[i] == input.column_ids [j]) {
604+ rewrite_index_exprs = i != j;
605+ index_column_to_input_pos.at (i) = j;
606+ break ;
607+ }
603608 }
604609 }
605610
606- // If found, update the bound column ref within index_expr
607- if (found_index_column_in_input) {
608- ExpressionIterator::EnumerateExpression (index_expr, [&](Expression &expr) {
609- if (expr.GetExpressionClass () != ExpressionClass::BOUND_COLUMN_REF ) {
610- return ;
611- }
612-
613- auto &bound_column_ref_expr = expr.Cast <BoundColumnRefExpression>();
611+ // Make sure that all indexed_columns were bound, or bail out
612+ for (auto col : index_column_to_input_pos) {
613+ if (col == std::numeric_limits<idx_t >::max ()) {
614+ return false ;
615+ }
616+ }
614617
615- // If the bound column references the index column, use updated_index_column
616- if (bound_column_ref_expr.binding .column_index == indexed_columns[0 ]) {
617- bound_column_ref_expr.binding .column_index = updated_index_column;
618+ // Allow scan only if index expressions reference ONE column each, and that column
619+ // is associated with an indexed_column
620+ // NOTE: We do not push down multi-column filters, e.g., 42 = a + b.
621+ for (idx_t i = 0 ; i < index_exprs.size (); ++i) {
622+ unordered_set<column_t > referenced_columns;
623+ auto expr = &index_exprs[i];
624+
625+ // Walk the expr in case of nesting (e.g. function)
626+ ExpressionIterator::EnumerateExpression (*expr, [&](Expression &child_expr) {
627+ if (child_expr.GetExpressionClass () == ExpressionClass::BOUND_COLUMN_REF ) {
628+ auto &col_ref = child_expr.Cast <BoundColumnRefExpression>();
629+ referenced_columns.insert (col_ref.binding .column_index );
618630 }
619631 });
632+
633+ if (referenced_columns.size () != 1 ) {
634+ return false ;
635+ }
636+
637+ // Make sure the column reference can be looked up
638+ auto ref_col_idx = *referenced_columns.begin ();
639+ if (ref_col_idx >= index_column_to_input_pos.size () || ref_col_idx >= input.column_ids .size ()) {
640+ return false ;
641+ }
642+
643+ // The column for this position matches the indexed_column ID for this position directly
644+ auto direct_match = input.column_ids [ref_col_idx] == indexed_columns[i];
645+
646+ // We should know if there is a different mapping for this reference.
647+ // If there is not, it won't match, so it is not worth trying.
648+ if (!direct_match && !rewrite_index_exprs) {
649+ return false ;
650+ }
651+
652+ auto remapped_cid_position = index_column_to_input_pos[ref_col_idx];
653+ auto remapped_match = remapped_cid_position < input.column_ids .size () &&
654+ input.column_ids [remapped_cid_position] == indexed_columns[i];
655+
656+ if (!(direct_match || remapped_match)) {
657+ return false ;
658+ }
620659 }
621660
622- // Get ART column.
623- auto &col = column_list.GetColumn (LogicalIndex (indexed_columns[0 ]));
661+ // If the position of the indexed_columns differs from the order of the input, remap the index expressions
662+ if (rewrite_index_exprs) {
663+ for (auto &index_expr : index_exprs) {
664+ ExpressionIterator::EnumerateExpression (index_expr, [&](Expression &expr) {
665+ if (expr.GetExpressionClass () != ExpressionClass::BOUND_COLUMN_REF ) {
666+ return ;
667+ }
624668
625- // The indexes of the filters match input.column_indexes, which are: i -> column_index.
626- // Try to find a filter on the ART column.
627- optional_idx storage_index;
628- for (idx_t i = 0 ; i < input.column_indexes .size (); i++) {
629- if (input.column_indexes [i].ToLogical () == col.Logical ()) {
630- storage_index = i;
631- break ;
669+ auto &bound_column_ref_expr = expr.Cast <BoundColumnRefExpression>();
670+
671+ // If the bound column references an indexed column, update it
672+ for (idx_t i = 0 ; i < indexed_columns.size (); ++i) {
673+ auto remapped_index = index_column_to_input_pos[bound_column_ref_expr.binding .column_index ];
674+ if (input.column_ids [remapped_index] == indexed_columns[i]) {
675+ bound_column_ref_expr.binding .column_index = index_column_to_input_pos[i];
676+ break ;
677+ }
678+ }
679+ });
632680 }
633681 }
634682
635- // No filter matches the ART column.
636- if (!storage_index.IsValid ()) {
637- return false ;
683+ // The indexes of the filters match input.column_indexes, which are: i -> column_index.
684+ // Reuse the index <-> projection mappings from index expr rebinding (which are canonical even if not rewriting)
685+ vector<vector<unique_ptr<Expression>>> index_filters;
686+
687+ for (idx_t i = 0 ; i < index_column_to_input_pos.size (); ++i) {
688+ auto column_def = &column_list.GetColumn (LogicalIndex (indexed_columns[i]));
689+ auto maybe_filter = filter_set.filters .find (index_column_to_input_pos[i]);
690+ if (maybe_filter != filter_set.filters .end ()) {
691+ auto filter = &maybe_filter->second ;
692+ auto filter_expressions = ExtractFilterExpressions (*column_def, *filter, index_column_to_input_pos[i]);
693+
694+ index_filters.push_back (std::move (filter_expressions));
695+ }
638696 }
639697
640- // Try to find a matching filter for the column.
641- auto filter = filter_set.filters .find (storage_index.GetIndex ());
642- if (filter == filter_set.filters .end ()) {
698+ // Index filters must:
699+ // - Match ART column count 1:1
700+ // - Match filter expression set 1:1 (there may be filters on non-indexed columns, bail out if so)
701+ if (index_filters.size () != indexed_columns.size () || filter_set.filters .size () != index_filters.size () ||
702+ index_filters.empty ()) {
643703 return false ;
644704 }
645705
@@ -659,22 +719,40 @@ bool TryScanIndex(ART &art, IndexEntry &entry, const ColumnList &column_list, Ta
659719 arts_to_scan.push_back (entry.added_data_during_checkpoint ->Cast <ART >());
660720 }
661721
662- auto expressions = ExtractFilterExpressions (col, filter-> second , storage_index. GetIndex ());
663- for ( const auto &filter_expr : expressions ) {
722+ // Do a compound scan if we have filter exprs bound for several columns
723+ if (index_filters. size () > 1 ) {
664724 for (auto &art_ref : arts_to_scan) {
665725 auto &art_to_scan = art_ref.get ();
666- auto scan_state = art_to_scan.TryInitializeScan (*index_expr, *filter_expr );
726+ auto scan_state = art_to_scan.TryInitializeCompoundKeyScan (index_exprs, index_filters );
667727 if (!scan_state) {
668728 return false ;
669729 }
670730
671- // Check if we can use an index scan, and already retrieve the matching row ids.
672- if (!art_to_scan.Scan (*scan_state, max_count, row_ids)) {
731+ if (!art_to_scan.CompoundKeyScan (*scan_state, max_count, row_ids)) {
673732 row_ids.clear ();
674733 return false ;
675734 }
676735 }
677736 }
737+ // Original single column index scan
738+ else {
739+ for (const auto &filter_expr : index_filters[0 ]) {
740+ for (auto &art_ref : arts_to_scan) {
741+ auto &art_to_scan = art_ref.get ();
742+ auto scan_state = art_to_scan.TryInitializeScan (*index_exprs[0 ], *filter_expr);
743+ if (!scan_state) {
744+ return false ;
745+ }
746+
747+ // Check if we can use an index scan, and already retrieve the matching row ids.
748+ if (!art_to_scan.Scan (*scan_state, max_count, row_ids)) {
749+ row_ids.clear ();
750+ return false ;
751+ }
752+ }
753+ }
754+ }
755+
678756 return true ;
679757}
680758
@@ -697,9 +775,9 @@ unique_ptr<GlobalTableFunctionState> TableScanInitGlobal(ClientContext &context,
697775 // 1.2. Find + scan one ART for b = 24.
698776 // 1.3. Return the intersecting row IDs.
699777 // 2. (Reorder and) scan a single ART with a compound key of (a, b).
700- if (filter_set.filters .size () != 1 ) {
701- return DuckTableScanInitGlobal (context, input, storage, bind_data);
702- }
778+ // if (filter_set.filters.size() != 1) {
779+ // return DuckTableScanInitGlobal(context, input, storage, bind_data);
780+ // }
703781
704782 auto &info = storage.GetDataTableInfo ();
705783 auto &indexes = info->GetIndexes ();
0 commit comments