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    #[cfg_attr(miri, ignore)] // can't call foreign function `llvm.aarch64.neon.uaddlv.i32.v16i8` on OS `linux`
646    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)] // can't call foreign function `llvm.aarch64.neon.uaddlv.i32.v16i8` on OS `linux`
655    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)] // can't call foreign function `llvm.aarch64.neon.uaddlv.i32.v16i8` on OS `linux`
664    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)] // can't call foreign function `llvm.aarch64.neon.uaddlv.i32.v16i8` on OS `linux`
673    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)] // can't call foreign function `llvm.aarch64.neon.uaddlv.i32.v16i8` on OS `linux`
682    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)] // can't call foreign function `llvm.aarch64.neon.uaddlv.i32.v16i8` on OS `linux`
691    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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
699    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
725    #[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        // URI is in "other" schema, so "foo" from "public" should be schema-qualified.
742        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        // "baz" is same-schema, so bare.
752        assert!(
753            labels.contains(&"baz"),
754            "expected bare 'baz', got: {:?}",
755            labels
756        );
757    }
758
759    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
760    #[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        // URI is in otherdb, so "foo" from mydb should be fully qualified.
773        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
785    #[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        // Write a types.lock (TOML) that declares the external dep.
797        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
824    #[mz_ore::test]
825    fn kind_mapping() {
826        let root = tempfile::tempdir().unwrap();
827        // Storage and computation objects must be in separate schemas.
828        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
854    #[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        // URI is outside models/
863        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
869    #[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        // Labels should be bare names since "public." prefix is stripped.
887        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
900    #[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        // User typed "mydb." — a database prefix, not a schema prefix.
909        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        // Label should be "public.foo" — the remainder after "mydb.".
918        assert!(
919            labels.contains(&"public.foo"),
920            "expected 'public.foo', got: {:?}",
921            labels
922        );
923    }
924
925    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
926    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
950    #[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        // Prefix "other." should only match objects in the "other" schema.
967        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        // "foo" is in "public" schema — should be filtered out.
977        assert!(
978            !labels.iter().any(|l| l.contains("foo")),
979            "expected no 'foo' items, got: {:?}",
980            labels
981        );
982    }
983
984    /// Helper: write a types.lock with the given tables and columns.
985    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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1003    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1039    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1080    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1102    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1136    #[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        // "storage.foo" qualified with schema — 2 parts resolves to (default_db, schema, object).
1164        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1181    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1225    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1264    #[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        // "other" is not a dependency of "v" — qualified as schema.object.
1302        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1317    #[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        // types.lock exists but has no columns for foo.
1332        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        // Must use schema-qualified since foo is in storage schema, not compute.
1337        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1348    #[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        // Uppercase "I" should match "id".
1376        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1397    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1437    #[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        // Column sort_text starts with "0_", object sort_text starts with "1_" or higher.
1471        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1489    #[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        // Typing "o." should resolve via alias to orders.
1517        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1534    #[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        // Typing "orders." should resolve via bare table name.
1562        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1574    #[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        // v depends on orders but NOT other. The alias "o" maps to orders.
1585        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        // "other" is not a dependency — alias resolves to it but dep check fails.
1613        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1628    #[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        // "x." should resolve to t1.
1656        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        // "y." should resolve to t2.
1669        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1683    #[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        // Lowercase "o." should match uppercase alias "O".
1711        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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
1727    #[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        // CREATE TABLE has no query — alias map is empty, falls back to normal behavior.
1741        // "t." with 1 dot resolves to ObjectId(mydb, storage, t) via fallback.
1742        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        // t is itself, not a dependency of itself, so empty.
1750        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}