From 2b8ac80d3e37d39349dbd594f904d8d7edca95eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dapeng=20Sun=28=E5=AD=99=E5=A4=A7=E9=B9=8F=29?= Date: Sat, 17 Oct 2026 10:27:28 +0800 Subject: [PATCH 1/2] [python] Plan natively with pypaimon-rust releases that lack Plan.stats() #10609 calls rust_plan.stats(), which pypaimon-rust 0.4.0, the release the Python test jobs install, does not have. The AttributeError sends every native plan back to the Python scanner, so ContiguousWindowPlanTest.test_native_plan_retains_independent_payload_column_files fails on master. Read the counters only when the binding has them, as snapshot_id already does; such plans report no native statistics. --- paimon-python/pypaimon/read/native_plan.py | 4 +++- paimon-python/pypaimon/tests/native_plan_test.py | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/paimon-python/pypaimon/read/native_plan.py b/paimon-python/pypaimon/read/native_plan.py index f968f8515492..2ce38bd28441 100644 --- a/paimon-python/pypaimon/read/native_plan.py +++ b/paimon-python/pypaimon/read/native_plan.py @@ -577,4 +577,6 @@ def _from_native_plan(table, rust_plan) -> Plan: # Older bindings cannot distinguish an empty committed snapshot from a # table without snapshots. Let the Python scanner recover the metadata. raise RuntimeError("Native runtime cannot report an empty plan's snapshot") - return Plan(splits, snapshot_id=snapshot_id, _native_stats=rust_plan.stats()) + # Bindings before Plan.stats() plan natively but report no counters. + stats = getattr(rust_plan, 'stats', None) + return Plan(splits, snapshot_id=snapshot_id, _native_stats=stats() if callable(stats) else None) diff --git a/paimon-python/pypaimon/tests/native_plan_test.py b/paimon-python/pypaimon/tests/native_plan_test.py index 5e1adde4ad47..3a7916bcef5f 100644 --- a/paimon-python/pypaimon/tests/native_plan_test.py +++ b/paimon-python/pypaimon/tests/native_plan_test.py @@ -125,6 +125,15 @@ def test_native_final_statistics_preserve_work_and_saturate_unknown_sizes(self): with patch.object(ScanStats, 'from_native', return_value=ScanStats.empty_native()): self.assertEqual(zero.stats().planning_counters['limit'], 0) + def test_bindings_without_plan_stats_still_plan_natively(self): + from pypaimon.read.native_plan import _from_native_plan + table = Mock(table_schema=SimpleNamespace(fields=[], partition_keys=[]), + trimmed_primary_keys_fields=[]) + table.options.native_read_enabled.return_value = False + plan = _from_native_plan(table, SimpleNamespace(splits=lambda: [], snapshot_id=lambda: 7)) + self.assertEqual(plan.snapshot_id, 7) + self.assertIsNone(plan.stats()) + def test_variant_read_type_matches_java_interop_fixture(self): actual = _variant_read_type_json( [DataField(7, 'payload', AtomicType('VARIANT'))], From 5fc3201f3d84bbc8f440a242608cc9094eb107e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dapeng=20Sun=28=E5=AD=99=E5=A4=A7=E9=B9=8F=29?= Date: Sat, 17 Oct 2026 13:18:59 +0800 Subject: [PATCH 2/2] [ci] Plan natively with the released pypaimon-rust 0.4.0 in CI Native CI builds pypaimon-rust from paimon-rust main, and Python Tests skip native planning tests, so no job forces native planning through a released binding. That is how #10609 broke 0.4.0 without notice: every native plan failed on Plan.stats() and silently fell back to the Python scanner. Native CI / pypaimon-rust 0.4.0 installs the release from PyPI and runs the regular suite with native planning forced. Tests marked native_plan need Rust main, so they stay in the main job. On the previous commit this job fails test_native_plan_retains_independent_payload_column_files and plans natively only twice. --- .github/workflows/ci-python.yml | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml index b6a0fb9f81dc..71c0c31a9b53 100644 --- a/.github/workflows/ci-python.yml +++ b/.github/workflows/ci-python.yml @@ -263,6 +263,44 @@ jobs: -k 'TIMESTAMP or temporal' done + # Released bindings lack Rust main features, so only native planning is forced here. + rust-release-plan: + name: Native CI / pypaimon-rust ${{ matrix.pypaimon-rust }} + timeout-minutes: 90 + runs-on: ubuntu-latest + container: "python:3.11-slim" + strategy: + fail-fast: false + matrix: + pypaimon-rust: [ '0.4.0' ] + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Install system dependencies + run: | + apt-get update && apt-get install -y --no-install-recommends \ + build-essential git curl cmake pkg-config libssl-dev libclang-dev tzdata \ + && rm -rf /var/lib/apt/lists/* + + - name: Install Python test dependencies + run: | + python -m pip install --upgrade pip + python -m pip install torch --index-url https://download.pytorch.org/whl/cpu + python -m pip install -e ./paimon-python --group ./paimon-python/pyproject.toml:dev \ + 'pypaimon-rust==${{ matrix.pypaimon-rust }}' + python -m pip check + python -c "from importlib.metadata import version; print('pypaimon-rust', version('pypaimon-rust'))" + + - name: Run Python tests with native planning + shell: bash + env: + PYPAIMON_TEST_NATIVE_PLAN: '1' + # Tests marked native_plan need Rust main. + PYTEST_ADDOPTS: "-m 'not native_plan'" + run: | + bash paimon-python/dev/lint-python.sh -i pytest + torch: name: PyTorch / Python 3.10 timeout-minutes: 60