Skip to main content

mz_deploy/lsp/
completion.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Context-aware completion for the LSP server.
11//!
12//! Completions are produced by a 3-phase pipeline:
13//!
14//! ```text
15//! Phase 1: RESOLVE CONTEXT    → CompletionContext
16//! Phase 2: GATHER CANDIDATES  → Vec<CompletionCandidate>
17//! Phase 3: FORMAT ITEMS       → Vec<CompletionItem>
18//! ```
19//!
20//! ## Phase 1: Resolve Context ([`resolve_context`])
21//!
22//! Builds a [`CompletionContext`] from the file URI, project cache, and prefix.
23//! Determines the default database/schema from the file path, and resolves
24//! the current file's dependencies and alias map (for column completions).
25//! All downstream logic operates on this resolved context — no further URI
26//! parsing or project lookups needed.
27//!
28//! ## Phase 2: Gather Candidates
29//!
30//! Four independent gatherers produce [`CompletionCandidate`]s:
31//!
32//! ### Functions ([`gather_functions`])
33//!
34//! Sourced from [`super::functions::FUNCTIONS`] (built from `mz_sql::func`
35//! registries). Only offered when `dots == 0`. Label is the function name,
36//! detail is the first overload's signature.
37//! Kind: `FUNCTION`. Sort: `4_`.
38//!
39//! ### Keywords ([`gather_keywords`])
40//!
41//! Static list from [`mz_sql_lexer::keywords::KEYWORDS`]. Only offered when
42//! `dots == 0`. Label is the uppercase keyword. Kind: `KEYWORD`.
43//!
44//! ### Object names ([`gather_objects`])
45//!
46//! Dynamic per-request from project objects and external dependencies.
47//!
48//! - **`dots == 0`:** all objects with minimum qualification — bare if
49//!   same-schema, `schema.object` if cross-schema, `db.schema.object` if
50//!   cross-database. No filtering; the editor handles fuzzy matching.
51//! - **`dots >= 1`:** filtered by prefix match with disambiguation. Each
52//!   object is matched against candidates `schema.object` then
53//!   `db.schema.object`. First case-insensitive prefix match wins. Label is
54//!   the remainder after the last dot in the prefix.
55//!
56//! Sort: `1_` same-schema, `2_` cross-schema, `3_` cross-database.
57//!
58//! ### Column names ([`gather_columns`])
59//!
60//! Dynamic per-request from the types cache. **Only offered for objects that
61//! are dependencies of the current file's object.**
62//!
63//! - **Unqualified** (`dots == 0`, [`gather_unqualified_columns`]): columns
64//!   from all dependencies, filtered by prefix.
65//! - **Qualified** (`dots >= 1`, [`gather_qualified_columns`]): resolves the
66//!   object prefix to an [`ObjectId`] (with alias map support), checks it is
67//!   a dependency, and returns that object's columns.
68//!
69//! Sort: `0_` (before object names).
70//!
71//! #### Alias Resolution
72//!
73//! When a qualified column prefix has a 1-part object (e.g., `o.col`), the
74//! alias map is checked before falling back to default db/schema resolution.
75//! Aliases are extracted from `FROM` clauses in views/materialized views.
76//!
77//! ## Phase 3: Format Items ([`format_candidate`])
78//!
79//! Converts each [`CompletionCandidate`] into an LSP [`CompletionItem`] with
80//! appropriate kind, detail, and sort text.
81
82use 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
91/// Describes the dot-qualified prefix at the cursor position.
92pub(super) struct PrefixContext<'a> {
93    /// Number of dots in the typed prefix (0, 1, or 2+).
94    pub dots: usize,
95    /// The raw prefix text the user has typed (e.g., `"public.f"`).
96    pub text: &'a str,
97}
98
99/// Everything the completion engine needs about the cursor position and file.
100///
101/// Built once by [`resolve_context`] and consumed by all candidate-gathering
102/// functions. No further URI parsing or project lookups are needed downstream.
103struct CompletionContext<'a> {
104    /// The default database derived from the file path.
105    default_db: String,
106    /// The default schema derived from the file path.
107    default_schema: String,
108    /// The parsed prefix at the cursor.
109    prefix: &'a PrefixContext<'a>,
110    /// The current file's dependencies and alias map (None if file not in project).
111    file_object: Option<FileObject>,
112}
113
114/// The current file's resolved context for column completions.
115struct FileObject {
116    /// Objects this file depends on.
117    dependencies: Vec<ObjectId>,
118    /// Alias/bare-table-name → target FQN map from the SQL AST.
119    alias_map: BTreeMap<String, String>,
120}
121
122/// Build a [`CompletionContext`] from the file URI, project cache, and prefix.
123///
124/// Returns `None` if the file is not under `models/<database>/<schema>/`
125/// (i.e., the default database/schema cannot be determined from the path).
126fn 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    // Try to resolve the current file's object for column completions.
135    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
158/// Find the dot-qualified identifier prefix at the cursor position.
159///
160/// Scans backward from `position` through identifier characters (alphanumeric,
161/// underscore) and dots to determine what the user has typed so far.
162pub(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
185/// A completion candidate before final formatting.
186///
187/// Intermediate representation produced by the gather phase. Captures what to
188/// complete and how to sort it, without LSP-specific formatting concerns.
189enum 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
208/// Gather keyword candidates. Only offered when `dots == 0`.
209fn 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
221/// Gather function candidates from the static function registry.
222/// Only offered when `dots == 0` (unqualified context).
223fn 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
232/// Gather object candidates from project objects and external dependencies.
233fn 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
278/// Gather column candidates from dependency objects via the types cache.
279fn 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
308/// Gather columns from all dependencies, filtered by prefix (case-insensitive).
309fn 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
335/// Gather columns from a specific qualified object reference.
336///
337/// Splits the prefix at the last `.` into `(object_text, col_filter)`,
338/// resolves `object_text` to an [`ObjectId`] via [`resolve_qualified_object`],
339/// checks it is a dependency, and returns its columns filtered by `col_filter`.
340fn 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
385/// Resolve a dot-qualified object prefix to an [`ObjectId`].
386///
387/// - 1 part: alias map lookup (case-insensitive), then fallback to
388///   `default_db.default_schema.name`
389/// - 2 parts: `default_db.part0.part1`
390/// - 3 parts: `part0.part1.part2`
391/// - 4+ parts: `None`
392fn 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
425/// Convert a [`CompletionCandidate`] into an LSP [`CompletionItem`].
426fn 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
466/// Run the 3-phase completion pipeline.
467///
468/// 1. **Resolve context** — determine default db/schema, file dependencies,
469///    and alias map from the file URI and project cache.
470/// 2. **Gather candidates** — collect keywords, objects, and columns that
471///    match the prefix.
472/// 3. **Format items** — convert candidates to LSP completion items.
473///
474/// When `project_cache` is `None` (no successful build yet), only keyword
475/// completions are returned. Keywords are included only when `dots == 0`
476/// (the module decides this, not the caller).
477pub(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    // Functions are always available (static registry, no project needed)
488    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
506/// Compute the label and sort prefix for an object, filtered by the typed prefix.
507///
508/// For `dots == 0`, returns minimum qualification (bare name if same-schema,
509/// `schema.object` if cross-schema, `db.schema.object` if cross-database).
510///
511/// For `dots >= 1`, tries matching against `schema.object` and
512/// `db.schema.object` candidates. Returns `None` if neither matches.
513pub(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
553/// Map an [`ObjectKind`] to the corresponding LSP [`CompletionItemKind`].
554fn 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
565/// Format a column type for the completion item detail field.
566fn 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    /// An empty prefix with no dots — the default for tests that don't care
580    /// about prefix context.
581    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    /// Test helper: run only the object-gathering phase and format results.
605    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    /// Test helper: run only the column-gathering phase and format results.
625    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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
693    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
719    #[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        // URI is in "other" schema, so "foo" from "public" should be schema-qualified.
736        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        // "baz" is same-schema, so bare.
746        assert!(
747            labels.contains(&"baz"),
748            "expected bare 'baz', got: {:?}",
749            labels
750        );
751    }
752
753    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
754    #[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        // URI is in otherdb, so "foo" from mydb should be fully qualified.
767        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
779    #[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        // Write a types.lock (TOML) that declares the external dep.
791        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
818    #[mz_ore::test]
819    fn kind_mapping() {
820        let root = tempfile::tempdir().unwrap();
821        // Storage and computation objects must be in separate schemas.
822        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
848    #[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        // URI is outside models/
857        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
863    #[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        // Labels should be bare names since "public." prefix is stripped.
881        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
894    #[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        // User typed "mydb." — a database prefix, not a schema prefix.
903        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        // Label should be "public.foo" — the remainder after "mydb.".
912        assert!(
913            labels.contains(&"public.foo"),
914            "expected 'public.foo', got: {:?}",
915            labels
916        );
917    }
918
919    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
920    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
944    #[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        // Prefix "other." should only match objects in the "other" schema.
961        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        // "foo" is in "public" schema — should be filtered out.
971        assert!(
972            !labels.iter().any(|l| l.contains("foo")),
973            "expected no 'foo' items, got: {:?}",
974            labels
975        );
976    }
977
978    /// Helper: write a types.lock with the given tables and columns.
979    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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
997    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1033    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1074    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1096    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1130    #[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        // "storage.foo" qualified with schema — 2 parts resolves to (default_db, schema, object).
1158        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1175    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1219    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1258    #[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        // "other" is not a dependency of "v" — qualified as schema.object.
1296        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1311    #[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        // types.lock exists but has no columns for foo.
1326        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        // Must use schema-qualified since foo is in storage schema, not compute.
1331        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1342    #[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        // Uppercase "I" should match "id".
1370        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1391    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1431    #[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        // Column sort_text starts with "0_", object sort_text starts with "1_" or higher.
1465        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1483    #[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        // Typing "o." should resolve via alias to orders.
1511        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1528    #[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        // Typing "orders." should resolve via bare table name.
1556        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1568    #[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        // v depends on orders but NOT other. The alias "o" maps to orders.
1579        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        // "other" is not a dependency — alias resolves to it but dep check fails.
1607        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1622    #[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        // "x." should resolve to t1.
1650        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        // "y." should resolve to t2.
1663        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1677    #[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        // Lowercase "o." should match uppercase alias "O".
1705        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1721    #[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        // CREATE TABLE has no query — alias map is empty, falls back to normal behavior.
1735        // "t." with 1 dot resolves to ObjectId(mydb, storage, t) via fallback.
1736        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        // t is itself, not a dependency of itself, so empty.
1744        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}