Skip to main content

mz_deploy/
client.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//! Database client layer for communicating with a Materialize region.
11//!
12//! All interaction with the live database flows through this module. The
13//! `Client` type (defined in `connection`) holds a `tokio_postgres`
14//! connection and exposes scoped sub-clients that group related operations:
15//!
16//! - **`introspection`** — Read-only catalog queries: schema/cluster/object
17//!   existence checks, dependency lookups, and batch metadata retrieval.
18//! - **`provisioning`** — DDL operations that create or alter databases,
19//!   schemas, and clusters to match the project definition.
20//! - **`deployment_ops`** — Blue/green deployment lifecycle: staging,
21//!   hydration monitoring, cutover, and abort.
22//! - **`validation`** — Pre-deployment validation: checks that the target
23//!   environment matches expected state before applying changes.
24//! - **`type_info`** — `SHOW COLUMNS` queries used to generate and refresh
25//!   the `types.lock` data-contract file.
26//!
27//! ## Supporting Submodules
28//!
29//! - **`models`** — Data structures shared across sub-clients (deployment
30//!   records, cluster configs, conflict records, etc.).
31//! - **`errors`** — Error types: `ConnectionError` for transport/query
32//!   failures, `DatabaseValidationError` for semantic mismatches.
33//!
34//! Most sub-client types are internal; this module re-exports the key public
35//! types so that consumers only need `use crate::client::*`.
36
37mod connection;
38mod deployment_ops;
39mod dev_overlays;
40mod errors;
41mod humanized_type;
42mod introspection;
43mod models;
44mod provisioning;
45mod type_info;
46mod validation;
47
48/// Name of the dedicated cluster mz-deploy creates during `setup` and
49/// pins every connection to via libpq options.
50pub const SERVER_CLUSTER_NAME: &str = "_mz_deploy_server";
51
52use mz_sql_parser::ast::{CreateClusterStatement, Raw, Statement};
53use mz_sql_parser::parser::parse_statements;
54
55pub use crate::config::Profile;
56pub use connection::{Client, DevOverlaysClient};
57pub(crate) use connection::{build_options_string, default_sslmode, is_loopback_host};
58
59/// Double-quote a SQL identifier, escaping any embedded double quotes.
60pub fn quote_identifier(name: &str) -> String {
61    format!("\"{}\"", name.replace('"', "\"\""))
62}
63
64/// Parse the `create_sql` column of a `SHOW CREATE CLUSTER` result.
65pub fn parse_create_cluster(sql: &str) -> Result<CreateClusterStatement<Raw>, String> {
66    let statements = parse_statements(sql)
67        .map_err(|e| format!("failed to parse SHOW CREATE CLUSTER output: {}", e.error))?;
68    match statements.into_iter().next().map(|statement| statement.ast) {
69        Some(Statement::CreateCluster(create)) => Ok(create),
70        Some(other) => Err(format!("expected CREATE CLUSTER, got: {}", other)),
71        None => Err("SHOW CREATE CLUSTER returned empty SQL".to_string()),
72    }
73}
74
75/// Build a comma-separated `$1, $2, …, $n` placeholder string for parameterized queries.
76pub fn sql_placeholders(n: usize) -> String {
77    (1..=n)
78        .map(|i| format!("${}", i))
79        .collect::<Vec<_>>()
80        .join(", ")
81}
82
83/// Build a `LIKE` pattern (used with `ESCAPE '\'`) matching any name that ends
84/// in the staging suffix `_<deploy_id>`.
85///
86/// The suffix is matched *literally*: `_`, `%`, and the escape character `\` are
87/// LIKE metacharacters, so they are escaped. Only the leading `%` stays a
88/// wildcard. Without escaping, the `_` separating the suffix would act as a
89/// single-character wildcard — pattern `%_prod` would match any name ending in
90/// `<any char>prod` (e.g. a production schema `fooprod` or cluster `dataprod`),
91/// and a `deploy_id` containing `%` would match nearly everything. Used by both
92/// the staging-discovery queries (which feed `DROP ... CASCADE`) and the
93/// hydration-status / `wait` readiness queries.
94pub(crate) fn staging_suffix_like_pattern(deploy_id: &str) -> String {
95    let mut pattern = String::from("%");
96    // The literal suffix is the separating underscore followed by the deploy id.
97    for ch in std::iter::once('_').chain(deploy_id.chars()) {
98        if matches!(ch, '\\' | '_' | '%') {
99            pattern.push('\\');
100        }
101        pattern.push(ch);
102    }
103    pattern
104}
105
106#[cfg(test)]
107mod tests {
108    use super::staging_suffix_like_pattern;
109
110    #[mz_ore::test]
111    fn test_staging_suffix_like_pattern_escapes_separator() {
112        // Regression test for QA Finding 3.
113        //
114        // The `_` separating the staging suffix must be escaped so it matches a
115        // literal underscore, not a single-character wildcard. With deploy id
116        // `prod` the pattern must be `%\_prod` (used with `ESCAPE '\'`), which
117        // matches only names ending in the literal `_prod` — NOT `fooprod` or any
118        // other `<char>prod`, which the unescaped `%_prod` would have matched.
119        assert_eq!(staging_suffix_like_pattern("prod"), r"%\_prod");
120    }
121
122    #[mz_ore::test]
123    fn test_staging_suffix_like_pattern_escapes_metacharacters() {
124        // A deploy id containing LIKE metacharacters must not inject wildcards.
125        // `%` and `_` inside the id are escaped to literals; the only wildcard is
126        // the leading `%`.
127        assert_eq!(staging_suffix_like_pattern("a%b_c"), r"%\_a\%b\_c");
128        // Backslashes (the escape char itself) are also escaped.
129        assert_eq!(staging_suffix_like_pattern(r"a\b"), r"%\_a\\b");
130    }
131
132    #[mz_ore::test]
133    fn test_staging_suffix_like_pattern_plain_id() {
134        // A plain alphanumeric id only escapes the separating underscore.
135        assert_eq!(staging_suffix_like_pattern("deploy123"), r"%\_deploy123");
136    }
137}
138pub use deployment_ops::{
139    ClusterDeploymentStatus, ClusterStatusContext, DEFAULT_ALLOWED_LAG_SECS, FailureReason,
140    HydrationStatusUpdate,
141};
142pub use errors::{ConnectionError, DatabaseValidationError, format_relative_path};
143pub use introspection::DependentSink;
144pub use models::{
145    ApplyState, Cluster, ClusterConfig, ClusterReplica, ConflictRecord, DeploymentDetails,
146    DeploymentHistoryEntry, DeploymentKind, DeploymentMetadata, DeploymentMode,
147    DeploymentObjectRecord, ObjectGrant, PendingStatement, ProductionClusterRecord,
148    ReplacementMvRecord, SchemaDeploymentRecord, StagingDeployment,
149};