use std::borrow::Cow;
use crate::writer::{Expression, SqlWriter};
use super::{MaybeAbsent, write_present, write_quoted_list};
#[derive(Debug, Clone, Default)]
pub struct Locks {
pub locks: Vec<Lock>,
}
impl Locks {
pub fn append_lock(&mut self, lock: Lock) {
self.locks.push(lock);
}
pub fn is_empty(&self) -> bool {
self.locks.is_empty()
}
}
impl Expression for Locks {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
write_present(w, &self.locks, "", " ", "");
}
}
pub trait HasLocks {
fn locks_mut(&mut self) -> &mut Locks;
}
impl HasLocks for Locks {
fn locks_mut(&mut self) -> &mut Locks {
self
}
}
#[derive(Debug, Clone, Default)]
pub struct Lock {
pub strength: Option<LockStrength>,
pub tables: Vec<Cow<'static, str>>,
pub wait: Option<LockWait>,
}
impl Lock {
pub fn new(strength: LockStrength) -> Self {
Lock {
strength: Some(strength),
..Lock::default()
}
}
pub fn append_table(&mut self, tables: impl IntoIterator<Item = impl Into<Cow<'static, str>>>) {
self.tables.extend(tables.into_iter().map(Into::into));
}
pub fn is_empty(&self) -> bool {
self.strength.is_none()
}
}
impl Expression for Lock {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
let Some(strength) = &self.strength else {
return;
};
w.push_str("FOR ");
w.push_str(strength.as_str());
write_quoted_list(w, &self.tables, " OF ", ", ", "");
if let Some(wait) = &self.wait {
w.push_str(" ");
w.push_str(wait.as_str());
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LockStrength {
Update,
NoKeyUpdate,
Share,
KeyShare,
}
impl LockStrength {
pub fn as_str(self) -> &'static str {
match self {
LockStrength::Update => "UPDATE",
LockStrength::NoKeyUpdate => "NO KEY UPDATE",
LockStrength::Share => "SHARE",
LockStrength::KeyShare => "KEY SHARE",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LockWait {
NoWait,
SkipLocked,
}
impl LockWait {
pub fn as_str(self) -> &'static str {
match self {
LockWait::NoWait => "NOWAIT",
LockWait::SkipLocked => "SKIP LOCKED",
}
}
}
impl MaybeAbsent for Lock {
fn is_absent(&self) -> bool {
self.is_empty()
}
}
#[cfg(test)]
mod tests {
use keelson_sqlcheck::testing::assert_frag_sql;
use super::*;
use crate::dialect::testing::Numbered;
use crate::writer::{Expression, build};
const FRAME: &str = r#"SELECT "id" FROM users {}"#;
const TWO_TABLE_FRAME: &str = r#"SELECT "users"."id" FROM users, posts {}"#;
fn sql(e: &impl Expression) -> String {
build(&Numbered, e).expect("render").0
}
#[test]
fn a_lock_without_a_strength_writes_nothing() {
assert_frag_sql(FRAME, &sql(&Lock::default()), "");
assert_frag_sql(FRAME, &sql(&Locks::default()), "");
assert!(Lock::default().is_empty());
assert!(Locks::default().is_empty());
}
#[test]
fn a_bare_lock_is_just_for_and_the_strength() {
assert_frag_sql(
FRAME,
&sql(&Lock::new(LockStrength::KeyShare)),
"FOR KEY SHARE",
);
}
#[test]
fn strength_tables_and_wait_render_in_grammar_order() {
let mut l = Lock::new(LockStrength::Update);
l.append_table(["users", "posts"]);
l.wait = Some(LockWait::SkipLocked);
let (rendered, args) = build(&Numbered, &l).unwrap();
assert_frag_sql(
TWO_TABLE_FRAME,
&rendered,
r#"FOR UPDATE OF "users", "posts" SKIP LOCKED"#,
);
assert!(args.is_empty(), "table names are identifiers");
}
#[test]
fn every_strength_and_wait_has_its_spelling() {
for (strength, keyword) in [
(LockStrength::Update, "FOR UPDATE"),
(LockStrength::NoKeyUpdate, "FOR NO KEY UPDATE"),
(LockStrength::Share, "FOR SHARE"),
(LockStrength::KeyShare, "FOR KEY SHARE"),
] {
assert_frag_sql(FRAME, &sql(&Lock::new(strength)), keyword);
}
let mut l = Lock::new(LockStrength::Share);
l.wait = Some(LockWait::NoWait);
assert_frag_sql(FRAME, &sql(&l), "FOR SHARE NOWAIT");
}
#[test]
fn several_locks_are_space_separated() {
let mut locks = Locks::default();
let mut first = Lock::new(LockStrength::Update);
first.append_table(["users"]);
let mut second = Lock::new(LockStrength::Share);
second.append_table(["posts"]);
locks.append_lock(first);
locks.append_lock(second);
assert_frag_sql(
TWO_TABLE_FRAME,
&sql(&locks),
r#"FOR UPDATE OF "users" FOR SHARE OF "posts""#,
);
}
}