From cb30afd1412f64c883dc49e0209c301ff2ee4dcc Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Thu, 23 Apr 2026 18:05:12 -0400 Subject: [PATCH] ide: internal refactor resolve module splitting it into collect, resolve, ast_nav still a ton more to do --- crates/squawk_ide/src/ast_nav.rs | 80 ++ crates/squawk_ide/src/collect.rs | 662 ++++++++++++++ crates/squawk_ide/src/completion.rs | 73 +- crates/squawk_ide/src/goto_definition.rs | 14 + crates/squawk_ide/src/hover.rs | 347 ++++---- crates/squawk_ide/src/inlay_hints.rs | 5 +- crates/squawk_ide/src/lib.rs | 2 + crates/squawk_ide/src/resolve.rs | 1014 ++-------------------- crates/squawk_syntax/src/ast/nodes.rs | 40 + 9 files changed, 1034 insertions(+), 1203 deletions(-) create mode 100644 crates/squawk_ide/src/ast_nav.rs create mode 100644 crates/squawk_ide/src/collect.rs diff --git a/crates/squawk_ide/src/ast_nav.rs b/crates/squawk_ide/src/ast_nav.rs new file mode 100644 index 00000000..087e9ceb --- /dev/null +++ b/crates/squawk_ide/src/ast_nav.rs @@ -0,0 +1,80 @@ +use squawk_syntax::ast; + +pub(crate) fn iter_from_clause( + from_clause: &ast::FromClause, +) -> impl Iterator { + from_clause.from_items().chain( + from_clause + .join_exprs() + .flat_map(|join_expr| JoinExprIter::new(&join_expr)), + ) +} + +pub(crate) fn iter_join_expr(join_expr: &ast::JoinExpr) -> impl Iterator { + JoinExprIter::new(join_expr) +} + +struct JoinExprIter { + stack: Vec, +} + +impl JoinExprIter { + fn new(join_expr: &ast::JoinExpr) -> Self { + Self { + stack: vec![JoinExprIterFrame { + join_expr: join_expr.clone(), + state: JoinExprIterState::JoinExpr, + }], + } + } +} + +struct JoinExprIterFrame { + join_expr: ast::JoinExpr, + state: JoinExprIterState, +} + +#[derive(Clone, Copy)] +enum JoinExprIterState { + FromItem, + Join, + JoinExpr, +} + +impl Iterator for JoinExprIter { + type Item = ast::FromItem; + + fn next(&mut self) -> Option { + while let Some(frame) = self.stack.last_mut() { + match frame.state { + JoinExprIterState::JoinExpr => { + frame.state = JoinExprIterState::FromItem; + + if let Some(nested_join) = frame.join_expr.join_expr() { + self.stack.push(JoinExprIterFrame { + join_expr: nested_join, + state: JoinExprIterState::JoinExpr, + }); + } + } + JoinExprIterState::FromItem => { + frame.state = JoinExprIterState::Join; + + if let Some(from_item) = frame.join_expr.from_item() { + return Some(from_item); + } + } + JoinExprIterState::Join => { + let from_item = frame.join_expr.join().and_then(|join| join.from_item()); + self.stack.pop(); + + if from_item.is_some() { + return from_item; + } + } + } + } + + None + } +} diff --git a/crates/squawk_ide/src/collect.rs b/crates/squawk_ide/src/collect.rs new file mode 100644 index 00000000..265360d9 --- /dev/null +++ b/crates/squawk_ide/src/collect.rs @@ -0,0 +1,662 @@ +use crate::ast_nav::iter_from_clause; +use crate::builtins::builtins_file; +use crate::column_name::ColumnName; +use crate::db::{File, bind, parse}; +use crate::infer::{Type, infer_type_from_expr, infer_type_from_ty}; +use crate::resolve::{ + ResolvedTableName, TableSource, extract_table_schema_from_path, find_from_item_in_from_clause, + find_table_source, qualified_star_table_name, resolve_cte_table, resolve_table_name, + resolve_table_name_ptr, resolve_view_name_ptr, select_from_with_query, + table_and_schema_from_from_item, table_ptr_from_from_item, +}; +use crate::symbols::{Name, SymbolKind}; +use salsa::Database as Db; +use squawk_syntax::{ + SyntaxNodePtr, + ast::{self, AstNode}, +}; + +pub(crate) fn columns_from_create_table( + db: &dyn Db, + file: File, + create_table: &ast::CreateTableLike, +) -> Vec<(Name, Option)> { + let mut columns = vec![]; + columns_from_create_table_impl(db, file, create_table, &mut columns, 0); + columns +} + +fn columns_from_create_table_impl( + db: &dyn Db, + file: File, + create_table: &ast::CreateTableLike, + columns: &mut Vec<(Name, Option)>, + depth: usize, +) { + if depth > 40 { + log::info!("max depth reached, probably in a cycle"); + return; + } + + if let Some(inherits) = create_table.inherits() { + for path in inherits.paths() { + if let Some((table_name, schema)) = extract_table_schema_from_path(&path) { + let position = path.syntax().text_range().start(); + if let Some(ResolvedTableName::Table(parent_table)) = + resolve_table_name(db, file, &table_name, &schema, position) + { + columns_from_create_table_impl(db, file, &parent_table, columns, depth + 1); + } + } + } + } + + if let Some(arg_list) = create_table.table_arg_list() { + for arg in arg_list.args() { + match &arg { + ast::TableArg::Column(column) => { + if let Some(name) = column.name() { + let col_name = Name::from_node(&name); + columns.push((col_name, Some(SyntaxNodePtr::new(name.syntax())))); + } + } + ast::TableArg::LikeClause(like_clause) => { + if let Some(path) = like_clause.path() + && let Some((table_name, schema)) = extract_table_schema_from_path(&path) + { + let position = path.syntax().text_range().start(); + if let Some(ResolvedTableName::Table(source_table)) = + resolve_table_name(db, file, &table_name, &schema, position) + { + columns_from_create_table_impl( + db, + file, + &source_table, + columns, + depth + 1, + ); + } + } + } + ast::TableArg::TableConstraint(_) => (), + } + } + } +} + +pub(crate) fn tables_from_item( + db: &dyn Db, + file: File, + from_item: &ast::FromItem, + results: &mut Vec, +) { + if let Some(alias) = from_item.alias() + && alias.column_list().is_some() + { + results.push(SyntaxNodePtr::new(alias.syntax())); + return; + } + + if let Some(paren_select) = from_item.paren_select() { + results.push(SyntaxNodePtr::new(paren_select.syntax())); + return; + } + + let Some((table_name, schema)) = table_and_schema_from_from_item(from_item) else { + return; + }; + + let position = from_item.syntax().text_range().start(); + if let Some(table_name_ptr) = resolve_table_name_ptr(db, file, &table_name, &schema, position) { + results.push(table_name_ptr); + return; + } + + if let Some(view_name_ptr) = resolve_view_name_ptr(db, file, &table_name, &schema, position) { + results.push(view_name_ptr); + return; + } + + if schema.is_none() + && let Some(name_ref) = from_item.name_ref() + && let Some(cte_ptr) = resolve_cte_table(&name_ref, &table_name) + { + results.push(cte_ptr); + } +} + +pub(crate) fn table_columns( + db: &dyn Db, + file: File, + create_table: &impl ast::HasCreateTable, +) -> Vec<(Name, Option)> { + table_columns_impl(db, file, create_table, 0) +} + +// TODO: combine with find_column_in_create_table_impl +fn table_columns_impl( + db: &dyn Db, + file: File, + create_table: &impl ast::HasCreateTable, + depth: usize, +) -> Vec<(Name, Option)> { + 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, schema)) = extract_table_schema_from_path(&path) { + let position = path.syntax().text_range().start(); + if let Some(ResolvedTableName::Table(parent_table)) = + resolve_table_name(db, file, &table_name, &schema, position) + { + let inherited_columns = table_columns_impl(db, file, &parent_table, depth + 1); + columns.extend(inherited_columns); + } + } + } + } + + if let Some(arg_list) = create_table.table_arg_list() { + for arg in arg_list.args() { + match &arg { + ast::TableArg::Column(column) => { + if let Some(name) = column.name() { + let ty = column.ty().and_then(|ty| infer_type_from_ty(&ty)); + columns.push((Name::from_node(&name), ty)); + } + } + ast::TableArg::LikeClause(like_clause) => { + if let Some(path) = like_clause.path() + && let Some((table_name, schema)) = extract_table_schema_from_path(&path) + { + let position = path.syntax().text_range().start(); + if let Some(ResolvedTableName::Table(source_table)) = + resolve_table_name(db, file, &table_name, &schema, position) + { + let like_columns = + table_columns_impl(db, file, &source_table, depth + 1); + columns.extend(like_columns); + } + } + } + ast::TableArg::TableConstraint(_) => (), + } + } + } + + columns +} + +fn select_from_view_like_query(create_view: &ast::CreateViewLike) -> Option { + let query = create_view.query()?; + match query { + ast::SelectVariant::Select(select) => Some(select), + ast::SelectVariant::ParenSelect(paren_select) => match paren_select.select()? { + ast::SelectVariant::Select(select) => Some(select), + _ => None, + }, + _ => None, + } +} + +fn columns_from_returning_clause_with_types( + query: &ast::WithQuery, +) -> Option)>> { + let returning_clause = match query { + ast::WithQuery::Delete(delete) => delete.returning_clause(), + ast::WithQuery::Insert(insert) => insert.returning_clause(), + ast::WithQuery::Merge(merge) => merge.returning_clause(), + ast::WithQuery::Update(update) => update.returning_clause(), + ast::WithQuery::Select(_) + | ast::WithQuery::CompoundSelect(_) + | ast::WithQuery::Table(_) + | ast::WithQuery::Values(_) + | ast::WithQuery::ParenSelect(_) => None, + }; + + if let Some(returning_clause) = returning_clause { + if let Some(target_list) = returning_clause.target_list() { + return Some(target_list_columns_with_types(&target_list)); + } + return Some(vec![]); + } + + None +} + +pub(crate) fn view_like_columns_with_types( + create_view: &ast::CreateViewLike, +) -> Vec<(Name, Option)> { + let alias_columns: Vec = create_view + .column_list() + .into_iter() + .flat_map(|column_list| column_list.columns()) + .filter_map(|column| column.name().map(|name| Name::from_node(&name))) + .collect(); + + let Some(select) = select_from_view_like_query(create_view) else { + return vec![]; + }; + let Some(select_clause) = select.select_clause() else { + return vec![]; + }; + let Some(target_list) = select_clause.target_list() else { + return vec![]; + }; + + let base_columns = target_list_columns_with_types(&target_list); + + if alias_columns.is_empty() { + return base_columns; + } + + let mut results = vec![]; + + for (idx, alias_name) in alias_columns.iter().enumerate() { + results.push(( + alias_name.clone(), + base_columns.get(idx).and_then(|(_, ty)| ty.clone()), + )); + } + + results.extend(base_columns.into_iter().skip(alias_columns.len())); + + results +} + +pub(crate) fn with_table_column_names( + db: &dyn Db, + file: File, + with_table: ast::WithTable, +) -> Vec { + with_table_columns_with_types(db, file, with_table) + .into_iter() + .map(|(name, _)| name) + .collect() +} + +pub(crate) fn with_table_columns_with_types( + db: &dyn Db, + file: File, + with_table: ast::WithTable, +) -> Vec<(Name, Option)> { + let alias_columns: Vec = with_table + .column_list() + .into_iter() + .flat_map(|column_list| column_list.columns()) + .filter_map(|column| column.name().map(|name| Name::from_node(&name))) + .collect(); + + let base_columns = with_table_query_columns_with_types(db, file, with_table); + + if alias_columns.is_empty() { + return base_columns; + } + + let mut results = vec![]; + + for (idx, alias_name) in alias_columns.iter().enumerate() { + results.push(( + alias_name.clone(), + base_columns.get(idx).and_then(|(_, ty)| ty.clone()), + )); + } + + results.extend(base_columns.into_iter().skip(alias_columns.len())); + + results +} + +fn with_table_query_columns_with_types( + db: &dyn Db, + file: File, + with_table: ast::WithTable, +) -> Vec<(Name, Option)> { + let Some(query) = with_table.query() else { + return vec![]; + }; + + if let ast::WithQuery::Values(values) = query { + let mut results = vec![]; + if let Some(row_list) = values.row_list() + && let Some(first_row) = row_list.rows().next() + { + for (idx, expr) in first_row.exprs().enumerate() { + let name = Name::from_string(format!("column{}", idx + 1)); + let ty = infer_type_from_expr(&expr); + results.push((name, ty)); + } + } + return results; + } + + if let Some(columns) = columns_from_returning_clause_with_types(&query) { + return columns; + } + + let Some(cte_select) = select_from_with_query(query) else { + return vec![]; + }; + let Some(select_clause) = cte_select.select_clause() else { + return vec![]; + }; + let Some(target_list) = select_clause.target_list() else { + return vec![]; + }; + + let from_clause = cte_select.from_clause(); + let mut columns = vec![]; + + for target in target_list.targets() { + if let Some((col_name, _node)) = ColumnName::from_target(target.clone()) { + if let Some(col_name_str) = col_name.to_string() { + let ty = target.expr().and_then(|e| infer_type_from_expr(&e)); + columns.push((Name::from_string(col_name_str), ty)); + continue; + } + + if target.star_token().is_some() + && let Some(from_clause) = &from_clause + { + columns.extend(columns_for_star_from_clause(db, file, from_clause)); + continue; + } + } + + if let Some(expr) = target.expr() + && let ast::Expr::FieldExpr(field_expr) = expr + && let Some(table_name) = qualified_star_table_name(&field_expr) + && let Some(from_clause) = &from_clause + && let Some(from_item) = find_from_item_in_from_clause(from_clause, &table_name) + { + columns.extend(columns_for_star_from_from_item(db, file, &from_item)); + } + } + + columns +} + +fn columns_for_star_from_clause( + db: &dyn Db, + file: File, + from_clause: &ast::FromClause, +) -> Vec<(Name, Option)> { + let mut columns = vec![]; + + for from_item in iter_from_clause(from_clause) { + columns.extend(columns_for_star_from_from_item(db, file, &from_item)); + } + + columns +} + +fn columns_for_star_from_from_item( + db: &dyn Db, + file: File, + from_item: &ast::FromItem, +) -> Vec<(Name, Option)> { + if let Some(alias) = from_item.alias() + && alias.column_list().is_some() + { + return columns_for_star_from_alias(db, file, from_item, &alias); + } + + let Some(table_ptr) = table_ptr_from_from_item(db, file, from_item) else { + return vec![]; + }; + + columns_for_star_from_table_ptr(db, file, &table_ptr) +} + +fn columns_for_star_from_alias( + db: &dyn Db, + file: File, + from_item: &ast::FromItem, + alias: &ast::Alias, +) -> Vec<(Name, Option)> { + let alias_columns: Vec = alias + .column_list() + .into_iter() + .flat_map(|column_list| column_list.columns()) + .filter_map(|column| column.name().map(|name| Name::from_node(&name))) + .collect(); + + let Some(table_ptr) = table_ptr_from_from_item(db, file, from_item) else { + return vec![]; + }; + + let base_columns = columns_for_star_from_table_ptr(db, file, &table_ptr); + let mut results = vec![]; + + for (idx, alias_name) in alias_columns.iter().enumerate() { + results.push(( + alias_name.clone(), + base_columns.get(idx).and_then(|(_, ty)| ty.clone()), + )); + } + + results.extend(base_columns.into_iter().skip(alias_columns.len())); + + results +} + +fn columns_for_star_from_table_ptr( + db: &dyn Db, + file: File, + table_ptr: &SyntaxNodePtr, +) -> Vec<(Name, Option)> { + let tree = parse(db, file).tree(); + let root = tree.syntax(); + let table_node = table_ptr.to_node(root); + + if let Some(paren_select) = ast::ParenSelect::cast(table_node.clone()) { + return paren_select_columns_with_types(db, file, &paren_select); + } + + match find_table_source(&table_node) { + Some(TableSource::Alias(alias)) => { + let Some(from_item) = alias.syntax().ancestors().find_map(ast::FromItem::cast) else { + return vec![]; + }; + columns_for_star_from_alias(db, file, &from_item, &alias) + } + Some(TableSource::WithTable(with_table)) => { + with_table_columns_with_types(db, file, with_table) + } + Some(TableSource::CreateTable(create_table)) => table_columns(db, file, &create_table), + Some(TableSource::CreateView(create_view)) => view_like_columns_with_types(&create_view), + Some(TableSource::ParenSelect(paren_select)) => { + paren_select_columns_with_types(db, file, &paren_select) + } + None => vec![], + } +} + +pub(crate) fn target_list_columns_with_types( + target_list: &ast::TargetList, +) -> Vec<(Name, Option)> { + let mut columns = vec![]; + for target in target_list.targets() { + if let Some((col_name, _node)) = ColumnName::from_target(target.clone()) + && let Some(col_name_str) = col_name.to_string() + { + let ty = target.expr().and_then(|e| infer_type_from_expr(&e)); + columns.push((Name::from_string(col_name_str), ty)); + } + } + columns +} + +pub(crate) fn paren_select_columns_with_types( + db: &dyn Db, + file: File, + paren_select: &ast::ParenSelect, +) -> Vec<(Name, Option)> { + let Some(select_variant) = paren_select.select() else { + return vec![]; + }; + select_variant_columns_with_types(db, file, &select_variant) +} + +fn select_variant_columns_with_types( + db: &dyn Db, + file: File, + select_variant: &ast::SelectVariant, +) -> Vec<(Name, Option)> { + let binder = bind(db, file); + let tree = parse(db, file).tree(); + let root = tree.syntax(); + match select_variant { + ast::SelectVariant::Values(values) => { + let mut results = vec![]; + if let Some(row_list) = values.row_list() + && let Some(first_row) = row_list.rows().next() + { + for (idx, expr) in first_row.exprs().enumerate() { + let name = Name::from_string(format!("column{}", idx + 1)); + let ty = infer_type_from_expr(&expr); + results.push((name, ty)); + } + } + results + } + ast::SelectVariant::Select(select) => { + let Some(select_clause) = select.select_clause() else { + return vec![]; + }; + let Some(target_list) = select_clause.target_list() else { + return vec![]; + }; + target_list_columns_with_types(&target_list) + } + ast::SelectVariant::SelectInto(select_into) => { + let Some(select_clause) = select_into.select_clause() else { + return vec![]; + }; + let Some(target_list) = select_clause.target_list() else { + return vec![]; + }; + target_list_columns_with_types(&target_list) + } + ast::SelectVariant::ParenSelect(nested) => { + paren_select_columns_with_types(db, file, nested) + } + ast::SelectVariant::CompoundSelect(compound) => { + let Some(lhs) = compound.lhs() else { + return vec![]; + }; + select_variant_columns_with_types(db, file, &lhs) + } + ast::SelectVariant::Table(table) => { + let Some(path) = table.relation_name().and_then(|r| r.path()) else { + return vec![]; + }; + let Some((table_name, schema)) = extract_table_schema_from_path(&path) else { + return vec![]; + }; + + if schema.is_none() + && let Some(name_ref) = path.segment().and_then(|segment| segment.name_ref()) + && let Some(with_table_ptr) = resolve_cte_table(&name_ref, &table_name) + && let Some(with_table) = with_table_ptr + .to_node(root) + .ancestors() + .find_map(ast::WithTable::cast) + { + return with_table_column_names(db, file, with_table) + .into_iter() + .map(|name| (name, None)) + .collect(); + } + + let position = table.syntax().text_range().start(); + let Some(table_ptr) = + binder.lookup_with(&table_name, SymbolKind::Table, position, &schema) + else { + return vec![]; + }; + let Some(create_table) = table_ptr + .to_node(root) + .ancestors() + .find_map(ast::CreateTableLike::cast) + else { + return vec![]; + }; + table_columns(db, file, &create_table) + } + } +} + +pub(crate) fn star_column_names(db: &dyn Db, file: File, table_ptr: &SyntaxNodePtr) -> Vec { + let source_file = parse(db, file).tree(); + let root = source_file.syntax(); + let table_name_node = table_ptr.to_node(root); + + if let Some(paren_select) = ast::ParenSelect::cast(table_name_node.clone()) { + let columns: Vec = paren_select_columns_with_types(db, file, &paren_select) + .into_iter() + .map(|(name, _ty)| name) + .collect(); + if !columns.is_empty() { + return columns; + } + return star_column_names_from_paren_select(db, file, &paren_select); + } + + match find_table_source(&table_name_node) { + Some(TableSource::Alias(alias)) => alias + .column_list() + .into_iter() + .flat_map(|column_list| column_list.columns()) + .filter_map(|column| column.name().map(|name| Name::from_node(&name))) + .collect(), + Some(TableSource::WithTable(with_table)) => { + let columns = with_table_column_names(db, file, with_table.clone()); + if !columns.is_empty() { + return columns; + } + + with_table_column_names(db, builtins_file(db), with_table) + } + Some(TableSource::CreateTable(create_table)) => table_columns(db, file, &create_table) + .into_iter() + .map(|(name, _)| name) + .collect(), + Some(TableSource::CreateView(create_view)) => view_like_columns_with_types(&create_view) + .into_iter() + .map(|(name, _)| name) + .collect(), + Some(TableSource::ParenSelect(paren_select)) => { + paren_select_columns_with_types(db, file, &paren_select) + .into_iter() + .map(|(name, _ty)| name) + .collect() + } + None => vec![], + } +} + +fn star_column_names_from_paren_select( + db: &dyn Db, + file: File, + paren_select: &ast::ParenSelect, +) -> Vec { + let Some(ast::SelectVariant::Select(select)) = paren_select.select() else { + return vec![]; + }; + let Some(from_clause) = select.from_clause() else { + return vec![]; + }; + let mut columns = vec![]; + for from_item in iter_from_clause(&from_clause) { + if let Some(table_ptr) = table_ptr_from_from_item(db, file, &from_item) { + columns.extend(star_column_names(db, file, &table_ptr)); + } + } + columns +} diff --git a/crates/squawk_ide/src/completion.rs b/crates/squawk_ide/src/completion.rs index 18b3a29d..ad3236f4 100644 --- a/crates/squawk_ide/src/completion.rs +++ b/crates/squawk_ide/src/completion.rs @@ -4,6 +4,7 @@ use squawk_syntax::ast::{self, AstNode}; use squawk_syntax::{SyntaxKind, SyntaxNode, SyntaxToken}; use crate::binder; +use crate::collect; use crate::db::{File, bind, parse}; use crate::resolve; use crate::symbols::{Name, Schema, SymbolKind}; @@ -344,23 +345,7 @@ fn column_completions_from_clause( let table_node = table_ptr.to_node(&syntax_root); match resolve::find_table_source(&table_node) { Some(resolve::TableSource::CreateTable(create_table)) => { - let columns = resolve::collect_table_columns(db, file, &create_table); - completions.extend(columns.into_iter().filter_map(|column| { - let name = column.name()?; - let detail = column.ty().map(|t| t.syntax().text().to_string()); - Some(CompletionItem { - label: Name::from_node(&name).to_string(), - kind: CompletionItemKind::Column, - detail, - insert_text: None, - insert_text_format: None, - trigger_completion_after_insert: false, - sort_text: None, - }) - })); - } - Some(resolve::TableSource::WithTable(with_table)) => { - let columns = resolve::collect_with_table_columns_with_types(db, file, with_table); + let columns = collect::table_columns(db, file, &create_table); completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem { label: name.to_string(), kind: CompletionItemKind::Column, @@ -371,8 +356,8 @@ fn column_completions_from_clause( sort_text: None, })); } - Some(resolve::TableSource::CreateView(create_view)) => { - let columns = resolve::collect_view_columns_with_types(&create_view); + Some(resolve::TableSource::WithTable(with_table)) => { + let columns = collect::with_table_columns_with_types(db, file, with_table); completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem { label: name.to_string(), kind: CompletionItemKind::Column, @@ -383,9 +368,8 @@ fn column_completions_from_clause( sort_text: None, })); } - Some(resolve::TableSource::CreateMaterializedView(create_materialized_view)) => { - let columns = - resolve::collect_materialized_view_columns_with_types(create_materialized_view); + Some(resolve::TableSource::CreateView(create_view)) => { + let columns = collect::view_like_columns_with_types(&create_view); completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem { label: name.to_string(), kind: CompletionItemKind::Column, @@ -435,8 +419,7 @@ fn column_completions_from_clause( ); } Some(resolve::TableSource::ParenSelect(paren_select)) => { - let columns = - resolve::collect_paren_select_columns_with_types(db, file, &paren_select); + let columns = collect::paren_select_columns_with_types(db, file, &paren_select); completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem { label: name.to_string(), kind: CompletionItemKind::Column, @@ -470,35 +453,25 @@ fn alias_base_columns_with_types( match resolve::find_table_source(&table_node) { Some(resolve::TableSource::CreateTable(create_table)) => { - resolve::collect_table_columns(db, file, &create_table) + collect::table_columns(db, file, &create_table) .into_iter() - .filter_map(|column| { - let name = column.name()?; - let detail = column.ty().map(|t| t.syntax().text().to_string()); - Some((Name::from_node(&name), detail)) - }) + .map(|(name, ty)| (name, ty.map(|t| t.to_string()))) .collect() } Some(resolve::TableSource::WithTable(with_table)) => { - resolve::collect_with_table_columns_with_types(db, file, with_table) + collect::with_table_columns_with_types(db, file, with_table) .into_iter() .map(|(name, ty)| (name, ty.map(|t| t.to_string()))) .collect() } Some(resolve::TableSource::CreateView(create_view)) => { - resolve::collect_view_columns_with_types(&create_view) - .into_iter() - .map(|(name, ty)| (name, ty.map(|t| t.to_string()))) - .collect() - } - Some(resolve::TableSource::CreateMaterializedView(create_materialized_view)) => { - resolve::collect_materialized_view_columns_with_types(create_materialized_view) + collect::view_like_columns_with_types(&create_view) .into_iter() .map(|(name, ty)| (name, ty.map(|t| t.to_string()))) .collect() } Some(resolve::TableSource::ParenSelect(paren_select)) => { - resolve::collect_paren_select_columns_with_types(db, file, &paren_select) + collect::paren_select_columns_with_types(db, file, &paren_select) .into_iter() .map(|(name, ty)| (name, ty.map(|t| t.to_string()))) .collect() @@ -683,19 +656,15 @@ fn delete_expr_completions( .ancestors() .find_map(ast::CreateTableLike::cast) { - let columns = resolve::collect_table_columns(db, file, &create_table); - completions.extend(columns.into_iter().filter_map(|column| { - let name = column.name()?; - let detail = column.ty().map(|t| t.syntax().text().to_string()); - Some(CompletionItem { - label: Name::from_node(&name).to_string(), - kind: CompletionItemKind::Column, - detail, - insert_text: None, - insert_text_format: None, - trigger_completion_after_insert: false, - sort_text: None, - }) + let columns = collect::table_columns(db, file, &create_table); + completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem { + label: name.to_string(), + kind: CompletionItemKind::Column, + detail: ty.map(|t| t.to_string()), + insert_text: None, + insert_text_format: None, + trigger_completion_after_insert: false, + sort_text: None, })); } diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index 0e4a0425..204e8cd4 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -2686,6 +2686,20 @@ select v.a$0 from v; "); } + #[test] + fn goto_materialized_view_column_with_explicit_column_list() { + assert_snapshot!(goto(" +create materialized view mv (x, y) as select 1 as a, 2 as b; +select x$0 from mv; +"), @r" + ╭▸ + 2 │ create materialized view mv (x, y) as select 1 as a, 2 as b; + │ ─ 2. destination + 3 │ select x from mv; + ╰╴ ─ 1. source + "); + } + #[test] fn goto_view_table_qualifier() { assert_snapshot!(goto(" diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index 1c210eea..8042e9f6 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -1,5 +1,6 @@ use crate::builtins::builtins_file; use crate::classify::{NameClass, classify_name}; +use crate::collect; use crate::column_name::ColumnName; use crate::comments::preceding_comment; use crate::db::{File, bind, parse}; @@ -249,6 +250,11 @@ impl ColumnHover { fn table_column(table_name: &str, column_name: &str) -> String { format!("column {table_name}.{column_name}") } + + fn table_column_type(table_name: &str, column_name: &str, ty: &str) -> String { + format!("column {table_name}.{column_name} {ty}") + } + fn schema_table_column_type( schema: &str, table_name: &str, @@ -331,7 +337,7 @@ fn format_hover_for_column_ptr( } else { Name::from_string(column_name_node.text().to_string()) }; - let ty = resolve::collect_paren_select_columns_with_types(db, file, &paren_select) + let ty = collect::paren_select_columns_with_types(db, file, &paren_select) .into_iter() .find(|(name, _)| *name == column_name) .and_then(|(_, ty)| ty)?; @@ -452,9 +458,8 @@ fn format_table_source(db: &dyn Db, file: File, source: resolve::TableSource) -> match source { resolve::TableSource::Alias(alias) => format_alias_with_column_list(db, file, alias), resolve::TableSource::WithTable(with_table) => format_with_table(with_table), - resolve::TableSource::CreateView(create_view) => format_create_view(db, file, create_view), - resolve::TableSource::CreateMaterializedView(create_materialized_view) => { - format_create_materialized_view(db, file, create_materialized_view) + resolve::TableSource::CreateView(create_view) => { + format_create_view_like(db, file, create_view) } resolve::TableSource::CreateTable(create_table) => { format_create_table(db, file, create_table) @@ -492,7 +497,7 @@ fn format_alias_with_column_list(db: &dyn Db, file: File, alias: ast::Alias) -> if let Some(from_item) = alias.syntax().ancestors().find_map(ast::FromItem::cast) && let Some(table_ptr) = resolve::table_ptr_from_from_item(db, file, &from_item) { - let base_columns = collect_star_column_names(db, file, &table_ptr); + let base_columns = collect::star_column_names(db, file, &table_ptr); for column in base_columns.iter().skip(columns.len()) { columns.push(column.clone()); } @@ -609,10 +614,7 @@ fn hover_qualified_star_columns( hover_qualified_star_columns_from_table(db, file, create_table) } resolve::TableSource::CreateView(create_view) => { - hover_qualified_star_columns_from_view(db, file, &create_view) - } - resolve::TableSource::CreateMaterializedView(create_materialized_view) => { - hover_qualified_star_columns_from_materialized_view(db, file, create_materialized_view) + hover_qualified_star_columns_from_view_like(db, file, &create_view) } resolve::TableSource::ParenSelect(paren_select) => { hover_qualified_star_columns_from_subquery(db, file, &paren_select) @@ -648,7 +650,7 @@ fn hover_qualified_star_columns_from_alias( let from_item = alias.syntax().ancestors().find_map(ast::FromItem::cast)?; let table_ptr = resolve::table_ptr_from_from_item(db, file, &from_item)?; - let base_column_names = collect_star_column_names(db, file, &table_ptr); + let base_column_names = collect::star_column_names(db, file, &table_ptr); for column_name in base_column_names.iter().skip(alias_columns.len()) { results.push(Hover::snippet(ColumnHover::table_column( @@ -660,87 +662,6 @@ fn hover_qualified_star_columns_from_alias( merge_hovers(results) } -fn collect_star_column_names( - db: &dyn Db, - file: File, - table_ptr: &squawk_syntax::SyntaxNodePtr, -) -> Vec { - let source_file = parse(db, file).tree(); - let root = source_file.syntax(); - let table_name_node = table_ptr.to_node(root); - - if let Some(paren_select) = ast::ParenSelect::cast(table_name_node.clone()) { - let columns: Vec = - resolve::collect_paren_select_columns_with_types(db, file, &paren_select) - .into_iter() - .map(|(name, _ty)| name) - .collect(); - if !columns.is_empty() { - return columns; - } - return collect_star_column_names_from_paren_select(db, file, &paren_select); - } - - match resolve::find_table_source(&table_name_node) { - Some(resolve::TableSource::Alias(alias)) => alias - .column_list() - .into_iter() - .flat_map(|column_list| column_list.columns()) - .filter_map(|column| column.name().map(|name| Name::from_node(&name))) - .collect(), - Some(resolve::TableSource::WithTable(with_table)) => { - let columns = resolve::collect_with_table_column_names(db, file, with_table.clone()); - if !columns.is_empty() { - return columns; - } - - resolve::collect_with_table_column_names(db, builtins_file(db), with_table) - } - Some(resolve::TableSource::CreateTable(create_table)) => { - resolve::collect_table_columns(db, file, &create_table) - .into_iter() - .filter_map(|column| column.name().map(|name| Name::from_node(&name))) - .collect() - } - Some(resolve::TableSource::CreateView(create_view)) => { - resolve::collect_view_column_names(&create_view) - } - Some(resolve::TableSource::CreateMaterializedView(create_materialized_view)) => { - resolve::collect_materialized_view_column_names(create_materialized_view) - } - Some(resolve::TableSource::ParenSelect(paren_select)) => { - resolve::collect_paren_select_columns_with_types(db, file, &paren_select) - .into_iter() - .map(|(name, _ty)| name) - .collect() - } - None => vec![], - } -} - -fn collect_star_column_names_from_paren_select( - db: &dyn Db, - file: File, - paren_select: &ast::ParenSelect, -) -> Vec { - let Some(select_variant) = paren_select.select() else { - return vec![]; - }; - let ast::SelectVariant::Select(select) = select_variant else { - return vec![]; - }; - let Some(from_clause) = select.from_clause() else { - return vec![]; - }; - let mut columns = vec![]; - for from_item in from_clause.from_items() { - if let Some(table_ptr) = resolve::table_ptr_from_from_item(db, file, &from_item) { - columns.extend(collect_star_column_names(db, file, &table_ptr)); - } - } - columns -} - fn hover_qualified_star_columns_from_table( db: &dyn Db, file: File, @@ -749,17 +670,15 @@ fn hover_qualified_star_columns_from_table( let path = create_table.path()?; let (schema, table_name) = resolve::resolve_table_info(db, file, &path)?; let schema = schema.to_string(); - let results: Vec = resolve::collect_table_columns(db, file, &create_table) + let results: Vec = collect::table_columns(db, file, &create_table) .into_iter() - .filter_map(|column| { - let column_name = Name::from_node(&column.name()?); - let ty = column.ty()?; - let ty = &ty.syntax().text().to_string(); + .filter_map(|(column_name, ty)| { + let ty = ty?; Some(Hover::snippet(ColumnHover::schema_table_column_type( &schema, &table_name, &column_name.to_string(), - ty, + &ty.to_string(), ))) }) .collect(); @@ -773,57 +692,49 @@ fn hover_qualified_star_columns_from_cte( with_table: ast::WithTable, ) -> Option { let cte_name = Name::from_node(&with_table.name()?); - let column_names = resolve::collect_with_table_column_names(db, file, with_table); - let results: Vec = column_names - .iter() - .map(|column_name| { - Hover::snippet(ColumnHover::table_column( - &cte_name.to_string(), - &column_name.to_string(), - )) + let cte_name = cte_name.to_string(); + let columns = collect::with_table_columns_with_types(db, file, with_table); + let results: Vec = columns + .into_iter() + .map(|(column_name, ty)| { + let column_name = column_name.to_string(); + if let Some(ty) = ty { + return Hover::snippet(ColumnHover::table_column_type( + &cte_name, + &column_name, + &ty.to_string(), + )); + } + + Hover::snippet(ColumnHover::table_column(&cte_name, &column_name)) }) .collect(); merge_hovers(results) } -fn hover_qualified_star_columns_from_view( +fn hover_qualified_star_columns_from_view_like( db: &dyn Db, file: File, - create_view: &ast::CreateView, + create_view: &ast::CreateViewLike, ) -> Option { let path = create_view.path()?; let (schema, view_name) = resolve::resolve_view_info(db, file, &path)?; let schema_str = schema.to_string(); - let column_names = resolve::collect_view_column_names(create_view); - let results: Vec = column_names - .iter() - .map(|column_name| { - Hover::snippet(ColumnHover::schema_table_column( - &schema_str, - &view_name, - &column_name.to_string(), - )) - }) - .collect(); - - merge_hovers(results) -} - -fn hover_qualified_star_columns_from_materialized_view( - db: &dyn Db, - file: File, - create_materialized_view: ast::CreateMaterializedView, -) -> Option { - let path = create_materialized_view.path()?; - let (schema, view_name) = resolve::resolve_view_info(db, file, &path)?; + let columns = collect::view_like_columns_with_types(create_view); + let results: Vec = columns + .into_iter() + .map(|(column_name, ty)| { + if let Some(ty) = ty { + return Hover::snippet(ColumnHover::schema_table_column_type( + &schema_str, + &view_name, + &column_name.to_string(), + &ty.to_string(), + )); + } - let schema_str = schema.to_string(); - let column_names = resolve::collect_materialized_view_column_names(create_materialized_view); - let results: Vec = column_names - .iter() - .map(|column_name| { Hover::snippet(ColumnHover::schema_table_column( &schema_str, &view_name, @@ -871,25 +782,24 @@ fn hover_qualified_star_columns_from_subquery( } let subquery_alias = subquery_alias_name(paren_select); - let results: Vec = - resolve::collect_paren_select_columns_with_types(db, file, paren_select) - .into_iter() - .map(|(column_name, ty)| { - if let Some(alias) = &subquery_alias { - return Hover::snippet(ColumnHover::table_column( - &alias.to_string(), - &column_name.to_string(), - )); - } - if let Some(ty) = ty { - return Hover::snippet(ColumnHover::anon_column_type( - &column_name.to_string(), - &ty.to_string(), - )); - } - Hover::snippet(ColumnHover::anon_column(&column_name.to_string())) - }) - .collect(); + let results: Vec = collect::paren_select_columns_with_types(db, file, paren_select) + .into_iter() + .map(|(column_name, ty)| { + if let Some(alias) = &subquery_alias { + return Hover::snippet(ColumnHover::table_column( + &alias.to_string(), + &column_name.to_string(), + )); + } + if let Some(ty) = ty { + return Hover::snippet(ColumnHover::anon_column_type( + &column_name.to_string(), + &ty.to_string(), + )); + } + Hover::snippet(ColumnHover::anon_column(&column_name.to_string())) + }) + .collect(); merge_hovers(results) } @@ -1212,6 +1122,15 @@ fn format_create_table( } fn format_create_view(db: &dyn Db, file: File, create_view: ast::CreateView) -> Option { + let create_view = ast::CreateViewLike::cast(create_view.syntax().clone())?; + format_create_view_like(db, file, create_view) +} + +fn format_create_view_like( + db: &dyn Db, + file: File, + create_view: ast::CreateViewLike, +) -> Option { let path = create_view.path()?; // TODO: we use this to infer the schema, we should either rename this or // create a different function @@ -1225,34 +1144,14 @@ fn format_create_view(db: &dyn Db, file: File, create_view: ast::CreateView) -> let query = create_view.query()?.syntax().text().to_string(); - Some(Hover::snippet(format!( - "view {}.{}{} as {}", - schema, view_name, column_list, query - ))) -} - -fn format_create_materialized_view( - db: &dyn Db, - file: File, - create_materialized_view: ast::CreateMaterializedView, -) -> Option { - let path = create_materialized_view.path()?; - let (schema, view_name) = resolve::resolve_view_info(db, file, &path)?; - let schema = schema.to_string(); - - let column_list = create_materialized_view - .column_list() - .map(|cl| cl.syntax().text().to_string()) - .unwrap_or_default(); - - let query = create_materialized_view - .query()? - .syntax() - .text() - .to_string(); + let view_kind = if create_view.syntax().kind() == SyntaxKind::CREATE_MATERIALIZED_VIEW { + "materialized view" + } else { + "view" + }; Some(Hover::snippet(format!( - "materialized view {}.{}{} as {}", + "{view_kind} {}.{}{} as {}", schema, view_name, column_list, query ))) } @@ -3296,9 +3195,9 @@ select column1$0 from (values (1, 'foo')); assert_snapshot!(check_hover(" with u as (select 1 id, 2 b) select u.*$0 from u; -"), @r" - hover: column u.id - column u.b +"), @" + hover: column u.id integer + column u.b integer ╭▸ 3 │ select u.* from u; ╰╴ ─ hover @@ -3310,9 +3209,9 @@ select u.*$0 from u; assert_snapshot!(check_hover(" with t as (values (1, 2), (3, 4)) select t.*$0 from t; -"), @r" - hover: column t.column1 - column t.column2 +"), @" + hover: column t.column1 integer + column t.column2 integer ╭▸ 3 │ select t.* from t; ╰╴ ─ hover @@ -3451,13 +3350,13 @@ select *$0 from t; assert_snapshot!(check_hover(" with u as (select 1 id, 2 b) select *$0 from (select *, *, * from u); -"), @r" - hover: column u.id - column u.b - column u.id - column u.b - column u.id - column u.b +"), @" + hover: column u.id integer + column u.b integer + column u.id integer + column u.b integer + column u.id integer + column u.b integer ╭▸ 3 │ select * from (select *, *, * from u); ╰╴ ─ hover @@ -3496,13 +3395,13 @@ select *$0 from (table t); assert_snapshot!(check_hover(" select *$0 from information_schema.sql_features; "), @" - hover: column information_schema.sql_features.feature_id information_schema.character_data - column information_schema.sql_features.feature_name information_schema.character_data - column information_schema.sql_features.sub_feature_id information_schema.character_data - column information_schema.sql_features.sub_feature_name information_schema.character_data - column information_schema.sql_features.is_supported information_schema.yes_or_no - column information_schema.sql_features.is_verified_by information_schema.character_data - column information_schema.sql_features.comments information_schema.character_data + hover: column information_schema.sql_features.feature_id character_data + column information_schema.sql_features.feature_name character_data + column information_schema.sql_features.sub_feature_id character_data + column information_schema.sql_features.sub_feature_name character_data + column information_schema.sql_features.is_supported yes_or_no + column information_schema.sql_features.is_verified_by character_data + column information_schema.sql_features.comments character_data ╭▸ 2 │ select * from information_schema.sql_features; ╰╴ ─ hover @@ -3538,15 +3437,59 @@ select *$0 from (select 1) as sub; assert_snapshot!(check_hover(" create view v as select 1 id, 2 b; select v.*$0 from v; -"), @r" - hover: column public.v.id - column public.v.b +"), @" + hover: column public.v.id integer + column public.v.b integer ╭▸ 3 │ select v.* from v; ╰╴ ─ hover "); } + #[test] + fn hover_on_materialized_view_qualified_star() { + assert_snapshot!(check_hover(" + create materialized view v as select 1 id, 2 b; + select v.*$0 from v; + "), @" + hover: column public.v.id integer + column public.v.b integer + ╭▸ + 3 │ select v.* from v; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_view_qualified_star_with_column_list() { + assert_snapshot!(check_hover(" +create view v (x, y) as select 1, 2, 3; +select v.*$0 from v; +"), @" + hover: column public.v.x integer + column public.v.y integer + column public.v.?column? integer + ╭▸ + 3 │ select v.* from v; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_materialized_view_qualified_star_with_column_list() { + assert_snapshot!(check_hover(" +create materialized view mv (x, y) as select 1, 2, 3; +select mv.*$0 from mv; +"), @" + hover: column public.mv.x integer + column public.mv.y integer + column public.mv.?column? integer + ╭▸ + 3 │ select mv.* from mv; + ╰╴ ─ hover + "); + } + #[test] fn hover_on_drop_procedure() { assert_snapshot!(check_hover(" diff --git a/crates/squawk_ide/src/inlay_hints.rs b/crates/squawk_ide/src/inlay_hints.rs index 75483c52..77ccf35b 100644 --- a/crates/squawk_ide/src/inlay_hints.rs +++ b/crates/squawk_ide/src/inlay_hints.rs @@ -1,3 +1,4 @@ +use crate::collect; use crate::db::{File, parse}; use crate::goto_definition; use crate::resolve; @@ -131,7 +132,7 @@ fn inlay_hint_insert( .collect() } else { // `insert into t values (1, 2, 3)` - resolve::collect_columns_from_create_table(db, def_file, &create_table?) + collect::columns_from_create_table(db, def_file, &create_table?) .into_iter() .map(|(col_name, ptr)| { let target = ptr.map(|p| p.text_range()); @@ -438,7 +439,7 @@ insert into t values (1, 2, 3); create table t (a int, b int); create table u (c int) inherits (t); insert into u select 1, 2, 3; -"), @r" +"), @" inlay hints: ╭▸ 4 │ insert into u select a: 1, b: 2, c: 3; diff --git a/crates/squawk_ide/src/lib.rs b/crates/squawk_ide/src/lib.rs index b2296366..193866c6 100644 --- a/crates/squawk_ide/src/lib.rs +++ b/crates/squawk_ide/src/lib.rs @@ -1,7 +1,9 @@ +mod ast_nav; mod binder; pub mod builtins; mod classify; pub mod code_actions; +mod collect; pub mod column_name; mod comments; pub mod completion; diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index 4c12767c..1d84cff8 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -1,3 +1,5 @@ +use crate::ast_nav::{iter_from_clause, iter_join_expr}; +use crate::collect; use rowan::TextSize; use smallvec::{SmallVec, smallvec}; use squawk_syntax::{ @@ -7,7 +9,6 @@ use squawk_syntax::{ use crate::column_name::ColumnName; use crate::db::File; -use crate::infer::{Type, infer_type_from_expr, infer_type_from_ty}; use crate::location::LocationKind; pub(crate) use crate::symbols::Schema; use crate::symbols::{Name, SymbolKind}; @@ -616,7 +617,7 @@ fn resolved_location_kind( LocationKind::from_node(&ptr.to_node(root)).unwrap_or(fallback) } -fn resolve_table_name_ptr( +pub(crate) fn resolve_table_name_ptr( db: &dyn Db, file: File, table_name: &Name, @@ -752,7 +753,7 @@ fn resolve_float_precision(name_ref: &ast::NameRef, type_name: Name) -> Name { type_name } -fn resolve_view_name_ptr( +pub(crate) fn resolve_view_name_ptr( db: &dyn Db, file: File, view_name: &Name, @@ -1196,7 +1197,7 @@ fn match_table_in_returning_clause( None } -fn extract_table_schema_from_path(path: &ast::Path) -> Option<(Name, Option)> { +pub(crate) fn extract_table_schema_from_path(path: &ast::Path) -> Option<(Name, Option)> { Some((extract_table_name(path)?, extract_schema_name(path))) } @@ -1428,7 +1429,7 @@ fn resolve_column_for_table( let resolved = resolve_table_name(db, file, table_name, schema, position)?; match resolved { ResolvedTableName::View(create_view) => { - if let Some(ptr) = find_column_in_create_view(&create_view, column_name) { + if let Some(ptr) = find_column_in_create_view_like(&create_view, column_name) { return Some(ptr); } return resolve_function(db, file, column_name, schema, None, position); @@ -1453,12 +1454,12 @@ fn resolve_column_for_table( } } -enum ResolvedTableName { - View(ast::CreateView), +pub(crate) enum ResolvedTableName { Table(ast::CreateTableLike), TableAs(ast::CreateTableAs), + View(ast::CreateViewLike), } -fn resolve_table_name( +pub(crate) fn resolve_table_name( db: &dyn Db, file: File, table_name: &Name, @@ -1486,7 +1487,10 @@ fn resolve_table_name( if let Some(view_name_ptr) = resolve_view_name_ptr(db, file, table_name, schema, position) { let view_name_node = view_name_ptr.to_node(root); - if let Some(create_view) = view_name_node.ancestors().find_map(ast::CreateView::cast) { + if let Some(create_view) = view_name_node + .ancestors() + .find_map(ast::CreateViewLike::cast) + { return Some(View(create_view)); } } @@ -1730,8 +1734,11 @@ fn resolve_column_from_table_or_view_impl( if let Some(view_name_ptr) = resolve_view_name_ptr(db, file, table_name, schema, position) { let view_name_node = view_name_ptr.to_node(root); - if let Some(create_view) = view_name_node.ancestors().find_map(ast::CreateView::cast) { - if let Some(ptr) = find_column_in_create_view(&create_view, column_name) { + if let Some(create_view) = view_name_node + .ancestors() + .find_map(ast::CreateViewLike::cast) + { + if let Some(ptr) = find_column_in_create_view_like(&create_view, column_name) { return Some(ptr); } @@ -1768,29 +1775,6 @@ fn resolve_from_item_for_cte_star( resolve_from_item_column_ptr(db, file, from_item, name_ref) } -fn resolve_from_join_expr(join_expr: &ast::JoinExpr, try_resolve: &F) -> Option -where - F: Fn(&ast::FromItem) -> Option, -{ - if let Some(nested_join) = join_expr.join_expr() - && let Some(result) = resolve_from_join_expr(&nested_join, try_resolve) - { - return Some(result); - } - if let Some(from_item) = join_expr.from_item() - && let Some(result) = try_resolve(&from_item) - { - return Some(result); - } - if let Some(join) = join_expr.join() - && let Some(from_item) = join.from_item() - && let Some(result) = try_resolve(&from_item) - { - return Some(result); - } - None -} - fn resolve_select_column_ptr( db: &dyn Db, file: File, @@ -1802,7 +1786,7 @@ fn resolve_select_column_ptr( .find_map(ast::Select::cast)?; let from_clause = select.from_clause()?; - for from_item in from_clause.from_items() { + for from_item in iter_from_clause(&from_clause) { if let Some(column_ptr) = resolve_from_item_column_ptr(db, file, &from_item, column_name_ref) { @@ -1810,16 +1794,6 @@ fn resolve_select_column_ptr( } } - for join_expr in from_clause.join_exprs() { - if let Some(column_ptr) = - resolve_from_join_expr(&join_expr, &|from_item: &ast::FromItem| { - resolve_from_item_column_ptr(db, file, from_item, column_name_ref) - }) - { - return Some(column_ptr); - } - } - None } @@ -1867,35 +1841,13 @@ fn resolve_join_using_columns( let mut results: SmallVec<[SyntaxNodePtr; 1]> = SmallVec::new(); - collect_from_join_expr(&join_expr, &mut results, &|from_item: &ast::FromItem| { - resolve_from_item_column_ptr(db, file, from_item, name_ref) - }); - - (!results.is_empty()).then_some(results) -} - -fn collect_from_join_expr( - join_expr: &ast::JoinExpr, - results: &mut SmallVec<[SyntaxNodePtr; 1]>, - try_resolve: &F, -) where - F: Fn(&ast::FromItem) -> Option, -{ - if let Some(nested_join) = join_expr.join_expr() { - collect_from_join_expr(&nested_join, results, try_resolve); - } - if let Some(from_item) = join_expr.from_item() - && let Some(result) = try_resolve(&from_item) - { - results.push(result); - } - if let Some(join) = join_expr.join() - && let Some(from_item) = join.from_item() - { - if let Some(result) = try_resolve(&from_item) { + for from_item in iter_join_expr(&join_expr) { + if let Some(result) = resolve_from_item_column_ptr(db, file, &from_item, name_ref) { results.push(result); } } + + (!results.is_empty()).then_some(results) } fn resolve_update_column_ptr( @@ -1912,23 +1864,13 @@ fn resolve_update_column_ptr( // `update t set a = b from u` if let Some(from_clause) = update.from_clause() { - for from_item in from_clause.from_items() { + for from_item in iter_from_clause(&from_clause) { if let Some(result) = resolve_from_item_column_ptr(db, file, &from_item, column_name_ref) { return Some(result); } } - - for join_expr in from_clause.join_exprs() { - if let Some(result) = - resolve_from_join_expr(&join_expr, &|from_item: &ast::FromItem| { - resolve_from_item_column_ptr(db, file, from_item, column_name_ref) - }) - { - return Some(result); - } - } } // `update t set a = b` @@ -1957,7 +1899,7 @@ fn resolve_fn_call_column( let select = name_ref.syntax().ancestors().find_map(ast::Select::cast)?; let from_clause = select.from_clause()?; - for from_item in from_clause.from_items() { + for from_item in iter_from_clause(&from_clause) { if let Some(result) = resolve_from_item_for_fn_call_column(db, file, &from_item, &column_name, name_ref) { @@ -1965,14 +1907,6 @@ fn resolve_fn_call_column( } } - for join_expr in from_clause.join_exprs() { - if let Some(result) = resolve_from_join_expr(&join_expr, &|from_item: &ast::FromItem| { - resolve_from_item_for_fn_call_column(db, file, from_item, &column_name, name_ref) - }) { - return Some(result); - } - } - None } @@ -1998,7 +1932,9 @@ fn resolve_from_item_for_fn_call_column( find_column_in_create_table(db, file, &create_table, column_name) } -fn table_and_schema_from_from_item(from_item: &ast::FromItem) -> Option<(Name, Option)> { +pub(crate) fn table_and_schema_from_from_item( + from_item: &ast::FromItem, +) -> Option<(Name, Option)> { if let Some(name_ref_node) = from_item.name_ref() { return Some((Name::from_node(&name_ref_node), None)); } @@ -2042,49 +1978,11 @@ fn is_from_item_match(from_item: &ast::FromItem, qualifier: &Name) -> bool { false } -fn find_from_item_in_join_expr( - join_expr: &ast::JoinExpr, - qualifier: &Name, -) -> Option { - if let Some(nested_join_expr) = join_expr.join_expr() - && let Some(found) = find_from_item_in_join_expr(&nested_join_expr, qualifier) - { - return Some(found); - } - - if let Some(from_item) = join_expr.from_item() - && is_from_item_match(&from_item, qualifier) - { - return Some(from_item); - } - - if let Some(join) = join_expr.join() - && let Some(from_item) = join.from_item() - && is_from_item_match(&from_item, qualifier) - { - return Some(from_item); - } - - None -} - pub(crate) fn find_from_item_in_from_clause( from_clause: &ast::FromClause, qualifier: &Name, ) -> Option { - for from_item in from_clause.from_items() { - if is_from_item_match(&from_item, qualifier) { - return Some(from_item); - } - } - - for join_expr in from_clause.join_exprs() { - if let Some(found) = find_from_item_in_join_expr(&join_expr, qualifier) { - return Some(found); - } - } - - None + iter_from_clause(from_clause).find(|from_item| is_from_item_match(from_item, qualifier)) } fn find_from_item_for_select_qualified_name_ref( @@ -2138,100 +2036,51 @@ pub(crate) fn extract_column_name(col: &ast::Column) -> Option { Some(name) } -pub(crate) fn collect_columns_from_create_table( +pub(crate) fn find_column_in_create_table( db: &dyn Db, file: File, - create_table: &ast::CreateTableLike, -) -> Vec<(Name, Option)> { - let mut columns = vec![]; - collect_columns_from_create_table_impl(db, file, create_table, &mut columns, 0); - columns + create_table: &impl ast::HasCreateTable, + column_name: &Name, +) -> Option { + find_column_in_create_table_impl(db, file, create_table, column_name, 0) } -fn collect_columns_from_create_table_impl( +fn find_column_in_create_table_impl( db: &dyn Db, file: File, - create_table: &ast::CreateTableLike, - columns: &mut Vec<(Name, Option)>, + create_table: &impl ast::HasCreateTable, + column_name: &Name, depth: usize, -) { +) -> Option { if depth > 40 { log::info!("max depth reached, probably in a cycle"); - return; + return None; } if let Some(inherits) = create_table.inherits() { for path in inherits.paths() { - if let Some((table_name, schema)) = extract_table_schema_from_path(&path) { - let position = path.syntax().text_range().start(); - if let Some(ResolvedTableName::Table(parent_table)) = - resolve_table_name(db, file, &table_name, &schema, position) - { - collect_columns_from_create_table_impl( + let (table_name, schema) = extract_table_schema_from_path(&path)?; + let position = path.syntax().text_range().start(); + + if let Some(resolved) = resolve_table_name(db, file, &table_name, &schema, position) { + if let Some(ptr) = match resolved { + ResolvedTableName::Table(parent_table) => find_column_in_create_table_impl( db, file, &parent_table, - columns, + column_name, depth + 1, - ); - } - } - } - } - - if let Some(arg_list) = create_table.table_arg_list() { - for arg in arg_list.args() { - match &arg { - ast::TableArg::Column(column) => { - if let Some(name) = column.name() { - let col_name = Name::from_node(&name); - columns.push((col_name, Some(SyntaxNodePtr::new(name.syntax())))); - } - } - ast::TableArg::LikeClause(like_clause) => { - if let Some(path) = like_clause.path() - && let Some((table_name, schema)) = extract_table_schema_from_path(&path) - { - let position = path.syntax().text_range().start(); - if let Some(ResolvedTableName::Table(source_table)) = - resolve_table_name(db, file, &table_name, &schema, position) - { - collect_columns_from_create_table_impl( - db, - file, - &source_table, - columns, - depth + 1, - ); - } + ), + ResolvedTableName::TableAs(create_table_as) => { + find_column_in_create_table_as(&create_table_as, column_name) } + ResolvedTableName::View(_) => None, + } { + return Some(ptr); } - ast::TableArg::TableConstraint(_) => (), } } } -} - -pub(crate) fn find_column_in_create_table( - db: &dyn Db, - file: File, - create_table: &impl ast::HasCreateTable, - column_name: &Name, -) -> Option { - find_column_in_create_table_impl(db, file, create_table, column_name, 0) -} - -fn find_column_in_create_table_impl( - db: &dyn Db, - file: File, - 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() { match &arg { @@ -2270,37 +2119,12 @@ fn find_column_in_create_table_impl( } } - if let Some(inherits) = create_table.inherits() { - for path in inherits.paths() { - let (table_name, schema) = extract_table_schema_from_path(&path)?; - let position = path.syntax().text_range().start(); - - if let Some(resolved) = resolve_table_name(db, file, &table_name, &schema, position) { - if let Some(ptr) = match resolved { - ResolvedTableName::Table(parent_table) => find_column_in_create_table_impl( - db, - file, - &parent_table, - column_name, - depth + 1, - ), - ResolvedTableName::TableAs(create_table_as) => { - find_column_in_create_table_as(&create_table_as, column_name) - } - ResolvedTableName::View(_) => None, - } { - return Some(ptr); - } - } - } - } - None } // TODO: this is similar to the CTE funcs, maybe we can simplify -fn find_column_in_create_view( - create_view: &ast::CreateView, +fn find_column_in_create_view_like( + create_view: &ast::CreateViewLike, column_name: &Name, ) -> Option { let column_list_len = if let Some(column_list) = create_view.column_list() { @@ -2357,7 +2181,7 @@ fn find_column_in_create_table_as( None } -fn resolve_cte_table(name_ref: &ast::NameRef, cte_name: &Name) -> Option { +pub(crate) fn resolve_cte_table(name_ref: &ast::NameRef, cte_name: &Name) -> Option { let with_clause = find_parent_with_clause(name_ref.syntax())?; let is_recursive = with_clause.recursive_token().is_some(); for with_table in with_clause.with_tables() { @@ -2438,7 +2262,10 @@ fn count_columns_for_table_name( if let Some(view_name_ptr) = resolve_view_name_ptr(db, file, table_name, schema, position) { let view_name_node = view_name_ptr.to_node(root); - if let Some(create_view) = view_name_node.ancestors().find_map(ast::CreateView::cast) { + if let Some(create_view) = view_name_node + .ancestors() + .find_map(ast::CreateViewLike::cast) + { if let Some(column_list) = create_view.column_list() { return Some(column_list.columns().count()); } @@ -2795,23 +2622,13 @@ fn resolve_column_from_select_targets( if matches!(col_name, ColumnName::Star) && let Some(from_clause) = &from_clause { - for from_item in from_clause.from_items() { + for from_item in iter_from_clause(from_clause) { if let Some(result) = resolve_from_item_column_ptr(db, file, &from_item, name_ref) { return Some(result); } } - - for join_expr in from_clause.join_exprs() { - if let Some(result) = - resolve_from_join_expr(&join_expr, &|from_item: &ast::FromItem| { - resolve_from_item_column_ptr(db, file, from_item, name_ref) - }) - { - return Some(result); - } - } } } @@ -2829,7 +2646,7 @@ fn resolve_column_from_select_targets( None } -fn qualified_star_table_name(field_expr: &ast::FieldExpr) -> Option { +pub(crate) fn qualified_star_table_name(field_expr: &ast::FieldExpr) -> Option { field_expr.star_token()?; match field_expr.base()? { @@ -3054,12 +2871,8 @@ pub(crate) fn table_ptrs_from_clause( ) -> Vec { let mut results = vec![]; - for from_item in from_clause.from_items() { - collect_tables_from_item(db, file, &from_item, &mut results); - } - - for join_expr in from_clause.join_exprs() { - collect_table_ptrs_from_join_expr(db, file, &join_expr, &mut results); + for from_item in iter_from_clause(from_clause) { + collect::tables_from_item(db, file, &from_item, &mut results); } results @@ -3113,74 +2926,10 @@ fn table_ptr_from_paren_expr( None } -fn collect_table_ptrs_from_join_expr( - db: &dyn Db, - file: File, - join_expr: &ast::JoinExpr, - results: &mut Vec, -) { - if let Some(nested) = join_expr.join_expr() { - collect_table_ptrs_from_join_expr(db, file, &nested, results); - } - - if let Some(from_item) = join_expr.from_item() { - collect_tables_from_item(db, file, &from_item, results); - } - - if let Some(join) = join_expr.join() - && let Some(from_item) = join.from_item() - { - collect_tables_from_item(db, file, &from_item, results); - } -} - -fn collect_tables_from_item( - db: &dyn Db, - file: File, - from_item: &ast::FromItem, - results: &mut Vec, -) { - if let Some(alias) = from_item.alias() - && alias.column_list().is_some() - { - results.push(SyntaxNodePtr::new(alias.syntax())); - return; - } - - if let Some(paren_select) = from_item.paren_select() { - results.push(SyntaxNodePtr::new(paren_select.syntax())); - return; - } - - let Some((table_name, schema)) = table_and_schema_from_from_item(from_item) else { - return; - }; - - let position = from_item.syntax().text_range().start(); - if let Some(table_name_ptr) = resolve_table_name_ptr(db, file, &table_name, &schema, position) { - results.push(table_name_ptr); - return; - } - - if let Some(view_name_ptr) = resolve_view_name_ptr(db, file, &table_name, &schema, position) { - results.push(view_name_ptr); - return; - } - - if schema.is_none() - && let Some(name_ref) = from_item.name_ref() - && let Some(cte_ptr) = resolve_cte_table(&name_ref, &table_name) - { - results.push(cte_ptr); - return; - } -} - pub(crate) enum TableSource { Alias(ast::Alias), - CreateMaterializedView(ast::CreateMaterializedView), CreateTable(ast::CreateTableLike), - CreateView(ast::CreateView), + CreateView(ast::CreateViewLike), ParenSelect(ast::ParenSelect), WithTable(ast::WithTable), } @@ -3201,17 +2950,10 @@ pub(crate) fn find_table_source(node: &SyntaxNode) -> Option { return Some(TableSource::WithTable(with_table)); } - if let Some(create_view) = ast::CreateView::cast(ancestor.clone()) { + if let Some(create_view) = ast::CreateViewLike::cast(ancestor.clone()) { return Some(TableSource::CreateView(create_view)); } - if let Some(create_materialized_view) = ast::CreateMaterializedView::cast(ancestor.clone()) - { - return Some(TableSource::CreateMaterializedView( - create_materialized_view, - )); - } - if let Some(create_table) = ast::CreateTableLike::cast(ancestor.clone()) { return Some(TableSource::CreateTable(create_table)); } @@ -3264,54 +3006,13 @@ fn count_columns_for_from_clause( let mut total: usize = 0; let mut found = false; - for from_item in from_clause.from_items() { + for from_item in iter_from_clause(from_clause) { if let Some(count) = count_columns_for_from_item(db, file, name_ref, &from_item) { total = total.saturating_add(count); found = true; } } - for join_expr in from_clause.join_exprs() { - if let Some(count) = count_columns_for_join_expr(db, file, name_ref, &join_expr) { - total = total.saturating_add(count); - found = true; - } - } - - found.then_some(total) -} - -fn count_columns_for_join_expr( - db: &dyn Db, - file: File, - name_ref: &ast::NameRef, - join_expr: &ast::JoinExpr, -) -> Option { - let mut total: usize = 0; - let mut found = false; - - if let Some(nested) = join_expr.join_expr() - && let Some(count) = count_columns_for_join_expr(db, file, name_ref, &nested) - { - total = total.saturating_add(count); - found = true; - } - - if let Some(from_item) = join_expr.from_item() - && let Some(count) = count_columns_for_from_item(db, file, name_ref, &from_item) - { - total = total.saturating_add(count); - found = true; - } - - if let Some(join) = join_expr.join() - && let Some(from_item) = join.from_item() - && let Some(count) = count_columns_for_from_item(db, file, name_ref, &from_item) - { - total = total.saturating_add(count); - found = true; - } - found.then_some(total) } @@ -3388,7 +3089,7 @@ fn resolve_from_clause_for_cte_star( column_name: &Name, from_clause: &ast::FromClause, ) -> Option { - for from_item in from_clause.from_items() { + for from_item in iter_from_clause(from_clause) { if let Some(result) = resolve_from_item_for_cte_star(db, file, &from_item, name_ref, cte_name, column_name) { @@ -3396,14 +3097,6 @@ fn resolve_from_clause_for_cte_star( } } - for join_expr in from_clause.join_exprs() { - if let Some(result) = resolve_from_join_expr(&join_expr, &|from_item: &ast::FromItem| { - resolve_from_item_for_cte_star(db, file, from_item, name_ref, cte_name, column_name) - }) { - return Some(result); - } - } - None } @@ -3588,168 +3281,6 @@ pub(crate) fn resolve_sequence_info( resolve_symbol_info(db, file, path, SymbolKind::Sequence) } -pub(crate) fn collect_table_columns( - db: &dyn Db, - file: File, - create_table: &impl ast::HasCreateTable, -) -> Vec { - collect_table_columns_impl(db, file, create_table, 0) -} - -// TODO: combine with find_column_in_create_table_impl -fn collect_table_columns_impl( - db: &dyn Db, - file: File, - 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, schema)) = extract_table_schema_from_path(&path) { - let position = path.syntax().text_range().start(); - if let Some(ResolvedTableName::Table(parent_table)) = - resolve_table_name(db, file, &table_name, &schema, position) - { - let inherited_columns = - collect_table_columns_impl(db, file, &parent_table, depth + 1); - columns.extend(inherited_columns); - } - } - } - } - - if let Some(arg_list) = create_table.table_arg_list() { - for arg in arg_list.args() { - 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, schema)) = extract_table_schema_from_path(&path) - { - let position = path.syntax().text_range().start(); - if let Some(ResolvedTableName::Table(source_table)) = - resolve_table_name(db, file, &table_name, &schema, position) - { - let like_columns = - collect_table_columns_impl(db, file, &source_table, depth + 1); - columns.extend(like_columns); - } - } - } - ast::TableArg::TableConstraint(_) => (), - } - } - } - - columns -} - -pub(crate) fn collect_view_column_names(create_view: &ast::CreateView) -> Vec { - if let Some(column_list) = create_view.column_list() { - let columns = collect_column_names_from_column_list(&column_list); - if !columns.is_empty() { - return columns; - } - } - - let Some(select) = select_from_view_query(create_view) else { - return vec![]; - }; - let Some(select_clause) = select.select_clause() else { - return vec![]; - }; - let Some(target_list) = select_clause.target_list() else { - return vec![]; - }; - - collect_target_list_column_names(&target_list) -} - -pub(crate) fn collect_view_columns_with_types( - create_view: &ast::CreateView, -) -> Vec<(Name, Option)> { - let Some(select) = select_from_view_query(create_view) else { - return vec![]; - }; - let Some(select_clause) = select.select_clause() else { - return vec![]; - }; - let Some(target_list) = select_clause.target_list() else { - return vec![]; - }; - - collect_target_list_columns_with_types(&target_list) -} - -pub(crate) fn collect_materialized_view_column_names( - create_materialized_view: ast::CreateMaterializedView, -) -> Vec { - if let Some(column_list) = create_materialized_view.column_list() { - let columns = collect_column_names_from_column_list(&column_list); - if !columns.is_empty() { - return columns; - } - } - - let Some(select) = select_from_materialized_view_query(create_materialized_view) else { - return vec![]; - }; - let Some(select_clause) = select.select_clause() else { - return vec![]; - }; - let Some(target_list) = select_clause.target_list() else { - return vec![]; - }; - - collect_target_list_column_names(&target_list) -} - -pub(crate) fn collect_materialized_view_columns_with_types( - create_materialized_view: ast::CreateMaterializedView, -) -> Vec<(Name, Option)> { - let Some(select) = select_from_materialized_view_query(create_materialized_view) else { - return vec![]; - }; - let Some(select_clause) = select.select_clause() else { - return vec![]; - }; - let Some(target_list) = select_clause.target_list() else { - return vec![]; - }; - - collect_target_list_columns_with_types(&target_list) -} - -fn select_from_materialized_view_query( - create_materialized_view: ast::CreateMaterializedView, -) -> Option { - let query = create_materialized_view.query()?; - match query { - ast::SelectVariant::Select(select) => Some(select), - _ => None, - } -} - -pub(crate) fn collect_with_table_column_names( - db: &dyn Db, - file: File, - with_table: ast::WithTable, -) -> Vec { - collect_with_table_columns_with_types(db, file, with_table) - .into_iter() - .map(|(name, _)| name) - .collect() -} - fn resolve_symbol_info( db: &dyn Db, file: File, @@ -3763,417 +3294,6 @@ fn resolve_symbol_info( binder.lookup_info(name_str, &schema, kind, position) } -fn collect_column_names_from_column_list(column_list: &ast::ColumnList) -> Vec { - let mut columns = vec![]; - for column in column_list.columns() { - if let Some(name) = column.name() { - columns.push(Name::from_node(&name)); - } - } - columns -} - -fn collect_target_list_column_names(target_list: &ast::TargetList) -> Vec { - let mut columns = vec![]; - for target in target_list.targets() { - if let Some((col_name, _node)) = ColumnName::from_target(target) - && let Some(col_name_str) = col_name.to_string() - { - columns.push(Name::from_string(col_name_str)); - } - } - columns -} - -pub(crate) fn collect_with_table_columns_with_types( - db: &dyn Db, - file: File, - with_table: ast::WithTable, -) -> Vec<(Name, Option)> { - let alias_columns: Vec = with_table - .column_list() - .into_iter() - .flat_map(|column_list| column_list.columns()) - .filter_map(|column| column.name().map(|name| Name::from_node(&name))) - .collect(); - - let base_columns = collect_with_table_query_columns_with_types(db, file, with_table); - - if alias_columns.is_empty() { - return base_columns; - } - - let mut results = vec![]; - - for (idx, alias_name) in alias_columns.iter().enumerate() { - results.push(( - alias_name.clone(), - base_columns.get(idx).and_then(|(_, ty)| ty.clone()), - )); - } - - results.extend(base_columns.into_iter().skip(alias_columns.len())); - - results -} - -fn collect_with_table_query_columns_with_types( - db: &dyn Db, - file: File, - with_table: ast::WithTable, -) -> Vec<(Name, Option)> { - let Some(query) = with_table.query() else { - return vec![]; - }; - - if let ast::WithQuery::Values(values) = query { - let mut results = vec![]; - if let Some(row_list) = values.row_list() - && let Some(first_row) = row_list.rows().next() - { - for (idx, expr) in first_row.exprs().enumerate() { - let name = Name::from_string(format!("column{}", idx + 1)); - let ty = infer_type_from_expr(&expr); - results.push((name, ty)); - } - } - return results; - } - - if let Some(columns) = columns_from_returning_clause_with_types(&query) { - return columns; - } - - let Some(cte_select) = select_from_with_query(query) else { - return vec![]; - }; - let Some(select_clause) = cte_select.select_clause() else { - return vec![]; - }; - let Some(target_list) = select_clause.target_list() else { - return vec![]; - }; - - let from_clause = cte_select.from_clause(); - let mut columns = vec![]; - - for target in target_list.targets() { - if let Some((col_name, _node)) = ColumnName::from_target(target.clone()) { - if let Some(col_name_str) = col_name.to_string() { - let ty = target.expr().and_then(|e| infer_type_from_expr(&e)); - columns.push((Name::from_string(col_name_str), ty)); - continue; - } - - if target.star_token().is_some() - && let Some(from_clause) = &from_clause - { - columns.extend(collect_columns_for_star_from_clause(db, file, from_clause)); - continue; - } - } - - if let Some(expr) = target.expr() - && let ast::Expr::FieldExpr(field_expr) = expr - && let Some(table_name) = qualified_star_table_name(&field_expr) - && let Some(from_clause) = &from_clause - && let Some(from_item) = find_from_item_in_from_clause(from_clause, &table_name) - { - columns.extend(collect_columns_for_star_from_from_item( - db, file, &from_item, - )); - } - } - - columns -} - -fn columns_from_returning_clause_with_types( - query: &ast::WithQuery, -) -> Option)>> { - let returning_clause = match query { - ast::WithQuery::Delete(delete) => delete.returning_clause(), - ast::WithQuery::Insert(insert) => insert.returning_clause(), - ast::WithQuery::Merge(merge) => merge.returning_clause(), - ast::WithQuery::Update(update) => update.returning_clause(), - ast::WithQuery::Select(_) - | ast::WithQuery::CompoundSelect(_) - | ast::WithQuery::Table(_) - | ast::WithQuery::Values(_) - | ast::WithQuery::ParenSelect(_) => None, - }; - - if let Some(returning_clause) = returning_clause { - if let Some(target_list) = returning_clause.target_list() { - return Some(collect_target_list_columns_with_types(&target_list)); - } - return Some(vec![]); - } - - None -} - -fn collect_columns_for_star_from_clause( - db: &dyn Db, - file: File, - from_clause: &ast::FromClause, -) -> Vec<(Name, Option)> { - let mut columns = vec![]; - - for from_item in from_clause.from_items() { - columns.extend(collect_columns_for_star_from_from_item( - db, file, &from_item, - )); - } - - for join_expr in from_clause.join_exprs() { - if let Some(from_item) = join_expr.from_item() { - columns.extend(collect_columns_for_star_from_from_item( - db, file, &from_item, - )); - } - - if let Some(join) = join_expr.join() - && let Some(from_item) = join.from_item() - { - columns.extend(collect_columns_for_star_from_from_item( - db, file, &from_item, - )); - } - } - - columns -} - -fn collect_columns_for_star_from_from_item( - db: &dyn Db, - file: File, - from_item: &ast::FromItem, -) -> Vec<(Name, Option)> { - if let Some(alias) = from_item.alias() - && alias.column_list().is_some() - { - return collect_columns_for_star_from_alias(db, file, from_item, &alias); - } - - let Some(table_ptr) = table_ptr_from_from_item(db, file, from_item) else { - return vec![]; - }; - - collect_columns_for_star_from_table_ptr(db, file, &table_ptr) -} - -fn collect_columns_for_star_from_alias( - db: &dyn Db, - file: File, - from_item: &ast::FromItem, - alias: &ast::Alias, -) -> Vec<(Name, Option)> { - let alias_columns: Vec = alias - .column_list() - .into_iter() - .flat_map(|column_list| column_list.columns()) - .filter_map(|column| column.name().map(|name| Name::from_node(&name))) - .collect(); - - let Some(table_ptr) = table_ptr_from_from_item(db, file, from_item) else { - return vec![]; - }; - - let base_columns = collect_columns_for_star_from_table_ptr(db, file, &table_ptr); - let mut results = vec![]; - - for (idx, alias_name) in alias_columns.iter().enumerate() { - results.push(( - alias_name.clone(), - base_columns.get(idx).and_then(|(_, ty)| ty.clone()), - )); - } - - results.extend(base_columns.into_iter().skip(alias_columns.len())); - - results -} - -fn collect_columns_for_star_from_table_ptr( - db: &dyn Db, - file: File, - table_ptr: &SyntaxNodePtr, -) -> Vec<(Name, Option)> { - let tree = parse(db, file).tree(); - let root = tree.syntax(); - let table_node = table_ptr.to_node(root); - - if let Some(paren_select) = ast::ParenSelect::cast(table_node.clone()) { - return collect_paren_select_columns_with_types(db, file, &paren_select); - } - - match find_table_source(&table_node) { - Some(TableSource::Alias(alias)) => { - let Some(from_item) = alias.syntax().ancestors().find_map(ast::FromItem::cast) else { - return vec![]; - }; - collect_columns_for_star_from_alias(db, file, &from_item, &alias) - } - Some(TableSource::WithTable(with_table)) => { - collect_with_table_columns_with_types(db, file, with_table) - } - Some(TableSource::CreateTable(create_table)) => { - collect_table_columns(db, file, &create_table) - .into_iter() - .filter_map(|column| { - let name = Name::from_node(&column.name()?); - let ty = column.ty().and_then(|ty| infer_type_from_ty(&ty)); - Some((name, ty)) - }) - .collect() - } - Some(TableSource::CreateView(create_view)) => collect_view_columns_with_types(&create_view), - Some(TableSource::CreateMaterializedView(create_materialized_view)) => { - collect_materialized_view_columns_with_types(create_materialized_view) - } - Some(TableSource::ParenSelect(paren_select)) => { - collect_paren_select_columns_with_types(db, file, &paren_select) - } - None => vec![], - } -} - -fn collect_target_list_columns_with_types( - target_list: &ast::TargetList, -) -> Vec<(Name, Option)> { - let mut columns = vec![]; - for target in target_list.targets() { - if let Some((col_name, _node)) = ColumnName::from_target(target.clone()) - && let Some(col_name_str) = col_name.to_string() - { - let ty = target.expr().and_then(|e| infer_type_from_expr(&e)); - columns.push((Name::from_string(col_name_str), ty)); - } - } - columns -} - -pub(crate) fn collect_paren_select_columns_with_types( - db: &dyn Db, - file: File, - paren_select: &ast::ParenSelect, -) -> Vec<(Name, Option)> { - let Some(select_variant) = paren_select.select() else { - return vec![]; - }; - collect_select_variant_columns_with_types(db, file, &select_variant) -} - -fn collect_select_variant_columns_with_types( - db: &dyn Db, - file: File, - select_variant: &ast::SelectVariant, -) -> Vec<(Name, Option)> { - let binder = bind(db, file); - let tree = parse(db, file).tree(); - let root = tree.syntax(); - match select_variant { - ast::SelectVariant::Values(values) => { - let mut results = vec![]; - if let Some(row_list) = values.row_list() - && let Some(first_row) = row_list.rows().next() - { - for (idx, expr) in first_row.exprs().enumerate() { - let name = Name::from_string(format!("column{}", idx + 1)); - let ty = infer_type_from_expr(&expr); - results.push((name, ty)); - } - } - results - } - ast::SelectVariant::Select(select) => { - let Some(select_clause) = select.select_clause() else { - return vec![]; - }; - let Some(target_list) = select_clause.target_list() else { - return vec![]; - }; - collect_target_list_columns_with_types(&target_list) - } - ast::SelectVariant::SelectInto(select_into) => { - let Some(select_clause) = select_into.select_clause() else { - return vec![]; - }; - let Some(target_list) = select_clause.target_list() else { - return vec![]; - }; - collect_target_list_columns_with_types(&target_list) - } - ast::SelectVariant::ParenSelect(nested) => { - collect_paren_select_columns_with_types(db, file, nested) - } - ast::SelectVariant::CompoundSelect(compound) => { - let Some(lhs) = compound.lhs() else { - return vec![]; - }; - collect_select_variant_columns_with_types(db, file, &lhs) - } - ast::SelectVariant::Table(table) => { - let Some(path) = table.relation_name().and_then(|r| r.path()) else { - return vec![]; - }; - let Some((table_name, schema)) = extract_table_schema_from_path(&path) else { - return vec![]; - }; - - if schema.is_none() - && let Some(name_ref) = path.segment().and_then(|segment| segment.name_ref()) - && let Some(with_table_ptr) = resolve_cte_table(&name_ref, &table_name) - && let Some(with_table) = with_table_ptr - .to_node(root) - .ancestors() - .find_map(ast::WithTable::cast) - { - return collect_with_table_column_names(db, file, with_table) - .into_iter() - .map(|name| (name, None)) - .collect(); - } - - let position = table.syntax().text_range().start(); - let Some(table_ptr) = - binder.lookup_with(&table_name, SymbolKind::Table, position, &schema) - else { - return vec![]; - }; - let Some(create_table) = table_ptr - .to_node(root) - .ancestors() - .find_map(ast::CreateTableLike::cast) - else { - return vec![]; - }; - let columns = collect_table_columns(db, file, &create_table); - columns - .into_iter() - .filter_map(|col| { - let name = Name::from_node(&col.name()?); - let ty = col.ty().and_then(|t| infer_type_from_ty(&t)); - Some((name, ty)) - }) - .collect() - } - } -} - -fn select_from_view_query(create_view: &ast::CreateView) -> Option { - let query = create_view.query()?; - match query { - ast::SelectVariant::Select(select) => Some(select), - ast::SelectVariant::ParenSelect(paren_select) => match paren_select.select()? { - ast::SelectVariant::Select(select) => Some(select), - _ => None, - }, - _ => None, - } -} - fn extract_table_name_from_path(path: &ast::Path) -> Option { let segment = path.segment()?; if let Some(name_ref) = segment.name_ref() { diff --git a/crates/squawk_syntax/src/ast/nodes.rs b/crates/squawk_syntax/src/ast/nodes.rs index 22505b0d..1e9e86c7 100644 --- a/crates/squawk_syntax/src/ast/nodes.rs +++ b/crates/squawk_syntax/src/ast/nodes.rs @@ -47,3 +47,43 @@ impl AstNode for CreateTableLike { &self.syntax } } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CreateViewLike { + pub(crate) syntax: SyntaxNode, +} +impl CreateViewLike { + #[inline] + pub fn column_list(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn path(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn query(&self) -> Option { + support::child(&self.syntax) + } +} +impl AstNode for CreateViewLike { + #[inline] + fn can_cast(kind: ast::SyntaxKind) -> bool { + matches!( + kind, + ast::SyntaxKind::CREATE_MATERIALIZED_VIEW | ast::SyntaxKind::CREATE_VIEW + ) + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +}