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?