diff --git a/crates/proto/src/schema.rs b/crates/proto/src/schema.rs index 6ba2f2b9..6b0dd175 100644 --- a/crates/proto/src/schema.rs +++ b/crates/proto/src/schema.rs @@ -331,3 +331,114 @@ impl TryFrom 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"); + } + } +} diff --git a/crates/sqlite/sqlite/src/executor/mod.rs b/crates/sqlite/sqlite/src/executor/mod.rs index c826b9e4..e8f593f8 100644 --- a/crates/sqlite/sqlite/src/executor/mod.rs +++ b/crates/sqlite/sqlite/src/executor/mod.rs @@ -571,6 +571,13 @@ impl 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."); } diff --git a/crates/sqlite/types/src/lib.rs b/crates/sqlite/types/src/lib.rs index 1b025eca..a5ee3686 100644 --- a/crates/sqlite/types/src/lib.rs +++ b/crates/sqlite/types/src/lib.rs @@ -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 = 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"); + } + } }