From 5abd9e7dec2569f92ea95f9cf47498fd42785b6c Mon Sep 17 00:00:00 2001 From: Jovansonlee Cesar Date: Sun, 30 Sep 2018 03:34:37 +0800 Subject: [PATCH] Modularized into separate files Add ToString implementation on the components --- src/sqlast.rs | 286 +++++++++------- src/sqlast/sql_operator.rs | 40 +++ src/sqlast/sqltype.rs | 98 ++++++ src/sqlast/table_key.rs | 58 ++++ src/sqlast/tests.rs | 0 src/sqlast/value.rs | 62 ++++ src/sqlparser.rs | 673 +------------------------------------ src/sqlparser/tests.rs | 661 ++++++++++++++++++++++++++++++++++++ 8 files changed, 1097 insertions(+), 781 deletions(-) create mode 100644 src/sqlast/sql_operator.rs create mode 100644 src/sqlast/sqltype.rs create mode 100644 src/sqlast/table_key.rs create mode 100644 src/sqlast/tests.rs create mode 100644 src/sqlast/value.rs create mode 100644 src/sqlparser/tests.rs diff --git a/src/sqlast.rs b/src/sqlast.rs index b2123b3a..ce4534bd 100644 --- a/src/sqlast.rs +++ b/src/sqlast.rs @@ -25,6 +25,20 @@ use chrono::{NaiveDate, }; use uuid::Uuid; +pub use self::value::Value; +pub use self::sqltype::SQLType; +pub use self::table_key::{ + AlterOperation, + TableKey, + Key +}; + +pub use self::sql_operator::SQLOperator; + +mod value; +mod sqltype; +mod table_key; +mod sql_operator; /// SQL Abstract Syntax Tree (AST) #[derive(Debug, Clone, PartialEq)] @@ -36,7 +50,7 @@ pub enum ASTNode { /// Multi part identifier e.g. `myschema.dbo.mytable` SQLCompoundIdentifier(Vec), /// Assigment e.g. `name = 'Fred'` in an UPDATE statement - SQLAssignment(String, Box), + SQLAssignment(SQLAssignment), /// `IS NULL` expression SQLIsNull(Box), /// `IS NOT NULL` expression @@ -102,7 +116,7 @@ pub enum ASTNode { /// TABLE table_name: String, /// Column assignments - assignemnts: Vec, + assignments: Vec, /// WHERE selection: Option>, }, @@ -112,9 +126,6 @@ pub enum ASTNode { relation: Option>, /// WHERE selection: Option>, - /// ORDER BY - order_by: Option>, - limit: Option>, }, /// CREATE TABLE SQLCreateTable { @@ -131,41 +142,134 @@ pub enum ASTNode { } } -/// SQL values such as int, double, string timestamp -#[derive(Debug, Clone, PartialEq)] -pub enum Value{ - /// Literal signed long - Long(i64), - /// Literal floating point value - Double(f64), - /// Unquoted string - String(String), - Uuid(Uuid), - /// 'string value' - SingleQuotedString(String), - /// "string value" - DoubleQuotedString(String), - /// Boolean value true or false, - Boolean(bool), - /// Date value - Date(NaiveDate), - // Time - Time(NaiveTime), - /// Date and time - DateTime(NaiveDateTime), - /// Timstamp with time zone - Timestamp(DateTime), - /// NULL value in insert statements, - Null, + +impl ToString for ASTNode{ + + fn to_string(&self) -> String { + match self{ + ASTNode::SQLIdentifier(s) => s.to_string(), + ASTNode::SQLWildcard => "*".to_string(), + ASTNode::SQLCompoundIdentifier(s) => s.join("."), + ASTNode::SQLAssignment(ass) => ass.to_string(), + ASTNode::SQLIsNull(ast) => format!("{} IS NULL",ast.as_ref().to_string()), + ASTNode::SQLIsNotNull(ast) => format!("{} IS NOT NULL", ast.as_ref().to_string()), + ASTNode::SQLBinaryExpr{left, op, right} => { + format!("{} {} {}", left.as_ref().to_string(), op.to_string(), right.as_ref().to_string()) + } + ASTNode::SQLCast{expr, data_type} => { + format!("CAST({} AS {})", expr.as_ref().to_string(), data_type.to_string()) + } + ASTNode::SQLNested(ast) => format!("({})", ast.as_ref().to_string()), + ASTNode::SQLUnary {operator, rex } => { + format!("{} {}", operator.to_string(), rex.as_ref().to_string()) + } + ASTNode::SQLValue(v) => v.to_string(), + ASTNode::SQLFunction{id, args} => format!("{}({})", id, args.iter().map(|a|a.to_string()).collect::>().join(", ")), + ASTNode::SQLSelect{ + projection, + relation, + selection, + order_by, + group_by, + having, + limit, + } => { + let mut s = format!("SELECT {}", projection.iter().map(|p|p.to_string()).collect::>().join(", ")); + if let Some(relation) = relation{ + s += &format!(" FROM {}", relation.as_ref().to_string()); + } + if let Some(selection) = selection{ + s += &format!(" WHERE {}", selection.as_ref().to_string()); + } + if let Some(group_by) = group_by { + s += &format!(" GROUP BY {}", group_by.iter().map(|g|g.to_string()).collect::>().join(", ")); + } + if let Some(having) = having{ + s += &format!(" HAVING {}", having.as_ref().to_string()); + } + if let Some(order_by) = order_by{ + s += &format!(" ORDER BY {}", order_by.iter().map(|o|o.to_string()).collect::>().join(", ")); + } + if let Some(limit) = limit { + s += &format!(" LIMIT {}", limit.as_ref().to_string()); + } + s + } + ASTNode::SQLInsert{ table_name, columns, values } => { + let mut s = format!("INSERT INTO {}", table_name); + if columns.len() > 0 { + s += &format!(" ({})", columns.join(", ")); + } + if values.len() > 0 { + s += &format!(" VALUES({})", + values.iter() + .map(|row|row.iter().map(|c|c.to_string()) + .collect::>().join(", ") + ).collect::>().join(", ") + ); + } + s + } + ASTNode::SQLCopy{table_name, columns, values} => { + let mut s = format!("COPY {}", table_name); + if columns.len() > 0 { + s += &format!(" ({})", columns.iter().map(|c|c.to_string()).collect::>().join(", ")); + } + s += " FROM stdin; "; + if values.len() > 0 { + s += &format!("\n{}", + values.iter() + .map(|v|v.to_string()) + .collect::>().join("\t") + ); + } + s + } + ASTNode::SQLUpdate{table_name, assignments, selection} => { + let mut s = format!("UPDATE {}", table_name); + if assignments.len() > 0 { + s += &format!("{}", assignments.iter().map(|ass|ass.to_string()).collect::>().join(", ")); + } + if let Some(selection) = selection{ + s += &format!(" WHERE {}", selection.as_ref().to_string()); + } + s + } + ASTNode::SQLDelete{relation, selection} => { + let mut s = String::from("DELETE"); + if let Some(relation) = relation{ + s += &format!(" FROM {}", relation.as_ref().to_string()); + } + if let Some(selection) = selection{ + s += &format!(" WHERE {}", selection.as_ref().to_string()); + } + s + } + ASTNode::SQLCreateTable{name, columns} => { + format!("CREATE TABLE {} ({})", name, columns.iter().map(|c|c.to_string()).collect::>().join(", ")) + } + ASTNode::SQLAlterTable{name, operation} => { + format!("ALTER TABLE {} {}", name, operation.to_string()) + } + } + } } /// SQL assignment `foo = expr` as used in SQLUpdate +/// TODO: unify this with the ASTNode SQLAssignment #[derive(Debug, Clone, PartialEq)] -pub struct SQLAssigment { +pub struct SQLAssignment { id: String, value: Box, } +impl ToString for SQLAssignment{ + + fn to_string(&self) -> String { + format!("SET {} = {}", self.id, self.value.as_ref().to_string()) + } +} + /// SQL ORDER BY expression #[derive(Debug, Clone, PartialEq)] pub struct SQLOrderByExpr { @@ -179,109 +283,45 @@ impl SQLOrderByExpr { } } +impl ToString for SQLOrderByExpr{ + fn to_string(&self) -> String { + if self.asc{ + format!("{} ASC", self.expr.as_ref().to_string()) + }else{ + format!("{} DESC", self.expr.as_ref().to_string()) + } + } +} + /// SQL column definition #[derive(Debug, Clone, PartialEq)] pub struct SQLColumnDef { pub name: String, pub data_type: SQLType, - pub allow_null: bool, pub is_primary: bool, pub is_unique: bool, pub default: Option>, + pub allow_null: bool, } -/// SQL datatypes for literals in SQL statements -#[derive(Debug, Clone, PartialEq)] -pub enum SQLType { - /// Fixed-length character type e.g. CHAR(10) - Char(Option), - /// Variable-length character type e.g. VARCHAR(10) - Varchar(Option), - /// Uuid value - Uuid, - /// Large character object e.g. CLOB(1000) - Clob(usize), - /// Fixed-length binary type e.g. BINARY(10) - Binary(usize), - /// Variable-length binary type e.g. VARBINARY(10) - Varbinary(usize), - /// Large binary object e.g. BLOB(1000) - Blob(usize), - /// Decimal type with precision and optional scale e.g. DECIMAL(10,2) - Decimal(usize, Option), - /// Small integer - SmallInt, - /// Integer - Int, - /// Big integer - BigInt, - /// Floating point with optional precision e.g. FLOAT(8) - Float(Option), - /// Floating point e.g. REAL - Real, - /// Double e.g. DOUBLE PRECISION - Double, - /// Boolean - Boolean, - /// Date - Date, - /// Time - Time, - /// Timestamp - Timestamp, - /// Regclass used in postgresql serial - Regclass, - /// Text - Text, - /// Bytea - Bytea, - /// Custom type such as enums - Custom(String), - /// Arrays - Array(Box), -} -/// SQL Operator -#[derive(Debug, PartialEq, Clone)] -pub enum SQLOperator { - Plus, - Minus, - Multiply, - Divide, - Modulus, - Gt, - Lt, - GtEq, - LtEq, - Eq, - NotEq, - And, - Or, -} +impl ToString for SQLColumnDef{ -#[derive(Debug, PartialEq, Clone)] -pub enum AlterOperation{ - AddConstraint(TableKey), - RemoveConstraint{ - name: String, + fn to_string(&self) -> String { + let mut s = format!("{} {}", self.name, self.data_type.to_string()); + if self.is_primary{ + s += " PRIMARY KEY"; + } + if self.is_unique{ + s += " UNIQUE"; + } + if let Some(ref default) = self.default{ + s += &format!(" DEFAULT {}", default.as_ref().to_string()); + } + if !self.allow_null{ + s += " NOT NULL"; + } + s } } - -#[derive(Debug, PartialEq, Clone)] -pub struct Key{ - pub name: Option, - pub columns: Vec, -} - -#[derive(Debug, PartialEq, Clone)] -pub enum TableKey{ - PrimaryKey(Key), - UniqueKey(Key), - Key(Key), - ForeignKey { - key: Key, - foreign_table: String, - referred_columns: Vec, - } -} diff --git a/src/sqlast/sql_operator.rs b/src/sqlast/sql_operator.rs new file mode 100644 index 00000000..bf054801 --- /dev/null +++ b/src/sqlast/sql_operator.rs @@ -0,0 +1,40 @@ + +/// SQL Operator +#[derive(Debug, PartialEq, Clone)] +pub enum SQLOperator { + Plus, + Minus, + Multiply, + Divide, + Modulus, + Gt, + Lt, + GtEq, + LtEq, + Eq, + NotEq, + And, + Or, +} + +impl ToString for SQLOperator{ + + fn to_string(&self) -> String { + match self{ + SQLOperator::Plus => "+".to_string(), + SQLOperator::Minus => "-".to_string(), + SQLOperator::Multiply => "*".to_string(), + SQLOperator::Divide => "/".to_string(), + SQLOperator::Modulus => "%".to_string(), + SQLOperator::Gt => ">".to_string(), + SQLOperator::Lt => "<".to_string(), + SQLOperator::GtEq => ">=".to_string(), + SQLOperator::LtEq => "<=".to_string(), + SQLOperator::Eq => "=".to_string(), + SQLOperator::NotEq => "!=".to_string(), + SQLOperator::And => "AND".to_string(), + SQLOperator::Or => "OR".to_string(), + } + } +} + diff --git a/src/sqlast/sqltype.rs b/src/sqlast/sqltype.rs new file mode 100644 index 00000000..c35edeaa --- /dev/null +++ b/src/sqlast/sqltype.rs @@ -0,0 +1,98 @@ + +/// SQL datatypes for literals in SQL statements +#[derive(Debug, Clone, PartialEq)] +pub enum SQLType { + /// Fixed-length character type e.g. CHAR(10) + Char(Option), + /// Variable-length character type e.g. VARCHAR(10) + Varchar(Option), + /// Uuid type + Uuid, + /// Large character object e.g. CLOB(1000) + Clob(usize), + /// Fixed-length binary type e.g. BINARY(10) + Binary(usize), + /// Variable-length binary type e.g. VARBINARY(10) + Varbinary(usize), + /// Large binary object e.g. BLOB(1000) + Blob(usize), + /// Decimal type with precision and optional scale e.g. DECIMAL(10,2) + Decimal(usize, Option), + /// Small integer + SmallInt, + /// Integer + Int, + /// Big integer + BigInt, + /// Floating point with optional precision e.g. FLOAT(8) + Float(Option), + /// Floating point e.g. REAL + Real, + /// Double e.g. DOUBLE PRECISION + Double, + /// Boolean + Boolean, + /// Date + Date, + /// Time + Time, + /// Timestamp + Timestamp, + /// Regclass used in postgresql serial + Regclass, + /// Text + Text, + /// Bytea + Bytea, + /// Custom type such as enums + Custom(String), + /// Arrays + Array(Box), +} + +impl ToString for SQLType{ + + fn to_string(&self) -> String { + match self { + SQLType::Char(size) => if let Some(size) = size { + format!("char({})", size) + }else{ + "char".to_string() + } + SQLType::Varchar(size) => if let Some(size) = size{ + format!("character varying({})", size) + }else{ + "character varying".to_string() + } + SQLType::Uuid => "uuid".to_string(), + SQLType::Clob(size) => format!("clob({})", size), + SQLType::Binary(size) => format!("binary({})", size), + SQLType::Varbinary(size) => format!("varbinary({})", size), + SQLType::Blob(size) => format!("blob({})", size), + SQLType::Decimal(precision, scale) => if let Some(scale) = scale{ + format!("numeric({},{})", precision, scale) + }else{ + format!("numeric({})", precision) + }, + SQLType::SmallInt => "smallint".to_string(), + SQLType::Int => "int".to_string(), + SQLType::BigInt => "bigint".to_string(), + SQLType::Float(size) => if let Some(size) = size{ + format!("float({})", size) + }else{ + "float".to_string() + }, + SQLType::Real => "real".to_string(), + SQLType::Double => "double".to_string(), + SQLType::Boolean => "boolean".to_string(), + SQLType::Date => "date".to_string(), + SQLType::Time => "time".to_string(), + SQLType::Timestamp => "timestamp".to_string(), + SQLType::Regclass => "regclass".to_string(), + SQLType::Text => "text".to_string(), + SQLType::Bytea => "bytea".to_string(), + SQLType::Array(ty) => format!("{}[]",ty.to_string()), + SQLType::Custom(ty) => ty.to_string(), + } + } +} diff --git a/src/sqlast/table_key.rs b/src/sqlast/table_key.rs new file mode 100644 index 00000000..894d7913 --- /dev/null +++ b/src/sqlast/table_key.rs @@ -0,0 +1,58 @@ + +#[derive(Debug, PartialEq, Clone)] +pub enum AlterOperation{ + AddConstraint(TableKey), + RemoveConstraint{ + name: String, + } +} + +impl ToString for AlterOperation { + + fn to_string(&self) -> String { + match self{ + AlterOperation::AddConstraint(table_key) => format!("ADD CONSTRAINT {}", table_key.to_string()), + AlterOperation::RemoveConstraint{name} => format!("REMOVE CONSTRAINT {}", name), + } + } +} + + +#[derive(Debug, PartialEq, Clone)] +pub struct Key{ + pub name: String, + pub columns: Vec, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum TableKey{ + PrimaryKey(Key), + UniqueKey(Key), + Key(Key), + ForeignKey { + key: Key, + foreign_table: String, + referred_columns: Vec, + } +} + + +impl ToString for TableKey{ + + fn to_string(&self) -> String { + match self{ + TableKey::PrimaryKey(ref key) => { + format!("{} PRIMARY KEY ({})", key.name, key.columns.join(", ")) + } + TableKey::UniqueKey(ref key) => { + format!("{} UNIQUE KEY ({})", key.name, key.columns.join(", ")) + } + TableKey::Key(ref key) => { + format!("{} KEY ({})", key.name, key.columns.join(", ")) + } + TableKey::ForeignKey{key, foreign_table, referred_columns} => { + format!("{} FOREIGN KEY ({}) REFERENCES {}({})", key.name, key.columns.join(", "), foreign_table, referred_columns.join(", ")) + } + } + } +} diff --git a/src/sqlast/tests.rs b/src/sqlast/tests.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/sqlast/value.rs b/src/sqlast/value.rs new file mode 100644 index 00000000..9c3e0442 --- /dev/null +++ b/src/sqlast/value.rs @@ -0,0 +1,62 @@ + +use chrono::{NaiveDate, + NaiveDateTime, + NaiveTime, + offset::{FixedOffset, + TimeZone, + }, + DateTime, + Utc, + }; + +use uuid::Uuid; + +/// SQL values such as int, double, string timestamp +#[derive(Debug, Clone, PartialEq)] +pub enum Value{ + /// Literal signed long + Long(i64), + /// Literal floating point value + Double(f64), + /// Unquoted string + String(String), + /// Uuid value + Uuid(Uuid), + /// 'string value' + SingleQuotedString(String), + /// "string value" + DoubleQuotedString(String), + /// Boolean value true or false, + Boolean(bool), + /// Date value + Date(NaiveDate), + // Time + Time(NaiveTime), + /// Date and time + DateTime(NaiveDateTime), + /// Timstamp with time zone + Timestamp(DateTime), + /// NULL value in insert statements, + Null, +} + + +impl ToString for Value{ + + fn to_string(&self) -> String { + match self{ + Value::Long(v) => v.to_string(), + Value::Double(v) => v.to_string(), + Value::String(v) => v.to_string(), + Value::Uuid(v) => v.to_string(), + Value::SingleQuotedString(v) => format!("'{}'", v), + Value::DoubleQuotedString(v) => format!("\"{}\"", v), + Value::Boolean(v) => v.to_string(), + Value::Date(v) => v.to_string(), + Value::Time(v) => v.to_string(), + Value::DateTime(v) => v.to_string(), + Value::Timestamp(v) => format!("{}", v), + Value::Null => "NULL".to_string(), + } + } +} diff --git a/src/sqlparser.rs b/src/sqlparser.rs index b598a7a2..92dcf65f 100644 --- a/src/sqlparser.rs +++ b/src/sqlparser.rs @@ -27,6 +27,9 @@ use chrono::{NaiveDate, Utc, }; +#[cfg(test)] +mod tests; + #[derive(Debug, Clone)] pub enum ParserError { TokenizerError(String), @@ -553,7 +556,7 @@ impl Parser { let column_names= self.parse_column_names()?; self.consume_token(&Token::RParen)?; let key = Key{ - name: Some(constraint_name.to_string()), + name: constraint_name.to_string(), columns: column_names }; if is_primary_key{ @@ -678,7 +681,17 @@ impl Parser { //TODO: parse the timestamp here Token::Number(ref n) if n.contains(".") => match n.parse::() { Ok(n) => Ok(Value::Double(n)), - Err(e) => parser_err!(format!("Could not parse '{}' as i64: {}", n, e)), + Err(e) => { + let index = self.index; + self.prev_token(); + if let Ok(timestamp) = self.parse_timestamp_value(){ + println!("timstamp: {:?}", timestamp); + Ok(timestamp) + }else{ + self.index = index; + parser_err!(format!("Could not parse '{}' as i64: {}", n, e)) + } + } }, Token::Number(ref n) => match n.parse::() { Ok(n) => { @@ -972,18 +985,6 @@ impl Parser { None }; - let order_by = if self.parse_keywords(vec!["ORDER", "BY"]) { - Some(self.parse_order_by_expr_list()?) - } else { - None - }; - - let limit = if self.parse_keyword("LIMIT") { - self.parse_limit()? - } else { - None - }; - let selection = if self.parse_keyword("WHERE") { Some(Box::new(self.parse_expr(0)?)) } else { @@ -1000,8 +1001,6 @@ impl Parser { Ok(ASTNode::SQLDelete { relation, selection, - order_by, - limit, }) } } @@ -1158,645 +1157,3 @@ impl Parser { } } -#[cfg(test)] -mod tests { - - use super::super::dialect::PostgreSqlDialect; - use super::*; - - #[test] - fn test_prev_index(){ - let sql: &str = "SELECT version()"; - let mut tokenizer = Tokenizer::new(&PostgreSqlDialect{}, &sql); - let tokens = tokenizer.tokenize().expect("error tokenizing"); - let mut parser = Parser::new(tokens); - assert_eq!(parser.prev_token(), None); - assert_eq!(parser.next_token(), Some(Token::Keyword("SELECT".into()))); - assert_eq!(parser.next_token(), Some(Token::Identifier("version".into()))); - assert_eq!(parser.prev_token(), Some(Token::Identifier("version".into()))); - assert_eq!(parser.peek_token(), Some(Token::Identifier("version".into()))); - assert_eq!(parser.prev_token(), Some(Token::Keyword("SELECT".into()))); - assert_eq!(parser.prev_token(), None); - } - - #[test] - fn parse_delete_statement() { - let sql: &str = "DELETE FROM 'table'"; - - match parse_sql(&sql) { - ASTNode::SQLDelete { relation, .. } => { - assert_eq!( - Some(Box::new(ASTNode::SQLValue(Value::SingleQuotedString("table".to_string())))), - relation - ); - } - - _ => assert!(false), - } - } - - #[test] - fn parse_where_delete_statement() { - let sql: &str = "DELETE FROM 'table' WHERE name = 5"; - - use self::ASTNode::*; - use self::SQLOperator::*; - - match parse_sql(&sql) { - ASTNode::SQLDelete { - relation, - selection, - .. - } => { - assert_eq!( - Some(Box::new(ASTNode::SQLValue(Value::SingleQuotedString("table".to_string())))), - relation - ); - - assert_eq!( - SQLBinaryExpr { - left: Box::new(SQLIdentifier("name".to_string())), - op: Eq, - right: Box::new(ASTNode::SQLValue(Value::Long(5))), - }, - *selection.unwrap(), - ); - } - - _ => assert!(false), - } - } - - #[test] - fn parse_simple_select() { - let sql = String::from("SELECT id, fname, lname FROM customer WHERE id = 1 LIMIT 5"); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLSelect { - projection, limit, .. - } => { - assert_eq!(3, projection.len()); - assert_eq!(Some(Box::new(ASTNode::SQLValue(Value::Long(5)))), limit); - } - _ => assert!(false), - } - } - - #[test] - fn parse_simple_insert() { - let sql = String::from("INSERT INTO customer VALUES(1, 2, 3)"); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLInsert { - table_name, columns, values, .. - } => { - assert_eq!(table_name, "customer"); - assert!(columns.is_empty()); - assert_eq!(vec![vec![ASTNode::SQLValue(Value::Long(1)),ASTNode::SQLValue(Value::Long(2)),ASTNode::SQLValue(Value::Long(3))]], values); - } - _ => assert!(false), - } - } - - #[test] - fn parse_common_insert() { - let sql = String::from("INSERT INTO public.customer VALUES(1, 2, 3)"); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLInsert { - table_name, columns, values, .. - } => { - assert_eq!(table_name, "public.customer"); - assert!(columns.is_empty()); - assert_eq!(vec![vec![ASTNode::SQLValue(Value::Long(1)),ASTNode::SQLValue(Value::Long(2)),ASTNode::SQLValue(Value::Long(3))]], values); - } - _ => assert!(false), - } - } - - #[test] - fn parse_complex_insert() { - let sql = String::from("INSERT INTO db.public.customer VALUES(1,2,3)"); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLInsert { - table_name, columns, values, .. - } => { - assert_eq!(table_name, "db.public.customer"); - assert!(columns.is_empty()); - assert_eq!(vec![vec![ASTNode::SQLValue(Value::Long(1)),ASTNode::SQLValue(Value::Long(2)),ASTNode::SQLValue(Value::Long(3))]], values); - } - _ => assert!(false), - } - } - - #[test] - fn parse_invalid_table_name() { - let mut parser = parser("db.public..customer"); - let ast = parser.parse_tablename(); - assert!(ast.is_err()); - } - - #[test] - fn parse_insert_with_columns() { - let sql = String::from("INSERT INTO public.customer (id, name, active) VALUES(1, 2, 3)"); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLInsert { - table_name, columns, values, .. - } => { - assert_eq!(table_name, "public.customer"); - assert_eq!(columns, vec!["id".to_string(), "name".to_string(), "active".to_string()]); - assert_eq!(vec![vec![ASTNode::SQLValue(Value::Long(1)),ASTNode::SQLValue(Value::Long(2)),ASTNode::SQLValue(Value::Long(3))]], values); - } - _ => assert!(false), - } - } - - #[test] - fn parse_select_wildcard() { - let sql = String::from("SELECT * FROM customer"); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLSelect { projection, .. } => { - assert_eq!(1, projection.len()); - assert_eq!(ASTNode::SQLWildcard, projection[0]); - } - _ => assert!(false), - } - } - - #[test] - fn parse_select_count_wildcard() { - let sql = String::from("SELECT COUNT(*) FROM customer"); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLSelect { projection, .. } => { - assert_eq!(1, projection.len()); - assert_eq!( - ASTNode::SQLFunction { - id: "COUNT".to_string(), - args: vec![ASTNode::SQLWildcard], - }, - projection[0] - ); - } - _ => assert!(false), - } - } - - #[test] - fn parse_select_string_predicate() { - let sql = String::from( - "SELECT id, fname, lname FROM customer \ - WHERE salary != 'Not Provided' AND salary != ''", - ); - let ast = parse_sql(&sql); - //TODO: add assertions - } - - #[test] - fn parse_projection_nested_type() { - let sql = String::from("SELECT customer.address.state FROM foo"); - let _ast = parse_sql(&sql); - //TODO: add assertions - } - - #[test] - fn parse_compound_expr_1() { - use self::ASTNode::*; - use self::SQLOperator::*; - let sql = String::from("a + b * c"); - let ast = parse_sql(&sql); - assert_eq!( - SQLBinaryExpr { - left: Box::new(SQLIdentifier("a".to_string())), - op: Plus, - right: Box::new(SQLBinaryExpr { - left: Box::new(SQLIdentifier("b".to_string())), - op: Multiply, - right: Box::new(SQLIdentifier("c".to_string())) - }) - }, - ast - ); - } - - #[test] - fn parse_compound_expr_2() { - use self::ASTNode::*; - use self::SQLOperator::*; - let sql = String::from("a * b + c"); - let ast = parse_sql(&sql); - assert_eq!( - SQLBinaryExpr { - left: Box::new(SQLBinaryExpr { - left: Box::new(SQLIdentifier("a".to_string())), - op: Multiply, - right: Box::new(SQLIdentifier("b".to_string())) - }), - op: Plus, - right: Box::new(SQLIdentifier("c".to_string())) - }, - ast - ); - } - - #[test] - fn parse_is_null() { - use self::ASTNode::*; - let sql = String::from("a IS NULL"); - let ast = parse_sql(&sql); - assert_eq!(SQLIsNull(Box::new(SQLIdentifier("a".to_string()))), ast); - } - - #[test] - fn parse_is_not_null() { - use self::ASTNode::*; - let sql = String::from("a IS NOT NULL"); - let ast = parse_sql(&sql); - assert_eq!(SQLIsNotNull(Box::new(SQLIdentifier("a".to_string()))), ast); - } - - #[test] - fn parse_select_order_by() { - let sql = String::from( - "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY lname ASC, fname DESC", - ); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLSelect { order_by, .. } => { - assert_eq!( - Some(vec![ - SQLOrderByExpr { - expr: Box::new(ASTNode::SQLIdentifier("lname".to_string())), - asc: true, - }, - SQLOrderByExpr { - expr: Box::new(ASTNode::SQLIdentifier("fname".to_string())), - asc: false, - }, - ]), - order_by - ); - } - _ => assert!(false), - } - } - - #[test] - fn parse_select_group_by() { - let sql = String::from("SELECT id, fname, lname FROM customer GROUP BY lname, fname"); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLSelect { group_by, .. } => { - assert_eq!( - Some(vec![ - ASTNode::SQLIdentifier("lname".to_string()), - ASTNode::SQLIdentifier("fname".to_string()), - ]), - group_by - ); - } - _ => assert!(false), - } - } - - #[test] - fn parse_limit_accepts_all() { - let sql = String::from("SELECT id, fname, lname FROM customer WHERE id = 1 LIMIT ALL"); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLSelect { - projection, limit, .. - } => { - assert_eq!(3, projection.len()); - assert_eq!(None, limit); - } - _ => assert!(false), - } - } - - #[test] - fn parse_cast() { - let sql = String::from("SELECT CAST(id AS BIGINT) FROM customer"); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLSelect { projection, .. } => { - assert_eq!(1, projection.len()); - assert_eq!( - ASTNode::SQLCast { - expr: Box::new(ASTNode::SQLIdentifier("id".to_string())), - data_type: SQLType::BigInt - }, - projection[0] - ); - } - _ => assert!(false), - } - } - - #[test] - fn parse_create_table() { - let sql = String::from( - "CREATE TABLE uk_cities (\ - name VARCHAR(100) NOT NULL,\ - lat DOUBLE NULL,\ - lng DOUBLE NULL)", - ); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLCreateTable { name, columns } => { - assert_eq!("uk_cities", name); - assert_eq!(3, columns.len()); - - let c_name = &columns[0]; - assert_eq!("name", c_name.name); - assert_eq!(SQLType::Varchar(Some(100)), c_name.data_type); - assert_eq!(false, c_name.allow_null); - - let c_lat = &columns[1]; - assert_eq!("lat", c_lat.name); - assert_eq!(SQLType::Double, c_lat.data_type); - assert_eq!(true, c_lat.allow_null); - - let c_lng = &columns[2]; - assert_eq!("lng", c_lng.name); - assert_eq!(SQLType::Double, c_lng.data_type); - assert_eq!(true, c_lng.allow_null); - } - _ => assert!(false), - } - } - - #[test] - fn parse_create_table_with_defaults() { - let sql = String::from( - "CREATE TABLE public.customer ( - customer_id integer DEFAULT nextval(public.customer_customer_id_seq) NOT NULL, - store_id smallint NOT NULL, - first_name character varying(45) NOT NULL, - last_name character varying(45) NOT NULL, - email character varying(50), - address_id smallint NOT NULL, - activebool boolean DEFAULT true NOT NULL, - create_date date DEFAULT now()::text NOT NULL, - last_update timestamp without time zone DEFAULT now() NOT NULL, - active integer NOT NULL)"); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLCreateTable { name, columns } => { - assert_eq!("public.customer", name); - assert_eq!(10, columns.len()); - - let c_name = &columns[0]; - assert_eq!("customer_id", c_name.name); - assert_eq!(SQLType::Int, c_name.data_type); - assert_eq!(false, c_name.allow_null); - - let c_lat = &columns[1]; - assert_eq!("store_id", c_lat.name); - assert_eq!(SQLType::SmallInt, c_lat.data_type); - assert_eq!(false, c_lat.allow_null); - - let c_lng = &columns[2]; - assert_eq!("first_name", c_lng.name); - assert_eq!(SQLType::Varchar(Some(45)), c_lng.data_type); - assert_eq!(false, c_lng.allow_null); - } - _ => assert!(false), - } - } - - #[test] - fn parse_create_table_from_pg_dump() { - let sql = String::from(" - CREATE TABLE public.customer ( - customer_id integer DEFAULT nextval('public.customer_customer_id_seq'::regclass) NOT NULL, - store_id smallint NOT NULL, - first_name character varying(45) NOT NULL, - last_name character varying(45) NOT NULL, - info text[], - address_id smallint NOT NULL, - activebool boolean DEFAULT true NOT NULL, - create_date date DEFAULT now()::date NOT NULL, - create_date1 date DEFAULT 'now'::text::date NOT NULL, - last_update timestamp without time zone DEFAULT now(), - release_year public.year, - active integer - )"); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLCreateTable { name, columns } => { - assert_eq!("public.customer", name); - - let c_name = &columns[0]; - assert_eq!("customer_id", c_name.name); - assert_eq!(SQLType::Int, c_name.data_type); - assert_eq!(false, c_name.allow_null); - - let c_lat = &columns[1]; - assert_eq!("store_id", c_lat.name); - assert_eq!(SQLType::SmallInt, c_lat.data_type); - assert_eq!(false, c_lat.allow_null); - - let c_lng = &columns[2]; - assert_eq!("first_name", c_lng.name); - assert_eq!(SQLType::Varchar(Some(45)), c_lng.data_type); - assert_eq!(false, c_lng.allow_null); - } - _ => assert!(false), - } - } - - #[test] - fn parse_create_table_with_inherit() { - let sql = String::from(" - CREATE TABLE bazaar.settings ( - settings_id uuid PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - user_id uuid UNIQUE, - value text[], - use_metric boolean DEFAULT true - ) - INHERITS (system.record)"); - let ast = parse_sql(&sql); - match ast { - ASTNode::SQLCreateTable { name, columns } => { - assert_eq!("bazaar.settings", name); - - let c_name = &columns[0]; - assert_eq!("settings_id", c_name.name); - assert_eq!(SQLType::Uuid, c_name.data_type); - assert_eq!(false, c_name.allow_null); - assert_eq!(true, c_name.is_primary); - assert_eq!(false, c_name.is_unique); - - let c_name = &columns[1]; - assert_eq!("user_id", c_name.name); - assert_eq!(SQLType::Uuid, c_name.data_type); - assert_eq!(true, c_name.allow_null); - assert_eq!(false, c_name.is_primary); - assert_eq!(true, c_name.is_unique); - } - _ => assert!(false), - } - } - - #[test] - fn parse_alter_table_constraint_primary_key(){ - let sql = String::from(" - ALTER TABLE ONLY bazaar.address - ADD CONSTRAINT address_pkey PRIMARY KEY (address_id)"); - let ast = parse_sql(&sql); - println!("ast: {:?}", ast); - match ast { - ASTNode::SQLAlterTable{ name, operation } => { - assert_eq!(name, "bazaar.address"); - } - _ => assert!(false), - } - } - - #[test] - fn parse_alter_table_constraint_foreign_key(){ - let sql = String::from(" - ALTER TABLE ONLY public.customer - ADD CONSTRAINT customer_address_id_fkey FOREIGN KEY (address_id) REFERENCES public.address(address_id) ON UPDATE CASCADE ON DELETE RESTRICT; - "); - let ast = parse_sql(&sql); - println!("ast: {:?}", ast); - match ast { - ASTNode::SQLAlterTable{ name, operation } => { - assert_eq!(name, "public.customer"); - } - _ => assert!(false), - } - } - - #[test] - fn parse_copy_example(){ - let sql = String::from(r#" - COPY public.actor (actor_id, first_name, last_name, last_update, value) FROM stdin; -1 PENELOPE GUINESS 2006-02-15 09:34:33 0.11111 -2 NICK WAHLBERG 2006-02-15 09:34:33 0.22222 -3 ED CHASE 2006-02-15 09:34:33 0.312323 -4 JENNIFER DAVIS 2006-02-15 09:34:33 0.3232 -5 JOHNNY LOLLOBRIGIDA 2006-02-15 09:34:33 1.343 -6 BETTE NICHOLSON 2006-02-15 09:34:33 5.0 -7 GRACE MOSTEL 2006-02-15 09:34:33 6.0 -8 MATTHEW JOHANSSON 2006-02-15 09:34:33 7.0 -9 JOE SWANK 2006-02-15 09:34:33 8.0 -10 CHRISTIAN GABLE 2006-02-15 09:34:33 9.1 -11 ZERO CAGE 2006-02-15 09:34:33 10.001 -12 KARL BERRY 2017-11-02 19:15:42.308637+08 11.001 -A Fateful Reflection of a Waitress And a Boat who must Discover a Sumo Wrestler in Ancient China -Kwara & Kogi -{"Deleted Scenes","Behind the Scenes"} -'awe':5 'awe-inspir':4 'barbarella':1 'cat':13 'conquer':16 'dog':18 'feminist':10 'inspir':6 'monasteri':21 'must':15 'stori':7 'streetcar':2 -PHP ₱ USD $ - - \\. - "#); - let mut parser = parser(&sql); - let ast = parser.parse(); - println!("ast: {:#?}", ast); - assert!(ast.is_ok()); - } - - #[test] - fn parse_timestamps_example(){ - let sql = "2016-02-15 09:43:33"; - let mut parser = parser(&sql); - let ast = parser.parse_timestamp_value(); - assert!(ast.is_ok()) - } - - #[test] - fn parse_timestamps_with_millis_example(){ - let sql = "2017-11-02 19:15:42.308637"; - let mut parser = parser(&sql); - let ast = parser.parse_timestamp_value(); - assert!(ast.is_ok()) - } - - #[test] - fn parse_example_value(){ - let sql = "SARAH.LEWIS@sakilacustomer.org"; - let mut parser = parser(&sql); - let ast = parser.parse_value(); - assert!(ast.is_ok()); - } - - #[test] - fn parse_scalar_function_in_projection() { - let sql = String::from("SELECT sqrt(id) FROM foo"); - let ast = parse_sql(&sql); - if let ASTNode::SQLSelect { projection, .. } = ast { - assert_eq!( - vec![ASTNode::SQLFunction { - id: String::from("sqrt"), - args: vec![ASTNode::SQLIdentifier(String::from("id"))], - }], - projection - ); - } else { - assert!(false); - } - } - - #[test] - fn parse_aggregate_with_group_by() { - let sql = String::from("SELECT a, COUNT(1), MIN(b), MAX(b) FROM foo GROUP BY a"); - let _ast = parse_sql(&sql); - //TODO: assertions - } - - #[test] - fn parse_literal_string() { - let sql = "SELECT 'one'"; - match parse_sql(&sql) { - ASTNode::SQLSelect { ref projection, .. } => { - assert_eq!(projection[0], ASTNode::SQLValue(Value::SingleQuotedString("one".to_string()))); - } - _ => panic!(), - } - } - - #[test] - fn parse_select_version() { - let sql = "SELECT @@version"; - match parse_sql(&sql) { - ASTNode::SQLSelect { ref projection, .. } => { - assert_eq!( - projection[0], - ASTNode::SQLIdentifier("@@version".to_string()) - ); - } - _ => panic!(), - } - } - - #[test] - fn parse_function_now(){ - let sql = "now()"; - let mut parser = parser(sql); - let ast = parser.parse(); - assert!(ast.is_ok()); - } - - fn parse_sql(sql: &str) -> ASTNode { - debug!("sql: {}", sql); - let mut parser = parser(sql); - let ast = parser.parse().unwrap(); - ast - } - - fn parser(sql: &str) -> Parser { - let dialect = PostgreSqlDialect {}; - let mut tokenizer = Tokenizer::new(&dialect, &sql); - let tokens = tokenizer.tokenize().unwrap(); - debug!("tokens: {:#?}", tokens); - Parser::new(tokens) - } - -} diff --git a/src/sqlparser/tests.rs b/src/sqlparser/tests.rs new file mode 100644 index 00000000..e61b9865 --- /dev/null +++ b/src/sqlparser/tests.rs @@ -0,0 +1,661 @@ + +use super::super::dialect::PostgreSqlDialect; +use super::*; + +#[test] +fn test_prev_index(){ + let sql: &str = "SELECT version()"; + let mut parser = parser(sql); + assert_eq!(parser.prev_token(), None); + assert_eq!(parser.next_token(), Some(Token::Keyword("SELECT".into()))); + assert_eq!(parser.next_token(), Some(Token::Identifier("version".into()))); + assert_eq!(parser.prev_token(), Some(Token::Identifier("version".into()))); + assert_eq!(parser.peek_token(), Some(Token::Identifier("version".into()))); + assert_eq!(parser.prev_token(), Some(Token::Keyword("SELECT".into()))); + assert_eq!(parser.prev_token(), None); +} + +#[test] +fn parse_delete_statement() { + let sql: &str = "DELETE FROM 'table'"; + let ast = parse_sql(sql); + assert_eq!("DELETE FROM 'table'", ast.to_string()); + + match parse_sql(&sql) { + ASTNode::SQLDelete { relation, .. } => { + assert_eq!( + Some(Box::new(ASTNode::SQLValue(Value::SingleQuotedString("table".to_string())))), + relation + ); + } + + _ => assert!(false), + } +} + +#[test] +fn parse_where_delete_statement() { + let sql: &str = "DELETE FROM 'table' WHERE name = 5"; + let ast = parse_sql(sql); + println!("ast: {:#?}", ast); + assert_eq!(sql, ast.to_string()); + + use self::ASTNode::*; + use self::SQLOperator::*; + + match parse_sql(&sql) { + ASTNode::SQLDelete { + relation, + selection, + .. + } => { + assert_eq!( + Some(Box::new(ASTNode::SQLValue(Value::SingleQuotedString("table".to_string())))), + relation + ); + + assert_eq!( + SQLBinaryExpr { + left: Box::new(SQLIdentifier("name".to_string())), + op: Eq, + right: Box::new(ASTNode::SQLValue(Value::Long(5))), + }, + *selection.unwrap(), + ); + } + + _ => assert!(false), + } +} + +#[test] +fn parse_simple_select() { + let sql = String::from("SELECT id, fname, lname FROM customer WHERE id = 1 LIMIT 5"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + match ast { + ASTNode::SQLSelect { + projection, limit, .. + } => { + assert_eq!(3, projection.len()); + assert_eq!(Some(Box::new(ASTNode::SQLValue(Value::Long(5)))), limit); + } + _ => assert!(false), + } +} + +#[test] +fn parse_simple_insert() { + let sql = String::from("INSERT INTO customer VALUES(1, 2, 3)"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + match ast { + ASTNode::SQLInsert { + table_name, columns, values, .. + } => { + assert_eq!(table_name, "customer"); + assert!(columns.is_empty()); + assert_eq!(vec![vec![ASTNode::SQLValue(Value::Long(1)),ASTNode::SQLValue(Value::Long(2)),ASTNode::SQLValue(Value::Long(3))]], values); + } + _ => assert!(false), + } +} + +#[test] +fn parse_common_insert() { + let sql = String::from("INSERT INTO public.customer VALUES(1, 2, 3)"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + match ast { + ASTNode::SQLInsert { + table_name, columns, values, .. + } => { + assert_eq!(table_name, "public.customer"); + assert!(columns.is_empty()); + assert_eq!(vec![vec![ASTNode::SQLValue(Value::Long(1)),ASTNode::SQLValue(Value::Long(2)),ASTNode::SQLValue(Value::Long(3))]], values); + } + _ => assert!(false), + } +} + +#[test] +fn parse_complex_insert() { + let sql = String::from("INSERT INTO db.public.customer VALUES(1, 2, 3)"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + match ast { + ASTNode::SQLInsert { + table_name, columns, values, .. + } => { + assert_eq!(table_name, "db.public.customer"); + assert!(columns.is_empty()); + assert_eq!(vec![vec![ASTNode::SQLValue(Value::Long(1)),ASTNode::SQLValue(Value::Long(2)),ASTNode::SQLValue(Value::Long(3))]], values); + } + _ => assert!(false), + } +} + +#[test] +fn parse_invalid_table_name() { + let mut parser = parser("db.public..customer"); + let ast = parser.parse_tablename(); + assert!(ast.is_err()); +} + +#[test] +fn parse_insert_with_columns() { + let sql = String::from("INSERT INTO public.customer (id, name, active) VALUES(1, 2, 3)"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + match ast { + ASTNode::SQLInsert { + table_name, columns, values, .. + } => { + assert_eq!(table_name, "public.customer"); + assert_eq!(columns, vec!["id".to_string(), "name".to_string(), "active".to_string()]); + assert_eq!(vec![vec![ASTNode::SQLValue(Value::Long(1)),ASTNode::SQLValue(Value::Long(2)),ASTNode::SQLValue(Value::Long(3))]], values); + } + _ => assert!(false), + } +} + +#[test] +fn parse_select_wildcard() { + let sql = String::from("SELECT * FROM customer"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + match ast { + ASTNode::SQLSelect { projection, .. } => { + assert_eq!(1, projection.len()); + assert_eq!(ASTNode::SQLWildcard, projection[0]); + } + _ => assert!(false), + } +} + +#[test] +fn parse_select_count_wildcard() { + let sql = String::from("SELECT COUNT(*) FROM customer"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + match ast { + ASTNode::SQLSelect { projection, .. } => { + assert_eq!(1, projection.len()); + assert_eq!( + ASTNode::SQLFunction { + id: "COUNT".to_string(), + args: vec![ASTNode::SQLWildcard], + }, + projection[0] + ); + } + _ => assert!(false), + } +} + +#[test] +fn parse_select_string_predicate() { + let sql = String::from( + "SELECT id, fname, lname FROM customer \ + WHERE salary != 'Not Provided' AND salary != ''", + ); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); +} + +#[test] +fn parse_projection_nested_type() { + let sql = String::from("SELECT customer.address.state FROM foo"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); +} + +#[test] +fn parse_compound_expr_1() { + use self::ASTNode::*; + use self::SQLOperator::*; + let sql = String::from("a + b * c"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + assert_eq!( + SQLBinaryExpr { + left: Box::new(SQLIdentifier("a".to_string())), + op: Plus, + right: Box::new(SQLBinaryExpr { + left: Box::new(SQLIdentifier("b".to_string())), + op: Multiply, + right: Box::new(SQLIdentifier("c".to_string())) + }) + }, + ast + ); +} + +#[test] +fn parse_compound_expr_2() { + use self::ASTNode::*; + use self::SQLOperator::*; + let sql = String::from("a * b + c"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + assert_eq!( + SQLBinaryExpr { + left: Box::new(SQLBinaryExpr { + left: Box::new(SQLIdentifier("a".to_string())), + op: Multiply, + right: Box::new(SQLIdentifier("b".to_string())) + }), + op: Plus, + right: Box::new(SQLIdentifier("c".to_string())) + }, + ast + ); +} + +#[test] +fn parse_is_null() { + use self::ASTNode::*; + let sql = String::from("a IS NULL"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + assert_eq!(SQLIsNull(Box::new(SQLIdentifier("a".to_string()))), ast); +} + +#[test] +fn parse_is_not_null() { + use self::ASTNode::*; + let sql = String::from("a IS NOT NULL"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + assert_eq!(SQLIsNotNull(Box::new(SQLIdentifier("a".to_string()))), ast); +} + +#[test] +fn parse_select_order_by() { + let sql = String::from( + "SELECT id, fname, lname FROM customer WHERE id < 5 ORDER BY lname ASC, fname DESC", + ); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + match ast { + ASTNode::SQLSelect { order_by, .. } => { + assert_eq!( + Some(vec![ + SQLOrderByExpr { + expr: Box::new(ASTNode::SQLIdentifier("lname".to_string())), + asc: true, + }, + SQLOrderByExpr { + expr: Box::new(ASTNode::SQLIdentifier("fname".to_string())), + asc: false, + }, + ]), + order_by + ); + } + _ => assert!(false), + } +} + +#[test] +fn parse_select_group_by() { + let sql = String::from("SELECT id, fname, lname FROM customer GROUP BY lname, fname"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + match ast { + ASTNode::SQLSelect { group_by, .. } => { + assert_eq!( + Some(vec![ + ASTNode::SQLIdentifier("lname".to_string()), + ASTNode::SQLIdentifier("fname".to_string()), + ]), + group_by + ); + } + _ => assert!(false), + } +} + +#[test] +fn parse_limit_accepts_all() { + let sql = String::from("SELECT id, fname, lname FROM customer WHERE id = 1 LIMIT ALL"); + let expected = String::from("SELECT id, fname, lname FROM customer WHERE id = 1"); + let ast = parse_sql(&sql); + assert_eq!(expected, ast.to_string()); + match ast { + ASTNode::SQLSelect { + projection, limit, .. + } => { + assert_eq!(3, projection.len()); + assert_eq!(None, limit); + } + _ => assert!(false), + } +} + +#[test] +fn parse_cast() { + let sql = String::from("SELECT CAST(id AS bigint) FROM customer"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + match ast { + ASTNode::SQLSelect { projection, .. } => { + assert_eq!(1, projection.len()); + assert_eq!( + ASTNode::SQLCast { + expr: Box::new(ASTNode::SQLIdentifier("id".to_string())), + data_type: SQLType::BigInt + }, + projection[0] + ); + } + _ => assert!(false), + } +} + +#[test] +fn parse_create_table() { + let sql = String::from( + "CREATE TABLE uk_cities (\ + name VARCHAR(100) NOT NULL,\ + lat DOUBLE NULL,\ + lng DOUBLE NULL)", + ); + let expected = String::from( + "CREATE TABLE uk_cities (\ + name character varying(100) NOT NULL, \ + lat double, \ + lng double)", + ); + let ast = parse_sql(&sql); + assert_eq!(expected, ast.to_string()); + match ast { + ASTNode::SQLCreateTable { name, columns } => { + assert_eq!("uk_cities", name); + assert_eq!(3, columns.len()); + + let c_name = &columns[0]; + assert_eq!("name", c_name.name); + assert_eq!(SQLType::Varchar(Some(100)), c_name.data_type); + assert_eq!(false, c_name.allow_null); + + let c_lat = &columns[1]; + assert_eq!("lat", c_lat.name); + assert_eq!(SQLType::Double, c_lat.data_type); + assert_eq!(true, c_lat.allow_null); + + let c_lng = &columns[2]; + assert_eq!("lng", c_lng.name); + assert_eq!(SQLType::Double, c_lng.data_type); + assert_eq!(true, c_lng.allow_null); + } + _ => assert!(false), + } +} + +#[test] +fn parse_create_table_with_defaults() { + let sql = String::from( + "CREATE TABLE public.customer ( + customer_id integer DEFAULT nextval(public.customer_customer_id_seq) NOT NULL, + store_id smallint NOT NULL, + first_name character varying(45) NOT NULL, + last_name character varying(45) NOT NULL, + email character varying(50), + address_id smallint NOT NULL, + activebool boolean DEFAULT true NOT NULL, + create_date date DEFAULT now()::text NOT NULL, + last_update timestamp without time zone DEFAULT now() NOT NULL, + active integer NOT NULL)"); + let ast = parse_sql(&sql); + match ast { + ASTNode::SQLCreateTable { name, columns } => { + assert_eq!("public.customer", name); + assert_eq!(10, columns.len()); + + let c_name = &columns[0]; + assert_eq!("customer_id", c_name.name); + assert_eq!(SQLType::Int, c_name.data_type); + assert_eq!(false, c_name.allow_null); + + let c_lat = &columns[1]; + assert_eq!("store_id", c_lat.name); + assert_eq!(SQLType::SmallInt, c_lat.data_type); + assert_eq!(false, c_lat.allow_null); + + let c_lng = &columns[2]; + assert_eq!("first_name", c_lng.name); + assert_eq!(SQLType::Varchar(Some(45)), c_lng.data_type); + assert_eq!(false, c_lng.allow_null); + } + _ => assert!(false), + } +} + +#[test] +fn parse_create_table_from_pg_dump() { + let sql = String::from(" + CREATE TABLE public.customer ( + customer_id integer DEFAULT nextval('public.customer_customer_id_seq'::regclass) NOT NULL, + store_id smallint NOT NULL, + first_name character varying(45) NOT NULL, + last_name character varying(45) NOT NULL, + info text[], + address_id smallint NOT NULL, + activebool boolean DEFAULT true NOT NULL, + create_date date DEFAULT now()::date NOT NULL, + create_date1 date DEFAULT 'now'::text::date NOT NULL, + last_update timestamp without time zone DEFAULT now(), + release_year public.year, + active integer + )"); + let ast = parse_sql(&sql); + match ast { + ASTNode::SQLCreateTable { name, columns } => { + assert_eq!("public.customer", name); + + let c_name = &columns[0]; + assert_eq!("customer_id", c_name.name); + assert_eq!(SQLType::Int, c_name.data_type); + assert_eq!(false, c_name.allow_null); + + let c_lat = &columns[1]; + assert_eq!("store_id", c_lat.name); + assert_eq!(SQLType::SmallInt, c_lat.data_type); + assert_eq!(false, c_lat.allow_null); + + let c_lng = &columns[2]; + assert_eq!("first_name", c_lng.name); + assert_eq!(SQLType::Varchar(Some(45)), c_lng.data_type); + assert_eq!(false, c_lng.allow_null); + } + _ => assert!(false), + } +} + +#[test] +fn parse_create_table_with_inherit() { + let sql = String::from("\ + CREATE TABLE bazaar.settings (\ + settings_id uuid PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, \ + user_id uuid UNIQUE, \ + value text[], \ + use_metric boolean DEFAULT true\ + )"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + match ast { + ASTNode::SQLCreateTable { name, columns } => { + assert_eq!("bazaar.settings", name); + + let c_name = &columns[0]; + assert_eq!("settings_id", c_name.name); + assert_eq!(SQLType::Uuid, c_name.data_type); + assert_eq!(false, c_name.allow_null); + assert_eq!(true, c_name.is_primary); + assert_eq!(false, c_name.is_unique); + + let c_name = &columns[1]; + assert_eq!("user_id", c_name.name); + assert_eq!(SQLType::Uuid, c_name.data_type); + assert_eq!(true, c_name.allow_null); + assert_eq!(false, c_name.is_primary); + assert_eq!(true, c_name.is_unique); + } + _ => assert!(false), + } +} + +#[test] +fn parse_alter_table_constraint_primary_key(){ + let sql = String::from("\ + ALTER TABLE bazaar.address \ + ADD CONSTRAINT address_pkey PRIMARY KEY (address_id)"); + let ast = parse_sql(&sql); + println!("ast: {:?}", ast); + assert_eq!(sql, ast.to_string()); + match ast { + ASTNode::SQLAlterTable{ name, operation } => { + assert_eq!(name, "bazaar.address"); + } + _ => assert!(false), + } +} + +#[test] +fn parse_alter_table_constraint_foreign_key(){ + let sql = String::from("\ + ALTER TABLE public.customer \ + ADD CONSTRAINT customer_address_id_fkey FOREIGN KEY (address_id) REFERENCES public.address(address_id)"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + println!("ast: {:?}", ast); + match ast { + ASTNode::SQLAlterTable{ name, operation } => { + assert_eq!(name, "public.customer"); + } + _ => assert!(false), + } +} + +#[test] +fn parse_copy_example(){ + let sql = String::from(r#"COPY public.actor (actor_id, first_name, last_name, last_update, value) FROM stdin; +1 PENELOPE GUINESS 2006-02-15 09:34:33 0.11111 +2 NICK WAHLBERG 2006-02-15 09:34:33 0.22222 +3 ED CHASE 2006-02-15 09:34:33 0.312323 +4 JENNIFER DAVIS 2006-02-15 09:34:33 0.3232 +5 JOHNNY LOLLOBRIGIDA 2006-02-15 09:34:33 1.343 +6 BETTE NICHOLSON 2006-02-15 09:34:33 5.0 +7 GRACE MOSTEL 2006-02-15 09:34:33 6.0 +8 MATTHEW JOHANSSON 2006-02-15 09:34:33 7.0 +9 JOE SWANK 2006-02-15 09:34:33 8.0 +10 CHRISTIAN GABLE 2006-02-15 09:34:33 9.1 +11 ZERO CAGE 2006-02-15 09:34:33 10.001 +12 KARL BERRY 2017-11-02 19:15:42.308637+08 11.001 +A Fateful Reflection of a Waitress And a Boat who must Discover a Sumo Wrestler in Ancient China +Kwara & Kogi +{"Deleted Scenes","Behind the Scenes"} +'awe':5 'awe-inspir':4 'barbarella':1 'cat':13 'conquer':16 'dog':18 'feminist':10 'inspir':6 'monasteri':21 'must':15 'stori':7 'streetcar':2 +PHP ₱ USD $ +\\."#); + let ast = parse_sql(&sql); + //assert_eq!(sql, ast.to_string()); +} + +#[test] +fn parse_timestamps_example(){ + let sql = "2016-02-15 09:43:33"; + let ast = parse_sql(sql); + assert_eq!(sql, ast.to_string()); +} + +#[test] +fn parse_timestamps_with_millis_example(){ + let sql = "2017-11-02 19:15:42.308637"; + let ast = parse_sql(sql); +} + +#[test] +fn parse_example_value(){ + let sql = "SARAH.LEWIS@sakilacustomer.org"; + let ast = parse_sql(sql); + assert_eq!(sql, ast.to_string()); +} + +#[test] +fn parse_scalar_function_in_projection() { + let sql = String::from("SELECT sqrt(id) FROM foo"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + if let ASTNode::SQLSelect { projection, .. } = ast { + assert_eq!( + vec![ASTNode::SQLFunction { + id: String::from("sqrt"), + args: vec![ASTNode::SQLIdentifier(String::from("id"))], + }], + projection + ); + } else { + assert!(false); + } +} + +#[test] +fn parse_aggregate_with_group_by() { + let sql = String::from("SELECT a, COUNT(1), MIN(b), MAX(b) FROM foo GROUP BY a"); + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); +} + +#[test] +fn parse_literal_string() { + let sql = "SELECT 'one'"; + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + match ast { + ASTNode::SQLSelect { ref projection, .. } => { + assert_eq!(projection[0], ASTNode::SQLValue(Value::SingleQuotedString("one".to_string()))); + } + _ => panic!(), + } +} + +#[test] +fn parse_select_version() { + let sql = "SELECT @@version"; + let ast = parse_sql(&sql); + assert_eq!(sql, ast.to_string()); + match ast{ + ASTNode::SQLSelect { ref projection, .. } => { + assert_eq!( + projection[0], + ASTNode::SQLIdentifier("@@version".to_string()) + ); + } + _ => panic!(), + } +} + +#[test] +fn parse_function_now(){ + let sql = "now()"; + let ast = parse_sql(sql); + assert_eq!(sql, ast.to_string()); +} + +fn parse_sql(sql: &str) -> ASTNode { + debug!("sql: {}", sql); + let mut parser = parser(sql); + let ast = parser.parse().unwrap(); + ast +} + +fn parser(sql: &str) -> Parser { + let dialect = PostgreSqlDialect {}; + let mut tokenizer = Tokenizer::new(&dialect, &sql); + let tokens = tokenizer.tokenize().unwrap(); + debug!("tokens: {:#?}", tokens); + Parser::new(tokens) +} +