blob: 02cc1e63d8cf94b52baa504d17d49fd761daf57f [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.
"""Row-tracking metadata and filters use Java's data-file versions."""
from contextlib import ExitStack
from unittest.mock import patch
import pyarrow as pa
import pytest
from pypaimon import CatalogFactory, Schema
pytestmark = pytest.mark.native_plan
_SCHEMA = pa.schema([('id', pa.int32()), ('v', pa.int32()), ('w', pa.int32())])
def _create_table(tmp_path, schema, options):
catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
catalog.create_database('default', True)
catalog.create_table('default.t', Schema.from_pyarrow_schema(schema, options=options), False)
return catalog.get_table('default.t')
def _table(tmp_path, evolution, native_write=True, deletion_vectors=False):
options = {'row-tracking.enabled': 'true', 'data-evolution.enabled': str(evolution).lower(),
'write.native.enabled': str(native_write).lower(),
'deletion-vectors.enabled': str(deletion_vectors).lower()}
table = _create_table(tmp_path, _SCHEMA, options)
for start in (100, 200):
builder = table.new_batch_write_builder()
writer, commit = builder.new_write(), builder.new_commit()
try:
writer.write_arrow(pa.table({'id': [start, start + 1], 'v': [10, 20],
'w': [100, 200]}, schema=_SCHEMA))
commit.commit(writer.prepare_commit())
finally:
writer.close()
commit.close()
return table
def _update(table, field, row_id, value):
builder = table.new_batch_write_builder()
messages = builder.new_update().update_by_arrow_with_row_id(pa.table({
'_ROW_ID': pa.array([row_id], pa.int64()), field: pa.array([value], pa.int32())}))
commit = builder.new_commit()
try:
commit.commit(messages)
finally:
commit.close()
def _read(table, native, projection, predicate=None, limit=None):
copy = table.copy({'read.native.enabled': str(native).lower(),
'scan.native-plan.enabled': str(native).lower()})
builder = copy.new_read_builder().with_projection(projection)
if predicate is not None:
builder.with_filter(predicate)
if limit is not None:
builder.with_limit(limit)
reader = 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 reader.to_arrow(builder.new_scan().plan().splits(), parallelism=1)
@pytest.mark.parametrize('native', [False, True])
@pytest.mark.parametrize('native_write', [False, True])
@pytest.mark.parametrize('deletion_vectors', [False, True])
def test_partial_column_updates_keep_latest_metadata_provider(tmp_path, native, native_write, deletion_vectors):
table = _table(tmp_path, True, native_write, deletion_vectors)
_update(table, 'v', 0, 77)
_update(table, 'w', 1, 88)
for projection in (['_SEQUENCE_NUMBER'], ['id', '_SEQUENCE_NUMBER'],
['_SEQUENCE_NUMBER', 'w', '_ROW_ID', 'v', 'id']):
result = _read(table, native, projection)
assert result.column_names == projection
assert result['_SEQUENCE_NUMBER'].null_count == 0
if 'id' in projection:
result = result.sort_by('id')
assert result['id'].to_pylist() == [100, 101, 200, 201]
assert result['_SEQUENCE_NUMBER'].to_pylist() == [4, 4, 2, 2]
if 'v' in projection:
assert result['v'].to_pylist() == [77, 20, 10, 20]
assert result['w'].to_pylist() == [100, 88, 100, 200]
assert result['_ROW_ID'].to_pylist() == [0, 1, 2, 3]
else:
assert sorted(result['_SEQUENCE_NUMBER'].to_pylist()) == [2, 2, 4, 4]
@pytest.mark.parametrize('native', [False, True])
@pytest.mark.parametrize('evolution', [False, True])
def test_metadata_predicates_with_unprojected_columns(tmp_path, native, evolution):
table = _table(tmp_path, evolution)
if evolution:
_update(table, 'v', 0, 77)
latest = 3 if evolution else 1
pb = table.new_read_builder().with_projection(['id', 'v', '_ROW_ID', '_SEQUENCE_NUMBER']).new_predicate_builder()
cases = [
(pb.equal('_SEQUENCE_NUMBER', latest), [100, 101]),
(pb.is_null('_SEQUENCE_NUMBER'), []),
(pb.is_not_null('_SEQUENCE_NUMBER'), [100, 101, 200, 201]),
(pb.and_predicates([pb.equal('_SEQUENCE_NUMBER', latest), pb.equal('id', 101)]), [101]),
(pb.or_predicates([pb.equal('_SEQUENCE_NUMBER', latest), pb.equal('id', 200)]), [100, 101, 200]),
(pb.or_predicates([pb.equal('_SEQUENCE_NUMBER', 2), pb.equal('_ROW_ID', 0)]), [100, 200, 201]),
]
for predicate, expected in cases:
result = _read(table, native, ['id'], predicate).sort_by('id')
assert result.column_names == ['id']
assert result['id'].to_pylist() == expected
result = _read(table, native, ['id'], pb.equal('_SEQUENCE_NUMBER', latest), limit=1)
assert result.num_rows == 1
assert result['id'][0].as_py() in [100, 101]
@pytest.mark.parametrize('native', [False, True])
def test_physical_versions_override_manifest_only_when_not_null(tmp_path, native):
from dataclasses import replace
from pathlib import Path
import pyarrow.parquet as pq
table = _create_table(tmp_path, _SCHEMA, {
'row-tracking.enabled': 'true', 'data-evolution.enabled': 'true',
'write.native.enabled': 'true'})
builder = table.new_batch_write_builder()
writer, commit = builder.new_write(), builder.new_commit()
try:
data = pa.table({'id': [100, 101], 'v': [10, 20], 'w': [100, 200]}, schema=_SCHEMA)
writer.write_arrow(data)
messages = writer.prepare_commit()
file = messages[0].new_files[0]
data = data.append_column('_SEQUENCE_NUMBER', pa.array([7, None], pa.int64()))
pq.write_table(data, file.file_path)
messages[0].new_files[0] = replace(file, file_size=Path(file.file_path).stat().st_size)
commit.commit(messages)
finally:
writer.close()
commit.close()
result = _read(table, native, ['id', '_SEQUENCE_NUMBER']).sort_by('id')
assert result['_SEQUENCE_NUMBER'].to_pylist() == [7, 1]
pb = table.new_read_builder().with_projection(['id', '_SEQUENCE_NUMBER']).new_predicate_builder()
assert _read(table, native, ['id'], pb.equal('_SEQUENCE_NUMBER', 7))['id'].to_pylist() == [100]
assert _read(table, native, ['id'], pb.equal('_SEQUENCE_NUMBER', 1))['id'].to_pylist() == [101]
@pytest.mark.parametrize('native', [False, True])
def test_case_sensitive_user_column_does_not_shadow_metadata(tmp_path, native):
schema = pa.schema([('id', pa.int32()), ('_sequence_number', pa.int32())])
table = _create_table(tmp_path, schema, {
'row-tracking.enabled': 'true', 'data-evolution.enabled': 'true',
'write.native.enabled': 'true'})
builder = table.new_batch_write_builder()
writer, commit = builder.new_write(), builder.new_commit()
try:
writer.write_arrow(pa.table({'id': [100, 101], '_sequence_number': [40, 50]}, schema=schema))
commit.commit(writer.prepare_commit())
finally:
writer.close()
commit.close()
projection = ['id', '_sequence_number', '_SEQUENCE_NUMBER']
pb = table.new_read_builder().with_projection(projection).new_predicate_builder()
predicate = pb.and_predicates([pb.equal('_SEQUENCE_NUMBER', 1), pb.equal('_sequence_number', 50)])
assert _read(table, native, projection, predicate).to_pydict() == {
'id': [101], '_sequence_number': [50], '_SEQUENCE_NUMBER': [1]}
@pytest.mark.parametrize('user_field,metadata', [
('_sequence_number', '_SEQUENCE_NUMBER'), ('_row_id', '_ROW_ID')])
def test_insensitive_metadata_name_collision_is_ambiguous(tmp_path, user_field, metadata):
from pypaimon.execution.native.write.native_commit import create_native_write_table
table = _create_table(tmp_path, pa.schema([(user_field, pa.int32())]), {
'row-tracking.enabled': 'true', 'write.native.enabled': 'true'})
reader = create_native_write_table(table).new_read_builder().with_case_sensitive(False)
for name in (metadata, user_field):
with pytest.raises(ValueError, match='Ambiguous'):
reader.with_projection([name]).new_read().read_arrow([])
with pytest.raises(ValueError, match='Ambiguous'):
reader.with_filter({'method': 'equal', 'field': name, 'literals': [1]})