Skip to content

Quickstart

Get CIUF running against your PostgreSQL database in 15 minutes. By the end you'll have a JOIN query returning cached results with sub-millisecond latency, and you'll see the before/after numbers yourself.


1. Install (1 min)

pip install ciuf[postgres]

Requires Python 3.11+. The [postgres] extra pulls in sqlalchemy[asyncio] and psycopg2-binary. For SQLite only (dev/testing): pip install ciuf.

Verify:

python -c "import ciuf; print(ciuf.__version__)"
# 0.1.0

2. Connect (2 min)

from ciuf import Engine

engine = Engine("postgresql://user:password@localhost:5432/mydb")

On construction, CIUF connects to your database and discovers the schema: one TableNode per table, no data loaded yet. The connection string is a standard SQLAlchemy URL.

Connection string formats

postgresql://user:pass@host:5432/db        # psycopg2 (default)
postgresql+psycopg://user:pass@host:5432/db  # psycopg3
sqlite:///./dev.db                            # SQLite (testing)

3. Run your first cached JOIN (5 min)

Use from_sql to parse any SQL SELECT and get a result backed by the in-memory DAG:

from ciuf import Engine, from_sql

engine = Engine("postgresql://user:password@localhost:5432/mydb")

result = from_sql(engine, """
    SELECT
        orders.id,
        orders.amount,
        customers.name,
        customers.plan
    FROM orders
    JOIN customers ON orders.customer_id = customers.id
    WHERE customers.plan = 'pro'
    ORDER BY orders.amount DESC
    LIMIT 100
""")

# First call: loads data from PostgreSQL and builds the DAG
df = result.query()
print(df.head())

The first call executes the full query against PostgreSQL and caches the result. Every subsequent call with the same query pattern returns from memory. No database roundtrip.


4. Measure the improvement (5 min)

Here's how to time the difference yourself:

import time
from ciuf import Engine, from_sql

engine = Engine("postgresql://user:password@localhost:5432/mydb")

result = from_sql(engine, """
    SELECT
        date_trunc('day', orders.created_at) AS day,
        SUM(orders.amount)                   AS revenue,
        COUNT(*)                             AS order_count,
        customers.plan
    FROM orders
    JOIN customers ON orders.customer_id = customers.id
    WHERE orders.status = 'completed'
    GROUP BY date_trunc('day', orders.created_at), customers.plan
    ORDER BY day DESC
    LIMIT 30
""")

# Cold read — loads from PostgreSQL
t0 = time.perf_counter()
df = result.query()
cold_ms = (time.perf_counter() - t0) * 1000
print(f"Cold read:  {cold_ms:.1f} ms  ({len(df)} rows)")

# Hot read — returns from in-process cache
t0 = time.perf_counter()
df2 = result.query()
hot_ms = (time.perf_counter() - t0) * 1000
print(f"Hot read:   {hot_ms:.3f} ms")
print(f"Speedup:    {cold_ms / hot_ms:.0f}×")

Typical output on a JOIN query over 100k rows:

Cold read:  42.3 ms  (30 rows)
Hot read:   0.031 ms
Speedup:    1365×

The hot read is sub-millisecond because CIUF returns the cached DataFrame directly. No network, no parsing, no serialization.


5. Keep the cache in sync (2 min)

CIUF does not poll the database for changes. You notify it of every write:

# After INSERT
engine.on_insert("orders", {
    "id": 12345,
    "amount": 99.0,
    "customer_id": 42,
    "status": "completed",
    "created_at": "2026-05-09T12:00:00",
})

# After UPDATE
engine.on_update("customers",
    new={"id": 42, "plan": "pro",  "name": "Acme Corp"},
    old={"id": 42, "plan": "free", "name": "Acme Corp"},
)

# After DELETE
engine.on_delete("orders", {"id": 12345})

CIUF propagates only the delta. No full table reload. Downstream joins and aggregations update incrementally.


Configuration reference

engine = Engine(
    "postgresql://user:pass@localhost/mydb",
    max_memory_mb=512,   # LRU eviction when cache exceeds this (default: no limit)
    ttl_seconds=3600,    # Evict results older than this (default: no expiry)
)

Next steps