runledger_postgres/lib.rs
1//! PostgreSQL persistence layer for Runledger durable execution.
2//!
3//! This crate owns the SQLx-backed storage and query helpers used by the
4//! runtime and application crates. The main entrypoint is the [`jobs`] module,
5//! which exposes APIs for:
6//! - queueing, resource-aware claiming, heartbeating, and completing jobs
7//! - typed direct-job recovery, successful-job replay, events, metrics, and
8//! admin reads
9//! - enqueueing, actively coordinating, querying, and immutably recovering
10//! workflow runs and steps
11//! - applying or validating the bundled Runledger schema migrations
12//!
13//! Typical consumers share a [`DbPool`] with `runledger-runtime`, then call the
14//! exported [`jobs`] functions from application setup, admin APIs, or tests.
15//!
16//! # Security Boundary
17//!
18//! This crate is a persistence layer, not an authentication or authorization
19//! layer. Public job and workflow APIs that accept `organization_id`,
20//! idempotency keys, workflow IDs, job IDs, or metadata expect those values to
21//! come from a trusted service boundary. HTTP or RPC handlers should derive
22//! organization scope from authenticated claims or server-side policy, not from
23//! untrusted request parameters alone.
24//!
25//! [`QueryError::client_message`] and [`QueryError::code`] are the stable values
26//! intended for public error responses and application logs. Detailed internal
27//! context remains available through [`QueryError::internal_message`] and the
28//! standard error source chain for targeted debugging, but may contain payload
29//! values, idempotency keys, or database policy details. Do not emit it at
30//! general logging boundaries without explicit redaction. Public formatting
31//! keeps raw SQLx details sanitized.
32//! Runtime lifecycle, workflow mutation, and idempotent enqueue APIs are designed
33//! for PostgreSQL's default `READ COMMITTED` transaction isolation so they can
34//! observe rows committed after lock waits or uniqueness conflicts.
35//! Release-sensitive, workflow-append, and keyed-enqueue paths validate this
36//! before running because their correctness depends on second reads after waits
37//! or conflicts.
38//!
39//! For simple embedding, call [`migrate_after_idempotency_cutover`] during
40//! startup:
41//!
42//! ```rust,no_run
43//! # async fn demo() -> Result<(), Box<dyn std::error::Error>> {
44//! let pool = sqlx::PgPool::connect("postgres://localhost/runledger").await?;
45//! runledger_postgres::migrate_after_idempotency_cutover(&pool).await?;
46//! # Ok(())
47//! # }
48//! ```
49//!
50//! For deployments that manage DDL elsewhere, call
51//! [`ensure_schema_compatible_after_idempotency_cutover`] instead to fail fast
52//! if the schema is missing, drifted, or still has keyed legacy rows without
53//! idempotency request snapshots. That check is read-only, but it expects the
54//! database to retain SQLx migration history in `_sqlx_migrations` and, when
55//! available, Runledger-owned migration state in `runledger_migration_history`.
56//!
57//! # Copy-Paste Examples
58//!
59//! - [Enqueue one job](https://github.com/bpcakes/runledger/blob/master/runledger-postgres/examples/enqueue_job.rs)
60//! - [Enqueue a workflow DAG](https://github.com/bpcakes/runledger/blob/master/runledger-postgres/examples/workflow_dag.rs)
61//! - [Use an external workflow gate](https://github.com/bpcakes/runledger/blob/master/runledger-postgres/examples/external_gate.rs)
62//! - [Create a scheduled job entrypoint](https://github.com/bpcakes/runledger/blob/master/runledger-postgres/examples/schedule_job.rs)
63//! - [Adopt continuation, coordination, replay, and recovery](https://github.com/bpcakes/runledger/blob/master/docs/downstream-agent-guide.md)
64//!
65//! Import `runledger_runtime::prelude::*` and use
66//! [the worker binary example](https://github.com/bpcakes/runledger/blob/master/runledger-runtime/examples/worker_binary.rs)
67//! when adding a worker process for the jobs and workflows enqueued through this
68//! crate.
69//!
70//! # Prelude
71//!
72//! ```rust
73//! use runledger_core::prelude::*;
74//! use runledger_postgres::prelude::*;
75//! ```
76//!
77//! The PostgreSQL prelude exports persistence functions and record/input types.
78//! It intentionally does not re-export core contract types, so import
79//! `runledger_core::prelude::*` beside it when building job or workflow inputs.
80//!
81//! # Enqueue One Job
82//!
83//! Use direct job enqueue for one independent retried unit of work.
84//!
85//! ```rust,no_run
86//! # async fn demo(pool: runledger_postgres::DbPool) -> Result<(), Box<dyn std::error::Error>> {
87//! use runledger_core::prelude::*;
88//! use runledger_postgres::prelude::*;
89//!
90//! let payload = serde_json::json!({"email_id": "email_123"});
91//! let job = JobEnqueue {
92//! job_type: JobType::new("jobs.email.send"),
93//! organization_id: None,
94//! payload: &payload,
95//! priority: None,
96//! max_attempts: None,
97//! timeout_seconds: None,
98//! next_run_at: None,
99//! idempotency_key: Some("email:email_123:send"),
100//! stage: None,
101//! };
102//!
103//! let _job_id = enqueue_job(&pool, &job).await?;
104//! # Ok(())
105//! # }
106//! ```
107//!
108//! # Record A Durable Transactional Handoff
109//!
110//! Use an enqueue intent when application state and a future job request must
111//! commit atomically before the job definition is available. Recording does not
112//! read `job_definitions` or create a queue row. A standard worker later promotes
113//! pending intents for its registered job types through the ordinary enqueue
114//! path.
115//!
116//! ```rust,no_run
117//! # async fn demo(pool: runledger_postgres::DbPool) -> Result<(), Box<dyn std::error::Error>> {
118//! use runledger_core::prelude::*;
119//! use runledger_postgres::prelude::*;
120//!
121//! let payload = serde_json::json!({"invoice_id": "invoice_123"});
122//! let intent = JobEnqueueIntent::new(
123//! JobType::new("billing.invoice.capture"),
124//! &payload,
125//! "invoice:invoice_123:capture",
126//! );
127//!
128//! let mut tx = pool.begin().await?;
129//! // Persist the application's business/audit mutation with this same `tx`.
130//! let outcome = record_job_enqueue_intent_tx(&mut tx, &intent).await?;
131//! if outcome.status() == JobEnqueueIntentStatus::Conflicted {
132//! return Err("the existing durable handoff is conflicted".into());
133//! }
134//! tx.commit().await?;
135//! # Ok(())
136//! # }
137//! ```
138//!
139//! The returned status is a point-in-time observation, not a promotion
140//! guarantee. An existing intent can be promoted or become conflicted
141//! concurrently, including while a caller-owned record transaction remains
142//! open. Continue monitoring pending age and conflicts after accepting a
143//! pending handoff.
144//!
145//! Concurrent calls that record the same `(job_type, organization_id,
146//! idempotency_key)` may wait for the transaction that first claimed that unique
147//! key. Include that wait in the caller-owned transaction's lock ordering and
148//! timeout budget.
149//! Call [`jobs::record_job_enqueue_intent_tx`] before any operation in the same
150//! transaction that can lock a `job_queue` row. A job-first recorder can create
151//! an inverse lock cycle with retention's canonical intent-before-job order.
152//!
153//! Intent payloads and idempotency keys cross the same trusted persistence
154//! boundary as ordinary queue inputs. Do not place secrets in them or emit them
155//! in logs. Monitor [`jobs::get_job_enqueue_intent_metrics`] for pending age,
156//! retrying count, maximum promotion attempts, and conflicts during the preceding
157//! 24 hours; Runledger never automatically deletes conflicted intent evidence.
158//! Database-level promotion failures leave the intent pending with bounded
159//! jittered exponential backoff, and read APIs expose the attempt/error metadata. They
160//! retry indefinitely so a prolonged outage cannot silently discard work;
161//! operators must alert on pending age and maximum attempts.
162//! Promoted intents retain their linked jobs. A queue-retention transaction
163//! must call [`jobs::delete_promoted_job_enqueue_intents_for_jobs_tx`] with its
164//! exact selected job IDs before deleting those jobs. Select candidate IDs
165//! without row locks, then call the helper as the transaction's first
166//! lock-taking operation. The transaction must use `READ COMMITTED`; stronger
167//! isolation levels return `job.intent_retention_unsupported_isolation` before
168//! fence acquisition. The helper waits for active promotions, fences new
169//! promotions, deletes promoted-intent links, then locks those jobs for the rest
170//! of the transaction. This intent-before-job order composes with duplicate
171//! recorders without an inverse lock cycle. Keep the transaction short and
172//! commit promptly. Time cutoffs alone are insufficient because a newly
173//! promoted intent may link to an older existing job.
174//!
175//! # Create A Scheduled Job Entrypoint
176//!
177//! Use [`jobs::JobScheduleUpsert`] to create or update the cron row consumed by
178//! the runtime scheduler. Schedules are UTC-only. Updating an existing schedule
179//! refreshes its definition while preserving `is_active` and `organization_id`;
180//! `next_fire_at` is refreshed when `cron_expr` changes. Cron expressions are
181//! validated with the same parser used by `runledger-runtime`. Use
182//! [`jobs::set_job_schedule_active`] to pause or resume a schedule, and
183//! [`jobs::set_job_schedule_next_fire_at`] to manually retime its cursor.
184//!
185//! ```rust,no_run
186//! # async fn demo(pool: runledger_postgres::DbPool) -> Result<(), Box<dyn std::error::Error>> {
187//! use chrono::Utc;
188//! use runledger_core::prelude::*;
189//! use runledger_postgres::prelude::*;
190//!
191//! let payload_template = serde_json::json!({"source": "api"});
192//! let schedule = JobScheduleUpsert {
193//! name: "profile-refresh-hourly",
194//! job_type: JobType::new("profiles.refresh"),
195//! organization_id: None,
196//! payload_template: &payload_template,
197//! cron_expr: "0 0 * * * *",
198//! is_active: true,
199//! next_fire_at: Utc::now(),
200//! max_jitter_seconds: 0,
201//! };
202//!
203//! let _schedule = upsert_job_schedule(&pool, &schedule).await?;
204//! # Ok(())
205//! # }
206//! ```
207//!
208//! # Enqueue A Workflow DAG
209//!
210//! Use workflows when the work has step dependencies, fan-out/fan-in, external
211//! gates, cancellation as one logical run, or workflow-level idempotency.
212//!
213//! ```rust,no_run
214//! # async fn demo(pool: runledger_postgres::DbPool) -> Result<(), Box<dyn std::error::Error>> {
215//! use runledger_core::prelude::*;
216//! use runledger_postgres::prelude::*;
217//!
218//! let crawl_payload = serde_json::json!({"profile_id": "p_123"});
219//! let classify_payload = serde_json::json!({"profile_id": "p_123"});
220//! let metadata = serde_json::json!({"source": "api"});
221//!
222//! let run = WorkflowDagBuilder::new("profiles.research", &metadata)
223//! .idempotency_key("profile:p_123:research")
224//! .job("crawl", "profiles.crawl", &crawl_payload)?
225//! .job("classify", "profiles.classify", &classify_payload)?
226//! .after_success("classify", ["crawl"])?
227//! .build()?;
228//!
229//! let _workflow_run = enqueue_workflow_run(&pool, &run).await?;
230//! # Ok(())
231//! # }
232//! ```
233//!
234//! # Use An External Workflow Gate
235//!
236//! Create the gate with `WorkflowStepEnqueueBuilder::new_external`, then
237//! complete it from a trusted service boundary when the external condition is
238//! known.
239//!
240//! ```rust,no_run
241//! # async fn demo(
242//! # pool: runledger_postgres::DbPool,
243//! # workflow_run_id: sqlx::types::Uuid,
244//! # ) -> Result<(), Box<dyn std::error::Error>> {
245//! use runledger_core::prelude::*;
246//! use runledger_postgres::prelude::*;
247//!
248//! let input = CompleteExternalWorkflowStepInput {
249//! workflow_run_id,
250//! organization_id: None,
251//! step_key: StepKey::new("approval"),
252//! outcome: ExternalWorkflowStepTerminalOutcome::Succeeded { output: None },
253//! status_reason: Some("approved"),
254//! last_error_code: None,
255//! last_error_message: None,
256//! };
257//!
258//! let _step = complete_external_workflow_step(&pool, &input).await?;
259//! # Ok(())
260//! # }
261//! ```
262//!
263//! # Inspect Workflow State
264//!
265//! ```rust,no_run
266//! # async fn demo(
267//! # pool: runledger_postgres::DbPool,
268//! # workflow_run_id: sqlx::types::Uuid,
269//! # ) -> Result<(), Box<dyn std::error::Error>> {
270//! use runledger_postgres::prelude::*;
271//!
272//! let scope = WorkflowRunReadScope::Admin;
273//! let _run = get_workflow_run_by_id_with_scope(&pool, scope, workflow_run_id).await?;
274//! let _steps = list_workflow_steps_with_scope(&pool, scope, workflow_run_id).await?;
275//! let _dependencies =
276//! list_workflow_step_dependencies_with_scope(&pool, scope, workflow_run_id).await?;
277//! # Ok(())
278//! # }
279//! ```
280//!
281//! # Coordinate And Recover Durable Work
282//!
283//! - Use [`jobs::enqueue_job_with_execution_resource`] or
284//! `runledger_core::jobs::WorkflowStepEnqueueBuilder::execution_resource`
285//! for lease-scoped single-permit resources.
286//! - Use [`jobs::enqueue_or_get_active_workflow`] with a workflow
287//! `runledger_core::jobs::WorkflowRunEnqueueBuilder::active_key` and inspect every
288//! [`jobs::EnqueueActiveWorkflowOutcome`].
289//! - Use [`jobs::compare_and_requeue_job`] for a failed/canceled/dead-lettered
290//! direct job and [`jobs::compare_and_replay_succeeded_job`] for intentional
291//! successful replay.
292//! - Use [`jobs::recover_workflow_run`] to reconstruct a terminal workflow as a
293//! new lineage-linked run rather than rewriting source history.
294//!
295//! The caller-transaction variants of recovery and replay require PostgreSQL
296//! `READ COMMITTED`. See the
297//! [downstream guide](https://github.com/bpcakes/runledger/blob/master/docs/downstream-agent-guide.md)
298//! for rollout fences, request idempotency, and retention behavior.
299//!
300//! # Handle Errors Safely
301//!
302//! `QueryError::client_message` and `QueryError::code` are safe for public
303//! responses. `QueryError::internal_message` is for trusted diagnostics.
304//!
305//! ```rust,no_run
306//! # async fn demo(
307//! # pool: runledger_postgres::DbPool,
308//! # job: runledger_postgres::jobs::JobEnqueue<'_>,
309//! # ) -> Result<(), runledger_postgres::Error> {
310//! match runledger_postgres::jobs::enqueue_job(&pool, &job).await {
311//! Ok(_job_id) => {}
312//! Err(runledger_postgres::Error::QueryError(query_error)) => {
313//! let _public_code = query_error.code();
314//! let _public_message = query_error.client_message();
315//! let _private_diagnostic = query_error.internal_message();
316//! }
317//! Err(error) => return Err(error),
318//! }
319//! # Ok(())
320//! # }
321//! ```
322
323use std::fmt;
324
325mod error;
326pub mod jobs;
327mod migrations;
328
329pub use error::{
330 FrameworkConstraintSpec, QueryError, QueryErrorCategory, QueryErrorKind,
331 classify_framework_constraint, classify_query_error,
332 classify_query_error_with_constraint_classifier, has_framework_constraint_classifier,
333};
334pub use migrations::{
335 MIGRATOR, SchemaCompatibilityError, WorkflowJobLinkTriggerDiagnostic,
336 WorkflowJobLinkTriggerProblem, ensure_schema_compatible_after_idempotency_cutover,
337 migrate_after_idempotency_cutover,
338};
339#[allow(
340 deprecated,
341 reason = "deprecated migration entrypoints remain re-exported for semver compatibility"
342)]
343pub use migrations::{ensure_schema_compatible, migrate};
344
345/// Common `runledger-postgres` imports for integration crates.
346///
347/// This prelude contains persistence APIs, DB types, and database record/input
348/// structs. It avoids generic `Result` or `Error` aliases and does not re-export
349/// `runledger-core` contracts, so it can be glob-imported alongside
350/// `runledger_core::prelude::*` and `runledger_runtime::prelude::*`.
351pub mod prelude {
352 #[allow(
353 deprecated,
354 reason = "the prelude retains deprecated exports for semver compatibility"
355 )]
356 pub use crate::jobs::{
357 AppendWorkflowStepsInput, AppendWorkflowStepsOutcome, AppendWorkflowStepsResult,
358 CompareAndReplaySucceededJob, CompareAndReplaySucceededJobOutcome, CompareAndRequeueJob,
359 CompareAndRequeueJobOutcome, CompleteExternalWorkflowStepInput,
360 DEFAULT_WORKFLOW_RUN_WAIT_TIMEOUT, DecodedJobEventPayload, DecodedRequeuedEventPayload,
361 EnqueueActiveWorkflowOutcome, ExternalWorkflowStepTerminalOutcome,
362 JOB_SCHEDULE_MAX_JITTER_SECONDS, JobCancellationScope, JobCompletionUpdate,
363 JobContinuationMetricsRecord, JobContinuationOutcome, JobContinuationUpdate,
364 JobDefinitionListFilter, JobDefinitionRecord, JobDefinitionUpdate, JobDefinitionUpsert,
365 JobEnqueue, JobEnqueueDisposition, JobEnqueueIntent, JobEnqueueIntentDisposition,
366 JobEnqueueIntentListFilter, JobEnqueueIntentMetricsFilter, JobEnqueueIntentMetricsRecord,
367 JobEnqueueIntentOutcome, JobEnqueueIntentOutcomeState, JobEnqueueIntentPromotionError,
368 JobEnqueueIntentPromotionReport, JobEnqueueIntentRecord, JobEnqueueIntentState,
369 JobEnqueueIntentStatus, JobEnqueueOutcome, JobEventRecord, JobFailureCompletionDisposition,
370 JobFailureCompletionOutcome, JobFailureUpdate, JobLeaseIdentity, JobListFilter,
371 JobLogRecord, JobLogRecordInput, JobMetricsRecord, JobOrdinaryProgressUpdate,
372 JobPayloadUuidArrayFieldUpdate, JobPayloadUuidArrayFieldUpdateRejection, JobProgressUpdate,
373 JobQueueRecord, JobRequeueStatePolicy, JobRunningUpdate, JobRuntimeConfigListFilter,
374 JobRuntimeConfigRecord, JobRuntimeConfigUpsert, JobScheduleRecord, JobScheduleUpsert,
375 JobScope, JobSuccessCompletionOutcome, NonRequeueableJobStatusError,
376 ReapExpiredLeaseCleanupError, ReapExpiredLeaseCleanupOperation,
377 ReapExpiredLeaseDeferredError, ReapExpiredLeasesDetailedResult, ReapExpiredLeasesResult,
378 ReapedLeaseDisposition, ReapedLeaseRecord, ReapedTerminalLeaseRecord, RequeueableJobStatus,
379 SuccessfulReplayEnqueuedEventPayload, WorkflowRecoveryDisposition, WorkflowRecoveryMode,
380 WorkflowRecoveryOutcome, WorkflowRecoveryRequest, WorkflowRunDbRecord, WorkflowRunHandle,
381 WorkflowRunHandleError, WorkflowRunHandleScope, WorkflowRunListFilter,
382 WorkflowRunReadCountFilter, WorkflowRunReadListFilter, WorkflowRunReadScope,
383 WorkflowRunResultRecord, WorkflowRunWaitOptions, WorkflowStepDbRecord,
384 WorkflowStepDependencyDbRecord, append_workflow_steps, append_workflow_steps_tx,
385 cancel_job, cancel_job_with_scope, cancel_workflow_run_tx,
386 compare_and_replay_succeeded_job, compare_and_replay_succeeded_job_tx,
387 compare_and_requeue_job, compare_and_requeue_job_tx, complete_external_workflow_step,
388 complete_external_workflow_step_tx, complete_job_continuation,
389 complete_job_continuation_for_lease, complete_job_continuation_with_outcome,
390 complete_job_continuation_with_outcome_for_lease, complete_job_failure,
391 complete_job_failure_for_lease, complete_job_failure_with_outcome,
392 complete_job_failure_with_outcome_for_lease, complete_job_success,
393 complete_job_success_for_lease, complete_job_success_with_outcome,
394 complete_job_success_with_outcome_for_lease, count_workflow_runs_with_scope,
395 count_workflow_step_dependencies, count_workflow_step_dependencies_with_scope,
396 count_workflow_steps, count_workflow_steps_with_scope,
397 delete_promoted_job_enqueue_intents_before,
398 delete_promoted_job_enqueue_intents_for_jobs_tx, enqueue_job, enqueue_job_tx,
399 enqueue_job_with_execution_resource, enqueue_job_with_execution_resource_tx,
400 enqueue_job_with_outcome_tx, enqueue_or_get_active_workflow,
401 enqueue_or_get_active_workflow_tx, enqueue_workflow_run, enqueue_workflow_run_handle,
402 enqueue_workflow_run_tx, get_job_by_id, get_job_continuation_metrics,
403 get_job_definition_by_type, get_job_enqueue_intent_by_id, get_job_enqueue_intent_metrics,
404 get_job_metrics, get_job_payload_by_idempotency_key, get_job_runtime_config_by_type,
405 get_job_schedule_by_name, get_latest_job_payload_for_run, get_latest_workflow_run_by_type,
406 get_latest_workflow_run_by_type_with_scope, get_required_job_runtime_config_by_type,
407 get_workflow_run_by_id, get_workflow_run_by_id_with_scope,
408 get_workflow_run_by_type_and_idempotency_key, get_workflow_run_id_for_job,
409 heartbeat_job_for_lease, insert_job_definition_if_missing_tx, insert_job_log,
410 insert_job_runtime_config_if_missing, list_job_definitions, list_job_enqueue_intents,
411 list_job_events, list_job_logs, list_job_runtime_configs, list_jobs, list_workflow_runs,
412 list_workflow_runs_with_scope, list_workflow_step_dependencies,
413 list_workflow_step_dependencies_page, list_workflow_step_dependencies_page_with_scope,
414 list_workflow_step_dependencies_with_scope, list_workflow_steps, list_workflow_steps_page,
415 list_workflow_steps_page_with_scope, list_workflow_steps_with_scope,
416 mark_job_running_for_lease, prepare_schedule_exact_sync_critical_section_tx,
417 promote_job_enqueue_intents_for_types, reap_expired_leases_with_diagnostics,
418 record_job_enqueue_intent, record_job_enqueue_intent_tx, recover_workflow_run,
419 recover_workflow_run_tx, retrieve_workflow_run_handle, set_job_schedule_active,
420 set_job_schedule_active_tx, set_job_schedule_next_fire_at,
421 set_job_schedule_next_fire_at_tx, sync_catalog_job_schedules_tx, update_job_definition,
422 update_job_ordinary_progress_for_lease, update_job_payload_uuid_array_field,
423 update_job_progress_for_lease, update_workflow_step_and_pending_job_payload_tx,
424 upsert_job_definition_tx, upsert_job_runtime_config, upsert_job_runtime_config_tx,
425 upsert_job_schedule, upsert_job_schedule_tx, workflow_run_handle,
426 };
427 pub use crate::jobs::{
428 JobScheduleCatalogSyncEntry, JobScheduleCatalogSyncReport,
429 deactivate_schedules_absent_from_names_tx,
430 };
431 pub use crate::{
432 DbPool, DbTx, FrameworkConstraintSpec, MIGRATOR, QueryError, QueryErrorCategory,
433 QueryErrorKind, SchemaCompatibilityError, WorkflowJobLinkTriggerDiagnostic,
434 WorkflowJobLinkTriggerProblem, ensure_schema_compatible_after_idempotency_cutover,
435 migrate_after_idempotency_cutover,
436 };
437}
438
439pub type DbPool = sqlx::PgPool;
440pub type DbTx<'a> = sqlx::Transaction<'a, sqlx::Postgres>;
441pub type Result<T> = std::result::Result<T, Error>;
442
443#[derive(Debug)]
444pub enum Error {
445 ConfigError(String),
446 ConnectionError(String),
447 MigrationError(String),
448 QueryError(QueryError),
449}
450
451impl fmt::Display for Error {
452 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453 match self {
454 Self::ConfigError(message) => write!(f, "{message}"),
455 Self::ConnectionError(message) => write!(f, "{message}"),
456 Self::MigrationError(message) => write!(f, "{message}"),
457 Self::QueryError(query_error) => write!(f, "{query_error}"),
458 }
459 }
460}
461
462impl std::error::Error for Error {
463 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
464 match self {
465 Self::QueryError(query_error) => Some(query_error),
466 Self::ConfigError(_) | Self::ConnectionError(_) | Self::MigrationError(_) => None,
467 }
468 }
469}
470
471impl Error {
472 #[must_use]
473 pub fn from_query_sqlx(error: sqlx::Error) -> Self {
474 Self::QueryError(QueryError::from_sqlx(error, None))
475 }
476
477 #[must_use]
478 pub fn from_query_sqlx_with_context(context: &str, error: sqlx::Error) -> Self {
479 Self::QueryError(QueryError::from_sqlx(error, Some(context)))
480 }
481}