Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,13 @@
- Members marked `[forced]` are adapter contract overrides required by `test_strict_adapter.py`. Record other intentional mirrors in `[dupes]` in `fensu.toml` with a reason.
- Public adapter contract methods are duplicated per adapter on purpose: `test_strict_adapter.py` requires every first-class adapter to define them, and `[dupes]` exempts them. The private helpers behind them must not be copied. Keep them in shared builders, such as the snapshot SQL builders in `src/sqlbuild/adapter/contract/classes/`, and let each contract method delegate. Express real dialect differences as explicit parameters, such as a `SnapshotSqlDialect` field, not as a forked copy of the helper.

## Rust-Hosted Migration

- Rust is moving to host compile, planning and non-warehouse CLI work. Python stays only for the modules listed in `python-boundary.toml`: user Python code, adapters, the executor and public Python APIs.
- A migration change deletes the Python it replaces in the same pull request and lowers `remaining_lines`. Never keep a parallel Python implementation, and never fall back to Python after a native failure; surface the failure as an error.
- Adding a `stays` or `handled_native_errors` entry, or raising `remaining_lines`, requires a stated reason in the pull request and explicit user approval. Difficulty or a small measured speed gain is not a reason.
- Moving code into a path listed in `stays`, or behind a reviewed handler, is not migration progress; only code that now runs in Rust lowers `remaining_lines`.

## Bounded Review Process

Orchestrating agents must run reviews as a single bounded cycle, not an open-ended loop:
Expand Down
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
SHELL := /bin/bash

.PHONY: verify verify-quick verify-pg coverage cli-preview rust-check check-native-layering \
.PHONY: verify verify-quick verify-pg coverage cli-preview rust-check check-native-layering check-python-boundary \
check-e2e-shards test-e2e-duckdb test-e2e-duckdb-build-core \
test-e2e-duckdb-build-incremental \
test-e2e-duckdb-cli-data test-e2e-duckdb-cli test-e2e-duckdb-inspection \
Expand Down Expand Up @@ -604,7 +604,7 @@ verify-quick:
exit $$status


check-ci: check-e2e-shards check-native-layering
check-ci: check-e2e-shards check-native-layering check-python-boundary
uv run ruff format --check .
uv run ruff check .
uv run ty check src tests
Expand All @@ -616,6 +616,10 @@ check-native-layering:
uv run python -m scripts.check_native_layering


check-python-boundary:
uv run python -m scripts.check_python_boundary


check-pr-metadata:
@test -n "$(PR_TITLE)" || { echo "PR_TITLE is required" >&2; exit 2; }
@test -n "$(PR_BODY_FILE)" || { echo "PR_BODY_FILE is required" >&2; exit 2; }
Expand Down
221 changes: 221 additions & 0 deletions python-boundary.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
# Python that stays once Rust hosts SQLBuild. Every other Python file under src/sqlbuild is
# migration work that moves to Rust. `remaining_lines` must equal the current count of that work:
# a PR that moves code lowers it, and adding movable Python fails the check.
# Native failures must surface as errors; `handled_native_errors` lists the reviewed exceptions.
remaining_lines = 146314

[[stays]]
path = "executor"
reason = "executor"
note = "Runs plans against warehouses; a separate later decision."

[[stays]]
path = "adapters"
reason = "adapter"
note = "Built-in warehouse adapters that project adapters subclass and override."

[[stays]]
path = "adapter/contract"
reason = "adapter"
note = "BaseAdapter and StrictAdapter, the documented classes project adapters subclass."

[[stays]]
path = "adapter/discovery"
reason = "user-code"
note = "Imports project adapter modules."

[[stays]]
path = "adapter/relations"
reason = "adapter"
note = "Relation inspection wrappers adapters call, with per-connection caches."

[[stays]]
path = "adapter/state_sql"
reason = "adapter"
note = "State-table SQL rendered through adapter callbacks."

[[stays]]
path = "adapter/type_system"
reason = "adapter"
note = "Type normalisation adapters import; already a thin native wrapper."

[[stays]]
path = "provider"
reason = "user-code"
note = "Provider base class users subclass, and injection into user functions."

[[stays]]
path = "python_nodes"
reason = "user-code"
note = "Decorators that run when user modules are imported."

[[stays]]
path = "microbatches"
reason = "executor"
note = "Microbatch state store the executor and adapters use."

[[stays]]
path = "rules"
reason = "public-api"
note = "Public custom-rule API and test helper."

