Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions crates/proto/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,3 +331,114 @@ impl TryFrom<proto::types::Ty> for Ty {
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use dojo_types::schema::{Member, Struct};

#[test]
fn test_nested_struct_proto_conversion_preserves_values() {
// Create a nested struct with primitive values
let nested_struct = Struct {
name: "TroopGuards".to_string(),
children: vec![
Member {
name: "knight_count".to_string(),
ty: Ty::Primitive(Primitive::U32(Some(100))),
key: false,
},
Member {
name: "crossbowman_count".to_string(),
ty: Ty::Primitive(Primitive::U32(Some(50))),
key: false,
},
],
};

let main_struct = Struct {
name: "Game-Structure".to_string(),
children: vec![Member {
name: "troop_guards".to_string(),
ty: Ty::Struct(nested_struct),
key: false,
}],
};

// Convert to proto
let proto_struct: proto::types::Struct = main_struct.into();

// Verify main struct
assert_eq!(proto_struct.name, "Game-Structure");
assert_eq!(proto_struct.children.len(), 1);

// Verify nested struct
let troop_guards = &proto_struct.children[0];
assert_eq!(troop_guards.name, "troop_guards");
assert!(!troop_guards.key);

let nested_ty = troop_guards.ty.as_ref().expect("ty should be present");
if let Some(proto::types::ty::TyType::Struct(nested)) = &nested_ty.ty_type {
assert_eq!(nested.name, "TroopGuards");
assert_eq!(nested.children.len(), 2);

// Verify knight_count
let knight_count = &nested.children[0];
assert_eq!(knight_count.name, "knight_count");
let knight_ty = knight_count.ty.as_ref().expect("ty should be present");
if let Some(proto::types::ty::TyType::Primitive(prim)) = &knight_ty.ty_type {
if let Some(proto::types::primitive::PrimitiveType::U32(val)) = prim.primitive_type
{
assert_eq!(val, 100, "knight_count should be 100, got {}", val);
} else {
panic!("knight_count should be U32");
}
} else {
panic!("knight_count should be Primitive");
}

// Verify crossbowman_count
let crossbowman_count = &nested.children[1];
assert_eq!(crossbowman_count.name, "crossbowman_count");
let crossbowman_ty = crossbowman_count.ty.as_ref().expect("ty should be present");
if let Some(proto::types::ty::TyType::Primitive(prim)) = &crossbowman_ty.ty_type {
if let Some(proto::types::primitive::PrimitiveType::U32(val)) = prim.primitive_type
{
assert_eq!(val, 50, "crossbowman_count should be 50, got {}", val);
} else {
panic!("crossbowman_count should be U32");
}
} else {
panic!("crossbowman_count should be Primitive");
}
} else {
panic!("troop_guards ty should be Struct");
}
}

#[test]
fn test_primitive_none_becomes_zero_in_proto() {
// Test that None primitive values become zero after conversion
let prim = Primitive::U32(None);
let proto_prim: proto::types::Primitive = prim.into();

if let Some(proto::types::primitive::PrimitiveType::U32(val)) = proto_prim.primitive_type {
assert_eq!(val, 0, "None U32 should become 0 in proto");
} else {
panic!("Should be U32");
}
}

#[test]
fn test_primitive_some_value_preserved_in_proto() {
// Test that Some(value) is preserved after conversion
let prim = Primitive::U32(Some(42));
let proto_prim: proto::types::Primitive = prim.into();

if let Some(proto::types::primitive::PrimitiveType::U32(val)) = proto_prim.primitive_type {
assert_eq!(val, 42, "Some(42) should become 42 in proto");
} else {
panic!("Should be U32");
}
}
}
7 changes: 7 additions & 0 deletions crates/sqlite/sqlite/src/executor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,13 @@ impl<P: Provider + Sync + Send + Clone + 'static> Executor<'_, P> {
let mut entity_updated = torii_sqlite_types::Entity::from_row(&row)?;
entity_updated.updated_model = Some(entity.ty.clone());

// Load full entity from DB for subscription matching
// This ensures MemberClause filters work with partial updates (write_member)
let full_model =
Self::entity_model(tx, entity.model_id.clone(), entity.entity_id.clone())
.await?;
entity_updated.match_model = full_model;

if entity_updated.keys.is_empty() {
warn!(target: LOG_TARGET, "Entity has been updated without being set before. Keys are not known and non-updated values will be NULL.");
}
Expand Down
135 changes: 135 additions & 0 deletions crates/sqlite/types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -784,4 +784,139 @@ mod tests {
let propagated_model = entity_with_metadata.match_model.unwrap();
assert_eq!(propagated_model.name(), "Game-Player");
}

#[test]
fn test_entity_conversion_preserves_nested_struct_values() {
use dojo_types::schema::Member;

let now = Utc::now();

// Create a nested struct similar to TroopGuards
let nested_struct = Ty::Struct(Struct {
name: "TroopGuards".to_string(),
children: vec![
Member {
name: "knight_count".to_string(),
ty: Ty::Primitive(dojo_types::primitive::Primitive::U32(Some(100))),
key: false,
},
Member {
name: "crossbowman_count".to_string(),
ty: Ty::Primitive(dojo_types::primitive::Primitive::U32(Some(50))),
key: false,
},
Member {
name: "paladin_count".to_string(),
ty: Ty::Primitive(dojo_types::primitive::Primitive::U32(Some(25))),
key: false,
},
],
});

// Create the main model with nested struct
let entity = Entity {
id: "0xworld:0xentity".to_string(),
entity_id: "0x123".to_string(),
world_address: "0xabc".to_string(),
keys: "0x1/0x2".to_string(),
event_id: "event_123".to_string(),
executed_at: now,
created_at: now,
updated_at: now,
updated_model: Some(Ty::Struct(Struct {
name: "Game-Structure".to_string(),
children: vec![
Member {
name: "base".to_string(),
ty: Ty::Struct(Struct {
name: "Position".to_string(),
children: vec![
Member {
name: "coord_x".to_string(),
ty: Ty::Primitive(dojo_types::primitive::Primitive::U32(Some(
10,
))),
key: false,
},
Member {
name: "coord_y".to_string(),
ty: Ty::Primitive(dojo_types::primitive::Primitive::U32(Some(
20,
))),
key: false,
},
],
}),
key: false,
},
Member {
name: "troop_guards".to_string(),
ty: nested_struct,
key: false,
},
],
})),
deleted: false,
match_model: None,
};

// Convert to proto Entity
let proto_entity: torii_proto::schema::Entity<false> = entity.into();

// Verify structure
assert_eq!(proto_entity.models.len(), 1);
let model = &proto_entity.models[0];
assert_eq!(model.name, "Game-Structure");
assert_eq!(model.children.len(), 2);

// Find the troop_guards member
let troop_guards = model
.children
.iter()
.find(|c| c.name == "troop_guards")
.expect("troop_guards member should exist");

// Verify nested struct values are preserved
if let Ty::Struct(nested) = &troop_guards.ty {
assert_eq!(nested.name, "TroopGuards");
assert_eq!(nested.children.len(), 3);

// Verify each primitive value
let knight_count = nested
.children
.iter()
.find(|c| c.name == "knight_count")
.expect("knight_count should exist");
if let Ty::Primitive(dojo_types::primitive::Primitive::U32(val)) = &knight_count.ty {
assert_eq!(*val, Some(100), "knight_count should be 100, not zero");
} else {
panic!("knight_count should be U32 primitive");
}

let crossbowman_count = nested
.children
.iter()
.find(|c| c.name == "crossbowman_count")
.expect("crossbowman_count should exist");
if let Ty::Primitive(dojo_types::primitive::Primitive::U32(val)) = &crossbowman_count.ty
{
assert_eq!(*val, Some(50), "crossbowman_count should be 50, not zero");
} else {
panic!("crossbowman_count should be U32 primitive");
}

let paladin_count = nested
.children
.iter()
.find(|c| c.name == "paladin_count")
.expect("paladin_count should exist");
if let Ty::Primitive(dojo_types::primitive::Primitive::U32(val)) = &paladin_count.ty {
assert_eq!(*val, Some(25), "paladin_count should be 25, not zero");
} else {
panic!("paladin_count should be U32 primitive");
}
} else {
panic!("troop_guards should be a Struct type");
}
}
}
Loading