Minimal demand-driven query framework for incremental computation.
Project description
Cascade Query
Cascade Query is a Python library for incremental dependency tracking. It caches function results and re-executes them only when their specific inputs or upstream dependencies change.
1. Introduction & Quickstart
Core Principles
- Automatic Caching: Results are stored. If dependencies are unchanged, the function body does not execute.
- Dependency Tracking: Cascade records every
@engine.inputor@engine.queryaccessed during execution. - Targeted Updates: When an input changes, Cascade identifies and invalidates only the affected downstream functions.
- Early Bail-out: If a function's output remains identical after its dependencies change, re-computation stops for that branch.
Installation
pip install query-cascade
# Or to enable persistent disk caching:
pip install query-cascade[disk]
Quickstart
import time
from cascade import Engine
engine = Engine()
@engine.input
def user_id():
return "user_1"
@engine.query
def fetch_data():
time.sleep(2)
return {"id": user_id(), "data": "value"}
@engine.query
def get_result():
data = fetch_data()
return f"Result for {data['id']}"
# First run: Executes for 2 seconds.
print(get_result())
# Second run: Returns immediately from cache.
print(get_result())
# Update input:
user_id.set("user_2")
# Third run: Executes for 2 seconds to refresh.
print(get_result())
2. Core Concepts
Engine Initialization & Configuration
The Engine manages caching, execution, and state tracking.
Engine(*, max_entries=10000, trace_limit=50000, stats=False, cache_dir=None, cache_map_size=2**30, incremental=True): Initializes the engine.max_entriessets the limit for the Least Recently Used (LRU) in-memory cache.- Passing
cache_direnables persistent disk caching.
Defining Nodes: @engine.input and @engine.query
@engine.input: Decorator for mutable data roots.input.set(value): Updates the value and increments the global revision.input.set(*args, value=value): Updates a keyed input.
@engine.query: Decorator for cached computations. Re-evaluates only if upstream data changes.
Snapshots & Transactions (Input Debouncing)
engine.snapshot(): Returns aSnapshotobject pinning the current global revision. Usequery(snapshot=s)to read data as it existed at that revision.engine.transaction(): When updating multiple inputs, intermediate read states ("flapping") can cause inconsistent evaluations. Use a transaction block to batch updates so they are committed atomically.
with engine.transaction():
theme.set("dark")
layout.set("grid")
3. Execution Modes
Synchronous Execution
Standard functions evaluate eagerly when called.
Asynchronous Execution
Cascade natively supports asynchronous queries via async def. This is extremely useful for I/O-bound workflows (such as making database or network calls). When an asynchronous query evaluates, it runs cooperatively on the active asyncio event loop.
import asyncio
from cascade import Engine
engine = Engine()
@engine.query
async def fetch_user(uid: str):
await asyncio.sleep(0.5) # Network I/O
return {"id": uid, "name": "Alice"}
@engine.query
async def process_user(uid: str):
# Await downstream async queries seamlessly
user = await fetch_user(uid)
return user["name"].upper()
async def main():
print(await process_user("123"))
asyncio.run(main())
Synchronous queries can also be called directly from inside asynchronous nodes (and vice versa). Cascade maintains type stability and performance segregation, ensuring pure-Python CPU-bound tasks suffer no context-switching overhead while I/O-bound tasks evaluate concurrently.
Parallel & Background Execution
engine.submit(query, *args, executor=None): Schedules a query for background execution. Returns aconcurrent.futures.Future.engine.compute_many(calls, workers=None): Executes a list of queries in parallel using a thread pool. Blocks until all are complete and returns a list of results.engine.compute_many_stream(calls, workers=None): Yields completed results as each call finishes. Yields(index, value, call_effects)in completion order.engine.shutdown(*, wait=True, cancel_futures=False): Shuts down the default thread pool executor used bysubmit.QueryCancelled: Exception raised if a background query's dependencies change before it completes.
4. Cache Management & Invalidations
Targeted Updates & Early Bail-out
If a function's logic evaluates to a result identical to its previous run, early bail-out prevents re-computation of downstream queries.
Code-Aware Auto-Invalidation
Cascade automatically inspects the Python bytecode of your @engine.query and @engine.input functions. If you edit a function's logic and the module is hot-reloaded (or you restart your script), Cascade compares the new function's bytecode hash against the previously cached logic. If the logic has changed, Cascade immediately invalidates that function's memory and persistent disk caches.
Time-To-Live (TTL) Caching
Cascade supports expiring cache entries automatically based on wall-clock time. By passing the ttl flag to @engine.query, you can guarantee that a node will recompute its value if it is accessed after the specified number of seconds has elapsed.
@engine.query(ttl=5.0)
def fetch_external_data():
return request.get("https://api.example.com/data")
Error Caching
By default, queries intercept and cache exceptions (excluding control flow exceptions like QueryCancelled).
@engine.query(cache_exceptions=(ValueError, TypeError))
def parse(source: str):
if not source:
raise ValueError("Empty source")
return {"ast": source}
Pass-Through Queries (memoize=False)
For intermediate queries generating large outputs, set memoize=False. This saves significant memory. Downstream nodes will still accurately detect when the query's inputs change, and the unmemoized query will recompute its output on-demand when an active caller needs it.
@engine.query(memoize=False)
def mapped_data() -> list[int]:
data = raw_data()
return [x * 2 for x in data]
Garbage Collection & Pruning
engine.prune(roots, vacuum_disk=False): Removes cached query results from the in-memory LRU cache that are not reachable from the specified roots. Setvacuum_disk=Trueto deep vacuum the persistent LMDB disk cache.engine.access_id: Property returning a monotonically increasing sequence number for memo accesses.engine.sweep_unaccessed(since_access_id): Evicts all memos that haven't been accessed sincesince_access_id. Useful for generational garbage collection.
5. State, Side-Effects & Persistence
Side-Effect Accumulators
Queries must be pure functions. Use Accumulator to record side-effects (like logs or warnings) that must be replayed when a result is served from the cache.
warnings = engine.accumulator("warnings")
@engine.query
def validate_data():
data = fetch_data()
if not data:
warnings.push("No data found")
return data
# On cache hit, 'warnings' are re-populated into the effects dictionary.
effects = {}
validate_data(effects=effects)
print(effects["warnings"])
Persistent Disk Caching
Passing cache_dir to the Engine turns on zero-config persistence. Cascade provisions an embedded LMDB store, serializes query results with a deterministic msgpack encoding, and fingerprints every input value by hashing its serialized bytes with blake2b.
from cascade import Engine
engine = Engine(max_entries=10_000, cache_dir=".cascade_cache")
lmdbandmsgpackare required (pip install query-cascade[disk]).- Values and arguments must be serializable (primitives, bytes, lists, dicts, dataclasses, etc.).
engine.clear_disk_cache(): Deletes every entry in the persistent disk cache.
Export / Import
engine.save(path)/engine.load(path): Persists all inputs and cached results to a SQLite database. Note: Collection event logs are not saved here, use persistent collections instead.
6. Observability & Debugging
Graph Inspection & Visualization
engine.inspect_graph(condense=False): Returns a dictionary of all nodes and edges in the dependency graph.engine.subgraph(roots, direction="deps"): Filters the graph to the dependency chain of the specified root nodes.
Cascade provides renderers for the dependency graph:
from cascade import export_dot, export_mermaid
graph = engine.inspect_graph()
# Generate Graphviz DOT format
dot_text = export_dot(graph)
# Generate Mermaid flowchart format
mermaid_text = export_mermaid(graph)
Performance Metrics
Set stats=True or use engine.enable_stats() to track execution timing.
engine.enable_stats(enabled=True): Dynamically enables or disables performance tracking.engine.stats_summary(): Returns wall-clock time spent in function bodies and cache eviction counts.engine.reset_stats(): Clears accumulated timing data.
Event Tracing
engine.traces(): Returns a list ofTraceEventobjects detailing internal operations (like recomputes, backdates).engine.clear_traces(): Clears the internal trace buffer.
7. Incremental Collections & Native Map/Reduce
CascadeList, CascadeSet, and CascadeDict behave like their builtin counterparts, but every mutation appends a diff to an event log. Queries written with ordinary comprehensions and reducers are rewritten at registration time into incremental pipelines that consume only new diffs. Appending one element to a list of a million reruns your mapping function once.
from cascade import Engine, CascadeList
engine = Engine()
docs = CascadeList(engine, ["alpha", "beta", "gamma"], name="docs")
@engine.query
def long_doc_count():
# Standard Python. Rewritten into filter -> len over the diff stream.
return len([d for d in docs if len(d) > 4])
long_doc_count() # walks all elements once
docs.append("delta") # one diff
long_doc_count() # processes only "delta"
The collections
CascadeListemits insert/update/remove diffs with hidden monotonic uids.CascadeSetemits add/remove diffs.CascadeDictemits upsert/remove diffs.
What gets rewritten
Rewriting covers comprehensions ([...], {...}, {k: v ...}), map, filter, reversed, and reducers: sum, len, min, max, any, all, sorted, list, set, dict, and string-literal .join.
| Reducer | Ingest per diff | Finalize |
|---|---|---|
sum, len, any, all |
O(1) | O(1) |
min, max |
O(log N) | O(1) |
sorted |
O(log N) | O(output) |
list, set, dict, .join |
O(1) | O(output) |
reversed |
O(1) positional flip | inherited |
- Arbitrary
forloops are never rewritten, but invalidate through read tracking. enumerate()andzip()sources are explicitly excluded.- Opt out globally via
Engine(incremental=False)or per-query@engine.query(incremental=False).
Persistence & Inspection
A named collection on an engine with cache_dir event-sources its log to disk automatically.
engine = Engine(cache_dir="./cache")
items = CascadeList(engine, name="items", compact_every=1024)
engine.inspect_pipelines(): Lists every observed pipeline with its source, logical stages, fused stage count, checkpoint revision, and consuming queries.
8. Limitations & Development
Limitations
- Cycle Detection: Cascade detects and rejects recursive function calls (cycles) with a
CycleError. - Thread Safety: While Cascade supports parallel query execution, the
Engineobject itself should be modified (.set(),@engine.query) from a single thread or with external synchronization. - Persistence Security:
engine.load()and the persistent disk cache resolve@dataclassandNamedTupletypes viaimportlib. Only load databases or open cache directories from trusted sources. - Python Version: Optimization for parallel CPU-bound work requires CPython 3.14+ free-threaded builds with
PYTHON_GIL=0. - Collections &
engine.save(): State snapshots viaengine.save()/engine.load()do not carry collection event logs; use a named collection withcache_dirfor durable collection state.
Examples (in examples/)
| Script | What it shows |
|---|---|
compiler_pipeline.py |
source → parse → symbols → typecheck, warnings accumulator, cache-hit narration |
incremental_collections.py |
CascadeList/Set/Dict diffs, comprehension rewriting, O(1) ingest, pipeline inspection |
async_execution.py |
Asynchronous query evaluation and asyncio event loop integration for IO-bound work |
ttl_invalidation.py |
Expiring stale query caches automatically based on wall-clock time |
error_caching.py |
Basic exception caching to prevent repeated re-evaluation on failure |
error_caching_persistence.py |
Disk cache hydration of exceptions across process runs |
code_versioning.py |
Automatic cache invalidation when a function's bytecode logic changes |
pass_through_queries.py |
memoize=False tracking inputs without keeping large outputs in the LRU cache |
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 |
compute_many_with_accumulators.py |
compute_many(..., effects=...) accumulator collection |
persistence_and_inspection.py |
Save/load and graph summaries |
gil_parallel_speedup.py |
Threaded CPU benchmark: GIL vs free-threaded |
Development & Tests
export PYTHON_GIL=0
python3.14t -m pip install -e ".[dev]"
python3.14t -m pytest -q --cov=src/cascade --cov-branch --cov-report=term-missing
- Stateful fuzz:
PYTHON_GIL=0 python3.14t -m pytest -q tests/test_stateful_engine_invariants.py - Formal model: TLA+ specs live under
docs/formal/. - Performance suite:
python -m benchmarks.performance_suite --report-dir artifacts/performance --assert-thresholds
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.3.11.tar.gz.
File metadata
- Download URL: query_cascade-0.3.11.tar.gz
- Upload date:
- Size: 104.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f3b39d1549b02f0255d4b0b03efd56da99003bb380e943d58f6e55b0bd71a8d8
|
|
| MD5 |
52b5f99293816c2dc8c22febbedee079
|
|
| BLAKE2b-256 |
a558d547317f32673dccab1b95dc71aaeea740dae758d406cab700e5c8236ed9
|
Provenance
The following attestation bundles were made for query_cascade-0.3.11.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.3.11.tar.gz -
Subject digest:
f3b39d1549b02f0255d4b0b03efd56da99003bb380e943d58f6e55b0bd71a8d8 - Sigstore transparency entry: 2218335169
- Sigstore integration time:
-
Permalink:
hmatt1/cascade-query@33b0f4a4dd70198b490d037af7e2a0efeca30cd7 -
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@33b0f4a4dd70198b490d037af7e2a0efeca30cd7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file query_cascade-0.3.11-py3-none-any.whl.
File metadata
- Download URL: query_cascade-0.3.11-py3-none-any.whl
- Upload date:
- Size: 58.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ce85a8e90642fa47e17d45f95dc1d14c6f8c4eb932515dbf59e6e943577977c
|
|
| MD5 |
56ec66d6c6454f1a249ed71d47af6e91
|
|
| BLAKE2b-256 |
a8c8d10098f7334657717387930dde55bab3b49d9ab50986a487645effd24e4a
|
Provenance
The following attestation bundles were made for query_cascade-0.3.11-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.3.11-py3-none-any.whl -
Subject digest:
1ce85a8e90642fa47e17d45f95dc1d14c6f8c4eb932515dbf59e6e943577977c - Sigstore transparency entry: 2218335186
- Sigstore integration time:
-
Permalink:
hmatt1/cascade-query@33b0f4a4dd70198b490d037af7e2a0efeca30cd7 -
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@33b0f4a4dd70198b490d037af7e2a0efeca30cd7 -
Trigger Event:
push
-
Statement type: