keelson_core/clause/
lock.rs1use std::borrow::Cow;
2
3use crate::writer::{Expression, SqlWriter};
4
5use super::{MaybeAbsent, write_present, write_quoted_list};
6
7#[derive(Debug, Clone, Default)]
13pub struct Locks {
14 pub locks: Vec<Lock>,
16}
17
18impl Locks {
19 pub fn append_lock(&mut self, lock: Lock) {
21 self.locks.push(lock);
22 }
23
24 pub fn is_empty(&self) -> bool {
26 self.locks.is_empty()
27 }
28}
29
30impl Expression for Locks {
31 fn write_sql(&self, w: &mut SqlWriter<'_>) {
32 write_present(w, &self.locks, "", " ", "");
33 }
34}
35
36pub trait HasLocks {
38 fn locks_mut(&mut self) -> &mut Locks;
40}
41
42impl HasLocks for Locks {
43 fn locks_mut(&mut self) -> &mut Locks {
44 self
45 }
46}
47
48#[derive(Debug, Clone, Default)]
59pub struct Lock {
60 pub strength: Option<LockStrength>,
63 pub tables: Vec<Cow<'static, str>>,
65 pub wait: Option<LockWait>,
67}
68
69impl Lock {
70 pub fn new(strength: LockStrength) -> Self {
72 Lock {
73 strength: Some(strength),
74 ..Lock::default()
75 }
76 }
77
78 pub fn append_table(&mut self, tables: impl IntoIterator<Item = impl Into<Cow<'static, str>>>) {
80 self.tables.extend(tables.into_iter().map(Into::into));
81 }
82
83 pub fn is_empty(&self) -> bool {
85 self.strength.is_none()
86 }
87}
88
89impl Expression for Lock {
90 fn write_sql(&self, w: &mut SqlWriter<'_>) {
91 let Some(strength) = &self.strength else {
92 return;
93 };
94
95 w.push_str("FOR ");
96 w.push_str(strength.as_str());
97
98 write_quoted_list(w, &self.tables, " OF ", ", ", "");
99
100 if let Some(wait) = &self.wait {
101 w.push_str(" ");
102 w.push_str(wait.as_str());
103 }
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum LockStrength {
110 Update,
112 NoKeyUpdate,
115 Share,
117 KeyShare,
119}
120
121impl LockStrength {
122 pub fn as_str(self) -> &'static str {
124 match self {
125 LockStrength::Update => "UPDATE",
126 LockStrength::NoKeyUpdate => "NO KEY UPDATE",
127 LockStrength::Share => "SHARE",
128 LockStrength::KeyShare => "KEY SHARE",
129 }
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub enum LockWait {
136 NoWait,
138 SkipLocked,
140}
141
142impl LockWait {
143 pub fn as_str(self) -> &'static str {
145 match self {
146 LockWait::NoWait => "NOWAIT",
147 LockWait::SkipLocked => "SKIP LOCKED",
148 }
149 }
150}
151
152impl MaybeAbsent for Lock {
153 fn is_absent(&self) -> bool {
154 self.is_empty()
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use keelson_sqlcheck::testing::assert_frag_sql;
161
162 use super::*;
163 use crate::dialect::testing::Numbered;
164 use crate::writer::{Expression, build};
165
166 const FRAME: &str = r#"SELECT "id" FROM users {}"#;
169 const TWO_TABLE_FRAME: &str = r#"SELECT "users"."id" FROM users, posts {}"#;
170
171 fn sql(e: &impl Expression) -> String {
172 build(&Numbered, e).expect("render").0
173 }
174
175 #[test]
176 fn a_lock_without_a_strength_writes_nothing() {
177 assert_frag_sql(FRAME, &sql(&Lock::default()), "");
178 assert_frag_sql(FRAME, &sql(&Locks::default()), "");
179 assert!(Lock::default().is_empty());
180 assert!(Locks::default().is_empty());
181 }
182
183 #[test]
184 fn a_bare_lock_is_just_for_and_the_strength() {
185 assert_frag_sql(
188 FRAME,
189 &sql(&Lock::new(LockStrength::KeyShare)),
190 "FOR KEY SHARE",
191 );
192 }
193
194 #[test]
195 fn strength_tables_and_wait_render_in_grammar_order() {
196 let mut l = Lock::new(LockStrength::Update);
198 l.append_table(["users", "posts"]);
199 l.wait = Some(LockWait::SkipLocked);
200
201 let (rendered, args) = build(&Numbered, &l).unwrap();
202 assert_frag_sql(
203 TWO_TABLE_FRAME,
204 &rendered,
205 r#"FOR UPDATE OF "users", "posts" SKIP LOCKED"#,
206 );
207 assert!(args.is_empty(), "table names are identifiers");
208 }
209
210 #[test]
211 fn every_strength_and_wait_has_its_spelling() {
212 for (strength, keyword) in [
213 (LockStrength::Update, "FOR UPDATE"),
214 (LockStrength::NoKeyUpdate, "FOR NO KEY UPDATE"),
215 (LockStrength::Share, "FOR SHARE"),
216 (LockStrength::KeyShare, "FOR KEY SHARE"),
217 ] {
218 assert_frag_sql(FRAME, &sql(&Lock::new(strength)), keyword);
219 }
220
221 let mut l = Lock::new(LockStrength::Share);
222 l.wait = Some(LockWait::NoWait);
223 assert_frag_sql(FRAME, &sql(&l), "FOR SHARE NOWAIT");
224 }
225
226 #[test]
227 fn several_locks_are_space_separated() {
228 let mut locks = Locks::default();
229 let mut first = Lock::new(LockStrength::Update);
230 first.append_table(["users"]);
231 let mut second = Lock::new(LockStrength::Share);
232 second.append_table(["posts"]);
233 locks.append_lock(first);
234 locks.append_lock(second);
235
236 assert_frag_sql(
237 TWO_TABLE_FRAME,
238 &sql(&locks),
239 r#"FOR UPDATE OF "users" FOR SHARE OF "posts""#,
240 );
241 }
242}