Skip to content

Commit 98faf7d

Browse files
authored
Add backwards-compatible validation for RawModuleDefV8. (#1606)
1 parent bba3572 commit 98faf7d

File tree

12 files changed

+1256
-81
lines changed

12 files changed

+1256
-81
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/data-structures/src/error_stream.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
//! and `CollectAllErrors::collect_all_errors` for collecting errors from iterators.
8686
8787
use crate::map::HashSet;
88-
use std::hash::Hash;
88+
use std::{fmt, hash::Hash};
8989

9090
/// A non-empty stream of errors.
9191
///
@@ -115,6 +115,41 @@ use std::hash::Hash;
115115
pub struct ErrorStream<E>(smallvec::SmallVec<[E; 1]>);
116116

117117
impl<E> ErrorStream<E> {
118+
/// Build an error stream from a non-empty collection.
119+
/// If the collection is empty, panic.
120+
pub fn expect_nonempty<I: IntoIterator<Item = E>>(errors: I) -> Self {
121+
let mut errors = errors.into_iter();
122+
let first = errors.next().expect("expected at least one error");
123+
let mut stream = Self::from(first);
124+
stream.extend(errors);
125+
stream
126+
}
127+
128+
/// Add some extra errors to a result.
129+
///
130+
/// If there are no errors, the result is not modified.
131+
/// If there are errors, and the result is `Err`, the errors are added to the stream.
132+
/// If there are errors, and the result is `Ok`, the `Ok` value is discarded, and the errors are returned in a stream.
133+
pub fn add_extra_errors<T>(
134+
result: Result<T, ErrorStream<E>>,
135+
extra_errors: impl IntoIterator<Item = E>,
136+
) -> Result<T, ErrorStream<E>> {
137+
match result {
138+
Ok(value) => {
139+
let errors: SmallVec<[E; 1]> = extra_errors.into_iter().collect();
140+
if errors.is_empty() {
141+
Ok(value)
142+
} else {
143+
Err(ErrorStream(errors))
144+
}
145+
}
146+
Err(mut errors) => {
147+
errors.extend(extra_errors);
148+
Err(errors)
149+
}
150+
}
151+
}
152+
118153
/// Returns an iterator over the errors in the stream.
119154
pub fn iter(&self) -> impl Iterator<Item = &E> {
120155
self.0.iter()
@@ -154,6 +189,15 @@ impl<E> ErrorStream<E> {
154189
}
155190
}
156191
}
192+
impl<E: fmt::Display> fmt::Display for ErrorStream<E> {
193+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194+
writeln!(f, "Errors occurred:")?;
195+
for error in self.iter() {
196+
writeln!(f, "{}\n", error)?;
197+
}
198+
Ok(())
199+
}
200+
}
157201

