Persistence: workspaces and attach#
The public surface for persisting a FactGraph to disk and reloading it: the workspace create / save_workspace / load_workspace lifecycle, the FactGraph.attach(db, ...) Database lifecycle, and the Database, CommitResult, AssertionInput, and MetaEntry types exported from factgraph.sdk.
FactGraph is a literal alias of SDKStore (FactGraph = SDKStore). The names are interchangeable; this page uses FactGraph.
FactGraph.create(schema_classes, *, ledger=None, ledger_path=None, path=None, artifact_store_root=None, registry_root=None, registry=None, default_row_format=None) -> SDKStore#
Classmethod. Creates a FactGraph from Python Entity classes and optionally binds it to a durable workspace directory. This is the constructor that produces a workspace-capable graph.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
schema_classes | list[type[Entity]] | (required) | Non-empty list of Entity subclasses. |
ledger | Ledger | None | None | Optional existing ledger object. Mutually exclusive with ledger_path. |
ledger_path | str | None | None | Optional SQLite ledger path. Mutually exclusive with ledger. |
path | str | Path | None | None | Optional workspace directory. When set, the graph owns a durable workspace that can later be saved and reloaded. |
artifact_store_root | str | None | None | Optional artifact sidecar root. The artifact sidecar is not included in workspace saves. |
registry_root | str | Path | None | None | Removed. Raises SDKStoreError immediately if provided. |
registry | Any | None | None | Removed. Raises SDKStoreError immediately if provided. |
default_row_format | str | None | None | Optional default output row format for rule evaluation. |
Returns: SDKStore bound to the compiled schema.
Raises: SDKStoreError if registry_root= or registry= is provided, if ledger_path is supplied alongside path and does not resolve to the workspace ledger path ("ledger_path conflicts with workspace path"), or if the schema classes are invalid.
When path is given, create writes the workspace schema object under db/objects/schema/<digest>.json before returning. The ledger path is resolved to <path>/ledger.db.
Example:
from factgraph.sdk import FactGraph
fg = FactGraph.create(schema_classes=[User], path="workspace")
fg.save_workspace()FactGraph.from_schema_classes(classes, *, ledger=None, ledger_path=None, artifact_store_root=None, registry_root=None, registry=None, default_row_format=None) -> SDKStore#
Classmethod. Lower-level constructor that builds a FactGraph from Entity classes. It does not accept a path parameter and does not bind a workspace.
Parameters: As create, without path.
Returns: SDKStore.
Raises: SDKStoreError if registry_root= or registry= is provided, or if both ledger and ledger_path are given ("provide either ledger or ledger_path, not both").
FactGraph.save_workspace(path=None) -> dict[str, Any]#
Instance method. Persists the graph as a FactGraph workspace: the schema object, the append-only ledger, and a workspace manifest.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
path | str | Path | None | None | Workspace directory. If omitted, the graph must already be bound to a workspace path. Passing a path binds it for future no-argument saves. |
Returns: dict[str, Any] with keys "path" (the workspace root) and "manifest" (the manifest file path).
Raises: SDKStoreError if the graph is not bound to a workspace path and no path is given ("workspace path not bound; pass fg.save_workspace(path=...) or create with FactGraph.create(path=...)"), or if the graph is an attached runtime (attached runtimes route writes only through fg.commit_assertions(...)).
The ledger is copied into <path>/ledger.db by SQLite backup. A save to the bound path is checkpointed in place. A save to a different path uses a fresh backup and rebinds the graph to the new path. The artifact sidecar, in-memory assertion views, and audit/evidence files are not written into the workspace.
Example:
fg = FactGraph.create(schema_classes=[User], path="workspace")
info = fg.save_workspace()
# info == {"path": ".../workspace", "manifest": ".../workspace/factgraph_workspace.json"}FactGraph.load_workspace(path, *, schema_classes=None, default_row_format=None) -> SDKStore#
Classmethod. Loads a saved workspace from disk, restoring the ledger and validating the workspace schema digest against the supplied classes.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
path | str | Path | (required) | Workspace directory created by save_workspace. |
schema_classes | list[type[Entity]] | None | None | Entity classes matching the saved workspace schema. Required; class-less dynamic load is not supported. |
default_row_format | str | None | None | Optional default output row format. |
Returns: SDKStore bound to the loaded workspace.
Raises: SDKStoreError when schema_classes is None ("schema_classes is required for FactGraph.load_workspace(...)"); when the workspace contains a legacy registry/ directory (run python -m factgraph migrate-workspace <path> first); when the manifest is missing ("workspace manifest missing: factgraph_workspace.json"); when the workspace schema object is missing or invalid; or when the schema digest does not match (see the schema-digest guard below).
Example:
loaded = FactGraph.load_workspace("workspace", schema_classes=[User])
snapshot = loaded.entities.get(User, user_id="Alice")Schema-digest guard on reload#
The schema digest is a content hash of the compiled schema (schema_digest(schema_ir), a sha256:-prefixed token). It is recorded at save time and re-checked on every reload path. Reload succeeds only when the classes you pass recompile to the same digest, so a workspace and a ledger cannot be opened against an incompatible schema.
Workspace manifest guard. load_workspace calls the application runtime validate_workspace_manifest, which compares the digest stored in factgraph_workspace.json against the digest of the supplied schema_classes. On mismatch it raises with a message containing both "schema" and "digest". After the manifest check, the workspace schema object under db/objects/schema/<digest>.json is validated to match the recompiled schema IR; a missing object raises "workspace schema object missing".
Ledger meta guard. When a ledger is opened with a stored schema_digest meta value, the constructor compares it to the current schema digest. If they differ it raises:
schema mismatch: ledger file was written with schema_digest=<stored>,
but current schema has digest=<current>. Use the same Entity classes that
were used when this ledger was created.If the ledger has no stored digest, the current digest is written into the ledger meta.
The digest is stable across schema recompilation that changes only the generated_at timestamp: a workspace saved, reloaded, and resaved keeps the same digest and schema-object bytes.
Workspace layout#
save_workspace writes a manifest named factgraph_workspace.json at the workspace root. The manifest is JSON with sorted keys and the following shape (constants from the application workspace runtime):
| Field | Value |
|---|---|
factgraph_workspace_version | "1" |
save_scope | "level_4" |
schema_digest | the schema digest token |
components.ledger | "ledger.db" |
components.db | "db/" |
components.views | "views/" |
created_at | ISO-8601 UTC timestamp (preserved across resaves) |
last_saved_at | ISO-8601 UTC timestamp (updated on each save) |
The ledger is stored at <root>/ledger.db. The schema object is stored at <root>/db/objects/schema/<digest>.json. The workspace layout is distinct from a package export layout (a package writes manifest.json, not factgraph_workspace.json).
FactGraph.attach(db, *, schema_classes, view=None, default_row_format=None, **kwargs) -> SDKStore#
Classmethod. Attaches a FactGraph to a Database substrate. Attached runtimes route all writes through fg.commit_assertions(...); the workspace and field-mutation write paths are unavailable on them.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
db | Database | (required) | A Database instance. |
schema_classes | list[type[Entity]] | (required) | Entity classes that must compile to the Database’s schema_digest. |
view | FrozenAssertionSet | None | None | Optional durable Database view from db.create_view(...). When provided, the attached runtime is read-only and scoped to the view. |
default_row_format | str | None | None | Optional default output row format. |
**kwargs | Any | (none) | Any extra keyword is rejected. The keywords artifact_store_root, ledger, ledger_path, path, policy, registry, registry_root, rules, workspace_path are explicitly rejected as constructor-style kwargs. |
Returns: SDKStore bound to the Database. A plain attach (no view) is writable; a view attach is read-only.
Raises: SDKStoreError if db is not a Database ("FactGraph.attach(db) expects a Database instance"); if any keyword is passed in **kwargs; if the compiled schema digest does not match db.schema_digest ("schema mismatch: ..."); or, for a view, if it is not a durable Database view, or its db_id, schema_digest, or base_tx_id do not match the Database ("view db_id mismatch", "view schema mismatch", "view base_tx_id not found in Database").
The attach schema check compares against db.schema_digest directly, not the ledger meta cache.
Example:
from factgraph.sdk import Database, FactGraph, compile_schema_from_classes
db = Database.create("workspace", schema_ir=compile_schema_from_classes([User]))
fg = FactGraph.attach(db, schema_classes=[User])FactGraph.commit_assertions(assertions) -> CommitResult#
Instance method. Routes a batch of assertions to the attached Database. Available only on runtimes created by attach without a view.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
assertions | Sequence[AssertionInput] | (required) | Assertions to commit. |
Returns: CommitResult.
Raises: SDKStoreError if the runtime is not attached ("fg.commit_assertions(...) is only available on FactGraph.attach(db) runtimes; ...") or if the runtime is view-attached and therefore read-only ("... view-attached runtimes are read-only").
Example:
result = fg.commit_assertions([
AssertionInput(
pred_id=name_pred_id,
fact_tuple=(("entity_ref", user_ref), ("string", "Ada")),
meta=(MetaEntry("source", "str", "attach-test"),),
)
])
record = result.assertions[0]Database#
The Database identity boundary above the append-only Ledger substrate. Exported from factgraph.sdk. Construct it through its classmethods, not directly.
Database.create(path=':memory:', *, schema_ir) -> Database#
Classmethod. Creates a new Database. A :memory: path creates an in-memory Database; any other path creates a durable Database workspace directory.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
path | str | Path | ':memory:' | Workspace directory, or ':memory:' for an in-memory Database. |
schema_ir | dict[str, Any] | (required, keyword) | Compiled schema IR (for example from compile_schema_from_classes(...)). |
Returns: Database.
Raises: DatabaseError if a Database already exists at the path, or if the underlying Ledger substrate is not empty ("Database.create requires an empty Ledger substrate").
Database.open(path, *, schema_ir) -> Database#
Classmethod. Opens an existing durable Database workspace.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
path | str | Path | (required) | Existing Database workspace directory. ':memory:' is rejected. |
schema_ir | dict[str, Any] | (required, keyword) | Compiled schema IR; must hash to the stored schema_digest. |
Returns: Database.
Raises: DatabaseError if path is ':memory:' ("Database.open does not support ':memory:'"), if the Database metadata is not found, or if the schema digest does not match ("schema_digest mismatch: ...").
Database.commit_assertions(assertions) -> CommitResult#
Classmethod-free instance method. Appends a transaction of assertions, advances the head transaction, and returns the new value. FactGraph.commit_assertions delegates here.
Parameters: assertions: Sequence[AssertionInput].
Returns: CommitResult.
Raises: DatabaseError if assertions is empty ("commit_assertions requires at least one assertion"); DuplicateAssertionError if a content-addressed assertion id repeats within the batch or already exists.
Database.create_view(name, asrt_ids, *, base=None) -> FrozenAssertionSet#
Instance method. Creates a durable named view: a frozen selection of assertion ids anchored at the current head transaction. The returned FrozenAssertionSet can be passed to FactGraph.attach(db, view=...).
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | (required) | Non-empty view name. |
asrt_ids | Iterable[str] | (required) | Assertion ids to include; each must exist in the Database. |
base | DatabaseValue | None | None | Optional head value; if provided, must equal the current head. |
Returns: FrozenAssertionSet.
Raises: DatabaseError if the Database has no durable workspace ("durable view persistence requires a new-layout Database workspace"), if base is not the current head, or if any assertion id does not exist.
Database.head() -> DatabaseValue#
Instance method. Returns the current head value.
Returns: DatabaseValue.
Raises: DatabaseError if the head metadata is missing.
Database.db_id -> str#
Property. The Database identity token (prefixed db: or mem:).
Database.schema_digest -> str#
Property. The Database’s schema digest token (prefixed sha256:).
CommitResult#
Frozen dataclass. The result of a commit. Exported from factgraph.sdk.
| Field | Type | Description |
|---|---|---|
parent_tx_id | str | The head transaction id before the commit. |
value | DatabaseValue | The new head value (db_id, tx_id, schema_digest, data_digest). |
assertions | tuple[AssertionRecord, ...] | One record per committed assertion, carrying its asrt_id, pred_id, fact_tuple, schema_digest, assertion_digest, tx_id, and meta. |
After a commit, result.value equals db.head(), and a later commit’s parent_tx_id equals the earlier commit’s value.tx_id.
AssertionInput#
Frozen dataclass. One assertion to commit. Exported from factgraph.sdk.
| Field | Type | Default | Description |
|---|---|---|---|
pred_id | str | (required) | The predicate id for the fact. |
fact_tuple | tuple[tuple[str, Any], ...] | (required) | Tagged (tag, value) pairs. Position 0 must be ("entity_ref", e_ref). |
meta | tuple[MetaEntry, ...] | () | Optional metadata entries. |
Constraints (enforced at commit): fact_tuple must be a non-empty tuple whose first element is ("entity_ref", e_ref); otherwise commit raises DatabaseError ("fact_tuple[0] must be ('entity_ref', e_ref)"). The remaining pairs become the fact’s value terms.
Example:
AssertionInput(
pred_id="user:name",
fact_tuple=(("entity_ref", user_ref), ("string", "Ada")),
meta=(MetaEntry("source", "str", "import"),),
)MetaEntry#
Frozen dataclass. One metadata entry attached to an assertion. Exported from factgraph.sdk.
| Field | Type | Description |
|---|---|---|
key | str | Non-empty metadata key. |
kind | str | Value kind. Must be one of str, int, float, bool, time, json. |
value | Any | The metadata value, interpreted per kind. |
Constraints (enforced at commit): key must be a non-empty string ("meta key must be non-empty string"), and kind must be one of the supported kinds ("unsupported meta kind: <kind>").
Example:
MetaEntry("source", "str", "attach-test")
MetaEntry("ingested_at", "time", 1)Related pages#
- Three-layer API: the
fg.entities/fg.fields/fg.assertionswrite and read surface used on non-attached graphs. - Errors: the
SDKStoreErrorand related error catalogue. - Auditability: why the ledger is append-only and what the digests anchor.