| # 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. | |
| """Java-compatible user sequence ordering across Python and Native files.""" | |
| from contextlib import ExitStack | |
| from decimal import Decimal | |
| import datetime | |
| from unittest.mock import patch | |
| import pyarrow as pa | |
| import pytest | |
| from pypaimon import CatalogFactory, Schema | |
| from pypaimon.common.predicate_builder import PredicateBuilder | |
| from pypaimon.write.write_dispatch import NativeTableWrite | |
| pytestmark = pytest.mark.native_plan | |
| def _table(tmp_path, sequence_type, native, order, engine): | |
| catalog = CatalogFactory.create({'warehouse': str(tmp_path)}) | |
| catalog.create_database('default', True) | |
| schema = pa.schema([('id', pa.int32()), ('seq', sequence_type), | |
| ('seq2', pa.int32()), ('value', pa.string())]) | |
| options = {'bucket': '1', 'sequence.field': 'seq,seq2', | |
| 'sequence.field.sort-order': order, 'merge-engine': engine, | |
| 'write.native.enabled': str(native).lower()} | |
| if engine == 'aggregation': | |
| options['fields.value.aggregate-function'] = 'last_value' | |
| catalog.create_table('default.t', Schema.from_pyarrow_schema( | |
| schema, primary_keys=['id'], options=options), False) | |
| return catalog.get_table('default.t'), schema | |
| def _write(table, groups, native, streaming): | |
| builder = table.new_stream_write_builder() if streaming else table.new_batch_write_builder() | |
| commit = builder.new_commit() | |
| try: | |
| for identifier, group in enumerate(groups, 1): | |
| writer = builder.new_write() | |
| if native: | |
| assert isinstance(writer, NativeTableWrite) | |
| try: | |
| for chunk in group: | |
| writer.write_arrow(chunk) | |
| messages = (writer.prepare_commit(identifier) if streaming | |
| else writer.prepare_commit()) | |
| if streaming: | |
| commit.commit(messages, identifier) | |
| else: | |
| commit.commit(messages) | |
| commit.close() | |
| commit = builder.new_commit() | |
| if native: | |
| assert writer._python_writer is None | |
| finally: | |
| writer.close() | |
| finally: | |
| commit.close() | |
| def _read(table, native, predicate=None, projection=None): | |
| copy = table.copy({'read.native.enabled': str(native).lower(), | |
| 'scan.native-plan.enabled': str(native).lower()}) | |
| builder = copy.new_read_builder() | |
| if predicate is not None: | |
| builder.with_filter(predicate) | |
| if projection is not None: | |
| builder.with_projection(projection) | |
| splits = builder.new_scan().plan().splits() | |
| read = builder.new_read() | |
| with ExitStack() as stack: | |
| if native: | |
| stack.enter_context(patch( | |
| 'pypaimon.execution.python.read.table_read.PythonTableRead._create_split_read', | |
| side_effect=AssertionError('Python read fallback') | |
| )) | |
| return read.to_arrow(splits).sort_by('id').to_pylist() | |
| @pytest.mark.parametrize('native', [False, True]) | |
| @pytest.mark.parametrize('streaming', [False, True]) | |
| @pytest.mark.parametrize('separate_commits', [False, True]) | |
| @pytest.mark.parametrize('engine', ['deduplicate', 'partial-update', 'aggregation']) | |
| @pytest.mark.parametrize('sequence_type,high,low', [ | |
| (pa.int64(), 100, 50), (pa.string(), 'z', 'a'), (pa.binary(), b'z', b'a'), | |
| (pa.float32(), 100.5, 50.25), (pa.float64(), 100.5, 50.25), | |
| (pa.decimal128(10, 2), Decimal('100.50'), Decimal('50.25')), | |
| (pa.timestamp('us'), datetime.datetime(2020, 1, 2), datetime.datetime(2020, 1, 1)), | |
| (pa.time32('ms'), datetime.time(12, 0), datetime.time(1, 0)), | |
| ]) | |
| def test_descending_sequence_dispatch( | |
| tmp_path, native, streaming, separate_commits, engine, sequence_type, high, low): | |
| table, schema = _table(tmp_path, sequence_type, native, 'descending', engine) | |
| batches = [] | |
| for i, value in enumerate([high, low, None]): | |
| batches.append(pa.table({'id': [1, 2, 3], 'seq': [value, high, None], | |
| 'seq2': [0, [2, 1, None][i], None], | |
| 'value': [str(i)] * 3}, schema=schema)) | |
| _write(table, [[batch] for batch in batches] if separate_commits else [batches], | |
| native, streaming) | |
| expected = [{'id': 1, 'value': '1'}, {'id': 2, 'value': '1'}, {'id': 3, 'value': '2'}] | |
| for native_read in (False, True): | |
| # A later NULL must not beat non-NULL, the second sequence field breaks | |
| # ties, and automatic sequence still resolves entirely equal tuples. | |
| assert _read(table, native_read, projection=['id', 'value']) == expected | |
| predicate = PredicateBuilder(table.fields).equal('value', '1') | |
| assert _read(table, native_read, predicate, ['id', 'value']) == expected[:2] |