Skip to content

Commit ddae041

Browse files
fix(grpc): require updated model for deletions
1 parent 4bae176 commit ddae041

5 files changed

Lines changed: 142 additions & 8 deletions

File tree

crates/grpc/server/src/subscriptions/entity.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ impl Service {
115115
// Process updates synchronously - no async overhead
116116
fn process_entity_update(subs: &EntityManager, entity: &EntityWithMetadata) {
117117
let mut closed_stream = Vec::new();
118+
let updated_model = entity
119+
.updated_model
120+
.clone()
121+
.or_else(|| entity.entity.models.first().map(|m| Ty::Struct(m.clone())));
118122

119123
for sub in subs.subscribers.iter() {
120124
let idx = sub.key();
@@ -131,7 +135,7 @@ impl Service {
131135
if !match_entity(
132136
entity.entity.hashed_keys,
133137
&entity.keys,
134-
&entity.entity.models.first().map(|m| Ty::Struct(m.clone())),
138+
&updated_model,
135139
clause,
136140
) {
137141
continue;

crates/grpc/server/src/subscriptions/event_message.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ impl Service {
118118
// Process updates synchronously - no async overhead
119119
fn process_event_message(subs: &EventMessageManager, event: &EntityWithMetadata<true>) {
120120
let mut closed_stream = Vec::new();
121+
let updated_model = event
122+
.updated_model
123+
.clone()
124+
.or_else(|| event.entity.models.first().map(|m| Ty::Struct(m.clone())));
121125

122126
for sub in subs.subscribers.iter() {
123127
let idx = sub.key();
@@ -134,7 +138,7 @@ impl Service {
134138
if !match_entity(
135139
event.entity.hashed_keys,
136140
&event.keys,
137-
&event.entity.models.first().map(|m| Ty::Struct(m.clone())),
141+
&updated_model,
138142
clause,
139143
) {
140144
continue;

crates/grpc/server/src/tests/entities_test.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,6 +1402,66 @@ async fn test_historical_query(sequencer: &RunnerCtx) {
14021402
);
14031403
}
14041404

1405+
#[tokio::test(flavor = "multi_thread")]
1406+
async fn test_entity_broker_delete_clause_matches_updated_model() {
1407+
use crate::subscriptions::entity::{EntityManager, Service};
1408+
use chrono::Utc;
1409+
use dojo_types::schema::{Struct, Ty};
1410+
use std::time::Duration;
1411+
use tokio::time::timeout;
1412+
use torii_broker::{types::EntityUpdate, MemoryBroker};
1413+
use torii_proto::schema::{Entity, EntityWithMetadata};
1414+
1415+
let config = GrpcConfig::default();
1416+
let entity_manager = Arc::new(EntityManager::new(config));
1417+
1418+
let service = Service::new(entity_manager.clone());
1419+
tokio::spawn(service);
1420+
1421+
let clause = Clause::Keys(KeysClause {
1422+
keys: vec![],
1423+
pattern_matching: PatternMatching::FixedLen,
1424+
models: vec!["ns-Model".to_string()],
1425+
});
1426+
let mut receiver = entity_manager.add_subscriber(Some(clause), vec![]).await;
1427+
1428+
// Skip the initial empty response
1429+
if let Ok(Some(response)) = timeout(Duration::from_secs(1), receiver.recv()).await {
1430+
match response {
1431+
Ok(resp) => assert!(resp.entity.is_none()),
1432+
Err(e) => panic!("Subscriber received error: {:?}", e),
1433+
}
1434+
}
1435+
1436+
let now = Utc::now();
1437+
let entity = Entity {
1438+
hashed_keys: Felt::from(1_u64),
1439+
world_address: Felt::ZERO,
1440+
models: vec![], // Simulate deletion payload where models are empty.
1441+
created_at: now,
1442+
updated_at: now,
1443+
executed_at: now,
1444+
};
1445+
let updated_model = Ty::Struct(Struct {
1446+
name: "ns-Model".to_string(),
1447+
children: vec![],
1448+
});
1449+
let entity_with_metadata = EntityWithMetadata {
1450+
entity,
1451+
event_id: "delete_event".to_string(),
1452+
keys: vec![],
1453+
updated_model: Some(updated_model),
1454+
};
1455+
1456+
MemoryBroker::publish(EntityUpdate::new(entity_with_metadata, false));
1457+
1458+
let response = timeout(Duration::from_secs(2), receiver.recv())
1459+
.await
1460+
.expect("timed out waiting for broker update");
1461+
let response = response.expect("receiver closed").expect("subscriber error");
1462+
assert!(response.entity.is_some());
1463+
}
1464+
14051465
#[tokio::test(flavor = "multi_thread")]
14061466
async fn test_entity_broker_multiple_subscriptions() {
14071467
use crate::subscriptions::entity::{EntityManager, Service};
@@ -1453,6 +1513,7 @@ async fn test_entity_broker_multiple_subscriptions() {
14531513
entity,
14541514
event_id: format!("event_{}", update_id),
14551515
keys,
1516+
updated_model: None,
14561517
};
14571518

14581519
// Publish the update to the broker
@@ -1721,6 +1782,7 @@ async fn test_entity_broker_stress_test() {
17211782
entity,
17221783
event_id: format!("stress_event_{}", update_id),
17231784
keys,
1785+
updated_model: None,
17241786
};
17251787

17261788
// Publish the update to the broker

crates/proto/src/schema.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub struct EntityWithMetadata<const EVENT_MESSAGE: bool = false> {
1313
pub entity: Entity<EVENT_MESSAGE>,
1414
pub event_id: String,
1515
pub keys: Vec<Felt>,
16+
#[serde(default, skip_serializing_if = "Option::is_none")]
17+
pub updated_model: Option<Ty>,
1618
}
1719

1820
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)]

crates/sqlite/types/src/lib.rs

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,18 @@ pub struct Entity {
5454

5555
impl<const EVENT_MESSAGE: bool> From<Entity> for torii_proto::schema::Entity<EVENT_MESSAGE> {
5656
fn from(value: Entity) -> Self {
57-
let models = if value.deleted {
58-
vec![]
59-
} else {
60-
vec![value.updated_model.unwrap().as_struct().unwrap().clone()]
61-
};
57+
// Always include model info so clause matching works for deletions too.
58+
// The `deleted` flag indicates deletion status - no need to empty models.
59+
let updated_model = value.updated_model.as_ref().unwrap_or_else(|| {
60+
if value.deleted {
61+
panic!("deleted entity missing updated_model: {}", value.id);
62+
}
63+
panic!("entity missing updated_model: {}", value.id);
64+
});
65+
let model_struct = updated_model.as_struct().unwrap_or_else(|| {
66+
panic!("entity updated_model is not a struct: {}", value.id);
67+
});
68+
let models = vec![model_struct.clone()];
6269

6370
// Use the dedicated entity_id column (no parsing needed!)
6471
Self {
@@ -74,6 +81,8 @@ impl<const EVENT_MESSAGE: bool> From<Entity> for torii_proto::schema::Entity<EVE
7481

7582
impl<const EVENT_MESSAGE: bool> From<Entity> for EntityWithMetadata<EVENT_MESSAGE> {
7683
fn from(value: Entity) -> Self {
84+
let updated_model = value.updated_model.clone();
85+
let event_id = value.event_id.clone();
7786
let keys = value
7887
.keys
7988
.split('/')
@@ -86,9 +95,10 @@ impl<const EVENT_MESSAGE: bool> From<Entity> for EntityWithMetadata<EVENT_MESSAG
8695
})
8796
.collect();
8897
Self {
89-
event_id: value.event_id.clone(),
98+
event_id,
9099
entity: value.into(),
91100
keys,
101+
updated_model,
92102
}
93103
}
94104
}
@@ -204,6 +214,58 @@ impl From<Event> for torii_proto::Event {
204214
}
205215
}
206216

217+
#[cfg(test)]
218+
mod tests {
219+
use super::*;
220+
use chrono::Utc;
221+
use dojo_types::schema::{Struct, Ty};
222+
223+
fn sample_entity(updated_model: Option<Ty>, deleted: bool) -> Entity {
224+
let now = Utc::now();
225+
Entity {
226+
id: "0x1:0x2".to_string(),
227+
entity_id: "0x2".to_string(),
228+
world_address: "0x1".to_string(),
229+
keys: "0x1/0x2".to_string(),
230+
event_id: "event_1".to_string(),
231+
executed_at: now,
232+
created_at: now,
233+
updated_at: now,
234+
updated_model,
235+
deleted,
236+
}
237+
}
238+
239+
#[test]
240+
#[should_panic(expected = "deleted entity missing updated_model")]
241+
fn deleted_entity_requires_updated_model() {
242+
let entity = sample_entity(None, true);
243+
let _: torii_proto::schema::Entity = entity.into();
244+
}
245+
246+
#[test]
247+
fn deleted_entity_includes_model_info() {
248+
let model = Struct {
249+
name: "ns-Model".to_string(),
250+
children: vec![],
251+
};
252+
let entity = sample_entity(Some(Ty::Struct(model.clone())), true);
253+
254+
let proto_entity: torii_proto::schema::Entity = entity.clone().into();
255+
assert_eq!(proto_entity.models.len(), 1);
256+
assert_eq!(proto_entity.models[0].name, model.name);
257+
258+
let entity_with_metadata: EntityWithMetadata = entity.into();
259+
let updated_model = entity_with_metadata
260+
.updated_model
261+
.expect("updated_model missing");
262+
match updated_model {
263+
Ty::Struct(struct_ty) => assert_eq!(struct_ty.name, "ns-Model"),
264+
_ => panic!("expected updated_model to be a struct"),
265+
}
266+
}
267+
}
268+
207269
#[derive(FromRow, Deserialize, Debug, Clone)]
208270
#[serde(rename_all = "camelCase")]
209271
pub struct Token {

0 commit comments

Comments
 (0)