diff --git a/crates/squawk_ide/src/classify.rs b/crates/squawk_ide/src/classify.rs index 9d87c59b..e19b480a 100644 --- a/crates/squawk_ide/src/classify.rs +++ b/crates/squawk_ide/src/classify.rs @@ -20,6 +20,7 @@ pub(crate) enum NameRefClass { SelectColumn, SelectQualifiedColumnTable, SelectQualifiedColumn, + CompositeTypeField, InsertTable, InsertColumn, DeleteTable, @@ -116,6 +117,8 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option && matches!(base, ast::Expr::NameRef(_) | ast::Expr::FieldExpr(_)) { return Some(NameRefClass::SelectQualifiedColumn); + } else if let Some(ast::Expr::ParenExpr(_)) = field_expr.base() { + return Some(NameRefClass::CompositeTypeField); } else { return Some(NameRefClass::SelectQualifiedColumnTable); } diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index 9e708331..12164dd9 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -443,6 +443,22 @@ drop type person$0; "); } + #[test] + fn goto_composite_type_field() { + assert_snapshot!(goto(" +create type person_info as (name text, email text); +create table user(id int, member person_info); +select (member).name$0 from user; +"), @r" + ╭▸ + 2 │ create type person_info as (name text, email text); + │ ──── 2. destination + 3 │ create table user(id int, member person_info); + 4 │ select (member).name from user; + ╰╴ ─ 1. source + "); + } + #[test] fn goto_drop_type_range() { assert_snapshot!(goto(" @@ -702,6 +718,114 @@ select x::public.baz$0; "); } + #[test] + fn goto_cast_composite_type() { + assert_snapshot!(goto(" +create type person_info as (name varchar(50), age int); +select ('Alice', 30)::person_info$0; +"), @r" + ╭▸ + 2 │ create type person_info as (name varchar(50), age int); + │ ─────────── 2. destination + 3 │ select ('Alice', 30)::person_info; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_cast_composite_type_in_cte() { + assert_snapshot!(goto(" +create type person_info as (name varchar(50), age int); +with team as ( + select 1 as id, ('Alice', 30)::person_info$0 as member +) +select * from team; +"), @r" + ╭▸ + 2 │ create type person_info as (name varchar(50), age int); + │ ─────────── 2. destination + 3 │ with team as ( + 4 │ select 1 as id, ('Alice', 30)::person_info as member + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_composite_type_field_name() { + assert_snapshot!(goto(" +create type person_info as (name varchar(50), age int); +with team as ( + select 1 as id, ('Alice', 30)::person_info as member +) +select (member).name$0, (member).age from team; +"), @r" + ╭▸ + 2 │ create type person_info as (name varchar(50), age int); + │ ──── 2. destination + ‡ + 6 │ select (member).name, (member).age from team; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_composite_type_field_in_where() { + assert_snapshot!(goto(" +create type person_info as (name varchar(50), age int); +with team as ( + select 1 as id, ('Alice', 30)::person_info as member + union all + select 2, ('Bob', 25)::person_info +) +select (member).name, (member).age +from team +where (member).age$0 >= 18; +"), @r" + ╭▸ + 2 │ create type person_info as (name varchar(50), age int); + │ ─── 2. destination + ‡ + 10 │ where (member).age >= 18; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_composite_type_field_base() { + assert_snapshot!(goto(" +create type person_info as (name varchar(50), age int); +with team as ( + select 1 as id, ('Alice', 30)::person_info as member +) +select (member$0).age from team; +"), @r" + ╭▸ + 4 │ select 1 as id, ('Alice', 30)::person_info as member + │ ────── 2. destination + 5 │ ) + 6 │ select (member).age from team; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_composite_type_field_nested_parens() { + assert_snapshot!(goto(" +create type person_info as (name varchar(50), age int); +with team as ( + select 1 as id, ('Alice', 30)::person_info as member +) +select ((((member))).name$0) from team; +"), @r" + ╭▸ + 2 │ create type person_info as (name varchar(50), age int); + │ ──── 2. destination + ‡ + 6 │ select ((((member))).name) from team; + ╰╴ ─ 1. source + "); + } + #[test] fn begin_to_rollback() { assert_snapshot!(goto( diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index 7164ffb7..4bbbf52c 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -22,6 +22,9 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { NameRefClass::TypeReference | NameRefClass::DropType => { return hover_type(file, &name_ref, &binder); } + NameRefClass::CompositeTypeField => { + return hover_composite_type_field(file, &name_ref, &binder); + } NameRefClass::SelectColumn | NameRefClass::SelectQualifiedColumn => { // Try hover as column first if let Some(result) = hover_column(file, &name_ref, &binder) { @@ -175,6 +178,41 @@ fn hover_column( )) } +fn hover_composite_type_field( + file: &ast::SourceFile, + name_ref: &ast::NameRef, + binder: &binder::Binder, +) -> Option { + let field_ptr = resolve::resolve_name_ref(binder, name_ref)?; + let root = file.syntax(); + let field_name_node = field_ptr.to_node(root); + + let column = field_name_node.ancestors().find_map(ast::Column::cast)?; + let field_name = column.name()?.syntax().text().to_string(); + let ty = column.ty()?; + + let create_type = column + .syntax() + .ancestors() + .find_map(ast::CreateType::cast)?; + let type_path = create_type.path()?; + let type_name = type_path.segment()?.name()?.syntax().text().to_string(); + + let schema = if let Some(qualifier) = type_path.qualifier() { + qualifier.syntax().text().to_string() + } else { + type_schema(&create_type, binder)? + }; + + Some(format!( + "field {}.{}.{} {}", + schema, + type_name, + field_name, + ty.syntax().text() + )) +} + fn hover_column_definition( create_table: &ast::CreateTable, column: &ast::Column, @@ -2571,4 +2609,52 @@ drop view v$0; ╰╴ ─ hover "); } + + #[test] + fn hover_composite_type_field() { + assert_snapshot!(check_hover(" +create type person_info as (name varchar(50), age int); +with team as ( + select 1 as id, ('Alice', 30)::person_info as member +) +select (member).name$0, (member).age from team; +"), @r" + hover: field public.person_info.name varchar(50) + ╭▸ + 6 │ select (member).name, (member).age from team; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_composite_type_field_age() { + assert_snapshot!(check_hover(" +create type person_info as (name varchar(50), age int); +with team as ( + select 1 as id, ('Alice', 30)::person_info as member +) +select (member).name, (member).age$0 from team; +"), @r" + hover: field public.person_info.age int + ╭▸ + 6 │ select (member).name, (member).age from team; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_composite_type_field_nested_parens() { + assert_snapshot!(check_hover(" +create type person_info as (name varchar(50), age int); +with team as ( + select 1 as id, ('Alice', 30)::person_info as member +) +select ((((member))).name$0) from team; +"), @r" + hover: field public.person_info.name varchar(50) + ╭▸ + 6 │ select ((((member))).name) from team; + ╰╴ ─ hover + "); + } } diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index e14b573b..db04495c 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -213,6 +213,7 @@ pub(crate) fn resolve_name_ref(binder: &Binder, name_ref: &ast::NameRef) -> Opti resolve_select_qualified_column_table(binder, name_ref) } NameRefClass::SelectQualifiedColumn => resolve_select_qualified_column(binder, name_ref), + NameRefClass::CompositeTypeField => resolve_composite_type_field(binder, name_ref), NameRefClass::InsertColumn => resolve_insert_column(binder, name_ref), NameRefClass::DeleteWhereColumn => resolve_delete_where_column(binder, name_ref), NameRefClass::UpdateWhereColumn | NameRefClass::UpdateSetColumn => { @@ -1341,3 +1342,102 @@ fn extract_param_signature(node: &impl ast::HasParamList) -> Option> { } (!params.is_empty()).then_some(params) } + +fn unwrap_paren_expr(expr: ast::Expr) -> Option { + let mut current = expr; + for _ in 0..10_000 { + match current { + ast::Expr::ParenExpr(paren_expr) => { + current = paren_expr.expr()?; + } + ast::Expr::NameRef(nr) => return Some(nr), + _ => return None, + } + } + None +} + +fn resolve_composite_type_field(binder: &Binder, name_ref: &ast::NameRef) -> Option { + let field_name = Name::from_node(name_ref); + let field_expr = name_ref.syntax().parent().and_then(ast::FieldExpr::cast)?; + let base = field_expr.base()?; + + let base_name_ref = unwrap_paren_expr(base)?; + let root = &name_ref.syntax().ancestors().last()?; + + let (type_name, schema) = + if let Some(type_info) = resolve_composite_type_from_column(binder, &base_name_ref, root) { + type_info + } else { + resolve_composite_type_from_cast(binder, &base_name_ref, root)? + }; + + let position = name_ref.syntax().text_range().start(); + let type_ptr = resolve_type(binder, &type_name, &schema, position)?; + let type_node = type_ptr.to_node(root); + + let create_type = type_node.ancestors().find_map(ast::CreateType::cast)?; + let column_list = create_type.column_list()?; + + for column in column_list.columns() { + if let Some(col_name) = column.name() + && Name::from_node(&col_name) == field_name + { + return Some(SyntaxNodePtr::new(col_name.syntax())); + } + } + + None +} + +fn resolve_composite_type_from_column( + binder: &Binder, + base_name_ref: &ast::NameRef, + root: &SyntaxNode, +) -> Option<(Name, Option)> { + let column_ptr = resolve_select_column(binder, base_name_ref)?; + let column_node = column_ptr.to_node(root); + let column = column_node.ancestors().find_map(ast::Column::cast)?; + let ty = column.ty()?; + extract_type_name_and_schema(&ty) +} + +fn resolve_composite_type_from_cast( + binder: &Binder, + base_name_ref: &ast::NameRef, + root: &SyntaxNode, +) -> Option<(Name, Option)> { + let column_ptr = resolve_select_column(binder, base_name_ref)?; + let column_node = column_ptr.to_node(root); + let target = column_node.ancestors().find_map(ast::Target::cast)?; + let ast::Expr::CastExpr(cast_expr) = target.expr()? else { + return None; + }; + let ty = cast_expr.ty()?; + extract_type_name_and_schema(&ty) +} + +fn extract_type_name_and_schema(ty: &ast::Type) -> Option<(Name, Option)> { + match ty { + ast::Type::PathType(path_type) => { + let path = path_type.path()?; + let type_name = extract_table_name(&path)?; + let schema = extract_schema_name(&path); + Some((type_name, schema)) + } + ast::Type::ExprType(expr_type) => { + let expr = expr_type.expr()?; + if let ast::Expr::FieldExpr(field_expr) = expr + && let Some(field) = field_expr.field() + && let Some(ast::Expr::NameRef(schema_name_ref)) = field_expr.base() + { + let type_name = Name::from_node(&field); + let schema = Some(Schema(Name::from_node(&schema_name_ref))); + Some((type_name, schema)) + } else { + None + } + } + _ => None, + } +}