Skip to content

Store

Event stores, URL parsing, and migration. For the conceptual model see concepts/graph (graph as projection of the event log) and concepts/replay.

Stores

Bases: Protocol

Append-only per-run event log. CONTRACT v0.5 #2.

The persistence seam: an instance is scoped to one run_id even when runs share a backing file. Deliberately minimal — append, iterate, count, lookup, truncate-after, close; no queries, no indexes beyond what the backend ships. Backends (in-memory, SQLite, Postgres) implement this protocol and pass EventStoreConformance. The log is the source of truth; the graph projection rebuilds from here and is never stored here.

Volatile, list-backed event store. CONTRACT v0.5 #2.

The reference implementation of the EventStore protocol: append-only, id-addressed, iteration in append order. Everything lives in process memory and nothing survives the interpreter — exactly right for tests and ephemeral runs, and exactly wrong for anything that must be resumed, forked, or audited later; use SQLiteEventStore or PostgresEventStore for those.

Per-run view onto a SQLite-backed event log.

Direct construction expects an explicit path and run_id. For most cases prefer Runtime(graph, persist_to=...), which opens the store, mints a run_id if needed, and wires it onto the runtime — the v1.0.1 user-test surfaced that constructing SQLiteEventStore by hand is a low-frequency operator path, not the happy path.

upsert_run(*, parent_run_id=None, forked_at_event_id=None, label=None, created_at, goal=None, frame_id=None)

Insert or update this run's row. None never clears a stored value (v1.3 fix): Runtime.load upserts with only created_at, and before the COALESCEs below that erased a fork's parent_run_id / forked_at_event_id / label on every reload — silently destroying the lineage records promote() verifies against.

put_snapshot(state_hash, blob, *, created_at)

Store a snapshot blob keyed by its state hash (idempotent).

get_snapshot(state_hash)

The snapshot blob for state_hash, or None.

archive_prefix(before_seq, *, archived_at)

Move this run's rows with seq < before_seq to the archive tier, in one transaction. Idempotent (re-running moves nothing). Returns the number of rows moved.

archive_run(*, archived_at)

Move ALL of this run's rows to the archive tier (retire).

iter_archived()

This run's archived events, in original seq order.

has_archived()

True when any of this run's events sit in the archive tier.

fork_run(path, *, parent_run_id, new_run_id, at_event_id, label, created_at) classmethod

