Minimal demand-driven query framework for incremental computation.
Project description
Cascade Query
PyPI: query-cascade
Cascade Query is a small Python library for incremental, on-demand computation. You write normal functions; the engine figures out what depends on what, caches results, and recomputes only what changed.
It fits the same mental space as a build system or an IDE’s analysis pipeline: lots of derived values, inputs that change in small steps, and a graph of steps you do not want to rerun from scratch every time.
What you get
| Capability | What it means in practice |
|---|---|
| Lazy evaluation | Nothing runs until something asks for a result. |
| Dependency tracking | The engine records which queries read which inputs and other queries. |
| Targeted invalidation | After a change, only downstream work that is still needed gets redone. |
| Deduplication | Identical in-flight requests share one computation. |
| Snapshots | Readers can pin a consistent view while inputs keep moving. |
| Background work | Submit work in the background; stale work can be cancelled safely. |
| Replayable side effects | Effects recorded through accumulators replay on cache hits so diagnostics stay consistent. |
| Save / load | Persist graph and cache state (SQLite-backed API below). |
| Inspection | Inspect the graph and trace events for debugging. |
Quickstart
from cascade import Engine
engine = Engine()
@engine.input
def text() -> str:
return ""
@engine.query
def lint_count() -> int:
return text().count("TODO")
text.set("TODO: one\nTODO: two")
assert lint_count() == 2
# Same value → no need to recompute downstream work.
text.set("TODO: one\nTODO: two")
assert lint_count() == 2
Install (free-threaded Python)
The library targets free-threaded CPython 3.14 so CPU-bound parallel work can scale without fighting the GIL. The package itself is pure Python; what matters is the interpreter you run with.
Windows
-
Install free-threaded Python 3.14 from python.org/downloads/windows
- Look for the build that includes free-threaded (
python3.14t/py -3.14t). - In the full installer, enable the free-threaded binaries option if needed.
- Look for the build that includes free-threaded (
-
Install from PyPI:
py -3.14t -m pip install -U query-cascade
- Confirm import and GIL-off mode (example):
py -3.14t -X gil=0 -c "import cascade, sys, sysconfig; print('cascade import ok from', cascade.__file__); print('Py_GIL_DISABLED=', sysconfig.get_config_var('Py_GIL_DISABLED')); print('GIL enabled?', sys._is_gil_enabled())"
You want to see Py_GIL_DISABLED= 1 and GIL enabled? False when exercising the free-threaded + no-GIL path.
Editable install (from this repo)
python3.14t -m pip install -e ".[dev]"
Minimal API example
from cascade import Engine
engine = Engine()
warnings = engine.accumulator("warnings")
@engine.input
def source(file_id: str) -> str:
return ""
@engine.query
def parse(file_id: str) -> tuple[str, ...]:
return tuple(line.strip() for line in source(file_id).splitlines() if line.strip())
@engine.query
def symbols(file_id: str) -> tuple[str, ...]:
return tuple(row.split("=")[0].strip() for row in parse(file_id))
API surface (cheat sheet)
engine.input(fn)— Mutable roots; call.set(...)to publish new revisions.engine.query(fn)— On-demand queries with memoization; dependencies are captured automatically.engine.accumulator(name)— Thread-safe channel for side effects that the engine replays on cache hits.engine.snapshot()— Immutable read view (Snapshot) for snapshot-style isolation.engine.submit(query, *args, snapshot=...)— Background execution; work can be cancelled if inputs move on (QueryCancelled).engine.compute_many([(query, args), ...], workers=N)— Parallel run with a work-stealing scheduler.engine.inspect_graph()/engine.traces()— Graph and trace introspection.engine.save(path)/engine.load(path)— Persist or restore state (SQLite).engine.prune(roots)— Drop memoized nodes not reachable from the given roots.
Guarantees (the short version)
- Incremental updates: Stale paths recompute; work that is still valid is reused.
- Selective invalidation: When a child signals “unchanged,” parents can stay valid without redoing everything above (the engine’s red/green style early bailout).
- One compute, many waiters: Concurrent identical requests share a single in-flight run.
- Cycles: Recursive query cycles raise
CycleError(there is no fixed-point solver in core). - Stale background work: Obsolete background queries raise
QueryCancelled.
Limitations (read before you bet the farm)
- Interpreter: Best results for parallel CPU work come from free-threaded 3.14 with the GIL disabled at runtime. Other interpreters work for correctness, but threaded speedups may disappoint. Building or publishing the package from a non-free-threaded Python does not block users on free-threaded Python—there is no native extension ABI tied to GIL mode.
- Persistence:
save/loadare point-in-time snapshots, not a multi-process transactional store with WAL semantics. - Trust
loadlike code: Snapshots use versioned JSON; types are resolved viaimportlib. Only load files from sources you trust (similar caution to pickle or “data that names types”). - Side effects: Only effects sent through
Accumulatorare replayed. Prints, network I/O, or writes inside query bodies are not magically replayed. - Cycles: Dynamic cycles are detected and rejected; cyclic dataflow is not solved to a fixed point inside this library.
Deliberately out of scope (for a small core)
- Nominal interning (
@interned), tracked structs (@tracked) - Fixed-point solvers for cyclic graphs
- Distributed or shared cache protocols
You can add these on top of the same query model if you need them.
Is this a good fit? (checklist)
Strong fits look like graphs of mostly pure steps over inputs that change a little at a time, with expensive recomputation if you always recompute everything.
Answer Yes/No:
- Can you express the logic as inputs → derived queries (
A → B → C)? - Are derived values mostly pure functions of tracked inputs (side effects routed through accumulators)?
- Do you ask the same queries repeatedly?
- Do inputs usually change in small increments rather than full replacement every time?
- Is a full recompute noticeably expensive?
- Do you need precise invalidation (only affected downstream nodes)?
- Do concurrent callers often request the same keys?
- Do some readers need a stable snapshot while writes continue?
- Can background work become waste when new writes land?
- Would graph/tracing help you debug cache hits and invalidation?
Rough read: 9–10 Yes → excellent · 7–8 → strong · 5–6 → try a spike · 0–4 → probably not this abstraction.
Problems that usually score high
- Incremental IDE analysis (parse, index, diagnostics, code actions)
- Monorepo “what tests ran” / impact planners
- Incremental static analysis or security scanning
- Compilers / DSL pipelines with live diagnostics and warning replay
- Policy-as-code over IaC (re-eval only what changed)
- Feature flags / entitlements from layered config
- Schema evolution impact
- Derived metrics / analytics compilers
- Build or asset graphs
- Rules engines with heavy duplicate concurrent requests
Examples (in examples/)
| Script | What it shows |
|---|---|
compiler_pipeline.py |
source → parse → symbols → typecheck, warnings accumulator, cache-hit narration |
dynamic_macro_expansion.py |
Query that changes downstream dependencies at runtime |
snapshot_isolation.py |
Snapshot reads while live inputs change |
concurrent_background_work.py |
Dedup under concurrency + cancellation after input changes |
persistence_and_inspection.py |
Save/load and graph summaries |
gil_parallel_speedup.py |
Threaded CPU benchmark: GIL vs free-threaded |
Run one:
python3.14t examples/compiler_pipeline.py
Run all (Unix-style shell):
for example in examples/*.py; do
echo "Running $example"
python3.14t "$example"
done
Examples print narration as they run so you can follow each behavior.
Compare GIL vs free-threaded (same machine)
Install both 3.14 and 3.14t if you want apples-to-apples. On Ubuntu (deadsnakes):
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install -y python3.14 python3.14-venv python3.14t python3.14t-venv
python3.14 -m pip install -e .
python3.14t -m pip install -e .
Quick check on free-threaded build:
python3.14t -c "import sys, sysconfig; print('Py_GIL_DISABLED=', sysconfig.get_config_var('Py_GIL_DISABLED')); print('GIL enabled?', sys._is_gil_enabled())"
Same interpreter, toggle GIL at runtime:
PYTHON_GIL=1 python3.14t examples/gil_parallel_speedup.py --workers 8 --tasks 96 --rounds 300000 --repeats 5
PYTHON_GIL=0 python3.14t examples/gil_parallel_speedup.py --workers 8 --tasks 96 --rounds 300000 --repeats 5
Or compare python3.14 vs PYTHON_GIL=0 python3.14t on the same script.
Compare median parallel seconds (lower is better) and threaded speedup in this runtime (higher is better). Keep args identical, reduce background load, and use --repeats (e.g. 5) to smooth noise. On multi-core machines, free-threaded + GIL off usually wins clearly for this CPU-bound demo.
Persistence and inspection
engine.save("state.db")
engine.load("state.db")
print(engine.inspect_graph())
for event in engine.traces():
print(event.event, event.key, event.detail)
Design stance
The core is intentionally minimal: pull-based evaluation, dependency capture, red/green style bailout, dedup, snapshots, cancellation, accumulator replay, tracing, and persistence. That set is enough for many real pipelines without baking in advanced internals (e.g. fixed-point cycle solving or custom AST red/green structures). CPU-bound parallelism is expected to matter when you use free-threaded CPython with the GIL disabled.
Development
Tests (match main CI)
export PYTHON_GIL=0 # Windows: set PYTHON_GIL=0
python3.14t -m pip install -e ".[dev]"
python3.14t -c "import sys, sysconfig; print('Py_GIL_DISABLED=', sysconfig.get_config_var('Py_GIL_DISABLED')); print('GIL enabled?', sys._is_gil_enabled())"
python3.14t -m pytest -q \
--ignore=tests/test_performance.py \
--cov=src/cascade \
--cov-branch \
--cov-report=term-missing \
--cov-fail-under=95
Branch coverage check (CI uses an equivalent step on coverage.json):
python3.14t - <<'PY'
import json
with open("coverage.json", encoding="utf-8") as fh:
b = json.load(fh)["totals"]["percent_branches_covered"]
print(f"branch coverage: {b:.2f}%")
assert b >= 90.0
PY
Stateful fuzz:
PYTHON_GIL=0 python3.14t -m pytest -q tests/test_stateful_engine_invariants.py
Mutation testing:
PATH="$HOME/.local/bin:$PATH" PYTHON_GIL=0 mutmut run
PATH="$HOME/.local/bin:$PATH" PYTHON_GIL=0 mutmut results
Use the mutmut CLI (mutmut run), not python -m mutmut run. Bounded local loop:
PYTHON_GIL=0 MUTMUT_MAX_CHILDREN=2 ./scripts/mutation_fast.sh
Focused mutants:
PYTHON_GIL=0 MUTMUT_MAX_CHILDREN=2 ./scripts/mutation_fast.sh "<mutant-name>" "<mutant-name>"
See docs/mutation_triage.md for survivor triage.
Formal model (TLA+)
Specs live under docs/formal/:
docs/formal/cascade_core.tladocs/formal/cascade_core.cfg
Run TLC (example):
java -cp tla2tools.jar tlc2.TLC docs/formal/cascade_core.tla -config docs/formal/cascade_core.cfg
Checked properties include snapshot consistency, active-dependency validity (red/green alignment), and cancellation epoch monotonicity.
Performance suite
Heavy behavior clusters around cache hits vs full recompute, concurrent dedup, compute_many throughput on free-threaded workloads, large-graph mutation vs rebuild, mark-green cost vs depth, and prune scaling.
python -m benchmarks.performance_suite --report-dir artifacts/performance --assert-thresholds
Outputs:
artifacts/performance/performance-report.jsonartifacts/performance/performance-report.md
CI runs the same suite and uploads performance-report.
The compute-many-parallel-speedup scenario (and tests/test_performance.py::test_compute_many_parallel_speedup_scenario) is sensitive to CPU scheduling. On a busy laptop or small VM, thresholds may flap without a real regression. Mitigations:
- Re-run the test, or set
CASCADE_QUERY_PARALLEL_PERF_RETRIES(e.g.3). - To skip while iterating:
CASCADE_QUERY_SKIP_PARALLEL_PERF=1(CI does not set this).
Nightly: .github/workflows/nightly-performance.yml runs a longer sweep (e.g. 8 runs) and publishes nightly-performance-report.
Scale and stress tests
tests/test_scale_behavior.py covers large-graph invalidation, dynamic dependency churn, prune stress, persistence at scale, eviction under churn, and mixed concurrency (submit + compute_many + writes). The heaviest cases are marked @pytest.mark.slow; default pytest skips them via pyproject.toml.
Internal invariants are concentrated in tests/test_internal_invariants.py (via engine._internals) to limit coupling while keeping safety checks.
Default CI-like run (no perf file, no slow):
PYTHON_GIL=0 python3.14t -m pytest -q --ignore=tests/test_performance.py
Slow only:
pytest -q -m slow
Everything including slow:
pytest -q -m "slow or not slow"
CI overview
- Workflow:
.github/workflows/ci.yml(pushes and PRs). - Ruff before tests.
- Separate package build (
python -m build) to catch packaging issues early.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file query_cascade-0.1.31.tar.gz.
File metadata
- Download URL: query_cascade-0.1.31.tar.gz
- Upload date:
- Size: 45.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b0d5d0c326ba10fc358f5304e945ddcba2a66874462be933953afd95385fa81
|
|
| MD5 |
d8b75bb3bf7f070211adf5b9549f2be0
|
|
| BLAKE2b-256 |
51ee19f3736202e677d5745512dc13148b7eea4629f4b4aa7a37d529eed4c4b9
|
Provenance
The following attestation bundles were made for query_cascade-0.1.31.tar.gz:
Publisher:
workflow.yml on hmatt1/cascade-query
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
query_cascade-0.1.31.tar.gz -
Subject digest:
2b0d5d0c326ba10fc358f5304e945ddcba2a66874462be933953afd95385fa81 - Sigstore transparency entry: 1268497094
- Sigstore integration time:
-
Permalink:
hmatt1/cascade-query@a5e70ae85602e5621a645b927c2ef6d63f2c094c -
Branch / Tag:
refs/heads/main - Owner: https://github.com/hmatt1
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@a5e70ae85602e5621a645b927c2ef6d63f2c094c -
Trigger Event:
push
-
Statement type:
File details
Details for the file query_cascade-0.1.31-py3-none-any.whl.
File metadata
- Download URL: query_cascade-0.1.31-py3-none-any.whl
- Upload date:
- Size: 22.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
13167365470a87f33e27388962a332c74ddf0467b7a803d96c823543f72bf683
|
|
| MD5 |
b73defad300708867dd6ba1ac7ea57ed
|
|
| BLAKE2b-256 |
f6e4a95fa9d50f18b04db83ad5b232cd8080733127ae3db837e16109401eb0ad
|
Provenance
The following attestation bundles were made for query_cascade-0.1.31-py3-none-any.whl:
Publisher:
workflow.yml on hmatt1/cascade-query
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
query_cascade-0.1.31-py3-none-any.whl -
Subject digest:
13167365470a87f33e27388962a332c74ddf0467b7a803d96c823543f72bf683 - Sigstore transparency entry: 1268497179
- Sigstore integration time:
-
Permalink:
hmatt1/cascade-query@a5e70ae85602e5621a645b927c2ef6d63f2c094c -
Branch / Tag:
refs/heads/main - Owner: https://github.com/hmatt1
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@a5e70ae85602e5621a645b927c2ef6d63f2c094c -
Trigger Event:
push
-
Statement type: