diff --git a/crates/torii/core/src/model.rs b/crates/torii/core/src/model.rs index 05790a3851..fff8ae6c53 100644 --- a/crates/torii/core/src/model.rs +++ b/crates/torii/core/src/model.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::str::FromStr; use async_trait::async_trait; @@ -211,46 +212,118 @@ pub fn build_sql_query( model_schemas: &Vec, entities_table: &str, entity_relation_column: &str, -) -> Result { - fn parse_struct( + where_clause: Option<&str>, + where_clause_arrays: Option<&str>, +) -> Result<(String, HashMap), Error> { + fn parse_ty( path: &str, - schema: &Struct, + name: &str, + ty: &Ty, selections: &mut Vec, tables: &mut Vec, + arrays_queries: &mut HashMap, Vec)>, ) { - for child in &schema.children { - match &child.ty { - Ty::Struct(s) => { - let table_name = format!("{}${}", path, child.name); - parse_struct(&table_name, s, selections, tables); + match &ty { + Ty::Struct(s) => { + // struct can be the main entrypoint to our model schema + // so we dont format the table name if the path is empty + let table_name = + if path.is_empty() { s.name.clone() } else { format!("{}${}", path, name) }; + + for child in &s.children { + parse_ty( + &table_name, + &child.name, + &child.ty, + selections, + tables, + arrays_queries, + ); + } - tables.push(table_name); + tables.push(table_name); + } + Ty::Tuple(t) => { + let table_name = format!("{}${}", path, name); + for (i, child) in t.iter().enumerate() { + parse_ty( + &table_name, + &format!("_{}", i), + child, + selections, + tables, + arrays_queries, + ); + } + + tables.push(table_name); + } + Ty::Array(t) => { + let table_name = format!("{}${}", path, name); + + let mut array_selections = Vec::new(); + let mut array_tables = vec![table_name.clone()]; + + parse_ty( + &table_name, + "data", + &t[0], + &mut array_selections, + &mut array_tables, + arrays_queries, + ); + + arrays_queries.insert(table_name, (array_selections, array_tables)); + } + Ty::Enum(e) => { + let table_name = format!("{}${}", path, name); + + let mut is_typed = false; + for option in &e.options { + if let Ty::Tuple(t) = &option.ty { + if t.is_empty() { + continue; + } + } + + parse_ty( + &table_name, + &option.name, + &option.ty, + selections, + tables, + arrays_queries, + ); + is_typed = true; } - _ => { - // alias selected columns to avoid conflicts in `JOIN` - selections.push(format!( - "{}.external_{} AS \"{}.{}\"", - path, child.name, path, child.name - )); + + selections.push(format!("{}.external_{} AS \"{}.{}\"", path, name, path, name)); + if is_typed { + tables.push(table_name); } } + _ => { + // alias selected columns to avoid conflicts in `JOIN` + selections.push(format!("{}.external_{} AS \"{}.{}\"", path, name, path, name)); + } } } let mut global_selections = Vec::new(); - let mut global_tables = - model_schemas.iter().enumerate().map(|(_, schema)| schema.name()).collect::>(); + let mut global_tables = Vec::new(); + + let mut arrays_queries = HashMap::new(); for ty in model_schemas { let schema = ty.as_struct().expect("schema should be struct"); - let model_table = &schema.name; - let mut selections = Vec::new(); - let mut tables = Vec::new(); - - parse_struct(model_table, schema, &mut selections, &mut tables); - - global_selections.push(selections.join(", ")); - global_tables.extend(tables); + parse_ty( + "", + &schema.name, + ty, + &mut global_selections, + &mut global_tables, + &mut arrays_queries, + ); } // TODO: Fallback to subqueries, SQLite has a max limit of 64 on 'table 'JOIN' @@ -267,91 +340,188 @@ pub fn build_sql_query( .collect::>() .join(" "); - Ok(format!( + let mut formatted_arrays_queries: HashMap = arrays_queries + .into_iter() + .map(|(table, (selections, tables))| { + let mut selections_clause = selections.join(", "); + if !selections_clause.is_empty() { + selections_clause = format!(", {}", selections_clause); + } + + let join_clause = tables + .iter() + .enumerate() + .map(|(idx, table)| { + if idx == 0 { + format!( + " JOIN {table} ON {entities_table}.id = \ + {table}.{entity_relation_column}" + ) + } else { + format!( + " JOIN {table} ON {table}.full_array_id = {prev_table}.full_array_id", + prev_table = tables[idx - 1] + ) + } + }) + .collect::>() + .join(" "); + + ( + table, + format!( + "SELECT {entities_table}.id, {entities_table}.keys{selections_clause} FROM \ + {entities_table}{join_clause}", + ), + ) + }) + .collect(); + + let mut query = format!( "SELECT {entities_table}.id, {entities_table}.keys, {selections_clause} FROM \ {entities_table}{join_clause}" - )) + ); + + if let Some(where_clause) = where_clause { + query = format!("{} WHERE {}", query, where_clause); + } + + if let Some(where_clause_arrays) = where_clause_arrays { + for (_, formatted_query) in formatted_arrays_queries.iter_mut() { + *formatted_query = format!("{} WHERE {}", formatted_query, where_clause_arrays); + } + } + + Ok((query, formatted_arrays_queries)) } /// Populate the values of a Ty (schema) from SQLite row. -pub fn map_row_to_ty(path: &str, struct_ty: &mut Struct, row: &SqliteRow) -> Result<(), Error> { - for member in struct_ty.children.iter_mut() { - let column_name = format!("{}.{}", path, member.name); - match &mut member.ty { - Ty::Primitive(primitive) => { - match &primitive { - Primitive::Bool(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_bool(Some(value))?; - } - Primitive::USize(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_usize(Some(value))?; - } - Primitive::U8(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_u8(Some(value))?; - } - Primitive::U16(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_u16(Some(value))?; - } - Primitive::U32(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_u32(Some(value))?; - } - Primitive::U64(_) => { - let value = row.try_get::(&column_name)?; - let hex_str = value.trim_start_matches("0x"); - primitive.set_u64(Some( - u64::from_str_radix(hex_str, 16).map_err(ParseError::ParseIntError)?, - ))?; - } - Primitive::U128(_) => { - let value = row.try_get::(&column_name)?; - let hex_str = value.trim_start_matches("0x"); - primitive.set_u128(Some( - u128::from_str_radix(hex_str, 16).map_err(ParseError::ParseIntError)?, - ))?; - } - Primitive::U256(_) => { - let value = row.try_get::(&column_name)?; - let hex_str = value.trim_start_matches("0x"); - primitive.set_u256(Some(U256::from_be_hex(hex_str)))?; - } - Primitive::Felt252(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_felt252(Some( - FieldElement::from_str(&value).map_err(ParseError::FromStr)?, - ))?; - } - Primitive::ClassHash(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_contract_address(Some( - FieldElement::from_str(&value).map_err(ParseError::FromStr)?, - ))?; - } - Primitive::ContractAddress(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_contract_address(Some( - FieldElement::from_str(&value).map_err(ParseError::FromStr)?, - ))?; - } - }; - } - Ty::Enum(enum_ty) => { - let value = row.try_get::(&column_name)?; - enum_ty.set_option(&value)?; +pub fn map_row_to_ty( + path: &str, + name: &str, + ty: &mut Ty, + // the row that contains non dynamic data for Ty + row: &SqliteRow, + // a hashmap where keys are the paths for the model + // arrays and values are the rows mapping to each element + // in the array + arrays_rows: &HashMap>, +) -> Result<(), Error> { + let column_name = format!("{}.{}", path, name); + match ty { + Ty::Primitive(primitive) => { + match &primitive { + Primitive::Bool(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_bool(Some(value))?; + } + Primitive::USize(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_usize(Some(value))?; + } + Primitive::U8(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_u8(Some(value))?; + } + Primitive::U16(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_u16(Some(value))?; + } + Primitive::U32(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_u32(Some(value))?; + } + Primitive::U64(_) => { + let value = row.try_get::(&column_name)?; + let hex_str = value.trim_start_matches("0x"); + primitive.set_u64(Some( + u64::from_str_radix(hex_str, 16).map_err(ParseError::ParseIntError)?, + ))?; + } + Primitive::U128(_) => { + let value = row.try_get::(&column_name)?; + let hex_str = value.trim_start_matches("0x"); + primitive.set_u128(Some( + u128::from_str_radix(hex_str, 16).map_err(ParseError::ParseIntError)?, + ))?; + } + Primitive::U256(_) => { + let value = row.try_get::(&column_name)?; + let hex_str = value.trim_start_matches("0x"); + primitive.set_u256(Some(U256::from_be_hex(hex_str)))?; + } + Primitive::Felt252(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_felt252(Some( + FieldElement::from_str(&value).map_err(ParseError::FromStr)?, + ))?; + } + Primitive::ClassHash(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_contract_address(Some( + FieldElement::from_str(&value).map_err(ParseError::FromStr)?, + ))?; + } + Primitive::ContractAddress(_) => { + let value = row.try_get::(&column_name)?; + primitive.set_contract_address(Some( + FieldElement::from_str(&value).map_err(ParseError::FromStr)?, + ))?; + } + }; + } + Ty::Enum(enum_ty) => { + let option = row.try_get::(&column_name)?; + enum_ty.set_option(&option)?; + + let path = [path, &name].join("$"); + for option in &mut enum_ty.options { + map_row_to_ty(&path, &option.name, &mut option.ty, row, arrays_rows)?; } - Ty::Struct(struct_ty) => { - let path = [path, &member.name].join("$"); - map_row_to_ty(&path, struct_ty, row)?; + } + Ty::Struct(struct_ty) => { + // struct can be the main entrypoint to our model schema + // so we dont format the table name if the path is empty + let path = + if path.is_empty() { struct_ty.name.clone() } else { [path, &name].join("$") }; + + for member in &mut struct_ty.children { + map_row_to_ty(&path, &member.name, &mut member.ty, row, arrays_rows)?; } - ty => { - unimplemented!("unimplemented type_enum: {ty}"); + } + Ty::Tuple(ty) => { + let path = [path, &name].join("$"); + + for (i, member) in ty.iter_mut().enumerate() { + map_row_to_ty(&path, &format!("_{}", i), member, row, arrays_rows)?; } - }; - } + } + Ty::Array(ty) => { + let path = [path, &name].join("$"); + // filter by entity id in case we have multiple entities + let rows = arrays_rows + .get(&path) + .expect("qed; rows should exist") + .iter() + .filter(|array_row| array_row.get::("id") == row.get::("id")) + .collect::>(); + + // map each row to the ty of the array + let tys = rows + .iter() + .map(|row| { + let mut ty = ty[0].clone(); + map_row_to_ty(&path, "data", &mut ty, row, arrays_rows).map(|_| ty) + }) + .collect::, _>>()?; + + *ty = tys; + } + Ty::ByteArray(bytearray) => { + let value = row.try_get::(&column_name)?; + *bytearray = value; + } + }; Ok(()) } @@ -386,9 +556,19 @@ mod tests { type_enum: "Primitive".into(), enum_options: None, }, + SqlModelMember { + id: "PlayerConfig".into(), + name: "name".into(), + r#type: "ByteArray".into(), + key: false, + model_idx: 0, + member_idx: 0, + type_enum: "ByteArray".into(), + enum_options: None, + }, ]; - let expected_ty = Ty::Struct(Struct { + let expected_position = Ty::Struct(Struct { name: "Position".into(), children: vec![ dojo_types::schema::Member { @@ -404,7 +584,17 @@ mod tests { ], }); - assert_eq!(parse_sql_model_members("Position", &model_members), expected_ty); + let expected_player_config = Ty::Struct(Struct { + name: "PlayerConfig".into(), + children: vec![dojo_types::schema::Member { + name: "name".into(), + key: false, + ty: Ty::ByteArray("".to_string()), + }], + }); + + assert_eq!(parse_sql_model_members("Position", &model_members), expected_position); + assert_eq!(parse_sql_model_members("PlayerConfig", &model_members), expected_player_config); } #[test] @@ -460,9 +650,79 @@ mod tests { type_enum: "Primitive".into(), enum_options: None, }, + SqlModelMember { + id: "PlayerConfig".into(), + name: "favorite_item".into(), + r#type: "Option".into(), + key: false, + model_idx: 0, + member_idx: 0, + type_enum: "Enum".into(), + enum_options: Some("None,Some".into()), + }, + SqlModelMember { + id: "PlayerConfig".into(), + name: "items".into(), + r#type: "Array".into(), + key: false, + model_idx: 0, + member_idx: 1, + type_enum: "Array".into(), + enum_options: None, + }, + SqlModelMember { + id: "PlayerConfig$items".into(), + name: "data".into(), + r#type: "PlayerItem".into(), + key: false, + model_idx: 0, + member_idx: 1, + type_enum: "Struct".into(), + enum_options: None, + }, + SqlModelMember { + id: "PlayerConfig$items$data".into(), + name: "item_id".into(), + r#type: "u32".into(), + key: false, + model_idx: 0, + member_idx: 0, + type_enum: "Primitive".into(), + enum_options: None, + }, + SqlModelMember { + id: "PlayerConfig$items$data".into(), + name: "quantity".into(), + r#type: "u32".into(), + key: false, + model_idx: 0, + member_idx: 1, + type_enum: "Primitive".into(), + enum_options: None, + }, + SqlModelMember { + id: "PlayerConfig$favorite_item".into(), + name: "Some".into(), + r#type: "u32".into(), + key: false, + model_idx: 1, + member_idx: 0, + type_enum: "Primitive".into(), + enum_options: None, + }, + SqlModelMember { + id: "PlayerConfig$favorite_item".into(), + name: "option".into(), + r#type: "Option".into(), + key: false, + model_idx: 1, + member_idx: 0, + type_enum: "Enum".into(), + enum_options: Some("None,Some".into()), + }, ]; - let expected_ty = Ty::Struct(Struct { + let expected_position = Ty::Struct(Struct { name: "Position".into(), children: vec![ dojo_types::schema::Member { @@ -497,7 +757,48 @@ mod tests { ], }); - assert_eq!(parse_sql_model_members("Position", &model_members), expected_ty); + let expected_player_config = Ty::Struct(Struct { + name: "PlayerConfig".into(), + children: vec![ + dojo_types::schema::Member { + name: "favorite_item".into(), + key: false, + ty: Ty::Enum(Enum { + name: "Option".into(), + option: None, + options: vec![ + EnumOption { name: "None".into(), ty: Ty::Tuple(vec![]) }, + EnumOption { + name: "Some".into(), + ty: Ty::Primitive("u32".parse().unwrap()), + }, + ], + }), + }, + dojo_types::schema::Member { + name: "items".into(), + key: false, + ty: Ty::Array(vec![Ty::Struct(Struct { + name: "PlayerItem".into(), + children: vec![ + Member { + name: "item_id".into(), + key: false, + ty: Ty::Primitive("u32".parse().unwrap()), + }, + Member { + name: "quantity".into(), + key: false, + ty: Ty::Primitive("u32".parse().unwrap()), + }, + ], + })]), + }, + ], + }); + + assert_eq!(parse_sql_model_members("Position", &model_members), expected_position); + assert_eq!(parse_sql_model_members("PlayerConfig", &model_members), expected_player_config); } #[test] @@ -536,18 +837,13 @@ mod tests { #[test] fn struct_ty_to_query() { - let ty = Ty::Struct(Struct { + let position = Ty::Struct(Struct { name: "Position".into(), children: vec![ dojo_types::schema::Member { - name: "name".into(), - key: false, - ty: Ty::Primitive("felt252".parse().unwrap()), - }, - dojo_types::schema::Member { - name: "age".into(), - key: false, - ty: Ty::Primitive("u8".parse().unwrap()), + name: "player".into(), + key: true, + ty: Ty::Primitive("ContractAddress".parse().unwrap()), }, dojo_types::schema::Member { name: "vec".into(), @@ -558,23 +854,105 @@ mod tests { Member { name: "x".into(), key: false, - ty: Ty::Primitive("u256".parse().unwrap()), + ty: Ty::Primitive("u32".parse().unwrap()), }, Member { name: "y".into(), key: false, - ty: Ty::Primitive("u256".parse().unwrap()), + ty: Ty::Primitive("u32".parse().unwrap()), + }, + ], + }), + }, + dojo_types::schema::Member { + name: "test_everything".into(), + key: false, + ty: Ty::Array(vec![Ty::Struct(Struct { + name: "TestEverything".into(), + children: vec![Member { + name: "data".into(), + key: false, + ty: Ty::Tuple(vec![ + Ty::Array(vec![Ty::Primitive("u32".parse().unwrap())]), + Ty::Array(vec![Ty::Array(vec![Ty::Tuple(vec![ + Ty::Primitive("u32".parse().unwrap()), + Ty::Struct(Struct { + name: "Vec2".into(), + children: vec![ + Member { + name: "x".into(), + key: false, + ty: Ty::Primitive("u32".parse().unwrap()), + }, + Member { + name: "y".into(), + key: false, + ty: Ty::Primitive("u32".parse().unwrap()), + }, + ], + }), + ])])]), + ]), + }], + })]), + }, + ], + }); + + let player_config = Ty::Struct(Struct { + name: "PlayerConfig".into(), + children: vec![ + dojo_types::schema::Member { + name: "favorite_item".into(), + key: false, + ty: Ty::Enum(Enum { + name: "Option".into(), + option: None, + options: vec![ + EnumOption { name: "None".into(), ty: Ty::Tuple(vec![]) }, + EnumOption { + name: "Some".into(), + ty: Ty::Primitive("u32".parse().unwrap()), }, ], }), }, + dojo_types::schema::Member { + name: "items".into(), + key: false, + ty: Ty::Array(vec![Ty::Struct(Struct { + name: "PlayerItem".into(), + children: vec![ + Member { + name: "item_id".into(), + key: false, + ty: Ty::Primitive("u32".parse().unwrap()), + }, + Member { + name: "quantity".into(), + key: false, + ty: Ty::Primitive("u32".parse().unwrap()), + }, + ], + })]), + }, ], }); - let query = build_sql_query(&vec![ty], "entities", "entity_id").unwrap(); - assert_eq!( - query, - r#"SELECT entities.id, entities.keys, Position.external_name AS "Position.name", Position.external_age AS "Position.age", Position$vec.external_x AS "Position$vec.x", Position$vec.external_y AS "Position$vec.y" FROM entities JOIN Position ON entities.id = Position.entity_id JOIN Position$vec ON entities.id = Position$vec.entity_id"# - ); + let query = + build_sql_query(&vec![position, player_config], "entities", "entity_id", None, None) + .unwrap(); + + let expected_query = + "SELECT entities.id, entities.keys, Position.external_player AS \"Position.player\", \ + Position$vec.external_x AS \"Position$vec.x\", Position$vec.external_y AS \ + \"Position$vec.y\", PlayerConfig$favorite_item.external_Some AS \ + \"PlayerConfig$favorite_item.Some\", PlayerConfig.external_favorite_item AS \ + \"PlayerConfig.favorite_item\" FROM entities JOIN Position$vec ON entities.id = \ + Position$vec.entity_id JOIN Position ON entities.id = Position.entity_id JOIN \ + PlayerConfig$favorite_item ON entities.id = PlayerConfig$favorite_item.entity_id \ + JOIN PlayerConfig ON entities.id = PlayerConfig.entity_id"; + // todo: completely tests arrays + assert_eq!(query.0, expected_query); } } diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index efe9190150..e0d89bf3b4 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -516,6 +516,16 @@ impl Sql { Argument::String(entity_id.to_string()), ]; + if !indexes.is_empty() { + columns.push("full_array_id".to_string()); + arguments.push(Argument::String( + std::iter::once(entity_id.to_string()) + .chain(indexes.iter().map(|i| i.to_string())) + .collect::>() + .join(FELT_DELIMITER), + )); + } + for (column_idx, idx) in indexes.iter().enumerate() { columns.push(format!("idx_{}", column_idx)); arguments.push(Argument::Int(*idx)); @@ -724,9 +734,13 @@ impl Sql { ); if array_idx > 0 { + // index columns for i in 0..array_idx { create_table_query.push_str(&format!("idx_{i} INTEGER NOT NULL, ", i = i)); } + + // full array id column + create_table_query.push_str("full_array_id TEXT NOT NULL UNIQUE, "); } let mut build_member = |name: &str, ty: &Ty, options: &mut Option| { diff --git a/crates/torii/grpc/src/server/mod.rs b/crates/torii/grpc/src/server/mod.rs index 3d0f8cbd81..15db131a30 100644 --- a/crates/torii/grpc/src/server/mod.rs +++ b/crates/torii/grpc/src/server/mod.rs @@ -4,6 +4,7 @@ pub mod subscriptions; #[cfg(test)] mod tests; +use std::collections::HashMap; use std::future::Future; use std::net::SocketAddr; use std::pin::Pin; @@ -45,9 +46,9 @@ pub(crate) static ENTITIES_TABLE: &str = "entities"; pub(crate) static ENTITIES_MODEL_RELATION_TABLE: &str = "entity_model"; pub(crate) static ENTITIES_ENTITY_RELATION_COLUMN: &str = "entity_id"; -pub(crate) static EVENTS_MESSAGES_TABLE: &str = "events_messages"; -pub(crate) static EVENTS_MESSAGES_MODEL_RELATION_TABLE: &str = "event_model"; -pub(crate) static EVENTS_MESSAGES_ENTITY_RELATION_COLUMN: &str = "event_message_id"; +pub(crate) static EVENT_MESSAGES_TABLE: &str = "event_messages"; +pub(crate) static EVENT_MESSAGES_MODEL_RELATION_TABLE: &str = "event_model"; +pub(crate) static EVENT_MESSAGES_ENTITY_RELATION_COLUMN: &str = "event_message_id"; #[derive(Clone)] pub struct DojoWorld { @@ -162,6 +163,22 @@ impl DojoWorld { .await } + async fn event_messages_all( + &self, + limit: u32, + offset: u32, + ) -> Result<(Vec, u32), Error> { + self.query_by_hashed_keys( + EVENT_MESSAGES_TABLE, + EVENT_MESSAGES_MODEL_RELATION_TABLE, + EVENT_MESSAGES_ENTITY_RELATION_COLUMN, + None, + limit, + offset, + ) + .await + } + async fn events_all(&self, limit: u32, offset: u32) -> Result, Error> { let query = r#" SELECT keys, data, transaction_hash @@ -235,19 +252,31 @@ impl DojoWorld { let model_ids: Vec<&str> = models_str.split(',').collect(); let schemas = self.model_cache.schemas(model_ids).await?; - let entity_query = format!( - "{} WHERE {table}.id = ?", - build_sql_query(&schemas, table, entity_relation_column)? - ); + let (entity_query, arrays_queries) = build_sql_query( + &schemas, + table, + entity_relation_column, + Some(&format!("{table}.id = ?")), + Some(&format!("{table}.id = ?")), + )?; + let row = sqlx::query(&entity_query).bind(&entity_id).fetch_one(&self.pool).await?; + let mut arrays_rows = HashMap::new(); + for (name, query) in arrays_queries { + let rows = sqlx::query(&query).bind(&entity_id).fetch_all(&self.pool).await?; + arrays_rows.insert(name, rows); + } let models = schemas - .iter() - .map(|s| { - let mut struct_ty = s.as_struct().expect("schema should be struct").to_owned(); - map_row_to_ty(&s.name(), &mut struct_ty, &row)?; - - Ok(struct_ty.try_into().unwrap()) + .into_iter() + .map(|mut s| { + map_row_to_ty("", &s.name(), &mut s, &row, &arrays_rows)?; + + Ok(s.as_struct() + .expect("schema should be struct") + .to_owned() + .try_into() + .unwrap()) }) .collect::, Error>>()?; @@ -317,21 +346,34 @@ impl DojoWorld { let schemas = self.model_cache.schemas(model_ids).await?; // query to filter with limit and offset - let entities_query = format!( - "{} WHERE {table}.keys LIKE ? ORDER BY {table}.event_id DESC LIMIT ? OFFSET ?", - build_sql_query(&schemas, table, entity_relation_column)? - ); + let (entities_query, arrays_queries) = build_sql_query( + &schemas, + table, + entity_relation_column, + Some(&format!("{table}.keys LIKE ? ORDER BY {table}.event_id DESC LIMIT ? OFFSET ?")), + Some(&format!("{table}.keys LIKE ? ORDER BY {table}.event_id DESC LIMIT ? OFFSET ?")), + )?; let db_entities = sqlx::query(&entities_query) .bind(&keys_pattern) .bind(limit) .bind(offset) .fetch_all(&self.pool) .await?; + let mut arrays_rows = HashMap::new(); + for (name, query) in arrays_queries { + let rows = sqlx::query(&query) + .bind(&keys_pattern) + .bind(limit) + .bind(offset) + .fetch_all(&self.pool) + .await?; + arrays_rows.insert(name, rows); + } Ok(( db_entities .iter() - .map(|row| Self::map_row_to_entity(row, &schemas)) + .map(|row| Self::map_row_to_entity(row, &arrays_rows, &schemas)) .collect::, Error>>()?, total_count, )) @@ -424,16 +466,26 @@ impl DojoWorld { let table_name = member_clause.model; let column_name = format!("external_{}", member_clause.member); - let member_query = format!( - "{} WHERE {table_name}.{column_name} {comparison_operator} ?", - build_sql_query(&schemas, table, entity_relation_column)? - ); + let (entity_query, arrays_queries) = build_sql_query( + &schemas, + table, + entity_relation_column, + Some(&format!("{table_name}.{column_name} {comparison_operator} ?")), + None, + )?; let db_entities = - sqlx::query(&member_query).bind(comparison_value).fetch_all(&self.pool).await?; + sqlx::query(&entity_query).bind(comparison_value.clone()).fetch_all(&self.pool).await?; + let mut arrays_rows = HashMap::new(); + for (name, query) in arrays_queries { + let rows = + sqlx::query(&query).bind(comparison_value.clone()).fetch_all(&self.pool).await?; + arrays_rows.insert(name, rows); + } + let entities_collection = db_entities .iter() - .map(|row| Self::map_row_to_entity(row, &schemas)) + .map(|row| Self::map_row_to_entity(row, &arrays_rows, &schemas)) .collect::, Error>>()?; // Since there is not limit and offset, total_count is same as number of entities let total_count = entities_collection.len() as u32; @@ -604,7 +656,7 @@ impl DojoWorld { query: proto::types::Query, ) -> Result { let (entities, total_count) = match query.clause { - None => self.entities_all(query.limit, query.offset).await?, + None => self.event_messages_all(query.limit, query.offset).await?, Some(clause) => { let clause_type = clause.clause_type.ok_or(QueryError::MissingParam("clause_type".into()))?; @@ -616,9 +668,9 @@ impl DojoWorld { } self.query_by_hashed_keys( - EVENTS_MESSAGES_TABLE, - EVENTS_MESSAGES_MODEL_RELATION_TABLE, - EVENTS_MESSAGES_ENTITY_RELATION_COLUMN, + EVENT_MESSAGES_TABLE, + EVENT_MESSAGES_MODEL_RELATION_TABLE, + EVENT_MESSAGES_ENTITY_RELATION_COLUMN, Some(hashed_keys), query.limit, query.offset, @@ -635,9 +687,9 @@ impl DojoWorld { } self.query_by_keys( - EVENTS_MESSAGES_TABLE, - EVENTS_MESSAGES_MODEL_RELATION_TABLE, - EVENTS_MESSAGES_ENTITY_RELATION_COLUMN, + EVENT_MESSAGES_TABLE, + EVENT_MESSAGES_MODEL_RELATION_TABLE, + EVENT_MESSAGES_ENTITY_RELATION_COLUMN, keys, query.limit, query.offset, @@ -646,9 +698,9 @@ impl DojoWorld { } ClauseType::Member(member) => { self.query_by_member( - EVENTS_MESSAGES_TABLE, - EVENTS_MESSAGES_MODEL_RELATION_TABLE, - EVENTS_MESSAGES_ENTITY_RELATION_COLUMN, + EVENT_MESSAGES_TABLE, + EVENT_MESSAGES_MODEL_RELATION_TABLE, + EVENT_MESSAGES_ENTITY_RELATION_COLUMN, member, query.limit, query.offset, @@ -657,8 +709,8 @@ impl DojoWorld { } ClauseType::Composite(composite) => { self.query_by_composite( - EVENTS_MESSAGES_TABLE, - EVENTS_MESSAGES_MODEL_RELATION_TABLE, + EVENT_MESSAGES_TABLE, + EVENT_MESSAGES_MODEL_RELATION_TABLE, ENTITIES_ENTITY_RELATION_COLUMN, composite, query.limit, @@ -684,16 +736,24 @@ impl DojoWorld { Ok(RetrieveEventsResponse { events }) } - fn map_row_to_entity(row: &SqliteRow, schemas: &[Ty]) -> Result { + fn map_row_to_entity( + row: &SqliteRow, + arrays_rows: &HashMap>, + schemas: &[Ty], + ) -> Result { let hashed_keys = FieldElement::from_str(&row.get::("id")).map_err(ParseError::FromStr)?; let models = schemas .iter() .map(|schema| { - let mut struct_ty = schema.as_struct().expect("schema should be struct").to_owned(); - map_row_to_ty(&schema.name(), &mut struct_ty, row)?; - - Ok(struct_ty.try_into().unwrap()) + let mut schema = schema.to_owned(); + map_row_to_ty("", &schema.name(), &mut schema, row, arrays_rows)?; + Ok(schema + .as_struct() + .expect("schema should be struct") + .to_owned() + .try_into() + .unwrap()) }) .collect::, Error>>()?; diff --git a/crates/torii/grpc/src/server/subscriptions/entity.rs b/crates/torii/grpc/src/server/subscriptions/entity.rs index a92b88543d..1573b5c61f 100644 --- a/crates/torii/grpc/src/server/subscriptions/entity.rs +++ b/crates/torii/grpc/src/server/subscriptions/entity.rs @@ -102,20 +102,31 @@ impl Service { let model_ids: Vec<&str> = model_ids.split(',').collect(); let schemas = cache.schemas(model_ids).await?; - let entity_query = format!( - "{} WHERE entities.id = ?", - build_sql_query(&schemas, "entities", "entity_id")? - ); + let (entity_query, arrays_queries) = build_sql_query( + &schemas, + "entities", + "entity_id", + Some("entities.id = ?"), + Some("entities.id = ?"), + )?; + let row = sqlx::query(&entity_query).bind(hashed_keys).fetch_one(&pool).await?; + let mut arrays_rows = HashMap::new(); + for (name, query) in arrays_queries { + let row = sqlx::query(&query).bind(hashed_keys).fetch_all(&pool).await?; + arrays_rows.insert(name, row); + } let models = schemas - .iter() - .map(|s| { - let mut struct_ty = - s.as_struct().expect("schema should be struct").to_owned(); - map_row_to_ty(&s.name(), &mut struct_ty, &row)?; - - Ok(struct_ty.try_into().unwrap()) + .into_iter() + .map(|mut s| { + map_row_to_ty("", &s.name(), &mut s, &row, &arrays_rows)?; + + Ok(s.as_struct() + .expect("schema should be a struct") + .to_owned() + .try_into() + .unwrap()) }) .collect::, Error>>()?; diff --git a/crates/torii/grpc/src/server/subscriptions/event_message.rs b/crates/torii/grpc/src/server/subscriptions/event_message.rs index 76796e30de..736f88c0f9 100644 --- a/crates/torii/grpc/src/server/subscriptions/event_message.rs +++ b/crates/torii/grpc/src/server/subscriptions/event_message.rs @@ -101,20 +101,30 @@ impl Service { let model_ids: Vec<&str> = model_ids.split(',').collect(); let schemas = cache.schemas(model_ids).await?; - let entity_query = format!( - "{} WHERE event_messages.id = ?", - build_sql_query(&schemas, "event_messages", "event_message_id")? - ); + let (entity_query, arrays_queries) = build_sql_query( + &schemas, + "event_messages", + "event_message_id", + Some("event_messages.id = ?"), + Some("event_messages.id = ?"), + )?; + let row = sqlx::query(&entity_query).bind(hashed_keys).fetch_one(&pool).await?; + let mut arrays_rows = HashMap::new(); + for (name, query) in arrays_queries { + let rows = sqlx::query(&query).bind(hashed_keys).fetch_all(&pool).await?; + arrays_rows.insert(name, rows); + } let models = schemas - .iter() - .map(|s| { - let mut struct_ty = - s.as_struct().expect("schema should be struct").to_owned(); - map_row_to_ty(&s.name(), &mut struct_ty, &row)?; - - Ok(struct_ty.try_into().unwrap()) + .into_iter() + .map(|mut s| { + map_row_to_ty("", &s.name(), &mut s, &row, &arrays_rows)?; + Ok(s.as_struct() + .expect("schema should be a struct") + .to_owned() + .try_into() + .unwrap()) }) .collect::, Error>>()?; diff --git a/crates/torii/grpc/src/types/schema.rs b/crates/torii/grpc/src/types/schema.rs index c1007d6e48..36c0e926b3 100644 --- a/crates/torii/grpc/src/types/schema.rs +++ b/crates/torii/grpc/src/types/schema.rs @@ -64,7 +64,7 @@ impl TryFrom for proto::types::Ty { Ty::Primitive(primitive) => { Some(proto::types::ty::TyType::Primitive(primitive.try_into()?)) } - Ty::Enum(r#enum) => Some(proto::types::ty::TyType::Enum(r#enum.into())), + Ty::Enum(r#enum) => Some(proto::types::ty::TyType::Enum(r#enum.try_into()?)), Ty::Struct(r#struct) => Some(proto::types::ty::TyType::Struct(r#struct.try_into()?)), Ty::Tuple(tuple) => Some(proto::types::ty::TyType::Tuple(proto::types::Array { children: tuple @@ -107,35 +107,50 @@ impl TryFrom for proto::types::Member { } } -impl From for EnumOption { - fn from(option: proto::types::EnumOption) -> Self { - EnumOption { name: option.name, ty: Ty::Tuple(vec![]) } +impl TryFrom for EnumOption { + type Error = SchemaError; + fn try_from(option: proto::types::EnumOption) -> Result { + Ok(EnumOption { + name: option.name, + ty: option.ty.ok_or(SchemaError::MissingExpectedData)?.try_into()?, + }) } } -impl From for proto::types::EnumOption { - fn from(option: EnumOption) -> Self { - proto::types::EnumOption { name: option.name, ty: None } +impl TryFrom for proto::types::EnumOption { + type Error = SchemaError; + fn try_from(option: EnumOption) -> Result { + Ok(proto::types::EnumOption { name: option.name, ty: Some(option.ty.try_into()?) }) } } -impl From for Enum { - fn from(r#enum: proto::types::Enum) -> Self { - Enum { +impl TryFrom for Enum { + type Error = SchemaError; + fn try_from(r#enum: proto::types::Enum) -> Result { + Ok(Enum { name: r#enum.name.clone(), option: Some(r#enum.option as u8), - options: r#enum.options.into_iter().map(Into::into).collect::>(), - } + options: r#enum + .options + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + }) } } -impl From for proto::types::Enum { - fn from(r#enum: Enum) -> Self { - proto::types::Enum { +impl TryFrom for proto::types::Enum { + type Error = SchemaError; + fn try_from(r#enum: Enum) -> Result { + Ok(proto::types::Enum { name: r#enum.name, option: r#enum.option.expect("option value") as u32, - options: r#enum.options.into_iter().map(Into::into).collect::>(), - } + options: r#enum + .options + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + }) } } @@ -280,7 +295,7 @@ impl TryFrom for Ty { Ok(Ty::Primitive(primitive.try_into()?)) } proto::types::ty::TyType::Struct(r#struct) => Ok(Ty::Struct(r#struct.try_into()?)), - proto::types::ty::TyType::Enum(r#enum) => Ok(Ty::Enum(r#enum.into())), + proto::types::ty::TyType::Enum(r#enum) => Ok(Ty::Enum(r#enum.try_into()?)), proto::types::ty::TyType::Tuple(array) => Ok(Ty::Tuple( array.children.into_iter().map(TryInto::try_into).collect::, _>>()?, )),