Skip to content

Commit

Permalink
Ignore case and quote
Browse files Browse the repository at this point in the history
  • Loading branch information
gwenn committed May 5, 2024
1 parent d5bb557 commit 4bb1464
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 69 deletions.
4 changes: 2 additions & 2 deletions src/lexer/sql/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ fn duplicate_column() {
);
expect_parser_err_msg(
b"CREATE TABLE t (x TEXT, \"x\" TEXT)",
"duplicate column name: x",
"duplicate column name: \"x\"",
);
expect_parser_err_msg(
b"CREATE TABLE t (x TEXT, `x` TEXT)",
"duplicate column name: x",
"duplicate column name: `x`",
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/parser/ast/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ impl Stmt {
} => {
if *temporary {
if let Some(ref db_name) = tbl_name.db_name {
if Uncased::from_borrowed("TEMP") != db_name.0 {
if db_name != "TEMP" {
return Err(custom_err!("temporary table name must be unqualified"));
}
}
Expand Down
54 changes: 0 additions & 54 deletions src/parser/ast/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2035,57 +2035,3 @@ fn double_quote<S: TokenStream>(name: &str, s: &mut S) -> Result<(), S::Error> {
f.write_char('"')*/
s.append(TK_ID, Some(name))
}

/// Convert an SQL-style quoted string into a normal string by removing
/// the quote characters.
pub fn dequote(n: Name) -> Result<Name, ParserError> {
let s = n.0.as_str();
if s.is_empty() {
return Ok(n);
}
let mut quote = s.chars().next().unwrap();
if quote != '"' && quote != '`' && quote != '\'' && quote != '[' {
return Ok(n);
} else if quote == '[' {
quote = ']';
}
debug_assert!(s.len() > 1);
debug_assert!(s.ends_with(quote));
let sub = &s[1..s.len() - 1];
let mut z = String::with_capacity(sub.len());
let mut escaped = false;
for c in sub.chars() {
if escaped {
if c != quote {
return Err(custom_err!("Malformed string literal: {}", s));
}
escaped = false;
} else if c == quote {
escaped = true;
continue;
}
z.push(c);
}
Ok(Name(Uncased::from_owned(z)))
}

#[cfg(test)]
mod test {
use super::{dequote, Name, ParserError};
use uncased::Uncased;

#[test]
fn test_dequote() -> Result<(), ParserError> {
assert_eq!(dequote(name("x"))?, name("x"));
assert_eq!(dequote(name("`x`"))?, name("x"));
assert_eq!(dequote(name("`x``y`"))?, name("x`y"));
assert_eq!(dequote(name(r#""x""#))?, name("x"));
assert_eq!(dequote(name(r#""x""y""#))?, name("x\"y"));
assert_eq!(dequote(name("[x]"))?, name("x"));
Ok(())
}

fn name(s: &'static str) -> Name {
Name(Uncased::from_borrowed(s))
}
}
124 changes: 115 additions & 9 deletions src/parser/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ pub mod fmt;

use std::num::ParseIntError;
use std::ops::Deref;
use std::str::FromStr;
use std::str::{Bytes, FromStr};

use fmt::{dequote, ToTokens, TokenStream};
use fmt::{ToTokens, TokenStream};
use indexmap::{IndexMap, IndexSet};
use uncased::Uncased;

use crate::custom_err;
use crate::dialect::TokenType::{self, *};
Expand Down Expand Up @@ -991,13 +990,101 @@ impl Id {
// TODO ids (identifier or string)

/// identifier or string or `CROSS` or `FULL` or `INNER` or `LEFT` or `NATURAL` or `OUTER` or `RIGHT`.
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Name(pub Uncased<'static>); // TODO distinction between Name and "Name"/[Name]/`Name`
#[derive(Clone, Debug, Eq)]
pub struct Name(pub String); // TODO distinction between Name and "Name"/[Name]/`Name`

impl Name {
/// Constructor
pub fn from_token(ty: YYCODETYPE, token: Token) -> Name {
Name(Uncased::from_owned(from_token(ty, token)))
Name(from_token(ty, token))
}

fn as_bytes(&self) -> QuotedIterator<'_> {
if self.0.is_empty() {
return QuotedIterator(self.0.bytes(), 0);
}
let bytes = self.0.as_bytes();
let mut quote = bytes[0];
if quote != b'"' && quote != b'`' && quote != b'\'' && quote != b'[' {
return QuotedIterator(self.0.bytes(), 0);
} else if quote == b'[' {
quote = b']';
}
debug_assert!(bytes.len() > 1);
debug_assert_eq!(quote, bytes[bytes.len() - 1]);
let sub = &self.0.as_str()[1..bytes.len() - 1];
if quote == b']' {
return QuotedIterator(sub.bytes(), 0); // no escape
}
QuotedIterator(sub.bytes(), quote)
}
}

struct QuotedIterator<'s>(Bytes<'s>, u8);
impl<'s> Iterator for QuotedIterator<'s> {
type Item = u8;

fn next(&mut self) -> Option<u8> {
match self.0.next() {
x @ Some(b) => {
if b == self.1 && self.0.next() != Some(self.1) {
panic!("Malformed string literal: {:?}", self.0);
}
x
}
x => x,
}
}

fn size_hint(&self) -> (usize, Option<usize>) {
if self.1 == 0 {
return self.0.size_hint();
}
(0, None)
}
}

fn eq_ignore_case_and_quote(mut it: QuotedIterator<'_>, mut other: QuotedIterator<'_>) -> bool {
loop {
match (it.next(), other.next()) {
(Some(b1), Some(b2)) => {
if !b1.eq_ignore_ascii_case(&b2) {
return false;
}
}
(None, None) => break,
_ => return false,
}
}
true
}

/// Ignore case and quote
impl std::hash::Hash for Name {
fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
self.as_bytes()
.for_each(|b| hasher.write_u8(b.to_ascii_lowercase()));
}
}
/// Ignore case and quote
impl PartialEq for Name {
#[inline(always)]
fn eq(&self, other: &Name) -> bool {
eq_ignore_case_and_quote(self.as_bytes(), other.as_bytes())
}
}
/// Ignore case and quote
impl PartialEq<str> for Name {
#[inline(always)]
fn eq(&self, other: &str) -> bool {
eq_ignore_case_and_quote(self.as_bytes(), QuotedIterator(other.bytes(), 0u8))
}
}
/// Ignore case and quote
impl PartialEq<&str> for Name {
#[inline(always)]
fn eq(&self, other: &&str) -> bool {
eq_ignore_case_and_quote(self.as_bytes(), QuotedIterator(other.bytes(), 0u8))
}
}

Expand Down Expand Up @@ -1151,8 +1238,8 @@ impl ColumnDefinition {
columns: &mut IndexMap<Name, ColumnDefinition>,
mut cd: ColumnDefinition,
) -> Result<(), ParserError> {
let col_name = dequote(cd.col_name.clone())?;
if columns.contains_key(&col_name) {
let col_name = &cd.col_name;
if columns.contains_key(col_name) {
// TODO unquote
return Err(custom_err!("duplicate column name: {}", col_name));
}
Expand Down Expand Up @@ -1202,7 +1289,7 @@ impl ColumnDefinition {
}
}
}
columns.insert(col_name, cd);
columns.insert(col_name.clone(), cd);
Ok(())
}
}
Expand Down Expand Up @@ -1767,3 +1854,22 @@ pub enum FrameExclude {
/// `TIES`
Ties,
}

#[cfg(test)]
mod test {
use super::Name;

#[test]
fn test_dequote() {
assert_eq!(name("x"), "x");
assert_eq!(name("`x`"), "x");
assert_eq!(name("`x``y`"), "x`y");
assert_eq!(name(r#""x""#), "x");
assert_eq!(name(r#""x""y""#), "x\"y");
assert_eq!(name("[x]"), "x");
}

fn name(s: &'static str) -> Name {
Name(s.to_owned())
}
}
5 changes: 2 additions & 3 deletions src/parser/parse.y
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ use crate::parser::{Context, ParserError};
use crate::dialect::{from_token, Token, TokenType};
use indexmap::IndexMap;
use log::{debug, error, log_enabled};
use uncased::Uncased;

#[allow(non_camel_case_types)]
type sqlite3ParserError = crate::parser::ParserError;
Expand Down Expand Up @@ -144,15 +143,15 @@ table_option_set(A) ::= table_option(A).
table_option_set(A) ::= table_option_set(X) COMMA table_option(Y). {A = X|Y;}
table_option(A) ::= WITHOUT nm(X). {
let option = X;
if Uncased::from_borrowed("rowid") == option.0 {
if option == "rowid" {
A = TableOptions::WITHOUT_ROWID;
}else{
return Err(custom_err!("unknown table option: {}", option));
}
}
table_option(A) ::= nm(X). {
let option = X;
if Uncased::from_borrowed("strict") == option.0 {
if option == "strict" {
A = TableOptions::STRICT;
}else{
return Err(custom_err!("unknown table option: {}", option));
Expand Down

0 comments on commit 4bb1464

Please sign in to comment.