1pub(crate) mod data_type;
49pub(crate) mod stub;
50
51pub(crate) use data_type::{DataType, RecordField};
52
53use crate::project::ir::object_id::ObjectId;
54use data_type::{FieldLock, TypeLock, TypeLockError};
55use serde::{Deserialize, Serialize};
56use std::collections::BTreeMap;
57use std::fmt;
58use std::fs;
59use std::path::{Path, PathBuf};
60use std::str::FromStr;
61use thiserror::Error;
62
63#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
68#[serde(rename_all = "kebab-case")]
69pub enum ObjectKind {
70 Table,
71 View,
72 MaterializedView,
73 Source,
74 Sink,
75 Secret,
76 Connection,
77}
78
79impl FromStr for ObjectKind {
80 type Err = String;
81
82 fn from_str(s: &str) -> Result<Self, Self::Err> {
83 match s {
84 "table" => Ok(ObjectKind::Table),
85 "view" => Ok(ObjectKind::View),
86 "materialized-view" => Ok(ObjectKind::MaterializedView),
87 "source" => Ok(ObjectKind::Source),
88 "sink" => Ok(ObjectKind::Sink),
89 "secret" => Ok(ObjectKind::Secret),
90 "connection" => Ok(ObjectKind::Connection),
91 _ => Err(format!("unknown object kind: {}", s)),
92 }
93 }
94}
95
96impl ObjectKind {
97 pub fn from_db_str(s: &str) -> Self {
99 match s {
100 "table" => ObjectKind::Table,
101 "view" => ObjectKind::View,
102 "materialized-view" => ObjectKind::MaterializedView,
103 "source" => ObjectKind::Source,
104 "sink" => ObjectKind::Sink,
105 "secret" => ObjectKind::Secret,
106 "connection" => ObjectKind::Connection,
107 _ => ObjectKind::Table,
108 }
109 }
110
111 pub fn as_str(self) -> &'static str {
113 match self {
114 ObjectKind::Table => "table",
115 ObjectKind::View => "view",
116 ObjectKind::MaterializedView => "materialized-view",
117 ObjectKind::Source => "source",
118 ObjectKind::Sink => "sink",
119 ObjectKind::Secret => "secret",
120 ObjectKind::Connection => "connection",
121 }
122 }
123}
124
125impl fmt::Display for ObjectKind {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 match self {
128 ObjectKind::Table => write!(f, "table"),
129 ObjectKind::View => write!(f, "view"),
130 ObjectKind::MaterializedView => write!(f, "materialized view"),
131 ObjectKind::Source => write!(f, "source"),
132 ObjectKind::Sink => write!(f, "sink"),
133 ObjectKind::Secret => write!(f, "secret"),
134 ObjectKind::Connection => write!(f, "connection"),
135 }
136 }
137}
138
139pub(crate) const BUILD_DIR: &str = "target";
141
142pub(crate) const LOCK_VERSION: u8 = 2;
149
150#[derive(Error, Debug)]
152pub enum TypesError {
153 #[error(transparent)]
154 BuildArtifactFailed(#[from] crate::project::compiler::cache::CacheError),
155
156 #[error("failed to read types.lock at {path}")]
157 FileReadFailed {
158 path: PathBuf,
159 #[source]
160 source: std::io::Error,
161 },
162 #[error("failed to write types.lock at {path}")]
163 FileWriteFailed {
164 path: PathBuf,
165 #[source]
166 source: std::io::Error,
167 },
168 #[error("failed to parse types.lock at {path}")]
169 ParseFailed {
170 path: PathBuf,
171 #[source]
172 source: toml::de::Error,
173 },
174 #[error(
175 "types.lock at {path} was written by a newer mz-deploy (format version {version}, this binary understands {supported}); upgrade mz-deploy"
176 )]
177 UnsupportedLockVersion {
178 path: PathBuf,
179 version: u8,
180 supported: u8,
181 },
182 #[error("types.lock at {path}: column `{column}` of `{object}` has an invalid type")]
183 InvalidColumnType {
184 path: PathBuf,
185 object: String,
186 column: String,
187 #[source]
188 source: TypeLockError,
189 },
190 #[error("failed to create directory {path}")]
191 DirectoryCreationFailed {
192 path: PathBuf,
193 #[source]
194 source: std::io::Error,
195 },
196 #[error(transparent)]
197 DependencyError(#[from] crate::project::error::DependencyError),
198}
199
200#[derive(Debug, Clone, PartialEq)]
202pub struct ColumnType {
203 pub r#type: DataType,
206 pub nullable: bool,
207 pub position: usize,
209 pub comment: Option<String>,
211}
212
213#[derive(Debug, Clone, PartialEq)]
220pub struct Types {
221 pub tables: BTreeMap<ObjectId, BTreeMap<String, ColumnType>>,
222 pub kinds: BTreeMap<ObjectId, ObjectKind>,
223 pub comments: BTreeMap<ObjectId, String>,
225}
226
227impl Default for Types {
228 fn default() -> Self {
229 Types {
230 tables: BTreeMap::new(),
231 kinds: BTreeMap::new(),
232 comments: BTreeMap::new(),
233 }
234 }
235}
236
237#[derive(Serialize, Deserialize)]
239struct TypesLock {
240 version: u8,
241 #[serde(default)]
242 table: Vec<ObjectLock>,
243 #[serde(default)]
244 view: Vec<ObjectLock>,
245 #[serde(default, rename = "materialized-view")]
246 materialized_view: Vec<ObjectLock>,
247 #[serde(default)]
248 source: Vec<ObjectLock>,
249 #[serde(default)]
250 sink: Vec<ObjectLock>,
251 #[serde(default)]
252 secret: Vec<ObjectLock>,
253 #[serde(default)]
254 connection: Vec<ObjectLock>,
255}
256
257impl Default for TypesLock {
258 fn default() -> Self {
259 Self {
260 version: LOCK_VERSION,
261 table: vec![],
262 view: vec![],
263 materialized_view: vec![],
264 source: vec![],
265 sink: vec![],
266 secret: vec![],
267 connection: vec![],
268 }
269 }
270}
271
272impl TypesLock {
273 fn into_objects(self) -> Vec<(ObjectKind, ObjectLock)> {
275 let kinds = [
276 (ObjectKind::Table, self.table),
277 (ObjectKind::View, self.view),
278 (ObjectKind::MaterializedView, self.materialized_view),
279 (ObjectKind::Source, self.source),
280 (ObjectKind::Sink, self.sink),
281 (ObjectKind::Secret, self.secret),
282 (ObjectKind::Connection, self.connection),
283 ];
284 kinds
285 .into_iter()
286 .flat_map(|(kind, objs)| objs.into_iter().map(move |obj| (kind, obj)))
287 .collect()
288 }
289
290 fn vec_for_kind(&mut self, kind: ObjectKind) -> &mut Vec<ObjectLock> {
292 match kind {
293 ObjectKind::Table => &mut self.table,
294 ObjectKind::View => &mut self.view,
295 ObjectKind::MaterializedView => &mut self.materialized_view,
296 ObjectKind::Source => &mut self.source,
297 ObjectKind::Sink => &mut self.sink,
298 ObjectKind::Secret => &mut self.secret,
299 ObjectKind::Connection => &mut self.connection,
300 }
301 }
302}
303
304#[derive(Serialize, Deserialize)]
305struct ObjectLock {
306 name: ObjectId,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
308 comment: Option<String>,
309 columns: Vec<ColumnLock>,
310}
311
312#[derive(Serialize, Deserialize)]
315struct ColumnLock {
316 name: String,
317 #[serde(rename = "type")]
318 type_name: String,
319 nullable: bool,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
321 comment: Option<String>,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
323 of: Option<Box<TypeLock>>,
324 #[serde(default, skip_serializing_if = "Vec::is_empty")]
325 fields: Vec<FieldLock>,
326}
327
328impl ColumnLock {
329 fn new(name: String, column: &ColumnType) -> Self {
330 let (type_name, of, fields) = data_type::split_data_type(&column.r#type);
331 ColumnLock {
332 name,
333 type_name,
334 nullable: column.nullable,
335 comment: column.comment.clone(),
336 of,
337 fields,
338 }
339 }
340
341 fn into_column_type(self, position: usize) -> Result<(String, ColumnType), TypeLockError> {
342 let r#type = data_type::join_data_type(self.type_name, self.of, self.fields)?;
343 Ok((
344 self.name,
345 ColumnType {
346 r#type,
347 nullable: self.nullable,
348 position,
349 comment: self.comment,
350 },
351 ))
352 }
353}
354
355impl From<&Types> for TypesLock {
356 fn from(types: &Types) -> Self {
357 let mut lock = TypesLock {
358 version: LOCK_VERSION,
359 table: Vec::new(),
360 view: Vec::new(),
361 materialized_view: Vec::new(),
362 source: Vec::new(),
363 sink: Vec::new(),
364 secret: Vec::new(),
365 connection: Vec::new(),
366 };
367
368 for (id, columns) in &types.tables {
369 let mut cols: Vec<_> = columns.iter().collect();
370 cols.sort_by_key(|(_, ct)| ct.position);
371 let cols: Vec<ColumnLock> = cols
372 .into_iter()
373 .map(|(col_name, col_type)| ColumnLock::new(col_name.clone(), col_type))
374 .collect();
375
376 let kind = types
377 .kinds
378 .get(id)
379 .unwrap_or_else(|| panic!("no kind for type {}", id.clone()));
380 let comment = types.comments.get(id).cloned();
381
382 let obj = ObjectLock {
383 name: id.clone(),
384 comment,
385 columns: cols,
386 };
387
388 lock.vec_for_kind(*kind).push(obj);
389 }
390
391 lock
392 }
393}
394
395impl TypesLock {
396 fn into_types(self, path: &Path) -> Result<Types, TypesError> {
399 let mut tables = BTreeMap::new();
400 let mut kinds = BTreeMap::new();
401 let mut comments = BTreeMap::new();
402 for (kind, obj) in self.into_objects() {
403 let id = obj.name;
404 let mut columns = BTreeMap::new();
405 for (position, col) in obj.columns.into_iter().enumerate() {
406 let object = id.to_string();
407 let column = col.name.clone();
408 let (name, column_type) = col.into_column_type(position).map_err(|source| {
409 TypesError::InvalidColumnType {
410 path: path.to_path_buf(),
411 object,
412 column,
413 source,
414 }
415 })?;
416 columns.insert(name, column_type);
417 }
418 kinds.insert(id.clone(), kind);
419 if let Some(comment) = obj.comment {
420 comments.insert(id.clone(), comment);
421 }
422 tables.insert(id, columns);
423 }
424
425 Ok(Types {
426 tables,
427 kinds,
428 comments,
429 })
430 }
431}
432
433fn escape_toml_string(s: &str) -> String {
435 let mut out = String::with_capacity(s.len());
436 for c in s.chars() {
437 match c {
438 '\\' => out.push_str("\\\\"),
439 '"' => out.push_str("\\\""),
440 '\n' => out.push_str("\\n"),
441 '\r' => out.push_str("\\r"),
442 '\t' => out.push_str("\\t"),
443 c if c.is_control() => {
444 out.push_str(&format!("\\u{:04X}", u32::from(c)));
445 }
446 c => out.push(c),
447 }
448 }
449 out
450}
451
452fn write_toml(lock: &TypesLock) -> String {
454 let mut out = String::new();
455 out.push_str("# This file is automatically @generated by mz-deploy.\n");
456 out.push_str("# It is not intended for manual editing.\n");
457 out.push_str(&format!("version = {}\n", lock.version));
458
459 let sections: &[(ObjectKind, &Vec<ObjectLock>)] = &[
460 (ObjectKind::Secret, &lock.secret),
461 (ObjectKind::Connection, &lock.connection),
462 (ObjectKind::Source, &lock.source),
463 (ObjectKind::Table, &lock.table),
464 (ObjectKind::View, &lock.view),
465 (ObjectKind::MaterializedView, &lock.materialized_view),
466 (ObjectKind::Sink, &lock.sink),
467 ];
468
469 for (kind, objs) in sections {
470 for obj in *objs {
471 out.push('\n');
472 out.push_str(&format!("[[{}]]\n", kind.as_str()));
473 out.push_str(&format!(
474 "name = \"{}\"\n",
475 escape_toml_string(&obj.name.to_string())
476 ));
477 if let Some(comment) = &obj.comment {
478 out.push_str(&format!("comment = \"{}\"\n", escape_toml_string(comment)));
479 }
480 out.push_str("columns = [\n");
481 for col in &obj.columns {
482 write_column(&mut out, col, COLUMN_INDENT);
483 }
484 out.push_str("]\n");
485 }
486 }
487
488 out
489}
490
491const COLUMN_INDENT: usize = 4;
493
494fn write_column(out: &mut String, col: &ColumnLock, indent: usize) {
500 let pad = " ".repeat(indent);
501 out.push_str(&pad);
502 out.push_str(&format!(
503 "{{ name = \"{}\", type = \"{}\", nullable = {}",
504 escape_toml_string(&col.name),
505 escape_toml_string(&col.type_name),
506 col.nullable,
507 ));
508 if let Some(comment) = &col.comment {
509 out.push_str(&format!(", comment = \"{}\"", escape_toml_string(comment)));
510 }
511 if let Some(of) = &col.of {
512 out.push_str(", of = ");
513 write_inline_type(out, of);
514 }
515 if !col.fields.is_empty() {
516 out.push_str(", fields = [\n");
517 for field in &col.fields {
518 write_field(out, field, indent + COLUMN_INDENT);
519 }
520 out.push_str(&pad);
521 out.push(']');
522 }
523 out.push_str(" },\n");
524}
525
526fn write_field(out: &mut String, field: &FieldLock, indent: usize) {
529 write_column(
530 out,
531 &ColumnLock {
532 name: field.name.clone(),
533 type_name: field.type_name.clone(),
534 nullable: field.nullable,
535 comment: None,
536 of: field.of.clone(),
537 fields: field.fields.clone(),
538 },
539 indent,
540 );
541}
542
543fn write_inline_type(out: &mut String, ty: &TypeLock) {
545 out.push_str(&format!("{{ type = \"{}\"", escape_toml_string(&ty.name)));
546 if let Some(of) = &ty.of {
547 out.push_str(", of = ");
548 write_inline_type(out, of);
549 }
550 if !ty.fields.is_empty() {
551 out.push_str(", fields = [");
552 for (i, field) in ty.fields.iter().enumerate() {
553 if i > 0 {
554 out.push_str(", ");
555 }
556 let mut buf = String::new();
557 write_field(&mut buf, field, 0);
558 out.push_str(buf.trim_end().trim_end_matches(','));
559 }
560 out.push(']');
561 }
562 out.push_str(" }");
563}
564
565pub(crate) fn load_types_lock(directory: &Path) -> Result<Types, TypesError> {
568 let path = directory.join("types.lock");
569
570 let contents = fs::read_to_string(&path).map_err(|source| TypesError::FileReadFailed {
571 path: path.clone(),
572 source,
573 })?;
574
575 let lock: TypesLock = toml::from_str(&contents).map_err(|source| TypesError::ParseFailed {
576 path: path.clone(),
577 source,
578 })?;
579 if lock.version > LOCK_VERSION {
580 return Err(TypesError::UnsupportedLockVersion {
581 path,
582 version: lock.version,
583 supported: LOCK_VERSION,
584 });
585 }
586 lock.into_types(&path)
587}
588
589impl Types {
590 pub fn write_types_lock(&self, directory: &Path) -> Result<(), TypesError> {
593 let path = directory.join("types.lock");
594
595 let lock = TypesLock::from(self);
596 let contents = write_toml(&lock);
597
598 fs::write(&path, contents).map_err(|source| TypesError::FileWriteFailed { path, source })
599 }
600
601 pub fn get_table(&self, id: &ObjectId) -> Option<&BTreeMap<String, ColumnType>> {
603 self.tables.get(id)
604 }
605
606 pub fn get_kind(&self, id: &ObjectId) -> ObjectKind {
611 self.kinds.get(id).copied().unwrap_or(ObjectKind::Table)
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618 use std::collections::BTreeMap;
619
620 #[mz_ore::test]
621 fn test_write_and_read_types_lock_round_trip() {
622 let mut tables = BTreeMap::new();
623
624 let mut order_cols = BTreeMap::new();
625 order_cols.insert(
626 "amount".to_string(),
627 ColumnType {
628 r#type: DataType::named("numeric"),
629 nullable: true,
630 position: 0,
631 comment: None,
632 },
633 );
634 order_cols.insert(
635 "id".to_string(),
636 ColumnType {
637 r#type: DataType::named("integer"),
638 nullable: false,
639 position: 1,
640 comment: None,
641 },
642 );
643 order_cols.insert(
644 "user_id".to_string(),
645 ColumnType {
646 r#type: DataType::named("integer"),
647 nullable: true,
648 position: 2,
649 comment: None,
650 },
651 );
652 tables.insert("app.ingest.orders".parse::<ObjectId>().unwrap(), order_cols);
653
654 let mut user_cols = BTreeMap::new();
655 user_cols.insert(
656 "name".to_string(),
657 ColumnType {
658 r#type: DataType::named("text"),
659 nullable: true,
660 position: 0,
661 comment: None,
662 },
663 );
664 user_cols.insert(
665 "user_id".to_string(),
666 ColumnType {
667 r#type: DataType::named("integer"),
668 nullable: false,
669 position: 1,
670 comment: None,
671 },
672 );
673 tables.insert("app.ingest.users".parse::<ObjectId>().unwrap(), user_cols);
674
675 let mut kinds = BTreeMap::new();
676 kinds.insert(
677 "app.ingest.orders".parse::<ObjectId>().unwrap(),
678 ObjectKind::Table,
679 );
680 kinds.insert(
681 "app.ingest.users".parse::<ObjectId>().unwrap(),
682 ObjectKind::Table,
683 );
684
685 let types = Types {
686 tables,
687 kinds,
688 comments: BTreeMap::new(),
689 };
690
691 let dir = tempfile::tempdir().expect("failed to create temp dir");
692 types
693 .write_types_lock(dir.path())
694 .expect("failed to write types.lock");
695
696 let loaded = load_types_lock(dir.path()).expect("failed to load types.lock");
697 assert_eq!(types, loaded);
698 }
699
700 #[mz_ore::test]
701 fn test_round_trip_with_kind() {
702 let mut tables = BTreeMap::new();
703 let mut cols = BTreeMap::new();
704 cols.insert(
705 "id".to_string(),
706 ColumnType {
707 r#type: DataType::named("integer"),
708 nullable: false,
709 position: 0,
710 comment: None,
711 },
712 );
713 tables.insert(
714 "app.ingest.orders".parse::<ObjectId>().unwrap(),
715 cols.clone(),
716 );
717 tables.insert(
718 "app.ingest.order_summary".parse::<ObjectId>().unwrap(),
719 cols,
720 );
721
722 let mut kinds = BTreeMap::new();
723 kinds.insert(
724 "app.ingest.orders".parse::<ObjectId>().unwrap(),
725 ObjectKind::Table,
726 );
727 kinds.insert(
728 "app.ingest.order_summary".parse::<ObjectId>().unwrap(),
729 ObjectKind::MaterializedView,
730 );
731
732 let types = Types {
733 tables,
734 kinds,
735 comments: BTreeMap::new(),
736 };
737
738 let dir = tempfile::tempdir().expect("failed to create temp dir");
739 types
740 .write_types_lock(dir.path())
741 .expect("failed to write types.lock");
742
743 let loaded = load_types_lock(dir.path()).expect("failed to load types.lock");
744 assert_eq!(types, loaded);
745 }
746
747 #[mz_ore::test]
748 fn test_round_trip_with_comments() {
749 let mut tables = BTreeMap::new();
750 let mut cols = BTreeMap::new();
751 cols.insert(
752 "id".to_string(),
753 ColumnType {
754 r#type: DataType::named("integer"),
755 nullable: false,
756 position: 0,
757 comment: Some("Primary key".to_string()),
758 },
759 );
760 cols.insert(
761 "name".to_string(),
762 ColumnType {
763 r#type: DataType::named("text"),
764 nullable: true,
765 position: 1,
766 comment: None,
767 },
768 );
769 tables.insert("app.ingest.orders".parse::<ObjectId>().unwrap(), cols);
770
771 let mut kinds = BTreeMap::new();
772 kinds.insert(
773 "app.ingest.orders".parse::<ObjectId>().unwrap(),
774 ObjectKind::Table,
775 );
776
777 let mut comments = BTreeMap::new();
778 comments.insert(
779 "app.ingest.orders".parse::<ObjectId>().unwrap(),
780 "All incoming customer orders".to_string(),
781 );
782
783 let types = Types {
784 tables,
785 kinds,
786 comments,
787 };
788
789 let dir = tempfile::tempdir().expect("failed to create temp dir");
790 types
791 .write_types_lock(dir.path())
792 .expect("failed to write types.lock");
793
794 let loaded = load_types_lock(dir.path()).expect("failed to load types.lock");
795 assert_eq!(types, loaded);
796 }
797
798 #[mz_ore::test]
799 fn test_backward_compat_no_comments() {
800 let toml = r#"
802version = 1
803
804[[table]]
805name = "app.ingest.orders"
806columns = [
807 { name = "id", type = "integer", nullable = false },
808]
809"#;
810 let dir = tempfile::tempdir().expect("failed to create temp dir");
811 fs::write(dir.path().join("types.lock"), toml).unwrap();
812
813 let loaded = load_types_lock(dir.path()).expect("should parse without comments");
814 assert_eq!(loaded.tables.len(), 1);
815 assert!(loaded.comments.is_empty());
816 let cols = loaded
817 .tables
818 .get(&"app.ingest.orders".parse::<ObjectId>().unwrap())
819 .unwrap();
820 assert!(cols.get("id").unwrap().comment.is_none());
821 }
822
823 #[mz_ore::test]
827 fn structured_types_round_trip_through_the_lock_file() {
828 let payload = DataType::Record(vec![
829 RecordField {
830 name: "a".into(),
831 r#type: DataType::named("integer"),
832 nullable: false,
833 },
834 RecordField {
835 name: "n".into(),
836 r#type: DataType::Record(vec![RecordField {
837 name: "x".into(),
838 r#type: DataType::List(Box::new(DataType::named("uint8"))),
839 nullable: true,
840 }]),
841 nullable: true,
842 },
843 ]);
844 let columns = BTreeMap::from([
845 (
846 "payload".to_string(),
847 ColumnType {
848 r#type: payload,
849 nullable: false,
850 position: 0,
851 comment: Some("nested".into()),
852 },
853 ),
854 (
855 "tags".to_string(),
856 ColumnType {
857 r#type: DataType::List(Box::new(DataType::named("text"))),
858 nullable: true,
859 position: 1,
860 comment: None,
861 },
862 ),
863 (
864 "grid".to_string(),
865 ColumnType {
866 r#type: DataType::Array(Box::new(DataType::Map(Box::new(DataType::named(
867 "int4",
868 ))))),
869 nullable: true,
870 position: 2,
871 comment: None,
872 },
873 ),
874 ]);
875
876 let id: ObjectId = "app.public.events".parse().unwrap();
877 let types = Types {
878 tables: BTreeMap::from([(id.clone(), columns)]),
879 kinds: BTreeMap::from([(id, ObjectKind::Table)]),
880 comments: BTreeMap::new(),
881 };
882
883 let dir = tempfile::tempdir().expect("failed to create temp dir");
884 types
885 .write_types_lock(dir.path())
886 .expect("failed to write types.lock");
887 let loaded = load_types_lock(dir.path()).expect("failed to load types.lock");
888 assert_eq!(types, loaded);
889 }
890
891 #[mz_ore::test]
894 fn version_1_lock_file_loads() {
895 let dir = tempfile::tempdir().expect("failed to create temp dir");
896 fs::write(
897 dir.path().join("types.lock"),
898 "version = 1\n\n[[table]]\nname = \"app.public.events\"\ncolumns = [\n \
899 { name = \"id\", type = \"integer\", nullable = true },\n]\n",
900 )
901 .unwrap();
902
903 let loaded = load_types_lock(dir.path()).expect("a version 1 file still loads");
904 let events = &loaded.tables[&"app.public.events".parse::<ObjectId>().unwrap()];
905 assert_eq!(events["id"].r#type, DataType::named("integer"));
906 }
907
908 #[mz_ore::test]
909 fn newer_lock_file_is_refused() {
910 let dir = tempfile::tempdir().expect("failed to create temp dir");
911 fs::write(
912 dir.path().join("types.lock"),
913 format!("version = {}\n", LOCK_VERSION + 1),
914 )
915 .unwrap();
916
917 let err = load_types_lock(dir.path()).expect_err("a newer format is refused");
918 assert!(
919 err.to_string().contains("upgrade mz-deploy"),
920 "unexpected error: {err}"
921 );
922 }
923
924 #[mz_ore::test]
929 fn pseudo_type_tokens_round_trip() {
930 let columns: BTreeMap<String, ColumnType> = ["record", "list", "map"]
931 .into_iter()
932 .enumerate()
933 .map(|(position, token)| {
934 (
935 token.to_string(),
936 ColumnType {
937 r#type: DataType::named(token),
938 nullable: true,
939 position,
940 comment: None,
941 },
942 )
943 })
944 .collect();
945
946 let id: ObjectId = "app.public.wide".parse().unwrap();
947 let types = Types {
948 tables: BTreeMap::from([(id.clone(), columns)]),
949 kinds: BTreeMap::from([(id, ObjectKind::View)]),
950 comments: BTreeMap::new(),
951 };
952
953 let dir = tempfile::tempdir().expect("failed to create temp dir");
954 types
955 .write_types_lock(dir.path())
956 .expect("failed to write types.lock");
957 let loaded = load_types_lock(dir.path()).expect("a pseudo token must load back");
958 assert_eq!(types, loaded);
959 }
960
961 #[mz_ore::test]
964 fn version_1_pseudo_type_tokens_load() {
965 let dir = tempfile::tempdir().expect("failed to create temp dir");
966 fs::write(
967 dir.path().join("types.lock"),
968 "version = 1\n\n[[table]]\nname = \"app.public.events\"\ncolumns = [\n \
969 { name = \"payload\", type = \"record\", nullable = true },\n \
970 { name = \"tags\", type = \"list\", nullable = true },\n]\n",
971 )
972 .unwrap();
973
974 let loaded = load_types_lock(dir.path()).expect("a version 1 file still loads");
975 let events = &loaded.tables[&"app.public.events".parse::<ObjectId>().unwrap()];
976 assert_eq!(events["payload"].r#type, DataType::named("record"));
977 assert_eq!(events["tags"].r#type, DataType::named("list"));
978 }
979
980 #[mz_ore::test]
981 fn structural_payload_on_the_wrong_type_is_rejected() {
982 let dir = tempfile::tempdir().expect("failed to create temp dir");
983 fs::write(
984 dir.path().join("types.lock"),
985 "version = 2\n\n[[table]]\nname = \"app.public.events\"\ncolumns = [\n \
986 { name = \"id\", type = \"integer\", nullable = true, fields = [\n \
987 { name = \"a\", type = \"int4\", nullable = true },\n ] },\n]\n",
988 )
989 .unwrap();
990
991 let err = load_types_lock(dir.path()).expect_err("fields on a scalar is rejected");
992 let message = err.to_string();
993 assert!(
994 message.contains("events") && message.contains("id"),
995 "error should name the object and column: {message}"
996 );
997 }
998}