diff --git a/crates/squawk_ide/src/binder.rs b/crates/squawk_ide/src/binder.rs index d6816179..242e87bc 100644 --- a/crates/squawk_ide/src/binder.rs +++ b/crates/squawk_ide/src/binder.rs @@ -84,6 +84,7 @@ fn bind_stmt(b: &mut Binder, stmt: ast::Stmt) { ast::Stmt::CreateProcedure(create_procedure) => bind_create_procedure(b, create_procedure), ast::Stmt::CreateSchema(create_schema) => bind_create_schema(b, create_schema), ast::Stmt::CreateType(create_type) => bind_create_type(b, create_type), + ast::Stmt::CreateView(create_view) => bind_create_view(b, create_view), ast::Stmt::Set(set) => bind_set(b, set), _ => {} } @@ -265,6 +266,33 @@ fn bind_create_type(b: &mut Binder, create_type: ast::CreateType) { b.scopes[root].insert(type_name, type_id); } +fn bind_create_view(b: &mut Binder, create_view: ast::CreateView) { + let Some(path) = create_view.path() else { + return; + }; + + let Some(view_name) = item_name(&path) else { + return; + }; + + let name_ptr = path_to_ptr(&path); + let is_temp = create_view.temp_token().is_some() || create_view.temporary_token().is_some(); + + let Some(schema) = schema_name(b, &path, is_temp) else { + return; + }; + + let view_id = b.symbols.alloc(Symbol { + kind: SymbolKind::View, + ptr: name_ptr, + schema, + params: None, + }); + + let root = b.root_scope(); + b.scopes[root].insert(view_name, view_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 02100f9c..9e708331 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -457,6 +457,209 @@ drop type int4_range$0; "); } + #[test] + fn goto_drop_view() { + assert_snapshot!(goto(" +create view v as select 1; +drop view v$0; +"), @r" + ╭▸ + 2 │ create view v as select 1; + │ ─ 2. destination + 3 │ drop view v; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_drop_view_with_schema() { + assert_snapshot!(goto(" +create view public.v as select 1; +drop view v$0; +"), @r" + ╭▸ + 2 │ create view public.v as select 1; + │ ─ 2. destination + 3 │ drop view v; + ╰╴ ─ 1. source + "); + + assert_snapshot!(goto(" +create view foo.v as select 1; +drop view foo.v$0; +"), @r" + ╭▸ + 2 │ create view foo.v as select 1; + │ ─ 2. destination + 3 │ drop view foo.v; + ╰╴ ─ 1. source + "); + + goto_not_found( + " +create view v as select 1; +drop view foo.v$0; +", + ); + } + + #[test] + fn goto_drop_temp_view() { + assert_snapshot!(goto(" +create temp view v as select 1; +drop view v$0; +"), @r" + ╭▸ + 2 │ create temp view v as select 1; + │ ─ 2. destination + 3 │ drop view v; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_select_from_view() { + assert_snapshot!(goto(" +create view v as select 1; +select * from v$0; +"), @r" + ╭▸ + 2 │ create view v as select 1; + │ ─ 2. destination + 3 │ select * from v; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_select_from_view_with_schema() { + assert_snapshot!(goto(" +create view public.v as select 1; +select * from public.v$0; +"), @r" + ╭▸ + 2 │ create view public.v as select 1; + │ ─ 2. destination + 3 │ select * from public.v; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_view_column() { + assert_snapshot!(goto(" +create view v as select 1 as a; +select a$0 from v; +"), @r" + ╭▸ + 2 │ create view v as select 1 as a; + │ ─ 2. destination + 3 │ select a from v; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_view_column_qualified() { + assert_snapshot!(goto(" +create view v as select 1 as a; +select v.a$0 from v; +"), @r" + ╭▸ + 2 │ create view v as select 1 as a; + │ ─ 2. destination + 3 │ select v.a from v; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_view_with_explicit_column_list() { + assert_snapshot!(goto(" +create view v(col1) as select 1; +select * from v$0; +"), @r" + ╭▸ + 2 │ create view v(col1) as select 1; + │ ─ 2. destination + 3 │ select * from v; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_view_column_with_explicit_column_list() { + assert_snapshot!(goto(" + create view v(col1) as select 1; + select col1$0 from v; + "), @r" + ╭▸ + 2 │ create view v(col1) as select 1; + │ ──── 2. destination + 3 │ select col1 from v; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_view_column_with_schema() { + assert_snapshot!(goto(" +create view public.v as select 1 as a; +select a$0 from public.v; +"), @r" + ╭▸ + 2 │ create view public.v as select 1 as a; + │ ─ 2. destination + 3 │ select a from public.v; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_view_multiple_columns() { + assert_snapshot!(goto(" +create view v as select 1 as a, 2 as b; +select b$0 from v; +"), @r" + ╭▸ + 2 │ create view v as select 1 as a, 2 as b; + │ ─ 2. destination + 3 │ select b from v; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_view_column_from_table() { + assert_snapshot!(goto(" +create table t(x int, y int); +create view v as select x, y from t; +select x$0 from v; +"), @r" + ╭▸ + 3 │ create view v as select x, y from t; + │ ─ 2. destination + 4 │ select x from v; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_view_column_with_table_preference() { + assert_snapshot!(goto(" +create table v(a int); +create view vw as select 1 as a; +select a$0 from v; +"), @r" + ╭▸ + 2 │ create table v(a int); + │ ─ 2. destination + 3 │ create view vw as select 1 as a; + 4 │ select a from v; + ╰╴ ─ 1. source + "); + } + #[test] fn goto_cast_operator() { assert_snapshot!(goto(" diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index 4482f866..12d9b029 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -15,6 +15,7 @@ enum NameRefContext { Table, DropIndex, DropType, + DropView, DropFunction, DropAggregate, DropProcedure, @@ -75,7 +76,12 @@ pub(crate) fn resolve_name_ref(binder: &Binder, name_ref: &ast::NameRef) -> Opti } let position = name_ref.syntax().text_range().start(); - resolve_table(binder, &table_name, &schema, position) + + if let Some(ptr) = resolve_table(binder, &table_name, &schema, position) { + return Some(ptr); + } + + resolve_view(binder, &table_name, &schema, position) } NameRefContext::DropIndex => { let path = find_containing_path(name_ref)?; @@ -109,6 +115,13 @@ pub(crate) fn resolve_name_ref(binder: &Binder, name_ref: &ast::NameRef) -> Opti let position = name_ref.syntax().text_range().start(); resolve_type(binder, &type_name, &schema, position) } + NameRefContext::DropView => { + let path = find_containing_path(name_ref)?; + let view_name = extract_table_name(&path)?; + let schema = extract_schema_name(&path); + let position = name_ref.syntax().text_range().start(); + resolve_view(binder, &view_name, &schema, position) + } NameRefContext::DropFunction => { let function_sig = name_ref .syntax() @@ -255,7 +268,12 @@ pub(crate) fn resolve_name_ref(binder: &Binder, name_ref: &ast::NameRef) -> Opti } let position = name_ref.syntax().text_range().start(); - resolve_table(binder, &table_name, &schema, position) + + if let Some(ptr) = resolve_table(binder, &table_name, &schema, position) { + return Some(ptr); + } + + resolve_view(binder, &table_name, &schema, position) } } } @@ -380,6 +398,9 @@ fn classify_name_ref_context(name_ref: &ast::NameRef) -> Option if ast::DropType::can_cast(ancestor.kind()) { return Some(NameRefContext::DropType); } + if ast::DropView::can_cast(ancestor.kind()) { + return Some(NameRefContext::DropView); + } if ast::CastExpr::can_cast(ancestor.kind()) && in_type { return Some(NameRefContext::TypeReference); } @@ -495,6 +516,15 @@ fn resolve_type( resolve_for_kind(binder, type_name, schema, position, SymbolKind::Type) } +fn resolve_view( + binder: &Binder, + view_name: &Name, + schema: &Option, + position: TextSize, +) -> Option { + resolve_for_kind(binder, view_name, schema, position, SymbolKind::View) +} + fn resolve_for_kind( binder: &Binder, name: &Name, @@ -739,6 +769,7 @@ fn resolve_select_qualified_column_table( resolve_table(binder, &table_name, &schema, position) } +// TODO: this is similar to resolve_from_item_for_column, maybe we can simplify fn resolve_select_qualified_column( binder: &Binder, name_ref: &ast::NameRef, @@ -843,23 +874,36 @@ fn resolve_select_qualified_column( } }; - 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)?; - // 1. Try to find a matching column (columns take precedence) + if let Some(table_ptr) = resolve_table(binder, &table_name, &schema, position) { + let table_name_node = table_ptr.to_node(root); - if let Some(ptr) = find_column_in_create_table(&create_table, &column_name) { - return Some(ptr); + if let Some(create_table) = table_name_node.ancestors().find_map(ast::CreateTable::cast) { + // 1. Try to find a matching column (columns take precedence) + if let Some(ptr) = find_column_in_create_table(&create_table, &column_name) { + return Some(ptr); + } + // 2. No column found, check for field-style function call + // e.g., select t.b from t where b is a function that takes t as an argument + return resolve_function(binder, &column_name, &schema, None, position); + } } - // 2. No column found, check for field-style function call - // e.g., select t.b from t where b is a function that takes t as an argument - resolve_function(binder, &column_name, &schema, None, position) + // ditto as above but with views + if let Some(view_ptr) = resolve_view(binder, &table_name, &schema, position) { + let view_name_node = view_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) { + return Some(ptr); + } + + return resolve_function(binder, &column_name, &schema, None, position); + } + } + + None } fn resolve_from_item_for_column( @@ -895,26 +939,42 @@ fn resolve_from_item_for_column( } let position = name_ref.syntax().text_range().start(); - 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)?; - // 1. try to find a matching column - if let Some(ptr) = find_column_in_create_table(&create_table, &column_name) { - return Some(ptr); + + if let Some(table_ptr) = resolve_table(binder, &table_name, &schema, position) { + let table_name_node = table_ptr.to_node(root); + + if let Some(create_table) = table_name_node.ancestors().find_map(ast::CreateTable::cast) { + // 1. try to find a matching column + if let Some(ptr) = find_column_in_create_table(&create_table, &column_name) { + return Some(ptr); + } + + // 2. No column found, check if the name matches the table name. + // For example, in: + // ```sql + // create table t(a int); + // select t from t; + // ``` + if column_name == table_name { + return Some(table_ptr); + } + } } - // 2. No column found, check if the name matches the table name. - // For example, in: - // ```sql - // create table t(a int); - // select t from t; - // ``` - if column_name == table_name { - return Some(table_ptr); + // ditto as above but with view + if let Some(view_ptr) = resolve_view(binder, &table_name, &schema, position) { + let view_name_node = view_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) { + return Some(ptr); + } + + if column_name == table_name { + return Some(view_ptr); + } + } } None @@ -1186,6 +1246,54 @@ pub(crate) fn find_column_in_create_table( }) } +// TODO: this is similar to the CTE funcs, maybe we can simplify +fn find_column_in_create_view( + create_view: &ast::CreateView, + column_name: &Name, +) -> Option { + let column_list_len = if let Some(column_list) = create_view.column_list() { + for column in column_list.columns() { + if let Some(col_name) = column.name() + && Name::from_node(&col_name) == *column_name + { + return Some(SyntaxNodePtr::new(col_name.syntax())); + } + } + column_list.columns().count() + } else { + 0 + }; + + let query = create_view.query()?; + let select = match query { + ast::SelectVariant::Select(s) => s, + ast::SelectVariant::ParenSelect(ps) => match ps.select()? { + ast::SelectVariant::Select(s) => s, + _ => return None, + }, + _ => return None, + }; + + let select_clause = select.select_clause()?; + let target_list = select_clause.target_list()?; + + for (idx, target) in target_list.targets().enumerate() { + if idx < column_list_len { + continue; + } + + if let Some((col_name, node)) = ColumnName::from_target(target.clone()) { + if let Some(col_name_str) = col_name.to_string() + && Name::from_string(col_name_str) == *column_name + { + return Some(SyntaxNodePtr::new(&node)); + } + } + } + + None +} + fn resolve_cte_table(name_ref: &ast::NameRef, cte_name: &Name) -> Option { let with_clause = find_parent_with_clause(name_ref.syntax())?; for with_table in with_clause.with_tables() { diff --git a/crates/squawk_ide/src/symbols.rs b/crates/squawk_ide/src/symbols.rs index 9f15146a..c0aff9e2 100644 --- a/crates/squawk_ide/src/symbols.rs +++ b/crates/squawk_ide/src/symbols.rs @@ -51,6 +51,7 @@ pub(crate) enum SymbolKind { Procedure, Schema, Type, + View, } #[derive(Clone, Debug)] diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index 0f3763da..8eefa2ff 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -11293,7 +11293,7 @@ fn create_view(p: &mut Parser<'_>) -> CompletedMarker { p.expect(VIEW_KW); path_name(p); // [ ( column_name [, ...] ) ] - opt_column_list(p); + opt_column_list_with(p, ColumnDefKind::Name); // [ WITH ( view_option_name [= view_option_value] [, ... ] ) ] // TODO: this can be more specific opt_with_params(p); diff --git a/crates/squawk_parser/tests/snapshots/tests__create_view_ok.snap b/crates/squawk_parser/tests/snapshots/tests__create_view_ok.snap index 2e736f2e..77fbe838 100644 --- a/crates/squawk_parser/tests/snapshots/tests__create_view_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__create_view_ok.snap @@ -352,7 +352,7 @@ SOURCE_FILE COLUMN_LIST L_PAREN "(" COLUMN - NAME_REF + NAME IDENT "n" R_PAREN ")" WHITESPACE " " @@ -430,17 +430,17 @@ SOURCE_FILE COLUMN_LIST L_PAREN "(" COLUMN - NAME_REF + NAME IDENT "a" COMMA "," WHITESPACE " " COLUMN - NAME_REF + NAME IDENT "b" COMMA "," WHITESPACE " " COLUMN - NAME_REF + NAME IDENT "c" R_PAREN ")" WHITESPACE "\n"