Skip to content

Commit

Permalink
feat(parser): create keyspace query
Browse files Browse the repository at this point in the history
  • Loading branch information
Fyko committed Oct 20, 2023
1 parent 7e95f59 commit f398dd9
Show file tree
Hide file tree
Showing 8 changed files with 403 additions and 15 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ args = ["fmt", "--all", "--", "--check"]


[tasks.test]
env = { "RUN_MODE" = "test", "RUST_LOG" = "info" }
env = { "RUN_MODE" = "test", "RUST_LOG" = "info", "RUST_BACKTRACE" = "short" }
command = "cargo"
args = ["nextest", "run", "--workspace", "${@}"]

Expand Down
1 change: 1 addition & 0 deletions scyllax-parser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ readme = 'crates.md'

[dependencies]
nom = "7"
serde_json = "1"

[dev-dependencies]
pretty_assertions = "1"
Expand Down
45 changes: 45 additions & 0 deletions scyllax-parser/src/comment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use nom::{
branch::alt,
bytes::complete::tag,
character::complete::{alpha1, alphanumeric1},
combinator::recognize,
multi::many0_count,
sequence::delimited,
IResult,
};

// matches a cql comment
// - `-- end of line comment`
// - `/* block comment */` (can be multiline)
// - `// end of line comment`
pub fn parse_comment(input: &str) -> IResult<&str, &str> {
alt((
parse_line_comment,
parse_block_comment,
parse_line_comment_slash_slash,
))(input)
}

fn parse_line_comment(input: &str) -> IResult<&str, &str> {
delimited(
tag("--"),
recognize(many0_count(alt((alpha1, alphanumeric1, tag(" "))))),
tag("\n"),
)(input)
}

fn parse_block_comment(input: &str) -> IResult<&str, &str> {
delimited(
tag("/*"),
recognize(many0_count(alt((alpha1, alphanumeric1, tag(" "))))),
tag("*/"),
)(input)
}

fn parse_line_comment_slash_slash(input: &str) -> IResult<&str, &str> {
delimited(
tag("//"),
recognize(many0_count(alt((alpha1, alphanumeric1, tag(" "))))),
tag("\n"),
)(input)
}
16 changes: 14 additions & 2 deletions scyllax-parser/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,22 +72,24 @@ pub enum Value {
Literal(String),
/// The value is a number
Number(usize),
/// The value is a boolean
Boolean(bool),
}

/// Parses a [`Value`]
pub fn parse_value(input: &str) -> IResult<&str, Value> {
alt((
map(parse_boolean, Value::Boolean),
map(parse_variable, Value::Variable),
map(parse_number, Value::Number),
map(parse_string, Value::Literal),
map(parse_string, Value::Literal), // must be last!
))(input)
}

