diff --git a/crates/squawk_linter/src/rules/prefer_timestamptz.rs b/crates/squawk_linter/src/rules/prefer_timestamptz.rs index efe2ee96..6f35a0f4 100644 --- a/crates/squawk_linter/src/rules/prefer_timestamptz.rs +++ b/crates/squawk_linter/src/rules/prefer_timestamptz.rs @@ -32,10 +32,8 @@ pub fn is_not_allowed_timestamp(ty: &ast::Type) -> bool { ast::Type::BitType(_) => false, ast::Type::DoubleType(_) => false, ast::Type::TimeType(time_type) => { - if let Some(ty_name) = time_type.name_ref() { - if ty_name.text() == "timestamp" && time_type.with_timezone().is_none() { - return true; - } + if time_type.timestamp_token().is_some() && time_type.with_timezone().is_none() { + return true; } false } diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index 30eef395..c6b6855f 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -811,6 +811,7 @@ pub enum SyntaxKind { JOIN_LEFT, JOIN_RIGHT, JOIN_TYPE, + JOIN_USING_CLAUSE, JSON_BEHAVIOR_CLAUSE, JSON_FORMAT_CLAUSE, JSON_KEYS_UNIQUE_CLAUSE, diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index 2d106347..a567656b 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -46,34 +46,26 @@ fn literal(p: &mut Parser<'_>) -> Option { // array[1,2,3] // array(select 1) fn array_expr(p: &mut Parser<'_>, m: Option) -> CompletedMarker { + assert!(p.at(L_PAREN) || p.at(L_BRACK)); let m = m.unwrap_or_else(|| p.start()); - // `[` or `(` - let closing = if p.eat(L_PAREN) { - R_PAREN + if p.at(L_PAREN) && p.nth_at_ts(1, SELECT_FIRST) { + p.expect(L_PAREN); + select(p, None, &SelectRestrictions::default()); + p.expect(R_PAREN); } else { - p.expect(L_BRACK); - R_BRACK - }; - while !p.at(EOF) && !p.at(closing) { - if p.at_ts(SELECT_FIRST) - && (select(p, None, &SelectRestrictions::default()).is_none() - || p.at(EOF) - || p.at(closing)) - { - break; - } - if expr(p).is_none() { - break; - } - if p.at(COMMA) && p.nth_at(1, closing) { - p.err_and_bump("unexpected trailing comma"); - break; - } - if !p.at(closing) && !p.expect(COMMA) { - break; - } + // `[` or `(` + let closing = if p.at(L_PAREN) { R_PAREN } else { R_BRACK }; + let opening = if p.at(L_PAREN) { L_PAREN } else { L_BRACK }; + delimited( + p, + opening, + closing, + COMMA, + || "unexpected comma".to_string(), + EXPR_FIRST, + |p| opt_expr(p).is_some(), + ); } - p.expect(closing); m.complete(p, ARRAY_EXPR) } @@ -277,14 +269,7 @@ fn overlay_fn(p: &mut Parser<'_>) -> CompletedMarker { p.error("expected an expression"); } } else if p.eat(COMMA) { - while !p.at(EOF) { - if expr(p).is_none() { - p.error("expected an expression"); - } - if !p.eat(COMMA) { - break; - } - } + expr_list(p); } }) } @@ -392,15 +377,7 @@ fn substring_fn(p: &mut Parser<'_>) -> CompletedMarker { } } _ if p.eat(COMMA) => { - // normal function call - while !p.at(EOF) { - if expr(p).is_none() { - p.error("expected an expression"); - } - if !p.eat(COMMA) { - break; - } - } + expr_list(p); } _ => {} } @@ -1428,7 +1405,7 @@ fn delimited( first_set: TokenSet, mut parser: impl FnMut(&mut Parser<'_>) -> bool, ) { - p.bump(bra); + p.expect(bra); while !p.at(ket) && !p.at(EOF) { if p.at(delim) { // Recover if an argument is missing and only got a delimiter, @@ -1718,29 +1695,14 @@ fn opt_type_name_with(p: &mut Parser<'_>, type_args_enabled: bool) -> Option char_type(p), TIMESTAMP_KW | TIME_KW => { - let name_ref = p.start(); p.bump_any(); - name_ref.complete(p, NAME_REF); if p.eat(L_PAREN) { if expr(p).is_none() { p.error("expected an expression"); } p.expect(R_PAREN); } - let m = p.start(); - if p.at(WITH_KW) || p.at(WITHOUT_KW) { - let kind = if p.eat(WITH_KW) { - WITH_TIMEZONE - } else { - p.bump(WITHOUT_KW); - WITHOUT_TIMEZONE - }; - p.expect(TIME_KW); - p.expect(ZONE_KW); - m.complete(p, kind); - } else { - m.abandon(p); - } + opt_with_timezone(p); TIME_TYPE } INTERVAL_KW => { @@ -1765,6 +1727,23 @@ fn opt_type_name_with(p: &mut Parser<'_>, type_args_enabled: bool) -> Option) { + let m = p.start(); + if p.at(WITH_KW) || p.at(WITHOUT_KW) { + let kind = if p.eat(WITH_KW) { + WITH_TIMEZONE + } else { + p.bump(WITHOUT_KW); + WITHOUT_TIMEZONE + }; + p.expect(TIME_KW); + p.expect(ZONE_KW); + m.complete(p, kind); + } else { + m.abandon(p); + } +} + fn opt_type_name(p: &mut Parser<'_>) -> bool { opt_type_name_with(p, true).is_some() } @@ -2125,6 +2104,13 @@ fn expr(p: &mut Parser) -> Option { expr_bp(p, 1, &Restrictions::default()) } +fn opt_expr(p: &mut Parser<'_>) -> Option { + if !p.at_ts(EXPR_FIRST) { + return None; + } + expr(p) +} + // Based on the Postgres grammar b_expr, it's expr without `AND`, `NOT`, `IS`, // and `IN` #[must_use] @@ -2693,7 +2679,7 @@ const JOIN_TYPE_FIRST: TokenSet = // LEFT [ OUTER ] JOIN // RIGHT [ OUTER ] JOIN // FULL [ OUTER ] JOIN -fn join_type(p: &mut Parser<'_>) -> Option { +fn join_type(p: &mut Parser<'_>) { assert!(p.at_ts(JOIN_TYPE_FIRST)); let m = p.start(); let kind = match p.current() { @@ -2727,10 +2713,11 @@ fn join_type(p: &mut Parser<'_>) -> Option { } _ => { p.error("expected join type"); - return None; + m.abandon(p); + return; } }; - Some(m.complete(p, kind)) + m.complete(p, kind); } const JOIN_FIRST: TokenSet = TokenSet::new(&[NATURAL_KW, CROSS_KW]).union(JOIN_TYPE_FIRST); @@ -3162,43 +3149,37 @@ fn join(p: &mut Parser<'_>) { assert!(p.at_ts(JOIN_FIRST)); let m = p.start(); p.eat(NATURAL_KW); - if join_type(p).is_none() { - p.error("expected join type"); - } + join_type(p); if !opt_from_item(p) { p.error("expected from_item"); } if p.at(ON_KW) { - let m = p.start(); - p.bump(ON_KW); - if expr(p).is_none() { - p.error("expected an expression"); - } - m.complete(p, ON_CLAUSE); + on_clause(p); } else if p.at(USING_KW) { - let m = p.start(); - // USING ( join_column [, ...] ) - p.expect(USING_KW); - if p.at(L_PAREN) { - column_list(p); - } else { - p.error("expected L_PAREN"); - } - { - let m = p.start(); - // [ AS join_using_alias ] - if p.eat(AS_KW) { - name(p); - m.complete(p, ALIAS); - } else { - m.abandon(p); - } - } - m.complete(p, USING_CLAUSE); + join_using_clause(p); } m.complete(p, JOIN); } +fn on_clause(p: &mut Parser<'_>) { + let m = p.start(); + p.bump(ON_KW); + if expr(p).is_none() { + p.error("expected an expression"); + } + m.complete(p, ON_CLAUSE); +} + +fn join_using_clause(p: &mut Parser<'_>) { + assert!(p.at(USING_KW)); + let m = p.start(); + // USING ( join_column [, ...] ) + p.expect(USING_KW); + column_list(p); + opt_alias(p); + m.complete(p, JOIN_USING_CLAUSE); +} + #[must_use] fn opt_numeric_literal(p: &mut Parser<'_>) -> Option { if p.at_ts(NUMERIC_FIRST) { @@ -3454,31 +3435,34 @@ fn opt_index_parameters(p: &mut Parser<'_>) { // referential_action in a FOREIGN KEY/REFERENCES constraint is: // { NO ACTION | RESTRICT | CASCADE | SET NULL [ ( column_name [, ... ] ) ] | SET DEFAULT [ ( column_name [, ... ] ) ] } -fn referential_action(p: &mut Parser<'_>) -> bool { - if p.eat(NO_KW) { - p.expect(ACTION_KW) - } else if opt_cascade_or_restrict(p) { - true - } else if p.at(SET_KW) && p.nth_at(1, NULL_KW) { - p.expect(SET_KW); - p.expect(NULL_KW); - opt_column_list(p); - true - } else if p.eat(SET_KW) { - p.expect(DEFAULT_KW); - if p.eat(L_PAREN) { - // column_name [, ... ] - while !p.at(EOF) && !p.at(COMMA) { - name_ref(p); - if !p.eat(COMMA) { - break; - } - } - return p.expect(R_PAREN); +fn referential_action(p: &mut Parser<'_>) { + match p.current() { + NO_KW => { + let m = p.start(); + p.bump(NO_KW); + p.expect(ACTION_KW); + m.complete(p, NO_ACTION); + } + CASCADE_KW | RESTRICT_KW => { + opt_cascade_or_restrict(p); + } + SET_KW if p.nth_at(1, NULL_KW) => { + let m = p.start(); + p.expect(SET_KW); + p.expect(NULL_KW); + opt_column_list(p); + m.complete(p, SET_NULL_COLUMNS); + } + SET_KW => { + let m = p.start(); + p.bump(SET_KW); + p.expect(DEFAULT_KW); + opt_column_list(p); + m.complete(p, SET_DEFAULT_COLUMNS); + } + _ => { + p.error("expected foreign key action"); } - true - } else { - false } } @@ -3531,6 +3515,15 @@ fn opt_column_constraint(p: &mut Parser<'_>) -> Option { } } +// [ column_constraint [ ... ] ] +fn opt_column_constraint_list(p: &mut Parser<'_>) { + while !p.at(EOF) { + if opt_column_constraint(p).is_none() { + break; + } + } +} + // { NOT NULL | // NULL | // CHECK ( expression ) [ NO INHERIT ] | @@ -3634,7 +3627,7 @@ fn opt_constraint_inner(p: &mut Parser<'_>) -> Option { p.expect(R_PAREN); } opt_match_type(p); - foreign_key_actions(p); + opt_foreign_key_actions(p); REFERENCES_CONSTRAINT } _ => { @@ -3689,27 +3682,39 @@ fn opt_no_inherit(p: &mut Parser<'_>) { // [ ON DELETE referential_action ] // [ ON UPDATE referential_action ] -fn foreign_key_actions(p: &mut Parser<'_>) { +fn opt_foreign_key_actions(p: &mut Parser<'_>) { // [ ON DELETE referential_action ] if p.at(ON_KW) && p.nth_at(1, DELETE_KW) { - p.expect(ON_KW); - p.expect(DELETE_KW); - referential_action(p); + on_delete_action(p); } // [ ON UPDATE referential_action ] if p.at(ON_KW) && p.nth_at(1, UPDATE_KW) { - p.expect(ON_KW); - p.expect(UPDATE_KW); - referential_action(p); + on_update_action(p); } // [ ON DELETE referential_action ] if p.at(ON_KW) && p.nth_at(1, DELETE_KW) { - p.expect(ON_KW); - p.expect(DELETE_KW); - referential_action(p); + on_delete_action(p); } } +fn on_update_action(p: &mut Parser<'_>) { + assert!(p.at(ON_KW)); + let m = p.start(); + p.expect(ON_KW); + p.expect(UPDATE_KW); + referential_action(p); + m.complete(p, ON_UPDATE_ACTION); +} + +fn on_delete_action(p: &mut Parser<'_>) { + assert!(p.at(ON_KW)); + let m = p.start(); + p.expect(ON_KW); + p.expect(DELETE_KW); + referential_action(p); + m.complete(p, ON_DELETE_ACTION); +} + const LIKE_OPTION: TokenSet = TokenSet::new(&[ COMMENTS_KW, COMPRESSION_KW, @@ -3877,38 +3882,10 @@ fn table_constraint(p: &mut Parser<'_>) -> CompletedMarker { // EXCLUDE [ USING index_method ] ( exclude_element WITH operator [, ... ] ) index_parameters [ WHERE ( predicate ) ] | EXCLUDE_KW => { p.bump(EXCLUDE_KW); - if p.at(USING_KW) { - let m = p.start(); - p.bump(USING_KW); - name_ref(p); - m.complete(p, CONSTRAINT_INDEX_METHOD); - } - 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; - } - } - p.expect(R_PAREN); - m.complete(p, CONSTRAINT_EXCLUSIONS); + opt_constraint_index_method(p); + constraint_exclusions(p); opt_index_parameters(p); - if p.at(WHERE_KW) { - let m = p.start(); - p.bump(WHERE_KW); - p.expect(L_PAREN); - if expr(p).is_none() { - p.error("expected expr"); - } - p.expect(R_PAREN); - m.complete(p, CONSTRAINT_WHERE_CLAUSE); - } + opt_constraint_where_clause(p); EXCLUDE_CONSTRAINT } NOT_KW => { @@ -3928,7 +3905,7 @@ fn table_constraint(p: &mut Parser<'_>) -> CompletedMarker { path_name_ref(p); opt_column_list(p); opt_match_type(p); - foreign_key_actions(p); + opt_foreign_key_actions(p); FOREIGN_KEY_CONSTRAINT } }; @@ -3952,6 +3929,46 @@ fn opt_nulls_not_distinct(p: &mut Parser<'_>) { } } +fn opt_constraint_where_clause(p: &mut Parser<'_>) { + if p.at(WHERE_KW) { + let m = p.start(); + p.bump(WHERE_KW); + p.expect(L_PAREN); + if expr(p).is_none() { + p.error("expected expr"); + } + p.expect(R_PAREN); + m.complete(p, CONSTRAINT_WHERE_CLAUSE); + } +} + +fn constraint_exclusions(p: &mut Parser<'_>) { + 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; + } + } + p.expect(R_PAREN); + m.complete(p, CONSTRAINT_EXCLUSIONS); +} + +fn opt_constraint_index_method(p: &mut Parser<'_>) { + if p.at(USING_KW) { + let m = p.start(); + p.bump(USING_KW); + name_ref(p); + m.complete(p, CONSTRAINT_INDEX_METHOD); + } +} + fn opt_without_overlaps(p: &mut Parser<'_>) { if p.eat(WITHOUT_KW) { p.expect(OVERLAPS_KW); @@ -4114,12 +4131,7 @@ fn col_def(p: &mut Parser<'_>, t: ColDefType) -> Option { } } opt_collate(p); - // [ column_constraint [ ... ] ] - while !p.at(EOF) { - if opt_column_constraint(p).is_none() { - break; - } - } + opt_column_constraint_list(p); Some(m.complete(p, COLUMN)) } @@ -4220,48 +4232,52 @@ fn group_by_list(p: &mut Parser<'_>) { // ambiguity, a GROUP BY name will be interpreted as an input-column name // rather than an output column name. while !p.at(EOF) && !p.at(SEMICOLON) { - let m = p.start(); - let kind = match p.current() { - ROLLUP_KW => { - p.bump_any(); - p.expect(L_PAREN); - if !expr_list(p) { - p.error("expected expression list"); - }; - p.expect(R_PAREN); - GROUPING_ROLLUP - } - CUBE_KW => { - p.bump_any(); - p.expect(L_PAREN); - if !expr_list(p) { - p.error("expected expression list"); - }; - p.expect(R_PAREN); - GROUPING_CUBE - } - GROUPING_KW if p.nth_at(1, SETS_KW) => { - p.bump(GROUPING_KW); - p.bump(SETS_KW); - p.expect(L_PAREN); - group_by_list(p); - p.expect(R_PAREN); - GROUPING_SETS - } - _ => { - if expr(p).is_none() { - p.error("expected an expression"); - } - GROUPING_EXPR - } - }; - m.complete(p, kind); + group_by_item(p); if !p.eat(COMMA) { break; } } } +fn group_by_item(p: &mut Parser<'_>) { + let m = p.start(); + let kind = match p.current() { + ROLLUP_KW => { + p.bump_any(); + p.expect(L_PAREN); + if !expr_list(p) { + p.error("expected expression list"); + }; + p.expect(R_PAREN); + GROUPING_ROLLUP + } + CUBE_KW => { + p.bump_any(); + p.expect(L_PAREN); + if !expr_list(p) { + p.error("expected expression list"); + }; + p.expect(R_PAREN); + GROUPING_CUBE + } + GROUPING_KW if p.nth_at(1, SETS_KW) => { + p.bump(GROUPING_KW); + p.bump(SETS_KW); + p.expect(L_PAREN); + group_by_list(p); + p.expect(R_PAREN); + GROUPING_SETS + } + _ => { + if expr(p).is_none() { + p.error("expected an expression"); + } + GROUPING_EXPR + } + }; + m.complete(p, kind); +} + /// fn opt_having_clause(p: &mut Parser<'_>) -> Option { if !p.at(HAVING_KW) { @@ -4342,13 +4358,8 @@ fn window_definition(p: &mut Parser<'_>) -> Option { opt_ident(p); if p.eat(PARTITION_KW) { p.expect(BY_KW); - if expr(p).is_none() { - p.error("expected an expression"); - } - while p.eat(COMMA) && !p.at(EOF) { - if expr(p).is_none() { - p.error("expected an expression"); - } + if !expr_list(p) { + p.error("expected expression") } } opt_order_by_clause(p); @@ -4817,12 +4828,7 @@ fn partition_option(p: &mut Parser<'_>) { fn opt_inherits_tables(p: &mut Parser<'_>) { if p.eat(INHERITS_KW) { p.expect(L_PAREN); - while !p.at(EOF) && !p.at(COMMA) { - path_name_ref(p); - if !p.eat(COMMA) { - break; - } - } + path_name_ref_list(p); p.expect(R_PAREN); } } @@ -5949,10 +5955,7 @@ fn alter_policy(p: &mut Parser<'_>) -> CompletedMarker { name(p); } else { if p.eat(TO_KW) { - role(p); - while !p.at(EOF) && p.eat(COMMA) { - role(p); - } + role_list(p); } if p.eat(USING_KW) { p.expect(L_PAREN); @@ -5973,6 +5976,13 @@ fn alter_policy(p: &mut Parser<'_>) -> CompletedMarker { m.complete(p, ALTER_POLICY) } +fn role_list(p: &mut Parser<'_>) { + role(p); + while !p.at(EOF) && p.eat(COMMA) { + role(p); + } +} + // ALTER OPERATOR FAMILY name USING index_method ADD // { OPERATOR strategy_number operator_name ( op_type, op_type ) // [ FOR SEARCH | FOR ORDER BY sort_family_name ] @@ -6155,10 +6165,7 @@ fn alter_materialized_view(p: &mut Parser<'_>) -> CompletedMarker { name_ref(p); if p.eat(OWNED_KW) { p.expect(BY_KW); - role(p); - while !p.at(EOF) && p.eat(COMMA) { - role(p); - } + role_list(p); } p.expect(SET_KW); p.expect(TABLESPACE_KW); @@ -6284,10 +6291,7 @@ fn alter_index(p: &mut Parser<'_>) -> CompletedMarker { path_name_ref(p); if p.eat(OWNED_KW) { p.expect(BY_KW); - role(p); - while !p.at(EOF) && p.eat(COMMA) { - role(p); - } + role_list(p); } p.expect(SET_KW); p.expect(TABLESPACE_KW); @@ -6369,10 +6373,7 @@ fn alter_group(p: &mut Parser<'_>) -> CompletedMarker { ADD_KW | DROP_KW => { p.bump_any(); p.expect(USER_KW); - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); } RENAME_KW => { p.bump(RENAME_KW); @@ -7004,18 +7005,12 @@ fn alter_default_privileges(p: &mut Parser<'_>) -> CompletedMarker { if !p.eat(ROLE_KW) && !p.eat(USER_KW) { p.error("expected ROLE or USER"); } - role(p); - while !p.at(EOF) && p.eat(COMMA) { - role(p); - } + role_list(p); } // [ IN SCHEMA schema_name [, ...] ] if p.eat(IN_KW) { - p.expect(SCHEMA_KW); - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + p.expect(SCHEMA_KW); + name_ref_list(p); } match p.current() { GRANT_KW => { @@ -7024,10 +7019,7 @@ fn alter_default_privileges(p: &mut Parser<'_>) -> CompletedMarker { p.expect(ON_KW); privilege_target_type(p); p.expect(TO_KW); - role(p); - while !p.at(EOF) && p.eat(COMMA) { - role(p); - } + role_list(p); if p.eat(WITH_KW) { p.expect(GRANT_KW); p.expect(OPTION_KW); @@ -7043,10 +7035,7 @@ fn alter_default_privileges(p: &mut Parser<'_>) -> CompletedMarker { p.expect(ON_KW); privilege_target_type(p); p.expect(FROM_KW); - role(p); - while !p.at(EOF) && p.eat(COMMA) { - role(p); - } + role_list(p); opt_cascade_or_restrict(p); } _ => { @@ -7269,19 +7258,13 @@ fn alter_subscription(p: &mut Parser<'_>) -> CompletedMarker { SET_KW | ADD_KW => { p.bump_any(); p.expect(PUBLICATION_KW); - name(p); - while !p.at(EOF) && p.eat(COMMA) { - name(p); - } + name_list(p); opt_with_options_list(p); } DROP_KW => { p.bump(DROP_KW); p.expect(PUBLICATION_KW); - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); opt_with_options_list(p); } REFRESH_KW => { @@ -7315,6 +7298,13 @@ fn alter_subscription(p: &mut Parser<'_>) -> CompletedMarker { m.complete(p, ALTER_SUBSCRIPTION) } +fn name_list(p: &mut Parser<'_>) { + name(p); + while !p.at(EOF) && p.eat(COMMA) { + name(p); + } +} + fn opt_with_options_list(p: &mut Parser<'_>) { if p.eat(WITH_KW) { attribute_list(p); @@ -8345,12 +8335,7 @@ fn create_domain(p: &mut Parser<'_>) -> CompletedMarker { p.eat(AS_KW); type_name(p); opt_collate(p); - while !p.at(EOF) { - // TODO: add validation to limit the types of constraints allowed - if opt_column_constraint(p).is_none() { - break; - } - } + opt_column_constraint_list(p); m.complete(p, CREATE_DOMAIN) } @@ -8447,9 +8432,7 @@ fn create_foreign_table(p: &mut Parser<'_>) -> CompletedMarker { } else { name_ref(p); opt_with_options(p); - while !p.at(EOF) && opt_column_constraint(p).is_some() { - // pass - } + opt_column_constraint_list(p); } while !p.at(EOF) && p.eat(COMMA) { if p.at_ts(TABLE_CONSTRAINT_FIRST) { @@ -8457,9 +8440,7 @@ fn create_foreign_table(p: &mut Parser<'_>) -> CompletedMarker { } else { name_ref(p); opt_with_options(p); - while !p.at(EOF) && opt_column_constraint(p).is_some() { - // pass - } + opt_column_constraint_list(p); } } p.expect(R_PAREN); @@ -8475,7 +8456,7 @@ fn create_foreign_table(p: &mut Parser<'_>) -> CompletedMarker { type_name(p); opt_options_list(p); opt_collate(p); - while !p.at(EOF) && opt_column_constraint(p).is_some() {} + opt_column_constraint_list(p); } if !p.eat(COMMA) { break; @@ -8802,10 +8783,7 @@ fn create_policy(p: &mut Parser<'_>) -> CompletedMarker { || p.eat(DELETE_KW); } if p.eat(TO_KW) { - role(p); - while !p.at(EOF) && p.eat(COMMA) { - role(p); - } + role_list(p); } if p.eat(USING_KW) { p.expect(L_PAREN); @@ -9071,21 +9049,10 @@ fn create_statistics(p: &mut Parser<'_>) -> CompletedMarker { if !p.at(L_PAREN) && !p.at(ON_KW) { path_name(p); } - if p.eat(L_PAREN) { - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } - p.expect(R_PAREN); - } + opt_paren_name_ref_list(p); if p.eat(ON_KW) { - if expr(p).is_none() { - p.error("expected expression"); - } - while !p.at(EOF) && p.eat(COMMA) { - if expr(p).is_none() { - p.error("expected expression"); - } + if !expr_list(p) { + p.error("expected expression") } } p.expect(FROM_KW); @@ -9093,6 +9060,16 @@ fn create_statistics(p: &mut Parser<'_>) -> CompletedMarker { m.complete(p, CREATE_STATISTICS) } +fn opt_paren_name_ref_list(p: &mut Parser<'_>) -> bool { + if p.eat(L_PAREN) { + name_ref_list(p); + p.expect(R_PAREN); + true + } else { + false + } +} + // CREATE SUBSCRIPTION subscription_name // CONNECTION 'conninfo' // PUBLICATION publication_name [, ...] @@ -9106,10 +9083,7 @@ fn create_subscription(p: &mut Parser<'_>) -> CompletedMarker { p.expect(CONNECTION_KW); string_literal(p); p.expect(PUBLICATION_KW); - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); opt_with_params(p); m.complete(p, CREATE_SUBSCRIPTION) } @@ -9321,17 +9295,11 @@ fn opt_role_option(p: &mut Parser<'_>) -> bool { } else { p.error("expected GROUP or ROLE"); } - role(p); - while !p.at(EOF) && p.eat(COMMA) { - role(p); - } + role_list(p); } ROLE_KW | ADMIN_KW | USER_KW => { p.bump_any(); - role(p); - while !p.at(EOF) && p.eat(COMMA) { - role(p); - } + role_list(p); } SYSID_KW => { p.bump(SYSID_KW); @@ -9405,10 +9373,7 @@ fn drop_group(p: &mut Parser<'_>) -> CompletedMarker { p.bump(DROP_KW); p.bump(GROUP_KW); opt_if_exists(p); - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); m.complete(p, DROP_GROUP) } @@ -9441,10 +9406,7 @@ fn drop_foreign_data(p: &mut Parser<'_>) -> CompletedMarker { p.bump(DATA_KW); p.bump(WRAPPER_KW); opt_if_exists(p); - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_FOREIGN_DATA_WRAPPER) } @@ -9457,10 +9419,7 @@ fn drop_foreign_table(p: &mut Parser<'_>) -> CompletedMarker { p.bump(FOREIGN_KW); p.bump(TABLE_KW); opt_if_exists(p); - path_name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - path_name_ref(p); - } + path_name_ref_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_FOREIGN_TABLE) } @@ -9531,10 +9490,7 @@ fn drop_collation(p: &mut Parser<'_>) -> CompletedMarker { p.bump(DROP_KW); p.bump(COLLATION_KW); opt_if_exists(p); - path_name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - path_name_ref(p); - } + path_name_ref_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_COLLATION) } @@ -9558,10 +9514,7 @@ fn drop_domain(p: &mut Parser<'_>) -> CompletedMarker { p.bump(DROP_KW); p.bump(DOMAIN_KW); opt_if_exists(p); - path_name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - path_name_ref(p); - } + path_name_ref_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_DOMAIN) } @@ -9586,10 +9539,7 @@ fn drop_extension(p: &mut Parser<'_>) -> CompletedMarker { p.bump(DROP_KW); p.bump(EXTENSION_KW); opt_if_exists(p); - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_EXTENSION) } @@ -9602,10 +9552,7 @@ fn drop_materialized_view(p: &mut Parser<'_>) -> CompletedMarker { p.bump(MATERIALIZED_KW); p.bump(VIEW_KW); opt_if_exists(p); - path_name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - path_name_ref(p); - } + path_name_ref_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_MATERIALIZED_VIEW) } @@ -9674,10 +9621,7 @@ fn drop_owned(p: &mut Parser<'_>) -> CompletedMarker { p.bump(DROP_KW); p.bump(OWNED_KW); p.expect(BY_KW); - role(p); - while !p.at(EOF) && p.eat(COMMA) { - role(p); - } + role_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_OWNED) } @@ -9721,10 +9665,7 @@ fn drop_publication(p: &mut Parser<'_>) -> CompletedMarker { p.bump(DROP_KW); p.bump(PUBLICATION_KW); opt_if_exists(p); - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_PUBLICATION) } @@ -9736,10 +9677,7 @@ fn drop_role(p: &mut Parser<'_>) -> CompletedMarker { p.bump(DROP_KW); p.bump(ROLE_KW); opt_if_exists(p); - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); m.complete(p, DROP_ROLE) } @@ -9782,10 +9720,7 @@ fn drop_sequence(p: &mut Parser<'_>) -> CompletedMarker { p.bump(DROP_KW); p.bump(SEQUENCE_KW); opt_if_exists(p); - path_name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - path_name_ref(p); - } + path_name_ref_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_SEQUENCE) } @@ -9797,10 +9732,7 @@ fn drop_server(p: &mut Parser<'_>) -> CompletedMarker { p.bump(DROP_KW); p.bump(SERVER_KW); opt_if_exists(p); - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_SERVER) } @@ -9812,10 +9744,7 @@ fn drop_statistics(p: &mut Parser<'_>) -> CompletedMarker { p.bump(DROP_KW); p.bump(STATISTICS_KW); opt_if_exists(p); - path_name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - path_name_ref(p); - } + path_name_ref_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_STATISTICS) } @@ -9940,10 +9869,7 @@ fn drop_user(p: &mut Parser<'_>) -> CompletedMarker { p.bump(DROP_KW); p.bump(USER_KW); opt_if_exists(p); - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); m.complete(p, DROP_USER) } @@ -10088,10 +10014,7 @@ fn import_foreign_schema(p: &mut Parser<'_>) -> CompletedMarker { } // ( table_name [, ...] ) p.expect(L_PAREN); - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); p.expect(R_PAREN); } // FROM SERVER server_name @@ -10309,15 +10232,9 @@ fn reassign(p: &mut Parser<'_>) -> CompletedMarker { p.bump(REASSIGN_KW); p.expect(OWNED_KW); p.expect(BY_KW); - role(p); - while !p.at(EOF) && p.eat(COMMA) { - role(p); - } + role_list(p); p.expect(TO_KW); - role(p); - while !p.at(EOF) && p.eat(COMMA) { - role(p); - } + role_list(p); m.complete(p, REASSIGN) } @@ -10381,10 +10298,7 @@ fn grant(p: &mut Parser<'_>) -> CompletedMarker { } // TO role_specification [, ...] p.expect(TO_KW); - role(p); - while !p.at(EOF) && p.eat(COMMA) { - role(p); - } + role_list(p); // TODO: need more validation here // [ WITH GRANT OPTION ] // [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] @@ -10423,10 +10337,7 @@ fn privilege_target(p: &mut Parser<'_>) { p.expect(IN_KW); p.expect(SCHEMA_KW); // schema_name [, ...] - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); } _ => p.error("expected TABLE"), } @@ -10434,10 +10345,7 @@ fn privilege_target(p: &mut Parser<'_>) { match p.current() { PARAMETER_KW => { p.bump(PARAMETER_KW); - path_name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - path_name_ref(p); - } + path_name_ref_list(p); } FUNCTION_KW | PROCEDURE_KW | ROUTINE_KW => { p.bump_any(); @@ -10461,18 +10369,12 @@ fn privilege_target(p: &mut Parser<'_>) { // no schema allowed for the name DATABASE_KW | TABLESPACE_KW | SCHEMA_KW | LANGUAGE_KW => { p.bump_any(); - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); } // these allow schema TABLE_KW | SEQUENCE_KW | DOMAIN_KW => { p.bump_any(); - path_name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - path_name_ref(p); - } + path_name_ref_list(p); } FOREIGN_KW => { p.bump(FOREIGN_KW); @@ -10481,10 +10383,7 @@ fn privilege_target(p: &mut Parser<'_>) { } else { p.expect(SERVER_KW); } - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); } LARGE_KW => { p.bump(LARGE_KW); @@ -10500,10 +10399,7 @@ fn privilege_target(p: &mut Parser<'_>) { } // table_name [, ...] _ if p.at_ts(COL_LABEL_FIRST) => { - path_name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - path_name_ref(p); - } + path_name_ref_list(p); } _ => (), } @@ -10563,10 +10459,7 @@ fn revoke(p: &mut Parser<'_>) -> CompletedMarker { } // FROM role_specification [, ...] p.expect(FROM_KW); - role(p); - while !p.at(EOF) && p.eat(COMMA) { - role(p); - } + role_list(p); // [ GRANTED BY role_specification ] opt_granted_by(p); opt_cascade_or_restrict(p); @@ -10821,15 +10714,7 @@ fn set_constraints(p: &mut Parser<'_>) -> CompletedMarker { p.bump(SET_KW); p.bump(CONSTRAINTS_KW); if !p.eat(ALL_KW) { - // TODO: generalize - path_name(p); - while !p.at(EOF) { - if p.eat(COMMA) { - path_name(p); - } else { - break; - } - } + path_name_list(p); } if !p.eat(DEFERRED_KW) && !p.eat(IMMEDIATE_KW) { p.error("expected DEFERRED or IMMEDIATE"); @@ -10837,6 +10722,17 @@ fn set_constraints(p: &mut Parser<'_>) -> CompletedMarker { m.complete(p, SET_CONSTRAINTS) } +fn path_name_list(p: &mut Parser<'_>) { + path_name(p); + while !p.at(EOF) { + if p.eat(COMMA) { + path_name(p); + } else { + break; + } + } +} + // SET [ SESSION | LOCAL ] ROLE role_name // SET [ SESSION | LOCAL ] ROLE NONE // RESET ROLE @@ -11034,16 +10930,7 @@ fn drop_view(p: &mut Parser<'_>) -> CompletedMarker { p.bump(VIEW_KW); opt_if_exists(p); // name [, ...] - // TODO: we need to generalize this pattern - path_name_ref(p); - if p.eat(COMMA) { - while !p.at(EOF) { - path_name_ref(p); - if !p.eat(COMMA) { - break; - } - } - } + path_name_ref_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_VIEW) } @@ -11580,19 +11467,13 @@ fn opt_copy_option_item(p: &mut Parser<'_>) -> bool { p.bump_any(); p.expect(NULL_KW); if !p.eat(STAR) { - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); } } QUOTE_KW | NULL_KW => { p.bump_any(); if !p.eat(STAR) { - name_ref(p); - while !p.at(EOF) && p.eat(COMMA) { - name_ref(p); - } + name_ref_list(p); } } _ => return false, @@ -11781,12 +11662,7 @@ fn create_trigger(p: &mut Parser<'_>) -> CompletedMarker { if p.eat(UPDATE_KW) { // [ OF column_name [, ... ] ] if p.eat(OF_KW) { - while !p.at(EOF) { - name_ref(p); - if !p.eat(COMMA) { - break; - } - } + name_ref_list(p); } } else if !(p.eat(INSERT_KW) || p.eat(DELETE_KW) || p.eat(TRUNCATE_KW)) { p.error("expected INSERT, UPDATE, DELETE, or TRUNCATE"); @@ -11867,17 +11743,7 @@ fn drop_schema(p: &mut Parser<'_>) -> CompletedMarker { p.bump(DROP_KW); p.bump(SCHEMA_KW); opt_if_exists(p); - let mut found_name = false; - while !p.at(EOF) { - name(p); - found_name = true; - if !p.eat(COMMA) { - break; - } - } - if !found_name { - p.error("expected name"); - } + name_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_SCHEMA) } @@ -12302,13 +12168,7 @@ fn drop_type(p: &mut Parser<'_>) -> CompletedMarker { p.bump(DROP_KW); p.bump(TYPE_KW); opt_if_exists(p); - // name [, ...] - while !p.at(EOF) { - path_name_ref(p); - if !p.eat(COMMA) { - break; - } - } + path_name_ref_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_TYPE) } @@ -12343,12 +12203,7 @@ fn drop_index(p: &mut Parser<'_>) -> CompletedMarker { p.eat(CONCURRENTLY_KW); opt_if_exists(p); // name [, ...] - while !p.at(EOF) { - path_name_ref(p); - if !p.eat(COMMA) { - break; - } - } + path_name_ref_list(p); opt_cascade_or_restrict(p); m.complete(p, DROP_INDEX) } @@ -13270,13 +13125,7 @@ fn alter_table(p: &mut Parser<'_>) -> CompletedMarker { // SET TABLESPACE new_tablespace [ NOWAIT ] if all_in_tablespace && p.eat(OWNED_KW) { p.expect(BY_KW); - while !p.at(EOF) { - // name - name_ref(p); - if !p.eat(COMMA) { - break; - } - } + name_ref_list(p); } opt_alter_table_action_list(p); m.complete(p, ALTER_TABLE) @@ -13539,12 +13388,7 @@ fn opt_alter_table_action(p: &mut Parser<'_>) -> Option { type_name(p); opt_options_list(p); opt_collate(p); - // [ column_constraint [ ... ] ] - while !p.at(EOF) { - if opt_column_constraint(p).is_none() { - break; - } - } + opt_column_constraint_list(p); ADD_COLUMN } } @@ -13662,7 +13506,6 @@ fn opt_alter_table_action(p: &mut Parser<'_>) -> Option { p.bump(ALTER_KW); // ALTER CONSTRAINT constraint_name [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] if p.eat(CONSTRAINT_KW) { - // name name_ref(p); opt_constraint_option_list(p); ALTER_CONSTRAINT @@ -13729,7 +13572,7 @@ fn col_label(p: &mut Parser<'_>) { // | ColLabel // | ColLabel '.' ColLabel '=' def_arg // | ColLabel '.' ColLabel -fn attribute_option(p: &mut Parser<'_>) -> bool { +fn opt_attribute_option(p: &mut Parser<'_>) -> bool { let m = p.start(); if !opt_col_label(p) { m.abandon(p); @@ -13991,16 +13834,15 @@ fn alter_column_option(p: &mut Parser<'_>) -> Option { fn attribute_list(p: &mut Parser<'_>) { let m = p.start(); - p.expect(L_PAREN); - while !p.at(EOF) && !p.at(R_PAREN) { - if !attribute_option(p) { - break; - } - if !p.eat(COMMA) { - break; - } - } - p.expect(R_PAREN); + delimited( + p, + L_PAREN, + R_PAREN, + COMMA, + || "unexpected comma".to_string(), + COL_LABEL_FIRST, + opt_attribute_option, + ); m.complete(p, ATTRIBUTE_LIST); } diff --git a/crates/squawk_parser/tests/data/err/alter_table.sql b/crates/squawk_parser/tests/data/err/alter_table.sql index 9872c3be..dfdce101 100644 --- a/crates/squawk_parser/tests/data/err/alter_table.sql +++ b/crates/squawk_parser/tests/data/err/alter_table.sql @@ -10,3 +10,11 @@ validate constraint foo validate constraint b ; -- pg 18 only, via: https://www.depesz.com/2025/05/01/waiting-for-postgresql-18-allow-not-null-constraints-to-be-added-as-not-valid/ alter table public.copy_2 add constraint id_not_null not null id not valid; + +-- missing comma +alter foreign table t + alter c set (a b = 1); + +-- extra comma +alter foreign table t + alter c set (a, , b = 1); diff --git a/crates/squawk_parser/tests/data/err/select.sql b/crates/squawk_parser/tests/data/err/select.sql index 59736be4..e1b04a15 100644 --- a/crates/squawk_parser/tests/data/err/select.sql +++ b/crates/squawk_parser/tests/data/err/select.sql @@ -25,6 +25,13 @@ select 1 in c; -- type cast must use a string literal select numeric 1234; +-- missing comma +select array[1 2,3]; +-- extra comma +select array[1, ,3]; +-- trailing comma +select array[1,2,3,]; + -- regression test: this would cause the parser to get stuck & panic, now it -- warns about a missing semicolon select select; diff --git a/crates/squawk_parser/tests/snapshots/tests__alter_table_err.snap b/crates/squawk_parser/tests/snapshots/tests__alter_table_err.snap index 121b9aaf..838cf0b6 100644 --- a/crates/squawk_parser/tests/snapshots/tests__alter_table_err.snap +++ b/crates/squawk_parser/tests/snapshots/tests__alter_table_err.snap @@ -120,6 +120,90 @@ SOURCE_FILE WHITESPACE " " VALID_KW "valid" SEMICOLON ";" + WHITESPACE "\n\n" + COMMENT "-- missing comma" + WHITESPACE "\n" + ALTER_FOREIGN_TABLE + ALTER_KW "alter" + WHITESPACE " " + FOREIGN_KW "foreign" + WHITESPACE " " + TABLE_KW "table" + WHITESPACE " " + RELATION_NAME + PATH + PATH_SEGMENT + NAME_REF + IDENT "t" + WHITESPACE "\n " + ALTER_COLUMN + ALTER_KW "alter" + WHITESPACE " " + IDENT "c" + WHITESPACE " " + SET_OPTIONS + SET_KW "set" + WHITESPACE " " + ATTRIBUTE_LIST + L_PAREN "(" + ATTRIBUTE_OPTION + NAME + IDENT "a" + WHITESPACE " " + ATTRIBUTE_OPTION + NAME + IDENT "b" + WHITESPACE " " + EQ "=" + WHITESPACE " " + LITERAL + INT_NUMBER "1" + R_PAREN ")" + SEMICOLON ";" + WHITESPACE "\n\n" + COMMENT "-- extra comma" + WHITESPACE "\n" + ALTER_FOREIGN_TABLE + ALTER_KW "alter" + WHITESPACE " " + FOREIGN_KW "foreign" + WHITESPACE " " + TABLE_KW "table" + WHITESPACE " " + RELATION_NAME + PATH + PATH_SEGMENT + NAME_REF + IDENT "t" + WHITESPACE "\n " + ALTER_COLUMN + ALTER_KW "alter" + WHITESPACE " " + IDENT "c" + WHITESPACE " " + SET_OPTIONS + SET_KW "set" + WHITESPACE " " + ATTRIBUTE_LIST + L_PAREN "(" + ATTRIBUTE_OPTION + NAME + IDENT "a" + COMMA "," + WHITESPACE " " + ERROR + COMMA "," + WHITESPACE " " + ATTRIBUTE_OPTION + NAME + IDENT "b" + WHITESPACE " " + EQ "=" + WHITESPACE " " + LITERAL + INT_NUMBER "1" + R_PAREN ")" + SEMICOLON ";" WHITESPACE "\n" --- ERROR@23: expected command, found ADD_KW @@ -127,3 +211,5 @@ ERROR@27: expected command, found COLUMN_KW ERROR@34: expected command, found IDENT ERROR@38: expected command, found BOOLEAN_KW ERROR@175: missing comma +ERROR@505: expected COMMA +ERROR@570: unexpected comma 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 990f0f75..c878f572 100644 --- a/crates/squawk_parser/tests/snapshots/tests__alter_table_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__alter_table_ok.snap @@ -2685,21 +2685,25 @@ SOURCE_FILE WHITESPACE " " FULL_KW "full" WHITESPACE " " - ON_KW "on" - WHITESPACE " " - DELETE_KW "delete" - WHITESPACE " " - NO_KW "no" - WHITESPACE " " - ACTION_KW "action" - WHITESPACE " " - ON_KW "on" - WHITESPACE " " - UPDATE_KW "update" - WHITESPACE " " - SET_KW "set" + ON_DELETE_ACTION + ON_KW "on" + WHITESPACE " " + DELETE_KW "delete" + WHITESPACE " " + NO_ACTION + NO_KW "no" + WHITESPACE " " + ACTION_KW "action" WHITESPACE " " - NULL_KW "null" + ON_UPDATE_ACTION + ON_KW "on" + WHITESPACE " " + UPDATE_KW "update" + WHITESPACE " " + SET_NULL_COLUMNS + SET_KW "set" + WHITESPACE " " + NULL_KW "null" SEMICOLON ";" WHITESPACE "\n\n" ALTER_TABLE @@ -2735,13 +2739,15 @@ SOURCE_FILE NAME_REF IDENT "t1" WHITESPACE " " - ON_KW "on" - WHITESPACE " " - DELETE_KW "delete" - WHITESPACE " " - NO_KW "no" - WHITESPACE " " - ACTION_KW "action" + ON_DELETE_ACTION + ON_KW "on" + WHITESPACE " " + DELETE_KW "delete" + WHITESPACE " " + NO_ACTION + NO_KW "no" + WHITESPACE " " + ACTION_KW "action" SEMICOLON ";" WHITESPACE "\n" ALTER_TABLE @@ -2777,11 +2783,12 @@ SOURCE_FILE NAME_REF IDENT "t1" WHITESPACE " " - ON_KW "on" - WHITESPACE " " - DELETE_KW "delete" - WHITESPACE " " - RESTRICT_KW "restrict" + ON_DELETE_ACTION + ON_KW "on" + WHITESPACE " " + DELETE_KW "delete" + WHITESPACE " " + RESTRICT_KW "restrict" SEMICOLON ";" WHITESPACE "\n" ALTER_TABLE @@ -2817,11 +2824,12 @@ SOURCE_FILE NAME_REF IDENT "t1" WHITESPACE " " - ON_KW "on" - WHITESPACE " " - DELETE_KW "delete" - WHITESPACE " " - CASCADE_KW "cascade" + ON_DELETE_ACTION + ON_KW "on" + WHITESPACE " " + DELETE_KW "delete" + WHITESPACE " " + CASCADE_KW "cascade" SEMICOLON ";" WHITESPACE "\n" ALTER_TABLE @@ -2857,13 +2865,15 @@ SOURCE_FILE NAME_REF IDENT "t1" WHITESPACE " " - ON_KW "on" - WHITESPACE " " - DELETE_KW "delete" - WHITESPACE " " - SET_KW "set" - WHITESPACE " " - NULL_KW "null" + ON_DELETE_ACTION + ON_KW "on" + WHITESPACE " " + DELETE_KW "delete" + WHITESPACE " " + SET_NULL_COLUMNS + SET_KW "set" + WHITESPACE " " + NULL_KW "null" SEMICOLON ";" WHITESPACE "\n" ALTER_TABLE @@ -2899,25 +2909,27 @@ SOURCE_FILE NAME_REF IDENT "t1" WHITESPACE " " - ON_KW "on" - WHITESPACE " " - DELETE_KW "delete" - WHITESPACE " " - SET_KW "set" - WHITESPACE " " - NULL_KW "null" - WHITESPACE " " - COLUMN_LIST - L_PAREN "(" - COLUMN - NAME_REF - IDENT "a" - COMMA "," + ON_DELETE_ACTION + ON_KW "on" WHITESPACE " " - COLUMN - NAME_REF - IDENT "b" - R_PAREN ")" + DELETE_KW "delete" + WHITESPACE " " + SET_NULL_COLUMNS + SET_KW "set" + WHITESPACE " " + NULL_KW "null" + WHITESPACE " " + COLUMN_LIST + L_PAREN "(" + COLUMN + NAME_REF + IDENT "a" + COMMA "," + WHITESPACE " " + COLUMN + NAME_REF + IDENT "b" + R_PAREN ")" SEMICOLON ";" WHITESPACE "\n" ALTER_TABLE @@ -2953,13 +2965,15 @@ SOURCE_FILE NAME_REF IDENT "t1" WHITESPACE " " - ON_KW "on" - WHITESPACE " " - DELETE_KW "delete" - WHITESPACE " " - SET_KW "set" - WHITESPACE " " - DEFAULT_KW "default" + ON_DELETE_ACTION + ON_KW "on" + WHITESPACE " " + DELETE_KW "delete" + WHITESPACE " " + SET_DEFAULT_COLUMNS + SET_KW "set" + WHITESPACE " " + DEFAULT_KW "default" SEMICOLON ";" WHITESPACE "\n" ALTER_TABLE @@ -2995,22 +3009,27 @@ SOURCE_FILE NAME_REF IDENT "t1" WHITESPACE " " - ON_KW "on" - WHITESPACE " " - DELETE_KW "delete" - WHITESPACE " " - SET_KW "set" - WHITESPACE " " - DEFAULT_KW "default" - WHITESPACE " " - L_PAREN "(" - NAME_REF - IDENT "a" - COMMA "," - WHITESPACE " " - NAME_REF - IDENT "b" - R_PAREN ")" + ON_DELETE_ACTION + ON_KW "on" + WHITESPACE " " + DELETE_KW "delete" + WHITESPACE " " + SET_DEFAULT_COLUMNS + SET_KW "set" + WHITESPACE " " + DEFAULT_KW "default" + WHITESPACE " " + COLUMN_LIST + L_PAREN "(" + COLUMN + NAME_REF + IDENT "a" + COMMA "," + WHITESPACE " " + COLUMN + NAME_REF + IDENT "b" + R_PAREN ")" SEMICOLON ";" WHITESPACE "\n\n" ALTER_TABLE @@ -3819,16 +3838,18 @@ SOURCE_FILE IDENT "id" R_PAREN ")" WHITESPACE " " - ON_KW "on" - WHITESPACE " " - UPDATE_KW "update" - WHITESPACE " " - CASCADE_KW "cascade" - WHITESPACE " " - ON_KW "on" - WHITESPACE " " - DELETE_KW "delete" + ON_UPDATE_ACTION + ON_KW "on" + WHITESPACE " " + UPDATE_KW "update" + WHITESPACE " " + CASCADE_KW "cascade" WHITESPACE " " - CASCADE_KW "cascade" + ON_DELETE_ACTION + ON_KW "on" + WHITESPACE " " + DELETE_KW "delete" + WHITESPACE " " + CASCADE_KW "cascade" SEMICOLON ";" WHITESPACE "\n" diff --git a/crates/squawk_parser/tests/snapshots/tests__comment_ok.snap b/crates/squawk_parser/tests/snapshots/tests__comment_ok.snap index 3fa66f7c..6442d668 100644 --- a/crates/squawk_parser/tests/snapshots/tests__comment_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__comment_ok.snap @@ -160,8 +160,7 @@ SOURCE_FILE WHITESPACE " " PARAM TIME_TYPE - NAME_REF - TIMESTAMP_KW "timestamp" + TIMESTAMP_KW "timestamp" R_PAREN ")" WHITESPACE " " IS_KW "is" diff --git a/crates/squawk_parser/tests/snapshots/tests__create_aggregate_ok.snap b/crates/squawk_parser/tests/snapshots/tests__create_aggregate_ok.snap index 4334607d..5f8559c2 100644 --- a/crates/squawk_parser/tests/snapshots/tests__create_aggregate_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__create_aggregate_ok.snap @@ -760,8 +760,7 @@ SOURCE_FILE IDENT "value2" WHITESPACE " " TIME_TYPE - NAME_REF - TIMESTAMP_KW "timestamp" + TIMESTAMP_KW "timestamp" WHITESPACE " " ORDER_KW "ORDER" WHITESPACE " " 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 f8980bd6..74578180 100644 --- a/crates/squawk_parser/tests/snapshots/tests__create_table_err.snap +++ b/crates/squawk_parser/tests/snapshots/tests__create_table_err.snap @@ -141,11 +141,12 @@ SOURCE_FILE NAME_REF IDENT "bar" WHITESPACE " " - ON_KW "on" - WHITESPACE " " - UPDATE_KW "update" - WHITESPACE " " - CASCADE_KW "cascade" + ON_UPDATE_ACTION + ON_KW "on" + WHITESPACE " " + UPDATE_KW "update" + WHITESPACE " " + CASCADE_KW "cascade" COMMA "," WHITESPACE "\n " COLUMN @@ -166,13 +167,15 @@ SOURCE_FILE NAME_REF IDENT "bar" WHITESPACE " " - ON_KW "on" - WHITESPACE " " - UPDATE_KW "update" - WHITESPACE " " - SET_KW "set" - WHITESPACE " " - NULL_KW "null" + ON_UPDATE_ACTION + ON_KW "on" + WHITESPACE " " + UPDATE_KW "update" + WHITESPACE " " + SET_NULL_COLUMNS + SET_KW "set" + WHITESPACE " " + NULL_KW "null" COMMA "," WHITESPACE "\n " COLUMN @@ -193,13 +196,15 @@ SOURCE_FILE NAME_REF IDENT "bar" WHITESPACE " " - ON_KW "on" - WHITESPACE " " - UPDATE_KW "update" - WHITESPACE " " - SET_KW "set" - WHITESPACE " " - DEFAULT_KW "default" + ON_UPDATE_ACTION + ON_KW "on" + WHITESPACE " " + UPDATE_KW "update" + WHITESPACE " " + SET_DEFAULT_COLUMNS + SET_KW "set" + WHITESPACE " " + DEFAULT_KW "default" COMMA "," WHITESPACE "\n " COLUMN @@ -220,26 +225,32 @@ SOURCE_FILE NAME_REF IDENT "bar" WHITESPACE " " - ON_KW "on" - WHITESPACE " " - UPDATE_KW "update" - WHITESPACE " " - SET_KW "set" - WHITESPACE " " - DEFAULT_KW "default" - WHITESPACE " " - L_PAREN "(" - NAME_REF - IDENT "a" - COMMA "," - WHITESPACE " " - NAME_REF - IDENT "b" - COMMA "," - WHITESPACE " " - NAME_REF - IDENT "c" - R_PAREN ")" + ON_UPDATE_ACTION + ON_KW "on" + WHITESPACE " " + UPDATE_KW "update" + WHITESPACE " " + SET_DEFAULT_COLUMNS + SET_KW "set" + WHITESPACE " " + DEFAULT_KW "default" + WHITESPACE " " + COLUMN_LIST + L_PAREN "(" + COLUMN + NAME_REF + IDENT "a" + COMMA "," + WHITESPACE " " + COLUMN + NAME_REF + IDENT "b" + COMMA "," + WHITESPACE " " + COLUMN + NAME_REF + IDENT "c" + R_PAREN ")" WHITESPACE "\n" R_PAREN ")" SEMICOLON ";" 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 5392ea71..6daeaa13 100644 --- a/crates/squawk_parser/tests/snapshots/tests__create_table_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__create_table_ok.snap @@ -1168,21 +1168,25 @@ SOURCE_FILE NAME_REF IDENT "foo" WHITESPACE "\n " - ON_KW "on" - WHITESPACE " " - UPDATE_KW "update" - WHITESPACE " " - NO_KW "no" - WHITESPACE " " - ACTION_KW "action" + ON_UPDATE_ACTION + ON_KW "on" + WHITESPACE " " + UPDATE_KW "update" + WHITESPACE " " + NO_ACTION + NO_KW "no" + WHITESPACE " " + ACTION_KW "action" WHITESPACE "\n " - ON_KW "on" - WHITESPACE " " - DELETE_KW "delete" - WHITESPACE " " - NO_KW "no" - WHITESPACE " " - ACTION_KW "action" + ON_DELETE_ACTION + ON_KW "on" + WHITESPACE " " + DELETE_KW "delete" + WHITESPACE " " + NO_ACTION + NO_KW "no" + WHITESPACE " " + ACTION_KW "action" WHITESPACE "\n" R_PAREN ")" SEMICOLON ";" @@ -1220,21 +1224,25 @@ SOURCE_FILE NAME_REF IDENT "foo" WHITESPACE "\n " - ON_KW "on" - WHITESPACE " " - DELETE_KW "delete" - WHITESPACE " " - NO_KW "no" - WHITESPACE " " - ACTION_KW "action" + ON_DELETE_ACTION + ON_KW "on" + WHITESPACE " " + DELETE_KW "delete" + WHITESPACE " " + NO_ACTION + NO_KW "no" + WHITESPACE " " + ACTION_KW "action" WHITESPACE "\n " - ON_KW "on" - WHITESPACE " " - UPDATE_KW "update" - WHITESPACE " " - NO_KW "no" - WHITESPACE " " - ACTION_KW "action" + ON_UPDATE_ACTION + ON_KW "on" + WHITESPACE " " + UPDATE_KW "update" + WHITESPACE " " + NO_ACTION + NO_KW "no" + WHITESPACE " " + ACTION_KW "action" WHITESPACE "\n" R_PAREN ")" SEMICOLON ";" diff --git a/crates/squawk_parser/tests/snapshots/tests__create_table_pg17_ok.snap b/crates/squawk_parser/tests/snapshots/tests__create_table_pg17_ok.snap index b5637de7..1c7d6ee4 100644 --- a/crates/squawk_parser/tests/snapshots/tests__create_table_pg17_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__create_table_pg17_ok.snap @@ -64,35 +64,39 @@ SOURCE_FILE WHITESPACE " " PARTIAL_KW "partial" WHITESPACE "\n " - ON_KW "on" - WHITESPACE " " - DELETE_KW "delete" - WHITESPACE " " - NO_KW "no" - WHITESPACE " " - ACTION_KW "action" - WHITESPACE "\n " - ON_KW "on" - WHITESPACE " " - UPDATE_KW "update" - WHITESPACE " " - SET_KW "set" - WHITESPACE " " - NULL_KW "null" - WHITESPACE " " - COLUMN_LIST - L_PAREN "(" + ON_DELETE_ACTION + ON_KW "on" WHITESPACE " " - COLUMN - NAME_REF - IDENT "a" - COMMA "," + DELETE_KW "delete" WHITESPACE " " - COLUMN - NAME_REF - IDENT "b" + NO_ACTION + NO_KW "no" + WHITESPACE " " + ACTION_KW "action" + WHITESPACE "\n " + ON_UPDATE_ACTION + ON_KW "on" WHITESPACE " " - R_PAREN ")" + UPDATE_KW "update" + WHITESPACE " " + SET_NULL_COLUMNS + SET_KW "set" + WHITESPACE " " + NULL_KW "null" + WHITESPACE " " + COLUMN_LIST + L_PAREN "(" + WHITESPACE " " + COLUMN + NAME_REF + IDENT "a" + COMMA "," + WHITESPACE " " + COLUMN + NAME_REF + IDENT "b" + WHITESPACE " " + R_PAREN ")" WHITESPACE "\n" R_PAREN ")" SEMICOLON ";" diff --git a/crates/squawk_parser/tests/snapshots/tests__drop_aggregate_ok.snap b/crates/squawk_parser/tests/snapshots/tests__drop_aggregate_ok.snap index b33f4ad9..559e48cd 100644 --- a/crates/squawk_parser/tests/snapshots/tests__drop_aggregate_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__drop_aggregate_ok.snap @@ -171,8 +171,7 @@ SOURCE_FILE IDENT "a" WHITESPACE " " TIME_TYPE - NAME_REF - TIMESTAMP_KW "timestamp" + TIMESTAMP_KW "timestamp" COMMA "," WHITESPACE "\n " PARAM @@ -228,8 +227,7 @@ SOURCE_FILE IDENT "a" WHITESPACE " " TIME_TYPE - NAME_REF - TIMESTAMP_KW "timestamp" + TIMESTAMP_KW "timestamp" COMMA "," WHITESPACE "\n " PARAM diff --git a/crates/squawk_parser/tests/snapshots/tests__misc_ok.snap b/crates/squawk_parser/tests/snapshots/tests__misc_ok.snap index 873c2c7d..e1fbf4de 100644 --- a/crates/squawk_parser/tests/snapshots/tests__misc_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__misc_ok.snap @@ -3070,7 +3070,7 @@ SOURCE_FILE NAME_REF IDENT "job" WHITESPACE " " - USING_CLAUSE + JOIN_USING_CLAUSE USING_KW "using" WHITESPACE " " COLUMN_LIST @@ -6441,7 +6441,7 @@ SOURCE_FILE NAME IDENT "b" WHITESPACE " " - USING_CLAUSE + JOIN_USING_CLAUSE USING_KW "USING" WHITESPACE " " COLUMN_LIST diff --git a/crates/squawk_parser/tests/snapshots/tests__security_label_ok.snap b/crates/squawk_parser/tests/snapshots/tests__security_label_ok.snap index 277bb2db..2cd50a36 100644 --- a/crates/squawk_parser/tests/snapshots/tests__security_label_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__security_label_ok.snap @@ -233,8 +233,7 @@ SOURCE_FILE IDENT "a" WHITESPACE " " TIME_TYPE - NAME_REF - TIMESTAMP_KW "timestamp" + TIMESTAMP_KW "timestamp" COMMA "," WHITESPACE "\n " PARAM @@ -297,8 +296,7 @@ SOURCE_FILE IDENT "a" WHITESPACE " " TIME_TYPE - NAME_REF - TIMESTAMP_KW "timestamp" + TIMESTAMP_KW "timestamp" COMMA "," WHITESPACE "\n " PARAM diff --git a/crates/squawk_parser/tests/snapshots/tests__select_err.snap b/crates/squawk_parser/tests/snapshots/tests__select_err.snap index e104effd..b924e6c7 100644 --- a/crates/squawk_parser/tests/snapshots/tests__select_err.snap +++ b/crates/squawk_parser/tests/snapshots/tests__select_err.snap @@ -210,6 +210,74 @@ SOURCE_FILE INT_NUMBER "1234" SEMICOLON ";" WHITESPACE "\n\n" + COMMENT "-- missing comma" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + ARRAY_EXPR + ARRAY_KW "array" + L_BRACK "[" + LITERAL + INT_NUMBER "1" + WHITESPACE " " + LITERAL + INT_NUMBER "2" + COMMA "," + LITERAL + INT_NUMBER "3" + R_BRACK "]" + SEMICOLON ";" + WHITESPACE "\n" + COMMENT "-- extra comma" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + ARRAY_EXPR + ARRAY_KW "array" + L_BRACK "[" + LITERAL + INT_NUMBER "1" + COMMA "," + WHITESPACE " " + ERROR + COMMA "," + LITERAL + INT_NUMBER "3" + R_BRACK "]" + SEMICOLON ";" + WHITESPACE "\n" + COMMENT "-- trailing comma" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + ARRAY_EXPR + ARRAY_KW "array" + L_BRACK "[" + LITERAL + INT_NUMBER "1" + COMMA "," + LITERAL + INT_NUMBER "2" + COMMA "," + LITERAL + INT_NUMBER "3" + ERROR + COMMA "," + R_BRACK "]" + SEMICOLON ";" + WHITESPACE "\n\n" COMMENT "-- regression test: this would cause the parser to get stuck & panic, now it" WHITESPACE "\n" COMMENT "-- warns about a missing semicolon" @@ -268,6 +336,9 @@ ERROR@395: expected expression ERROR@396: expected expression ERROR@397: expected expression ERROR@520: missing comma -ERROR@646: expected SEMICOLON -ERROR@679: unexpected trailing comma -ERROR@723: unexpected trailing comma +ERROR@559: expected COMMA +ERROR@597: unexpected comma +ERROR@638: unexpected trailing comma +ERROR@761: expected SEMICOLON +ERROR@794: unexpected trailing comma +ERROR@838: unexpected trailing comma diff --git a/crates/squawk_parser/tests/snapshots/tests__select_ok.snap b/crates/squawk_parser/tests/snapshots/tests__select_ok.snap index fe24df34..c29f96ee 100644 --- a/crates/squawk_parser/tests/snapshots/tests__select_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__select_ok.snap @@ -4478,7 +4478,7 @@ SOURCE_FILE NAME_REF IDENT "t2" WHITESPACE " " - USING_CLAUSE + JOIN_USING_CLAUSE USING_KW "using" WHITESPACE " " COLUMN_LIST @@ -4515,7 +4515,7 @@ SOURCE_FILE NAME_REF IDENT "t2" WHITESPACE " " - USING_CLAUSE + JOIN_USING_CLAUSE USING_KW "using" WHITESPACE " " COLUMN_LIST @@ -4559,7 +4559,7 @@ SOURCE_FILE NAME_REF IDENT "t2" WHITESPACE " " - USING_CLAUSE + JOIN_USING_CLAUSE USING_KW "using" WHITESPACE " " COLUMN_LIST @@ -4598,7 +4598,7 @@ SOURCE_FILE NAME_REF IDENT "t2" WHITESPACE " " - USING_CLAUSE + JOIN_USING_CLAUSE USING_KW "using" WHITESPACE " " COLUMN_LIST @@ -4743,7 +4743,7 @@ SOURCE_FILE NAME_REF IDENT "t2" WHITESPACE " " - USING_CLAUSE + JOIN_USING_CLAUSE USING_KW "using" COLUMN_LIST L_PAREN "(" @@ -4804,7 +4804,7 @@ SOURCE_FILE NAME_REF IDENT "t2" WHITESPACE " " - USING_CLAUSE + JOIN_USING_CLAUSE USING_KW "using" WHITESPACE " " COLUMN_LIST @@ -4962,7 +4962,7 @@ SOURCE_FILE NAME_REF IDENT "u" WHITESPACE " " - USING_CLAUSE + JOIN_USING_CLAUSE USING_KW "using" WHITESPACE " " COLUMN_LIST @@ -4982,7 +4982,7 @@ SOURCE_FILE NAME_REF IDENT "k" WHITESPACE " " - USING_CLAUSE + JOIN_USING_CLAUSE USING_KW "using" WHITESPACE " " COLUMN_LIST @@ -5099,7 +5099,7 @@ SOURCE_FILE NAME IDENT "f" WHITESPACE " " - USING_CLAUSE + JOIN_USING_CLAUSE USING_KW "USING" WHITESPACE " " COLUMN_LIST @@ -5134,7 +5134,7 @@ SOURCE_FILE NAME_REF IDENT "t2" WHITESPACE " " - USING_CLAUSE + JOIN_USING_CLAUSE USING_KW "using" WHITESPACE " " COLUMN_LIST diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index d2ee3a64..0bfb0e52 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -5558,7 +5558,7 @@ impl Join { support::child(&self.syntax) } #[inline] - pub fn using_clause(&self) -> Option { + pub fn using_clause(&self) -> Option { support::child(&self.syntax) } #[inline] @@ -5673,6 +5673,25 @@ impl JoinRight { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct JoinUsingClause { + pub(crate) syntax: SyntaxNode, +} +impl JoinUsingClause { + #[inline] + pub fn alias(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn column_list(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn using_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::USING_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct JsonBehaviorClause { pub(crate) syntax: SyntaxNode, @@ -8769,10 +8788,6 @@ impl TimeType { support::child(&self.syntax) } #[inline] - pub fn name_ref(&self) -> Option { - support::child(&self.syntax) - } - #[inline] pub fn with_timezone(&self) -> Option { support::child(&self.syntax) } @@ -14556,6 +14571,24 @@ impl AstNode for JoinRight { &self.syntax } } +impl AstNode for JoinUsingClause { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::JOIN_USING_CLAUSE + } + #[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 JsonBehaviorClause { #[inline] fn can_cast(kind: SyntaxKind) -> bool { diff --git a/crates/squawk_syntax/src/lib.rs b/crates/squawk_syntax/src/lib.rs index 6a559ac3..543c8998 100644 --- a/crates/squawk_syntax/src/lib.rs +++ b/crates/squawk_syntax/src/lib.rs @@ -382,3 +382,98 @@ fn api_walkthrough() { } assert_eq!(exprs_cast, exprs_visit); } + +#[test] +fn create_table() { + use insta::assert_debug_snapshot; + + let source_code = " + create table users ( + id int8 primary key, + name varchar(255) not null, + email text, + created_at timestamp default now() + ); + + create table posts ( + id serial primary key, + title varchar(500), + content text, + user_id int8 references users(id) + ); + "; + + let parse = SourceFile::parse(source_code); + assert!(parse.errors().is_empty()); + let file: SourceFile = parse.tree(); + + let mut tables: Vec<(String, Vec<(String, String)>)> = vec![]; + + for stmt in file.stmts() { + if let ast::Stmt::CreateTable(create_table) = stmt { + let table_name = create_table.path().unwrap().syntax().to_string(); + let mut columns = vec![]; + for arg in create_table.table_arg_list().unwrap().args() { + match arg { + ast::TableArg::Column(column) => { + let column_name = column.name().unwrap(); + let column_type = column.ty().unwrap(); + columns.push(( + column_name.syntax().to_string(), + column_type.syntax().to_string(), + )); + } + ast::TableArg::TableConstraint(_) | ast::TableArg::LikeClause(_) => (), + } + } + tables.push((table_name, columns)); + } + } + + assert_debug_snapshot!(tables, @r#" + [ + ( + "users", + [ + ( + "id", + "int8", + ), + ( + "name", + "varchar(255)", + ), + ( + "email", + "text", + ), + ( + "created_at", + "timestamp", + ), + ], + ), + ( + "posts", + [ + ( + "id", + "serial", + ), + ( + "title", + "varchar(500)", + ), + ( + "content", + "text", + ), + ( + "user_id", + "int8", + ), + ], + ), + ] + "#) +} diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 8b0c7258..45e4d0d9 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -245,7 +245,6 @@ DoubleType = TimeType = ('time' | 'timestamp') - NameRef ('(' Literal ')')? (WithTimezone | WithoutTimezone)? @@ -603,8 +602,11 @@ JoinType = OnClause = 'on' Expr +JoinUsingClause = + 'using' ColumnList Alias? + Join = - 'natural'? JoinType (UsingClause | OnClause)? + 'natural'? JoinType (using_clause:JoinUsingClause | OnClause)? JoinExpr = (FromItem | JoinExpr) Join diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__drop_aggregate_params_validation.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__drop_aggregate_params_validation.snap index 8368aaf3..b01bf008 100644 --- a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__drop_aggregate_params_validation.snap +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__drop_aggregate_params_validation.snap @@ -157,8 +157,7 @@ SOURCE_FILE@0..389 IDENT@248..249 "a" WHITESPACE@249..250 " " TIME_TYPE@250..259 - NAME_REF@250..259 - TIMESTAMP_KW@250..259 "timestamp" + TIMESTAMP_KW@250..259 "timestamp" COMMA@259..260 "," WHITESPACE@260..265 "\n " PARAM@265..278 @@ -217,8 +216,7 @@ SOURCE_FILE@0..389 IDENT@344..345 "a" WHITESPACE@345..346 " " TIME_TYPE@346..355 - NAME_REF@346..355 - TIMESTAMP_KW@346..355 "timestamp" + TIMESTAMP_KW@346..355 "timestamp" COMMA@355..356 "," WHITESPACE@356..361 "\n " PARAM@361..374 diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__join_clauses_validation.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__join_clauses_validation.snap index 0a32a84a..2c8b8fbb 100644 --- a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__join_clauses_validation.snap +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__join_clauses_validation.snap @@ -139,7 +139,7 @@ SOURCE_FILE@0..653 NAME_REF@201..202 IDENT@201..202 "u" WHITESPACE@202..203 " " - USING_CLAUSE@203..213 + JOIN_USING_CLAUSE@203..213 USING_KW@203..208 "using" WHITESPACE@208..209 " " COLUMN_LIST@209..213 @@ -225,7 +225,7 @@ SOURCE_FILE@0..653 NAME_REF@336..337 IDENT@336..337 "u" WHITESPACE@337..338 " " - USING_CLAUSE@338..348 + JOIN_USING_CLAUSE@338..348 USING_KW@338..343 "using" WHITESPACE@343..344 " " COLUMN_LIST@344..348 @@ -385,7 +385,7 @@ SOURCE_FILE@0..653 NAME_REF@510..511 IDENT@510..511 "c" WHITESPACE@511..512 " " - USING_CLAUSE@512..521 + JOIN_USING_CLAUSE@512..521 USING_KW@512..517 "using" COLUMN_LIST@517..521 L_PAREN@517..518 "(" @@ -496,7 +496,7 @@ SOURCE_FILE@0..653 NAME_REF@619..620 IDENT@619..620 "u" WHITESPACE@620..621 " " - USING_CLAUSE@621..631 + JOIN_USING_CLAUSE@621..631 USING_KW@621..626 "using" WHITESPACE@626..627 " " COLUMN_LIST@627..631 @@ -514,7 +514,7 @@ SOURCE_FILE@0..653 NAME_REF@639..640 IDENT@639..640 "c" WHITESPACE@640..641 " " - USING_CLAUSE@641..651 + JOIN_USING_CLAUSE@641..651 USING_KW@641..646 "using" WHITESPACE@646..647 " " COLUMN_LIST@647..651