blob: 0cf292065208a1e315f542417668fec84549a5f7 [file] [view]
<!--
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.
-->
# Python and Native execution backends
`execution` contains the Python and Native implementations of corresponding
Paimon operations. A module belongs here because it implements one of those
backends, not merely because it performs work at runtime.
Keep this boundary when adding or moving code:
* `execution/python` implements Paimon scanning, file reading, merging, writing,
updates, commits and indexes in Python.
* `execution/native` adapts the corresponding operations to `pypaimon_rust`.
General Native execution algorithms belong in Rust Core.
* Public contracts, request validation, backend selection, Arrow/Pandas output
adapters, shared input conversion and resource helpers stay in their owning
`read`, `write`, `globalindex` or `table` modules.
* Ray and Torch worker scheduling, dataset/sink adapters, prefetch and shuffle
stay with their framework integrations. They call the public table APIs and
do not need duplicate Paimon backend implementations.
* Match Paimon capabilities, not filenames: Python reader/index helper modules
can support a corresponding Rust operation without a same-named Rust file.
Format/Object file readers have Rust Core counterparts and remain backends.
```text
pypaimon/
common/
arrow_predicate.py Standalone Arrow predicate and transform helpers
external_path_provider.py Shared external file placement strategies
read/ Public read/scan APIs, Arrow output and explain
read_dispatch.py Read/scan selection across the two backends
read_support.py Shared output, schema and reader ownership
read_explain.py ExplainResult construction and rendering
datasource/ Public Ray/Torch framework adapters
write/ Public write/update/commit APIs and shared inputs
backend_dispatch.py Preparation, backend selection and commit coordination
write_dispatch.py Writer coordination and safe pre-write switching
update_dispatch.py Update/upsert/delete/MERGE coordination
input_adapter.py Pandas transport and distributed input conversion
merge_condition.py Shared MERGE condition parsing and Arrow batch filtering
merge_transform.py Shared Arrow assignments and callable inputs
ray_datasink.py Ray worker writes and coordinator commits
globalindex/ Public index contracts and backend coordination
index_dispatch.py Index selection and Vindex resource ownership
vindex_input.py Python SDK/Native callback streams and ownership
index/ Shared index metadata, definitions and input routing
table/source/
search_dispatch.py Search selection and transported-plan coordination
table/system/ SystemSplit metadata and common Arrow output
ray/
vector_index_build.py Ray index tasks, shard workers and failure draining
execution/
python/
casting/ Old-file array value casts
map_blob_key_serializer.py Physical MAP BLOB key encoding for readers/writers
read/ File scanners/readers and merge execution
search/ Vector/text/hybrid search and Python factories
blob_view_lookup.py Upstream Blob descriptor planning, loading and resolution
interval_partition.py Primary-key sorted-run grouping
map_shared_shredding.py Physical MAP decoding and selected-column pruning
variant_shredding.py Physical VARIANT reconstruction and shredding
deletionvectors/ DV bitmap codecs, file I/O and deleted-row filtering
write/ File writers, committers and update execution
globalindex/ Index file codecs, readers, writers and build algorithms
index/ Primary-key index source selection and sorted-index grouping
merge_engine_dispatch.py Python merge-function selection
native/
runtime.py Lazy SDK discovery and process safety
table.py Rust table/configuration bridge
read/ Rust scan/read/search and system metadata adapters
write/ Rust write/commit/update/index adapters
```
## Public APIs and dependencies
Corresponding Paimon APIs keep configuration, validation and explicit delegation;
they do not inherit either backend. Documented public method signatures, defaults, class
paths and supported subclass/instance hooks remain stable. Shared input/result
contracts include schemas, predicates, plans, splits and commit messages.
Neither backend imports the other. Native adapters do not construct or retain
Python executors. Python implementations do not load the optional Native SDK
just because it is installed. Planning, reading, writing and committing retain
independent options; shared plans and commit messages connect those stages.
The public `TableScan.file_scanner`, `AsyncStreamingTableScan` and
`StreamTableScan` are backend-neutral facades, including direct construction.
Accessing file configuration does not construct a Python scanner. File plans
require unrestricted whole-table authorization because they expose raw file
metadata. Internal writer metadata restoration uses the selected executor's
private scanner and retains its write authorization boundary.
Direct `TableUpdateByRowId`, `TableDeleteByRowId` and `ShardTableUpdator`
construction also selects one execution backend before loading target files.
Direct `TableWrite`, batch/stream writers and postpone fixed-bucket writers
use the same selection boundary as builders while retaining their public class
and subclass hooks. Writer resources and update executors are created in
`write/backend_dispatch.py`; the public classes only delegate to the selected
implementation. Python-only nested writers use explicit Python factories.
An operation that selected Python keeps its nested update/delete writers and
writer-owned metadata restoration in Python, including MERGE actions and
dynamic-bucket HASH validation. A committer that selected Python retains it
for subsequent rounds even if later messages could use Native. Private schema,
routing and rewrite algorithms
call their executor's helpers; public method overrides and user callbacks remain
public hooks. Broadcast file indexes retain their captured snapshot and are
consumed by the explicitly selected Python workers without per-worker rescans.
Python Blob View and search readers keep their internal source reads and scans
in Python, including captured snapshot views. Public table copies only share
resource containers; backend caches are allocated by the backend that uses them.
Hybrid queries that select Python keep their default route planning, child
execution and rank fusion in Python. Explicit subclass and instance hooks retain
their original owners; standalone public route and rank calls still select their
own backend. This also applies when hooks call `super()` or saved public methods;
the query and its route workers retain Python selection until each call exits.
Scan selection, including LeRobot's indexed-read preference, belongs
to `read_dispatch`; public scans do not mirror private Native capability methods.
Each scan retains its selected executor until its configuration changes. The
Python executor owns its private file scanner and applies public file-level
configuration. Installing, replacing or removing scanner execution hooks
invalidates that executor; explicit instance and subclass hooks select Python
before any Native capability probe and run after authorization.
The Python executor also compares captured requests before reusing its scanner,
so in-place changes to range containers refresh planning without selecting a
different backend or retaining an earlier authorization scope.
Scanner hooks delegate base calls to the current Python scanner, preserving its
snapshot and statistics when nested hooks change configuration. Unchanged query
scopes reuse their grant and wrap the plan once. Adding protected filter fields
requires a new grant; explicit raw-file calls still require whole-table permission.
Postpone bucket metadata is writer state. Native obtains it from the Core write
builder, preserving per-file layout validation and write authorization without
adding SELECT requirements. Shared input statistics and bucket arithmetic stay
outside either backend.
Manifest-source and follow-up-scanner callbacks belong to Python execution
internals. Public scanner configuration selects the existing snapshot, delta
or changelog strategies without accepting custom scanner callbacks.
Public writers and committers do not expose file writers, key extractors or
file committers. Inspecting an unknown attribute never initializes another
backend. Backend-owned schemas, routing and file state remain on the selected
executor; public table/user/partition configuration stays on the facade.
Deletion-vector codecs, file loading and row filtering belong to Python
execution, corresponding to Rust Core's deletion-vector implementation.
`DeletionFile` and `DeletionVectorMeta` remain shared metadata in `table/source`
and `index`; importing or transporting them does not load either backend.
Primary-key index source selection, payload eligibility and sorted-index coverage
planning belong to `execution/python/index`. Index definitions, source metadata
and their wire formats remain in `index`, shared with public builders and Native
adapters. Native index planning performs source selection and grouping in Rust.
Arrow read schemas and logical type-change rules stay in `schema`. Shared
numeric, calendar and metadata text helpers stay in `utils`; Native read and
commit adapters use them without importing Python cast executors. Array value
conversion belongs to Python execution. The schema manager validates local
type changes using the shared static executable-cast rules. Python commit
partition-spec parsing also belongs to its
executor; Native commits pass the spec to Rust.
Physical MAP/VARIANT codecs and Blob View resolution belong to Python
execution. Public value objects, schema/spec parsing and selected-key metadata
stay in `data`. Table MERGE action selection belongs to the selected backend.
Standalone Arrow condition filtering, SQL rewriting, callable transport and
shared Arrow assignment adapters stay in `write`.
Dynamic bucket HASH index publication and writer-owned pending state belong
to the Python writer. The shared input bucket assigner remains in `index`:
distributed producers use it to route inputs before either backend writes.
`input_routing` and `bucket_planning` similarly serve both backends' input
protocols without retaining a file writer or publishing data.
External-path round-robin, weighted selection and entropy directories are shared
path utilities in `common`. Shared path factories retain configuration and
existing-file resolution, loading a provider when a caller allocates a new file.
MAP BLOB key serializers implement the physical file format and are
shared within the Python backend. Generic URI/FileIO contracts, resource
caches and integer codecs stay in `common`; Native Blob inputs also use the
URI reader contract. FileIO supplies streams and locations. Physical Row, Blob
and Vortex codec construction belongs to the selected Python executor, rather
than to format-specific FileIO convenience methods.
Standalone Arrow predicate and transform helpers live in `common/arrow_predicate`.
JSON field-reference extraction and rule/schema validation remain independent
of that Arrow evaluator. The public authorization result creates an Arrow
filter lazily when explicitly requested without selecting either backend.
Python readers use the shared evaluator; Native reads pass the original rules to Rust.
Framework adapters keep their actual implementation in their original modules,
including their public class identities, serialization and worker closures.
Local and Native MERGE use shared `write` helpers without importing the public
Ray package. Public Ray helper entry points delegate to those same helpers.
System-table output applies common Arrow filtering/projection to `SystemSplit`
results from either metadata provider without creating a Python file reader.
Search readers accept an optional `SearchTaskExecutor` when executing a complete
plan. `SearchTask` and its dependency/context transport contract stay in
`table/source/search_execution`; Ray only implements bounded scheduling,
context sharing and cancellation. It does not inherit a Python search reader
or select Python on behalf of a Native reader. Python's distributed adapters
own their index/filter/raw/refinement operations. Native adapters transport
opaque shared context and task bytes; Core owns both their interpretation and
global query coordination. The global candidate limit is applied before
refinement, and full-text raw fallback retains one corpus for BM25 statistics.
Vector and full-text plans are snapshot-and-split data in both backends. The
Rust SDK constructors accept typed `Snapshot` and search split objects; indexed,
raw and primary-key splits keep their own fields and index metadata. The Native
adapter translates ordinary Python plans into these values, including plans
made by Python scans or sent to another process. Existing snapshot and index
metadata codecs are private adapters, rather than a public JSON plan protocol.
Native primary-key result conversion also stays in the Native adapter; shared
scored results keep only their metadata, validation and ownership contract.
Readers own the table, schema, query and filter. Native selection is retained
after transport, and source subsets never add missing indexes or raw ranges.
For Native, a plan without a captured snapshot must have no sources and never
selects latest. Shard callbacks use an internal binding bridge; they are not part of Rust's public
Plan/Read API. SDK vector, batch-vector, full-text and hybrid builders share
one `execute()` entry point. PyPaimon's existing public search methods delegate
to it through the Native adapters.
## Corresponding execution APIs
Compare the operation's inputs, results and lifecycle when changing a backend.
The public facade and the Native bridge normalize Python objects and Rust FFI
types at that boundary.
| Operation | Python execution | Native execution / Rust Core |
| --- | --- | --- |
| Batch scan | `PythonTableScan` | `NativeTableScan` / `TableScan` |
| Split read | `PythonTableRead` | `NativeTableRead` / `TableRead` |
| Stream scan | `AsyncStreamingTableScan` | `StreamTableScan` / `StreamTableScan` |
| Write | `PythonTableWrite` | `NativeTableWrite` / `TableWrite` |
| Commit | `PythonTableCommit` | `NativeTableCommit` / `TableCommit` |
| Row ID / predicate update, delete and key upsert | Python update executors | Native update adapters / `TableUpdate` |
| Shard update | `ShardTableUpdator` | `NativeShardTableUpdator` / `ShardTableUpdater` |
| Index build, drop and search | Python index/search executors | Native index/search adapters / Core index builders and readers |
Both commit adapters expose `commit(messages, commit_identifier,
snapshot_properties=None)`, `abort(messages)`,
`truncate_table(commit_identifier)`,
`truncate_partitions(partitions, commit_identifier)` and `close()`.
Message conversion happens during preparation; publication uses the selected
executor's prepared messages. Batch/stream binding details stay in the Native
adapter, and overwrite details stay in each backend.
Both batch scanners expose `plan(auth_result=None)`, `plan_all()` and
`scan_with_stats(auth_result=None)`. Both readers expose `read_rows(splits)` and
`read_batches(splits, schema, parallelism=None, blob_parallelism=1,
ordered=False)`. Output materialization and Python parallel-read helpers are
separate from this execution protocol. Native read adaptation calls its own
helpers directly; TableRead does not mirror private Native methods.
Writer executors take an explicit identifier in `prepare_commit(identifier)`.
The public batch facade exposes only `prepare_commit()`, with a one-time guard;
the public stream facade requires `prepare_commit(identifier)`. These signatures
also apply to Native facades, including after an explicit switch to Python.
Invalid call arguments fail before either executor prepares files.
Public stream write, update and commit entry points normalize integer-like
commit identifiers to Java `long` / Rust `i64` before execution starts.
Write-column selections reject empty selections and duplicate names before
replacing the current configuration. Append writers keep one ordered write type
after their first effective write; empty batches and ignored events do not lock
it. Repeating the same selection is a no-op, while a different selection requires
a new writer, including after prepares or idle cleanup. Full-schema inputs are
projected to the selected columns by name. Selected Arrow inputs may also carry
routing/event fields while retaining the selected column order. Named-row
conversion encodes selected ordinary and inline reference fields before capturing
a bucket writer; encoding failures leave the configuration open. Dedicated live
Blob payloads are consumed only during execution, and unused payloads remain
untouched.
Blob consumers are captured per partition/bucket; changes apply to newly created
writers, and active writers retain their consumer across prepares. Idle writers
retire at the Java commit boundary, so reopening uses the current consumer.
Index build/drop requests bind partition
predicates to the partition layout and reject predicates containing
non-partition fields.
Both stream scans expose `plan()`, `checkpoint()`,
`restore(next_snapshot_id=None)`, `watermark()`,
`notify_checkpoint_complete(next_snapshot)`, `stream()`, `stream_sync()` and
`close()`. A plan call checks available snapshots once. Consumer progress is
persisted explicitly or after the caller resumes a yielded plan. Cancelling a
yielded plan preserves its unacknowledged consumer position. Snapshot gaps must
preserve the configured changelog sequence.
Both shard updaters expose `arrow_reader()`, `update_by_arrow_batch(data)`,
`prepare_commit()` and `close()`. They track actual visible row IDs, require a
complete read/update cycle before preparation and preserve prepared files when
closed. This matters when deletion vectors hide physical rows. The selected
executor owns the reader, writer, pending batches and read/update cycle state;
the public facade only delegates these operations.
Python executors bind an explicit public context and retain its instance hooks;
Native adapters own Rust handles. The context is not copied or hidden behind a
generic proxy. Predicate/row-ID operations use bound executors, and prepared
upsert and MERGE requests expose `upsert()` and `prepare_commit()` respectively
on either backend. Private helper structure follows each implementation's
algorithms.
The operation protocols do not imply identical backend capabilities. Python
Data Evolution reads still reject nested ROW field projections supported by
Rust. `Plan.stats()` is optional: ordinary Python planning returns no counters,
while Native planning always records lightweight counters. Python's explicit
`scan_with_stats()` / explain pass collects additional bookkeeping and can
change planning concurrency, so it is not enabled by ordinary `plan()` calls.
## Failure and resource boundaries
Only preparation can select a fallback backend. Reads fail without replaying
through another backend after iteration begins. A writer cannot switch after
accepting a mutation. Update, MERGE and commit publication calls execute outside
preparation fallback handlers, because an exception may follow a successful
write or publication. Preserve prepared files when an outcome is uncertain;
explicit `TableCommit.abort` remains for known abandoned, uncommitted files.
Close retained readers on completion, cancellation and failures. Cleanup errors
must not replace a primary read/conversion failure. Ray adapters close their
worker readers, and Torch prefetch producers observe cancellation while waiting
on a full queue. Vindex initialization/close support callback reentry without
publishing a candidate cancelled during initialization.
Tests cover process-isolated backend imports, public hooks and serialization,
empty results versus unsupported operations, source cleanup, fallback boundaries
and prevention of write replay or accidental deletion. Packaging tests check
that backend modules and framework integrations ship at their intended paths.