diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index beb8d6f1..49eb0959 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -5615,6 +5615,22 @@ fn string_literal(p: &mut Parser<'_>) { const BOOL_FIRST: TokenSet = TokenSet::new(&[TRUE_KW, FALSE_KW, OFF_KW, ON_KW, INT_NUMBER]); +const UTILITY_OPTION_ARG_FIRST: TokenSet = BOOL_FIRST + .union(NUMERIC_FIRST) + .union(STRING_FIRST) + .union(NON_RESERVED_WORD); + +fn opt_utility_option_arg(p: &mut Parser<'_>) -> bool { + if opt_bool_literal(p) || opt_numeric_literal(p).is_some() || opt_string_literal(p).is_some() { + return true; + } + if p.at_ts(NON_RESERVED_WORD) { + p.bump_any(); + return true; + } + false +} + fn opt_bool_literal(p: &mut Parser<'_>) -> bool { let m = p.start(); // TOOD: add validation to check for `1` or `0` inside the INT_NUMBER @@ -11178,15 +11194,7 @@ fn opt_explain_option(p: &mut Parser<'_>) -> bool { fn opt_explain_option_value(p: &mut Parser<'_>) -> Option { let m = p.start(); - // boolean, { NONE | TEXT | BINARY }, or { TEXT | XML | JSON | YAML } - if opt_bool_literal(p) - || p.eat(NONE_KW) - || p.eat(TEXT_KW) - || p.eat(BINARY_KW) - || p.eat(XML_KW) - || p.eat(JSON_KW) - || opt_ident(p) - { + if opt_utility_option_arg(p) { return Some(m.complete(p, EXPLAIN_OPTION_VALUE)); } m.abandon(p); @@ -12214,7 +12222,7 @@ fn opt_reindex_option(p: &mut Parser<'_>) -> bool { let parsed = match p.current() { CONCURRENTLY_KW | VERBOSE_KW => { p.bump_any(); - opt_bool_literal(p); + opt_utility_option_arg(p); true } TABLESPACE_KW => { @@ -12800,9 +12808,7 @@ fn opt_table_and_columns(p: &mut Parser<'_>) -> bool { const VACUUM_OPTION_FIRST: TokenSet = NON_RESERVED_WORD .union(TokenSet::new(&[ANALYZE_KW, ANALYSE_KW, FORMAT_KW, ON_KW])) - .union(NUMERIC_FIRST) - .union(STRING_FIRST) - .union(BOOL_FIRST); + .union(UTILITY_OPTION_ARG_FIRST); // where option can be one of: // FORMAT format_name @@ -12835,11 +12841,7 @@ fn opt_vacuum_option(p: &mut Parser<'_>) -> Option { // utility_option_arg fn opt_vacuum_option_value(p: &mut Parser<'_>) -> Option { let m = p.start(); - if p.at_ts(NON_RESERVED_WORD) || p.at(ON_KW) { - col_label(p); - return Some(m.complete(p, VACUUM_OPTION_VALUE)); - } - if opt_numeric_literal(p).is_some() || opt_string_literal(p).is_some() || opt_bool_literal(p) { + if opt_utility_option_arg(p) { return Some(m.complete(p, VACUUM_OPTION_VALUE)); } m.abandon(p); diff --git a/crates/squawk_parser/tests/data/ok/analyze.sql b/crates/squawk_parser/tests/data/ok/analyze.sql index 2188a313..1919bcc0 100644 --- a/crates/squawk_parser/tests/data/ok/analyze.sql +++ b/crates/squawk_parser/tests/data/ok/analyze.sql @@ -7,4 +7,5 @@ analyze verbose foo.bar, foo.bar(a, b, c), foo; -- full_parens analyze (verbose false, skip_locked, buffer_usage_limit 10) foo.bar(a, b, c); +analyze (verbose no, skip_locked 'on') foo; diff --git a/crates/squawk_parser/tests/data/ok/explain.sql b/crates/squawk_parser/tests/data/ok/explain.sql index 23aecabd..e0719639 100644 --- a/crates/squawk_parser/tests/data/ok/explain.sql +++ b/crates/squawk_parser/tests/data/ok/explain.sql @@ -31,7 +31,10 @@ explain ( format text, format xml, format json, - format yaml + format yaml, + verbose 'true', + costs yes, + settings no ) select 1; diff --git a/crates/squawk_parser/tests/data/ok/reindex.sql b/crates/squawk_parser/tests/data/ok/reindex.sql index 234a8acd..192fdd2e 100644 --- a/crates/squawk_parser/tests/data/ok/reindex.sql +++ b/crates/squawk_parser/tests/data/ok/reindex.sql @@ -7,6 +7,7 @@ REINDEX TABLE CONCURRENTLY my_broken_table; -- complete_syntax reindex (concurrently true, tablespace fooo, verbose false) database concurrently foo; +reindex (concurrently 'off', verbose yes) table foo; reindex system foo; diff --git a/crates/squawk_parser/tests/data/ok/vacuum.sql b/crates/squawk_parser/tests/data/ok/vacuum.sql index 3c98f9d6..7a9dd337 100644 --- a/crates/squawk_parser/tests/data/ok/vacuum.sql +++ b/crates/squawk_parser/tests/data/ok/vacuum.sql @@ -35,7 +35,10 @@ VACUUM ( only_database_stats true, only_database_stats false, buffer_usage_limit 10, - buffer_usage_limit '10 TB' + buffer_usage_limit '10 TB', + full no, + verbose 'off', + analyze yes ) t1; -- pre_pg_9_syntax diff --git a/crates/squawk_parser/tests/snapshots/tests__analyze_ok.snap b/crates/squawk_parser/tests/snapshots/tests__analyze_ok.snap index 5699029e..57a42197 100644 --- a/crates/squawk_parser/tests/snapshots/tests__analyze_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__analyze_ok.snap @@ -125,4 +125,31 @@ SOURCE_FILE IDENT "c" R_PAREN ")" SEMICOLON ";" + WHITESPACE "\n" + ANALYZE + ANALYZE_KW "analyze" + WHITESPACE " " + OPTION_ITEM_LIST + L_PAREN "(" + OPTION_ITEM + VERBOSE_KW "verbose" + WHITESPACE " " + NO_KW "no" + COMMA "," + WHITESPACE " " + OPTION_ITEM + IDENT "skip_locked" + WHITESPACE " " + LITERAL + STRING "'on'" + R_PAREN ")" + WHITESPACE " " + TABLE_AND_COLUMNS_LIST + TABLE_AND_COLUMNS + RELATION_NAME + PATH + PATH_SEGMENT + NAME_REF + IDENT "foo" + SEMICOLON ";" WHITESPACE "\n\n" diff --git a/crates/squawk_parser/tests/snapshots/tests__explain_ok.snap b/crates/squawk_parser/tests/snapshots/tests__explain_ok.snap index 9bd20a47..03b4a2e6 100644 --- a/crates/squawk_parser/tests/snapshots/tests__explain_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__explain_ok.snap @@ -236,6 +236,31 @@ SOURCE_FILE WHITESPACE " " EXPLAIN_OPTION_VALUE IDENT "yaml" + COMMA "," + WHITESPACE "\n " + EXPLAIN_OPTION + NAME + VERBOSE_KW "verbose" + WHITESPACE " " + EXPLAIN_OPTION_VALUE + LITERAL + STRING "'true'" + COMMA "," + WHITESPACE "\n " + EXPLAIN_OPTION + NAME + IDENT "costs" + WHITESPACE " " + EXPLAIN_OPTION_VALUE + YES_KW "yes" + COMMA "," + WHITESPACE "\n " + EXPLAIN_OPTION + NAME + IDENT "settings" + WHITESPACE " " + EXPLAIN_OPTION_VALUE + NO_KW "no" WHITESPACE "\n" R_PAREN ")" WHITESPACE "\n" diff --git a/crates/squawk_parser/tests/snapshots/tests__reindex_err.snap b/crates/squawk_parser/tests/snapshots/tests__reindex_err.snap index 58ccf421..e9941693 100644 --- a/crates/squawk_parser/tests/snapshots/tests__reindex_err.snap +++ b/crates/squawk_parser/tests/snapshots/tests__reindex_err.snap @@ -12,8 +12,7 @@ SOURCE_FILE L_PAREN "(" REINDEX_OPTION CONCURRENTLY_KW "concurrently" - WHITESPACE " " - REINDEX_OPTION + WHITESPACE " " VERBOSE_KW "verbose" WHITESPACE " " REINDEX_OPTION @@ -32,10 +31,6 @@ SOURCE_FILE SEMICOLON ";" WHITESPACE "\n" --- -error[syntax-error]: expected COMMA - ╭▸ -2 │ reindex (concurrently verbose tablespace t) index i; - ╰╴ ━ error[syntax-error]: expected COMMA ╭▸ 2 │ reindex (concurrently verbose tablespace t) index i; diff --git a/crates/squawk_parser/tests/snapshots/tests__reindex_ok.snap b/crates/squawk_parser/tests/snapshots/tests__reindex_ok.snap index 975e25f0..a9af44db 100644 --- a/crates/squawk_parser/tests/snapshots/tests__reindex_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__reindex_ok.snap @@ -77,6 +77,32 @@ SOURCE_FILE NAME_REF IDENT "foo" SEMICOLON ";" + WHITESPACE "\n" + REINDEX + REINDEX_KW "reindex" + WHITESPACE " " + REINDEX_OPTION_LIST + L_PAREN "(" + REINDEX_OPTION + CONCURRENTLY_KW "concurrently" + WHITESPACE " " + LITERAL + STRING "'off'" + COMMA "," + WHITESPACE " " + REINDEX_OPTION + VERBOSE_KW "verbose" + WHITESPACE " " + YES_KW "yes" + R_PAREN ")" + WHITESPACE " " + TABLE_KW "table" + WHITESPACE " " + PATH + PATH_SEGMENT + NAME_REF + IDENT "foo" + SEMICOLON ";" WHITESPACE "\n\n" REINDEX REINDEX_KW "reindex" diff --git a/crates/squawk_parser/tests/snapshots/tests__vacuum_ok.snap b/crates/squawk_parser/tests/snapshots/tests__vacuum_ok.snap index 582b7b4b..78586c7c 100644 --- a/crates/squawk_parser/tests/snapshots/tests__vacuum_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__vacuum_ok.snap @@ -140,8 +140,7 @@ SOURCE_FILE IDENT "index_cleanup" WHITESPACE " " VACUUM_OPTION_VALUE - NAME - IDENT "auto" + IDENT "auto" COMMA "," WHITESPACE "\n " VACUUM_OPTION @@ -149,7 +148,7 @@ SOURCE_FILE IDENT "index_cleanup" WHITESPACE " " VACUUM_OPTION_VALUE - NAME + LITERAL ON_KW "on" COMMA "," WHITESPACE "\n " @@ -158,7 +157,7 @@ SOURCE_FILE IDENT "index_cleanup" WHITESPACE " " VACUUM_OPTION_VALUE - NAME + LITERAL OFF_KW "off" COMMA "," WHITESPACE "\n " @@ -279,6 +278,31 @@ SOURCE_FILE VACUUM_OPTION_VALUE LITERAL STRING "'10 TB'" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + NAME + FULL_KW "full" + WHITESPACE " " + VACUUM_OPTION_VALUE + NO_KW "no" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + NAME + VERBOSE_KW "verbose" + WHITESPACE " " + VACUUM_OPTION_VALUE + LITERAL + STRING "'off'" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + NAME + ANALYZE_KW "analyze" + WHITESPACE " " + VACUUM_OPTION_VALUE + YES_KW "yes" WHITESPACE "\n" R_PAREN ")" WHITESPACE " " diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 41e5cb68..24916c0b 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -10479,6 +10479,10 @@ impl ExplainOptionValue { support::token(&self.syntax, SyntaxKind::JSON_KW) } #[inline] + pub fn no_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::NO_KW) + } + #[inline] pub fn none_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::NONE_KW) } @@ -10490,6 +10494,10 @@ impl ExplainOptionValue { pub fn xml_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::XML_KW) } + #[inline] + pub fn yes_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::YES_KW) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -16880,6 +16888,14 @@ impl ReindexOption { support::token(&self.syntax, SyntaxKind::CONCURRENTLY_KW) } #[inline] + pub fn ident_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::IDENT) + } + #[inline] + pub fn no_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::NO_KW) + } + #[inline] pub fn tablespace_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::TABLESPACE_KW) } @@ -16887,6 +16903,10 @@ impl ReindexOption { pub fn verbose_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::VERBOSE_KW) } + #[inline] + pub fn yes_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::YES_KW) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -20731,6 +20751,18 @@ impl VacuumOptionValue { pub fn name(&self) -> Option { support::child(&self.syntax) } + #[inline] + pub fn ident_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::IDENT) + } + #[inline] + pub fn no_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::NO_KW) + } + #[inline] + pub fn yes_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::YES_KW) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/crates/squawk_syntax/src/ast/node_ext.rs b/crates/squawk_syntax/src/ast/node_ext.rs index 5af2b6b1..bf149555 100644 --- a/crates/squawk_syntax/src/ast/node_ext.rs +++ b/crates/squawk_syntax/src/ast/node_ext.rs @@ -500,8 +500,43 @@ impl ast::CharType { } } -fn is_falsey_option(text: &str) -> bool { - text == "0" || text.eq_ignore_ascii_case("false") || text.eq_ignore_ascii_case("off") +fn string_literal_contents(token: &SyntaxToken) -> Option<&str> { + match token.kind() { + SyntaxKind::STRING => token.text().strip_prefix('\'')?.strip_suffix('\''), + SyntaxKind::ESC_STRING | SyntaxKind::NATIONAL_STRING => { + token.text().get(2..)?.strip_suffix('\'') + } + SyntaxKind::UNICODE_ESC_STRING => token.text().get(3..)?.strip_suffix('\''), + SyntaxKind::DOLLAR_QUOTED_STRING => { + let text = token.text(); + let rest = text.strip_prefix('$')?; + let tag_len = rest.find('$')?; + let delimiter = text.get(..=tag_len + 1)?; + text.get(delimiter.len()..)?.strip_suffix(delimiter) + } + _ => None, + } +} + +fn is_falsey_token(token: &SyntaxToken) -> bool { + match token.kind() { + SyntaxKind::FALSE_KW | SyntaxKind::NO_KW | SyntaxKind::OFF_KW => true, + SyntaxKind::INT_NUMBER => token.text() == "0", + SyntaxKind::STRING + | SyntaxKind::ESC_STRING + | SyntaxKind::NATIONAL_STRING + | SyntaxKind::UNICODE_ESC_STRING + | SyntaxKind::DOLLAR_QUOTED_STRING => string_literal_contents(token) + .is_some_and(|text| matches!(text.to_ascii_lowercase().as_str(), "false" | "off")), + _ => false, + } +} + +fn is_falsey_vacuum_option_value(value: &ast::VacuumOptionValue) -> bool { + value + .syntax() + .first_token() + .is_some_and(|token| is_falsey_token(&token)) } impl ast::Vacuum { @@ -510,18 +545,13 @@ impl ast::Vacuum { // TODO: we need a better way of handling option lists || self.vacuum_option_list().is_some_and(|opt_list| { opt_list.vacuum_options().any(|opt| { - let mut tokens = opt - .syntax() - .descendants_with_tokens() - .filter_map(|child| child.into_token()) - .filter(|token| !token.kind().is_trivia()); - - tokens - .next() - .is_some_and(|token| token.text().eq_ignore_ascii_case("full")) - && tokens - .next() - .is_none_or(|token| !is_falsey_option(token.text())) + opt.name().is_some_and(|name| { + name.syntax() + .first_token() + .is_some_and(|token| token.text().eq_ignore_ascii_case("full")) + }) && opt + .vacuum_option_value() + .is_none_or(|value| !is_falsey_vacuum_option_value(&value)) }) }) } @@ -1038,6 +1068,31 @@ fn vacuum_full_off_is_not_full() { assert!(!extract_vacuum("VACUUM (FULL OFF) foo;").is_full()); } +#[test] +fn vacuum_full_no_is_not_full() { + assert!(!extract_vacuum("VACUUM (FULL NO) foo;").is_full()); +} + +#[test] +fn vacuum_full_quoted_off_is_not_full() { + assert!(!extract_vacuum("VACUUM (FULL 'off') foo;").is_full()); +} + +#[test] +fn vacuum_full_escaped_string_off_is_not_full() { + assert!(!extract_vacuum("VACUUM (FULL E'off') foo;").is_full()); +} + +#[test] +fn vacuum_full_unicode_escaped_string_off_is_not_full() { + assert!(!extract_vacuum("VACUUM (FULL U&'off') foo;").is_full()); +} + +#[test] +fn vacuum_full_dollar_quoted_off_is_not_full() { + assert!(!extract_vacuum("VACUUM (FULL $$off$$) t;").is_full()); +} + #[test] fn vacuum_full_0_is_not_full() { assert!(!extract_vacuum("VACUUM (FULL 0) foo;").is_full()); diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index c87b6e1a..bf00ebb8 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -3185,12 +3185,14 @@ ExplainOption = ExplainOptionValue = Literal +| 'binary' +| '#ident' +| 'json' +| 'no' | 'none' | 'text' | 'xml' -| 'json' -| 'binary' -| '#ident' +| 'yes' ExplainStmt = Select @@ -3379,7 +3381,7 @@ ReindexOptionList = '(' (ReindexOption (',' ReindexOption)*)? ')' ReindexOption = - ('concurrently' | 'verbose') Literal? + ('concurrently' | 'verbose') (Literal | '#ident' | 'no' | 'yes')? | 'tablespace' Name CreateView = @@ -3520,7 +3522,11 @@ VacuumOption = Name VacuumOptionValue? VacuumOptionValue = - Literal? Name? + Literal +| Name +| '#ident' +| 'no' +| 'yes' Copy = 'copy'