Experimental Polars reasoning

gestaltdb.reasoning.adapters.polars.PolarsDatalogPrototype is an explicit, optional experiment for the existing binary, positive, named-entity Datalog profile. Install gestaltdb[polars]. The default remains DatalogEngine; there is no automatic backend selection, source extraction or publication. The adapter/compiler import boundary remains inward; only this optional adapter loads Polars and Arrow, on demand.

from gestaltdb.reasoning import Atom, Fact, Program, ReasoningLimits, Rule, Variable
from gestaltdb.reasoning.adapters.polars import PolarsDatalogPrototype
from gestaltdb.reasoning.protocols import InputContext

x, y, z = Variable("x"), Variable("y"), Variable("z")
program = Program([Rule("transitive", [Atom(x, "p", y), Atom(y, "p", z)], Atom(x, "p", z))])
run = PolarsDatalogPrototype().evaluate(
    program, [Fact("a", "p", "b"), Fact("b", "p", "c")],
    context=InputContext("in-memory-example", "default", "none", "all", "identity"),
    limits=ReasoningLimits(), max_intermediate_rows=100_000,
)
assert Fact("a", "p", "c") in run.result
assert run.result.proofs is None
assert run.identity.executor_version == "polars-prototype-v1"
assert run.metrics["join_calls"] > 0

PolarsRun contains an ordinary complete EvaluationResult plus an exact CompilationIdentity and immutable telemetry. It is deliberately not the existing Evaluator protocol: witnesses, all-support provenance, incremental maintenance, authenticated source evaluation and persisted generations are not implemented. collect_proofs=True fails before input consumption. Evidence inputs affect logical truth as facts only; references are not returned as proofs. Do not silently substitute this prototype into claims/publication adapters.

Execution and semantic boundary

Input conversion is bounded Python work: deduplicate facts, intern full entity/ predicate/constant names with the RE-02 dictionary and transfer compact uint64 relations through Arrow. Final decoding iterates result rows in Python. These boundary costs are measured, not described as zero-copy KV decoding.

Between boundaries, Polars performs filters, repeated-variable equality checks, projection, exact equijoins, distinct, union and stable-minus-new anti-joins. There are no Python per-row callbacks or dictionary conversions inside these operators. Each body occurrence can serve as the designated delta relation; identical repeated atoms need only one pivot. A common unit column preserves ground-atom truth and disconnected-body joins. All new facts are visible in the next synchronous round, never midway through an active round. Results have exact set semantics and canonical external-ID ordering, independent of native row order.

Internal semi/anti-join primitives are existence/set operations, not exposed negation-as-failure semantics. Literals, arbitrary-arity execution, negative premises, aggregates and equality canonicalization remain unsupported. Safe Program construction and v1 digests are unchanged. Small finite grounding oracles, generated programs, late-delta/repeated/ground/disconnected bodies and analytical ontology closures test the selected semantics, not full OWL support.

The prototype rebuilds eager native expression/join plans per rule occurrence and round. It does not cache lazy plans, reorder body joins by cost, implement SCC scheduling or offer native proof propagation. Telemetry separates input, conversion, dispatch planning, per-occurrence expression construction, operator execution, join-count preflight, fixpoint and decoding. Nested timings overlap; do not sum fixpoint and operator/preflight durations as independent stages. planning_s measures Python dispatch construction; expression_s measures Python expression construction. Eager native query planning and kernel execution remain combined in operator_s/preflight_s; these are not native compiler instrumentation or isolated join-kernel timings.

Limits are intentionally different

ReasoningLimits.max_input_facts, max_rules, max_iterations and max_facts retain their input/output meaning. Duplicate evidence counts toward input bounds; failures return no successful partial run and never publish data.

max_candidate_rows instead counts conservative materialized/output row work: atom input upper bounds, join outputs, projections, filter-join input upper bounds and concatenations. This is not attempted-unification parity with the indexed Python engine, and can fail earlier even for the same successful logical closure. The benchmark reports actual counts separately; do not compare limited runs as speedups. Proof budgets have no effect because proof mode is rejected.

Before an equijoin, a native grouped multiplicity query counts matching outputs. max_intermediate_rows is checked before allocating that output. Key-count query state is bounded by input rows, not the potentially quadratic result. Native count overflow fails explicitly. Selection/concatenation use conservative row upper bounds. This protects obvious Cartesian explosions without claiming exact allocator, hash-table, dictionary, string-size, RSS or disk limits. No spill is implemented. A running native call is not cooperatively interrupted; use a separate worker for hard deadlines and external memory controls.

Context identities are compatibility keys supplied by an already validated caller, not authenticated database horizons. A new invocation rebuilds its state; different contexts/dictionaries never share caches implicitly. No PyRex scan API, storage dependency or JVM runtime is required by this prototype.

Measurements and decision

Extend the existing analytical comparison runner rather than introduce a second oracle. For example:

uv run --with pyarrow --with polars --with clingo python -m benchmarks.ontology --backends native polars clingo --sizes 16 128 --repeats 3 --threads 1 --temp-root /tmp/opencode --output /tmp/opencode/re03-comparison.json
uv run --with pyarrow --with polars --with clingo python -m benchmarks.ontology --backends native polars clingo --workloads hierarchy recursive duplicates --sizes 1024 --repeats 3 --threads 4 --temp-root /tmp/opencode --output /tmp/opencode/re03-sparse.json

The runner now records thread caps for Polars/OpenMP/BLAS, per-stage telemetry, runtime installed-file footprint and source digests. Every successful row checks the identical analytical closure and digest. native retains the historical benchmark name for indexed Python, not a compiled extension. Clingo remains a benchmark-only dependency without witness/resource-accounting equivalence.

Clean-source 14ade5a results (Python 3.13, three fresh workers per cell; indexed Python and Polars receive identical independent Evidence wrappers):

Workload

Indexed Python

Polars

Clingo

Dense N=128

534.73ms

46.44ms

54.29ms

Sparse recursive N=1024

90.37ms

40.40ms

33.03ms

Hierarchy N=1024

17.66ms

21.27ms

18.22ms

Duplicate evidence N=1024

16.42ms

21.12ms

25.69ms

These are median pipeline times including ontology compilation, initial fact conversion, full prototype conversion/fixpoint/decoding, but excluding separately measured package/runtime imports and fixture construction. Small N=16 cases were consistently slower with Polars. Peak full-worker RSS in the comparison was about 127 MiB for Polars versus 54 MiB indexed Python. Four threads did not materially improve the tested N=1024 sparse workloads; the one- and four-thread matrices were rerun sequentially on the same shared host (not a dedicated CPU-isolated machine).

This supports continuing the columnar experiment, not switching the default. No million-row, universal scaling, proof-mode or maintenance-performance claim is made. Full RE-06 backend selection still needs representative user workloads, source extraction, footprint, failure/cancellation and supported-mode evidence. The compact Python batch container has no independent fixpoint executor to time; its conversion/transfer costs are measured in the RE-02 report instead.