[[stays]]
path = "rule_engine/_helpers/host"
reason = "user-code"
note = "Subprocess host that loads and runs user rule modules."

[[stays]]
path = "rule_engine/classes/rule_context.py"
reason = "user-code"
note = "The context object user rules receive."

[[stays]]
path = "runtime/observability"
reason = "public-api"
note = "Documented observability API used throughout the executor."

[[stays]]
path = "runtime/execution_history"
reason = "public-api"
note = "Documented SQLite and Postgres execution-history backends."

[[stays]]
path = "runtime/execution_limits"
reason = "executor"
note = "Deadline the executor scheduler and warehouse cursors enforce."

[[stays]]
path = "integrations/dagster"
reason = "public-api"
note = "Documented orchestrator integration."

[[stays]]
path = "integrations/rivers"
reason = "public-api"
note = "Documented orchestrator integration."

[[stays]]
path = "integrations/dlt"
reason = "user-code"
note = "Loader runtime that runs inside user loaders."

[[stays]]
path = "integrations/ingestr"
reason = "user-code"
note = "Loader runtime that runs inside the executor load phase."

[[stays]]
path = "compiler/python_nodes/_helpers/identity.py"
reason = "user-code"
note = "Identity hashing over live user functions (source and globals)."

[[stays]]
path = "compiler/discovery/_helpers/filesystem/command_output_sinks.py"
reason = "user-code"
note = "Inspects loaded user sink functions."

[[stays]]
path = "compiler/pipeline/_helpers/materializations.py"
reason = "user-code"
note = "Imports user materialization modules."

[[stays]]
path = "assets.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "audits.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "checks.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "compute_logs.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "execution_history.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "factories.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "functions.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "hooks.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "loaders.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "observability.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "postgres_history.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "providers.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "refs.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "retries.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "sinks.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "sqlite_history.py"
reason = "public-api"
note = "Public facade."

[[stays]]
path = "tasks.py"
reason = "public-api"
note = "Public facade."

[[handled_native_errors]]
location = "compiler/macro_bridge/_helpers/store_keys.py::macro_store_token"
note = "A macro without a stable key is not stored; no Python implementation runs."

[[handled_native_errors]]
location = "compiler/macro_bridge/_helpers/store_keys.py::context_store_token"
note = "A context without a stable key is not stored; no Python implementation runs."

[[handled_native_errors]]
location = "lint/_helpers/native_sql.py::_native_responses"
note = "A native lint failure is reported as a lint error result, not recomputed in Python."

[[handled_native_errors]]
location = "rule_engine/_helpers/run/rules.py::_cached_sql_findings"
note = "An undecodable cached finding is a cache miss; the rules run again natively."
14 changes: 14 additions & 0 deletions scripts/check_python_boundary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Direct wrapper for the Python boundary ratchet."""

from __future__ import annotations

from scripts.python_boundary.main.check import run_python_boundary_check


def main(argv: list[str] | None = None) -> int:
"""Check the Python boundary ratchet."""
return run_python_boundary_check(argv)


if __name__ == "__main__":
raise SystemExit(main())
1 change: 1 addition & 0 deletions scripts/python_boundary/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Python boundary ratchet for the Rust-hosted migration."""
1 change: 1 addition & 0 deletions scripts/python_boundary/_helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Python boundary ratchet helpers."""
26 changes: 26 additions & 0 deletions scripts/python_boundary/_helpers/lines.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Count the Python lines that remain to move to Rust."""

from __future__ import annotations

from pathlib import Path

from scripts.python_boundary.constants import SOURCE_ROOT
from scripts.python_boundary.models import StayEntry


def tracked_python_files(*, root: Path, stays: tuple[StayEntry, ...]) -> tuple[Path, ...]:
"""Every Python file under the source root outside the modules that stay Python."""
source: Path = root / SOURCE_ROOT
staying: tuple[Path, ...] = tuple(source / entry.path for entry in stays)
return tuple(
path for path in sorted(source.rglob("*.py")) if not _stays(path=path, staying=staying)
)


def _stays(*, path: Path, staying: tuple[Path, ...]) -> bool:
return any(path == stay or stay in path.parents for stay in staying)


def count_lines(paths: tuple[Path, ...]) -> int:
"""The total physical line count of `paths`."""
return sum(len(path.read_text(encoding="utf-8").splitlines()) for path in paths)
Loading
Loading