From 3412cb99c3982abe864248f297d91c1bd9ecaae3 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Fri, 10 Oct 2025 16:15:16 -0400 Subject: [PATCH 1/4] parser: improve error recovery for list items --- crates/squawk_ide/src/expand_selection.rs | 90 +++-- .../src/generated/syntax_kind.rs | 5 +- crates/squawk_parser/src/grammar.rs | 308 ++++++++------- .../tests/data/err/select_cte.sql | 16 + .../snapshots/tests__alter_table_ok.snap | 100 ++--- .../snapshots/tests__create_table_ok.snap | 105 +++--- .../snapshots/tests__select_cte_err.snap | 92 +++++ .../tests/snapshots/tests__vacuum_ok.snap | 356 ++++++++++-------- .../squawk_syntax/src/ast/generated/nodes.rs | 109 +++++- crates/squawk_syntax/src/postgresql.ungram | 14 +- 10 files changed, 776 insertions(+), 419 deletions(-) diff --git a/crates/squawk_ide/src/expand_selection.rs b/crates/squawk_ide/src/expand_selection.rs index 1417795e..1017451a 100644 --- a/crates/squawk_ide/src/expand_selection.rs +++ b/crates/squawk_ide/src/expand_selection.rs @@ -33,12 +33,34 @@ use squawk_syntax::{ ast::{self, AstToken}, }; +const ALL_LIST_KINDS: &[SyntaxKind] = &[ + SyntaxKind::ARG_LIST, + SyntaxKind::ATTRIBUTE_LIST, + SyntaxKind::COLUMN_LIST, + SyntaxKind::CONSTRAINT_EXCLUSION_LIST, + // only separated by whitespace + // SyntaxKind::FUNC_OPTION_LIST, + SyntaxKind::JSON_TABLE_COLUMN_LIST, + SyntaxKind::OPTIONS_LIST, + SyntaxKind::PARAM_LIST, + // only separated by whitespace + // SyntaxKind::SEQUENCE_OPTION_LIST, + SyntaxKind::SET_OPTIONS_LIST, + SyntaxKind::TABLE_ARG_LIST, + SyntaxKind::TABLE_LIST, + SyntaxKind::TARGET_LIST, + SyntaxKind::TRANSACTION_MODE_LIST, + SyntaxKind::VACUUM_OPTION_LIST, + // only separated by whitespace + // SyntaxKind::XML_COLUMN_OPTION_LIST, + SyntaxKind::XML_TABLE_COLUMN_LIST, +]; + pub fn extend_selection(root: &SyntaxNode, range: TextRange) -> TextRange { try_extend_selection(root, range).unwrap_or(range) } fn try_extend_selection(root: &SyntaxNode, range: TextRange) -> Option { - // TODO: more list_kinds, and add the strings that rust analyzer has let string_kinds = [ SyntaxKind::COMMENT, SyntaxKind::STRING, @@ -48,28 +70,6 @@ fn try_extend_selection(root: &SyntaxNode, range: TextRange) -> Option Option = (0..SyntaxKind::__LAST as u16) + .map(|n| SyntaxKind::from(n)) + .filter(|kind| { + format!("{:?}", kind).ends_with("_LIST") && !EXCLUDED_LIST_KINDS.contains(kind) + }) + .collect(); + + assert_debug_snapshot!(generated_list_kinds, @r" + [ + ARG_LIST, + ATTRIBUTE_LIST, + COLUMN_LIST, + CONSTRAINT_EXCLUSION_LIST, + JSON_TABLE_COLUMN_LIST, + OPTIONS_LIST, + PARAM_LIST, + SET_OPTIONS_LIST, + TABLE_ARG_LIST, + TABLE_LIST, + TARGET_LIST, + TRANSACTION_MODE_LIST, + VACUUM_OPTION_LIST, + XML_TABLE_COLUMN_LIST, + ] + "); + + assert_eq!( + ALL_LIST_KINDS, + generated_list_kinds.as_slice(), + "ALL_LIST_KINDS constant is out of sync with actual _LIST variants" + ); + } } diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index 3eb49e10..853afea6 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -625,7 +625,8 @@ pub enum SyntaxKind { COMMIT, COMPOUND_SELECT, COMPRESSION_METHOD, - CONSTRAINT_EXCLUSIONS, + CONSTRAINT_EXCLUSION, + CONSTRAINT_EXCLUSION_LIST, CONSTRAINT_INCLUDE_CLAUSE, CONSTRAINT_INDEX_METHOD, CONSTRAINT_INDEX_TABLESPACE, @@ -1006,6 +1007,8 @@ pub enum SyntaxKind { USING_INDEX, USING_METHOD, VACUUM, + VACUUM_OPTION, + VACUUM_OPTION_LIST, VALIDATE_CONSTRAINT, VALUES, VOLATILITY_FUNC_OPTION, diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index f26e7a02..83886562 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -2496,7 +2496,7 @@ fn opt_materialized(p: &mut Parser<'_>) { } const WITH_FOLLOW: TokenSet = TokenSet::new(&[ - DELETE_KW, SELECT_KW, TABLE_KW, INSERT_KW, UPDATE_KW, MERGE_KW, + DELETE_KW, SELECT_KW, TABLE_KW, INSERT_KW, UPDATE_KW, MERGE_KW, VALUES_KW, ]); // [ WITH [ RECURSIVE ] with_query [, ...] ] @@ -2511,10 +2511,10 @@ fn with_query_clause(p: &mut Parser<'_>) -> Option { break; } if !p.eat(COMMA) { - if p.at(IDENT) { - p.error("missing comma"); - } else { + if p.at_ts(WITH_FOLLOW) { break; + } else { + p.error("missing comma"); } } } @@ -3834,7 +3834,10 @@ fn opt_like_option(p: &mut Parser<'_>) -> Option { // | ColId index_elem_options // | func_expr_windowless index_elem_options // | '(' a_expr ')' index_elem_options -fn index_elem(p: &mut Parser<'_>) { +fn opt_index_elem(p: &mut Parser<'_>) -> bool { + if !p.at(L_PAREN) && !p.at_ts(EXPR_FIRST) { + return false; + } if p.eat(L_PAREN) { if expr(p).is_none() { p.error("expected an expression"); @@ -3845,6 +3848,7 @@ fn index_elem(p: &mut Parser<'_>) { p.error("expected expression"); } } + true } fn opt_operator(p: &mut Parser<'_>) -> bool { @@ -3959,7 +3963,7 @@ fn table_constraint(p: &mut Parser<'_>) -> CompletedMarker { EXCLUDE_KW => { p.bump(EXCLUDE_KW); opt_constraint_index_method(p); - constraint_exclusions(p); + constraint_exclusion_list(p); opt_index_parameters(p); opt_constraint_where_clause(p); EXCLUDE_CONSTRAINT @@ -4018,22 +4022,34 @@ fn opt_constraint_where_clause(p: &mut Parser<'_>) { } } -fn constraint_exclusions(p: &mut Parser<'_>) { +const CONSTRAINT_EXCLUSION_FIRST: TokenSet = EXPR_FIRST.union(TokenSet::new(&[L_PAREN])); + +fn opt_constraint_exclusion(p: &mut Parser<'_>) -> Option { let m = p.start(); - p.expect(L_PAREN); - while !p.at(EOF) && !p.at(R_PAREN) { - index_elem(p); - p.expect(WITH_KW); - // support: - // with > - // with foo.bar.buzz.> - operator(p); - if !p.eat(COMMA) { - break; - } + if !opt_index_elem(p) { + m.abandon(p); + return None; } - p.expect(R_PAREN); - m.complete(p, CONSTRAINT_EXCLUSIONS); + p.expect(WITH_KW); + // support: + // with > + // with foo.bar.buzz.> + operator(p); + Some(m.complete(p, CONSTRAINT_EXCLUSION)) +} + +fn constraint_exclusion_list(p: &mut Parser<'_>) { + let m = p.start(); + delimited( + p, + L_PAREN, + R_PAREN, + COMMA, + || "unexpected comma".to_string(), + CONSTRAINT_EXCLUSION_FIRST, + |p| opt_constraint_exclusion(p).is_some(), + ); + m.complete(p, CONSTRAINT_EXCLUSION_LIST); } fn opt_constraint_index_method(p: &mut Parser<'_>) { @@ -4794,7 +4810,7 @@ fn drop_table(p: &mut Parser<'_>) -> CompletedMarker { // | ColId opt_collate opt_qualified_name // | func_expr_windowless opt_collate opt_qualified_name // | '(' a_expr ')' opt_collate opt_qualified_name -fn partition_item(p: &mut Parser<'_>, allow_extra_params: bool) { +fn partition_item(p: &mut Parser<'_>, allow_extra_params: bool) -> CompletedMarker { let m = p.start(); // TODO: this can be more strict if expr(p).is_none() { @@ -4811,7 +4827,14 @@ fn partition_item(p: &mut Parser<'_>, allow_extra_params: bool) { opt_sort_order(p); opt_nulls_order(p); } - m.complete(p, PARTITION_ITEM); + m.complete(p, PARTITION_ITEM) +} + +fn opt_partition_item(p: &mut Parser<'_>, allow_extra_params: bool) -> Option { + if !p.at_ts(EXPR_FIRST) { + return None; + } + Some(partition_item(p, allow_extra_params)) } // [ NULLS { FIRST | LAST } ] @@ -5256,11 +5279,14 @@ fn string_literal(p: &mut Parser<'_>) { } } +const BOOL_FIRST: TokenSet = TokenSet::new(&[TRUE_KW, FALSE_KW, OFF_KW, ON_KW, INT_NUMBER]); + 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 // https://www.postgresql.org/docs/current/sql-explain.html - if p.eat(TRUE_KW) || p.eat(FALSE_KW) || p.eat(OFF_KW) || p.eat(ON_KW) || p.eat(INT_NUMBER) { + if p.at_ts(BOOL_FIRST) { + p.bump_any(); m.complete(p, LITERAL); true } else { @@ -10921,47 +10947,44 @@ fn values(p: &mut Parser<'_>, m: Option) -> CompletedMarker { m.complete(p, VALUES) } -// REINDEX [ ( option [, ...] ) ] { INDEX | TABLE | SCHEMA } [ CONCURRENTLY ] name -// REINDEX [ ( option [, ...] ) ] { DATABASE | SYSTEM } [ CONCURRENTLY ] [ name ] -// -// where option can be one of: +const REINDEX_OPTION_FIRST: TokenSet = TokenSet::new(&[CONCURRENTLY_KW, VERBOSE_KW, TABLESPACE_KW]); + +// option can be one of: // CONCURRENTLY [ boolean ] // TABLESPACE new_tablespace // VERBOSE [ boolean ] +fn opt_reindex_option(p: &mut Parser<'_>) -> bool { + match p.current() { + CONCURRENTLY_KW | VERBOSE_KW => { + p.bump_any(); + opt_bool_literal(p); + true + } + TABLESPACE_KW => { + p.bump_any(); + name(p); + true + } + _ => false, + } +} + +// REINDEX [ ( option [, ...] ) ] { INDEX | TABLE | SCHEMA } [ CONCURRENTLY ] name +// REINDEX [ ( option [, ...] ) ] { DATABASE | SYSTEM } [ CONCURRENTLY ] [ name ] fn reindex(p: &mut Parser<'_>) -> CompletedMarker { assert!(p.at(REINDEX_KW)); let m = p.start(); p.bump(REINDEX_KW); - // TODO: we need to general this stuff - if p.eat(L_PAREN) { - let mut found = false; - while !p.at(EOF) { - match p.current() { - CONCURRENTLY_KW | VERBOSE_KW => { - p.bump_any(); - opt_bool_literal(p); - found = true; - } - TABLESPACE_KW => { - p.bump_any(); - name(p); - found = true; - } - kind => { - p.error(format!( - "expected CONCURRENTLY, TABLESPACE, or VERBOSE option, got {kind:?}", - )); - break; - } - } - if !p.eat(COMMA) { - break; - } - } - if !found { - p.error("expected CONCURRENTLY, TABLESPACE, or VERBOSE option"); - } - p.expect(R_PAREN); + if p.at(L_PAREN) { + delimited( + p, + L_PAREN, + R_PAREN, + COMMA, + || "unexpected comma".to_string(), + REINDEX_OPTION_FIRST, + |p| opt_reindex_option(p), + ); } let name_required = match p.current() { // { INDEX | TABLE | SCHEMA } @@ -11053,14 +11076,16 @@ fn prepare(p: &mut Parser<'_>) -> CompletedMarker { let m = p.start(); p.bump(PREPARE_KW); name(p); - if p.eat(L_PAREN) { - while !p.at(EOF) { - type_name(p); - if !p.eat(COMMA) { - break; - } - } - p.expect(R_PAREN); + if p.at(L_PAREN) { + delimited( + p, + L_PAREN, + R_PAREN, + COMMA, + || "unexpected comma".to_string(), + NAME_REF_FIRST, + |p| opt_type_name(p), + ); } p.expect(AS_KW); preparable_stmt(p); @@ -11377,21 +11402,28 @@ fn vacuum(p: &mut Parser<'_>) -> CompletedMarker { p.eat(VERBOSE_KW); // [ ANALYZE ] let _ = p.eat(ANALYZE_KW) || p.eat(ANALYSE_KW); - // [ ( option [, ...] ) ] - if p.at(L_PAREN) { - p.expect(L_PAREN); - while !p.at(EOF) { - vacuum_option(p); - if !p.eat(COMMA) { - break; - } - } - p.expect(R_PAREN); - } + opt_vacuum_option_list(p); opt_relation_list(p); m.complete(p, VACUUM) } +// [ ( option [, ...] ) ] +fn opt_vacuum_option_list(p: &mut Parser<'_>) { + if p.at(L_PAREN) { + let m = p.start(); + delimited( + p, + L_PAREN, + R_PAREN, + COMMA, + || "unexpected comma".to_string(), + VACUUM_OPTION_FIRST, + |p| opt_vacuum_option(p).is_some(), + ); + m.complete(p, VACUUM_OPTION_LIST); + } +} + // [ table_and_columns [, ...] ] // where table_and_coumns is: // table_name [ ( column_name [, ...] ) ] @@ -11407,6 +11439,12 @@ fn opt_relation_list(p: &mut Parser<'_>) { } } +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); + // where option can be one of: // FORMAT format_name // FREEZE [ boolean ] @@ -11422,25 +11460,30 @@ fn opt_relation_list(p: &mut Parser<'_>) { // ON_ERROR error_action // ENCODING 'encoding_name' // LOG_VERBOSITY verbosity -fn vacuum_option(p: &mut Parser<'_>) { +fn opt_vacuum_option(p: &mut Parser<'_>) -> Option { + if !p.at_ts(VACUUM_OPTION_FIRST) { + return None; + } + let m = p.start(); // utility_option_name if p.at_ts(NON_RESERVED_WORD) || p.at(ANALYZE_KW) || p.at(ANALYSE_KW) || p.at(FORMAT_KW) { p.bump_any(); } if p.at_ts(NON_RESERVED_WORD) || p.at(ON_KW) { p.bump_any(); - return; + return Some(m.complete(p, VACUUM_OPTION)); } // utility_option_arg if opt_numeric_literal(p).is_some() { - return; + return Some(m.complete(p, VACUUM_OPTION)); } if opt_string_literal(p).is_some() { - return; + return Some(m.complete(p, VACUUM_OPTION)); } if opt_bool_literal(p) { - return; + return Some(m.complete(p, VACUUM_OPTION)); } + Some(m.complete(p, VACUUM_OPTION)) } // copy_generic_opt_elem: @@ -11487,16 +11530,15 @@ fn copy_option_arg(p: &mut Parser<'_>) { } fn copy_option_list(p: &mut Parser<'_>) { - p.expect(L_PAREN); - while !p.at(EOF) { - if !opt_copy_option(p) { - p.error("expected copy option"); - } - if !p.eat(COMMA) { - break; - } - } - p.expect(R_PAREN); + delimited( + p, + L_PAREN, + R_PAREN, + COMMA, + || "unexpected comma".to_string(), + COL_LABEL_FIRST, + |p| opt_copy_option(p), + ); } fn opt_copy_option_item(p: &mut Parser<'_>) -> bool { @@ -12255,6 +12297,8 @@ fn drop_index(p: &mut Parser<'_>) -> CompletedMarker { m.complete(p, DROP_INDEX) } +const DROP_DATABASE_OPTION_FIRST: TokenSet = TokenSet::new(&[FORCE_KW]); + // DROP DATABASE [ IF EXISTS ] name [ [ WITH ] ( option [, ...] ) ] // // where option can be: @@ -12270,14 +12314,15 @@ fn drop_database(p: &mut Parser<'_>) -> CompletedMarker { name_ref(p); // [ [ WITH ] ( option [, ...] ) ] if p.at(L_PAREN) || p.eat(WITH_KW) { - p.expect(L_PAREN); - while !p.at(EOF) { - p.expect(FORCE_KW); - if !p.eat(COMMA) { - break; - } - } - p.expect(R_PAREN); + delimited( + p, + L_PAREN, + R_PAREN, + COMMA, + || "unexpected comma".to_string(), + DROP_DATABASE_OPTION_FIRST, + |p| p.eat(FORCE_KW), + ); } m.complete(p, DROP_DATABASE) } @@ -12362,14 +12407,15 @@ fn index_params(p: &mut Parser<'_>) { // [, ...] // ) fn partition_items(p: &mut Parser<'_>, allow_extra_params: bool) { - p.expect(L_PAREN); - while !p.at(EOF) && !p.at(R_PAREN) { - partition_item(p, allow_extra_params); - if !p.eat(COMMA) { - break; - } - } - p.expect(R_PAREN); + delimited( + p, + L_PAREN, + R_PAREN, + COMMA, + || "unexpected comma".to_string(), + EXPR_FIRST, + |p| opt_partition_item(p, allow_extra_params).is_some(), + ); } // [ argmode ] @@ -12710,17 +12756,24 @@ fn opt_ret_type(p: &mut Parser<'_>) { let m = p.start(); if p.eat(RETURNS_KW) { if p.eat(TABLE_KW) { - p.expect(L_PAREN); - while !p.at(EOF) { - // column_name - name_ref(p); - // column_type - type_name(p); - if !p.eat(COMMA) { - break; - } - } - p.expect(R_PAREN); + delimited( + p, + L_PAREN, + R_PAREN, + COMMA, + || "unexpected comma".to_string(), + NAME_REF_FIRST, + |p| { + // TODO: should this be the column def name? + // column_name + if opt_name_ref(p).is_none() { + return false; + } + // column_type + type_name(p); + true + }, + ); } else { p.eat(SETOF_KW); type_name(p); @@ -12873,16 +12926,15 @@ fn create_type(p: &mut Parser<'_>) -> CompletedMarker { if p.eat(AS_KW) { // AS ENUM if p.eat(ENUM_KW) { - p.expect(L_PAREN); - while !p.at(EOF) { - if opt_string_literal(p).is_none() { - break; - } - if !p.eat(COMMA) { - break; - } - } - p.expect(R_PAREN); + delimited( + p, + L_PAREN, + R_PAREN, + COMMA, + || "unexpected comma".to_string(), + STRING_FIRST, + |p| opt_string_literal(p).is_some(), + ); // AS RANGE } else if p.eat(RANGE_KW) { attribute_list(p); diff --git a/crates/squawk_parser/tests/data/err/select_cte.sql b/crates/squawk_parser/tests/data/err/select_cte.sql index 026d53c5..2cd0eb0d 100644 --- a/crates/squawk_parser/tests/data/err/select_cte.sql +++ b/crates/squawk_parser/tests/data/err/select_cte.sql @@ -21,3 +21,19 @@ with select 3 ) select 2; + +-- table name isn't an plain ident +with + a as ( + select 1 + ) -- <-- missing a comma + row as (select 1) +select 1; + + +-- extra comma with values (we didn't support values before) +with + a as ( + select 1 + ), -- <-- extra comma +values (2); diff --git a/crates/squawk_parser/tests/snapshots/tests__alter_table_ok.snap b/crates/squawk_parser/tests/snapshots/tests__alter_table_ok.snap index 8938daf5..98330bc2 100644 --- a/crates/squawk_parser/tests/snapshots/tests__alter_table_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__alter_table_ok.snap @@ -2401,35 +2401,37 @@ SOURCE_FILE EXCLUDE_CONSTRAINT EXCLUDE_KW "exclude" WHITESPACE " " - CONSTRAINT_EXCLUSIONS + CONSTRAINT_EXCLUSION_LIST L_PAREN "(" WHITESPACE "\n " - NAME_REF - IDENT "a" - WHITESPACE " " - WITH_KW "with" - WHITESPACE " " - EQ "=" + CONSTRAINT_EXCLUSION + NAME_REF + IDENT "a" + WHITESPACE " " + WITH_KW "with" + WHITESPACE " " + EQ "=" COMMA "," WHITESPACE "\n " - CALL_EXPR - NAME_REF - IDENT "tsrange" - ARG_LIST - L_PAREN "(" + CONSTRAINT_EXCLUSION + CALL_EXPR NAME_REF - IDENT "b" - COMMA "," - WHITESPACE " " - NAME_REF - IDENT "c" - R_PAREN ")" - WHITESPACE " " - WITH_KW "with" - WHITESPACE " " - CUSTOM_OP - AMP "&" - AMP "&" + IDENT "tsrange" + ARG_LIST + L_PAREN "(" + NAME_REF + IDENT "b" + COMMA "," + WHITESPACE " " + NAME_REF + IDENT "c" + R_PAREN ")" + WHITESPACE " " + WITH_KW "with" + WHITESPACE " " + CUSTOM_OP + AMP "&" + AMP "&" WHITESPACE "\n" R_PAREN ")" SEMICOLON ";" @@ -2457,35 +2459,37 @@ SOURCE_FILE NAME_REF IDENT "gist" WHITESPACE " " - CONSTRAINT_EXCLUSIONS + CONSTRAINT_EXCLUSION_LIST L_PAREN "(" WHITESPACE "\n " - NAME_REF - IDENT "a" - WHITESPACE " " - WITH_KW "with" - WHITESPACE " " - EQ "=" + CONSTRAINT_EXCLUSION + NAME_REF + IDENT "a" + WHITESPACE " " + WITH_KW "with" + WHITESPACE " " + EQ "=" COMMA "," WHITESPACE "\n " - CALL_EXPR - NAME_REF - IDENT "tsrange" - ARG_LIST - L_PAREN "(" + CONSTRAINT_EXCLUSION + CALL_EXPR NAME_REF - IDENT "b" - COMMA "," - WHITESPACE " " - NAME_REF - IDENT "c" - R_PAREN ")" - WHITESPACE " " - WITH_KW "with" - WHITESPACE " " - CUSTOM_OP - AMP "&" - AMP "&" + IDENT "tsrange" + ARG_LIST + L_PAREN "(" + NAME_REF + IDENT "b" + COMMA "," + WHITESPACE " " + NAME_REF + IDENT "c" + R_PAREN ")" + WHITESPACE " " + WITH_KW "with" + WHITESPACE " " + CUSTOM_OP + AMP "&" + AMP "&" WHITESPACE "\n" R_PAREN ")" WHITESPACE " " diff --git a/crates/squawk_parser/tests/snapshots/tests__create_table_ok.snap b/crates/squawk_parser/tests/snapshots/tests__create_table_ok.snap index b471a1a2..4581d710 100644 --- a/crates/squawk_parser/tests/snapshots/tests__create_table_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__create_table_ok.snap @@ -1967,27 +1967,28 @@ SOURCE_FILE NAME_REF IDENT "btree" WHITESPACE " " - CONSTRAINT_EXCLUSIONS + CONSTRAINT_EXCLUSION_LIST L_PAREN "(" WHITESPACE " " - NAME_REF - IDENT "a" - WHITESPACE " " - WITH_KW "with" - WHITESPACE " " - PATH + CONSTRAINT_EXCLUSION + NAME_REF + IDENT "a" + WHITESPACE " " + WITH_KW "with" + WHITESPACE " " PATH PATH + PATH + PATH_SEGMENT + NAME_REF + IDENT "f" + DOT "." PATH_SEGMENT NAME_REF - IDENT "f" + IDENT "buzz" DOT "." PATH_SEGMENT - NAME_REF - IDENT "buzz" - DOT "." - PATH_SEGMENT - R_ANGLE ">" + R_ANGLE ">" WHITESPACE " " R_PAREN ")" WHITESPACE " " @@ -2056,30 +2057,32 @@ SOURCE_FILE NAME_REF IDENT "btree" WHITESPACE " " - CONSTRAINT_EXCLUSIONS + CONSTRAINT_EXCLUSION_LIST L_PAREN "(" WHITESPACE " " - NAME_REF - IDENT "a" - WHITESPACE " " - WITH_KW "with" - WHITESPACE " " - PATH + CONSTRAINT_EXCLUSION + NAME_REF + IDENT "a" + WHITESPACE " " + WITH_KW "with" + WHITESPACE " " PATH + PATH + PATH_SEGMENT + NAME_REF + IDENT "buzz" + DOT "." PATH_SEGMENT - NAME_REF - IDENT "buzz" - DOT "." - PATH_SEGMENT - R_ANGLE ">" + R_ANGLE ">" COMMA "," WHITESPACE " " - NAME_REF - IDENT "b" - WHITESPACE " " - WITH_KW "with" - WHITESPACE " " - L_ANGLE "<" + CONSTRAINT_EXCLUSION + NAME_REF + IDENT "b" + WHITESPACE " " + WITH_KW "with" + WHITESPACE " " + L_ANGLE "<" WHITESPACE " " R_PAREN ")" WHITESPACE " \n " @@ -2160,15 +2163,16 @@ SOURCE_FILE NAME_REF IDENT "btree" WHITESPACE " " - CONSTRAINT_EXCLUSIONS + CONSTRAINT_EXCLUSION_LIST L_PAREN "(" WHITESPACE " " - NAME_REF - IDENT "a" - WHITESPACE " " - WITH_KW "with" - WHITESPACE " " - R_ANGLE ">" + CONSTRAINT_EXCLUSION + NAME_REF + IDENT "a" + WHITESPACE " " + WITH_KW "with" + WHITESPACE " " + R_ANGLE ">" WHITESPACE " " R_PAREN ")" WHITESPACE " \n " @@ -2490,22 +2494,23 @@ SOURCE_FILE NAME_REF IDENT "btree" WHITESPACE " " - CONSTRAINT_EXCLUSIONS + CONSTRAINT_EXCLUSION_LIST L_PAREN "(" WHITESPACE " " - NAME_REF - IDENT "a" - WHITESPACE " " - WITH_KW "with" - WHITESPACE " " - PATH + CONSTRAINT_EXCLUSION + NAME_REF + IDENT "a" + WHITESPACE " " + WITH_KW "with" + WHITESPACE " " PATH + PATH + PATH_SEGMENT + NAME_REF + IDENT "buzz" + DOT "." PATH_SEGMENT - NAME_REF - IDENT "buzz" - DOT "." - PATH_SEGMENT - R_ANGLE ">" + R_ANGLE ">" WHITESPACE " " R_PAREN ")" WHITESPACE " " diff --git a/crates/squawk_parser/tests/snapshots/tests__select_cte_err.snap b/crates/squawk_parser/tests/snapshots/tests__select_cte_err.snap index fad12299..1b4ecf99 100644 --- a/crates/squawk_parser/tests/snapshots/tests__select_cte_err.snap +++ b/crates/squawk_parser/tests/snapshots/tests__select_cte_err.snap @@ -237,9 +237,101 @@ SOURCE_FILE LITERAL INT_NUMBER "2" SEMICOLON ";" + WHITESPACE "\n\n" + COMMENT "-- table name isn't an plain ident" + WHITESPACE "\n" + SELECT + WITH_CLAUSE + WITH_KW "with" + WHITESPACE " \n " + WITH_TABLE + NAME + IDENT "a" + WHITESPACE " " + AS_KW "as" + WHITESPACE " " + L_PAREN "(" + WHITESPACE "\n " + SELECT + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "1" + WHITESPACE "\n " + R_PAREN ")" + WHITESPACE " " + COMMENT "-- <-- missing a comma" + WHITESPACE "\n " + WITH_TABLE + NAME + ROW_KW "row" + WHITESPACE " " + AS_KW "as" + WHITESPACE " " + L_PAREN "(" + SELECT + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "1" + R_PAREN ")" + WHITESPACE "\n" + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "1" + SEMICOLON ";" + WHITESPACE "\n\n\n" + COMMENT "-- extra comma with values (we didn't support values before)" + WHITESPACE "\n" + VALUES + WITH_CLAUSE + WITH_KW "with" + WHITESPACE " \n " + WITH_TABLE + NAME + IDENT "a" + WHITESPACE " " + AS_KW "as" + WHITESPACE " " + L_PAREN "(" + WHITESPACE "\n " + SELECT + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "1" + WHITESPACE "\n " + R_PAREN ")" + ERROR + COMMA "," + WHITESPACE " " + COMMENT "-- <-- extra comma" + WHITESPACE "\n" + VALUES_KW "values" + WHITESPACE " " + L_PAREN "(" + LITERAL + INT_NUMBER "2" + R_PAREN ")" + SEMICOLON ";" WHITESPACE "\n" --- ERROR@24: unexpected comma ERROR@140: unexpected comma, expected a column name ERROR@270: expected COMMA ERROR@357: missing comma +ERROR@492: missing comma +ERROR@645: unexpected comma diff --git a/crates/squawk_parser/tests/snapshots/tests__vacuum_ok.snap b/crates/squawk_parser/tests/snapshots/tests__vacuum_ok.snap index a4c40dd3..1772a3ca 100644 --- a/crates/squawk_parser/tests/snapshots/tests__vacuum_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__vacuum_ok.snap @@ -14,12 +14,15 @@ SOURCE_FILE VACUUM VACUUM_KW "VACUUM" WHITESPACE " " - L_PAREN "(" - VERBOSE_KW "VERBOSE" - COMMA "," - WHITESPACE " " - ANALYZE_KW "ANALYZE" - R_PAREN ")" + VACUUM_OPTION_LIST + L_PAREN "(" + VACUUM_OPTION + VERBOSE_KW "VERBOSE" + COMMA "," + WHITESPACE " " + VACUUM_OPTION + ANALYZE_KW "ANALYZE" + R_PAREN ")" WHITESPACE " " RELATION_NAME PATH @@ -33,161 +36,192 @@ SOURCE_FILE VACUUM VACUUM_KW "VACUUM" WHITESPACE " " - L_PAREN "(" - WHITESPACE "\n " - FULL_KW "full" - COMMA "," - WHITESPACE " \n " - FULL_KW "full" - WHITESPACE " " - LITERAL - TRUE_KW "true" - COMMA "," - WHITESPACE " \n " - FULL_KW "full" - WHITESPACE " " - LITERAL - FALSE_KW "false" - COMMA "," - WHITESPACE " \n " - ANALYZE_KW "analyze" - COMMA "," - WHITESPACE "\n " - ANALYZE_KW "analyze" - WHITESPACE " " - LITERAL - TRUE_KW "true" - COMMA "," - WHITESPACE "\n " - ANALYZE_KW "analyze" - WHITESPACE " " - LITERAL - FALSE_KW "false" - COMMA "," - WHITESPACE "\n " - IDENT "disable_page_skipping" - COMMA "," - WHITESPACE "\n " - IDENT "disable_page_skipping" - WHITESPACE " " - LITERAL - TRUE_KW "true" - COMMA "," - WHITESPACE "\n " - IDENT "disable_page_skipping" - WHITESPACE " " - LITERAL - FALSE_KW "false" - COMMA "," - WHITESPACE "\n " - IDENT "skip_locked" - COMMA "," - WHITESPACE "\n " - IDENT "skip_locked" - WHITESPACE " " - LITERAL - TRUE_KW "true" - COMMA "," - WHITESPACE "\n " - IDENT "skip_locked" - WHITESPACE " " - LITERAL - FALSE_KW "false" - COMMA "," - WHITESPACE "\n " - IDENT "index_cleanup" - WHITESPACE " " - IDENT "auto" - COMMA "," - WHITESPACE "\n " - IDENT "index_cleanup" - WHITESPACE " " - ON_KW "on" - COMMA "," - WHITESPACE "\n " - IDENT "index_cleanup" - WHITESPACE " " - OFF_KW "off" - COMMA "," - WHITESPACE "\n " - IDENT "process_main" - COMMA "," - WHITESPACE "\n " - IDENT "process_main" - WHITESPACE " " - LITERAL - TRUE_KW "true" - COMMA "," - WHITESPACE "\n " - IDENT "process_main" - WHITESPACE " " - LITERAL - FALSE_KW "false" - COMMA "," - WHITESPACE "\n " - TRUNCATE_KW "truncate" - COMMA "," - WHITESPACE "\n " - TRUNCATE_KW "truncate" - WHITESPACE " " - LITERAL - TRUE_KW "true" - COMMA "," - WHITESPACE "\n " - TRUNCATE_KW "truncate" - WHITESPACE " " - LITERAL - FALSE_KW "false" - COMMA "," - WHITESPACE "\n " - PARALLEL_KW "parallel" - WHITESPACE " " - LITERAL - INT_NUMBER "100" - COMMA "," - WHITESPACE "\n " - IDENT "skip_database_stats" - COMMA "," - WHITESPACE "\n " - IDENT "skip_database_stats" - WHITESPACE " " - LITERAL - TRUE_KW "true" - COMMA "," - WHITESPACE "\n " - IDENT "skip_database_stats" - WHITESPACE " " - LITERAL - FALSE_KW "false" - COMMA "," - WHITESPACE "\n " - IDENT "only_database_stats" - COMMA "," - WHITESPACE "\n " - IDENT "only_database_stats" - WHITESPACE " " - LITERAL - TRUE_KW "true" - COMMA "," - WHITESPACE "\n " - IDENT "only_database_stats" - WHITESPACE " " - LITERAL - FALSE_KW "false" - COMMA "," - WHITESPACE "\n " - IDENT "buffer_usage_limit" - WHITESPACE " " - LITERAL - INT_NUMBER "10" - COMMA "," - WHITESPACE "\n " - IDENT "buffer_usage_limit" - WHITESPACE " " - LITERAL - STRING "'10 TB'" - WHITESPACE "\n" - R_PAREN ")" + VACUUM_OPTION_LIST + L_PAREN "(" + WHITESPACE "\n " + VACUUM_OPTION + FULL_KW "full" + COMMA "," + WHITESPACE " \n " + VACUUM_OPTION + FULL_KW "full" + WHITESPACE " " + LITERAL + TRUE_KW "true" + COMMA "," + WHITESPACE " \n " + VACUUM_OPTION + FULL_KW "full" + WHITESPACE " " + LITERAL + FALSE_KW "false" + COMMA "," + WHITESPACE " \n " + VACUUM_OPTION + ANALYZE_KW "analyze" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + ANALYZE_KW "analyze" + WHITESPACE " " + LITERAL + TRUE_KW "true" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + ANALYZE_KW "analyze" + WHITESPACE " " + LITERAL + FALSE_KW "false" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "disable_page_skipping" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "disable_page_skipping" + WHITESPACE " " + LITERAL + TRUE_KW "true" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "disable_page_skipping" + WHITESPACE " " + LITERAL + FALSE_KW "false" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "skip_locked" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "skip_locked" + WHITESPACE " " + LITERAL + TRUE_KW "true" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "skip_locked" + WHITESPACE " " + LITERAL + FALSE_KW "false" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "index_cleanup" + WHITESPACE " " + IDENT "auto" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "index_cleanup" + WHITESPACE " " + ON_KW "on" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "index_cleanup" + WHITESPACE " " + OFF_KW "off" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "process_main" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "process_main" + WHITESPACE " " + LITERAL + TRUE_KW "true" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "process_main" + WHITESPACE " " + LITERAL + FALSE_KW "false" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + TRUNCATE_KW "truncate" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + TRUNCATE_KW "truncate" + WHITESPACE " " + LITERAL + TRUE_KW "true" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + TRUNCATE_KW "truncate" + WHITESPACE " " + LITERAL + FALSE_KW "false" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + PARALLEL_KW "parallel" + WHITESPACE " " + LITERAL + INT_NUMBER "100" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "skip_database_stats" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "skip_database_stats" + WHITESPACE " " + LITERAL + TRUE_KW "true" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "skip_database_stats" + WHITESPACE " " + LITERAL + FALSE_KW "false" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "only_database_stats" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "only_database_stats" + WHITESPACE " " + LITERAL + TRUE_KW "true" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "only_database_stats" + WHITESPACE " " + LITERAL + FALSE_KW "false" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "buffer_usage_limit" + WHITESPACE " " + LITERAL + INT_NUMBER "10" + COMMA "," + WHITESPACE "\n " + VACUUM_OPTION + IDENT "buffer_usage_limit" + WHITESPACE " " + LITERAL + STRING "'10 TB'" + WHITESPACE "\n" + R_PAREN ")" WHITESPACE " " RELATION_NAME PATH diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 5dd63dc2..8e9f8d90 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -1781,10 +1781,21 @@ impl CompressionMethod { } #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ConstraintExclusions { +pub struct ConstraintExclusion { pub(crate) syntax: SyntaxNode, } -impl ConstraintExclusions { +impl ConstraintExclusion { + #[inline] + pub fn with_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::WITH_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ConstraintExclusionList { + pub(crate) syntax: SyntaxNode, +} +impl ConstraintExclusionList { #[inline] pub fn exclude_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::EXCLUDE_KW) @@ -4579,7 +4590,7 @@ pub struct ExcludeConstraint { } impl ExcludeConstraint { #[inline] - pub fn constraint_exclusions(&self) -> Option { + pub fn constraint_exclusion_list(&self) -> Option { support::child(&self.syntax) } #[inline] @@ -9544,12 +9555,46 @@ pub struct Vacuum { pub(crate) syntax: SyntaxNode, } impl Vacuum { + #[inline] + pub fn vacuum_option_list(&self) -> Option { + support::child(&self.syntax) + } #[inline] pub fn vacuum_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::VACUUM_KW) } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VacuumOption { + pub(crate) syntax: SyntaxNode, +} +impl VacuumOption { + #[inline] + pub fn full_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::FULL_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VacuumOptionList { + pub(crate) syntax: SyntaxNode, +} +impl VacuumOptionList { + #[inline] + pub fn vacuum_options(&self) -> AstChildren { + support::children(&self.syntax) + } + #[inline] + pub fn l_paren_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::L_PAREN) + } + #[inline] + pub fn r_paren_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::R_PAREN) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ValidateConstraint { pub(crate) syntax: SyntaxNode, @@ -11947,10 +11992,28 @@ impl AstNode for CompressionMethod { &self.syntax } } -impl AstNode for ConstraintExclusions { +impl AstNode for ConstraintExclusion { #[inline] fn can_cast(kind: SyntaxKind) -> bool { - kind == SyntaxKind::CONSTRAINT_EXCLUSIONS + kind == SyntaxKind::CONSTRAINT_EXCLUSION + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for ConstraintExclusionList { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::CONSTRAINT_EXCLUSION_LIST } #[inline] fn cast(syntax: SyntaxNode) -> Option { @@ -18805,6 +18868,42 @@ impl AstNode for Vacuum { &self.syntax } } +impl AstNode for VacuumOption { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::VACUUM_OPTION + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for VacuumOptionList { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::VACUUM_OPTION_LIST + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for ValidateConstraint { #[inline] fn can_cast(kind: SyntaxKind) -> bool { diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 88325aba..bba51536 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -1160,14 +1160,17 @@ XmlColumnOption = ConstraintIndexMethod = 'using' -ConstraintExclusions = +ConstraintExclusionList = 'exclude' +ConstraintExclusion = + 'with' + ConstraintWhereClause = 'where' ExcludeConstraint = - 'exclude' ConstraintIndexMethod? ConstraintExclusions + 'exclude' ConstraintIndexMethod? ConstraintExclusionList FrameClause = ('range' | 'rows' | 'groups') @@ -1587,8 +1590,15 @@ Fetch = Close = 'close' +VacuumOptionList = + '(' (VacuumOption (',' VacuumOption)*)? ')' + Vacuum = 'vacuum' + VacuumOptionList? + +VacuumOption = + 'full' Copy = 'copy' From ae8463ab8238c276e6c10e3e3936b17e77667426 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Fri, 10 Oct 2025 17:34:26 -0400 Subject: [PATCH 2/4] add some more tests --- crates/squawk_parser/tests/data/err/copy.sql | 2 + .../tests/data/err/create_function.sql | 6 ++ .../tests/data/err/create_index.sql | 2 + .../tests/data/err/create_table.sql | 7 ++ .../tests/data/err/drop_database.sql | 2 + .../squawk_parser/tests/data/err/prepare.sql | 3 + .../squawk_parser/tests/data/err/reindex.sql | 2 + .../squawk_parser/tests/data/err/vacuum.sql | 2 + .../tests/snapshots/tests__copy_err.snap | 47 +++++++++++ .../snapshots/tests__create_function_err.snap | 60 +++++++++++++- .../snapshots/tests__create_index_err.snap | 47 +++++++++++ .../snapshots/tests__create_table_err.snap | 79 ++++++++++++++++++- .../snapshots/tests__drop_database_err.snap | 28 +++++++ .../tests/snapshots/tests__prepare_err.snap | 75 ++++++++++++++++++ .../tests/snapshots/tests__reindex_err.snap | 32 ++++++++ .../tests/snapshots/tests__vacuum_err.snap | 32 ++++++++ 16 files changed, 422 insertions(+), 4 deletions(-) create mode 100644 crates/squawk_parser/tests/data/err/copy.sql create mode 100644 crates/squawk_parser/tests/data/err/create_index.sql create mode 100644 crates/squawk_parser/tests/data/err/drop_database.sql create mode 100644 crates/squawk_parser/tests/data/err/prepare.sql create mode 100644 crates/squawk_parser/tests/data/err/reindex.sql create mode 100644 crates/squawk_parser/tests/data/err/vacuum.sql create mode 100644 crates/squawk_parser/tests/snapshots/tests__copy_err.snap create mode 100644 crates/squawk_parser/tests/snapshots/tests__create_index_err.snap create mode 100644 crates/squawk_parser/tests/snapshots/tests__drop_database_err.snap create mode 100644 crates/squawk_parser/tests/snapshots/tests__prepare_err.snap create mode 100644 crates/squawk_parser/tests/snapshots/tests__reindex_err.snap create mode 100644 crates/squawk_parser/tests/snapshots/tests__vacuum_err.snap diff --git a/crates/squawk_parser/tests/data/err/copy.sql b/crates/squawk_parser/tests/data/err/copy.sql new file mode 100644 index 00000000..bef9ad68 --- /dev/null +++ b/crates/squawk_parser/tests/data/err/copy.sql @@ -0,0 +1,2 @@ +-- missing a couple commas +copy x (i y) from '/tmp/input.file' (on_error ignore log_verbosity verbose); diff --git a/crates/squawk_parser/tests/data/err/create_function.sql b/crates/squawk_parser/tests/data/err/create_function.sql index 77b38476..27f403d4 100644 --- a/crates/squawk_parser/tests/data/err/create_function.sql +++ b/crates/squawk_parser/tests/data/err/create_function.sql @@ -1,2 +1,8 @@ +create function f() +-- missing comma +returns table (a text b int) +as '' language sql; + -- regression partial definition create function + diff --git a/crates/squawk_parser/tests/data/err/create_index.sql b/crates/squawk_parser/tests/data/err/create_index.sql new file mode 100644 index 00000000..5e9dff41 --- /dev/null +++ b/crates/squawk_parser/tests/data/err/create_index.sql @@ -0,0 +1,2 @@ +-- missing comma +create index i on t (a nulls first b nulls first); diff --git a/crates/squawk_parser/tests/data/err/create_table.sql b/crates/squawk_parser/tests/data/err/create_table.sql index fbfae173..58862f39 100644 --- a/crates/squawk_parser/tests/data/err/create_table.sql +++ b/crates/squawk_parser/tests/data/err/create_table.sql @@ -46,6 +46,13 @@ create unlogged table t ( ) ); +-- exclude missing a comma +create table t ( + a int, + b text, + exclude using btree ( a with buzz.> b with <) +); + create table z ( a int ) diff --git a/crates/squawk_parser/tests/data/err/drop_database.sql b/crates/squawk_parser/tests/data/err/drop_database.sql new file mode 100644 index 00000000..c263d21a --- /dev/null +++ b/crates/squawk_parser/tests/data/err/drop_database.sql @@ -0,0 +1,2 @@ +-- missing comma +drop database d with ( force force ); diff --git a/crates/squawk_parser/tests/data/err/prepare.sql b/crates/squawk_parser/tests/data/err/prepare.sql new file mode 100644 index 00000000..74a45e47 --- /dev/null +++ b/crates/squawk_parser/tests/data/err/prepare.sql @@ -0,0 +1,3 @@ +-- missing commas +PREPARE fooplan (int text bool, numeric) AS + INSERT INTO foo VALUES($1, $2, $3, $4); diff --git a/crates/squawk_parser/tests/data/err/reindex.sql b/crates/squawk_parser/tests/data/err/reindex.sql new file mode 100644 index 00000000..f9403dd4 --- /dev/null +++ b/crates/squawk_parser/tests/data/err/reindex.sql @@ -0,0 +1,2 @@ +-- missing commas +reindex (concurrently verbose tablespace t) index i; diff --git a/crates/squawk_parser/tests/data/err/vacuum.sql b/crates/squawk_parser/tests/data/err/vacuum.sql new file mode 100644 index 00000000..08268eb6 --- /dev/null +++ b/crates/squawk_parser/tests/data/err/vacuum.sql @@ -0,0 +1,2 @@ +-- missing some commas +vacuum (full analyze false skip_locked true); diff --git a/crates/squawk_parser/tests/snapshots/tests__copy_err.snap b/crates/squawk_parser/tests/snapshots/tests__copy_err.snap new file mode 100644 index 00000000..00f1f502 --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__copy_err.snap @@ -0,0 +1,47 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: crates/squawk_parser/tests/data/err/copy.sql +--- +SOURCE_FILE + COMMENT "-- missing a couple commas" + WHITESPACE "\n" + COPY + COPY_KW "copy" + WHITESPACE " " + PATH + PATH_SEGMENT + NAME + IDENT "x" + WHITESPACE " " + COLUMN_LIST + L_PAREN "(" + COLUMN + NAME_REF + IDENT "i" + WHITESPACE " " + COLUMN + NAME_REF + IDENT "y" + R_PAREN ")" + WHITESPACE " " + FROM_KW "from" + WHITESPACE " " + LITERAL + STRING "'/tmp/input.file'" + WHITESPACE " " + L_PAREN "(" + NAME + IDENT "on_error" + WHITESPACE " " + IDENT "ignore" + WHITESPACE " " + NAME + IDENT "log_verbosity" + WHITESPACE " " + VERBOSE_KW "verbose" + R_PAREN ")" + SEMICOLON ";" + WHITESPACE "\n" +--- +ERROR@36: expected COMMA +ERROR@79: expected COMMA diff --git a/crates/squawk_parser/tests/snapshots/tests__create_function_err.snap b/crates/squawk_parser/tests/snapshots/tests__create_function_err.snap index bc5d4085..72f12fda 100644 --- a/crates/squawk_parser/tests/snapshots/tests__create_function_err.snap +++ b/crates/squawk_parser/tests/snapshots/tests__create_function_err.snap @@ -3,13 +3,67 @@ source: crates/squawk_parser/tests/tests.rs input_file: crates/squawk_parser/tests/data/err/create_function.sql --- SOURCE_FILE + CREATE_FUNCTION + CREATE_KW "create" + WHITESPACE " " + FUNCTION_KW "function" + WHITESPACE " " + PATH + PATH_SEGMENT + NAME + IDENT "f" + PARAM_LIST + L_PAREN "(" + R_PAREN ")" + WHITESPACE "\n" + COMMENT "-- missing comma" + WHITESPACE "\n" + RET_TYPE + RETURNS_KW "returns" + WHITESPACE " " + TABLE_KW "table" + WHITESPACE " " + L_PAREN "(" + NAME_REF + IDENT "a" + WHITESPACE " " + PATH_TYPE + PATH + PATH_SEGMENT + NAME_REF + TEXT_KW "text" + WHITESPACE " " + NAME_REF + IDENT "b" + WHITESPACE " " + PATH_TYPE + PATH + PATH_SEGMENT + NAME_REF + INT_KW "int" + R_PAREN ")" + WHITESPACE "\n" + FUNC_OPTION_LIST + AS_FUNC_OPTION + AS_KW "as" + WHITESPACE " " + LITERAL + STRING "''" + WHITESPACE " " + LANGUAGE_FUNC_OPTION + LANGUAGE_KW "language" + WHITESPACE " " + SQL_KW "sql" + SEMICOLON ";" + WHITESPACE "\n\n" COMMENT "-- regression partial definition" WHITESPACE "\n" CREATE_FUNCTION CREATE_KW "create" WHITESPACE " " FUNCTION_KW "function" - WHITESPACE "\n" + WHITESPACE "\n\n" --- -ERROR@48: expected path name -ERROR@48: expected L_PAREN +ERROR@58: expected COMMA +ERROR@136: expected path name +ERROR@136: expected L_PAREN diff --git a/crates/squawk_parser/tests/snapshots/tests__create_index_err.snap b/crates/squawk_parser/tests/snapshots/tests__create_index_err.snap new file mode 100644 index 00000000..2ed22359 --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__create_index_err.snap @@ -0,0 +1,47 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: crates/squawk_parser/tests/data/err/create_index.sql +--- +SOURCE_FILE + COMMENT "-- missing comma" + WHITESPACE "\n" + CREATE_INDEX + CREATE_KW "create" + WHITESPACE " " + INDEX_KW "index" + WHITESPACE " " + NAME + IDENT "i" + WHITESPACE " " + ON_KW "on" + WHITESPACE " " + RELATION_NAME + PATH + PATH_SEGMENT + NAME_REF + IDENT "t" + WHITESPACE " " + INDEX_PARAMS + L_PAREN "(" + PARTITION_ITEM + NAME_REF + IDENT "a" + WHITESPACE " " + NULLS_FIRST + NULLS_KW "nulls" + WHITESPACE " " + FIRST_KW "first" + WHITESPACE " " + PARTITION_ITEM + NAME_REF + IDENT "b" + WHITESPACE " " + NULLS_FIRST + NULLS_KW "nulls" + WHITESPACE " " + FIRST_KW "first" + R_PAREN ")" + SEMICOLON ";" + WHITESPACE "\n" +--- +ERROR@51: expected COMMA diff --git a/crates/squawk_parser/tests/snapshots/tests__create_table_err.snap b/crates/squawk_parser/tests/snapshots/tests__create_table_err.snap index 1ecd591f..14784698 100644 --- a/crates/squawk_parser/tests/snapshots/tests__create_table_err.snap +++ b/crates/squawk_parser/tests/snapshots/tests__create_table_err.snap @@ -422,6 +422,82 @@ SOURCE_FILE R_PAREN ")" SEMICOLON ";" WHITESPACE "\n\n" + CREATE_TABLE + COMMENT "-- exclude missing a comma" + WHITESPACE "\n" + CREATE_KW "create" + WHITESPACE " " + TABLE_KW "table" + WHITESPACE " " + PATH + PATH_SEGMENT + NAME + IDENT "t" + WHITESPACE " " + TABLE_ARG_LIST + L_PAREN "(" + WHITESPACE "\n " + COLUMN + NAME + IDENT "a" + WHITESPACE " " + PATH_TYPE + PATH + PATH_SEGMENT + NAME_REF + INT_KW "int" + COMMA "," + WHITESPACE "\n " + COLUMN + NAME + IDENT "b" + WHITESPACE " " + PATH_TYPE + PATH + PATH_SEGMENT + NAME_REF + TEXT_KW "text" + COMMA "," + WHITESPACE "\n " + EXCLUDE_CONSTRAINT + EXCLUDE_KW "exclude" + WHITESPACE " " + CONSTRAINT_INDEX_METHOD + USING_KW "using" + WHITESPACE " " + NAME_REF + IDENT "btree" + WHITESPACE " " + CONSTRAINT_EXCLUSION_LIST + L_PAREN "(" + WHITESPACE " " + CONSTRAINT_EXCLUSION + NAME_REF + IDENT "a" + WHITESPACE " " + WITH_KW "with" + WHITESPACE " " + PATH + PATH + PATH_SEGMENT + NAME_REF + IDENT "buzz" + DOT "." + PATH_SEGMENT + R_ANGLE ">" + WHITESPACE " " + CONSTRAINT_EXCLUSION + NAME_REF + IDENT "b" + WHITESPACE " " + WITH_KW "with" + WHITESPACE " " + L_ANGLE "<" + R_PAREN ")" + WHITESPACE " \n" + R_PAREN ")" + SEMICOLON ";" + WHITESPACE "\n\n" CREATE_TABLE CREATE_KW "create" WHITESPACE " " @@ -495,4 +571,5 @@ ERROR@198: unexpected comma ERROR@199: unexpected comma ERROR@200: unexpected comma ERROR@201: unexpected comma -ERROR@947: expected SEMICOLON +ERROR@1021: expected COMMA +ERROR@1063: expected SEMICOLON diff --git a/crates/squawk_parser/tests/snapshots/tests__drop_database_err.snap b/crates/squawk_parser/tests/snapshots/tests__drop_database_err.snap new file mode 100644 index 00000000..066e844a --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__drop_database_err.snap @@ -0,0 +1,28 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: crates/squawk_parser/tests/data/err/drop_database.sql +--- +SOURCE_FILE + COMMENT "-- missing comma" + WHITESPACE "\n" + DROP_DATABASE + DROP_KW "drop" + WHITESPACE " " + DATABASE_KW "database" + WHITESPACE " " + NAME_REF + IDENT "d" + WHITESPACE " " + WITH_KW "with" + WHITESPACE " " + L_PAREN "(" + WHITESPACE " " + FORCE_KW "force" + WHITESPACE " " + FORCE_KW "force" + WHITESPACE " " + R_PAREN ")" + SEMICOLON ";" + WHITESPACE "\n" +--- +ERROR@45: expected COMMA diff --git a/crates/squawk_parser/tests/snapshots/tests__prepare_err.snap b/crates/squawk_parser/tests/snapshots/tests__prepare_err.snap new file mode 100644 index 00000000..fb5ec86b --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__prepare_err.snap @@ -0,0 +1,75 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: crates/squawk_parser/tests/data/err/prepare.sql +--- +SOURCE_FILE + COMMENT "-- missing commas" + WHITESPACE "\n" + PREPARE + PREPARE_KW "PREPARE" + WHITESPACE " " + NAME + IDENT "fooplan" + WHITESPACE " " + L_PAREN "(" + PATH_TYPE + PATH + PATH_SEGMENT + NAME_REF + INT_KW "int" + WHITESPACE " " + PATH_TYPE + PATH + PATH_SEGMENT + NAME_REF + TEXT_KW "text" + WHITESPACE " " + PATH_TYPE + PATH + PATH_SEGMENT + NAME_REF + IDENT "bool" + COMMA "," + WHITESPACE " " + PATH_TYPE + PATH + PATH_SEGMENT + NAME_REF + NUMERIC_KW "numeric" + R_PAREN ")" + WHITESPACE " " + AS_KW "AS" + WHITESPACE "\n " + INSERT + INSERT_KW "INSERT" + WHITESPACE " " + INTO_KW "INTO" + WHITESPACE " " + PATH + PATH_SEGMENT + NAME_REF + IDENT "foo" + WHITESPACE " " + VALUES + VALUES_KW "VALUES" + L_PAREN "(" + LITERAL + POSITIONAL_PARAM "$1" + COMMA "," + WHITESPACE " " + LITERAL + POSITIONAL_PARAM "$2" + COMMA "," + WHITESPACE " " + LITERAL + POSITIONAL_PARAM "$3" + COMMA "," + WHITESPACE " " + LITERAL + POSITIONAL_PARAM "$4" + R_PAREN ")" + SEMICOLON ";" + WHITESPACE "\n" +--- +ERROR@38: expected COMMA +ERROR@44: expected COMMA diff --git a/crates/squawk_parser/tests/snapshots/tests__reindex_err.snap b/crates/squawk_parser/tests/snapshots/tests__reindex_err.snap new file mode 100644 index 00000000..164cdde2 --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__reindex_err.snap @@ -0,0 +1,32 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: crates/squawk_parser/tests/data/err/reindex.sql +--- +SOURCE_FILE + COMMENT "-- missing commas" + WHITESPACE "\n" + REINDEX + REINDEX_KW "reindex" + WHITESPACE " " + L_PAREN "(" + CONCURRENTLY_KW "concurrently" + WHITESPACE " " + VERBOSE_KW "verbose" + WHITESPACE " " + TABLESPACE_KW "tablespace" + WHITESPACE " " + NAME + IDENT "t" + R_PAREN ")" + WHITESPACE " " + INDEX_KW "index" + WHITESPACE " " + PATH + PATH_SEGMENT + NAME + IDENT "i" + SEMICOLON ";" + WHITESPACE "\n" +--- +ERROR@39: expected COMMA +ERROR@47: expected COMMA diff --git a/crates/squawk_parser/tests/snapshots/tests__vacuum_err.snap b/crates/squawk_parser/tests/snapshots/tests__vacuum_err.snap new file mode 100644 index 00000000..ddca3753 --- /dev/null +++ b/crates/squawk_parser/tests/snapshots/tests__vacuum_err.snap @@ -0,0 +1,32 @@ +--- +source: crates/squawk_parser/tests/tests.rs +input_file: crates/squawk_parser/tests/data/err/vacuum.sql +--- +SOURCE_FILE + COMMENT "-- missing some commas" + WHITESPACE "\n" + VACUUM + VACUUM_KW "vacuum" + WHITESPACE " " + VACUUM_OPTION_LIST + L_PAREN "(" + VACUUM_OPTION + FULL_KW "full" + WHITESPACE " " + VACUUM_OPTION + ANALYZE_KW "analyze" + WHITESPACE " " + LITERAL + FALSE_KW "false" + WHITESPACE " " + VACUUM_OPTION + IDENT "skip_locked" + WHITESPACE " " + LITERAL + TRUE_KW "true" + R_PAREN ")" + SEMICOLON ";" + WHITESPACE "\n" +--- +ERROR@35: expected COMMA +ERROR@49: expected COMMA From a2f4d4f2207153d0a900d7f192be7a8c61a903bf Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Fri, 10 Oct 2025 17:35:27 -0400 Subject: [PATCH 3/4] lint --- crates/squawk_parser/src/grammar.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index 83886562..c802d335 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -10983,7 +10983,7 @@ fn reindex(p: &mut Parser<'_>) -> CompletedMarker { COMMA, || "unexpected comma".to_string(), REINDEX_OPTION_FIRST, - |p| opt_reindex_option(p), + opt_reindex_option, ); } let name_required = match p.current() { @@ -11084,7 +11084,7 @@ fn prepare(p: &mut Parser<'_>) -> CompletedMarker { COMMA, || "unexpected comma".to_string(), NAME_REF_FIRST, - |p| opt_type_name(p), + opt_type_name, ); } p.expect(AS_KW); @@ -11537,7 +11537,7 @@ fn copy_option_list(p: &mut Parser<'_>) { COMMA, || "unexpected comma".to_string(), COL_LABEL_FIRST, - |p| opt_copy_option(p), + opt_copy_option, ); } From d5c1550f2eb40475aa1552923b0a0eab1762f343 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Fri, 10 Oct 2025 17:43:12 -0400 Subject: [PATCH 4/4] lint --- crates/squawk_ide/src/expand_selection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/squawk_ide/src/expand_selection.rs b/crates/squawk_ide/src/expand_selection.rs index 1017451a..e5e9b94b 100644 --- a/crates/squawk_ide/src/expand_selection.rs +++ b/crates/squawk_ide/src/expand_selection.rs @@ -551,7 +551,7 @@ $0 ]; let generated_list_kinds: Vec = (0..SyntaxKind::__LAST as u16) - .map(|n| SyntaxKind::from(n)) + .map(SyntaxKind::from) .filter(|kind| { format!("{:?}", kind).ends_with("_LIST") && !EXCLUDED_LIST_KINDS.contains(kind) })