1use mz_expr::func::variadic::Coalesce;
22use mz_expr::{MirRelationExpr, MirScalarExpr, func};
23use mz_repr::{
24 ColumnName, Datum, GlobalId, RelationDesc, ReprRelationType, ReprScalarType, Row, SqlScalarType,
25};
26
27#[allow(dead_code)]
36const METRIC_NAME_PATTERN: &str = "^[a-zA-Z_:][a-zA-Z0-9_:]*$";
37
38#[allow(dead_code)]
57fn shape_metric_sink_source(
58 from_id: GlobalId,
59 from_desc: &RelationDesc,
60) -> (MirRelationExpr, RelationDesc) {
61 let get_idx = |name: &str| {
66 from_desc
67 .get_by_name(&ColumnName::from(name))
68 .expect("metric-sink source relation must expose the canonical columns")
69 };
70 let (metric_name_idx, metric_name_ct) = get_idx("metric_name");
71 let (metric_type_idx, metric_type_ct) = get_idx("metric_type");
72 let (labels_idx, labels_ct) = get_idx("labels");
73 let (value_idx, value_ct) = get_idx("value");
74 let (help_idx, help_ct) = get_idx("help");
75
76 let repr_typ = ReprRelationType::from(from_desc.typ());
77 let arity = repr_typ.column_types.len();
78 let labels_repr_type = ReprScalarType::from(&labels_ct.scalar_type);
79
80 let empty_map_row = {
81 let mut row = Row::default();
82 row.packer().push_dict_with(|_| {});
83 row
84 };
85 let labels_coalesced = MirScalarExpr::call_variadic(
86 Coalesce,
87 vec![
88 MirScalarExpr::column(labels_idx),
89 MirScalarExpr::literal_from_single_element_row(empty_map_row, labels_repr_type),
90 ],
91 );
92 let help_coalesced = MirScalarExpr::call_variadic(
93 Coalesce,
94 vec![
95 MirScalarExpr::column(help_idx),
96 MirScalarExpr::literal_ok(Datum::String(""), ReprScalarType::String),
97 ],
98 );
99
100 let metric_type_literal = |s: &'static str| {
101 MirScalarExpr::column(metric_type_idx).call_binary(
102 MirScalarExpr::literal_ok(Datum::String(s), ReprScalarType::String),
103 func::Eq,
104 )
105 };
106 let metric_kind = metric_type_literal("gauge").if_then_else(
107 MirScalarExpr::literal_ok(Datum::Int32(0), ReprScalarType::Int32),
108 metric_type_literal("counter").if_then_else(
109 MirScalarExpr::literal_ok(Datum::Int32(1), ReprScalarType::Int32),
110 MirScalarExpr::literal_null(ReprScalarType::Int32),
111 ),
112 );
113
114 let name_valid = MirScalarExpr::column(metric_name_idx)
117 .call_is_null()
118 .not()
119 .and(MirScalarExpr::column(metric_name_idx).call_binary(
120 MirScalarExpr::literal_ok(Datum::String(METRIC_NAME_PATTERN), ReprScalarType::String),
121 func::IsRegexpMatchCaseSensitive,
122 ));
123
124 let shaped_expr = MirRelationExpr::global_get(from_id, repr_typ)
125 .map(vec![
126 labels_coalesced,
127 help_coalesced,
128 metric_kind,
129 name_valid,
130 ])
131 .project(vec![
132 metric_name_idx,
133 metric_type_idx,
134 arity, value_idx,
136 arity + 1, arity + 2, arity + 3, ]);
140
141 let mut labels_shaped_ct = labels_ct.clone();
142 labels_shaped_ct.nullable = false;
143 let mut help_shaped_ct = help_ct.clone();
144 help_shaped_ct.nullable = false;
145 let shaped_desc = RelationDesc::from_names_and_types([
146 ("metric_name", metric_name_ct.clone()),
147 ("metric_type", metric_type_ct.clone()),
148 ("labels", labels_shaped_ct),
149 ("value", value_ct.clone()),
150 ("help", help_shaped_ct),
151 ("metric_kind", SqlScalarType::Int32.nullable(true)),
152 ("name_valid", SqlScalarType::Bool.nullable(true)),
153 ]);
154
155 (shaped_expr, shaped_desc)
156}
157
158#[cfg(test)]
159mod tests {
160 use mz_expr::Eval;
161 use mz_repr::{RowArena, SqlColumnType};
162
163 use super::*;
164
165 fn source_desc() -> RelationDesc {
169 RelationDesc::builder()
170 .with_column("metric_name", SqlScalarType::String.nullable(true))
171 .with_column("metric_type", SqlScalarType::String.nullable(false))
172 .with_column(
173 "labels",
174 SqlScalarType::Map {
175 value_type: Box::new(SqlScalarType::String),
176 custom_id: None,
177 }
178 .nullable(true),
179 )
180 .with_column("value", SqlScalarType::Float64.nullable(true))
181 .with_column("help", SqlScalarType::String.nullable(true))
182 .with_column("extra", SqlScalarType::String.nullable(true))
183 .finish()
184 }
185
186 #[mz_ore::test]
187 fn shaped_desc_column_contract() {
188 let (_expr, desc) = shape_metric_sink_source(GlobalId::Transient(0), &source_desc());
189
190 let cols: Vec<(String, SqlColumnType)> = desc
191 .iter()
192 .map(|(name, ty)| (name.as_str().to_string(), ty.clone()))
193 .collect();
194
195 let names: Vec<&str> = cols.iter().map(|(n, _)| n.as_str()).collect();
198 assert_eq!(
199 names,
200 vec![
201 "metric_name",
202 "metric_type",
203 "labels",
204 "value",
205 "help",
206 "metric_kind",
207 "name_valid",
208 ]
209 );
210
211 let by_name = |name: &str| {
212 cols.iter()
213 .find(|(n, _)| n == name)
214 .expect("column present in shaped desc")
215 .1
216 .clone()
217 };
218
219 assert!(!by_name("labels").nullable);
221 assert!(!by_name("help").nullable);
222
223 assert!(by_name("metric_name").nullable);
225 assert!(by_name("value").nullable);
226
227 assert_eq!(by_name("metric_kind"), SqlScalarType::Int32.nullable(true));
229 assert_eq!(by_name("name_valid"), SqlScalarType::Bool.nullable(true));
230 }
231
232 #[mz_ore::test]
233 fn shaped_expr_projects_seven_columns() {
234 let (expr, _desc) = shape_metric_sink_source(GlobalId::Transient(0), &source_desc());
235
236 match &expr {
239 MirRelationExpr::Project { outputs, .. } => {
240 assert_eq!(outputs.len(), 7);
241 }
242 other => panic!("expected a Project at the root of the shaped expr, got {other:?}"),
243 }
244 }
245
246 fn shaped_map_scalars(desc: &RelationDesc) -> Vec<MirScalarExpr> {
250 let (expr, _desc) = shape_metric_sink_source(GlobalId::Transient(0), desc);
251 match expr {
252 MirRelationExpr::Project { input, .. } => match *input {
253 MirRelationExpr::Map { scalars, .. } => scalars,
254 other => panic!("expected a Map under the Project, got {other:?}"),
255 },
256 other => panic!("expected a Project at the root, got {other:?}"),
257 }
258 }
259
260 #[mz_ore::test]
261 fn metric_kind_classifies_type() {
262 let scalars = shaped_map_scalars(&source_desc());
263 let metric_kind = &scalars[2];
264 let arena = RowArena::new();
265 for (metric_type, expected) in [
267 ("gauge", Datum::Int32(0)),
268 ("counter", Datum::Int32(1)),
269 ("histogram", Datum::Null),
270 ("summary", Datum::Null),
271 ] {
272 let row = [
273 Datum::Null,
274 Datum::String(metric_type),
275 Datum::Null,
276 Datum::Null,
277 Datum::Null,
278 Datum::Null,
279 ];
280 assert_eq!(
281 metric_kind
282 .eval(&row, &arena)
283 .expect("metric_kind eval succeeds"),
284 expected,
285 "metric_type = {metric_type}",
286 );
287 }
288 }
289
290 #[mz_ore::test]
291 fn name_valid_matches_prometheus_grammar() {
292 let scalars = shaped_map_scalars(&source_desc());
293 let name_valid = &scalars[3];
294 let arena = RowArena::new();
295 for (metric_name, expected) in [
296 (Datum::String("http_requests_total"), Datum::True),
297 (Datum::String("with:colons_and_1_digit"), Datum::True),
298 (Datum::String("1_leading_digit"), Datum::False),
299 (Datum::String("has-a-dash"), Datum::False),
300 (Datum::String(""), Datum::False),
301 (Datum::Null, Datum::False),
302 ] {
303 let row = [
304 metric_name,
305 Datum::Null,
306 Datum::Null,
307 Datum::Null,
308 Datum::Null,
309 Datum::Null,
310 ];
311 assert_eq!(
312 name_valid
313 .eval(&row, &arena)
314 .expect("name_valid eval succeeds"),
315 expected,
316 "metric_name = {metric_name:?}",
317 );
318 }
319 }
320}