use std::any::{Any as Dynamic, TypeId};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use sqlx::{Any, AnyConnection, Database};
#[cfg(feature = "sqlite")]
use sqlx::{Sqlite, SqliteConnection};
use tokio::task::{self, Id};
static TRACKING: AtomicBool = AtomicBool::new(false);
static BOOKS: OnceLock<&'static Listed> = OnceLock::new();
struct Listed {
book: &'static dyn Lookup,
next: OnceLock<&'static Self>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Watch {
pub(crate) database: u64,
pub(crate) at_begin: bool,
}
pub(crate) trait Lookup: Sync {
fn holding(&self, task: Id, database: u64) -> Holding;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Holding {
Nothing,
MayHoldWriteLock,
HoldsWriteLock,
}
impl Holding {
pub(crate) const fn of(watch: Watch, ran: bool) -> Self {
if watch.at_begin {
Self::HoldsWriteLock
} else if ran {
Self::MayHoldWriteLock
} else {
Self::Nothing
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Ran(&'static AtomicBool);
impl Ran {
pub(crate) fn new() -> Self {
Self(Box::leak(Box::new(AtomicBool::new(false))))
}
pub(crate) fn clear(self) {
self.0.store(false, Ordering::Relaxed);
}
pub(crate) fn set(self) {
self.0.store(true, Ordering::Relaxed);
}
pub(crate) fn get(self) -> bool {
self.0.load(Ordering::Relaxed)
}
}
pub(crate) fn track() {
TRACKING.store(true, Ordering::Relaxed);
}
pub(crate) fn noted_task<DB: Database>() -> Option<Id> {
let may_be_sqlite = TypeId::of::<DB>() == TypeId::of::<Any>();
#[cfg(feature = "sqlite")]
let may_be_sqlite = may_be_sqlite || TypeId::of::<DB>() == TypeId::of::<Sqlite>();
if may_be_sqlite && TRACKING.load(Ordering::Relaxed) {
task::try_id()
} else {
None
}
}
pub(crate) fn on_sqlite<DB: Database>(conn: &DB::Connection) -> bool {
let conn: &dyn Dynamic = conn;
#[cfg(feature = "sqlite")]
if conn.is::<SqliteConnection>() {
return true;
}
conn.downcast_ref::<AnyConnection>()
.is_some_and(|conn| conn.backend_name() == "SQLite")
}
pub(crate) fn locks_at_begin(statement: Option<&str>) -> bool {
statement.is_some_and(|statement| {
let statement = statement.trim_start();
["BEGIN IMMEDIATE", "BEGIN EXCLUSIVE"].iter().any(|mode| {
statement
.get(..mode.len())
.is_some_and(|start| start.eq_ignore_ascii_case(mode))
})
})
}
pub(crate) fn list(book: &'static dyn Lookup) {
let listed: &'static Listed = Box::leak(Box::new(Listed {
book,
next: OnceLock::new(),
}));
let mut tail = &BOOKS;
while tail.set(listed).is_err() {
if let Some(taken) = tail.get() {
tail = &taken.next;
}
}
}
pub(crate) fn any_listed() -> bool {
BOOKS.get().is_some()
}
pub(crate) fn holding(database: u64) -> Holding {
let Some(task) = task::try_id() else {
return Holding::Nothing;
};
let mut next = BOOKS.get();
while let Some(listed) = next {
let holding = listed.book.holding(task, database);
if holding != Holding::Nothing {
return holding;
}
next = listed.next.get();
}
Holding::Nothing
}
#[cfg(test)]
mod tests {
use super::{Holding, Watch, locks_at_begin};
#[test]
fn an_immediate_or_exclusive_begin_takes_the_write_lock() {
assert!(locks_at_begin(Some("BEGIN IMMEDIATE")));
assert!(locks_at_begin(Some("begin exclusive transaction")));
assert!(!locks_at_begin(Some("BEGIN DEFERRED")));
assert!(!locks_at_begin(Some("BEGIN")));
assert!(!locks_at_begin(None));
}
#[test]
fn a_lock_taken_at_begin_is_held_and_a_statement_may_have_taken_it() {
let deferred = Watch {
database: 1,
at_begin: false,
};
let immediate = Watch {
at_begin: true,
..deferred
};
assert_eq!(Holding::of(immediate, false), Holding::HoldsWriteLock);
assert_eq!(Holding::of(deferred, true), Holding::MayHoldWriteLock);
assert_eq!(Holding::of(deferred, false), Holding::Nothing);
}
}