use sha2::{Digest, Sha256};
use thiserror::Error;
#[cfg(feature = "db")]
#[derive(Clone)]
pub enum ReadRoute {
Primary,
ReadPool(diesel_async::pooled_connection::deadpool::Pool<diesel_async::AsyncPgConnection>),
Unavailable,
}
#[cfg(feature = "db")]
impl ReadRoute {
#[must_use]
pub fn from_state(state: &crate::AppState) -> Self {
if state.replica_pool().is_some() {
state
.read_pool()
.map_or(Self::Unavailable, |pool| Self::ReadPool(pool.clone()))
} else {
Self::Primary
}
}
}
#[cfg(feature = "db")]
impl std::fmt::Debug for ReadRoute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Primary => f.write_str("ReadRoute::Primary"),
Self::ReadPool(pool) => {
write!(f, "ReadRoute::ReadPool(max={})", pool.status().max_size)
}
Self::Unavailable => f.write_str("ReadRoute::Unavailable"),
}
}
}
#[derive(Debug, Clone, Error)]
pub enum RepositoryError {
#[error(
"optimistic lock conflict on record {id}: \
client expected version {expected_version}, \
row was already modified (actual: {actual_version:?})"
)]
Conflict {
id: i64,
expected_version: i64,
actual_version: Option<i64>,
},
}
#[doc(hidden)]
pub trait AutumnLockVersionModelExt {
fn __autumn_lock_version_actual(&self) -> Option<i64> {
None
}
}
#[doc(hidden)]
pub trait AutumnLockVersionUpdateExt {
fn __autumn_lock_version_expected(&self) -> Option<i64> {
None
}
}
impl<T: ?Sized> AutumnLockVersionModelExt for T {}
impl<T: ?Sized> AutumnLockVersionUpdateExt for T {}
#[doc(hidden)]
pub trait AutumnColumnCountExt {
fn __autumn_column_count(&self) -> usize;
}
#[doc(hidden)]
pub trait AutumnColumnCountSpecific {
fn __autumn_column_count(self) -> usize;
}
impl<T: AutumnColumnCountExt> AutumnColumnCountSpecific for &T {
fn __autumn_column_count(self) -> usize {
self.__autumn_column_count()
}
}
#[doc(hidden)]
pub trait AutumnColumnCountFallback {
fn __autumn_column_count(self) -> usize;
}
impl<T: ?Sized> AutumnColumnCountFallback for &&T {
fn __autumn_column_count(self) -> usize {
30
}
}
#[doc(hidden)]
pub trait AutumnUpsertSetExt {
type UpsertSet;
fn __autumn_upsert_set() -> Self::UpsertSet;
}
#[doc(hidden)]
pub trait AutumnUpsertExecutionExt {
type Model;
fn __autumn_execute_upsert<'a>(
chunk: &'a [Self::Model],
tenant_id: ::core::option::Option<&'a str>,
conn: &'a mut ::diesel_async::AsyncPgConnection,
) -> impl ::std::future::Future<
Output = ::core::result::Result<::std::vec::Vec<Self::Model>, ::diesel::result::Error>,
> + Send
+ 'a;
}
#[doc(hidden)]
pub trait AutumnCorrelateExt: Sized {
type NewModel: Sized;
fn __autumn_correlate_new(
inputs: &[Self::NewModel],
record: &Self,
matched: &mut [bool],
) -> ::core::option::Option<usize>;
fn __autumn_correlate_model(
inputs: &[Self],
record: &Self,
matched: &mut [bool],
) -> ::core::option::Option<usize>;
}
pub trait CanSetTenantId {
fn set_tenant_id(&mut self, tenant_id: String);
}
pub trait AutumnSearchableModel {
const IS_SEARCHABLE: bool;
const SEARCH_LANGUAGE: &'static str;
const SEARCH_FIELDS: &'static [(&'static str, char)];
}
#[doc(hidden)]
#[must_use]
pub fn repository_upsert_advisory_lock_key(table_name: &str, record_id: i64) -> i64 {
let mut hasher = Sha256::new();
hasher.update(b"repository_upsert\0");
hasher.update(table_name.as_bytes());
hasher.update(b"\0");
hasher.update(record_id.to_be_bytes());
let digest = hasher.finalize();
let mut bytes = [0_u8; 8];
bytes.copy_from_slice(&digest[..8]);
i64::from_be_bytes(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn conflict_variant_stores_all_fields() {
let err = RepositoryError::Conflict {
id: 42,
expected_version: 3,
actual_version: Some(4),
};
match err {
RepositoryError::Conflict {
id,
expected_version,
actual_version,
} => {
assert_eq!(id, 42);
assert_eq!(expected_version, 3);
assert_eq!(actual_version, Some(4));
}
}
}
#[test]
fn conflict_with_no_actual_version() {
let err = RepositoryError::Conflict {
id: 1,
expected_version: 0,
actual_version: None,
};
assert!(matches!(
err,
RepositoryError::Conflict {
actual_version: None,
..
}
));
}
#[test]
fn conflict_display_includes_id_and_expected_version() {
let err = RepositoryError::Conflict {
id: 99,
expected_version: 7,
actual_version: Some(8),
};
let s = err.to_string();
assert!(s.contains("99"), "display should include id");
assert!(s.contains('7'), "display should include expected_version");
}
#[test]
fn conflict_is_clone() {
let err = RepositoryError::Conflict {
id: 1,
expected_version: 0,
actual_version: Some(1),
};
let cloned = err.clone();
assert!(matches!(err, RepositoryError::Conflict { id: 1, .. }));
assert!(matches!(cloned, RepositoryError::Conflict { id: 1, .. }));
}
#[test]
fn conflict_implements_std_error() {
let err = RepositoryError::Conflict {
id: 1,
expected_version: 0,
actual_version: None,
};
let _: &dyn std::error::Error = &err;
}
#[test]
fn repository_upsert_advisory_lock_key_is_stable_for_same_table_and_id() {
let a = repository_upsert_advisory_lock_key("posts", 42);
let b = repository_upsert_advisory_lock_key("posts", 42);
assert_eq!(a, b);
assert_ne!(a, 0);
}
#[test]
fn repository_upsert_advisory_lock_key_separates_table_and_id() {
let key = repository_upsert_advisory_lock_key("posts", 42);
assert_ne!(key, repository_upsert_advisory_lock_key("comments", 42));
assert_ne!(key, repository_upsert_advisory_lock_key("posts", 43));
}
}