blob: a2495f5f244b994448669e9f097700c4ca8980ca [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 decimal import Decimal, localcontext
import operator
from pathlib import Path
from typing import Callable, List, Optional, Union
import pyarrow as pa
import pyarrow.dataset as ds
import pyarrow.parquet as pq
import pytest
from pypaimon import Schema
from pypaimon.common.predicate import Predicate
from pypaimon.filesystem.local_file_io import LocalFileIO
from pypaimon.execution.python.read.reader.format_pyarrow_reader import (FormatPyArrowReader)
from pypaimon.execution.python.read.reader.filter_record_batch_reader import (FilterRecordBatchReader)
COMPARISONS = [
('equal', operator.eq),
('notEqual', operator.ne),
('lessThan', operator.lt),
('lessOrEqual', operator.le),
('greaterThan', operator.gt),
('greaterOrEqual', operator.ge),
]
@pytest.mark.parametrize('method,compare', COMPARISONS)
@pytest.mark.parametrize(
'bound',
[
Decimal('2'),
Decimal('24.000'),
Decimal('1.001'),
Decimal('-1.001'),
Decimal('1E-41'),
Decimal('1E+100'),
Decimal('-1E+100'),
24,
],
)
def test_decimal_comparison_matches_python(
method: str,
compare: Callable[[Decimal, Union[Decimal, int]], bool],
bound: Union[Decimal, int],
) -> None:
decimal_values: List[Optional[Decimal]] = [
Decimal(value)
for value in (
'-24.00',
'-1.01',
'-1.00',
'0.00',
'1.00',
'1.01',
'2.00',
'24.00',
)
] + [None]
table = pa.table({'value': pa.array(decimal_values, type=pa.decimal128(15, 2))})
predicate = Predicate(method, 0, 'value', [bound])
expected = [
value for value in decimal_values if value is not None and compare(value, bound)
]
# A small context must not round a high precision bound or column value.
with localcontext() as context:
context.prec = 6
expr = predicate.to_arrow(table.schema)
actual = ds.Scanner.from_batches(table.to_reader(), filter=expr).to_table()
assert actual.column('value').to_pylist() == expected
@pytest.mark.parametrize(
'method,literals,expected',
[
('in', [Decimal('1'), Decimal('2.001'), None], [Decimal('1.00')]),
('notIn', [Decimal('1'), None], []),
(
'between',
[Decimal('-1'), Decimal('1.001')],
[Decimal('-1.00'), Decimal('1.00')],
),
('notBetween', [Decimal('-1'), Decimal('1.001')], [Decimal('2.00')]),
(
'in',
[Decimal('1E-41'), Decimal('1E+100'), Decimal('1.00')],
[Decimal('1.00')],
),
(
'notIn',
[Decimal('1E-41'), Decimal('1E+100'), Decimal('1.00')],
[Decimal('-1.00'), Decimal('2.00')],
),
],
)
def test_decimal_set_and_range_filters(
method: str, literals: List[Optional[Decimal]], expected: List[Decimal]
) -> None:
values = [Decimal('-1.00'), Decimal('1.00'), Decimal('2.00'), None]
table = pa.table({'value': pa.array(values, type=pa.decimal128(15, 2))})
predicate = Predicate(method, 0, 'value', literals)
actual = ds.Scanner.from_batches(
table.to_reader(), filter=predicate.to_arrow(table.schema)
).to_table()
assert actual.column('value').to_pylist() == expected
def test_decimal_large_literal_preserves_precision() -> None:
values = [Decimal('9007199254740992.00'), Decimal('9007199254740993.00')]
table = pa.table({'value': pa.array(values, type=pa.decimal128(38, 2))})
predicate = Predicate('equal', 0, 'value', [Decimal('9007199254740993.00')])
actual = ds.Scanner.from_batches(
table.to_reader(), filter=predicate.to_arrow(table.schema)
).to_table()
assert actual.column('value').to_pylist() == [values[1]]
@pytest.mark.parametrize(
'bound,expected',
[
(Decimal('24.000'), [Decimal('1.00'), Decimal('17.00')]),
(Decimal('24.001'), [Decimal('1.00'), Decimal('17.00'), Decimal('24.00')]),
],
)
def test_decimal_bound_preserves_row_group_pruning(
tmp_path: Path, bound: Decimal, expected: List[Decimal]
) -> None:
values = [Decimal('1.00'), Decimal('17.00'), Decimal('24.00'), Decimal('50.00')]
table = pa.table({'value': pa.array(values, type=pa.decimal128(15, 2))})
path = tmp_path / 'decimals.parquet'
pq.write_table(table, path, row_group_size=1)
dataset = ds.dataset(path)
predicate = Predicate('lessThan', 0, 'value', [bound])
expression = predicate.to_arrow(table.schema)
selected_groups = [
group
for fragment in dataset.get_fragments()
for group in fragment.split_by_row_group(filter=expression)
]
assert len(selected_groups) == len(expected)
assert dataset.to_table(filter=expression).column('value').to_pylist() == expected
def test_decimal_batch_filter_after_normalization(tmp_path: Path) -> None:
batch = pa.record_batch(
[
pa.array(
[Decimal('1.00'), Decimal('17.00'), Decimal('24.00'), None],
type=pa.decimal128(15, 2),
)
],
names=['value'],
)
# Exercise the real post-read filter separately from physical file pushdown.
path = tmp_path / 'batch.parquet'
pq.write_table(pa.Table.from_batches([batch]), path)
source = FormatPyArrowReader(
LocalFileIO(),
'parquet',
str(path),
Schema.from_pyarrow_schema(batch.schema).fields,
None,
)
reader = FilterRecordBatchReader(
source, Predicate('lessThan', 0, 'value', [Decimal('24')])
)
try:
first = reader.read_arrow_batch()
assert first is not None
assert first.column(0).to_pylist() == [Decimal('1.00'), Decimal('17.00')]
assert reader.read_arrow_batch() is None
finally:
reader.close()
def test_decimal_composed_predicate_uses_schema() -> None:
values = [Decimal('-1.00'), Decimal('0.00'), Decimal('1.00'), Decimal('2.00')]
table = pa.table({'value': pa.array(values, type=pa.decimal128(15, 2))})
predicate = Predicate(
'or',
None,
None,
[
Predicate(
'and',
None,
None,
[
Predicate('greaterOrEqual', 0, 'value', [Decimal('0')]),
Predicate('lessThan', 0, 'value', [Decimal('1.001')]),
],
),
Predicate('equal', 0, 'value', [Decimal('2')]),
],
)
actual = ds.Scanner.from_batches(
table.to_reader(), filter=predicate.to_arrow(table.schema)
).to_table()
assert actual.column('value').to_pylist() == values[1:]