1use crate::client::quote_identifier;
23use crate::project::ir::object_id::ObjectId;
24use crate::types::{ColumnType, DataType, RecordField};
25use std::collections::BTreeMap;
26use thiserror::Error;
27
28#[derive(Error, Debug)]
30pub(crate) enum StubError {
31 #[error(
32 "`{object}`.`{column}` is recorded as the pseudo-type `{found}`, which does not describe the column's real type; re-run `mz-deploy lock` to capture it"
33 )]
34 UnreconstructibleType {
35 object: ObjectId,
36 column: String,
37 found: String,
38 },
39 #[error(
40 "`{object}`.`{column}` has type `{found}`; a record inside a container is not supported"
41 )]
42 RecordInContainer {
43 object: ObjectId,
44 column: String,
45 found: String,
46 },
47}
48
49pub(crate) struct StubTarget {
51 pub name: String,
53 pub helper_prefix: String,
56 pub helper_stem: String,
58}
59
60struct StubNames<'a> {
66 target: &'a StubTarget,
67 next: usize,
68}
69
70impl StubNames<'_> {
71 fn take(&mut self) -> usize {
72 let n = self.next;
73 self.next += 1;
74 n
75 }
76
77 fn helper(&mut self) -> String {
78 let n = self.take();
79 format!(
80 "{}{}",
81 self.target.helper_prefix,
82 quote_identifier(&format!("{}_h{}", self.target.helper_stem, n))
83 )
84 }
85}
86
87pub(crate) fn build_stub_statements(
92 object_id: &ObjectId,
93 target: &StubTarget,
94 columns: &BTreeMap<String, ColumnType>,
95) -> Result<Vec<String>, StubError> {
96 let mut ordered: Vec<_> = columns.iter().collect();
99 ordered.sort_by_key(|(_, ct)| ct.position);
100
101 for (name, column) in &ordered {
102 check_reconstructible(object_id, name, &column.r#type)?;
103 }
104
105 if !ordered.iter().any(|(_, ct)| ct.r#type.contains_record()) {
106 let defs: Vec<String> = ordered
107 .iter()
108 .map(|(name, ct)| column_def(name, &ct.r#type, ct.nullable))
109 .collect();
110 return Ok(vec![format!(
111 "CREATE TABLE {} ({})",
112 target.name,
113 defs.join(", ")
114 )]);
115 }
116
117 let fields: Vec<RecordField> = ordered
118 .iter()
119 .map(|(name, ct)| RecordField {
120 name: (*name).clone(),
121 r#type: ct.r#type.clone(),
122 nullable: ct.nullable,
123 })
124 .collect();
125
126 let mut names = StubNames { target, next: 0 };
127 let mut statements = Vec::new();
128 let select = build_select(&fields, &mut names, &mut statements);
129 statements.push(format!("CREATE VIEW {} AS {}", target.name, select));
130 Ok(statements)
131}
132
133fn check_reconstructible(
135 object_id: &ObjectId,
136 column: &str,
137 r#type: &DataType,
138) -> Result<(), StubError> {
139 if r#type.contains_pseudo_token() {
140 return Err(StubError::UnreconstructibleType {
141 object: object_id.clone(),
142 column: column.to_string(),
143 found: r#type.to_string(),
144 });
145 }
146 match r#type {
147 DataType::Array(inner) | DataType::List(inner) | DataType::Map(inner)
148 if inner.contains_record() =>
149 {
150 Err(StubError::RecordInContainer {
151 object: object_id.clone(),
152 column: column.to_string(),
153 found: r#type.to_string(),
154 })
155 }
156 DataType::Record(fields) => fields
157 .iter()
158 .try_for_each(|field| check_reconstructible(object_id, column, &field.r#type)),
159 _ => Ok(()),
160 }
161}
162
163fn column_def(name: &str, r#type: &DataType, nullable: bool) -> String {
165 format!(
166 "{} {}{}",
167 quote_identifier(name),
168 r#type,
169 if nullable { "" } else { " NOT NULL" }
170 )
171}
172
173fn build_select(
178 fields: &[RecordField],
179 names: &mut StubNames<'_>,
180 statements: &mut Vec<String>,
181) -> String {
182 let scalars: Vec<&RecordField> = fields
183 .iter()
184 .filter(|f| !f.r#type.contains_record())
185 .collect();
186
187 let mut from = Vec::new();
191 let base = if scalars.is_empty() {
192 String::new()
193 } else {
194 let helper = names.helper();
195 let defs: Vec<String> = scalars
196 .iter()
197 .map(|f| column_def(&f.name, &f.r#type, f.nullable))
198 .collect();
199 statements.push(format!("CREATE TABLE {} ({})", helper, defs.join(", ")));
200 let base = quote_identifier(&format!("__b{}", names.take()));
201 from.push(format!("{} AS {}", helper, base));
202 base
203 };
204
205 let mut projection = Vec::new();
206 for field in fields {
207 let alias = quote_identifier(&field.name);
208 match &field.r#type {
209 DataType::Record(inner) => {
210 let inner_select = build_select(inner, names, statements);
211 let record = quote_identifier(&format!("__r{}", names.take()));
212 if field.nullable {
213 projection.push(format!(
215 "(SELECT {record} FROM ({inner_select}) AS {record} LIMIT 1) AS {alias}"
216 ));
217 } else {
218 from.push(format!("({}) AS {}", inner_select, record));
219 projection.push(format!("{} AS {}", record, alias));
220 }
221 }
222 _ => projection.push(format!("{}.{} AS {}", base, alias, alias)),
223 }
224 }
225
226 if from.is_empty() {
227 return format!("SELECT {}", projection.join(", "));
228 }
229 format!("SELECT {} FROM {}", projection.join(", "), from.join(", "))
230}