#[non_exhaustive]pub enum DjogiError {
Show 41 variants
Auth(AuthError),
Db(DbError),
#[non_exhaustive] NotFound {
table: &'static str,
},
#[non_exhaustive] MultipleObjects {
table: &'static str,
count_seen: usize,
},
IdGeneration(IdGenerationError),
#[non_exhaustive] RelationUnloaded {
model: &'static str,
field: &'static str,
},
Serde(Error),
Visage(VisageError),
#[non_exhaustive] GoneAggregate {
model: &'static str,
id: String,
reason: &'static str,
},
Decode(String),
#[non_exhaustive] MissingIdempotencyKey {
model: &'static str,
},
Validation(String),
LockConflict(DbError),
StreamOutsideTransaction,
#[non_exhaustive] TransactionPoisoned {
reason: &'static str,
},
#[non_exhaustive] SessionStatementDisallowedInTransaction {
statement: &'static str,
},
#[non_exhaustive] RawTransactionControlDisallowedInTransaction {
statement: &'static str,
},
#[non_exhaustive] UnsupportedAggregate {
op: &'static str,
reason: &'static str,
},
#[non_exhaustive] InvalidSubqueryModifier {
op: &'static str,
reason: &'static str,
},
AliasCollision {
alias: String,
},
#[non_exhaustive] PoolTimeout {
phase: &'static str,
},
SetRoleOutsideTransaction,
InvalidRoleName(String),
Predicate(PortablePredicateError),
#[non_exhaustive] SetOpArmInvalid {
table: &'static str,
side: &'static str,
reason: &'static str,
},
#[non_exhaustive] SetOpOuterOrderingInvalid {
table: &'static str,
reason: &'static str,
},
IsolationLevelOnNestedScope {
requested: IsolationLevel,
},
ConstraintModeOutsideTransaction,
UnknownConstraintName(String),
ConstraintNotDeferrable(String),
EmptyDeferConstraintsScope,
ConflictingDeferrabilitySpec {
model_type_name: String,
field_name: String,
first: (bool, bool),
second: (bool, bool),
},
OrphanDeferrabilitySpec {
model_type_name: String,
field_name: String,
},
DuplicateConstraintName {
constraint_name: String,
first_model: String,
first_field: String,
second_model: String,
second_field: String,
},
ConcurrentReadsRequirePoolContext,
#[non_exhaustive] MergeSourceInvalid {
table: &'static str,
reason: &'static str,
},
#[non_exhaustive] MergeBranchInvalid {
table: &'static str,
reason: String,
},
#[non_exhaustive] MergeNoBranches {
table: &'static str,
reason: String,
},
PresentationStartup(Vec<PresentationStartupError>),
#[non_exhaustive] UnsupportedPostgresVersion {
detected_major: u32,
detected_minor: u32,
minimum_major: u32,
},
#[non_exhaustive] StalePhaseZeroStatement {
refusal_reason: &'static str,
statement: String,
},
}Expand description
The single error type returned by every Djogi CRUD operation.
Every fallible call site in djogi (Model::create, QuerySet::fetch_all,
transaction::atomic, DjogiContext::set_tenant, every raw escape hatch,
every spatial / FTS / JSONB helper) returns Result<T, DjogiError>. The
crate-scoped crate::Result<T> alias spells exactly that — adopter code
signs functions with -> djogi::Result<T> and uses ? to propagate.
§Error taxonomy
DjogiError groups failures by where they originate, not by HTTP-style
status code. The most common branches:
- Database-driver errors —
Dbwraps everytokio_postgres::Error(network, constraints, syntax, auth) behind theDbErrorfacade so this enum does not leaktokio_postgrestypes. - Expected-row-count violations —
NotFoundfor zero rows fromModel::get/QuerySet::fetch_one,MultipleObjectsfor >1 row fromfetch_one. Both carry the offending table name. - Concurrency / contention —
LockConflictfor40001/40P01/55P03SQLSTATE classes (serialization failures, deadlocks,NOWAITrejections),PoolTimeoutfordeadpoolcheckout exhaustion. Both classify as transient — seeDjogiError::is_transient. - Auth / RLS —
AuthwrapsAuthErrorfor authentication / authorization failures from the auth substrate;SetRoleOutsideTransactionandInvalidRoleNamesurface RLS- overlay misuse. - Misuse / runtime invariants
Validationfor runtime argument validation failures,MissingIdempotencyKeyfor upsert-attribute gaps,StreamOutsideTransactionfor cursor /QuerySet::streamoutsideatomic,UnsupportedAggregatefor IR / Postgres aggregate mismatches. - Decode / serialization —
DecodeforFromPgRowfailures,Serdefor outbox JSON serialization,Visagefor visage-projectionTryFrom<&Model>failures. - ID generation —
IdGenerationwraps HeeRanjID-side codec failures. - Aggregate lifecycle —
GoneAggregatefor terminal “already gone” signals on a previously-observed aggregate;RelationUnloadedfor prefetch-cache misses on a strict-mode resolved-relation accessor. - Spatial —
Geo(gated on thespatialfeature) wraps coordinate / EWKB codec errors.
§Retry classification
DjogiError::is_transient returns true for LockConflict,
PoolTimeout, and the small set of variants whose failures are expected
to be retryable. The framework also recognises the SQLSTATE classes that
indicate contention vs. other database failures — both classifications
are intended for use in caller-side retry policies (see also
retry_on_conflict).
§#[non_exhaustive]
Both DjogiError and its struct-form variants (NotFound,
MultipleObjects, RelationUnloaded, GoneAggregate,
MissingIdempotencyKey, UnsupportedAggregate, PoolTimeout,
InvalidRoleName, etc.) are #[non_exhaustive]. Downstream match
expressions MUST include a wildcard arm, and downstream destructuring
MUST use .. — adding a new variant or new field to a struct variant
is therefore a non-breaking change. The cost is one extra wildcard arm
at every match site; the benefit is the framework can grow new failure
shapes (added a half-dozen) without breaking adopter code.
Construction from outside this crate is also blocked by the variant-level
#[non_exhaustive]. Use the public constructors
(DjogiError::not_found, DjogiError::multiple_objects, etc.) when
surfacing a Djogi-flavoured error from adopter code; this matches the
pattern set by std::io::Error, hyper::Error, and similar
well-designed error types.
§Why one error type
Every framework call returning Result<T, DjogiError> keeps ?
propagation ergonomic across CRUD, raw SQL, transactions, auth, and
spatial/JSONB layers. Per-subsystem error types would force adopter
code into manual conversion at every layer boundary — exactly the
friction that drove the framework’s “one error type at the public API”
design.
Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
Auth(AuthError)
Authentication or authorization failure bubbled up through the auth
substrate. Wraps AuthError so
DjogiAuth::authenticate and
DjogiAuth::verify failures flow
through ? inside atomic()-managed operations without explicit
mapping.
§Transitivity
Because AuthError is #[non_exhaustive], this variant also inherits
that forward-compatibility contract at the DjogiError level: a
match on DjogiError::Auth(e) that then matches on e must still
include a wildcard arm for AuthError.
Db(DbError)
Raw database or driver error.
#[non_exhaustive]NotFound
Model::get / QuerySet::fetch_one saw zero rows. The table field
records the SQL table that was queried so log lines and error reports
remain meaningful once errors propagate far from the call site.
Fields
This variant is marked as non-exhaustive
#[non_exhaustive]MultipleObjects
QuerySet::fetch_one (or similar) saw more than one row when exactly
one was expected. count_seen is the number actually observed — with
the LIMIT 2 strategy this is always exactly 2, but storing the real
count keeps the error meaningful for future code paths that may
pre-count rows differently.
Fields
This variant is marked as non-exhaustive
IdGeneration(IdGenerationError)
ID generation failed.
#[non_exhaustive]RelationUnloaded
A relation accessor that requires an eagerly-loaded cache
(ForeignKeyResolved::expect_resolved / OneToOneFieldResolved::expect_resolved)
was invoked against a wrapper whose cache is empty. The caller
asserted a prefetch() / select_related() ran earlier but none
did — this is a strict-mode user error, not a query failure.
model is the source model name (e.g. "Vehicle"), field is the
relation field on that model (e.g. "owner_id"). Both are compile-time
&'static strs — the macro fills them in from the struct definition
in . Until then, callers supply them at the call site.
Fields
This variant is marked as non-exhaustive
Serde(Error)
JSON serialization / deserialization failed. Raised by the
Task 6 transactional-outbox emitter when serde_json::to_value
cannot lower a model row into a JSON document — typically because
a user field’s Serialize impl returned an error. Wraps the
serde_json::Error verbatim so the caller can inspect the
underlying failure.
Visage(VisageError)
Visage projection failure — raised when a TryFrom<&Model> impl on
a generated visage cannot convert the row.
Known triggers include
VisageError::UnresolvedRelation, raised
when a relation-nesting visage is projected from a model whose
relation fields were not prefetch()-ed or select_related()-ed,
and VisageError::PresentationCodec
from fallible protected-field presentation codecs.
Introduces this variant so the visage-scoped
reverse-FK / M2M accessors can flow a fallible peer-visage conversion
through ? without losing the VisageError structure. The
#[from] shorthand on the inner type produces the
impl From<VisageError> for DjogiError conversion the emitted
accessors rely on.
#[non_exhaustive]GoneAggregate
A DDD-style aggregate was hard-deleted mid-operation,
invalidating any further work against its id. Task
7.7 introduces this variant as the canonical terminal signal
for “the aggregate you’re operating on no longer exists”
distinct from NotFound (which covers initial-lookup
misses) in that the caller already observed the aggregate
earlier in the same operation.
model is the owning model’s type_name (same source as
MissingIdempotencyKey::model). id is the PK rendered to
a string (no generic parameter so the error type stays
object-safe and usable across model boundaries). reason is
a &'static str describing why the aggregate is gone (e.g.
"hard-deleted by admin", "retention policy evicted").
Classified as terminal by
DjogiError::is_transient — retry_on_conflict does not
retry this variant because retrying against a deleted row
cannot succeed.
Fields
This variant is marked as non-exhaustive
Decode(String)
A column decode failure produced by
FromPgRow::from_pg_row.
Raised when tokio_postgres::Row::try_get returns an error for a
model field — for example, when the wire type at a given ordinal
position cannot be converted to the expected Rust type. Preserves
the contract: every CRUD failure flows through
DjogiError rather than aborting the task via panic!.
The inner String carries the column name and the driver error
so the caller can identify which field failed without inspecting
the raw tokio_postgres::Error.
#[non_exhaustive]MissingIdempotencyKey
A convenience method that consumes the descriptor’s
idempotency_key slot
(create_or_find /
bulk_upsert_by_descriptor) was invoked against a model
whose #[model(...)] attribute does not set the key.
Task 7.5 introduces this variant as the runtime pointer at
the attribute the caller needs to add.
model is the type_name from [ModelDescriptor::type_name]
a &'static str the macro lifts directly from the struct
identifier.
Fields
This variant is marked as non-exhaustive
Validation(String)
A runtime argument-validation failure produced by a CRUD
convenience method — the caller’s request is well-typed at
compile time but fails a runtime invariant (e.g.
bulk_upsert’s conflict_cols naming a column that does not
exist on the model). Task 7d introduces this variant
for bulk_upsert’s allow-list check; future phases may add
more callers.
The inner String is a human-readable description of the
failure. No &'static str because callers interpolate the
offending column name / table name into the message.
LockConflict(DbError)
A FOR UPDATE NOWAIT / FOR UPDATE request could not acquire
its row lock, or a SERIALIZABLE / REPEATABLE READ transaction
encountered a serialization failure, or Postgres detected a
deadlock and aborted one participant. introduces
this variant so the retry helper (retry_on_conflict) and the
caller can branch on lock-contention vs other database errors.
Retryable SQLSTATE classes carried here:
40001—serialization_failure40P01—deadlock_detected55P03—lock_not_available(NOWAITrejection) Other database failures —unique_violation,foreign_key_violation, connection drops — still flow throughDb. The classifier [is_lock_error] keeps the variant boundary tight.
StreamOutsideTransaction
QuerySet::stream or DjogiContext::raw_stream was called on a
pool-backed context (i.e. outside an atomic() scope).
Postgres named cursors are transaction-local — they require an open
transaction to exist. Calling stream on a pool-backed context is a
caller error that is detected at stream construction time, not at the
first poll_next. This makes the error surface immediate and
actionable rather than deferred to the first row consume.
Fix: wrap the stream consumer in
atomic(&mut ctx, |ctx| Box::pin(async move { … })) so ctx is
transaction-backed when stream is called.
#[non_exhaustive]TransactionPoisoned
A transaction-backed crate::DjogiContext was marked unsafe to
continue after a nested atomic() future was dropped before the
framework could run savepoint cleanup.
Rust Drop cannot await ROLLBACK TO SAVEPOINT / RELEASE SAVEPOINT, so the safe contract is fail-closed: framework-owned
operations reject further work, commit rolls the outer transaction
back instead of committing it, and the caller must retry the outer unit
of work from a fresh transaction.
Classified as terminal by DjogiError::is_transient — retrying
against the same poisoned context cannot make the transaction safe to
commit.
Fields
This variant is marked as non-exhaustive
#[non_exhaustive]SessionStatementDisallowedInTransaction
A transaction-backed raw SQL call attempted a session-scoped statement
that atomic() cannot safely scrub on commit/rollback.
This variant is used by the raw SQL bypass harness to reject
session-level control statements such as plain SET, RESET,
LISTEN, UNLISTEN, PREPARE, DEALLOCATE, and DISCARD when the
context is already inside an atomic() transaction. Those statements
either outlive the surrounding transaction entirely or invite callers
to assume rollback will clean them up when Postgres semantics say
otherwise.
statement is the canonical top-level keyword ("SET", "RESET",
etc.) that triggered the refusal. The fix is structural: use a
transaction-local form such as SET LOCAL / SET CONSTRAINTS /
SET TRANSACTION, or run the session-scoped statement on a pool-backed
context outside the transaction.
Classified as terminal by DjogiError::is_transient
retrying the same closure against the same SQL will fail the same way.
Fields
This variant is marked as non-exhaustive
#[non_exhaustive]RawTransactionControlDisallowedInTransaction
A transaction-backed raw SQL call attempted to issue transaction-control
SQL through the raw escape hatch instead of using Djogi’s transaction
lifecycle methods.
This variant is used by the raw SQL bypass harness (#306) to reject
transaction-control statements such as BEGIN, START TRANSACTION,
COMMIT, ROLLBACK, END, ABORT, SAVEPOINT, RELEASE [SAVEPOINT],
and ROLLBACK [WORK|TRANSACTION] TO [SAVEPOINT] when the context is
already inside an atomic() transaction. Those statements bypass
framework bookkeeping: raw COMMIT skips on_commit callback drain,
raw ROLLBACK skips rollback cleanup and callback discard, and raw
savepoint control desynchronizes savepoint_depth.
statement is the canonical top-level transaction-control keyword
("BEGIN", "COMMIT", etc.) that triggered the refusal. The fix is
structural: use Djogi’s atomic() / commit() / rollback() API, or
run the transaction-control SQL on a pool-backed context outside any
atomic() scope.
Classified as terminal by DjogiError::is_transient
retrying the same closure against the same SQL will fail the same way.
Fields
This variant is marked as non-exhaustive
#[non_exhaustive]UnsupportedAggregate
An aggregate’s DISTINCT modifier combination is not supported by Postgres syntax or by Djogi’s current IR.
§When this surfaces
Raised by the fetch-time legality check in
[crate::expr::sql::check_aggregate_legality] for three value-aggregate
cases that cannot be represented by the kind-state split alone:
COUNT(DISTINCT *)—COUNT(DISTINCT *)is not valid SQL. UseCOUNT(DISTINCT col)viacrate::query::field::FieldRef::countinstead.STRING_AGG(DISTINCT col, sep)without per-aggregateORDER BYPostgres requires an explicit ordering when DISTINCT is combined withSTRING_AGG. Chain.order_by(...)on the aggregate to make the shape well-formed.COUNT(*) ORDER BY ...— theCOUNT(*)emitter has no argument slot to attach per-aggregate ordering to, so accepting the modifier would silently drop it.opis the aggregate function keyword (e.g."COUNT(*)","STRING_AGG").reasonis a human-readable description of why the combination is rejected.
Fields
This variant is marked as non-exhaustive
#[non_exhaustive]InvalidSubqueryModifier
A subquery conversion rejected a modifier or quantified operator that does not have valid SQL meaning in that subquery position.
§When this surfaces
VisageQuerySet::selectingandVisageExists::newrejectorder_by/limit/offsetcarried on a visage queryset: silently dropping those clauses would change semantics.- Quantified subquery comparisons reject
IS [NOT] DISTINCT FROM, which has no PostgresANY/ALLform.
Fields
This variant is marked as non-exhaustive
AliasCollision
The emitted SELECT list contains two columns with the same alias; the decoder would read the wrong value for one of the columns. This is a Djogi internal bug — a future API extension likely introduced a path that collides with a group-key column name or another aggregate alias. The check runs before any SQL is sent to Postgres so the collision is caught immediately rather than silently returning wrong data.
#[non_exhaustive]PoolTimeout
A pool checkout exceeded its configured wait / create / recycle
timeout. Pairs with
DjogiPoolBuilder::timeout
so callers can branch on saturation explicitly without inspecting
the underlying deadpool_postgres::PoolError.
phase distinguishes the deadpool timeout type:
"wait"— the pool is atmax_sizeand no slot freed within the configured wait window. Tunemax_sizeupward or stop holding connections across awaits unrelated to the database."create"—Manager::create(opening a fresh socket) timed out. Network or DB-side problem, not pool sizing."recycle"— recycling an existing object on the checkout path timed out. Same root cause as"create"forVerified/Cleanrecycling methods that issue queries. All three are saturation / slow-recovery signals: the right response is to back off and retry, not to fail the operation permanently.DjogiError::is_transientreturnstrueforPoolTimeoutso generic retry helpers that branch onis_transient(or its inverseis_terminal) treat pool timeouts as retryable rather than dead-lettering them as permanent business failures. Note that djogi exposes two retry helpers with different policy:crate::transaction::retry_on_conflictretries immediately, whilecrate::transaction::retry_on_conflict_with_backoffsleeps between transient failures usingcrate::transaction::TransactionRetryBackoff. Pool saturation usually belongs on the backoff path, not the immediate-retry path. Callers that need a bespoke policy can still match onPoolTimeoutexplicitly, and the backoff policy can include/excludePoolTimeoutretries viawith_retryable_error_classes(...).
Fields
This variant is marked as non-exhaustive
SetRoleOutsideTransaction
DjogiContext::set_role was invoked on a pool-backed context
rather than inside an atomic transaction.
introduces this variant for the security-overlay row-level
security helper: SET LOCAL ROLE is bound to the surrounding
transaction and reverts at COMMIT/ROLLBACK, so calling it
outside a transaction would either fail outright or — worse
leak the role onto the pooled connection where the next
checkout-victim would inherit it. Surfacing this as a
dedicated variant lets callers branch on the misuse without
inspecting the underlying SQLSTATE.
Classified as terminal by DjogiError::is_transient
retrying the same closure cannot turn a pool-backed context
into a transactional one.
InvalidRoleName(String)
DjogiContext::set_role was invoked with a role name that
fails the byte-level Postgres identifier check.
introduces this variant so role-name validation surfaces
before any SQL is sent — the framework refuses to interpolate
an untrusted string into SET LOCAL ROLE even when the
caller has already quoted it.
The accepted grammar is the standard Postgres unquoted
identifier shape: an ASCII letter or underscore followed by
ASCII alphanumerics or underscores, up to 63 bytes. Embedded
quotes, control characters, and non-ASCII bytes are all
rejected. The variant carries the offending name so log
lines and error reports can identify what was rejected.
Classified as terminal by DjogiError::is_transient
a malformed role name is a programming error, not a
race-condition.
Predicate(PortablePredicateError)
A portable predicate could not be lowered to SQL.
installs the direct-Q<T> SQL walker; portable predicate leaves
dispatch through crate::model::Model::__djogi_emit_field_predicate
(overridden by PR2d’s macro on every PK-backed #[model]-emitted
impl). Failure modes — unknown model, unknown field, unknown
LookupOp for a known field, payload-shape mismatch, future
Sassi predicate variant — surface here as a typed
[crate::query::PortablePredicateError] before the SQL ever
touches the database.
Classified as terminal by DjogiError::is_transient — a
portable-predicate lowering failure is a framework / model
invariant violation, not a transient database condition.
Retrying the same closure cannot turn an unknown field into a
known one.
#[non_exhaustive]SetOpArmInvalid
A SetOpQuerySet arm carried
state that cannot ride through a Postgres set-operation
subquery: registered prefetch paths, registered select_related
paths, a row-level lock, or a cache binding. Cluster
4B (issue #101) introduces this variant alongside the typed
set-op surface (.union(...) / .union_all(...) /
.intersect(...) / .except(...)).
§Why this is a typed error, not a silent drop
Quietly stripping select_related / prefetch registrations
when an arm enters a set op would silently change the row
shape the caller expected: select_related extends the
projection, prefetch fans out follow-up queries on the
returned rows. Both would either change the column count
(breaking the set-op type compatibility rule) or silently drop
data the caller asked for. Returning a typed error at the
terminal — before any SQL hits the database — keeps the
failure mode actionable. Locks (FOR UPDATE) inside a set-op
subquery are rejected by Postgres at parse time anyway; we
surface a higher-fidelity error before the round trip.
side identifies which arm tripped the check ("left" or
"right"); reason is a short human-readable explanation
that names the offending registration.
Classified as terminal by DjogiError::is_transient
the caller built an incompatible set-op shape; retrying the
same call cannot turn a .cache(...)-bound arm into a
cache-free one.
Fields
This variant is marked as non-exhaustive
#[non_exhaustive]SetOpOuterOrderingInvalid
A SetOpQuerySet’s outer
ORDER BY carried an expression-form ordering term that
Postgres rejects on set-operation outer ordering.
(issue #101) introduces this variant alongside the
typed set-op surface.
§Why this is a typed error, not a silent pass-through
Postgres set-operation ORDER BY only accepts output column
names (or column position numbers) — arbitrary expressions are
rejected at parse time. Today the only way to produce a
non-column outer ordering is the spatial
order_by_distance(...) helper, which emits a ST_Distance(...)
expression. Letting that ride through to Postgres would surface
a low-level parser error (syntax error at or near "(",
ORDER BY position out of range, or similar) that does not name
the offending operation. Djogi catches the case at SQL-build
time and surfaces a higher-fidelity error before the round
trip, naming the table and explaining the constraint.
table identifies the model whose set-op carries the
incompatible ordering; reason is a short human-readable
explanation that names the kind of ordering rejected and the
recommended workaround.
Classified as terminal by DjogiError::is_transient
the caller built an incompatible set-op shape; retrying the
same call cannot turn an expression-form ordering into a
column-form one. The fix is at the call site.
Fields
This variant is marked as non-exhaustive
IsolationLevelOnNestedScope
djogi::transaction::atomic_with(level, &mut tx_ctx, ...) was
invoked on a transaction-backed crate::DjogiContext — i.e.
inside an already-open atomic scope.
(issue #168) introduces this variant alongside the typed
crate::transaction::IsolationLevel surface.
Postgres pins the isolation level for the entire transaction at
the outer BEGIN; SAVEPOINT does not open a sub-transaction
with its own isolation knob, and SET TRANSACTION ISOLATION LEVEL issued after the first non-control statement is rejected
with SQLSTATE 25001 (active SQL transaction). Surfacing this
as a typed variant lets callers branch on the misuse before
the SQL flies; the alternative would be a deferred SQLSTATE
surprise that names neither the outer BEGIN nor the requested
level.
The variant carries the crate::transaction::IsolationLevel
the caller requested so log lines and error reports identify
what was rejected. Use crate::transaction::atomic for
nested scopes — the savepoint inherits the outermost
transaction’s isolation level.
Classified as terminal by DjogiError::is_transient — a
nested-scope isolation request is a programming error, not a
race condition. Retrying the same closure cannot turn a
savepoint into a fresh outermost transaction.
Fields
requested: IsolationLevelIsolation level the caller requested. &'static-like
the enum is Copy so logging callers can read it without
borrowing the variant.
ConstraintModeOutsideTransaction
DjogiContext::defer_constraints / set_constraints_immediate
was invoked on a pool-backed context rather than inside an
atomic transaction. issue #169)
introduces this variant alongside the typed
crate::transaction::DeferScope surface.
SET CONSTRAINTS is transaction-scoped in Postgres — outside a
transaction it would either fail outright or, on the
implicit per-statement transaction surrounding a single
statement, evaporate before any subsequent statement could
observe the deferred state. Both outcomes are programming
errors, so the framework refuses to issue the SQL.
Classified as terminal by DjogiError::is_transient
retrying cannot turn a pool-backed context into a
transactional one. Wrap the call in
crate::transaction::atomic to get a transaction scope.
UnknownConstraintName(String)
DjogiContext::defer_constraints /
set_constraints_immediate was called with a
crate::transaction::DeferScope::Named payload that
referenced an unknown constraint. issue
#169) introduces this variant.
“Unknown” means the constraint name was not found on any
#[derive(Model)]-emitted crate::DeferrabilitySpec
inventory entry. The lookup uses the conventional
<table>_<column>_fkey shape ({table}_{column}_fkey,
truncated to Postgres’ 63-byte identifier limit when
necessary) for foreign-key constraints declared in
adopter #[model] structs.
Surfacing the typo as a typed error before the SQL flies is
the value-add over ctx.raw_execute("SET CONSTRAINTS \"typo\" DEFERRED"): Postgres would raise 42704 (undefined_object) for an unknown constraint, but only
after a round trip and without naming the descriptor it
should have come from.
The variant carries the offending name so log lines identify
what was rejected. Classified as terminal by
DjogiError::is_transient — retrying cannot turn an
unknown name into a known one.
ConstraintNotDeferrable(String)
DjogiContext::defer_constraints was called with a
crate::transaction::DeferScope::Named payload that
referenced a constraint declared as
non-deferrable (#[field(deferrable = false)], the default).
Issue #169) introduces this variant.
Postgres rejects SET CONSTRAINTS <name> DEFERRED on a
non-deferrable constraint with SQLSTATE 0A000
(feature_not_supported). The framework checks the descriptor’s
crate::DeferrabilitySpec inventory and surfaces a typed
error before the SQL flies — same value-add as
Self::UnknownConstraintName.
The fix is at the model declaration: declare the FK as
#[field(deferrable = true)] (and optionally
initially_deferred = true for DEFERRABLE INITIALLY DEFERRED). The constraint must be deferrable to participate
in SET CONSTRAINTS at runtime.
Classified as terminal by DjogiError::is_transient — a
non-deferrable constraint cannot be deferred at runtime
regardless of how many retries.
EmptyDeferConstraintsScope
DjogiContext::defer_constraints /
set_constraints_immediate was called with
crate::transaction::DeferScope::Named carrying an empty
slice. issue #169)
follow-up fix.
SET CONSTRAINTS <name list> DEFERRED|IMMEDIATE requires at
least one name; Postgres rejects the bare-comma grammar with
SQLSTATE 42601 (syntax error). Composing the SQL from an
empty slice would produce SET CONSTRAINTS DEFERRED — an
extra space + missing list. Reject before SQL composition so
the caller gets a typed error naming the misuse rather than a
deferred Postgres parse error.
The canonical fix is one of:
- drop the
Namedwrapper and useDeferScope::Allif the intent is “every deferrable constraint”; - skip the call entirely when the names slice is empty;
- pass at least one valid constraint name.
Classified as terminal by
DjogiError::is_transientretrying with the same empty slice cannot succeed.
ConflictingDeferrabilitySpec
Runtime inventory walk for
[DjogiContext::defer_constraints] /
[DjogiContext::set_constraints_immediate] observed two
crate::DeferrabilitySpec entries sharing the same
(model_type_name, field_name) key but disagreeing on
(deferrable, initially_deferred). issue
#169) — .
inventory::iter order is not deterministic across builds.
A silent last-writer-wins on a disagreeing duplicate would
make the runtime validator non-deterministic — one build
would accept a SET CONSTRAINTS <name> DEFERRED request, the
next would reject it as ConstraintNotDeferrable. Mirror the
projection-time [ConflictingDeferrabilitySpec] gate in
migrate::projection so the framework fails closed at both
schema-build time and transaction-control time.
Idempotent duplicates (same key, identical values) are
accepted — they can arise from inventory::submit! chains
across crates re-exporting the same model and carry no
semantic disagreement.
Classified as terminal by DjogiError::is_transient
the conflict is a build-time inventory misconfiguration, not
a race condition. Fix is at the model declaration.
[ConflictingDeferrabilitySpec]: crate::migrate::projection::ProjectionError::ConflictingDeferrabilitySpec
Fields
OrphanDeferrabilitySpec
Runtime inventory walk for
[DjogiContext::defer_constraints] /
[DjogiContext::set_constraints_immediate] observed a
crate::DeferrabilitySpec whose model_type_name has no
matching crate::ModelDescriptor entry. Cluster
4 (issue #169) — .
The descriptor is the source of truth for type_name → table_name. A DeferrabilitySpec without a matching
ModelDescriptor means the FK is not really registered, but
would be silently skipped by the prior implementation. That
silent skip is the bug: a valid-looking constraint name
<expected_table>_<field>_fkey would then surface as
UnknownConstraintName
instead of the actual root cause (the missing descriptor).
#[derive(Model)] emits the descriptor + the deferrability
spec side by side, so an orphan spec only fires under
pathological partial-emission conditions — typically a
hand-written inventory::submit! outside the macro.
Classified as terminal by DjogiError::is_transient
the cause is a build-time inventory misconfiguration.
Fields
DuplicateConstraintName
Runtime inventory walk for
[DjogiContext::defer_constraints] /
[DjogiContext::set_constraints_immediate] observed two
distinct (model_type_name, field_name) pairs whose
conventional FK constraint names collide. Cluster
4 (issue #169) — .
The constraint-name convention is
<table>_<column>_fkey (truncated to Postgres’ 63-byte
identifier limit). Truncation can produce collisions for
long table or column names — and a collision means the
runtime validator has no way to know which FK the adopter
meant. Fail closed.
The fix at the model declaration is to shorten the offending
table or column name, or to declare an explicit constraint
name once that surface lands (out of scope for #169).
Classified as terminal by DjogiError::is_transient
the conflict is a build-time naming collision.
Fields
ConcurrentReadsRequirePoolContext
QuerySet::merge_into was invoked on a transaction-backed context. (issue #173)
introduces this variant alongside the typed concurrent-reads
helper.
A transaction owns one Postgres connection; cloning the
context for concurrent reads would either hand out aliasing
access to the same connection (Postgres protocol violation)
or silently break the transaction boundary. Both are
programming errors, so the framework refuses.
The correct shape for concurrent reads is on a pool-backed
context: each clone gets a fresh pool checkout, the two
contexts operate on independent connections, and
tokio::try_join! over typed reads composes without an
E0499 mutable-borrow conflict.
Classified as terminal by DjogiError::is_transient
retrying cannot turn a transaction-backed context into a
pool-backed one. Move the concurrent-reads block outside the
surrounding atomic().
#[non_exhaustive]MergeSourceInvalid
QuerySet::merge_into observed a source queryset with state that
cannot be safely represented in a MERGE statement: prefetch,
select_related, cache, lock, or distinct.
Issue #178.
Classified as terminal by DjogiError::is_transient.
Fields
This variant is marked as non-exhaustive
#[non_exhaustive]MergeBranchInvalid
QuerySet::merge_into observed an invalid branch configuration:
unreachable branches (same-kind unconditional branch follows another),
duplicate target columns in an update or insert action, or manual
updated_at assignment in an update action. Issue #178.
Classified as terminal by DjogiError::is_transient.
Fields
This variant is marked as non-exhaustive
#[non_exhaustive]MergeNoBranches
QuerySet::merge_into was invoked without any ON conditions or
without any WHEN branches. Issue #178.
Classified as terminal by DjogiError::is_transient.
Fields
This variant is marked as non-exhaustive
PresentationStartup(Vec<PresentationStartupError>)
One or more presentation codecs failed startup validation.
Returned by
validate_startup_inventory
when any PresentationCodecUsage
entry’s validate_startup hook returns an error.
This variant is the conversion target for pool-construction callers
(DjogiPool::connect, DjogiPool::from_database_config,
DjogiPoolBuilder::build) that call validate_startup_inventory
before accepting traffic. GH #227 wires those callers.
The inner Vec carries one
PresentationStartupError
per failing codec usage. Each entry names the (model, field, scope, codec) quadruple and the underlying error so operators can identify
every misconfigured codec in one pass rather than discovering failures
one at a time.
Classified as terminal by the framework — a codec with a missing
or invalid key cannot serve traffic until the key is provided. The
fix is an environment-variable or configuration change, not a retry.
Display includes the total count plus a concise summary of each
failing usage so operator logs can point directly at the actionable
(model, field, scope, codec) entry without requiring Debug.
#[non_exhaustive]UnsupportedPostgresVersion
Connected PostgreSQL server version is below Djogi’s minimum supported
version. Raised by check_postgres_version
preflight when the detected major version is less than 18.
detected_major and detected_minor are the actual server version components.
minimum_major is Djogi’s minimum supported major version (always 18).
Fields
This variant is marked as non-exhaustive
#[non_exhaustive]StalePhaseZeroStatement
A Phase 0 migration statement was classified as stale at the deepest
execution boundary (immediately before raw batch_execute).
This variant is the statement-level safety net in
[migrate::runner::execute_runner_statement] — the runner’s carve-out
for Phase 0 version allows raw batch_execute to bypass the
session-statement guard, so this classifier catches stale artifacts
that somehow evaded the pre-bootstrap artifact checks.
What triggers this. The statement-level guard emits seed-dml
for top-level HeeRanjID seed-table mutations and generated-stale
for literal database-default statements such as
ALTER DATABASE "<hardcoded_name>" SET heer.node_id or
heer.ranj_node_id instead of dynamic defaults like
current_database(). Current production Phase 0 omits node seeding
entirely; single-node-dev current uses dynamic
EXECUTE format('ALTER DATABASE %I ...', current_database(), ...).
statement carries the offending SQL prefix so log lines can
identify what was rejected. Classified as terminal — a stale
statement against the same database cannot succeed on retry.
Fields
This variant is marked as non-exhaustive
Implementations§
Source§impl DjogiError
impl DjogiError
Sourcepub fn not_found(table: &'static str) -> Self
pub fn not_found(table: &'static str) -> Self
Construct a NotFound error with a table-name context.
This is the public escape hatch for #[non_exhaustive] on the
NotFound variant: struct-expression construction is blocked outside
this crate, so #[model]-expanded CRUD methods (which run in user
crates) call this constructor instead. Keep the signature stable
any future additional context fields must gain their own constructor
or builder rather than changing this one.
Sourcepub fn multiple_objects(table: &'static str, count_seen: usize) -> Self
pub fn multiple_objects(table: &'static str, count_seen: usize) -> Self
Construct a MultipleObjects error with a table name and the number
of rows actually observed.
Mirror of not_found — exists so that cross-crate callers (macro
output, future filter-layer builders) can produce this variant
without running into #[non_exhaustive].
Sourcepub fn relation_unloaded(model: &'static str, field: &'static str) -> Self
pub fn relation_unloaded(model: &'static str, field: &'static str) -> Self
Construct a RelationUnloaded error naming the model and relation
field that the caller asked to resolve without loading.
Exists for the same reason as not_found / multiple_objects: the
#[non_exhaustive] attribute on the variant blocks struct-expression
construction outside this crate, so macro-expanded code and +
relation wrappers go through this constructor instead.
Sourcepub fn missing_idempotency_key(model: &'static str) -> Self
pub fn missing_idempotency_key(model: &'static str) -> Self
Construct a MissingIdempotencyKey error naming the model
whose #[model(idempotency_key = "...")] attribute was not
declared.
Mirror of not_found / multiple_objects — exists so that
cross-crate callers (macro output in user crates) can produce
this variant despite #[non_exhaustive] blocking struct-
expression construction outside this crate.
Sourcepub fn gone_aggregate(
model: &'static str,
id: String,
reason: &'static str,
) -> Self
pub fn gone_aggregate( model: &'static str, id: String, reason: &'static str, ) -> Self
Construct a GoneAggregate error.
Mirror of the other constructors — exists so that cross-crate
callers can produce this #[non_exhaustive] variant. id is
typically produced via format!("{}", pk) so the error is
independent of the originating model’s Pk type.
Sourcepub fn unsupported_postgres_version(
detected_major: u32,
detected_minor: u32,
minimum_major: u32,
) -> Self
pub fn unsupported_postgres_version( detected_major: u32, detected_minor: u32, minimum_major: u32, ) -> Self
Construct an UnsupportedPostgresVersion error with the detected
server version and the framework’s minimum supported major version.
Mirror of the other constructors — exists so that cross-crate callers
(the pg::preflight module, CLI entry points) can produce this variant
despite #[non_exhaustive] blocking struct-expression construction
outside this crate.
Sourcepub fn invalid_subquery_modifier(op: &'static str, reason: &'static str) -> Self
pub fn invalid_subquery_modifier(op: &'static str, reason: &'static str) -> Self
Construct an InvalidSubqueryModifier
error. The call sites for this variant all use literal strings, so the
constructor keeps the shape &'static str rather than widening to an
owned String.
Sourcepub fn is_transient(&self) -> bool
pub fn is_transient(&self) -> bool
Classify this error as transient (retrying the closure
may succeed) or terminal (retrying will not help).
retry_on_conflict uses this predicate to decide whether to
re-run its closure. The contract:
| Variant | Classification |
|---|---|
LockConflict | transient |
Db with SQLSTATE 40001 / 40P01 / 55P03 | transient |
Db with any other SQLSTATE | terminal |
NotFound | terminal |
MultipleObjects | terminal |
IdGeneration | terminal |
RelationUnloaded | terminal |
Decode | terminal |
Serde | terminal |
Validation | terminal |
MissingIdempotencyKey | terminal |
GoneAggregate | terminal |
StreamOutsideTransaction | terminal |
TransactionPoisoned | terminal |
SessionStatementDisallowedInTransaction | terminal |
RawTransactionControlDisallowedInTransaction | terminal |
PoolTimeout | transient |
SetRoleOutsideTransaction | terminal |
InvalidRoleName | terminal |
IsolationLevelOnNestedScope | terminal |
ConstraintModeOutsideTransaction | terminal |
UnknownConstraintName | terminal |
ConstraintNotDeferrable | terminal |
EmptyDeferConstraintsScope | terminal |
ConflictingDeferrabilitySpec | terminal |
OrphanDeferrabilitySpec | terminal |
DuplicateConstraintName | terminal |
ConcurrentReadsRequirePoolContext | terminal |
UnsupportedPostgresVersion | terminal |
The Db row reflects the existing is_lock_error | |
classifier: Postgres SQLSTATEs 40001 (serialization | |
failure), 40P01 (deadlock detected), and 55P03 | |
(lock not available / NOWAIT rejection) are the three | |
| retryable codes. Unique-violation, foreign-key violation, | |
| connection drops, and protocol errors all fall through to | |
| terminal because retrying the same closure against a | |
| constraint violation will fail the same way. |
Sourcepub fn is_terminal(&self) -> bool
pub fn is_terminal(&self) -> bool
Inverse of is_transient — returns
true when retrying will not help.
Provided as a convenience for call sites that read more
naturally as err.is_terminal() than !err.is_transient().
Same contract, inverted.
Trait Implementations§
Source§impl Debug for DjogiError
impl Debug for DjogiError
Source§impl Display for DjogiError
impl Display for DjogiError
Source§impl Error for DjogiError
impl Error for DjogiError
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<AuthError> for DjogiError
impl From<AuthError> for DjogiError
Source§impl From<DjogiError> for BackfillError
impl From<DjogiError> for BackfillError
Source§fn from(source: DjogiError) -> Self
fn from(source: DjogiError) -> Self
Source§impl From<DjogiError> for DaemonError
impl From<DjogiError> for DaemonError
Source§fn from(source: DjogiError) -> Self
fn from(source: DjogiError) -> Self
Source§impl From<DjogiError> for HookError
impl From<DjogiError> for HookError
Source§fn from(source: DjogiError) -> Self
fn from(source: DjogiError) -> Self
Source§impl From<Error> for DjogiError
impl From<Error> for DjogiError
Source§impl From<Error> for DjogiError
Bridge: convert tokio_postgres::Error into DjogiError.
impl From<Error> for DjogiError
Bridge: convert tokio_postgres::Error into DjogiError.
Source§impl From<Infallible> for DjogiError
Infallible → DjogiError coercion.
Infallible has no inhabitants, so match never {} is exhaustive.
The impl exists so macro-emitted chains that invoke
<Visage as TryFrom<&Model>>::try_from(row)? propagate through ?
uniformly regardless of whether the visage is scalar-only (the
stdlib blanket returns Infallible) or relation-nesting (returns
VisageError). Without this impl the scalar-only path fails
compilation with “? couldn’t convert the error to DjogiError”.
impl From<Infallible> for DjogiError
Infallible → DjogiError coercion.
Infallible has no inhabitants, so match never {} is exhaustive.
The impl exists so macro-emitted chains that invoke
<Visage as TryFrom<&Model>>::try_from(row)? propagate through ?
uniformly regardless of whether the visage is scalar-only (the
stdlib blanket returns Infallible) or relation-nesting (returns
VisageError). Without this impl the scalar-only path fails
compilation with “? couldn’t convert the error to DjogiError”.
Source§fn from(never: Infallible) -> Self
fn from(never: Infallible) -> Self
Source§impl From<VisageError> for DjogiError
impl From<VisageError> for DjogiError
Source§fn from(source: VisageError) -> Self
fn from(source: VisageError) -> Self
Auto Trait Implementations§
impl !RefUnwindSafe for DjogiError
impl !UnwindSafe for DjogiError
impl Freeze for DjogiError
impl Send for DjogiError
impl Sync for DjogiError
impl Unpin for DjogiError
impl UnsafeUnpin for DjogiError
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
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<V> IntoPortableFieldValue<Option<V>> for V
impl<V> IntoPortableFieldValue<Option<V>> for V
Source§fn into_portable_field_value(self) -> Option<V>
fn into_portable_field_value(self) -> Option<V>
self into the field’s declared portable value type.
The returned value flows into Sassi’s Field<M, FieldValue>::eq /
.gt / .in_ / etc. as the right-hand-side operand. Implementors
must produce a value whose PartialEq / PartialOrd / Hash
behaviour matches the on-disk column ordering — otherwise Punnu and
the database row will disagree at evaluation time.Source§impl<V> IntoPortableFieldValue<Tracked<V>> for V
impl<V> IntoPortableFieldValue<Tracked<V>> for V
Source§fn into_portable_field_value(self) -> Tracked<V>
fn into_portable_field_value(self) -> Tracked<V>
self into the field’s declared portable value type.
The returned value flows into Sassi’s Field<M, FieldValue>::eq /
.gt / .in_ / etc. as the right-hand-side operand. Implementors
must produce a value whose PartialEq / PartialOrd / Hash
behaviour matches the on-disk column ordering — otherwise Punnu and
the database row will disagree at evaluation time.Source§impl<V> IntoPortableFieldValue<V> for V
impl<V> IntoPortableFieldValue<V> for V
Source§fn into_portable_field_value(self) -> V
fn into_portable_field_value(self) -> V
self into the field’s declared portable value type.
The returned value flows into Sassi’s Field<M, FieldValue>::eq /
.gt / .in_ / etc. as the right-hand-side operand. Implementors
must produce a value whose PartialEq / PartialOrd / Hash
behaviour matches the on-disk column ordering — otherwise Punnu and
the database row will disagree at evaluation time.Source§impl<T> Paint for Twhere
T: ?Sized,
impl<T> Paint for Twhere
T: ?Sized,
Source§fn fg(&self, value: Color) -> Painted<&T>
fn fg(&self, value: Color) -> Painted<&T>
Returns a styled value derived from self with the foreground set to
value.
This method should be used rarely. Instead, prefer to use color-specific
builder methods like red() and
green(), which have the same functionality but are
pithier.
§Example
Set foreground color to white using fg():
use yansi::{Paint, Color};
painted.fg(Color::White);Set foreground color to white using white().
use yansi::Paint;
painted.white();Source§fn bright_black(&self) -> Painted<&T>
fn bright_black(&self) -> Painted<&T>
Source§fn bright_red(&self) -> Painted<&T>
fn bright_red(&self) -> Painted<&T>
Source§fn bright_green(&self) -> Painted<&T>
fn bright_green(&self) -> Painted<&T>
Source§fn bright_yellow(&self) -> Painted<&T>
fn bright_yellow(&self) -> Painted<&T>
Source§fn bright_blue(&self) -> Painted<&T>
fn bright_blue(&self) -> Painted<&T>
Source§fn bright_magenta(&self) -> Painted<&T>
fn bright_magenta(&self) -> Painted<&T>
Source§fn bright_cyan(&self) -> Painted<&T>
fn bright_cyan(&self) -> Painted<&T>
Source§fn bright_white(&self) -> Painted<&T>
fn bright_white(&self) -> Painted<&T>
Source§fn bg(&self, value: Color) -> Painted<&T>
fn bg(&self, value: Color) -> Painted<&T>
Returns a styled value derived from self with the background set to
value.
This method should be used rarely. Instead, prefer to use color-specific
builder methods like on_red() and
on_green(), which have the same functionality but
are pithier.
§Example
Set background color to red using fg():
use yansi::{Paint, Color};
painted.bg(Color::Red);Set background color to red using on_red().
use yansi::Paint;
painted.on_red();Source§fn on_primary(&self) -> Painted<&T>
fn on_primary(&self) -> Painted<&T>
Source§fn on_magenta(&self) -> Painted<&T>
fn on_magenta(&self) -> Painted<&T>
Source§fn on_bright_black(&self) -> Painted<&T>
fn on_bright_black(&self) -> Painted<&T>
Source§fn on_bright_red(&self) -> Painted<&T>
fn on_bright_red(&self) -> Painted<&T>
Source§fn on_bright_green(&self) -> Painted<&T>
fn on_bright_green(&self) -> Painted<&T>
Source§fn on_bright_yellow(&self) -> Painted<&T>
fn on_bright_yellow(&self) -> Painted<&T>
Source§fn on_bright_blue(&self) -> Painted<&T>
fn on_bright_blue(&self) -> Painted<&T>
Source§fn on_bright_magenta(&self) -> Painted<&T>
fn on_bright_magenta(&self) -> Painted<&T>
Source§fn on_bright_cyan(&self) -> Painted<&T>
fn on_bright_cyan(&self) -> Painted<&T>
Source§fn on_bright_white(&self) -> Painted<&T>
fn on_bright_white(&self) -> Painted<&T>
Source§fn attr(&self, value: Attribute) -> Painted<&T>
fn attr(&self, value: Attribute) -> Painted<&T>
Enables the styling Attribute value.
This method should be used rarely. Instead, prefer to use
attribute-specific builder methods like bold() and
underline(), which have the same functionality
but are pithier.
§Example
Make text bold using attr():
use yansi::{Paint, Attribute};
painted.attr(Attribute::Bold);Make text bold using using bold().
use yansi::Paint;
painted.bold();Source§fn rapid_blink(&self) -> Painted<&T>
fn rapid_blink(&self) -> Painted<&T>
Source§fn quirk(&self, value: Quirk) -> Painted<&T>
fn quirk(&self, value: Quirk) -> Painted<&T>
Enables the yansi Quirk value.
This method should be used rarely. Instead, prefer to use quirk-specific
builder methods like mask() and
wrap(), which have the same functionality but are
pithier.
§Example
Enable wrapping using .quirk():
use yansi::{Paint, Quirk};
painted.quirk(Quirk::Wrap);Enable wrapping using wrap().
use yansi::Paint;
painted.wrap();Source§fn clear(&self) -> Painted<&T>
👎Deprecated since 1.0.1: renamed to resetting() due to conflicts with Vec::clear().
The clear() method will be removed in a future release.
fn clear(&self) -> Painted<&T>
renamed to resetting() due to conflicts with Vec::clear().
The clear() method will be removed in a future release.
Source§fn whenever(&self, value: Condition) -> Painted<&T>
fn whenever(&self, value: Condition) -> Painted<&T>
Conditionally enable styling based on whether the Condition value
applies. Replaces any previous condition.
See the crate level docs for more details.
§Example
Enable styling painted only when both stdout and stderr are TTYs:
use yansi::{Paint, Condition};
painted.red().on_yellow().whenever(Condition::STDOUTERR_ARE_TTY);