From 33d2c8313b57df11ae2df3b40d1af46a0d53657c Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 28 Dec 2025 11:36:29 -0500 Subject: [PATCH] ide: support goto def on schema --- crates/squawk_ide/src/binder.rs | 19 ++ crates/squawk_ide/src/goto_definition.rs | 280 +++++++++++++++++++++++ crates/squawk_ide/src/hover.rs | 135 ++++++++++- crates/squawk_ide/src/resolve.rs | 264 +++++++++++++++++++++ crates/squawk_ide/src/symbols.rs | 1 + 5 files changed, 698 insertions(+), 1 deletion(-) diff --git a/crates/squawk_ide/src/binder.rs b/crates/squawk_ide/src/binder.rs index cc5c65c5..ccf273b8 100644 --- a/crates/squawk_ide/src/binder.rs +++ b/crates/squawk_ide/src/binder.rs @@ -80,6 +80,7 @@ fn bind_stmt(b: &mut Binder, stmt: ast::Stmt) { ast::Stmt::CreateTable(create_table) => bind_create_table(b, create_table), ast::Stmt::CreateIndex(create_index) => bind_create_index(b, create_index), ast::Stmt::CreateFunction(create_function) => bind_create_function(b, create_function), + ast::Stmt::CreateSchema(create_schema) => bind_create_schema(b, create_schema), ast::Stmt::Set(set) => bind_set(b, set), _ => {} } @@ -155,6 +156,24 @@ fn bind_create_function(b: &mut Binder, create_function: ast::CreateFunction) { b.scopes[root].insert(function_name, function_id); } +fn bind_create_schema(b: &mut Binder, create_schema: ast::CreateSchema) { + let Some(schema_name_node) = create_schema.name() else { + return; + }; + + let schema_name = Name::new(schema_name_node.syntax().text().to_string()); + let name_ptr = SyntaxNodePtr::new(schema_name_node.syntax()); + + let schema_id = b.symbols.alloc(Symbol { + kind: SymbolKind::Schema, + ptr: name_ptr, + schema: Schema(schema_name.clone()), + }); + + let root = b.root_scope(); + b.scopes[root].insert(schema_name, schema_id); +} + fn item_name(path: &ast::Path) -> Option { let segment = path.segment()?; diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index ca47b4d8..93c6ab80 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -1397,4 +1397,284 @@ select id$0 from users; ╰╴ ─ 1. source "); } + + #[test] + fn goto_select_table_as_column() { + assert_snapshot!(goto(" +create table t(x bigint, y bigint); +select t$0 from t; +"), @r" + ╭▸ + 2 │ create table t(x bigint, y bigint); + │ ─ 2. destination + 3 │ select t from t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_select_table_as_column_with_schema() { + assert_snapshot!(goto(" +create table public.t(x bigint, y bigint); +select t$0 from public.t; +"), @r" + ╭▸ + 2 │ create table public.t(x bigint, y bigint); + │ ─ 2. destination + 3 │ select t from public.t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_select_table_as_column_with_search_path() { + assert_snapshot!(goto(" +set search_path to foo; +create table foo.users(id int, email text); +select users$0 from users; +"), @r" + ╭▸ + 3 │ create table foo.users(id int, email text); + │ ───── 2. destination + 4 │ select users from users; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_select_column_with_same_name_as_table() { + assert_snapshot!(goto(" +create table t(t int); +select t$0 from t; +"), @r" + ╭▸ + 2 │ create table t(t int); + │ ─ 2. destination + 3 │ select t from t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_drop_schema() { + assert_snapshot!(goto(" +create schema foo; +drop schema foo$0; +"), @r" + ╭▸ + 2 │ create schema foo; + │ ─── 2. destination + 3 │ drop schema foo; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_drop_schema_defined_after() { + assert_snapshot!(goto(" +drop schema foo$0; +create schema foo; +"), @r" + ╭▸ + 2 │ drop schema foo; + │ ─ 1. source + 3 │ create schema foo; + ╰╴ ─── 2. destination + "); + } + + #[test] + fn goto_schema_qualifier_in_table() { + assert_snapshot!(goto(" +create schema foo; +create table foo$0.t(a int); +"), @r" + ╭▸ + 2 │ create schema foo; + │ ─── 2. destination + 3 │ create table foo.t(a int); + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_schema_qualifier_in_drop_table() { + assert_snapshot!(goto(" +create schema foo; +create table foo.t(a int); +drop table foo$0.t; +"), @r" + ╭▸ + 2 │ create schema foo; + │ ─── 2. destination + 3 │ create table foo.t(a int); + 4 │ drop table foo.t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_schema_qualifier_multiple_schemas() { + assert_snapshot!(goto(" +create schema foo; +create schema bar; +create table bar$0.t(a int); +"), @r" + ╭▸ + 3 │ create schema bar; + │ ─── 2. destination + 4 │ create table bar.t(a int); + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_schema_qualifier_in_function_call() { + assert_snapshot!(goto(r#" +create schema foo; +create function foo.bar() returns int as $$ begin return 1; end; $$ language plpgsql; +select foo$0.bar(); +"#), @r" + ╭▸ + 2 │ create schema foo; + │ ─── 2. destination + 3 │ create function foo.bar() returns int as $$ begin return 1; end; $$ language plpgsql; + 4 │ select foo.bar(); + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_schema_qualifier_in_function_call_from_clause() { + assert_snapshot!(goto(r#" +create schema myschema; +create function myschema.get_data() returns table(id int) as $$ begin return query select 1; end; $$ language plpgsql; +select * from myschema$0.get_data(); +"#), @r" + ╭▸ + 2 │ create schema myschema; + │ ──────── 2. destination + 3 │ create function myschema.get_data() returns table(id int) as $$ begin return query select 1; end; $$ language plpgsql; + 4 │ select * from myschema.get_data(); + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_schema_qualifier_in_select_from() { + assert_snapshot!(goto(" +create schema foo; +create table foo.t(x int); +select x from foo$0.t; +"), @r" + ╭▸ + 2 │ create schema foo; + │ ─── 2. destination + 3 │ create table foo.t(x int); + 4 │ select x from foo.t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_qualified_column_table() { + assert_snapshot!(goto(" +create table t(a int); +select t$0.a from t; +"), @r" + ╭▸ + 2 │ create table t(a int); + │ ─ 2. destination + 3 │ select t.a from t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_qualified_column_column() { + assert_snapshot!(goto(" +create table t(a int); +select t.a$0 from t; +"), @r" + ╭▸ + 2 │ create table t(a int); + │ ─ 2. destination + 3 │ select t.a from t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_three_part_qualified_column_schema() { + assert_snapshot!(goto(" +create schema foo; +create table foo.t(a int); +select foo$0.t.a from t; +"), @r" + ╭▸ + 2 │ create schema foo; + │ ─── 2. destination + 3 │ create table foo.t(a int); + 4 │ select foo.t.a from t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_three_part_qualified_column_table() { + assert_snapshot!(goto(" +create schema foo; +create table foo.t(a int); +select foo.t$0.a from t; +"), @r" + ╭▸ + 3 │ create table foo.t(a int); + │ ─ 2. destination + 4 │ select foo.t.a from t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_three_part_qualified_column_column() { + assert_snapshot!(goto(" +create schema foo; +create table foo.t(a int); +select foo.t.a$0 from t; +"), @r" + ╭▸ + 3 │ create table foo.t(a int); + │ ─ 2. destination + 4 │ select foo.t.a from t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_qualified_column_with_schema_in_from_table() { + assert_snapshot!(goto(" +create table foo.t(a int, b int); +select t$0.a from foo.t; +"), @r" + ╭▸ + 2 │ create table foo.t(a int, b int); + │ ─ 2. destination + 3 │ select t.a from foo.t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_qualified_column_with_schema_in_from_column() { + assert_snapshot!(goto(" +create table foo.t(a int, b int); +select t.a$0 from foo.t; +"), @r" + ╭▸ + 2 │ create table foo.t(a int, b int); + │ ─ 2. destination + 3 │ select t.a from foo.t; + ╰╴ ─ 1. source + "); + } } diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index 3b276445..b3ddad85 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -16,7 +16,12 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { } if is_select_column(&name_ref) { - return hover_column(file, &name_ref, &binder); + // Try hover as column first, if that fails try as table + // (handles case like `select t from t;` where t is the table) + if let Some(result) = hover_column(file, &name_ref, &binder) { + return Some(result); + } + return hover_table(file, &name_ref, &binder); } if is_table_ref(&name_ref) { @@ -38,6 +43,10 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { if is_select_function_call(&name_ref) { return hover_function(file, &name_ref, &binder); } + + if is_schema_ref(&name_ref) { + return hover_schema(file, &name_ref, &binder); + } } if let Some(name) = ast::Name::cast(parent) { @@ -62,6 +71,10 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { { return format_create_function(&create_function, &binder); } + + if let Some(create_schema) = name.syntax().ancestors().find_map(ast::CreateSchema::cast) { + return format_create_schema(&create_schema); + } } None @@ -347,6 +360,35 @@ fn is_select_column(name_ref: &ast::NameRef) -> bool { false } +fn is_schema_ref(name_ref: &ast::NameRef) -> bool { + for ancestor in name_ref.syntax().ancestors() { + if ast::DropSchema::can_cast(ancestor.kind()) { + return true; + } + } + false +} + +fn hover_schema( + file: &ast::SourceFile, + name_ref: &ast::NameRef, + binder: &binder::Binder, +) -> Option { + let schema_ptr = resolve::resolve_name_ref(binder, name_ref)?; + + let root = file.syntax(); + let schema_name_node = schema_ptr.to_node(root); + + let create_schema = ast::CreateSchema::cast(schema_name_node.parent()?)?; + + format_create_schema(&create_schema) +} + +fn format_create_schema(create_schema: &ast::CreateSchema) -> Option { + let schema_name = create_schema.name()?.syntax().text().to_string(); + Some(format!("schema {}", schema_name)) +} + fn hover_function( file: &ast::SourceFile, name_ref: &ast::NameRef, @@ -1214,4 +1256,95 @@ delete from public.users where email$0 = 'test'; ╰╴ ─ hover "); } + + #[test] + fn hover_on_select_table_as_column() { + assert_snapshot!(check_hover(" +create table t(x bigint, y bigint); +select t$0 from t; +"), @r" + hover: table public.t(x bigint, y bigint) + ╭▸ + 3 │ select t from t; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_select_table_as_column_with_schema() { + assert_snapshot!(check_hover(" +create table public.t(x bigint, y bigint); +select t$0 from public.t; +"), @r" + hover: table public.t(x bigint, y bigint) + ╭▸ + 3 │ select t from public.t; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_select_table_as_column_with_search_path() { + assert_snapshot!(check_hover(" +set search_path to foo; +create table foo.users(id int, email text); +select users$0 from users; +"), @r" + hover: table foo.users(id int, email text) + ╭▸ + 4 │ select users from users; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_select_column_with_same_name_as_table() { + assert_snapshot!(check_hover(" +create table t(t int); +select t$0 from t; +"), @r" + hover: public.t.t int + ╭▸ + 3 │ select t from t; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_create_schema() { + assert_snapshot!(check_hover(" +create schema foo$0; +"), @r" + hover: schema foo + ╭▸ + 2 │ create schema foo; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_drop_schema() { + assert_snapshot!(check_hover(" +create schema foo; +drop schema foo$0; +"), @r" + hover: schema foo + ╭▸ + 3 │ drop schema foo; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_schema_after_definition() { + assert_snapshot!(check_hover(" +drop schema foo$0; +create schema foo; +"), @r" + hover: schema foo + ╭▸ + 2 │ drop schema foo; + ╰╴ ─ hover + "); + } } diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index 8b01c61d..dd2a24e1 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -14,15 +14,19 @@ enum NameRefContext { Table, DropIndex, DropFunction, + DropSchema, CreateIndex, CreateIndexColumn, SelectFunctionCall, SelectFromTable, SelectColumn, + SelectQualifiedColumnTable, + SelectQualifiedColumn, InsertTable, InsertColumn, DeleteTable, DeleteWhereColumn, + SchemaQualifier, } pub(crate) fn resolve_name_ref(binder: &Binder, name_ref: &ast::NameRef) -> Option { @@ -69,6 +73,10 @@ pub(crate) fn resolve_name_ref(binder: &Binder, name_ref: &ast::NameRef) -> Opti let position = name_ref.syntax().text_range().start(); resolve_function(binder, &function_name, &schema, position) } + NameRefContext::DropSchema | NameRefContext::SchemaQualifier => { + let schema_name = Name::new(name_ref.syntax().text().to_string()); + resolve_schema(binder, &schema_name) + } NameRefContext::SelectFunctionCall => { let schema = if let Some(parent_node) = name_ref.syntax().parent() && let Some(field_expr) = ast::FieldExpr::cast(parent_node) @@ -86,6 +94,10 @@ pub(crate) fn resolve_name_ref(binder: &Binder, name_ref: &ast::NameRef) -> Opti } NameRefContext::CreateIndexColumn => resolve_create_index_column(binder, name_ref), NameRefContext::SelectColumn => resolve_select_column(binder, name_ref), + NameRefContext::SelectQualifiedColumnTable => { + resolve_select_qualified_column_table(binder, name_ref) + } + NameRefContext::SelectQualifiedColumn => resolve_select_qualified_column(binder, name_ref), NameRefContext::InsertColumn => resolve_insert_column(binder, name_ref), NameRefContext::DeleteWhereColumn => resolve_delete_where_column(binder, name_ref), } @@ -99,6 +111,85 @@ fn classify_name_ref_context(name_ref: &ast::NameRef) -> Option let mut in_from_clause = false; let mut in_target_list = false; + // TODO: can we combine this if and the one that follows? + if let Some(parent) = name_ref.syntax().parent() + && let Some(field_expr) = ast::FieldExpr::cast(parent.clone()) + && let Some(base) = field_expr.base() + && let ast::Expr::NameRef(base_name_ref) = base + // check that the name_ref we're looking at in the field expr is the + // base name_ref, i.e., the schema, rather than the item + && base_name_ref.syntax() == name_ref.syntax() + { + let is_function_call = field_expr + .syntax() + .parent() + .and_then(ast::CallExpr::cast) + .is_some(); + let is_schema_table_col = field_expr + .syntax() + .parent() + .and_then(ast::FieldExpr::cast) + .is_some(); + + for ancestor in parent.ancestors() { + if ast::TargetList::can_cast(ancestor.kind()) { + if is_function_call || is_schema_table_col { + return Some(NameRefContext::SchemaQualifier); + } else { + return Some(NameRefContext::SelectQualifiedColumnTable); + } + } + } + return Some(NameRefContext::SchemaQualifier); + } + + if let Some(parent) = name_ref.syntax().parent() + && let Some(field_expr) = ast::FieldExpr::cast(parent.clone()) + && field_expr + .field() + // we're at the field in a FieldExpr, i.e., foo.bar + // ^^^ + .is_some_and(|field_name_ref| field_name_ref.syntax() == name_ref.syntax()) + // we're not inside a call expr + && field_expr + .syntax() + .parent() + .and_then(ast::CallExpr::cast) + .is_none() + { + let is_base_of_outer_field_expr = field_expr + .syntax() + .parent() + .and_then(ast::FieldExpr::cast) + .is_some(); + + for ancestor in parent.ancestors() { + if ast::TargetList::can_cast(ancestor.kind()) { + if is_base_of_outer_field_expr { + return Some(NameRefContext::SelectQualifiedColumnTable); + } else if let Some(base) = field_expr.base() + && matches!(base, ast::Expr::NameRef(_) | ast::Expr::FieldExpr(_)) + { + return Some(NameRefContext::SelectQualifiedColumn); + } else { + return Some(NameRefContext::SelectQualifiedColumnTable); + } + } + } + } + + if let Some(parent) = name_ref.syntax().parent() + && let Some(inner_path) = ast::PathSegment::cast(parent) + .and_then(|p| p.syntax().parent().and_then(ast::Path::cast)) + && let Some(outer_path) = inner_path + .syntax() + .parent() + .and_then(|p| ast::Path::cast(p).and_then(|p| p.qualifier())) + && outer_path.syntax() == inner_path.syntax() + { + return Some(NameRefContext::SchemaQualifier); + } + for ancestor in name_ref.syntax().ancestors() { if ast::DropTable::can_cast(ancestor.kind()) { return Some(NameRefContext::DropTable); @@ -112,6 +203,9 @@ fn classify_name_ref_context(name_ref: &ast::NameRef) -> Option if ast::DropFunction::can_cast(ancestor.kind()) { return Some(NameRefContext::DropFunction); } + if ast::DropSchema::can_cast(ancestor.kind()) { + return Some(NameRefContext::DropSchema); + } if ast::PartitionItem::can_cast(ancestor.kind()) { in_partition_item = true; } @@ -226,6 +320,15 @@ fn resolve_function( ) } +fn resolve_schema(binder: &Binder, schema_name: &Name) -> Option { + let symbols = binder.scopes[binder.root_scope()].get(schema_name)?; + let symbol_id = symbols.iter().copied().find(|id| { + let symbol = &binder.symbols[*id]; + symbol.kind == SymbolKind::Schema + })?; + Some(binder.symbols[symbol_id].ptr) +} + fn resolve_create_index_column(binder: &Binder, name_ref: &ast::NameRef) -> Option { let column_name = Name::new(name_ref.syntax().text().to_string()); @@ -296,6 +399,156 @@ fn resolve_insert_column(binder: &Binder, name_ref: &ast::NameRef) -> Option Option { + let table_name = Name::new(name_ref.syntax().text().to_string()); + + let field_expr = name_ref.syntax().parent().and_then(ast::FieldExpr::cast)?; + + let explicit_schema = if field_expr + .field() + .is_some_and(|f| f.syntax() == name_ref.syntax()) + { + // if we're at the field `bar` in `foo.bar` + if let ast::Expr::NameRef(schema_name_ref) = field_expr.base()? { + Some(Schema(Name::new( + schema_name_ref.syntax().text().to_string(), + ))) + } else { + None + } + } else if let Some(base) = field_expr.base() + && let ast::Expr::FieldExpr(inner_field_expr) = base + && let Some(inner_base) = inner_field_expr.base() + && let ast::Expr::NameRef(schema_name_ref) = inner_base + { + // if we're at the field `foo` in `foo.buzz.bar` + Some(Schema(Name::new( + schema_name_ref.syntax().text().to_string(), + ))) + } else { + None + }; + + if let Some(schema) = explicit_schema { + let position = name_ref.syntax().text_range().start(); + return resolve_table(binder, &table_name, &Some(schema), position); + } + + let select = name_ref.syntax().ancestors().find_map(ast::Select::cast)?; + let from_clause = select.from_clause()?; + let from_item = from_clause.from_items().next()?; + + let (table_name, schema) = if let Some(name_ref_node) = from_item.name_ref() { + // `from foo` + let from_table_name = Name::new(name_ref_node.syntax().text().to_string()); + if from_table_name == table_name { + (from_table_name, None) + } else { + return None; + } + } else { + // `from bar.foo` + let from_field_expr = from_item.field_expr()?; + let from_table_name = Name::new(from_field_expr.field()?.syntax().text().to_string()); + if from_table_name != table_name { + return None; + } + let ast::Expr::NameRef(schema_name_ref) = from_field_expr.base()? else { + return None; + }; + let schema = Schema(Name::new(schema_name_ref.syntax().text().to_string())); + (from_table_name, Some(schema)) + }; + + let position = name_ref.syntax().text_range().start(); + resolve_table(binder, &table_name, &schema, position) +} + +fn resolve_select_qualified_column( + binder: &Binder, + name_ref: &ast::NameRef, +) -> Option { + let column_name = Name::new(name_ref.syntax().text().to_string()); + + let field_expr = name_ref.syntax().parent().and_then(ast::FieldExpr::cast)?; + + let (column_table_name, explicit_schema) = + // if we're at `base` in `base.field` + if let Some(base) = field_expr.base() + && let ast::Expr::NameRef(table_name_ref) = base + { + (Name::new(table_name_ref.syntax().text().to_string()), None) + // we have `foo.bar.buzz` + } else if let Some(base) = field_expr.base() + && let ast::Expr::FieldExpr(inner_field_expr) = base + && let Some(table_field) = inner_field_expr.field() + && let Some(inner_base) = inner_field_expr.base() + && let ast::Expr::NameRef(schema_name_ref) = inner_base + { + ( + Name::new(table_field.syntax().text().to_string()), + Some(Schema(Name::new( + schema_name_ref.syntax().text().to_string(), + ))), + ) + } else { + return None; + }; + + let position = name_ref.syntax().text_range().start(); + + let (table_name, schema) = if let Some(schema) = explicit_schema { + (column_table_name, Some(schema)) + } else { + let select = name_ref.syntax().ancestors().find_map(ast::Select::cast)?; + let from_clause = select.from_clause()?; + let from_item = from_clause.from_items().next()?; + + if let Some(name_ref_node) = from_item.name_ref() { + // `from bar` + let from_table_name = Name::new(name_ref_node.syntax().text().to_string()); + if from_table_name == column_table_name { + (from_table_name, None) + } else { + return None; + } + } else { + // `from foo.bar` + let from_field_expr = from_item.field_expr()?; + let from_table_name = Name::new(from_field_expr.field()?.syntax().text().to_string()); + if from_table_name != column_table_name { + return None; + } + let ast::Expr::NameRef(schema_name_ref) = from_field_expr.base()? else { + return None; + }; + let schema = Schema(Name::new(schema_name_ref.syntax().text().to_string())); + (from_table_name, Some(schema)) + } + }; + + let table_ptr = resolve_table(binder, &table_name, &schema, position)?; + + let root = &name_ref.syntax().ancestors().last()?; + let table_name_node = table_ptr.to_node(root); + let create_table = table_name_node + .ancestors() + .find_map(ast::CreateTable::cast)?; + for arg in create_table.table_arg_list()?.args() { + if let ast::TableArg::Column(column) = arg + && let Some(col_name) = column.name() + && Name::new(col_name.syntax().text().to_string()) == column_name + { + return Some(SyntaxNodePtr::new(col_name.syntax())); + } + } + + None +} + fn resolve_select_column(binder: &Binder, name_ref: &ast::NameRef) -> Option { let column_name = Name::new(name_ref.syntax().text().to_string()); @@ -325,6 +578,7 @@ fn resolve_select_column(binder: &Binder, name_ref: &ast::NameRef) -> Option Option