Skip to main content

DbError

Enum DbError 

Source
#[non_exhaustive]
pub enum DbError {
Show 41 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, }, InvalidBranchId(String), UnknownBranch(String), BranchExists(String), BranchNotArchivable { branch: String, reason: String, }, ForkPrecedesParent { branch: String, parent: String, forked_at: String, parent_forked_at: String, }, CrossLineage { id: String, held_by: String, attempted: String, }, BranchMismatch { view: String, named: 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, }, SnapshotWriteFailed { 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
Non-exhaustive enums could have additional variants added in future. Therefore, when matching against variants of non-exhaustive enums, an extra wildcard arm must be added to account for any future variants.
§

Engine(Error)

§

Migration

Fields

§to: u32
§reason: String
§

InvalidEdgeType(String)

§

SingleOpenViolation

Fields

§source_id: String
§target_id: String
§edge_type: String
§

NotFound(String)

§

DimMismatch

Fields

§got: usize
§expected: usize
§model: String
§

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

Fields

§model: String
§table: String
§

InvalidBranchId(String)

branches is append-only under two unconditional triggers, so a name is written once and can never be corrected. The rule is deliberately not Self::InvalidModelName’s — a branch_id is always a bound value and never a spliced identifier, so what is refused here is the pair of names that read as one another rather than the ones that would break SQL.

§

UnknownBranch(String)

Named rather than left to the foreign key, because the caller asked about a branch and a constraint violation would answer about a column.

§

BranchExists(String)

Refused rather than ignored, which is the interesting half: an INSERT OR IGNORE would return a handle to a lineage with a different parent and fork point than the caller asked for, which is D-069’s right-looking answer to a question nobody asked.

§

BranchNotArchivable

crate::Database::archive_branch refused (0.14.13, §15.4, D-230).

Four conditions share one variant because they share one answer: the lineage stays, and the caller has to change something about the ledger before asking again. The reason says which — the trunk, a lineage with descendants, or a lineage whose concepts another lineage’s hot edges still name.

The fourth condition, a name that is not registered, is deliberately not here: it is Self::UnknownBranch, the same answer every other branch-taking surface gives, because a typo should read the same way wherever it is made.

Fields

§branch: String
§reason: String
§

ForkPrecedesParent

The cross-row half of the fork-point invariant, which no CHECK can see (§15.2): fork points must not decrease down a root path.

A branch cut before its parent was is a branch that inherits nothing whatever from the parent it names — every row that parent wrote falls past the child’s cutoff — so its parent_id and its visible history say different things, silently.

Compared against the parent’s forked_at and not its created_at, which the schema comment originally called for: created_at on the trunk is stamped from SystemTime::now() during migration, before the database’s clock exists, so it is not on the same timeline as anything else. See Database::fork.

Fields

§branch: String
§parent: String
§forked_at: String
§parent_forked_at: String
§

CrossLineage

A branch tried to restate a concept another lineage already holds (§15.2, v12, D-225).

§A guard that existed for three releases with nothing able to fire it

trg_concepts_cross_lineage has been in the schema since v12 and AbortKind::CrossLineage has recognised it since, but classify had no arm for that kind, so it fell through to DbError::Engine — the opaque variant every other guard exists to avoid. Nothing was wrong with that until 0.14.8, because until 0.14.8 no write in this crate could name a lineage and no caller could reach the trigger. It is the same shape D-224 found in a comment and D-223 found in a filter: machinery written for an unbuilt caller is exercised by nothing, so a gap in it is invisible in a green suite.

held_by is read back on the error path rather than parsed out of the abort message, for RecordedAtRegression’s reason: the trigger cannot put it in the text, and the database knows it.

Fields

§held_by: String
§attempted: String
§

BranchMismatch

A write reached a BranchView carrying a different lineage’s name (§15.4, 0.14.9, D-226).

§Why this is refused rather than overwritten

The view exists to spare a caller from threading a BranchId through every call, so the obvious reading is that it should simply stamp its own lineage on whatever it is handed. That is right for an assertion that names none — which is the shape a caller building through the view produces, and the one this does not refuse. It is wrong for an assertion that names a different lineage, because that assertion is evidence the caller believed something about where the write was going, and silently relabelling it discards the belief instead of contradicting it.

The failure this catches is holding two views and passing one’s assertion to the other, which nothing in the type system prevents: BranchView is Clone and both views have the same methods, so the mistake reads correctly at the call site and produces rows on the wrong lineage. On a ledger where a lineage is what a belief means, that is not a misfiled row — it is an assertion attributed to the wrong belief.

Fields

§view: String

The lineage the view carries.

§named: String

The lineage the assertion named.

§

SubgraphTooLarge

Fields

§budget: usize
§

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.

Fields

§source_id: String
§target_id: String
§weight: f64
§

ReplayCorrupt

Fields

§seq: i64
§reason: String
§

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).