Copy events from parent_run_id up to and including at_event_id into new_run_id (CONTRACT v0.5 #11: copy rows, no row-sharing).

Returns the number of events copied.

Graph stores

The materialized graph projection (objects, relations, patches) lives behind a GraphStore, distinct from the durable EventStore above. See the Using the FalkorDB graph store guide.

Bases: ABC

Backend for the materialized graph projection.

Implementations store objects, relations, and patches keyed by id. put_* is an upsert (insert or overwrite the entity with the same id). get_* returns None when the id is unknown. remove_* is a no-op when the id is unknown (the projector already guards against double-removal, but tolerating it keeps backends simple).

put_object(obj) abstractmethod

Insert or overwrite obj keyed by obj.id.

get_object(object_id) abstractmethod

Return the object with object_id or None.

remove_object(object_id) abstractmethod

Drop the object with object_id if present.

all_objects() abstractmethod

Return every object. Order is unspecified.

put_relation(rel) abstractmethod

Insert or overwrite rel keyed by rel.id.

get_relation(relation_id) abstractmethod

Return the relation with relation_id or None.

remove_relation(relation_id) abstractmethod

Drop the relation with relation_id if present.

all_relations() abstractmethod

Return every relation. Order is unspecified.

put_patch(patch) abstractmethod

Insert or overwrite patch keyed by patch.id.

get_patch(patch_id) abstractmethod

Return the patch with patch_id or None.

all_patches() abstractmethod

Return every patch. Order is unspecified.

find_objects(type=None)

Return objects, optionally filtered to those whose type equals type. None means "every object". Type-equality only — the where predicate is applied by the caller, not here.

find_objects_in_types(types)

Return objects whose type is any of types (OR), preserving :meth:all_objects enumeration order. An empty types returns []. Like :meth:find_objects this is type-equality only — the where predicate stays with the caller. The default scans once; a backend MAY push a type IN [...] filter into the database, but MUST preserve the single-pass order so it matches this base class.

find_relations(source=None, target=None, type=None)

Return relations matching every provided filter (AND). A None filter is unconstrained; passing none returns every relation.

neighborhood(object_id, depth=1)

Undirected breadth-first walk from object_id out to depth edges. Returns (objects, relations) where objects are the reachable materialized objects (the start included; endpoints that are not objects are skipped) and relations are every relation traversed. Returns ([], []) if object_id is not an object.

match_chain(node_types, rels)

Enumerate every structural match of a linear node→rel→node chain.

node_types is one entry per node position (None = any type); rels is one (rel_type, direction) per hop, where direction is "right" ((a)-[]->(b)) or "left" ((a)<-[]-(b)) and rel_type None means any. len(rels) must be len(node_types) - 1.

Only the structural filters — node types, relation types, and directions — are applied here; node {prop: value} equality and WHERE are layered on by the caller. Semantics are homomorphic: a single object or relation may fill more than one position (matching the pattern matcher and FalkorDB, neither of which enforces node/relationship uniqueness). The default walks the chain depth-first via :meth:find_objects / :meth:find_relations; a backend MAY override to resolve the whole chain in one query, but MUST return exactly what this default would (order aside).

clear()

Drop all objects, relations, and patches. Default: per-kind removal.

remove_patch(patch_id)

Drop the patch with patch_id if present.

Not part of the projector's hot path (patches are never deleted by events — only superseded), but :meth:clear needs it. Subclasses that store patches must override.

close()

Release any backend resources. Default: no-op.

Bases: GraphStore

Volatile, dict-backed GraphStore. The default backend.

Stores the live :class:~activegraph.core.graph.Object / :class:~activegraph.core.graph.Relation / :class:~activegraph.core.patch.Patch instances by id and returns them directly (no copy), so the projector's in-place mutations (obj.data.update(...), obj.version += 1) behave exactly as they did before the GraphStore seam existed.

Bases: GraphStore

A :class:GraphStore backed by a FalkorDB graph.

Parameters

path: Filesystem path for the embedded falkordblite database. Used only in embedded mode (no connection info given). When None, an ephemeral embedded instance is created. graph_name: Name of the FalkorDB graph to use within the database/server. Defaults to "activegraph". Ignored when graph is supplied. graph: An existing FalkorDB graph handle (anything exposing query / ro_query). When provided, all other connection parameters are ignored and this store does not own the connection's lifecycle. url: FalkorDB server URL (e.g. "falkor://host:6379"). Triggers server mode via the falkordb client. host, port, username, password: FalkorDB server connection settings. Supplying host triggers server mode. port defaults to 6379.

Connection resolution order: explicit graph → explicit url / hostFALKORDB_URL / FALKORDB_HOST env vars → embedded falkordblite.

Install the server client with pip install 'activegraph[falkordb]' or the embedded engine with pip install 'activegraph[falkordb-embedded]'.

URL parsing + helpers

Open a store for run_id at url. Returns an EventStore.

This is the single entry point the runtime and CLI use to open a store from a URL. Drivers are imported lazily so the Postgres dependency stays optional.

Parse a store URL, or raise InvalidStoreURL with a helpful message.

Accepts the sqlite:///path and postgres://... families and returns a :class:StoreURL with the normalised scheme, the raw URL, and the filesystem path for SQLite forms. The single entry point for URL validation — open_store and the CLI both route through here, so a malformed URL fails identically everywhere.

Migration

Copy every run (or a subset) from source_url into dest_url.

Parameters:

Name Type Description Default
source_url str

e.g. sqlite:///dev.db

required
dest_url str

e.g. postgres://localhost/prod

required
only_run_ids Optional[list[str]]

if given, migrate only these runs.

None
on_progress Optional[Callable[[MigrationRunReport], None]]

called after each run finishes (success or failure) with the MigrationRunReport for that run.

None
skip_corrupted bool

if True, rows whose payload fails JSON decode are skipped (not migrated, not failing the run). The skipped event ids appear in the per-run report's skipped_events. The resulting destination run is partial — the operator is on notice.

False

Returns:

Type Description
MigrationReport

A MigrationReport. The overall operation is considered

MigrationReport

successful iff every run's status is "ok" or "skipped".

Aggregate result of a store-to-store migration.

One :class:MigrationRunReport per run found at the source; ok is True when nothing failed (skips are fine), and failures filters the runs that need attention. Value object returned by migrate and rendered by the CLI.

Outcome of migrating one run between stores.

status is ok (copied), skipped (already present at the destination), or failed (with error set); events_migrated counts rows written this invocation, so a rerun after a partial failure reports only the delta. skipped_events lists ids dropped under --skip-corrupted.

The canonical row describing one run in a multi-run store.

Fork lineage (parent_run_id, forked_at_event_id), an optional human label, and the originating goal / frame_id — enough for run listings and for Runtime.load to pick the right run without replaying anything.