1use crate::project::compiler::cache::ProjectCache;
83use crate::project::ir::object_id::ObjectId;
84use crate::types::{ColumnType, ObjectKind, Types};
85use mz_sql_lexer::keywords::KEYWORDS;
86use ropey::Rope;
87use std::collections::BTreeMap;
88use std::path::Path;
89use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind, Position, Url};
90
91pub(super) struct PrefixContext<'a> {
93 pub dots: usize,
95 pub text: &'a str,
97}
98
99struct CompletionContext<'a> {
104 default_db: String,
106 default_schema: String,
108 prefix: &'a PrefixContext<'a>,
110 file_object: Option<FileObject>,
112}
113
114struct FileObject {
116 dependencies: Vec<ObjectId>,
118 alias_map: BTreeMap<String, String>,
120}
121
122fn resolve_context<'a>(
127 file_uri: &Url,
128 root: &Path,
129 project_cache: &ProjectCache,
130 prefix: &'a PrefixContext<'a>,
131) -> Option<CompletionContext<'a>> {
132 let (default_db, default_schema) = ObjectId::default_db_schema_from_uri(file_uri, root)?;
133
134 let file_object = file_uri
136 .to_file_path()
137 .ok()
138 .and_then(|p| p.file_stem().map(|s| s.to_string_lossy().into_owned()))
139 .and_then(|object_name| {
140 let file_object_id =
141 ObjectId::new(default_db.clone(), default_schema.clone(), object_name);
142 project_cache
143 .get_object(&file_object_id)
144 .map(|obj| FileObject {
145 dependencies: project_cache.get_dependencies(&file_object_id),
146 alias_map: obj.aliases.clone(),
147 })
148 });
149
150 Some(CompletionContext {
151 default_db,
152 default_schema,
153 prefix,
154 file_object,
155 })
156}
157
158pub(super) fn prefix_context(text: &str, position: Position) -> PrefixContext<'_> {
163 let rope = Rope::from_str(text);
164 let byte_offset = crate::lsp::diagnostics::position_to_offset(position, &rope)
165 .unwrap_or(text.len())
166 .min(text.len());
167
168 let prefix_bytes = &text.as_bytes()[..byte_offset];
169 let mut start = prefix_bytes.len();
170 while start > 0 {
171 let ch = char::from(prefix_bytes[start - 1]);
172 if ch.is_alphanumeric() || ch == '_' || ch == '.' {
173 start -= 1;
174 } else {
175 break;
176 }
177 }
178
179 let prefix = &text[start..byte_offset];
180 let dots = prefix.chars().filter(|&c| c == '.').count();
181
182 PrefixContext { dots, text: prefix }
183}
184
185enum CompletionCandidate<'a> {
190 Keyword {
191 label: String,
192 },
193 Object {
194 label: String,
195 sort_key: String,
196 kind: ObjectKind,
197 is_external: bool,
198 },
199 Column {
200 name: String,
201 col_type: ColumnType,
202 },
203 Function {
204 info: &'a super::functions::FunctionInfo,
205 },
206}
207
208fn gather_keywords(ctx: &CompletionContext<'_>) -> Vec<CompletionCandidate<'static>> {
210 if ctx.prefix.dots > 0 {
211 return Vec::new();
212 }
213 KEYWORDS
214 .entries()
215 .map(|(_, kw)| CompletionCandidate::Keyword {
216 label: kw.as_str().to_string(),
217 })
218 .collect()
219}
220
221fn gather_functions(prefix: &PrefixContext<'_>) -> Vec<CompletionCandidate<'static>> {
224 if prefix.dots > 0 {
225 return Vec::new();
226 }
227 super::functions::search_prefix(prefix.text)
228 .map(|info| CompletionCandidate::Function { info })
229 .collect()
230}
231
232fn gather_objects<'a>(
234 ctx: &CompletionContext<'a>,
235 project_cache: &ProjectCache,
236 types_lock: &Types,
237) -> Vec<CompletionCandidate<'a>> {
238 let mut candidates = Vec::new();
239
240 for summary in project_cache.list_objects() {
241 let id = ObjectId::new(
242 summary.database.clone(),
243 summary.schema.clone(),
244 summary.name.clone(),
245 );
246 if let Some((label, sort_key)) =
247 qualify_and_filter(&id, &ctx.default_db, &ctx.default_schema, ctx.prefix)
248 {
249 candidates.push(CompletionCandidate::Object {
250 label,
251 sort_key,
252 kind: summary.kind,
253 is_external: false,
254 });
255 }
256 }
257
258 for id in project_cache.list_external_dependencies() {
259 let kind = project_cache
260 .get_kind(&id)
261 .or_else(|| types_lock.kinds.get(&id).copied())
262 .unwrap_or(ObjectKind::Table);
263 if let Some((label, sort_key)) =
264 qualify_and_filter(&id, &ctx.default_db, &ctx.default_schema, ctx.prefix)
265 {
266 candidates.push(CompletionCandidate::Object {
267 label,
268 sort_key,
269 kind,
270 is_external: true,
271 });
272 }
273 }
274
275 candidates
276}
277
278fn gather_columns<'a>(
280 ctx: &CompletionContext<'a>,
281 project_cache: Option<&ProjectCache>,
282 types_lock: &Types,
283) -> Vec<CompletionCandidate<'a>> {
284 let file_obj = match &ctx.file_object {
285 Some(fo) => fo,
286 None => return Vec::new(),
287 };
288
289 if ctx.prefix.dots == 0 {
290 gather_unqualified_columns(
291 &file_obj.dependencies,
292 project_cache,
293 types_lock,
294 ctx.prefix.text,
295 )
296 } else {
297 gather_qualified_columns(
298 ctx.prefix.text,
299 file_obj,
300 project_cache,
301 types_lock,
302 &ctx.default_db,
303 &ctx.default_schema,
304 )
305 }
306}
307
308fn gather_unqualified_columns<'a>(
310 dependencies: &[ObjectId],
311 project_cache: Option<&ProjectCache>,
312 types_lock: &Types,
313 filter_text: &str,
314) -> Vec<CompletionCandidate<'a>> {
315 let filter = filter_text.to_lowercase();
316 let mut candidates = Vec::new();
317 for id in dependencies {
318 let columns = project_cache
319 .and_then(|tc| tc.get_columns(id))
320 .or_else(|| types_lock.get_table(id).cloned());
321 if let Some(columns) = columns {
322 for (col_name, col_type) in columns {
323 if filter.is_empty() || col_name.to_lowercase().starts_with(&filter) {
324 candidates.push(CompletionCandidate::Column {
325 name: col_name.clone(),
326 col_type: col_type.clone(),
327 });
328 }
329 }
330 }
331 }
332 candidates
333}
334
335fn gather_qualified_columns<'a>(
341 prefix_text: &str,
342 file_object: &FileObject,
343 project_cache: Option<&ProjectCache>,
344 types_lock: &Types,
345 default_db: &str,
346 default_schema: &str,
347) -> Vec<CompletionCandidate<'a>> {
348 let last_dot = match prefix_text.rfind('.') {
349 Some(pos) => pos,
350 None => return Vec::new(),
351 };
352 let object_text = &prefix_text[..last_dot];
353 let col_filter = prefix_text[last_dot + 1..].to_lowercase();
354
355 let object_id = match resolve_qualified_object(
356 object_text,
357 &file_object.alias_map,
358 default_db,
359 default_schema,
360 ) {
361 Some(id) => id,
362 None => return Vec::new(),
363 };
364
365 if !file_object.dependencies.contains(&object_id) {
366 return Vec::new();
367 }
368
369 let columns = project_cache
370 .and_then(|tc| tc.get_columns(&object_id))
371 .or_else(|| types_lock.get_table(&object_id).cloned());
372
373 match columns {
374 Some(columns) => columns
375 .into_iter()
376 .filter(|(name, _)| {
377 col_filter.is_empty() || name.to_lowercase().starts_with(&col_filter)
378 })
379 .map(|(name, col_type)| CompletionCandidate::Column { name, col_type })
380 .collect(),
381 None => Vec::new(),
382 }
383}
384
385fn resolve_qualified_object(
393 object_text: &str,
394 alias_map: &BTreeMap<String, String>,
395 default_db: &str,
396 default_schema: &str,
397) -> Option<ObjectId> {
398 let parts: Vec<&str> = object_text.split('.').collect();
399 match parts.len() {
400 1 => {
401 if let Some(fqn) = alias_map.get(&parts[0].to_lowercase()) {
402 fqn.parse::<ObjectId>().ok()
403 } else {
404 Some(ObjectId::new(
405 default_db.to_string(),
406 default_schema.to_string(),
407 parts[0].to_string(),
408 ))
409 }
410 }
411 2 => Some(ObjectId::new(
412 default_db.to_string(),
413 parts[0].to_string(),
414 parts[1].to_string(),
415 )),
416 3 => Some(ObjectId::new(
417 parts[0].to_string(),
418 parts[1].to_string(),
419 parts[2].to_string(),
420 )),
421 _ => None,
422 }
423}
424
425fn format_candidate(candidate: &CompletionCandidate<'_>) -> CompletionItem {
427 match candidate {
428 CompletionCandidate::Keyword { label } => CompletionItem {
429 label: label.clone(),
430 kind: Some(CompletionItemKind::KEYWORD),
431 ..Default::default()
432 },
433 CompletionCandidate::Object {
434 label,
435 sort_key,
436 kind,
437 is_external,
438 } => CompletionItem {
439 label: label.clone(),
440 kind: Some(object_kind_to_completion_kind(*kind)),
441 detail: Some(if *is_external {
442 format!("{} (external)", kind)
443 } else {
444 kind.to_string()
445 }),
446 sort_text: Some(sort_key.clone()),
447 ..Default::default()
448 },
449 CompletionCandidate::Column { name, col_type } => CompletionItem {
450 label: name.to_string(),
451 kind: Some(CompletionItemKind::FIELD),
452 detail: Some(format_column_detail(col_type)),
453 sort_text: Some(format!("0_{}", name)),
454 ..Default::default()
455 },
456 CompletionCandidate::Function { info } => CompletionItem {
457 label: info.name.to_string(),
458 kind: Some(CompletionItemKind::FUNCTION),
459 detail: info.signatures.first().cloned(),
460 sort_text: Some(format!("4_{}", info.name)),
461 ..Default::default()
462 },
463 }
464}
465
466pub(super) fn complete(
478 project_cache: Option<&ProjectCache>,
479 types_lock: &Types,
480 file_uri: &Url,
481 root: &Path,
482 prefix: &PrefixContext<'_>,
483) -> Vec<CompletionItem> {
484 let ctx = project_cache.and_then(|pc| resolve_context(file_uri, root, pc, prefix));
485
486 let mut candidates: Vec<CompletionCandidate<'_>> = Vec::new();
487 candidates.extend(gather_functions(prefix));
489 if let Some(ctx) = &ctx {
490 candidates.extend(gather_keywords(ctx));
491 candidates.extend(gather_objects(ctx, project_cache.unwrap(), types_lock));
492 candidates.extend(gather_columns(ctx, project_cache, types_lock));
493 } else if prefix.dots == 0 {
494 candidates.extend(
495 KEYWORDS
496 .entries()
497 .map(|(_, kw)| CompletionCandidate::Keyword {
498 label: kw.as_str().to_string(),
499 }),
500 );
501 }
502
503 candidates.iter().map(format_candidate).collect()
504}
505
506pub(crate) fn qualify_and_filter(
514 id: &ObjectId,
515 default_db: &str,
516 default_schema: &str,
517 prefix: &PrefixContext<'_>,
518) -> Option<(String, String)> {
519 let in_default_db = id.database() == Some(default_db);
520 let sort_key = if in_default_db && id.schema() == default_schema {
521 "1"
522 } else if in_default_db {
523 "2"
524 } else {
525 "3"
526 };
527
528 if prefix.dots == 0 {
529 let label = if in_default_db && id.schema() == default_schema {
530 id.object().to_string()
531 } else if in_default_db || id.database().is_none() {
532 format!("{}.{}", id.schema(), id.object())
533 } else {
534 id.to_string()
535 };
536 return Some((label.clone(), format!("{}_{}", sort_key, label)));
537 }
538
539 let candidates = [format!("{}.{}", id.schema(), id.object()), id.to_string()];
540
541 let prefix_lower = prefix.text.to_lowercase();
542 for candidate in &candidates {
543 if candidate.to_lowercase().starts_with(&prefix_lower) {
544 let last_dot = prefix.text.rfind('.').expect("dots >= 1 guarantees a dot");
545 let label = candidate[last_dot + 1..].to_string();
546 return Some((label, format!("{}_{}", sort_key, candidate)));
547 }
548 }
549
550 None
551}
552
553fn object_kind_to_completion_kind(kind: ObjectKind) -> CompletionItemKind {
555 match kind {
556 ObjectKind::Table | ObjectKind::View | ObjectKind::MaterializedView => {
557 CompletionItemKind::STRUCT
558 }
559 ObjectKind::Source | ObjectKind::Sink => CompletionItemKind::EVENT,
560 ObjectKind::Secret => CompletionItemKind::CONSTANT,
561 ObjectKind::Connection => CompletionItemKind::INTERFACE,
562 }
563}
564
565fn format_column_detail(col_type: &ColumnType) -> String {
567 if col_type.nullable {
568 format!("{} (nullable)", col_type.r#type)
569 } else {
570 col_type.r#type.clone()
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use std::path::Path;
578
579 fn no_prefix() -> PrefixContext<'static> {
582 PrefixContext { dots: 0, text: "" }
583 }
584
585 fn write_project_toml(root: &Path) {
586 std::fs::write(root.join("project.toml"), "[project]\nname = \"test\"\n").unwrap();
587 }
588
589 fn build_cache(root: &tempfile::TempDir) -> ProjectCache {
590 write_project_toml(root.path());
591 let _project = crate::project::plan_sync(
592 &crate::fs::FileSystem::new(),
593 root.path(),
594 None,
595 None,
596 &Default::default(),
597 )
598 .expect("project should compile");
599 ProjectCache::open(root.path(), "", None, &Default::default())
600 .expect("cache should open")
601 .expect("cache DB should exist")
602 }
603
604 fn object_completions(
606 cache: &ProjectCache,
607 types_lock: Option<&Types>,
608 file_uri: &Url,
609 root: &Path,
610 prefix: &PrefixContext<'_>,
611 ) -> Vec<CompletionItem> {
612 let empty = Types::default();
613 let types_lock = types_lock.unwrap_or(&empty);
614 let ctx = match resolve_context(file_uri, root, cache, prefix) {
615 Some(ctx) => ctx,
616 None => return Vec::new(),
617 };
618 gather_objects(&ctx, cache, types_lock)
619 .iter()
620 .map(format_candidate)
621 .collect()
622 }
623
624 fn column_completions(
626 cache: &ProjectCache,
627 types_lock: Option<&Types>,
628 file_uri: &Url,
629 root: &Path,
630 prefix: &PrefixContext<'_>,
631 ) -> Vec<CompletionItem> {
632 let empty = Types::default();
633 let types_lock = types_lock.unwrap_or(&empty);
634 let ctx = match resolve_context(file_uri, root, cache, prefix) {
635 Some(ctx) => ctx,
636 None => return Vec::new(),
637 };
638 gather_columns(&ctx, Some(cache), types_lock)
639 .iter()
640 .map(format_candidate)
641 .collect()
642 }
643
644 #[mz_ore::test]
645 fn prefix_no_prefix() {
646 let text = "SELECT ";
647 let ctx = prefix_context(text, Position::new(0, 7));
648 assert_eq!(ctx.dots, 0);
649 assert_eq!(ctx.text, "");
650 }
651
652 #[mz_ore::test]
653 fn prefix_bare_ident() {
654 let text = "SELECT foo";
655 let ctx = prefix_context(text, Position::new(0, 10));
656 assert_eq!(ctx.dots, 0);
657 assert_eq!(ctx.text, "foo");
658 }
659
660 #[mz_ore::test]
661 fn prefix_one_dot() {
662 let text = "SELECT schema.foo";
663 let ctx = prefix_context(text, Position::new(0, 17));
664 assert_eq!(ctx.dots, 1);
665 assert_eq!(ctx.text, "schema.foo");
666 }
667
668 #[mz_ore::test]
669 fn prefix_two_dots() {
670 let text = "SELECT db.schema.foo";
671 let ctx = prefix_context(text, Position::new(0, 20));
672 assert_eq!(ctx.dots, 2);
673 assert_eq!(ctx.text, "db.schema.foo");
674 }
675
676 #[mz_ore::test]
677 fn prefix_mid_line() {
678 let text = "SELECT * FROM schema.f";
679 let ctx = prefix_context(text, Position::new(0, 22));
680 assert_eq!(ctx.dots, 1);
681 assert_eq!(ctx.text, "schema.f");
682 }
683
684 #[mz_ore::test]
685 fn prefix_text_stored() {
686 let text = "SELECT public.f";
687 let ctx = prefix_context(text, Position::new(0, 15));
688 assert_eq!(ctx.dots, 1);
689 assert_eq!(ctx.text, "public.f");
690 }
691
692 #[cfg_attr(miri, ignore)] #[mz_ore::test]
694 fn same_schema_bare_name() {
695 let root = tempfile::tempdir().unwrap();
696 let dir = root.path().join("models/mydb/public");
697 std::fs::create_dir_all(&dir).unwrap();
698 std::fs::write(dir.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;").unwrap();
699 std::fs::write(dir.join("bar.sql"), "CREATE VIEW bar AS SELECT * FROM foo;").unwrap();
700 let cache = build_cache(&root);
701
702 let uri = Url::from_file_path(root.path().join("models/mydb/public/bar.sql")).unwrap();
703 let items = object_completions(&cache, None, &uri, root.path(), &no_prefix());
704
705 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
706 assert!(
707 labels.contains(&"foo"),
708 "expected bare 'foo', got: {:?}",
709 labels
710 );
711 assert!(
712 labels.contains(&"bar"),
713 "expected bare 'bar', got: {:?}",
714 labels
715 );
716 }
717
718 #[cfg_attr(miri, ignore)] #[mz_ore::test]
720 fn cross_schema_qualified() {
721 let root = tempfile::tempdir().unwrap();
722 let public = root.path().join("models/mydb/public");
723 std::fs::create_dir_all(&public).unwrap();
724 std::fs::write(public.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
725
726 let other = root.path().join("models/mydb/other");
727 std::fs::create_dir_all(&other).unwrap();
728 std::fs::write(
729 other.join("baz.sql"),
730 "CREATE VIEW baz AS SELECT * FROM mydb.public.foo;",
731 )
732 .unwrap();
733 let cache = build_cache(&root);
734
735 let uri = Url::from_file_path(root.path().join("models/mydb/other/baz.sql")).unwrap();
737 let items = object_completions(&cache, None, &uri, root.path(), &no_prefix());
738
739 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
740 assert!(
741 labels.contains(&"public.foo"),
742 "expected 'public.foo', got: {:?}",
743 labels
744 );
745 assert!(
747 labels.contains(&"baz"),
748 "expected bare 'baz', got: {:?}",
749 labels
750 );
751 }
752
753 #[cfg_attr(miri, ignore)] #[mz_ore::test]
755 fn cross_database_fully_qualified() {
756 let root = tempfile::tempdir().unwrap();
757 let db1 = root.path().join("models/mydb/public");
758 std::fs::create_dir_all(&db1).unwrap();
759 std::fs::write(db1.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;").unwrap();
760
761 let db2 = root.path().join("models/otherdb/public");
762 std::fs::create_dir_all(&db2).unwrap();
763 std::fs::write(db2.join("bar.sql"), "CREATE VIEW bar AS SELECT 1 AS id;").unwrap();
764 let cache = build_cache(&root);
765
766 let uri = Url::from_file_path(root.path().join("models/otherdb/public/bar.sql")).unwrap();
768 let items = object_completions(&cache, None, &uri, root.path(), &no_prefix());
769
770 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
771 assert!(
772 labels.contains(&"mydb.public.foo"),
773 "expected 'mydb.public.foo', got: {:?}",
774 labels
775 );
776 }
777
778 #[cfg_attr(miri, ignore)] #[mz_ore::test]
780 fn external_deps_included() {
781 let root = tempfile::tempdir().unwrap();
782 let dir = root.path().join("models/mydb/public");
783 std::fs::create_dir_all(&dir).unwrap();
784 std::fs::write(
785 dir.join("foo.sql"),
786 "CREATE VIEW foo AS SELECT * FROM mydb.ext.src;",
787 )
788 .unwrap();
789
790 std::fs::write(
792 root.path().join("types.lock"),
793 "version = 1\n\n\
794 [[source]]\n\
795 name = \"mydb.ext.src\"\n\
796 \n\
797 [[source.columns]]\n\
798 name = \"id\"\n\
799 type = \"integer\"\n\
800 nullable = false\n",
801 )
802 .unwrap();
803 let cache = build_cache(&root);
804
805 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
806 let uri = Url::from_file_path(root.path().join("models/mydb/public/foo.sql")).unwrap();
807 let items = object_completions(&cache, Some(&types_cache), &uri, root.path(), &no_prefix());
808
809 let ext_items: Vec<_> = items
810 .iter()
811 .filter(|i| i.detail.as_deref() == Some("source (external)"))
812 .collect();
813 assert_eq!(ext_items.len(), 1, "expected one external source");
814 assert_eq!(ext_items[0].label, "ext.src");
815 }
816
817 #[cfg_attr(miri, ignore)] #[mz_ore::test]
819 fn kind_mapping() {
820 let root = tempfile::tempdir().unwrap();
821 let storage = root.path().join("models/mydb/storage");
823 std::fs::create_dir_all(&storage).unwrap();
824 std::fs::write(storage.join("t.sql"), "CREATE TABLE t (id INT);").unwrap();
825
826 let compute = root.path().join("models/mydb/compute");
827 std::fs::create_dir_all(&compute).unwrap();
828 std::fs::write(
829 compute.join("v.sql"),
830 "CREATE VIEW v AS SELECT * FROM mydb.storage.t;",
831 )
832 .unwrap();
833 let cache = build_cache(&root);
834
835 let uri = Url::from_file_path(root.path().join("models/mydb/storage/t.sql")).unwrap();
836 let items = object_completions(&cache, None, &uri, root.path(), &no_prefix());
837
838 let table_item = items.iter().find(|i| i.label == "t").unwrap();
839 assert_eq!(table_item.detail.as_deref(), Some("table"));
840 assert_eq!(table_item.kind, Some(CompletionItemKind::STRUCT));
841
842 let view_item = items.iter().find(|i| i.label.ends_with("v")).unwrap();
843 assert_eq!(view_item.detail.as_deref(), Some("view"));
844 assert_eq!(view_item.kind, Some(CompletionItemKind::STRUCT));
845 }
846
847 #[cfg_attr(miri, ignore)] #[mz_ore::test]
849 fn file_outside_models_returns_empty() {
850 let root = tempfile::tempdir().unwrap();
851 let dir = root.path().join("models/mydb/public");
852 std::fs::create_dir_all(&dir).unwrap();
853 std::fs::write(dir.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;").unwrap();
854 let cache = build_cache(&root);
855
856 let uri = Url::from_file_path(root.path().join("random/file.sql")).unwrap();
858 let items = object_completions(&cache, None, &uri, root.path(), &no_prefix());
859 assert!(items.is_empty());
860 }
861
862 #[cfg_attr(miri, ignore)] #[mz_ore::test]
864 fn schema_prefix_strips_label_to_bare_name() {
865 let root = tempfile::tempdir().unwrap();
866 let dir = root.path().join("models/mydb/public");
867 std::fs::create_dir_all(&dir).unwrap();
868 std::fs::write(dir.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;").unwrap();
869 std::fs::write(dir.join("bar.sql"), "CREATE VIEW bar AS SELECT * FROM foo;").unwrap();
870 let cache = build_cache(&root);
871
872 let prefix = PrefixContext {
873 dots: 1,
874 text: "public.",
875 };
876 let uri = Url::from_file_path(root.path().join("models/mydb/public/bar.sql")).unwrap();
877 let items = object_completions(&cache, None, &uri, root.path(), &prefix);
878
879 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
880 assert!(
882 labels.contains(&"foo"),
883 "expected bare 'foo', got: {:?}",
884 labels
885 );
886 assert!(
887 labels.contains(&"bar"),
888 "expected bare 'bar', got: {:?}",
889 labels
890 );
891 }
892
893 #[cfg_attr(miri, ignore)] #[mz_ore::test]
895 fn db_prefix_disambiguates_to_schema_dot_object() {
896 let root = tempfile::tempdir().unwrap();
897 let dir = root.path().join("models/mydb/public");
898 std::fs::create_dir_all(&dir).unwrap();
899 std::fs::write(dir.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;").unwrap();
900 let cache = build_cache(&root);
901
902 let prefix = PrefixContext {
904 dots: 1,
905 text: "mydb.",
906 };
907 let uri = Url::from_file_path(root.path().join("models/mydb/public/foo.sql")).unwrap();
908 let items = object_completions(&cache, None, &uri, root.path(), &prefix);
909
910 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
911 assert!(
913 labels.contains(&"public.foo"),
914 "expected 'public.foo', got: {:?}",
915 labels
916 );
917 }
918
919 #[cfg_attr(miri, ignore)] #[mz_ore::test]
921 fn full_qualification_strips_to_bare_name() {
922 let root = tempfile::tempdir().unwrap();
923 let dir = root.path().join("models/mydb/public");
924 std::fs::create_dir_all(&dir).unwrap();
925 std::fs::write(dir.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;").unwrap();
926 let cache = build_cache(&root);
927
928 let prefix = PrefixContext {
929 dots: 2,
930 text: "mydb.public.",
931 };
932 let uri = Url::from_file_path(root.path().join("models/mydb/public/foo.sql")).unwrap();
933 let items = object_completions(&cache, None, &uri, root.path(), &prefix);
934
935 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
936 assert!(
937 labels.contains(&"foo"),
938 "expected bare 'foo', got: {:?}",
939 labels
940 );
941 }
942
943 #[cfg_attr(miri, ignore)] #[mz_ore::test]
945 fn prefix_filters_non_matching_objects() {
946 let root = tempfile::tempdir().unwrap();
947 let public = root.path().join("models/mydb/public");
948 std::fs::create_dir_all(&public).unwrap();
949 std::fs::write(public.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
950
951 let other = root.path().join("models/mydb/other");
952 std::fs::create_dir_all(&other).unwrap();
953 std::fs::write(
954 other.join("baz.sql"),
955 "CREATE VIEW baz AS SELECT * FROM mydb.public.foo;",
956 )
957 .unwrap();
958 let cache = build_cache(&root);
959
960 let prefix = PrefixContext {
962 dots: 1,
963 text: "other.",
964 };
965 let uri = Url::from_file_path(root.path().join("models/mydb/other/baz.sql")).unwrap();
966 let items = object_completions(&cache, None, &uri, root.path(), &prefix);
967
968 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
969 assert!(labels.contains(&"baz"), "expected 'baz', got: {:?}", labels);
970 assert!(
972 !labels.iter().any(|l| l.contains("foo")),
973 "expected no 'foo' items, got: {:?}",
974 labels
975 );
976 }
977
978 fn write_types_lock(root: &Path, tables: &[(&str, &str, &str, &str, &[(&str, &str, bool)])]) {
980 let mut toml = String::from("version = 1\n\n");
981 for (db, schema, name, kind, columns) in tables {
982 toml.push_str(&format!(
983 "[[{}]]\nname = \"{}.{}.{}\"\n\n",
984 kind, db, schema, name
985 ));
986 for (col_name, col_type, nullable) in *columns {
987 toml.push_str(&format!(
988 "[[{}.columns]]\nname = \"{}\"\ntype = \"{}\"\nnullable = {}\n\n",
989 kind, col_name, col_type, nullable
990 ));
991 }
992 }
993 std::fs::write(root.join("types.lock"), toml).unwrap();
994 }
995
996 #[cfg_attr(miri, ignore)] #[mz_ore::test]
998 fn column_deps_at_zero_dots() {
999 let root = tempfile::tempdir().unwrap();
1000 let storage = root.path().join("models/mydb/storage");
1001 std::fs::create_dir_all(&storage).unwrap();
1002 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1003
1004 let compute = root.path().join("models/mydb/compute");
1005 std::fs::create_dir_all(&compute).unwrap();
1006 std::fs::write(
1007 compute.join("v.sql"),
1008 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1009 )
1010 .unwrap();
1011 write_types_lock(
1012 root.path(),
1013 &[(
1014 "mydb",
1015 "storage",
1016 "foo",
1017 "table",
1018 &[("id", "integer", false), ("bar", "text", true)],
1019 )],
1020 );
1021 let cache = build_cache(&root);
1022 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1023
1024 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1025 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &no_prefix());
1026
1027 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1028 assert!(labels.contains(&"id"), "expected 'id', got: {:?}", labels);
1029 assert!(labels.contains(&"bar"), "expected 'bar', got: {:?}", labels);
1030 }
1031
1032 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1034 fn column_deps_filtered_by_prefix() {
1035 let root = tempfile::tempdir().unwrap();
1036 let storage = root.path().join("models/mydb/storage");
1037 std::fs::create_dir_all(&storage).unwrap();
1038 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1039
1040 let compute = root.path().join("models/mydb/compute");
1041 std::fs::create_dir_all(&compute).unwrap();
1042 std::fs::write(
1043 compute.join("v.sql"),
1044 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1045 )
1046 .unwrap();
1047 write_types_lock(
1048 root.path(),
1049 &[(
1050 "mydb",
1051 "storage",
1052 "foo",
1053 "table",
1054 &[("id", "integer", false), ("name", "text", false)],
1055 )],
1056 );
1057 let cache = build_cache(&root);
1058 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1059
1060 let prefix = PrefixContext { dots: 0, text: "i" };
1061 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1062 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1063
1064 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1065 assert!(labels.contains(&"id"), "expected 'id', got: {:?}", labels);
1066 assert!(
1067 !labels.contains(&"name"),
1068 "should not contain 'name', got: {:?}",
1069 labels
1070 );
1071 }
1072
1073 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1075 fn column_deps_no_types_cache() {
1076 let root = tempfile::tempdir().unwrap();
1077 let storage = root.path().join("models/mydb/storage");
1078 std::fs::create_dir_all(&storage).unwrap();
1079 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1080
1081 let compute = root.path().join("models/mydb/compute");
1082 std::fs::create_dir_all(&compute).unwrap();
1083 std::fs::write(
1084 compute.join("v.sql"),
1085 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1086 )
1087 .unwrap();
1088 let cache = build_cache(&root);
1089
1090 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1091 let items = column_completions(&cache, None, &uri, root.path(), &no_prefix());
1092 assert!(items.is_empty(), "expected empty without types cache");
1093 }
1094
1095 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1097 fn column_deps_multiple_dependencies() {
1098 let root = tempfile::tempdir().unwrap();
1099 let storage = root.path().join("models/mydb/storage");
1100 std::fs::create_dir_all(&storage).unwrap();
1101 std::fs::write(storage.join("t1.sql"), "CREATE TABLE t1 (a INT);").unwrap();
1102 std::fs::write(storage.join("t2.sql"), "CREATE TABLE t2 (b INT);").unwrap();
1103
1104 let compute = root.path().join("models/mydb/compute");
1105 std::fs::create_dir_all(&compute).unwrap();
1106 std::fs::write(
1107 compute.join("v.sql"),
1108 "CREATE VIEW v AS SELECT * FROM mydb.storage.t1, mydb.storage.t2;",
1109 )
1110 .unwrap();
1111 write_types_lock(
1112 root.path(),
1113 &[
1114 ("mydb", "storage", "t1", "table", &[("a", "integer", false)]),
1115 ("mydb", "storage", "t2", "table", &[("b", "integer", false)]),
1116 ],
1117 );
1118 let cache = build_cache(&root);
1119 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1120
1121 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1122 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &no_prefix());
1123
1124 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1125 assert!(labels.contains(&"a"), "expected 'a', got: {:?}", labels);
1126 assert!(labels.contains(&"b"), "expected 'b', got: {:?}", labels);
1127 }
1128
1129 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1131 fn column_qualified_bare_object() {
1132 let root = tempfile::tempdir().unwrap();
1133 let storage = root.path().join("models/mydb/storage");
1134 std::fs::create_dir_all(&storage).unwrap();
1135 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1136
1137 let compute = root.path().join("models/mydb/compute");
1138 std::fs::create_dir_all(&compute).unwrap();
1139 std::fs::write(
1140 compute.join("v.sql"),
1141 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1142 )
1143 .unwrap();
1144 write_types_lock(
1145 root.path(),
1146 &[(
1147 "mydb",
1148 "storage",
1149 "foo",
1150 "table",
1151 &[("id", "integer", false), ("name", "text", false)],
1152 )],
1153 );
1154 let cache = build_cache(&root);
1155 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1156
1157 let prefix = PrefixContext {
1159 dots: 2,
1160 text: "storage.foo.",
1161 };
1162 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1163 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1164
1165 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1166 assert!(labels.contains(&"id"), "expected 'id', got: {:?}", labels);
1167 assert!(
1168 labels.contains(&"name"),
1169 "expected 'name', got: {:?}",
1170 labels
1171 );
1172 }
1173
1174 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1176 fn column_qualified_schema_object() {
1177 let root = tempfile::tempdir().unwrap();
1178 let storage = root.path().join("models/mydb/storage");
1179 std::fs::create_dir_all(&storage).unwrap();
1180 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1181
1182 let compute = root.path().join("models/mydb/compute");
1183 std::fs::create_dir_all(&compute).unwrap();
1184 std::fs::write(
1185 compute.join("v.sql"),
1186 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1187 )
1188 .unwrap();
1189 write_types_lock(
1190 root.path(),
1191 &[(
1192 "mydb",
1193 "storage",
1194 "foo",
1195 "table",
1196 &[("id", "integer", false), ("name", "text", false)],
1197 )],
1198 );
1199 let cache = build_cache(&root);
1200 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1201
1202 let prefix = PrefixContext {
1203 dots: 2,
1204 text: "storage.foo.i",
1205 };
1206 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1207 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1208
1209 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1210 assert!(labels.contains(&"id"), "expected 'id', got: {:?}", labels);
1211 assert!(
1212 !labels.contains(&"name"),
1213 "should not contain 'name', got: {:?}",
1214 labels
1215 );
1216 }
1217
1218 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1220 fn column_qualified_fully_qualified() {
1221 let root = tempfile::tempdir().unwrap();
1222 let storage = root.path().join("models/mydb/storage");
1223 std::fs::create_dir_all(&storage).unwrap();
1224 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1225
1226 let compute = root.path().join("models/mydb/compute");
1227 std::fs::create_dir_all(&compute).unwrap();
1228 std::fs::write(
1229 compute.join("v.sql"),
1230 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1231 )
1232 .unwrap();
1233 write_types_lock(
1234 root.path(),
1235 &[(
1236 "mydb",
1237 "storage",
1238 "foo",
1239 "table",
1240 &[("id", "integer", false)],
1241 )],
1242 );
1243 let cache = build_cache(&root);
1244 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1245
1246 let prefix = PrefixContext {
1247 dots: 3,
1248 text: "mydb.storage.foo.",
1249 };
1250 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1251 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1252
1253 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1254 assert!(labels.contains(&"id"), "expected 'id', got: {:?}", labels);
1255 }
1256
1257 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1259 fn column_qualified_non_dependency_excluded() {
1260 let root = tempfile::tempdir().unwrap();
1261 let storage = root.path().join("models/mydb/storage");
1262 std::fs::create_dir_all(&storage).unwrap();
1263 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1264 std::fs::write(storage.join("other.sql"), "CREATE TABLE other (x INT);").unwrap();
1265
1266 let compute = root.path().join("models/mydb/compute");
1267 std::fs::create_dir_all(&compute).unwrap();
1268 std::fs::write(
1269 compute.join("v.sql"),
1270 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1271 )
1272 .unwrap();
1273 write_types_lock(
1274 root.path(),
1275 &[
1276 (
1277 "mydb",
1278 "storage",
1279 "foo",
1280 "table",
1281 &[("id", "integer", false)],
1282 ),
1283 (
1284 "mydb",
1285 "storage",
1286 "other",
1287 "table",
1288 &[("x", "integer", false)],
1289 ),
1290 ],
1291 );
1292 let cache = build_cache(&root);
1293 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1294
1295 let prefix = PrefixContext {
1297 dots: 2,
1298 text: "storage.other.",
1299 };
1300 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1301 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1302
1303 assert!(
1304 items.is_empty(),
1305 "expected empty for non-dependency, got: {:?}",
1306 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1307 );
1308 }
1309
1310 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1312 fn column_qualified_object_not_in_cache() {
1313 let root = tempfile::tempdir().unwrap();
1314 let storage = root.path().join("models/mydb/storage");
1315 std::fs::create_dir_all(&storage).unwrap();
1316 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1317
1318 let compute = root.path().join("models/mydb/compute");
1319 std::fs::create_dir_all(&compute).unwrap();
1320 std::fs::write(
1321 compute.join("v.sql"),
1322 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1323 )
1324 .unwrap();
1325 write_types_lock(root.path(), &[]);
1327 let cache = build_cache(&root);
1328 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1329
1330 let prefix = PrefixContext {
1332 dots: 2,
1333 text: "storage.foo.",
1334 };
1335 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1336 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1337
1338 assert!(items.is_empty(), "expected empty when object not in cache");
1339 }
1340
1341 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1343 fn column_qualified_filter_case_insensitive() {
1344 let root = tempfile::tempdir().unwrap();
1345 let storage = root.path().join("models/mydb/storage");
1346 std::fs::create_dir_all(&storage).unwrap();
1347 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1348
1349 let compute = root.path().join("models/mydb/compute");
1350 std::fs::create_dir_all(&compute).unwrap();
1351 std::fs::write(
1352 compute.join("v.sql"),
1353 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1354 )
1355 .unwrap();
1356 write_types_lock(
1357 root.path(),
1358 &[(
1359 "mydb",
1360 "storage",
1361 "foo",
1362 "table",
1363 &[("id", "integer", false), ("name", "text", false)],
1364 )],
1365 );
1366 let cache = build_cache(&root);
1367 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1368
1369 let prefix = PrefixContext {
1371 dots: 2,
1372 text: "storage.foo.I",
1373 };
1374 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1375 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1376
1377 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1378 assert!(
1379 labels.contains(&"id"),
1380 "expected 'id' with case-insensitive match, got: {:?}",
1381 labels
1382 );
1383 assert!(
1384 !labels.contains(&"name"),
1385 "should not contain 'name', got: {:?}",
1386 labels
1387 );
1388 }
1389
1390 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1392 fn column_kind_and_detail() {
1393 let root = tempfile::tempdir().unwrap();
1394 let storage = root.path().join("models/mydb/storage");
1395 std::fs::create_dir_all(&storage).unwrap();
1396 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1397
1398 let compute = root.path().join("models/mydb/compute");
1399 std::fs::create_dir_all(&compute).unwrap();
1400 std::fs::write(
1401 compute.join("v.sql"),
1402 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1403 )
1404 .unwrap();
1405 write_types_lock(
1406 root.path(),
1407 &[(
1408 "mydb",
1409 "storage",
1410 "foo",
1411 "table",
1412 &[("id", "integer", false), ("name", "text", true)],
1413 )],
1414 );
1415 let cache = build_cache(&root);
1416 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1417
1418 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1419 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &no_prefix());
1420
1421 let id_item = items.iter().find(|i| i.label == "id").unwrap();
1422 assert_eq!(id_item.kind, Some(CompletionItemKind::FIELD));
1423 assert_eq!(id_item.detail.as_deref(), Some("integer"));
1424
1425 let name_item = items.iter().find(|i| i.label == "name").unwrap();
1426 assert_eq!(name_item.kind, Some(CompletionItemKind::FIELD));
1427 assert_eq!(name_item.detail.as_deref(), Some("text (nullable)"));
1428 }
1429
1430 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1432 fn column_sort_before_objects() {
1433 let root = tempfile::tempdir().unwrap();
1434 let storage = root.path().join("models/mydb/storage");
1435 std::fs::create_dir_all(&storage).unwrap();
1436 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1437
1438 let compute = root.path().join("models/mydb/compute");
1439 std::fs::create_dir_all(&compute).unwrap();
1440 std::fs::write(
1441 compute.join("v.sql"),
1442 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1443 )
1444 .unwrap();
1445 write_types_lock(
1446 root.path(),
1447 &[(
1448 "mydb",
1449 "storage",
1450 "foo",
1451 "table",
1452 &[("id", "integer", false)],
1453 )],
1454 );
1455 let cache = build_cache(&root);
1456 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1457
1458 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1459 let col_items =
1460 column_completions(&cache, Some(&types_cache), &uri, root.path(), &no_prefix());
1461 let obj_items =
1462 object_completions(&cache, Some(&types_cache), &uri, root.path(), &no_prefix());
1463
1464 for item in &col_items {
1466 assert!(
1467 item.sort_text.as_ref().unwrap().starts_with("0_"),
1468 "column sort_text should start with '0_', got: {:?}",
1469 item.sort_text
1470 );
1471 }
1472 for item in &obj_items {
1473 let sort = item.sort_text.as_ref().unwrap();
1474 assert!(
1475 sort.starts_with("1_") || sort.starts_with("2_") || sort.starts_with("3_"),
1476 "object sort_text should start with '1_'/'2_'/'3_', got: {:?}",
1477 sort
1478 );
1479 }
1480 }
1481
1482 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1484 fn column_alias_explicit() {
1485 let root = tempfile::tempdir().unwrap();
1486 let storage = root.path().join("models/mydb/storage");
1487 std::fs::create_dir_all(&storage).unwrap();
1488 std::fs::write(storage.join("orders.sql"), "CREATE TABLE orders (id INT);").unwrap();
1489
1490 let compute = root.path().join("models/mydb/compute");
1491 std::fs::create_dir_all(&compute).unwrap();
1492 std::fs::write(
1493 compute.join("v.sql"),
1494 "CREATE VIEW v AS SELECT o.id FROM mydb.storage.orders o;",
1495 )
1496 .unwrap();
1497 write_types_lock(
1498 root.path(),
1499 &[(
1500 "mydb",
1501 "storage",
1502 "orders",
1503 "table",
1504 &[("id", "integer", false), ("name", "text", false)],
1505 )],
1506 );
1507 let cache = build_cache(&root);
1508 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1509
1510 let prefix = PrefixContext {
1512 dots: 1,
1513 text: "o.",
1514 };
1515 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1516 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1517
1518 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1519 assert!(labels.contains(&"id"), "expected 'id', got: {:?}", labels);
1520 assert!(
1521 labels.contains(&"name"),
1522 "expected 'name', got: {:?}",
1523 labels
1524 );
1525 }
1526
1527 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1529 fn column_alias_bare_table_name() {
1530 let root = tempfile::tempdir().unwrap();
1531 let storage = root.path().join("models/mydb/storage");
1532 std::fs::create_dir_all(&storage).unwrap();
1533 std::fs::write(storage.join("orders.sql"), "CREATE TABLE orders (id INT);").unwrap();
1534
1535 let compute = root.path().join("models/mydb/compute");
1536 std::fs::create_dir_all(&compute).unwrap();
1537 std::fs::write(
1538 compute.join("v.sql"),
1539 "CREATE VIEW v AS SELECT * FROM mydb.storage.orders;",
1540 )
1541 .unwrap();
1542 write_types_lock(
1543 root.path(),
1544 &[(
1545 "mydb",
1546 "storage",
1547 "orders",
1548 "table",
1549 &[("id", "integer", false)],
1550 )],
1551 );
1552 let cache = build_cache(&root);
1553 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1554
1555 let prefix = PrefixContext {
1557 dots: 1,
1558 text: "orders.",
1559 };
1560 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1561 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1562
1563 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1564 assert!(labels.contains(&"id"), "expected 'id', got: {:?}", labels);
1565 }
1566
1567 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1569 fn column_alias_non_dependency_empty() {
1570 let root = tempfile::tempdir().unwrap();
1571 let storage = root.path().join("models/mydb/storage");
1572 std::fs::create_dir_all(&storage).unwrap();
1573 std::fs::write(storage.join("orders.sql"), "CREATE TABLE orders (id INT);").unwrap();
1574 std::fs::write(storage.join("other.sql"), "CREATE TABLE other (x INT);").unwrap();
1575
1576 let compute = root.path().join("models/mydb/compute");
1577 std::fs::create_dir_all(&compute).unwrap();
1578 std::fs::write(
1580 compute.join("v.sql"),
1581 "CREATE VIEW v AS SELECT o.id FROM mydb.storage.orders o;",
1582 )
1583 .unwrap();
1584 write_types_lock(
1585 root.path(),
1586 &[
1587 (
1588 "mydb",
1589 "storage",
1590 "orders",
1591 "table",
1592 &[("id", "integer", false)],
1593 ),
1594 (
1595 "mydb",
1596 "storage",
1597 "other",
1598 "table",
1599 &[("x", "integer", false)],
1600 ),
1601 ],
1602 );
1603 let cache = build_cache(&root);
1604 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1605
1606 let prefix = PrefixContext {
1608 dots: 1,
1609 text: "other.",
1610 };
1611 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1612 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1613
1614 assert!(
1615 items.is_empty(),
1616 "expected empty for non-dependency alias, got: {:?}",
1617 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1618 );
1619 }
1620
1621 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1623 fn column_alias_multiple_joins() {
1624 let root = tempfile::tempdir().unwrap();
1625 let storage = root.path().join("models/mydb/storage");
1626 std::fs::create_dir_all(&storage).unwrap();
1627 std::fs::write(storage.join("t1.sql"), "CREATE TABLE t1 (a INT);").unwrap();
1628 std::fs::write(storage.join("t2.sql"), "CREATE TABLE t2 (b INT);").unwrap();
1629
1630 let compute = root.path().join("models/mydb/compute");
1631 std::fs::create_dir_all(&compute).unwrap();
1632 std::fs::write(
1633 compute.join("v.sql"),
1634 "CREATE VIEW v AS SELECT x.a, y.b FROM mydb.storage.t1 x JOIN mydb.storage.t2 y ON x.a = y.b;",
1635 )
1636 .unwrap();
1637 write_types_lock(
1638 root.path(),
1639 &[
1640 ("mydb", "storage", "t1", "table", &[("a", "integer", false)]),
1641 ("mydb", "storage", "t2", "table", &[("b", "integer", false)]),
1642 ],
1643 );
1644 let cache = build_cache(&root);
1645 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1646
1647 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1648
1649 let prefix_x = PrefixContext {
1651 dots: 1,
1652 text: "x.",
1653 };
1654 let items_x = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix_x);
1655 let labels_x: Vec<&str> = items_x.iter().map(|i| i.label.as_str()).collect();
1656 assert!(
1657 labels_x.contains(&"a"),
1658 "expected 'a' for x., got: {:?}",
1659 labels_x
1660 );
1661
1662 let prefix_y = PrefixContext {
1664 dots: 1,
1665 text: "y.",
1666 };
1667 let items_y = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix_y);
1668 let labels_y: Vec<&str> = items_y.iter().map(|i| i.label.as_str()).collect();
1669 assert!(
1670 labels_y.contains(&"b"),
1671 "expected 'b' for y., got: {:?}",
1672 labels_y
1673 );
1674 }
1675
1676 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1678 fn column_alias_case_insensitive() {
1679 let root = tempfile::tempdir().unwrap();
1680 let storage = root.path().join("models/mydb/storage");
1681 std::fs::create_dir_all(&storage).unwrap();
1682 std::fs::write(storage.join("orders.sql"), "CREATE TABLE orders (id INT);").unwrap();
1683
1684 let compute = root.path().join("models/mydb/compute");
1685 std::fs::create_dir_all(&compute).unwrap();
1686 std::fs::write(
1687 compute.join("v.sql"),
1688 "CREATE VIEW v AS SELECT O.id FROM mydb.storage.orders O;",
1689 )
1690 .unwrap();
1691 write_types_lock(
1692 root.path(),
1693 &[(
1694 "mydb",
1695 "storage",
1696 "orders",
1697 "table",
1698 &[("id", "integer", false)],
1699 )],
1700 );
1701 let cache = build_cache(&root);
1702 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1703
1704 let prefix = PrefixContext {
1706 dots: 1,
1707 text: "o.",
1708 };
1709 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1710 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1711
1712 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1713 assert!(
1714 labels.contains(&"id"),
1715 "expected 'id' with case-insensitive alias, got: {:?}",
1716 labels
1717 );
1718 }
1719
1720 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1722 fn column_alias_non_query_stmt_empty_map() {
1723 let root = tempfile::tempdir().unwrap();
1724 let storage = root.path().join("models/mydb/storage");
1725 std::fs::create_dir_all(&storage).unwrap();
1726 std::fs::write(storage.join("t.sql"), "CREATE TABLE t (id INT);").unwrap();
1727 write_types_lock(
1728 root.path(),
1729 &[("mydb", "storage", "t", "table", &[("id", "integer", false)])],
1730 );
1731 let cache = build_cache(&root);
1732 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1733
1734 let prefix = PrefixContext {
1737 dots: 1,
1738 text: "t.",
1739 };
1740 let uri = Url::from_file_path(root.path().join("models/mydb/storage/t.sql")).unwrap();
1741 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1742
1743 assert!(
1745 items.is_empty(),
1746 "expected empty for non-query statement self-reference, got: {:?}",
1747 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1748 );
1749 }
1750
1751 #[mz_ore::test]
1752 fn prefix_context_uses_utf16_cursor_positions() {
1753 let text = "SELECT 😀foo";
1754 let ctx = prefix_context(text, Position::new(0, 12));
1755 assert_eq!(ctx.dots, 0);
1756 assert_eq!(ctx.text, "foo");
1757 }
1758
1759 #[mz_ore::test]
1760 fn qualify_same_schema_bare_label() {
1761 let id = ObjectId::new("mydb".to_string(), "public".to_string(), "foo".to_string());
1762 let prefix = no_prefix();
1763 let result = qualify_and_filter(&id, "mydb", "public", &prefix);
1764 assert_eq!(result, Some(("foo".to_string(), "1_foo".to_string())));
1765 }
1766
1767 #[mz_ore::test]
1768 fn qualify_cross_schema_qualified() {
1769 let id = ObjectId::new("mydb".to_string(), "other".to_string(), "bar".to_string());
1770 let prefix = no_prefix();
1771 let result = qualify_and_filter(&id, "mydb", "public", &prefix);
1772 assert_eq!(
1773 result,
1774 Some(("other.bar".to_string(), "2_other.bar".to_string()))
1775 );
1776 }
1777
1778 #[mz_ore::test]
1779 fn qualify_cross_database_fully_qualified() {
1780 let id = ObjectId::new("otherdb".to_string(), "s".to_string(), "x".to_string());
1781 let prefix = no_prefix();
1782 let result = qualify_and_filter(&id, "mydb", "public", &prefix);
1783 assert_eq!(
1784 result,
1785 Some(("otherdb.s.x".to_string(), "3_otherdb.s.x".to_string()))
1786 );
1787 }
1788
1789 #[mz_ore::test]
1790 fn qualify_dotted_prefix_matches_schema_qualified() {
1791 let id = ObjectId::new("mydb".to_string(), "public".to_string(), "foo".to_string());
1792 let prefix = PrefixContext {
1793 dots: 1,
1794 text: "public.",
1795 };
1796 let result = qualify_and_filter(&id, "mydb", "public", &prefix);
1797 assert_eq!(
1798 result,
1799 Some(("foo".to_string(), "1_public.foo".to_string()))
1800 );
1801 }
1802
1803 #[mz_ore::test]
1804 fn qualify_dotted_prefix_no_match() {
1805 let id = ObjectId::new("mydb".to_string(), "public".to_string(), "foo".to_string());
1806 let prefix = PrefixContext {
1807 dots: 1,
1808 text: "other.",
1809 };
1810 let result = qualify_and_filter(&id, "mydb", "public", &prefix);
1811 assert_eq!(result, None);
1812 }
1813
1814 #[mz_ore::test]
1815 fn qualify_case_insensitive() {
1816 let id = ObjectId::new("mydb".to_string(), "public".to_string(), "foo".to_string());
1817 let prefix = PrefixContext {
1818 dots: 1,
1819 text: "PUBLIC.F",
1820 };
1821 let result = qualify_and_filter(&id, "mydb", "public", &prefix);
1822 assert_eq!(
1823 result,
1824 Some(("foo".to_string(), "1_public.foo".to_string()))
1825 );
1826 }
1827
1828 #[mz_ore::test]
1829 fn resolve_qualified_object_alias_hit() {
1830 let mut aliases = BTreeMap::new();
1831 aliases.insert("o".to_string(), "mydb.storage.orders".to_string());
1832 let result = resolve_qualified_object("o", &aliases, "mydb", "public");
1833 assert_eq!(
1834 result,
1835 Some(ObjectId::new(
1836 "mydb".to_string(),
1837 "storage".to_string(),
1838 "orders".to_string()
1839 ))
1840 );
1841 }
1842
1843 #[mz_ore::test]
1844 fn resolve_qualified_object_bare_fallback() {
1845 let aliases = BTreeMap::new();
1846 let result = resolve_qualified_object("foo", &aliases, "mydb", "public");
1847 assert_eq!(
1848 result,
1849 Some(ObjectId::new(
1850 "mydb".to_string(),
1851 "public".to_string(),
1852 "foo".to_string()
1853 ))
1854 );
1855 }
1856
1857 #[mz_ore::test]
1858 fn resolve_qualified_object_two_parts() {
1859 let aliases = BTreeMap::new();
1860 let result = resolve_qualified_object("storage.orders", &aliases, "mydb", "public");
1861 assert_eq!(
1862 result,
1863 Some(ObjectId::new(
1864 "mydb".to_string(),
1865 "storage".to_string(),
1866 "orders".to_string()
1867 ))
1868 );
1869 }
1870
1871 #[mz_ore::test]
1872 fn resolve_qualified_object_three_parts() {
1873 let aliases = BTreeMap::new();
1874 let result = resolve_qualified_object("otherdb.s.x", &aliases, "mydb", "public");
1875 assert_eq!(
1876 result,
1877 Some(ObjectId::new(
1878 "otherdb".to_string(),
1879 "s".to_string(),
1880 "x".to_string()
1881 ))
1882 );
1883 }
1884
1885 #[mz_ore::test]
1886 fn resolve_qualified_object_four_parts_none() {
1887 let aliases = BTreeMap::new();
1888 let result = resolve_qualified_object("a.b.c.d", &aliases, "mydb", "public");
1889 assert_eq!(result, None);
1890 }
1891}