blob: f369abbe1714f097b5999588becac930436feb88 [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.
"""Read Java composite BTree keys with left-prefix predicate bounds."""
from dataclasses import dataclass
import datetime
from decimal import Decimal
import numbers
import struct
from pypaimon.execution.python.globalindex.btree.btree_index_reader import BTreeIndexReader
from pypaimon.execution.python.globalindex.btree.btree_posting_list import add_row_ids
from pypaimon.execution.python.globalindex.btree.composite_key import (
CompositeKeySerializer, compare_nullable,
)
from pypaimon.globalindex.global_index_result import GlobalIndexResult
from pypaimon.utils.roaring_bitmap import RoaringBitmap64
MAX_INTERVALS = 256
_POINT = frozenset(('equal', 'in', 'isNull'))
_RANGE = frozenset(('greaterThan', 'greaterOrEqual', 'lessThan',
'lessOrEqual', 'between', 'isNotNull'))
@dataclass(frozen=True)
class Bound:
values: tuple
after: bool
@dataclass
class CompositePlan:
intervals: list
bound_columns: int
equal_columns: int
field_count: int
covered_predicates: tuple
@property
def point_lookup(self):
return self.equal_columns == self.field_count
def _compare_values(left, right, comparators):
for index, (a, b) in enumerate(zip(left, right)):
result = compare_nullable(a, b, comparators[index])
if result:
return result
return 0
def compare_bounds(left, right, comparators):
difference = _compare_values(left.values, right.values, comparators)
if difference:
return difference
if len(left.values) != len(right.values):
if len(left.values) < len(right.values):
return 1 if left.after else -1
return -1 if right.after else 1
return (left.after > right.after) - (left.after < right.after)
def compare_key_bound(key, bound, comparators):
difference = _compare_values(key, bound.values, comparators)
return difference if difference else (-1 if bound.after else 1)
def _conjuncts(predicate):
if predicate.method != 'and':
return [predicate]
result = []
for child in predicate.literals:
result.extend(_conjuncts(child))
return result
def _as_leaf(predicate):
if predicate.method != 'or':
return predicate
children = predicate.literals or []
if not children or any(child.method != 'equal' or child.field != children[0].field
for child in children):
return None
from pypaimon.common.predicate import Predicate
return Predicate('in', children[0].index, children[0].field,
[child.literals[0] for child in children])
def _matches(value, predicate, comparator):
method = predicate.method
values = predicate.literals or []
if method == 'isNull':
return value is None
if method == 'isNotNull':
return value is not None
if value is None:
return False
if method == 'in':
return any(literal is not None and comparator(value, literal) == 0
for literal in values)
if any(literal is None for literal in values):
return False
if method == 'equal':
return comparator(value, values[0]) == 0
if method == 'between':
return (comparator(value, values[0]) >= 0
and comparator(value, values[1]) <= 0)
difference = comparator(value, values[0])
return {'greaterThan': difference > 0,
'greaterOrEqual': difference >= 0,
'lessThan': difference < 0,
'lessOrEqual': difference <= 0}[method]
def _literal_supported(type_name, literal):
if type_name == 'BOOLEAN':
return isinstance(literal, bool)
if type_name in ('TINYINT', 'SMALLINT', 'INT', 'INTEGER', 'BIGINT'):
return isinstance(literal, numbers.Integral) and not isinstance(literal, bool)
if type_name.startswith(('CHAR', 'VARCHAR', 'STRING')):
return isinstance(literal, str)
if type_name in ('FLOAT', 'DOUBLE'):
# Java orders signed zeros separately, while Python row filtering does not.
return (isinstance(literal, numbers.Real)
and not isinstance(literal, bool) and literal != 0)
if type_name.startswith(('DECIMAL', 'NUMERIC')):
return isinstance(literal, (Decimal, numbers.Integral)) and not isinstance(literal, bool)
if type_name == 'DATE':
return ((isinstance(literal, datetime.date)
and not isinstance(literal, datetime.datetime))
or isinstance(literal, numbers.Integral)
and not isinstance(literal, bool))
if type_name.startswith('TIME') and not type_name.startswith('TIMESTAMP'):
return (isinstance(literal, datetime.time)
and literal.microsecond % 1000 == 0
or isinstance(literal, numbers.Integral)
and not isinstance(literal, bool))
if type_name.startswith('TIMESTAMP'):
return (isinstance(literal, datetime.datetime)
and not getattr(literal, 'nanosecond', 0))
return False
def plan_composite(fields, predicate):
"""Return None when the index cannot safely bound this predicate."""
try:
serializer = CompositeKeySerializer(fields)
except ValueError:
return None
comparators = serializer._comparators
conjuncts = _conjuncts(predicate)
prefixes = [()]
equal_columns = 0
range_predicates = []
covered_predicates = []
for field, comparator, scalar in zip(
fields, comparators, serializer._serializers):
column = []
sources = []
for child in conjuncts:
leaf = _as_leaf(child)
if leaf is not None and leaf.field == field.name and (
leaf.method in _POINT or leaf.method in _RANGE):
column.append(leaf)
sources.append(child)
if not column:
break
for leaf in column:
for literal in leaf.literals or []:
if literal is None:
continue
if not _literal_supported(field.type.type.upper(), literal):
return None
try:
converted = scalar.deserialize(scalar.serialize(literal))
if comparator(converted, literal) != 0:
return None
except (ArithmeticError, ValueError, TypeError, struct.error):
return None
point = [leaf for leaf in column if leaf.method in _POINT]
if not point:
range_predicates = column
covered_predicates.extend(sources)
break
selected = min(point, key=lambda leaf: len(leaf.literals or [None]))
candidates = ([None] if selected.method == 'isNull'
else selected.literals or [])
values = []
for value in candidates:
if value is None and (selected.method != 'isNull' or not field.type.nullable):
continue
if all(_matches(value, leaf, comparator) for leaf in column):
if not any(compare_nullable(value, old, comparator) == 0 for old in values):
values.append(value)
equal_columns += 1
covered_predicates.extend(sources)
if not values:
return CompositePlan([], equal_columns, equal_columns, len(fields),
tuple(covered_predicates))
if len(prefixes) * len(values) > MAX_INTERVALS:
return None
prefixes = [prefix + (value,) for prefix in prefixes for value in values]
bound_columns = equal_columns + bool(range_predicates)
if not bound_columns:
return None
intervals = []
for prefix in prefixes:
lower, upper = Bound(prefix, False), Bound(prefix, True)
if range_predicates:
lower = Bound(prefix + (None,), True)
empty = False
for leaf in range_predicates:
if leaf.method == 'isNotNull':
continue
if None in (leaf.literals or []):
empty = True
break
if leaf.method in ('greaterThan', 'greaterOrEqual', 'between'):
candidate = Bound(prefix + (leaf.literals[0],),
leaf.method == 'greaterThan')
if compare_bounds(candidate, lower, comparators) > 0:
lower = candidate
if leaf.method in ('lessThan', 'lessOrEqual', 'between'):
candidate = Bound(prefix + (leaf.literals[-1],),
leaf.method != 'lessThan')
if compare_bounds(candidate, upper, comparators) < 0:
upper = candidate
if empty:
continue
if compare_bounds(lower, upper, comparators) < 0:
intervals.append((lower, upper))
return CompositePlan(intervals, bound_columns, equal_columns, len(fields),
tuple(covered_predicates))
class CompositeBTreeIndexReader(BTreeIndexReader):
def query(self, plan):
result = RoaringBitmap64()
if self.min_key is None:
return GlobalIndexResult.create(result, is_exact=False)
for lower, upper in plan.intervals:
if (compare_key_bound(self.max_key, lower, self.key_serializer._comparators) <= 0
or compare_key_bound(self.min_key, upper,
self.key_serializer._comparators) >= 0):
continue
if plan.point_lookup:
posting = self.reader.lookup(
self.key_serializer.serialize(lower.values))
if posting is not None:
add_row_ids(posting, self.footer.version, result)
continue
seek = lower.values + (None,) * (plan.field_count - len(lower.values))
for entry in self._entries_from(self.key_serializer.serialize(seek)):
key = self.key_serializer.deserialize(entry.key)
if compare_key_bound(key, upper, self.key_serializer._comparators) >= 0:
break
if compare_key_bound(key, lower, self.key_serializer._comparators) > 0:
add_row_ids(entry.value, self.footer.version, result)
return GlobalIndexResult.create(result, is_exact=False)