From 9c031966ebcf0e0fcb8a9beb2327ca36d6e05f00 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 4 Jan 2026 12:14:25 -0500 Subject: [PATCH] ide: goto def foreign tables --- crates/squawk_ide/src/binder.rs | 26 ++++++++++++++ crates/squawk_ide/src/classify.rs | 14 ++++++++ crates/squawk_ide/src/goto_definition.rs | 34 ++++++++++++++++++ crates/squawk_ide/src/hover.rs | 36 +++++++++++++++++-- crates/squawk_ide/src/resolve.rs | 40 +++++++++++++-------- crates/squawk_syntax/src/ast.rs | 14 +------- crates/squawk_syntax/src/ast/node_ext.rs | 4 +++ crates/squawk_syntax/src/ast/nodes.rs | 44 ++++++++++++++++++++++++ crates/squawk_syntax/src/ast/traits.rs | 30 +++++----------- 9 files changed, 191 insertions(+), 51 deletions(-) diff --git a/crates/squawk_ide/src/binder.rs b/crates/squawk_ide/src/binder.rs index 5caec747..13077db4 100644 --- a/crates/squawk_ide/src/binder.rs +++ b/crates/squawk_ide/src/binder.rs @@ -78,6 +78,9 @@ fn bind_file(b: &mut Binder, file: &ast::SourceFile) { fn bind_stmt(b: &mut Binder, stmt: ast::Stmt) { match stmt { ast::Stmt::CreateTable(create_table) => bind_create_table(b, create_table), + ast::Stmt::CreateForeignTable(create_foreign_table) => { + bind_create_foreign_table(b, create_foreign_table) + } ast::Stmt::CreateIndex(create_index) => bind_create_index(b, create_index), ast::Stmt::CreateFunction(create_function) => bind_create_function(b, create_function), ast::Stmt::CreateAggregate(create_aggregate) => bind_create_aggregate(b, create_aggregate), @@ -122,6 +125,29 @@ fn bind_create_table(b: &mut Binder, create_table: ast::CreateTable) { b.scopes[root].insert(table_name, table_id); } +fn bind_create_foreign_table(b: &mut Binder, create_foreign_table: ast::CreateForeignTable) { + let Some(path) = create_foreign_table.path() else { + return; + }; + let Some(table_name) = item_name(&path) else { + return; + }; + let name_ptr = path_to_ptr(&path); + let Some(schema) = schema_name(b, &path, false) else { + return; + }; + + let table_id = b.symbols.alloc(Symbol { + kind: SymbolKind::Table, + ptr: name_ptr, + schema, + params: None, + }); + + let root = b.root_scope(); + b.scopes[root].insert(table_name, table_id); +} + fn bind_create_index(b: &mut Binder, create_index: ast::CreateIndex) { let Some(name) = create_index.name() else { return; diff --git a/crates/squawk_ide/src/classify.rs b/crates/squawk_ide/src/classify.rs index 55ec72f3..c8f51aa3 100644 --- a/crates/squawk_ide/src/classify.rs +++ b/crates/squawk_ide/src/classify.rs @@ -424,7 +424,12 @@ pub(crate) enum NameClass { create_table: ast::CreateTable, column: ast::Column, }, + ColumnDefinitionForeignTable { + create_foreign_table: ast::CreateForeignTable, + column: ast::Column, + }, CreateTable(ast::CreateTable), + CreateForeignTable(ast::CreateForeignTable), WithTable(ast::WithTable), CreateIndex(ast::CreateIndex), CreateSequence(ast::CreateSequence), @@ -461,6 +466,15 @@ pub(crate) fn classify_name(name: &ast::Name) -> Option { } return Some(NameClass::CreateTable(create_table)); } + if let Some(create_foreign_table) = ast::CreateForeignTable::cast(ancestor.clone()) { + if let Some(column) = column_parent { + return Some(NameClass::ColumnDefinitionForeignTable { + create_foreign_table, + column, + }); + } + return Some(NameClass::CreateForeignTable(create_foreign_table)); + } if let Some(create_index) = ast::CreateIndex::cast(ancestor.clone()) { return Some(NameClass::CreateIndex(create_index)); } diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index 79daad0f..0f710d18 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -519,6 +519,40 @@ create table t$0(x bigint, y bigint); "); } + #[test] + fn goto_foreign_table_column() { + assert_snapshot!(goto(" +create foreign table ft(a int) + server s; + +select a$0 from ft; +"), @r" + ╭▸ + 2 │ create foreign table ft(a int) + │ ─ 2. destination + ‡ + 5 │ select a from ft; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_foreign_table_definition() { + assert_snapshot!(goto(" +create foreign table ft(a int) + server s; + +select a from ft$0; +"), @r" + ╭▸ + 2 │ create foreign table ft(a int) + │ ── 2. destination + ‡ + 5 │ select a from ft; + ╰╴ ─ 1. source + "); + } + #[test] fn goto_foreign_key_references_table() { assert_snapshot!(goto(" diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index dc0f77b3..69e422b8 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -132,9 +132,18 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { create_table, column, } => return hover_column_definition(&create_table, &column, &binder), + NameClass::ColumnDefinitionForeignTable { + create_foreign_table, + column, + } => { + return hover_column_definition(&create_foreign_table, &column, &binder); + } NameClass::CreateTable(create_table) => { return format_create_table(&create_table, &binder); } + NameClass::CreateForeignTable(create_foreign_table) => { + return format_create_foreign_table(&create_foreign_table, &binder); + } NameClass::WithTable(with_table) => return format_with_table(&with_table), NameClass::CreateIndex(create_index) => { return format_create_index(&create_index, &binder); @@ -268,7 +277,7 @@ fn format_hover_for_column_node( let create_table = column .syntax() .ancestors() - .find_map(ast::CreateTable::cast)?; + .find_map(ast::CreateTableLike::cast)?; let path = create_table.path()?; let (schema, table_name) = resolve::resolve_table_info(binder, &path)?; @@ -314,7 +323,7 @@ fn hover_composite_type_field( } fn hover_column_definition( - create_table: &ast::CreateTable, + create_table: &impl ast::HasCreateTable, column: &ast::Column, binder: &binder::Binder, ) -> Option { @@ -360,6 +369,9 @@ fn hover_table_from_ptr( resolve::TableSource::CreateTable(create_table) => { format_create_table(&create_table, binder) } + resolve::TableSource::CreateForeignTable(create_foreign_table) => { + format_create_foreign_table(&create_foreign_table, binder) + } } } @@ -448,6 +460,9 @@ fn hover_qualified_star_columns( resolve::TableSource::CreateTable(create_table) => { hover_qualified_star_columns_from_table(&create_table, binder) } + resolve::TableSource::CreateForeignTable(create_foreign_table) => { + hover_qualified_star_columns_from_table(&create_foreign_table, binder) + } resolve::TableSource::CreateView(create_view) => { hover_qualified_star_columns_from_view(&create_view, binder) } @@ -455,7 +470,7 @@ fn hover_qualified_star_columns( } fn hover_qualified_star_columns_from_table( - create_table: &ast::CreateTable, + create_table: &impl ast::HasCreateTable, binder: &binder::Binder, ) -> Option { let path = create_table.path()?; @@ -641,6 +656,21 @@ fn format_create_table(create_table: &ast::CreateTable, binder: &binder::Binder) Some(format!("table {}.{}{}", schema, table_name, args)) } +fn format_create_foreign_table( + create_foreign_table: &ast::CreateForeignTable, + binder: &binder::Binder, +) -> Option { + let path = create_foreign_table.path()?; + let (schema, table_name) = resolve::resolve_table_info(binder, &path)?; + let schema = schema.to_string(); + let args = create_foreign_table + .table_arg_list() + .map(|list| list.syntax().text().to_string()) + .unwrap_or_default(); + + Some(format!("foreign table {}.{}{}", schema, table_name, args)) +} + fn format_create_view(create_view: &ast::CreateView, binder: &binder::Binder) -> Option { let path = create_view.path()?; let (schema, view_name) = resolve::resolve_view_info(binder, &path)?; diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index 3ef95157..fcafba76 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -666,7 +666,7 @@ fn resolve_column_for_path( let create_table = table_name_node .ancestors() - .find_map(ast::CreateTable::cast)?; + .find_map(ast::CreateTableLike::cast)?; find_column_in_create_table(&create_table, &column_name) } @@ -882,8 +882,10 @@ fn resolve_select_qualified_column( if let Some(table_ptr) = resolve_table(binder, &table_name, &schema, position) { let table_name_node = table_ptr.to_node(root); - - if let Some(create_table) = table_name_node.ancestors().find_map(ast::CreateTable::cast) { + if let Some(create_table) = table_name_node + .ancestors() + .find_map(ast::CreateTableLike::cast) + { // 1. Try to find a matching column (columns take precedence) if let Some(ptr) = find_column_in_create_table(&create_table, &column_name) { return Some(ptr); @@ -950,7 +952,10 @@ fn resolve_column_from_table_or_view( if let Some(table_ptr) = resolve_table(binder, table_name, schema, position) { let table_name_node = table_ptr.to_node(root); - if let Some(create_table) = table_name_node.ancestors().find_map(ast::CreateTable::cast) { + if let Some(create_table) = table_name_node + .ancestors() + .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) { return Some(ptr); @@ -1204,7 +1209,7 @@ fn resolve_from_item_for_fn_call_column( let table_name_node = table_ptr.to_node(root); let create_table = table_name_node .ancestors() - .find_map(ast::CreateTable::cast)?; + .find_map(ast::CreateTableLike::cast)?; find_column_in_create_table(&create_table, column_name) } @@ -1324,19 +1329,18 @@ pub(crate) fn extract_column_name(col: &ast::Column) -> Option { } pub(crate) fn find_column_in_create_table( - create_table: &ast::CreateTable, + create_table: &impl ast::HasCreateTable, column_name: &Name, ) -> Option { - create_table.table_arg_list()?.args().find_map(|arg| { - if let ast::TableArg::Column(column) = arg + 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())); - } else { - None } - }) + } + None } // TODO: this is similar to the CTE funcs, maybe we can simplify @@ -1748,6 +1752,7 @@ pub(crate) enum TableSource { WithTable(ast::WithTable), CreateView(ast::CreateView), CreateTable(ast::CreateTable), + CreateForeignTable(ast::CreateForeignTable), } pub(crate) fn find_table_source(node: &SyntaxNode) -> Option { @@ -1760,9 +1765,13 @@ pub(crate) fn find_table_source(node: &SyntaxNode) -> Option { return Some(TableSource::CreateView(create_view)); } - if let Some(create_table) = ast::CreateTable::cast(ancestor) { + if let Some(create_table) = ast::CreateTable::cast(ancestor.clone()) { return Some(TableSource::CreateTable(create_table)); } + + if let Some(create_foreign_table) = ast::CreateForeignTable::cast(ancestor) { + return Some(TableSource::CreateForeignTable(create_foreign_table)); + } } None @@ -1880,7 +1889,10 @@ fn count_columns_for_from_item( if let Some(table_ptr) = resolve_table(binder, &table_name, &schema, position) { let table_name_node = table_ptr.to_node(root); - if let Some(create_table) = table_name_node.ancestors().find_map(ast::CreateTable::cast) { + if let Some(create_table) = table_name_node + .ancestors() + .find_map(ast::CreateTableLike::cast) + { let mut count: usize = 0; if let Some(args) = create_table.table_arg_list() { for arg in args.args() { @@ -2104,7 +2116,7 @@ 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: &ast::CreateTable) -> Vec { +pub(crate) fn collect_table_columns(create_table: &impl ast::HasCreateTable) -> Vec { let mut columns = vec![]; if let Some(arg_list) = create_table.table_arg_list() { for arg in arg_list.args() { diff --git a/crates/squawk_syntax/src/ast.rs b/crates/squawk_syntax/src/ast.rs index aa377541..2eac14c2 100644 --- a/crates/squawk_syntax/src/ast.rs +++ b/crates/squawk_syntax/src/ast.rs @@ -36,7 +36,6 @@ use crate::syntax_node::{SyntaxNode, SyntaxNodeChildren, SyntaxToken}; use squawk_parser::SyntaxKind; pub use self::{ - // generated::{nodes::*, tokens::*}, generated::tokens::*, nodes::*, // node_ext::{ @@ -46,18 +45,7 @@ pub use self::{ // }, // operators::{ArithOp, BinaryOp, CmpOp, LogicOp, Ordering, RangeOp, UnaryOp}, // token_ext::{CommentKind, CommentPlacement, CommentShape, IsString, QuoteOffsets, Radix}, - traits::{ - // AttrDocCommentIter, DocCommentIter, - HasArgList, // HasAttrs, HasDocComments, HasGenericArgs, - HasIfExists, - HasIfNotExists, // HasTypeBounds, - // HasVisibility, - // HasGenericParams, HasLoopBody, - HasName, - HasParamList, - HasWithClause, - NameLike, - }, + traits::{HasCreateTable, HasParamList, HasWithClause, NameLike}, }; /// The main trait to go from untyped `SyntaxNode` to a typed ast. The diff --git a/crates/squawk_syntax/src/ast/node_ext.rs b/crates/squawk_syntax/src/ast/node_ext.rs index 9ec68236..8ccb9c5d 100644 --- a/crates/squawk_syntax/src/ast/node_ext.rs +++ b/crates/squawk_syntax/src/ast/node_ext.rs @@ -241,6 +241,10 @@ impl ast::HasWithClause for ast::Insert {} impl ast::HasWithClause for ast::Update {} impl ast::HasWithClause for ast::Delete {} +impl ast::HasCreateTable for ast::CreateTable {} +impl ast::HasCreateTable for ast::CreateForeignTable {} +impl ast::HasCreateTable for ast::CreateTableLike {} + #[test] fn index_expr() { let source_code = " diff --git a/crates/squawk_syntax/src/ast/nodes.rs b/crates/squawk_syntax/src/ast/nodes.rs index eb1cb4c9..03691038 100644 --- a/crates/squawk_syntax/src/ast/nodes.rs +++ b/crates/squawk_syntax/src/ast/nodes.rs @@ -1 +1,45 @@ pub use crate::ast::generated::nodes::*; +use crate::{ + SyntaxNode, + ast::{self, AstNode, support}, +}; + +// TODO: Initial attempt to try and unify the CreateTable and +// CreateForeignTable. Not sure this is the right approach, we may want to be +// more general, like TableSource, which can be a View, CTE, Table, +// ForeignTable, Subquery, etc. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CreateTableLike { + pub(crate) syntax: SyntaxNode, +} +impl CreateTableLike { + #[inline] + pub fn path(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn table_arg_list(&self) -> Option { + support::child(&self.syntax) + } +} +impl AstNode for CreateTableLike { + #[inline] + fn can_cast(kind: ast::SyntaxKind) -> bool { + matches!( + kind, + ast::SyntaxKind::CREATE_TABLE | ast::SyntaxKind::CREATE_FOREIGN_TABLE + ) + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} diff --git a/crates/squawk_syntax/src/ast/traits.rs b/crates/squawk_syntax/src/ast/traits.rs index ed0ba2fb..0a39ecb7 100644 --- a/crates/squawk_syntax/src/ast/traits.rs +++ b/crates/squawk_syntax/src/ast/traits.rs @@ -3,41 +3,29 @@ use crate::ast; use crate::ast::{AstNode, support}; -pub trait HasName: AstNode { - fn name(&self) -> Option { - support::child(self.syntax()) - } -} - pub trait NameLike: AstNode {} -pub trait HasWithClause: AstNode { +pub trait HasCreateTable: AstNode { #[inline] - fn with_clause(&self) -> Option { + fn path(&self) -> Option { support::child(self.syntax()) } -} -pub trait HasArgList: AstNode { - fn arg_list(&self) -> Option { - support::child(self.syntax()) - } -} - -pub trait HasParamList: AstNode { - fn param_list(&self) -> Option { + #[inline] + fn table_arg_list(&self) -> Option { support::child(self.syntax()) } } -pub trait HasIfExists: AstNode { - fn if_exists(&self) -> Option { +pub trait HasWithClause: AstNode { + #[inline] + fn with_clause(&self) -> Option { support::child(self.syntax()) } } -pub trait HasIfNotExists: AstNode { - fn if_not_exists(&self) -> Option { +pub trait HasParamList: AstNode { + fn param_list(&self) -> Option { support::child(self.syntax()) } }