1use crate::project::ast::Statement;
32use crate::project::ir::compiled::FullyQualifiedName;
33use crate::project::ir::object_id::ObjectId;
34use crate::project::ir::unit_test::{ExpectedResult, MockView, UnitTest};
35use crate::project::resolve::normalize::NormalizingVisitor;
36use crate::types::ColumnType;
37#[cfg(test)]
38use crate::types::Types;
39use mz_sql_parser::ast::{CreateViewStatement, IfExistsBehavior, ViewDefinition};
40use owo_colors::{OwoColorize, Stream, Style};
41use serde::Serialize;
42use std::collections::{BTreeMap, BTreeSet};
43use std::fmt;
44use thiserror::Error;
45
46#[derive(Debug, Error, Serialize)]
48pub enum TestValidationError {
49 #[error("unmocked dependency")]
51 UnmockedDependency(UnmockedDependencyError),
52
53 #[error("mock schema mismatch")]
55 MockSchemaMismatch(MockSchemaMismatchError),
56
57 #[error("expected output schema mismatch")]
59 ExpectedSchemaMismatch(ExpectedSchemaMismatchError),
60
61 #[error("invalid at_time timestamp")]
63 InvalidAtTime(InvalidAtTimeError),
64
65 #[error("types cache unavailable: {reason}")]
67 TypesCacheUnavailable { reason: String },
68}
69
70#[derive(Debug, Serialize)]
72pub struct UnmockedDependencyError {
73 pub test_name: String,
75 pub target_view: String,
77 pub missing_mocks: Vec<String>,
79}
80
81impl fmt::Display for UnmockedDependencyError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 let error_style = Style::new().bright_red().bold();
84 let marker_style = Style::new().bright_blue().bold();
85 writeln!(
86 f,
87 "{}: test '{}' has unmocked dependencies",
88 "error".if_supports_color(Stream::Stderr, |t| error_style.style(t)),
89 self.test_name
90 .if_supports_color(Stream::Stderr, |t| t.cyan())
91 )?;
92 writeln!(
93 f,
94 " {} target view: {}",
95 "-->".if_supports_color(Stream::Stderr, |t| marker_style.style(t)),
96 self.target_view
97 .if_supports_color(Stream::Stderr, |t| t.yellow())
98 )?;
99 writeln!(f)?;
100 writeln!(
101 f,
102 " {} The following dependencies must be mocked:",
103 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t))
104 )?;
105 for dep in &self.missing_mocks {
106 writeln!(
107 f,
108 " {} - {}",
109 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t)),
110 dep.if_supports_color(Stream::Stderr, |t| t.yellow())
111 )?;
112 }
113 writeln!(f)?;
114 writeln!(
115 f,
116 " {} Add mocks for these dependencies in the WITH clause of the test",
117 "=".if_supports_color(Stream::Stderr, |t| marker_style.style(t))
118 )?;
119 Ok(())
120 }
121}
122
123impl std::error::Error for UnmockedDependencyError {}
124
125#[derive(Debug, Serialize)]
127pub struct MockSchemaMismatchError {
128 pub test_name: String,
130 pub mock_fqn: String,
132 pub extra_columns: Vec<String>,
134 pub missing_columns: Vec<(String, String)>,
136 pub type_mismatches: Vec<(String, String, String)>,
138 pub actual_schema: Vec<(String, String)>,
140}
141
142impl fmt::Display for MockSchemaMismatchError {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 let error_style = Style::new().bright_red().bold();
145 let marker_style = Style::new().bright_blue().bold();
146 writeln!(
147 f,
148 "{}: mock '{}' schema doesn't match actual schema",
149 "error".if_supports_color(Stream::Stderr, |t| error_style.style(t)),
150 self.mock_fqn
151 .if_supports_color(Stream::Stderr, |t| t.cyan())
152 )?;
153 writeln!(
154 f,
155 " {} in test: {}",
156 "-->".if_supports_color(Stream::Stderr, |t| marker_style.style(t)),
157 self.test_name
158 .if_supports_color(Stream::Stderr, |t| t.yellow())
159 )?;
160 writeln!(f)?;
161
162 if !self.missing_columns.is_empty() {
163 writeln!(
164 f,
165 " {} Missing columns (required but not in mock):",
166 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t))
167 )?;
168 for (col, typ) in &self.missing_columns {
169 writeln!(
170 f,
171 " {} - {} {}",
172 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t)),
173 col.if_supports_color(Stream::Stderr, |t| t.red()),
174 typ.to_uppercase()
175 .if_supports_color(Stream::Stderr, |t| t.dimmed())
176 )?;
177 }
178 }
179
180 if !self.extra_columns.is_empty() {
181 writeln!(
182 f,
183 " {} Extra columns (in mock but not in actual schema):",
184 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t))
185 )?;
186 for col in &self.extra_columns {
187 writeln!(
188 f,
189 " {} - {}",
190 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t)),
191 col.if_supports_color(Stream::Stderr, |t| t.yellow())
192 )?;
193 }
194 }
195
196 if !self.type_mismatches.is_empty() {
197 writeln!(
198 f,
199 " {} Type mismatches:",
200 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t))
201 )?;
202 for (col, mock_type, actual_type) in &self.type_mismatches {
203 writeln!(
204 f,
205 " {} - {}: mock has '{}', expected '{}'",
206 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t)),
207 col.if_supports_color(Stream::Stderr, |t| t.cyan()),
208 mock_type.if_supports_color(Stream::Stderr, |t| t.red()),
209 actual_type.if_supports_color(Stream::Stderr, |t| t.green())
210 )?;
211 }
212 }
213
214 writeln!(f)?;
215
216 if !self.actual_schema.is_empty() {
217 writeln!(
218 f,
219 " {} Expected mock signature:",
220 "=".if_supports_color(Stream::Stderr, |t| marker_style.style(t))
221 )?;
222 let cols: Vec<String> = self
223 .actual_schema
224 .iter()
225 .map(|(name, typ)| format!("{} {}", name, typ.to_uppercase()))
226 .collect();
227 writeln!(
228 f,
229 " {} MOCK {}({}) AS (...)",
230 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t)),
231 self.mock_fqn
232 .if_supports_color(Stream::Stderr, |t| t.green()),
233 cols.join(", ")
234 .if_supports_color(Stream::Stderr, |t| t.green())
235 )?;
236 }
237
238 Ok(())
239 }
240}
241
242impl std::error::Error for MockSchemaMismatchError {}
243
244#[derive(Debug, Serialize)]
246pub struct ExpectedSchemaMismatchError {
247 pub test_name: String,
249 pub target_view: String,
251 pub extra_columns: Vec<String>,
253 pub missing_columns: Vec<(String, String)>,
255 pub type_mismatches: Vec<(String, String, String)>,
257 pub actual_schema: Vec<(String, String)>,
259}
260
261impl fmt::Display for ExpectedSchemaMismatchError {
262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263 let error_style = Style::new().bright_red().bold();
264 let marker_style = Style::new().bright_blue().bold();
265 writeln!(
266 f,
267 "{}: expected output schema doesn't match target view",
268 "error".if_supports_color(Stream::Stderr, |t| error_style.style(t))
269 )?;
270 writeln!(
271 f,
272 " {} target: {} | test: {}",
273 "-->".if_supports_color(Stream::Stderr, |t| marker_style.style(t)),
274 self.target_view
275 .if_supports_color(Stream::Stderr, |t| t.cyan()),
276 self.test_name
277 .if_supports_color(Stream::Stderr, |t| t.yellow())
278 )?;
279 writeln!(f)?;
280
281 if !self.missing_columns.is_empty() {
282 writeln!(
283 f,
284 " {} Missing columns (in target view but not in expected):",
285 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t))
286 )?;
287 for (col, typ) in &self.missing_columns {
288 writeln!(
289 f,
290 " {} - {} {}",
291 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t)),
292 col.if_supports_color(Stream::Stderr, |t| t.red()),
293 typ.to_uppercase()
294 .if_supports_color(Stream::Stderr, |t| t.dimmed())
295 )?;
296 }
297 }
298
299 if !self.extra_columns.is_empty() {
300 writeln!(
301 f,
302 " {} Extra columns (in expected but not in target view):",
303 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t))
304 )?;
305 for col in &self.extra_columns {
306 writeln!(
307 f,
308 " {} - {}",
309 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t)),
310 col.if_supports_color(Stream::Stderr, |t| t.yellow())
311 )?;
312 }
313 }
314
315 if !self.type_mismatches.is_empty() {
316 writeln!(
317 f,
318 " {} Type mismatches:",
319 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t))
320 )?;
321 for (col, expected_type, actual_type) in &self.type_mismatches {
322 writeln!(
323 f,
324 " {} - {}: has '{}', expected '{}'",
325 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t)),
326 col.if_supports_color(Stream::Stderr, |t| t.cyan()),
327 expected_type.if_supports_color(Stream::Stderr, |t| t.red()),
328 actual_type.if_supports_color(Stream::Stderr, |t| t.green())
329 )?;
330 }
331 }
332
333 writeln!(f)?;
334
335 if !self.actual_schema.is_empty() {
336 writeln!(
337 f,
338 " {} Expected signature:",
339 "=".if_supports_color(Stream::Stderr, |t| marker_style.style(t))
340 )?;
341 let cols: Vec<String> = self
342 .actual_schema
343 .iter()
344 .map(|(name, typ)| format!("{} {}", name, typ.to_uppercase()))
345 .collect();
346 writeln!(
347 f,
348 " {} EXPECTED({}) AS (...)",
349 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t)),
350 cols.join(", ")
351 .if_supports_color(Stream::Stderr, |t| t.green())
352 )?;
353 }
354
355 Ok(())
356 }
357}
358
359impl std::error::Error for ExpectedSchemaMismatchError {}
360
361#[derive(Debug, Serialize)]
363pub struct InvalidAtTimeError {
364 pub test_name: String,
366 pub at_time_value: String,
368 pub db_error: String,
370}
371
372impl fmt::Display for InvalidAtTimeError {
373 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374 let error_style = Style::new().bright_red().bold();
375 let marker_style = Style::new().bright_blue().bold();
376 writeln!(
377 f,
378 "{}: test '{}' has invalid AT TIME value",
379 "error".if_supports_color(Stream::Stderr, |t| error_style.style(t)),
380 self.test_name
381 .if_supports_color(Stream::Stderr, |t| t.cyan())
382 )?;
383 writeln!(
384 f,
385 " {} value: {}",
386 "-->".if_supports_color(Stream::Stderr, |t| marker_style.style(t)),
387 self.at_time_value
388 .if_supports_color(Stream::Stderr, |t| t.yellow())
389 )?;
390 writeln!(f)?;
391
392 let display_error = self
395 .db_error
396 .find("invalid input syntax")
397 .map(|idx| &self.db_error[idx..])
398 .unwrap_or(&self.db_error);
399
400 writeln!(
401 f,
402 " {} {}",
403 "|".if_supports_color(Stream::Stderr, |t| marker_style.style(t)),
404 display_error.if_supports_color(Stream::Stderr, |t| t.red())
405 )?;
406 writeln!(f)?;
407 writeln!(
408 f,
409 " {} The AT TIME value must be a valid timestamp that can be cast to mz_timestamp",
410 "=".if_supports_color(Stream::Stderr, |t| marker_style.style(t))
411 )?;
412 writeln!(
413 f,
414 " {} Example: AT TIME '2024-01-15 10:00:00'",
415 "=".if_supports_color(Stream::Stderr, |t| marker_style.style(t))
416 )?;
417 Ok(())
418 }
419}
420
421impl std::error::Error for InvalidAtTimeError {}
422
423pub(super) fn validate_unit_test(
438 test: &UnitTest,
439 target_id: &ObjectId,
440 get_columns: &dyn Fn(&ObjectId) -> Option<BTreeMap<String, ColumnType>>,
441 dependencies: &BTreeSet<ObjectId>,
442) -> Result<(), TestValidationError> {
443 let mocked_ids: BTreeSet<ObjectId> = test
444 .mocks
445 .iter()
446 .map(|m| normalize_fqn(&m.fqn, target_id))
447 .collect();
448
449 let missing_mocks: Vec<String> = dependencies
450 .iter()
451 .filter(|dep| !mocked_ids.contains(*dep))
452 .map(|dep| dep.to_string())
453 .collect();
454
455 if !missing_mocks.is_empty() {
456 return Err(TestValidationError::UnmockedDependency(
457 UnmockedDependencyError {
458 test_name: test.name.clone(),
459 target_view: target_id.to_string(),
460 missing_mocks,
461 },
462 ));
463 }
464
465 for mock in &test.mocks {
466 let mock_id = normalize_fqn(&mock.fqn, target_id);
467
468 if let Some(actual_columns) = get_columns(&mock_id) {
469 let (extra, missing, type_mismatches) = compare_columns(&mock.columns, &actual_columns);
470
471 if !extra.is_empty() || !missing.is_empty() || !type_mismatches.is_empty() {
472 let actual_schema: Vec<(String, String)> = actual_columns
473 .iter()
474 .map(|(name, col_type)| (name.clone(), col_type.r#type.to_string()))
475 .collect();
476
477 return Err(TestValidationError::MockSchemaMismatch(
478 MockSchemaMismatchError {
479 test_name: test.name.clone(),
480 mock_fqn: mock_id.to_string(),
481 extra_columns: extra,
482 missing_columns: missing,
483 type_mismatches,
484 actual_schema,
485 },
486 ));
487 }
488 }
489 }
493
494 if let Some(target_columns) = get_columns(target_id) {
495 let (extra, missing, type_mismatches) =
496 compare_columns(&test.expected.columns, &target_columns);
497
498 if !extra.is_empty() || !missing.is_empty() || !type_mismatches.is_empty() {
499 let actual_schema: Vec<(String, String)> = target_columns
500 .iter()
501 .map(|(name, col_type)| (name.clone(), col_type.r#type.to_string()))
502 .collect();
503
504 return Err(TestValidationError::ExpectedSchemaMismatch(
505 ExpectedSchemaMismatchError {
506 test_name: test.name.clone(),
507 target_view: target_id.to_string(),
508 extra_columns: extra,
509 missing_columns: missing,
510 type_mismatches,
511 actual_schema,
512 },
513 ));
514 }
515 }
516 Ok(())
519}
520
521fn normalize_fqn(fqn: &str, target_id: &ObjectId) -> ObjectId {
523 let parts: Vec<&str> = fqn.split('.').collect();
524 match parts.as_slice() {
525 [object] => ObjectId::new(
526 target_id.expect_database().to_string(),
527 target_id.schema().to_string(),
528 (*object).to_string(),
529 ),
530 [schema, object] => ObjectId::new(
531 target_id.expect_database().to_string(),
532 (*schema).to_string(),
533 (*object).to_string(),
534 ),
535 [db, schema, object] => ObjectId::new(
536 (*db).to_string(),
537 (*schema).to_string(),
538 (*object).to_string(),
539 ),
540 _ => ObjectId::new(String::new(), String::new(), fqn.to_string()),
541 }
542}
543
544fn compare_columns(
548 test_columns: &[(String, String)],
549 actual_columns: &BTreeMap<String, ColumnType>,
550) -> (
551 Vec<String>,
552 Vec<(String, String)>,
553 Vec<(String, String, String)>,
554) {
555 let test_col_names: BTreeSet<&str> = test_columns.iter().map(|(n, _)| n.as_str()).collect();
556 let actual_col_names: BTreeSet<&str> = actual_columns.keys().map(|s| s.as_str()).collect();
557
558 let extra: Vec<String> = test_col_names
559 .difference(&actual_col_names)
560 .map(|s| (*s).to_string())
561 .collect();
562
563 let missing: Vec<(String, String)> = actual_col_names
564 .difference(&test_col_names)
565 .map(|s| {
566 let typ = actual_columns
567 .get(*s)
568 .map(|c| c.r#type.to_string())
569 .unwrap_or_default();
570 ((*s).to_string(), typ)
571 })
572 .collect();
573
574 let type_mismatches: Vec<(String, String, String)> = test_columns
575 .iter()
576 .filter_map(|(name, test_type)| {
577 actual_columns.get(name).and_then(|actual| {
578 let test_normalized = normalize_type(test_type);
579 let actual_type = actual.r#type.to_string();
580 let actual_normalized = normalize_type(&actual_type);
581
582 if test_normalized != actual_normalized {
583 if types_match_with_bare_containers(&test_normalized, &actual_normalized) {
586 None
587 } else {
588 Some((name.clone(), test_type.clone(), actual_type))
589 }
590 } else {
591 None
592 }
593 })
594 })
595 .collect();
596
597 (extra, missing, type_mismatches)
598}
599
600fn types_match_with_bare_containers(a: &str, b: &str) -> bool {
605 if a == "list" && b.ends_with(" list") || b == "list" && a.ends_with(" list") {
606 return true;
607 }
608 if a == "[]" && b.ends_with("[]") || b == "[]" && a.ends_with("[]") {
609 return true;
610 }
611 if a == "map" && b.starts_with("map[") || b == "map" && a.starts_with("map[") {
612 return true;
613 }
614 false
615}
616
617fn normalize_type(t: &str) -> String {
622 let normalized = t.trim().to_lowercase();
623
624 if let Some(element) = normalized.strip_suffix(" list") {
625 if !element.is_empty() {
626 return format!("{} list", normalize_type(element));
627 }
628 }
629
630 if let Some(element) = normalized.strip_suffix("[]") {
631 if !element.is_empty() {
632 return format!("{}[]", normalize_type(element));
633 }
634 }
635
636 if let Some(inner) = normalized
637 .strip_prefix("map[")
638 .and_then(|s| s.strip_suffix(']'))
639 {
640 if let Some((key, value)) = inner.split_once("=>") {
641 return format!("map[{}=>{}]", normalize_type(key), normalize_type(value));
642 }
643 }
644
645 match normalized.as_str() {
646 "int" | "int4" | "integer" => "integer".to_string(),
647 "int8" | "bigint" => "bigint".to_string(),
648 "int2" | "smallint" => "smallint".to_string(),
649
650 "float4" | "real" => "real".to_string(),
651 "float" | "float8" | "double" | "double precision" => "double precision".to_string(),
652
653 "bool" | "boolean" => "boolean".to_string(),
654
655 "string" | "text" => "text".to_string(),
656 "varchar" | "character varying" => "text".to_string(),
657
658 "decimal" | "numeric" => "numeric".to_string(),
659
660 "json" | "jsonb" => "jsonb".to_string(),
661
662 "timestamp" | "timestamp without time zone" => "timestamp without time zone".to_string(),
663 "timestamptz" | "timestamp with time zone" => "timestamp with time zone".to_string(),
664
665 "_aclitem" => "aclitem[]".to_string(),
669 "_mz_aclitem" => "mz_aclitem[]".to_string(),
670 "_mz_timestamp" => "mz_timestamp[]".to_string(),
671 "_record" => "record[]".to_string(),
672 "_uint2" => "uint2[]".to_string(),
673 "_uint4" => "uint4[]".to_string(),
674 "_uint8" => "uint8[]".to_string(),
675
676 _ => {
677 if normalized.starts_with("varchar") || normalized.starts_with("character varying") {
678 "text".to_string()
679 } else if normalized.starts_with("numeric") || normalized.starts_with("decimal") {
680 "numeric".to_string()
681 } else if normalized.starts_with("timestamp with time zone")
682 || normalized.starts_with("timestamptz")
683 {
684 "timestamp with time zone".to_string()
685 } else if normalized.starts_with("timestamp without time zone")
686 || normalized == "timestamp"
687 {
688 "timestamp without time zone".to_string()
689 } else {
690 normalized
691 }
692 }
693 }
694}
695
696pub(super) fn lower_unit_test(
704 test: &UnitTest,
705 target_stmt: &Statement,
706 target_fqn: &FullyQualifiedName,
707) -> Result<Vec<String>, String> {
708 let mut statements = Vec::new();
709
710 for mock in &test.mocks {
711 let qualified_mock = qualify_mock_name(mock, target_fqn);
712 statements.push(create_mock_view_sql(&qualified_mock));
713 }
714
715 statements.push(create_expected_view_sql(&test.expected));
716
717 statements.push(create_target_view_sql(target_stmt, target_fqn)?);
718
719 let target_fqn_str = format!(
720 "{}.{}.{}",
721 target_fqn.database(),
722 target_fqn.schema(),
723 target_fqn.object()
724 );
725 let flattened_target_name = flatten_fqn(&target_fqn_str);
726 statements.push(create_test_query_sql(
727 &flattened_target_name,
728 test.at_time.as_deref(),
729 ));
730
731 Ok(statements)
732}
733
734fn flatten_fqn(fqn: &str) -> String {
736 format!("\"{}\"", fqn)
737}
738
739fn qualify_mock_name(mock: &MockView, target_fqn: &FullyQualifiedName) -> MockView {
741 let parts = mock.fqn.matches('.').count() + 1;
742
743 let qualified_fqn = match parts {
744 1 => format!(
745 "{}.{}.{}",
746 target_fqn.database(),
747 target_fqn.schema(),
748 mock.fqn
749 ),
750 2 => format!("{}.{}", target_fqn.database(), mock.fqn),
751 _ => mock.fqn.clone(),
752 };
753
754 MockView {
755 fqn: qualified_fqn,
756 columns: mock.columns.clone(),
757 query: mock.query.clone(),
758 }
759}
760
761fn create_mock_view_sql(mock: &MockView) -> String {
762 let flattened_name = flatten_fqn(&mock.fqn);
763 let columns_def = mock
764 .columns
765 .iter()
766 .map(|(name, typ)| format!("{} {}", name, typ))
767 .collect::<Vec<_>>()
768 .join(", ");
769
770 format!(
771 "CREATE TEMPORARY VIEW {} AS\nWITH MUTUALLY RECURSIVE data({}) AS (\n {}\n)\nSELECT * FROM data;",
772 flattened_name, columns_def, mock.query
773 )
774}
775
776fn create_expected_view_sql(expected: &ExpectedResult) -> String {
777 let columns_def = expected
778 .columns
779 .iter()
780 .map(|(name, typ)| format!("{} {}", name, typ))
781 .collect::<Vec<_>>()
782 .join(", ");
783
784 format!(
785 "CREATE TEMPORARY VIEW expected AS\nWITH MUTUALLY RECURSIVE data({}) AS (\n {}\n)\nSELECT * FROM data;",
786 columns_def, expected.query
787 )
788}
789
790fn create_target_view_sql(stmt: &Statement, fqn: &FullyQualifiedName) -> Result<String, String> {
795 let mut visitor = NormalizingVisitor::flattening(fqn);
796 let transformed_stmt = stmt
797 .clone()
798 .normalize_name_with(&visitor, &fqn.to_item_name())
799 .normalize_dependencies_with(&mut visitor);
800
801 let view_stmt = match transformed_stmt {
802 Statement::CreateView(view) => CreateViewStatement {
803 if_exists: IfExistsBehavior::Error,
804 temporary: true,
805 definition: view.definition.clone(),
806 },
807 Statement::CreateMaterializedView(mv) => CreateViewStatement {
808 if_exists: IfExistsBehavior::Error,
809 temporary: true,
810 definition: ViewDefinition {
811 name: mv.name,
812 columns: mv.columns,
813 query: mv.query,
814 },
815 },
816 other => {
817 return Err(format!(
818 "unit tests are only supported on views and materialized views; \
819 target '{}.{}.{}' is a {}",
820 fqn.database(),
821 fqn.schema(),
822 fqn.object(),
823 other.kind(),
824 ));
825 }
826 };
827 Ok(view_stmt.to_string())
828}
829
830fn create_test_query_sql(flattened_target_name: &str, at_time: Option<&str>) -> String {
841 let as_of_clause = at_time
842 .map(|t| format!(" AS OF {}::mz_timestamp", t))
843 .unwrap_or_default();
844 format!(
845 r#"SELECT 'MISSING' as status, * FROM expected
846EXCEPT
847SELECT 'MISSING', * FROM {}
848
849UNION ALL
850
851SELECT 'UNEXPECTED' as status, * FROM {}
852EXCEPT
853SELECT 'UNEXPECTED', * FROM expected{}"#,
854 flattened_target_name, flattened_target_name, as_of_clause
855 )
856}
857
858#[cfg(test)]
859mod tests {
860 use super::*;
861 use crate::types::{ColumnType, DataType};
862 use std::collections::BTreeMap;
863
864 #[mz_ore::test]
865 fn test_flatten_fqn() {
866 assert_eq!(
867 flatten_fqn("materialize.public.flippers"),
868 "\"materialize.public.flippers\""
869 );
870 assert_eq!(flatten_fqn("a.b.c"), "\"a.b.c\"");
871 assert_eq!(flatten_fqn("single"), "\"single\"");
872 }
873
874 #[mz_ore::test]
875 fn test_create_mock_view_sql() {
876 let mock = MockView {
877 fqn: "materialize.public.users".to_string(),
878 columns: vec![
879 ("id".to_string(), "BIGINT".to_string()),
880 ("name".to_string(), "TEXT".to_string()),
881 ],
882 query: "SELECT * FROM VALUES ((1, 'alice'))".to_string(),
883 };
884
885 let sql = create_mock_view_sql(&mock);
886
887 assert!(sql.contains("CREATE TEMPORARY VIEW \"materialize.public.users\""));
888 assert!(sql.contains("WITH MUTUALLY RECURSIVE data(id BIGINT, name TEXT)"));
889 assert!(sql.contains("SELECT * FROM VALUES ((1, 'alice'))"));
890 assert!(sql.contains("SELECT * FROM data"));
891 }
892
893 #[mz_ore::test]
894 fn test_create_expected_view_sql() {
895 let expected = ExpectedResult {
896 columns: vec![
897 ("id".to_string(), "BIGINT".to_string()),
898 ("count".to_string(), "INT".to_string()),
899 ],
900 query: "SELECT * FROM VALUES ((1, 10))".to_string(),
901 };
902
903 let sql = create_expected_view_sql(&expected);
904
905 assert!(sql.contains("CREATE TEMPORARY VIEW expected"));
906 assert!(sql.contains("WITH MUTUALLY RECURSIVE data(id BIGINT, count INT)"));
907 assert!(sql.contains("SELECT * FROM VALUES ((1, 10))"));
908 assert!(sql.contains("SELECT * FROM data"));
909 }
910
911 #[mz_ore::test]
912 fn test_create_test_query_sql() {
913 let sql = create_test_query_sql("materialize_public_my_view", None);
914
915 assert!(sql.contains("SELECT 'MISSING' as status, * FROM expected"));
916 assert!(sql.contains("SELECT 'MISSING', * FROM materialize_public_my_view"));
917 assert!(sql.contains("SELECT 'UNEXPECTED' as status, * FROM materialize_public_my_view"));
918 assert!(sql.contains("SELECT 'UNEXPECTED', * FROM expected"));
919 assert!(sql.contains("UNION ALL"));
920 assert!(sql.contains("EXCEPT"));
921 assert!(!sql.contains("AS OF"));
922 }
923
924 #[mz_ore::test]
925 fn test_create_test_query_sql_with_at_time() {
926 let sql =
927 create_test_query_sql("materialize_public_my_view", Some("'2024-01-15 10:00:00'"));
928
929 assert!(sql.contains("SELECT 'MISSING' as status, * FROM expected"));
930 assert!(sql.contains("AS OF '2024-01-15 10:00:00'::mz_timestamp"));
931 }
932
933 fn make_test_types() -> Types {
934 let mut objects = BTreeMap::new();
935
936 let mut users_cols = BTreeMap::new();
937 users_cols.insert(
938 "id".to_string(),
939 ColumnType {
940 r#type: DataType::named("bigint"),
941 nullable: false,
942 position: 0,
943 comment: None,
944 },
945 );
946 users_cols.insert(
947 "name".to_string(),
948 ColumnType {
949 r#type: DataType::named("text"),
950 nullable: true,
951 position: 1,
952 comment: None,
953 },
954 );
955 users_cols.insert(
956 "email".to_string(),
957 ColumnType {
958 r#type: DataType::named("text"),
959 nullable: true,
960 position: 2,
961 comment: None,
962 },
963 );
964 objects.insert(
965 "materialize.public.users".parse::<ObjectId>().unwrap(),
966 users_cols,
967 );
968
969 let mut orders_cols = BTreeMap::new();
970 orders_cols.insert(
971 "id".to_string(),
972 ColumnType {
973 r#type: DataType::named("bigint"),
974 nullable: false,
975 position: 0,
976 comment: None,
977 },
978 );
979 orders_cols.insert(
980 "user_id".to_string(),
981 ColumnType {
982 r#type: DataType::named("bigint"),
983 nullable: false,
984 position: 1,
985 comment: None,
986 },
987 );
988 orders_cols.insert(
989 "amount".to_string(),
990 ColumnType {
991 r#type: DataType::named("numeric"),
992 nullable: true,
993 position: 2,
994 comment: None,
995 },
996 );
997 objects.insert(
998 "materialize.public.orders".parse::<ObjectId>().unwrap(),
999 orders_cols,
1000 );
1001
1002 let mut summary_cols = BTreeMap::new();
1003 summary_cols.insert(
1004 "user_id".to_string(),
1005 ColumnType {
1006 r#type: DataType::named("bigint"),
1007 nullable: false,
1008 position: 0,
1009 comment: None,
1010 },
1011 );
1012 summary_cols.insert(
1013 "user_name".to_string(),
1014 ColumnType {
1015 r#type: DataType::named("text"),
1016 nullable: true,
1017 position: 1,
1018 comment: None,
1019 },
1020 );
1021 summary_cols.insert(
1022 "total_orders".to_string(),
1023 ColumnType {
1024 r#type: DataType::named("bigint"),
1025 nullable: true,
1026 position: 2,
1027 comment: None,
1028 },
1029 );
1030 objects.insert(
1031 "materialize.public.user_order_summary"
1032 .parse::<ObjectId>()
1033 .unwrap(),
1034 summary_cols,
1035 );
1036
1037 Types {
1038 tables: objects,
1039 kinds: BTreeMap::new(),
1040 comments: BTreeMap::new(),
1041 }
1042 }
1043
1044 fn make_target_id() -> ObjectId {
1045 ObjectId::new(
1046 "materialize".to_string(),
1047 "public".to_string(),
1048 "user_order_summary".to_string(),
1049 )
1050 }
1051
1052 fn make_dependencies() -> BTreeSet<ObjectId> {
1053 let mut deps = BTreeSet::new();
1054 deps.insert(ObjectId::new(
1055 "materialize".to_string(),
1056 "public".to_string(),
1057 "users".to_string(),
1058 ));
1059 deps.insert(ObjectId::new(
1060 "materialize".to_string(),
1061 "public".to_string(),
1062 "orders".to_string(),
1063 ));
1064 deps
1065 }
1066
1067 #[mz_ore::test]
1068 fn test_validate_unit_test_passes_with_correct_mocks() {
1069 let test = UnitTest {
1070 name: "test_user_summary".to_string(),
1071 target_view: "materialize.public.user_order_summary".to_string(),
1072 at_time: None,
1073 mocks: vec![
1074 MockView {
1075 fqn: "materialize.public.users".to_string(),
1076 columns: vec![
1077 ("id".to_string(), "bigint".to_string()),
1078 ("name".to_string(), "text".to_string()),
1079 ("email".to_string(), "text".to_string()),
1080 ],
1081 query: "SELECT * FROM VALUES (1, 'alice', 'alice@example.com')".to_string(),
1082 },
1083 MockView {
1084 fqn: "materialize.public.orders".to_string(),
1085 columns: vec![
1086 ("id".to_string(), "bigint".to_string()),
1087 ("user_id".to_string(), "bigint".to_string()),
1088 ("amount".to_string(), "numeric".to_string()),
1089 ],
1090 query: "SELECT * FROM VALUES (1, 1, 100.00)".to_string(),
1091 },
1092 ],
1093 expected: ExpectedResult {
1094 columns: vec![
1095 ("user_id".to_string(), "bigint".to_string()),
1096 ("user_name".to_string(), "text".to_string()),
1097 ("total_orders".to_string(), "bigint".to_string()),
1098 ],
1099 query: "SELECT * FROM VALUES (1, 'alice', 1)".to_string(),
1100 },
1101 };
1102
1103 let types = make_test_types();
1104 let target_id = make_target_id();
1105 let dependencies = make_dependencies();
1106
1107 let result = validate_unit_test(
1108 &test,
1109 &target_id,
1110 &|fqn| types.get_table(fqn).cloned(),
1111 &dependencies,
1112 );
1113 assert!(result.is_ok(), "Expected validation to pass: {:?}", result);
1114 }
1115
1116 #[mz_ore::test]
1117 fn test_validate_unit_test_fails_with_unmocked_dependency() {
1118 let test = UnitTest {
1119 name: "test_user_summary".to_string(),
1120 target_view: "materialize.public.user_order_summary".to_string(),
1121 at_time: None,
1122 mocks: vec![MockView {
1123 fqn: "materialize.public.users".to_string(),
1124 columns: vec![
1125 ("id".to_string(), "bigint".to_string()),
1126 ("name".to_string(), "text".to_string()),
1127 ("email".to_string(), "text".to_string()),
1128 ],
1129 query: "SELECT * FROM VALUES (1, 'alice', 'alice@example.com')".to_string(),
1130 }],
1131 expected: ExpectedResult {
1132 columns: vec![
1133 ("user_id".to_string(), "bigint".to_string()),
1134 ("user_name".to_string(), "text".to_string()),
1135 ("total_orders".to_string(), "bigint".to_string()),
1136 ],
1137 query: "SELECT * FROM VALUES (1, 'alice', 1)".to_string(),
1138 },
1139 };
1140
1141 let types = make_test_types();
1142 let target_id = make_target_id();
1143 let dependencies = make_dependencies();
1144
1145 let result = validate_unit_test(
1146 &test,
1147 &target_id,
1148 &|fqn| types.get_table(fqn).cloned(),
1149 &dependencies,
1150 );
1151 assert!(result.is_err());
1152
1153 match result.unwrap_err() {
1154 TestValidationError::UnmockedDependency(err) => {
1155 assert_eq!(err.test_name, "test_user_summary");
1156 assert!(
1157 err.missing_mocks
1158 .contains(&"materialize.public.orders".to_string())
1159 );
1160 }
1161 other => panic!("Expected UnmockedDependency error, got: {:?}", other),
1162 }
1163 }
1164
1165 #[mz_ore::test]
1166 fn test_validate_unit_test_fails_with_missing_mock_column() {
1167 let test = UnitTest {
1168 name: "test_user_summary".to_string(),
1169 target_view: "materialize.public.user_order_summary".to_string(),
1170 at_time: None,
1171 mocks: vec![
1172 MockView {
1173 fqn: "materialize.public.users".to_string(),
1174 columns: vec![
1175 ("id".to_string(), "bigint".to_string()),
1176 ("name".to_string(), "text".to_string()),
1177 ],
1178 query: "SELECT * FROM VALUES (1, 'alice')".to_string(),
1179 },
1180 MockView {
1181 fqn: "materialize.public.orders".to_string(),
1182 columns: vec![
1183 ("id".to_string(), "bigint".to_string()),
1184 ("user_id".to_string(), "bigint".to_string()),
1185 ("amount".to_string(), "numeric".to_string()),
1186 ],
1187 query: "SELECT * FROM VALUES (1, 1, 100.00)".to_string(),
1188 },
1189 ],
1190 expected: ExpectedResult {
1191 columns: vec![
1192 ("user_id".to_string(), "bigint".to_string()),
1193 ("user_name".to_string(), "text".to_string()),
1194 ("total_orders".to_string(), "bigint".to_string()),
1195 ],
1196 query: "SELECT * FROM VALUES (1, 'alice', 1)".to_string(),
1197 },
1198 };
1199
1200 let types = make_test_types();
1201 let target_id = make_target_id();
1202 let dependencies = make_dependencies();
1203
1204 let result = validate_unit_test(
1205 &test,
1206 &target_id,
1207 &|fqn| types.get_table(fqn).cloned(),
1208 &dependencies,
1209 );
1210 assert!(result.is_err());
1211
1212 match result.unwrap_err() {
1213 TestValidationError::MockSchemaMismatch(err) => {
1214 assert_eq!(err.test_name, "test_user_summary");
1215 assert_eq!(err.mock_fqn, "materialize.public.users");
1216 assert!(err.missing_columns.iter().any(|(name, _)| name == "email"));
1217 assert!(err.extra_columns.is_empty());
1218 }
1219 other => panic!("Expected MockSchemaMismatch error, got: {:?}", other),
1220 }
1221 }
1222
1223 #[mz_ore::test]
1224 fn test_validate_unit_test_fails_with_extra_mock_column() {
1225 let test = UnitTest {
1226 name: "test_user_summary".to_string(),
1227 target_view: "materialize.public.user_order_summary".to_string(),
1228 at_time: None,
1229 mocks: vec![
1230 MockView {
1231 fqn: "materialize.public.users".to_string(),
1232 columns: vec![
1233 ("id".to_string(), "bigint".to_string()),
1234 ("name".to_string(), "text".to_string()),
1235 ("email".to_string(), "text".to_string()),
1236 ("extra_column".to_string(), "int".to_string()),
1237 ],
1238 query: "SELECT * FROM VALUES (1, 'alice', 'alice@example.com', 42)".to_string(),
1239 },
1240 MockView {
1241 fqn: "materialize.public.orders".to_string(),
1242 columns: vec![
1243 ("id".to_string(), "bigint".to_string()),
1244 ("user_id".to_string(), "bigint".to_string()),
1245 ("amount".to_string(), "numeric".to_string()),
1246 ],
1247 query: "SELECT * FROM VALUES (1, 1, 100.00)".to_string(),
1248 },
1249 ],
1250 expected: ExpectedResult {
1251 columns: vec![
1252 ("user_id".to_string(), "bigint".to_string()),
1253 ("user_name".to_string(), "text".to_string()),
1254 ("total_orders".to_string(), "bigint".to_string()),
1255 ],
1256 query: "SELECT * FROM VALUES (1, 'alice', 1)".to_string(),
1257 },
1258 };
1259
1260 let types = make_test_types();
1261 let target_id = make_target_id();
1262 let dependencies = make_dependencies();
1263
1264 let result = validate_unit_test(
1265 &test,
1266 &target_id,
1267 &|fqn| types.get_table(fqn).cloned(),
1268 &dependencies,
1269 );
1270 assert!(result.is_err());
1271
1272 match result.unwrap_err() {
1273 TestValidationError::MockSchemaMismatch(err) => {
1274 assert_eq!(err.mock_fqn, "materialize.public.users");
1275 assert!(err.extra_columns.contains(&"extra_column".to_string()));
1276 assert!(err.missing_columns.is_empty());
1277 }
1278 other => panic!("Expected MockSchemaMismatch error, got: {:?}", other),
1279 }
1280 }
1281
1282 #[mz_ore::test]
1283 fn test_validate_unit_test_fails_with_type_mismatch() {
1284 let test = UnitTest {
1285 name: "test_user_summary".to_string(),
1286 target_view: "materialize.public.user_order_summary".to_string(),
1287 at_time: None,
1288 mocks: vec![
1289 MockView {
1290 fqn: "materialize.public.users".to_string(),
1291 columns: vec![
1292 ("id".to_string(), "text".to_string()),
1293 ("name".to_string(), "text".to_string()),
1294 ("email".to_string(), "text".to_string()),
1295 ],
1296 query: "SELECT * FROM VALUES ('1', 'alice', 'alice@example.com')".to_string(),
1297 },
1298 MockView {
1299 fqn: "materialize.public.orders".to_string(),
1300 columns: vec![
1301 ("id".to_string(), "bigint".to_string()),
1302 ("user_id".to_string(), "bigint".to_string()),
1303 ("amount".to_string(), "numeric".to_string()),
1304 ],
1305 query: "SELECT * FROM VALUES (1, 1, 100.00)".to_string(),
1306 },
1307 ],
1308 expected: ExpectedResult {
1309 columns: vec![
1310 ("user_id".to_string(), "bigint".to_string()),
1311 ("user_name".to_string(), "text".to_string()),
1312 ("total_orders".to_string(), "bigint".to_string()),
1313 ],
1314 query: "SELECT * FROM VALUES (1, 'alice', 1)".to_string(),
1315 },
1316 };
1317
1318 let types = make_test_types();
1319 let target_id = make_target_id();
1320 let dependencies = make_dependencies();
1321
1322 let result = validate_unit_test(
1323 &test,
1324 &target_id,
1325 &|fqn| types.get_table(fqn).cloned(),
1326 &dependencies,
1327 );
1328 assert!(result.is_err());
1329
1330 match result.unwrap_err() {
1331 TestValidationError::MockSchemaMismatch(err) => {
1332 assert_eq!(err.mock_fqn, "materialize.public.users");
1333 assert!(
1334 err.type_mismatches
1335 .iter()
1336 .any(|(col, mock_t, _)| { col == "id" && mock_t == "text" })
1337 );
1338 }
1339 other => panic!("Expected MockSchemaMismatch error, got: {:?}", other),
1340 }
1341 }
1342
1343 #[mz_ore::test]
1344 fn test_validate_unit_test_fails_with_expected_schema_mismatch() {
1345 let test = UnitTest {
1346 name: "test_user_summary".to_string(),
1347 target_view: "materialize.public.user_order_summary".to_string(),
1348 at_time: None,
1349 mocks: vec![
1350 MockView {
1351 fqn: "materialize.public.users".to_string(),
1352 columns: vec![
1353 ("id".to_string(), "bigint".to_string()),
1354 ("name".to_string(), "text".to_string()),
1355 ("email".to_string(), "text".to_string()),
1356 ],
1357 query: "SELECT * FROM VALUES (1, 'alice', 'alice@example.com')".to_string(),
1358 },
1359 MockView {
1360 fqn: "materialize.public.orders".to_string(),
1361 columns: vec![
1362 ("id".to_string(), "bigint".to_string()),
1363 ("user_id".to_string(), "bigint".to_string()),
1364 ("amount".to_string(), "numeric".to_string()),
1365 ],
1366 query: "SELECT * FROM VALUES (1, 1, 100.00)".to_string(),
1367 },
1368 ],
1369 expected: ExpectedResult {
1370 columns: vec![
1371 ("user_id".to_string(), "bigint".to_string()),
1372 ("total_orders".to_string(), "bigint".to_string()),
1373 ],
1374 query: "SELECT * FROM VALUES (1, 1)".to_string(),
1375 },
1376 };
1377
1378 let types = make_test_types();
1379 let target_id = make_target_id();
1380 let dependencies = make_dependencies();
1381
1382 let result = validate_unit_test(
1383 &test,
1384 &target_id,
1385 &|fqn| types.get_table(fqn).cloned(),
1386 &dependencies,
1387 );
1388 assert!(result.is_err());
1389
1390 match result.unwrap_err() {
1391 TestValidationError::ExpectedSchemaMismatch(err) => {
1392 assert_eq!(err.test_name, "test_user_summary");
1393 assert_eq!(err.target_view, "materialize.public.user_order_summary");
1394 assert!(
1395 err.missing_columns
1396 .iter()
1397 .any(|(name, _)| name == "user_name")
1398 );
1399 }
1400 other => panic!("Expected ExpectedSchemaMismatch error, got: {:?}", other),
1401 }
1402 }
1403
1404 #[mz_ore::test]
1405 fn test_validate_unit_test_fails_with_expected_type_mismatch() {
1406 let test = UnitTest {
1407 name: "test_user_summary".to_string(),
1408 target_view: "materialize.public.user_order_summary".to_string(),
1409 at_time: None,
1410 mocks: vec![
1411 MockView {
1412 fqn: "materialize.public.users".to_string(),
1413 columns: vec![
1414 ("id".to_string(), "bigint".to_string()),
1415 ("name".to_string(), "text".to_string()),
1416 ("email".to_string(), "text".to_string()),
1417 ],
1418 query: "SELECT * FROM VALUES (1, 'alice', 'alice@example.com')".to_string(),
1419 },
1420 MockView {
1421 fqn: "materialize.public.orders".to_string(),
1422 columns: vec![
1423 ("id".to_string(), "bigint".to_string()),
1424 ("user_id".to_string(), "bigint".to_string()),
1425 ("amount".to_string(), "numeric".to_string()),
1426 ],
1427 query: "SELECT * FROM VALUES (1, 1, 100.00)".to_string(),
1428 },
1429 ],
1430 expected: ExpectedResult {
1431 columns: vec![
1432 ("user_id".to_string(), "bigint".to_string()),
1433 ("user_name".to_string(), "bigint".to_string()),
1434 ("total_orders".to_string(), "bigint".to_string()),
1435 ],
1436 query: "SELECT * FROM VALUES (1, 1, 1)".to_string(),
1437 },
1438 };
1439
1440 let types = make_test_types();
1441 let target_id = make_target_id();
1442 let dependencies = make_dependencies();
1443
1444 let result = validate_unit_test(
1445 &test,
1446 &target_id,
1447 &|fqn| types.get_table(fqn).cloned(),
1448 &dependencies,
1449 );
1450 assert!(result.is_err());
1451
1452 match result.unwrap_err() {
1453 TestValidationError::ExpectedSchemaMismatch(err) => {
1454 assert!(
1455 err.type_mismatches
1456 .iter()
1457 .any(|(col, exp_t, _)| { col == "user_name" && exp_t == "bigint" })
1458 );
1459 }
1460 other => panic!("Expected ExpectedSchemaMismatch error, got: {:?}", other),
1461 }
1462 }
1463
1464 #[mz_ore::test]
1465 fn test_normalize_fqn_unqualified() {
1466 let target_id = ObjectId::new(
1467 "mydb".to_string(),
1468 "myschema".to_string(),
1469 "myview".to_string(),
1470 );
1471
1472 let normalized = normalize_fqn("users", &target_id);
1473 assert_eq!(normalized.to_string(), "mydb.myschema.users");
1474 }
1475
1476 #[mz_ore::test]
1477 fn test_normalize_fqn_schema_qualified() {
1478 let target_id = ObjectId::new(
1479 "mydb".to_string(),
1480 "myschema".to_string(),
1481 "myview".to_string(),
1482 );
1483
1484 let normalized = normalize_fqn("other_schema.users", &target_id);
1485 assert_eq!(normalized.to_string(), "mydb.other_schema.users");
1486 }
1487
1488 #[mz_ore::test]
1489 fn test_normalize_fqn_fully_qualified() {
1490 let target_id = ObjectId::new(
1491 "mydb".to_string(),
1492 "myschema".to_string(),
1493 "myview".to_string(),
1494 );
1495
1496 let normalized = normalize_fqn("other_db.other_schema.users", &target_id);
1497 assert_eq!(normalized.to_string(), "other_db.other_schema.users");
1498 }
1499
1500 #[mz_ore::test]
1501 fn test_normalize_type_integer_aliases() {
1502 assert_eq!(normalize_type("INT"), "integer");
1503 assert_eq!(normalize_type("int4"), "integer");
1504 assert_eq!(normalize_type("integer"), "integer");
1505 assert_eq!(normalize_type("INTEGER"), "integer");
1506 }
1507
1508 #[mz_ore::test]
1509 fn test_normalize_type_bigint_aliases() {
1510 assert_eq!(normalize_type("INT8"), "bigint");
1511 assert_eq!(normalize_type("bigint"), "bigint");
1512 assert_eq!(normalize_type("BIGINT"), "bigint");
1513 }
1514
1515 #[mz_ore::test]
1516 fn test_normalize_type_smallint_aliases() {
1517 assert_eq!(normalize_type("INT2"), "smallint");
1518 assert_eq!(normalize_type("smallint"), "smallint");
1519 assert_eq!(normalize_type("SMALLINT"), "smallint");
1520 }
1521
1522 #[mz_ore::test]
1523 fn test_normalize_type_real_aliases() {
1524 assert_eq!(normalize_type("float4"), "real");
1525 assert_eq!(normalize_type("FLOAT4"), "real");
1526 assert_eq!(normalize_type("real"), "real");
1527 assert_eq!(normalize_type("REAL"), "real");
1528 }
1529
1530 #[mz_ore::test]
1531 fn test_normalize_type_double_precision_aliases() {
1532 assert_eq!(normalize_type("float"), "double precision");
1533 assert_eq!(normalize_type("FLOAT"), "double precision");
1534 assert_eq!(normalize_type("float8"), "double precision");
1535 assert_eq!(normalize_type("FLOAT8"), "double precision");
1536 assert_eq!(normalize_type("double"), "double precision");
1537 assert_eq!(normalize_type("DOUBLE"), "double precision");
1538 assert_eq!(normalize_type("double precision"), "double precision");
1539 assert_eq!(normalize_type("DOUBLE PRECISION"), "double precision");
1540 }
1541
1542 #[mz_ore::test]
1543 fn test_normalize_type_boolean_aliases() {
1544 assert_eq!(normalize_type("bool"), "boolean");
1545 assert_eq!(normalize_type("boolean"), "boolean");
1546 assert_eq!(normalize_type("BOOL"), "boolean");
1547 assert_eq!(normalize_type("BOOLEAN"), "boolean");
1548 }
1549
1550 #[mz_ore::test]
1551 fn test_normalize_type_text_aliases() {
1552 assert_eq!(normalize_type("text"), "text");
1553 assert_eq!(normalize_type("TEXT"), "text");
1554 assert_eq!(normalize_type("string"), "text");
1555 assert_eq!(normalize_type("STRING"), "text");
1556 assert_eq!(normalize_type("varchar"), "text");
1557 assert_eq!(normalize_type("VARCHAR"), "text");
1558 assert_eq!(normalize_type("varchar(255)"), "text");
1559 assert_eq!(normalize_type("character varying"), "text");
1560 assert_eq!(normalize_type("character varying(100)"), "text");
1561 }
1562
1563 #[mz_ore::test]
1564 fn test_normalize_type_numeric_aliases() {
1565 assert_eq!(normalize_type("numeric"), "numeric");
1566 assert_eq!(normalize_type("NUMERIC"), "numeric");
1567 assert_eq!(normalize_type("decimal"), "numeric");
1568 assert_eq!(normalize_type("DECIMAL"), "numeric");
1569 assert_eq!(normalize_type("numeric(10,2)"), "numeric");
1570 assert_eq!(normalize_type("decimal(18,4)"), "numeric");
1571 }
1572
1573 #[mz_ore::test]
1574 fn test_normalize_type_jsonb_aliases() {
1575 assert_eq!(normalize_type("json"), "jsonb");
1576 assert_eq!(normalize_type("JSON"), "jsonb");
1577 assert_eq!(normalize_type("jsonb"), "jsonb");
1578 assert_eq!(normalize_type("JSONB"), "jsonb");
1579 }
1580
1581 #[mz_ore::test]
1582 fn test_normalize_type_timestamptz_aliases() {
1583 assert_eq!(normalize_type("timestamptz"), "timestamp with time zone");
1584 assert_eq!(normalize_type("TIMESTAMPTZ"), "timestamp with time zone");
1585 assert_eq!(
1586 normalize_type("timestamp with time zone"),
1587 "timestamp with time zone"
1588 );
1589 assert_eq!(
1590 normalize_type("TIMESTAMP WITH TIME ZONE"),
1591 "timestamp with time zone"
1592 );
1593 }
1594
1595 #[mz_ore::test]
1596 fn test_normalize_type_preserves_other_types() {
1597 assert_eq!(normalize_type("timestamp"), "timestamp without time zone");
1598 assert_eq!(normalize_type("TIMESTAMP"), "timestamp without time zone");
1599 assert_eq!(normalize_type("date"), "date");
1600 assert_eq!(normalize_type("time"), "time");
1601 assert_eq!(normalize_type("interval"), "interval");
1602 assert_eq!(normalize_type("uuid"), "uuid");
1603 assert_eq!(normalize_type("bytea"), "bytea");
1604 assert_eq!(normalize_type("oid"), "oid");
1605 assert_eq!(normalize_type("uint2"), "uint2");
1606 assert_eq!(normalize_type("uint4"), "uint4");
1607 assert_eq!(normalize_type("uint8"), "uint8");
1608 }
1609
1610 #[mz_ore::test]
1611 fn test_normalize_type_custom_array_spellings() {
1612 assert_eq!(normalize_type("_aclitem"), "aclitem[]");
1613 assert_eq!(normalize_type("_mz_aclitem"), "mz_aclitem[]");
1614 assert_eq!(normalize_type("_mz_timestamp"), "mz_timestamp[]");
1615 assert_eq!(normalize_type("_record"), "record[]");
1616 assert_eq!(normalize_type("_uint2"), "uint2[]");
1617 assert_eq!(normalize_type("_uint4"), "uint4[]");
1618 assert_eq!(normalize_type("_uint8"), "uint8[]");
1619 assert_eq!(normalize_type("_UINT4"), "uint4[]");
1620 assert_eq!(normalize_type("uint4[]"), "uint4[]");
1621 assert_eq!(normalize_type("_uint4"), normalize_type("uint4[]"));
1622 }
1623
1624 #[mz_ore::test]
1625 fn test_normalize_type_handles_whitespace() {
1626 assert_eq!(normalize_type(" INT "), "integer");
1627 assert_eq!(normalize_type("\ttext\n"), "text");
1628 assert_eq!(normalize_type(" double precision "), "double precision");
1629 }
1630
1631 #[mz_ore::test]
1632 fn test_normalize_type_case_insensitive() {
1633 assert_eq!(normalize_type("integer"), normalize_type("INTEGER"));
1634 assert_eq!(normalize_type("integer"), normalize_type("Integer"));
1635 assert_eq!(normalize_type("integer"), normalize_type("iNtEgEr"));
1636 assert_eq!(normalize_type("int"), normalize_type("INT"));
1637 assert_eq!(normalize_type("int"), normalize_type("Int"));
1638
1639 assert_eq!(normalize_type("bigint"), normalize_type("BIGINT"));
1640 assert_eq!(normalize_type("bigint"), normalize_type("BigInt"));
1641 assert_eq!(normalize_type("int8"), normalize_type("INT8"));
1642
1643 assert_eq!(normalize_type("text"), normalize_type("TEXT"));
1644 assert_eq!(normalize_type("text"), normalize_type("Text"));
1645 assert_eq!(normalize_type("string"), normalize_type("STRING"));
1646 assert_eq!(normalize_type("string"), normalize_type("String"));
1647
1648 assert_eq!(normalize_type("boolean"), normalize_type("BOOLEAN"));
1649 assert_eq!(normalize_type("boolean"), normalize_type("Boolean"));
1650 assert_eq!(normalize_type("bool"), normalize_type("BOOL"));
1651 assert_eq!(normalize_type("bool"), normalize_type("Bool"));
1652
1653 assert_eq!(normalize_type("numeric"), normalize_type("NUMERIC"));
1654 assert_eq!(normalize_type("numeric"), normalize_type("Numeric"));
1655 assert_eq!(normalize_type("decimal"), normalize_type("DECIMAL"));
1656
1657 assert_eq!(
1658 normalize_type("double precision"),
1659 normalize_type("DOUBLE PRECISION")
1660 );
1661 assert_eq!(
1662 normalize_type("double precision"),
1663 normalize_type("Double Precision")
1664 );
1665
1666 assert_eq!(
1667 normalize_type("timestamp with time zone"),
1668 normalize_type("TIMESTAMP WITH TIME ZONE")
1669 );
1670 assert_eq!(normalize_type("timestamptz"), normalize_type("TIMESTAMPTZ"));
1671 assert_eq!(normalize_type("timestamptz"), normalize_type("TimestampTZ"));
1672
1673 assert_eq!(normalize_type("jsonb"), normalize_type("JSONB"));
1674 assert_eq!(normalize_type("jsonb"), normalize_type("JsonB"));
1675 assert_eq!(normalize_type("json"), normalize_type("JSON"));
1676 }
1677
1678 #[mz_ore::test]
1679 fn test_compare_columns_exact_match() {
1680 let test_columns = vec![
1681 ("id".to_string(), "bigint".to_string()),
1682 ("name".to_string(), "text".to_string()),
1683 ];
1684
1685 let mut actual_columns = BTreeMap::new();
1686 actual_columns.insert(
1687 "id".to_string(),
1688 ColumnType {
1689 r#type: DataType::named("bigint"),
1690 nullable: false,
1691 position: 0,
1692 comment: None,
1693 },
1694 );
1695 actual_columns.insert(
1696 "name".to_string(),
1697 ColumnType {
1698 r#type: DataType::named("text"),
1699 nullable: true,
1700 position: 0,
1701 comment: None,
1702 },
1703 );
1704
1705 let (extra, missing, type_mismatches) = compare_columns(&test_columns, &actual_columns);
1706 assert!(extra.is_empty());
1707 assert!(missing.is_empty());
1708 assert!(type_mismatches.is_empty());
1709 }
1710
1711 #[mz_ore::test]
1712 fn test_compare_columns_with_type_aliases() {
1713 let test_columns = vec![
1714 ("id".to_string(), "INT".to_string()),
1715 ("count".to_string(), "INT8".to_string()),
1716 ];
1717
1718 let mut actual_columns = BTreeMap::new();
1719 actual_columns.insert(
1720 "id".to_string(),
1721 ColumnType {
1722 r#type: DataType::named("integer"),
1723 nullable: false,
1724 position: 0,
1725 comment: None,
1726 },
1727 );
1728 actual_columns.insert(
1729 "count".to_string(),
1730 ColumnType {
1731 r#type: DataType::named("bigint"),
1732 nullable: false,
1733 position: 0,
1734 comment: None,
1735 },
1736 );
1737
1738 let (extra, missing, type_mismatches) = compare_columns(&test_columns, &actual_columns);
1739 assert!(extra.is_empty());
1740 assert!(missing.is_empty());
1741 assert!(type_mismatches.is_empty());
1742 }
1743
1744 #[mz_ore::test]
1745 fn test_compare_columns_detects_extra() {
1746 let test_columns = vec![
1747 ("id".to_string(), "bigint".to_string()),
1748 ("extra".to_string(), "text".to_string()),
1749 ];
1750
1751 let mut actual_columns = BTreeMap::new();
1752 actual_columns.insert(
1753 "id".to_string(),
1754 ColumnType {
1755 r#type: DataType::named("bigint"),
1756 nullable: false,
1757 position: 0,
1758 comment: None,
1759 },
1760 );
1761
1762 let (extra, missing, _) = compare_columns(&test_columns, &actual_columns);
1763 assert_eq!(extra, vec!["extra".to_string()]);
1764 assert!(missing.is_empty());
1765 }
1766
1767 #[mz_ore::test]
1768 fn test_compare_columns_detects_missing() {
1769 let test_columns = vec![("id".to_string(), "bigint".to_string())];
1770
1771 let mut actual_columns = BTreeMap::new();
1772 actual_columns.insert(
1773 "id".to_string(),
1774 ColumnType {
1775 r#type: DataType::named("bigint"),
1776 nullable: false,
1777 position: 0,
1778 comment: None,
1779 },
1780 );
1781 actual_columns.insert(
1782 "name".to_string(),
1783 ColumnType {
1784 r#type: DataType::named("text"),
1785 nullable: true,
1786 position: 0,
1787 comment: None,
1788 },
1789 );
1790
1791 let (extra, missing, _) = compare_columns(&test_columns, &actual_columns);
1792 assert!(extra.is_empty());
1793 assert_eq!(missing, vec![("name".to_string(), "text".to_string())]);
1794 }
1795
1796 #[mz_ore::test]
1797 fn test_compare_columns_detects_type_mismatch() {
1798 let test_columns = vec![("id".to_string(), "text".to_string())];
1799
1800 let mut actual_columns = BTreeMap::new();
1801 actual_columns.insert(
1802 "id".to_string(),
1803 ColumnType {
1804 r#type: DataType::named("bigint"),
1805 nullable: false,
1806 position: 0,
1807 comment: None,
1808 },
1809 );
1810
1811 let (_, _, type_mismatches) = compare_columns(&test_columns, &actual_columns);
1812 assert_eq!(type_mismatches.len(), 1);
1813 assert_eq!(type_mismatches[0].0, "id");
1814 assert_eq!(type_mismatches[0].1, "text");
1815 assert_eq!(type_mismatches[0].2, "bigint");
1816 }
1817
1818 #[mz_ore::test]
1819 fn test_validate_with_unqualified_mock_name() {
1820 let test = UnitTest {
1821 name: "test_partial_fqn".to_string(),
1822 target_view: "materialize.public.user_order_summary".to_string(),
1823 at_time: None,
1824 mocks: vec![
1825 MockView {
1826 fqn: "users".to_string(),
1827 columns: vec![
1828 ("id".to_string(), "bigint".to_string()),
1829 ("name".to_string(), "text".to_string()),
1830 ("email".to_string(), "text".to_string()),
1831 ],
1832 query: "SELECT * FROM VALUES (1, 'alice', 'alice@example.com')".to_string(),
1833 },
1834 MockView {
1835 fqn: "public.orders".to_string(),
1836 columns: vec![
1837 ("id".to_string(), "bigint".to_string()),
1838 ("user_id".to_string(), "bigint".to_string()),
1839 ("amount".to_string(), "numeric".to_string()),
1840 ],
1841 query: "SELECT * FROM VALUES (1, 1, 100.00)".to_string(),
1842 },
1843 ],
1844 expected: ExpectedResult {
1845 columns: vec![
1846 ("user_id".to_string(), "bigint".to_string()),
1847 ("user_name".to_string(), "text".to_string()),
1848 ("total_orders".to_string(), "bigint".to_string()),
1849 ],
1850 query: "SELECT * FROM VALUES (1, 'alice', 1)".to_string(),
1851 },
1852 };
1853
1854 let types = make_test_types();
1855 let target_id = make_target_id();
1856 let dependencies = make_dependencies();
1857
1858 let result = validate_unit_test(
1859 &test,
1860 &target_id,
1861 &|fqn| types.get_table(fqn).cloned(),
1862 &dependencies,
1863 );
1864 assert!(result.is_ok(), "Expected validation to pass: {:?}", result);
1865 }
1866
1867 #[mz_ore::test]
1868 fn test_validate_passes_with_no_dependencies() {
1869 let test = UnitTest {
1870 name: "test_no_deps".to_string(),
1871 target_view: "materialize.public.my_view".to_string(),
1872 at_time: None,
1873 mocks: vec![],
1874 expected: ExpectedResult {
1875 columns: vec![("result".to_string(), "integer".to_string())],
1876 query: "SELECT * FROM VALUES (42)".to_string(),
1877 },
1878 };
1879
1880 let types = Types::default();
1881 let target_id = ObjectId::new(
1882 "materialize".to_string(),
1883 "public".to_string(),
1884 "my_view".to_string(),
1885 );
1886 let dependencies = BTreeSet::new();
1887
1888 let result = validate_unit_test(
1889 &test,
1890 &target_id,
1891 &|fqn| types.get_table(fqn).cloned(),
1892 &dependencies,
1893 );
1894 assert!(result.is_ok());
1895 }
1896
1897 #[mz_ore::test]
1898 fn test_validate_skips_unknown_mock() {
1899 let test = UnitTest {
1900 name: "test_unknown_mock".to_string(),
1901 target_view: "materialize.public.my_view".to_string(),
1902 at_time: None,
1903 mocks: vec![MockView {
1904 fqn: "materialize.public.unknown_table".to_string(),
1905 columns: vec![("id".to_string(), "bigint".to_string())],
1906 query: "SELECT * FROM VALUES (1)".to_string(),
1907 }],
1908 expected: ExpectedResult {
1909 columns: vec![("result".to_string(), "integer".to_string())],
1910 query: "SELECT * FROM VALUES (42)".to_string(),
1911 },
1912 };
1913
1914 let types = Types::default();
1915 let target_id = ObjectId::new(
1916 "materialize".to_string(),
1917 "public".to_string(),
1918 "my_view".to_string(),
1919 );
1920
1921 let mut dependencies = BTreeSet::new();
1922 dependencies.insert(ObjectId::new(
1923 "materialize".to_string(),
1924 "public".to_string(),
1925 "unknown_table".to_string(),
1926 ));
1927
1928 let result = validate_unit_test(
1929 &test,
1930 &target_id,
1931 &|fqn| types.get_table(fqn).cloned(),
1932 &dependencies,
1933 );
1934 assert!(result.is_ok());
1935 }
1936
1937 #[mz_ore::test]
1938 fn test_normalize_type_list() {
1939 assert_eq!(normalize_type("int8 list"), "bigint list");
1940 assert_eq!(normalize_type("INT LIST"), "integer list");
1941 assert_eq!(normalize_type("text list"), "text list");
1942 assert_eq!(normalize_type("INT8 LIST"), "bigint list");
1943 }
1944
1945 #[mz_ore::test]
1946 fn test_normalize_type_array() {
1947 assert_eq!(normalize_type("int8[]"), "bigint[]");
1948 assert_eq!(normalize_type("INT[]"), "integer[]");
1949 assert_eq!(normalize_type("text[]"), "text[]");
1950 }
1951
1952 #[mz_ore::test]
1953 fn test_normalize_type_map() {
1954 assert_eq!(normalize_type("map[text=>int8]"), "map[text=>bigint]");
1955 assert_eq!(normalize_type("map[STRING=>BOOL]"), "map[text=>boolean]");
1956 }
1957
1958 #[mz_ore::test]
1959 fn test_normalize_type_bare_list() {
1960 assert_eq!(normalize_type("list"), "list");
1961 assert_eq!(normalize_type("LIST"), "list");
1962 }
1963
1964 #[mz_ore::test]
1965 fn test_compare_columns_list_matches_bare() {
1966 let test_columns = vec![("ids".to_string(), "int8 list".to_string())];
1967
1968 let mut actual_columns = BTreeMap::new();
1969 actual_columns.insert(
1970 "ids".to_string(),
1971 ColumnType {
1972 r#type: DataType::named("list"),
1973 nullable: true,
1974 position: 0,
1975 comment: None,
1976 },
1977 );
1978
1979 let (extra, missing, type_mismatches) = compare_columns(&test_columns, &actual_columns);
1980 assert!(extra.is_empty());
1981 assert!(missing.is_empty());
1982 assert!(
1983 type_mismatches.is_empty(),
1984 "Expected no type mismatches for 'int8 list' vs bare 'list', got: {:?}",
1985 type_mismatches
1986 );
1987 }
1988
1989 #[mz_ore::test]
1990 fn test_compare_columns_map_matches_bare() {
1991 let test_columns = vec![("data".to_string(), "map[text=>int8]".to_string())];
1992
1993 let mut actual_columns = BTreeMap::new();
1994 actual_columns.insert(
1995 "data".to_string(),
1996 ColumnType {
1997 r#type: DataType::named("map"),
1998 nullable: true,
1999 position: 0,
2000 comment: None,
2001 },
2002 );
2003
2004 let (_, _, type_mismatches) = compare_columns(&test_columns, &actual_columns);
2005 assert!(
2006 type_mismatches.is_empty(),
2007 "Expected no type mismatches for 'map[text=>int8]' vs bare 'map', got: {:?}",
2008 type_mismatches
2009 );
2010 }
2011}