1use crate::entity::Entity;
7use crate::error::{Error, Result};
8use crate::handle::SyncHandle;
9use crate::row::FromRow;
10use rusqlite::types::Value;
11
12pub fn col(name: impl Into<String>) -> Col {
14 Col { name: name.into() }
15}
16
17#[derive(Debug, Clone)]
19pub struct Col {
20 name: String,
21}
22
23pub trait IntoDbValue {
25 fn into_db_value(self) -> Value;
26}
27
28macro_rules! impl_into_db_value {
29 ($($t:ty => $v:expr),+ $(,)?) => {$(
30 impl IntoDbValue for $t {
31 fn into_db_value(self) -> Value {
32 let f: fn($t) -> Value = $v;
33 f(self)
34 }
35 }
36 )+};
37}
38
39impl_into_db_value! {
40 i8 => |v| Value::Integer(v as i64),
41 i16 => |v| Value::Integer(v as i64),
42 i32 => |v| Value::Integer(v as i64),
43 i64 => Value::Integer,
44 u8 => |v| Value::Integer(v as i64),
45 u16 => |v| Value::Integer(v as i64),
46 u32 => |v| Value::Integer(v as i64),
47 f32 => |v| Value::Real(v as f64),
48 f64 => Value::Real,
49 bool => |v| Value::Integer(v as i64),
50 String => Value::Text,
51 &str => |v: &str| Value::Text(v.to_string()),
52 Vec<u8> => Value::Blob,
53 Value => |v| v,
54}
55
56#[derive(Debug, Clone)]
58pub enum Expr {
59 Cmp {
61 col: String,
62 op: &'static str,
63 value: Value,
64 },
65 LikeEscaped {
67 col: String,
68 pattern: String,
69 escape: char,
70 },
71 InList {
73 col: String,
74 values: Vec<Value>,
75 },
76 Null {
78 col: String,
79 not: bool,
80 },
81 And(Box<Expr>, Box<Expr>),
83 Or(Box<Expr>, Box<Expr>),
84}
85
86impl Col {
87 fn cmp(self, op: &'static str, v: impl IntoDbValue) -> Expr {
88 Expr::Cmp {
89 col: self.name,
90 op,
91 value: v.into_db_value(),
92 }
93 }
94 pub fn eq(self, v: impl IntoDbValue) -> Expr {
95 let value = v.into_db_value();
96 if value == Value::Null {
97 self.is_null()
98 } else {
99 Expr::Cmp {
100 col: self.name,
101 op: "=",
102 value,
103 }
104 }
105 }
106 pub fn ne(self, v: impl IntoDbValue) -> Expr {
107 let value = v.into_db_value();
108 if value == Value::Null {
109 self.is_not_null()
110 } else {
111 Expr::Cmp {
112 col: self.name,
113 op: "<>",
114 value,
115 }
116 }
117 }
118 pub fn lt(self, v: impl IntoDbValue) -> Expr {
119 self.cmp("<", v)
120 }
121 pub fn le(self, v: impl IntoDbValue) -> Expr {
122 self.cmp("<=", v)
123 }
124 pub fn gt(self, v: impl IntoDbValue) -> Expr {
125 self.cmp(">", v)
126 }
127 pub fn ge(self, v: impl IntoDbValue) -> Expr {
128 self.cmp(">=", v)
129 }
130 pub fn like(self, pattern: impl Into<String>) -> Expr {
132 self.cmp("LIKE", Value::Text(pattern.into()))
133 }
134 pub fn like_escaped(self, pattern: impl Into<String>, escape: char) -> Expr {
139 Expr::LikeEscaped {
140 col: self.name,
141 pattern: pattern.into(),
142 escape,
143 }
144 }
145 pub fn in_list<V: IntoDbValue>(self, vs: impl IntoIterator<Item = V>) -> Expr {
146 Expr::InList {
147 col: self.name,
148 values: vs.into_iter().map(IntoDbValue::into_db_value).collect(),
149 }
150 }
151 pub fn is_null(self) -> Expr {
152 Expr::Null {
153 col: self.name,
154 not: false,
155 }
156 }
157 pub fn is_not_null(self) -> Expr {
158 Expr::Null {
159 col: self.name,
160 not: true,
161 }
162 }
163}
164
165impl Expr {
166 pub fn and(self, other: Expr) -> Expr {
168 Expr::And(Box::new(self), Box::new(other))
169 }
170 pub fn or(self, other: Expr) -> Expr {
172 Expr::Or(Box::new(self), Box::new(other))
173 }
174
175 fn collect_cols<'a>(&'a self, out: &mut Vec<&'a str>) {
177 match self {
178 Expr::Cmp { col, .. }
179 | Expr::LikeEscaped { col, .. }
180 | Expr::InList { col, .. }
181 | Expr::Null { col, .. } => {
182 out.push(col);
183 }
184 Expr::And(a, b) | Expr::Or(a, b) => {
185 a.collect_cols(out);
186 b.collect_cols(out);
187 }
188 }
189 }
190
191 fn render(&self, sql: &mut String, params: &mut Vec<Value>) {
193 match self {
194 Expr::Cmp { col, op, value } => {
195 params.push(value.clone());
196 sql.push_str(&format!("\"{col}\" {op} ?{}", params.len()));
197 }
198 Expr::LikeEscaped {
199 col,
200 pattern,
201 escape,
202 } => {
203 params.push(Value::Text(pattern.clone()));
204 let pattern_index = params.len();
205 params.push(Value::Text(escape.to_string()));
206 let escape_index = params.len();
207 sql.push_str(&format!(
208 "\"{col}\" LIKE ?{pattern_index} ESCAPE ?{escape_index}"
209 ));
210 }
211 Expr::InList { col, values } => {
212 if values.is_empty() {
213 sql.push_str("0 = 1");
215 return;
216 }
217 let start = params.len() + 1;
218 params.extend(values.iter().cloned());
219 let ph = (start..start + values.len())
220 .map(|i| format!("?{i}"))
221 .collect::<Vec<_>>()
222 .join(", ");
223 sql.push_str(&format!("\"{col}\" IN ({ph})"));
224 }
225 Expr::Null { col, not } => {
226 sql.push_str(&format!(
227 "\"{col}\" IS {}NULL",
228 if *not { "NOT " } else { "" }
229 ));
230 }
231 Expr::And(a, b) => {
232 sql.push('(');
233 a.render(sql, params);
234 sql.push_str(" AND ");
235 b.render(sql, params);
236 sql.push(')');
237 }
238 Expr::Or(a, b) => {
239 sql.push('(');
240 a.render(sql, params);
241 sql.push_str(" OR ");
242 b.render(sql, params);
243 sql.push(')');
244 }
245 }
246 }
247}
248
249#[derive(Debug, Clone, Copy)]
251pub enum Order {
252 Asc,
253 Desc,
254}
255
256pub struct Query;
258
259impl Query {
260 pub fn select<T: Entity>() -> SelectBuilder<T> {
262 SelectBuilder::new(false)
263 }
264
265 pub fn count<T: Entity>() -> SelectBuilder<T> {
267 SelectBuilder::new(true)
268 }
269}
270
271pub struct SelectBuilder<T: Entity> {
273 count: bool,
274 wheres: Option<Expr>,
275 order: Vec<(String, Order)>,
276 limit: Option<u64>,
277 offset: Option<u64>,
278 _t: std::marker::PhantomData<T>,
279}
280
281impl<T: Entity> Clone for SelectBuilder<T> {
282 fn clone(&self) -> Self {
284 Self {
285 count: self.count,
286 wheres: self.wheres.clone(),
287 order: self.order.clone(),
288 limit: self.limit,
289 offset: self.offset,
290 _t: std::marker::PhantomData,
291 }
292 }
293}
294
295impl<T: Entity> SelectBuilder<T> {
296 fn new(count: bool) -> Self {
297 Self {
298 count,
299 wheres: None,
300 order: Vec::new(),
301 limit: None,
302 offset: None,
303 _t: std::marker::PhantomData,
304 }
305 }
306
307 pub fn and_where(mut self, e: Expr) -> Self {
309 self.wheres = Some(match self.wheres {
310 None => e,
311 Some(w) => w.and(e),
312 });
313 self
314 }
315
316 pub fn or_where(mut self, e: Expr) -> Self {
318 self.wheres = Some(match self.wheres {
319 None => e,
320 Some(w) => w.or(e),
321 });
322 self
323 }
324
325 pub fn order_by(mut self, col: impl Into<String>, order: Order) -> Self {
327 self.order.push((col.into(), order));
328 self
329 }
330
331 pub fn limit(mut self, n: u64) -> Self {
332 self.limit = Some(n);
333 self
334 }
335
336 pub fn offset(mut self, n: u64) -> Self {
337 self.offset = Some(n);
338 self
339 }
340
341 pub fn build(&self) -> Result<(String, Vec<Value>)> {
343 let mut cols: Vec<&str> = Vec::new();
345 if let Some(w) = &self.wheres {
346 w.collect_cols(&mut cols);
347 }
348 for (c, _) in &self.order {
349 cols.push(c);
350 }
351 for c in cols {
352 if !T::COLUMNS_META.iter().any(|m| m.name == c) {
353 return Err(Error::Config(format!(
354 "엔티티 \"{}\"에 없는 컬럼: \"{c}\" (쿼리빌더 스키마 검증)",
355 T::TABLE
356 )));
357 }
358 }
359
360 let mut sql = if self.count {
361 format!("SELECT COUNT(*) FROM \"{}\"", T::TABLE)
362 } else {
363 format!("SELECT {} FROM \"{}\"", T::COLUMNS, T::TABLE)
364 };
365 let mut params: Vec<Value> = Vec::new();
366 if let Some(w) = &self.wheres {
367 sql.push_str(" WHERE ");
368 w.render(&mut sql, &mut params);
369 }
370 if !self.order.is_empty() {
371 let parts: Vec<String> = self
372 .order
373 .iter()
374 .map(|(c, o)| {
375 format!(
376 "\"{c}\" {}",
377 match o {
378 Order::Asc => "ASC",
379 Order::Desc => "DESC",
380 }
381 )
382 })
383 .collect();
384 sql.push_str(&format!(" ORDER BY {}", parts.join(", ")));
385 }
386 if let Some(n) = self.limit {
387 sql.push_str(&format!(" LIMIT {n}"));
388 }
389 if let Some(n) = self.offset {
390 sql.push_str(&format!(" OFFSET {n}"));
391 }
392 Ok((sql, params))
393 }
394
395 pub fn fetch_all<E: Execute>(self, ex: E) -> E::Out<Vec<T>>
397 where
398 T: FromRow + Send + 'static,
399 {
400 match self.build() {
401 Ok((sql, params)) => ex.run_all(sql, params),
402 Err(e) => E::fail(e),
403 }
404 }
405
406 pub fn fetch_optional<E: Execute>(self, ex: E) -> E::Out<Option<T>>
408 where
409 T: FromRow + Send + 'static,
410 {
411 match self.build() {
412 Ok((sql, params)) => ex.run_optional(sql, params),
413 Err(e) => E::fail(e),
414 }
415 }
416
417 pub fn fetch_one<E: Execute>(self, ex: E) -> E::Out<T>
419 where
420 T: FromRow + Send + 'static,
421 {
422 match self.build() {
423 Ok((sql, params)) => ex.run_one(sql, params),
424 Err(e) => E::fail(e),
425 }
426 }
427
428 pub fn fetch_scalar<E: Execute>(self, ex: E) -> E::Out<i64>
430 where
431 T: Send + 'static,
432 {
433 match self.build() {
434 Ok((sql, params)) => ex.run_scalar(sql, params),
435 Err(e) => E::fail(e),
436 }
437 }
438}
439
440pub trait Execute {
443 type Out<R: Send + 'static>;
445
446 fn run_all<T: FromRow + Send + 'static>(
447 self,
448 sql: String,
449 params: Vec<Value>,
450 ) -> Self::Out<Vec<T>>;
451 fn run_optional<T: FromRow + Send + 'static>(
452 self,
453 sql: String,
454 params: Vec<Value>,
455 ) -> Self::Out<Option<T>>;
456 fn run_one<T: FromRow + Send + 'static>(self, sql: String, params: Vec<Value>) -> Self::Out<T>;
457 fn run_scalar(self, sql: String, params: Vec<Value>) -> Self::Out<i64>;
458 fn fail<R: Send + 'static>(e: Error) -> Self::Out<R>;
460}
461
462impl Execute for SyncHandle<'_> {
463 type Out<R: Send + 'static> = Result<R>;
464
465 fn run_all<T: FromRow + Send + 'static>(
466 self,
467 sql: String,
468 params: Vec<Value>,
469 ) -> Result<Vec<T>> {
470 self.query_all(&sql, rusqlite::params_from_iter(params))
471 }
472 fn run_optional<T: FromRow + Send + 'static>(
473 self,
474 sql: String,
475 params: Vec<Value>,
476 ) -> Result<Option<T>> {
477 self.query_optional(&sql, rusqlite::params_from_iter(params))
478 }
479 fn run_one<T: FromRow + Send + 'static>(self, sql: String, params: Vec<Value>) -> Result<T> {
480 self.query_one(&sql, rusqlite::params_from_iter(params))
481 }
482 fn run_scalar(self, sql: String, params: Vec<Value>) -> Result<i64> {
483 self.query_scalar(&sql, rusqlite::params_from_iter(params))
484 }
485 fn fail<R: Send + 'static>(e: Error) -> Result<R> {
486 Err(e)
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
496 fn null_comparisons_render_is_null() {
497 let mut sql = String::new();
498 let mut params = Vec::new();
499 col("value").eq(Value::Null).render(&mut sql, &mut params);
500 assert_eq!(sql, "\"value\" IS NULL");
501 assert!(params.is_empty());
502
503 sql.clear();
504 col("value").ne(Value::Null).render(&mut sql, &mut params);
505 assert_eq!(sql, "\"value\" IS NOT NULL");
506 assert!(params.is_empty());
507 }
508
509 #[test]
511 fn escaped_like_binds_pattern_and_escape() {
512 let mut sql = String::new();
513 let mut params = vec![Value::Integer(7)];
514 col("name")
515 .like_escaped("100!%", '!')
516 .render(&mut sql, &mut params);
517 assert_eq!(sql, "\"name\" LIKE ?2 ESCAPE ?3");
518 assert_eq!(
519 params,
520 vec![
521 Value::Integer(7),
522 Value::Text("100!%".into()),
523 Value::Text("!".into())
524 ]
525 );
526
527 sql.clear();
528 params.clear();
529 col("name")
530 .like_escaped("한界%", '界')
531 .render(&mut sql, &mut params);
532 assert_eq!(params[1], Value::Text("界".into()));
533 }
534
535 struct T0;
537 impl crate::row::FromRow for T0 {
538 fn from_row(_: &rusqlite::Row<'_>) -> rusqlite::Result<Self> {
539 Ok(T0)
540 }
541 }
542 impl Entity for T0 {
543 const TABLE: &'static str = "t0";
544 const DDL: &'static [&'static str] = &[];
545 const COLUMNS: &'static str = "\"id\", \"name\"";
546 const COLUMNS_META: &'static [crate::database::ColumnMeta] = &[
547 crate::database::ColumnMeta {
548 name: "id",
549 sql_type: "INTEGER",
550 not_null: true,
551 pk: true,
552 renamed_from: None,
553 },
554 crate::database::ColumnMeta {
555 name: "name",
556 sql_type: "TEXT",
557 not_null: true,
558 pk: false,
559 renamed_from: None,
560 },
561 ];
562 }
563
564 #[test]
566 fn renders_sql() {
567 let (sql, params) = Query::select::<T0>()
568 .and_where(col("id").in_list([1i64, 2]).and(col("name").like("k%")))
569 .or_where(col("name").is_null())
570 .order_by("id", Order::Desc)
571 .limit(10)
572 .offset(5)
573 .build()
574 .unwrap();
575 assert_eq!(
576 sql,
577 "SELECT \"id\", \"name\" FROM \"t0\" WHERE ((\"id\" IN (?1, ?2) AND \"name\" LIKE ?3) OR \"name\" IS NULL) ORDER BY \"id\" DESC LIMIT 10 OFFSET 5"
578 );
579 assert_eq!(params.len(), 3);
580 }
581
582 #[test]
584 fn unknown_column_rejected() {
585 let r = Query::select::<T0>()
586 .and_where(col("nope").eq(1i64))
587 .build();
588 assert!(matches!(r, Err(Error::Config(_))));
589 }
590
591 #[test]
593 fn empty_in_list() {
594 let (sql, _) = Query::select::<T0>()
595 .and_where(col("id").in_list(Vec::<i64>::new()))
596 .build()
597 .unwrap();
598 assert!(sql.contains("0 = 1"));
599 }
600}