olai_store/backend/sql_common.rs
1//! Glue shared by the sqlx-backed backends ([`sqlite`](super::sqlite) and
2//! [`postgres`](super::postgres)).
3//!
4//! Everything here is **dialect-independent**: it never names a concrete
5//! `sqlx::Database`, so it is defined exactly once regardless of which (or both)
6//! of the `sqlite` / `postgres` features are enabled. The SQL-bearing code — the
7//! `op_*` functions with their compile-time-checked `sqlx::query!` literals, which
8//! *are* dialect-bound — lives in each backend module.
9
10use std::sync::Arc;
11
12use crate::{Error, Result};
13
14/// The current span's OpenTelemetry status/error fields, recorded on failure.
15///
16/// The span must declare `otel.status_code` and `error.type` as
17/// [`tracing::field::Empty`]. Only the error *kind* ([`Error::kind_str`]) is
18/// recorded — never a payload or message body.
19pub(crate) fn record_err<T>(result: &Result<T>) {
20 if let Err(e) = result {
21 let span = tracing::Span::current();
22 span.record("otel.status_code", "ERROR");
23 span.record("error.type", e.kind_str());
24 }
25}
26
27/// Resolves an edge label to its paired inverse label, if any (see
28/// [`InMemoryStore`](crate::InMemoryStore)).
29pub type InverseResolver = Arc<dyn Fn(&str) -> Option<String> + Send + Sync>;
30
31/// Map an [`sqlx::Error`] onto the store's [`Error`], collapsing the two cases the
32/// store gives dedicated variants (`RowNotFound` → [`Error::NotFound`], a unique
33/// violation → [`Error::AlreadyExists`]) and treating everything else as a generic
34/// backend error. Dialect-independent: `is_unique_violation()` is honored by every
35/// sqlx driver.
36impl From<sqlx::Error> for Error {
37 fn from(e: sqlx::Error) -> Self {
38 match e {
39 sqlx::Error::RowNotFound => Error::NotFound,
40 sqlx::Error::Database(db) if db.is_unique_violation() => Error::AlreadyExists,
41 other => Error::generic(other.to_string()),
42 }
43 }
44}
45
46/// Merge a backend's embedded schema migrations with a consumer's own into one
47/// ordered ledger.
48///
49/// This is the shared body behind each backend's `migrator_with`. Two
50/// `sqlx::migrate!` migrators would share sqlx's single hardcoded
51/// `_sqlx_migrations` ledger, forcing non-overlapping version ranges and
52/// `set_ignore_missing(true)` on both; merging into one migrator sidesteps that.
53/// Versions across the combined set must be unique and are applied in ascending
54/// order, so a consumer numbers its migrations above the backend's low range.
55pub(crate) fn merge(
56 base: sqlx::migrate::Migrator,
57 extra: impl IntoIterator<Item = sqlx::migrate::Migration>,
58) -> sqlx::migrate::Migrator {
59 let mut migrations: Vec<sqlx::migrate::Migration> = base.migrations.iter().cloned().collect();
60 migrations.extend(extra);
61 migrations.sort_by_key(|m| m.version);
62 sqlx::migrate::Migrator {
63 migrations: std::borrow::Cow::Owned(migrations),
64 ..sqlx::migrate::Migrator::DEFAULT
65 }
66}