blob: c3e7446fee7d9ed19334c0f7a386ed30907ea208 [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.
"""Historical read views retain their schema while writes publish at latest."""
from contextlib import ExitStack
from unittest.mock import patch
import pyarrow as pa
import pytest
from pypaimon import Schema
from pypaimon.execution.native.runtime import (native_runtime_available)
from pypaimon.schema.data_types import AtomicType
from pypaimon.schema.schema_change import SchemaChange
from pypaimon.tests import native_plan_rest_test
from pypaimon.write.write_dispatch import NativeTableWrite
pytestmark = [pytest.mark.python_write, pytest.mark.python_commit]
rest_catalog = native_plan_rest_test.rest_catalog
_ENGINES = [False, pytest.param(True, marks=[pytest.mark.native_plan, pytest.mark.skipif(
not native_runtime_available(), reason='pypaimon-rust not installed')])]
_SCHEMA = pa.schema([('id', pa.int32()), ('pt', pa.int32()), ('value', pa.int32())])
_LATEST_SCHEMA = _SCHEMA.append(pa.field('extra', pa.int32()))
def _commit_guard(commit, native):
stack = ExitStack()
if native:
for method in ('commit', 'overwrite', 'truncate_table', 'drop_partitions'):
stack.enter_context(patch('pypaimon.execution.python.write.file_store_commit.'
'FileStoreCommit.' + method,
side_effect=AssertionError('Python commit fallback')))
return stack
def _write(table, rows, native=False, overwrite=False):
builder = table.new_batch_write_builder()
if overwrite:
builder = builder.overwrite({'pt': '1'})
writer = builder.new_write()
try:
assert isinstance(writer, NativeTableWrite) == native
schema = _LATEST_SCHEMA if 'extra' in table.field_names else _SCHEMA
writer.write_arrow(pa.Table.from_pylist(rows, schema=schema))
return writer.prepare_commit()
finally:
writer.close()
def _commit(table, messages, native=False, overwrite=False):
builder = table.new_batch_write_builder()
if overwrite:
builder = builder.overwrite({'pt': '1'})
commit = builder.new_commit()
try:
with _commit_guard(commit, native):
commit.commit(messages)
finally:
commit.close()
def _rows(table):
builder = table.new_read_builder()
return builder.new_read().to_arrow(builder.new_scan().plan().splits()).sort_by('id').to_pylist()
def _evolved(catalog, mode, native, view='snapshot'):
options = {key: 'false' for key in (
'write.native.enabled', 'commit.native.enabled', 'read.native.enabled', 'scan.native-plan.enabled')}
if mode == 'de':
options.update({'data-evolution.enabled': 'true', 'row-tracking.enabled': 'true'})
if mode == 'pk':
options['bucket'] = '1'
catalog.create_table('default.t', Schema.from_pyarrow_schema(
_SCHEMA, partition_keys=['pt'], primary_keys=['id', 'pt'] if mode == 'pk' else [],
options=options), False)
original = catalog.get_table('default.t')
_commit(original, _write(original, [dict(id=1, pt=1, value=10)]))
catalog.alter_table('default.t', [SchemaChange.add_column('extra', AtomicType('INT'))])
latest = catalog.get_table('default.t')
data = [dict(id=2, pt=1, value=20, extra=200), dict(id=4, pt=2, value=40, extra=400)]
if mode == 'pk':
data.append(dict(id=1, pt=1, value=25, extra=250))
_commit(latest, _write(latest, data))
historical = latest.copy({'scan.snapshot-id': '1'})
if view == 'empty':
historical = historical._copy_with_snapshot(None)
options = {'write.native.enabled': str(native).lower(), 'commit.native.enabled': str(native).lower()}
return latest.copy(options), historical.copy(options)
@pytest.mark.parametrize('native', _ENGINES, ids=['python', 'native'])
@pytest.mark.parametrize('mode', ['append', 'de', 'pk'])
@pytest.mark.parametrize('operation', ['append', 'commit', 'overwrite', 'truncate', 'partitions'])
def test_historical_write_and_commit_use_latest_state(rest_catalog, native, mode, operation):
catalog, _ = rest_catalog
latest, historical = _evolved(catalog, mode, native)
expected = _rows(latest)
assert _rows(historical) == [dict(id=1, pt=1, value=10)]
if operation in ('truncate', 'partitions'):
commit = historical.new_batch_write_builder().new_commit()
try:
with _commit_guard(commit, native):
if operation == 'truncate':
commit.truncate_table()
expected = []
else:
commit.truncate_partitions([{'pt': '1'}])
expected = [row for row in expected if row['pt'] != 1]
finally:
commit.close()
else:
current_input = operation == 'commit'
row = dict(id=3, pt=1, value=30)
if current_input:
row['extra'] = 300
messages = _write(latest if current_input else historical, [row], native, operation == 'overwrite')
assert all(file.schema_id == int(current_input) for message in messages for file in message.new_files)
_commit(historical, messages, native, operation == 'overwrite')
if operation == 'overwrite':
expected = [row for row in expected if row['pt'] != 1]
expected.append(dict(row, extra=300 if current_input else None))
expected.sort(key=lambda row: row['id'])
assert _rows(latest) == expected
snapshot = latest.snapshot_manager().get_latest_snapshot()
assert snapshot.id == 3
assert snapshot.schema_id == 1
assert _rows(historical) == [dict(id=1, pt=1, value=10)]
@pytest.mark.parametrize('native', _ENGINES, ids=['python', 'native'])
@pytest.mark.parametrize('mode', ['append', 'de', 'pk'])
def test_captured_empty_write_does_not_change_its_read_view(rest_catalog, native, mode):
catalog, _ = rest_catalog
latest, empty = _evolved(catalog, mode, native, 'empty')
assert _rows(empty) == []
before = _rows(latest)
messages = _write(empty, [dict(id=3, pt=1, value=30)], native)
_commit(empty, messages, native)
assert _rows(empty) == []
assert _rows(latest) == sorted(before + [dict(id=3, pt=1, value=30, extra=None)], key=lambda row: row['id'])
@pytest.mark.parametrize('native', _ENGINES, ids=['python', 'native'])
def test_historical_primary_key_write_restores_current_sequences(rest_catalog, native):
catalog, _ = rest_catalog
latest, historical = _evolved(catalog, 'pk', native)
messages = _write(historical, [dict(id=1, pt=1, value=99)], native)
_commit(historical, messages, native)
assert _rows(latest)[0] == dict(id=1, pt=1, value=99, extra=None)
assert _rows(historical) == [dict(id=1, pt=1, value=10)]
@pytest.mark.parametrize('native', _ENGINES, ids=['python', 'native'])
def test_historical_branch_publication_does_not_change_main(rest_catalog, native):
catalog, _ = rest_catalog
main, _ = _evolved(catalog, 'de', False)
catalog.create_tag(main.identifier, 'base', 2)
catalog.create_branch(main.identifier, 'dev', tag_name='base')
branch = catalog.get_table('default.t$branch_dev').copy({
'write.native.enabled': str(native).lower(), 'commit.native.enabled': str(native).lower()})
historical = branch.copy({'scan.snapshot-id': '2'})
before = _rows(main)
_commit(branch, _write(branch, [dict(id=5, pt=1, value=50, extra=500)], native), native)
_commit(historical, _write(historical, [dict(id=6, pt=1, value=60, extra=600)], native), native)
assert _rows(main) == before
assert len(_rows(branch)) == 5
commit = historical.new_batch_write_builder().new_commit()
try:
with _commit_guard(commit, native):
commit.truncate_table()
finally:
commit.close()
assert _rows(branch) == []
assert _rows(main) == before