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 _ => {
666 if normalized.starts_with("varchar") || normalized.starts_with("character varying") {
667 "text".to_string()
668 } else if normalized.starts_with("numeric") || normalized.starts_with("decimal") {
669 "numeric".to_string()
670 } else if normalized.starts_with("timestamp with time zone")
671 || normalized.starts_with("timestamptz")
672 {
673 "timestamp with time zone".to_string()
674 } else if normalized.starts_with("timestamp without time zone")
675 || normalized == "timestamp"
676 {
677 "timestamp without time zone".to_string()
678 } else {
679 normalized
680 }
681 }
682 }
683}
684
685pub(super) fn lower_unit_test(
693 test: &UnitTest,
694 target_stmt: &Statement,
695 target_fqn: &FullyQualifiedName,
696) -> Result<Vec<String>, String> {
697 let mut statements = Vec::new();
698
699 for mock in &test.mocks {
700 let qualified_mock = qualify_mock_name(mock, target_fqn);
701 statements.push(create_mock_view_sql(&qualified_mock));
702 }
703
704 statements.push(create_expected_view_sql(&test.expected));
705
706 statements.push(create_target_view_sql(target_stmt, target_fqn)?);
707
708 let target_fqn_str = format!(
709 "{}.{}.{}",
710 target_fqn.database(),
711 target_fqn.schema(),
712 target_fqn.object()
713 );
714 let flattened_target_name = flatten_fqn(&target_fqn_str);
715 statements.push(create_test_query_sql(
716 &flattened_target_name,
717 test.at_time.as_deref(),
718 ));
719
720 Ok(statements)
721}
722
723fn flatten_fqn(fqn: &str) -> String {
725 format!("\"{}\"", fqn)
726}
727
728fn qualify_mock_name(mock: &MockView, target_fqn: &FullyQualifiedName) -> MockView {
730 let parts = mock.fqn.matches('.').count() + 1;
731
732 let qualified_fqn = match parts {
733 1 => format!(
734 "{}.{}.{}",
735 target_fqn.database(),
736 target_fqn.schema(),
737 mock.fqn
738 ),
739 2 => format!("{}.{}", target_fqn.database(), mock.fqn),
740 _ => mock.fqn.clone(),
741 };
742
743 MockView {
744 fqn: qualified_fqn,
745 columns: mock.columns.clone(),
746 query: mock.query.clone(),
747 }
748}
749
750fn create_mock_view_sql(mock: &MockView) -> String {
751 let flattened_name = flatten_fqn(&mock.fqn);
752 let columns_def = mock
753 .columns
754 .iter()
755 .map(|(name, typ)| format!("{} {}", name, typ))
756 .collect::<Vec<_>>()
757 .join(", ");
758
759 format!(
760 "CREATE TEMPORARY VIEW {} AS\nWITH MUTUALLY RECURSIVE data({}) AS (\n {}\n)\nSELECT * FROM data;",
761 flattened_name, columns_def, mock.query
762 )
763}
764
765fn create_expected_view_sql(expected: &ExpectedResult) -> String {
766 let columns_def = expected
767 .columns
768 .iter()
769 .map(|(name, typ)| format!("{} {}", name, typ))
770 .collect::<Vec<_>>()
771 .join(", ");
772
773 format!(
774 "CREATE TEMPORARY VIEW expected AS\nWITH MUTUALLY RECURSIVE data({}) AS (\n {}\n)\nSELECT * FROM data;",
775 columns_def, expected.query
776 )
777}
778
779fn create_target_view_sql(stmt: &Statement, fqn: &FullyQualifiedName) -> Result<String, String> {
784 let mut visitor = NormalizingVisitor::flattening(fqn);
785 let transformed_stmt = stmt
786 .clone()
787 .normalize_name_with(&visitor, &fqn.to_item_name())
788 .normalize_dependencies_with(&mut visitor);
789
790 let view_stmt = match transformed_stmt {
791 Statement::CreateView(view) => CreateViewStatement {
792 if_exists: IfExistsBehavior::Error,
793 temporary: true,
794 definition: view.definition.clone(),
795 },
796 Statement::CreateMaterializedView(mv) => CreateViewStatement {
797 if_exists: IfExistsBehavior::Error,
798 temporary: true,
799 definition: ViewDefinition {
800 name: mv.name,
801 columns: mv.columns,
802 query: mv.query,
803 },
804 },
805 other => {
806 return Err(format!(
807 "unit tests are only supported on views and materialized views; \
808 target '{}.{}.{}' is a {}",
809 fqn.database(),
810 fqn.schema(),
811 fqn.object(),
812 other.kind(),
813 ));
814 }
815 };
816 Ok(view_stmt.to_string())
817}
818
819fn create_test_query_sql(flattened_target_name: &str, at_time: Option<&str>) -> String {
830 let as_of_clause = at_time
831 .map(|t| format!(" AS OF {}::mz_timestamp", t))
832 .unwrap_or_default();
833 format!(
834 r#"SELECT 'MISSING' as status, * FROM expected
835EXCEPT
836SELECT 'MISSING', * FROM {}
837
838UNION ALL
839
840SELECT 'UNEXPECTED' as status, * FROM {}
841EXCEPT
842SELECT 'UNEXPECTED', * FROM expected{}"#,
843 flattened_target_name, flattened_target_name, as_of_clause
844 )
845}
846
847#[cfg(test)]
848mod tests {
849 use super::*;
850 use crate::types::{ColumnType, DataType};
851 use std::collections::BTreeMap;
852
853 #[mz_ore::test]
854 fn test_flatten_fqn() {
855 assert_eq!(
856 flatten_fqn("materialize.public.flippers"),
857 "\"materialize.public.flippers\""
858 );
859 assert_eq!(flatten_fqn("a.b.c"), "\"a.b.c\"");
860 assert_eq!(flatten_fqn("single"), "\"single\"");
861 }
862
863 #[mz_ore::test]
864 fn test_create_mock_view_sql() {
865 let mock = MockView {
866 fqn: "materialize.public.users".to_string(),
867 columns: vec![
868 ("id".to_string(), "BIGINT".to_string()),
869 ("name".to_string(), "TEXT".to_string()),
870 ],
871 query: "SELECT * FROM VALUES ((1, 'alice'))".to_string(),
872 };
873
874 let sql = create_mock_view_sql(&mock);
875
876 assert!(sql.contains("CREATE TEMPORARY VIEW \"materialize.public.users\""));
877 assert!(sql.contains("WITH MUTUALLY RECURSIVE data(id BIGINT, name TEXT)"));
878 assert!(sql.contains("SELECT * FROM VALUES ((1, 'alice'))"));
879 assert!(sql.contains("SELECT * FROM data"));
880 }
881
882 #[mz_ore::test]
883 fn test_create_expected_view_sql() {
884 let expected = ExpectedResult {
885 columns: vec![
886 ("id".to_string(), "BIGINT".to_string()),
887 ("count".to_string(), "INT".to_string()),
888 ],
889 query: "SELECT * FROM VALUES ((1, 10))".to_string(),
890 };
891
892 let sql = create_expected_view_sql(&expected);
893
894 assert!(sql.contains("CREATE TEMPORARY VIEW expected"));
895 assert!(sql.contains("WITH MUTUALLY RECURSIVE data(id BIGINT, count INT)"));
896 assert!(sql.contains("SELECT * FROM VALUES ((1, 10))"));
897 assert!(sql.contains("SELECT * FROM data"));
898 }
899
900 #[mz_ore::test]
901 fn test_create_test_query_sql() {
902 let sql = create_test_query_sql("materialize_public_my_view", None);
903
904 assert!(sql.contains("SELECT 'MISSING' as status, * FROM expected"));
905 assert!(sql.contains("SELECT 'MISSING', * FROM materialize_public_my_view"));
906 assert!(sql.contains("SELECT 'UNEXPECTED' as status, * FROM materialize_public_my_view"));
907 assert!(sql.contains("SELECT 'UNEXPECTED', * FROM expected"));
908 assert!(sql.contains("UNION ALL"));
909 assert!(sql.contains("EXCEPT"));
910 assert!(!sql.contains("AS OF"));
911 }
912
913 #[mz_ore::test]
914 fn test_create_test_query_sql_with_at_time() {
915 let sql =
916 create_test_query_sql("materialize_public_my_view", Some("'2024-01-15 10:00:00'"));
917
918 assert!(sql.contains("SELECT 'MISSING' as status, * FROM expected"));
919 assert!(sql.contains("AS OF '2024-01-15 10:00:00'::mz_timestamp"));
920 }
921
922 fn make_test_types() -> Types {
923 let mut objects = BTreeMap::new();
924
925 let mut users_cols = BTreeMap::new();
926 users_cols.insert(
927 "id".to_string(),
928 ColumnType {
929 r#type: DataType::named("bigint"),
930 nullable: false,
931 position: 0,
932 comment: None,
933 },
934 );
935 users_cols.insert(
936 "name".to_string(),
937 ColumnType {
938 r#type: DataType::named("text"),
939 nullable: true,
940 position: 1,
941 comment: None,
942 },
943 );
944 users_cols.insert(
945 "email".to_string(),
946 ColumnType {
947 r#type: DataType::named("text"),
948 nullable: true,
949 position: 2,
950 comment: None,
951 },
952 );
953 objects.insert(
954 "materialize.public.users".parse::<ObjectId>().unwrap(),
955 users_cols,
956 );
957
958 let mut orders_cols = BTreeMap::new();
959 orders_cols.insert(
960 "id".to_string(),
961 ColumnType {
962 r#type: DataType::named("bigint"),
963 nullable: false,
964 position: 0,
965 comment: None,
966 },
967 );
968 orders_cols.insert(
969 "user_id".to_string(),
970 ColumnType {
971 r#type: DataType::named("bigint"),
972 nullable: false,
973 position: 1,
974 comment: None,
975 },
976 );
977 orders_cols.insert(
978 "amount".to_string(),
979 ColumnType {
980 r#type: DataType::named("numeric"),
981 nullable: true,
982 position: 2,
983 comment: None,
984 },
985 );
986 objects.insert(
987 "materialize.public.orders".parse::<ObjectId>().unwrap(),
988 orders_cols,
989 );
990
991 let mut summary_cols = BTreeMap::new();
992 summary_cols.insert(
993 "user_id".to_string(),
994 ColumnType {
995 r#type: DataType::named("bigint"),
996 nullable: false,
997 position: 0,
998 comment: None,
999 },
1000 );
1001 summary_cols.insert(
1002 "user_name".to_string(),
1003 ColumnType {
1004 r#type: DataType::named("text"),
1005 nullable: true,
1006 position: 1,
1007 comment: None,
1008 },
1009 );
1010 summary_cols.insert(
1011 "total_orders".to_string(),
1012 ColumnType {
1013 r#type: DataType::named("bigint"),
1014 nullable: true,
1015 position: 2,
1016 comment: None,
1017 },
1018 );
1019 objects.insert(
1020 "materialize.public.user_order_summary"
1021 .parse::<ObjectId>()
1022 .unwrap(),
1023 summary_cols,
1024 );
1025
1026 Types {
1027 tables: objects,
1028 kinds: BTreeMap::new(),
1029 comments: BTreeMap::new(),
1030 }
1031 }
1032
1033 fn make_target_id() -> ObjectId {
1034 ObjectId::new(
1035 "materialize".to_string(),
1036 "public".to_string(),
1037 "user_order_summary".to_string(),
1038 )
1039 }
1040
1041 fn make_dependencies() -> BTreeSet<ObjectId> {
1042 let mut deps = BTreeSet::new();
1043 deps.insert(ObjectId::new(
1044 "materialize".to_string(),
1045 "public".to_string(),
1046 "users".to_string(),
1047 ));
1048 deps.insert(ObjectId::new(
1049 "materialize".to_string(),
1050 "public".to_string(),
1051 "orders".to_string(),
1052 ));
1053 deps
1054 }
1055
1056 #[mz_ore::test]
1057 fn test_validate_unit_test_passes_with_correct_mocks() {
1058 let test = UnitTest {
1059 name: "test_user_summary".to_string(),
1060 target_view: "materialize.public.user_order_summary".to_string(),
1061 at_time: None,
1062 mocks: vec![
1063 MockView {
1064 fqn: "materialize.public.users".to_string(),
1065 columns: vec![
1066 ("id".to_string(), "bigint".to_string()),
1067 ("name".to_string(), "text".to_string()),
1068 ("email".to_string(), "text".to_string()),
1069 ],
1070 query: "SELECT * FROM VALUES (1, 'alice', 'alice@example.com')".to_string(),
1071 },
1072 MockView {
1073 fqn: "materialize.public.orders".to_string(),
1074 columns: vec![
1075 ("id".to_string(), "bigint".to_string()),
1076 ("user_id".to_string(), "bigint".to_string()),
1077 ("amount".to_string(), "numeric".to_string()),
1078 ],
1079 query: "SELECT * FROM VALUES (1, 1, 100.00)".to_string(),
1080 },
1081 ],
1082 expected: ExpectedResult {
1083 columns: vec![
1084 ("user_id".to_string(), "bigint".to_string()),
1085 ("user_name".to_string(), "text".to_string()),
1086 ("total_orders".to_string(), "bigint".to_string()),
1087 ],
1088 query: "SELECT * FROM VALUES (1, 'alice', 1)".to_string(),
1089 },
1090 };
1091
1092 let types = make_test_types();
1093 let target_id = make_target_id();
1094 let dependencies = make_dependencies();
1095
1096 let result = validate_unit_test(
1097 &test,
1098 &target_id,
1099 &|fqn| types.get_table(fqn).cloned(),
1100 &dependencies,
1101 );
1102 assert!(result.is_ok(), "Expected validation to pass: {:?}", result);
1103 }
1104
1105 #[mz_ore::test]
1106 fn test_validate_unit_test_fails_with_unmocked_dependency() {
1107 let test = UnitTest {
1108 name: "test_user_summary".to_string(),
1109 target_view: "materialize.public.user_order_summary".to_string(),
1110 at_time: None,
1111 mocks: vec![MockView {
1112 fqn: "materialize.public.users".to_string(),
1113 columns: vec![
1114 ("id".to_string(), "bigint".to_string()),
1115 ("name".to_string(), "text".to_string()),
1116 ("email".to_string(), "text".to_string()),
1117 ],
1118 query: "SELECT * FROM VALUES (1, 'alice', 'alice@example.com')".to_string(),
1119 }],
1120 expected: ExpectedResult {
1121 columns: vec![
1122 ("user_id".to_string(), "bigint".to_string()),
1123 ("user_name".to_string(), "text".to_string()),
1124 ("total_orders".to_string(), "bigint".to_string()),
1125 ],
1126 query: "SELECT * FROM VALUES (1, 'alice', 1)".to_string(),
1127 },
1128 };
1129
1130 let types = make_test_types();
1131 let target_id = make_target_id();
1132 let dependencies = make_dependencies();
1133
1134 let result = validate_unit_test(
1135 &test,
1136 &target_id,
1137 &|fqn| types.get_table(fqn).cloned(),
1138 &dependencies,
1139 );
1140 assert!(result.is_err());
1141
1142 match result.unwrap_err() {
1143 TestValidationError::UnmockedDependency(err) => {
1144 assert_eq!(err.test_name, "test_user_summary");
1145 assert!(
1146 err.missing_mocks
1147 .contains(&"materialize.public.orders".to_string())
1148 );
1149 }
1150 other => panic!("Expected UnmockedDependency error, got: {:?}", other),
1151 }
1152 }
1153
1154 #[mz_ore::test]
1155 fn test_validate_unit_test_fails_with_missing_mock_column() {
1156 let test = UnitTest {
1157 name: "test_user_summary".to_string(),
1158 target_view: "materialize.public.user_order_summary".to_string(),
1159 at_time: None,
1160 mocks: vec![
1161 MockView {
1162 fqn: "materialize.public.users".to_string(),
1163 columns: vec![
1164 ("id".to_string(), "bigint".to_string()),
1165 ("name".to_string(), "text".to_string()),
1166 ],
1167 query: "SELECT * FROM VALUES (1, 'alice')".to_string(),
1168 },
1169 MockView {
1170 fqn: "materialize.public.orders".to_string(),
1171 columns: vec![
1172 ("id".to_string(), "bigint".to_string()),
1173 ("user_id".to_string(), "bigint".to_string()),
1174 ("amount".to_string(), "numeric".to_string()),
1175 ],
1176 query: "SELECT * FROM VALUES (1, 1, 100.00)".to_string(),
1177 },
1178 ],
1179 expected: ExpectedResult {
1180 columns: vec![
1181 ("user_id".to_string(), "bigint".to_string()),
1182 ("user_name".to_string(), "text".to_string()),
1183 ("total_orders".to_string(), "bigint".to_string()),
1184 ],
1185 query: "SELECT * FROM VALUES (1, 'alice', 1)".to_string(),
1186 },
1187 };
1188
1189 let types = make_test_types();
1190 let target_id = make_target_id();
1191 let dependencies = make_dependencies();
1192
1193 let result = validate_unit_test(
1194 &test,
1195 &target_id,
1196 &|fqn| types.get_table(fqn).cloned(),
1197 &dependencies,
1198 );
1199 assert!(result.is_err());
1200
1201 match result.unwrap_err() {
1202 TestValidationError::MockSchemaMismatch(err) => {
1203 assert_eq!(err.test_name, "test_user_summary");
1204 assert_eq!(err.mock_fqn, "materialize.public.users");
1205 assert!(err.missing_columns.iter().any(|(name, _)| name == "email"));
1206 assert!(err.extra_columns.is_empty());
1207 }
1208 other => panic!("Expected MockSchemaMismatch error, got: {:?}", other),
1209 }
1210 }
1211
1212 #[mz_ore::test]
1213 fn test_validate_unit_test_fails_with_extra_mock_column() {
1214 let test = UnitTest {
1215 name: "test_user_summary".to_string(),
1216 target_view: "materialize.public.user_order_summary".to_string(),
1217 at_time: None,
1218 mocks: vec![
1219 MockView {
1220 fqn: "materialize.public.users".to_string(),
1221 columns: vec![
1222 ("id".to_string(), "bigint".to_string()),
1223 ("name".to_string(), "text".to_string()),
1224 ("email".to_string(), "text".to_string()),
1225 ("extra_column".to_string(), "int".to_string()),
1226 ],
1227 query: "SELECT * FROM VALUES (1, 'alice', 'alice@example.com', 42)".to_string(),
1228 },
1229 MockView {
1230 fqn: "materialize.public.orders".to_string(),
1231 columns: vec![
1232 ("id".to_string(), "bigint".to_string()),
1233 ("user_id".to_string(), "bigint".to_string()),
1234 ("amount".to_string(), "numeric".to_string()),
1235 ],
1236 query: "SELECT * FROM VALUES (1, 1, 100.00)".to_string(),
1237 },
1238 ],
1239 expected: ExpectedResult {
1240 columns: vec![
1241 ("user_id".to_string(), "bigint".to_string()),
1242 ("user_name".to_string(), "text".to_string()),
1243 ("total_orders".to_string(), "bigint".to_string()),
1244 ],
1245 query: "SELECT * FROM VALUES (1, 'alice', 1)".to_string(),
1246 },
1247 };
1248
1249 let types = make_test_types();
1250 let target_id = make_target_id();
1251 let dependencies = make_dependencies();
1252
1253 let result = validate_unit_test(
1254 &test,
1255 &target_id,
1256 &|fqn| types.get_table(fqn).cloned(),
1257 &dependencies,
1258 );
1259 assert!(result.is_err());
1260
1261 match result.unwrap_err() {
1262 TestValidationError::MockSchemaMismatch(err) => {
1263 assert_eq!(err.mock_fqn, "materialize.public.users");
1264 assert!(err.extra_columns.contains(&"extra_column".to_string()));
1265 assert!(err.missing_columns.is_empty());
1266 }
1267 other => panic!("Expected MockSchemaMismatch error, got: {:?}", other),
1268 }
1269 }
1270
1271 #[mz_ore::test]
1272 fn test_validate_unit_test_fails_with_type_mismatch() {
1273 let test = UnitTest {
1274 name: "test_user_summary".to_string(),
1275 target_view: "materialize.public.user_order_summary".to_string(),
1276 at_time: None,
1277 mocks: vec![
1278 MockView {
1279 fqn: "materialize.public.users".to_string(),
1280 columns: vec![
1281 ("id".to_string(), "text".to_string()),
1282 ("name".to_string(), "text".to_string()),
1283 ("email".to_string(), "text".to_string()),
1284 ],
1285 query: "SELECT * FROM VALUES ('1', 'alice', 'alice@example.com')".to_string(),
1286 },
1287 MockView {
1288 fqn: "materialize.public.orders".to_string(),
1289 columns: vec![
1290 ("id".to_string(), "bigint".to_string()),
1291 ("user_id".to_string(), "bigint".to_string()),
1292 ("amount".to_string(), "numeric".to_string()),
1293 ],
1294 query: "SELECT * FROM VALUES (1, 1, 100.00)".to_string(),
1295 },
1296 ],
1297 expected: ExpectedResult {
1298 columns: vec![
1299 ("user_id".to_string(), "bigint".to_string()),
1300 ("user_name".to_string(), "text".to_string()),
1301 ("total_orders".to_string(), "bigint".to_string()),
1302 ],
1303 query: "SELECT * FROM VALUES (1, 'alice', 1)".to_string(),
1304 },
1305 };
1306
1307 let types = make_test_types();
1308 let target_id = make_target_id();
1309 let dependencies = make_dependencies();
1310
1311 let result = validate_unit_test(
1312 &test,
1313 &target_id,
1314 &|fqn| types.get_table(fqn).cloned(),
1315 &dependencies,
1316 );
1317 assert!(result.is_err());
1318
1319 match result.unwrap_err() {
1320 TestValidationError::MockSchemaMismatch(err) => {
1321 assert_eq!(err.mock_fqn, "materialize.public.users");
1322 assert!(
1323 err.type_mismatches
1324 .iter()
1325 .any(|(col, mock_t, _)| { col == "id" && mock_t == "text" })
1326 );
1327 }
1328 other => panic!("Expected MockSchemaMismatch error, got: {:?}", other),
1329 }
1330 }
1331
1332 #[mz_ore::test]
1333 fn test_validate_unit_test_fails_with_expected_schema_mismatch() {
1334 let test = UnitTest {
1335 name: "test_user_summary".to_string(),
1336 target_view: "materialize.public.user_order_summary".to_string(),
1337 at_time: None,
1338 mocks: vec![
1339 MockView {
1340 fqn: "materialize.public.users".to_string(),
1341 columns: vec![
1342 ("id".to_string(), "bigint".to_string()),
1343 ("name".to_string(), "text".to_string()),
1344 ("email".to_string(), "text".to_string()),
1345 ],
1346 query: "SELECT * FROM VALUES (1, 'alice', 'alice@example.com')".to_string(),
1347 },
1348 MockView {
1349 fqn: "materialize.public.orders".to_string(),
1350 columns: vec![
1351 ("id".to_string(), "bigint".to_string()),
1352 ("user_id".to_string(), "bigint".to_string()),
1353 ("amount".to_string(), "numeric".to_string()),
1354 ],
1355 query: "SELECT * FROM VALUES (1, 1, 100.00)".to_string(),
1356 },
1357 ],
1358 expected: ExpectedResult {
1359 columns: vec![
1360 ("user_id".to_string(), "bigint".to_string()),
1361 ("total_orders".to_string(), "bigint".to_string()),
1362 ],
1363 query: "SELECT * FROM VALUES (1, 1)".to_string(),
1364 },
1365 };
1366
1367 let types = make_test_types();
1368 let target_id = make_target_id();
1369 let dependencies = make_dependencies();
1370
1371 let result = validate_unit_test(
1372 &test,
1373 &target_id,
1374 &|fqn| types.get_table(fqn).cloned(),
1375 &dependencies,
1376 );
1377 assert!(result.is_err());
1378
1379 match result.unwrap_err() {
1380 TestValidationError::ExpectedSchemaMismatch(err) => {
1381 assert_eq!(err.test_name, "test_user_summary");
1382 assert_eq!(err.target_view, "materialize.public.user_order_summary");
1383 assert!(
1384 err.missing_columns
1385 .iter()
1386 .any(|(name, _)| name == "user_name")
1387 );
1388 }
1389 other => panic!("Expected ExpectedSchemaMismatch error, got: {:?}", other),
1390 }
1391 }
1392
1393 #[mz_ore::test]
1394 fn test_validate_unit_test_fails_with_expected_type_mismatch() {
1395 let test = UnitTest {
1396 name: "test_user_summary".to_string(),
1397 target_view: "materialize.public.user_order_summary".to_string(),
1398 at_time: None,
1399 mocks: vec![
1400 MockView {
1401 fqn: "materialize.public.users".to_string(),
1402 columns: vec![
1403 ("id".to_string(), "bigint".to_string()),
1404 ("name".to_string(), "text".to_string()),
1405 ("email".to_string(), "text".to_string()),
1406 ],
1407 query: "SELECT * FROM VALUES (1, 'alice', 'alice@example.com')".to_string(),
1408 },
1409 MockView {
1410 fqn: "materialize.public.orders".to_string(),
1411 columns: vec![
1412 ("id".to_string(), "bigint".to_string()),
1413 ("user_id".to_string(), "bigint".to_string()),
1414 ("amount".to_string(), "numeric".to_string()),
1415 ],
1416 query: "SELECT * FROM VALUES (1, 1, 100.00)".to_string(),
1417 },
1418 ],
1419 expected: ExpectedResult {
1420 columns: vec![
1421 ("user_id".to_string(), "bigint".to_string()),
1422 ("user_name".to_string(), "bigint".to_string()),
1423 ("total_orders".to_string(), "bigint".to_string()),
1424 ],
1425 query: "SELECT * FROM VALUES (1, 1, 1)".to_string(),
1426 },
1427 };
1428
1429 let types = make_test_types();
1430 let target_id = make_target_id();
1431 let dependencies = make_dependencies();
1432
1433 let result = validate_unit_test(
1434 &test,
1435 &target_id,
1436 &|fqn| types.get_table(fqn).cloned(),
1437 &dependencies,
1438 );
1439 assert!(result.is_err());
1440
1441 match result.unwrap_err() {
1442 TestValidationError::ExpectedSchemaMismatch(err) => {
1443 assert!(
1444 err.type_mismatches
1445 .iter()
1446 .any(|(col, exp_t, _)| { col == "user_name" && exp_t == "bigint" })
1447 );
1448 }
1449 other => panic!("Expected ExpectedSchemaMismatch error, got: {:?}", other),
1450 }
1451 }
1452
1453 #[mz_ore::test]
1454 fn test_normalize_fqn_unqualified() {
1455 let target_id = ObjectId::new(
1456 "mydb".to_string(),
1457 "myschema".to_string(),
1458 "myview".to_string(),
1459 );
1460
1461 let normalized = normalize_fqn("users", &target_id);
1462 assert_eq!(normalized.to_string(), "mydb.myschema.users");
1463 }
1464
1465 #[mz_ore::test]
1466 fn test_normalize_fqn_schema_qualified() {
1467 let target_id = ObjectId::new(
1468 "mydb".to_string(),
1469 "myschema".to_string(),
1470 "myview".to_string(),
1471 );
1472
1473 let normalized = normalize_fqn("other_schema.users", &target_id);
1474 assert_eq!(normalized.to_string(), "mydb.other_schema.users");
1475 }
1476
1477 #[mz_ore::test]
1478 fn test_normalize_fqn_fully_qualified() {
1479 let target_id = ObjectId::new(
1480 "mydb".to_string(),
1481 "myschema".to_string(),
1482 "myview".to_string(),
1483 );
1484
1485 let normalized = normalize_fqn("other_db.other_schema.users", &target_id);
1486 assert_eq!(normalized.to_string(), "other_db.other_schema.users");
1487 }
1488
1489 #[mz_ore::test]
1490 fn test_normalize_type_integer_aliases() {
1491 assert_eq!(normalize_type("INT"), "integer");
1492 assert_eq!(normalize_type("int4"), "integer");
1493 assert_eq!(normalize_type("integer"), "integer");
1494 assert_eq!(normalize_type("INTEGER"), "integer");
1495 }
1496
1497 #[mz_ore::test]
1498 fn test_normalize_type_bigint_aliases() {
1499 assert_eq!(normalize_type("INT8"), "bigint");
1500 assert_eq!(normalize_type("bigint"), "bigint");
1501 assert_eq!(normalize_type("BIGINT"), "bigint");
1502 }
1503
1504 #[mz_ore::test]
1505 fn test_normalize_type_smallint_aliases() {
1506 assert_eq!(normalize_type("INT2"), "smallint");
1507 assert_eq!(normalize_type("smallint"), "smallint");
1508 assert_eq!(normalize_type("SMALLINT"), "smallint");
1509 }
1510
1511 #[mz_ore::test]
1512 fn test_normalize_type_real_aliases() {
1513 assert_eq!(normalize_type("float4"), "real");
1514 assert_eq!(normalize_type("FLOAT4"), "real");
1515 assert_eq!(normalize_type("real"), "real");
1516 assert_eq!(normalize_type("REAL"), "real");
1517 }
1518
1519 #[mz_ore::test]
1520 fn test_normalize_type_double_precision_aliases() {
1521 assert_eq!(normalize_type("float"), "double precision");
1522 assert_eq!(normalize_type("FLOAT"), "double precision");
1523 assert_eq!(normalize_type("float8"), "double precision");
1524 assert_eq!(normalize_type("FLOAT8"), "double precision");
1525 assert_eq!(normalize_type("double"), "double precision");
1526 assert_eq!(normalize_type("DOUBLE"), "double precision");
1527 assert_eq!(normalize_type("double precision"), "double precision");
1528 assert_eq!(normalize_type("DOUBLE PRECISION"), "double precision");
1529 }
1530
1531 #[mz_ore::test]
1532 fn test_normalize_type_boolean_aliases() {
1533 assert_eq!(normalize_type("bool"), "boolean");
1534 assert_eq!(normalize_type("boolean"), "boolean");
1535 assert_eq!(normalize_type("BOOL"), "boolean");
1536 assert_eq!(normalize_type("BOOLEAN"), "boolean");
1537 }
1538
1539 #[mz_ore::test]
1540 fn test_normalize_type_text_aliases() {
1541 assert_eq!(normalize_type("text"), "text");
1542 assert_eq!(normalize_type("TEXT"), "text");
1543 assert_eq!(normalize_type("string"), "text");
1544 assert_eq!(normalize_type("STRING"), "text");
1545 assert_eq!(normalize_type("varchar"), "text");
1546 assert_eq!(normalize_type("VARCHAR"), "text");
1547 assert_eq!(normalize_type("varchar(255)"), "text");
1548 assert_eq!(normalize_type("character varying"), "text");
1549 assert_eq!(normalize_type("character varying(100)"), "text");
1550 }
1551
1552 #[mz_ore::test]
1553 fn test_normalize_type_numeric_aliases() {
1554 assert_eq!(normalize_type("numeric"), "numeric");
1555 assert_eq!(normalize_type("NUMERIC"), "numeric");
1556 assert_eq!(normalize_type("decimal"), "numeric");
1557 assert_eq!(normalize_type("DECIMAL"), "numeric");
1558 assert_eq!(normalize_type("numeric(10,2)"), "numeric");
1559 assert_eq!(normalize_type("decimal(18,4)"), "numeric");
1560 }
1561
1562 #[mz_ore::test]
1563 fn test_normalize_type_jsonb_aliases() {
1564 assert_eq!(normalize_type("json"), "jsonb");
1565 assert_eq!(normalize_type("JSON"), "jsonb");
1566 assert_eq!(normalize_type("jsonb"), "jsonb");
1567 assert_eq!(normalize_type("JSONB"), "jsonb");
1568 }
1569
1570 #[mz_ore::test]
1571 fn test_normalize_type_timestamptz_aliases() {
1572 assert_eq!(normalize_type("timestamptz"), "timestamp with time zone");
1573 assert_eq!(normalize_type("TIMESTAMPTZ"), "timestamp with time zone");
1574 assert_eq!(
1575 normalize_type("timestamp with time zone"),
1576 "timestamp with time zone"
1577 );
1578 assert_eq!(
1579 normalize_type("TIMESTAMP WITH TIME ZONE"),
1580 "timestamp with time zone"
1581 );
1582 }
1583
1584 #[mz_ore::test]
1585 fn test_normalize_type_preserves_other_types() {
1586 assert_eq!(normalize_type("timestamp"), "timestamp without time zone");
1587 assert_eq!(normalize_type("TIMESTAMP"), "timestamp without time zone");
1588 assert_eq!(normalize_type("date"), "date");
1589 assert_eq!(normalize_type("time"), "time");
1590 assert_eq!(normalize_type("interval"), "interval");
1591 assert_eq!(normalize_type("uuid"), "uuid");
1592 assert_eq!(normalize_type("bytea"), "bytea");
1593 assert_eq!(normalize_type("oid"), "oid");
1594 assert_eq!(normalize_type("uint2"), "uint2");
1595 assert_eq!(normalize_type("uint4"), "uint4");
1596 assert_eq!(normalize_type("uint8"), "uint8");
1597 }
1598
1599 #[mz_ore::test]
1600 fn test_normalize_type_handles_whitespace() {
1601 assert_eq!(normalize_type(" INT "), "integer");
1602 assert_eq!(normalize_type("\ttext\n"), "text");
1603 assert_eq!(normalize_type(" double precision "), "double precision");
1604 }
1605
1606 #[mz_ore::test]
1607 fn test_normalize_type_case_insensitive() {
1608 assert_eq!(normalize_type("integer"), normalize_type("INTEGER"));
1609 assert_eq!(normalize_type("integer"), normalize_type("Integer"));
1610 assert_eq!(normalize_type("integer"), normalize_type("iNtEgEr"));
1611 assert_eq!(normalize_type("int"), normalize_type("INT"));
1612 assert_eq!(normalize_type("int"), normalize_type("Int"));
1613
1614 assert_eq!(normalize_type("bigint"), normalize_type("BIGINT"));
1615 assert_eq!(normalize_type("bigint"), normalize_type("BigInt"));
1616 assert_eq!(normalize_type("int8"), normalize_type("INT8"));
1617
1618 assert_eq!(normalize_type("text"), normalize_type("TEXT"));
1619 assert_eq!(normalize_type("text"), normalize_type("Text"));
1620 assert_eq!(normalize_type("string"), normalize_type("STRING"));
1621 assert_eq!(normalize_type("string"), normalize_type("String"));
1622
1623 assert_eq!(normalize_type("boolean"), normalize_type("BOOLEAN"));
1624 assert_eq!(normalize_type("boolean"), normalize_type("Boolean"));
1625 assert_eq!(normalize_type("bool"), normalize_type("BOOL"));
1626 assert_eq!(normalize_type("bool"), normalize_type("Bool"));
1627
1628 assert_eq!(normalize_type("numeric"), normalize_type("NUMERIC"));
1629 assert_eq!(normalize_type("numeric"), normalize_type("Numeric"));
1630 assert_eq!(normalize_type("decimal"), normalize_type("DECIMAL"));
1631
1632 assert_eq!(
1633 normalize_type("double precision"),
1634 normalize_type("DOUBLE PRECISION")
1635 );
1636 assert_eq!(
1637 normalize_type("double precision"),
1638 normalize_type("Double Precision")
1639 );
1640
1641 assert_eq!(
1642 normalize_type("timestamp with time zone"),
1643 normalize_type("TIMESTAMP WITH TIME ZONE")
1644 );
1645 assert_eq!(normalize_type("timestamptz"), normalize_type("TIMESTAMPTZ"));
1646 assert_eq!(normalize_type("timestamptz"), normalize_type("TimestampTZ"));
1647
1648 assert_eq!(normalize_type("jsonb"), normalize_type("JSONB"));
1649 assert_eq!(normalize_type("jsonb"), normalize_type("JsonB"));
1650 assert_eq!(normalize_type("json"), normalize_type("JSON"));
1651 }
1652
1653 #[mz_ore::test]
1654 fn test_compare_columns_exact_match() {
1655 let test_columns = vec![
1656 ("id".to_string(), "bigint".to_string()),
1657 ("name".to_string(), "text".to_string()),
1658 ];
1659
1660 let mut actual_columns = BTreeMap::new();
1661 actual_columns.insert(
1662 "id".to_string(),
1663 ColumnType {
1664 r#type: DataType::named("bigint"),
1665 nullable: false,
1666 position: 0,
1667 comment: None,
1668 },
1669 );
1670 actual_columns.insert(
1671 "name".to_string(),
1672 ColumnType {
1673 r#type: DataType::named("text"),
1674 nullable: true,
1675 position: 0,
1676 comment: None,
1677 },
1678 );
1679
1680 let (extra, missing, type_mismatches) = compare_columns(&test_columns, &actual_columns);
1681 assert!(extra.is_empty());
1682 assert!(missing.is_empty());
1683 assert!(type_mismatches.is_empty());
1684 }
1685
1686 #[mz_ore::test]
1687 fn test_compare_columns_with_type_aliases() {
1688 let test_columns = vec![
1689 ("id".to_string(), "INT".to_string()),
1690 ("count".to_string(), "INT8".to_string()),
1691 ];
1692
1693 let mut actual_columns = BTreeMap::new();
1694 actual_columns.insert(
1695 "id".to_string(),
1696 ColumnType {
1697 r#type: DataType::named("integer"),
1698 nullable: false,
1699 position: 0,
1700 comment: None,
1701 },
1702 );
1703 actual_columns.insert(
1704 "count".to_string(),
1705 ColumnType {
1706 r#type: DataType::named("bigint"),
1707 nullable: false,
1708 position: 0,
1709 comment: None,
1710 },
1711 );
1712
1713 let (extra, missing, type_mismatches) = compare_columns(&test_columns, &actual_columns);
1714 assert!(extra.is_empty());
1715 assert!(missing.is_empty());
1716 assert!(type_mismatches.is_empty());
1717 }
1718
1719 #[mz_ore::test]
1720 fn test_compare_columns_detects_extra() {
1721 let test_columns = vec![
1722 ("id".to_string(), "bigint".to_string()),
1723 ("extra".to_string(), "text".to_string()),
1724 ];
1725
1726 let mut actual_columns = BTreeMap::new();
1727 actual_columns.insert(
1728 "id".to_string(),
1729 ColumnType {
1730 r#type: DataType::named("bigint"),
1731 nullable: false,
1732 position: 0,
1733 comment: None,
1734 },
1735 );
1736
1737 let (extra, missing, _) = compare_columns(&test_columns, &actual_columns);
1738 assert_eq!(extra, vec!["extra".to_string()]);
1739 assert!(missing.is_empty());
1740 }
1741
1742 #[mz_ore::test]
1743 fn test_compare_columns_detects_missing() {
1744 let test_columns = vec![("id".to_string(), "bigint".to_string())];
1745
1746 let mut actual_columns = BTreeMap::new();
1747 actual_columns.insert(
1748 "id".to_string(),
1749 ColumnType {
1750 r#type: DataType::named("bigint"),
1751 nullable: false,
1752 position: 0,
1753 comment: None,
1754 },
1755 );
1756 actual_columns.insert(
1757 "name".to_string(),
1758 ColumnType {
1759 r#type: DataType::named("text"),
1760 nullable: true,
1761 position: 0,
1762 comment: None,
1763 },
1764 );
1765
1766 let (extra, missing, _) = compare_columns(&test_columns, &actual_columns);
1767 assert!(extra.is_empty());
1768 assert_eq!(missing, vec![("name".to_string(), "text".to_string())]);
1769 }
1770
1771 #[mz_ore::test]
1772 fn test_compare_columns_detects_type_mismatch() {
1773 let test_columns = vec![("id".to_string(), "text".to_string())];
1774
1775 let mut actual_columns = BTreeMap::new();
1776 actual_columns.insert(
1777 "id".to_string(),
1778 ColumnType {
1779 r#type: DataType::named("bigint"),
1780 nullable: false,
1781 position: 0,
1782 comment: None,
1783 },
1784 );
1785
1786 let (_, _, type_mismatches) = compare_columns(&test_columns, &actual_columns);
1787 assert_eq!(type_mismatches.len(), 1);
1788 assert_eq!(type_mismatches[0].0, "id");
1789 assert_eq!(type_mismatches[0].1, "text");
1790 assert_eq!(type_mismatches[0].2, "bigint");
1791 }
1792
1793 #[mz_ore::test]
1794 fn test_validate_with_unqualified_mock_name() {
1795 let test = UnitTest {
1796 name: "test_partial_fqn".to_string(),
1797 target_view: "materialize.public.user_order_summary".to_string(),
1798 at_time: None,
1799 mocks: vec![
1800 MockView {
1801 fqn: "users".to_string(),
1802 columns: vec![
1803 ("id".to_string(), "bigint".to_string()),
1804 ("name".to_string(), "text".to_string()),
1805 ("email".to_string(), "text".to_string()),
1806 ],
1807 query: "SELECT * FROM VALUES (1, 'alice', 'alice@example.com')".to_string(),
1808 },
1809 MockView {
1810 fqn: "public.orders".to_string(),
1811 columns: vec![
1812 ("id".to_string(), "bigint".to_string()),
1813 ("user_id".to_string(), "bigint".to_string()),
1814 ("amount".to_string(), "numeric".to_string()),
1815 ],
1816 query: "SELECT * FROM VALUES (1, 1, 100.00)".to_string(),
1817 },
1818 ],
1819 expected: ExpectedResult {
1820 columns: vec![
1821 ("user_id".to_string(), "bigint".to_string()),
1822 ("user_name".to_string(), "text".to_string()),
1823 ("total_orders".to_string(), "bigint".to_string()),
1824 ],
1825 query: "SELECT * FROM VALUES (1, 'alice', 1)".to_string(),
1826 },
1827 };
1828
1829 let types = make_test_types();
1830 let target_id = make_target_id();
1831 let dependencies = make_dependencies();
1832
1833 let result = validate_unit_test(
1834 &test,
1835 &target_id,
1836 &|fqn| types.get_table(fqn).cloned(),
1837 &dependencies,
1838 );
1839 assert!(result.is_ok(), "Expected validation to pass: {:?}", result);
1840 }
1841
1842 #[mz_ore::test]
1843 fn test_validate_passes_with_no_dependencies() {
1844 let test = UnitTest {
1845 name: "test_no_deps".to_string(),
1846 target_view: "materialize.public.my_view".to_string(),
1847 at_time: None,
1848 mocks: vec![],
1849 expected: ExpectedResult {
1850 columns: vec![("result".to_string(), "integer".to_string())],
1851 query: "SELECT * FROM VALUES (42)".to_string(),
1852 },
1853 };
1854
1855 let types = Types::default();
1856 let target_id = ObjectId::new(
1857 "materialize".to_string(),
1858 "public".to_string(),
1859 "my_view".to_string(),
1860 );
1861 let dependencies = BTreeSet::new();
1862
1863 let result = validate_unit_test(
1864 &test,
1865 &target_id,
1866 &|fqn| types.get_table(fqn).cloned(),
1867 &dependencies,
1868 );
1869 assert!(result.is_ok());
1870 }
1871
1872 #[mz_ore::test]
1873 fn test_validate_skips_unknown_mock() {
1874 let test = UnitTest {
1875 name: "test_unknown_mock".to_string(),
1876 target_view: "materialize.public.my_view".to_string(),
1877 at_time: None,
1878 mocks: vec![MockView {
1879 fqn: "materialize.public.unknown_table".to_string(),
1880 columns: vec![("id".to_string(), "bigint".to_string())],
1881 query: "SELECT * FROM VALUES (1)".to_string(),
1882 }],
1883 expected: ExpectedResult {
1884 columns: vec![("result".to_string(), "integer".to_string())],
1885 query: "SELECT * FROM VALUES (42)".to_string(),
1886 },
1887 };
1888
1889 let types = Types::default();
1890 let target_id = ObjectId::new(
1891 "materialize".to_string(),
1892 "public".to_string(),
1893 "my_view".to_string(),
1894 );
1895
1896 let mut dependencies = BTreeSet::new();
1897 dependencies.insert(ObjectId::new(
1898 "materialize".to_string(),
1899 "public".to_string(),
1900 "unknown_table".to_string(),
1901 ));
1902
1903 let result = validate_unit_test(
1904 &test,
1905 &target_id,
1906 &|fqn| types.get_table(fqn).cloned(),
1907 &dependencies,
1908 );
1909 assert!(result.is_ok());
1910 }
1911
1912 #[mz_ore::test]
1913 fn test_normalize_type_list() {
1914 assert_eq!(normalize_type("int8 list"), "bigint list");
1915 assert_eq!(normalize_type("INT LIST"), "integer list");
1916 assert_eq!(normalize_type("text list"), "text list");
1917 assert_eq!(normalize_type("INT8 LIST"), "bigint list");
1918 }
1919
1920 #[mz_ore::test]
1921 fn test_normalize_type_array() {
1922 assert_eq!(normalize_type("int8[]"), "bigint[]");
1923 assert_eq!(normalize_type("INT[]"), "integer[]");
1924 assert_eq!(normalize_type("text[]"), "text[]");
1925 }
1926
1927 #[mz_ore::test]
1928 fn test_normalize_type_map() {
1929 assert_eq!(normalize_type("map[text=>int8]"), "map[text=>bigint]");
1930 assert_eq!(normalize_type("map[STRING=>BOOL]"), "map[text=>boolean]");
1931 }
1932
1933 #[mz_ore::test]
1934 fn test_normalize_type_bare_list() {
1935 assert_eq!(normalize_type("list"), "list");
1936 assert_eq!(normalize_type("LIST"), "list");
1937 }
1938
1939 #[mz_ore::test]
1940 fn test_compare_columns_list_matches_bare() {
1941 let test_columns = vec![("ids".to_string(), "int8 list".to_string())];
1942
1943 let mut actual_columns = BTreeMap::new();
1944 actual_columns.insert(
1945 "ids".to_string(),
1946 ColumnType {
1947 r#type: DataType::named("list"),
1948 nullable: true,
1949 position: 0,
1950 comment: None,
1951 },
1952 );
1953
1954 let (extra, missing, type_mismatches) = compare_columns(&test_columns, &actual_columns);
1955 assert!(extra.is_empty());
1956 assert!(missing.is_empty());
1957 assert!(
1958 type_mismatches.is_empty(),
1959 "Expected no type mismatches for 'int8 list' vs bare 'list', got: {:?}",
1960 type_mismatches
1961 );
1962 }
1963
1964 #[mz_ore::test]
1965 fn test_compare_columns_map_matches_bare() {
1966 let test_columns = vec![("data".to_string(), "map[text=>int8]".to_string())];
1967
1968 let mut actual_columns = BTreeMap::new();
1969 actual_columns.insert(
1970 "data".to_string(),
1971 ColumnType {
1972 r#type: DataType::named("map"),
1973 nullable: true,
1974 position: 0,
1975 comment: None,
1976 },
1977 );
1978
1979 let (_, _, type_mismatches) = compare_columns(&test_columns, &actual_columns);
1980 assert!(
1981 type_mismatches.is_empty(),
1982 "Expected no type mismatches for 'map[text=>int8]' vs bare 'map', got: {:?}",
1983 type_mismatches
1984 );
1985 }
1986}