Adapters and capabilities¶
rhizodat.core.adapter ¶
Store adapters as plug-ins: what a store must declare to join rhizodat.
The core knows no store. An Adapter names its kind, the DSN schemes it
accepts and how to open a store (and, if the store is a shared ACID one, the
journal). rhizodat's own adapters register exactly like a third-party package,
which declares one entry point::
[project.entry-points."rhizodat.adapters"]
mysql = "rhizodat_mysql:ADAPTER"
or calls register_adapter itself. Adapters are loaded on first use; a plug-in
that fails to load is skipped and reported when a DSN needs a missing scheme.
What a store can do beyond Store is a capability protocol here
(ReadOnlyQueries, AppliedLocation, Fenced, Migrates, Loads,
LoadsGraphs, Vectors),
checked with isinstance.
Adapter
dataclass
¶
A store kind as rhizodat sees it.
open_store(name, dsn): theStore;Nonefor a journal-only kind;open_journal(dsn, wall_ms): the journal, only for a shared ACID store;journal_location(dsn): where a journal atdsnlives, compared with thefence_locationofFencedstores;query_help: one line on the store's native query language, for clients such as rhizodat-MCP.
Scheme
dataclass
¶
One DSN scheme and its rules: host and path required, optional or
forbidden ("none"), the port used when a host has none.
ReadOnlyQueries ¶
Bases: Protocol
Native reads the store itself refuses to turn into writes.
AppliedLocation ¶
Bases: Protocol
Where the store's dat_applied markers live. Two stores in one place
would block or confuse each other, so Rhizodat refuses them.
Fenced ¶
Bases: Protocol
While a branch of this store is prepared, nothing else commits in
fence_location (drevo's prepared fence), so the journal must live elsewhere.
Migrates ¶
Bases: Protocol
Runs schema migration steps of the kinds it names (sql, cypher, ...).
A step must be safe to run again (IF NOT EXISTS, a check before a change):
a migration over several stores is not atomic, and after a failure the whole
migration runs again.
Loads ¶
Bases: Protocol
Imports files: tables from portable columns, and rows loaded as one unit.
The table is created and widened only through migration steps (the store's
Migrates kinds), so every schema change is recorded. load takes
pyarrow record batches whose columns are a subset of the table's (left-out
columns are null) and makes all of them visible at once, or none: a failure
halfway, the source's or the store's, leaves the table as it was. replace
swaps the table's contents for the batches, as one unit too.
table_columns ¶
table_columns(table: str) -> list[str] | None
The table's column names in order; None if there is no such table.
create_table_steps ¶
create_table_steps(
table: str,
columns: Sequence[Column],
*,
order_by: Sequence[str] = (),
) -> list[Step]
Idempotent migration steps creating the table; order_by is the sort key
for stores that have one (others ignore it).
add_column_steps ¶
add_column_steps(
table: str, columns: Sequence[Column]
) -> list[Step]
Idempotent migration steps adding nullable columns.
load ¶
load(
table: str,
batches: Iterable[Any],
*,
replace: bool = False,
) -> int
Load the batches as one unit; the number of rows loaded.
LoadsGraphs ¶
Bases: Protocol
Imports graphs: nodes keyed by their IRI and typed edges, written as one unit.
Every node gets the base label Resource and its classes as further labels;
a node or edge that is already there is updated, not duplicated, so a second
load of the same file changes only what changed. A failure halfway writes
nothing.
load_graph ¶
load_graph(
graph: GraphData, *, batch: int = 500
) -> tuple[int, int]
Write graph; the numbers of nodes and edges written.
Vectors ¶
Bases: Protocol
Holds embeddings beside the nodes they describe and finds the nearest ones.
A vector is stored in a node property, with the model that made it and a hash
of the text it was made from (<property>_model, <property>_hash), so
an embedding run can skip what has not changed.
vector_candidates ¶
vector_candidates(
label: str, text_property: str, vector_property: str
) -> list[VectorCandidate]
Every label node with text in text_property.
write_vectors ¶
write_vectors(
label: str,
vector_property: str,
rows: list[dict[str, Any]],
*,
batch: int = 500,
) -> int
Write {key, vector, hash, model} rows as one unit; the number written.
nearest ¶
nearest(
label: str,
vector_property: str,
vector: list[float],
*,
k: int = 10,
) -> list[Neighbour]
The k label nodes closest to vector by cosine, closest first.
register_adapter ¶
register_adapter(adapter: Adapter) -> None
Register a store kind in this process (entry points do it on installation).
rhizodat.core.parts ¶
Entity parts: what a store keeps of an entity, and how the repository asks for it.
An entity's fields are split by store (@entity(stores=...)); each store's
share is a part. A store keeps every version of a part: one per write,
tagged with the meta-transaction (dat_txn) and its transaction time
(dat_tx_time, HLC). A soft delete is one more version marked deleted;
a hard delete erases every version.
Stores join by implementing two capabilities, checked with isinstance:
PartBranchon a branch: writes inside a Unit of Work, and reads that see them.dat_tx_timeis only known at stage, so the branch fills it (and a missing creation time) then;PartStoreon a store: committed reads outside any Unit of Work;ScansPartson a store: every entity's committed versions at once, for searches over all entities of a type (similar).
Values reach the store as plain data: references are their dat_id.
PartStore ¶
Bases: Protocol
install_part ¶
install_part(part: Part) -> None
Create what the part needs (table, indexes) if missing. Idempotent.
read_part ¶
read_part(
part: Part, dat_id: UUID, *, known_at: int | None = None
) -> Version | None
The latest committed version with dat_tx_time <= known_at (any, if None).
part_history ¶
part_history(part: Part, dat_id: UUID) -> list[Version]
Every committed version, oldest first.
expired_parts ¶
expired_parts(
part: Part, *, deleted_before: int, limit: int
) -> list[uuid.UUID]
Entities whose latest version is a soft delete older than deleted_before.
PartBranch ¶
Bases: Protocol
write_part ¶
write_part(
part: Part,
dat_id: UUID,
values: Mapping[str, Any],
*,
created: int | None,
deleted: bool = False,
) -> None
Add a version. created=None: this version is the first (its own time).
erase_part ¶
erase_part(part: Part, dat_id: UUID) -> None
Physically remove every version of the part.
read_part ¶
read_part(part: Part, dat_id: UUID) -> Version | None
The latest version, this branch's own writes included.
Part
dataclass
¶
One store's share of one entity type.
rhizodat.core.errors ¶
What a store's error means: worth a retry, a full store, a constraint, or a bug.
Adapters classify their own errors: a branch or participant may implement
classify(error) -> ErrorKind (Classifies). One that only has
is_retryable is classified by it (TRANSIENT or PERMANENT), and any
error caused by a device out of space (ENOSPC, EDQUOT) is
STORE_FULL whoever raised it.
A full store fails the meta-transaction with StoreFull
(rhizodat.core.coordinator), an Aborted that names the store. Before
the decision that is all it takes: nothing is committed. A store written after
the decision (ClickHouse, Iceberg) can only fail before it if it is asked
first: a store with Capacity reports its free space, and with min_free
set the coordinator refuses a meta-transaction touching a store below it,
before anything is written.
ErrorKind ¶
Bases: Enum
TRANSIENT
class-attribute
instance-attribute
¶
TRANSIENT = 'transient'
Re-running the Unit of Work may succeed: a conflict, a timeout, a lost connection.
STORE_FULL
class-attribute
instance-attribute
¶
STORE_FULL = 'store_full'
The store has no room left: disk, quota or memory for the write.
CONSTRAINT
class-attribute
instance-attribute
¶
CONSTRAINT = 'constraint'
The data breaks a rule of the store: unique key, foreign key, check.
PERMANENT
class-attribute
instance-attribute
¶
PERMANENT = 'permanent'
Anything else: re-running cannot help.
Capacity ¶
Bases: Protocol
A store that can tell how much room it has left.
no_space ¶
no_space(error: BaseException) -> bool
Whether error or anything that caused it is a device out of space.
classify ¶
classify(
source: _Retryable, error: BaseException
) -> ErrorKind
source.classify(error) (a branch or participant), else by is_retryable.