diff --git a/crates/postgresql-cst-parser/src/cst.rs b/crates/postgresql-cst-parser/src/cst.rs index 8c6566b..b78d864 100644 --- a/crates/postgresql-cst-parser/src/cst.rs +++ b/crates/postgresql-cst-parser/src/cst.rs @@ -1,3 +1,11 @@ +pub(crate) mod extra; +pub(crate) mod lr_parse_state; + +pub(crate) use extra::*; +pub(crate) use lr_parse_state::*; + +use std::collections::HashSet; + use cstree::{ build::GreenNodeBuilder, green::GreenNode, interning::Resolver, RawSyntaxKind, Syntax, }; @@ -10,18 +18,25 @@ use crate::{ rule_name_to_component_id, token_kind_to_component_id, Action, ACTION_TABLE, GOTO_TABLE, RULES, }, + transform::{ParseTransform, ParseTransformer}, }; use super::{lexer::Token, syntax_kind::SyntaxKind}; -struct Node { +pub(crate) struct Node { token: Option, - component_id: u32, + pub component_id: u32, children: Vec, start_byte_pos: usize, end_byte_pos: usize, } +impl From<&Node> for SyntaxKind { + fn from(value: &Node) -> Self { + SyntaxKind::from_raw(RawSyntaxKind(value.component_id)) + } +} + pub type PostgreSQLSyntax = SyntaxKind; impl From for cstree::RawSyntaxKind { @@ -55,21 +70,30 @@ impl Parser { fn parse_rec( &mut self, node: &Node, - peekable: &mut std::iter::Peekable>, + peekable: &mut std::iter::Peekable>, + complement_token: &HashSet, ) { if cfg!(feature = "remove-empty-node") { - if node.start_byte_pos == node.end_byte_pos { + if node.start_byte_pos == node.end_byte_pos + && !complement_token.contains(&node.start_byte_pos) + { return; } } - while let Some((kind, start, _, text)) = peekable.peek() { + while let Some(Extra { + kind, + start_byte_pos, + comment, + .. + }) = peekable.peek() + { // TODO: Consider whether the presence or absence of an equals sign changes the position of comments. Determine which option is preferable - if *start >= node.start_byte_pos { + if *start_byte_pos >= node.start_byte_pos { // if *start > node.start_byte_pos { break; } - self.builder.token(*kind, text); + self.builder.token(*kind, &comment); peekable.next(); } @@ -80,7 +104,7 @@ impl Parser { self.builder.start_node(kind); node.children .iter() - .for_each(|c| self.parse_rec(c, peekable)); + .for_each(|c| self.parse_rec(c, peekable, complement_token)); self.builder.finish_node(); } } @@ -88,18 +112,19 @@ impl Parser { fn parse( mut self, nodes: &Vec<&Node>, - extras: Vec<(SyntaxKind, usize, usize, &str)>, + extras: Vec, + complement_token: &HashSet, ) -> (GreenNode, impl Resolver) { let mut peekable = extras.into_iter().peekable(); self.builder.start_node(SyntaxKind::Root); for node in nodes { - self.parse_rec(node, &mut peekable); + self.parse_rec(node, &mut peekable, complement_token); } - while let Some((kind, _, _, text)) = peekable.peek() { - self.builder.token(*kind, text); + while let Some(Extra { kind, comment, .. }) = peekable.peek() { + self.builder.token(*kind, comment); peekable.next(); } @@ -184,6 +209,14 @@ fn init_tokens(tokens: &mut [Token]) { /// Parsing a string as PostgreSQL syntax and converting it into a ResolvedNode pub fn parse(input: &str) -> Result { + parse_with_transformer(input, &[]) +} + +/// Parsing a string as PostgreSQL syntax and converting it into a ResolvedNode +pub fn parse_with_transformer( + input: &str, + transformers: &[&dyn ParseTransformer], +) -> Result { let mut tokens = lex(input); if !tokens.is_empty() { @@ -217,12 +250,13 @@ pub fn parse(input: &str) -> Result { )); let mut last_pos = 0; - let mut extras: Vec<(SyntaxKind, usize, usize, &str)> = Vec::new(); + let mut extras: Vec = Vec::new(); + let mut complement_token = HashSet::new(); loop { let state = stack.last().unwrap().0; - let token = match tokens.peek() { - Some(token) => token, + let mut token = match tokens.peek() { + Some(token) => token.clone(), None => { return Err(ParseError { message: "unexpected end of input".to_string(), @@ -232,39 +266,105 @@ pub fn parse(input: &str) -> Result { } }; - let cid = token_kind_to_component_id(&token.kind); + let mut cid = token_kind_to_component_id(&token.kind); if matches!(token.kind, TokenKind::C_COMMENT | TokenKind::SQL_COMMENT) { if last_pos < token.start_byte_pos { - extras.push(( - SyntaxKind::Whitespace, - last_pos, - token.start_byte_pos, - &input[last_pos..token.start_byte_pos], - )); + extras.push(Extra { + kind: SyntaxKind::Whitespace, + start_byte_pos: last_pos, + end_byte_pos: token.start_byte_pos, + comment: &input[last_pos..token.start_byte_pos], + }); } last_pos = token.end_byte_pos; let kind = SyntaxKind::from_raw(RawSyntaxKind(cid)); - extras.push(( + extras.push(Extra { kind, - token.start_byte_pos, - token.end_byte_pos, - &input[token.start_byte_pos..token.end_byte_pos], - )); + start_byte_pos: token.start_byte_pos, + end_byte_pos: token.end_byte_pos, + comment: &input[token.start_byte_pos..token.end_byte_pos], + }); tokens.next(); continue; } - let action = match action_table[(state * num_terminal_symbol() + cid) as usize] { + let mut insert_dummy_token = false; + let mut action = match action_table[(state * num_terminal_symbol() + cid) as usize] { 0x7FFF => Action::Error, v if v > 0 => Action::Shift((v - 1) as usize), v if v < 0 => Action::Reduce((-v - 1) as usize), _ => Action::Accept, }; + // transform + { + let lr_parse_state = LRParseState { + state, + stack: &stack, + action_table, + goto_table, + extras: &extras, + token: &token, + }; + + if let Some(parse_transform) = transformers + .iter() + .find_map(|t| t.transform(&lr_parse_state)) + { + match parse_transform { + ParseTransform::InsertToken(token_kind) => { + let last_extra = extras.last().unwrap(); + + cid = token_kind_to_component_id(&token_kind); + token = Token { + start_byte_pos: last_extra.end_byte_pos, + end_byte_pos: last_extra.end_byte_pos, + kind: token_kind, + value: String::new(), + }; + complement_token.insert(token.start_byte_pos); + + action = match action_table[(state * num_terminal_symbol() + cid) as usize] + { + 0x7FFF => Action::Error, + v if v > 0 => Action::Shift((v - 1) as usize), + v if v < 0 => Action::Reduce((-v - 1) as usize), + _ => Action::Accept, + }; + insert_dummy_token = true; + } + + ParseTransform::SkipToken => { + // Skip tokens are treated as extras + if last_pos < token.start_byte_pos { + extras.push(Extra { + kind: SyntaxKind::Whitespace, + start_byte_pos: last_pos, + end_byte_pos: token.start_byte_pos, + comment: &input[last_pos..token.start_byte_pos], + }); + } + + last_pos = token.end_byte_pos; + + let kind = SyntaxKind::from_raw(RawSyntaxKind(cid)); + extras.push(Extra { + kind, + start_byte_pos: token.start_byte_pos, + end_byte_pos: token.end_byte_pos, + comment: &input[token.start_byte_pos..token.end_byte_pos], + }); + tokens.next(); + continue; + } + } + } + } + match action { Action::Shift(next_state) => { let node = Node { @@ -276,18 +376,20 @@ pub fn parse(input: &str) -> Result { }; if last_pos < token.start_byte_pos { - extras.push(( - SyntaxKind::Whitespace, - last_pos, - token.start_byte_pos, - &input[last_pos..token.start_byte_pos], - )); + extras.push(Extra { + kind: SyntaxKind::Whitespace, + start_byte_pos: last_pos, + end_byte_pos: token.start_byte_pos, + comment: &input[last_pos..token.start_byte_pos], + }); } last_pos = token.end_byte_pos; stack.push((next_state as u32, node)); - tokens.next(); + if !insert_dummy_token { + tokens.next(); + } } Action::Reduce(rule_index) => { let rule = &RULES[rule_index]; @@ -308,7 +410,7 @@ pub fn parse(input: &str) -> Result { // Adopt the larger of the end position of the previous token or the end of the space. extras .last() - .map(|e| e.2) + .map(|e| e.end_byte_pos) .unwrap_or_default() .max(stack.last().unwrap().1.end_byte_pos) }); @@ -364,12 +466,12 @@ pub fn parse(input: &str) -> Result { while let Some(token) = tokens.next() { if last_pos < token.start_byte_pos { - extras.push(( - SyntaxKind::Whitespace, - last_pos, - token.start_byte_pos, - &input[last_pos..token.start_byte_pos], - )); + extras.push(Extra { + kind: SyntaxKind::Whitespace, + start_byte_pos: last_pos, + end_byte_pos: token.start_byte_pos, + comment: &input[last_pos..token.start_byte_pos], + }); } last_pos = token.end_byte_pos; @@ -381,19 +483,19 @@ pub fn parse(input: &str) -> Result { let cid = token_kind_to_component_id(&token.kind); let kind = SyntaxKind::from_raw(RawSyntaxKind(cid)); - extras.push(( + extras.push(Extra { kind, - token.start_byte_pos, - token.end_byte_pos, - &input[token.start_byte_pos..token.end_byte_pos], - )); + start_byte_pos: token.start_byte_pos, + end_byte_pos: token.end_byte_pos, + comment: &input[token.start_byte_pos..token.end_byte_pos], + }); } let parser = Parser { builder: GreenNodeBuilder::new(), }; let root: Vec<&Node> = stack[1..].iter().map(|s| &s.1).collect(); - let (ast, resolver) = parser.parse(&root, extras); + let (ast, resolver) = parser.parse(&root, extras, &complement_token); Ok(SyntaxNode::new_root_with_resolver(ast, resolver)) } diff --git a/crates/postgresql-cst-parser/src/cst/extra.rs b/crates/postgresql-cst-parser/src/cst/extra.rs new file mode 100644 index 0000000..4172c05 --- /dev/null +++ b/crates/postgresql-cst-parser/src/cst/extra.rs @@ -0,0 +1,9 @@ +use crate::syntax_kind::SyntaxKind; + +#[derive(Debug)] +pub(crate) struct Extra<'a> { + pub(crate) kind: SyntaxKind, + pub(crate) start_byte_pos: usize, + pub(crate) end_byte_pos: usize, + pub(crate) comment: &'a str, +} diff --git a/crates/postgresql-cst-parser/src/cst/lr_parse_state.rs b/crates/postgresql-cst-parser/src/cst/lr_parse_state.rs new file mode 100644 index 0000000..09c9307 --- /dev/null +++ b/crates/postgresql-cst-parser/src/cst/lr_parse_state.rs @@ -0,0 +1,50 @@ +use crate::{lexer::Token, syntax_kind::SyntaxKind}; + +use super::{Extra, Node}; + +#[allow(dead_code)] +pub(crate) struct LRParseState<'a> { + pub(crate) state: u32, + pub(crate) stack: &'a [(u32, Node)], + pub(crate) action_table: &'a [i16], + pub(crate) goto_table: &'a [i16], + pub(crate) extras: &'a [Extra<'a>], + pub(crate) token: &'a Token, +} + +impl<'a> LRParseState<'a> { + // Determine whether the previous C comment and the token to be processed are adjacent + pub fn adjacent_c_comment(&self) -> bool { + match self.extras.last() { + Some(e) + if e.end_byte_pos != self.token.start_byte_pos + && e.kind == SyntaxKind::C_COMMENT => + { + true + } + _ => false, + } + } + + pub fn previous_extra(&self) -> Option<&Extra> { + let Some(last_extra) = self.extras.last() else { + return None; + }; + + let stack_end_byte_pos = self + .stack + .last() + .map(|(_, node)| node.end_byte_pos) + .unwrap_or(0); + + if last_extra.end_byte_pos > stack_end_byte_pos { + Some(last_extra) + } else { + None + } + } + + pub fn last_syntax_kind(&self) -> Option { + self.stack.last().map(|(_, node)| node.into()) + } +} diff --git a/crates/postgresql-cst-parser/src/lexer.rs b/crates/postgresql-cst-parser/src/lexer.rs index 86dd4c7..9d004ed 100644 --- a/crates/postgresql-cst-parser/src/lexer.rs +++ b/crates/postgresql-cst-parser/src/lexer.rs @@ -11,6 +11,7 @@ use self::generated::{RuleKind, State}; pub const NAMEDATALEN: usize = 64; +#[allow(dead_code)] #[derive(Debug, Clone)] pub enum Yylval { Str(String), diff --git a/crates/postgresql-cst-parser/src/lib.rs b/crates/postgresql-cst-parser/src/lib.rs index 634afc9..1a096c3 100644 --- a/crates/postgresql-cst-parser/src/lib.rs +++ b/crates/postgresql-cst-parser/src/lib.rs @@ -6,10 +6,12 @@ mod parser; mod cst; pub mod syntax_kind; +mod transform; #[cfg(feature = "tree-sitter-like")] pub mod tree_sitter; +use cst::parse_with_transformer; pub use cst::NodeOrToken; pub use cst::ParseError; pub use cst::PostgreSQLSyntax; @@ -20,11 +22,37 @@ pub use cst::SyntaxElementRef; pub use cst::SyntaxNode; pub use cst::SyntaxToken; +use transform::ComplementMissingFromTableTransformer; +use transform::ComplementMissingSampleValueTransformer; +use transform::ParseTransformer; +use transform::SkipExtraComma; +use transform::SkipExtraOperator; pub use tree_sitter::parse as ts_parse; pub fn parse(input: &str) -> Result { cst::parse(input) } -#[cfg(test)] -mod tests {} +/// Corrects and parses the following syntax errors found in 2Way SQL +/// 1. Missing sample values ​​when specifying a table name in the from clause as a replacement string +/// 2. Missing sample values ​​in expressions found in select clauses, etc. +/// 3. Extra commas in select clauses, from clauses, and order by clauses +/// 4. Extra and/or in the where clause +pub fn parse_2way(input: &str) -> Result { + parse_with_transformer( + input, + &[ + &ComplementMissingFromTableTransformer, + &ComplementMissingSampleValueTransformer, + &SkipExtraComma, + &SkipExtraOperator, + ], + ) +} + +pub fn parse_2way_with_transformers( + input: &str, + transformers: &[&dyn ParseTransformer], +) -> Result { + parse_with_transformer(input, transformers) +} diff --git a/crates/postgresql-cst-parser/src/transform.rs b/crates/postgresql-cst-parser/src/transform.rs new file mode 100644 index 0000000..c852ee2 --- /dev/null +++ b/crates/postgresql-cst-parser/src/transform.rs @@ -0,0 +1,20 @@ +pub(crate) mod missing_from_table; +pub(crate) mod missing_sample_value; +pub(crate) mod skip_extra_comma; +pub(crate) mod skip_extra_operator; + +pub use missing_from_table::*; +pub use missing_sample_value::*; +pub use skip_extra_comma::*; +pub use skip_extra_operator::*; + +use crate::{cst::LRParseState, lexer::TokenKind, parser::num_terminal_symbol}; + +pub enum ParseTransform { + InsertToken(TokenKind), + SkipToken, +} + +pub trait ParseTransformer { + fn transform<'a>(&self, lr_parse_state: &LRParseState<'a>) -> Option; +} diff --git a/crates/postgresql-cst-parser/src/transform/missing_from_table.rs b/crates/postgresql-cst-parser/src/transform/missing_from_table.rs new file mode 100644 index 0000000..a3094e0 --- /dev/null +++ b/crates/postgresql-cst-parser/src/transform/missing_from_table.rs @@ -0,0 +1,74 @@ +use cstree::{RawSyntaxKind, Syntax}; + +use crate::{lexer::TokenKind, syntax_kind::SyntaxKind}; + +use super::{num_terminal_symbol, LRParseState, ParseTransform, ParseTransformer}; + +/// Complete missing replacement string sample values ​​(FROM clause only) +pub struct ComplementMissingFromTableTransformer; + +fn is_replacement_string_comment(comment: &str) -> bool { + comment.starts_with("/*#") && comment.ends_with("*/") +} + +fn is_missing_replacement_string_comment<'a>(lr_parse_state: &LRParseState<'a>) -> bool { + let Some(extra) = lr_parse_state.previous_extra() else { + return false; + }; + + if !is_replacement_string_comment(extra.comment) { + return false; + } + + // If there is a space after the comment immediately following the replacement string, the table name that should be there is omitted. + extra.end_byte_pos != lr_parse_state.token.start_byte_pos +} + +fn is_from_table<'a>(lr_parse_state: &LRParseState<'a>) -> bool { + match SyntaxKind::from_raw(RawSyntaxKind( + lr_parse_state.stack.last().unwrap().1.component_id, + )) { + SyntaxKind::FROM => true, + SyntaxKind::Comma => { + let prev2_kind = lr_parse_state + .stack + .iter() + .nth_back(1) + .map(|(_, node)| SyntaxKind::from_raw(RawSyntaxKind(node.component_id))); + + if prev2_kind == Some(SyntaxKind::from_list) { + true + } else { + false + } + } + _ => false, + } +} + +fn is_missing_from_replacement_value<'a>(lr_parse_state: &LRParseState<'a>) -> bool { + if is_from_table(lr_parse_state) { + // Check if IDENT is in SHIFT enabled state + let action_index = + (lr_parse_state.state * num_terminal_symbol()) as usize + SyntaxKind::IDENT as usize; + + let a = lr_parse_state.action_table[action_index]; + a != 0x7FFF + } else { + false + } +} + +impl ParseTransformer for ComplementMissingFromTableTransformer { + fn transform<'a>(&self, lr_parse_state: &LRParseState<'a>) -> Option { + if !is_missing_replacement_string_comment(lr_parse_state) { + return None; + } + + if !is_missing_from_replacement_value(&lr_parse_state) { + return None; + } + + Some(ParseTransform::InsertToken(TokenKind::IDENT)) + } +} diff --git a/crates/postgresql-cst-parser/src/transform/missing_sample_value.rs b/crates/postgresql-cst-parser/src/transform/missing_sample_value.rs new file mode 100644 index 0000000..68154d9 --- /dev/null +++ b/crates/postgresql-cst-parser/src/transform/missing_sample_value.rs @@ -0,0 +1,46 @@ +use crate::{cst::Extra, lexer::TokenKind, syntax_kind::SyntaxKind}; + +use super::{num_terminal_symbol, LRParseState, ParseTransform, ParseTransformer}; + +/// Complete missing bind variable sample values +pub struct ComplementMissingSampleValueTransformer; + +impl ComplementMissingSampleValueTransformer { + fn is_bind_variable_comment(s: impl AsRef) -> bool { + let s = s.as_ref(); + s.starts_with("/*") + && s.ends_with("*/") + && !s.contains('\n') + && !matches!(s.chars().nth(2).unwrap(), '$' | '#') + } + + fn is_missing_bind_variable<'a>(lr_parse_state: &LRParseState<'a>) -> bool { + let Some(Extra { comment, .. }) = lr_parse_state.previous_extra() else { + return false; + }; + + if !Self::is_bind_variable_comment(comment) { + return false; + } + + let action_index = + (lr_parse_state.state * num_terminal_symbol()) as usize + SyntaxKind::SCONST as usize; + + let a = lr_parse_state.action_table[action_index]; + a != 0x7FFF + } +} + +impl ParseTransformer for ComplementMissingSampleValueTransformer { + fn transform<'a>(&self, lr_parse_state: &LRParseState<'a>) -> Option { + if !lr_parse_state.adjacent_c_comment() { + return None; + } + + if !Self::is_missing_bind_variable(&lr_parse_state) { + return None; + } + + Some(ParseTransform::InsertToken(TokenKind::SCONST)) + } +} diff --git a/crates/postgresql-cst-parser/src/transform/skip_extra_comma.rs b/crates/postgresql-cst-parser/src/transform/skip_extra_comma.rs new file mode 100644 index 0000000..9cb81e5 --- /dev/null +++ b/crates/postgresql-cst-parser/src/transform/skip_extra_comma.rs @@ -0,0 +1,29 @@ +use crate::{lexer::TokenKind, syntax_kind::SyntaxKind}; + +use super::{LRParseState, ParseTransform, ParseTransformer}; + +/// Skip extra commas at the beginning of SELECT, FROM, and ORDER BY clauses +pub struct SkipExtraComma; + +impl SkipExtraComma { + fn allow_extra_comma<'a>(lr_parse_state: &LRParseState<'a>) -> bool { + match lr_parse_state.last_syntax_kind() { + Some(SyntaxKind::FROM) | Some(SyntaxKind::BY) | Some(SyntaxKind::SELECT) => true, + _ => false, + } + } +} + +impl ParseTransformer for SkipExtraComma { + fn transform<'a>(&self, lr_parse_state: &LRParseState<'a>) -> Option { + if !Self::allow_extra_comma(&lr_parse_state) { + return None; + } + + if lr_parse_state.token.kind != TokenKind::RAW(",".to_string()) { + return None; + } + + Some(ParseTransform::SkipToken) + } +} diff --git a/crates/postgresql-cst-parser/src/transform/skip_extra_operator.rs b/crates/postgresql-cst-parser/src/transform/skip_extra_operator.rs new file mode 100644 index 0000000..569ac2b --- /dev/null +++ b/crates/postgresql-cst-parser/src/transform/skip_extra_operator.rs @@ -0,0 +1,25 @@ +use crate::{lexer::TokenKind, syntax_kind::SyntaxKind}; + +use super::{LRParseState, ParseTransform, ParseTransformer}; + +/// Skip extra AND/OR at the beginning of the WHERE clause +pub struct SkipExtraOperator; + +impl SkipExtraOperator { + fn allow_extra_operator<'a>(lr_parse_state: &LRParseState<'a>) -> bool { + matches!(lr_parse_state.last_syntax_kind(), Some(SyntaxKind::WHERE)) + } +} + +impl ParseTransformer for SkipExtraOperator { + fn transform<'a>(&self, lr_parse_state: &LRParseState<'a>) -> Option { + if !Self::allow_extra_operator(&lr_parse_state) { + return None; + } + + match &lr_parse_state.token.kind { + TokenKind::KEYWORD(s) if s == "AND" || s == "OR" => Some(ParseTransform::SkipToken), + _ => None, + } + } +} diff --git a/crates/postgresql-cst-parser/tests/data/dst/2way.txt b/crates/postgresql-cst-parser/tests/data/dst/2way.txt new file mode 100644 index 0000000..f521b22 --- /dev/null +++ b/crates/postgresql-cst-parser/tests/data/dst/2way.txt @@ -0,0 +1,160 @@ +Root@0..269 + Whitespace@0..1 "\n" + SQL_COMMENT@1..46 "-- バインド変数 ..." + Whitespace@46..47 "\n" + parse_toplevel@47..268 + stmtmulti@47..268 + stmtmulti@47..266 + stmtmulti@47..204 + stmtmulti@47..149 + stmtmulti@47..66 + toplevel_stmt@47..66 + stmt@47..66 + SelectStmt@47..66 + select_no_parens@47..66 + simple_select@47..66 + SELECT@47..53 "select" + Whitespace@53..55 "\n\t" + opt_target_list@55..66 + target_list@55..66 + target_list@55..56 + target_el@55..56 + a_expr@55..56 + c_expr@55..56 + AexprConst@55..56 + Iconst@55..56 + ICONST@55..56 "1" + Whitespace@56..57 "\n" + Comma@57..58 "," + Whitespace@58..59 "\t" + C_COMMENT@59..66 "/*foo*/" + target_el@66..66 + a_expr@66..66 + c_expr@66..66 + AexprConst@66..66 + Sconst@66..66 + SCONST@66..66 "" + into_clause@66..66 + from_clause@66..66 + where_clause@66..66 + group_clause@66..66 + having_clause@66..66 + window_clause@66..66 + Whitespace@66..67 "\n" + Semicolon@67..68 ";" + Whitespace@68..70 "\n\n" + SQL_COMMENT@70..112 "-- 置換文字列の ..." + Whitespace@112..113 "\n" + toplevel_stmt@113..149 + stmt@113..149 + SelectStmt@113..149 + select_no_parens@113..149 + simple_select@113..149 + SELECT@113..119 "select" + Whitespace@119..121 "\n\t" + opt_target_list@121..122 + target_list@121..122 + target_el@121..122 + Star@121..122 "*" + Whitespace@122..123 "\n" + from_clause@123..149 + FROM@123..127 "from" + Whitespace@127..129 "\n\t" + C_COMMENT@129..137 "/*#tbl*/" + from_list@137..149 + from_list@137..137 + table_ref@137..137 + relation_expr@137..137 + qualified_name@137..137 + ColId@137..137 + IDENT@137..137 "" + opt_alias_clause@137..137 + Whitespace@137..138 "\n" + Comma@138..139 "," + Whitespace@139..140 "\t" + C_COMMENT@140..149 "/*#tbl2*/" + table_ref@149..149 + relation_expr@149..149 + qualified_name@149..149 + ColId@149..149 + IDENT@149..149 "" + opt_alias_clause@149..149 + where_clause@149..149 + group_clause@149..149 + having_clause@149..149 + window_clause@149..149 + Whitespace@149..150 "\n" + Semicolon@150..151 ";" + Whitespace@151..153 "\n\n" + SQL_COMMENT@153..183 "-- 先頭に余計な ..." + Whitespace@183..184 "\n" + toplevel_stmt@184..204 + stmt@184..204 + SelectStmt@184..204 + select_no_parens@184..204 + simple_select@184..204 + SELECT@184..190 "select" + Whitespace@190..191 "\n" + Comma@191..192 "," + Whitespace@192..193 "\t" + opt_target_list@193..194 + target_list@193..194 + target_el@193..194 + Star@193..194 "*" + Whitespace@194..195 "\n" + from_clause@195..204 + FROM@195..199 "from" + Whitespace@199..201 "\n\t" + from_list@201..204 + table_ref@201..204 + relation_expr@201..204 + qualified_name@201..204 + ColId@201..204 + IDENT@201..204 "TBL" + Whitespace@204..205 "\n" + Semicolon@205..206 ";" + Whitespace@206..208 "\n\n" + SQL_COMMENT@208..232 "-- 先頭に余計なand" + Whitespace@232..233 "\n" + toplevel_stmt@233..266 + stmt@233..266 + SelectStmt@233..266 + select_no_parens@233..266 + simple_select@233..266 + SELECT@233..239 "select" + Whitespace@239..241 "\n\t" + opt_target_list@241..242 + target_list@241..242 + target_el@241..242 + Star@241..242 "*" + Whitespace@242..243 "\n" + from_clause@243..252 + FROM@243..247 "from" + Whitespace@247..249 "\n\t" + from_list@249..252 + table_ref@249..252 + relation_expr@249..252 + qualified_name@249..252 + ColId@249..252 + IDENT@249..252 "TBL" + Whitespace@252..253 "\n" + where_clause@253..266 + WHERE@253..258 "where" + Whitespace@258..259 "\n" + AND@259..262 "and" + Whitespace@262..263 " " + a_expr@263..266 + a_expr@263..264 + c_expr@263..264 + AexprConst@263..264 + Iconst@263..264 + ICONST@263..264 "1" + Equals@264..265 "=" + a_expr@265..266 + c_expr@265..266 + AexprConst@265..266 + Iconst@265..266 + ICONST@265..266 "1" + Whitespace@266..267 "\n" + Semicolon@267..268 ";" + Whitespace@268..269 "\n" diff --git a/crates/postgresql-cst-parser/tests/data/src/2way.sql b/crates/postgresql-cst-parser/tests/data/src/2way.sql new file mode 100644 index 0000000..6866080 --- /dev/null +++ b/crates/postgresql-cst-parser/tests/data/src/2way.sql @@ -0,0 +1,30 @@ + +-- バインド変数のサンプル値欠如 +select + 1 +, /*foo*/ +; + +-- 置換文字列のサンプル値欠如 +select + * +from + /*#tbl*/ +, /*#tbl2*/ +; + +-- 先頭に余計なカンマ +select +, * +from + TBL +; + +-- 先頭に余計なand +select + * +from + TBL +where +and 1=1 +; diff --git a/crates/postgresql-cst-parser/tests/test.rs b/crates/postgresql-cst-parser/tests/test.rs index 3d0fbda..48fc728 100644 --- a/crates/postgresql-cst-parser/tests/test.rs +++ b/crates/postgresql-cst-parser/tests/test.rs @@ -1,4 +1,4 @@ -use postgresql_cst_parser::parse; +use postgresql_cst_parser::parse_2way; #[test] fn test_all() -> Result<(), std::io::Error> { @@ -20,7 +20,7 @@ fn test_all() -> Result<(), std::io::Error> { .join(format!("{}.txt", file_stem)); let content = std::fs::read_to_string(path)?; - let tree = parse(&content).unwrap(); + let tree = parse_2way(&content).unwrap(); std::fs::write(dst_path, format!("{:#?}", tree))?; } }