From 4fb3afa3e8c8e12d1c7f97951a04aafaa0cd2da2 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Wed, 7 Jan 2026 12:11:37 -0500 Subject: [PATCH] ide: improved goto def & hover for merge stmts --- crates/squawk_ide/src/classify.rs | 6 +- crates/squawk_ide/src/goto_definition.rs | 164 +++++++++++++++ crates/squawk_ide/src/hover.rs | 183 +++++++++++++++- crates/squawk_ide/src/resolve.rs | 197 ++++++++++++++---- crates/squawk_parser/src/grammar.rs | 9 +- crates/squawk_parser/tests/data/ok/select.sql | 1 + .../tests/data/ok/select_cte.sql | 4 + .../snapshots/tests__regression_errors.snap | 18 +- .../tests/snapshots/tests__select_cte_ok.snap | 35 ++++ .../tests/snapshots/tests__select_ok.snap | 26 +++ .../squawk_syntax/src/ast/generated/nodes.rs | 16 ++ crates/squawk_syntax/src/ast/node_ext.rs | 7 + crates/squawk_syntax/src/postgresql.ungram | 4 + 13 files changed, 616 insertions(+), 54 deletions(-) diff --git a/crates/squawk_ide/src/classify.rs b/crates/squawk_ide/src/classify.rs index 4e9c7202..bf64fb82 100644 --- a/crates/squawk_ide/src/classify.rs +++ b/crates/squawk_ide/src/classify.rs @@ -146,7 +146,7 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option if ast::WhereClause::can_cast(ancestor.kind()) { in_where_clause = true; } - if ast::MergeWhenMatched::can_cast(ancestor.kind()) { + if ast::MergeWhenClause::can_cast(ancestor.kind()) { in_when_clause = true; } if ast::Update::can_cast(ancestor.kind()) { @@ -256,7 +256,7 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option if ast::FromClause::can_cast(ancestor.kind()) { in_from_clause = true; } - if ast::MergeWhenMatched::can_cast(ancestor.kind()) { + if ast::MergeWhenClause::can_cast(ancestor.kind()) { in_when_clause = true; } if ast::ReturningClause::can_cast(ancestor.kind()) { @@ -613,7 +613,7 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option } return Some(NameRefClass::UpdateTable); } - if ast::MergeWhenMatched::can_cast(ancestor.kind()) { + if ast::MergeWhenClause::can_cast(ancestor.kind()) { in_when_clause = true; } if ast::Merge::can_cast(ancestor.kind()) { diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index 30335efa..7c29306f 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -6038,6 +6038,95 @@ merge into x "); } + #[test] + fn goto_merge_when_not_matched_insert_values_qualified_column() { + assert_snapshot!(goto(" +create table inventory ( + product_id int, + quantity int, + updated_at timestamp +); +create table orders ( + id int, + product_id int, + qty int +); +merge into inventory as t +using orders as o + on t.product_id = o.product_id +when matched then + do nothing +when not matched then + insert values (o$0.product_id, o.qty, now()); +" + ), @r" + ╭▸ + 13 │ using orders as o + │ ─ 2. destination + ‡ + 18 │ insert values (o.product_id, o.qty, now()); + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_merge_when_not_matched_insert_values_qualified_column_field() { + assert_snapshot!(goto(" +create table inventory ( + product_id int, + quantity int, + updated_at timestamp +); +create table orders ( + id int, + product_id int, + qty int +); +merge into inventory as t +using orders as o + on t.product_id = o.product_id +when matched then + do nothing +when not matched then + insert values (o.product_id$0, o.qty, now()); +" + ), @r" + ╭▸ + 9 │ product_id int, + │ ────────── 2. destination + ‡ + 18 │ insert values (o.product_id, o.qty, now()); + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_merge_when_not_matched_insert_values_unqualified_column() { + assert_snapshot!(goto(" +create table inventory ( + product_id int, + quantity int +); +create table orders ( + product_id int, + qty int +); +merge into inventory as t +using orders as o + on t.product_id = o.product_id +when not matched then + insert values (product_id$0, qty); +" + ), @r" + ╭▸ + 7 │ product_id int, + │ ────────── 2. destination + ‡ + 14 │ insert values (product_id, qty); + ╰╴ ─ 1. source + "); + } + #[test] fn goto_insert_returning_old_table() { assert_snapshot!(goto(" @@ -6289,4 +6378,79 @@ returning old$0.a, new.a; ╰╴ ─ 1. source "); } + + #[test] + fn goto_merge_returning_cte_column_unqualified() { + assert_snapshot!(goto(" +create table t(a int, b int); +with u(x, y) as ( + select 1, 2 +) +merge into t + using u on true +when matched then + do nothing +when not matched then + do nothing +returning x$0, u.y; +" + ), @r" + ╭▸ + 3 │ with u(x, y) as ( + │ ─ 2. destination + ‡ + 12 │ returning x, u.y; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_merge_returning_cte_column_qualified_table() { + assert_snapshot!(goto(" +create table t(a int, b int); +with u(x, y) as ( + select 1, 2 +) +merge into t + using u on true +when matched then + do nothing +when not matched then + do nothing +returning x, u$0.y; +" + ), @r" + ╭▸ + 3 │ with u(x, y) as ( + │ ─ 2. destination + ‡ + 12 │ returning x, u.y; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_merge_returning_cte_column_qualified_column() { + assert_snapshot!(goto(" +create table t(a int, b int); +with u(x, y) as ( + select 1, 2 +) +merge into t + using u on true +when matched then + do nothing +when not matched then + do nothing +returning x, u.y$0; +" + ), @r" + ╭▸ + 3 │ with u(x, y) as ( + │ ─ 2. destination + ‡ + 12 │ returning x, u.y; + ╰╴ ─ 1. source + "); + } } diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index f90a554b..679f623b 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -408,7 +408,7 @@ fn hover_qualified_star( field_expr: &ast::FieldExpr, binder: &binder::Binder, ) -> Option { - let table_ptr = resolve::resolve_qualified_star_table(binder, field_expr)?; + let table_ptr = resolve::resolve_qualified_star_table_ptr(binder, field_expr)?; hover_qualified_star_columns(root, &table_ptr, binder) } @@ -417,7 +417,7 @@ fn hover_unqualified_star( target: &ast::Target, binder: &binder::Binder, ) -> Option { - let table_ptrs = resolve::resolve_unqualified_star_tables(binder, target)?; + let table_ptrs = resolve::resolve_unqualified_star_table_ptrs(binder, target)?; let mut results = vec![]; for table_ptr in table_ptrs { if let Some(columns) = hover_qualified_star_columns(root, &table_ptr, binder) { @@ -437,7 +437,7 @@ fn hover_unqualified_star_in_arg_list( arg_list: &ast::ArgList, binder: &binder::Binder, ) -> Option { - let table_ptrs = resolve::resolve_unqualified_star_tables_in_arg_list(binder, arg_list)?; + let table_ptrs = resolve::resolve_unqualified_star_in_arg_list_ptrs(binder, arg_list)?; let mut results = vec![]; for table_ptr in table_ptrs { if let Some(columns) = hover_qualified_star_columns(root, &table_ptr, binder) { @@ -606,7 +606,7 @@ fn hover_qualified_star_columns_from_subquery( for target in target_list.targets() { if target.star_token().is_some() { - let table_ptrs = resolve::resolve_unqualified_star_tables(binder, &target)?; + let table_ptrs = resolve::resolve_unqualified_star_table_ptrs(binder, &target)?; for table_ptr in table_ptrs { if let Some(columns) = hover_qualified_star_columns(root, &table_ptr, binder) { results.push(columns) @@ -3423,4 +3423,179 @@ select *$0 from merged; ╰╴ ─ hover "); } + + #[test] + fn hover_update_returning_star() { + assert_snapshot!(check_hover(" +create table t(a int, b int); +update t set a = 1 +returning *$0; +"), @r" + hover: column public.t.a int + column public.t.b int + ╭▸ + 4 │ returning *; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_insert_returning_star() { + assert_snapshot!(check_hover(" +create table t(a int, b int); +insert into t values (1, 2) +returning *$0; +"), @r" + hover: column public.t.a int + column public.t.b int + ╭▸ + 4 │ returning *; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_delete_returning_star() { + assert_snapshot!(check_hover(" +create table t(a int, b int); +delete from t +returning *$0; +"), @r" + hover: column public.t.a int + column public.t.b int + ╭▸ + 4 │ returning *; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_merge_returning_star() { + assert_snapshot!(check_hover(" +create table t(a int, b int); +merge into t + using (select 1 as x, 2 as y) u + on t.a = u.x + when matched then + do nothing +returning *$0; +"), @r" + hover: column public.t.a int + column public.t.b int + ╭▸ + 8 │ returning *; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_merge_returning_qualified_star_old() { + assert_snapshot!(check_hover(" +create table t(a int, b int); +merge into t + using (select 1 as x, 2 as y) u + on t.a = u.x + when matched then + update set a = 99 +returning old$0.*; +"), @r" + hover: table public.t(a int, b int) + ╭▸ + 8 │ returning old.*; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_merge_returning_qualified_star_new() { + assert_snapshot!(check_hover(" +create table t(a int, b int); +merge into t + using (select 1 as x, 2 as y) u + on t.a = u.x + when matched then + update set a = 99 +returning new$0.*; +"), @r" + hover: table public.t(a int, b int) + ╭▸ + 8 │ returning new.*; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_merge_returning_qualified_star_table() { + assert_snapshot!(check_hover(" +create table t(a int, b int); +merge into t + using (select 1 as x, 2 as y) u + on t.a = u.x + when matched then + update set a = 99 +returning t$0.*; +"), @r" + hover: table public.t(a int, b int) + ╭▸ + 8 │ returning t.*; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_merge_returning_qualified_star_old_on_star() { + assert_snapshot!(check_hover(" +create table t(a int, b int); +merge into t + using (select 1 as x, 2 as y) u + on t.a = u.x + when matched then + update set a = 99 +returning old.*$0; +"), @r" + hover: column public.t.a int + column public.t.b int + ╭▸ + 8 │ returning old.*; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_merge_returning_qualified_star_new_on_star() { + assert_snapshot!(check_hover(" +create table t(a int, b int); +merge into t + using (select 1 as x, 2 as y) u + on t.a = u.x + when matched then + update set a = 99 +returning new.*$0; +"), @r" + hover: column public.t.a int + column public.t.b int + ╭▸ + 8 │ returning new.*; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_merge_returning_qualified_star_table_on_star() { + assert_snapshot!(check_hover(" +create table t(a int, b int); +merge into t + using (select 1 as x, 2 as y) u + on t.a = u.x + when matched then + update set a = 99 +returning t.*$0; +"), @r" + hover: column public.t.a int + column public.t.b int + ╭▸ + 8 │ returning t.*; + ╰╴ ─ hover + "); + } } diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index a538d783..8aa1510d 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -1596,14 +1596,8 @@ fn resolve_cte_table(name_ref: &ast::NameRef, cte_name: &Name) -> Option Option { node.ancestors().find_map(|x| { - if let Some(select) = ast::Select::cast(x.clone()) { - select.with_clause() - } else if let Some(delete) = ast::Delete::cast(x.clone()) { - delete.with_clause() - } else if let Some(insert) = ast::Insert::cast(x.clone()) { - insert.with_clause() - } else if let Some(update) = ast::Update::cast(x) { - update.with_clause() + if let Some(query) = ast::WithQuery::cast(x) { + query.with_clause() } else { None } @@ -1934,65 +1928,189 @@ fn qualified_star_table_name(field_expr: &ast::FieldExpr) -> Option { } } -pub(crate) fn resolve_qualified_star_table( +pub(crate) fn resolve_qualified_star_table_ptr( binder: &Binder, field_expr: &ast::FieldExpr, ) -> Option { let table_name = qualified_star_table_name(field_expr)?; - let select = field_expr - .syntax() - .ancestors() - .find_map(ast::Select::cast)?; - let from_clause = select.from_clause()?; - let from_item = find_from_item_in_from_clause(&from_clause, &table_name)?; - let (table_name, schema) = table_and_schema_from_from_item(&from_item)?; let position = field_expr.syntax().text_range().start(); + for ancestor in field_expr.syntax().ancestors() { + if let Some(select) = ast::Select::cast(ancestor.clone()) { + let from_clause = select.from_clause()?; + let from_item = find_from_item_in_from_clause(&from_clause, &table_name)?; + let (table_name, schema) = table_and_schema_from_from_item(&from_item)?; + + if let Some(table_name_ptr) = + resolve_table_name_ptr(binder, &table_name, &schema, position) + { + return Some(table_name_ptr); + } + + if let Some(view_name_ptr) = + resolve_view_name_ptr(binder, &table_name, &schema, position) + { + return Some(view_name_ptr); + } + + if schema.is_none() + && let Some(name_ref) = from_item.name_ref() + { + return resolve_cte_table(&name_ref, &table_name); + } + + return None; + } + + if let Some(update) = ast::Update::cast(ancestor.clone()) { + let path = update.relation_name()?.path()?; + return resolve_table_or_view_or_cte_ptrs(binder, position, &path)? + .into_iter() + .next(); + } + + if let Some(insert) = ast::Insert::cast(ancestor.clone()) { + let path = insert.path()?; + return resolve_table_or_view_or_cte_ptrs(binder, position, &path)? + .into_iter() + .next(); + } + + if let Some(delete) = ast::Delete::cast(ancestor.clone()) { + let path = delete.relation_name()?.path()?; + return resolve_table_or_view_or_cte_ptrs(binder, position, &path)? + .into_iter() + .next(); + } + + if let Some(merge) = ast::Merge::cast(ancestor) { + let path = merge.relation_name()?.path()?; + return resolve_table_or_view_or_cte_ptrs(binder, position, &path)? + .into_iter() + .next(); + } + } + + None +} + +fn resolve_table_or_view_or_cte_ptrs( + binder: &Binder, + position: TextSize, + path: &ast::Path, +) -> Option> { + let (table_name, schema) = extract_table_schema_from_path(path)?; + + let mut results = vec![]; + if let Some(table_name_ptr) = resolve_table_name_ptr(binder, &table_name, &schema, position) { - return Some(table_name_ptr); + results.push(table_name_ptr); } if let Some(view_name_ptr) = resolve_view_name_ptr(binder, &table_name, &schema, position) { - return Some(view_name_ptr); + results.push(view_name_ptr); } if schema.is_none() - && let Some(name_ref) = from_item.name_ref() + && let Some(segment) = path.segment() + && let Some(name_ref) = segment.name_ref() + && let Some(cte_ptr) = resolve_cte_table(&name_ref, &table_name) { - return resolve_cte_table(&name_ref, &table_name); + results.push(cte_ptr); } - None + if results.is_empty() { + return None; + } + + Some(results) +} + +fn resolve_table_from_update_ptrs( + binder: &Binder, + position: TextSize, + update: &ast::Update, +) -> Option> { + let path = update.relation_name()?.path()?; + resolve_table_or_view_or_cte_ptrs(binder, position, &path) +} + +fn resolve_table_from_insert_ptrs( + binder: &Binder, + position: TextSize, + insert: &ast::Insert, +) -> Option> { + let path = insert.path()?; + resolve_table_or_view_or_cte_ptrs(binder, position, &path) +} + +fn resolve_table_from_delete_ptrs( + binder: &Binder, + position: TextSize, + delete: &ast::Delete, +) -> Option> { + let path = delete.relation_name()?.path()?; + resolve_table_or_view_or_cte_ptrs(binder, position, &path) +} + +fn resolve_table_from_merge_ptrs( + binder: &Binder, + position: TextSize, + merge: &ast::Merge, +) -> Option> { + let path = merge.relation_name()?.path()?; + resolve_table_or_view_or_cte_ptrs(binder, position, &path) } -pub(crate) fn resolve_unqualified_star_tables( +pub(crate) fn resolve_unqualified_star_table_ptrs( binder: &Binder, target: &ast::Target, ) -> Option> { target.star_token()?; - let select = target.syntax().ancestors().find_map(ast::Select::cast)?; - let from_clause = select.from_clause()?; let position = target.syntax().text_range().start(); - let mut results = vec![]; + for ancestor in target.syntax().ancestors() { + if let Some(select) = ast::Select::cast(ancestor.clone()) { + let from_clause = select.from_clause()?; + let mut results = vec![]; - for from_item in from_clause.from_items() { - collect_tables_from_item(binder, position, &from_item, &mut results); - } + for from_item in from_clause.from_items() { + collect_tables_from_item(binder, position, &from_item, &mut results); + } - for join_expr in from_clause.join_exprs() { - collect_tables_from_join_expr(binder, position, &join_expr, &mut results); - } + for join_expr in from_clause.join_exprs() { + collect_table_ptrs_from_join_expr(binder, position, &join_expr, &mut results); + } - if results.is_empty() { - return None; + if results.is_empty() { + return None; + } + + return Some(results); + } + + if let Some(update) = ast::Update::cast(ancestor.clone()) { + return resolve_table_from_update_ptrs(binder, position, &update); + } + + if let Some(insert) = ast::Insert::cast(ancestor.clone()) { + return resolve_table_from_insert_ptrs(binder, position, &insert); + } + + if let Some(delete) = ast::Delete::cast(ancestor.clone()) { + return resolve_table_from_delete_ptrs(binder, position, &delete); + } + + if let Some(merge) = ast::Merge::cast(ancestor) { + return resolve_table_from_merge_ptrs(binder, position, &merge); + } } - Some(results) + None } -pub(crate) fn resolve_unqualified_star_tables_in_arg_list( +pub(crate) fn resolve_unqualified_star_in_arg_list_ptrs( binder: &Binder, arg_list: &ast::ArgList, ) -> Option> { @@ -2007,7 +2125,7 @@ pub(crate) fn resolve_unqualified_star_tables_in_arg_list( } for join_expr in from_clause.join_exprs() { - collect_tables_from_join_expr(binder, position, &join_expr, &mut results); + collect_table_ptrs_from_join_expr(binder, position, &join_expr, &mut results); } if results.is_empty() { @@ -2017,14 +2135,14 @@ pub(crate) fn resolve_unqualified_star_tables_in_arg_list( Some(results) } -fn collect_tables_from_join_expr( +fn collect_table_ptrs_from_join_expr( binder: &Binder, position: TextSize, join_expr: &ast::JoinExpr, results: &mut Vec, ) { if let Some(nested) = join_expr.join_expr() { - collect_tables_from_join_expr(binder, position, &nested, results); + collect_table_ptrs_from_join_expr(binder, position, &nested, results); } if let Some(from_item) = join_expr.from_item() { @@ -2871,6 +2989,9 @@ fn resolve_merge_table_name_ptr( if let Some(item_name_ref) = from_item.name_ref() { let item_name = Name::from_node(&item_name_ref); if item_name == table_name { + if let Some(cte_ptr) = resolve_cte_table(table_name_ref, &table_name) { + return Some(cte_ptr); + } let position = table_name_ref.syntax().text_range().start(); return resolve_table_name_ptr(binder, &item_name, &None, position); } diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index a5421b39..f3a5c9a5 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -124,6 +124,7 @@ fn opt_paren_select(p: &mut Parser<'_>, m: Option) -> Option) -> CompletedMarker { assert!(p.at(L_PAREN) || p.at(ROW_KW)); @@ -2649,7 +2650,7 @@ fn with_query_clause(p: &mut Parser<'_>) -> Option { break; } if !p.eat(COMMA) { - if p.at_ts(WITH_FOLLOW) { + if p.at_ts(WITH_FOLLOW) || (p.at(L_PAREN) && p.nth_at_ts(1, PAREN_SELECT_FIRST)) { break; } else { p.error("missing comma"); @@ -5799,7 +5800,7 @@ fn stmt(p: &mut Parser, r: &StmtRestrictions) -> Option { (GRANT_KW, _) => Some(grant(p)), (IMPORT_KW, FOREIGN_KW) => Some(import_foreign_schema(p)), (INSERT_KW, _) => Some(insert(p, None)), - (L_PAREN, _) if p.nth_at_ts(1, SELECT_FIRST) || p.at(L_PAREN) => { + (L_PAREN, _) if p.nth_at_ts(1, PAREN_SELECT_FIRST) => { // can have select nested in parens, i.e., ((select 1)); opt_paren_select(p, None) } @@ -12585,6 +12586,10 @@ fn with(p: &mut Parser<'_>, m: Option) -> Option { INSERT_KW => Some(insert(p, Some(m))), UPDATE_KW => Some(update(p, Some(m))), MERGE_KW => Some(merge(p, Some(m))), + L_PAREN if p.nth_at_ts(1, PAREN_SELECT_FIRST) => { + // can have select nested in parens, i.e., ((select 1)); + opt_paren_select(p, Some(m)) + } _ => { m.abandon(p); p.error(format!( diff --git a/crates/squawk_parser/tests/data/ok/select.sql b/crates/squawk_parser/tests/data/ok/select.sql index 72fa0f6f..b9483744 100644 --- a/crates/squawk_parser/tests/data/ok/select.sql +++ b/crates/squawk_parser/tests/data/ok/select.sql @@ -48,6 +48,7 @@ select localtimestamp(5); -- array_select -- array with subquery select array(select oid from pg_proc where proname like 'bytea%'); +select array(((select 1))); -- positional_param select $1; diff --git a/crates/squawk_parser/tests/data/ok/select_cte.sql b/crates/squawk_parser/tests/data/ok/select_cte.sql index 70a4348d..8f4f3982 100644 --- a/crates/squawk_parser/tests/data/ok/select_cte.sql +++ b/crates/squawk_parser/tests/data/ok/select_cte.sql @@ -133,3 +133,7 @@ SET dismissed_at = current_timestamp WHERE notification_id IN ( SELECT notification_id FROM ranked_notifications WHERE rn > 1 ); + +-- paren select cte +with t as (select 1) +(select 1); diff --git a/crates/squawk_parser/tests/snapshots/tests__regression_errors.snap b/crates/squawk_parser/tests/snapshots/tests__regression_errors.snap index 6fd39f86..ecc7fae9 100644 --- a/crates/squawk_parser/tests/snapshots/tests__regression_errors.snap +++ b/crates/squawk_parser/tests/snapshots/tests__regression_errors.snap @@ -1,6 +1,6 @@ --- source: crates/squawk_parser/tests/tests.rs -input_file: crates/squawk_parser/tests/data/regression_suite/errors.sql +input_file: postgres/regression_suite/errors.sql --- --- error[syntax-error]: expected relation name @@ -75,14 +75,10 @@ error[syntax-error]: expected command, found INT_NUMBER ╭▸ 150 │ drop aggregate 314159 (int); ╰╴ ━ -error[syntax-error]: expected R_PAREN +error[syntax-error]: expected command, found L_PAREN ╭▸ 150 │ drop aggregate 314159 (int); - ╰╴ ━ -error[syntax-error]: expected SEMICOLON - ╭▸ -150 │ drop aggregate 314159 (int); - ╰╴ ━ + ╰╴ ━ error[syntax-error]: expected command, found INT_KW ╭▸ 150 │ drop aggregate 314159 (int); @@ -107,6 +103,14 @@ error[syntax-error]: expected command, found INT_NUMBER ╭▸ 169 │ drop function 314159(); ╰╴ ━ +error[syntax-error]: expected command, found L_PAREN + ╭▸ +169 │ drop function 314159(); + ╰╴ ━ +error[syntax-error]: expected command, found R_PAREN + ╭▸ +169 │ drop function 314159(); + ╰╴ ━ error[syntax-error]: expected path name ╭▸ 179 │ drop type; diff --git a/crates/squawk_parser/tests/snapshots/tests__select_cte_ok.snap b/crates/squawk_parser/tests/snapshots/tests__select_cte_ok.snap index ad835462..dd32be30 100644 --- a/crates/squawk_parser/tests/snapshots/tests__select_cte_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__select_cte_ok.snap @@ -1475,4 +1475,39 @@ SOURCE_FILE WHITESPACE "\n" R_PAREN ")" SEMICOLON ";" + WHITESPACE "\n\n" + COMMENT "-- paren select cte" + WHITESPACE "\n" + PAREN_SELECT + WITH_CLAUSE + WITH_KW "with" + WHITESPACE " " + WITH_TABLE + NAME + IDENT "t" + 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" + L_PAREN "(" + SELECT + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "1" + R_PAREN ")" + SEMICOLON ";" WHITESPACE "\n" diff --git a/crates/squawk_parser/tests/snapshots/tests__select_ok.snap b/crates/squawk_parser/tests/snapshots/tests__select_ok.snap index a06a3470..e9dde86a 100644 --- a/crates/squawk_parser/tests/snapshots/tests__select_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__select_ok.snap @@ -558,6 +558,32 @@ SOURCE_FILE STRING "'bytea%'" R_PAREN ")" SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + ARRAY_EXPR + ARRAY_KW "array" + L_PAREN "(" + PAREN_EXPR + L_PAREN "(" + PAREN_EXPR + L_PAREN "(" + SELECT + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "1" + R_PAREN ")" + R_PAREN ")" + R_PAREN ")" + SEMICOLON ";" WHITESPACE "\n\n" COMMENT "-- positional_param" WHITESPACE "\n" diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 9f6e5a3c..7529fef2 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -10219,6 +10219,10 @@ impl Merge { support::child(&self.syntax) } #[inline] + pub fn with_clause(&self) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn into_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::INTO_KW) } @@ -11751,6 +11755,10 @@ impl ParenSelect { support::child(&self.syntax) } #[inline] + pub fn with_clause(&self) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn l_paren_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::L_PAREN) } @@ -14881,6 +14889,10 @@ impl Table { support::child(&self.syntax) } #[inline] + pub fn with_clause(&self) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn table_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::TABLE_KW) } @@ -15556,6 +15568,10 @@ impl Values { support::child(&self.syntax) } #[inline] + pub fn with_clause(&self) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn values_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::VALUES_KW) } diff --git a/crates/squawk_syntax/src/ast/node_ext.rs b/crates/squawk_syntax/src/ast/node_ext.rs index 8ccb9c5d..12996372 100644 --- a/crates/squawk_syntax/src/ast/node_ext.rs +++ b/crates/squawk_syntax/src/ast/node_ext.rs @@ -229,6 +229,13 @@ pub(crate) fn text_of_first_token(node: &SyntaxNode) -> TokenText<'_> { } } +impl ast::WithQuery { + #[inline] + pub fn with_clause(&self) -> Option { + support::child(self.syntax()) + } +} + impl ast::HasParamList for ast::FunctionSig {} impl ast::HasParamList for ast::Aggregate {} diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index b2e81977..dbb5bb2f 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -971,6 +971,7 @@ JoinExpr = (FromItem | JoinExpr) Join ParenSelect = + WithClause? '(' select:SelectVariant ')' @@ -1260,9 +1261,11 @@ RowList = (Row (',' Row)*) Values = + WithClause? 'values' RowList Table = + WithClause? 'table' RelationName Insert = @@ -1403,6 +1406,7 @@ MergeAction = | MergeDoNothing Merge = + WithClause? 'merge' 'into' RelationName Alias? UsingOnClause MergeWhenClause*