/// Parses a [`Value::Literal`].
/// If there are any escaped quotes, they should be included in the output.
/// e.g. `\"` should be parsed as `\"`
/// - `foo` -> `foo`
/// TODO: - `"foo"` -> `"foo"`
fn parse_string(input: &str) -> IResult<&str, String> {
let (input, alpha) = alt((
// barf
Expand All @@ -110,6 +112,16 @@ fn parse_number(input: &str) -> IResult<&str, usize> {
Ok((input, number.parse().unwrap()))
}

/// Parses a [`Value::Boolean`]
fn parse_boolean(input: &str) -> IResult<&str, bool> {
let (input, boolean) = alt((
map(tag_no_case("true"), |_| true),
map(tag_no_case("false"), |_| false),
))(input)?;

Ok((input, boolean))
}

/// Parses a Rust flavored variable wrapped in double quotes
pub fn parse_string_escaped_rust_flavored_variable(input: &str) -> IResult<&str, String> {
let (input, alpha) = delimited(tag("\""), parse_rust_flavored_variable, tag("\""))(input)?;
Expand Down
276 changes: 276 additions & 0 deletions scyllax-parser/src/create_keyspace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
//! Parses a create keyspace query.
//! ```ignore
//! create_keyspace_statement: CREATE KEYSPACE [ IF NOT EXISTS ] `keyspace_name` WITH `options`
//! ```
//! ## Examples
//! ```cql,ignore
//! CREATE KEYSPACE Excalibur
//! WITH replication = {'class': 'NetworkTopologyStrategy', 'DC1' : 1, 'DC2' : 3}
//! AND durable_writes = true;
//! ```
//!
//! ```cql,ignore
//! CREATE KEYSPACE Excelsior
//! WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};
//! ```

use std::collections::HashMap;

use nom::{
branch::alt,
bytes::complete::{tag, tag_no_case, take_while_m_n},
character::complete::{alphanumeric0, char, multispace0},
combinator::opt,
error::Error,
multi::separated_list0,
sequence::delimited,
Err, IResult,
};

use crate::{common::parse_rust_flavored_variable, r#where::parse_comparisons, Column, Value};
#[derive(Debug, PartialEq)]
pub struct CreateKeyspaceQuery {
pub name: String,
pub if_not_exists: bool,
pub replication: ReplicationOption,
pub durable_writes: Option<bool>,
}

#[derive(Debug, PartialEq)]
pub enum ReplicationOption {
SimpleStrategy(i32),
NetworkTopologyStrategy(HashMap<String, i32>),
}

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

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

pub fn parse_create_keyspace(input: &str) -> IResult<&str, CreateKeyspaceQuery> {
let (input, _) = tag_no_case("create keyspace ")(input)?;
let (input, if_not_exists) = parse_if_not_exists(input)?;

let (input, name) = parse_keyspace_name(input)?;

let (input, _) = multispace0(input)?;
let (input, replication) = parse_replication(input)?;
let (input, _) = multispace0(input)?;
let (input, durable_writes) = parse_durable_writes(input)?;
let (input, _) = opt(tag(";"))(input)?;

Ok((
input,
CreateKeyspaceQuery {
name,
if_not_exists,
replication,
durable_writes,
},
))
}

fn parse_if_not_exists(input: &str) -> IResult<&str, bool> {
let (input, exists) = opt(tag_no_case("if not exists "))(input)?;
Ok((input, exists.is_some()))
}

fn parse_keyspace_name(input: &str) -> IResult<&str, String> {
let (input, name) = parse_rust_flavored_variable(input)?;
Ok((input, name.to_string()))
}

fn parse_replication(input: &str) -> IResult<&str, ReplicationOption> {
let (input, _) = tag_no_case("with replication =")(input)?;
let (input, strategy) = parse_replication_object(input)?;

let class = strategy.get("class").unwrap();
match *class {
"SimpleStrategy" => {
let replication_factor = strategy.get("replication_factor").unwrap();
let replication_factor = replication_factor.parse::<i32>().unwrap();
Ok((input, ReplicationOption::SimpleStrategy(replication_factor)))
}
"NetworkTopologyStrategy" => {
let mut map = HashMap::new();
for (key, value) in strategy {
if key == "class" {
continue;
}
let value = value.parse::<i32>().unwrap();
map.insert(key.to_string(), value);
}
Ok((input, ReplicationOption::NetworkTopologyStrategy(map)))
}
_ => panic!("Unknown replication strategy: {}", class),
}
}

/// parse the weird json like replication strategy
/// eg: `{'class': 'NetworkTopologyStrategy', 'DC1' : 1, 'DC2' : 3}`
/// remember to parse the single quotes
fn parse_replication_object(input: &str) -> IResult<&str, HashMap<&str, &str>> {
let (input, _) = multispace0(input)?;
let (input, _) = tag("{")(input)?;
let (input, _) = multispace0(input)?;
let (input, pairs) = separated_list0(tag(","), parse_replication_pair)(input)?;
let (input, _) = multispace0(input)?;
let (input, _) = tag("}")(input)?;

let mut map = HashMap::new();
for (key, value) in pairs {
map.insert(key, value);
}

Ok((input, map))
}

// - 'class': 'NetworkTopologyStrategy'
// - 'DC1' : 1
// - 'DC2' : 3
/// remember to parse the single quotes and colon and command whitespaces
fn parse_replication_pair(input: &str) -> IResult<&str, (&str, &str)> {
let (input, _) = multispace0(input)?;

let (input, key) = delimited(char('\''), parse_rust_flavored_variable, char('\''))(input)?;

let (input, _) = multispace0(input)?;
let (input, _) = tag(":")(input)?;
let (input, _) = multispace0(input)?;

let string_value = delimited(char('\''), alphanumeric0, char('\''));
let int_value = take_while_m_n(1, usize::MAX, char::is_numeric);

let (input, value) = alt((string_value, int_value))(input)?;

let (input, _) = multispace0(input)?;

Ok((input, (key, value)))
}

fn parse_durable_writes(input: &str) -> IResult<&str, Option<bool>> {
let (input, comparisons) = opt(parse_comparisons)(input)?;

let durable_writes = comparisons.and_then(|x| {
x.into_iter().find_map(|x| match x.column {
Column::Identifier(ref name) if name == "durable_writes" => Some(match x.value {
Value::Boolean(value) => value,
_ => panic!("Expected a boolean value for durable_writes"),
}),
_ => None,
})
});

Ok((input, durable_writes))
}

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

#[test]
fn test_simple_strategy() {
assert_eq!(
parse_create_keyspace(
"CREATE KEYSPACE Excalibur WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 } AND durable_writes = true;"
),
Ok((
"",
CreateKeyspaceQuery {
name: "Excalibur".to_string(),
if_not_exists: false,
replication: ReplicationOption::SimpleStrategy(3),
durable_writes: Some(true)
}
))
);
}

#[test]
fn test_network_topology_strategy() {
assert_eq!(
parse_create_keyspace(
r#"CREATE KEYSPACE Excelsior WITH replication = { 'class': 'NetworkTopologyStrategy', 'DC1' : 1, 'DC2' : 3};"#
),
Ok((
"",
CreateKeyspaceQuery {
name: "Excelsior".to_string(),
if_not_exists: false,
replication: ReplicationOption::NetworkTopologyStrategy(
vec![("DC1".to_string(), 1), ("DC2".to_string(), 3)]
.into_iter()
.collect()
),
durable_writes: None
}
))
);
}

#[test]
fn test_if_not_exists() {
assert_eq!(
parse_create_keyspace(
r#"CREATE KEYSPACE IF NOT EXISTS Excelsior WITH replication = {'class': 'NetworkTopologyStrategy', 'DC1' : 1, 'DC2' : 3};"#
),
Ok((
"",
CreateKeyspaceQuery {
name: "Excelsior".to_string(),
if_not_exists: true,
replication: ReplicationOption::NetworkTopologyStrategy(
vec![("DC1".to_string(), 1), ("DC2".to_string(), 3)]
.into_iter()
.collect()
),
durable_writes: None
}
))
);
}

#[test]
fn test_durable_writes() {
assert_eq!(
parse_create_keyspace(
r#"CREATE KEYSPACE Excelsior WITH replication = {'class': 'NetworkTopologyStrategy', 'DC1' : 1, 'DC2' : 3} AND durable_writes = true;"#
),
Ok((
"",
CreateKeyspaceQuery {
name: "Excelsior".to_string(),
if_not_exists: false,
replication: ReplicationOption::NetworkTopologyStrategy(
vec![("DC1".to_string(), 1), ("DC2".to_string(), 3)]
.into_iter()
.collect()
),
durable_writes: Some(true)
}
))
);
}

#[test]
fn test_parse_replication_object() {
let res: HashMap<&str, &str> = vec![
("class", "NetworkTopologyStrategy"),
("DC1", "1"),
("DC2", "3"),
]
.into_iter()
.collect();

assert_eq!(
parse_replication_object(
r#"{ 'class' : 'NetworkTopologyStrategy', 'DC1' : 1, 'DC2': 3}"#
),
Ok(("", res))
);
}
}
Loading

0 comments on commit f398dd9

Please sign in to comment.