Skip to main content

mz_deploy/cli/commands/
compile.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//! Compile command — validate project and show deployment plan.
11//!
12//! Compiles the project through a multi-stage pipeline:
13//!
14//! 1. **Parse** — Load and parse SQL files from the project directory.
15//! 2. **Validate** — Check project structure and dependencies.
16//! 3. **Build graph** — Assemble the dependency-aware project graph.
17//! 4. **Typecheck** — Incrementally validate SQL against Materialize. Only
18//!    objects whose definitions changed since the last build are re-validated;
19//!    unchanged builds skip typechecking entirely.
20//! 5. **Display** — Print the deployment plan with dependencies and SQL.
21
22use crate::cli::CliError;
23use crate::cli::progress;
24use crate::config::Settings;
25use crate::project::ir::graph::Project;
26use crate::{project, verbose};
27use std::time::Instant;
28
29/// Compile and validate the project, showing the deployment plan.
30///
31/// This command:
32/// - Loads and parses SQL files from the project directory
33/// - Validates the project structure and dependencies
34/// - Type-checks SQL statements (incremental when possible)
35/// - Displays the deployment plan including dependencies and SQL statements
36///
37/// Type checking uses compiler-owned incremental artifacts to identify dirty
38/// runtime objects. Dependencies are restored lazily, and unchanged builds
39/// skip type checking entirely.
40///
41/// # Arguments
42/// * `settings` - Resolved project and profile configuration
43/// * `show_progress` - If true, displays progress indicators during compilation
44///
45/// # Returns
46/// Compiled planned project ready for deployment
47///
48/// # Errors
49/// Returns `CliError::Project` if compilation or validation fails
50pub async fn run(settings: &Settings, show_progress: bool) -> Result<Project, CliError> {
51    run_with_fs(settings, show_progress, crate::fs::FileSystem::new()).await
52}
53
54/// Like [`run`] but uses the provided [`crate::fs::FileSystem`] (typically an
55/// overlay built from unsaved editor buffers) instead of constructing a
56/// disk-only one.
57pub(crate) async fn run_with_fs(
58    settings: &Settings,
59    show_progress: bool,
60    fs: crate::fs::FileSystem,
61) -> Result<Project, CliError> {
62    let settings = settings.clone();
63    mz_ore::task::spawn_blocking(
64        || "compile-run",
65        move || run_inner(&settings, show_progress, false, fs),
66    )
67    .await
68}
69
70/// Compile the project without type checking.
71///
72/// Used by `apply` commands which create infrastructure objects that don't
73/// exist yet in the database — type checking would fail because it validates
74/// views against the live catalog, but the tables they reference haven't
75/// been created yet.
76pub async fn run_without_typecheck(
77    settings: &Settings,
78    show_progress: bool,
79) -> Result<Project, CliError> {
80    let settings = settings.clone();
81    mz_ore::task::spawn_blocking(
82        || "compile-run",
83        move || run_inner(&settings, show_progress, true, crate::fs::FileSystem::new()),
84    )
85    .await
86}
87
88fn run_inner(
89    settings: &Settings,
90    show_progress: bool,
91    skip_typecheck: bool,
92    fs: crate::fs::FileSystem,
93) -> Result<Project, CliError> {
94    let start_time = Instant::now();
95    let directory = &settings.directory;
96
97    if show_progress {
98        let canonical = directory.canonicalize();
99        let shown = canonical.as_deref().unwrap_or(directory);
100        progress::action("Compiling", &shown.display().to_string());
101    }
102
103    let planned_project = project::plan_sync(
104        &fs,
105        directory.clone(),
106        settings.profile_name(),
107        settings.profile_suffix(),
108        settings.variables(),
109    )?;
110
111    let validation = project::analysis::deps::validate_dependencies(
112        &settings.dependencies,
113        &planned_project.external_dependencies,
114    );
115
116    if !validation.unused.is_empty() {
117        let mut unused: Vec<_> = validation.unused.iter().collect();
118        unused.sort();
119        for dep in unused {
120            progress::warn(&format!(
121                "unused dependency: \"{}\" is declared in project.toml but not referenced",
122                dep
123            ));
124        }
125    }
126
127    if !validation.undeclared.is_empty() {
128        let mut undeclared: Vec<_> = validation.undeclared.into_iter().collect();
129        undeclared.sort();
130        return Err(CliError::UndeclaredDependencies { undeclared });
131    }
132
133    if !skip_typecheck {
134        typecheck_project(settings, &planned_project)?;
135    }
136
137    if show_progress && crate::log::verbose_enabled() {
138        print_verbose_details(&planned_project);
139    }
140
141    if show_progress {
142        let total_duration = start_time.elapsed();
143        progress::finished("compile", total_duration);
144    }
145
146    Ok(planned_project)
147}
148
149/// Perform type checking using the in-process catalog backend.
150fn typecheck_project(settings: &Settings, planned_project: &Project) -> Result<(), CliError> {
151    let directory = &settings.directory;
152    use crate::project::compiler::typecheck;
153
154    let external_types = crate::types::load_types_lock(directory).unwrap_or_default();
155
156    let (_, stats) = typecheck::run(
157        directory,
158        settings.profile_name().unwrap_or(""),
159        settings.profile_suffix(),
160        settings.variables(),
161        planned_project,
162        external_types,
163    )?;
164    crate::verbose!(
165        "typecheck: ran={} skipped={} schema_stable={} schema_changed={}",
166        stats.ran,
167        stats.skipped,
168        stats.schema_stable,
169        stats.schema_changed,
170    );
171
172    Ok(())
173}
174
175/// Print verbose details about the project (only shown with VERBOSE env var)
176fn print_verbose_details(planned_project: &Project) {
177    print_external_dependencies(planned_project);
178    print_cluster_dependencies(planned_project);
179    print_dependency_graph(planned_project);
180}
181
182/// Prints dependencies that are referenced but not declared in this project tree.
183///
184/// These are the objects operators must provision externally before deployment.
185fn print_external_dependencies(planned_project: &Project) {
186    if planned_project.external_dependencies.is_empty() {
187        return;
188    }
189    verbose!("\nExternal Dependencies (not defined in this project):");
190    let mut external: Vec<_> = planned_project.external_dependencies.iter().collect();
191    external.sort();
192    for dep in external {
193        verbose!("  - {}", dep);
194    }
195}
196
197/// Prints cluster prerequisites inferred from object and index definitions.
198fn print_cluster_dependencies(planned_project: &Project) {
199    if planned_project.cluster_dependencies.is_empty() {
200        return;
201    }
202    verbose!("\nCluster Dependencies:");
203    let mut clusters: Vec<_> = planned_project.cluster_dependencies.iter().collect();
204    clusters.sort_by_key(|c| &c.name);
205    for cluster in clusters {
206        verbose!("  - {}", cluster.name);
207    }
208}
209
210/// Prints per-object dependency edges for troubleshooting deployment ordering.
211///
212/// External dependencies are annotated inline to separate project-internal edges
213/// from dependencies that are expected to pre-exist.
214fn print_dependency_graph(planned_project: &Project) {
215    verbose!("\nDependency Graph:");
216    for (object_id, deps) in &planned_project.dependency_graph {
217        if deps.is_empty() {
218            continue;
219        }
220        verbose!("  {} depends on:", object_id);
221        for dep in deps {
222            if planned_project.external_dependencies.contains(dep) {
223                verbose!("    - {} (external)", dep);
224            } else {
225                verbose!("    - {}", dep);
226            }
227        }
228    }
229}