1use crate::parser::{ParseState, Schema};
2use crate::recursive_parser::{detect_cursor_context, CursorContext, LogicalOp};
3
4#[derive(Debug, Clone)]
5pub struct CursorAwareParser {
6 schema: Schema,
7}
8
9#[derive(Debug)]
10pub struct ParseResult {
11 pub suggestions: Vec<String>,
12 pub context: String,
13 pub partial_word: Option<String>,
14}
15
16impl CursorAwareParser {
17 pub fn new() -> Self {
18 Self {
19 schema: Schema::new(),
20 }
21 }
22
23 pub fn set_schema(&mut self, schema: Schema) {
24 self.schema = schema;
25 }
26
27 pub fn update_single_table(&mut self, table_name: String, columns: Vec<String>) {
28 self.schema.set_single_table(table_name, columns);
29 }
30
31 pub fn get_table_columns(&self, table_name: &str) -> Vec<String> {
32 self.schema.get_columns(table_name)
33 }
34
35 pub fn get_completions(&self, query: &str, cursor_pos: usize) -> ParseResult {
36 let (cursor_context, partial_word) = detect_cursor_context(query, cursor_pos);
38
39 let partial_word = partial_word.or_else(|| self.extract_word_at_cursor(query, cursor_pos));
41
42 let default_table = self.schema.get_first_table_name().unwrap_or("trade_deal");
43
44 let (suggestions, context_str) = match &cursor_context {
45 CursorContext::SelectClause => {
46 let mut cols = self.schema.get_columns(default_table);
48 cols.push("*".to_string());
49
50 cols.extend(vec![
52 "ROUND(".to_string(),
53 "ABS(".to_string(),
54 "FLOOR(".to_string(),
55 "CEILING(".to_string(),
56 "CEIL(".to_string(),
57 "MOD(".to_string(),
58 "QUOTIENT(".to_string(),
59 "POWER(".to_string(),
60 "POW(".to_string(),
61 "SQRT(".to_string(),
62 "EXP(".to_string(),
63 "LN(".to_string(),
64 "LOG(".to_string(),
65 "LOG10(".to_string(),
66 "PI(".to_string(),
67 "TEXTJOIN(".to_string(),
68 "DATEDIFF(".to_string(),
69 "DATEADD(".to_string(),
70 "NOW(".to_string(),
71 "TODAY(".to_string(),
72 ]);
73
74 (cols, "SelectClause".to_string())
80 }
81 CursorContext::FromClause => {
82 let tables = self.schema.get_table_names();
83 (tables, "FromClause".to_string())
84 }
85 CursorContext::WhereClause | CursorContext::AfterLogicalOp(_) => {
86 let mut suggestions = self.schema.get_columns(default_table);
88
89 suggestions.extend(vec![
91 "ROUND(".to_string(),
92 "ABS(".to_string(),
93 "FLOOR(".to_string(),
94 "CEILING(".to_string(),
95 "CEIL(".to_string(),
96 "MOD(".to_string(),
97 "QUOTIENT(".to_string(),
98 "POWER(".to_string(),
99 "POW(".to_string(),
100 "SQRT(".to_string(),
101 "EXP(".to_string(),
102 "LN(".to_string(),
103 "LOG(".to_string(),
104 "LOG10(".to_string(),
105 "PI(".to_string(),
106 "TEXTJOIN(".to_string(),
107 "DATEDIFF(".to_string(),
108 "DATEADD(".to_string(),
109 "NOW(".to_string(),
110 "TODAY(".to_string(),
111 ]);
112
113 let add_keywords = if let Some(ref partial) = partial_word {
115 let partial_lower = partial.to_lowercase();
116 !suggestions
117 .iter()
118 .any(|col| col.to_lowercase().starts_with(&partial_lower))
119 } else {
120 true
121 };
122
123 if add_keywords {
124 suggestions.extend(vec![
125 "AND".to_string(),
126 "OR".to_string(),
127 "IN".to_string(),
128 "ORDER BY".to_string(),
129 ]);
130 }
131
132 let ctx = match &cursor_context {
133 CursorContext::AfterLogicalOp(LogicalOp::And) => "AfterAND",
134 CursorContext::AfterLogicalOp(LogicalOp::Or) => "AfterOR",
135 _ => "WhereClause",
136 };
137 (suggestions, ctx.to_string())
138 }
139 CursorContext::AfterColumn(col_name) => {
140 let property_type = self
142 .get_property_type(col_name)
143 .unwrap_or("string".to_string());
144 let suggestions = self.get_string_method_suggestions(&property_type, &partial_word);
145 (suggestions, "AfterColumn".to_string())
146 }
147 CursorContext::AfterComparisonOp(col_name, op) => {
148 let property_type = self
150 .get_property_type(&col_name)
151 .unwrap_or("string".to_string());
152 let suggestions = match property_type.as_str() {
153 "datetime" => {
154 let mut suggestions = vec!["DateTime(".to_string()];
156 suggestions.extend(vec![
158 "DateTime.Today".to_string(),
159 "DateTime.Now".to_string(),
160 ]);
161 suggestions
162 }
163 "string" => {
164 vec!["''".to_string()]
166 }
167 "numeric" => {
168 vec![]
170 }
171 _ => vec![],
172 };
173 (suggestions, format!("AfterComparison({} {})", col_name, op))
174 }
175 CursorContext::InMethodCall(obj, method) => {
176 let property_type = self.get_property_type(obj).unwrap_or("string".to_string());
177 let suggestions = self.get_string_method_suggestions(&property_type, &partial_word);
178 (suggestions, format!("InMethodCall({}.{})", obj, method))
179 }
180 CursorContext::InExpression => {
181 let mut suggestions = self.schema.get_columns(default_table);
183
184 suggestions.extend(vec![
186 "ROUND(".to_string(),
187 "ABS(".to_string(),
188 "FLOOR(".to_string(),
189 "CEILING(".to_string(),
190 "CEIL(".to_string(),
191 "MOD(".to_string(),
192 "QUOTIENT(".to_string(),
193 "POWER(".to_string(),
194 "POW(".to_string(),
195 "SQRT(".to_string(),
196 "EXP(".to_string(),
197 "LN(".to_string(),
198 "LOG(".to_string(),
199 "LOG10(".to_string(),
200 "PI(".to_string(),
201 "AND".to_string(),
202 "OR".to_string(),
203 ]);
204 (suggestions, "InExpression".to_string())
205 }
206 CursorContext::OrderByClause => {
207 let mut suggestions = Vec::new();
209
210 let selected_columns = self.extract_selected_columns(query, query.len());
212
213 if !selected_columns.is_empty() && !selected_columns.contains(&"*".to_string()) {
215 suggestions.extend(selected_columns);
216 } else {
217 suggestions.extend(self.schema.get_columns(default_table));
220 }
221
222 suggestions.extend(vec!["ASC".to_string(), "DESC".to_string()]);
224 (suggestions, "OrderByClause".to_string())
225 }
226 CursorContext::Unknown => {
227 let safe_cursor_pos = self.find_safe_boundary(query, cursor_pos.min(query.len()));
230 let query_before_cursor = &query[..safe_cursor_pos];
231 let context = self.determine_context(query_before_cursor);
232 let suggestions = self.get_suggestions_for_context(&context, &partial_word, query);
233 return ParseResult {
234 suggestions,
235 context: format!("{:?} (partial: {:?})", context, partial_word),
236 partial_word,
237 };
238 }
239 };
240
241 let mut final_suggestions = suggestions;
243 let is_method_context = matches!(
244 cursor_context,
245 CursorContext::AfterColumn(_)
246 | CursorContext::InMethodCall(_, _)
247 | CursorContext::AfterComparisonOp(_, _)
248 );
249
250 if let Some(ref partial) = partial_word {
251 if !is_method_context {
252 final_suggestions.retain(|suggestion| {
254 if partial.starts_with('"') {
256 let partial_without_quote = &partial[1..]; if suggestion.starts_with('"')
261 && suggestion.ends_with('"')
262 && suggestion.len() > 2
263 {
264 let suggestion_without_quotes = &suggestion[1..suggestion.len() - 1];
266 suggestion_without_quotes
267 .to_lowercase()
268 .starts_with(&partial_without_quote.to_lowercase())
269 } else if suggestion.starts_with('"') && suggestion.len() > 1 {
270 let suggestion_without_quote = &suggestion[1..];
272 suggestion_without_quote
273 .to_lowercase()
274 .starts_with(&partial_without_quote.to_lowercase())
275 } else {
276 suggestion
278 .to_lowercase()
279 .starts_with(&partial_without_quote.to_lowercase())
280 }
281 } else {
282 let suggestion_to_check = if suggestion.starts_with('"')
285 && suggestion.ends_with('"')
286 && suggestion.len() > 2
287 {
288 &suggestion[1..suggestion.len() - 1]
290 } else if suggestion.starts_with('"') && suggestion.len() > 1 {
291 &suggestion[1..]
293 } else {
294 suggestion
295 };
296
297 suggestion_to_check
299 .to_lowercase()
300 .starts_with(&partial.to_lowercase())
301 }
302 });
303 }
304 }
305
306 ParseResult {
307 suggestions: final_suggestions,
308 context: format!("{} (partial: {:?})", context_str, partial_word),
309 partial_word,
310 }
311 }
312
313 fn extract_word_at_cursor(&self, query: &str, cursor_pos: usize) -> Option<String> {
314 if cursor_pos == 0 || cursor_pos > query.len() {
315 return None;
316 }
317
318 let chars: Vec<char> = query.chars().collect();
319
320 let mut start = cursor_pos;
322 let mut end = cursor_pos;
323
324 while start > 0 && Self::is_word_char(chars.get(start - 1).copied().unwrap_or(' ')) {
326 start -= 1;
327 }
328
329 while end < chars.len() && Self::is_word_char(chars.get(end).copied().unwrap_or(' ')) {
331 end += 1;
332 }
333
334 if start < end {
336 let partial: String = chars[start..cursor_pos.min(end)].iter().collect();
338 if !partial.is_empty() {
339 Some(partial)
340 } else {
341 None
342 }
343 } else {
344 None
345 }
346 }
347
348 fn is_word_char(ch: char) -> bool {
349 ch.is_alphanumeric() || ch == '_'
350 }
351
352 fn determine_context(&self, query_before_cursor: &str) -> ParseState {
353 let query_upper = query_before_cursor.to_uppercase();
354
355 let trimmed = query_before_cursor.trim();
358 let upper_trimmed = trimmed.to_uppercase();
362 let ends_with_and_or = upper_trimmed.ends_with(" AND") ||
363 upper_trimmed.ends_with(" OR") ||
364 upper_trimmed.ends_with(" AND ") || upper_trimmed.ends_with(" OR "); let words_check: Vec<&str> = query_upper.split_whitespace().collect();
369 let last_word_is_and_or = words_check
370 .last()
371 .map(|w| *w == "AND" || *w == "OR")
372 .unwrap_or(false);
373
374 if ends_with_and_or || last_word_is_and_or {
375 if query_upper.contains("WHERE") {
377 return ParseState::InWhere;
379 }
380 }
381
382 let words: Vec<&str> = query_upper.split_whitespace().collect();
383
384 if words.is_empty() {
385 return ParseState::Start;
386 }
387
388 let mut last_keyword_idx = None;
390 let mut last_keyword = "";
391
392 for (i, word) in words.iter().enumerate() {
393 match *word {
394 "SELECT" => {
395 last_keyword_idx = Some(i);
396 last_keyword = "SELECT";
397 }
398 "FROM" => {
399 last_keyword_idx = Some(i);
400 last_keyword = "FROM";
401 }
402 "WHERE" => {
403 last_keyword_idx = Some(i);
404 last_keyword = "WHERE";
405 }
406 "AND" | "OR" => {
407 if last_keyword == "WHERE" {
409 last_keyword_idx = Some(i);
410 last_keyword = "WHERE"; }
412 }
413 "IN" => {
414 if last_keyword == "WHERE" {
416 last_keyword_idx = Some(i);
417 last_keyword = "WHERE";
418 }
419 }
420 "ORDER" => {
421 if i + 1 < words.len() && words[i + 1] == "BY" {
423 last_keyword_idx = Some(i);
424 last_keyword = "ORDER BY";
425 }
426 }
427 _ => {}
428 }
429 }
430
431 match last_keyword {
432 "SELECT" => {
433 if let Some(idx) = last_keyword_idx {
434 let tokens_after_select = words.len() - idx - 1;
436 if tokens_after_select == 0 {
437 ParseState::AfterSelect
438 } else {
439 if words[(idx + 1)..].contains(&"FROM") {
441 ParseState::AfterTable } else {
443 ParseState::InColumnList
444 }
445 }
446 } else {
447 ParseState::AfterSelect
448 }
449 }
450 "FROM" => {
451 if let Some(idx) = last_keyword_idx {
452 let tokens_after_from = words.len() - idx - 1;
453 if tokens_after_from == 0 {
454 ParseState::AfterFrom
455 } else {
456 ParseState::AfterTable
457 }
458 } else {
459 ParseState::AfterFrom
460 }
461 }
462 "WHERE" => ParseState::InWhere,
463 "ORDER BY" => ParseState::InOrderBy,
464 _ => {
465 if query_upper.contains("SELECT")
467 && query_upper.contains("FROM")
468 && query_upper.contains("WHERE")
469 {
470 ParseState::InWhere
471 } else if query_upper.contains("SELECT") && query_upper.contains("FROM") {
472 ParseState::AfterTable
473 } else if query_upper.contains("SELECT") {
474 ParseState::InColumnList
475 } else {
476 ParseState::Start
477 }
478 }
479 }
480 }
481
482 fn get_suggestions_for_context(
483 &self,
484 context: &ParseState,
485 partial_word: &Option<String>,
486 query: &str,
487 ) -> Vec<String> {
488 let default_table = self.schema.get_first_table_name().unwrap_or("trade_deal");
489
490 let mut suggestions = match context {
491 ParseState::Start => vec!["SELECT".to_string()],
492 ParseState::AfterSelect => {
493 let mut cols = self.schema.get_columns(default_table);
494 cols.push("*".to_string());
495 cols
496 }
497 ParseState::InColumnList => {
498 let mut cols = self.schema.get_columns(default_table);
499 cols.push("FROM".to_string());
500 cols
501 }
502 ParseState::AfterFrom => self.schema.get_table_names(),
503 ParseState::AfterTable => {
504 vec!["WHERE".to_string(), "ORDER BY".to_string()]
505 }
506 ParseState::InWhere => {
507 let mut suggestions = self.schema.get_columns(default_table);
509
510 let add_keywords = if let Some(partial) = partial_word {
512 let partial_lower = partial.to_lowercase();
513 let matching_columns = suggestions
514 .iter()
515 .any(|col| col.to_lowercase().starts_with(&partial_lower));
516 !matching_columns } else {
518 true };
520
521 if add_keywords {
522 suggestions.extend(vec![
523 "AND".to_string(),
524 "OR".to_string(),
525 "IN".to_string(),
526 "ORDER BY".to_string(),
527 ]);
528 }
529
530 suggestions
531 }
532 ParseState::InOrderBy => {
533 let mut suggestions = Vec::new();
534
535 let selected_columns = self.extract_selected_columns(query, query.len());
537
538 if !selected_columns.is_empty() && !selected_columns.contains(&"*".to_string()) {
540 suggestions.extend(selected_columns);
541 } else {
542 suggestions.extend(self.schema.get_columns(default_table));
544 }
545
546 suggestions.extend(vec!["ASC".to_string(), "DESC".to_string()]);
548 suggestions
549 }
550 _ => vec![],
551 };
552
553 if let Some(partial) = partial_word {
555 suggestions.retain(|suggestion| {
556 suggestion
557 .to_lowercase()
558 .starts_with(&partial.to_lowercase())
559 });
560 }
561
562 suggestions
563 }
564
565 fn extract_selected_columns(&self, query: &str, cursor_pos: usize) -> Vec<String> {
566 let mut selected_columns = Vec::new();
568
569 let query_upper = query.to_uppercase();
571 if let Some(select_pos) = query_upper.find("SELECT") {
572 let end_pos = query_upper
574 .find("FROM")
575 .unwrap_or(cursor_pos)
576 .min(cursor_pos);
577
578 if select_pos + 6 < end_pos {
580 let select_clause = &query[(select_pos + 6)..end_pos];
581
582 for part in select_clause.split(',') {
584 let trimmed = part.trim();
585 if !trimmed.is_empty() {
586 let col_name = if trimmed.starts_with('"') {
588 if let Some(close_quote_pos) = trimmed[1..].find('"') {
590 &trimmed[..close_quote_pos + 2]
592 } else {
593 trimmed
595 }
596 } else {
597 if let Some(space_pos) = trimmed.find(char::is_whitespace) {
599 &trimmed[..space_pos]
600 } else {
601 trimmed
602 }
603 };
604
605 selected_columns.push(col_name.to_string());
607 }
608 }
609 }
610 }
611
612 selected_columns
613 }
614
615 fn detect_method_call_context(
616 &self,
617 query_before_cursor: &str,
618 _cursor_pos: usize,
619 ) -> Option<(String, String)> {
620 if let Some(dot_pos) = query_before_cursor.rfind('.') {
626 let text_after_dot = &query_before_cursor[dot_pos + 1..];
629
630 if text_after_dot.contains(')')
633 && (text_after_dot.contains(" AND ")
634 || text_after_dot.contains(" OR ")
635 || text_after_dot.trim().ends_with(" AND")
636 || text_after_dot.trim().ends_with(" OR"))
637 {
638 return None; }
640
641 let before_dot = &query_before_cursor[..dot_pos];
643
644 let mut property_start = dot_pos;
646 let chars: Vec<char> = before_dot.chars().collect();
647
648 while property_start > 0 {
649 let char_pos = property_start - 1;
650 if char_pos < chars.len() {
651 let ch = chars[char_pos];
652 if ch.is_alphanumeric() || ch == '_' {
653 property_start -= 1;
654 } else {
655 break;
656 }
657 } else {
658 break;
659 }
660 }
661
662 if property_start < dot_pos {
663 let property_name = before_dot[property_start..].trim().to_string();
664
665 if let Some(property_type) = self.get_property_type(&property_name) {
667 return Some((property_name, property_type));
668 }
669 }
670 }
671
672 None
673 }
674
675 fn get_property_type(&self, property_name: &str) -> Option<String> {
676 let property_lower = property_name.to_lowercase();
680
681 let string_properties = [
683 "platformorderid",
684 "dealid",
685 "externalorderid",
686 "parentorderid",
687 "instrumentid",
688 "instrumentname",
689 "instrumenttype",
690 "isin",
691 "cusip",
692 "ticker",
693 "exchange",
694 "counterparty",
695 "counterpartyid",
696 "counterpartytype",
697 "counterpartycountry",
698 "trader",
699 "portfolio",
700 "strategy",
701 "desk",
702 "status",
703 "confirmationstatus",
704 "settlementstatus",
705 "allocationstatus",
706 "currency",
707 "side",
708 "producttype",
709 "venue",
710 "clearinghouse",
711 "prime",
712 "comments",
713 "book",
714 "source",
715 "sourcesystem",
716 ];
717
718 let numeric_properties = [
720 "price",
721 "quantity",
722 "notional",
723 "commission",
724 "accrual",
725 "netamount",
726 "accruedinterest",
727 "grossamount",
728 "settlementamount",
729 "fees",
730 "tax",
731 ];
732
733 let datetime_properties = [
735 "tradedate",
736 "settlementdate",
737 "createddate",
738 "modifieddate",
739 "valuedate",
740 "maturitydate",
741 "confirmationdate",
742 "executiondate",
743 "lastmodifieddate",
744 ];
745
746 if string_properties.contains(&property_lower.as_str()) {
747 Some("string".to_string())
748 } else if numeric_properties.contains(&property_lower.as_str()) {
749 Some("numeric".to_string())
750 } else if datetime_properties.contains(&property_lower.as_str()) {
751 Some("datetime".to_string())
752 } else {
753 Some("string".to_string())
755 }
756 }
757
758 fn find_safe_boundary(&self, s: &str, pos: usize) -> usize {
760 if pos >= s.len() {
761 return s.len();
762 }
763
764 if s.is_char_boundary(pos) {
766 return pos;
767 }
768
769 let mut safe_pos = pos;
771 while safe_pos > 0 && !s.is_char_boundary(safe_pos) {
772 safe_pos -= 1;
773 }
774 safe_pos
775 }
776
777 #[cfg(test)]
778 pub fn test_extract_selected_columns(&self, query: &str, cursor_pos: usize) -> Vec<String> {
779 self.extract_selected_columns(query, cursor_pos)
780 }
781
782 fn get_string_method_suggestions(
783 &self,
784 property_type: &str,
785 partial_word: &Option<String>,
786 ) -> Vec<String> {
787 let mut suggestions = Vec::new();
788
789 match property_type {
790 "string" => {
791 let string_methods = vec![
795 "Contains('')",
796 "StartsWith('')",
797 "EndsWith('')",
798 "IndexOf('')",
799 "Substring(0, 5)",
800 "ToLower()",
801 "ToUpper()",
802 "Trim()",
803 "TrimStart()",
804 "TrimEnd()",
805 "IsNullOrEmpty()",
806 "Replace('', '')",
807 "Length()", ];
809
810 if let Some(partial) = partial_word {
811 let partial_lower = partial.to_lowercase();
812 for method in string_methods {
813 if method.to_lowercase().starts_with(&partial_lower) {
814 suggestions.push(method.to_string());
815 }
816 }
817 } else {
818 suggestions.extend(string_methods.into_iter().map(|s| s.to_string()));
819 }
820 }
821 "numeric" | "integer" | "float" | "decimal" => {
822 let numeric_string_methods = vec![
824 "Contains('')",
825 "StartsWith('')",
826 "EndsWith('')",
827 "ToString()",
828 "Length()", ];
831
832 if let Some(partial) = partial_word {
833 let partial_lower = partial.to_lowercase();
834 for method in numeric_string_methods {
835 if method.to_lowercase().starts_with(&partial_lower) {
836 suggestions.push(method.to_string());
837 }
838 }
839 } else {
840 suggestions.extend(numeric_string_methods.into_iter().map(|s| s.to_string()));
841 }
842 }
843 "datetime" => {
844 let datetime_methods = vec![
846 "Year()", "Month()", "Day()", "ToString(\"yyyy-MM-dd\")",
850 "AddDays(1)",
851 "Contains('')",
853 "StartsWith('')",
854 "EndsWith('')",
855 "Length()", ];
857
858 if let Some(partial) = partial_word {
859 let partial_lower = partial.to_lowercase();
860 for method in datetime_methods {
861 if method.to_lowercase().starts_with(&partial_lower) {
862 suggestions.push(method.to_string());
863 }
864 }
865 } else {
866 suggestions.extend(datetime_methods.into_iter().map(|s| s.to_string()));
867 }
868 }
869 _ => {
870 suggestions.push("ToString()".to_string());
872 }
873 }
874
875 suggestions
876 }
877}
878
879#[cfg(test)]
880mod tests {
881 use super::*;
882
883 fn create_test_parser() -> CursorAwareParser {
884 CursorAwareParser::new()
885 }
886
887 #[test]
888 fn test_basic_select_completion() {
889 let parser = create_test_parser();
890
891 let result = parser.get_completions("", 0);
893 println!("Context for empty query: {}", result.context);
894 assert_eq!(result.suggestions, vec!["SELECT"]);
895 assert!(result.context.contains("Start") || result.context.contains("Unknown"));
896
897 let result = parser.get_completions("SELECT ", 7);
899 println!("Context for 'SELECT ': {}", result.context);
900 assert!(result.suggestions.contains(&"*".to_string()));
901 assert!(result.suggestions.contains(&"dealId".to_string()));
902 assert!(result.context.contains("AfterSelect") || result.context.contains("SelectClause"));
903 }
904
905 #[test]
906 fn test_where_clause_completion() {
907 let parser = create_test_parser();
908
909 let query = "SELECT * FROM trade_deal WHERE ";
911 let result = parser.get_completions(query, query.len());
912 println!("Context for WHERE clause: {}", result.context);
913 assert!(result.suggestions.contains(&"dealId".to_string()));
914 assert!(result.suggestions.contains(&"platformOrderId".to_string()));
915 assert!(result.context.contains("InWhere") || result.context.contains("WhereClause"));
916 }
917
918 #[test]
919 fn test_method_call_detection() {
920 let parser = create_test_parser();
921
922 let query = "SELECT * FROM trade_deal WHERE platformOrderId.";
924 let result = parser.get_completions(query, query.len());
925 println!("Context for method call: {}", result.context);
926 println!("Suggestions: {:?}", result.suggestions);
927 assert!(result.suggestions.contains(&"Contains('')".to_string()));
928 assert!(result.suggestions.contains(&"StartsWith('')".to_string()));
929 assert!(result.context.contains("MethodCall") || result.context.contains("AfterColumn"));
930 }
931
932 #[test]
933 fn test_and_operator_context() {
934 let parser = create_test_parser();
935
936 let query = "SELECT * FROM trade_deal WHERE allocationStatus.Contains(\"All\") AND ";
938 let result = parser.get_completions(query, query.len());
939 println!("Context after AND: {}", result.context);
940 assert!(result.suggestions.contains(&"dealId".to_string()));
941 assert!(result.suggestions.contains(&"platformOrderId".to_string()));
942 assert!(
943 result.context.contains("InWhere")
944 || result.context.contains("AfterAND")
945 || result.context.contains("WhereClause")
946 );
947 assert!(!result.context.contains("MethodCall"));
948 }
949
950 #[test]
951 fn test_and_operator_with_partial_word() {
952 let parser = create_test_parser();
953
954 let query = "SELECT * FROM trade_deal WHERE allocationStatus.Contains(\"All\") AND p";
956 let result = parser.get_completions(query, query.len());
957
958 assert!(result.suggestions.contains(&"platformOrderId".to_string()));
960 assert!(result.suggestions.contains(&"price".to_string()));
961 assert!(result.suggestions.contains(&"portfolio".to_string()));
962
963 assert!(!result.suggestions.contains(&"dealId".to_string()));
965 assert!(!result.suggestions.contains(&"quantity".to_string()));
966
967 assert!(
969 result.context.contains("InWhere")
970 || result.context.contains("WhereClause")
971 || result.context.contains("AfterAND")
972 );
973 assert!(!result.context.contains("MethodCall"));
974
975 assert!(result.context.contains("(partial: Some(\"p\"))"));
977 }
978
979 #[test]
980 fn test_or_operator_context() {
981 let parser = create_test_parser();
982
983 let query = "SELECT * FROM trade_deal WHERE price > 100 OR ";
985 let result = parser.get_completions(query, query.len());
986 println!("Context after OR: {}", result.context);
987 assert!(result.suggestions.contains(&"dealId".to_string()));
988 assert!(
989 result.context.contains("InWhere")
990 || result.context.contains("AfterOR")
991 || result.context.contains("WhereClause")
992 );
993 }
994
995 #[test]
996 fn test_partial_word_extraction() {
997 let parser = create_test_parser();
998
999 assert_eq!(
1001 parser.extract_word_at_cursor("SELECT deal", 11),
1002 Some("deal".to_string())
1003 );
1004 assert_eq!(
1005 parser.extract_word_at_cursor("WHERE p", 7),
1006 Some("p".to_string())
1007 );
1008 assert_eq!(
1009 parser.extract_word_at_cursor("AND platf", 9),
1010 Some("platf".to_string())
1011 );
1012
1013 assert_eq!(parser.extract_word_at_cursor("", 0), None);
1015 assert_eq!(parser.extract_word_at_cursor("SELECT ", 7), None);
1016 }
1017
1018 #[test]
1019 fn test_complex_query_with_multiple_conditions() {
1020 let parser = create_test_parser();
1021
1022 let query = "SELECT * FROM trade_deal WHERE platformOrderId.StartsWith(\"ABC\") AND price > 100 AND ";
1024 let result = parser.get_completions(query, query.len());
1025 println!("Context for complex query: {}", result.context);
1026 assert!(result.suggestions.contains(&"dealId".to_string()));
1027 assert!(
1028 result.context.contains("InWhere")
1029 || result.context.contains("AfterAND")
1030 || result.context.contains("WhereClause")
1031 );
1032 assert!(!result.context.contains("MethodCall"));
1033 }
1034
1035 #[test]
1036 fn test_in_clause_support() {
1037 let parser = create_test_parser();
1038
1039 let query = "SELECT * FROM trade_deal WHERE status IN ";
1041 let result = parser.get_completions(query, query.len());
1042 println!("Context after IN: {}", result.context);
1043 assert!(
1045 result.context.contains("InWhere")
1046 || result.context.contains("WhereClause")
1047 || result.context.contains("Unknown")
1048 );
1049 }
1050
1051 #[test]
1052 fn test_partial_method_name_completion() {
1053 let parser = create_test_parser();
1054
1055 let query = "SELECT * FROM trade_deal WHERE instrumentName.Con";
1057 let result = parser.get_completions(query, query.len());
1058 println!("Context for partial method: {}", result.context);
1059 println!("Suggestions: {:?}", result.suggestions);
1060
1061 assert!(result.context.contains("MethodCall") || result.context.contains("AfterColumn"));
1063 assert!(result.context.contains("(partial: Some(\"Con\"))"));
1064
1065 assert!(result.suggestions.contains(&"Contains('')".to_string()));
1067 assert!(!result.suggestions.contains(&"StartsWith('')".to_string())); }
1069
1070 #[test]
1071 fn test_partial_matching_quoted_identifier() {
1072 let parser = CursorAwareParser::new();
1073 let mut parser = parser;
1075 parser.update_single_table(
1076 "customers".to_string(),
1077 vec![
1078 "Index".to_string(),
1079 "Customer Id".to_string(), "First Name".to_string(), "Company".to_string(),
1082 ],
1083 );
1084
1085 let query = "SELECT customer";
1087 let result = parser.get_completions(query, query.len());
1088
1089 assert!(
1091 result.suggestions.iter().any(|s| s == "\"Customer Id\""),
1092 "Should suggest quoted Customer Id for partial \"customer\". Got: {:?}",
1093 result.suggestions
1094 );
1095 }
1096
1097 #[test]
1098 fn test_case_preservation_in_order_by() {
1099 let parser = CursorAwareParser::new();
1100 let mut parser = parser;
1101 parser.update_single_table(
1102 "customers".to_string(),
1103 vec!["Company".to_string(), "Country".to_string()],
1104 );
1105
1106 let query = "SELECT Company, Country FROM customers ORDER BY Com";
1108 let result = parser.get_completions(query, query.len());
1109
1110 assert!(
1112 result.suggestions.iter().any(|s| s == "Company"),
1113 "Should preserve case in ORDER BY suggestions. Got: {:?}",
1114 result.suggestions
1115 );
1116 }
1117
1118 #[test]
1119 fn test_extract_selected_columns_preserves_case() {
1120 let parser = CursorAwareParser::new();
1121
1122 let query = "SELECT Company, Country FROM customers";
1123 let columns = parser.test_extract_selected_columns(query, query.len());
1124
1125 assert_eq!(columns, vec!["Company", "Country"]);
1126 assert_ne!(
1127 columns,
1128 vec!["company", "country"],
1129 "Should preserve original case"
1130 );
1131 }
1132
1133 #[test]
1134 fn test_filtering_already_selected_columns() {
1135 let parser = CursorAwareParser::new();
1136 let mut parser = parser;
1137 parser.update_single_table(
1138 "customers".to_string(),
1139 vec![
1140 "Company".to_string(),
1141 "Country".to_string(),
1142 "Customer Id".to_string(),
1143 ],
1144 );
1145
1146 let query = "SELECT Company, ";
1150 let result = parser.get_completions(query, query.len());
1151
1152 assert!(
1153 result.suggestions.iter().any(|s| s == "Company"),
1154 "Should still suggest Company even though already selected"
1155 );
1156 assert!(
1157 result.suggestions.iter().any(|s| s == "Country"),
1158 "Should suggest Country"
1159 );
1160 assert!(
1161 result.suggestions.iter().any(|s| s == "\"Customer Id\""),
1162 "Should suggest Customer Id"
1163 );
1164 }
1165
1166 #[test]
1167 fn test_order_by_completion_with_quoted_columns() {
1168 let parser = CursorAwareParser::new();
1169 let mut parser = parser;
1170 parser.update_single_table(
1171 "customers".to_string(),
1172 vec![
1173 "City".to_string(),
1174 "Company".to_string(),
1175 "Country".to_string(),
1176 "Customer Id".to_string(),
1177 ],
1178 );
1179
1180 let query = r#"SELECT City,Company,Country,"Customer Id" FROM customers ORDER BY coun"#;
1182 let result = parser.get_completions(query, query.len());
1183
1184 assert_eq!(
1186 result.partial_word,
1187 Some("coun".to_string()),
1188 "Should extract 'coun' as partial, not something weird"
1189 );
1190
1191 assert!(
1193 result.suggestions.iter().any(|s| s == "Country"),
1194 "Should suggest Country for partial 'coun'. Got: {:?}",
1195 result.suggestions
1196 );
1197 }
1198
1199 #[test]
1200 fn test_order_by_quoted_partial_completion() {
1201 let parser = CursorAwareParser::new();
1202 let mut parser = parser;
1203 parser.update_single_table(
1204 "customers".to_string(),
1205 vec![
1206 "City".to_string(),
1207 "Company".to_string(),
1208 "Country".to_string(),
1209 "Customer Id".to_string(),
1210 ],
1211 );
1212
1213 let query =
1215 r#"select City,Company,Country,"Customer Id" from customers order by City, "Customer"#;
1216 let result = parser.get_completions(query, query.len());
1217
1218 assert_eq!(
1220 result.partial_word,
1221 Some("\"Customer".to_string()),
1222 "Should extract '\"Customer' as partial"
1223 );
1224
1225 assert!(
1227 result.suggestions.iter().any(|s| s == "\"Customer Id\""),
1228 "Should suggest properly quoted 'Customer Id' for partial '\"Customer'. Got: {:?}",
1229 result.suggestions
1230 );
1231
1232 assert!(
1234 !result.suggestions.iter().any(|s| s == "\"Customer"),
1235 "Should not have truncated suggestion '\"Customer'. Got: {:?}",
1236 result.suggestions
1237 );
1238 }
1239}