Skip to main content

mz_deploy/cli/commands/
lock.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//! Generate data contracts command — resolves declared dependencies from
11//! `project.toml` into a `types.lock` file.
12//!
13//! Reads the `dependencies` list from `project.toml` and queries the target
14//! database for each declared object's column schema and kind. Also discovers
15//! `CREATE TABLE FROM SOURCE` tables via full project compilation (a
16//! lightweight syntax-only path is a planned follow-up). Hard-errors if any
17//! declared dependency does not exist in the target database.
18
19use std::time::Instant;
20
21use crate::cli::CliError;
22use crate::cli::progress;
23use crate::client::Client;
24use crate::config::Settings;
25use crate::project::ir::object_id::ObjectId;
26
27/// Resolve declared dependencies into a types.lock file.
28pub async fn run(settings: &Settings) -> Result<(), CliError> {
29    let directory = &settings.directory;
30
31    let start = Instant::now();
32    let canonical = directory.canonicalize();
33    let shown = canonical.as_deref().unwrap_or(directory);
34    progress::action("Locking", &shown.display().to_string());
35
36    // Discover source tables via compilation (pragmatic first step;
37    // lightweight syntax-only extraction is a follow-up optimization)
38    let source_tables = discover_source_tables(settings)?;
39
40    if settings.dependencies.is_empty() && source_tables.is_empty() {
41        progress::finished("lock", start.elapsed());
42        return Ok(());
43    }
44
45    // Connect to the database
46    let profile = settings.connection();
47    let client = Client::connect_with_profile(profile.clone())
48        .await
49        .map_err(CliError::Connection)?;
50
51    // Resolve types for declared dependencies and source tables in one
52    // catalog query. Missing objects (those not in the target catalog) are
53    // surfaced as DeclaredDependenciesMissing with a user-friendly hint.
54    let declared: Vec<ObjectId> = settings.dependencies.iter().cloned().collect();
55    let (types, missing) = client
56        .types()
57        .query_types_for_objects(&declared, &source_tables)
58        .await
59        .map_err(CliError::Connection)?;
60
61    if !missing.is_empty() {
62        return Err(CliError::DeclaredDependenciesMissing { missing });
63    }
64
65    types.write_types_lock(directory)?;
66
67    progress::finished(
68        &format!("locking {} objects", types.tables.len()),
69        start.elapsed(),
70    );
71
72    Ok(())
73}
74
75/// Discover CREATE TABLE FROM SOURCE tables by compiling the project.
76fn discover_source_tables(settings: &Settings) -> Result<Vec<ObjectId>, CliError> {
77    let fs = crate::fs::FileSystem::new();
78    let planned = crate::project::plan_sync(
79        &fs,
80        &settings.directory,
81        settings.profile_name(),
82        settings.profile_suffix(),
83        settings.variables(),
84    )?;
85    Ok(planned.get_tables_from_source().collect())
86}