Skip to content

Commit 2134c87

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

4 files changed

Lines changed: 209 additions & 35 deletions

File tree

src/ui/intellisense_context.rs

Lines changed: 86 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ pub struct CursorContext {
218218
pub qualifier: Option<String>,
219219
/// Resolved table names for the qualifier
220220
pub qualifier_tables: Vec<String>,
221+
/// Visible Oracle row sources that use a `VERSIONS ... BETWEEN` clause.
222+
/// Kept separately from catalog tables so completion can expose the
223+
/// `VERSIONS_*` pseudocolumns only through the owning row-source alias.
224+
oracle_flashback_versions_sources: Vec<ScopedTableRef>,
221225
/// The cursor introduces a brand-new DDL name or expects a data type
222226
/// (`ALTER TABLE t ADD <col> …`, `RENAME … TO <newname>`), where existing
223227
/// relations/columns/keywords are all irrelevant. Drives completion
@@ -229,6 +233,26 @@ pub struct CursorContext {
229233
pub(crate) with_plsql_awaiting_main_query_before_cursor_token: bool,
230234
}
231235

236+
impl CursorContext {
237+
pub(crate) fn qualifier_has_oracle_flashback_versions(&self, qualifier: &str) -> bool {
238+
resolve_qualifier_table_refs(qualifier, &self.tables_in_scope)
239+
.into_iter()
240+
.any(|resolved| {
241+
self.oracle_flashback_versions_sources.iter().any(|source| {
242+
resolved.depth == source.depth
243+
&& resolved.is_cte == source.is_cte
244+
&& normalize_identifier_for_lookup(&resolved.name)
245+
== normalize_identifier_for_lookup(&source.name)
246+
&& resolved
247+
.alias
248+
.as_deref()
249+
.map(normalize_identifier_for_lookup)
250+
== source.alias.as_deref().map(normalize_identifier_for_lookup)
251+
})
252+
})
253+
}
254+
}
255+
232256
/// CTE parsing state machine
233257
#[derive(Debug, Clone, Copy, PartialEq)]
234258
enum CteState {
@@ -367,6 +391,16 @@ pub(crate) fn analyze_cursor_context_arc(
367391
) -> CursorContext {
368392
let clamped_cursor_token_len = cursor_token_len.min(statement_tokens.len());
369393
let parse_result = scan_cursor_context(statement_tokens.as_ref(), clamped_cursor_token_len);
394+
let visible_scope_ids: HashSet<usize> =
395+
parse_result.visible_scope_chain.iter().copied().collect();
396+
let oracle_flashback_versions_sources = parse_result
397+
.parsed_tables
398+
.iter()
399+
.filter(|entry| {
400+
entry.has_oracle_flashback_versions && visible_scope_ids.contains(&entry.scope_id)
401+
})
402+
.map(|entry| entry.table.clone())
403+
.collect();
370404
let (active_query_range, in_set_operation_query_branch) = find_active_query_range(
371405
statement_tokens.as_ref(),
372406
&parse_result.parsed_ctes,
@@ -489,6 +523,7 @@ pub(crate) fn analyze_cursor_context_arc(
489523
focused_tables,
490524
qualifier: None,
491525
qualifier_tables: Vec::new(),
526+
oracle_flashback_versions_sources,
492527
ddl_new_name_position,
493528
with_plsql_awaiting_main_query_before_cursor_token: parse_result
494529
.with_plsql_awaiting_main_query_before_cursor_token,
@@ -2026,6 +2061,7 @@ struct ParsedTableEntry {
20262061
scope_id: usize,
20272062
token_start: usize,
20282063
origin: ParsedTableEntryOrigin,
2064+
has_oracle_flashback_versions: bool,
20292065
}
20302066

20312067
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -3335,6 +3371,7 @@ fn scan_cursor_context(tokens: &[SqlToken], cursor_token_len: usize) -> CursorSc
33353371
scope_id: parent_scope_id,
33363372
token_start: start_idx,
33373373
origin: ParsedTableEntryOrigin::DerivedAlias,
3374+
has_oracle_flashback_versions: false,
33383375
});
33393376
idx = next_idx;
33403377
close_parenthesis_scope(
@@ -3383,6 +3420,7 @@ fn scan_cursor_context(tokens: &[SqlToken], cursor_token_len: usize) -> CursorSc
33833420
scope_id: parent_scope_id,
33843421
token_start: start_idx,
33853422
origin: ParsedTableEntryOrigin::DerivedAlias,
3423+
has_oracle_flashback_versions: false,
33863424
});
33873425
if depth_frames
33883426
.get(depth.saturating_sub(1))
@@ -4979,10 +5017,11 @@ fn scan_cursor_context(tokens: &[SqlToken], cursor_token_len: usize) -> CursorSc
49795017
direct_after_alias,
49805018
direct_explicit_columns,
49815019
direct_explicit_column_range,
5020+
has_oracle_flashback_versions,
49825021
) = if allow_relation_alias {
49835022
parse_alias_deep_with_columns(tokens, relation_arg_end)
49845023
} else {
4985-
(None, relation_arg_end, Vec::new(), None)
5024+
(None, relation_arg_end, Vec::new(), None, false)
49865025
};
49875026
let (direct_alias, direct_after_alias) = sanitize_lock_table_alias(
49885027
tokens,
@@ -5078,6 +5117,7 @@ fn scan_cursor_context(tokens: &[SqlToken], cursor_token_len: usize) -> CursorSc
50785117
scope_id,
50795118
token_start: idx,
50805119
origin: ParsedTableEntryOrigin::Relation,
5120+
has_oracle_flashback_versions,
50815121
});
50825122
if matches!(current_phase, SqlPhase::FromClause) {
50835123
push_recent_relation_table(
@@ -6455,9 +6495,17 @@ fn push_bracket_identifier_part(inner: &mut String, part: &str, word_like: bool)
64556495
fn parse_alias_deep_with_columns(
64566496
tokens: &[SqlToken],
64576497
start: usize,
6458-
) -> (Option<String>, usize, Vec<String>, Option<TokenRange>) {
6459-
let start = skip_relation_postfix_clauses(tokens, start);
6460-
parse_relation_alias_at_with_columns(tokens, start, true)
6498+
) -> (Option<String>, usize, Vec<String>, Option<TokenRange>, bool) {
6499+
let (start, has_oracle_flashback_versions) = scan_relation_postfix_clauses(tokens, start);
6500+
let (alias, next_idx, explicit_columns, explicit_column_range) =
6501+
parse_relation_alias_at_with_columns(tokens, start, true);
6502+
(
6503+
alias,
6504+
next_idx,
6505+
explicit_columns,
6506+
explicit_column_range,
6507+
has_oracle_flashback_versions,
6508+
)
64616509
}
64626510

64636511
fn parse_alias_after_derived_relation_clauses(
@@ -6484,7 +6532,12 @@ fn parse_alias_after_derived_relation_clauses(
64846532
}
64856533

64866534
fn skip_relation_postfix_clauses(tokens: &[SqlToken], start: usize) -> usize {
6535+
scan_relation_postfix_clauses(tokens, start).0
6536+
}
6537+
6538+
fn scan_relation_postfix_clauses(tokens: &[SqlToken], start: usize) -> (usize, bool) {
64876539
let mut idx = skip_comment_tokens(tokens, start);
6540+
let mut has_oracle_flashback_versions = false;
64886541

64896542
loop {
64906543
let Some(SqlToken::Word(word)) = tokens.get(idx) else {
@@ -6645,6 +6698,7 @@ fn skip_relation_postfix_clauses(tokens: &[SqlToken], start: usize) -> usize {
66456698

66466699
idx = skip_flashback_bound_expression(tokens, and_idx + 1);
66476700
idx = skip_comment_tokens(tokens, idx);
6701+
has_oracle_flashback_versions = true;
66486702
continue;
66496703
}
66506704
"AS" => {
@@ -6678,7 +6732,7 @@ fn skip_relation_postfix_clauses(tokens: &[SqlToken], start: usize) -> usize {
66786732
}
66796733
}
66806734

6681-
idx
6735+
(idx, has_oracle_flashback_versions)
66826736
}
66836737

66846738
fn skip_mysql_index_hint_clause(tokens: &[SqlToken], start: usize) -> Option<usize> {
@@ -7448,23 +7502,22 @@ fn filter_visible_scope_entries_declared_before_cursor_with_pos(
74487502
.collect()
74497503
}
74507504

7451-
/// Resolve a qualifier only against row sources visible in the current scope.
7452-
pub fn resolve_qualifier_tables(
7505+
fn resolve_qualifier_table_refs<'a>(
74537506
qualifier: &str,
7454-
tables_in_scope: &[ScopedTableRef],
7455-
) -> Vec<String> {
7456-
fn unique_deepest_match(candidates: &[(usize, String)]) -> Vec<String> {
7457-
let Some(max_depth) = candidates.iter().map(|(depth, _)| *depth).max() else {
7507+
tables_in_scope: &'a [ScopedTableRef],
7508+
) -> Vec<&'a ScopedTableRef> {
7509+
fn unique_deepest_match(candidates: Vec<&ScopedTableRef>) -> Vec<&ScopedTableRef> {
7510+
let Some(max_depth) = candidates.iter().map(|table_ref| table_ref.depth).max() else {
74587511
return Vec::new();
74597512
};
74607513
let mut deepest = candidates
7461-
.iter()
7462-
.filter(|(depth, _)| *depth == max_depth)
7463-
.map(|(_, name)| name.clone())
7514+
.into_iter()
7515+
.filter(|table_ref| table_ref.depth == max_depth)
74647516
.collect::<Vec<_>>();
7465-
deepest.sort_by_key(|name| normalize_identifier_for_lookup(name));
7517+
deepest.sort_by_key(|table_ref| normalize_identifier_for_lookup(&table_ref.name));
74667518
deepest.dedup_by(|left, right| {
7467-
normalize_identifier_for_lookup(left) == normalize_identifier_for_lookup(right)
7519+
normalize_identifier_for_lookup(&left.name)
7520+
== normalize_identifier_for_lookup(&right.name)
74687521
});
74697522
if deepest.len() == 1 {
74707523
deepest
@@ -7486,37 +7539,48 @@ pub fn resolve_qualifier_tables(
74867539
.map(|a| normalize_identifier_for_lookup(a));
74877540

74887541
if alias_upper.as_deref() == Some(qualifier_upper.as_str()) {
7489-
alias_matches.push((table_ref.depth, table_ref.name.clone()));
7542+
alias_matches.push(table_ref);
74907543
continue;
74917544
}
74927545

74937546
if name_upper == qualifier_upper {
7494-
name_matches.push((table_ref.depth, table_ref.name.clone()));
7547+
name_matches.push(table_ref);
74957548
continue;
74967549
}
74977550

74987551
if last_identifier_part_for_lookup(&table_ref.name)
74997552
.is_some_and(|short| short.eq_ignore_ascii_case(&qualifier_upper))
75007553
{
7501-
short_name_matches.push((table_ref.depth, table_ref.name.clone()));
7554+
short_name_matches.push(table_ref);
75027555
}
75037556
}
75047557

75057558
if !alias_matches.is_empty() {
7506-
return unique_deepest_match(&alias_matches);
7559+
return unique_deepest_match(alias_matches);
75077560
}
75087561

75097562
if !name_matches.is_empty() {
7510-
return unique_deepest_match(&name_matches);
7563+
return unique_deepest_match(name_matches);
75117564
}
75127565

75137566
if !short_name_matches.is_empty() {
7514-
return unique_deepest_match(&short_name_matches);
7567+
return unique_deepest_match(short_name_matches);
75157568
}
75167569

75177570
Vec::new()
75187571
}
75197572

7573+
/// Resolve a qualifier only against row sources visible in the current scope.
7574+
pub fn resolve_qualifier_tables(
7575+
qualifier: &str,
7576+
tables_in_scope: &[ScopedTableRef],
7577+
) -> Vec<String> {
7578+
resolve_qualifier_table_refs(qualifier, tables_in_scope)
7579+
.into_iter()
7580+
.map(|table_ref| table_ref.name.clone())
7581+
.collect()
7582+
}
7583+
75207584
/// Resolve all table names from scope (for unqualified column suggestions).
75217585
pub fn resolve_all_scope_tables(tables_in_scope: &[ScopedTableRef]) -> Vec<String> {
75227586
let mut ordered_tables: Vec<(usize, &ScopedTableRef)> =

src/ui/intellisense_context/tests.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10483,6 +10483,7 @@ fn oracle_as_of_clause_preserves_alias_after_modifier() {
1048310483
let ctx = analyze("SELECT e.| FROM employees AS OF SCN 12345 e");
1048410484
let tables = resolve_qualifier_tables("e", &ctx.tables_in_scope);
1048510485
assert_eq!(tables, vec!["employees".to_string()]);
10486+
assert!(!ctx.qualifier_has_oracle_flashback_versions("e"));
1048610487
}
1048710488

1048810489
#[test]
@@ -10497,6 +10498,23 @@ fn oracle_versions_clause_preserves_alias_after_modifier() {
1049710498
let ctx = analyze("SELECT e.| FROM employees VERSIONS BETWEEN SCN MINVALUE AND MAXVALUE e");
1049810499
let tables = resolve_qualifier_tables("e", &ctx.tables_in_scope);
1049910500
assert_eq!(tables, vec!["employees".to_string()]);
10501+
assert!(ctx.qualifier_has_oracle_flashback_versions("e"));
10502+
}
10503+
10504+
#[test]
10505+
fn oracle_versions_metadata_follows_its_visible_alias_and_scope() {
10506+
let joined = analyze(
10507+
"SELECT d.| FROM employees VERSIONS BETWEEN SCN MINVALUE AND MAXVALUE e \
10508+
JOIN departments d ON e.department_id = d.department_id",
10509+
);
10510+
assert!(joined.qualifier_has_oracle_flashback_versions("e"));
10511+
assert!(!joined.qualifier_has_oracle_flashback_versions("d"));
10512+
10513+
let shadowed = analyze(
10514+
"SELECT * FROM employees VERSIONS BETWEEN SCN MINVALUE AND MAXVALUE e \
10515+
WHERE EXISTS (SELECT 1 FROM departments e WHERE e.|)",
10516+
);
10517+
assert!(!shadowed.qualifier_has_oracle_flashback_versions("e"));
1050010518
}
1050110519

1050210520
#[test]

src/ui/sql_editor/intellisense/completion.rs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5381,18 +5381,23 @@ impl SqlEditorWidget {
53815381
Some(snapshot.preferred_db_type),
53825382
)
53835383
{
5384-
for pseudo in [
5385-
"ROWID",
5386-
"ORA_ROWSCN",
5387-
// A `VERSIONS BETWEEN ...` row source also exposes the
5388-
// flashback version pseudo-columns through its alias.
5389-
"VERSIONS_STARTSCN",
5390-
"VERSIONS_STARTTIME",
5391-
"VERSIONS_ENDSCN",
5392-
"VERSIONS_ENDTIME",
5393-
"VERSIONS_XID",
5394-
"VERSIONS_OPERATION",
5395-
] {
5384+
let has_flashback_versions =
5385+
deep_ctx.qualifier_has_oracle_flashback_versions(qualifier);
5386+
let pseudocolumns: &[&str] = if has_flashback_versions {
5387+
&[
5388+
"ROWID",
5389+
"ORA_ROWSCN",
5390+
"VERSIONS_STARTSCN",
5391+
"VERSIONS_STARTTIME",
5392+
"VERSIONS_ENDSCN",
5393+
"VERSIONS_ENDTIME",
5394+
"VERSIONS_XID",
5395+
"VERSIONS_OPERATION",
5396+
]
5397+
} else {
5398+
&["ROWID", "ORA_ROWSCN"]
5399+
};
5400+
for &pseudo in pseudocolumns {
53965401
if crate::ui::intellisense::suggestion_matches_completion_prefix(
53975402
pseudo,
53985403
&snapshot.prefix,
@@ -7542,7 +7547,7 @@ impl SqlEditorWidget {
75427547
Self::merge_suggestions_with_context_aliases(
75437548
suggestions,
75447549
early_qualified_visible_relation_column_suggestions,
7545-
true,
7550+
false,
75467551
)
75477552
};
75487553
let suggestions = if early_oracle_graph_table_property_suggestions.is_empty() {

0 commit comments

Comments
 (0)