blob: ebf4da3be5c2cfe924a71e8093920443cc8f9adf [file]
# 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