This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 88d40b83 docs(python): add Python Integration documentation page (#538)
88d40b83 is described below

commit 88d40b83e20784b8ea389340f761631d6b2732d6
Author: hechao <[email protected]>
AuthorDate: Mon Jul 20 11:08:21 2026 +0800

    docs(python): add Python Integration documentation page (#538)
---
 docs/mkdocs.yml            |   1 +
 docs/src/python-binding.md | 455 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 456 insertions(+)

diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index e4cca707..de8e727d 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -50,6 +50,7 @@ nav:
   - Getting Started: getting-started.md
   - SQL Integration: sql.md
   - Go Integration: go-binding.md
+  - Python Integration: python-binding.md
   - Architecture: architecture.md
   - Contributing: contributing.md
   - Releases:
diff --git a/docs/src/python-binding.md b/docs/src/python-binding.md
new file mode 100644
index 00000000..57dc657f
--- /dev/null
+++ b/docs/src/python-binding.md
@@ -0,0 +1,455 @@
+<!--
+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 Integration
+
+The Python integration is a binding built on top of Apache Paimon Rust, 
allowing you to access Paimon tables from Python programs. It uses 
[PyArrow](https://arrow.apache.org/docs/python/) for zero-copy data transfer 
via the [Arrow C Data 
Interface](https://arrow.apache.org/docs/format/CDataInterface.html).
+
+## Prerequisites
+
+- Python 3.10 or later
+- Supported platforms: Linux (amd64, arm64), macOS (amd64, arm64), Windows 
(amd64)
+
+## Installation
+
+```bash
+pip install pypaimon-rust pyarrow
+```
+
+The pre-built native library is embedded in the package and automatically 
loaded at runtime — no manual build step is needed. 
[PyArrow](https://arrow.apache.org/docs/python/) is a required peer dependency 
and must be installed separately.
+
+## Creating a Catalog
+
+Use `PaimonCatalog` with a dictionary of options to create a catalog. The 
catalog type is determined by the `metastore` option (default: `filesystem`).
+
+```python
+from pypaimon_rust.datafusion import PaimonCatalog
+
+# Local filesystem
+catalog = PaimonCatalog({"warehouse": "/path/to/warehouse"})
+
+# List databases and tables
+print(catalog.list_databases())
+print(catalog.list_tables("default"))
+
+# Get a table handle
+table = catalog.get_table("default.my_table")
+```
+
+### Alibaba Cloud OSS
+
+```python
+catalog = PaimonCatalog({
+    "warehouse": "oss://bucket/warehouse",
+    "fs.oss.accessKeyId": "your-access-key-id",
+    "fs.oss.accessKeySecret": "your-access-key-secret",
+    "fs.oss.endpoint": "oss-cn-hangzhou.aliyuncs.com",
+})
+```
+
+### REST Catalog
+
+```python
+catalog = PaimonCatalog({
+    "metastore": "rest",
+    "uri": "http://localhost:8080";,
+    "warehouse": "my_warehouse",
+})
+```
+
+## SQL Context
+
+`SQLContext` supports registering multiple Paimon catalogs and executing SQL 
queries with DataFusion.
+
+```python
+from pypaimon_rust.datafusion import SQLContext
+
+ctx = SQLContext()
+ctx.register_catalog("paimon", {"warehouse": "/path/to/warehouse"})
+
+# DDL and DML
+ctx.sql("CREATE SCHEMA paimon.my_db")
+ctx.sql("CREATE TABLE paimon.my_db.t (id INT, name STRING)")
+ctx.sql("INSERT INTO paimon.my_db.t VALUES (1, 'alice'), (2, 'bob')")
+
+# Query returns a list of PyArrow RecordBatches
+batches = ctx.sql("SELECT * FROM paimon.my_db.t")
+for batch in batches:
+    print(batch)
+```
+
+## Reading a Table
+
+Paimon Python uses a **scan-then-read** pattern: first scan the table to 
produce splits, then read data from those splits as PyArrow RecordBatches.
+
+```python
+import pyarrow as pa
+from pypaimon_rust.datafusion import PaimonCatalog
+
+catalog = PaimonCatalog({"warehouse": "/path/to/warehouse"})
+table = catalog.get_table("default.my_table")
+
+# Create a read builder
+rb = table.new_read_builder()
+
+# Step 1: Scan — produces a Plan containing Splits
+scan = rb.new_scan()
+plan = scan.plan()
+splits = plan.splits()
+
+# Step 2: Read — consumes splits and returns PyArrow RecordBatches
+read = rb.new_read()
+batches = read.read(splits)
+
+for batch in batches:
+    print(batch)
+```
+
+Alternatively, read via SQL using `SQLContext`:
+
+```python
+from pypaimon_rust.datafusion import SQLContext
+
+ctx = SQLContext()
+ctx.register_catalog("paimon", {"warehouse": "/path/to/warehouse"})
+
+batches = ctx.sql("SELECT id, name FROM paimon.default.my_table")
+for batch in batches:
+    print(batch)
+```
+
+## Writing to a Table
+
+Paimon Python uses a **write-then-commit** pattern: write PyArrow 
RecordBatches to a writer, prepare commit messages, then commit.
+
+```python
+import pyarrow as pa
+from pypaimon_rust.datafusion import PaimonCatalog
+
+catalog = PaimonCatalog({"warehouse": "/path/to/warehouse"})
+table = catalog.get_table("default.my_table")
+
+# Build a batch matching the table schema
+batch = pa.record_batch(
+    [pa.array([1, 2, 3], pa.int32()), pa.array(["a", "b", "c"], pa.string())],
+    names=["id", "name"],
+)
+
+# Create a write builder (shared commit_user for writer and committer)
+wb = table.new_write_builder()
+
+# Write batches
+write = wb.new_write()
+write.write_arrow(batch)
+
+# Prepare commit messages
+messages = write.prepare_commit()
+
+# Commit
+wb.new_commit().commit(messages)
+```
+
+Alternatively, write via SQL using `SQLContext`:
+
+```python
+from pypaimon_rust.datafusion import SQLContext
+
+ctx = SQLContext()
+ctx.register_catalog("paimon", {"warehouse": "/path/to/warehouse"})
+
+ctx.sql("INSERT INTO paimon.default.my_table VALUES (1, 'alice'), (2, 'bob')")
+```
+
+!!! warning "Schema Validation"
+    The input batch schema is strictly validated against the table schema: 
field count, order, names, and types must match exactly. A `ValueError` is 
raised on mismatch.
+
+!!! note "Write Builder Consistency"
+    The writer and committer must come from the same `WriteBuilder` — they 
share a `commit_user` for duplicate-commit detection. Passing messages from one 
builder's writer to another builder's committer will raise a `ValueError`.
+
+## Column Projection
+
+Use `with_projection` to select specific columns. Only the requested columns 
are read, reducing I/O.
+
+```python
+rb = table.new_read_builder()
+rb.with_projection(["id", "name"])
+
+# Continue with scan-then-read as above...
+```
+
+## Limit
+
+Use `with_limit` to set a hint for the number of rows returned. A limit of `0` 
returns zero rows.
+
+```python
+rb = table.new_read_builder()
+rb.with_limit(100)
+```
+
+!!! warning
+    `with_limit` is a scan-planning hint, not an exact row cap. When all rows 
fall within a single split, the entire split is returned regardless of the 
limit value. Callers should apply application-level limiting if an exact upper 
bound is required.
+
+## Case Sensitivity
+
+Use `with_case_sensitive` to control whether column-name matching in 
projections and predicates is case-sensitive. Defaults to `True` (exact match). 
Set to `False` for case-insensitive matching (ASCII case-folding).
+
+```python
+rb = table.new_read_builder()
+rb.with_case_sensitive(False)
+```
+
+!!! note
+    `with_case_sensitive` must be called **before** `with_filter` to affect 
predicate construction. The predicate is built using the case-sensitivity 
setting at the time `with_filter` is invoked; changing it afterward has no 
effect on an already-constructed predicate.
+
+## Filter Push-Down
+
+Filter push-down prunes data at two levels:
+
+1. **Scan planning** — skips partitions, buckets, and data files based on 
file-level statistics (min/max).
+2. **Read-side** — applies exact residual filtering, ensuring only rows that 
match the predicate are returned.
+
+!!! note
+    Filter push-down at the scan-planning level is **best-effort**: it may 
conservatively include files that do not contain matching rows. The read-side 
applies exact residual filtering and will not return rows that fail the 
predicate.
+
+### Predicate Format
+
+Predicates use a lightweight dictionary format. Each leaf node specifies a 
`method`, `field`, and `literals`:
+
+```python
+# Comparison predicates
+{"method": "equal", "field": "id", "literals": [1]}           # id = 1
+{"method": "notEqual", "field": "name", "literals": ["bob"]}   # name != "bob"
+{"method": "lessThan", "field": "id", "literals": [3]}         # id < 3
+{"method": "lessOrEqual", "field": "id", "literals": [2]}      # id <= 2
+{"method": "greaterThan", "field": "id", "literals": [1]}      # id > 1
+{"method": "greaterOrEqual", "field": "id", "literals": [2]}   # id >= 2
+
+# Null checks
+{"method": "isNull", "field": "name"}                           # name IS NULL
+{"method": "isNotNull", "field": "name"}                        # name IS NOT 
NULL
+
+# IN / NOT IN
+{"method": "in", "field": "id", "literals": [1, 2, 3]}          # id IN (1, 2, 
3)
+{"method": "notIn", "field": "name", "literals": ["x", "y"]}    # name NOT IN 
("x", "y")
+
+# String predicates
+{"method": "startsWith", "field": "name", "literals": ["al"]}   # name LIKE 
'al%'
+{"method": "endsWith", "field": "name", "literals": ["ce"]}     # name LIKE 
'%ce'
+{"method": "contains", "field": "name", "literals": ["ic"]}     # name LIKE 
'%ic%'
+{"method": "like", "field": "name", "literals": ["a%b%c"]}      # name LIKE 
'a%b%c'
+```
+
+### Applying Filters
+
+Pass a predicate dict to `with_filter` on the `ReadBuilder`:
+
+```python
+rb = table.new_read_builder()
+rb.with_filter({"method": "equal", "field": "id", "literals": [1]})
+
+# Continue with scan-then-read...
+```
+
+### Compound Predicates
+
+Combine predicates with `"and"` / `"or"` methods using a `"children"` list:
+
+```python
+# id >= 1 AND id <= 3
+rb.with_filter({
+    "method": "and",
+    "children": [
+        {"method": "greaterOrEqual", "field": "id", "literals": [1]},
+        {"method": "lessOrEqual", "field": "id", "literals": [3]},
+    ]
+})
+
+# (id = 1 OR id = 2) AND name = "alice"
+rb.with_filter({
+    "method": "and",
+    "children": [
+        {
+            "method": "or",
+            "children": [
+                {"method": "equal", "field": "id", "literals": [1]},
+                {"method": "equal", "field": "id", "literals": [2]},
+            ]
+        },
+        {"method": "equal", "field": "name", "literals": ["alice"]},
+    ]
+})
+```
+
+### Supported Literal Types
+
+Literal values are automatically converted from Python types based on the 
column's declared schema type:
+
+| Python Type                    | Paimon Type          |
+|--------------------------------|----------------------|
+| `bool`                         | Bool                 |
+| `int`                          | TinyInt / SmallInt / Int / BigInt |
+| `int` / `float`                | Float / Double       |
+| `str`                          | String               |
+| `datetime.date`                | Date                 |
+| `datetime.time` (naive)        | Time                 |
+| `datetime.datetime` (naive)    | Timestamp            |
+| `datetime.datetime` (aware)    | LocalZonedTimestamp  |
+| `decimal.Decimal` / `int`      | Decimal              |
+
+For temporal types, ensure the Python object matches the column type exactly:
+
+```python
+import datetime
+from decimal import Decimal
+
+# Date
+{"method": "equal", "field": "dt", "literals": [datetime.date(2024, 1, 1)]}
+
+# Decimal at scale 2 — int or Decimal accepted
+{"method": "equal", "field": "amount", "literals": [Decimal("123.45")]}
+
+# Naive datetime for TIMESTAMP (no timezone)
+{"method": "equal", "field": "ts", "literals": [datetime.datetime(2024, 1, 1, 
12, 0, 0)]}
+
+# Aware datetime for TIMESTAMP WITH LOCAL TIME ZONE
+from zoneinfo import ZoneInfo
+{"method": "equal", "field": "ts_ltz", "literals": [
+    datetime.datetime(2024, 1, 1, 12, 0, 0, tzinfo=ZoneInfo("Asia/Shanghai"))
+]}
+```
+
+## Time Travel
+
+Use scan options on `new_read_builder` to read a table at a specific point in 
time:
+
+```python
+# By snapshot ID
+rb = table.new_read_builder({"scan.snapshot-id": "1"})
+
+# By timestamp (epoch millis)
+rb = table.new_read_builder({"scan.timestamp-millis": "1700000000000"})
+
+# By version
+rb = table.new_read_builder({"scan.version": "3"})
+
+# By tag name
+rb = table.new_read_builder({"scan.tag-name": "release-1.0"})
+```
+
+!!! warning
+    Only one time-travel selector may be set. Providing multiple selectors 
will raise a `ValueError`.
+
+## Table Inspection
+
+Inspect snapshots, tags, and partition statistics on a table:
+
+```python
+# Latest snapshot
+snap = table.latest_snapshot()
+if snap:
+    print(f"Snapshot {snap.id()} at {snap.commit_time_ms()}")
+    print(f"  commit kind: {snap.commit_kind()}")
+    print(f"  total records: {snap.total_record_count()}")
+
+# All snapshots (newest first)
+for snap in table.list_snapshots():
+    print(snap.id(), snap.commit_kind())
+
+# Tags
+for tag in table.list_tags():
+    print(tag.name(), tag.snapshot_id())
+
+# Partition stats
+for stat in table.partition_stats():
+    print(stat.partition(), stat.record_count(), stat.total_size_bytes())
+```
+
+## Python UDF
+
+Register Python scalar UDFs into a `SQLContext`:
+
+```python
+from pypaimon_rust.datafusion import SQLContext, udf
+import pyarrow as pa
+
+ctx = SQLContext()
+ctx.register_catalog("paimon", {"warehouse": "/tmp/paimon-warehouse"})
+
+batch = pa.record_batch([[1, None, 3]], names=["id"])
+ctx.register_batch("my_temp", batch)
+
+def plus_ten(values):
+    # values is a PyArrow Array (not a tuple)
+    return pa.array(
+        [None if value is None else value + 10 for value in 
values.to_pylist()],
+        type=pa.int64(),
+    )
+
+ctx.register_udf(udf(plus_ten, [pa.int64()], pa.int64(), "volatile", 
"plus_ten"))
+
+batches = ctx.sql(
+    "SELECT plus_ten(id) AS id FROM paimon.default.my_temp ORDER BY id"
+)
+for batch in batches:
+    print(batch)
+```
+
+## Complete Example
+
+```python
+import pyarrow as pa
+from pypaimon_rust.datafusion import PaimonCatalog, SQLContext
+
+# 1. Write data via SQL
+ctx = SQLContext()
+ctx.register_catalog("paimon", {"warehouse": "/tmp/paimon-warehouse"})
+ctx.sql("CREATE SCHEMA paimon.wdb")
+ctx.sql("CREATE TABLE paimon.wdb.t (id INT, name STRING)")
+
+# 2. Write using the programmatic API
+catalog = PaimonCatalog({"warehouse": "/tmp/paimon-warehouse"})
+table = catalog.get_table("wdb.t")
+
+batch = pa.record_batch(
+    [pa.array([1, 2, 3], pa.int32()), pa.array(["alice", "bob", "carol"], 
pa.string())],
+    names=["id", "name"],
+)
+wb = table.new_write_builder()
+write = wb.new_write()
+write.write_arrow(batch)
+wb.new_commit().commit(write.prepare_commit())
+
+# 3. Read with projection and filter
+rb = table.new_read_builder()
+rb.with_projection(["id", "name"])
+rb.with_filter({"method": "greaterThan", "field": "id", "literals": [0]})
+
+scan = rb.new_scan()
+plan = scan.plan()
+splits = plan.splits()
+
+read = rb.new_read()
+for batch in read.read(splits):
+    tbl = pa.Table.from_batches([batch]).sort_by("id")
+    print(tbl)
+
+# 4. Read via SQL
+for batch in ctx.sql("SELECT id, name FROM paimon.wdb.t ORDER BY id"):
+    print(batch)
+```

Reply via email to