# Store

Event stores, URL parsing, and migration. For the conceptual model see [`concepts/graph`](https://docs.activegraph.ai/concepts/graph/index.md) (graph as projection of the event log) and [`concepts/replay`](https://docs.activegraph.ai/concepts/replay/index.md).

## 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)`

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](https://docs.activegraph.ai/guides/using-falkordb/index.md) 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)`

Insert or overwrite `obj` keyed by `obj.id`.

## `get_object(object_id)`

Return the object with `object_id` or `None`.

## `remove_object(object_id)`

Drop the object with `object_id` if present.

## `all_objects()`

Return every object. Order is unspecified.

## `put_relation(rel)`

Insert or overwrite `rel` keyed by `rel.id`.

## `get_relation(relation_id)`

Return the relation with `relation_id` or `None`.

## `remove_relation(relation_id)`

Drop the relation with `relation_id` if present.

## `all_relations()`

Return every relation. Order is unspecified.

## `put_patch(patch)`

Insert or overwrite `patch` keyed by `patch.id`.

## `get_patch(patch_id)`

Return the patch with `patch_id` or `None`.

## `all_patches()`

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` / `host` → `FALKORDB_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.
