Skip to main content

mz_deploy/project/ir/
infrastructure.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//! Infrastructure property extraction from AST statements.
11//!
12//! Extracts structured metadata from connections, sources, and
13//! tables-from-source statements. Used by both the compiler (to persist
14//! to SQLite) and the LSP catalog (to build the explore page).
15//!
16//! ## Extraction Rules
17//!
18//! | Statement Kind         | Produces                       |
19//! |------------------------|--------------------------------|
20//! | `CREATE CONNECTION`    | `Infrastructure::Connection`   |
21//! | `CREATE SOURCE`        | `Infrastructure::Source`        |
22//! | `CREATE TABLE .. FROM` | `Infrastructure::TableFromSource` |
23//! | Everything else        | `None`                         |
24//!
25//! For connections, `PublicKey1` and `PublicKey2` options are filtered out
26//! (they are auto-generated SSH key material, not user-configurable).
27//!
28//! Property values that reference secrets or objects carry the reference
29//! name in `secret_ref` / `object_ref` for downstream linking.
30
31use mz_sql_parser::ast::{
32    ConnectionOptionName, CreateConnectionType, CreateSourceConnection, Raw, RawItemName,
33    WithOptionValue,
34};
35
36/// Extracted infrastructure metadata for an object.
37#[derive(Debug, Clone)]
38pub(crate) enum Infrastructure {
39    /// Connection properties (HOST, PORT, USER, PASSWORD, etc.)
40    Connection {
41        /// Connection type (e.g., "Postgres", "Kafka", "MySQL").
42        connector_type: String,
43        /// Key-value properties extracted from ConnectionOption values.
44        properties: Vec<Property>,
45    },
46    /// Source configuration (connection ref, cluster, publication/topic, etc.)
47    Source {
48        /// Source type (e.g., "Postgres", "Kafka", "Load Generator").
49        connector_type: String,
50        /// The connection object this source uses (for linking).
51        connection_ref: Option<String>,
52        /// Key-value properties extracted from source options.
53        properties: Vec<Property>,
54    },
55    /// Table-from-source metadata.
56    TableFromSource {
57        /// The source object this table reads from (for linking).
58        source_ref: String,
59        /// External reference (e.g., "public.orders" in the upstream DB).
60        external_reference: Option<String>,
61    },
62}
63
64/// A key-value property extracted from an infrastructure object's AST.
65#[derive(Debug, Clone)]
66pub(crate) struct Property {
67    /// Property name (e.g., "HOST", "DATABASE", "PUBLICATION").
68    pub key: String,
69    /// Display value. For secrets: the secret's fully-qualified name.
70    pub value: String,
71    /// If this property references a secret, the secret's object ID (for linking).
72    pub secret_ref: Option<String>,
73    /// If this property references another object, its object ID (for linking).
74    pub object_ref: Option<String>,
75}
76
77/// Extract infrastructure properties from a statement, if applicable.
78///
79/// Returns `Some` for connections, sources, and tables-from-source;
80/// `None` for all other statement types.
81pub(crate) fn extract(stmt: &crate::project::ast::Statement) -> Option<Infrastructure> {
82    match stmt {
83        crate::project::ast::Statement::CreateConnection(s) => Some(extract_connection(s)),
84        crate::project::ast::Statement::CreateSource(s) => Some(extract_source(s)),
85        crate::project::ast::Statement::CreateTableFromSource(s) => {
86            Some(extract_table_from_source(s))
87        }
88        _ => None,
89    }
90}
91
92/// Format a `CreateConnectionType` as a human-readable string.
93fn connection_type_name(ct: &CreateConnectionType) -> &'static str {
94    match ct {
95        CreateConnectionType::Postgres => "Postgres",
96        CreateConnectionType::Kafka => "Kafka",
97        CreateConnectionType::MySql => "MySQL",
98        CreateConnectionType::Ssh => "SSH Tunnel",
99        CreateConnectionType::Aws => "AWS",
100        CreateConnectionType::AwsPrivatelink => "AWS PrivateLink",
101        CreateConnectionType::Gcp => "GCP",
102        CreateConnectionType::Csr => "Confluent Schema Registry",
103        CreateConnectionType::GlueSchemaRegistry => "Glue Schema Registry",
104        CreateConnectionType::SqlServer => "SQL Server",
105        CreateConnectionType::IcebergCatalog => "Iceberg Catalog",
106    }
107}
108
109/// Format a `WithOptionValue<Raw>` into a display string and optional ref IDs.
110fn format_option_value(value: &WithOptionValue<Raw>) -> (String, Option<String>, Option<String>) {
111    match value {
112        WithOptionValue::Secret(name) => {
113            let name_str = raw_item_name_to_string(name);
114            (name_str.clone(), Some(name_str), None)
115        }
116        WithOptionValue::Item(name) => {
117            let name_str = raw_item_name_to_string(name);
118            (name_str.clone(), None, Some(name_str))
119        }
120        other => (format!("{}", other), None, None),
121    }
122}
123
124/// Extract the unqualified string from a `RawItemName`.
125fn raw_item_name_to_string(name: &RawItemName) -> String {
126    match name {
127        RawItemName::Name(n) => n.to_string(),
128        RawItemName::Id(_, n, _) => n.to_string(),
129    }
130}
131
132/// Convert a slice of source/connection options into [`Property`] entries.
133///
134/// Each option must have a displayable `.name` and an `Option<WithOptionValue<Raw>>`
135/// `.value`. Options with no value are skipped.
136macro_rules! options_to_properties {
137    ($options:expr) => {
138        $options
139            .iter()
140            .filter_map(|opt| {
141                let value = opt.value.as_ref()?;
142                let (display, secret_ref, object_ref) = format_option_value(value);
143                Some(Property {
144                    key: format!("{}", opt.name),
145                    value: display,
146                    secret_ref,
147                    object_ref,
148                })
149            })
150            .collect()
151    };
152}
153
154/// Extract structured properties from a CREATE CONNECTION statement.
155fn extract_connection(stmt: &mz_sql_parser::ast::CreateConnectionStatement<Raw>) -> Infrastructure {
156    let connector_type = connection_type_name(&stmt.connection_type).to_string();
157    let properties = stmt
158        .values
159        .iter()
160        .filter_map(|opt| {
161            if matches!(
162                opt.name,
163                ConnectionOptionName::PublicKey1 | ConnectionOptionName::PublicKey2
164            ) {
165                return None;
166            }
167            let value = opt.value.as_ref()?;
168            let (display, secret_ref, object_ref) = format_option_value(value);
169            Some(Property {
170                key: format!("{}", opt.name),
171                value: display,
172                secret_ref,
173                object_ref,
174            })
175        })
176        .collect();
177    Infrastructure::Connection {
178        connector_type,
179        properties,
180    }
181}
182
183/// Extract structured properties from a CREATE SOURCE statement.
184fn extract_source(stmt: &mz_sql_parser::ast::CreateSourceStatement<Raw>) -> Infrastructure {
185    let (connector_type, connection_ref, properties) = match &stmt.connection {
186        CreateSourceConnection::Postgres {
187            connection,
188            options,
189        } => (
190            "Postgres".to_string(),
191            Some(raw_item_name_to_string(connection)),
192            options_to_properties!(options),
193        ),
194        CreateSourceConnection::Kafka {
195            connection,
196            options,
197        } => (
198            "Kafka".to_string(),
199            Some(raw_item_name_to_string(connection)),
200            options_to_properties!(options),
201        ),
202        CreateSourceConnection::MySql {
203            connection,
204            options,
205        } => (
206            "MySQL".to_string(),
207            Some(raw_item_name_to_string(connection)),
208            options_to_properties!(options),
209        ),
210        CreateSourceConnection::SqlServer {
211            connection,
212            options,
213        } => (
214            "SQL Server".to_string(),
215            Some(raw_item_name_to_string(connection)),
216            options_to_properties!(options),
217        ),
218        CreateSourceConnection::LoadGenerator { generator, options } => (
219            format!("Load Generator ({})", generator),
220            None,
221            options_to_properties!(options),
222        ),
223    };
224    Infrastructure::Source {
225        connector_type,
226        connection_ref,
227        properties,
228    }
229}
230
231/// Extract structured properties from a CREATE TABLE FROM SOURCE statement.
232fn extract_table_from_source(
233    stmt: &mz_sql_parser::ast::CreateTableFromSourceStatement<Raw>,
234) -> Infrastructure {
235    let source_ref = raw_item_name_to_string(&stmt.source);
236    let external_reference = stmt.external_reference.as_ref().map(|n| n.to_string());
237    Infrastructure::TableFromSource {
238        source_ref,
239        external_reference,
240    }
241}