Fields

§path: String
§reason: String
§

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::SnapshotIncompatiblea different build wrote this. Ordinary after an upgrade.
  • Self::ReplayCorruptthe 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.

Fields

§path: String
§reason: String
§

SnapshotWriteFailed

A snapshot that could not be written (0.14.23, W12.23, C-2, D-240).

The fourth case in the family above and the last one missing, which is why it is worth saying what the other three had in common: each names the subject a caller has to go and fix. Every failure inside save_snapshot — the directory, the serialization, the compression, the temp file, the write, the flush, the rename, and the directory flush after it — used to be Self::ReplayCorrupt, which says the ledger is damaged, the worst thing this system can say. A full disk said it.

  • Self::SnapshotIncompatiblea different build wrote this.
  • Self::SnapshotCorruptthe cache is damaged, delete the file.
  • Self::ReplayCorruptthe ledger is damaged.
  • This — the cache could not be written. Nothing is damaged and nothing is lost: Doctrine VI makes a snapshot derivative, so the next start folds from the previous anchor and the whole cost is a slower start. The subject is the filesystem.

The read half of this correction shipped at 0.13.12: load_snapshot stopped answering ReplayCorrupt { seq: 0 } for a damaged file, for exactly this reason (D-185). The write half kept it for ten releases.

One variant covers the directory flush as well, and that is D-186’s decision rather than a simplification here: a failed sync_directory leaves the snapshot at its final name and readable, unable only to promise the name survives a power loss, and D-186 already placed it in “the same class the file’s own sync_all failure already returns”. reason names which step failed.

Fields

§path: String
§reason: String
§

PayloadVersion

Fields

§got: u8
§max: u8
§

ArchiveViolation

Fields

§table: String
§

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.

Fields

§marker: String
§

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

§

DiagnosticConn

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.

Fields

§path: String
§reason: String
§

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.

Fields

§window: Duration
§reason: String
§

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.

Fields

§

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).

Fields

§value: String
§reason: String
§

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.

Fields

§reason: String
§

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.

Fields

§overlap: Box<Overlap>
§

CurrentDrift

Fields

§

RebuildFailed

Fields

§

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.

Fields

§reason: String
§

WriterUnavailable

§

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

Fields

§

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.

Fields

§stamp: String
§limit: String
§

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.

Implementations§

Source§

impl DbError

Source

pub fn kind(&self) -> ErrorKind

This error’s ErrorKind.

The match below has no wildcard arm, and that is the whole point (D-242, §14.1 C-3). DbError is #[non_exhaustive], so no match outside this crate can be checked for completeness — but inside it, the compiler still checks. A variant added without a line here fails to build, which is the guarantee D-207 traded away for the binding and could not get back there.

Trait Implementations§

Source§

impl Debug for DbError

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Display for DbError

Source§

fn fmt(&self, __formatter: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Error for DbError

Source§

fn source(&self) -> Option<&(dyn Error + 'static)>

Returns the lower-level source of this error, if any. Read more
1.0.0 · Source§

fn description(&self) -> &str

👎Deprecated since 1.42.0:

use the Display impl or to_string()

1.0.0 · Source§

fn cause(&self) -> Option<&dyn Error>

👎Deprecated since 1.33.0:

replaced by Error::source, which can support downcasting

Source§

fn provide<'a>(&'a self, request: &mut Request<'a>)

🔬This is a nightly-only experimental API. (error_generic_member_access)
Provides type-based access to context intended for error reports. Read more
Source§

impl From<BulkInterrupted> for DbError

Source§

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.

Source§

impl From<Error> for DbError

Source§

fn from(source: Error) -> Self

Converts to this type from the input type.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoRequest<T> for T

Source§

fn into_request(self) -> Request<T>

Wrap the input message T in a tonic::Request
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToString for T
where T: Display + ?Sized,

Source§

fn to_string(&self) -> String

Converts the given value to a String. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more