flowscope_core/completion/
context.rs

1use sqlparser::keywords::Keyword;
2use sqlparser::tokenizer::{Token, TokenWithSpan, Tokenizer, Word};
3
4use crate::analyzer::helpers::line_col_to_offset;
5use crate::analyzer::schema_registry::SchemaRegistry;
6use crate::types::{
7    AstContext, CompletionClause, CompletionColumn, CompletionContext, CompletionItem,
8    CompletionItemCategory, CompletionItemKind, CompletionItemsResult, CompletionKeywordHints,
9    CompletionKeywordSet, CompletionRequest, CompletionTable, CompletionToken, CompletionTokenKind,
10    Dialect, SchemaMetadata, Span,
11};
12
13use super::ast_extractor::extract_ast_context;
14use super::parse_strategies::try_parse_for_completion;
15
16/// Maximum SQL input size (10MB) to prevent memory exhaustion.
17/// This matches the TypeScript validation limit.
18const MAX_SQL_LENGTH: usize = 10 * 1024 * 1024;
19
20// Scoring constants for completion item ranking.
21// Higher scores = higher priority in completion list.
22
23/// Bonus for column name prefix matches (when typing matches the column name portion of "table.column")
24const SCORE_COLUMN_NAME_MATCH_BONUS: i32 = 150;
25/// Bonus for items that are specific to the current clause context
26const SCORE_CLAUSE_SPECIFIC_BONUS: i32 = 50;
27/// Special boost for FROM keyword when typing 'f' in SELECT clause (most common transition)
28const SCORE_FROM_KEYWORD_BOOST: i32 = 800;
29/// Penalty for non-FROM keywords when typing 'f' in SELECT clause
30const SCORE_OTHER_KEYWORD_PENALTY: i32 = -200;
31/// Penalty for function names starting with 'f' to deprioritize vs FROM keyword
32const SCORE_F_FUNCTION_PENALTY: i32 = -250;
33/// Additional penalty for functions starting with 'from_' (e.g., from_json)
34const SCORE_FROM_FUNCTION_PENALTY: i32 = -300;
35
36#[derive(Debug, Clone)]
37struct TokenInfo {
38    token: Token,
39    span: Span,
40}
41
42#[derive(Debug, Clone)]
43struct StatementInfo {
44    index: usize,
45    span: Span,
46    tokens: Vec<TokenInfo>,
47}
48
49const GLOBAL_KEYWORDS: &[&str] = &[
50    "SELECT",
51    "FROM",
52    "WHERE",
53    "JOIN",
54    "LEFT",
55    "RIGHT",
56    "FULL",
57    "INNER",
58    "CROSS",
59    "OUTER",
60    "ON",
61    "USING",
62    "GROUP",
63    "BY",
64    "HAVING",
65    "ORDER",
66    "LIMIT",
67    "OFFSET",
68    "QUALIFY",
69    "WINDOW",
70    "INSERT",
71    "UPDATE",
72    "DELETE",
73    "CREATE",
74    "ALTER",
75    "DROP",
76    "VALUES",
77    "WITH",
78    "DISTINCT",
79    "UNION",
80    "INTERSECT",
81    "EXCEPT",
82    "ATTACH",
83    "DETACH",
84    "COPY",
85    "EXPORT",
86    "IMPORT",
87    "PIVOT",
88    "UNPIVOT",
89    "EXPLAIN",
90    "SUMMARIZE",
91    "DESCRIBE",
92    "SHOW",
93];
94
95const OPERATOR_HINTS: &[&str] = &[
96    "=", "!=", "<>", "<", "<=", ">", ">=", "+", "-", "*", "/", "%", "||", "AND", "OR", "NOT", "IN",
97    "LIKE", "ILIKE", "IS", "IS NOT", "BETWEEN",
98];
99
100const AGGREGATE_HINTS: &[&str] = &[
101    "COUNT",
102    "SUM",
103    "AVG",
104    "MIN",
105    "MAX",
106    "ARRAY_AGG",
107    "STRING_AGG",
108    "BOOL_AND",
109    "BOOL_OR",
110    "STDDEV",
111    "VARIANCE",
112];
113
114const SNIPPET_HINTS: &[&str] = &[
115    "CASE WHEN ... THEN ... END",
116    "COALESCE(expr, ...)",
117    "CAST(expr AS type)",
118    "COUNT(*)",
119    "FILTER (WHERE ...)",
120    "OVER (PARTITION BY ...)",
121];
122
123const SELECT_KEYWORDS: &[&str] = &[
124    "DISTINCT", "ALL", "AS", "CASE", "WHEN", "THEN", "ELSE", "END", "NULLIF", "COALESCE", "CAST",
125    "FILTER", "OVER",
126];
127
128const FROM_KEYWORDS: &[&str] = &[
129    "JOIN", "LEFT", "RIGHT", "FULL", "INNER", "CROSS", "OUTER", "LATERAL", "UNNEST", "AS", "ON",
130    "USING",
131];
132
133const WHERE_KEYWORDS: &[&str] = &[
134    "AND", "OR", "NOT", "IN", "EXISTS", "LIKE", "ILIKE", "IS", "NULL", "TRUE", "FALSE", "BETWEEN",
135];
136
137const GROUP_BY_KEYWORDS: &[&str] = &["HAVING", "ROLLUP", "CUBE", "GROUPING", "SETS"];
138
139const ORDER_BY_KEYWORDS: &[&str] = &["ASC", "DESC", "NULLS", "FIRST", "LAST"];
140
141const JOIN_KEYWORDS: &[&str] = &["ON", "USING"];
142
143fn keyword_set_for_clause(clause: CompletionClause) -> CompletionKeywordSet {
144    let keywords = match clause {
145        CompletionClause::Select => SELECT_KEYWORDS,
146        CompletionClause::From => FROM_KEYWORDS,
147        CompletionClause::Where | CompletionClause::On => WHERE_KEYWORDS,
148        CompletionClause::GroupBy => GROUP_BY_KEYWORDS,
149        CompletionClause::OrderBy => ORDER_BY_KEYWORDS,
150        CompletionClause::Join => JOIN_KEYWORDS,
151        CompletionClause::Limit => &["OFFSET"],
152        CompletionClause::Qualify => &["OVER", "WINDOW"],
153        CompletionClause::Window => &["PARTITION", "ORDER", "ROWS", "RANGE"],
154        CompletionClause::Insert => &["INTO", "VALUES", "SELECT"],
155        CompletionClause::Update => &["SET", "WHERE"],
156        CompletionClause::Delete => &["FROM", "WHERE"],
157        CompletionClause::With => &["AS", "SELECT"],
158        CompletionClause::Having => WHERE_KEYWORDS,
159        CompletionClause::Unknown => &[],
160    };
161
162    CompletionKeywordSet {
163        keywords: keywords.iter().map(|k| k.to_string()).collect(),
164        operators: OPERATOR_HINTS.iter().map(|op| op.to_string()).collect(),
165        aggregates: AGGREGATE_HINTS.iter().map(|agg| agg.to_string()).collect(),
166        snippets: SNIPPET_HINTS
167            .iter()
168            .map(|snippet| snippet.to_string())
169            .collect(),
170    }
171}
172
173fn global_keyword_set() -> CompletionKeywordSet {
174    CompletionKeywordSet {
175        keywords: GLOBAL_KEYWORDS.iter().map(|k| k.to_string()).collect(),
176        operators: OPERATOR_HINTS.iter().map(|op| op.to_string()).collect(),
177        aggregates: AGGREGATE_HINTS.iter().map(|agg| agg.to_string()).collect(),
178        snippets: SNIPPET_HINTS
179            .iter()
180            .map(|snippet| snippet.to_string())
181            .collect(),
182    }
183}
184
185fn token_span_to_offsets(sql: &str, span: &sqlparser::tokenizer::Span) -> Option<Span> {
186    let start = line_col_to_offset(sql, span.start.line as usize, span.start.column as usize)?;
187    let end = line_col_to_offset(sql, span.end.line as usize, span.end.column as usize)?;
188    Some(Span::new(start, end))
189}
190
191fn tokenize_sql(sql: &str, dialect: Dialect) -> Result<Vec<TokenInfo>, String> {
192    use sqlparser::tokenizer::Whitespace;
193
194    let dialect = dialect.to_sqlparser_dialect();
195    let mut tokenizer = Tokenizer::new(dialect.as_ref(), sql);
196    let tokens: Vec<TokenWithSpan> = tokenizer
197        .tokenize_with_location()
198        .map_err(|err| err.to_string())?;
199
200    let mut token_infos = Vec::new();
201    for token in tokens {
202        // Skip regular whitespace but keep comments for cursor detection
203        if let Token::Whitespace(ws) = &token.token {
204            match ws {
205                Whitespace::SingleLineComment { .. } | Whitespace::MultiLineComment(_) => {
206                    // Keep comment tokens
207                }
208                _ => continue, // Skip spaces, newlines, tabs
209            }
210        }
211        if let Some(span) = token_span_to_offsets(sql, &token.span) {
212            token_infos.push(TokenInfo {
213                token: token.token,
214                span,
215            });
216        }
217    }
218
219    Ok(token_infos)
220}
221
222/// Split tokenized SQL into statement boundaries.
223///
224/// Note: This is intentionally separate from `analyzer/input.rs::compute_statement_ranges`.
225/// That function operates on raw SQL text (for parsing before tokenization), while this
226/// function works with already-tokenized input and preserves per-statement token lists
227/// for clause detection and completion context building.
228fn split_statements(tokens: &[TokenInfo], sql_len: usize) -> Vec<StatementInfo> {
229    if tokens.is_empty() {
230        return vec![StatementInfo {
231            index: 0,
232            span: Span::new(0, sql_len),
233            tokens: Vec::new(),
234        }];
235    }
236
237    let mut statements = Vec::new();
238    let mut current_tokens = Vec::new();
239    let mut current_start: Option<usize> = None;
240    let mut statement_index = 0;
241
242    for token in tokens {
243        if current_start.is_none() {
244            current_start = Some(token.span.start);
245        }
246
247        if matches!(token.token, Token::SemiColon) {
248            let end = token.span.start;
249            if let Some(start) = current_start {
250                statements.push(StatementInfo {
251                    index: statement_index,
252                    span: Span::new(start, end.max(start)),
253                    tokens: current_tokens.clone(),
254                });
255                statement_index += 1;
256                current_tokens.clear();
257                current_start = None;
258            }
259            continue;
260        }
261
262        current_tokens.push(token.clone());
263    }
264
265    if let Some(start) = current_start {
266        let end = current_tokens
267            .last()
268            .map(|token| token.span.end)
269            .unwrap_or(start);
270        statements.push(StatementInfo {
271            index: statement_index,
272            span: Span::new(start, end.max(start)),
273            tokens: current_tokens,
274        });
275    }
276
277    statements
278}
279
280fn find_statement_for_cursor(statements: &[StatementInfo], cursor_offset: usize) -> StatementInfo {
281    if statements.is_empty() {
282        return StatementInfo {
283            index: 0,
284            span: Span::new(0, 0),
285            tokens: Vec::new(),
286        };
287    }
288
289    // Cursor is within a statement's bounds
290    for statement in statements {
291        if cursor_offset >= statement.span.start && cursor_offset <= statement.span.end {
292            return statement.clone();
293        }
294    }
295
296    // Cursor is between statements or after all statements - find the closest preceding statement
297    let mut candidate = &statements[0];
298    for statement in statements {
299        if cursor_offset < statement.span.start {
300            return candidate.clone();
301        }
302        candidate = statement;
303    }
304
305    // Cursor is after all statements - return the last one
306    candidate.clone()
307}
308
309fn keyword_from_token(token: &Token) -> Option<String> {
310    match token {
311        Token::Word(word) if word.keyword != Keyword::NoKeyword => Some(word.value.to_uppercase()),
312        _ => None,
313    }
314}
315
316fn is_identifier_word(word: &Word) -> bool {
317    word.quote_style.is_some() || word.keyword == Keyword::NoKeyword
318}
319
320fn detect_clause(tokens: &[TokenInfo], cursor_offset: usize) -> CompletionClause {
321    let mut clause = CompletionClause::Unknown;
322
323    for (index, token_info) in tokens.iter().enumerate() {
324        if token_info.span.start > cursor_offset {
325            break;
326        }
327
328        if let Some(keyword) = keyword_from_token(&token_info.token) {
329            match keyword.as_str() {
330                "SELECT" => clause = CompletionClause::Select,
331                "FROM" => clause = CompletionClause::From,
332                "WHERE" => clause = CompletionClause::Where,
333                "JOIN" => clause = CompletionClause::Join,
334                "ON" => clause = CompletionClause::On,
335                "HAVING" => clause = CompletionClause::Having,
336                "LIMIT" => clause = CompletionClause::Limit,
337                "QUALIFY" => clause = CompletionClause::Qualify,
338                "WINDOW" => clause = CompletionClause::Window,
339                "INSERT" => clause = CompletionClause::Insert,
340                "UPDATE" => clause = CompletionClause::Update,
341                "DELETE" => clause = CompletionClause::Delete,
342                "WITH" => clause = CompletionClause::With,
343                "GROUP" => {
344                    if let Some(next) = tokens.get(index + 1) {
345                        if keyword_from_token(&next.token).as_deref() == Some("BY") {
346                            clause = CompletionClause::GroupBy;
347                        }
348                    }
349                }
350                "ORDER" => {
351                    if let Some(next) = tokens.get(index + 1) {
352                        if keyword_from_token(&next.token).as_deref() == Some("BY") {
353                            clause = CompletionClause::OrderBy;
354                        }
355                    }
356                }
357                _ => {}
358            }
359        }
360    }
361
362    clause
363}
364
365fn token_kind(token: &Token) -> CompletionTokenKind {
366    use sqlparser::tokenizer::Whitespace;
367
368    match token {
369        Token::Word(word) => {
370            // Quoted identifiers (double quotes, backticks, brackets depending on dialect)
371            // should suppress completions when cursor is inside them
372            if word.quote_style.is_some() {
373                CompletionTokenKind::QuotedIdentifier
374            } else if word.keyword == Keyword::NoKeyword {
375                CompletionTokenKind::Identifier
376            } else {
377                CompletionTokenKind::Keyword
378            }
379        }
380        Token::Number(_, _)
381        | Token::SingleQuotedString(_)
382        | Token::DoubleQuotedString(_)
383        | Token::NationalStringLiteral(_)
384        | Token::EscapedStringLiteral(_)
385        | Token::HexStringLiteral(_) => CompletionTokenKind::Literal,
386        Token::Eq
387        | Token::Neq
388        | Token::Lt
389        | Token::Gt
390        | Token::LtEq
391        | Token::GtEq
392        | Token::Plus
393        | Token::Minus
394        | Token::Mul
395        | Token::Div
396        | Token::Mod
397        | Token::StringConcat => CompletionTokenKind::Operator,
398        Token::Comma
399        | Token::Period
400        | Token::LParen
401        | Token::RParen
402        | Token::SemiColon
403        | Token::LBracket
404        | Token::RBracket
405        | Token::LBrace
406        | Token::RBrace
407        | Token::Colon
408        | Token::DoubleColon
409        | Token::Assignment => CompletionTokenKind::Symbol,
410        // Comments (line and block)
411        Token::Whitespace(Whitespace::SingleLineComment { .. })
412        | Token::Whitespace(Whitespace::MultiLineComment(_)) => CompletionTokenKind::Comment,
413        _ => CompletionTokenKind::Unknown,
414    }
415}
416
417fn find_token_at_cursor(
418    tokens: &[TokenInfo],
419    cursor_offset: usize,
420    sql: &str,
421) -> Option<CompletionToken> {
422    for token in tokens {
423        if cursor_offset >= token.span.start && cursor_offset <= token.span.end {
424            let value = sql
425                .get(token.span.start..token.span.end)
426                .unwrap_or_default()
427                .to_string();
428            return Some(CompletionToken {
429                value,
430                kind: token_kind(&token.token),
431                span: token.span,
432            });
433        }
434    }
435    None
436}
437
438fn parse_tables(tokens: &[TokenInfo]) -> Vec<(String, Option<String>)> {
439    let mut tables = Vec::new();
440    let mut in_from_clause = false;
441    let mut expecting_table = false;
442    let mut index = 0;
443
444    while index < tokens.len() {
445        let token = &tokens[index].token;
446        let keyword = keyword_from_token(token);
447
448        if let Some(keyword) = keyword.as_deref() {
449            match keyword {
450                "FROM" => {
451                    in_from_clause = true;
452                    expecting_table = true;
453                    index += 1;
454                    continue;
455                }
456                "JOIN" => {
457                    expecting_table = true;
458                    index += 1;
459                    continue;
460                }
461                "WHERE" | "GROUP" | "ORDER" | "HAVING" | "LIMIT" | "QUALIFY" | "WINDOW" => {
462                    in_from_clause = false;
463                    expecting_table = false;
464                }
465                "UPDATE" | "INTO" => {
466                    expecting_table = true;
467                    index += 1;
468                    continue;
469                }
470                _ => {}
471            }
472        }
473
474        if in_from_clause && matches!(token, Token::Comma) {
475            expecting_table = true;
476            index += 1;
477            continue;
478        }
479
480        if !expecting_table {
481            index += 1;
482            continue;
483        }
484
485        if matches!(token, Token::LParen) {
486            let mut depth = 1;
487            index += 1;
488            while index < tokens.len() && depth > 0 {
489                match tokens[index].token {
490                    Token::LParen => depth += 1,
491                    Token::RParen => depth -= 1,
492                    _ => {}
493                }
494                index += 1;
495            }
496
497            let (alias, consumed) = parse_alias(tokens, index);
498            tables.push((String::new(), alias));
499            index = consumed;
500
501            expecting_table = false;
502            continue;
503        }
504
505        let (table_name, consumed) = match parse_table_name(tokens, index) {
506            Some(result) => result,
507            None => {
508                index += 1;
509                continue;
510            }
511        };
512
513        let (alias, consumed_alias) = parse_alias(tokens, consumed);
514        tables.push((table_name, alias));
515        index = consumed_alias;
516        expecting_table = false;
517    }
518
519    tables
520}
521
522fn parse_table_name(tokens: &[TokenInfo], start: usize) -> Option<(String, usize)> {
523    let mut parts = Vec::new();
524    let mut index = start;
525
526    loop {
527        let token = tokens.get(index)?;
528        match &token.token {
529            // Accept any word token in table name context.
530            // SQL keywords like PUBLIC, USER, TABLE are commonly used as schema/table names.
531            Token::Word(word) => {
532                parts.push(word.value.clone());
533                index += 1;
534            }
535            _ => break,
536        }
537
538        if matches!(tokens.get(index).map(|t| &t.token), Some(Token::Period)) {
539            index += 1;
540            continue;
541        }
542        break;
543    }
544
545    if parts.is_empty() {
546        None
547    } else {
548        Some((parts.join("."), index))
549    }
550}
551
552fn parse_alias(tokens: &[TokenInfo], start: usize) -> (Option<String>, usize) {
553    let mut index = start;
554
555    if let Some(token) = tokens.get(index) {
556        if keyword_from_token(&token.token).as_deref() == Some("AS") {
557            index += 1;
558        }
559    }
560
561    if let Some(token) = tokens.get(index) {
562        if let Token::Word(word) = &token.token {
563            if is_identifier_word(word) {
564                return (Some(word.value.clone()), index + 1);
565            }
566        }
567    }
568
569    (None, index)
570}
571
572fn build_columns(tables: &[CompletionTable], registry: &SchemaRegistry) -> Vec<CompletionColumn> {
573    let mut columns = Vec::new();
574    let mut column_counts = std::collections::HashMap::new();
575
576    for table in tables {
577        if table.canonical.is_empty() {
578            continue;
579        }
580        if let Some(entry) = registry.get(&table.canonical) {
581            for column in &entry.table.columns {
582                let normalized = registry.normalize_identifier(&column.name);
583                *column_counts.entry(normalized).or_insert(0usize) += 1;
584            }
585        }
586    }
587
588    for table in tables {
589        if table.canonical.is_empty() {
590            continue;
591        }
592        let table_label = table.alias.clone().unwrap_or_else(|| table.name.clone());
593        if let Some(entry) = registry.get(&table.canonical) {
594            for column in &entry.table.columns {
595                let normalized = registry.normalize_identifier(&column.name);
596                let is_ambiguous = column_counts.get(&normalized).copied().unwrap_or(0) > 1;
597                columns.push(CompletionColumn {
598                    name: column.name.clone(),
599                    data_type: column.data_type.clone(),
600                    table: Some(table_label.clone()),
601                    canonical_table: Some(table.canonical.clone()),
602                    is_ambiguous,
603                });
604            }
605        }
606    }
607
608    columns
609}
610
611fn token_list_for_statement(tokens: &[TokenInfo], span: &Span) -> Vec<TokenInfo> {
612    tokens
613        .iter()
614        .filter(|token| token.span.start >= span.start && token.span.end <= span.end)
615        .cloned()
616        .collect()
617}
618
619#[must_use]
620pub fn completion_context(request: &CompletionRequest) -> CompletionContext {
621    let sql = request.sql.as_str();
622    let sql_len = sql.len();
623
624    // Validate input size to prevent memory exhaustion
625    if sql_len > MAX_SQL_LENGTH {
626        return CompletionContext::from_error(format!(
627            "SQL exceeds maximum length of {} bytes ({} bytes provided)",
628            MAX_SQL_LENGTH, sql_len
629        ));
630    }
631
632    // Validate cursor_offset is within bounds and on a valid UTF-8 char boundary
633    if request.cursor_offset > sql_len {
634        return CompletionContext::from_error(format!(
635            "cursor_offset ({}) exceeds SQL length ({})",
636            request.cursor_offset, sql_len
637        ));
638    }
639    if !sql.is_char_boundary(request.cursor_offset) {
640        return CompletionContext::from_error(format!(
641            "cursor_offset ({}) does not land on a valid UTF-8 character boundary",
642            request.cursor_offset
643        ));
644    }
645
646    // SchemaRegistry::new returns (registry, issues) where issues contains schema validation
647    // warnings. We intentionally discard these for completion context since we want to
648    // provide completions even when schema metadata has minor issues.
649    let (registry, _schema_issues) = SchemaRegistry::new(request.schema.as_ref(), request.dialect);
650
651    let tokens = match tokenize_sql(sql, request.dialect) {
652        Ok(tokens) => tokens,
653        Err(_) => {
654            return CompletionContext::empty();
655        }
656    };
657
658    let statements = split_statements(&tokens, sql_len);
659    let statement = find_statement_for_cursor(&statements, request.cursor_offset);
660    let statement_tokens = if statement.tokens.is_empty() {
661        token_list_for_statement(&tokens, &statement.span)
662    } else {
663        statement.tokens.clone()
664    };
665
666    let clause = detect_clause(&statement_tokens, request.cursor_offset);
667    let token = find_token_at_cursor(&statement_tokens, request.cursor_offset, sql);
668
669    let tables_raw = parse_tables(&statement_tokens);
670    let mut tables = Vec::new();
671
672    for (name, alias) in tables_raw {
673        if name.is_empty() {
674            tables.push(CompletionTable {
675                name: name.clone(),
676                canonical: String::new(),
677                alias,
678                matched_schema: false,
679            });
680            continue;
681        }
682
683        let resolution = registry.canonicalize_table_reference(&name);
684        tables.push(CompletionTable {
685            name,
686            canonical: resolution.canonical,
687            alias,
688            matched_schema: resolution.matched_schema,
689        });
690    }
691
692    let columns = build_columns(&tables, &registry);
693
694    CompletionContext {
695        statement_index: statement.index,
696        statement_span: statement.span,
697        clause,
698        token,
699        tables_in_scope: tables,
700        columns_in_scope: columns,
701        keyword_hints: CompletionKeywordHints {
702            global: global_keyword_set(),
703            clause: keyword_set_for_clause(clause),
704        },
705        error: None,
706    }
707}
708
709fn clause_category_order(clause: CompletionClause) -> &'static [CompletionItemCategory] {
710    use CompletionItemCategory as Category;
711    match clause {
712        CompletionClause::Select => &[
713            Category::Column,
714            Category::Function,
715            Category::Aggregate,
716            Category::Table,
717            Category::Keyword,
718            Category::Operator,
719            Category::Snippet,
720            Category::SchemaTable,
721        ],
722        CompletionClause::From | CompletionClause::Join => &[
723            Category::Table,
724            Category::SchemaTable,
725            Category::Keyword,
726            Category::Column,
727            Category::Function,
728            Category::Operator,
729            Category::Aggregate,
730            Category::Snippet,
731        ],
732        CompletionClause::On
733        | CompletionClause::Where
734        | CompletionClause::Having
735        | CompletionClause::Qualify => &[
736            Category::Column,
737            Category::Operator,
738            Category::Function,
739            Category::Aggregate,
740            Category::Keyword,
741            Category::Table,
742            Category::SchemaTable,
743            Category::Snippet,
744        ],
745        CompletionClause::GroupBy | CompletionClause::OrderBy => &[
746            Category::Column,
747            Category::Function,
748            Category::Aggregate,
749            Category::Keyword,
750            Category::Table,
751            Category::SchemaTable,
752            Category::Operator,
753            Category::Snippet,
754        ],
755        CompletionClause::Limit => &[
756            Category::Keyword,
757            Category::Column,
758            Category::Function,
759            Category::Aggregate,
760            Category::Table,
761            Category::SchemaTable,
762            Category::Operator,
763            Category::Snippet,
764        ],
765        CompletionClause::Window => &[
766            Category::Function,
767            Category::Column,
768            Category::Keyword,
769            Category::Aggregate,
770            Category::Table,
771            Category::SchemaTable,
772            Category::Operator,
773            Category::Snippet,
774        ],
775        CompletionClause::Insert | CompletionClause::Update => &[
776            Category::Table,
777            Category::SchemaTable,
778            Category::Column,
779            Category::Keyword,
780            Category::Function,
781            Category::Operator,
782            Category::Aggregate,
783            Category::Snippet,
784        ],
785        CompletionClause::Delete => &[
786            Category::Table,
787            Category::SchemaTable,
788            Category::Keyword,
789            Category::Column,
790            Category::Function,
791            Category::Operator,
792            Category::Aggregate,
793            Category::Snippet,
794        ],
795        CompletionClause::With => &[
796            Category::Keyword,
797            Category::Table,
798            Category::SchemaTable,
799            Category::Column,
800            Category::Function,
801            Category::Operator,
802            Category::Aggregate,
803            Category::Snippet,
804        ],
805        CompletionClause::Unknown => &[
806            Category::Column,
807            Category::Table,
808            Category::SchemaTable,
809            Category::Keyword,
810            Category::Function,
811            Category::Operator,
812            Category::Aggregate,
813            Category::Snippet,
814        ],
815    }
816}
817
818fn category_score(clause: CompletionClause, category: CompletionItemCategory) -> i32 {
819    let order = clause_category_order(clause);
820    let index = order
821        .iter()
822        .position(|item| *item == category)
823        .unwrap_or(order.len());
824    1000 - (index as i32 * 100)
825}
826
827fn prefix_score(label: &str, token: &str) -> i32 {
828    if token.is_empty() {
829        return 0;
830    }
831    let normalized_label = label.to_lowercase();
832    if normalized_label == token {
833        return 300;
834    }
835    if normalized_label.starts_with(token) {
836        return 200;
837    }
838    if normalized_label.contains(token) {
839        return 100;
840    }
841    0
842}
843
844/// Extracts the column name portion from a potentially qualified label.
845///
846/// Used for prefix scoring to match user input against just the column name,
847/// even when the label includes a table qualifier for disambiguation.
848///
849/// # Examples
850/// - `"name"` → `"name"`
851/// - `"users.name"` → `"name"`
852/// - `"public.users.name"` → `"name"`
853fn column_name_from_label(label: &str) -> &str {
854    label.rsplit_once('.').map(|(_, col)| col).unwrap_or(label)
855}
856
857fn should_show_for_cursor(sql: &str, cursor_offset: usize, token_value: &str) -> bool {
858    if !token_value.is_empty() {
859        return true;
860    }
861    if cursor_offset == 0 || cursor_offset > sql.len() {
862        return false;
863    }
864    let bytes = sql.as_bytes();
865    let prev = bytes[cursor_offset.saturating_sub(1)];
866    let prev_char = prev as char;
867    if prev_char == '.' || prev_char == '(' || prev_char == ',' {
868        return true;
869    }
870    // Whitespace after SQL keywords is a valid completion position
871    // (e.g., "SELECT |" or "FROM |"). Return true to allow completions.
872    if prev_char.is_whitespace() {
873        return true;
874    }
875    true
876}
877
878/// Checks if a character is valid in an unquoted SQL identifier.
879///
880/// Currently only handles ASCII identifiers (alphanumeric, underscore, dollar sign).
881/// Note: Some SQL dialects support Unicode identifiers, but this function intentionally
882/// restricts to ASCII for consistent cross-dialect behavior. Quoted identifiers can
883/// still contain any Unicode characters.
884fn is_identifier_char(ch: char) -> bool {
885    ch.is_ascii_alphanumeric() || ch == '_' || ch == '$'
886}
887
888/// Extracts the last identifier from a SQL fragment.
889///
890/// Handles both quoted identifiers (e.g., `"My Table"`) and unquoted identifiers.
891/// Returns `None` if the source is empty or contains only non-identifier characters.
892///
893/// # Examples
894/// - `"SELECT users"` → `Some("users")`
895/// - `"\"My Table\""` → `Some("My Table")`
896/// - `"schema.table"` → `Some("table")`
897fn extract_last_identifier(source: &str) -> Option<String> {
898    let trimmed = source.trim_end();
899    if trimmed.is_empty() {
900        return None;
901    }
902
903    if let Some(stripped) = trimmed.strip_suffix('"') {
904        if let Some(start) = stripped.rfind('"') {
905            return Some(stripped[start + 1..].to_string());
906        }
907    }
908
909    let end = trimmed.len();
910    let mut start = end;
911    for (idx, ch) in trimmed.char_indices().rev() {
912        if is_identifier_char(ch) {
913            start = idx;
914        } else {
915            break;
916        }
917    }
918
919    if start == end {
920        None
921    } else {
922        Some(trimmed[start..end].to_string())
923    }
924}
925
926/// Extracts the qualifier (table alias or schema name) from SQL at the cursor position.
927///
928/// This function identifies when the user is typing after a dot (`.`), indicating
929/// they want completions scoped to a specific table, alias, or schema.
930///
931/// # Examples
932/// - `"users."` at offset 6 → `Some("users")` (trailing dot)
933/// - `"u.name"` at offset 6 → `Some("u")` (mid-token after dot)
934/// - `"SELECT"` at offset 6 → `None` (no qualifier)
935///
936/// # Safety
937/// Returns `None` if `cursor_offset` is out of bounds or not on a valid UTF-8 boundary.
938fn extract_qualifier(sql: &str, cursor_offset: usize) -> Option<String> {
939    if cursor_offset == 0 || cursor_offset > sql.len() {
940        return None;
941    }
942    // Ensure cursor_offset lands on a valid UTF-8 char boundary to prevent panic
943    if !sql.is_char_boundary(cursor_offset) {
944        return None;
945    }
946
947    let prefix = &sql[..cursor_offset];
948    let trimmed = prefix.trim_end();
949    if trimmed.is_empty() {
950        return None;
951    }
952
953    if let Some(stripped) = trimmed.strip_suffix('.') {
954        let before_dot = stripped.trim_end();
955        return extract_last_identifier(before_dot);
956    }
957
958    if let Some(dot_idx) = trimmed.rfind('.') {
959        let whitespace_idx = trimmed.rfind(|ch: char| ch.is_whitespace());
960        let dot_after_space = whitespace_idx.is_none_or(|space| dot_idx > space);
961        if dot_after_space {
962            let before_dot = trimmed[..dot_idx].trim_end();
963            return extract_last_identifier(before_dot);
964        }
965    }
966
967    None
968}
969
970fn build_columns_from_schema(
971    schema: &SchemaMetadata,
972    registry: &SchemaRegistry,
973) -> Vec<CompletionColumn> {
974    let mut columns = Vec::new();
975    let mut column_counts = std::collections::HashMap::new();
976
977    for table in &schema.tables {
978        for column in &table.columns {
979            let normalized = registry.normalize_identifier(&column.name);
980            *column_counts.entry(normalized).or_insert(0usize) += 1;
981        }
982    }
983
984    for table in &schema.tables {
985        let table_label = table.name.clone();
986        for column in &table.columns {
987            let normalized = registry.normalize_identifier(&column.name);
988            let is_ambiguous = column_counts.get(&normalized).copied().unwrap_or(0) > 1;
989            columns.push(CompletionColumn {
990                name: column.name.clone(),
991                data_type: column.data_type.clone(),
992                table: Some(table_label.clone()),
993                canonical_table: Some(table_label.clone()),
994                is_ambiguous,
995            });
996        }
997    }
998
999    columns
1000}
1001
1002fn build_columns_for_table(
1003    schema: &SchemaMetadata,
1004    registry: &SchemaRegistry,
1005    target_schema: Option<&str>,
1006    table_name: &str,
1007) -> Vec<CompletionColumn> {
1008    let normalized_target = registry.normalize_identifier(table_name);
1009    let mut columns = Vec::new();
1010
1011    for table in &schema.tables {
1012        let schema_matches = target_schema.is_none_or(|schema_name| {
1013            table
1014                .schema
1015                .as_ref()
1016                .map(|schema| {
1017                    registry.normalize_identifier(schema)
1018                        == registry.normalize_identifier(schema_name)
1019                })
1020                .unwrap_or(false)
1021        });
1022        if !schema_matches {
1023            continue;
1024        }
1025        if registry.normalize_identifier(&table.name) != normalized_target {
1026            continue;
1027        }
1028
1029        for column in &table.columns {
1030            columns.push(CompletionColumn {
1031                name: column.name.clone(),
1032                data_type: column.data_type.clone(),
1033                table: Some(table.name.clone()),
1034                canonical_table: Some(table.name.clone()),
1035                is_ambiguous: false,
1036            });
1037        }
1038    }
1039
1040    columns
1041}
1042
1043fn schema_tables_for_qualifier(
1044    schema: &SchemaMetadata,
1045    registry: &SchemaRegistry,
1046    qualifier: &str,
1047) -> Vec<(String, String)> {
1048    let normalized = registry.normalize_identifier(qualifier);
1049    let mut tables = Vec::new();
1050
1051    for table in &schema.tables {
1052        let schema_matches = table
1053            .schema
1054            .as_ref()
1055            .is_some_and(|table_schema| registry.normalize_identifier(table_schema) == normalized);
1056        let catalog_matches = table
1057            .catalog
1058            .as_ref()
1059            .is_some_and(|catalog| registry.normalize_identifier(catalog) == normalized);
1060
1061        if schema_matches {
1062            let label = match table.schema.as_ref() {
1063                Some(table_schema) => format!("{table_schema}.{}", table.name),
1064                None => table.name.clone(),
1065            };
1066            tables.push((label, table.name.clone()));
1067            continue;
1068        }
1069
1070        if catalog_matches {
1071            let label = match table.catalog.as_ref() {
1072                Some(catalog) => format!("{catalog}.{}", table.name),
1073                None => table.name.clone(),
1074            };
1075            tables.push((label, table.name.clone()));
1076        }
1077    }
1078
1079    tables
1080}
1081
1082#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1083enum QualifierTarget {
1084    ColumnLabel,
1085    SchemaTable,
1086    SchemaOnly,
1087}
1088
1089#[derive(Debug)]
1090struct QualifierResolution {
1091    target: QualifierTarget,
1092    label: Option<String>,
1093    schema: Option<String>,
1094    table: Option<String>,
1095}
1096
1097fn resolve_qualifier(
1098    qualifier: &str,
1099    tables: &[CompletionTable],
1100    schema: Option<&SchemaMetadata>,
1101    registry: &SchemaRegistry,
1102) -> Option<QualifierResolution> {
1103    let normalized = registry.normalize_identifier(qualifier);
1104
1105    for table in tables {
1106        if let Some(alias) = table.alias.as_ref() {
1107            if registry.normalize_identifier(alias) == normalized {
1108                return Some(QualifierResolution {
1109                    target: QualifierTarget::ColumnLabel,
1110                    label: Some(alias.clone()),
1111                    schema: None,
1112                    table: None,
1113                });
1114            }
1115        }
1116    }
1117
1118    let schema = schema?;
1119
1120    let schema_name = schema.tables.iter().find_map(|table| {
1121        table.schema.as_ref().and_then(|table_schema| {
1122            if registry.normalize_identifier(table_schema) == normalized {
1123                Some(table_schema.clone())
1124            } else {
1125                None
1126            }
1127        })
1128    });
1129    let catalog_name = schema.tables.iter().find_map(|table| {
1130        table.catalog.as_ref().and_then(|catalog| {
1131            if registry.normalize_identifier(catalog) == normalized {
1132                Some(catalog.clone())
1133            } else {
1134                None
1135            }
1136        })
1137    });
1138    let table_name_matches_schema = schema
1139        .tables
1140        .iter()
1141        .any(|table| registry.normalize_identifier(&table.name) == normalized);
1142
1143    if let Some(schema_name) = schema_name.as_ref() {
1144        if !table_name_matches_schema {
1145            return Some(QualifierResolution {
1146                target: QualifierTarget::SchemaOnly,
1147                label: None,
1148                schema: Some(schema_name.clone()),
1149                table: None,
1150            });
1151        }
1152    }
1153
1154    if let Some(catalog_name) = catalog_name.as_ref() {
1155        if !table_name_matches_schema {
1156            return Some(QualifierResolution {
1157                target: QualifierTarget::SchemaOnly,
1158                label: None,
1159                schema: Some(catalog_name.clone()),
1160                table: None,
1161            });
1162        }
1163    }
1164
1165    for table in tables {
1166        if registry.normalize_identifier(&table.name) == normalized {
1167            let label = table.alias.clone().unwrap_or_else(|| table.name.clone());
1168            return Some(QualifierResolution {
1169                target: QualifierTarget::ColumnLabel,
1170                label: Some(label),
1171                schema: None,
1172                table: None,
1173            });
1174        }
1175    }
1176
1177    for table in &schema.tables {
1178        if registry.normalize_identifier(&table.name) == normalized {
1179            return Some(QualifierResolution {
1180                target: QualifierTarget::SchemaTable,
1181                label: None,
1182                schema: table.schema.clone(),
1183                table: Some(table.name.clone()),
1184            });
1185        }
1186    }
1187
1188    if let Some(schema_name) = schema_name {
1189        return Some(QualifierResolution {
1190            target: QualifierTarget::SchemaOnly,
1191            label: None,
1192            schema: Some(schema_name),
1193            table: None,
1194        });
1195    }
1196
1197    None
1198}
1199
1200fn uppercase_keyword(value: &str) -> String {
1201    value.to_ascii_uppercase()
1202}
1203
1204/// Determines if completions should be suppressed in SELECT clause.
1205///
1206/// Suppresses completions when schema metadata suggests columns should exist
1207/// but we couldn't derive any for this context. This prevents showing misleading
1208/// keyword-only completions when the user expects column suggestions.
1209///
1210/// Returns `true` (suppress) in these cases:
1211/// - Schema is provided but contains no column metadata at all
1212/// - Schema has columns but none could be derived for the current scope
1213///
1214/// Returns `false` (show completions) when:
1215/// - Not in SELECT clause
1216/// - A qualifier is present (e.g., `users.`)
1217/// - Columns were successfully derived
1218/// - No schema metadata was provided
1219fn should_suppress_select_completions(
1220    clause: CompletionClause,
1221    has_qualifier: bool,
1222    columns_empty: bool,
1223    schema_provided: bool,
1224    schema_has_columns: bool,
1225) -> bool {
1226    // Only applies to SELECT clause without qualifier and no columns
1227    if clause != CompletionClause::Select || has_qualifier || !columns_empty {
1228        return false;
1229    }
1230
1231    // Suppress when schema is provided but has no column metadata
1232    if schema_provided && !schema_has_columns {
1233        return true;
1234    }
1235
1236    // Suppress when schema has columns but we couldn't derive any for this context
1237    if schema_has_columns {
1238        return true;
1239    }
1240
1241    false
1242}
1243
1244/// Generate completion items from a keyword set with the given clause_specific flag.
1245fn items_from_keyword_set(
1246    keyword_set: &CompletionKeywordSet,
1247    clause_specific: bool,
1248) -> Vec<CompletionItem> {
1249    let mut items = Vec::new();
1250
1251    for keyword in &keyword_set.keywords {
1252        let label = uppercase_keyword(keyword);
1253        items.push(CompletionItem {
1254            label: label.clone(),
1255            insert_text: label,
1256            kind: CompletionItemKind::Keyword,
1257            category: CompletionItemCategory::Keyword,
1258            score: 0,
1259            clause_specific,
1260            detail: None,
1261        });
1262    }
1263
1264    for operator in &keyword_set.operators {
1265        items.push(CompletionItem {
1266            label: operator.clone(),
1267            insert_text: operator.clone(),
1268            kind: CompletionItemKind::Operator,
1269            category: CompletionItemCategory::Operator,
1270            score: 0,
1271            clause_specific,
1272            detail: None,
1273        });
1274    }
1275
1276    for aggregate in &keyword_set.aggregates {
1277        let label = uppercase_keyword(aggregate);
1278        items.push(CompletionItem {
1279            label: label.clone(),
1280            insert_text: format!("{label}("),
1281            kind: CompletionItemKind::Function,
1282            category: CompletionItemCategory::Aggregate,
1283            score: 0,
1284            clause_specific,
1285            detail: None,
1286        });
1287    }
1288
1289    for snippet in &keyword_set.snippets {
1290        items.push(CompletionItem {
1291            label: snippet.clone(),
1292            insert_text: snippet.clone(),
1293            kind: CompletionItemKind::Snippet,
1294            category: CompletionItemCategory::Snippet,
1295            score: 0,
1296            clause_specific,
1297            detail: None,
1298        });
1299    }
1300
1301    items
1302}
1303
1304/// Try to parse SQL and extract AST context for enrichment.
1305/// Returns None if parsing fails - token-based completion will be used instead.
1306fn try_extract_ast_context(
1307    sql: &str,
1308    cursor_offset: usize,
1309    dialect: Dialect,
1310) -> Option<AstContext> {
1311    let parse_result = try_parse_for_completion(sql, cursor_offset, dialect)?;
1312    Some(extract_ast_context(&parse_result.statements))
1313}
1314
1315/// Enrich columns with CTE and subquery columns from AST context.
1316///
1317/// Uses a HashSet for O(1) deduplication instead of O(n²) iteration.
1318fn enrich_columns_from_ast(
1319    columns: &mut Vec<CompletionColumn>,
1320    tables: &[CompletionTable],
1321    ast_ctx: &AstContext,
1322) {
1323    use std::collections::HashSet;
1324
1325    // Build a set of existing (table, column) pairs for O(1) dedup lookups
1326    // Key: (lowercased_table_name, lowercased_column_name)
1327    let mut seen: HashSet<(String, String)> = columns
1328        .iter()
1329        .filter_map(|c| {
1330            c.table
1331                .as_ref()
1332                .map(|t| (t.to_lowercase(), c.name.to_lowercase()))
1333        })
1334        .collect();
1335
1336    // Add columns from CTEs
1337    for (cte_name, cte_info) in &ast_ctx.cte_definitions {
1338        // Check if this CTE is referenced in tables
1339        let cte_in_scope = tables.iter().any(|t| {
1340            t.name.eq_ignore_ascii_case(cte_name) || t.canonical.eq_ignore_ascii_case(cte_name)
1341        });
1342
1343        if cte_in_scope {
1344            // Use declared columns if available, otherwise use projected columns
1345            let cte_columns = if !cte_info.declared_columns.is_empty() {
1346                cte_info
1347                    .declared_columns
1348                    .iter()
1349                    .map(|name| CompletionColumn {
1350                        name: name.clone(),
1351                        table: Some(cte_name.clone()),
1352                        canonical_table: Some(cte_name.clone()),
1353                        data_type: None,
1354                        is_ambiguous: false,
1355                    })
1356                    .collect::<Vec<_>>()
1357            } else {
1358                cte_info
1359                    .projected_columns
1360                    .iter()
1361                    .filter(|c| c.name != "*") // Skip wildcards
1362                    .map(|col| CompletionColumn {
1363                        name: col.name.clone(),
1364                        table: Some(cte_name.clone()),
1365                        canonical_table: Some(cte_name.clone()),
1366                        data_type: col.data_type.clone(),
1367                        is_ambiguous: false,
1368                    })
1369                    .collect::<Vec<_>>()
1370            };
1371
1372            for col in cte_columns {
1373                let key = (cte_name.to_lowercase(), col.name.to_lowercase());
1374                if seen.insert(key) {
1375                    columns.push(col);
1376                }
1377            }
1378        }
1379    }
1380
1381    // Add columns from subquery aliases
1382    for (alias, subquery_info) in &ast_ctx.subquery_aliases {
1383        let subquery_in_scope = tables.iter().any(|t| {
1384            t.name.eq_ignore_ascii_case(alias)
1385                || t.alias
1386                    .as_ref()
1387                    .map(|a| a.eq_ignore_ascii_case(alias))
1388                    .unwrap_or(false)
1389        });
1390
1391        if subquery_in_scope {
1392            for col in &subquery_info.projected_columns {
1393                if col.name == "*" {
1394                    continue; // Skip wildcards
1395                }
1396
1397                let key = (alias.to_lowercase(), col.name.to_lowercase());
1398                if seen.insert(key) {
1399                    columns.push(CompletionColumn {
1400                        name: col.name.clone(),
1401                        table: Some(alias.clone()),
1402                        canonical_table: Some(alias.clone()),
1403                        data_type: col.data_type.clone(),
1404                        is_ambiguous: false,
1405                    });
1406                }
1407            }
1408        }
1409    }
1410}
1411
1412/// Enrich tables with CTE definitions from AST context.
1413fn enrich_tables_from_ast(tables: &mut Vec<CompletionTable>, ast_ctx: &AstContext) {
1414    // Add CTE definitions as completable tables
1415    for cte_name in ast_ctx.cte_definitions.keys() {
1416        if !tables.iter().any(|t| t.name.eq_ignore_ascii_case(cte_name)) {
1417            tables.push(CompletionTable {
1418                name: cte_name.clone(),
1419                canonical: cte_name.clone(),
1420                alias: None,
1421                matched_schema: false,
1422            });
1423        }
1424    }
1425}
1426
1427#[must_use]
1428pub fn completion_items(request: &CompletionRequest) -> CompletionItemsResult {
1429    let context = completion_context(request);
1430    if let Some(error) = context.error.clone() {
1431        return CompletionItemsResult {
1432            clause: context.clause,
1433            token: context.token,
1434            should_show: false,
1435            items: Vec::new(),
1436            error: Some(error),
1437        };
1438    }
1439
1440    let token_value = context
1441        .token
1442        .as_ref()
1443        .map(|token| token.value.trim().to_lowercase())
1444        .unwrap_or_default();
1445
1446    // Suppress completions when cursor is inside special tokens
1447    // (string literals, number literals, comments, quoted identifiers)
1448    if let Some(ref token) = context.token {
1449        let suppress_inside = matches!(
1450            token.kind,
1451            CompletionTokenKind::Literal
1452                | CompletionTokenKind::Comment
1453                | CompletionTokenKind::QuotedIdentifier
1454        );
1455        if suppress_inside
1456            && request.cursor_offset > token.span.start
1457            && request.cursor_offset < token.span.end
1458        {
1459            return CompletionItemsResult {
1460                clause: context.clause,
1461                token: context.token,
1462                should_show: false,
1463                items: Vec::new(),
1464                error: None,
1465            };
1466        }
1467    }
1468
1469    let should_show = should_show_for_cursor(&request.sql, request.cursor_offset, &token_value);
1470    if !should_show {
1471        return CompletionItemsResult {
1472            clause: context.clause,
1473            token: context.token,
1474            should_show,
1475            items: Vec::new(),
1476            error: None,
1477        };
1478    }
1479
1480    // SchemaRegistry::new returns (registry, issues). Issues are intentionally discarded
1481    // because completion should work even with schema validation warnings.
1482    let (registry, _schema_issues) = SchemaRegistry::new(request.schema.as_ref(), request.dialect);
1483    let qualifier = extract_qualifier(&request.sql, request.cursor_offset);
1484    let qualifier_resolution = qualifier.as_ref().and_then(|value| {
1485        resolve_qualifier(
1486            value,
1487            &context.tables_in_scope,
1488            request.schema.as_ref(),
1489            &registry,
1490        )
1491    });
1492    let restrict_to_columns = qualifier_resolution.is_some();
1493
1494    let mut items = Vec::new();
1495    let mut seen = std::collections::HashSet::new();
1496
1497    let mut push_item = |item: CompletionItem| {
1498        let key = format!("{:?}:{}:{}", item.category, item.label, item.insert_text);
1499        if seen.insert(key) {
1500            items.push(item);
1501        }
1502    };
1503
1504    if !restrict_to_columns {
1505        for item in items_from_keyword_set(&context.keyword_hints.clause, true) {
1506            push_item(item);
1507        }
1508        for item in items_from_keyword_set(&context.keyword_hints.global, false) {
1509            push_item(item);
1510        }
1511    }
1512
1513    let mut columns = context.columns_in_scope.clone();
1514    if columns.is_empty() && context.clause == CompletionClause::Select {
1515        if let Some(schema) = request.schema.as_ref() {
1516            columns = build_columns_from_schema(schema, &registry);
1517        }
1518    }
1519
1520    // Try AST-based enrichment for CTE and subquery columns
1521    let mut tables_enriched = context.tables_in_scope.clone();
1522    if let Some(ast_ctx) =
1523        try_extract_ast_context(&request.sql, request.cursor_offset, request.dialect)
1524    {
1525        // Enrich tables with CTE definitions
1526        enrich_tables_from_ast(&mut tables_enriched, &ast_ctx);
1527        // Enrich columns with CTE and subquery columns
1528        enrich_columns_from_ast(&mut columns, &tables_enriched, &ast_ctx);
1529    }
1530
1531    if let Some(resolution) = qualifier_resolution.as_ref() {
1532        match resolution.target {
1533            QualifierTarget::ColumnLabel => {
1534                if let Some(label) = resolution.label.as_ref() {
1535                    let normalized = registry.normalize_identifier(label);
1536                    columns.retain(|column| {
1537                        column
1538                            .table
1539                            .as_ref()
1540                            .map(|table| registry.normalize_identifier(table) == normalized)
1541                            .unwrap_or(false)
1542                    });
1543                }
1544            }
1545            QualifierTarget::SchemaTable => {
1546                columns = request
1547                    .schema
1548                    .as_ref()
1549                    .map(|schema| {
1550                        build_columns_for_table(
1551                            schema,
1552                            &registry,
1553                            resolution.schema.as_deref(),
1554                            resolution.table.as_deref().unwrap_or_default(),
1555                        )
1556                    })
1557                    .unwrap_or_default();
1558            }
1559            QualifierTarget::SchemaOnly => {
1560                columns.clear();
1561            }
1562        }
1563    }
1564
1565    let schema_has_columns = request
1566        .schema
1567        .as_ref()
1568        .map(|schema| schema.tables.iter().any(|table| !table.columns.is_empty()))
1569        .unwrap_or(false);
1570    let schema_provided = request.schema.is_some();
1571
1572    // Cache emptiness check before consuming columns to avoid clone during iteration
1573    let has_columns = !columns.is_empty();
1574
1575    if should_suppress_select_completions(
1576        context.clause,
1577        qualifier_resolution.is_some(),
1578        !has_columns,
1579        schema_provided,
1580        schema_has_columns,
1581    ) {
1582        return CompletionItemsResult {
1583            clause: context.clause,
1584            token: context.token,
1585            should_show: false,
1586            items: Vec::new(),
1587            error: None,
1588        };
1589    }
1590
1591    // Use into_iter() to take ownership of columns, avoiding clones where possible
1592    for column in columns {
1593        let (label, insert_text) = if restrict_to_columns {
1594            // Both label and insert_text are the column name
1595            let name = column.name;
1596            (name.clone(), name)
1597        } else if column.is_ambiguous {
1598            if let Some(table) = &column.table {
1599                let label = format!("{table}.{}", column.name);
1600                let insert_text = label.clone();
1601                (label, insert_text)
1602            } else {
1603                let name = column.name;
1604                (name.clone(), name)
1605            }
1606        } else {
1607            let name = column.name;
1608            (name.clone(), name)
1609        };
1610        push_item(CompletionItem {
1611            label,
1612            insert_text,
1613            kind: CompletionItemKind::Column,
1614            category: CompletionItemCategory::Column,
1615            score: 0,
1616            clause_specific: true,
1617            detail: column.data_type,
1618        });
1619    }
1620
1621    let schema_tables_only = qualifier_resolution
1622        .as_ref()
1623        .map(|resolution| resolution.target == QualifierTarget::SchemaOnly)
1624        .unwrap_or(false);
1625
1626    if schema_tables_only {
1627        if let Some(schema_name) = qualifier_resolution
1628            .as_ref()
1629            .and_then(|resolution| resolution.schema.as_deref())
1630        {
1631            if let Some(schema) = request.schema.as_ref() {
1632                for (label, insert_text) in
1633                    schema_tables_for_qualifier(schema, &registry, schema_name)
1634                {
1635                    push_item(CompletionItem {
1636                        label,
1637                        insert_text,
1638                        kind: CompletionItemKind::SchemaTable,
1639                        category: CompletionItemCategory::SchemaTable,
1640                        score: 0,
1641                        clause_specific: false,
1642                        detail: None,
1643                    });
1644                }
1645            }
1646        }
1647    }
1648
1649    let suppress_tables = restrict_to_columns
1650        || schema_tables_only
1651        || (context.clause == CompletionClause::Select && has_columns);
1652
1653    if !suppress_tables {
1654        for table in &tables_enriched {
1655            let label = table
1656                .alias
1657                .as_ref()
1658                .map(|alias| format!("{alias} ({})", table.name))
1659                .unwrap_or_else(|| table.name.clone());
1660            let insert_text = table.alias.clone().unwrap_or_else(|| table.name.clone());
1661            push_item(CompletionItem {
1662                label,
1663                insert_text,
1664                kind: CompletionItemKind::Table,
1665                category: CompletionItemCategory::Table,
1666                score: 0,
1667                clause_specific: true,
1668                detail: if table.canonical.is_empty() {
1669                    None
1670                } else {
1671                    Some(table.canonical.clone())
1672                },
1673            });
1674        }
1675
1676        if let Some(schema) = &request.schema {
1677            for table in &schema.tables {
1678                let label = match &table.schema {
1679                    Some(schema_name) => format!("{schema_name}.{}", table.name),
1680                    None => table.name.clone(),
1681                };
1682                let insert_text = label.clone();
1683                push_item(CompletionItem {
1684                    label,
1685                    insert_text,
1686                    kind: CompletionItemKind::SchemaTable,
1687                    category: CompletionItemCategory::SchemaTable,
1688                    score: 0,
1689                    clause_specific: false,
1690                    detail: None,
1691                });
1692            }
1693        }
1694    }
1695
1696    for item in items.iter_mut() {
1697        let base = category_score(context.clause, item.category);
1698        let prefix = prefix_score(&item.label, &token_value);
1699        let column_prefix = if item.category == CompletionItemCategory::Column {
1700            let column_name = column_name_from_label(&item.label);
1701            let column_score = prefix_score(column_name, &token_value);
1702            if column_score > 0 {
1703                column_score.saturating_add(SCORE_COLUMN_NAME_MATCH_BONUS)
1704            } else {
1705                0
1706            }
1707        } else {
1708            0
1709        };
1710        let clause_score = if item.clause_specific {
1711            SCORE_CLAUSE_SPECIFIC_BONUS
1712        } else {
1713            0
1714        };
1715        let mut special = 0;
1716        if context.clause == CompletionClause::Select && token_value.starts_with('f') {
1717            let label_lower = item.label.to_lowercase();
1718            if item.category == CompletionItemCategory::Keyword && label_lower == "from" {
1719                special = SCORE_FROM_KEYWORD_BOOST;
1720            } else if item.category == CompletionItemCategory::Keyword {
1721                special = SCORE_OTHER_KEYWORD_PENALTY;
1722            } else if item.kind == CompletionItemKind::Function && label_lower.starts_with("from_")
1723            {
1724                special = SCORE_FROM_FUNCTION_PENALTY;
1725            } else if item.kind == CompletionItemKind::Function && label_lower.starts_with('f') {
1726                special = SCORE_F_FUNCTION_PENALTY;
1727            }
1728        }
1729        let prefix_score = prefix.max(column_prefix);
1730        // Use saturating arithmetic to prevent overflow with extreme inputs
1731        item.score = base
1732            .saturating_add(prefix_score)
1733            .saturating_add(clause_score)
1734            .saturating_add(special);
1735    }
1736
1737    items.sort_by(|a, b| {
1738        b.score
1739            .cmp(&a.score)
1740            .then_with(|| a.label.to_lowercase().cmp(&b.label.to_lowercase()))
1741    });
1742
1743    CompletionItemsResult {
1744        clause: context.clause,
1745        token: context.token,
1746        should_show,
1747        items,
1748        error: None,
1749    }
1750}
1751
1752#[cfg(test)]
1753mod tests {
1754    use super::*;
1755    use crate::types::{
1756        ColumnSchema, CompletionClause, CompletionItemCategory, CompletionRequest, Dialect,
1757        SchemaMetadata, SchemaTable,
1758    };
1759
1760    #[test]
1761    fn test_completion_clause_detection() {
1762        let sql = "SELECT * FROM users WHERE ";
1763        let request = CompletionRequest {
1764            sql: sql.to_string(),
1765            dialect: Dialect::Duckdb,
1766            // Cursor at end of string (after trailing space)
1767            cursor_offset: sql.len(),
1768            schema: None,
1769        };
1770
1771        let context = completion_context(&request);
1772        assert_eq!(context.clause, CompletionClause::Where);
1773    }
1774
1775    #[test]
1776    fn test_completion_tables_and_columns() {
1777        let schema = SchemaMetadata {
1778            default_catalog: None,
1779            default_schema: Some("public".to_string()),
1780            search_path: None,
1781            case_sensitivity: None,
1782            allow_implied: true,
1783            tables: vec![
1784                SchemaTable {
1785                    catalog: None,
1786                    schema: Some("public".to_string()),
1787                    name: "users".to_string(),
1788                    columns: vec![
1789                        ColumnSchema {
1790                            name: "id".to_string(),
1791                            data_type: Some("integer".to_string()),
1792                            is_primary_key: None,
1793                            foreign_key: None,
1794                        },
1795                        ColumnSchema {
1796                            name: "name".to_string(),
1797                            data_type: Some("varchar".to_string()),
1798                            is_primary_key: None,
1799                            foreign_key: None,
1800                        },
1801                    ],
1802                },
1803                SchemaTable {
1804                    catalog: None,
1805                    schema: Some("public".to_string()),
1806                    name: "orders".to_string(),
1807                    columns: vec![
1808                        ColumnSchema {
1809                            name: "id".to_string(),
1810                            data_type: Some("integer".to_string()),
1811                            is_primary_key: None,
1812                            foreign_key: None,
1813                        },
1814                        ColumnSchema {
1815                            name: "user_id".to_string(),
1816                            data_type: Some("integer".to_string()),
1817                            is_primary_key: None,
1818                            foreign_key: None,
1819                        },
1820                    ],
1821                },
1822            ],
1823        };
1824
1825        let sql = "SELECT u. FROM users u JOIN orders o ON u.id = o.user_id";
1826        let cursor_offset = sql.find("u.").unwrap() + 2;
1827
1828        let request = CompletionRequest {
1829            sql: sql.to_string(),
1830            dialect: Dialect::Duckdb,
1831            cursor_offset,
1832            schema: Some(schema),
1833        };
1834
1835        let context = completion_context(&request);
1836        assert_eq!(context.tables_in_scope.len(), 2);
1837        assert!(context
1838            .columns_in_scope
1839            .iter()
1840            .any(|col| col.name == "name"));
1841        assert!(context
1842            .columns_in_scope
1843            .iter()
1844            .any(|col| col.name == "user_id"));
1845        assert!(context
1846            .columns_in_scope
1847            .iter()
1848            .any(|col| col.name == "id" && col.is_ambiguous));
1849    }
1850
1851    #[test]
1852    fn test_completion_items_respects_table_qualifier() {
1853        let schema = SchemaMetadata {
1854            default_catalog: None,
1855            default_schema: Some("public".to_string()),
1856            search_path: None,
1857            case_sensitivity: None,
1858            allow_implied: true,
1859            tables: vec![
1860                SchemaTable {
1861                    catalog: None,
1862                    schema: Some("public".to_string()),
1863                    name: "users".to_string(),
1864                    columns: vec![
1865                        ColumnSchema {
1866                            name: "id".to_string(),
1867                            data_type: Some("integer".to_string()),
1868                            is_primary_key: None,
1869                            foreign_key: None,
1870                        },
1871                        ColumnSchema {
1872                            name: "name".to_string(),
1873                            data_type: Some("varchar".to_string()),
1874                            is_primary_key: None,
1875                            foreign_key: None,
1876                        },
1877                    ],
1878                },
1879                SchemaTable {
1880                    catalog: None,
1881                    schema: Some("public".to_string()),
1882                    name: "orders".to_string(),
1883                    columns: vec![ColumnSchema {
1884                        name: "total".to_string(),
1885                        data_type: Some("integer".to_string()),
1886                        is_primary_key: None,
1887                        foreign_key: None,
1888                    }],
1889                },
1890            ],
1891        };
1892
1893        let sql = "SELECT u. FROM users u";
1894        let cursor_offset = sql.find("u.").unwrap() + 2;
1895
1896        let request = CompletionRequest {
1897            sql: sql.to_string(),
1898            dialect: Dialect::Duckdb,
1899            cursor_offset,
1900            schema: Some(schema),
1901        };
1902
1903        let result = completion_items(&request);
1904        assert!(result.should_show);
1905        assert!(result
1906            .items
1907            .iter()
1908            .all(|item| item.category == CompletionItemCategory::Column));
1909        assert!(result.items.iter().any(|item| item.label == "id"));
1910        assert!(!result.items.iter().any(|item| item.label == "total"));
1911    }
1912
1913    #[test]
1914    fn test_completion_items_select_prefers_columns_over_tables() {
1915        let schema = SchemaMetadata {
1916            default_catalog: None,
1917            default_schema: Some("public".to_string()),
1918            search_path: None,
1919            case_sensitivity: None,
1920            allow_implied: true,
1921            tables: vec![SchemaTable {
1922                catalog: None,
1923                schema: Some("public".to_string()),
1924                name: "users".to_string(),
1925                columns: vec![ColumnSchema {
1926                    name: "email".to_string(),
1927                    data_type: Some("varchar".to_string()),
1928                    is_primary_key: None,
1929                    foreign_key: None,
1930                }],
1931            }],
1932        };
1933
1934        let sql = "SELECT e";
1935        let cursor_offset = sql.len();
1936
1937        let request = CompletionRequest {
1938            sql: sql.to_string(),
1939            dialect: Dialect::Duckdb,
1940            cursor_offset,
1941            schema: Some(schema),
1942        };
1943
1944        let result = completion_items(&request);
1945        assert!(result.should_show);
1946        assert!(result
1947            .items
1948            .iter()
1949            .any(|item| item.category == CompletionItemCategory::Column));
1950        assert!(!result
1951            .items
1952            .iter()
1953            .any(|item| item.category == CompletionItemCategory::Table));
1954        assert!(!result
1955            .items
1956            .iter()
1957            .any(|item| item.category == CompletionItemCategory::SchemaTable));
1958    }
1959
1960    // Unit tests for string helper functions
1961
1962    #[test]
1963    fn test_extract_last_identifier_simple() {
1964        assert_eq!(extract_last_identifier("users"), Some("users".to_string()));
1965        assert_eq!(
1966            extract_last_identifier("foo_bar"),
1967            Some("foo_bar".to_string())
1968        );
1969        assert_eq!(
1970            extract_last_identifier("table123"),
1971            Some("table123".to_string())
1972        );
1973    }
1974
1975    #[test]
1976    fn test_extract_last_identifier_with_spaces() {
1977        assert_eq!(
1978            extract_last_identifier("SELECT users"),
1979            Some("users".to_string())
1980        );
1981        assert_eq!(extract_last_identifier("users "), Some("users".to_string()));
1982        assert_eq!(
1983            extract_last_identifier("  users  "),
1984            Some("users".to_string())
1985        );
1986    }
1987
1988    #[test]
1989    fn test_extract_last_identifier_quoted() {
1990        assert_eq!(
1991            extract_last_identifier("\"MyTable\""),
1992            Some("MyTable".to_string())
1993        );
1994        assert_eq!(
1995            extract_last_identifier("SELECT \"My Table\""),
1996            Some("My Table".to_string())
1997        );
1998        assert_eq!(
1999            extract_last_identifier("\"schema\".\"table\""),
2000            Some("table".to_string())
2001        );
2002    }
2003
2004    #[test]
2005    fn test_extract_last_identifier_empty() {
2006        assert_eq!(extract_last_identifier(""), None);
2007        assert_eq!(extract_last_identifier("   "), None);
2008        // Note: "SELECT " extracts "SELECT" because the function doesn't distinguish keywords
2009        assert_eq!(
2010            extract_last_identifier("SELECT "),
2011            Some("SELECT".to_string())
2012        );
2013        // Only punctuation/operators return None
2014        assert_eq!(extract_last_identifier("("), None);
2015        assert_eq!(extract_last_identifier(", "), None);
2016    }
2017
2018    #[test]
2019    fn test_extract_qualifier_with_trailing_dot() {
2020        assert_eq!(extract_qualifier("users.", 6), Some("users".to_string()));
2021        assert_eq!(extract_qualifier("SELECT u.", 9), Some("u".to_string()));
2022        assert_eq!(
2023            extract_qualifier("schema.table.", 13),
2024            Some("table".to_string())
2025        );
2026    }
2027
2028    #[test]
2029    fn test_extract_qualifier_mid_token() {
2030        assert_eq!(
2031            extract_qualifier("users.name", 10),
2032            Some("users".to_string())
2033        );
2034        assert_eq!(extract_qualifier("SELECT u.id", 11), Some("u".to_string()));
2035    }
2036
2037    #[test]
2038    fn test_extract_qualifier_no_qualifier() {
2039        assert_eq!(extract_qualifier("SELECT", 6), None);
2040        assert_eq!(extract_qualifier("users", 5), None);
2041        assert_eq!(extract_qualifier("", 0), None);
2042    }
2043
2044    #[test]
2045    fn test_extract_qualifier_cursor_at_start() {
2046        assert_eq!(extract_qualifier("users.name", 0), None);
2047    }
2048
2049    #[test]
2050    fn test_extract_qualifier_cursor_out_of_bounds() {
2051        assert_eq!(extract_qualifier("users", 100), None);
2052    }
2053
2054    #[test]
2055    fn test_extract_qualifier_utf8_boundary() {
2056        // Multi-byte UTF-8 character (emoji is 4 bytes)
2057        let sql = "SELECT 🎉.";
2058        // Cursor in middle of emoji (invalid boundary) should return None
2059        assert_eq!(extract_qualifier(sql, 8), None); // Middle of emoji
2060                                                     // Cursor after emoji + dot should work
2061        assert_eq!(extract_qualifier(sql, sql.len()), None); // 🎉 is not identifier char
2062    }
2063
2064    #[test]
2065    fn test_extract_qualifier_quoted_identifier() {
2066        assert_eq!(
2067            extract_qualifier("\"My Schema\".", 12),
2068            Some("My Schema".to_string())
2069        );
2070    }
2071
2072    // Unit tests for resolve_qualifier
2073
2074    #[test]
2075    fn test_resolve_qualifier_alias_match() {
2076        let tables = vec![CompletionTable {
2077            name: "users".to_string(),
2078            canonical: "public.users".to_string(),
2079            alias: Some("u".to_string()),
2080            matched_schema: true,
2081        }];
2082        let (registry, _) = SchemaRegistry::new(None, Dialect::Duckdb);
2083
2084        let result = resolve_qualifier("u", &tables, None, &registry);
2085        assert!(result.is_some());
2086        let resolution = result.unwrap();
2087        assert_eq!(resolution.target, QualifierTarget::ColumnLabel);
2088        assert_eq!(resolution.label, Some("u".to_string()));
2089    }
2090
2091    #[test]
2092    fn test_resolve_qualifier_table_name_match() {
2093        // When table is in tables_in_scope (without alias), qualifier matches table name
2094        // Note: Schema metadata is required for table name matching (vs just alias matching)
2095        let schema = SchemaMetadata {
2096            default_catalog: None,
2097            default_schema: Some("public".to_string()),
2098            search_path: None,
2099            case_sensitivity: None,
2100            allow_implied: true,
2101            tables: vec![SchemaTable {
2102                catalog: None,
2103                schema: Some("public".to_string()),
2104                name: "users".to_string(),
2105                columns: vec![],
2106            }],
2107        };
2108        let tables = vec![CompletionTable {
2109            name: "users".to_string(),
2110            canonical: "public.users".to_string(),
2111            alias: None,
2112            matched_schema: true,
2113        }];
2114        let (registry, _) = SchemaRegistry::new(Some(&schema), Dialect::Duckdb);
2115
2116        let result = resolve_qualifier("users", &tables, Some(&schema), &registry);
2117        assert!(
2118            result.is_some(),
2119            "Should match table name in tables_in_scope"
2120        );
2121        let resolution = result.unwrap();
2122        assert_eq!(resolution.target, QualifierTarget::ColumnLabel);
2123        // When no alias, label is the table name itself
2124        assert_eq!(resolution.label, Some("users".to_string()));
2125    }
2126
2127    #[test]
2128    fn test_resolve_qualifier_schema_only() {
2129        let schema = SchemaMetadata {
2130            default_catalog: None,
2131            default_schema: None,
2132            search_path: None,
2133            case_sensitivity: None,
2134            allow_implied: true,
2135            tables: vec![SchemaTable {
2136                catalog: None,
2137                schema: Some("myschema".to_string()),
2138                name: "mytable".to_string(),
2139                columns: vec![],
2140            }],
2141        };
2142        let (registry, _) = SchemaRegistry::new(Some(&schema), Dialect::Duckdb);
2143
2144        let result = resolve_qualifier("myschema", &[], Some(&schema), &registry);
2145        assert!(result.is_some());
2146        let resolution = result.unwrap();
2147        assert_eq!(resolution.target, QualifierTarget::SchemaOnly);
2148        assert_eq!(resolution.schema, Some("myschema".to_string()));
2149    }
2150
2151    #[test]
2152    fn test_resolve_qualifier_schema_table() {
2153        let schema = SchemaMetadata {
2154            default_catalog: None,
2155            default_schema: None,
2156            search_path: None,
2157            case_sensitivity: None,
2158            allow_implied: true,
2159            tables: vec![SchemaTable {
2160                catalog: None,
2161                schema: Some("public".to_string()),
2162                name: "users".to_string(),
2163                columns: vec![ColumnSchema {
2164                    name: "id".to_string(),
2165                    data_type: Some("integer".to_string()),
2166                    is_primary_key: None,
2167                    foreign_key: None,
2168                }],
2169            }],
2170        };
2171        let (registry, _) = SchemaRegistry::new(Some(&schema), Dialect::Duckdb);
2172
2173        // When qualifier matches a table name in schema (but not in tables_in_scope)
2174        let result = resolve_qualifier("users", &[], Some(&schema), &registry);
2175        assert!(result.is_some());
2176        let resolution = result.unwrap();
2177        assert_eq!(resolution.target, QualifierTarget::SchemaTable);
2178        assert_eq!(resolution.table, Some("users".to_string()));
2179    }
2180
2181    #[test]
2182    fn test_resolve_qualifier_no_match() {
2183        let (registry, _) = SchemaRegistry::new(None, Dialect::Duckdb);
2184        let result = resolve_qualifier("nonexistent", &[], None, &registry);
2185        assert!(result.is_none());
2186    }
2187
2188    #[test]
2189    fn test_resolve_qualifier_case_insensitive() {
2190        let tables = vec![CompletionTable {
2191            name: "Users".to_string(),
2192            canonical: "public.users".to_string(),
2193            alias: Some("U".to_string()),
2194            matched_schema: true,
2195        }];
2196        let (registry, _) = SchemaRegistry::new(None, Dialect::Duckdb);
2197
2198        // Should match case-insensitively
2199        let result = resolve_qualifier("u", &tables, None, &registry);
2200        assert!(result.is_some());
2201        assert_eq!(result.unwrap().target, QualifierTarget::ColumnLabel);
2202    }
2203
2204    // Test for column_name_from_label
2205
2206    #[test]
2207    fn test_column_name_from_label() {
2208        assert_eq!(column_name_from_label("name"), "name");
2209        assert_eq!(column_name_from_label("users.name"), "name");
2210        assert_eq!(column_name_from_label("public.users.name"), "name");
2211    }
2212
2213    // Tests for hybrid AST-based completion enrichment
2214
2215    #[test]
2216    fn test_cte_column_completion() {
2217        // Test that CTE columns appear in completion
2218        let sql = "WITH cte AS (SELECT id, name FROM users) SELECT cte. FROM cte";
2219        let cursor_offset = sql.find("cte.").unwrap() + 4; // Position after "cte."
2220
2221        let request = CompletionRequest {
2222            sql: sql.to_string(),
2223            dialect: Dialect::Generic,
2224            cursor_offset,
2225            schema: None,
2226        };
2227
2228        let result = completion_items(&request);
2229        assert!(result.should_show, "Should show completions after 'cte.'");
2230
2231        // Check that CTE columns are in the completion items
2232        let column_names: Vec<&str> = result
2233            .items
2234            .iter()
2235            .filter(|item| item.category == CompletionItemCategory::Column)
2236            .map(|item| item.label.as_str())
2237            .collect();
2238
2239        assert!(
2240            column_names.contains(&"id"),
2241            "Should have 'id' column from CTE. Columns found: {:?}",
2242            column_names
2243        );
2244        assert!(
2245            column_names.contains(&"name"),
2246            "Should have 'name' column from CTE. Columns found: {:?}",
2247            column_names
2248        );
2249    }
2250
2251    #[test]
2252    fn test_cte_with_declared_columns() {
2253        // Test CTE with explicit column declaration: WITH cte(a, b) AS (...)
2254        let sql = "WITH cte(x, y) AS (SELECT id, name FROM users) SELECT cte. FROM cte";
2255        let cursor_offset = sql.find("cte.").unwrap() + 4;
2256
2257        let request = CompletionRequest {
2258            sql: sql.to_string(),
2259            dialect: Dialect::Generic,
2260            cursor_offset,
2261            schema: None,
2262        };
2263
2264        let result = completion_items(&request);
2265        assert!(result.should_show);
2266
2267        let column_names: Vec<&str> = result
2268            .items
2269            .iter()
2270            .filter(|item| item.category == CompletionItemCategory::Column)
2271            .map(|item| item.label.as_str())
2272            .collect();
2273
2274        // Should use declared names (x, y) not projected names (id, name)
2275        assert!(
2276            column_names.contains(&"x"),
2277            "Should have declared column 'x'. Columns found: {:?}",
2278            column_names
2279        );
2280        assert!(
2281            column_names.contains(&"y"),
2282            "Should have declared column 'y'. Columns found: {:?}",
2283            column_names
2284        );
2285    }
2286
2287    #[test]
2288    fn test_subquery_alias_column_completion() {
2289        // Test that subquery alias columns appear in completion
2290        // Note: The cursor must be AFTER the FROM clause for AST parsing to include the subquery
2291        let sql = "SELECT * FROM (SELECT a, b FROM t) AS sub WHERE sub.";
2292        let cursor_offset = sql.len(); // Position at the end after "sub."
2293
2294        let request = CompletionRequest {
2295            sql: sql.to_string(),
2296            dialect: Dialect::Generic,
2297            cursor_offset,
2298            schema: None,
2299        };
2300
2301        let result = completion_items(&request);
2302        assert!(result.should_show, "Should show completions after 'sub.'");
2303
2304        let column_names: Vec<&str> = result
2305            .items
2306            .iter()
2307            .filter(|item| item.category == CompletionItemCategory::Column)
2308            .map(|item| item.label.as_str())
2309            .collect();
2310
2311        assert!(
2312            column_names.contains(&"a"),
2313            "Should have 'a' column from subquery. Columns found: {:?}",
2314            column_names
2315        );
2316        assert!(
2317            column_names.contains(&"b"),
2318            "Should have 'b' column from subquery. Columns found: {:?}",
2319            column_names
2320        );
2321    }
2322
2323    #[test]
2324    fn test_recursive_cte_column_completion() {
2325        // Test that recursive CTE base case columns appear in completion
2326        let sql = r#"
2327            WITH RECURSIVE cte AS (
2328                SELECT 1 AS n
2329                UNION ALL
2330                SELECT n + 1 FROM cte WHERE n < 10
2331            )
2332            SELECT cte. FROM cte
2333        "#;
2334        let cursor_offset = sql.find("cte.").unwrap() + 4;
2335
2336        let request = CompletionRequest {
2337            sql: sql.to_string(),
2338            dialect: Dialect::Generic,
2339            cursor_offset,
2340            schema: None,
2341        };
2342
2343        let result = completion_items(&request);
2344        assert!(result.should_show);
2345
2346        let column_names: Vec<&str> = result
2347            .items
2348            .iter()
2349            .filter(|item| item.category == CompletionItemCategory::Column)
2350            .map(|item| item.label.as_str())
2351            .collect();
2352
2353        assert!(
2354            column_names.contains(&"n"),
2355            "Should have 'n' column from recursive CTE base case. Columns found: {:?}",
2356            column_names
2357        );
2358    }
2359
2360    #[test]
2361    fn test_multiple_ctes_column_completion() {
2362        // Test completion with multiple CTEs
2363        let sql = r#"
2364            WITH
2365                users_cte AS (SELECT id, name FROM users),
2366                orders_cte AS (SELECT order_id, user_id FROM orders)
2367            SELECT users_cte. FROM users_cte, orders_cte
2368        "#;
2369        let cursor_offset = sql.find("users_cte.").unwrap() + 10;
2370
2371        let request = CompletionRequest {
2372            sql: sql.to_string(),
2373            dialect: Dialect::Generic,
2374            cursor_offset,
2375            schema: None,
2376        };
2377
2378        let result = completion_items(&request);
2379        assert!(result.should_show);
2380
2381        let column_names: Vec<&str> = result
2382            .items
2383            .iter()
2384            .filter(|item| item.category == CompletionItemCategory::Column)
2385            .map(|item| item.label.as_str())
2386            .collect();
2387
2388        // Should have columns from users_cte (the qualified table)
2389        assert!(
2390            column_names.contains(&"id"),
2391            "Should have 'id' column from users_cte. Columns found: {:?}",
2392            column_names
2393        );
2394        assert!(
2395            column_names.contains(&"name"),
2396            "Should have 'name' column from users_cte. Columns found: {:?}",
2397            column_names
2398        );
2399    }
2400}