Caching¶
Source: src/cache/cache_manager.py
memFrame caches analytical method calls in a per-backend transient registry so repeat work is either recorded (cheap signature logging) or fully reused (deep cache reloads a saved result table without re-executing the method).
The record_call decorator¶
record_call is a CacheManager instance (from memframe.cache import
record_call, CacheManager). Every analytics orchestrator method (selection,
inspection, cleaning, stats, arithmetic) is decorated with it. The
decorator wraps the method, records the call into the transient_registry
table, and — for deep cache — persists the returned DataFrame as a transient
table.
@record_call # signature-only logging (default)
@record_call() # same as above
@record_call(deep_cache=False) # explicit signature-only
@record_call(deep_cache=True) # persist result table + reload on repeat call
How caching works¶
Resolution order¶
The effective cache mode for a call is decided in this order:
- The decorator argument, e.g.
@record_call(deep_cache=True). MemFrame(deep_cache=...)when the decorator does not specify one.False(signature-only) as the default.
MemFrame(deep_cache=False) is the master switch: it overrides decorator
requests and disables deep caching for the whole session.
Level 1 — signature-only (default)¶
Every call records its identity into transient_registry:
data_id— the dataset the call ran onopidx— monotonically increasing operation index per datasetclass_name/method_name— the decorated methodargs/kwargs— canonical JSON-serialized call arguments (keys sorted, numpy/pandas scalars coerced), so argument order in kwargs does not affect cache-key equalitygenerated_table_name,schema,is_deep_cache
On a repeat call with identical arguments, the method re-executes. Any table the method created is dropped — signature-only mode never persists results.
Level 2 — deep cache¶
When deep caching is active, a successful result (a dict with result holding a
DataFrame) is written to a typed transient table (column types are inferred
from the DataFrame's Arrow schema, so cached results keep their dtypes):
On a repeat call with identical arguments, the registry lookup finds the saved table and the decorator returns a cache-hit response without executing the method:
{
"is_error": False,
"message": "Cache hit for SelectionOrchestrator.where; reused generated table '...'",
"generated_cols": [...],
"result": <DataFrame from saved table>,
"new_table": "<saved table name>",
"result_metadata": {
"from_cache": True,
"saved_table": "<saved table name>",
"row_count": N,
"column_count": M,
"strict_args_kwargs_match": True,
},
}
Table relocation¶
Methods that generate a table (e.g. a cleaned dataset) normally produce it in
the upload schema. With deep caching, that table is moved into
transient_schema so it can be reloaded on later calls and cleaned up with
aclear_cache. The relocation mechanism is backend-specific:
- DuckDB —
CREATE TABLE transient AS SELECT * FROM upload...thenDROP. - PostgreSQL —
ALTER TABLE upload... SET SCHEMA transient. - ClickHouse —
RENAME TABLE upload... TO transient....
Cache invalidation¶
aclear_cache(data_id) drops every transient table recorded for a dataset and
clears its registry rows. The original uploaded table is left untouched.
Backend registry¶
The transient registry lives in the backend registry schema:
| Backend | Registry table | Transient schema |
|---|---|---|
| DuckDB | registry.transient_registry |
transient |
| PostgreSQL | registry.transient_registry |
transient |
| ClickHouse | registry.transient_registry |
transient |
Registry and transient schemas are fixed: registry, transient, and
upload.
Cache-key and registry index¶
Lookups filter transient_registry by data_id, class_name, method_name,
args, kwargs. DuckDB and PostgreSQL backends install an index on
(data_id, class_name, method_name) so repeated lookups do not scan the whole
registry.
Debugging hit/miss¶
The cache logs every lookup through the memFrame.cache logger:
[cache] MISS ArithmeticWrapper.mul data_id=HW4dev — no matching registry row
[cache] STORE ArithmeticWrapper.mul data_id=HW4dev → transient.HW4dev__op_... (deep)
[cache] HIT ArithmeticWrapper.mul data_id=HW4dev → HW4dev__op_... (3 rows, lookup+reload 0.013s)
MISS ... no matching registry row— deep lookup found nothing (different args, or deep caching off for this call). If this happens on a repeat call that just loggedSTORE ... (deep), the registry filter is the problem: ClickHouse storesis_deep_cacheasBool, which stringifies to'true'/'false', so the lookup must compare it asCAST(is_deep_cache AS UInt8) = 1, not= '1'.MISS ... reload failed for <table>— registry row existed but the saved table could not be reloaded (warning level; the underlying error is logged).HIT ...— a saved table was reloaded; the timing includes the registry lookup plus the table fetch.STORE ... (deep)— a result was persisted to a transient table.STORE ... (signature-only)— L1 mode recorded the call only.
The result_metadata.from_cache flag on the returned dict also reports hit vs
miss programmatically.