diff --git a/src/sqlast/mod.rs b/src/sqlast/mod.rs index eef10fcdb..d5a2d7874 100644 --- a/src/sqlast/mod.rs +++ b/src/sqlast/mod.rs @@ -111,7 +111,7 @@ impl ToString for ASTNode { match self { ASTNode::SQLIdentifier(s) => s.to_string(), ASTNode::SQLWildcard => "*".to_string(), - ASTNode::SQLQualifiedWildcard(q) => q.join(".") + "*", + ASTNode::SQLQualifiedWildcard(q) => q.join(".") + ".*", ASTNode::SQLCompoundIdentifier(s) => s.join("."), ASTNode::SQLIsNull(ast) => format!("{} IS NULL", ast.as_ref().to_string()), ASTNode::SQLIsNotNull(ast) => format!("{} IS NOT NULL", ast.as_ref().to_string()), diff --git a/src/sqlast/value.rs b/src/sqlast/value.rs index a061080a8..eedb14a93 100644 --- a/src/sqlast/value.rs +++ b/src/sqlast/value.rs @@ -35,7 +35,7 @@ impl ToString for Value { Value::Long(v) => v.to_string(), Value::Double(v) => v.to_string(), Value::Uuid(v) => v.to_string(), - Value::SingleQuotedString(v) => format!("'{}'", v), + Value::SingleQuotedString(v) => format!("'{}'", escape_single_quote_string(v)), Value::NationalStringLiteral(v) => format!("N'{}'", v), Value::Boolean(v) => v.to_string(), Value::Date(v) => v.to_string(), @@ -46,3 +46,15 @@ impl ToString for Value { } } } + +fn escape_single_quote_string(s: &str) -> String { + let mut escaped = String::new(); + for c in s.chars() { + if c == '\'' { + escaped.push_str("\'\'"); + } else { + escaped.push(c); + } + } + escaped +} diff --git a/src/sqltokenizer.rs b/src/sqltokenizer.rs index 83105736c..64faecabf 100644 --- a/src/sqltokenizer.rs +++ b/src/sqltokenizer.rs @@ -462,7 +462,13 @@ impl<'a> Tokenizer<'a> { match ch { '\'' => { chars.next(); // consume - break; + let escaped_quote = chars.peek().map(|c| *c == '\'').unwrap_or(false); + if escaped_quote { + s.push('\''); + chars.next(); + } else { + break; + } } _ => { chars.next(); // consume diff --git a/tests/sqlparser_generic.rs b/tests/sqlparser_generic.rs index ea60e6d47..8481797da 100644 --- a/tests/sqlparser_generic.rs +++ b/tests/sqlparser_generic.rs @@ -91,6 +91,13 @@ fn parse_select_wildcard() { ); } +#[test] +fn parse_count_wildcard() { + verified_only_select( + "SELECT COUNT(Employee.*) FROM Order JOIN Employee ON Order.employee = Employee.id", + ); +} + #[test] fn parse_column_aliases() { let sql = "SELECT a.col + 1 AS newname FROM foo AS a"; @@ -148,6 +155,25 @@ fn parse_projection_nested_type() { //TODO: add assertions } +#[test] +fn parse_escaped_single_quote_string_predicate() { + use self::ASTNode::*; + use self::SQLOperator::*; + let sql = "SELECT id, fname, lname FROM customer \ + WHERE salary != 'Jim''s salary'"; + let ast = verified_only_select(sql); + assert_eq!( + Some(SQLBinaryExpr { + left: Box::new(SQLIdentifier("salary".to_string())), + op: NotEq, + right: Box::new(SQLValue(Value::SingleQuotedString( + "Jim's salary".to_string() + ))) + }), + ast.selection, + ); +} + #[test] fn parse_compound_expr_1() { use self::ASTNode::*;