blob: a9f8ec4b13c868ab7a8e82d1a6a2b4f94f5e2e1f [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.
"""Existing constructed schema changes must read physical Arrow lists natively."""
import os
from contextlib import ExitStack
from unittest.mock import patch
from urllib.parse import unquote, urlparse
import pyarrow as pa
import pyarrow.parquet as pq
import pytest
from pypaimon.schema.data_types import AtomicType
from pypaimon.schema.schema_change import SchemaChange
from pypaimon.tests import native_schema_evolution_cast_test
rest_catalog = native_schema_evolution_cast_test.rest_catalog
pytestmark = native_schema_evolution_cast_test.pytestmark
_LIST = pa.list_(pa.int32())
_LARGE_LIST = pa.large_list(pa.int32())
_CASES = [
(_LIST, _LARGE_LIST, [[9], [1, None], [], None, [2, 3]], 'v',
['[9]', '[1, null]', '[]', None, '[2, 3]']),
(pa.struct([('a', _LIST)]), pa.struct([('a', _LARGE_LIST)]),
[{'a': None}, None, {'a': [1, None]}, {'a': []}], 'v',
['{null}', None, '{[1, null]}', '{[]}']),
(pa.map_(pa.string(), _LIST), pa.map_(pa.string(), _LARGE_LIST),
[[('one', [1, None]), ('empty', []), ('missing', None)], None, []], 'v',
['{one -> [1, null], empty -> [], missing -> null}', None, '{}']),
(pa.struct([('a', _LIST)]), pa.struct([('a', _LARGE_LIST)]),
[{'a': None}, None, {'a': [1, None]}, {'a': []}], ['v', 'a'],
[{'a': None}, None, {'a': '[1, null]'}, {'a': '[]'}]),
]
@pytest.mark.parametrize('mode', ['append', 'pk', 'de'])
@pytest.mark.parametrize('target', ['STRING', 'CHAR(2)'])
@pytest.mark.parametrize('source,physical,values,path,expected', _CASES,
ids=['array', 'row', 'map', 'row-child'])
def test_large_list_constructed_strings_use_native_read(
rest_catalog, mode, target, source, physical, values, path, expected):
catalog, _ = rest_catalog
native_schema_evolution_cast_test._table(catalog, mode, source, values)
# The original nested schema must also plan through Rust, before changing
# only the logical type and preserving the old physical data files.
original = catalog.get_table('default.t').copy({'scan.native-plan.enabled': 'true'})
original_plan = original.new_read_builder().new_scan().plan()
assert sum(file.row_count for split in original_plan.splits() for file in split.files) == len(values)
catalog.alter_table('default.t', [SchemaChange.update_column_type(path, AtomicType(target))], False)
table = catalog.get_table('default.t')
planning = table.copy({'scan.native-plan.enabled': 'false', 'read.native.enabled': 'false'})
splits = planning.new_read_builder().new_scan().plan().splits()
assert splits
for split in splits:
for meta in split.files:
location = meta.file_path
if location.startswith('file:'):
location = unquote(urlparse(location).path)
data = pq.read_table(location)
index = data.schema.get_field_index('v')
field = data.schema.field(index)
data = data.set_column(index, pa.field(
field.name, physical, nullable=field.nullable, metadata=field.metadata),
data.column(index).cast(physical))
pq.write_table(data, location, row_group_size=2)
meta.file_size = os.stat(location).st_size
if not pa.types.is_map(physical):
assert pq.read_schema(location).field('v').type == physical
# PyArrow restores LargeList at the top level and inside ROW,
# while its Parquet reader normalizes MAP values to regular List.
for native in (False, True):
reader = table.copy({'read.native.enabled': str(native).lower()}).new_read_builder().new_read()
guard = patch(
'pypaimon.execution.python.read.table_read.PythonTableRead._create_split_read',
side_effect=AssertionError('Native read fell back')
) if native else ExitStack()
with guard:
result = reader.to_arrow(splits).sort_by([('id', 'ascending')])
# Java's constructed casts use STRING for children and ignore the
# outer CHAR length. NULL containers and NULL elements stay distinct.
assert result.column('v').to_pylist() == expected