blob: 8527093e943795c929bb03bfd0bd528075dd128c [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.
"""Pending files carry source events, not merged primary-key rows."""
import pyarrow as pa
import pyarrow.parquet as pq
import pytest
from pypaimon import CatalogFactory, Schema
def make_table(tmp_path, options=None, partition_keys=None):
catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
catalog.create_database('default', True)
settings = {'bucket': '-2', 'file.format': 'parquet', 'write.native.enabled': 'false'}
settings.update(options or {})
schema = pa.schema([('id', pa.int32()), ('v', pa.int32()), ('op', pa.string())])
catalog.create_table('default.events', Schema.from_pyarrow_schema(
schema, primary_keys=['id'] + (partition_keys or []),
partition_keys=partition_keys or [], options=settings), False)
return catalog.get_table('default.events'), schema
def stored_rows(table, messages):
rows = []
for message in messages:
for file in message.new_files:
with table.file_io.new_input_stream(file.file_path) as source:
rows.extend(pq.ParquetFile(source).read().to_pylist())
return rows
@pytest.mark.parametrize('engine', ['deduplicate', 'first-row', 'partial-update', 'aggregation'])
def test_postpone_preserves_unsorted_duplicate_rows(tmp_path, engine):
table, schema = make_table(tmp_path, {'merge-engine': engine})
writer = table.new_batch_write_builder().new_write()
try:
writer.write_arrow(pa.table({'id': [3, 1, 3], 'v': [10, 20, 30],
'op': ['+I'] * 3}, schema=schema))
messages = writer.prepare_commit()
assert [(row['id'], row['v']) for row in stored_rows(table, messages)] == [(3, 10), (1, 20), (3, 30)]
finally:
writer.close()
@pytest.mark.parametrize('row_input', [False, True])
@pytest.mark.parametrize(
'options,expected',
[
({}, [0, 1, 2, 3]),
({'ignore-delete': 'true'}, [0, 2]),
({'ignore-update-before': 'true'}, [0, 2, 3]),
({'ignore-delete': 'true', 'ignore-update-before': 'true'}, [0, 2]),
],
)
def test_python_postpone_row_kinds_are_filtered_before_writing(tmp_path, row_input, options, expected):
from pypaimon.table.row.generic_row import GenericRow
table, schema = make_table(tmp_path, {'rowkind.field': 'op', **options})
writer = table.new_batch_write_builder().new_write()
rows = [{'id': i, 'v': i, 'op': kind} for i, kind in enumerate(['+I', '-U', '+U', '-D'])]
try:
if row_input:
for row in rows:
writer.write_row(GenericRow(list(row.values()), table.fields))
else:
writer.write_arrow(pa.Table.from_pylist(rows, schema=schema))
messages = writer.prepare_commit()
physical = stored_rows(table, messages)
assert [row['id'] for row in physical] == expected
assert [row['_VALUE_KIND'] for row in physical] == expected
assert sum(f.delete_row_count for m in messages for f in m.new_files) == sum(i in (1, 3) for i in expected)
finally:
writer.close()
@pytest.mark.parametrize('engine', ['first-row', 'partial-update', 'aggregation'])
def test_python_postpone_rejects_unsupported_retract_and_aborts_rolled_files(tmp_path, engine):
table, schema = make_table(tmp_path, {
'merge-engine': engine, 'rowkind.field': 'op', 'target-file-row-num': '1'})
writer = table.new_stream_write_builder().new_write()
try:
writer.write_arrow(pa.table({'id': [1], 'v': [1], 'op': ['+I']}, schema=schema))
assert list(tmp_path.rglob('*.parquet'))
with pytest.raises((ValueError, NotImplementedError), match='(?i)retract|DELETE|non-INSERT'):
writer.write_arrow(pa.table({'id': [1], 'v': [1], 'op': ['-D']}, schema=schema))
assert not list(tmp_path.rglob('*.parquet'))
with pytest.raises(RuntimeError, match='failed'):
writer.prepare_commit(1)
finally:
writer.close()
def test_python_postpone_checkpoints_do_not_repeat_prepared_files(tmp_path):
table, schema = make_table(tmp_path, {'target-file-row-num': '2', 'rowkind.field': 'op'})
writer = table.new_stream_write_builder().new_write()
try:
for checkpoint in range(3):
writer.write_arrow(pa.table({'id': [3, 1, 3], 'v': [10, 20, 30],
'op': ['+I', '-U', '-D']}, schema=schema))
messages = writer.prepare_commit(checkpoint)
rows = stored_rows(table, messages)
assert [row['id'] for row in rows] == [3, 1, 3]
assert [row['_SEQUENCE_NUMBER'] for row in rows] == list(range(checkpoint * 3, checkpoint * 3 + 3))
assert [file.row_count for message in messages for file in message.new_files] == [3]
assert writer.prepare_commit(checkpoint + 100) == []
paths = [f.file_path for m in messages for f in m.new_files]
writer.abort()
assert all(table.file_io.exists(path) for path in paths)
finally:
writer.close()
def test_python_aggregation_writer_uses_field_aggregators(tmp_path):
table, schema = make_table(tmp_path, {
'bucket': '1', 'merge-engine': 'aggregation', 'fields.v.aggregate-function': 'sum'})
writer = table.new_batch_write_builder().new_write()
try:
writer.write_arrow(pa.table({'id': [3, 1, 3], 'v': [10, 20, 30], 'op': ['+I'] * 3}, schema=schema))
rows = stored_rows(table, writer.prepare_commit())
assert [(row['id'], row['v']) for row in rows] == [(1, 20), (3, 40)]
finally:
writer.close()
def test_python_postpone_prepare_failure_keeps_earlier_partition_owned(tmp_path, monkeypatch):
table, schema = make_table(tmp_path, partition_keys=['v'])
writer = table.new_stream_write_builder().new_write()
writer.write_arrow(pa.table({'id': [1, 2], 'v': [10, 20], 'op': ['+I', '+I']}, schema=schema))
writers = list(writer._execution.file_store_write.data_writers.values())
assert len(writers) == 2
def fail_prepare():
raise OSError('injected prepare failure')
monkeypatch.setattr(writers[1], 'prepare_commit', fail_prepare)
try:
with pytest.raises(OSError, match='injected'):
writer.prepare_commit(1)
assert list(tmp_path.rglob('*.parquet'))
finally:
writer.close()
assert not list(tmp_path.rglob('*.parquet'))