This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 5d02e09dbd [Python] Enable native primary-key BLOB limits and floating
sequence fields (#10410)
5d02e09dbd is described below
commit 5d02e09dbd3f140520fd7c33f33765118f5b098d
Author: Jingsong Lee <[email protected]>
AuthorDate: Wed Oct 7 11:11:05 2026 +0800
[Python] Enable native primary-key BLOB limits and floating sequence fields
(#10410)
---
paimon-python/pypaimon/read/table_read.py | 42 ++-
.../pypaimon/tests/native_pk_blob_limit_test.py | 283 +++++++++++++++++++++
paimon-python/pypaimon/tests/native_read_test.py | 76 ++++--
.../pypaimon/tests/native_sequence_test.py | 1 +
paimon-python/pypaimon/tests/native_write_test.py | 4 +-
paimon-python/pypaimon/write/native_write.py | 4 -
6 files changed, 352 insertions(+), 58 deletions(-)
diff --git a/paimon-python/pypaimon/read/table_read.py
b/paimon-python/pypaimon/read/table_read.py
index 551d0300be..ee8f8e9f8d 100644
--- a/paimon-python/pypaimon/read/table_read.py
+++ b/paimon-python/pypaimon/read/table_read.py
@@ -43,7 +43,7 @@ from pypaimon.read.split_read import (DataEvolutionSplitRead,
MergeFileSplitRead, RawFileSplitRead,
SplitRead, deferred_blob_field_names)
from pypaimon.schema.data_types import (
- DataField, MapType, PyarrowFieldParser, is_map_blob_type)
+ DataField, MapType, PyarrowFieldParser, is_blob_file_field,
is_map_blob_type)
from pypaimon.table.row.offset_row import OffsetRow
from pypaimon.utils.arrow_utils import zero_column_batch
@@ -254,13 +254,12 @@ class TableRead:
table reads do not guarantee row order. Python fallback reads remain
serial.
"""
- # Cleanup ownership follows the uncapped concurrency decision. LIMIT
- # may reduce the actual native worker count to one, but the returned
- # PyArrow reader still does not close its suspended batch iterator.
+ # PyArrow does not close a suspended Python batch iterator. Keep its
+ # cleanup even when LIMIT or BLOB pruning serializes native reads.
effective = self._resolve_parallelism(parallelism, len(splits))
reader, batch_iterator = self._new_arrow_batch_reader(
splits, blob_parallelism, parallelism)
- if self._should_run_parallel(splits, effective):
+ if self._requires_batch_iterator_cleanup(splits, effective):
return _ClosableArrowBatchReader(reader, batch_iterator)
return reader
@@ -280,7 +279,7 @@ class TableRead:
effective = self._resolve_parallelism(parallelism, len(splits))
reader, batch_iterator = self._new_arrow_batch_reader(
splits, blob_parallelism, parallelism)
- if self._should_run_parallel(splits, effective):
+ if self._requires_batch_iterator_cleanup(splits, effective):
return _ClosableArrowBatchReader(reader, batch_iterator)
if (_RECORD_BATCH_READER_FROM_STREAM is not None
and hasattr(reader, "close")):
@@ -377,9 +376,9 @@ class TableRead:
``>= 1``. Note that with ``>= 2`` (or auto)
and a ``limit`` set, the returned rows are an arbitrary
subset of the requested size, since which splits fill the row
- quota first is non-deterministic. Data-evolution reads with
- deferred BLOB resolution falls back to the Python reader when
- a limit may discard rows, so discarded payloads are not read.
+ quota first is non-deterministic. Reads with BLOB payloads run
+ serially when a limit may discard rows, so discarded payloads
+ are not fetched by speculative split workers.
blob_parallelism: maximum concurrent blob range reads within each
split reader. ``None`` or ``1`` (default) reads blobs serially;
``>= 2`` enables concurrent ranged reads. On the
@@ -498,22 +497,9 @@ class TableRead:
"the table file format is not supported")
if not splits:
return []
- sequence_fields = self.table.options.sequence_field()
- if self.table.is_primary_key_table and sequence_fields:
- sequence_schema = PyarrowFieldParser.from_paimon_schema(
- [self.table.field_dict[name] for name in sequence_fields])
- # Native merge cannot extract floating sequence values: they
- # would silently become missing sequence values.
- if any(pyarrow.types.is_floating(field.type) for field in
sequence_schema):
- return self._native_fallback(
- "floating sequence fields are not supported")
if not self._native_blob_view_supported():
return self._native_fallback(
"the table uses an unsupported BLOB view")
- if (self._deferred_blob_limit_may_prune(splits)
- and not self.table.options.data_evolution_enabled()):
- return self._native_fallback(
- "deferred BLOB LIMIT pruning requires the Python reader")
# Query authorization has additional filtering, masking and projection
# semantics which are already implemented by the Python reader.
if any(isinstance(split, QueryAuthSplit) for split in splits):
@@ -982,9 +968,13 @@ class TableRead:
def _deferred_blob_limit_may_prune(self, splits: List[Split]) -> bool:
return (self.limit is not None
and (self._deferred_blob_fields
- or self._native_inline_blob_fields())
+ or self._native_resolved_blob_fields())
and not self._limit_covers_all_splits(splits))
+ def _requires_batch_iterator_cleanup(self, splits: List[Split], effective:
int) -> bool:
+ return (self._should_run_parallel(splits, effective)
+ or self._deferred_blob_limit_may_prune(splits))
+
def _native_blob_view_supported(self) -> bool:
"""Use native view resolution only with the REST catalog
environment."""
read_names = {field.name for field in self._scan_read_type}
@@ -999,11 +989,13 @@ class TableRead:
from pypaimon.read.native_plan import _catalog_metastore
return _catalog_metastore(loader) == 'rest'
- def _native_inline_blob_fields(self) -> set:
- """Return configured BLOB fields that native reads resolve eagerly."""
+ def _native_resolved_blob_fields(self) -> set:
+ """BLOB fields Rust resolves before emitting a batch."""
options = self.table.options
if options.blob_as_descriptor():
return set()
+ if self.table.is_primary_key_table:
+ return {field.name for field in self._scan_read_type if
is_blob_file_field(field)}
inline_fields = (
options.blob_descriptor_fields()
| options.blob_view_fields()
diff --git a/paimon-python/pypaimon/tests/native_pk_blob_limit_test.py
b/paimon-python/pypaimon/tests/native_pk_blob_limit_test.py
new file mode 100644
index 0000000000..c3a49b79d0
--- /dev/null
+++ b/paimon-python/pypaimon/tests/native_pk_blob_limit_test.py
@@ -0,0 +1,283 @@
+# 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.
+
+"""PK BLOB filtering and LIMIT must leave unselected payloads unopened."""
+
+from unittest.mock import patch
+
+import pyarrow as pa
+import pytest
+
+from pypaimon import CatalogFactory
+from pypaimon.read.table_read import TableRead
+from pypaimon.schema.data_types import PyarrowFieldParser
+from pypaimon.table.row.blob import BlobDescriptor
+from pypaimon.write.native_write import NativeTableWrite
+
+pytestmark = pytest.mark.native_plan
+
+
+def _table(tmp_path, engine='deduplicate', batch_size=8192, partitioned=False):
+ from pypaimon_rust.datafusion import SQLContext
+
+ options = {'bucket': '1', 'merge-engine': engine, 'read.batch-size':
str(batch_size),
+ 'blob-descriptor-field': 'payload,output',
'read.native.enabled': 'true',
+ 'write.native.enabled': 'true', 'scan.native-plan.enabled':
'true'}
+ if engine == 'aggregation':
+ options['fields.default-aggregate-function'] = 'last_value'
+ ctx = SQLContext()
+ ctx.register_catalog('paimon', {'warehouse': str(tmp_path)})
+ ctx.sql('CREATE SCHEMA paimon.pkblob')
+ columns = 'id INT, tag INT, payload BLOB, output BLOB'
+ key, partition = 'PRIMARY KEY (id)', ''
+ if partitioned:
+ columns += ', pt STRING'
+ key, partition = 'PRIMARY KEY (pt, id)', ' PARTITIONED BY (pt)'
+ properties = ', '.join("'%s' = '%s'" % item for item in options.items())
+ ctx.sql('CREATE TABLE paimon.pkblob.t (' + columns + ', ' + key + ')' +
partition
+ + ' WITH (' + properties + ')')
+ table = CatalogFactory.create({'warehouse':
str(tmp_path)}).get_table('pkblob.t')
+ if engine == 'first-row':
+ # Java pk clustering permits first-row DVs. Loaded options let the
+ # reader consume level-0 files directly, without requiring compaction.
+ table = table.copy({'pk-clustering-override': 'true',
'deletion-vectors.enabled': 'true',
+ 'deletion-vectors.merge-on-read': 'true'})
+ return table
+
+
+def _descriptor(tmp_path, name, value=None):
+ file = tmp_path / name
+ if value is not None:
+ file.write_bytes(value)
+ return BlobDescriptor(file.as_uri(), 0, len(value) if value is not None
else 8).serialize()
+
+
+def _write(table, rows):
+ builder = table.new_batch_write_builder()
+ writer, commit = builder.new_write(), builder.new_commit()
+ assert isinstance(writer, NativeTableWrite)
+ try:
+ writer.write_arrow(pa.Table.from_pylist(
+ rows, schema=PyarrowFieldParser.from_paimon_schema(table.fields)))
+ commit.commit(writer.prepare_commit())
+ assert writer._python_writer is None
+ finally:
+ writer.close()
+ commit.close()
+
+
+def _read(table, projection, predicate=None, limit=None, parallelism=1,
streaming=False, splits=None):
+ builder = table.new_read_builder().with_projection(projection)
+ if predicate is not None:
+ builder.with_filter(predicate)
+ if limit is not None:
+ builder.with_limit(limit)
+ splits = builder.new_scan().plan().splits() if splits is None else splits
+ reader = builder.new_read()
+ with patch.object(reader, '_create_split_read',
side_effect=AssertionError('Python read fallback')):
+ if streaming:
+ with reader.to_arrow_batch_reader(splits, parallelism=parallelism,
blob_parallelism=2) as batches:
+ return pa.Table.from_batches(list(batches),
schema=batches.schema).to_pylist()
+ return reader.to_arrow(splits, parallelism=parallelism,
blob_parallelism=2).to_pylist()
+
+
[email protected]('engine', ['deduplicate', 'first-row',
'partial-update', 'aggregation'])
[email protected]('batch_size', [1, 8192])
[email protected]('payload_filter', [False, True])
+def test_pk_blob_limit_stops_before_later_invalid_uri(tmp_path, engine,
batch_size, payload_filter):
+ table = _table(tmp_path, engine, batch_size)
+ payload = _descriptor(tmp_path, 'payload', b'selected')
+ output = _descriptor(tmp_path, 'output', b'returned')
+ missing = _descriptor(tmp_path, 'missing')
+ _write(table, [dict(id=1, tag=1, payload=payload, output=output),
+ dict(id=2, tag=1, payload=missing, output=missing)])
+ pb = table.new_read_builder().new_predicate_builder()
+ predicate = pb.equal('payload', b'selected') if payload_filter else None
+ assert _read(table, ['id', 'payload', 'output'], predicate, 1) == [
+ dict(id=1, payload=b'selected', output=b'returned')]
+
+
[email protected]('limit', [None, 1])
[email protected]('streaming', [False, True])
+def test_pk_output_blob_is_resolved_only_for_matching_rows(tmp_path, limit,
streaming):
+ table = _table(tmp_path)
+ rejected = _descriptor(tmp_path, 'rejected', b'no')
+ selected = _descriptor(tmp_path, 'selected', b'yes')
+ output = _descriptor(tmp_path, 'output', b'returned')
+ missing = _descriptor(tmp_path, 'missing')
+ _write(table, [dict(id=1, tag=1, payload=rejected, output=missing),
+ dict(id=2, tag=1, payload=selected, output=output)])
+ pb = table.new_read_builder().new_predicate_builder()
+ assert _read(table, ['output', 'id'], pb.equal('payload', b'yes'), limit,
+ streaming=streaming) == [dict(output=b'returned', id=2)]
+
+
+def test_pk_ordinary_conjunct_filters_before_payload_access(tmp_path):
+ table = _table(tmp_path)
+ payload = _descriptor(tmp_path, 'payload', b'yes')
+ missing = _descriptor(tmp_path, 'missing')
+ _write(table, [dict(id=1, tag=0, payload=missing, output=missing),
+ dict(id=2, tag=1, payload=payload, output=payload)])
+ pb = table.new_read_builder().new_predicate_builder()
+ predicate = pb.and_predicates([pb.equal('tag', 1), pb.equal('payload',
b'yes')])
+ assert _read(table, ['id'], predicate, 1) == [dict(id=2)]
+
+
[email protected]('batch_size', [1, 8192])
+def test_pk_blob_limit_shrinks_candidate_batches_after_rejected_rows(tmp_path,
batch_size):
+ table = _table(tmp_path, batch_size=batch_size)
+ missing = _descriptor(tmp_path, 'must-not-open-after-second-match')
+ rejected = _descriptor(tmp_path, 'rejected', b'no')
+ selected = _descriptor(tmp_path, 'selected', b'yes')
+ _write(table, [dict(id=1, tag=1, payload=rejected, output=missing),
+ dict(id=2, tag=1, payload=selected, output=selected),
+ dict(id=3, tag=1, payload=selected, output=selected),
+ dict(id=4, tag=1, payload=missing, output=missing)])
+ pb = table.new_read_builder().new_predicate_builder()
+ assert _read(table, ['id', 'output'], pb.equal('payload', b'yes'), 2) == [
+ dict(id=2, output=b'yes'), dict(id=3, output=b'yes')]
+
+
[email protected]('limit', [None, 1, 2])
+def test_pk_blob_or_short_circuits_payload_access(tmp_path, limit):
+ table = _table(tmp_path)
+ missing = _descriptor(tmp_path, 'unneeded-branch')
+ selected = _descriptor(tmp_path, 'selected', b'yes')
+ rejected = _descriptor(tmp_path, 'rejected', b'no')
+ _write(table, [dict(id=1, tag=1, payload=missing, output=missing),
+ dict(id=2, tag=0, payload=selected, output=missing),
+ dict(id=3, tag=0, payload=rejected, output=missing)])
+ pb = table.new_read_builder().new_predicate_builder()
+ predicate = pb.or_predicates([pb.equal('tag', 1), pb.equal('payload',
b'yes')])
+ assert _read(table, ['id'], predicate, limit) == [dict(id=1),
dict(id=2)][:limit]
+
+
+def test_pk_blob_nested_boolean_branches_only_resolve_pending_rows(tmp_path):
+ table = _table(tmp_path)
+ missing = _descriptor(tmp_path, 'unneeded-branch')
+ selected = _descriptor(tmp_path, 'selected', b'yes')
+ _write(table, [dict(id=1, tag=1, payload=missing, output=missing),
+ dict(id=2, tag=0, payload=selected, output=missing)])
+ pb = table.new_read_builder().new_predicate_builder()
+ predicate = pb.or_predicates([
+ pb.and_predicates([pb.equal('tag', 1), pb.equal('id', 1)]),
+ pb.and_predicates([pb.equal('payload', b'yes'), pb.equal('payload',
b'yes')])])
+ assert _read(table, ['id'], predicate) == [dict(id=1), dict(id=2)]
+
+
+def test_pk_blob_predicate_payload_is_reused_for_output(tmp_path):
+ table = _table(tmp_path)
+ inner_descriptor = _descriptor(tmp_path, 'must-not-resolve-returned-bytes')
+ payload = _descriptor(tmp_path, 'outer', inner_descriptor)
+ _write(table, [dict(id=1, tag=1, payload=payload, output=payload)])
+ pb = table.new_read_builder().new_predicate_builder()
+ assert _read(table, ['payload'], pb.equal('payload', inner_descriptor)) ==
[
+ dict(payload=inner_descriptor)]
+
+
[email protected]('limit', [None, 1])
[email protected]('not_null', [False, True])
+def test_pk_blob_null_predicates_do_not_open_payload(tmp_path, limit,
not_null):
+ table = _table(tmp_path)
+ missing = _descriptor(tmp_path, 'must-not-read-for-null-check')
+ _write(table, [dict(id=1, tag=1, payload=missing, output=missing),
+ dict(id=2, tag=1, payload=None, output=None)])
+ pb = table.new_read_builder().new_predicate_builder()
+ predicate = pb.is_not_null('payload') if not_null else
pb.is_null('payload')
+ assert _read(table, ['id'], predicate, limit) == [dict(id=1 if not_null
else 2)]
+
+
[email protected]('engine', ['deduplicate', 'first-row',
'partial-update', 'aggregation'])
+def test_pk_blob_limit_and_payload_filter_run_after_merge(tmp_path, engine):
+ table = _table(tmp_path, engine)
+ good = _descriptor(tmp_path, 'good', b'kept')
+ missing = _descriptor(tmp_path, 'discarded-version')
+ older, newer = (good, missing) if engine == 'first-row' else (missing,
good)
+ for reference in (older, newer):
+ _write(table, [dict(id=1, tag=1, payload=reference, output=reference)])
+ pb = table.new_read_builder().new_predicate_builder()
+ assert _read(table, ['id', 'output'], pb.equal('output', b'kept'), 1) == [
+ dict(id=1, output=b'kept')]
+
+
[email protected]('parallelism', [None, 1, 4])
[email protected]('streaming', [False, True])
+def test_pk_blob_limit_is_shared_across_partitions(tmp_path, parallelism,
streaming):
+ table = _table(tmp_path, partitioned=True)
+ payload = _descriptor(tmp_path, 'payload', b'yes')
+ missing = _descriptor(tmp_path, 'missing')
+ _write(table, [dict(id=1, tag=1, payload=payload, output=payload, pt='a'),
+ dict(id=2, tag=1, payload=missing, output=missing, pt='b')])
+ splits = sorted(table.new_read_builder().new_scan().plan().splits(),
+ key=lambda split: split.files[0].file_path)
+ assert len(splits) == 2
+ assert _read(table, ['id', 'payload'], limit=1, parallelism=parallelism,
+ streaming=streaming, splits=splits) == [dict(id=1,
payload=b'yes')]
+
+
+def test_pk_blob_zero_limit_and_descriptor_output_require_no_payload(tmp_path):
+ table = _table(tmp_path)
+ missing = _descriptor(tmp_path, 'missing')
+ _write(table, [dict(id=1, tag=1, payload=missing, output=missing)])
+ splits = table.new_read_builder().new_scan().plan().splits()
+ assert _read(table, ['payload'], limit=0, splits=splits) == []
+ assert _read(table.copy({'blob-as-descriptor': 'true'}), ['payload'],
limit=1) == [dict(payload=missing)]
+ with pytest.raises(ValueError, match='missing'):
+ _read(table, ['payload'], limit=1)
+
+
[email protected]('payload_filter', [False, True])
+def test_streaming_pk_blob_row_kinds_resolve_only_selected_payload(tmp_path,
payload_filter):
+ table = _table(tmp_path)
+ payload = _descriptor(tmp_path, 'payload', b'selected')
+ missing = _descriptor(tmp_path, 'unselected')
+ _write(table, [dict(id=1, tag=1, payload=payload, output=payload),
+ dict(id=2, tag=1, payload=missing, output=missing)])
+ builder = table.new_stream_read_builder().with_projection(['id',
'payload'])
+ scan = builder.new_streaming_scan()
+ splits =
scan._create_delta_plan(table.snapshot_manager().get_latest_snapshot()).splits()
+ assert splits and all(split.is_streaming for split in splits)
+ predicate = builder.new_predicate_builder().equal('payload', b'selected')
if payload_filter else None
+ reader = TableRead(table, predicate, builder.read_type(),
include_row_kind=True, limit=1)
+ with patch.object(reader, '_create_split_read',
side_effect=AssertionError('Python read fallback')):
+ assert reader.to_arrow(splits).to_pylist() == [
+ {'_row_kind': '+I', 'id': 1, 'payload': b'selected'}]
+
+
+def
test_pk_row_kind_limit_is_global_across_snapshot_and_delta_splits(tmp_path):
+ from pypaimon.read.native_plan import native_read, native_split_from_python
+
+ table = _table(tmp_path, partitioned=True)
+ payload = _descriptor(tmp_path, 'payload', b'selected')
+ missing = _descriptor(tmp_path, 'unselected-delta')
+ _write(table, [dict(id=1, tag=1, payload=payload, output=payload, pt='a')])
+ splits = table.new_read_builder().new_scan().plan().splits()
+ _write(table, [dict(id=2, tag=1, payload=missing, output=missing, pt='b')])
+ scan = table.new_stream_read_builder().new_streaming_scan()
+ splits +=
scan._create_delta_plan(table.snapshot_manager().get_latest_snapshot()).splits()
+ assert len(splits) == 2 and not splits[0].is_streaming and
splits[1].is_streaming
+ # Exercise the Rust quota directly: TableRead's outer truncation must not
+ # conceal extra rows or payload reads in the core row-kind path.
+ fields = [field for field in table.fields if field.name in ('id',
'payload')]
+ rust_splits = [native_split_from_python(split) for split in splits]
+ reader = native_read(table, rust_splits, read_type=fields,
include_row_kind=True, limit=1)
+ try:
+ rows = [row for batch in reader for row in batch.to_pylist()]
+ finally:
+ reader.close()
+ assert len(rows) == 1
+ assert rows[0]['id'] == 1 and rows[0]['payload'] == b'selected'
diff --git a/paimon-python/pypaimon/tests/native_read_test.py
b/paimon-python/pypaimon/tests/native_read_test.py
index b50c9c15e4..46a568515f 100644
--- a/paimon-python/pypaimon/tests/native_read_test.py
+++ b/paimon-python/pypaimon/tests/native_read_test.py
@@ -38,6 +38,7 @@ class _Split:
def _table_read(limit=None):
read = TableRead.__new__(TableRead)
read.table = Mock()
+ read.table.is_primary_key_table = False
read.table.options.native_read_enabled.return_value = True
read.table.options.sequence_field.return_value = []
read.table.options.file_format.return_value = 'parquet'
@@ -160,16 +161,17 @@ def
test_variant_fields_never_silently_falls_back_to_python():
@pytest.mark.parametrize('type_', ['FLOAT', 'DOUBLE'])
-def test_floating_sequence_falls_back_before_native_read(type_):
+def test_floating_sequence_uses_native_read(type_):
read = _table_read()
read.table.is_primary_key_table = True
read.table.options.sequence_field.return_value = ['seq']
read.table.field_dict = {'seq': DataField(1, 'seq', AtomicType(type_))}
split = _Split()
split._native_split = object()
- with patch('pypaimon.read.native_plan.native_read') as native:
- assert read._try_native_batches([split], pa.schema([('id',
pa.int32())])) is None
- native.assert_not_called()
+ batch = _id_batch([1])
+ with patch('pypaimon.read.native_plan.native_read', return_value=[batch])
as native:
+ assert list(read._try_native_batches([split], pa.schema([('id',
pa.int32())]))) == [batch]
+ native.assert_called_once()
def test_native_read_consumes_retained_rust_splits_and_enforces_limit():
@@ -678,6 +680,37 @@ def
test_native_read_limit_close_reaches_capped_native_reader():
assert native_reader.closed
[email protected]('managed_reader', [False, True])
+def
test_pruning_pk_blob_limit_close_reaches_serial_native_reader(managed_reader):
+ read = _blob_table_read(limit=2)
+ read.table.is_primary_key_table = True
+ splits = [_Split(), _Split()]
+ for index, split in enumerate(splits):
+ split._native_split = index
+ split.merged_row_count = Mock(return_value=None)
+
+ class CloseTrackingReader:
+ closed = False
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ return pa.record_batch([pa.array([b'payload'],
type=pa.large_binary())], names=['payload'])
+
+ def close(self):
+ self.closed = True
+
+ native_reader = CloseTrackingReader()
+ method = read._to_managed_arrow_batch_reader if managed_reader else
read.to_arrow_batch_reader
+ with patch('pypaimon.read.native_plan.native_read',
return_value=native_reader):
+ reader = method(splits, parallelism=4)
+ assert reader.read_next_batch().column('payload').to_pylist() ==
[b'payload']
+ assert not native_reader.closed
+ reader.close()
+ assert native_reader.closed
+
+
def test_filtered_native_read_limit_keeps_split_parallelism():
read = _table_read(limit=1)
read.predicate = Mock()
@@ -965,35 +998,24 @@ def
test_native_read_preserves_parallel_large_binary_blob_schema():
assert result.schema == pa.schema([('payload', pa.large_binary())])
-def test_native_read_defers_to_python_for_pruning_blob_limit():
- read = _blob_table_read(limit=1)
- read._deferred_blob_fields = {'payload'}
- split = _Split('payload.blob')
- split._native_split = object()
- split.merged_row_count = Mock(return_value=2)
-
- with patch(
- 'pypaimon.read.native_plan.native_read') as native:
- assert read._try_native_batches(
- [split], pa.schema([('payload', pa.large_binary())]),
- blob_parallelism=1) is None
-
- native.assert_not_called()
-
-
-def test_native_read_defers_to_python_for_pruning_descriptor_blob_limit():
[email protected]('descriptor', [False, True])
+def test_native_primary_key_blob_limit_passes_quota_to_rust(descriptor):
read = _blob_table_read(limit=1)
- read.table.options.blob_descriptor_fields.return_value = {'payload'}
+ read.table.is_primary_key_table = True
+ if descriptor:
+ read.table.options.blob_descriptor_fields.return_value = {'payload'}
split = _Split('payload.parquet')
split._native_split = object()
split.merged_row_count = Mock(return_value=2)
- with patch('pypaimon.read.native_plan.native_read') as native:
- assert read._try_native_batches(
+ batch = pa.record_batch([pa.array([b'first'], type=pa.large_binary())],
names=['payload'])
+ with patch('pypaimon.read.native_plan.native_read', return_value=[batch])
as native:
+ assert list(read._try_native_batches(
[split], pa.schema([('payload', pa.large_binary())]),
- blob_parallelism=1) is None
-
- native.assert_not_called()
+ blob_parallelism=1)) == [batch]
+ native.assert_called_once()
+ assert native.call_args.kwargs['limit'] == 1
+ assert not read._should_run_parallel([split, split], 2)
@pytest.mark.parametrize('descriptor', [False, True])
diff --git a/paimon-python/pypaimon/tests/native_sequence_test.py
b/paimon-python/pypaimon/tests/native_sequence_test.py
index 20058bcc12..aa29c8f22b 100644
--- a/paimon-python/pypaimon/tests/native_sequence_test.py
+++ b/paimon-python/pypaimon/tests/native_sequence_test.py
@@ -98,6 +98,7 @@ def _read(table, native, predicate=None, projection=None):
@pytest.mark.parametrize('engine', ['deduplicate', 'partial-update',
'aggregation'])
@pytest.mark.parametrize('sequence_type,high,low', [
(pa.int64(), 100, 50), (pa.string(), 'z', 'a'), (pa.binary(), b'z', b'a'),
+ (pa.float32(), 100.5, 50.25), (pa.float64(), 100.5, 50.25),
(pa.decimal128(10, 2), Decimal('100.50'), Decimal('50.25')),
(pa.timestamp('us'), datetime.datetime(2020, 1, 2),
datetime.datetime(2020, 1, 1)),
(pa.time32('ms'), datetime.time(12, 0), datetime.time(1, 0)),
diff --git a/paimon-python/pypaimon/tests/native_write_test.py
b/paimon-python/pypaimon/tests/native_write_test.py
index 8403d06c4d..85145cfa52 100644
--- a/paimon-python/pypaimon/tests/native_write_test.py
+++ b/paimon-python/pypaimon/tests/native_write_test.py
@@ -80,8 +80,8 @@ def
test_sequence_validation_precedes_native_selection(tmp_path, streaming, sequ
@pytest.mark.parametrize('type_,order,supported', [
(pa.int64(), 'ascending', True),
(pa.int64(), 'descending', True),
- (pa.float32(), 'ascending', False),
- (pa.float64(), 'ascending', False),
+ (pa.float32(), 'ascending', True),
+ (pa.float64(), 'ascending', True),
])
def test_native_sequence_write_capabilities(tmp_path, type_, order, supported):
from pypaimon.write.native_write import create_native_write
diff --git a/paimon-python/pypaimon/write/native_write.py
b/paimon-python/pypaimon/write/native_write.py
index be04e4f9bd..43cf8e40c2 100644
--- a/paimon-python/pypaimon/write/native_write.py
+++ b/paimon-python/pypaimon/write/native_write.py
@@ -51,10 +51,6 @@ def create_native_write(table, commit_user,
static_partition=None, stream=False,
*, fixed_bucket=False, bucket_plan=None):
"""Return a native writer if the table can use the filesystem write
path."""
schema = PyarrowFieldParser.from_paimon_schema(table.table_schema.fields)
- sequence_fields = table.options.sequence_field()
- if (table.is_primary_key_table and any(
- pa.types.is_floating(schema.field(name).type) for name in
sequence_fields)):
- return None
if (not native_write_available()
or table.bucket_mode() not in (BucketMode.HASH_FIXED,
BucketMode.HASH_DYNAMIC,
BucketMode.BUCKET_UNAWARE,