1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//! A parser for CQL queries
//! See the source code and tests for examples of usage (for now).
pub mod comment;
pub mod common;
pub mod create_keyspace;
pub mod delete;
pub mod reserved;
pub mod select;
pub mod r#where;

use comment::parse_comment;
pub use common::{Column, Value, Variable};
use create_keyspace::CreateKeyspaceQuery;
pub use delete::DeleteQuery;
pub use r#where::{ComparisonOperator, WhereClause};
pub use select::SelectQuery;

use nom::{branch::alt, combinator::map, error::Error, multi::many0, Err, IResult};

/// Represents a query
/// ```rust
/// use scyllax_parser::*;
///
/// let query = Query::try_from("select id, name from person where id = ?");
/// assert_eq!(
///     query,
///     Ok(Query::Select(SelectQuery {
///         table: "person".to_string(),
///         columns: vec![
///             Column::Identifier("id".to_string()),
///             Column::Identifier("name".to_string()),
///         ],
///         condition: vec![
///             WhereClause {
///                 column: Column::Identifier("id".to_string()),
///                 operator: ComparisonOperator::Equal,
///                 value: Value::Variable(Variable::Placeholder),
///             },
///         ],
///         limit: None,
///     }))
/// );
/// ```
#[derive(Debug, PartialEq)]
pub enum Query {
    /// A select query
    Select(SelectQuery),
    /// A delete query
    Delete(DeleteQuery),
    /// A create keyspace query
    CreateKeyspace(CreateKeyspaceQuery),
}

/// Parse a CQL query.
pub fn parse_query(input: &str) -> IResult<&str, Query> {
    // trim whitespace
    let input = input.trim();
    // strip comments
    let (input, _) = many0(parse_comment)(input)?;
    let input = input.trim();
    println!("input: {input:#?}");

    alt((
        map(select::parse_select, Query::Select),
        map(delete::parse_delete, Query::Delete),
        map(
            create_keyspace::parse_create_keyspace,
            Query::CreateKeyspace,
        ),
    ))(input)
}

impl<'a> TryFrom<&'a str> for Query {
    type Error = Err<Error<&'a str>>;

    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
        Ok(parse_query(value)?.1)
    }
}

/// Parse a file that can contain multiple CQL queries. The queries are separated by a semicolon.
/// There may be an indeterminate number of newlines between the semicolon and the next query.
pub fn parse_query_file(input: &str) -> IResult<&str, Vec<Query>> {
    let trimmed = input.trim();

    let (input, queries) = nom::multi::separated_list1(
        nom::character::complete::multispace0,
        nom::sequence::terminated(parse_query, nom::character::complete::multispace0),
    )(trimmed)?;

    Ok((input, queries))
}

#[cfg(test)]
mod test {
    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn test_query_select() {
        let query = "/* this is a comment */ select id, name, age
        from person
        where id = :id
        and name = :name
        and age > ?
        limit 10";
        println!("query: {:#?}", query);

        let query = Query::try_from(query);

        assert_eq!(
            query,
            Ok(Query::Select(SelectQuery {
                table: "person".to_string(),
                columns: vec![
                    Column::Identifier("id".to_string()),
                    Column::Identifier("name".to_string()),
                    Column::Identifier("age".to_string()),
                ],
                condition: vec![
                    WhereClause {
                        column: Column::Identifier("id".to_string()),
                        operator: ComparisonOperator::Equal,
                        value: Value::Variable(Variable::NamedVariable("id".to_string())),
                    },
                    WhereClause {
                        column: Column::Identifier("name".to_string()),
                        operator: ComparisonOperator::Equal,
                        value: Value::Variable(Variable::NamedVariable("name".to_string())),
                    },
                    WhereClause {
                        column: Column::Identifier("age".to_string()),
                        operator: ComparisonOperator::GreaterThan,
                        value: Value::Variable(Variable::Placeholder),
                    },
                ],
                limit: Some(Value::Number(10)),
            }))
        );
    }
}