#[non_exhaustive]pub enum DbError {
Show 33 variants
Engine(Error),
Migration {
to: u32,
reason: String,
},
InvalidEdgeType(String),
SingleOpenViolation {
source_id: String,
target_id: String,
edge_type: String,
},
NotFound(String),
DimMismatch {
got: usize,
expected: usize,
model: String,
},
InvalidModelName(String),
ModelNotRegistered {
model: String,
table: String,
},
SubgraphTooLarge {
n: usize,
budget: usize,
},
NegativeEdgeWeight {
source_id: String,
target_id: String,
weight: f64,
},
ReplayCorrupt {
seq: i64,
reason: String,
},
SnapshotIncompatible {
path: String,
reason: String,
},
SnapshotCorrupt {
path: String,
reason: String,
},
PayloadVersion {
got: u8,
max: u8,
},
ArchiveViolation {
table: String,
},
ArchiveSessionLeaked {
marker: String,
},
AttributeModeUnstated {
instants: StatedInstants,
},
DiagnosticConn {
path: String,
reason: String,
},
ArchiveWindow {
window: Duration,
reason: String,
},
HalfLifeWithoutInstant,
RecordedInstantUnreachable {
ts: String,
},
InvalidTimestamp {
value: String,
reason: String,
},
InvalidId {
id: String,
reason: String,
},
OverlappingInterval {
overlap: Box<Overlap>,
},
CurrentDrift {
n: usize,
},
RebuildFailed {
n: usize,
},
RebuildInterrupted {
reason: String,
},
WriterUnavailable,
WriterDroppedResponder,
WriterStopped(String),
RecordedAtRegression {
got: String,
had: String,
},
FutureRecordedAt {
stamp: String,
limit: String,
},
BulkCancelled,
}Expand description
Central error type for the Macrame bitemporal ledger database.
Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
Engine(Error)
Migration
InvalidEdgeType(String)
SingleOpenViolation
NotFound(String)
DimMismatch
InvalidModelName(String)
A model name is spliced into DDL and queries as a table identifier, and identifiers cannot be bound as parameters. Validating the name is what makes that splice safe, so an invalid one is refused rather than escaped.
ModelNotRegistered
SubgraphTooLarge
NegativeEdgeWeight
Dijkstra and A* settle a node permanently the first time they pop it,
which is only sound when no later edge can reduce the distance — that is,
when weights are non-negative. links.weight is a bare REAL NOT NULL
with no CHECK, so the guarantee has to be established at load time. The
alternative is a shortest-path result that is quietly just a path.
ReplayCorrupt
SnapshotIncompatible
A snapshot this build cannot read. Distinct from Self::ReplayCorrupt
on purpose: corruption is a fault to report, an incompatible snapshot is
the ordinary consequence of an upgrade, and the correct response is to
discard the file and fold from the log instead (D-043).
SnapshotCorrupt
A snapshot that is damaged rather than foreign (0.13.12, W8.2, D-185).
The third case in the same family, and it needed its own name for the reason D-069 gives: an error that names the wrong subject sends a caller to fix the wrong thing.
Self::SnapshotIncompatible— a different build wrote this. Ordinary after an upgrade.Self::ReplayCorrupt— the ledger is damaged. The log is the only authority in this system and this is the worst thing it can say.- This — the cache is damaged. The ledger is untouched. Deleting the file restores correctness and costs a slower reconstruction, because Doctrine VI makes a snapshot derivative and disposable.
Every failure of load_snapshot used to be ReplayCorrupt { seq: 0 },
which claimed the ledger was damaged and carried a sequence number that
cannot exist — AUTOINCREMENT starts at 1. That is the same placeholder
D-069 removed from InvalidTimestamp, left in place here because
nothing had cause to look at it.
It carries the path and not a seq, because a snapshot is identified by
its file. The reason names the check that failed, and the checks are
ordered so that the earliest possible one fires: declared length, then
checksum, then the decompressed size against what the header declared.
PayloadVersion
ArchiveViolation
ArchiveSessionLeaked
The archive-session marker exists as committed state (0.10.0, W2).
ArchiveViolation is this guard working. This variant is the guard
having been silently switched off: while
macrame_archive_session is present, trg_concepts_guard_delete,
trg_links_guard_delete and trg_txlog_guard_delete all evaluate their
WHEN to false and permit the deletes they exist to refuse, and
trg_concepts_log_insert writes no transaction_log row for a concept
insert. Doctrine IV and Doctrine V are both suspended, with no error
and no counter — which is why the condition needs a name of its own.
It cannot be produced by an archive session, crashed or otherwise.
archive() and archive_windowed() create and drop the marker inside
the same transaction that does the work, so a commit drops it and a
rollback discards it; and the check that raises this error —
verify in src/schema/migrations.rs, which is private, hence the file
reference rather than a link — reads committed state, so it cannot see an
in-flight session. Reaching this
error therefore means something wrote the table outside the write actor
— the raw-writer case §4.7 concedes exists.
Not a Migration error: the schema is intact. What is wrong is the
database’s contents, and saying “your schema is wrong” would send the
reader to the migration ladder for a fault a DROP TABLE fixes.
AttributeModeUnstated
A traversal asked about the past without saying which text it wanted (T3.2, D-085).
An instant on either axis fixes the topology. Node attributes are a
second, independent question, and the default answer —
AttributeMode::Current — is live text. That combination returns the
past’s graph wearing the present’s titles, which is a legitimate thing to
want and a terrible thing to get by accident.
It used to be a tracing::warn!, which is invisible in any application
that has not configured a subscriber. This is the same statement as a
value the caller cannot miss.
Fix by stating the mode: .attribute_mode(AttributeMode::AtTime) for the
past’s text, or .attribute_mode(AttributeMode::Current) to affirm that
live text is what was meant.
§It carries StatedInstants rather than one string (0.13.10, W7.7, D-183)
The field was as_of: String and the message rendered it as
as_of(…) — a method removed in 0.12.17 when
D-174 split the
axes. Both instants collapsed into it through an .or(), so a caller who
set as_of_recorded was told about as_of, a caller who set both was
told about one of them, and neither was told which clock they had asked
about. Naming the axis is the whole remedy this error offers.
Fields
instants: StatedInstantsDiagnosticConn
crate::Database::diagnostic_conn could not open the file read-only
(T5.1, D-091).
Its own variant rather than NotFound, which renders “node {0} not
found” — naming the wrong subject is the defect D-069 was written to
correct, and a file is not a node.
The case worth the sentence is a missing file:
SQLITE_OPEN_READ_ONLY drops SQLITE_OPEN_CREATE with it, so a path
that does not exist is SQLITE_CANTOPEN rather than a fresh empty
database. That is the right behaviour and an opaque error to receive.
ArchiveWindow
crate::Database::archive_windowed was given a window it cannot use
(T1.1, D-080).
Carries a reason rather than the numbers as fields because the two
cases it covers are not the same shape — a zero-length window never
advances at all, while a merely narrow one produces a session count that
has to be quoted against the limit to mean anything. A caller reading
this needs the sentence, not the struct.
It is an error rather than a silent clamp on purpose. Rounding a one-second window up to something workable would archive over boundaries the caller did not choose, and the caller cannot see that it happened.
HalfLifeWithoutInstant
A search asked for decay without saying what age is measured from (0.13.20, W9.5, D-193).
Decay ranks a hit by how old the thing it matched is, and old is only
meaningful relative to an instant. The crate does not read a wall clock
on a read path — that is what makes the suite’s FakeClock able to
pin these answers at all — so the instant has to be stated, and the one
to state is the one the search is already bounded by.
Refusing rather than defaulting to now: a default here would silently make every decayed search a search about the present, which is exactly the class of quiet substitution F-35 and D-175 were about.
RecordedInstantUnreachable
A read named a transaction-time instant the hot log can no longer answer for (0.13.2, W7.1, D-174; extended 0.13.16, W9.1, D-189).
A transaction-time read folds transaction_log, and
crate::Database::archive removes superseded rows from it. Once
anything has been archived, an instant below the cutoff is not before
history, it is history that is in the other file — and these readers
take a connection, not an archive path, so they cannot go and get it.
Two surfaces fold the log and both raise this.
crate::graph::TraversalBuilder::as_of_recorded folds it for topology;
crate::temporal::hydrate_attributes folds it for the text under
crate::graph::AttributeMode::AtTime. The second was added in 0.13.16
(W9.1), where it had been returning a quietly shorter Vec — §3.2 of the
review, and the same silence in the same wave as the first.
Conservative by one bit, deliberately. The test is
hot_log_is_intact: whether anything was ever removed. It cannot ask
whether this particular instant is above the archive cutoff, because the
cutoff is not recorded in the hot log — that is exactly what the hot-side
marker D-132 refused would have carried. So an archived database
refuses every as_of_recorded, including instants it could in principle
have answered. The alternative is answering some of them from a partial
fold, which returns nearly the right topology, and on a ledger that is
the worst failure available.
crate::temporal::reconstruct takes the archive path and answers the
same question, which is why the message names it.
InvalidTimestamp
A timestamp that is not in canonical form (§4.1, D-029).
Distinct from Self::ReplayCorrupt, which is what this used to be
(Wave 4.5). timestamp::normalize and timestamp::parse reported bad
caller input as ReplayCorrupt { seq: 0 } — a claim that the ledger is
damaged, carrying a sequence number that cannot exist because
AUTOINCREMENT starts at 1. The same mistake as defect J: an error that
names the wrong subject sends a caller to fix the wrong thing.
The value is reported rather than the provenance, because one function
serves both directions — a caller passing 2026-01-01T00:00:00Z and a
stored recorded_at that will not parse produce the same complaint about
the same string. SystemClock::new is where the second case is
interpreted, and it already logs and floors to the wall clock (D-027).
InvalidId
An identifier the crate’s own encodings cannot represent (D-061).
Distinct from Self::NotFound, and the distinction is defect J: this
id was refused, not looked up. validate_id used to return NotFound
here, which tells a caller the thing is missing and invites them to
create it — with the same id, which will be refused again.
OverlappingInterval
Two valid-time intervals for one relationship claim the same instant.
Distinct from Self::SingleOpenViolation, which is the storage layer’s
guard and covers only the open sentinel. This is the general case, and
it is refused at the API rather than by a trigger (D-060): raw SQL against
the same file can still write an overlap, and §4.2 says so.
The consequence of allowing one is not an error later but a wrong answer:
query_as_of_edges at an instant inside both returns the relationship
twice, and every weighted algorithm downstream double-counts that edge.
Boxed, and it is the only variant that is (D-075). Seven Strings is
168 bytes, which made DbError — and therefore every Result in the
crate, on the Ok path too — larger than clippy::result_large_err’s
threshold the moment D-060 added it. The other variants are well under.
Boxing the rarest one keeps the whole error small rather than trimming
what a caller is told; matches!(err, OverlappingInterval { .. }) is
unaffected, which is how every call site uses it.
CurrentDrift
RebuildFailed
RebuildInterrupted
A chunked shadow rebuild was abandoned rather than committed (T1.2, D-082).
Distinct from Self::RebuildFailed, and the distinction is the whole
point: RebuildFailed means the repair ran and did not repair, which is
a reason to distrust the ledger. This means the repair did not run —
something invalidated the work in progress and it was discarded before it
could be swapped in. links_current is untouched and whatever was true
of it before is still true. The action is to retry.
WriterDroppedResponder
WriterStopped(String)
The actor’s task did not join cleanly at crate::Database::close.
Distinct from Self::WriterUnavailable, which means the channel is
gone while the handle is still in use. This is the shutdown path telling
a caller that the write actor panicked — which close() used to swallow,
so a database whose write path had died closed “successfully” (Wave 4.2).
RecordedAtRegression
FutureRecordedAt
The stored transaction-time floor is in the future (0.13.5, W7.4, §3.4).
The clock is raised to MAX(recorded_at) at open so that stamps stay
strictly increasing across restarts. That makes a single row from the
future — a skewed host, a bad import, a fixture that escaped — this
process’s floor, and every stamp it issues lands at or after it. Those
rows are then written, so the next open reads the same floor back: the
damage is permanent, and it spreads.
Refused at open rather than absorbed, which is where the crate can still
tell the difference between a stamp it wrote and one it did not.
macrame::FutureStampPolicy widens or waives the bound; waiving it
opens the file to be read, and does not repair it.
BulkCancelled
A chunked bulk write stopped because its caller asked it to (0.13.8, W7.6, D-181).
Not a failure of the ledger, and the only DbError a caller can
cause on purpose. Nothing is rolled back: the chunks that committed
before the token was seen are committed, which is the same per-chunk
boundary crate::Database::bulk_import already documents. How many
rows those were is on BulkInterrupted::written, the error this
arrives inside.
It carries no count of its own precisely so that there is one place to read the count from, whether the stop was a cancellation or a constraint.
Trait Implementations§
Source§impl Error for DbError
impl Error for DbError
Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()
Source§impl From<BulkInterrupted> for DbError
impl From<BulkInterrupted> for DbError
Source§fn from(e: BulkInterrupted) -> Self
fn from(e: BulkInterrupted) -> Self
Discards written. That is the point: a caller writing ? into a
function returning Result has decided the partial count is not
something they will act on, and this puts that decision at the place it
is taken rather than inside the crate.
Auto Trait Implementations§
impl !RefUnwindSafe for DbError
impl !UnwindSafe for DbError
impl Freeze for DbError
impl Send for DbError
impl Sync for DbError
impl Unpin for DbError
impl UnsafeUnpin for DbError
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
Source§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request