qail_core/parser/grammar/
base.rs

1use crate::ast::values::IntervalUnit;
2use crate::ast::*;
3use nom::{
4    IResult, Parser,
5    branch::alt,
6    bytes::complete::{tag, tag_no_case, take_while1},
7    character::complete::{char, digit1, multispace1},
8    combinator::{map, opt, recognize, value},
9    sequence::{delimited, preceded},
10};
11
12/// Parse checking identifier (table name, column name, or qualified name like table.column)
13pub fn parse_identifier(input: &str) -> IResult<&str, &str> {
14    take_while1(|c: char| c.is_alphanumeric() || c == '_' || c == '.').parse(input)
15}
16
17/// Parse interval shorthand: 24h, 7d, 1w, 30m, 6mo, 1y
18pub fn parse_interval(input: &str) -> IResult<&str, Value> {
19    let (input, num_str) = digit1(input)?;
20    let amount: i64 = num_str.parse().unwrap_or(0);
21
22    let (input, unit) = alt((
23        value(IntervalUnit::Second, tag_no_case("s")),
24        value(IntervalUnit::Minute, tag_no_case("m")),
25        value(IntervalUnit::Hour, tag_no_case("h")),
26        value(IntervalUnit::Day, tag_no_case("d")),
27        value(IntervalUnit::Week, tag_no_case("w")),
28        value(IntervalUnit::Month, tag_no_case("mo")),
29        value(IntervalUnit::Year, tag_no_case("y")),
30    ))
31    .parse(input)?;
32
33    Ok((input, Value::Interval { amount, unit }))
34}
35
36/// Parse value: string, number, bool, null, $param, :named_param, interval
37pub fn parse_value(input: &str) -> IResult<&str, Value> {
38    alt((
39        // Parameter: $1, $2
40        map(preceded(char('$'), digit1), |d: &str| {
41            Value::Param(d.parse().unwrap_or(0))
42        }),
43        // Named parameter: :name, :id, :user_id
44        map(
45            preceded(
46                char(':'),
47                take_while1(|c: char| c.is_alphanumeric() || c == '_'),
48            ),
49            |name: &str| Value::NamedParam(name.to_string()),
50        ),
51        // Boolean
52        value(Value::Bool(true), tag_no_case("true")),
53        value(Value::Bool(false), tag_no_case("false")),
54        // Null
55        value(Value::Null, tag_no_case("null")),
56        // String (double quoted) - allow empty strings
57        map(
58            delimited(
59                char('"'),
60                nom::bytes::complete::take_while(|c| c != '"'),
61                char('"'),
62            ),
63            |s: &str| Value::String(s.to_string()),
64        ),
65        // String (single quoted) - allow empty strings
66        map(
67            delimited(
68                char('\''),
69                nom::bytes::complete::take_while(|c| c != '\''),
70                char('\''),
71            ),
72            |s: &str| Value::String(s.to_string()),
73        ),
74        // Float (must check before int)
75        map(
76            recognize((opt(char('-')), digit1, char('.'), digit1)),
77            |s: &str| Value::Float(s.parse().unwrap_or(0.0)),
78        ),
79        // Interval shorthand before plain integers: 24h, 7d, 1w
80        parse_interval,
81        // Integer (last, after interval)
82        map(recognize((opt(char('-')), digit1)), |s: &str| {
83            Value::Int(s.parse().unwrap_or(0))
84        }),
85    ))
86    .parse(input)
87}
88
89/// Parse comparison operator
90pub fn parse_operator(input: &str) -> IResult<&str, Operator> {
91    alt((
92        // Multi-char operators first
93        value(Operator::NotBetween, tag_no_case("not between")),
94        value(Operator::Between, tag_no_case("between")),
95        value(Operator::IsNotNull, tag_no_case("is not null")),
96        value(Operator::IsNull, tag_no_case("is null")),
97        value(Operator::NotIn, tag_no_case("not in")),
98        value(Operator::NotILike, tag_no_case("not ilike")),
99        value(Operator::NotLike, tag_no_case("not like")),
100        value(Operator::ILike, tag_no_case("ilike")),
101        value(Operator::Like, tag_no_case("like")),
102        value(Operator::In, tag_no_case("in")),
103        value(Operator::Gte, tag(">=")),
104        value(Operator::Lte, tag("<=")),
105        value(Operator::Ne, tag("!=")),
106        value(Operator::Ne, tag("<>")),
107        // Single char operators
108        value(Operator::Eq, tag("=")),
109        value(Operator::Gt, tag(">")),
110        value(Operator::Lt, tag("<")),
111        value(Operator::Fuzzy, tag("~")),
112    ))
113    .parse(input)
114}
115
116/// Parse action keyword: get, set, del, add, make
117pub fn parse_action(input: &str) -> IResult<&str, (Action, bool)> {
118    alt((
119        // get distinct
120        map(
121            (tag_no_case("get"), multispace1, tag_no_case("distinct")),
122            |_| (Action::Get, true),
123        ),
124        // get
125        value((Action::Get, false), tag_no_case("get")),
126        // set
127        value((Action::Set, false), tag_no_case("set")),
128        // del / delete
129        alt((
130            value((Action::Del, false), tag_no_case("delete")),
131            value((Action::Del, false), tag_no_case("del")),
132        )),
133        // add / insert
134        alt((
135            value((Action::Add, false), tag_no_case("insert")),
136            value((Action::Add, false), tag_no_case("add")),
137        )),
138        // make / create
139        alt((
140            value((Action::Make, false), tag_no_case("create")),
141            value((Action::Make, false), tag_no_case("make")),
142        )),
143    ))
144    .parse(input)
145}
146
147/// Parse transaction commands: begin, commit, rollback
148pub fn parse_txn_command(input: &str) -> IResult<&str, Qail> {
149    let (input, action) = alt((
150        value(Action::TxnStart, tag_no_case("begin")),
151        value(Action::TxnCommit, tag_no_case("commit")),
152        value(Action::TxnRollback, tag_no_case("rollback")),
153    ))
154    .parse(input)?;
155
156    Ok((
157        input,
158        Qail {
159            action,
160            table: String::new(),
161            columns: vec![],
162            joins: vec![],
163            cages: vec![],
164            distinct: false,
165            distinct_on: vec![],
166            index_def: None,
167            table_constraints: vec![],
168            set_ops: vec![],
169            having: vec![],
170            group_by_mode: GroupByMode::default(),
171            ctes: vec![],
172            returning: None,
173            on_conflict: None,
174            source_query: None,
175            channel: None,
176            payload: None,
177            savepoint_name: None,
178            from_tables: vec![],
179            using_tables: vec![],
180            lock_mode: None,
181            fetch: None,
182            default_values: false,
183            overriding: None,
184            sample: None,
185            only_table: false,
186        },
187    ))
188}