Skip to main content

roomrs_core/
query.rs

1//! 동적 쿼리빌더 (명세 §5.3, 결정 로그 3) — 스키마 인지 + 핸들 대칭 실행[C-6]
2//!
3//! 명세의 `col(User::id)` 표기는 Rust에서 필드 토큰이 불가하므로
4//! `col("id")` 문자열 + 실행 시 `COLUMNS_META` 대조로 대체한다.
5
6use crate::entity::Entity;
7use crate::error::{Error, Result};
8use crate::handle::SyncHandle;
9use crate::row::FromRow;
10use rusqlite::types::Value;
11
12/// 컬럼 참조 시작점
13pub fn col(name: impl Into<String>) -> Col {
14    Col { name: name.into() }
15}
16
17/// 컬럼 참조 — 비교 콤비네이터의 시작점
18#[derive(Debug, Clone)]
19pub struct Col {
20    name: String,
21}
22
23/// 빌더 값 변환 — `&str` 포함 흔한 타입 열거 (코히어런스 안전)
24pub 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/// WHERE 식 트리
57#[derive(Debug, Clone)]
58pub enum Expr {
59    /// `col OP ?`
60    Cmp {
61        col: String,
62        op: &'static str,
63        value: Value,
64    },
65    /// `col LIKE pattern ESCAPE char` with both values bound as parameters.
66    LikeEscaped {
67        col: String,
68        pattern: String,
69        escape: char,
70    },
71    /// `col IN (?, …)`
72    InList {
73        col: String,
74        values: Vec<Value>,
75    },
76    /// `col IS NULL` / `IS NOT NULL`
77    Null {
78        col: String,
79        not: bool,
80    },
81    /// 논리 결합
82    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    /// LIKE 검색 (명세 §5.8 — 패턴은 호출자가 구성)
131    pub fn like(self, pattern: impl Into<String>) -> Expr {
132        self.cmp("LIKE", Value::Text(pattern.into()))
133    }
134    /// Builds a `LIKE ... ESCAPE ...` expression.
135    ///
136    /// Both the pattern and the single Unicode escape character are bound as
137    /// parameters. Existing [`like`](Self::like) behavior is unchanged.
138    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    /// 논리 AND
167    pub fn and(self, other: Expr) -> Expr {
168        Expr::And(Box::new(self), Box::new(other))
169    }
170    /// 논리 OR
171    pub fn or(self, other: Expr) -> Expr {
172        Expr::Or(Box::new(self), Box::new(other))
173    }
174
175    /// 참조 컬럼 수집 — 스키마 검증용
176    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    /// SQL 렌더 — 파라미터는 순서대로 누적
192    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                    // 빈 IN = 항상 거짓 (SQLite 유효 표현)
214                    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/// 정렬 방향
250#[derive(Debug, Clone, Copy)]
251pub enum Order {
252    Asc,
253    Desc,
254}
255
256/// 쿼리 빌더 진입점 (명세 §5.3)
257pub struct Query;
258
259impl Query {
260    /// 엔티티 전체 컬럼 SELECT
261    pub fn select<T: Entity>() -> SelectBuilder<T> {
262        SelectBuilder::new(false)
263    }
264
265    /// COUNT(*) — `fetch_scalar`로 소비
266    pub fn count<T: Entity>() -> SelectBuilder<T> {
267        SelectBuilder::new(true)
268    }
269}
270
271/// SELECT 빌더 — 조건·정렬·페이지 조합
272pub 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    /// 명세 §5.3 — `q.clone()` 후 양쪽 핸들 실행
283    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    /// AND 조건 추가
308    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    /// OR 조건 추가
317    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    /// 정렬 추가
326    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    /// SQL + 파라미터 렌더 — 스키마 인지 검증 포함 (명세 §5.3)
342    pub fn build(&self) -> Result<(String, Vec<Value>)> {
343        // 참조 컬럼 전수 검증 — SQLite 도달 전 명확한 에러
344        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    /// N건 실행 — 핸들 대칭 (동기=Result, 비동기=Future)
396    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    /// 0~1건 실행
407    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    /// 정확히 1건 (0건 = NotFound)
418    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    /// 스칼라 (COUNT 등)
429    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
440/// 핸들 대칭 실행자 (명세 §5.3 [C-6]) —
441/// 동기 = `Result<R>`, 비동기 = Boxed `Future`. `#[database]` 생성 핸들 래퍼에도 구현된다.
442pub trait Execute {
443    /// 실행 결과 컨테이너
444    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    /// 빌드 단계 에러를 결과 컨테이너로
459    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    /// NULL 비교는 SQL의 3값 논리 대신 IS NULL 계열로 렌더한다.
495    #[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    /// LIKE ESCAPE는 패턴과 escape를 모두 바인딩하고 번호를 연속 배정한다.
510    #[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    /// 렌더 단위 검증용 미니 엔티티 (매크로 없이 수동 구현)
536    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    /// SQL 렌더 — 조건 조합·IN·정렬·페이지
565    #[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    /// 스키마 검증 — 미지 컬럼 = Config 에러
583    #[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    /// 빈 IN = 항상 거짓
592    #[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}