Skip to main content

modelvault_core/sql/
mod.rs

1//! Minimal SQL adapter for DB-API (0.10.0+).
2//!
3//! This is intentionally small: a `SELECT` subset that maps onto the existing typed query AST.
4
5mod lexer;
6mod parser;
7
8use crate::error::DbError;
9use crate::query::OrderBy;
10use crate::schema::FieldPath;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct SqlSelect {
14    pub columns: SqlColumns,
15    pub collection: String,
16    pub predicate: Option<SqlPredicate>,
17    pub order_by: Option<OrderBy>,
18    pub limit: Option<usize>,
19    pub param_count: usize,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum SqlColumns {
24    Star,
25    Paths(Vec<FieldPath>),
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum SqlValue {
30    Param(usize),
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum SqlPredicate {
35    Eq { path: FieldPath, value: SqlValue },
36    Lt { path: FieldPath, value: SqlValue },
37    Lte { path: FieldPath, value: SqlValue },
38    Gt { path: FieldPath, value: SqlValue },
39    Gte { path: FieldPath, value: SqlValue },
40    And(Vec<SqlPredicate>),
41    Or(Vec<SqlPredicate>),
42}
43
44/// Parse a minimal `SELECT` statement into a structured form.
45///
46/// Notes:
47/// - This accepts only parameter placeholders (`?`) for predicate values (no SQL literals yet).
48/// - Keywords are ASCII case-insensitive.
49pub fn parse_select(sql: &str) -> Result<SqlSelect, DbError> {
50    parser::parse_select_tokens(lexer::lex(sql)?)
51}
52
53#[cfg(test)]
54mod parse_select_token_tests {
55    use super::parser::parse_select_tokens;
56    use crate::error::DbError;
57    use crate::sql::lexer::Tok;
58
59    /// The lexer always emits digit runs as [`Tok::Number`], but `LIMIT` still accepts [`Tok::Ident`]
60    /// so callers/tests can feed synthetic token streams and `usize`-parsable names stay valid.
61    #[test]
62    fn limit_accepts_ident_that_parses_as_usize() {
63        let toks = vec![
64            Tok::Ident("select".into()),
65            Tok::Star,
66            Tok::Ident("from".into()),
67            Tok::Ident("t".into()),
68            Tok::Ident("limit".into()),
69            Tok::Ident("42".into()),
70        ];
71        let s = parse_select_tokens(toks).unwrap();
72        assert_eq!(s.limit, Some(42));
73    }
74
75    #[test]
76    fn parse_errors_when_where_clause_ends_after_path() {
77        let toks = vec![
78            Tok::Ident("select".into()),
79            Tok::Star,
80            Tok::Ident("from".into()),
81            Tok::Ident("t".into()),
82            Tok::Ident("where".into()),
83            Tok::Ident("x".into()),
84        ];
85        let e = parse_select_tokens(toks).unwrap_err();
86        assert!(matches!(e, DbError::Query(_)));
87    }
88}