use sha2::{Digest, Sha256};
use thiserror::Error;
#[cfg(all(feature = "db", not(feature = "sqlite")))]
#[macro_export]
macro_rules! maybe_for_update {
($query:expr $(,)?) => {
$crate::reexports::diesel::query_dsl::QueryDsl::for_update($query)
};
}
#[cfg(all(feature = "db", feature = "sqlite"))]
#[macro_export]
macro_rules! maybe_for_update {
($query:expr $(,)?) => {
$query
};
}
#[cfg(all(feature = "db", not(feature = "sqlite")))]
#[macro_export]
macro_rules! backend_select {
(pg => $pg:block, sqlite => $sqlite:block $(,)?) => {
$pg
};
}
#[cfg(all(feature = "db", feature = "sqlite"))]
#[macro_export]
macro_rules! backend_select {
(pg => $pg:block, sqlite => $sqlite:block $(,)?) => {
$sqlite
};
}
#[cfg(feature = "db")]
#[derive(Clone)]
pub enum ReadRoute {
Primary,
ReadPool(diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>),
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"),
}
}
}
#[cfg(not(feature = "sqlite"))]
pub const MAX_BIND_PARAMS: usize = 65535;
#[cfg(feature = "sqlite")]
pub const MAX_BIND_PARAMS: usize = 32766;
#[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>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DependentAction {
Destroy,
DeleteAll,
Nullify,
Restrict,
}
pub type RuntimeDependentCascadeFuture<'a> = ::std::pin::Pin<
::std::boxed::Box<
dyn ::std::future::Future<Output = crate::AutumnResult<Vec<(String, String)>>> + Send + 'a,
>,
>;
pub type RuntimeDependentCascadeFn = for<'a> fn(
&'a ::diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>,
&'a mut crate::db::RuntimeConnection,
i64,
bool,
&'a mut ::std::collections::HashSet<(&'static str, i64)>,
&'a mut ::std::collections::HashSet<(&'static str, i64)>,
&'a mut ::std::collections::HashSet<(&'static str, i64)>,
) -> RuntimeDependentCascadeFuture<'a>;
pub struct RuntimeDependentSpec {
pub fk: &'static str,
pub action: DependentAction,
pub cascade: RuntimeDependentCascadeFn,
}
pub trait AutumnDependents {
#[must_use]
fn dependents() -> &'static [RuntimeDependentSpec] {
&[]
}
}
impl<T: ?Sized> AutumnDependents for T {}
#[cfg(all(feature = "ws", feature = "maud", feature = "htmx"))]
#[doc(hidden)]
pub fn publish_deferred_dependent_broadcasts(broadcasts: Vec<(String, String)>) {
if broadcasts.is_empty() {
return;
}
if let Some(channels) = crate::__private::get_global_channels() {
let fragment = crate::html! {};
for (topic, dom_id) in broadcasts {
if let Err(err) = channels.broadcast().publish_oob(
&topic,
&dom_id,
&crate::htmx::OobSwap::Delete,
&fragment,
) {
tracing::warn!(
error = %err,
"auto-broadcast dependent-destroy delete failed"
);
}
}
}
}
#[cfg(not(all(feature = "ws", feature = "maud", feature = "htmx")))]
#[doc(hidden)]
pub fn publish_deferred_dependent_broadcasts(_broadcasts: Vec<(String, String)>) {}
#[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 crate::db::RuntimeConnection,
) -> 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 ModelPrimaryKey {
type IdType: ::std::fmt::Display + ::core::clone::Clone + Send + Sync + 'static;
fn primary_key_value(&self) -> Self::IdType;
}
#[cfg(feature = "db")]
#[doc(hidden)]
pub trait M2mConnSource: Send + Sync {
type Model;
fn __autumn_m2m_write_conn(
&self,
) -> impl ::std::future::Future<
Output = crate::AutumnResult<
diesel_async::pooled_connection::deadpool::Object<crate::db::RuntimeConnection>,
>,
> + Send;
}
pub trait AutumnSearchableModel {
const IS_SEARCHABLE: bool;
const SEARCH_LANGUAGE: &'static str;
const SEARCH_FIELDS: &'static [(&'static str, char)];
}
#[must_use]
pub fn sqlite_fts5_match_query(input: &str) -> Option<String> {
let mut out = String::with_capacity(input.len() + 2);
for token in input.split_whitespace() {
if !out.is_empty() {
out.push(' ');
}
out.push('"');
for ch in token.chars() {
if ch == '"' {
out.push('"');
}
out.push(ch);
}
out.push('"');
}
if out.is_empty() { None } else { Some(out) }
}
#[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)
}
pub trait DisplayTopicField {
fn to_topic_string(&self) -> String;
}
impl DisplayTopicField for String {
fn to_topic_string(&self) -> String {
self.clone()
}
}
impl DisplayTopicField for &str {
fn to_topic_string(&self) -> String {
(*self).to_string()
}
}
impl DisplayTopicField for bool {
fn to_topic_string(&self) -> String {
self.to_string()
}
}
impl DisplayTopicField for char {
fn to_topic_string(&self) -> String {
self.to_string()
}
}
macro_rules! impl_display_topic_field_num {
($($t:ty),*) => {
$(
impl DisplayTopicField for $t {
fn to_topic_string(&self) -> String {
self.to_string()
}
}
)*
};
}
impl_display_topic_field_num!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64);
impl DisplayTopicField for ::uuid::Uuid {
fn to_topic_string(&self) -> String {
self.to_string()
}
}
impl DisplayTopicField for ::chrono::NaiveDateTime {
fn to_topic_string(&self) -> String {
self.to_string()
}
}
impl DisplayTopicField for ::chrono::NaiveDate {
fn to_topic_string(&self) -> String {
self.to_string()
}
}
impl DisplayTopicField for ::chrono::NaiveTime {
fn to_topic_string(&self) -> String {
self.to_string()
}
}
impl<T> DisplayTopicField for ::chrono::DateTime<T>
where
T: ::chrono::TimeZone,
T::Offset: std::fmt::Display,
{
fn to_topic_string(&self) -> String {
self.to_string()
}
}
impl<T: DisplayTopicField> DisplayTopicField for Option<T> {
fn to_topic_string(&self) -> String {
self.as_ref()
.map_or_else(|| "none".to_string(), DisplayTopicField::to_topic_string)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fts5_match_quotes_plain_terms_and_ands_them() {
assert_eq!(
sqlite_fts5_match_query("rust web").as_deref(),
Some("\"rust\" \"web\"")
);
assert_eq!(
sqlite_fts5_match_query("hello").as_deref(),
Some("\"hello\"")
);
}
#[test]
fn fts5_match_neutralizes_operators_as_literals() {
assert_eq!(
sqlite_fts5_match_query("foo OR bar").as_deref(),
Some("\"foo\" \"OR\" \"bar\"")
);
assert_eq!(
sqlite_fts5_match_query("title:secret").as_deref(),
Some("\"title:secret\"")
);
assert_eq!(sqlite_fts5_match_query("pre*").as_deref(), Some("\"pre*\""));
assert_eq!(
sqlite_fts5_match_query("NEAR(a b)").as_deref(),
Some("\"NEAR(a\" \"b)\"")
);
}
#[test]
fn fts5_match_doubles_embedded_quotes() {
assert_eq!(
sqlite_fts5_match_query("a\"b").as_deref(),
Some("\"a\"\"b\"")
);
assert_eq!(
sqlite_fts5_match_query("\" OR x").as_deref(),
Some("\"\"\"\" \"OR\" \"x\"")
);
}
#[test]
fn fts5_match_empty_input_is_none() {
assert_eq!(sqlite_fts5_match_query(""), None);
assert_eq!(sqlite_fts5_match_query(" "), None);
assert_eq!(sqlite_fts5_match_query("\t\n \r"), None);
}
#[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));
}
}