blob: 263aaf878a120d5e734cd34666015e9f644111e6 [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.
"""Storage and table metadata must not eagerly load Python write engines."""
import subprocess
import sys
from types import ModuleType
from unittest.mock import patch
import pytest
def test_native_table_preparation_does_not_load_python_write_implementations():
source = """
import importlib.abc
import sys
from types import SimpleNamespace
class RejectPythonWriters(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname.startswith('pypaimon.execution.python'):
raise AssertionError('Native preparation loaded a Python writer: ' + fullname)
sys.meta_path.insert(0, RejectPythonWriters())
from pypaimon.catalog.catalog_environment import CatalogEnvironment
from pypaimon.filesystem.local_file_io import LocalFileIO
from pypaimon.filesystem.hdfs_native_file_io import HdfsNativeFileIO
from pypaimon.table.file_store_table import FileStoreTable
from pypaimon.execution.native.table import _resolved_schema_file_io_options
file_io = LocalFileIO()
table = SimpleNamespace(file_io=file_io, catalog_environment=CatalogEnvironment.empty())
assert _resolved_schema_file_io_options(table) == {}
assert FileStoreTable.__module__ == 'pypaimon.table.file_store_table'
"""
result = subprocess.run([sys.executable, '-c', source], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True, timeout=30)
assert result.returncode == 0, result.stdout + result.stderr
@pytest.mark.parametrize('link', ['direct', 'cause', 'context'])
def test_loaded_fork_error_is_propagated_without_loading_native(link):
from pypaimon.write.backend_dispatch import propagate_fork_safety_error
class ForkSafetyError(RuntimeError):
pass
sdk = ModuleType('pypaimon_rust')
sdk.ForkSafetyError = ForkSafetyError
failure = ForkSafetyError('inherited SDK state')
wrapped = failure if link == 'direct' else RuntimeError('prefetch failed')
if link == 'cause':
wrapped.__cause__ = failure
elif link == 'context':
wrapped.__context__ = failure
with patch.dict(sys.modules, {'pypaimon_rust': sdk, 'pypaimon.execution.native.runtime': None}):
with pytest.raises(ForkSafetyError) as caught:
propagate_fork_safety_error(wrapped)
assert caught.value is failure
def test_loaded_fork_error_check_terminates_on_cyclic_exception_chain():
from pypaimon.write.backend_dispatch import propagate_fork_safety_error
sdk = ModuleType('pypaimon_rust')
sdk.ForkSafetyError = type('ForkSafetyError', (RuntimeError,), {})
first, second = ValueError('first'), RuntimeError('second')
first.__cause__, second.__context__ = second, first
with patch.dict(sys.modules, {'pypaimon_rust': sdk, 'pypaimon.execution.native.runtime': None}):
assert propagate_fork_safety_error(first) is None
def test_python_streaming_prefetch_failure_never_loads_native():
source = """
import importlib.abc
import sys
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
class BlockNative(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname.startswith(('pypaimon.execution.native', 'pypaimon_rust')):
raise AssertionError('Python prefetch loaded Native: ' + fullname)
sys.meta_path.insert(0, BlockNative())
from pypaimon.execution.python.read.streaming_table_scan import AsyncStreamingTableScan
scan = object.__new__(AsyncStreamingTableScan)
failure = OSError('prefetch unavailable')
scan._snapshot_manager = SimpleNamespace(find_next_scannable=Mock(side_effect=failure))
scan.follow_up_scanner = SimpleNamespace(should_scan=Mock())
scan._lookahead_size = 4
with pytest.raises(OSError) as caught:
scan._fetch_plan_with_lookahead(1)
assert caught.value is failure
scan._snapshot_manager.find_next_scannable.assert_called_once_with(
1, scan.follow_up_scanner.should_scan, lookahead_size=4)
assert not any(name.startswith(('pypaimon.execution.native', 'pypaimon_rust')) for name in sys.modules)
"""
result = subprocess.run([sys.executable, '-c', source], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True, timeout=30)
assert result.returncode == 0, result.stdout + result.stderr
def test_native_read_schema_and_commit_path_adapters_do_not_load_python_casts():
source = """
import importlib.abc
import sys
from datetime import datetime
from decimal import Decimal
from types import SimpleNamespace
import pyarrow as pa
class BlockPythonCasts(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname.startswith(('pypaimon.execution.python', 'pypaimon_rust')):
raise AssertionError('Shared Native adapter loaded execution: ' + fullname)
sys.meta_path.insert(0, BlockPythonCasts())
from pypaimon.schema.arrow_schema import read_arrow_schema
from pypaimon.schema.data_types import ArrayType, AtomicType, DataField, RowType
from pypaimon.execution.native.read.table_read import NativeTableRead
from pypaimon.execution.native.write.native_commit import native_messages_supported
from pypaimon.utils.file_store_path_factory import FileStorePathFactory, canonical_data_file_path
fields = [DataField(0, 'nested', RowType(False, [
DataField(1, 'values', ArrayType(False, AtomicType('INT', nullable=False)))]))]
schema = read_arrow_schema(fields)
assert schema.field('nested').nullable
assert schema.field('nested').type.field('values').nullable
assert schema.field('nested').type.field('values').type.value_field.nullable
assert pa.array([{'values': [1, None]}, None], type=schema.field('nested').type).to_pylist() == [
{'values': [1, None]}, None]
factory = FileStorePathFactory(
'/warehouse', ['amount', 'created'], '__DEFAULT_PARTITION__', 'parquet', 'data-', 'changelog-',
False, False, 'zstd', external_paths=['file:///external/a', 'file:///external/b'],
external_path_strategy='entropy-inject', index_file_in_data_file_dir=True,
partition_types=[AtomicType('DECIMAL(10,2)'), AtomicType('TIMESTAMP(6)')])
table = SimpleNamespace(path_factory=lambda: factory)
partition = (Decimal('2.30'), datetime(2024, 1, 2, 3, 4, 5, 123456))
path = canonical_data_file_path(table, partition, 0, 'data.parquet')
assert '/amount=2.30/created=2024-01-02 03%3A04%3A05.123456/bucket-0/' in path
index = SimpleNamespace(file_name='index-1', external_path=None)
assert factory.bucket_index_path(partition, 0, index) == path.rsplit('/', 1)[0] + '/index-1'
index.external_path = '/previous-external-root/index-1'
assert factory.bucket_index_path(partition, 0, index) == index.external_path
file = SimpleNamespace(external_path=None, file_path=path, file_name='data.parquet')
message = SimpleNamespace(partition=partition, bucket=0, new_files=[file], changelog_files=[],
compact_before=[], compact_after=[], compact_changelog_files=[],
compact_index_adds=[], compact_index_deletes=[])
assert native_messages_supported(table, [message])
file.file_path = path + '.wrong'
assert not native_messages_supported(table, [message])
assert not any(name.startswith(('pypaimon.execution.python', 'pypaimon_rust')) for name in sys.modules)
"""
result = subprocess.run([sys.executable, '-c', source], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True, timeout=30)
assert result.returncode == 0, result.stdout + result.stderr
def test_all_native_adapters_import_without_python_execution_or_sdk_initialization():
source = """
import importlib
import importlib.abc
import pkgutil
import sys
class BlockExecution(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname.startswith(('pypaimon.execution.python', 'pypaimon_rust')):
raise AssertionError('Native adapter initialized another executor: ' + fullname)
sys.meta_path.insert(0, BlockExecution())
from pypaimon.execution import native
for module in pkgutil.walk_packages(native.__path__, native.__name__ + '.'):
importlib.import_module(module.name)
assert not any(name.startswith(('pypaimon.execution.python', 'pypaimon_rust')) for name in sys.modules)
"""
result = subprocess.run([sys.executable, '-c', source], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True, timeout=30)
assert result.returncode == 0, result.stdout + result.stderr