use serde_json::{json, Value};
use crate::compact;
use crate::grammar::parse_statement;
use crate::json_schema;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
UnsupportedDialect(String),
Parse(String),
Format(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::UnsupportedDialect(d) => write!(
f,
"Unsupported SQL dialect given to parser: '{}'. Please provide 'mysql', 'mariadb' or none to use default.",
d
),
Error::Parse(m) => write!(f, "{}", m),
Error::Format(m) => write!(f, "{}", m),
}
}
}
impl std::error::Error for Error {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct JsonSchemaOptions {
pub use_ref: bool,
}
impl Default for JsonSchemaOptions {
fn default() -> Self {
JsonSchemaOptions { use_ref: true }
}
}
pub struct Parser {
statements: Vec<String>,
remains: String,
escaped: bool,
quoted: Option<char>,
comment: Option<Comment>,
previous_unit: Option<u16>,
second_previous_unit: Option<u16>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Comment {
HashLine,
DashLine,
Block { previous_was_star: bool },
}
impl Parser {
pub fn new(dialect: &str) -> Result<Self, Error> {
if dialect.is_empty() || dialect == "mysql" || dialect == "mariadb" {
Ok(Parser {
statements: Vec::new(),
remains: String::new(),
escaped: false,
quoted: None,
comment: None,
previous_unit: None,
second_previous_unit: None,
})
} else {
Err(Error::UnsupportedDialect(dialect.to_string()))
}
}
pub fn feed(&mut self, chunk: &str) -> &mut Self {
let units: Vec<u16> = chunk.encode_utf16().collect();
let mut parsed: Vec<u16> = Vec::with_capacity(units.len());
let mut last_statement_index = 0usize;
for (i, &unit) in units.iter().enumerate() {
parsed.push(unit);
let ch = char::from_u32(unit as u32);
let previous_unit = self.previous_unit;
let second_previous_unit = self.second_previous_unit;
if let Some(comment) = self.comment {
match comment {
Comment::HashLine | Comment::DashLine => {
if matches!(ch, Some('\n') | Some('\r')) {
self.comment = None;
}
}
Comment::Block { previous_was_star } => {
if unit == b'/' as u16 && previous_was_star {
self.comment = None;
} else {
self.comment = Some(Comment::Block {
previous_was_star: unit == b'*' as u16,
});
}
}
}
self.escaped = false;
} else if ch == Some('\\') {
self.escaped = !self.escaped;
} else {
if !self.escaped && is_quote_char(ch) {
let c = ch.unwrap();
match self.quoted {
Some(open) => {
if open == c {
self.quoted = None;
}
}
None => self.quoted = Some(c),
}
} else if self.quoted.is_none() {
if ch == Some('#') {
self.comment = Some(Comment::HashLine);
} else if second_previous_unit == Some(b'-' as u16)
&& previous_unit == Some(b'-' as u16)
&& unit == b' ' as u16
{
self.comment = Some(Comment::DashLine);
} else if previous_unit == Some(b'/' as u16) && unit == b'*' as u16 {
self.comment = Some(Comment::Block {
previous_was_star: false,
});
} else if ch == Some(';') {
let slice = &parsed[last_statement_index..i + 1];
let statement = format!("{}{}", self.remains, utf16_to_string(slice));
self.statements.push(statement);
self.remains.clear();
last_statement_index = i + 1;
}
}
self.escaped = false;
}
self.second_previous_unit = previous_unit;
self.previous_unit = Some(unit);
}
let tail = &parsed[last_statement_index..];
self.remains.push_str(&utf16_to_string(tail));
self
}
pub fn parse(&mut self) -> Result<Value, Error> {
let mut line_count = 1usize;
let mut results = Vec::new();
let statements = std::mem::take(&mut self.statements);
for statement in statements {
if statement.is_empty() {
break;
}
match parse_statement(&statement) {
Ok(node) => {
line_count += count_line_breaks(&statement);
results.push(node);
}
Err(e) => {
let error_line = e.line;
let new_count = line_count + error_line - 1;
self.statements.clear();
self.remains.clear();
self.escaped = false;
self.quoted = None;
self.comment = None;
self.previous_unit = None;
self.second_previous_unit = None;
return Err(Error::Parse(format!(
"invalid syntax at line {}",
new_count
)));
}
}
}
self.remains.clear();
self.escaped = false;
self.quoted = None;
self.comment = None;
self.previous_unit = None;
self.second_previous_unit = None;
Ok(json!({ "id": "MAIN", "def": results }))
}
pub fn parse_compact(&mut self) -> Result<Vec<Value>, Error> {
let tree = self.parse()?;
compact_from_tree(&tree)
}
pub fn parse_json_schema(&mut self, options: JsonSchemaOptions) -> Result<Vec<Value>, Error> {
let tables = self.parse_compact()?;
Ok(json_schema_from_tables(&tables, options))
}
}
pub fn compact_from_tree(tree: &Value) -> Result<Vec<Value>, Error> {
compact::format(tree).map_err(Error::Format)
}
#[must_use]
pub fn json_schema_from_tables(tables: &[Value], options: JsonSchemaOptions) -> Vec<Value> {
json_schema::format(tables, options)
}
fn is_quote_char(ch: Option<char>) -> bool {
matches!(ch, Some('"') | Some('\'') | Some('`'))
}
fn count_line_breaks(s: &str) -> usize {
let bytes = s.as_bytes();
let mut count = 0;
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\r' => {
if i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
i += 2;
} else {
i += 1;
}
count += 1;
}
b'\n' => {
count += 1;
i += 1;
}
_ => i += 1,
}
}
count
}
fn utf16_to_string(units: &[u16]) -> String {
String::from_utf16_lossy(units)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tracks_escape_quote_and_statements() {
let mut p = Parser::new("mysql").unwrap();
p.feed("a");
assert!(!p.escaped);
assert_eq!(p.quoted, None);
p.feed("\\");
assert!(p.escaped);
assert_eq!(p.quoted, None);
p.feed("\\");
assert!(!p.escaped);
assert_eq!(p.quoted, None);
p.feed("\\");
assert!(p.escaped);
p.feed("n");
assert!(!p.escaped);
assert_eq!(p.quoted, None);
p.feed("\"");
assert!(!p.escaped);
assert_eq!(p.quoted, Some('"'));
p.feed("\"");
assert_eq!(p.quoted, None);
p.feed("\"a");
assert_eq!(p.quoted, Some('"'));
p.feed("a\"");
assert_eq!(p.quoted, None);
p.feed("'");
assert_eq!(p.quoted, Some('\''));
p.feed("'");
assert_eq!(p.quoted, None);
p.feed("'a");
assert_eq!(p.quoted, Some('\''));
p.feed("a'");
assert_eq!(p.quoted, None);
p.feed("`");
assert_eq!(p.quoted, Some('`'));
p.feed("`");
assert_eq!(p.quoted, None);
p.feed("`a");
assert_eq!(p.quoted, Some('`'));
p.feed("a`");
assert_eq!(p.quoted, None);
p.feed("`\\`");
assert!(!p.escaped);
assert_eq!(p.quoted, Some('`'));
p.feed("\\\\`");
assert_eq!(p.quoted, None);
p.feed("\"\\");
assert!(p.escaped);
assert_eq!(p.quoted, Some('"'));
p.feed("\"a`");
assert_eq!(p.quoted, Some('"'));
p.feed("'\\'");
assert_eq!(p.quoted, Some('"'));
p.feed("\"");
assert_eq!(p.quoted, None);
p.feed("\\;");
assert!(!p.escaped);
assert_eq!(p.quoted, None);
assert_eq!(p.statements.len(), 1);
p.feed("a;");
assert_eq!(p.statements.len(), 2);
p.feed("a\";");
assert_eq!(p.quoted, Some('"'));
assert_eq!(p.statements.len(), 2);
p.feed("a\";");
assert_eq!(p.quoted, None);
assert_eq!(p.statements.len(), 3);
}
#[test]
fn splits_on_utf16_code_units() {
let mut p = Parser::new("mysql").unwrap();
p.feed("USE \u{1F600};USE b;");
assert_eq!(p.statements.len(), 2);
}
#[test]
fn semicolon_in_block_comment_does_not_split_statement() {
let mut p = Parser::new("mysql").unwrap();
p.feed("CREATE TABLE c (id INT /* ; */);");
let tables = p.parse_compact().unwrap();
assert_eq!(tables[0]["name"], "c");
}
#[test]
fn comment_semicolons_do_not_split_at_chunk_boundaries() {
let scenarios: &[&[&str]] = &[
&["CREATE TABLE c (id INT /", "* ; *", "/);"],
&["CREATE TABLE c (id INT) -", "- ;\n;"],
&["CREATE TABLE c (id INT /*/ ; */);"],
&["CREATE TABLE c (id INT /* ; *", "/);"],
];
for chunks in scenarios {
let mut p = Parser::new("mysql").unwrap();
for chunk in *chunks {
p.feed(chunk);
}
let tables = p.parse_compact().unwrap();
assert_eq!(tables[0]["name"], "c");
}
}
}