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
14 changes: 12 additions & 2 deletions crates/squawk_ide/src/column_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
));
}
Expand Down Expand Up @@ -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('<a/>', version '1.0')"), @"xml_root");
assert_snapshot!(name("xmlserialize(document '<a/>' as text)"), @"xml_serialize");
assert_snapshot!(name("xmlelement(name foo, 'bar')"), @"xml_element");
Expand Down
51 changes: 51 additions & 0 deletions crates/squawk_ide/src/goto_definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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("
Expand Down
201 changes: 99 additions & 102 deletions crates/squawk_ide/src/hover.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -43,109 +44,15 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option<String> {
}

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) {
Expand Down Expand Up @@ -222,6 +129,84 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option<String> {
None
}

fn hover_name_ref(
root: &SyntaxNode,
name_ref: &ast::NameRef,
binder: &binder::Binder,
) -> Option<String> {
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 {
Expand Down Expand Up @@ -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("
Expand Down
21 changes: 20 additions & 1 deletion crates/squawk_ide/src/resolve.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use rowan::TextSize;
use smallvec::{SmallVec, smallvec};
use squawk_syntax::{
SyntaxNode, SyntaxNodePtr,
SyntaxKind, SyntaxNode, SyntaxNodePtr,
ast::{self, AstNode, SelectVariant},
};

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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<SmallVec<[SyntaxNodePtr; 1]>> {
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,
Expand Down
Loading
Loading