Skip to main content

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, JSON
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        // Triple-quoted multi-line string (must come before single/double quotes)
57        parse_triple_quoted_string,
58        // JSON object literal: { ... } or array: [ ... ]
59        parse_json_literal,
60        // String (double quoted) - allow empty strings
61        map(
62            delimited(
63                char('"'),
64                nom::bytes::complete::take_while(|c| c != '"'),
65                char('"'),
66            ),
67            |s: &str| Value::String(s.to_string()),
68        ),
69        // String (single quoted) - allow empty strings
70        map(
71            delimited(
72                char('\''),
73                nom::bytes::complete::take_while(|c| c != '\''),
74                char('\''),
75            ),
76            |s: &str| Value::String(s.to_string()),
77        ),
78        // Float (must check before int)
79        map(
80            recognize((opt(char('-')), digit1, char('.'), digit1)),
81            |s: &str| Value::Float(s.parse().unwrap_or(0.0)),
82        ),
83        // Interval shorthand before plain integers: 24h, 7d, 1w
84        parse_interval,
85        // Integer (last, after interval)
86        map(recognize((opt(char('-')), digit1)), |s: &str| {
87            Value::Int(s.parse().unwrap_or(0))
88        }),
89    ))
90    .parse(input)
91}
92
93/// Parse triple-quoted multi-line string: '''content''' or """content"""
94fn parse_triple_quoted_string(input: &str) -> IResult<&str, Value> {
95    alt((
96        // Triple single quotes
97        map(
98            delimited(
99                tag("'''"),
100                nom::bytes::complete::take_until("'''"),
101                tag("'''"),
102            ),
103            |s: &str| Value::String(s.to_string()),
104        ),
105        // Triple double quotes
106        map(
107            delimited(
108                tag("\"\"\""),
109                nom::bytes::complete::take_until("\"\"\""),
110                tag("\"\"\""),
111            ),
112            |s: &str| Value::String(s.to_string()),
113        ),
114    ))
115    .parse(input)
116}
117
118/// Parse JSON object literal: { key: value, ... } or array: [...]
119/// This captures the entire JSON structure as a string for Value::Json
120fn parse_json_literal(input: &str) -> IResult<&str, Value> {
121    // Determine if it's an object or array
122    let trimmed = input.trim_start();
123    if trimmed.is_empty() {
124        return Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag)));
125    }
126    
127    let (open_char, close_char) = match trimmed.chars().next() {
128        Some('{') => ('{', '}'),
129        Some('[') => ('[', ']'),
130        _ => return Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag))),
131    };
132    
133    // Count brackets to find matching close
134    let mut depth = 0;
135    let mut in_string = false;
136    let mut escape_next = false;
137    let mut end_pos = 0;
138    
139    for (i, c) in trimmed.char_indices() {
140        if escape_next {
141            escape_next = false;
142            continue;
143        }
144        
145        if c == '\\' && in_string {
146            escape_next = true;
147            continue;
148        }
149        
150        if c == '"' {
151            in_string = !in_string;
152            continue;
153        }
154        
155        if !in_string {
156            if c == open_char {
157                depth += 1;
158            } else if c == close_char {
159                depth -= 1;
160                if depth == 0 {
161                    end_pos = i + 1;
162                    break;
163                }
164            }
165        }
166    }
167    
168    if depth != 0 || end_pos == 0 {
169        return Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Eof)));
170    }
171    
172    let json_str = &trimmed[..end_pos];
173    let _remaining = &trimmed[end_pos..];
174    
175    // Calculate how much of original input we consumed (account for leading whitespace)
176    let consumed = input.len() - trimmed.len() + end_pos;
177    let remaining_original = &input[consumed..];
178    
179    Ok((remaining_original, Value::Json(json_str.to_string())))
180}
181
182/// Parse comparison operator
183pub fn parse_operator(input: &str) -> IResult<&str, Operator> {
184    alt((
185        // Multi-char operators first
186        value(Operator::NotBetween, tag_no_case("not between")),
187        value(Operator::Between, tag_no_case("between")),
188        value(Operator::IsNotNull, tag_no_case("is not null")),
189        value(Operator::IsNull, tag_no_case("is null")),
190        value(Operator::NotIn, tag_no_case("not in")),
191        value(Operator::NotILike, tag_no_case("not ilike")),
192        value(Operator::NotLike, tag_no_case("not like")),
193        value(Operator::ILike, tag_no_case("ilike")),
194        value(Operator::Like, tag_no_case("like")),
195        value(Operator::In, tag_no_case("in")),
196        value(Operator::Gte, tag(">=")),
197        value(Operator::Lte, tag("<=")),
198        value(Operator::Ne, tag("!=")),
199        value(Operator::Ne, tag("<>")),
200        // Single char operators
201        value(Operator::Eq, tag("=")),
202        value(Operator::Gt, tag(">")),
203        value(Operator::Lt, tag("<")),
204        value(Operator::Fuzzy, tag("~")),
205    ))
206    .parse(input)
207}
208
209/// Parse action keyword: get, set, del, add, make, cnt
210pub fn parse_action(input: &str) -> IResult<&str, (Action, bool)> {
211    alt((
212        // get distinct
213        map(
214            (tag_no_case("get"), multispace1, tag_no_case("distinct")),
215            |_| (Action::Get, true),
216        ),
217        // get
218        value((Action::Get, false), tag_no_case("get")),
219        // cnt / count (must come before general keywords)
220        alt((
221            value((Action::Cnt, false), tag_no_case("count")),
222            value((Action::Cnt, false), tag_no_case("cnt")),
223        )),
224        // set
225        value((Action::Set, false), tag_no_case("set")),
226        // del / delete
227        alt((
228            value((Action::Del, false), tag_no_case("delete")),
229            value((Action::Del, false), tag_no_case("del")),
230        )),
231        // add / insert
232        alt((
233            value((Action::Add, false), tag_no_case("insert")),
234            value((Action::Add, false), tag_no_case("add")),
235        )),
236        // make / create
237        alt((
238            value((Action::Make, false), tag_no_case("create")),
239            value((Action::Make, false), tag_no_case("make")),
240        )),
241    ))
242    .parse(input)
243}
244
245/// Parse transaction commands: begin, commit, rollback
246pub fn parse_txn_command(input: &str) -> IResult<&str, Qail> {
247    let (input, action) = alt((
248        value(Action::TxnStart, tag_no_case("begin")),
249        value(Action::TxnCommit, tag_no_case("commit")),
250        value(Action::TxnRollback, tag_no_case("rollback")),
251    ))
252    .parse(input)?;
253
254    Ok((
255        input,
256        Qail {
257            action,
258            table: String::new(),
259            columns: vec![],
260            joins: vec![],
261            cages: vec![],
262            distinct: false,
263            distinct_on: vec![],
264            index_def: None,
265            table_constraints: vec![],
266            set_ops: vec![],
267            having: vec![],
268            group_by_mode: GroupByMode::default(),
269            ctes: vec![],
270            returning: None,
271            on_conflict: None,
272            source_query: None,
273            channel: None,
274            payload: None,
275            savepoint_name: None,
276            from_tables: vec![],
277            using_tables: vec![],
278            lock_mode: None,
279            fetch: None,
280            default_values: false,
281            overriding: None,
282            sample: None,
283            only_table: false,
284            vector: None,
285            score_threshold: None,
286            vector_name: None,
287            with_vector: false,
288            vector_size: None,
289            distance: None,
290            on_disk: None,
291            function_def: None,
292            trigger_def: None,
293            raw_value: None,
294            redis_ttl: None,
295            redis_set_condition: None,
296        },
297    ))
298}