blob: 5e47c0a3e3616510bfaf9487cf7d39d3bb58376f [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.
"""Shared requests keep Native planning independent of Python execution."""
from dataclasses import replace
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from pypaimon.common.options.core_options import CoreOptions
from pypaimon.common.options.options import Options
from pypaimon.common.predicate import Predicate
from pypaimon.execution.native.read.plan import NativeTableScan
from pypaimon.read.plan import Plan
from pypaimon.read.scan_request import ScanRequest
from pypaimon.read.table_scan import TableScan
from pypaimon.schema.data_types import AtomicType, DataField
from pypaimon.utils.range import Range
class _Split:
partition = None
def __init__(self, merged):
self._merged = merged
def merged_row_count(self):
return self._merged
def _scan():
predicate = Predicate('equal', 0, 'pt', [1])
scan = TableScan.__new__(TableScan)
scan.table = SimpleNamespace(
is_primary_key_table=False, partition_keys=['pt'], options=CoreOptions(Options({})))
scan.file_scanner = SimpleNamespace(
predicate=predicate, _user_predicate=predicate,
partition_key_predicate=predicate, _explicit_partition_filter=False,
auth_has_non_partition_filter=False, limit=10,
data_evolution=True, deletion_vectors_enabled=False,
idx_of_this_subtask=None, number_of_para_subtasks=None,
start_pos_of_this_subtask=None, end_pos_of_this_subtask=None,
chunk_shuffle=None, _global_index_result=None, _row_ranges=[Range(1, 3)])
scan.predicate = predicate
scan.limit = 10
scan.partition_predicate = None
scan._read_type = [DataField(0, 'pt', AtomicType('INT'))]
return scan
def test_request_recaptures_external_configuration_and_authorization_changes():
scan = _scan()
before = scan._scan_request()
scanner = scan.file_scanner
scanner._row_ranges[0].to = 8
scanner.limit = 2
scanner.predicate = None
scanner.auth_has_non_partition_filter = True
scanner.idx_of_this_subtask, scanner.number_of_para_subtasks = 1, 3
scan._read_type.append(DataField(1, 'extra', AtomicType('STRING')))
after = scan._scan_request()
assert before.row_ranges == ((1, 3),)
assert after.row_ranges == ((1, 8),)
assert before.limit == 10 and after.limit == 2
assert after.read_limit == 10
assert after.predicate is None and after.read_predicate is scan.predicate
assert after.user_predicate is scan.predicate
assert not before.auth_has_non_partition_filter and after.auth_has_non_partition_filter
assert (after.idx_of_this_subtask, after.number_of_para_subtasks) == (1, 3)
assert len(before.read_type) == 1 and len(after.read_type) == 2
def test_native_prepare_observes_the_latest_public_scanner_configuration():
scan = _scan()
with patch('pypaimon.execution.native.read.plan.native_plan', return_value=Plan([], 7)) as native:
assert scan._try_native_plan().snapshot_id == 7
assert native.call_args[1]['row_ranges'] == [(1, 3)]
scan.file_scanner._row_ranges = []
scan.file_scanner.idx_of_this_subtask, scan.file_scanner.number_of_para_subtasks = 1, 3
assert scan._try_native_plan().snapshot_id == 7
assert native.call_args[1]['row_ranges'] == []
assert native.call_args[1]['row_position_shard'] == (1, 3)
def test_native_planner_and_metadata_limit_only_consume_a_shared_request():
class Scan:
table = SimpleNamespace(options=CoreOptions(Options({})))
@property
def file_scanner(self):
raise AssertionError('Native planning touched a Python scanner')
def _scan_request(self):
return ScanRequest(limit=2, read_limit=2, idx_of_this_subtask=0, number_of_para_subtasks=2)
splits = [_Split(2), _Split(8)]
with patch('pypaimon.execution.native.read.plan.native_plan', return_value=Plan(splits, 7)) as native:
plan = NativeTableScan(Scan()).plan()
assert plan.splits() == splits[:1]
assert plan.snapshot_id == 7 and plan._stats_limit == 2
assert native.call_args[1]['limit'] is None
assert native.call_args[1]['row_position_shard'] == (0, 2)
def test_metadata_limit_uses_merged_counts_and_keeps_unknown_counts():
request = ScanRequest(limit=3, deletion_vectors_enabled=True)
splits = [_Split(2), _Split(2), _Split(8)]
assert request.apply_push_down_limit(splits) == splits[:2]
unknown = [splits[0], _Split(None), splits[2]]
assert request.apply_push_down_limit(unknown) is unknown
@pytest.mark.parametrize('changes', [
{'data_evolution': True, 'deletion_vectors_enabled': True},
{'auth_has_non_partition_filter': True},
{'predicate': Predicate('equal', 0, 'payload', [1])},
{'explicit_partition_filter': True, 'predicate': Predicate('equal', 0, 'pt', [1])},
])
def test_metadata_limit_waits_for_row_filters_and_deletion_vectors(changes):
class UnreadSplit:
def merged_row_count(self):
raise AssertionError('Unsafe metadata LIMIT inspected row counts')
request = replace(ScanRequest(limit=1, partition_keys=('pt',)), **changes)
splits = [UnreadSplit()]
assert request.apply_push_down_limit(splits) is splits
def test_chunk_validation_keeps_the_original_filter_and_empty_range_selection():
request = ScanRequest(
partition_keys=('pt',), user_predicate=Predicate('equal', 0, 'payload', [1]))
with pytest.raises(ValueError, match='only partition keys'):
request.validate_chunk_shuffle_compat()
with pytest.raises(ValueError, match='row ranges'):
ScanRequest(row_ranges=()).validate_chunk_shuffle_compat()