@@ -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 ) ]
234258enum 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)
64556495fn 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
64636511fn parse_alias_after_derived_relation_clauses (
@@ -6484,7 +6532,12 @@ fn parse_alias_after_derived_relation_clauses(
64846532}
64856533
64866534fn 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
66846738fn 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).
75217585pub fn resolve_all_scope_tables ( tables_in_scope : & [ ScopedTableRef ] ) -> Vec < String > {
75227586 let mut ordered_tables: Vec < ( usize , & ScopedTableRef ) > =
0 commit comments