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 #[cfg_attr(miri, ignore)] fn prefix_no_prefix() {
647 let text = "SELECT ";
648 let ctx = prefix_context(text, Position::new(0, 7));
649 assert_eq!(ctx.dots, 0);
650 assert_eq!(ctx.text, "");
651 }
652
653 #[mz_ore::test]
654 #[cfg_attr(miri, ignore)] fn prefix_bare_ident() {
656 let text = "SELECT foo";
657 let ctx = prefix_context(text, Position::new(0, 10));
658 assert_eq!(ctx.dots, 0);
659 assert_eq!(ctx.text, "foo");
660 }
661
662 #[mz_ore::test]
663 #[cfg_attr(miri, ignore)] fn prefix_one_dot() {
665 let text = "SELECT schema.foo";
666 let ctx = prefix_context(text, Position::new(0, 17));
667 assert_eq!(ctx.dots, 1);
668 assert_eq!(ctx.text, "schema.foo");
669 }
670
671 #[mz_ore::test]
672 #[cfg_attr(miri, ignore)] fn prefix_two_dots() {
674 let text = "SELECT db.schema.foo";
675 let ctx = prefix_context(text, Position::new(0, 20));
676 assert_eq!(ctx.dots, 2);
677 assert_eq!(ctx.text, "db.schema.foo");
678 }
679
680 #[mz_ore::test]
681 #[cfg_attr(miri, ignore)] fn prefix_mid_line() {
683 let text = "SELECT * FROM schema.f";
684 let ctx = prefix_context(text, Position::new(0, 22));
685 assert_eq!(ctx.dots, 1);
686 assert_eq!(ctx.text, "schema.f");
687 }
688
689 #[mz_ore::test]
690 #[cfg_attr(miri, ignore)] fn prefix_text_stored() {
692 let text = "SELECT public.f";
693 let ctx = prefix_context(text, Position::new(0, 15));
694 assert_eq!(ctx.dots, 1);
695 assert_eq!(ctx.text, "public.f");
696 }
697
698 #[cfg_attr(miri, ignore)] #[mz_ore::test]
700 fn same_schema_bare_name() {
701 let root = tempfile::tempdir().unwrap();
702 let dir = root.path().join("models/mydb/public");
703 std::fs::create_dir_all(&dir).unwrap();
704 std::fs::write(dir.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;").unwrap();
705 std::fs::write(dir.join("bar.sql"), "CREATE VIEW bar AS SELECT * FROM foo;").unwrap();
706 let cache = build_cache(&root);
707
708 let uri = Url::from_file_path(root.path().join("models/mydb/public/bar.sql")).unwrap();
709 let items = object_completions(&cache, None, &uri, root.path(), &no_prefix());
710
711 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
712 assert!(
713 labels.contains(&"foo"),
714 "expected bare 'foo', got: {:?}",
715 labels
716 );
717 assert!(
718 labels.contains(&"bar"),
719 "expected bare 'bar', got: {:?}",
720 labels
721 );
722 }
723
724 #[cfg_attr(miri, ignore)] #[mz_ore::test]
726 fn cross_schema_qualified() {
727 let root = tempfile::tempdir().unwrap();
728 let public = root.path().join("models/mydb/public");
729 std::fs::create_dir_all(&public).unwrap();
730 std::fs::write(public.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
731
732 let other = root.path().join("models/mydb/other");
733 std::fs::create_dir_all(&other).unwrap();
734 std::fs::write(
735 other.join("baz.sql"),
736 "CREATE VIEW baz AS SELECT * FROM mydb.public.foo;",
737 )
738 .unwrap();
739 let cache = build_cache(&root);
740
741 let uri = Url::from_file_path(root.path().join("models/mydb/other/baz.sql")).unwrap();
743 let items = object_completions(&cache, None, &uri, root.path(), &no_prefix());
744
745 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
746 assert!(
747 labels.contains(&"public.foo"),
748 "expected 'public.foo', got: {:?}",
749 labels
750 );
751 assert!(
753 labels.contains(&"baz"),
754 "expected bare 'baz', got: {:?}",
755 labels
756 );
757 }
758
759 #[cfg_attr(miri, ignore)] #[mz_ore::test]
761 fn cross_database_fully_qualified() {
762 let root = tempfile::tempdir().unwrap();
763 let db1 = root.path().join("models/mydb/public");
764 std::fs::create_dir_all(&db1).unwrap();
765 std::fs::write(db1.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;").unwrap();
766
767 let db2 = root.path().join("models/otherdb/public");
768 std::fs::create_dir_all(&db2).unwrap();
769 std::fs::write(db2.join("bar.sql"), "CREATE VIEW bar AS SELECT 1 AS id;").unwrap();
770 let cache = build_cache(&root);
771
772 let uri = Url::from_file_path(root.path().join("models/otherdb/public/bar.sql")).unwrap();
774 let items = object_completions(&cache, None, &uri, root.path(), &no_prefix());
775
776 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
777 assert!(
778 labels.contains(&"mydb.public.foo"),
779 "expected 'mydb.public.foo', got: {:?}",
780 labels
781 );
782 }
783
784 #[cfg_attr(miri, ignore)] #[mz_ore::test]
786 fn external_deps_included() {
787 let root = tempfile::tempdir().unwrap();
788 let dir = root.path().join("models/mydb/public");
789 std::fs::create_dir_all(&dir).unwrap();
790 std::fs::write(
791 dir.join("foo.sql"),
792 "CREATE VIEW foo AS SELECT * FROM mydb.ext.src;",
793 )
794 .unwrap();
795
796 std::fs::write(
798 root.path().join("types.lock"),
799 "version = 1\n\n\
800 [[source]]\n\
801 name = \"mydb.ext.src\"\n\
802 \n\
803 [[source.columns]]\n\
804 name = \"id\"\n\
805 type = \"integer\"\n\
806 nullable = false\n",
807 )
808 .unwrap();
809 let cache = build_cache(&root);
810
811 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
812 let uri = Url::from_file_path(root.path().join("models/mydb/public/foo.sql")).unwrap();
813 let items = object_completions(&cache, Some(&types_cache), &uri, root.path(), &no_prefix());
814
815 let ext_items: Vec<_> = items
816 .iter()
817 .filter(|i| i.detail.as_deref() == Some("source (external)"))
818 .collect();
819 assert_eq!(ext_items.len(), 1, "expected one external source");
820 assert_eq!(ext_items[0].label, "ext.src");
821 }
822
823 #[cfg_attr(miri, ignore)] #[mz_ore::test]
825 fn kind_mapping() {
826 let root = tempfile::tempdir().unwrap();
827 let storage = root.path().join("models/mydb/storage");
829 std::fs::create_dir_all(&storage).unwrap();
830 std::fs::write(storage.join("t.sql"), "CREATE TABLE t (id INT);").unwrap();
831
832 let compute = root.path().join("models/mydb/compute");
833 std::fs::create_dir_all(&compute).unwrap();
834 std::fs::write(
835 compute.join("v.sql"),
836 "CREATE VIEW v AS SELECT * FROM mydb.storage.t;",
837 )
838 .unwrap();
839 let cache = build_cache(&root);
840
841 let uri = Url::from_file_path(root.path().join("models/mydb/storage/t.sql")).unwrap();
842 let items = object_completions(&cache, None, &uri, root.path(), &no_prefix());
843
844 let table_item = items.iter().find(|i| i.label == "t").unwrap();
845 assert_eq!(table_item.detail.as_deref(), Some("table"));
846 assert_eq!(table_item.kind, Some(CompletionItemKind::STRUCT));
847
848 let view_item = items.iter().find(|i| i.label.ends_with("v")).unwrap();
849 assert_eq!(view_item.detail.as_deref(), Some("view"));
850 assert_eq!(view_item.kind, Some(CompletionItemKind::STRUCT));
851 }
852
853 #[cfg_attr(miri, ignore)] #[mz_ore::test]
855 fn file_outside_models_returns_empty() {
856 let root = tempfile::tempdir().unwrap();
857 let dir = root.path().join("models/mydb/public");
858 std::fs::create_dir_all(&dir).unwrap();
859 std::fs::write(dir.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;").unwrap();
860 let cache = build_cache(&root);
861
862 let uri = Url::from_file_path(root.path().join("random/file.sql")).unwrap();
864 let items = object_completions(&cache, None, &uri, root.path(), &no_prefix());
865 assert!(items.is_empty());
866 }
867
868 #[cfg_attr(miri, ignore)] #[mz_ore::test]
870 fn schema_prefix_strips_label_to_bare_name() {
871 let root = tempfile::tempdir().unwrap();
872 let dir = root.path().join("models/mydb/public");
873 std::fs::create_dir_all(&dir).unwrap();
874 std::fs::write(dir.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;").unwrap();
875 std::fs::write(dir.join("bar.sql"), "CREATE VIEW bar AS SELECT * FROM foo;").unwrap();
876 let cache = build_cache(&root);
877
878 let prefix = PrefixContext {
879 dots: 1,
880 text: "public.",
881 };
882 let uri = Url::from_file_path(root.path().join("models/mydb/public/bar.sql")).unwrap();
883 let items = object_completions(&cache, None, &uri, root.path(), &prefix);
884
885 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
886 assert!(
888 labels.contains(&"foo"),
889 "expected bare 'foo', got: {:?}",
890 labels
891 );
892 assert!(
893 labels.contains(&"bar"),
894 "expected bare 'bar', got: {:?}",
895 labels
896 );
897 }
898
899 #[cfg_attr(miri, ignore)] #[mz_ore::test]
901 fn db_prefix_disambiguates_to_schema_dot_object() {
902 let root = tempfile::tempdir().unwrap();
903 let dir = root.path().join("models/mydb/public");
904 std::fs::create_dir_all(&dir).unwrap();
905 std::fs::write(dir.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;").unwrap();
906 let cache = build_cache(&root);
907
908 let prefix = PrefixContext {
910 dots: 1,
911 text: "mydb.",
912 };
913 let uri = Url::from_file_path(root.path().join("models/mydb/public/foo.sql")).unwrap();
914 let items = object_completions(&cache, None, &uri, root.path(), &prefix);
915
916 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
917 assert!(
919 labels.contains(&"public.foo"),
920 "expected 'public.foo', got: {:?}",
921 labels
922 );
923 }
924
925 #[cfg_attr(miri, ignore)] #[mz_ore::test]
927 fn full_qualification_strips_to_bare_name() {
928 let root = tempfile::tempdir().unwrap();
929 let dir = root.path().join("models/mydb/public");
930 std::fs::create_dir_all(&dir).unwrap();
931 std::fs::write(dir.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;").unwrap();
932 let cache = build_cache(&root);
933
934 let prefix = PrefixContext {
935 dots: 2,
936 text: "mydb.public.",
937 };
938 let uri = Url::from_file_path(root.path().join("models/mydb/public/foo.sql")).unwrap();
939 let items = object_completions(&cache, None, &uri, root.path(), &prefix);
940
941 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
942 assert!(
943 labels.contains(&"foo"),
944 "expected bare 'foo', got: {:?}",
945 labels
946 );
947 }
948
949 #[cfg_attr(miri, ignore)] #[mz_ore::test]
951 fn prefix_filters_non_matching_objects() {
952 let root = tempfile::tempdir().unwrap();
953 let public = root.path().join("models/mydb/public");
954 std::fs::create_dir_all(&public).unwrap();
955 std::fs::write(public.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
956
957 let other = root.path().join("models/mydb/other");
958 std::fs::create_dir_all(&other).unwrap();
959 std::fs::write(
960 other.join("baz.sql"),
961 "CREATE VIEW baz AS SELECT * FROM mydb.public.foo;",
962 )
963 .unwrap();
964 let cache = build_cache(&root);
965
966 let prefix = PrefixContext {
968 dots: 1,
969 text: "other.",
970 };
971 let uri = Url::from_file_path(root.path().join("models/mydb/other/baz.sql")).unwrap();
972 let items = object_completions(&cache, None, &uri, root.path(), &prefix);
973
974 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
975 assert!(labels.contains(&"baz"), "expected 'baz', got: {:?}", labels);
976 assert!(
978 !labels.iter().any(|l| l.contains("foo")),
979 "expected no 'foo' items, got: {:?}",
980 labels
981 );
982 }
983
984 fn write_types_lock(root: &Path, tables: &[(&str, &str, &str, &str, &[(&str, &str, bool)])]) {
986 let mut toml = String::from("version = 1\n\n");
987 for (db, schema, name, kind, columns) in tables {
988 toml.push_str(&format!(
989 "[[{}]]\nname = \"{}.{}.{}\"\n\n",
990 kind, db, schema, name
991 ));
992 for (col_name, col_type, nullable) in *columns {
993 toml.push_str(&format!(
994 "[[{}.columns]]\nname = \"{}\"\ntype = \"{}\"\nnullable = {}\n\n",
995 kind, col_name, col_type, nullable
996 ));
997 }
998 }
999 std::fs::write(root.join("types.lock"), toml).unwrap();
1000 }
1001
1002 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1004 fn column_deps_at_zero_dots() {
1005 let root = tempfile::tempdir().unwrap();
1006 let storage = root.path().join("models/mydb/storage");
1007 std::fs::create_dir_all(&storage).unwrap();
1008 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1009
1010 let compute = root.path().join("models/mydb/compute");
1011 std::fs::create_dir_all(&compute).unwrap();
1012 std::fs::write(
1013 compute.join("v.sql"),
1014 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1015 )
1016 .unwrap();
1017 write_types_lock(
1018 root.path(),
1019 &[(
1020 "mydb",
1021 "storage",
1022 "foo",
1023 "table",
1024 &[("id", "integer", false), ("bar", "text", true)],
1025 )],
1026 );
1027 let cache = build_cache(&root);
1028 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1029
1030 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1031 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &no_prefix());
1032
1033 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1034 assert!(labels.contains(&"id"), "expected 'id', got: {:?}", labels);
1035 assert!(labels.contains(&"bar"), "expected 'bar', got: {:?}", labels);
1036 }
1037
1038 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1040 fn column_deps_filtered_by_prefix() {
1041 let root = tempfile::tempdir().unwrap();
1042 let storage = root.path().join("models/mydb/storage");
1043 std::fs::create_dir_all(&storage).unwrap();
1044 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1045
1046 let compute = root.path().join("models/mydb/compute");
1047 std::fs::create_dir_all(&compute).unwrap();
1048 std::fs::write(
1049 compute.join("v.sql"),
1050 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1051 )
1052 .unwrap();
1053 write_types_lock(
1054 root.path(),
1055 &[(
1056 "mydb",
1057 "storage",
1058 "foo",
1059 "table",
1060 &[("id", "integer", false), ("name", "text", false)],
1061 )],
1062 );
1063 let cache = build_cache(&root);
1064 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1065
1066 let prefix = PrefixContext { dots: 0, text: "i" };
1067 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1068 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1069
1070 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1071 assert!(labels.contains(&"id"), "expected 'id', got: {:?}", labels);
1072 assert!(
1073 !labels.contains(&"name"),
1074 "should not contain 'name', got: {:?}",
1075 labels
1076 );
1077 }
1078
1079 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1081 fn column_deps_no_types_cache() {
1082 let root = tempfile::tempdir().unwrap();
1083 let storage = root.path().join("models/mydb/storage");
1084 std::fs::create_dir_all(&storage).unwrap();
1085 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1086
1087 let compute = root.path().join("models/mydb/compute");
1088 std::fs::create_dir_all(&compute).unwrap();
1089 std::fs::write(
1090 compute.join("v.sql"),
1091 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1092 )
1093 .unwrap();
1094 let cache = build_cache(&root);
1095
1096 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1097 let items = column_completions(&cache, None, &uri, root.path(), &no_prefix());
1098 assert!(items.is_empty(), "expected empty without types cache");
1099 }
1100
1101 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1103 fn column_deps_multiple_dependencies() {
1104 let root = tempfile::tempdir().unwrap();
1105 let storage = root.path().join("models/mydb/storage");
1106 std::fs::create_dir_all(&storage).unwrap();
1107 std::fs::write(storage.join("t1.sql"), "CREATE TABLE t1 (a INT);").unwrap();
1108 std::fs::write(storage.join("t2.sql"), "CREATE TABLE t2 (b INT);").unwrap();
1109
1110 let compute = root.path().join("models/mydb/compute");
1111 std::fs::create_dir_all(&compute).unwrap();
1112 std::fs::write(
1113 compute.join("v.sql"),
1114 "CREATE VIEW v AS SELECT * FROM mydb.storage.t1, mydb.storage.t2;",
1115 )
1116 .unwrap();
1117 write_types_lock(
1118 root.path(),
1119 &[
1120 ("mydb", "storage", "t1", "table", &[("a", "integer", false)]),
1121 ("mydb", "storage", "t2", "table", &[("b", "integer", false)]),
1122 ],
1123 );
1124 let cache = build_cache(&root);
1125 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1126
1127 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1128 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &no_prefix());
1129
1130 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1131 assert!(labels.contains(&"a"), "expected 'a', got: {:?}", labels);
1132 assert!(labels.contains(&"b"), "expected 'b', got: {:?}", labels);
1133 }
1134
1135 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1137 fn column_qualified_bare_object() {
1138 let root = tempfile::tempdir().unwrap();
1139 let storage = root.path().join("models/mydb/storage");
1140 std::fs::create_dir_all(&storage).unwrap();
1141 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1142
1143 let compute = root.path().join("models/mydb/compute");
1144 std::fs::create_dir_all(&compute).unwrap();
1145 std::fs::write(
1146 compute.join("v.sql"),
1147 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1148 )
1149 .unwrap();
1150 write_types_lock(
1151 root.path(),
1152 &[(
1153 "mydb",
1154 "storage",
1155 "foo",
1156 "table",
1157 &[("id", "integer", false), ("name", "text", false)],
1158 )],
1159 );
1160 let cache = build_cache(&root);
1161 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1162
1163 let prefix = PrefixContext {
1165 dots: 2,
1166 text: "storage.foo.",
1167 };
1168 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1169 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1170
1171 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1172 assert!(labels.contains(&"id"), "expected 'id', got: {:?}", labels);
1173 assert!(
1174 labels.contains(&"name"),
1175 "expected 'name', got: {:?}",
1176 labels
1177 );
1178 }
1179
1180 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1182 fn column_qualified_schema_object() {
1183 let root = tempfile::tempdir().unwrap();
1184 let storage = root.path().join("models/mydb/storage");
1185 std::fs::create_dir_all(&storage).unwrap();
1186 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1187
1188 let compute = root.path().join("models/mydb/compute");
1189 std::fs::create_dir_all(&compute).unwrap();
1190 std::fs::write(
1191 compute.join("v.sql"),
1192 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1193 )
1194 .unwrap();
1195 write_types_lock(
1196 root.path(),
1197 &[(
1198 "mydb",
1199 "storage",
1200 "foo",
1201 "table",
1202 &[("id", "integer", false), ("name", "text", false)],
1203 )],
1204 );
1205 let cache = build_cache(&root);
1206 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1207
1208 let prefix = PrefixContext {
1209 dots: 2,
1210 text: "storage.foo.i",
1211 };
1212 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1213 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1214
1215 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1216 assert!(labels.contains(&"id"), "expected 'id', got: {:?}", labels);
1217 assert!(
1218 !labels.contains(&"name"),
1219 "should not contain 'name', got: {:?}",
1220 labels
1221 );
1222 }
1223
1224 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1226 fn column_qualified_fully_qualified() {
1227 let root = tempfile::tempdir().unwrap();
1228 let storage = root.path().join("models/mydb/storage");
1229 std::fs::create_dir_all(&storage).unwrap();
1230 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1231
1232 let compute = root.path().join("models/mydb/compute");
1233 std::fs::create_dir_all(&compute).unwrap();
1234 std::fs::write(
1235 compute.join("v.sql"),
1236 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1237 )
1238 .unwrap();
1239 write_types_lock(
1240 root.path(),
1241 &[(
1242 "mydb",
1243 "storage",
1244 "foo",
1245 "table",
1246 &[("id", "integer", false)],
1247 )],
1248 );
1249 let cache = build_cache(&root);
1250 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1251
1252 let prefix = PrefixContext {
1253 dots: 3,
1254 text: "mydb.storage.foo.",
1255 };
1256 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1257 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1258
1259 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1260 assert!(labels.contains(&"id"), "expected 'id', got: {:?}", labels);
1261 }
1262
1263 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1265 fn column_qualified_non_dependency_excluded() {
1266 let root = tempfile::tempdir().unwrap();
1267 let storage = root.path().join("models/mydb/storage");
1268 std::fs::create_dir_all(&storage).unwrap();
1269 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1270 std::fs::write(storage.join("other.sql"), "CREATE TABLE other (x INT);").unwrap();
1271
1272 let compute = root.path().join("models/mydb/compute");
1273 std::fs::create_dir_all(&compute).unwrap();
1274 std::fs::write(
1275 compute.join("v.sql"),
1276 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1277 )
1278 .unwrap();
1279 write_types_lock(
1280 root.path(),
1281 &[
1282 (
1283 "mydb",
1284 "storage",
1285 "foo",
1286 "table",
1287 &[("id", "integer", false)],
1288 ),
1289 (
1290 "mydb",
1291 "storage",
1292 "other",
1293 "table",
1294 &[("x", "integer", false)],
1295 ),
1296 ],
1297 );
1298 let cache = build_cache(&root);
1299 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1300
1301 let prefix = PrefixContext {
1303 dots: 2,
1304 text: "storage.other.",
1305 };
1306 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1307 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1308
1309 assert!(
1310 items.is_empty(),
1311 "expected empty for non-dependency, got: {:?}",
1312 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1313 );
1314 }
1315
1316 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1318 fn column_qualified_object_not_in_cache() {
1319 let root = tempfile::tempdir().unwrap();
1320 let storage = root.path().join("models/mydb/storage");
1321 std::fs::create_dir_all(&storage).unwrap();
1322 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1323
1324 let compute = root.path().join("models/mydb/compute");
1325 std::fs::create_dir_all(&compute).unwrap();
1326 std::fs::write(
1327 compute.join("v.sql"),
1328 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1329 )
1330 .unwrap();
1331 write_types_lock(root.path(), &[]);
1333 let cache = build_cache(&root);
1334 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1335
1336 let prefix = PrefixContext {
1338 dots: 2,
1339 text: "storage.foo.",
1340 };
1341 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1342 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1343
1344 assert!(items.is_empty(), "expected empty when object not in cache");
1345 }
1346
1347 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1349 fn column_qualified_filter_case_insensitive() {
1350 let root = tempfile::tempdir().unwrap();
1351 let storage = root.path().join("models/mydb/storage");
1352 std::fs::create_dir_all(&storage).unwrap();
1353 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1354
1355 let compute = root.path().join("models/mydb/compute");
1356 std::fs::create_dir_all(&compute).unwrap();
1357 std::fs::write(
1358 compute.join("v.sql"),
1359 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1360 )
1361 .unwrap();
1362 write_types_lock(
1363 root.path(),
1364 &[(
1365 "mydb",
1366 "storage",
1367 "foo",
1368 "table",
1369 &[("id", "integer", false), ("name", "text", false)],
1370 )],
1371 );
1372 let cache = build_cache(&root);
1373 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1374
1375 let prefix = PrefixContext {
1377 dots: 2,
1378 text: "storage.foo.I",
1379 };
1380 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1381 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1382
1383 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1384 assert!(
1385 labels.contains(&"id"),
1386 "expected 'id' with case-insensitive match, got: {:?}",
1387 labels
1388 );
1389 assert!(
1390 !labels.contains(&"name"),
1391 "should not contain 'name', got: {:?}",
1392 labels
1393 );
1394 }
1395
1396 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1398 fn column_kind_and_detail() {
1399 let root = tempfile::tempdir().unwrap();
1400 let storage = root.path().join("models/mydb/storage");
1401 std::fs::create_dir_all(&storage).unwrap();
1402 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1403
1404 let compute = root.path().join("models/mydb/compute");
1405 std::fs::create_dir_all(&compute).unwrap();
1406 std::fs::write(
1407 compute.join("v.sql"),
1408 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1409 )
1410 .unwrap();
1411 write_types_lock(
1412 root.path(),
1413 &[(
1414 "mydb",
1415 "storage",
1416 "foo",
1417 "table",
1418 &[("id", "integer", false), ("name", "text", true)],
1419 )],
1420 );
1421 let cache = build_cache(&root);
1422 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1423
1424 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1425 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &no_prefix());
1426
1427 let id_item = items.iter().find(|i| i.label == "id").unwrap();
1428 assert_eq!(id_item.kind, Some(CompletionItemKind::FIELD));
1429 assert_eq!(id_item.detail.as_deref(), Some("integer"));
1430
1431 let name_item = items.iter().find(|i| i.label == "name").unwrap();
1432 assert_eq!(name_item.kind, Some(CompletionItemKind::FIELD));
1433 assert_eq!(name_item.detail.as_deref(), Some("text (nullable)"));
1434 }
1435
1436 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1438 fn column_sort_before_objects() {
1439 let root = tempfile::tempdir().unwrap();
1440 let storage = root.path().join("models/mydb/storage");
1441 std::fs::create_dir_all(&storage).unwrap();
1442 std::fs::write(storage.join("foo.sql"), "CREATE TABLE foo (id INT);").unwrap();
1443
1444 let compute = root.path().join("models/mydb/compute");
1445 std::fs::create_dir_all(&compute).unwrap();
1446 std::fs::write(
1447 compute.join("v.sql"),
1448 "CREATE VIEW v AS SELECT * FROM mydb.storage.foo;",
1449 )
1450 .unwrap();
1451 write_types_lock(
1452 root.path(),
1453 &[(
1454 "mydb",
1455 "storage",
1456 "foo",
1457 "table",
1458 &[("id", "integer", false)],
1459 )],
1460 );
1461 let cache = build_cache(&root);
1462 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1463
1464 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1465 let col_items =
1466 column_completions(&cache, Some(&types_cache), &uri, root.path(), &no_prefix());
1467 let obj_items =
1468 object_completions(&cache, Some(&types_cache), &uri, root.path(), &no_prefix());
1469
1470 for item in &col_items {
1472 assert!(
1473 item.sort_text.as_ref().unwrap().starts_with("0_"),
1474 "column sort_text should start with '0_', got: {:?}",
1475 item.sort_text
1476 );
1477 }
1478 for item in &obj_items {
1479 let sort = item.sort_text.as_ref().unwrap();
1480 assert!(
1481 sort.starts_with("1_") || sort.starts_with("2_") || sort.starts_with("3_"),
1482 "object sort_text should start with '1_'/'2_'/'3_', got: {:?}",
1483 sort
1484 );
1485 }
1486 }
1487
1488 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1490 fn column_alias_explicit() {
1491 let root = tempfile::tempdir().unwrap();
1492 let storage = root.path().join("models/mydb/storage");
1493 std::fs::create_dir_all(&storage).unwrap();
1494 std::fs::write(storage.join("orders.sql"), "CREATE TABLE orders (id INT);").unwrap();
1495
1496 let compute = root.path().join("models/mydb/compute");
1497 std::fs::create_dir_all(&compute).unwrap();
1498 std::fs::write(
1499 compute.join("v.sql"),
1500 "CREATE VIEW v AS SELECT o.id FROM mydb.storage.orders o;",
1501 )
1502 .unwrap();
1503 write_types_lock(
1504 root.path(),
1505 &[(
1506 "mydb",
1507 "storage",
1508 "orders",
1509 "table",
1510 &[("id", "integer", false), ("name", "text", false)],
1511 )],
1512 );
1513 let cache = build_cache(&root);
1514 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1515
1516 let prefix = PrefixContext {
1518 dots: 1,
1519 text: "o.",
1520 };
1521 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1522 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1523
1524 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1525 assert!(labels.contains(&"id"), "expected 'id', got: {:?}", labels);
1526 assert!(
1527 labels.contains(&"name"),
1528 "expected 'name', got: {:?}",
1529 labels
1530 );
1531 }
1532
1533 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1535 fn column_alias_bare_table_name() {
1536 let root = tempfile::tempdir().unwrap();
1537 let storage = root.path().join("models/mydb/storage");
1538 std::fs::create_dir_all(&storage).unwrap();
1539 std::fs::write(storage.join("orders.sql"), "CREATE TABLE orders (id INT);").unwrap();
1540
1541 let compute = root.path().join("models/mydb/compute");
1542 std::fs::create_dir_all(&compute).unwrap();
1543 std::fs::write(
1544 compute.join("v.sql"),
1545 "CREATE VIEW v AS SELECT * FROM mydb.storage.orders;",
1546 )
1547 .unwrap();
1548 write_types_lock(
1549 root.path(),
1550 &[(
1551 "mydb",
1552 "storage",
1553 "orders",
1554 "table",
1555 &[("id", "integer", false)],
1556 )],
1557 );
1558 let cache = build_cache(&root);
1559 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1560
1561 let prefix = PrefixContext {
1563 dots: 1,
1564 text: "orders.",
1565 };
1566 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1567 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1568
1569 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1570 assert!(labels.contains(&"id"), "expected 'id', got: {:?}", labels);
1571 }
1572
1573 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1575 fn column_alias_non_dependency_empty() {
1576 let root = tempfile::tempdir().unwrap();
1577 let storage = root.path().join("models/mydb/storage");
1578 std::fs::create_dir_all(&storage).unwrap();
1579 std::fs::write(storage.join("orders.sql"), "CREATE TABLE orders (id INT);").unwrap();
1580 std::fs::write(storage.join("other.sql"), "CREATE TABLE other (x INT);").unwrap();
1581
1582 let compute = root.path().join("models/mydb/compute");
1583 std::fs::create_dir_all(&compute).unwrap();
1584 std::fs::write(
1586 compute.join("v.sql"),
1587 "CREATE VIEW v AS SELECT o.id FROM mydb.storage.orders o;",
1588 )
1589 .unwrap();
1590 write_types_lock(
1591 root.path(),
1592 &[
1593 (
1594 "mydb",
1595 "storage",
1596 "orders",
1597 "table",
1598 &[("id", "integer", false)],
1599 ),
1600 (
1601 "mydb",
1602 "storage",
1603 "other",
1604 "table",
1605 &[("x", "integer", false)],
1606 ),
1607 ],
1608 );
1609 let cache = build_cache(&root);
1610 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1611
1612 let prefix = PrefixContext {
1614 dots: 1,
1615 text: "other.",
1616 };
1617 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1618 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1619
1620 assert!(
1621 items.is_empty(),
1622 "expected empty for non-dependency alias, got: {:?}",
1623 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1624 );
1625 }
1626
1627 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1629 fn column_alias_multiple_joins() {
1630 let root = tempfile::tempdir().unwrap();
1631 let storage = root.path().join("models/mydb/storage");
1632 std::fs::create_dir_all(&storage).unwrap();
1633 std::fs::write(storage.join("t1.sql"), "CREATE TABLE t1 (a INT);").unwrap();
1634 std::fs::write(storage.join("t2.sql"), "CREATE TABLE t2 (b INT);").unwrap();
1635
1636 let compute = root.path().join("models/mydb/compute");
1637 std::fs::create_dir_all(&compute).unwrap();
1638 std::fs::write(
1639 compute.join("v.sql"),
1640 "CREATE VIEW v AS SELECT x.a, y.b FROM mydb.storage.t1 x JOIN mydb.storage.t2 y ON x.a = y.b;",
1641 )
1642 .unwrap();
1643 write_types_lock(
1644 root.path(),
1645 &[
1646 ("mydb", "storage", "t1", "table", &[("a", "integer", false)]),
1647 ("mydb", "storage", "t2", "table", &[("b", "integer", false)]),
1648 ],
1649 );
1650 let cache = build_cache(&root);
1651 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1652
1653 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1654
1655 let prefix_x = PrefixContext {
1657 dots: 1,
1658 text: "x.",
1659 };
1660 let items_x = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix_x);
1661 let labels_x: Vec<&str> = items_x.iter().map(|i| i.label.as_str()).collect();
1662 assert!(
1663 labels_x.contains(&"a"),
1664 "expected 'a' for x., got: {:?}",
1665 labels_x
1666 );
1667
1668 let prefix_y = PrefixContext {
1670 dots: 1,
1671 text: "y.",
1672 };
1673 let items_y = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix_y);
1674 let labels_y: Vec<&str> = items_y.iter().map(|i| i.label.as_str()).collect();
1675 assert!(
1676 labels_y.contains(&"b"),
1677 "expected 'b' for y., got: {:?}",
1678 labels_y
1679 );
1680 }
1681
1682 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1684 fn column_alias_case_insensitive() {
1685 let root = tempfile::tempdir().unwrap();
1686 let storage = root.path().join("models/mydb/storage");
1687 std::fs::create_dir_all(&storage).unwrap();
1688 std::fs::write(storage.join("orders.sql"), "CREATE TABLE orders (id INT);").unwrap();
1689
1690 let compute = root.path().join("models/mydb/compute");
1691 std::fs::create_dir_all(&compute).unwrap();
1692 std::fs::write(
1693 compute.join("v.sql"),
1694 "CREATE VIEW v AS SELECT O.id FROM mydb.storage.orders O;",
1695 )
1696 .unwrap();
1697 write_types_lock(
1698 root.path(),
1699 &[(
1700 "mydb",
1701 "storage",
1702 "orders",
1703 "table",
1704 &[("id", "integer", false)],
1705 )],
1706 );
1707 let cache = build_cache(&root);
1708 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1709
1710 let prefix = PrefixContext {
1712 dots: 1,
1713 text: "o.",
1714 };
1715 let uri = Url::from_file_path(root.path().join("models/mydb/compute/v.sql")).unwrap();
1716 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1717
1718 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1719 assert!(
1720 labels.contains(&"id"),
1721 "expected 'id' with case-insensitive alias, got: {:?}",
1722 labels
1723 );
1724 }
1725
1726 #[cfg_attr(miri, ignore)] #[mz_ore::test]
1728 fn column_alias_non_query_stmt_empty_map() {
1729 let root = tempfile::tempdir().unwrap();
1730 let storage = root.path().join("models/mydb/storage");
1731 std::fs::create_dir_all(&storage).unwrap();
1732 std::fs::write(storage.join("t.sql"), "CREATE TABLE t (id INT);").unwrap();
1733 write_types_lock(
1734 root.path(),
1735 &[("mydb", "storage", "t", "table", &[("id", "integer", false)])],
1736 );
1737 let cache = build_cache(&root);
1738 let types_cache = crate::types::load_types_lock(root.path()).unwrap();
1739
1740 let prefix = PrefixContext {
1743 dots: 1,
1744 text: "t.",
1745 };
1746 let uri = Url::from_file_path(root.path().join("models/mydb/storage/t.sql")).unwrap();
1747 let items = column_completions(&cache, Some(&types_cache), &uri, root.path(), &prefix);
1748
1749 assert!(
1751 items.is_empty(),
1752 "expected empty for non-query statement self-reference, got: {:?}",
1753 items.iter().map(|i| &i.label).collect::<Vec<_>>()
1754 );
1755 }
1756
1757 #[mz_ore::test]
1758 fn prefix_context_uses_utf16_cursor_positions() {
1759 let text = "SELECT 😀foo";
1760 let ctx = prefix_context(text, Position::new(0, 12));
1761 assert_eq!(ctx.dots, 0);
1762 assert_eq!(ctx.text, "foo");
1763 }
1764
1765 #[mz_ore::test]
1766 fn qualify_same_schema_bare_label() {
1767 let id = ObjectId::new("mydb".to_string(), "public".to_string(), "foo".to_string());
1768 let prefix = no_prefix();
1769 let result = qualify_and_filter(&id, "mydb", "public", &prefix);
1770 assert_eq!(result, Some(("foo".to_string(), "1_foo".to_string())));
1771 }
1772
1773 #[mz_ore::test]
1774 fn qualify_cross_schema_qualified() {
1775 let id = ObjectId::new("mydb".to_string(), "other".to_string(), "bar".to_string());
1776 let prefix = no_prefix();
1777 let result = qualify_and_filter(&id, "mydb", "public", &prefix);
1778 assert_eq!(
1779 result,
1780 Some(("other.bar".to_string(), "2_other.bar".to_string()))
1781 );
1782 }
1783
1784 #[mz_ore::test]
1785 fn qualify_cross_database_fully_qualified() {
1786 let id = ObjectId::new("otherdb".to_string(), "s".to_string(), "x".to_string());
1787 let prefix = no_prefix();
1788 let result = qualify_and_filter(&id, "mydb", "public", &prefix);
1789 assert_eq!(
1790 result,
1791 Some(("otherdb.s.x".to_string(), "3_otherdb.s.x".to_string()))
1792 );
1793 }
1794
1795 #[mz_ore::test]
1796 fn qualify_dotted_prefix_matches_schema_qualified() {
1797 let id = ObjectId::new("mydb".to_string(), "public".to_string(), "foo".to_string());
1798 let prefix = PrefixContext {
1799 dots: 1,
1800 text: "public.",
1801 };
1802 let result = qualify_and_filter(&id, "mydb", "public", &prefix);
1803 assert_eq!(
1804 result,
1805 Some(("foo".to_string(), "1_public.foo".to_string()))
1806 );
1807 }
1808
1809 #[mz_ore::test]
1810 fn qualify_dotted_prefix_no_match() {
1811 let id = ObjectId::new("mydb".to_string(), "public".to_string(), "foo".to_string());
1812 let prefix = PrefixContext {
1813 dots: 1,
1814 text: "other.",
1815 };
1816 let result = qualify_and_filter(&id, "mydb", "public", &prefix);
1817 assert_eq!(result, None);
1818 }
1819
1820 #[mz_ore::test]
1821 fn qualify_case_insensitive() {
1822 let id = ObjectId::new("mydb".to_string(), "public".to_string(), "foo".to_string());
1823 let prefix = PrefixContext {
1824 dots: 1,
1825 text: "PUBLIC.F",
1826 };
1827 let result = qualify_and_filter(&id, "mydb", "public", &prefix);
1828 assert_eq!(
1829 result,
1830 Some(("foo".to_string(), "1_public.foo".to_string()))
1831 );
1832 }
1833
1834 #[mz_ore::test]
1835 fn resolve_qualified_object_alias_hit() {
1836 let mut aliases = BTreeMap::new();
1837 aliases.insert("o".to_string(), "mydb.storage.orders".to_string());
1838 let result = resolve_qualified_object("o", &aliases, "mydb", "public");
1839 assert_eq!(
1840 result,
1841 Some(ObjectId::new(
1842 "mydb".to_string(),
1843 "storage".to_string(),
1844 "orders".to_string()
1845 ))
1846 );
1847 }
1848
1849 #[mz_ore::test]
1850 fn resolve_qualified_object_bare_fallback() {
1851 let aliases = BTreeMap::new();
1852 let result = resolve_qualified_object("foo", &aliases, "mydb", "public");
1853 assert_eq!(
1854 result,
1855 Some(ObjectId::new(
1856 "mydb".to_string(),
1857 "public".to_string(),
1858 "foo".to_string()
1859 ))
1860 );
1861 }
1862
1863 #[mz_ore::test]
1864 fn resolve_qualified_object_two_parts() {
1865 let aliases = BTreeMap::new();
1866 let result = resolve_qualified_object("storage.orders", &aliases, "mydb", "public");
1867 assert_eq!(
1868 result,
1869 Some(ObjectId::new(
1870 "mydb".to_string(),
1871 "storage".to_string(),
1872 "orders".to_string()
1873 ))
1874 );
1875 }
1876
1877 #[mz_ore::test]
1878 fn resolve_qualified_object_three_parts() {
1879 let aliases = BTreeMap::new();
1880 let result = resolve_qualified_object("otherdb.s.x", &aliases, "mydb", "public");
1881 assert_eq!(
1882 result,
1883 Some(ObjectId::new(
1884 "otherdb".to_string(),
1885 "s".to_string(),
1886 "x".to_string()
1887 ))
1888 );
1889 }
1890
1891 #[mz_ore::test]
1892 fn resolve_qualified_object_four_parts_none() {
1893 let aliases = BTreeMap::new();
1894 let result = resolve_qualified_object("a.b.c.d", &aliases, "mydb", "public");
1895 assert_eq!(result, None);
1896 }
1897}