1use super::cache::BuildArtifact;
16use crate::project::ast::Statement;
17use crate::project::ir::compiled::FullyQualifiedName;
18use crate::project::ir::graph::Project;
19use crate::project::ir::object_id::ObjectId;
20use crate::types::{ColumnType, ObjectKind, Types, TypesError};
21use crate::verbose;
22use sha2::{Digest, Sha256};
23use std::collections::{BTreeMap, BTreeSet};
24use std::path::Path;
25use std::sync::Arc;
26
27mod bootstrap;
28mod catalog;
29mod convert;
30mod error;
31mod executor;
32
33pub(crate) use error::{ObjectTypeCheckError, ObjectTypeCheckErrorKind, TypeCheckError};
34
35#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
40pub(crate) struct TypecheckStats {
41 pub ran: usize,
42 pub skipped: usize,
43 pub schema_stable: usize,
44 pub schema_changed: usize,
45}
46
47#[derive(Debug, Clone)]
50struct NodeValue {
51 columns: BTreeMap<String, ColumnType>,
52 schema_stable: bool,
53}
54
55pub(crate) fn run(
73 directory: &Path,
74 profile: &str,
75 profile_suffix: Option<&str>,
76 variables: &BTreeMap<String, String>,
77 project: &Project,
78 external_types: Types,
79) -> Result<(Types, TypecheckStats), TypeCheckError> {
80 let sorted = project.get_sorted_objects()?;
81 let typed_objects: BTreeMap<ObjectId, &crate::project::ir::compiled::DatabaseObject> = sorted
82 .iter()
83 .filter(|(_, db_obj)| {
84 matches!(
85 db_obj.stmt,
86 Statement::CreateView(_) | Statement::CreateMaterializedView(_)
87 )
88 })
89 .map(|(id, db_obj)| (id.clone(), *db_obj))
90 .collect();
91
92 let mut db = BuildArtifact::open(directory, profile, profile_suffix, variables)
95 .map_err(TypesError::from)?;
96
97 let cached_columns_by_key: BTreeMap<String, BTreeMap<String, ColumnType>> =
101 db.load_typecheck_columns().map_err(TypesError::from)?;
102
103 let current_ext_digests = compute_external_digests(&external_types);
107 let cached_ext_digests = db.load_external_type_digests().map_err(TypesError::from)?;
108 let changed_externals: BTreeSet<ObjectId> = current_ext_digests
109 .iter()
110 .filter(|(k, v)| cached_ext_digests.get(*k) != Some(*v))
111 .filter_map(|(k, _)| k.parse().ok())
112 .chain(
113 cached_ext_digests
114 .keys()
115 .filter(|k| !current_ext_digests.contains_key(*k))
116 .filter_map(|k| k.parse().ok()),
117 )
118 .collect();
119
120 let reverse_graph = project.build_reverse_dependency_graph();
121
122 let initial_dirty: BTreeSet<ObjectId> = typed_objects
123 .keys()
124 .filter(|id| {
125 if project.compile_dirty.contains(id) {
127 return true;
128 }
129 if !cached_columns_by_key.contains_key(&id.to_string()) {
132 return true;
133 }
134 let Some(deps) = project.dependency_graph.get(id) else {
138 return false;
139 };
140 let external_schema_changed = |d: &ObjectId| changed_externals.contains(d);
141 let non_view_dep_recompiled =
142 |d: &ObjectId| project.compile_dirty.contains(d) && !typed_objects.contains_key(d);
143 deps.iter()
144 .any(|d| external_schema_changed(d) || non_view_dep_recompiled(d))
145 })
146 .cloned()
147 .collect();
148
149 let mut pessimistic_dirty: BTreeSet<ObjectId> = BTreeSet::new();
152 let mut stack: Vec<ObjectId> = initial_dirty.iter().cloned().collect();
153 while let Some(id) = stack.pop() {
154 if !pessimistic_dirty.insert(id.clone()) {
155 continue;
156 }
157 if let Some(downs) = reverse_graph.get(&id) {
158 for d in downs {
159 if typed_objects.contains_key(d) {
160 stack.push(d.clone());
161 }
162 }
163 }
164 }
165
166 let mut dag_nodes: BTreeSet<ObjectId> = pessimistic_dirty.clone();
170 let mut bootstrap_set: BTreeSet<ObjectId> = BTreeSet::new();
171 for id in &pessimistic_dirty {
172 let Some(deps) = project.dependency_graph.get(id) else {
173 continue;
174 };
175 for d in deps {
176 if typed_objects.contains_key(d) {
177 dag_nodes.insert(d.clone());
178 } else {
179 bootstrap_set.insert(d.clone());
180 }
181 }
182 }
183
184 let (base_catalog, base_columns) =
185 bootstrap::bootstrap_catalog(project, &external_types, Some(&bootstrap_set))?;
186
187 let dag_node_ids: Vec<ObjectId> = typed_objects
191 .keys()
192 .filter(|id| dag_nodes.contains(id))
193 .cloned()
194 .collect();
195 let mut direct_deps: BTreeMap<ObjectId, Vec<ObjectId>> = BTreeMap::new();
196 let mut dependents: BTreeMap<ObjectId, Vec<ObjectId>> = BTreeMap::new();
197 for node_id in &dag_node_ids {
198 let node_deps = project
199 .dependency_graph
200 .get(node_id)
201 .into_iter()
202 .flatten()
203 .filter(|d| dag_nodes.contains(d))
204 .cloned()
205 .collect();
206 direct_deps.insert(node_id.clone(), node_deps);
207
208 let node_dependents = reverse_graph
209 .get(node_id)
210 .into_iter()
211 .flatten()
212 .filter(|d| dag_nodes.contains(d))
213 .cloned()
214 .collect();
215 dependents.insert(node_id.clone(), node_dependents);
216 }
217
218 let stats_counter = Arc::new(StatsCounter::default());
219
220 let typed_objects = Arc::new(typed_objects);
221 let cached_columns_by_key = Arc::new(cached_columns_by_key);
222 let outcomes = {
223 let typed_objects = Arc::clone(&typed_objects);
224 let base_catalog = Arc::clone(&base_catalog);
225 let initial_dirty = Arc::new(initial_dirty);
226 let cached_columns_by_key = Arc::clone(&cached_columns_by_key);
227 let stats_counter = Arc::clone(&stats_counter);
228 executor::run::<NodeValue, _>(
229 dag_node_ids.clone(),
230 direct_deps,
231 dependents,
232 move |node_id, dep_results| {
233 let db_obj = typed_objects
234 .get(node_id)
235 .expect("typed_object exists for every scheduled node");
236
237 let cached_columns = cached_columns_by_key.get(&node_id.to_string()).cloned();
238 let any_dep_changed = dep_results.values().any(|v| !v.schema_stable);
239 let must_typecheck =
240 initial_dirty.contains(node_id) || any_dep_changed || cached_columns.is_none();
241
242 if !must_typecheck {
243 let columns = cached_columns.expect("must_typecheck guards None");
244 return Ok(NodeValue {
245 columns,
246 schema_stable: true,
247 });
248 }
249
250 let value = typecheck_node(
251 node_id,
252 db_obj,
253 Arc::clone(&base_catalog),
254 dep_results,
255 cached_columns.as_ref(),
256 )?;
257 stats_counter.record(value.schema_stable);
258 Ok(value)
259 },
260 )
261 };
262
263 let mut errors: Vec<ObjectTypeCheckError> = Vec::new();
264 let mut upsert_rows: Vec<(String, String, BTreeMap<String, ColumnType>)> = Vec::new();
265 let mut merged_tables: BTreeMap<ObjectId, BTreeMap<String, ColumnType>> = BTreeMap::new();
266 let mut merged_kinds: BTreeMap<ObjectId, ObjectKind> = BTreeMap::new();
267
268 let project_kinds: BTreeMap<&ObjectId, ObjectKind> = project
269 .iter_objects()
270 .map(|obj| (&obj.id, obj.typed_object.stmt.kind()))
271 .collect();
272 for (id, columns) in base_columns.iter() {
273 merged_tables.insert(id.clone(), columns.clone());
274 if let Some(kind) = project_kinds.get(id) {
275 merged_kinds.insert(id.clone(), *kind);
276 }
277 }
278 for (id, columns) in &external_types.tables {
279 merged_tables.insert(id.clone(), columns.clone());
280 if let Some(kind) = external_types.kinds.get(id) {
281 merged_kinds.insert(id.clone(), *kind);
282 }
283 }
284
285 let mut unhealthy: BTreeSet<String> = BTreeSet::new();
286 for node_id in typed_objects.keys() {
287 let Some(outcome) = outcomes.get(node_id) else {
288 continue;
289 };
290 match outcome {
291 executor::NodeOutcome::Ok(value) => {
292 let db_obj = typed_objects
293 .get(node_id)
294 .expect("typed_object exists for outcome");
295 let kind = db_obj.stmt.kind();
296 merged_tables.insert(node_id.clone(), value.columns.clone());
297 merged_kinds.insert(node_id.clone(), kind);
298 if !value.schema_stable {
302 upsert_rows.push((
303 node_id.to_string(),
304 kind.as_str().to_string(),
305 value.columns.clone(),
306 ));
307 }
308 }
309 executor::NodeOutcome::Failed(err) => {
310 let mut err = err.clone();
313 if let Some(db_obj) = typed_objects.get(node_id) {
314 err.file_path = directory.join(&db_obj.path);
315 }
316 errors.push(err);
317 unhealthy.insert(node_id.to_string());
318 }
319 executor::NodeOutcome::Blocked(blocker) => {
320 verbose!(
321 "Skipping {}: blocked by upstream error in {}",
322 node_id,
323 blocker
324 );
325 unhealthy.insert(node_id.to_string());
326 }
327 }
328 }
329
330 db.upsert_typecheck_results(&upsert_rows)
334 .map_err(TypesError::from)?;
335 let keep: BTreeSet<String> = typed_objects
336 .keys()
337 .map(|id| id.to_string())
338 .filter(|key| !unhealthy.contains(key))
339 .collect();
340 db.prune_typecheck_results(&keep)
341 .map_err(TypesError::from)?;
342 db.replace_external_type_digests(¤t_ext_digests)
343 .map_err(TypesError::from)?;
344
345 if !errors.is_empty() {
346 return Err(TypeCheckError::Multiple(errors));
347 }
348
349 let stats = stats_counter.snapshot(typed_objects.len());
350
351 Ok((
352 Types {
353 version: 1,
354 tables: merged_tables,
355 kinds: merged_kinds,
356 comments: BTreeMap::new(),
357 },
358 stats,
359 ))
360}
361
362#[derive(Default)]
364struct StatsCounter {
365 schema_stable: std::sync::atomic::AtomicUsize,
366 schema_changed: std::sync::atomic::AtomicUsize,
367}
368
369impl StatsCounter {
370 fn record(&self, schema_stable: bool) {
371 let counter = if schema_stable {
372 &self.schema_stable
373 } else {
374 &self.schema_changed
375 };
376 counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
377 }
378
379 fn snapshot(&self, total_nodes: usize) -> TypecheckStats {
380 use std::sync::atomic::Ordering::Relaxed;
381 let schema_stable = self.schema_stable.load(Relaxed);
382 let schema_changed = self.schema_changed.load(Relaxed);
383 let ran = schema_stable + schema_changed;
384 TypecheckStats {
385 ran,
386 skipped: total_nodes.saturating_sub(ran),
387 schema_stable,
388 schema_changed,
389 }
390 }
391}
392
393fn typecheck_node(
394 node_id: &ObjectId,
395 db_obj: &crate::project::ir::compiled::DatabaseObject,
396 base_catalog: Arc<catalog::CatalogRuntime>,
397 dep_results: &BTreeMap<ObjectId, Arc<NodeValue>>,
398 cached_columns: Option<&BTreeMap<String, ColumnType>>,
399) -> Result<NodeValue, ObjectTypeCheckError> {
400 let mut runtime = catalog::TaskCatalog::new(base_catalog);
401 for (dep_id, dep_value) in dep_results {
402 runtime
403 .create_stub_table(dep_id, &dep_value.columns)
404 .map_err(|err| {
405 ObjectTypeCheckError::internal(
406 dep_id.clone(),
407 db_obj.path.clone(),
408 format!("failed to stub dependency: {err}"),
409 )
410 })?;
411 }
412 let fqn: FullyQualifiedName = node_id.clone().into();
413 let ast = convert::create_catalog_item_ast(&db_obj.stmt, &fqn).ok_or_else(|| {
414 ObjectTypeCheckError::internal(
415 node_id.clone(),
416 db_obj.path.clone(),
417 "internal: failed to build catalog AST".into(),
418 )
419 })?;
420 let desc = runtime.create_item_from_ast(node_id, ast)?;
421 let columns = convert::relation_desc_to_columns(&desc);
422 let schema_stable = cached_columns.is_some_and(|cached| cached == &columns);
423 Ok(NodeValue {
424 columns,
425 schema_stable,
426 })
427}
428
429fn digest_columns(cols: &BTreeMap<String, ColumnType>) -> String {
432 let mut hasher = Sha256::new();
433 for (name, t) in cols {
434 hasher.update(name.as_bytes());
435 hasher.update(b"\0");
436 hasher.update(t.r#type.as_bytes());
437 hasher.update(b"\0");
438 hasher.update([u8::from(t.nullable)]);
439 hasher.update(b"\0");
440 hasher.update(u64::try_from(t.position).unwrap_or(u64::MAX).to_le_bytes());
441 hasher.update(b"\0");
442 }
443 format!("{:x}", hasher.finalize())
444}
445
446fn compute_external_digests(external_types: &Types) -> BTreeMap<String, String> {
448 external_types
449 .tables
450 .iter()
451 .map(|(id, cols)| (id.to_string(), digest_columns(cols)))
452 .collect()
453}
454
455#[cfg(test)]
456mod run_tests {
457 use super::*;
458 use crate::project::compiler::compile_sync;
459 use std::collections::BTreeMap;
460 use std::fs;
461 use tempfile::tempdir;
462
463 fn write_sql(root: &Path, rel: &str, sql: &str) {
464 let path = root.join(rel);
465 fs::create_dir_all(path.parent().unwrap()).unwrap();
466 fs::write(path, sql).unwrap();
467 }
468
469 #[cfg_attr(miri, ignore)] #[mz_ore::test]
471 fn run_typechecks_simple_view_and_persists_columns() {
472 let temp = tempdir().unwrap();
473 let root = temp.path();
474 write_sql(
476 root,
477 "models/materialize/storage/t1.sql",
478 "CREATE TABLE t1 (a int)",
479 );
480 write_sql(
481 root,
482 "models/materialize/public/v1.sql",
483 "CREATE VIEW v1 AS SELECT a FROM materialize.storage.t1",
484 );
485
486 let fs = crate::fs::FileSystem::new();
487 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
488 let (merged, _stats) = run(
489 root,
490 "default",
491 None,
492 &BTreeMap::new(),
493 &project,
494 Types::default(),
495 )
496 .unwrap();
497
498 assert!(
499 merged
500 .tables
501 .contains_key(&"materialize.public.v1".parse::<ObjectId>().unwrap())
502 );
503 assert!(
504 merged
505 .tables
506 .contains_key(&"materialize.storage.t1".parse::<ObjectId>().unwrap())
507 );
508 }
509
510 #[cfg_attr(miri, ignore)] #[mz_ore::test]
513 fn second_run_skips_all_nodes_when_nothing_changed() {
514 let temp = tempdir().unwrap();
515 let root = temp.path();
516 write_sql(
517 root,
518 "models/materialize/storage/t1.sql",
519 "CREATE TABLE t1 (a int)",
520 );
521 write_sql(
522 root,
523 "models/materialize/public/v1.sql",
524 "CREATE VIEW v1 AS SELECT a FROM materialize.storage.t1",
525 );
526 write_sql(
527 root,
528 "models/materialize/public/v2.sql",
529 "CREATE VIEW v2 AS SELECT a FROM materialize.public.v1",
530 );
531
532 let fs = crate::fs::FileSystem::new();
533 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
535 let (_, first) = run(
536 root,
537 "default",
538 None,
539 &BTreeMap::new(),
540 &project,
541 Types::default(),
542 )
543 .unwrap();
544 assert_eq!(first.ran, 2, "first run should typecheck v1 and v2");
545 assert_eq!(first.skipped, 0);
546
547 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
549 let (_, second) = run(
550 root,
551 "default",
552 None,
553 &BTreeMap::new(),
554 &project,
555 Types::default(),
556 )
557 .unwrap();
558 assert_eq!(second.ran, 0, "second run should skip everything");
559 assert_eq!(second.skipped, 2);
560 }
561
562 #[cfg_attr(miri, ignore)] #[mz_ore::test]
566 fn schema_stable_edit_does_not_dirty_dependents() {
567 let temp = tempdir().unwrap();
568 let root = temp.path();
569 write_sql(
570 root,
571 "models/materialize/storage/t1.sql",
572 "CREATE TABLE t1 (a int)",
573 );
574 write_sql(
575 root,
576 "models/materialize/public/v1.sql",
577 "CREATE VIEW v1 AS SELECT a FROM materialize.storage.t1",
578 );
579 write_sql(
580 root,
581 "models/materialize/public/v2.sql",
582 "CREATE VIEW v2 AS SELECT a FROM materialize.public.v1",
583 );
584
585 let fs = crate::fs::FileSystem::new();
586 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
587 let _ = run(
588 root,
589 "default",
590 None,
591 &BTreeMap::new(),
592 &project,
593 Types::default(),
594 )
595 .unwrap();
596
597 write_sql(
599 root,
600 "models/materialize/public/v1.sql",
601 "CREATE VIEW v1 AS SELECT a FROM (SELECT * FROM materialize.storage.t1)",
602 );
603 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
604 let (_, stats) = run(
605 root,
606 "default",
607 None,
608 &BTreeMap::new(),
609 &project,
610 Types::default(),
611 )
612 .unwrap();
613
614 assert_eq!(stats.ran, 1, "only v1 should re-typecheck");
615 assert_eq!(stats.schema_stable, 1, "v1 output unchanged");
616 assert_eq!(stats.schema_changed, 0);
617 assert_eq!(stats.skipped, 1, "v2 should skip on stable upstream");
618 }
619
620 #[cfg_attr(miri, ignore)] #[mz_ore::test]
624 fn external_type_change_dirties_only_consumers() {
625 use crate::types::ObjectKind;
626
627 let temp = tempdir().unwrap();
628 let root = temp.path();
629 write_sql(
633 root,
634 "models/materialize/public/v_ext_a.sql",
635 "CREATE VIEW v_ext_a AS SELECT a FROM ext.public.t_a",
636 );
637 write_sql(
638 root,
639 "models/materialize/public/v_ext_b.sql",
640 "CREATE VIEW v_ext_b AS SELECT a FROM ext.public.t_b",
641 );
642
643 let mk_types = |a_type: &str, b_type: &str| {
644 let mut tables: BTreeMap<ObjectId, BTreeMap<String, ColumnType>> = BTreeMap::new();
645 let mut kinds: BTreeMap<ObjectId, ObjectKind> = BTreeMap::new();
646 let t_a: ObjectId = "ext.public.t_a".parse().unwrap();
647 let t_b: ObjectId = "ext.public.t_b".parse().unwrap();
648 tables.insert(
649 t_a.clone(),
650 BTreeMap::from([(
651 "a".to_string(),
652 ColumnType {
653 r#type: a_type.to_string(),
654 nullable: true,
655 position: 0,
656 comment: None,
657 },
658 )]),
659 );
660 tables.insert(
661 t_b.clone(),
662 BTreeMap::from([(
663 "a".to_string(),
664 ColumnType {
665 r#type: b_type.to_string(),
666 nullable: true,
667 position: 0,
668 comment: None,
669 },
670 )]),
671 );
672 kinds.insert(t_a, ObjectKind::Table);
673 kinds.insert(t_b, ObjectKind::Table);
674 Types {
675 version: 1,
676 tables,
677 kinds,
678 comments: BTreeMap::new(),
679 }
680 };
681
682 let fs = crate::fs::FileSystem::new();
683 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
685 let _ = run(
686 root,
687 "default",
688 None,
689 &BTreeMap::new(),
690 &project,
691 mk_types("integer", "integer"),
692 )
693 .unwrap();
694
695 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
697 let (_, stats) = run(
698 root,
699 "default",
700 None,
701 &BTreeMap::new(),
702 &project,
703 mk_types("integer", "integer"),
704 )
705 .unwrap();
706 assert_eq!(stats.skipped, 2, "no external change → both skip");
707 assert_eq!(stats.ran, 0);
708
709 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
711 let (_, stats) = run(
712 root,
713 "default",
714 None,
715 &BTreeMap::new(),
716 &project,
717 mk_types("text", "integer"),
718 )
719 .unwrap();
720 assert_eq!(stats.ran, 1, "only v_ext_a should re-run");
721 assert_eq!(stats.skipped, 1, "v_ext_b should skip");
722 }
723
724 #[cfg_attr(miri, ignore)] #[mz_ore::test]
727 fn schema_change_dirties_dependents() {
728 let temp = tempdir().unwrap();
729 let root = temp.path();
730 write_sql(
731 root,
732 "models/materialize/storage/t1.sql",
733 "CREATE TABLE t1 (a int, b int)",
734 );
735 write_sql(
736 root,
737 "models/materialize/public/v1.sql",
738 "CREATE VIEW v1 AS SELECT a FROM materialize.storage.t1",
739 );
740 write_sql(
741 root,
742 "models/materialize/public/v2.sql",
743 "CREATE VIEW v2 AS SELECT * FROM materialize.public.v1",
744 );
745
746 let fs = crate::fs::FileSystem::new();
747 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
748 let _ = run(
749 root,
750 "default",
751 None,
752 &BTreeMap::new(),
753 &project,
754 Types::default(),
755 )
756 .unwrap();
757
758 write_sql(
760 root,
761 "models/materialize/public/v1.sql",
762 "CREATE VIEW v1 AS SELECT a, b FROM materialize.storage.t1",
763 );
764 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
765 let (_, stats) = run(
766 root,
767 "default",
768 None,
769 &BTreeMap::new(),
770 &project,
771 Types::default(),
772 )
773 .unwrap();
774
775 assert_eq!(stats.ran, 2, "v1 changed, v2 must re-run");
776 assert_eq!(stats.schema_changed, 2);
777 assert_eq!(stats.skipped, 0);
778 }
779
780 #[cfg_attr(miri, ignore)] #[mz_ore::test]
785 fn previous_typecheck_failure_re_runs_next_invocation() {
786 let temp = tempdir().unwrap();
787 let root = temp.path();
788 write_sql(
789 root,
790 "models/materialize/storage/t1.sql",
791 "CREATE TABLE t1 (a int)",
792 );
793 write_sql(
795 root,
796 "models/materialize/public/v1.sql",
797 "CREATE VIEW v1 AS SELECT no_such_column FROM materialize.storage.t1",
798 );
799
800 let fs = crate::fs::FileSystem::new();
801 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
802 let first = run(
803 root,
804 "default",
805 None,
806 &BTreeMap::new(),
807 &project,
808 Types::default(),
809 );
810 assert!(first.is_err(), "first run should fail typechecking v1");
811
812 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
815 let second = run(
816 root,
817 "default",
818 None,
819 &BTreeMap::new(),
820 &project,
821 Types::default(),
822 );
823 assert!(
824 second.is_err(),
825 "second run must also fail — typecheck cache must not mask an unfixed error"
826 );
827 }
828
829 #[cfg_attr(miri, ignore)] #[mz_ore::test]
833 fn typecheck_failure_after_successful_run_persists() {
834 let temp = tempdir().unwrap();
835 let root = temp.path();
836 write_sql(
837 root,
838 "models/materialize/storage/t1.sql",
839 "CREATE TABLE t1 (a int)",
840 );
841 write_sql(
842 root,
843 "models/materialize/public/v1.sql",
844 "CREATE VIEW v1 AS SELECT a FROM materialize.storage.t1",
845 );
846
847 let fs = crate::fs::FileSystem::new();
848 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
849 run(
850 root,
851 "default",
852 None,
853 &BTreeMap::new(),
854 &project,
855 Types::default(),
856 )
857 .expect("first run typechecks cleanly");
858
859 write_sql(
860 root,
861 "models/materialize/public/v1.sql",
862 "CREATE VIEW v1 AS SELECT no_such_column FROM materialize.storage.t1",
863 );
864 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
865 let second = run(
866 root,
867 "default",
868 None,
869 &BTreeMap::new(),
870 &project,
871 Types::default(),
872 );
873 assert!(second.is_err(), "edit should surface typecheck error");
874
875 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
876 let third = run(
877 root,
878 "default",
879 None,
880 &BTreeMap::new(),
881 &project,
882 Types::default(),
883 );
884 assert!(
885 third.is_err(),
886 "stale cache row from before the edit must not let a broken view skip typecheck"
887 );
888 }
889
890 #[cfg_attr(miri, ignore)] #[mz_ore::test]
895 fn table_edit_dirties_dependent_view() {
896 let temp = tempdir().unwrap();
897 let root = temp.path();
898 write_sql(
899 root,
900 "models/materialize/storage/t1.sql",
901 "CREATE TABLE t1 (a int)",
902 );
903 write_sql(
904 root,
905 "models/materialize/public/v1.sql",
906 "CREATE VIEW v1 AS SELECT a FROM materialize.storage.t1",
907 );
908
909 let fs = crate::fs::FileSystem::new();
910 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
911 let _ = run(
912 root,
913 "default",
914 None,
915 &BTreeMap::new(),
916 &project,
917 Types::default(),
918 )
919 .unwrap();
920
921 write_sql(
923 root,
924 "models/materialize/storage/t1.sql",
925 "CREATE TABLE t1 (b int)",
926 );
927 let project = compile_sync(&fs, root, None, None, &BTreeMap::new()).unwrap();
928 let result = run(
929 root,
930 "default",
931 None,
932 &BTreeMap::new(),
933 &project,
934 Types::default(),
935 );
936 assert!(
937 result.is_err(),
938 "v1 references column `a` which no longer exists on t1; \
939 dependent view must re-typecheck and surface the error"
940 );
941 }
942}