blob: b53ae95a7f3aeb0d78bc624604e55bfb06a6eee1 [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.
import operator
from decimal import Decimal
from typing import List, Optional, Sequence, Tuple, Union
import pyarrow
from pyarrow import dataset as pyarrow_dataset
DecimalLiteral = Union[Decimal, int]
def _decimal_scaled_integer(value: DecimalLiteral, scale: int) -> Tuple[int, int]:
"""Return the floor and remainder of the exact value in column units."""
numerator, denominator = Decimal(value).as_integer_ratio()
if scale >= 0:
numerator *= 10**scale
else:
denominator *= 10**-scale
return divmod(numerator, denominator)
def _decimal_scalar(unscaled: int, field_type: pyarrow.DataType) -> pyarrow.Scalar:
"""Build a scalar at the column's precision and scale."""
digits = tuple(int(digit) for digit in str(abs(unscaled)))
return pyarrow.scalar(
Decimal((int(unscaled < 0), digits, -field_type.scale)), type=field_type
)
def _decimal_comparison_on_field_scale(
field: pyarrow_dataset.Expression,
field_type: pyarrow.DataType,
method: str,
value: Optional[DecimalLiteral],
) -> pyarrow_dataset.Expression:
"""Compare against the column's representable values without rounding."""
# https://github.com/apache/arrow/issues/41011
# TODO: Recheck this workaround after upgrading PyArrow.
if value is None:
return field.is_valid() & field.is_null()
floor, remainder = _decimal_scaled_integer(value, field_type.scale)
ceiling = floor + (remainder != 0)
if method in ('equal', 'notEqual'):
if remainder:
return (
field.is_valid()
if method == 'notEqual'
else field.is_valid() & field.is_null()
)
threshold = floor
elif method in ('lessThan', 'greaterOrEqual'):
threshold = ceiling
else:
threshold = floor
compare = {
'equal': operator.eq,
'notEqual': operator.ne,
'lessThan': operator.lt,
'lessOrEqual': operator.le,
'greaterThan': operator.gt,
'greaterOrEqual': operator.ge,
}[method]
maximum = 10**field_type.precision - 1
if method in ('equal', 'notEqual'):
if abs(threshold) > maximum:
return (
field.is_valid()
if method == 'notEqual'
else field.is_valid() & field.is_null()
)
else:
low, high = compare(-maximum, threshold), compare(maximum, threshold)
if low == high:
return field.is_valid() if low else field.is_valid() & field.is_null()
return compare(field, _decimal_scalar(threshold, field_type))
def decimal_arrow_expression(
field: pyarrow_dataset.Expression,
field_type: pyarrow.DataType,
method: str,
literals: Sequence[Optional[DecimalLiteral]],
) -> pyarrow_dataset.Expression:
"""Compose exact DECIMAL comparisons for one predicate leaf."""
def compare(
comparison: str, value: Optional[DecimalLiteral]
) -> pyarrow_dataset.Expression:
return _decimal_comparison_on_field_scale(field, field_type, comparison, value)
if method in (
'equal',
'notEqual',
'lessThan',
'lessOrEqual',
'greaterThan',
'greaterOrEqual',
):
return compare(method, literals[0])
if method == 'between':
return compare('greaterOrEqual', literals[0]) & compare(
'lessOrEqual', literals[1]
)
if method == 'notBetween':
return compare('lessThan', literals[0]) | compare('greaterThan', literals[1])
if method == 'notIn' and any(value is None for value in literals):
return field.is_valid() & field.is_null()
if method in ('in', 'notIn'):
maximum = 10**field_type.precision - 1
scalars: List[pyarrow.Scalar] = []
for value in literals:
if value is None:
continue
unscaled, remainder = _decimal_scaled_integer(value, field_type.scale)
if not remainder and abs(unscaled) <= maximum:
scalars.append(_decimal_scalar(unscaled, field_type))
if not scalars:
return (
field.is_valid()
if method == 'notIn'
else field.is_valid() & field.is_null()
)
matches = field.isin(scalars)
return (~matches if method == 'notIn' else matches) & field.is_valid()
raise ValueError(f'Unsupported decimal predicate method: {method}')