diff --git a/PLAN.md b/PLAN.md index 0b9d4b5e..7958bb1c 100644 --- a/PLAN.md +++ b/PLAN.md @@ -504,6 +504,31 @@ select (x * 2) + 4; related: https://eslint.style/rules/no-extra-parens +### Rule: no-constant-condition + +```sql +select a from t where false = true; +-- ^^^^^^^^^^^^ constant condition + +select case when 1 = 2 then 2 else 3 end; +-- ^^^^^ constant condition +``` + +related: https://eslint.org/docs/latest/rules/no-constant-condition + +### Rule: with query missing returning clause + +```sql +create table t(a int, b int); +create table u(a int, b int); +with x as (merge into t + using u on true + when matched then do nothing) +select * from x; +-- Query 1 ERROR at Line 19: : ERROR: WITH query "x" does not have a RETURNING clause +-- LINE 6: select * from x; +``` + ### Rule: dialect: now() to dest should support various fixes so people can write in one dialect of SQL and have it easily convert to the other one diff --git a/crates/squawk_ide/src/classify.rs b/crates/squawk_ide/src/classify.rs index bf64fb82..a2dde223 100644 --- a/crates/squawk_ide/src/classify.rs +++ b/crates/squawk_ide/src/classify.rs @@ -85,6 +85,7 @@ pub(crate) enum NameRefClass { ReindexSchema, ReindexDatabase, ReindexSystem, + AttachPartition, } pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option { @@ -335,6 +336,9 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option if ast::AlterTable::can_cast(ancestor.kind()) { return Some(NameRefClass::AlterTable); } + if ast::AttachPartition::can_cast(ancestor.kind()) { + return Some(NameRefClass::AttachPartition); + } if ast::Refresh::can_cast(ancestor.kind()) { return Some(NameRefClass::RefreshMaterializedView); } diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index 7c29306f..a261ced6 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -965,6 +965,58 @@ create table t_2026_01_02 partition of t$0 "); } + #[test] + fn goto_table_partition_of_cycle() { + goto_not_found( + " +create table part1 partition of part2 + for values from ('2026-01-02') to ('2026-01-03'); +create table part2 partition of part1 + for values from ('2026-01-02') to ('2026-01-03'); +select a$0 from part2; +", + ); + } + + #[test] + fn goto_partition_table_column() { + assert_snapshot!(goto(" +create table part ( + a int, + inserted_at timestamptz not null default now() +) partition by range (inserted_at); +create table part_2026_01_02 partition of part + for values from ('2026-01-02') to ('2026-01-03'); +select a$0 from part_2026_01_02; +"), @r" + ╭▸ + 3 │ a int, + │ ─ 2. destination + ‡ + 8 │ select a from part_2026_01_02; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_alter_index_attach_partition() { + assert_snapshot!(goto(" +create table t ( + inserted_at timestamptz not null default now() +) partition by range (inserted_at); +create table part partition of t + for values from ('2026-01-02') to ('2026-01-03'); +alter index t attach partition part$0; +"), @r" + ╭▸ + 5 │ create table part partition of t + │ ──── 2. destination + 6 │ for values from ('2026-01-02') to ('2026-01-03'); + 7 │ alter index t attach partition part; + ╰╴ ─ 1. source + "); + } + #[test] fn goto_create_table_like_clause() { assert_snapshot!(goto(" @@ -1000,6 +1052,138 @@ inherits (foo.bar, bar$0, buzz); "); } + #[test] + fn goto_create_table_like_clause_columns() { + assert_snapshot!(goto(" +create table t(a int, b int); +create table u(like t, c int); +select a$0, c from u; +"), @r" + ╭▸ + 2 │ create table t(a int, b int); + │ ─ 2. destination + 3 │ create table u(like t, c int); + 4 │ select a, c from u; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_create_table_like_clause_local_column() { + assert_snapshot!(goto(" +create table t(a int, b int); +create table u(like t, c int); +select a, c$0 from u; +"), @r" + ╭▸ + 3 │ create table u(like t, c int); + │ ─ 2. destination + 4 │ select a, c from u; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_create_table_like_clause_multi() { + assert_snapshot!(goto(" +create table t(a int, b int); +create table u(x int, y int); +create table k(like t, like u, c int); +select y$0 from k; +"), @r" + ╭▸ + 3 │ create table u(x int, y int); + │ ─ 2. destination + 4 │ create table k(like t, like u, c int); + 5 │ select y from k; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_create_table_inherits_column() { + assert_snapshot!(goto(" +create table t ( + a int, b text +); +create table u ( + c int +) inherits (t); +select a$0 from u; +"), @r" + ╭▸ + 3 │ a int, b text + │ ─ 2. destination + ‡ + 8 │ select a from u; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_create_table_inherits_local_column() { + assert_snapshot!(goto(" +create table t ( + a int, b text +); +create table u ( + c int +) inherits (t); +select c$0 from u; +"), @r" + ╭▸ + 6 │ c int + │ ─ 2. destination + 7 │ ) inherits (t); + 8 │ select c from u; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_create_table_inherits_multiple_parents() { + assert_snapshot!(goto(" +create table t1 ( + a int +); +create table t2 ( + b text +); +create table u ( + c int +) inherits (t1, t2); +select b$0 from u; +"), @r" + ╭▸ + 6 │ b text + │ ─ 2. destination + ‡ + 11 │ select b from u; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_create_foreign_table_inherits_column() { + assert_snapshot!(goto(" +create server myserver foreign data wrapper postgres_fdw; +create table t ( + a int, b text +); +create foreign table u ( + c int +) inherits (t) server myserver; +select a$0 from u; +"), @r" + ╭▸ + 4 │ a int, b text + │ ─ 2. destination + ‡ + 9 │ select a from u; + ╰╴ ─ 1. source + "); + } + #[test] fn goto_drop_temp_table_shadows_public() { // temp tables shadow public tables when no schema is specified diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index 679f623b..943e7291 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -117,7 +117,8 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { | NameRefClass::VacuumTable | NameRefClass::AlterTable | NameRefClass::ReindexTable - | NameRefClass::RefreshMaterializedView => { + | NameRefClass::RefreshMaterializedView + | NameRefClass::AttachPartition => { return hover_table(root, &name_ref, &binder); } NameRefClass::DropSequence => return hover_sequence(root, &name_ref, &binder), @@ -228,6 +229,10 @@ impl ColumnHover { fn schema_table_column(schema: &str, table_name: &str, column_name: &str) -> String { format!("column {schema}.{table_name}.{column_name}") } + + fn anon_column(col_name: &str) -> String { + format!("column {}", col_name) + } } fn hover_column( @@ -486,7 +491,7 @@ fn hover_qualified_star_columns( hover_qualified_star_columns_from_cte(&with_table) } resolve::TableSource::CreateTable(create_table) => { - hover_qualified_star_columns_from_table(&create_table, binder) + hover_qualified_star_columns_from_table(root, &create_table, binder) } resolve::TableSource::CreateView(create_view) => { hover_qualified_star_columns_from_view(&create_view, binder) @@ -498,13 +503,14 @@ fn hover_qualified_star_columns( } fn hover_qualified_star_columns_from_table( + root: &SyntaxNode, create_table: &impl ast::HasCreateTable, binder: &binder::Binder, ) -> Option { let path = create_table.path()?; let (schema, table_name) = resolve::resolve_table_info(binder, &path)?; let schema = schema.to_string(); - let results: Vec = resolve::collect_table_columns(create_table) + let results: Vec = resolve::collect_table_columns(binder, root, create_table) .into_iter() .filter_map(|column| { let column_name = Name::from_node(&column.name()?); @@ -651,14 +657,26 @@ fn hover_subquery_target_column( return Some(ColumnHover::table_column(&alias.to_string(), &col_name)); } - match target.expr()? { + let result = match target.expr()? { ast::Expr::NameRef(name_ref) => hover_column(root, &name_ref, binder), ast::Expr::FieldExpr(field_expr) => { let field = field_expr.field()?; hover_column(root, &field, binder) } _ => None, + }; + + if result.is_some() { + return result; + } + + if let Some((col_name, _node)) = ColumnName::from_target(target.clone()) + && let Some(col_name) = col_name.to_string() + { + return Some(ColumnHover::anon_column(&col_name)); } + + None } fn hover_index( @@ -2642,6 +2660,30 @@ select *$0 from (select a from t); "); } + #[test] + fn hover_on_star_with_subquery_literal() { + assert_snapshot!(check_hover(" +select *$0 from (select 1); +"), @r" + hover: column ?column? + ╭▸ + 2 │ select * from (select 1); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_star_with_subquery_literal_with_alias() { + assert_snapshot!(check_hover(" +select *$0 from (select 1) as sub; +"), @r" + hover: column sub.?column? + ╭▸ + 2 │ select * from (select 1) as sub; + ╰╴ ─ hover + "); + } + #[test] fn hover_on_view_qualified_star() { assert_snapshot!(check_hover(" @@ -3598,4 +3640,137 @@ returning t.*$0; ╰╴ ─ hover "); } + + #[test] + fn hover_partition_table_column() { + assert_snapshot!(check_hover(" +create table part ( + a int, + inserted_at timestamptz not null default now() +) partition by range (inserted_at); +create table part_2026_01_02 partition of part + for values from ('2026-01-02') to ('2026-01-03'); +select a$0 from part_2026_01_02; +"), @r" + hover: column public.part.a int + ╭▸ + 8 │ select a from part_2026_01_02; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_create_table_like_multi_star() { + assert_snapshot!(check_hover(" +create table t(a int, b int); +create table u(x int, y int); +create table k(like t, like u, c int); +select *$0 from k; +"), @r" + hover: column public.k.a int + column public.k.b int + column public.k.x int + column public.k.y int + column public.k.c int + ╭▸ + 5 │ select * from k; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_create_table_inherits_star() { + assert_snapshot!(check_hover(" +create table t ( + a int, b text +); +create table u ( + c int +) inherits (t); +select *$0 from u; +"), @r" + hover: column public.u.a int + column public.u.b text + column public.u.c int + ╭▸ + 8 │ select * from u; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_create_table_inherits_column() { + assert_snapshot!(check_hover(" +create table t ( + a int, b text +); +create table u ( + c int +) inherits (t); +select a$0 from u; +"), @r" + hover: column public.t.a int + ╭▸ + 8 │ select a from u; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_create_table_inherits_local_column() { + assert_snapshot!(check_hover(" +create table t ( + a int, b text +); +create table u ( + c int +) inherits (t); +select c$0 from u; +"), @r" + hover: column public.u.c int + ╭▸ + 8 │ select c from u; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_create_table_inherits_multiple_parents() { + assert_snapshot!(check_hover(" +create table t1 ( + a int +); +create table t2 ( + b text +); +create table u ( + c int +) inherits (t1, t2); +select b$0 from u; +"), @r" + hover: column public.t2.b text + ╭▸ + 11 │ select b from u; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_create_foreign_table_inherits_column() { + assert_snapshot!(check_hover(" +create server myserver foreign data wrapper postgres_fdw; +create table t ( + a int, b text +); +create foreign table u ( + c int +) inherits (t) server myserver; +select a$0 from u; +"), @r" + hover: column public.t.a int + ╭▸ + 9 │ select a from u; + ╰╴ ─ hover + "); + } } diff --git a/crates/squawk_ide/src/inlay_hints.rs b/crates/squawk_ide/src/inlay_hints.rs index 043be56a..fd3cf3c7 100644 --- a/crates/squawk_ide/src/inlay_hints.rs +++ b/crates/squawk_ide/src/inlay_hints.rs @@ -100,7 +100,7 @@ fn inlay_hint_insert( let col_name = resolve::extract_column_name(&col)?; let target = create_table .as_ref() - .and_then(|x| resolve::find_column_in_create_table(x, &col_name)) + .and_then(|x| resolve::find_column_in_create_table(binder, root, x, &col_name)) .map(|x| x.text_range()); Some((col_name, target)) }) diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index 8aa1510d..bb7a551f 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -32,7 +32,8 @@ pub(crate) fn resolve_name_ref( | NameRefClass::VacuumTable | NameRefClass::AlterTable | NameRefClass::ReindexTable - | NameRefClass::MergeTable => { + | NameRefClass::MergeTable + | NameRefClass::AttachPartition => { let path = find_containing_path(name_ref)?; let table_name = extract_table_name(&path)?; let schema = extract_schema_name(&path); @@ -187,7 +188,8 @@ pub(crate) fn resolve_name_ref( .ancestors() .find_map(ast::CreateTableLike::cast)?; let column_name = Name::from_node(name_ref); - find_column_in_create_table(&create_table, &column_name).map(|ptr| smallvec![ptr]) + find_column_in_create_table(binder, root, &create_table, &column_name) + .map(|ptr| smallvec![ptr]) } NameRefClass::LikeTable => { let like_clause = name_ref @@ -654,7 +656,7 @@ fn resolve_column_for_path( find_column_in_create_view(&create_view, &column_name) } ResolvedTableName::Table(create_table_like) => { - find_column_in_create_table(&create_table_like, &column_name) + find_column_in_create_table(binder, root, &create_table_like, &column_name) } } } else { @@ -1010,7 +1012,9 @@ fn resolve_select_qualified_column_ptr( } ResolvedTableName::Table(create_table_like) => { // 1. Try to find a matching column (columns take precedence) - if let Some(ptr) = find_column_in_create_table(&create_table_like, &column_name) { + if let Some(ptr) = + find_column_in_create_table(binder, root, &create_table_like, &column_name) + { return Some(ptr); } // 2. No column found, check for field-style function call @@ -1125,6 +1129,31 @@ fn resolve_column_from_table_or_view( schema: &Option, column_name: &Name, ) -> Option { + resolve_column_from_table_or_view_impl( + binder, + root, + name_ref, + table_name, + schema, + column_name, + 0, + ) +} + +fn resolve_column_from_table_or_view_impl( + binder: &Binder, + root: &SyntaxNode, + name_ref: &ast::NameRef, + table_name: &Name, + schema: &Option, + column_name: &Name, + depth: u32, +) -> Option { + if depth > 40 { + log::info!("max resolve depth reached, probably in a cycle"); + return None; + } + let position = name_ref.syntax().text_range().start(); if let Some(table_name_ptr) = resolve_table_name_ptr(binder, table_name, schema, position) { @@ -1135,11 +1164,30 @@ fn resolve_column_from_table_or_view( .find_map(ast::CreateTableLike::cast) { // 1. try to find a matching column - if let Some(ptr) = find_column_in_create_table(&create_table, column_name) { + if let Some(ptr) = find_column_in_create_table(binder, root, &create_table, column_name) + { return Some(ptr); } - // 2. No column found, check if the name matches the table name. + // 2. No column found, check if this is a partitioned table + if let Some(create_table_node) = ast::CreateTable::cast(create_table.syntax().clone()) + && let Some(partition_of) = create_table_node.partition_of() + && let Some(parent_path) = partition_of.path() + { + let parent_table_name = extract_table_name(&parent_path)?; + let parent_schema = extract_schema_name(&parent_path); + return resolve_column_from_table_or_view_impl( + binder, + root, + name_ref, + &parent_table_name, + &parent_schema, + column_name, + depth + 1, + ); + } + + // 3. No column found, check if the name matches the table name. // For example, in: // ```sql // create table t(a int); @@ -1402,7 +1450,7 @@ fn resolve_from_item_for_fn_call_column( .ancestors() .find_map(ast::CreateTableLike::cast)?; - find_column_in_create_table(&create_table, column_name) + find_column_in_create_table(binder, root, &create_table, column_name) } fn table_and_schema_from_from_item(from_item: &ast::FromItem) -> Option<(Name, Option)> { @@ -1520,17 +1568,79 @@ pub(crate) fn extract_column_name(col: &ast::Column) -> Option { } pub(crate) fn find_column_in_create_table( + binder: &Binder, + root: &SyntaxNode, + create_table: &impl ast::HasCreateTable, + column_name: &Name, +) -> Option { + find_column_in_create_table_impl(binder, root, create_table, column_name, 0) +} + +fn find_column_in_create_table_impl( + binder: &Binder, + root: &SyntaxNode, create_table: &impl ast::HasCreateTable, column_name: &Name, + depth: usize, ) -> Option { + if depth > 40 { + log::info!("max depth reached, probably in a cycle"); + return None; + } + for arg in create_table.table_arg_list()?.args() { - if let ast::TableArg::Column(column) = &arg - && let Some(name) = column.name() - && Name::from_node(&name) == *column_name - { - return Some(SyntaxNodePtr::new(name.syntax())); + match &arg { + ast::TableArg::Column(column) => { + if let Some(name) = column.name() + && Name::from_node(&name) == *column_name + { + return Some(SyntaxNodePtr::new(name.syntax())); + } + } + ast::TableArg::LikeClause(like_clause) => { + let path = like_clause.path()?; + let table_name = extract_table_name(&path)?; + let schema = extract_schema_name(&path); + let position = path.syntax().text_range().start(); + + if let Some(ResolvedTableName::Table(source_table)) = + resolve_table_name(binder, root, &table_name, &schema, position) + && let Some(ptr) = find_column_in_create_table_impl( + binder, + root, + &source_table, + column_name, + depth + 1, + ) + { + return Some(ptr); + } + } + ast::TableArg::TableConstraint(_) => (), } } + + if let Some(inherits) = create_table.inherits() { + for path in inherits.paths() { + let table_name = extract_table_name(&path)?; + let schema = extract_schema_name(&path); + let position = path.syntax().text_range().start(); + + if let Some(ResolvedTableName::Table(parent_table)) = + resolve_table_name(binder, root, &table_name, &schema, position) + && let Some(ptr) = find_column_in_create_table_impl( + binder, + root, + &parent_table, + column_name, + depth + 1, + ) + { + return Some(ptr); + } + } + } + None } @@ -2526,15 +2636,70 @@ pub(crate) fn resolve_sequence_info(binder: &Binder, path: &ast::Path) -> Option resolve_symbol_info(binder, path, SymbolKind::Sequence) } -pub(crate) fn collect_table_columns(create_table: &impl ast::HasCreateTable) -> Vec { +pub(crate) fn collect_table_columns( + binder: &Binder, + root: &SyntaxNode, + create_table: &impl ast::HasCreateTable, +) -> Vec { + collect_table_columns_impl(binder, root, create_table, 0) +} + +// TODO: combine with find_column_in_create_table_impl +fn collect_table_columns_impl( + binder: &Binder, + root: &SyntaxNode, + create_table: &impl ast::HasCreateTable, + depth: usize, +) -> Vec { + if depth > 40 { + log::info!("max depth reached, probably in a cycle"); + return vec![]; + } + let mut columns = vec![]; + + if let Some(inherits) = create_table.inherits() { + for path in inherits.paths() { + if let Some(table_name) = extract_table_name(&path) { + let schema = extract_schema_name(&path); + let position = path.syntax().text_range().start(); + if let Some(ResolvedTableName::Table(parent_table)) = + resolve_table_name(binder, root, &table_name, &schema, position) + { + let inherited_columns = + collect_table_columns_impl(binder, root, &parent_table, depth + 1); + columns.extend(inherited_columns); + } + } + } + } + if let Some(arg_list) = create_table.table_arg_list() { for arg in arg_list.args() { - if let ast::TableArg::Column(column) = arg { - columns.push(column); + match &arg { + ast::TableArg::Column(column) => { + columns.push(column.clone()); + } + ast::TableArg::LikeClause(like_clause) => { + if let Some(path) = like_clause.path() + && let Some(table_name) = extract_table_name(&path) + { + let schema = extract_schema_name(&path); + let position = path.syntax().text_range().start(); + if let Some(ResolvedTableName::Table(source_table)) = + resolve_table_name(binder, root, &table_name, &schema, position) + { + let like_columns = + collect_table_columns_impl(binder, root, &source_table, depth + 1); + columns.extend(like_columns); + } + } + } + ast::TableArg::TableConstraint(_) => (), } } } + columns } diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 7529fef2..8c00105c 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -11989,7 +11989,7 @@ pub struct PartitionOf { } impl PartitionOf { #[inline] - pub fn ty(&self) -> Option { + pub fn path(&self) -> Option { support::child(&self.syntax) } #[inline] diff --git a/crates/squawk_syntax/src/ast/nodes.rs b/crates/squawk_syntax/src/ast/nodes.rs index 03691038..22505b0d 100644 --- a/crates/squawk_syntax/src/ast/nodes.rs +++ b/crates/squawk_syntax/src/ast/nodes.rs @@ -21,6 +21,10 @@ impl CreateTableLike { pub fn table_arg_list(&self) -> Option { support::child(&self.syntax) } + #[inline] + pub fn inherits(&self) -> Option { + support::child(&self.syntax) + } } impl AstNode for CreateTableLike { #[inline] diff --git a/crates/squawk_syntax/src/ast/traits.rs b/crates/squawk_syntax/src/ast/traits.rs index 0a39ecb7..11c09c19 100644 --- a/crates/squawk_syntax/src/ast/traits.rs +++ b/crates/squawk_syntax/src/ast/traits.rs @@ -15,6 +15,11 @@ pub trait HasCreateTable: AstNode { fn table_arg_list(&self) -> Option { support::child(self.syntax()) } + + #[inline] + fn inherits(&self) -> Option { + support::child(self.syntax()) + } } pub trait HasWithClause: AstNode { diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index dbb5bb2f..d1e8a46b 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -1067,7 +1067,7 @@ IfNotExists = 'if' 'not' 'exists' PartitionOf = - 'partition' 'of' Type + 'partition' 'of' Path PreserveRows = 'preserve' 'rows'