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