From 6b740873c5fb9691339eaeb2ca8da9ca1b203023 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sat, 21 Feb 2026 15:57:04 -0500 Subject: [PATCH] ide: goto def & hover for now() + current_timestamp also - fix some ungram issues - column naming for trim --- crates/squawk_ide/src/column_name.rs | 14 +- crates/squawk_ide/src/goto_definition.rs | 51 +++++ crates/squawk_ide/src/hover.rs | 201 +++++++++--------- crates/squawk_ide/src/resolve.rs | 21 +- .../squawk_syntax/src/ast/generated/nodes.rs | 48 ++++- crates/squawk_syntax/src/postgresql.ungram | 26 ++- 6 files changed, 243 insertions(+), 118 deletions(-) diff --git a/crates/squawk_ide/src/column_name.rs b/crates/squawk_ide/src/column_name.rs index f0c23d22..dc3d4d48 100644 --- a/crates/squawk_ide/src/column_name.rs +++ b/crates/squawk_ide/src/column_name.rs @@ -311,8 +311,15 @@ fn name_from_expr(expr: ast::Expr, in_type: bool) -> Option<(ColumnName, SyntaxN )); } if let Some(trim_fn) = call_expr.trim_fn() { + let name = if trim_fn.leading_token().is_some() { + "ltrim" + } else if trim_fn.trailing_token().is_some() { + "rtrim" + } else { + "btrim" + }; return Some(( - ColumnName::Column("trim".to_string()), + ColumnName::Column(name.to_string()), trim_fn.syntax().clone(), )); } @@ -494,7 +501,10 @@ fn examples() { assert_snapshot!(name("substring('hello' from 2 for 3)"), @"substring"); assert_snapshot!(name("position('a' in 'abc')"), @"position"); assert_snapshot!(name("overlay('hello' placing 'X' from 2)"), @"overlay"); - assert_snapshot!(name("trim(' hi ')"), @"trim"); + assert_snapshot!(name("trim(' hi ')"), @"btrim"); + assert_snapshot!(name("trim(leading ' ' from ' hi ')"), @"ltrim"); + assert_snapshot!(name("trim(trailing ' ' from ' hi ')"), @"rtrim"); + assert_snapshot!(name("trim(both ' ' from ' hi ')"), @"btrim"); assert_snapshot!(name("xmlroot('', version '1.0')"), @"xml_root"); assert_snapshot!(name("xmlserialize(document '' as text)"), @"xml_serialize"); assert_snapshot!(name("xmlelement(name foo, 'bar')"), @"xml_element"); diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index 10d95904..8690e4e0 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -762,6 +762,57 @@ select now$0(); "); } + #[test] + fn goto_current_timestamp() { + assert_snapshot!(goto(" +select current_timestamp$0; +"), @r" + ╭▸ current.sql:2:24 + │ + 2 │ select current_timestamp; + │ ─ 1. source + ╰╴ + + ╭▸ builtin.sql:10798:28 + │ + 10798 │ create function pg_catalog.now() returns timestamp with time zone + ╰╴ ─── 2. destination + "); + } + + #[test] + fn goto_current_timestamp_cte_column() { + assert_snapshot!(goto(" +with t as (select 1 current_timestamp) +select current_timestamp$0 from t; +"), @r" + ╭▸ + 2 │ with t as (select 1 current_timestamp) + │ ───────────────── 2. destination + 3 │ select current_timestamp from t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_current_timestamp_in_where() { + assert_snapshot!(goto(" +create table t(created_at timestamptz); +select * from t where current_timestamp$0 > t.created_at; +"), @r" + ╭▸ current.sql:3:39 + │ + 3 │ select * from t where current_timestamp > t.created_at; + │ ─ 1. source + ╰╴ + + ╭▸ builtin.sql:10798:28 + │ + 10798 │ create function pg_catalog.now() returns timestamp with time zone + ╰╴ ─── 2. destination + "); + } + #[test] fn goto_create_policy_schema_qualified_table() { assert_snapshot!(goto(" diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index 29af7d27..0748c870 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -1,3 +1,4 @@ +use crate::builtins::BUILTINS_SQL; use crate::classify::{NameClass, NameRefClass, classify_name, classify_name_ref}; use crate::column_name::ColumnName; use crate::offsets::token_from_offset; @@ -43,109 +44,15 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { } if let Some(name_ref) = ast::NameRef::cast(parent.clone()) { - let context = classify_name_ref(&name_ref)?; - match context { - NameRefClass::CreateIndexColumn - | NameRefClass::InsertColumn - | NameRefClass::DeleteColumn - | NameRefClass::UpdateColumn - | NameRefClass::MergeColumn - | NameRefClass::ConstraintColumn - | NameRefClass::JoinUsingColumn - | NameRefClass::ForeignKeyColumn - | NameRefClass::AlterColumn - | NameRefClass::QualifiedColumn => { - return hover_column(root, &name_ref, &binder); - } - NameRefClass::Type => { - return hover_type(root, &name_ref, &binder); - } - NameRefClass::CompositeTypeField => { - return hover_composite_type_field(root, &name_ref, &binder); - } - NameRefClass::SelectColumn - | NameRefClass::SelectQualifiedColumn - | NameRefClass::PolicyColumn => { - // Try hover as column first - if let Some(result) = hover_column(root, &name_ref, &binder) { - return Some(result); - } - // If no column, try as function (handles field-style function calls like `t.b`) - if let Some(result) = hover_function(root, &name_ref, &binder) { - return Some(result); - } - // Finally try as table (handles case like `select t from t;` where t is the table) - return hover_table(root, &name_ref, &binder); - } - NameRefClass::DeleteQualifiedColumnTable - | NameRefClass::ForeignKeyTable - | NameRefClass::FromTable - | NameRefClass::InsertQualifiedColumnTable - | NameRefClass::LikeTable - | NameRefClass::MergeQualifiedColumnTable - | NameRefClass::PolicyQualifiedColumnTable - | NameRefClass::SelectQualifiedColumnTable - | NameRefClass::Table - | NameRefClass::UpdateQualifiedColumnTable - | NameRefClass::View => { - return hover_table(root, &name_ref, &binder); - } - NameRefClass::Sequence => return hover_sequence(root, &name_ref, &binder), - NameRefClass::Trigger => return hover_trigger(root, &name_ref, &binder), - NameRefClass::Policy => { - return hover_policy(root, &name_ref, &binder); - } - NameRefClass::EventTrigger => { - return hover_event_trigger(root, &name_ref, &binder); - } - NameRefClass::Database => { - return hover_database(root, &name_ref, &binder); - } - NameRefClass::Server => { - return hover_server(root, &name_ref, &binder); - } - NameRefClass::Extension => { - return hover_extension(root, &name_ref, &binder); - } - NameRefClass::Role => { - return hover_role(root, &name_ref, &binder); - } - NameRefClass::Tablespace => return hover_tablespace(root, &name_ref, &binder), - NameRefClass::Index => { - return hover_index(root, &name_ref, &binder); - } - NameRefClass::Function | NameRefClass::FunctionCall | NameRefClass::FunctionName => { - return hover_function(root, &name_ref, &binder); - } - NameRefClass::Aggregate => return hover_aggregate(root, &name_ref, &binder), - NameRefClass::Procedure | NameRefClass::CallProcedure | NameRefClass::ProcedureCall => { - return hover_procedure(root, &name_ref, &binder); - } - NameRefClass::Routine => return hover_routine(root, &name_ref, &binder), - NameRefClass::SelectFunctionCall => { - // Try function first, but fall back to column if no function found - // (handles function-call-style column access like `select a(t)`) - if let Some(result) = hover_function(root, &name_ref, &binder) { - return Some(result); - } - return hover_column(root, &name_ref, &binder); - } - NameRefClass::Schema => { - return hover_schema(root, &name_ref, &binder); - } - NameRefClass::NamedArgParameter => { - return hover_named_arg_parameter(root, &name_ref, &binder); - } - NameRefClass::Cursor => { - return hover_cursor(root, &name_ref, &binder); - } - NameRefClass::PreparedStatement => { - return hover_prepared_statement(root, &name_ref, &binder); - } - NameRefClass::Channel => { - return hover_channel(root, &name_ref, &binder); - } + if let Some(result) = hover_name_ref(root, &name_ref, &binder) { + return Some(result); } + + // Fall back to builtins + let builtins_tree = ast::SourceFile::parse(BUILTINS_SQL).tree(); + let builtins_binder = binder::bind(&builtins_tree); + let builtins_root = builtins_tree.syntax(); + return hover_name_ref(builtins_root, &name_ref, &builtins_binder); } if let Some(name) = ast::Name::cast(parent) { @@ -222,6 +129,84 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { None } +fn hover_name_ref( + root: &SyntaxNode, + name_ref: &ast::NameRef, + binder: &binder::Binder, +) -> Option { + let context = classify_name_ref(name_ref)?; + match context { + NameRefClass::CreateIndexColumn + | NameRefClass::InsertColumn + | NameRefClass::DeleteColumn + | NameRefClass::UpdateColumn + | NameRefClass::MergeColumn + | NameRefClass::ConstraintColumn + | NameRefClass::JoinUsingColumn + | NameRefClass::ForeignKeyColumn + | NameRefClass::AlterColumn + | NameRefClass::QualifiedColumn => hover_column(root, name_ref, binder), + NameRefClass::Type => hover_type(root, name_ref, binder), + NameRefClass::CompositeTypeField => hover_composite_type_field(root, name_ref, binder), + NameRefClass::SelectColumn + | NameRefClass::SelectQualifiedColumn + | NameRefClass::PolicyColumn => { + // Try hover as column first + if let Some(result) = hover_column(root, name_ref, binder) { + return Some(result); + } + // If no column, try as function (handles field-style function calls like `t.b`) + if let Some(result) = hover_function(root, name_ref, binder) { + return Some(result); + } + // Finally try as table (handles case like `select t from t;` where t is the table) + hover_table(root, name_ref, binder) + } + NameRefClass::DeleteQualifiedColumnTable + | NameRefClass::ForeignKeyTable + | NameRefClass::FromTable + | NameRefClass::InsertQualifiedColumnTable + | NameRefClass::LikeTable + | NameRefClass::MergeQualifiedColumnTable + | NameRefClass::PolicyQualifiedColumnTable + | NameRefClass::SelectQualifiedColumnTable + | NameRefClass::Table + | NameRefClass::UpdateQualifiedColumnTable + | NameRefClass::View => hover_table(root, name_ref, binder), + NameRefClass::Sequence => hover_sequence(root, name_ref, binder), + NameRefClass::Trigger => hover_trigger(root, name_ref, binder), + NameRefClass::Policy => hover_policy(root, name_ref, binder), + NameRefClass::EventTrigger => hover_event_trigger(root, name_ref, binder), + NameRefClass::Database => hover_database(root, name_ref, binder), + NameRefClass::Server => hover_server(root, name_ref, binder), + NameRefClass::Extension => hover_extension(root, name_ref, binder), + NameRefClass::Role => hover_role(root, name_ref, binder), + NameRefClass::Tablespace => hover_tablespace(root, name_ref, binder), + NameRefClass::Index => hover_index(root, name_ref, binder), + NameRefClass::Function | NameRefClass::FunctionCall | NameRefClass::FunctionName => { + hover_function(root, name_ref, binder) + } + NameRefClass::Aggregate => hover_aggregate(root, name_ref, binder), + NameRefClass::Procedure | NameRefClass::CallProcedure | NameRefClass::ProcedureCall => { + hover_procedure(root, name_ref, binder) + } + NameRefClass::Routine => hover_routine(root, name_ref, binder), + NameRefClass::SelectFunctionCall => { + // Try function first, but fall back to column if no function found + // (handles function-call-style column access like `select a(t)`) + if let Some(result) = hover_function(root, name_ref, binder) { + return Some(result); + } + hover_column(root, name_ref, binder) + } + NameRefClass::Schema => hover_schema(root, name_ref, binder), + NameRefClass::NamedArgParameter => hover_named_arg_parameter(root, name_ref, binder), + NameRefClass::Cursor => hover_cursor(root, name_ref, binder), + NameRefClass::PreparedStatement => hover_prepared_statement(root, name_ref, binder), + NameRefClass::Channel => hover_channel(root, name_ref, binder), + } +} + struct ColumnHover {} impl ColumnHover { fn table_column(table_name: &str, column_name: &str) -> String { @@ -2194,6 +2179,18 @@ select add$0(1, 2); "); } + #[test] + fn hover_on_builtin_function_call() { + assert_snapshot!(check_hover(" +select now$0(); +"), @r" + hover: function pg_catalog.now() returns timestamp with time zone + ╭▸ + 2 │ select now(); + ╰╴ ─ hover + "); + } + #[test] fn hover_on_named_arg_param() { assert_snapshot!(check_hover(" diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index d1932ce9..0ae0c243 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -1,7 +1,7 @@ use rowan::TextSize; use smallvec::{SmallVec, smallvec}; use squawk_syntax::{ - SyntaxNode, SyntaxNodePtr, + SyntaxKind, SyntaxNode, SyntaxNodePtr, ast::{self, AstNode, SelectVariant}, }; @@ -496,6 +496,7 @@ pub(crate) fn resolve_name_ref_ptrs( .map(|ptr| smallvec![ptr]) } } + .or_else(|| resolve_current_timestamp_as_now(binder, name_ref)) } fn resolve_table_name_ptr( @@ -703,6 +704,24 @@ fn resolve_for_kind_with_params( binder.lookup_with_params(name, kind, position, schema, params) } +/// `current_timestamp` is equivalent to `now()` +fn resolve_current_timestamp_as_now( + binder: &Binder, + name_ref: &ast::NameRef, +) -> Option> { + if name_ref + .syntax() + .first_child_or_token() + .is_some_and(|t| t.kind() == SyntaxKind::CURRENT_TIMESTAMP_KW) + { + let now_name = Name::from_string("now"); + let position = name_ref.syntax().text_range().start(); + return resolve_function(binder, &now_name, &None, None, position) + .map(|ptr| smallvec![ptr]); + } + None +} + fn resolve_function( binder: &Binder, function_name: &Name, diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 2238d440..fb73eca6 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -9589,6 +9589,10 @@ impl JsonKeyValue { pub fn colon_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::COLON) } + #[inline] + pub fn value_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::VALUE_KW) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -9750,6 +9754,14 @@ impl JsonPassingArg { pub fn expr(&self) -> Option { support::child(&self.syntax) } + #[inline] + pub fn name(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn as_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::AS_KW) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -9847,9 +9859,21 @@ impl JsonQuotesClause { support::token(&self.syntax, SyntaxKind::OMIT_KW) } #[inline] + pub fn on_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::ON_KW) + } + #[inline] pub fn quotes_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::QUOTES_KW) } + #[inline] + pub fn scalar_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SCALAR_KW) + } + #[inline] + pub fn string_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::STRING_KW) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -15150,6 +15174,10 @@ impl SubstringFn { support::token(&self.syntax, SyntaxKind::R_PAREN) } #[inline] + pub fn escape_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::ESCAPE_KW) + } + #[inline] pub fn for_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::FOR_KW) } @@ -15522,10 +15550,22 @@ impl TrimFn { support::token(&self.syntax, SyntaxKind::R_PAREN) } #[inline] + pub fn both_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::BOTH_KW) + } + #[inline] pub fn from_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::FROM_KW) } #[inline] + pub fn leading_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::LEADING_KW) + } + #[inline] + pub fn trailing_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::TRAILING_KW) + } + #[inline] pub fn trim_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::TRIM_KW) } @@ -16768,8 +16808,8 @@ impl XmlSerializeFn { support::token(&self.syntax, SyntaxKind::DOCUMENT_KW) } #[inline] - pub fn ident_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::IDENT) + pub fn indent_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::INDENT_KW) } #[inline] pub fn no_token(&self) -> Option { @@ -16856,6 +16896,10 @@ impl XmlTableColumnList { pub fn xml_table_columns(&self) -> AstChildren { support::children(&self.syntax) } + #[inline] + pub fn columns_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::COLUMNS_KW) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 0219a517..98b204a1 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -184,7 +184,7 @@ JsonQueryFn = XmlTable = 'xmltable' '(' - ('xmlnamespaces' XmlNamespaceList ',') + ('xmlnamespaces' XmlNamespaceList ',')? XmlRowPassingClause XmlTableColumnList ')' @@ -237,7 +237,7 @@ SubstringFn = 'substring' '(' Expr 'for' Expr ('from' Expr) | Expr 'from' Expr ('for' Expr) - | Expr 'similar' Expr + | Expr 'similar' Expr 'escape' Expr | (Expr (',' Expr)*) ')' @@ -254,9 +254,12 @@ OverlayFn = TrimFn = 'trim' '(' - 'from' (Expr (',' Expr)*) - | Expr 'from' Expr - | (Expr (',' Expr)*)? + ('both' | 'leading' | 'trailing')? + ( + 'from' (Expr (',' Expr)*) + | Expr 'from' Expr + | (Expr (',' Expr)*)? + ) ')' XmlRootFn = @@ -276,7 +279,7 @@ XmlSerializeFn = Expr 'as' Type - ('no' 'ident' | 'ident')? + ('no' 'indent' | 'indent')? ')' XmlElementFn = @@ -289,7 +292,7 @@ XmlElementFn = ')' (',' (Expr (',' Expr)*))? | ',' (Expr (',' Expr)*) - ) + )? ')' ExprAsName = @@ -303,6 +306,7 @@ XmlForestFn = XmlExistsFn = 'xmlexists' '(' + Expr 'passing' XmlPassingMech? Expr @@ -362,7 +366,7 @@ JsonValueExpr = Expr JsonFormatClause? JsonKeyValue = - Expr ':' JsonValueExpr + Expr (':' | 'value') JsonValueExpr Gteq = '>' '=' @@ -1529,7 +1533,7 @@ JsonKeysUniqueClause = 'with' 'unique' 'keys' | 'without' 'unique' 'keys' JsonQuotesClause = - 'keep' 'quotes' | 'omit' 'quotes' + ('keep' | 'omit') 'quotes' ('on' 'scalar' 'string')? JsonBehaviorDefault = 'default' Expr @@ -1580,7 +1584,7 @@ JsonBehaviorClause = JsonBehavior JsonPassingArg = - Expr + Expr 'as' Name JsonPassingClause = 'passing' (JsonPassingArg (',' JsonPassingArg)*) @@ -1725,7 +1729,7 @@ FromClause = (JoinExpr (',' JoinExpr)*)? XmlTableColumnList = - (XmlTableColumn (',' XmlTableColumn)*) + 'columns' (XmlTableColumn (',' XmlTableColumn)*) XmlTableColumn = Name Type XmlColumnOptionList?