diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index d518f2ac5ef..9892500ad0c 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -153,7 +153,7 @@ pub struct ModuleDef { raw_module_def_version: RawModuleDefVersion, /// Mounted submodules, keyed by the namespace they are mounted under. - mounts: Vec<(String, ModuleDef)>, + mounts: IndexMap, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -171,7 +171,7 @@ impl ModuleDef { } /// The mounted submodules of the module definition. - pub fn mounts(&self) -> &[(String, ModuleDef)] { + pub fn mounts(&self) -> &IndexMap { &self.mounts } diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index 7f2b4b99238..a949b93d224 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -271,8 +271,8 @@ pub fn validate(def: RawModuleDefV10) -> Result { let (tables, types, reducers, procedures, views, mounts) = (tables_types_reducers_procedures_views, mounts) .combine_errors() - .map(|((tables, types, reducers, procedures, views), mounts)| { - (tables, types, reducers, procedures, views, mounts) + .and_then(|((tables, types, reducers, procedures, views), mounts)| { + validate_mount_names_are_unique(mounts).map(|mounts| (tables, types, reducers, procedures, views, mounts)) }) .map_err(|errors: ValidationErrors| errors.sort_deduplicate())?; @@ -302,6 +302,21 @@ fn validate_mount(mount: RawModuleMountV10) -> Result<(String, ModuleDef)> { Ok((mount.namespace, validate(mount.module)?)) } +fn validate_mount_names_are_unique(mounts: Vec<(String, ModuleDef)>) -> Result> { + let mut errors = vec![]; + let mut map = IndexMap::with_capacity(mounts.len()); + + for (namespace, def) in mounts { + if map.contains_key(&namespace) { + errors.push(ValidationError::DuplicateName { name: namespace.into() }); + } else { + map.insert(namespace, def); + } + } + + ValidationErrors::add_extra_errors(Ok(map), errors) +} + /// Change the visibility of scheduled functions and lifecycle reducers to Internal. /// fn change_scheduled_functions_and_lifetimes_visibility( @@ -907,6 +922,7 @@ mod tests { use spacetimedb_lib::db::raw_def::*; use spacetimedb_lib::ScheduleAt; use spacetimedb_primitives::{ColId, ColList, ColSet}; + use spacetimedb_sats::raw_identifier::RawIdentifier; use spacetimedb_sats::{AlgebraicType, AlgebraicTypeRef, AlgebraicValue, ProductType, SumValue}; use v9::{Lifecycle, TableAccess, TableType}; @@ -1299,8 +1315,8 @@ mod tests { let mounts = def.mounts(); assert_eq!(mounts.len(), 1); - assert_eq!(mounts[0].0, "authlib"); - assert!(mounts[0].1.table(&expect_identifier("sessions")).is_some()); + let mounted = mounts.get("authlib").expect("authlib mount should exist"); + assert!(mounted.table(&expect_identifier("sessions")).is_some()); } #[test] @@ -1319,6 +1335,28 @@ mod tests { }); } + #[test] + fn duplicate_mount_namespace() { + let raw = RawModuleDefV10 { + sections: vec![RawModuleDefV10Section::Mounts(vec![ + RawModuleMountV10 { + namespace: "authlib".to_string(), + module: RawModuleDefV10::default(), + }, + RawModuleMountV10 { + namespace: "authlib".to_string(), + module: RawModuleDefV10::default(), + }, + ])], + }; + + let result: Result = raw.try_into(); + + expect_error_matching!(result, ValidationError::DuplicateName { name } => { + name == &RawIdentifier::from("authlib") + }); + } + #[test] fn invalid_unique_constraint_column_ref() { let mut builder = RawModuleDefV10Builder::new(); diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index 289b4a991f5..25b10b8d77d 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -166,7 +166,7 @@ pub fn validate(def: RawModuleDefV9) -> Result { lifecycle_reducers, procedures, raw_module_def_version: RawModuleDefVersion::V9OrEarlier, - mounts: Vec::new(), + mounts: IndexMap::new(), }) }