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
16const MAX_SQL_LENGTH: usize = 10 * 1024 * 1024;
19
20const SCORE_COLUMN_NAME_MATCH_BONUS: i32 = 150;
25const SCORE_CLAUSE_SPECIFIC_BONUS: i32 = 50;
27const SCORE_FROM_KEYWORD_BOOST: i32 = 800;
29const SCORE_OTHER_KEYWORD_PENALTY: i32 = -200;
31const SCORE_F_FUNCTION_PENALTY: i32 = -250;
33const 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 if let Token::Whitespace(ws) = &token.token {
204 match ws {
205 Whitespace::SingleLineComment { .. } | Whitespace::MultiLineComment(_) => {
206 }
208 _ => continue, }
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
222fn 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 for statement in statements {
291 if cursor_offset >= statement.span.start && cursor_offset <= statement.span.end {
292 return statement.clone();
293 }
294 }
295
296 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 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 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 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 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 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 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 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, ®istry);
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
844fn 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 if prev_char.is_whitespace() {
873 return true;
874 }
875 true
876}
877
878fn is_identifier_char(ch: char) -> bool {
885 ch.is_ascii_alphanumeric() || ch == '_' || ch == '$'
886}
887
888fn 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
926fn extract_qualifier(sql: &str, cursor_offset: usize) -> Option<String> {
939 if cursor_offset == 0 || cursor_offset > sql.len() {
940 return None;
941 }
942 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
1204fn 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 if clause != CompletionClause::Select || has_qualifier || !columns_empty {
1228 return false;
1229 }
1230
1231 if schema_provided && !schema_has_columns {
1233 return true;
1234 }
1235
1236 if schema_has_columns {
1238 return true;
1239 }
1240
1241 false
1242}
1243
1244fn 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
1304fn 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
1315fn enrich_columns_from_ast(
1319 columns: &mut Vec<CompletionColumn>,
1320 tables: &[CompletionTable],
1321 ast_ctx: &AstContext,
1322) {
1323 use std::collections::HashSet;
1324
1325 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 for (cte_name, cte_info) in &ast_ctx.cte_definitions {
1338 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 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 != "*") .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 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; }
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
1412fn enrich_tables_from_ast(tables: &mut Vec<CompletionTable>, ast_ctx: &AstContext) {
1414 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 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 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 ®istry,
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, ®istry);
1517 }
1518 }
1519
1520 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_from_ast(&mut tables_enriched, &ast_ctx);
1527 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 ®istry,
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 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 for column in columns {
1593 let (label, insert_text) = if restrict_to_columns {
1594 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, ®istry, 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 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_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 #[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 assert_eq!(
2010 extract_last_identifier("SELECT "),
2011 Some("SELECT".to_string())
2012 );
2013 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 let sql = "SELECT 🎉.";
2058 assert_eq!(extract_qualifier(sql, 8), None); assert_eq!(extract_qualifier(sql, sql.len()), None); }
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 #[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, ®istry);
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 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), ®istry);
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 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), ®istry);
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 let result = resolve_qualifier("users", &[], Some(&schema), ®istry);
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, ®istry);
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 let result = resolve_qualifier("u", &tables, None, ®istry);
2200 assert!(result.is_some());
2201 assert_eq!(result.unwrap().target, QualifierTarget::ColumnLabel);
2202 }
2203
2204 #[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 #[test]
2216 fn test_cte_column_completion() {
2217 let sql = "WITH cte AS (SELECT id, name FROM users) SELECT cte. FROM cte";
2219 let cursor_offset = sql.find("cte.").unwrap() + 4; 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 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 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 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 let sql = "SELECT * FROM (SELECT a, b FROM t) AS sub WHERE sub.";
2292 let cursor_offset = sql.len(); 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 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 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 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}