Concepts

The change-driven model: sources, continuous queries, reactions and bootstrap.

Why continuous queries

A normal query answers a question once. To keep an answer current you either re-run it on a timer, which is wasteful and always slightly stale, or you hand-write logic that works out which events could have changed the answer — which is where the bugs live.

A continuous query is declared once and maintained. Drasi tracks what the result set is, and when a change arrives it works out precisely which rows entered, left or changed, and tells you about those. You get the difference, not a new snapshot to diff yourself.

That difference is the useful part. Knowing an order is no longer in the “open orders” set is a fact you can act on directly, and it is exactly what polling makes you reconstruct.

The pieces

Source

Where changes come from — a database's change feed, a stream, or your own code.

Continuous query

A Cypher or GQL query whose result set is kept current as changes arrive.

Reaction

What happens when the result set changes — your callback, or a plugin.

Sources

A source turns changes in some system into graph changes: nodes and relations being inserted, updated or deleted. It can be a plugin that follows a real system — a Postgres write-ahead log, a Kafka topic, a Kubernetes cluster — or a Python source you push changes into yourself with push_change.

Every change carries an id, which is the node’s key in the graph. That is worth holding onto: id is not automatically a property, so a query selecting o.id reads a property of that name and gets None unless the source emits it in properties as well.

Continuous queries

Queries are written in Cypher (the default) or GQL, against the graph the sources produce. Node labels come from the source, which decides how it labels what it emits; its own documentation is what tells you.

One query can span several sources. That is where Drasi differs from a database view: the sources can be entirely different systems, and the join between them is maintained incrementally as either side changes.

Sources that know nothing about each other emit no relationship between their nodes, so a pattern cannot traverse from one to the other on its own. Declare a synthetic join and the engine maintains one, matching on a property value:

await drasi.add_query(
    "orders-with-customers",
    "MATCH (o:Order)-[:PLACED_BY]->(c:Customer) RETURN o.id AS id, c.name AS customer",
    ["orders", "customers"],
    joins=[
        {
            "id": "PLACED_BY",
            "keys": [
                {"label": "Order", "property": "customer_email"},
                {"label": "Customer", "property": "email"},
            ],
        }
    ],
)

The join’s id is the relationship type the pattern matches, and it exists only because the join declares it. Each key names a label and the property to match on, so an Order is related to the Customer whose email equals its customer_email.

Reactions

A reaction receives result-set diffs. Each diff has a type:

TypeMeaningCarries
ADDA row entered the result setdata
UPDATEA row already in the set changedbefore and after
DELETEA row left the result setdata

A reaction can be a Python callable, an async callable, an async iterator you consume with async for, or a plugin such as dashboard, http or grpc.

Note that DELETE means left the result set, which is not the same as a row being deleted. Shipping an order removes it from “open orders” without deleting anything.

Bootstrap

A change feed only tells you what changed since you started listening. A query over a table that already holds a million rows would start empty and stay wrong until every row happened to be touched.

A bootstrap provider closes that gap by loading the current contents when the query subscribes, after which the change feed takes over. Attach one when you add the source:

await drasi.add_source(
    "postgres",
    "db",
    config,
    bootstrap={"kind": "postgres", **connection},
)

Python sources need no bootstrap, because nothing exists before you push it.

How it runs

The engine is the same Rust implementation Drasi runs in Kubernetes, hosted in your process through a PyO3 extension. There is no broker, no sidecar, and no network hop between your code and the query engine.

Work happens on a Tokio runtime inside the extension, bridged to asyncio so that awaiting a Drasi call does not block your event loop. Python callbacks are invoked on your loop, so they can await your own async code.

Where state lives

By default queries keep their indexes in memory, which is fastest and loses everything on restart. For state that outlives the process, configure a store when creating the engine:

  • state_store — component state and reaction checkpoints, backed by redb.
  • index_store — query indexes, in memory or RocksDB when the wheel was built with that feature. host_info()["index_backends"] reports which are available.

A durable reaction records how far it got, so after a restart it resumes rather than replaying from the beginning or silently skipping what it missed.

Next