Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/squawk_ide/src/classify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub(crate) enum NameRefClass {
SelectColumn,
SelectQualifiedColumnTable,
SelectQualifiedColumn,
CompositeTypeField,
InsertTable,
InsertColumn,
DeleteTable,
Expand Down Expand Up @@ -116,6 +117,8 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option<NameRefClass>
&& 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);
}
Expand Down
124 changes: 124 additions & 0 deletions crates/squawk_ide/src/goto_definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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("
Expand Down Expand Up @@ -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(
Expand Down
86 changes: 86 additions & 0 deletions crates/squawk_ide/src/hover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option<String> {
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) {
Expand Down Expand Up @@ -175,6 +178,41 @@ fn hover_column(
))
}

fn hover_composite_type_field(
file: &ast::SourceFile,
name_ref: &ast::NameRef,
binder: &binder::Binder,
) -> Option<String> {
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,
Expand Down Expand Up @@ -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
");
}
}
100 changes: 100 additions & 0 deletions crates/squawk_ide/src/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -1341,3 +1342,102 @@ fn extract_param_signature(node: &impl ast::HasParamList) -> Option<Vec<Name>> {
}
(!params.is_empty()).then_some(params)
}

fn unwrap_paren_expr(expr: ast::Expr) -> Option<ast::NameRef> {
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<SyntaxNodePtr> {
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<Schema>)> {
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<Schema>)> {
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<Schema>)> {
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,
}
}
Loading