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