158202
impl<E: Ord + Eq> ErrorStream<E> {
159203
/// Sort and deduplicate the errors in the error stream.
@@ -454,6 +498,7 @@ macro_rules! expect_error_matching (
454498
);
455499
// Make available in this module as well as crate root.
456500
pub use expect_error_matching;
501+
use smallvec::SmallVec;
457502

458503
#[cfg(test)]
459504
mod tests {

crates/lib/src/db/raw_def/v8.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ use spacetimedb_data_structures::map::HashSet;
1010
use spacetimedb_primitives::*;
1111
use spacetimedb_sats::{de, ser};
1212

13-
// TODO(1.0): move this definition into this file,
13+
// TODO(1.0): move these definitions into this file,
1414
// along with the other structs contained in it,
1515
// which are currently in the crate root.
16+
pub use crate::ModuleDefBuilder as RawModuleDefV8Builder;
1617
pub use crate::RawModuleDefV8;
1718

1819
/// The amount sequences allocate each time they over-run their allocation.
@@ -343,6 +344,11 @@ impl RawTableDefV8 {
343344
}
344345
}
345346

347+
#[cfg(feature = "test")]
348+
pub fn new_for_tests(table_name: impl Into<Box<str>>, columns: ProductType) -> Self {
349+
Self::new(table_name.into(), RawColumnDefV8::from_product_type(columns))
350+
}
351+
346352
/// Set the type of the table and return a new `TableDef` instance with the updated type.
347353
pub fn with_type(self, table_type: StTableType) -> Self {
348354
let mut x = self;

crates/lib/src/db/raw_def/v9.rs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,21 @@ impl RawModuleDefV9Builder {
493493
}
494494
}
495495

496+
/// Convert a string from a sats type-name annotation like `#[sats(name = "namespace.name")]` to a `RawScopedTypeNameV9`.
497+
/// We split the input on the strings `"::"` and `"."` to split up module paths.
498+
///
499+
/// TODO(1.0): build namespacing directly into the bindings macros so that we don't need to do this.
500+
pub fn sats_name_to_scoped_name(sats_name: &str) -> RawScopedTypeNameV9 {
501+
// We can't use `&[char]: Pattern` for `split` here because "::" is not a char :/
502+
let mut scope: Vec<RawIdentifier> = sats_name.split("::").flat_map(|s| s.split('.')).map_into().collect();
503+
// Unwrapping to "" will result in a validation error down the line, which is exactly what we want.
504+
let name = scope.pop().unwrap_or_default();
505+
RawScopedTypeNameV9 {
506+
scope: scope.into(),
507+
name,
508+
}
509+
}
510+
496511
impl TypespaceBuilder for RawModuleDefV9Builder {
497512
fn add(
498513
&mut self,
@@ -509,19 +524,11 @@ impl TypespaceBuilder for RawModuleDefV9Builder {
509524
v.insert(slot_ref);
510525

511526
// Alias provided? Relate `name -> slot_ref`.
512-
if let Some(name) = name {
513-
// Right now, we just split the name on patterns likely to split up module paths.
514-
// TODO(1.0): build namespacing directly into the bindings macros so that we don't need to do this.
515-
// Note that we can't use `&[char]: Pattern` for `split` here because "::" is not a char :/
516-
let mut scope: Vec<RawIdentifier> =
517-
name.split("::").flat_map(|s| s.split('.')).map_into().collect();
518-
let name = scope.pop().expect("empty name forbidden");
527+
if let Some(sats_name) = name {
528+
let name = sats_name_to_scoped_name(sats_name);
519529

520530
self.module.types.push(RawTypeDefV9 {
521-
name: RawScopedTypeNameV9 {
522-
name,
523-
scope: scope.into(),
524-
},
531+
name,
525532
ty: slot_ref,
526533
// TODO(1.0): we need to update the `TypespaceBuilder` trait to include
527534
// a `custom_ordering` parameter.
@@ -622,8 +629,9 @@ impl<'a> RawTableDefBuilder<'a> {
622629
self
623630
}
624631

625-
/// Build the table and add it to the module.
626-
pub fn finish(self) {
632+
/// Build the table and add it to the module, returning the `product_type_ref` of the table.
633+
pub fn finish(self) -> AlgebraicTypeRef {
634+
self.table.product_type_ref
627635
// self is now dropped.
628636
}
629637

crates/lib/src/lib.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,38 @@ impl ModuleDefBuilder {
222222
TypespaceBuilder::add_type::<T>(self)
223223
}
224224

225+
/// Add a type that may not correspond to a Rust type.
226+
/// Used only in tests.
227+
#[cfg(feature = "test")]
228+
pub fn add_type_for_tests(&mut self, name: &str, ty: AlgebraicType) -> spacetimedb_sats::AlgebraicTypeRef {
229+
let slot_ref = self.module.typespace.add(ty);
230+
self.module.misc_exports.push(MiscModuleExport::TypeAlias(TypeAlias {
231+
name: name.to_owned(),
232+
ty: slot_ref,
233+
}));
234+
slot_ref
235+
}
236+
237+
/// Add a table that may not correspond to a Rust type.
238+
/// Wraps it in a `TableDesc` and generates a corresponding `ProductType` in the typespace.
239+
/// Used only in tests.
240+
/// Returns the `AlgebraicTypeRef` of the generated `ProductType`.
241+
#[cfg(feature = "test")]
242+
pub fn add_table_for_tests(&mut self, schema: RawTableDefV8) -> spacetimedb_sats::AlgebraicTypeRef {
243+
let ty: ProductType = schema
244+
.columns
245+
.iter()
246+
.map(|c| ProductTypeElement {
247+
name: Some(c.col_name.clone()),
248+
algebraic_type: c.col_type.clone(),
249+
})
250+
.collect();
251+
// do NOT add a `TypeAlias`: in v8, the `RawTableDef` itself serves as a `TypeAlias`.
252+
let data = self.module.typespace.add(ty.into());
253+
self.add_table(TableDesc { schema, data });
254+
data
255+
}
256+
225257
pub fn add_table(&mut self, table: TableDesc) {
226258
self.module.tables.push(table)
227259
}
@@ -230,6 +262,14 @@ impl ModuleDefBuilder {
230262
self.module.reducers.push(reducer)
231263
}
232264

265+
#[cfg(feature = "test")]
266+
pub fn add_reducer_for_tests(&mut self, name: impl Into<Box<str>>, args: ProductType) {
267+
self.add_reducer(ReducerDef {
268+
name: name.into(),
269+
args: args.elements.to_vec(),
270+
});
271+
}
272+
233273
pub fn add_misc_export(&mut self, misc_export: MiscModuleExport) {
234274
self.module.misc_exports.push(misc_export)
235275
}

crates/schema/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ smallvec.workspace = true
2121

2222
[dev-dependencies]
2323
spacetimedb-lib = { workspace = true, features = ["test"] }
24+
# these are circular dependencies, but only in tests, so it's fine
25+
spacetimedb-testing = { path = "../testing" }
26+
spacetimedb-cli.workspace = true
27+
2428
proptest.workspace = true

crates/schema/src/def.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,17 @@ pub struct ModuleDef {
9090
}
9191

9292
impl ModuleDef {
93+
/// Construct a `ModuleDef` by validating a `RawModuleDef`.
94+
/// This is the only way to construct a `ModuleDef`.
95+
/// (The `TryFrom` impls for this type just call this method.)
96+
pub fn validate(raw: RawModuleDef) -> Result<Self, ValidationErrors> {
97+
match raw {
98+
RawModuleDef::V8BackCompat(v8_mod) => validate::v8::validate(v8_mod),
99+
RawModuleDef::V9(v9_mod) => validate::v9::validate(v9_mod),
100+
_ => unimplemented!(),
101+
}
102+
}
103+
93104
/// The tables of the module definition.
94105
pub fn tables(&self) -> impl Iterator<Item = &TableDef> {
95106
self.tables.values()
@@ -128,7 +139,7 @@ impl ModuleDef {
128139
/// Generate indexes for the module definition.
129140
/// We guarantee that all `unique` constraints have an index generated for them.
130141
/// This will be removed once another enforcement mechanism is implemented.
131-
/// We implement this after validation to share logic between v8 and v9.
142+
/// This is a noop if there are already usable indexes present.
132143
fn generate_indexes(&mut self) {
133144
for table in self.tables.values_mut() {
134145
for constraint in table.unique_constraints.values() {

crates/schema/src/def/validate.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ pub mod tests {
1414
use spacetimedb_lib::{db::raw_def::v9::RawScopedTypeNameV9, AlgebraicType};
1515
use spacetimedb_sats::{Typespace, WithTypespace};
1616

17-
use crate::{def::ScopedTypeName, identifier::Identifier};
17+
use crate::{
18+
def::{ModuleDef, ScopedTypeName, TableDef},
19+
identifier::Identifier,
20+
};
1821

1922
/// Create an identifier, panicking if invalid.
2023
pub fn expect_identifier(data: impl Into<Box<str>>) -> Identifier {
@@ -53,6 +56,22 @@ pub mod tests {
5356
.expect("failed to resolve type")
5457
}
5558

59+
/// Check that the columns of a `TableDef` correctly correspond the the `TableDef`'s
60+
/// `product_type_ref`.
61+
pub fn check_product_type(module_def: &ModuleDef, table_def: &TableDef) {
62+
let product_type = module_def
63+
.typespace
64+
.get(table_def.product_type_ref)
65+
.unwrap()
66+
.as_product()
67+
.unwrap();
68+
69+
for (element, column) in product_type.elements.iter().zip(table_def.columns.iter()) {
70+
assert_eq!(Some(&*column.name), element.name());
71+
assert_eq!(column.ty, element.algebraic_type);
72+
}
73+
}
74+
5675
#[test]
5776
fn test_expect_type_name() {
5877
assert_eq!(

0 commit comments

Comments
 (0)