Skip to content

Commit f2dd0b7

Browse files
committed
인텔리센스 기능 개선
1 parent 2134c87 commit f2dd0b7

35 files changed

Lines changed: 3002 additions & 91 deletions

src/ui/intellisense_context.rs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2888,7 +2888,11 @@ impl RelationModifierState {
28882888
fn is_row_limiting_fetch_clause(tokens: &[SqlToken], fetch_idx: usize) -> bool {
28892889
matches!(
28902890
next_word_upper(tokens, fetch_idx + 1),
2891-
Some((next_keyword, _)) if matches!(next_keyword.as_str(), "FIRST" | "NEXT")
2891+
Some((next_keyword, _))
2892+
if matches!(
2893+
next_keyword.as_str(),
2894+
"APPROXIMATE" | "EXACT" | "FIRST" | "NEXT"
2895+
)
28922896
)
28932897
}
28942898

@@ -3341,7 +3345,7 @@ fn scan_cursor_context(tokens: &[SqlToken], cursor_token_len: usize) -> CursorSc
33413345
if let Some((
33423346
alias,
33433347
next_idx,
3344-
body_end,
3348+
derived_body_end,
33453349
explicit_columns,
33463350
explicit_column_range,
33473351
)) = parse_subquery_alias(tokens, idx + 1)
@@ -3355,7 +3359,7 @@ fn scan_cursor_context(tokens: &[SqlToken], cursor_token_len: usize) -> CursorSc
33553359
explicit_column_range,
33563360
body_range: TokenRange {
33573361
start: body_range.start,
3358-
end: body_end.max(body_range.end),
3362+
end: derived_body_end.unwrap_or(body_range.end),
33593363
},
33603364
depth: depth.saturating_sub(1),
33613365
},
@@ -3786,6 +3790,26 @@ fn scan_cursor_context(tokens: &[SqlToken], cursor_token_len: usize) -> CursorSc
37863790
idx += 1;
37873791
continue;
37883792
}
3793+
if matches!(upper.as_str(), "TABLE" | "THE")
3794+
&& matches!(current_phase, SqlPhase::FromClause)
3795+
&& relation_state.is_expect_table()
3796+
{
3797+
let open_idx = skip_comment_tokens(tokens, idx.saturating_add(1));
3798+
if matches!(tokens.get(open_idx), Some(SqlToken::Symbol(sym)) if sym == "(")
3799+
&& is_query_expression_start(tokens, open_idx.saturating_add(1))
3800+
{
3801+
// Oracle collection-query wrappers (`TABLE (SELECT …)` and
3802+
// legacy `THE (SELECT …)`) introduce a derived row source.
3803+
// Keep the pending relation expectation until `(` so the
3804+
// normal subquery tracker owns the body and its trailing
3805+
// alias. Otherwise THE/TABLE is consumed as a relation
3806+
// name and the first SELECT word is misparsed as its table.
3807+
last_word = Some(upper);
3808+
relation_state.expect_table();
3809+
idx += 1;
3810+
continue;
3811+
}
3812+
}
37893813
if !(relation_modifier_state.blocks_outer_scope_cutoff()
37903814
&& relation_state.is_expect_table())
37913815
{
@@ -7310,7 +7334,13 @@ fn matching_open_paren_before_close(tokens: &[SqlToken], close_idx: usize) -> Op
73107334
fn parse_subquery_alias(
73117335
tokens: &[SqlToken],
73127336
start: usize,
7313-
) -> Option<(String, usize, usize, Vec<String>, Option<TokenRange>)> {
7337+
) -> Option<(
7338+
String,
7339+
usize,
7340+
Option<usize>,
7341+
Vec<String>,
7342+
Option<TokenRange>,
7343+
)> {
73147344
let mut idx = start;
73157345
// An alias belongs to this derived relation only when it follows the closing
73167346
// parenthesis in the same scope. Never step across another `)`: that token
@@ -7326,14 +7356,15 @@ fn parse_subquery_alias(
73267356

73277357
idx = skip_relation_postfix_clauses(tokens, idx);
73287358
let body_end = skip_derived_relation_postfix_clauses(tokens, idx);
7359+
let derived_body_end = (body_end > idx).then_some(body_end);
73297360
let alias_start = skip_relation_postfix_clauses(tokens, body_end);
73307361
let (alias, next_idx, explicit_columns, explicit_column_range) =
73317362
parse_relation_alias_at_with_columns(tokens, alias_start, true);
73327363
alias.map(|name| {
73337364
(
73347365
name,
73357366
next_idx,
7336-
body_end,
7367+
derived_body_end,
73377368
explicit_columns,
73387369
explicit_column_range,
73397370
)

src/ui/intellisense_context/tests.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10556,6 +10556,49 @@ fn extract_select_list_columns_supports_three_part_identifier_in_inline_view() {
1055610556
);
1055710557
}
1055810558

10559+
#[test]
10560+
fn collection_query_wrappers_keep_inner_relation_scope() {
10561+
for wrapper in ["TABLE", "THE"] {
10562+
let sql = format!(
10563+
"SELECT legacy_owner.COLUMN_VALUE \
10564+
FROM {wrapper} (\
10565+
SELECT bag_owner.nums \
10566+
FROM sq_hard_bag bag_owner \
10567+
WHERE bag_owner.|\
10568+
) legacy_owner"
10569+
);
10570+
let ctx = analyze(&sql);
10571+
10572+
assert_eq!(ctx.phase, SqlPhase::WhereClause, "wrapper={wrapper}");
10573+
assert_eq!(
10574+
resolve_qualifier_tables("bag_owner", &ctx.tables_in_scope),
10575+
vec!["sq_hard_bag".to_string()],
10576+
"wrapper={wrapper}, tables={:?}",
10577+
ctx.tables_in_scope
10578+
);
10579+
}
10580+
}
10581+
10582+
#[test]
10583+
fn fromless_lateral_subquery_body_excludes_enclosing_close_parenthesis() {
10584+
let ctx = analyze(
10585+
"SELECT sample_owner.| \
10586+
FROM LATERAL (SELECT make_point(route_path) AS point_value) sample_owner",
10587+
);
10588+
let subquery = ctx
10589+
.subqueries
10590+
.iter()
10591+
.find(|subquery| subquery.alias.eq_ignore_ascii_case("sample_owner"))
10592+
.expect("LATERAL subquery alias");
10593+
let body = token_range_slice(ctx.statement_tokens.as_ref(), subquery.body_range);
10594+
10595+
assert_eq!(extract_select_list_columns(body), vec!["point_value"]);
10596+
assert!(
10597+
!matches!(body.last(), Some(SqlToken::Symbol(symbol)) if symbol == ")"),
10598+
"subquery body must not contain its enclosing close parenthesis: {body:?}"
10599+
);
10600+
}
10601+
1055910602
// ─── DDL column-name context (CREATE INDEX / ALTER TABLE) ────────────────────
1056010603

1056110604
#[test]

src/ui/sql_editor/formatter.rs

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7879,25 +7879,7 @@ impl SqlEditorWidget {
78797879
{
78807880
return true;
78817881
}
7882-
if format_stack.construct_flag_is_active(ConstructFlagKind::GrantRevokeActive)
7883-
&& matches!(
7884-
keyword,
7885-
"SELECT"
7886-
| "INSERT"
7887-
| "UPDATE"
7888-
| "DELETE"
7889-
| "EXECUTE"
7890-
| "ALTER"
7891-
| "DROP"
7892-
| "CREATE"
7893-
| "INDEX"
7894-
| "REFERENCES"
7895-
| "ALL"
7896-
| "PRIVILEGES"
7897-
| "MERGE"
7898-
| "ON"
7899-
)
7900-
{
7882+
if format_stack.construct_flag_is_active(ConstructFlagKind::GrantRevokeActive) {
79017883
return true;
79027884
}
79037885
if format_stack.construct_flag_is_active(ConstructFlagKind::CreateSequenceActive)
@@ -31059,6 +31041,7 @@ ORDER BY v.amt DESC;"#;
3105931041
#[cfg(test)]
3106031042
mod format_indent_gap_tests {
3106131043
use super::FormatIndentedParenOwnerKind;
31044+
use crate::db::connection::DatabaseType;
3106231045
use crate::sql_delimiter::line_start_snapshot_before_token;
3106331046
use crate::ui::sql_editor::SqlEditorWidget;
3106431047
use crate::ui::SqlToken;
@@ -31712,6 +31695,29 @@ recursive_tree (node_id, parent_id, node_name, DEPTH, PATH) AS (
3171231695
);
3171331696
}
3171431697

31698+
#[test]
31699+
fn format_sql_basic_keeps_grant_revoke_clause_keywords_inline() {
31700+
let source = "grant set user on *.* to 'actor'@'localhost'; revoke select on schema1.t1 from 'actor'@'localhost';";
31701+
let formatted =
31702+
SqlEditorWidget::format_sql_basic_for_db_type(source, DatabaseType::MariaDB);
31703+
31704+
assert!(
31705+
formatted.contains("GRANT SET USER ON *.* TO 'actor'@'localhost';"),
31706+
"a multi-word privilege must stay in the GRANT frame, got:\n{}",
31707+
formatted
31708+
);
31709+
assert!(
31710+
formatted.contains("REVOKE SELECT ON schema1.t1 FROM 'actor'@'localhost';"),
31711+
"FROM must stay in the REVOKE frame, got:\n{}",
31712+
formatted
31713+
);
31714+
assert_eq!(
31715+
SqlEditorWidget::format_sql_basic_for_db_type(&formatted, DatabaseType::MariaDB),
31716+
formatted,
31717+
"GRANT/REVOKE account statements must be idempotent"
31718+
);
31719+
}
31720+
3171531721
// ── MERGE WHEN MATCHED indent ──
3171631722

3171731723
#[test]

0 commit comments

Comments
 (0)