| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| from typing import Dict, List, Optional, Set |
| |
| from pypaimon.common.predicate import Predicate |
| from pypaimon.common.predicate_builder import PredicateBuilder |
| from pypaimon.schema.data_types import DataField |
| |
| _UNSAFE_ARROW_FILTER_METHODS = frozenset([ |
| 'startsWith', |
| 'endsWith', |
| 'contains', |
| 'like', |
| ]) |
| |
| |
| def extract_partition_spec_from_predicate( |
| predicate: Predicate, partition_keys: List[str] |
| ) -> Optional[Dict[str, str]]: |
| if not predicate or not partition_keys: |
| return None |
| parts = _split_and(predicate) |
| spec: Dict[str, str] = {} |
| for p in parts: |
| if p.method != "equal" or p.field is None or p.literals is None or len(p.literals) != 1: |
| continue |
| if p.field in partition_keys: |
| spec[p.field] = str(p.literals[0]) |
| if set(spec.keys()) == set(partition_keys): |
| return spec |
| return None |
| |
| |
| def trim_and_transform_predicate(input_predicate: Predicate, all_fields: List[str], trimmed_keys: List[str]): |
| new_predicate = trim_predicate_by_fields(input_predicate, trimmed_keys) |
| part_to_index = {element: idx for idx, element in enumerate(trimmed_keys)} |
| mapping: Dict[int, int] = { |
| i: part_to_index.get(all_fields[i], -1) |
| for i in range(len(all_fields)) |
| } |
| return _change_index(new_predicate, mapping) |
| |
| |
| def trim_predicate_by_fields(input_predicate: Predicate, trimmed_keys: List[str]): |
| if not input_predicate or not trimmed_keys: |
| return None |
| |
| predicates: list[Predicate] = _split_and(input_predicate) |
| predicates = [element for element in predicates if _get_all_fields(element).issubset(trimmed_keys)] |
| return PredicateBuilder.and_predicates(predicates) |
| |
| |
| def _split_and(input_predicate: Predicate): |
| if not input_predicate: |
| return list() |
| |
| if input_predicate.method == 'and': |
| return [p for element in (input_predicate.literals or []) for p in _split_and(element)] |
| |
| return [input_predicate] |
| |
| |
| def rewrite_predicate_indices( |
| input_predicate: Optional[Predicate], |
| read_fields: List[DataField], |
| ) -> Optional[Predicate]: |
| """Rewrite predicate leaf indices to match positions in ``read_fields``. |
| |
| Predicate leaves are built against the original table schema (via |
| PredicateBuilder), so their ``index`` field encodes that schema's column |
| order. When the same predicate is later evaluated row-by-row against a |
| projected scan (read_type narrower or reordered), those indices no longer |
| match the OffsetRow layout the reader hands to FilterRecordReader, and |
| ``OffsetRow.get_field(idx)`` raises IndexError. |
| |
| Returns a new predicate where every leaf's ``index`` is rebound to its |
| column's position in ``read_fields``. The caller is responsible for |
| ensuring that every leaf field is present in ``read_fields``. |
| """ |
| if input_predicate is None: |
| return None |
| name_to_pos = {f.name: i for i, f in enumerate(read_fields)} |
| return _rewrite_by_name(input_predicate, name_to_pos) |
| |
| |
| def _rewrite_by_name(p: Predicate, name_to_pos: Dict[str, int]) -> Predicate: |
| if p.method == 'and' or p.method == 'or': |
| return p.new_literals( |
| [_rewrite_by_name(c, name_to_pos) for c in (p.literals or [])] |
| ) |
| if p.field is None or p.field not in name_to_pos: |
| raise ValueError( |
| "Cannot rewrite predicate index for leaf {!r}: field {!r} is not " |
| "in read fields {}. The caller must ensure all referenced columns " |
| "are projected.".format(p, p.field, list(name_to_pos)) |
| ) |
| return p.new_index(name_to_pos[p.field]) |
| |
| |
| def _change_index(input_predicate: Predicate, mapping: Dict[int, int]): |
| if not input_predicate: |
| return None |
| |
| if input_predicate.method == 'and' or input_predicate.method == 'or': |
| predicates: list[Predicate] = input_predicate.literals |
| new_predicates = [_change_index(element, mapping) for element in predicates] |
| return input_predicate.new_literals(new_predicates) |
| |
| return input_predicate.new_index(mapping[input_predicate.index]) |
| |
| |
| def predicate_field_names(predicate: Predicate) -> Set[str]: |
| """Return all column names referenced by predicate leaves.""" |
| if predicate.field is not None: |
| return {predicate.field} |
| involved_fields = set() |
| if predicate.literals: |
| for sub_predicate in predicate.literals: |
| involved_fields.update(predicate_field_names(sub_predicate)) |
| return involved_fields |
| |
| |
| def _get_all_fields(predicate: Predicate) -> Set[str]: |
| return predicate_field_names(predicate) |
| |
| |
| def predicate_supports_arrow_filter(predicate: Optional[Predicate]) -> bool: |
| """Whether ``predicate.to_arrow()`` is safe for batch filtering. |
| |
| PyArrow 6 accepts dataset expressions for comparisons, null checks, and |
| isin, but string match compute functions do not accept dataset expressions. |
| Predicate.to_arrow() currently falls back to a truthy expression or None for |
| those methods, which is safe for file pruning but not for final row filters. |
| """ |
| if predicate is None: |
| return True |
| if predicate.method == 'and' or predicate.method == 'or': |
| return all( |
| predicate_supports_arrow_filter(p) |
| for p in (predicate.literals or []) |
| ) |
| return predicate.method not in _UNSAFE_ARROW_FILTER_METHODS |
| |
| |
| def remove_row_id_filter(predicate: Predicate) -> Optional[Predicate]: |
| from pypaimon.table.special_fields import SpecialFields |
| |
| if not predicate: |
| return None |
| if predicate.field == SpecialFields.ROW_ID.name: |
| return None |
| if predicate.method == "and": |
| parts = _split_and(predicate) |
| non_row_id = [ |
| p for p in parts |
| if _get_all_fields(p) != {SpecialFields.ROW_ID.name} |
| ] |
| if not non_row_id: |
| return None |
| filtered = [] |
| for p in non_row_id: |
| r = remove_row_id_filter(p) |
| if r is None: |
| return None |
| filtered.append(r) |
| return PredicateBuilder.and_predicates(filtered) |
| if predicate.method == "or": |
| fields = _get_all_fields(predicate) |
| if ( |
| SpecialFields.ROW_ID.name in fields |
| and fields != {SpecialFields.ROW_ID.name} |
| ): |
| return predicate |
| new_children = [] |
| for c in predicate.literals or []: |
| r = remove_row_id_filter(c) |
| if r is not None: |
| new_children.append(r) |
| if not new_children: |
| return None |
| if len(new_children) == 1: |
| return new_children[0] |
| return PredicateBuilder.or_predicates(new_children) |
| return predicate |
| |
| |
| def exclude_predicate_with_fields(predicate: Optional[Predicate], fields: Set[str]) -> Optional[Predicate]: |
| """Drop predicate parts referencing any of ``fields`` (mirrors Java |
| PredicateBuilder.excludePredicateWithFields).""" |
| if not predicate or not fields: |
| return predicate |
| if predicate.method == "and": |
| kept = [] |
| for p in _split_and(predicate): |
| r = exclude_predicate_with_fields(p, fields) |
| if r is not None: |
| kept.append(r) |
| return PredicateBuilder.and_predicates(kept) if kept else None |
| # leaf or OR: drop the whole thing if it touches any field (OR isn't split apart) |
| if _get_all_fields(predicate) & fields: |
| return None |
| return predicate |