diff --git a/crates/torii/core/src/executor/mod.rs b/crates/torii/core/src/executor/mod.rs index 006d942efd..c525ccd910 100644 --- a/crates/torii/core/src/executor/mod.rs +++ b/crates/torii/core/src/executor/mod.rs @@ -756,10 +756,12 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { } } QueryType::Other => { - query.execute(&mut **tx).await.with_context(|| { - format!( - "Failed to execute query: {:?}, args: {:?}", - query_message.statement, query_message.arguments + query.execute(&mut **tx).await.map_err(|e| { + anyhow::anyhow!( + "Failed to execute query: {:?}, args: {:?}, error: {:?}", + query_message.statement, + query_message.arguments, + e ) })?; } diff --git a/crates/torii/core/src/model.rs b/crates/torii/core/src/model.rs index 2266993e9b..3c4bd315ee 100644 --- a/crates/torii/core/src/model.rs +++ b/crates/torii/core/src/model.rs @@ -1,19 +1,18 @@ -use std::collections::HashMap; use std::str::FromStr; use async_trait::async_trait; use crypto_bigint::U256; -use dojo_types::naming::get_tag; -use dojo_types::primitive::Primitive; -use dojo_types::schema::{Enum, EnumOption, Member, Struct, Ty}; +use dojo_types::primitive::{Primitive, PrimitiveError}; +use dojo_types::schema::Ty; use dojo_world::contracts::abigen::model::Layout; use dojo_world::contracts::model::ModelReader; +use serde_json::Value as JsonValue; use sqlx::sqlite::SqliteRow; use sqlx::{Pool, Row, Sqlite}; use starknet::core::types::Felt; use super::error::{self, Error}; -use crate::error::{ParseError, QueryError}; +use crate::error::ParseError; #[derive(Debug)] pub struct ModelSQLReader { @@ -95,15 +94,12 @@ impl ModelReader for ModelSQLReader { } async fn schema(&self) -> Result { - let model_members: Vec = sqlx::query_as( - "SELECT id, model_idx, member_idx, name, type, type_enum, enum_options, key FROM \ - model_members WHERE model_id = ? ORDER BY model_idx ASC, member_idx ASC", - ) - .bind(format!("{:#x}", self.selector)) - .fetch_all(&self.pool) - .await?; + let schema: String = sqlx::query_scalar("SELECT schema FROM models WHERE id = ?") + .bind(format!("{:#x}", self.selector)) + .fetch_one(&self.pool) + .await?; - Ok(parse_sql_model_members(&self.namespace, &self.name, &model_members)) + Ok(serde_json::from_str(&schema).map_err(error::ParseError::FromJsonStr)?) } async fn packed_size(&self) -> Result { @@ -119,344 +115,88 @@ impl ModelReader for ModelSQLReader { } } -#[allow(unused)] -#[derive(Debug, sqlx::FromRow)] -pub struct SqlModelMember { - id: String, - model_idx: u32, - member_idx: u32, - name: String, - r#type: String, - type_enum: String, - enum_options: Option, - key: bool, -} - -// assume that the model members are sorted by model_idx and member_idx -// `id` is the type id of the model member -/// A helper function to parse the model members from sql table to `Ty` -pub fn parse_sql_model_members( - namespace: &str, - model: &str, - model_members_all: &[SqlModelMember], -) -> Ty { - fn parse_sql_member(member: &SqlModelMember, model_members_all: &[SqlModelMember]) -> Ty { - match member.type_enum.as_str() { - "Primitive" => Ty::Primitive(member.r#type.parse().unwrap()), - "ByteArray" => Ty::ByteArray("".to_string()), - "Struct" => { - let children = model_members_all - .iter() - .filter(|m| m.id == format!("{}${}", member.id, member.name)) - .map(|child| Member { - key: child.key, - name: child.name.to_owned(), - ty: parse_sql_member(child, model_members_all), - }) - .collect::>(); - - Ty::Struct(Struct { name: member.r#type.clone(), children }) - } - "Enum" => { - let options = member - .enum_options - .as_ref() - .expect("qed; enum_options should exist") - .split(',') - .map(|s| { - let member = if let Some(member) = model_members_all.iter().find(|m| { - m.id == format!("{}${}", member.id, member.name) && m.name == s - }) { - parse_sql_member(member, model_members_all) - } else { - Ty::Tuple(vec![]) - }; - - EnumOption { name: s.to_owned(), ty: member } - }) - .collect::>(); - - Ty::Enum(Enum { option: None, name: member.r#type.clone(), options }) - } - "Tuple" => { - let children = model_members_all - .iter() - .filter(|m| m.id == format!("{}${}", member.id, member.name)) - .map(|child| Member { - key: child.key, - name: child.name.to_owned(), - ty: parse_sql_member(child, model_members_all), - }) - .collect::>(); - - Ty::Tuple(children.into_iter().map(|m| m.ty).collect()) - } - "Array" => { - let children = model_members_all - .iter() - .filter(|m| m.id == format!("{}${}", member.id, member.name)) - .map(|child| Member { - key: child.key, - name: child.name.to_owned(), - ty: parse_sql_member(child, model_members_all), - }) - .collect::>(); - - Ty::Array(children.into_iter().map(|m| m.ty).collect()) - } - ty => { - unimplemented!("unimplemented type_enum: {ty}"); - } - } - } - - Ty::Struct(Struct { - name: get_tag(namespace, model), - children: model_members_all - .iter() - .filter(|m| m.id == get_tag(namespace, model)) - .map(|m| Member { - key: m.key, - name: m.name.to_owned(), - ty: parse_sql_member(m, model_members_all), - }) - .collect::>(), - }) - // parse_sql_model_members_impl(model, model, model_members_all) -} - /// Creates a query that fetches all models and their nested data. pub fn build_sql_query( schemas: &Vec, - entities_table: &str, + table_name: &str, entity_relation_column: &str, where_clause: Option<&str>, - where_clause_arrays: Option<&str>, limit: Option, offset: Option, -) -> Result<(String, HashMap, String), Error> { - #[derive(Default)] - struct TableInfo { - table_name: String, - parent_table: Option, - // is_optional: bool, - depth: usize, // Track nesting depth for proper ordering - } - - #[allow(clippy::too_many_arguments)] - fn parse_ty( - path: &str, - name: &str, - ty: &Ty, - selections: &mut Vec, - tables: &mut Vec, - arrays_queries: &mut HashMap, Vec)>, - _parent_is_optional: bool, - depth: usize, - ) { - match &ty { +) -> Result<(String, String), Error> { + fn collect_columns(table_prefix: &str, path: &str, ty: &Ty, selections: &mut Vec) { + match ty { Ty::Struct(s) => { - let table_name = - if path.is_empty() { name.to_string() } else { format!("{}${}", path, name) }; - - tables.push(TableInfo { - table_name: table_name.clone(), - parent_table: if path.is_empty() { None } else { Some(path.to_string()) }, - // is_optional: parent_is_optional, - depth, - }); - for child in &s.children { - parse_ty( - &table_name, - &child.name, - &child.ty, - selections, - tables, - arrays_queries, - _parent_is_optional, - depth + 1, - ); + let new_path = if path.is_empty() { + child.name.clone() + } else { + format!("{}.{}", path, child.name) + }; + collect_columns(table_prefix, &new_path, &child.ty, selections); } } Ty::Tuple(t) => { - let table_name = format!("{}${}", path, name); - - tables.push(TableInfo { - table_name: table_name.clone(), - parent_table: Some(path.to_string()), - // is_optional: parent_is_optional, - depth, - }); - for (i, child) in t.iter().enumerate() { - parse_ty( - &table_name, - &format!("_{}", i), - child, - selections, - tables, - arrays_queries, - _parent_is_optional, - depth + 1, - ); + let new_path = + if path.is_empty() { format!("{}", i) } else { format!("{}.{}", path, i) }; + collect_columns(table_prefix, &new_path, child, selections); } } - Ty::Array(t) => { - let table_name = format!("{}${}", path, name); - let is_optional = true; - - let mut array_selections = Vec::new(); - let mut array_tables = vec![TableInfo { - table_name: table_name.clone(), - parent_table: Some(path.to_string()), - // is_optional: true, - depth, - }]; - - parse_ty( - &table_name, - "data", - &t[0], - &mut array_selections, - &mut array_tables, - arrays_queries, - is_optional, - depth + 1, - ); - - arrays_queries.insert(table_name, (array_selections, array_tables)); - } Ty::Enum(e) => { - let table_name = format!("{}${}", path, name); - let is_optional = true; + // Add the enum variant column with table prefix and alias + selections.push(format!("[{table_prefix}].[{path}] as \"{table_prefix}.{path}\"",)); - let mut is_typed = false; + // Add columns for each variant's value (if not empty tuple) 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_optional, - depth + 1, - ); - is_typed = true; - } - - selections.push(format!("[{}].external_{} AS \"{}.{}\"", path, name, path, name)); - if is_typed { - tables.push(TableInfo { - table_name, - parent_table: Some(path.to_string()), - // is_optional: parent_is_optional || is_optional, - depth, - }); + let variant_path = format!("{}.{}", path, option.name); + collect_columns(table_prefix, &variant_path, &option.ty, selections); } } - _ => { - selections.push(format!("[{}].external_{} AS \"{}.{}\"", path, name, path, name)); + Ty::Array(_) | Ty::Primitive(_) | Ty::ByteArray(_) => { + selections.push(format!("[{table_prefix}].[{path}] as \"{table_prefix}.{path}\"",)); } } } - let mut global_selections = Vec::new(); - let mut global_tables = Vec::new(); - let mut arrays_queries: HashMap, Vec)> = HashMap::new(); + let mut selections = Vec::new(); + let mut joins = Vec::new(); - for model in schemas { - parse_ty( - "", - &model.name(), - model, - &mut global_selections, - &mut global_tables, - &mut arrays_queries, - false, - 0, - ); - } + // Add base table columns + selections.push(format!("{}.id", table_name)); + selections.push(format!("{}.keys", table_name)); - if global_tables.len() > 64 { - return Err(QueryError::SqliteJoinLimit.into()); + // Process each model schema + for model in schemas { + let model_table = model.name(); + joins.push(format!( + "LEFT JOIN [{model_table}] ON {table_name}.id = \ + [{model_table}].{entity_relation_column}", + )); + + // Collect columns with table prefix + collect_columns(&model_table, "", model, &mut selections); } - // Sort tables by depth to ensure proper join order - global_tables.sort_by_key(|table| table.depth); - - let selections_clause = global_selections.join(", "); - let join_clause = global_tables - .iter() - .map(|table| { - let join_condition = - format!("{entities_table}.id = [{}].{entity_relation_column}", table.table_name); - format!(" LEFT JOIN [{}] ON {join_condition}", table.table_name) - }) - .collect::>() - .join(" "); - - let mut formatted_arrays_queries: HashMap = arrays_queries - .into_iter() - .map(|(table, (selections, mut tables))| { - let mut selections_clause = selections.join(", "); - if !selections_clause.is_empty() { - selections_clause = format!(", {}", selections_clause); - } - - // Sort array tables by depth - tables.sort_by_key(|table| table.depth); - - let join_clause = tables - .iter() - .enumerate() - .map(|(i, table)| { - if i == 0 { - format!( - " JOIN [{}] ON {entities_table}.id = [{}].{entity_relation_column}", - table.table_name, table.table_name - ) - } else { - format!( - " LEFT JOIN [{}] ON [{}].full_array_id = [{}].full_array_id", - table.table_name, - table.table_name, - table.parent_table.as_ref().unwrap() - ) - } - }) - .collect::>() - .join(" "); + let selections_clause = selections.join(", "); + let joins_clause = joins.join(" "); - ( - table, - format!( - "SELECT {entities_table}.id, {entities_table}.keys{selections_clause} FROM \ - {entities_table}{join_clause}", - ), - ) - }) - .collect(); + let mut query = format!("SELECT {} FROM [{}] {}", selections_clause, table_name, joins_clause); - let mut query = format!( - "SELECT {entities_table}.id, {entities_table}.keys, {selections_clause} FROM \ - {entities_table}{join_clause}" - ); let mut count_query = - format!("SELECT COUNT({entities_table}.id) FROM {entities_table}{join_clause}"); + format!("SELECT COUNT(DISTINCT {}.id) FROM [{}] {}", table_name, table_name, joins_clause); if let Some(where_clause) = where_clause { query += &format!(" WHERE {}", where_clause); count_query += &format!(" WHERE {}", where_clause); } - query += &format!(" ORDER BY {entities_table}.event_id DESC"); + + query += &format!(" ORDER BY {}.event_id DESC", table_name); if let Some(limit) = limit { query += &format!(" LIMIT {}", limit); @@ -466,13 +206,7 @@ pub fn build_sql_query( query += &format!(" OFFSET {}", offset); } - 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, count_query)) + Ok((query, count_query)) } /// Populate the values of a Ty (schema) from SQLite row. @@ -482,34 +216,30 @@ pub fn map_row_to_ty( 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); + let column_name = if path.is_empty() { name } else { &format!("{}.{}", path, name) }; match ty { Ty::Primitive(primitive) => { match &primitive { Primitive::I8(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; primitive.set_i8(Some(value))?; } Primitive::I16(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; primitive.set_i16(Some(value))?; } Primitive::I32(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; primitive.set_i32(Some(value))?; } Primitive::I64(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; primitive.set_i64(Some(value))?; } Primitive::I128(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; let hex_str = value.trim_start_matches("0x"); if !hex_str.is_empty() { @@ -519,19 +249,19 @@ pub fn map_row_to_ty( } } Primitive::U8(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; primitive.set_u8(Some(value))?; } Primitive::U16(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; primitive.set_u16(Some(value))?; } Primitive::U32(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; primitive.set_u32(Some(value))?; } Primitive::U64(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; let hex_str = value.trim_start_matches("0x"); if !hex_str.is_empty() { @@ -541,7 +271,7 @@ pub fn map_row_to_ty( } } Primitive::U128(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; let hex_str = value.trim_start_matches("0x"); if !hex_str.is_empty() { @@ -551,7 +281,7 @@ pub fn map_row_to_ty( } } Primitive::U256(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; let hex_str = value.trim_start_matches("0x"); if !hex_str.is_empty() { @@ -559,15 +289,15 @@ pub fn map_row_to_ty( } } Primitive::USize(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; primitive.set_usize(Some(value))?; } Primitive::Bool(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; primitive.set_bool(Some(value))?; } Primitive::Felt252(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; if !value.is_empty() { primitive.set_felt252(Some( Felt::from_str(&value).map_err(ParseError::FromStr)?, @@ -575,7 +305,7 @@ pub fn map_row_to_ty( } } Primitive::ClassHash(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; if !value.is_empty() { primitive.set_class_hash(Some( Felt::from_str(&value).map_err(ParseError::FromStr)?, @@ -583,7 +313,7 @@ pub fn map_row_to_ty( } } Primitive::ContractAddress(_) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; if !value.is_empty() { primitive.set_contract_address(Some( Felt::from_str(&value).map_err(ParseError::FromStr)?, @@ -593,60 +323,46 @@ pub fn map_row_to_ty( }; } Ty::Enum(enum_ty) => { - let option_name = row.try_get::(&column_name)?; + let option_name = row.try_get::(column_name)?; if !option_name.is_empty() { enum_ty.set_option(&option_name)?; } - let path = [path, name].join("$"); for option in &mut enum_ty.options { if option.name != option_name { continue; } - map_row_to_ty(&path, &option.name, &mut option.ty, row, arrays_rows)?; + map_row_to_ty(column_name, &option.name, &mut option.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)?; + map_row_to_ty(column_name, &member.name, &mut member.ty, row)?; } } 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)?; + map_row_to_ty(column_name, &i.to_string(), member, row)?; } } 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::>(); + let schema = ty[0].clone(); + let serialized_array = row.try_get::(column_name)?; - // map each row to the ty of the array - let tys = rows + let values: Vec = + serde_json::from_str(&serialized_array).map_err(ParseError::FromJsonStr)?; + *ty = values .iter() - .map(|row| { - let mut ty = ty[0].clone(); - map_row_to_ty(&path, "data", &mut ty, row, arrays_rows).map(|_| ty) + .map(|v| { + let mut ty = schema.clone(); + ty.from_json_value(v.clone())?; + Result::<_, PrimitiveError>::Ok(ty) }) - .collect::, _>>()?; - - *ty = tys; + .collect::, _>>()?; } Ty::ByteArray(bytearray) => { - let value = row.try_get::(&column_name)?; + let value = row.try_get::(column_name)?; *bytearray = value; } }; @@ -658,316 +374,7 @@ pub fn map_row_to_ty( mod tests { use dojo_types::schema::{Enum, EnumOption, Member, Struct, Ty}; - use super::{build_sql_query, SqlModelMember}; - use crate::model::parse_sql_model_members; - - #[test] - fn parse_simple_model_members_to_ty() { - let model_members = vec![ - SqlModelMember { - id: "Test-Position".into(), - name: "x".into(), - r#type: "u256".into(), - key: false, - model_idx: 0, - member_idx: 0, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-Position".into(), - name: "y".into(), - r#type: "u256".into(), - key: false, - model_idx: 0, - member_idx: 1, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-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_position = Ty::Struct(Struct { - name: "Test-Position".into(), - children: vec![ - dojo_types::schema::Member { - name: "x".into(), - key: false, - ty: Ty::Primitive("u256".parse().unwrap()), - }, - dojo_types::schema::Member { - name: "y".into(), - key: false, - ty: Ty::Primitive("u256".parse().unwrap()), - }, - ], - }); - - let expected_player_config = Ty::Struct(Struct { - name: "Test-PlayerConfig".into(), - children: vec![dojo_types::schema::Member { - name: "name".into(), - key: false, - ty: Ty::ByteArray("".to_string()), - }], - }); - - assert_eq!(parse_sql_model_members("Test", "Position", &model_members), expected_position); - assert_eq!( - parse_sql_model_members("Test", "PlayerConfig", &model_members), - expected_player_config - ); - } - - #[test] - fn parse_complex_model_members_to_ty() { - let model_members = vec![ - SqlModelMember { - id: "Test-Position".into(), - name: "name".into(), - r#type: "felt252".into(), - key: false, - model_idx: 0, - member_idx: 0, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-Position".into(), - name: "age".into(), - r#type: "u8".into(), - key: false, - model_idx: 0, - member_idx: 1, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-Position".into(), - name: "vec".into(), - r#type: "Vec2".into(), - key: false, - model_idx: 0, - member_idx: 1, - type_enum: "Struct".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-Position$vec".into(), - name: "x".into(), - r#type: "u256".into(), - key: false, - model_idx: 1, - member_idx: 0, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-Position$vec".into(), - name: "y".into(), - r#type: "u256".into(), - key: false, - model_idx: 1, - member_idx: 1, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-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: "Test-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: "Test-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: "Test-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: "Test-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: "Test-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: "Test-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_position = Ty::Struct(Struct { - name: "Test-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()), - }, - dojo_types::schema::Member { - name: "vec".into(), - key: false, - ty: Ty::Struct(Struct { - name: "Vec2".into(), - children: vec![ - Member { - name: "x".into(), - key: false, - ty: Ty::Primitive("u256".parse().unwrap()), - }, - Member { - name: "y".into(), - key: false, - ty: Ty::Primitive("u256".parse().unwrap()), - }, - ], - }), - }, - ], - }); - - let expected_player_config = Ty::Struct(Struct { - name: "Test-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("Test", "Position", &model_members), expected_position); - assert_eq!( - parse_sql_model_members("Test", "PlayerConfig", &model_members), - expected_player_config - ); - } - - #[test] - fn parse_model_members_with_enum_to_ty() { - let model_members = vec![SqlModelMember { - id: "Test-Moves".into(), - name: "direction".into(), - r#type: "Direction".into(), - key: false, - model_idx: 0, - member_idx: 0, - type_enum: "Enum".into(), - enum_options: Some("Up,Down,Left,Right".into()), - }]; - - let expected_ty = Ty::Struct(Struct { - name: "Test-Moves".into(), - children: vec![dojo_types::schema::Member { - name: "direction".into(), - key: false, - ty: Ty::Enum(Enum { - name: "Direction".into(), - option: None, - options: vec![ - EnumOption { name: "Up".into(), ty: Ty::Tuple(vec![]) }, - EnumOption { name: "Down".into(), ty: Ty::Tuple(vec![]) }, - EnumOption { name: "Left".into(), ty: Ty::Tuple(vec![]) }, - EnumOption { name: "Right".into(), ty: Ty::Tuple(vec![]) }, - ], - }), - }], - }); - - assert_eq!(parse_sql_model_members("Test", "Moves", &model_members), expected_ty); - } + use super::build_sql_query; #[test] fn struct_ty_to_query() { @@ -1076,8 +483,7 @@ mod tests { let query = build_sql_query( &vec![position, player_config], "entities", - "entity_id", - None, + "internal_entity_id", None, None, None, @@ -1085,18 +491,16 @@ mod tests { .unwrap(); let expected_query = - "SELECT entities.id, entities.keys, [Test-Position].external_player AS \ - \"Test-Position.player\", [Test-Position$vec].external_x AS \"Test-Position$vec.x\", \ - [Test-Position$vec].external_y AS \"Test-Position$vec.y\", \ - [Test-PlayerConfig$favorite_item].external_Some AS \ - \"Test-PlayerConfig$favorite_item.Some\", [Test-PlayerConfig].external_favorite_item \ - AS \"Test-PlayerConfig.favorite_item\" FROM entities LEFT JOIN [Test-Position] ON \ - entities.id = [Test-Position].entity_id LEFT JOIN [Test-PlayerConfig] ON \ - entities.id = [Test-PlayerConfig].entity_id LEFT JOIN [Test-Position$vec] ON \ - entities.id = [Test-Position$vec].entity_id LEFT JOIN \ - [Test-PlayerConfig$favorite_item] ON entities.id = \ - [Test-PlayerConfig$favorite_item].entity_id ORDER BY entities.event_id DESC"; - // todo: completely tests arrays + "SELECT entities.id, entities.keys, [Test-Position].[player] as \ + \"Test-Position.player\", [Test-Position].[vec.x] as \"Test-Position.vec.x\", \ + [Test-Position].[vec.y] as \"Test-Position.vec.y\", \ + [Test-Position].[test_everything] as \"Test-Position.test_everything\", \ + [Test-PlayerConfig].[favorite_item] as \"Test-PlayerConfig.favorite_item\", \ + [Test-PlayerConfig].[favorite_item.Some] as \ + \"Test-PlayerConfig.favorite_item.Some\", [Test-PlayerConfig].[items] as \ + \"Test-PlayerConfig.items\" FROM [entities] LEFT JOIN [Test-Position] ON entities.id \ + = [Test-Position].internal_entity_id LEFT JOIN [Test-PlayerConfig] ON entities.id = \ + [Test-PlayerConfig].internal_entity_id ORDER BY entities.event_id DESC"; assert_eq!(query.0, expected_query); } } diff --git a/crates/torii/core/src/sql/cache.rs b/crates/torii/core/src/sql/cache.rs index 76ea4a0574..3d72bd093e 100644 --- a/crates/torii/core/src/sql/cache.rs +++ b/crates/torii/core/src/sql/cache.rs @@ -7,8 +7,7 @@ use starknet_crypto::Felt; use tokio::sync::RwLock; use crate::constants::TOKEN_BALANCE_TABLE; -use crate::error::{Error, ParseError, QueryError}; -use crate::model::{parse_sql_model_members, SqlModelMember}; +use crate::error::{Error, ParseError}; use crate::sql::utils::I256; use crate::types::ContractType; @@ -62,19 +61,18 @@ impl ModelCache { } async fn update_model(&self, selector: &Felt) -> Result { - let formatted_selector = format!("{:#x}", selector); - - let (namespace, name, class_hash, contract_address, packed_size, unpacked_size, layout): ( - String, - String, - String, - String, - u32, - u32, - String, - ) = sqlx::query_as( + let ( + namespace, + name, + class_hash, + contract_address, + packed_size, + unpacked_size, + layout, + schema, + ): (String, String, String, String, u32, u32, String, String) = sqlx::query_as( "SELECT namespace, name, class_hash, contract_address, packed_size, unpacked_size, \ - layout FROM models WHERE id = ?", + layout, schema FROM models WHERE id = ?", ) .bind(format!("{:#x}", selector)) .fetch_one(&self.pool) @@ -84,20 +82,8 @@ impl ModelCache { let contract_address = Felt::from_hex(&contract_address).map_err(ParseError::FromStr)?; let layout = serde_json::from_str(&layout).map_err(ParseError::FromJsonStr)?; + let schema = serde_json::from_str(&schema).map_err(ParseError::FromJsonStr)?; - let model_members: Vec = sqlx::query_as( - "SELECT id, model_idx, member_idx, name, type, type_enum, enum_options, key FROM \ - model_members WHERE model_id = ? ORDER BY model_idx ASC, member_idx ASC", - ) - .bind(formatted_selector) - .fetch_all(&self.pool) - .await?; - - if model_members.is_empty() { - return Err(QueryError::ModelNotFound(name.clone()).into()); - } - - let schema = parse_sql_model_members(&namespace, &name, &model_members); let mut cache = self.model_cache.write().await; let model = Model { diff --git a/crates/torii/core/src/sql/mod.rs b/crates/torii/core/src/sql/mod.rs index 5f7a8f5093..5a9ee03eb1 100644 --- a/crates/torii/core/src/sql/mod.rs +++ b/crates/torii/core/src/sql/mod.rs @@ -5,8 +5,7 @@ use std::sync::Arc; use anyhow::{anyhow, Context, Result}; use dojo_types::naming::get_tag; -use dojo_types::primitive::Primitive; -use dojo_types::schema::{EnumOption, Member, Struct, Ty}; +use dojo_types::schema::{Struct, Ty}; use dojo_world::config::WorldMetadata; use dojo_world::contracts::abigen::model::Layout; use dojo_world::contracts::naming::compute_selector_from_names; @@ -259,12 +258,16 @@ impl Sql { ) -> Result<()> { let selector = compute_selector_from_names(namespace, &model.name()); let namespaced_name = get_tag(namespace, &model.name()); + let namespaced_schema = Ty::Struct(Struct { + name: namespaced_name.clone(), + children: model.as_struct().unwrap().children.clone(), + }); let insert_models = "INSERT INTO models (id, namespace, name, class_hash, contract_address, layout, \ - packed_size, unpacked_size, executed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON \ - CONFLICT(id) DO UPDATE SET contract_address=EXCLUDED.contract_address, \ - class_hash=EXCLUDED.class_hash, layout=EXCLUDED.layout, \ + schema, packed_size, unpacked_size, executed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, \ + ?) ON CONFLICT(id) DO UPDATE SET contract_address=EXCLUDED.contract_address, \ + class_hash=EXCLUDED.class_hash, layout=EXCLUDED.layout, schema=EXCLUDED.schema, \ packed_size=EXCLUDED.packed_size, unpacked_size=EXCLUDED.unpacked_size, \ executed_at=EXCLUDED.executed_at RETURNING *"; let arguments = vec![ @@ -274,6 +277,7 @@ impl Sql { Argument::String(format!("{class_hash:#x}")), Argument::String(format!("{contract_address:#x}")), Argument::String(serde_json::to_string(&layout)?), + Argument::String(serde_json::to_string(&namespaced_schema)?), Argument::Int(packed_size as i64), Argument::Int(unpacked_size as i64), Argument::String(utc_dt_string_from_timestamp(block_timestamp)), @@ -284,17 +288,7 @@ impl Sql { QueryType::RegisterModel, ))?; - let mut model_idx = 0_i64; - self.build_register_queries_recursive( - selector, - model, - vec![namespaced_name.clone()], - &mut model_idx, - block_timestamp, - &mut 0, - &mut 0, - upgrade_diff, - )?; + self.build_model_query(vec![namespaced_name.clone()], model, upgrade_diff)?; // we set the model in the cache directly // because entities might be using it before the query queue is processed @@ -310,11 +304,7 @@ impl Sql { packed_size, unpacked_size, layout, - // we need to update the name of the struct to include the namespace - schema: Ty::Struct(Struct { - name: namespaced_name, - children: model.as_struct().unwrap().children.clone(), - }), + schema: namespaced_schema, }, ) .await; @@ -370,14 +360,12 @@ impl Sql { vec![Argument::String(entity_id.clone()), Argument::String(model_id.clone())], ))?; - let path = vec![namespaced_name]; - self.build_set_entity_queries_recursive( - path, + self.set_entity_model( + &namespaced_name, event_id, (&entity_id, false), (&entity, keys_str.is_none()), block_timestamp, - &vec![], )?; Ok(()) @@ -432,14 +420,12 @@ impl Sql { }), ))?; - let path = vec![namespaced_name]; - self.build_set_entity_queries_recursive( - path, + self.set_entity_model( + &namespaced_name, event_id, (&entity_id, true), (&entity, false), block_timestamp, - &vec![], )?; Ok(()) @@ -454,12 +440,14 @@ impl Sql { block_timestamp: u64, ) -> Result<()> { let entity_id = format!("{:#x}", entity_id); - let path = vec![entity.name()]; - // delete entity models data - self.build_delete_entity_queries_recursive(path, &entity_id, &entity)?; + let model_table = entity.name(); self.executor.send(QueryMessage::new( - "DELETE FROM entity_model WHERE entity_id = ? AND model_id = ?".to_string(), + format!( + "DELETE FROM [{model_table}] WHERE internal_id = ?; DELETE FROM entity_model \ + WHERE entity_id = ? AND model_id = ?" + ) + .to_string(), vec![Argument::String(entity_id.clone()), Argument::String(format!("{:#x}", model_id))], QueryType::DeleteEntity(DeleteEntityQuery { entity_id: entity_id.clone(), @@ -619,398 +607,134 @@ impl Sql { Ok(()) } - #[allow(clippy::too_many_arguments)] - fn build_register_queries_recursive( + fn set_entity_model( &mut self, - selector: Felt, - model: &Ty, - path: Vec, - model_idx: &mut i64, - block_timestamp: u64, - array_idx: &mut usize, - parent_array_idx: &mut usize, - upgrade_diff: Option<&Ty>, - ) -> Result<()> { - if let Ty::Enum(e) = model { - if e.options.iter().all(|o| if let Ty::Tuple(t) = &o.ty { t.is_empty() } else { false }) - { - return Ok(()); - } - } - - self.build_model_query( - selector, - path.clone(), - model, - *model_idx, - block_timestamp, - *array_idx, - *parent_array_idx, - upgrade_diff, - )?; - - let mut build_member = |pathname: &str, member: &Ty| -> Result<()> { - if let Ty::Primitive(_) = member { - return Ok(()); - } else if let Ty::ByteArray(_) = member { - return Ok(()); - } - - let mut path_clone = path.clone(); - path_clone.push(pathname.to_string()); - - self.build_register_queries_recursive( - selector, - member, - path_clone, - &mut (*model_idx + 1), - block_timestamp, - &mut (*array_idx + if let Ty::Array(_) = member { 1 } else { 0 }), - &mut (*parent_array_idx + if let Ty::Array(_) = model { 1 } else { 0 }), - // nested members are not upgrades - None, - )?; - - Ok(()) - }; - - if let Ty::Struct(s) = model { - for member in s.children.iter() { - build_member(&member.name, &member.ty)?; - } - } else if let Ty::Tuple(t) = model { - for (idx, member) in t.iter().enumerate() { - build_member(format!("_{}", idx).as_str(), member)?; - } - } else if let Ty::Array(array) = model { - let ty = &array[0]; - build_member("data", ty)?; - } else if let Ty::Enum(e) = model { - for child in e.options.iter() { - // Skip enum options that have no type / member - if let Ty::Tuple(t) = &child.ty { - if t.is_empty() { - continue; - } - } - - build_member(&child.name, &child.ty)?; - } - } - - Ok(()) - } - - fn build_set_entity_queries_recursive( - &mut self, - path: Vec, + model_name: &str, event_id: &str, - // The id of the entity and if the entity is an event message entity_id: (&str, IsEventMessage), entity: (&Ty, IsStoreUpdate), block_timestamp: u64, - indexes: &Vec, ) -> Result<()> { let (entity_id, is_event_message) = entity_id; - let (entity, is_store_update_member) = entity; - - let update_members = |members: &[Member], - executor: &mut UnboundedSender, - indexes: &Vec| - -> Result<()> { - let table_id = path.join("$"); - let mut columns = vec![ - "id".to_string(), - "event_id".to_string(), - "executed_at".to_string(), - "updated_at".to_string(), - if is_event_message { - "event_message_id".to_string() - } else { - "entity_id".to_string() - }, - ]; - - let mut arguments = vec![ - Argument::String(if is_event_message { - "event:".to_string() + entity_id - } else { - entity_id.to_string() - }), - Argument::String(event_id.to_string()), - Argument::String(utc_dt_string_from_timestamp(block_timestamp)), - Argument::String(chrono::Utc::now().to_rfc3339()), - 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(SQL_FELT_DELIMITER), - )); - } - - for (column_idx, idx) in indexes.iter().enumerate() { - columns.push(format!("idx_{}", column_idx)); - arguments.push(Argument::Int(*idx)); - } - - for member in members.iter() { - match &member.ty { - Ty::Primitive(ty) => { - columns.push(format!("external_{}", &member.name)); - arguments.push(Argument::String(ty.to_sql_value())); - } - Ty::Enum(e) => { - columns.push(format!("external_{}", &member.name)); - arguments.push(Argument::String(e.to_sql_value())); - } - Ty::ByteArray(b) => { - columns.push(format!("external_{}", &member.name)); - arguments.push(Argument::String(b.clone())); - } - _ => {} - } - } - - let placeholders: Vec<&str> = arguments.iter().map(|_| "?").collect(); - let statement = if is_store_update_member && indexes.is_empty() { - arguments.push(Argument::String(if is_event_message { - "event:".to_string() + entity_id - } else { - entity_id.to_string() - })); - - // row has to exist. update it directly - format!( - "UPDATE [{table_id}] SET {updates} WHERE id = ?", - table_id = table_id, - updates = columns - .iter() - .zip(placeholders.iter()) - .map(|(column, placeholder)| format!("{} = {}", column, placeholder)) - .collect::>() - .join(", ") - ) + let (entity, is_store_update) = entity; + + let mut columns = vec![ + "internal_id".to_string(), + "internal_event_id".to_string(), + "internal_executed_at".to_string(), + "internal_updated_at".to_string(), + if is_event_message { + "internal_event_message_id".to_string() } else { - format!( - "INSERT OR REPLACE INTO [{table_id}] ({}) VALUES ({})", - columns.join(","), - placeholders.join(",") - ) - }; - - executor.send(QueryMessage::other(statement, arguments))?; + "internal_entity_id".to_string() + }, + ]; - Ok(()) - }; + let mut arguments = vec![ + Argument::String(if is_event_message { + "event:".to_string() + entity_id + } else { + entity_id.to_string() + }), + Argument::String(event_id.to_string()), + Argument::String(utc_dt_string_from_timestamp(block_timestamp)), + Argument::String(chrono::Utc::now().to_rfc3339()), + Argument::String(entity_id.to_string()), + ]; - match entity { - Ty::Struct(s) => { - update_members(&s.children, &mut self.executor, indexes)?; - - for member in s.children.iter() { - let mut path_clone = path.clone(); - path_clone.push(member.name.clone()); - self.build_set_entity_queries_recursive( - path_clone, - event_id, - (entity_id, is_event_message), - (&member.ty, is_store_update_member), - block_timestamp, - indexes, - )?; + fn collect_members( + prefix: &str, + ty: &Ty, + columns: &mut Vec, + arguments: &mut Vec, + ) -> Result<()> { + match ty { + Ty::Struct(s) => { + for member in &s.children { + let column_name = if prefix.is_empty() { + member.name.clone() + } else { + format!("{}.{}", prefix, member.name) + }; + collect_members(&column_name, &member.ty, columns, arguments)?; + } } - } - Ty::Enum(e) => { - if e.options.iter().all( - |o| { - if let Ty::Tuple(t) = &o.ty { t.is_empty() } else { false } - }, - ) { - return Ok(()); + Ty::Enum(e) => { + columns.push(format!("\"{}\"", prefix)); + arguments.push(Argument::String(e.to_sql_value())); + + if let Some(option_idx) = e.option { + let option = &e.options[option_idx as usize]; + if let Ty::Tuple(t) = &option.ty { + if t.is_empty() { + return Ok(()); + } + } + let variant_path = format!("{}.{}", prefix, option.name); + collect_members(&variant_path, &option.ty, columns, arguments)?; + } } - - let option = e.options[e.option.unwrap() as usize].clone(); - - update_members( - &[ - Member { name: "option".to_string(), ty: Ty::Enum(e.clone()), key: false }, - Member { name: option.name.clone(), ty: option.ty.clone(), key: false }, - ], - &mut self.executor, - indexes, - )?; - - match &option.ty { - // Skip enum options that have no type / member - Ty::Tuple(t) if t.is_empty() => {} - _ => { - let mut path_clone = path.clone(); - path_clone.push(option.name.clone()); - self.build_set_entity_queries_recursive( - path_clone, - event_id, - (entity_id, is_event_message), - (&option.ty, is_store_update_member), - block_timestamp, - indexes, - )?; + Ty::Tuple(t) => { + for (idx, member) in t.iter().enumerate() { + let column_name = if prefix.is_empty() { + format!("{}", idx) + } else { + format!("{}.{}", prefix, idx) + }; + collect_members(&column_name, member, columns, arguments)?; } } - } - Ty::Tuple(t) => { - update_members( - t.iter() - .enumerate() - .map(|(idx, member)| Member { - name: format!("_{}", idx), - ty: member.clone(), - key: false, - }) - .collect::>() - .as_slice(), - &mut self.executor, - indexes, - )?; - - for (idx, member) in t.iter().enumerate() { - let mut path_clone = path.clone(); - path_clone.push(format!("_{}", idx)); - self.build_set_entity_queries_recursive( - path_clone, - event_id, - (entity_id, is_event_message), - (member, is_store_update_member), - block_timestamp, - indexes, - )?; + Ty::Array(array) => { + columns.push(format!("\"{}\"", prefix)); + let values = + array.iter().map(|v| v.to_json_value()).collect::, _>>()?; + arguments.push(Argument::String(serde_json::to_string(&values)?)); } - } - Ty::Array(array) => { - // delete all previous array elements with the array indexes - let table_id = path.join("$"); - let mut query = - format!("DELETE FROM [{table_id}] WHERE entity_id = ? ", table_id = table_id); - for idx in 0..indexes.len() { - query.push_str(&format!("AND idx_{} = ? ", idx)); + Ty::Primitive(ty) => { + columns.push(format!("\"{}\"", prefix)); + arguments.push(Argument::String(ty.to_sql_value())); } - - // flatten indexes with entity id - let mut arguments = vec![Argument::String(entity_id.to_string())]; - arguments.extend(indexes.iter().map(|idx| Argument::Int(*idx))); - - self.executor.send(QueryMessage::other(query, arguments))?; - - // insert the new array elements - for (idx, member) in array.iter().enumerate() { - let mut indexes = indexes.clone(); - indexes.push(idx as i64); - - update_members( - &[Member { name: "data".to_string(), ty: member.clone(), key: false }], - &mut self.executor, - &indexes, - )?; - - let mut path_clone = path.clone(); - path_clone.push("data".to_string()); - self.build_set_entity_queries_recursive( - path_clone, - event_id, - (entity_id, is_event_message), - (member, is_store_update_member), - block_timestamp, - &indexes, - )?; + Ty::ByteArray(b) => { + columns.push(format!("\"{}\"", prefix)); + arguments.push(Argument::String(b.clone())); } } - _ => {} + Ok(()) } - Ok(()) - } + // Collect all columns and arguments recursively + collect_members("", entity, &mut columns, &mut arguments)?; - fn build_delete_entity_queries_recursive( - &mut self, - path: Vec, - entity_id: &str, - entity: &Ty, - ) -> Result<()> { - match entity { - Ty::Struct(s) => { - let table_id = path.join("$"); - let statement = format!("DELETE FROM [{table_id}] WHERE entity_id = ?"); - self.executor.send(QueryMessage::other( - statement, - vec![Argument::String(entity_id.to_string())], - ))?; - for member in s.children.iter() { - let mut path_clone = path.clone(); - path_clone.push(member.name.clone()); - self.build_delete_entity_queries_recursive(path_clone, entity_id, &member.ty)?; - } - } - Ty::Enum(e) => { - if e.options - .iter() - .all(|o| if let Ty::Tuple(t) = &o.ty { t.is_empty() } else { false }) - { - return Ok(()); - } + // Build the final query + let placeholders: Vec<&str> = arguments.iter().map(|_| "?").collect(); + let statement = if is_store_update { + arguments.push(Argument::String(if is_event_message { + "event:".to_string() + entity_id + } else { + entity_id.to_string() + })); - let table_id = path.join("$"); - let statement = format!("DELETE FROM [{table_id}] WHERE entity_id = ?"); - self.executor.send(QueryMessage::other( - statement, - vec![Argument::String(entity_id.to_string())], - ))?; - - for child in e.options.iter() { - if let Ty::Tuple(t) = &child.ty { - if t.is_empty() { - continue; - } - } + format!( + "UPDATE [{}] SET {} WHERE internal_id = ?", + model_name, + columns + .iter() + .zip(placeholders.iter()) + .map(|(column, placeholder)| format!("{} = {}", column, placeholder)) + .collect::>() + .join(", ") + ) + } else { + format!( + "INSERT OR REPLACE INTO [{}] ({}) VALUES ({})", + model_name, + columns.join(","), + placeholders.join(",") + ) + }; - let mut path_clone = path.clone(); - path_clone.push(child.name.clone()); - self.build_delete_entity_queries_recursive(path_clone, entity_id, &child.ty)?; - } - } - Ty::Array(array) => { - let table_id = path.join("$"); - let statement = format!("DELETE FROM [{table_id}] WHERE entity_id = ?"); - self.executor.send(QueryMessage::other( - statement, - vec![Argument::String(entity_id.to_string())], - ))?; - - for member in array.iter() { - let mut path_clone = path.clone(); - path_clone.push("data".to_string()); - self.build_delete_entity_queries_recursive(path_clone, entity_id, member)?; - } - } - Ty::Tuple(t) => { - let table_id = path.join("$"); - let statement = format!("DELETE FROM [{table_id}] WHERE entity_id = ?"); - self.executor.send(QueryMessage::other( - statement, - vec![Argument::String(entity_id.to_string())], - ))?; - - for (idx, member) in t.iter().enumerate() { - let mut path_clone = path.clone(); - path_clone.push(format!("_{}", idx)); - self.build_delete_entity_queries_recursive(path_clone, entity_id, member)?; - } - } - _ => {} - } + // Execute the single query + self.executor.send(QueryMessage::other(statement, arguments))?; Ok(()) } @@ -1018,271 +742,51 @@ impl Sql { #[allow(clippy::too_many_arguments)] fn build_model_query( &mut self, - selector: Felt, path: Vec, model: &Ty, - model_idx: i64, - block_timestamp: u64, - array_idx: usize, - parent_array_idx: usize, upgrade_diff: Option<&Ty>, ) -> Result<()> { - let table_id = path.join("$"); + let table_id = path[0].clone(); // Use only the root path component + let mut columns = Vec::new(); let mut indices = Vec::new(); + let mut alter_table_queries = Vec::new(); + // Start building the create table query with internal columns let mut create_table_query = format!( - "CREATE TABLE IF NOT EXISTS [{table_id}] (id TEXT NOT NULL, event_id TEXT NOT NULL, \ - entity_id TEXT, event_message_id TEXT, " + "CREATE TABLE IF NOT EXISTS [{table_id}] (internal_id TEXT NOT NULL PRIMARY KEY, \ + internal_event_id TEXT NOT NULL, internal_entity_id TEXT, internal_event_message_id \ + TEXT, " ); - let mut alter_table_queries = Vec::new(); - - if array_idx > 0 { - // index columns - for i in 0..array_idx { - let column = format!("idx_{i} INTEGER NOT NULL"); - create_table_query.push_str(&format!("{column}, ")); - - alter_table_queries.push(format!( - "ALTER TABLE [{table_id}] ADD COLUMN idx_{i} INTEGER NOT NULL DEFAULT 0" - )); - } - - // full array id column - create_table_query.push_str("full_array_id TEXT NOT NULL UNIQUE, "); - alter_table_queries.push(format!( - "ALTER TABLE [{table_id}] ADD COLUMN full_array_id TEXT NOT NULL UNIQUE DEFAULT ''" - )); - } - - let mut build_member = |name: &str, ty: &Ty, options: &mut Option| { - if let Ok(cairo_type) = Primitive::from_str(&ty.name()) { - let sql_type = cairo_type.to_sql_type(); - let column = format!("external_{name} {sql_type}"); - - create_table_query.push_str(&format!("{column}, ")); - - alter_table_queries.push(format!( - "ALTER TABLE [{table_id}] ADD COLUMN external_{name} {sql_type}" - )); - - indices.push(format!( - "CREATE INDEX IF NOT EXISTS [idx_{table_id}_{name}] ON [{table_id}] \ - (external_{name});" - )); - } else if let Ty::Enum(e) = &ty { - let all_options = e - .options - .iter() - .map(|c| format!("'{}'", c.name)) - .collect::>() - .join(", "); - - let column = - format!("external_{name} TEXT CHECK(external_{name} IN ({all_options}))",); - - create_table_query.push_str(&format!("{column}, ")); - - alter_table_queries.push(format!("ALTER TABLE [{table_id}] ADD COLUMN {column}")); - - indices.push(format!( - "CREATE INDEX IF NOT EXISTS [idx_{table_id}_{name}] ON [{table_id}] \ - (external_{name});" - )); - - *options = Some(Argument::String( - e.options - .iter() - .map(|c: &dojo_types::schema::EnumOption| c.name.clone()) - .collect::>() - .join(",") - .to_string(), - )); - } else if let Ty::ByteArray(_) = &ty { - let column = format!("external_{name} TEXT"); - - create_table_query.push_str(&format!("{column}, ")); - - alter_table_queries.push(format!("ALTER TABLE [{table_id}] ADD COLUMN {column}")); - - indices.push(format!( - "CREATE INDEX IF NOT EXISTS [idx_{table_id}_{name}] ON [{table_id}] \ - (external_{name});" - )); - } - }; - - match model { - Ty::Struct(s) => { - for (member_idx, member) in s.children.iter().enumerate() { - if let Some(upgrade_diff) = upgrade_diff { - if !upgrade_diff - .as_struct() - .unwrap() - .children - .iter() - .any(|m| m.name == member.name) - { - continue; - } - } - - let name = member.name.clone(); - let mut options = None; // TEMP: doesnt support complex enums yet - - build_member(&name, &member.ty, &mut options); - - // NOTE: this might cause some errors to fail silently - // due to the ignore clause. check migrations for type_enum check - let statement = "INSERT OR IGNORE INTO model_members (id, model_id, \ - model_idx, member_idx, name, type, type_enum, enum_options, \ - key, executed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - - let arguments = vec![ - Argument::String(table_id.clone()), - // TEMP: this is temporary until the model hash is precomputed - Argument::String(format!("{:#x}", selector)), - Argument::Int(model_idx), - Argument::Int(member_idx as i64), - Argument::String(name), - Argument::String(member.ty.name()), - Argument::String(member.ty.as_ref().into()), - options.unwrap_or(Argument::Null), - Argument::Bool(member.key), - Argument::String(utc_dt_string_from_timestamp(block_timestamp)), - ]; - - self.executor.send(QueryMessage::other(statement.to_string(), arguments))?; - } - } - Ty::Tuple(tuple) => { - for (idx, member) in tuple.iter().enumerate() { - let mut options = None; // TEMP: doesnt support complex enums yet - - build_member(&format!("_{}", idx), member, &mut options); - - let statement = "INSERT OR IGNORE INTO model_members (id, model_id, \ - model_idx, member_idx, name, type, type_enum, enum_options, \ - key, executed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - let arguments = vec![ - Argument::String(table_id.clone()), - // TEMP: this is temporary until the model hash is precomputed - Argument::String(format!("{:#x}", selector)), - Argument::Int(model_idx), - Argument::Int(idx as i64), - Argument::String(format!("_{}", idx)), - Argument::String(member.name()), - Argument::String(member.as_ref().into()), - options.unwrap_or(Argument::Null), - // NOTE: should we consider the case where - // a tuple is used as a key? should its members be keys? - Argument::Bool(false), - Argument::String(utc_dt_string_from_timestamp(block_timestamp)), - ]; - - self.executor.send(QueryMessage::other(statement.to_string(), arguments))?; - } - } - Ty::Array(array) => { - let mut options = None; // TEMP: doesnt support complex enums yet - let ty = &array[0]; - build_member("data", ty, &mut options); - - let statement = "INSERT OR IGNORE INTO model_members (id, model_id, model_idx, \ - member_idx, name, type, type_enum, enum_options, key, \ - executed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - let arguments = vec![ - Argument::String(table_id.clone()), - // TEMP: this is temporary until the model hash is precomputed - Argument::String(format!("{:#x}", selector)), - Argument::Int(model_idx), - Argument::Int(0), - Argument::String("data".to_string()), - Argument::String(ty.name()), - Argument::String(ty.as_ref().into()), - options.unwrap_or(Argument::Null), - Argument::Bool(false), - Argument::String(utc_dt_string_from_timestamp(block_timestamp)), - ]; - - self.executor.send(QueryMessage::other(statement.to_string(), arguments))?; - } - Ty::Enum(e) => { - for (idx, child) in e - .options - .iter() - .chain(vec![&EnumOption { - name: "option".to_string(), - ty: Ty::Enum(e.clone()), - }]) - .enumerate() - { - // Skip enum options that have no type / member - if let Ty::Tuple(tuple) = &child.ty { - if tuple.is_empty() { - continue; - } - } + // Recursively add columns for all nested type + add_columns_recursive( + &path, + model, + &mut columns, + &mut alter_table_queries, + &mut indices, + &table_id, + upgrade_diff, + )?; - let mut options = None; // TEMP: doesnt support complex enums yet - build_member(&child.name, &child.ty, &mut options); - - let statement = "INSERT OR IGNORE INTO model_members (id, model_id, \ - model_idx, member_idx, name, type, type_enum, enum_options, \ - key, executed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - let arguments = vec![ - Argument::String(table_id.clone()), - // TEMP: this is temporary until the model hash is precomputed - Argument::String(format!("{:#x}", selector)), - Argument::Int(model_idx), - Argument::Int(idx as i64), - Argument::String(child.name.clone()), - Argument::String(child.ty.name()), - Argument::String(child.ty.as_ref().into()), - options.unwrap_or(Argument::Null), - Argument::Bool(false), - Argument::String(utc_dt_string_from_timestamp(block_timestamp)), - ]; - - self.executor.send(QueryMessage::other(statement.to_string(), arguments))?; - } - } - _ => {} + // Add all columns to the create table query + for column in columns { + create_table_query.push_str(&format!("{}, ", column)); } - create_table_query.push_str("executed_at DATETIME NOT NULL, "); - create_table_query.push_str("created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "); - create_table_query.push_str("updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "); - - // If this is not the Model's root table, create a reference to the parent. - if path.len() > 1 { - let parent_table_id = path[..path.len() - 1].join("$"); - - create_table_query.push_str("FOREIGN KEY (id"); - for i in 0..parent_array_idx { - create_table_query.push_str(&format!(", idx_{i}", i = i)); - } - create_table_query.push_str(&format!( - ") REFERENCES [{parent_table_id}] (id", - parent_table_id = parent_table_id - )); - for i in 0..parent_array_idx { - create_table_query.push_str(&format!(", idx_{i}", i = i)); - } - create_table_query.push_str(") ON DELETE CASCADE, "); - }; - - create_table_query.push_str("PRIMARY KEY (id"); - for i in 0..array_idx { - create_table_query.push_str(&format!(", idx_{i}", i = i)); - } - create_table_query.push_str("), "); + // Add internal timestamps + create_table_query.push_str("internal_executed_at DATETIME NOT NULL, "); + create_table_query + .push_str("internal_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "); + create_table_query + .push_str("internal_updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "); - create_table_query.push_str("FOREIGN KEY (entity_id) REFERENCES entities(id), "); - // create_table_query.push_str("FOREIGN KEY (event_id) REFERENCES events(id), "); + // Add foreign key constraints + create_table_query.push_str("FOREIGN KEY (internal_entity_id) REFERENCES entities(id), "); create_table_query - .push_str("FOREIGN KEY (event_message_id) REFERENCES event_messages(id));"); + .push_str("FOREIGN KEY (internal_event_message_id) REFERENCES event_messages(id));"); + // Execute the queries if upgrade_diff.is_some() { for alter_query in alter_table_queries { self.executor.send(QueryMessage::other(alter_query, vec![]))?; @@ -1291,6 +795,7 @@ impl Sql { self.executor.send(QueryMessage::other(create_table_query, vec![]))?; } + // Create indices for index_query in indices { self.executor.send(QueryMessage::other(index_query, vec![]))?; } @@ -1316,3 +821,126 @@ impl Sql { recv.await? } } + +fn add_columns_recursive( + path: &[String], + ty: &Ty, + columns: &mut Vec, + alter_table_queries: &mut Vec, + indices: &mut Vec, + table_id: &str, + upgrade_diff: Option<&Ty>, +) -> Result<()> { + let column_prefix = if path.len() > 1 { path[1..].join(".") } else { String::new() }; + + let mut add_column = |name: &str, sql_type: &str| { + if upgrade_diff.is_some() { + alter_table_queries + .push(format!("ALTER TABLE [{table_id}] ADD COLUMN [{name}] {sql_type}")); + } else { + columns.push(format!("[{name}] {sql_type}")); + } + indices.push(format!( + "CREATE INDEX IF NOT EXISTS [idx_{table_id}_{name}] ON [{table_id}] ([{name}]);" + )); + }; + + match ty { + Ty::Struct(s) => { + for member in &s.children { + if let Some(upgrade_diff) = upgrade_diff { + if !upgrade_diff + .as_struct() + .unwrap() + .children + .iter() + .any(|m| m.name == member.name) + { + continue; + } + } + + let mut new_path = path.to_vec(); + new_path.push(member.name.clone()); + + add_columns_recursive( + &new_path, + &member.ty, + columns, + alter_table_queries, + indices, + table_id, + None, + )?; + } + } + Ty::Tuple(tuple) => { + for (idx, member) in tuple.iter().enumerate() { + let mut new_path = path.to_vec(); + new_path.push(idx.to_string()); + + add_columns_recursive( + &new_path, + member, + columns, + alter_table_queries, + indices, + table_id, + None, + )?; + } + } + Ty::Array(_) => { + let column_name = + if column_prefix.is_empty() { "value".to_string() } else { column_prefix }; + + add_column(&column_name, "TEXT"); + } + Ty::Enum(e) => { + // The variant of the enum + let column_name = + if column_prefix.is_empty() { "option".to_string() } else { column_prefix }; + + let all_options = + e.options.iter().map(|c| format!("'{}'", c.name)).collect::>().join(", "); + + let sql_type = format!("TEXT CHECK([{column_name}] IN ({all_options}))"); + add_column(&column_name, &sql_type); + + for child in &e.options { + if let Ty::Tuple(tuple) = &child.ty { + if tuple.is_empty() { + continue; + } + } + + let mut new_path = path.to_vec(); + new_path.push(child.name.clone()); + + add_columns_recursive( + &new_path, + &child.ty, + columns, + alter_table_queries, + indices, + table_id, + None, + )?; + } + } + Ty::ByteArray(_) => { + let column_name = + if column_prefix.is_empty() { "value".to_string() } else { column_prefix }; + + add_column(&column_name, "TEXT"); + } + Ty::Primitive(p) => { + let column_name = + if column_prefix.is_empty() { "value".to_string() } else { column_prefix }; + + add_column(&column_name, p.to_sql_type().as_ref()); + } + } + + Ok(()) +} diff --git a/crates/torii/core/src/types.rs b/crates/torii/core/src/types.rs index fef378f162..d56d33cb50 100644 --- a/crates/torii/core/src/types.rs +++ b/crates/torii/core/src/types.rs @@ -107,6 +107,8 @@ pub struct Model { pub class_hash: String, pub contract_address: String, pub transaction_hash: String, + pub layout: String, + pub schema: String, pub executed_at: DateTime, pub created_at: DateTime, } diff --git a/crates/torii/graphql/src/constants.rs b/crates/torii/graphql/src/constants.rs index f309b82b91..fc7d7c0285 100644 --- a/crates/torii/graphql/src/constants.rs +++ b/crates/torii/graphql/src/constants.rs @@ -12,8 +12,8 @@ pub const METADATA_TABLE: &str = "metadata"; pub const ID_COLUMN: &str = "id"; pub const EVENT_ID_COLUMN: &str = "event_id"; -pub const ENTITY_ID_COLUMN: &str = "entity_id"; -pub const EVENT_MESSAGE_ID_COLUMN: &str = "event_message_id"; +pub const ENTITY_ID_COLUMN: &str = "internal_entity_id"; +pub const EVENT_MESSAGE_ID_COLUMN: &str = "internal_event_message_id"; pub const JSON_COLUMN: &str = "json"; pub const TRANSACTION_HASH_COLUMN: &str = "transaction_hash"; diff --git a/crates/torii/graphql/src/object/connection/mod.rs b/crates/torii/graphql/src/object/connection/mod.rs index dc9931ac58..540a59c574 100644 --- a/crates/torii/graphql/src/object/connection/mod.rs +++ b/crates/torii/graphql/src/object/connection/mod.rs @@ -111,13 +111,15 @@ pub fn connection_arguments(field: Field) -> Field { .argument(InputValue::new("limit", TypeRef::named(TypeRef::INT))) } +#[allow(clippy::too_many_arguments)] pub fn connection_output( data: &[SqliteRow], types: &TypeMapping, order: &Option, id_column: &str, total_count: i64, - is_external: bool, + is_internal: bool, + snake_case: bool, page_info: PageInfo, ) -> sqlx::Result { let model_edges = data @@ -125,8 +127,8 @@ pub fn connection_output( .map(|row| { let order_field = match order { Some(order) => { - if is_external { - format!("external_{}", order.field) + if is_internal { + format!("internal_{}", order.field) } else { order.field.to_string() } @@ -136,7 +138,7 @@ pub fn connection_output( let primary_order = row.try_get::(id_column)?; let secondary_order = row.try_get_unchecked::(&order_field)?; let cursor = cursor::encode(&primary_order, &secondary_order); - let value_mapping = value_mapping_from_row(row, types, is_external)?; + let value_mapping = value_mapping_from_row(row, types, is_internal, snake_case)?; let mut edge = ValueMapping::new(); edge.insert(Name::new("node"), Value::Object(value_mapping)); diff --git a/crates/torii/graphql/src/object/entity.rs b/crates/torii/graphql/src/object/entity.rs index 357b428979..b99935f29c 100644 --- a/crates/torii/graphql/src/object/entity.rs +++ b/crates/torii/graphql/src/object/entity.rs @@ -3,9 +3,8 @@ use async_graphql::dynamic::{ Field, FieldFuture, FieldValue, InputValue, SubscriptionField, SubscriptionFieldFuture, TypeRef, }; use async_graphql::{Name, Value}; -use async_recursion::async_recursion; use dojo_types::naming::get_tag; -use sqlx::pool::PoolConnection; +use dojo_types::schema::Ty; use sqlx::{Pool, Sqlite}; use tokio_stream::StreamExt; use torii_core::simple_broker::SimpleBroker; @@ -14,13 +13,11 @@ use torii_core::types::Entity; use super::inputs::keys_input::keys_argument; use super::{BasicObject, ResolvableObject, TypeMapping, ValueMapping}; use crate::constants::{ - DATETIME_FORMAT, ENTITY_ID_COLUMN, ENTITY_NAMES, ENTITY_TABLE, ENTITY_TYPE_NAME, - EVENT_ID_COLUMN, ID_COLUMN, + DATETIME_FORMAT, ENTITY_NAMES, ENTITY_TABLE, ENTITY_TYPE_NAME, EVENT_ID_COLUMN, ID_COLUMN, }; use crate::mapping::ENTITY_TYPE_MAPPING; use crate::object::{resolve_many, resolve_one}; -use crate::query::{type_mapping_query, value_mapping_from_row}; -use crate::types::TypeData; +use crate::query::{build_type_mapping, value_mapping_from_row}; use crate::utils; #[derive(Debug)] pub struct EntityObject; @@ -124,7 +121,7 @@ fn model_union_field() -> Field { // fetch name from the models table // using the model id (hashed model name) let model_ids: Vec<(String, String, String)> = sqlx::query_as( - "SELECT id, namespace, name + "SELECT namespace, name, schema FROM models WHERE id IN ( SELECT model_id @@ -137,25 +134,23 @@ fn model_union_field() -> Field { .await?; let mut results: Vec> = Vec::new(); - for (id, namespace, name) in model_ids { - // the model id in the model mmeebrs table is the hashed model name (id) - let type_mapping = type_mapping_query(&mut conn, &id).await?; + for (namespace, name, schema) in model_ids { + let schema: Ty = serde_json::from_str(&schema).map_err(|e| { + anyhow::anyhow!(format!("Failed to parse model schema: {e}")) + })?; + let type_mapping = build_type_mapping(&namespace, &schema); - // but the table name for the model data is the unhashed model name - let data: ValueMapping = match model_data_recursive_query( - &mut conn, - ENTITY_ID_COLUMN, - vec![get_tag(&namespace, &name)], - &entity_id, - &[], - &type_mapping, - false, - ) - .await? - { - Value::Object(map) => map, - _ => unreachable!(), - }; + // Get the table name + let table_name = get_tag(&namespace, &name); + + // Fetch the row data + let query = + format!("SELECT * FROM [{}] WHERE internal_entity_id = ?", table_name); + let row = + sqlx::query(&query).bind(&entity_id).fetch_one(&mut *conn).await?; + + // Use value_mapping_from_row to handle nested structures + let data = value_mapping_from_row(&row, &type_mapping, false, false)?; results.push(FieldValue::with_type( FieldValue::owned_any(data), @@ -170,116 +165,3 @@ fn model_union_field() -> Field { }) }) } - -// TODO: flatten query -#[async_recursion] -pub async fn model_data_recursive_query( - conn: &mut PoolConnection, - entity_id_column: &str, - path_array: Vec, - entity_id: &str, - indexes: &[i64], - type_mapping: &TypeMapping, - is_list: bool, -) -> sqlx::Result { - // For nested types, we need to remove prefix in path array - let namespace = format!("{}_", path_array[0]); - let table_name = &path_array.join("$").replace(&namespace, ""); - let mut query = - format!("SELECT * FROM [{}] WHERE {entity_id_column} = '{}' ", table_name, entity_id); - for (column_idx, index) in indexes.iter().enumerate() { - query.push_str(&format!("AND idx_{} = {} ", column_idx, index)); - } - - let rows = sqlx::query(&query).fetch_all(conn.as_mut()).await?; - if rows.is_empty() { - return Ok(Value::List(vec![])); - } - - let value_mapping: Value; - let mut nested_value_mappings = Vec::new(); - - for (idx, row) in rows.iter().enumerate() { - let mut nested_value_mapping = value_mapping_from_row(row, type_mapping, true)?; - - for (field_name, type_data) in type_mapping { - if let TypeData::Nested((_, nested_mapping)) = type_data { - let mut nested_path = path_array.clone(); - nested_path.push(field_name.to_string()); - - let nested_values = model_data_recursive_query( - conn, - entity_id_column, - nested_path, - entity_id, - &if is_list { - let mut indexes = indexes.to_vec(); - indexes.push(idx as i64); - indexes - } else { - indexes.to_vec() - }, - nested_mapping, - false, - ) - .await?; - - nested_value_mapping.insert(Name::new(field_name), nested_values); - } else if let TypeData::List(inner) = type_data { - let mut nested_path = path_array.clone(); - nested_path.push(field_name.to_string()); - - let data = match model_data_recursive_query( - conn, - entity_id_column, - nested_path, - entity_id, - // this might need to be changed to support 2d+ arrays - &if is_list { - let mut indexes = indexes.to_vec(); - indexes.push(idx as i64); - indexes - } else { - indexes.to_vec() - }, - &IndexMap::from([(Name::new("data"), *inner.clone())]), - true, - ) - .await? - { - // map our list which uses a data field as a place holder - // for all elements to get the elemnt directly - Value::List(data) => data - .iter() - .map(|v| match v { - Value::Object(map) => map.get(&Name::new("data")).unwrap().clone(), - ty => unreachable!( - "Expected Value::Object for list \"data\" field, got {:?}", - ty - ), - }) - .collect(), - Value::Object(map) => map.get(&Name::new("data")).unwrap().clone(), - ty => { - unreachable!( - "Expected Value::List or Value::Object for list, got {:?}", - ty - ); - } - }; - - nested_value_mapping.insert(Name::new(field_name), data); - } - } - - nested_value_mappings.push(Value::Object(nested_value_mapping)); - } - - if is_list { - value_mapping = Value::List(nested_value_mappings); - } else { - value_mapping = nested_value_mappings.pop().unwrap(); - } - - Ok(value_mapping) -} diff --git a/crates/torii/graphql/src/object/event_message.rs b/crates/torii/graphql/src/object/event_message.rs index 2173140bd2..155db80f95 100644 --- a/crates/torii/graphql/src/object/event_message.rs +++ b/crates/torii/graphql/src/object/event_message.rs @@ -4,22 +4,23 @@ use async_graphql::dynamic::{ }; use async_graphql::{Name, Value}; use dojo_types::naming::get_tag; +use dojo_types::schema::Ty; use sqlx::{Pool, Sqlite}; use tokio_stream::StreamExt; use torii_core::simple_broker::SimpleBroker; use torii_core::types::EventMessage; -use super::entity::model_data_recursive_query; use super::inputs::keys_input::keys_argument; use super::{BasicObject, ResolvableObject, TypeMapping, ValueMapping}; use crate::constants::{ - DATETIME_FORMAT, EVENT_ID_COLUMN, EVENT_MESSAGE_ID_COLUMN, EVENT_MESSAGE_NAMES, - EVENT_MESSAGE_TABLE, EVENT_MESSAGE_TYPE_NAME, ID_COLUMN, + DATETIME_FORMAT, EVENT_ID_COLUMN, EVENT_MESSAGE_NAMES, EVENT_MESSAGE_TABLE, + EVENT_MESSAGE_TYPE_NAME, ID_COLUMN, }; use crate::mapping::ENTITY_TYPE_MAPPING; use crate::object::{resolve_many, resolve_one}; -use crate::query::type_mapping_query; +use crate::query::{build_type_mapping, value_mapping_from_row}; use crate::utils; + #[derive(Debug)] pub struct EventMessageObject; @@ -74,8 +75,6 @@ impl ResolvableObject for EventMessageObject { Some(id) => Some(id.string()?.to_string()), None => None, }; - // if id is None, then subscribe to all entities - // if id is Some, then subscribe to only the entity with that id Ok(SimpleBroker::::subscribe().filter_map( move |entity: EventMessage| { if id.is_none() || id == Some(entity.id.clone()) { @@ -83,7 +82,6 @@ impl ResolvableObject for EventMessageObject { entity, )))) } else { - // id != entity.id , then don't send anything, still listening None } }, @@ -128,9 +126,8 @@ fn model_union_field() -> Field { let entity_id = utils::extract::(indexmap, "id")?; // fetch name from the models table - // using the model id (hashed model name) let model_ids: Vec<(String, String, String)> = sqlx::query_as( - "SELECT id, namespace, name + "SELECT namespace, name, schema FROM models WHERE id IN ( SELECT model_id @@ -143,25 +140,25 @@ fn model_union_field() -> Field { .await?; let mut results: Vec> = Vec::new(); - for (id, namespace, name) in model_ids { - // the model id in the model mmeebrs table is the hashed model name (id) - let type_mapping = type_mapping_query(&mut conn, &id).await?; - - // but the table name for the model data is the unhashed model name - let data: ValueMapping = match model_data_recursive_query( - &mut conn, - EVENT_MESSAGE_ID_COLUMN, - vec![get_tag(&namespace, &name)], - &entity_id, - &[], - &type_mapping, - false, - ) - .await? - { - Value::Object(map) => map, - _ => unreachable!(), - }; + for (namespace, name, schema) in model_ids { + let schema: Ty = serde_json::from_str(&schema).map_err(|e| { + anyhow::anyhow!(format!("Failed to parse model schema: {e}")) + })?; + let type_mapping = build_type_mapping(&namespace, &schema); + + // Get the table name + let table_name = get_tag(&namespace, &name); + + // Fetch the row data + let query = format!( + "SELECT * FROM [{}] WHERE internal_event_message_id = ?", + table_name + ); + let row = + sqlx::query(&query).bind(&entity_id).fetch_one(&mut *conn).await?; + + // Use value_mapping_from_row to handle nested structures + let data = value_mapping_from_row(&row, &type_mapping, false, false)?; results.push(FieldValue::with_type( FieldValue::owned_any(data), diff --git a/crates/torii/graphql/src/object/metadata/mod.rs b/crates/torii/graphql/src/object/metadata/mod.rs index 07a7771380..8f5efca950 100644 --- a/crates/torii/graphql/src/object/metadata/mod.rs +++ b/crates/torii/graphql/src/object/metadata/mod.rs @@ -105,7 +105,7 @@ fn metadata_connection_output( .map(|row| { let order = row.try_get::(ID_COLUMN)?; let cursor = cursor::encode(&order, &order); - let mut value_mapping = value_mapping_from_row(row, row_types, false)?; + let mut value_mapping = value_mapping_from_row(row, row_types, false, true)?; value_mapping.insert(Name::new("worldAddress"), Value::from(world_address)); let json_str = row.try_get::(JSON_COLUMN)?; diff --git a/crates/torii/graphql/src/object/mod.rs b/crates/torii/graphql/src/object/mod.rs index b4201e0989..5791d606e2 100644 --- a/crates/torii/graphql/src/object/mod.rs +++ b/crates/torii/graphql/src/object/mod.rs @@ -261,7 +261,7 @@ pub fn resolve_one( let id: String = extract::(ctx.args.as_index_map(), &id_column.to_case(Case::Camel))?; let data = fetch_single_row(&mut conn, &table_name, &id_column, &id).await?; - let model = value_mapping_from_row(&data, &type_mapping, false)?; + let model = value_mapping_from_row(&data, &type_mapping, false, true)?; Ok(Some(Value::Object(model))) }) }) @@ -311,6 +311,7 @@ pub fn resolve_many( &id_column, total_count, false, + true, page_info, )?; diff --git a/crates/torii/graphql/src/object/model_data.rs b/crates/torii/graphql/src/object/model_data.rs index 689226b55c..8f6f0d0bc7 100644 --- a/crates/torii/graphql/src/object/model_data.rs +++ b/crates/torii/graphql/src/object/model_data.rs @@ -1,9 +1,8 @@ use async_graphql::dynamic::{Enum, Field, FieldFuture, InputObject, Object, TypeRef}; use async_graphql::Value; -use chrono::{DateTime, Utc}; use dojo_types::naming::get_tag; -use serde::Deserialize; -use sqlx::{FromRow, Pool, Sqlite}; +use dojo_types::schema::Ty; +use sqlx::{Pool, Sqlite}; use super::connection::{connection_arguments, connection_output, parse_connection_arguments}; use super::inputs::order_input::{order_argument, parse_order_argument, OrderInputObject}; @@ -11,8 +10,8 @@ use super::inputs::where_input::{parse_where_argument, where_argument, WhereInpu use super::inputs::InputObjectTrait; use super::{BasicObject, ResolvableObject, TypeMapping, ValueMapping}; use crate::constants::{ - ENTITY_ID_COLUMN, ENTITY_TABLE, ENTITY_TYPE_NAME, EVENT_ID_COLUMN, EVENT_MESSAGE_TABLE, - EVENT_MESSAGE_TYPE_NAME, ID_COLUMN, INTERNAL_ENTITY_ID_KEY, + ENTITY_ID_COLUMN, ENTITY_TABLE, ENTITY_TYPE_NAME, EVENT_MESSAGE_TABLE, EVENT_MESSAGE_TYPE_NAME, + ID_COLUMN, INTERNAL_ENTITY_ID_KEY, }; use crate::mapping::ENTITY_TYPE_MAPPING; use crate::query::data::{count_rows, fetch_multiple_rows, fetch_single_row}; @@ -20,36 +19,23 @@ use crate::query::value_mapping_from_row; use crate::types::TypeData; use crate::utils; -#[derive(FromRow, Deserialize, PartialEq, Eq, Debug)] -pub struct ModelMember { - pub id: String, - pub model_id: String, - pub model_idx: i64, - pub name: String, - #[serde(rename = "type")] - pub ty: String, - pub type_enum: String, - pub key: bool, - pub executed_at: DateTime, - pub created_at: DateTime, -} - #[derive(Debug)] pub struct ModelDataObject { pub name: String, pub plural_name: String, pub type_name: String, pub type_mapping: TypeMapping, + pub schema: Ty, pub where_input: WhereInputObject, pub order_input: OrderInputObject, } impl ModelDataObject { - pub fn new(name: String, type_name: String, type_mapping: TypeMapping) -> Self { + pub fn new(name: String, type_name: String, type_mapping: TypeMapping, schema: Ty) -> Self { let where_input = WhereInputObject::new(type_name.as_str(), &type_mapping); let order_input = OrderInputObject::new(type_name.as_str(), &type_mapping); let plural_name = format!("{}Models", name); - Self { name, plural_name, type_name, type_mapping, where_input, order_input } + Self { name, plural_name, type_name, type_mapping, schema, where_input, order_input } } } @@ -119,7 +105,7 @@ impl ResolvableObject for ModelDataObject { let (data, page_info) = fetch_multiple_rows( &mut conn, &table_name, - EVENT_ID_COLUMN, + "internal_event_id", &None, &order, &filters, @@ -131,9 +117,10 @@ impl ResolvableObject for ModelDataObject { &data, &type_mapping, &order, - EVENT_ID_COLUMN, + "internal_event_id", total_count, - true, + false, + false, page_info, )?; @@ -214,7 +201,8 @@ pub fn object(type_name: &str, type_mapping: &TypeMapping, path_array: Vec Field { let entity_id = utils::extract::(indexmap, INTERNAL_ENTITY_ID_KEY)?; let data = fetch_single_row(&mut conn, ENTITY_TABLE, ID_COLUMN, &entity_id).await?; - let entity = value_mapping_from_row(&data, &ENTITY_TYPE_MAPPING, false)?; + let entity = value_mapping_from_row(&data, &ENTITY_TYPE_MAPPING, false, true)?; Ok(Some(Value::Object(entity))) } @@ -276,7 +264,8 @@ fn event_message_field() -> Field { let data = fetch_single_row(&mut conn, EVENT_MESSAGE_TABLE, ID_COLUMN, &entity_id) .await?; - let event_message = value_mapping_from_row(&data, &ENTITY_TYPE_MAPPING, false)?; + let event_message = + value_mapping_from_row(&data, &ENTITY_TYPE_MAPPING, false, true)?; Ok(Some(Value::Object(event_message))) } diff --git a/crates/torii/graphql/src/query/data.rs b/crates/torii/graphql/src/query/data.rs index 5cf1fb5c76..c01895730b 100644 --- a/crates/torii/graphql/src/query/data.rs +++ b/crates/torii/graphql/src/query/data.rs @@ -5,7 +5,7 @@ use torii_core::constants::WORLD_CONTRACT_TYPE; use super::filter::{Filter, FilterValue}; use super::order::{CursorDirection, Direction, Order}; -use crate::constants::{DEFAULT_LIMIT, MODEL_TABLE}; +use crate::constants::DEFAULT_LIMIT; use crate::object::connection::{cursor, ConnectionArguments}; pub async fn count_rows( @@ -87,10 +87,7 @@ pub async fn fetch_multiple_rows( // `first` or `last` param. Explicit ordering take precedence match order { Some(order) => { - let mut column_name = order.field.clone(); - if table_name != MODEL_TABLE { - column_name = format!("external_{}", column_name); - } + let column_name = order.field.clone(); query.push_str(&format!( " ORDER BY {column_name} {}, {id_column} {} LIMIT {limit}", order.direction.as_ref(), @@ -127,9 +124,10 @@ pub async fn fetch_multiple_rows( Ok((data, page_info)) } else if is_cursor_based { let order_field = match order { - Some(order) => format!("external_{}", order.field), + Some(order) => order.field.clone(), None => id_column.to_string(), }; + match cursor_param { Some(cursor_query) => { let first_cursor = cursor::encode( @@ -190,20 +188,17 @@ fn handle_cursor( ) -> Result { match cursor::decode(cursor) { Ok((event_id, field_value)) => match order { - Some(order) => { - let field_name = format!("external_{}", order.field); - Ok(format!( - "(({} {} '{}' AND {} = '{}') OR {} {} '{}')", - id_column, - direction.as_ref(), - event_id, - field_name, - field_value, - field_name, - direction.as_ref(), - field_value - )) - } + Some(order) => Ok(format!( + "(({} {} '{}' AND {} = '{}') OR {} {} '{}')", + id_column, + direction.as_ref(), + event_id, + order.field, + field_value, + order.field, + direction.as_ref(), + field_value + )), None => Ok(format!("{} {} '{}'", id_column, direction.as_ref(), event_id)), }, Err(_) => Err(sqlx::Error::Decode("Invalid cursor format".into())), diff --git a/crates/torii/graphql/src/query/filter.rs b/crates/torii/graphql/src/query/filter.rs index 54e14b768c..0f39c073dc 100644 --- a/crates/torii/graphql/src/query/filter.rs +++ b/crates/torii/graphql/src/query/filter.rs @@ -56,16 +56,10 @@ pub struct Filter { pub fn parse_filter(input: &Name, value: FilterValue) -> Filter { for comparator in Comparator::iter() { if let Some(field) = input.strip_suffix(comparator.as_ref()) { - // Filtering only applies to model members which are stored in db with - // external_{name} - return Filter { - field: format!("external_{}", field), - comparator: comparator.clone(), - value, - }; + return Filter { field: field.to_string(), comparator: comparator.clone(), value }; } } // If no suffix found assume equality comparison - Filter { field: format!("external_{}", input), comparator: Comparator::Eq, value } + Filter { field: input.to_string(), comparator: Comparator::Eq, value } } diff --git a/crates/torii/graphql/src/query/mod.rs b/crates/torii/graphql/src/query/mod.rs index 6586b150c9..69c8d4113b 100644 --- a/crates/torii/graphql/src/query/mod.rs +++ b/crates/torii/graphql/src/query/mod.rs @@ -1,157 +1,103 @@ use std::str::FromStr; +use async_graphql::dynamic::indexmap::IndexMap; use async_graphql::dynamic::TypeRef; use async_graphql::{Name, Value}; use chrono::{DateTime, Utc}; use convert_case::{Case, Casing}; use dojo_types::primitive::{Primitive, SqlType}; +use dojo_types::schema::Ty; use regex::Regex; use sqlx::sqlite::SqliteRow; -use sqlx::{Row, SqliteConnection}; +use sqlx::Row; use torii_core::constants::SQL_FELT_DELIMITER; use crate::constants::{ BOOLEAN_TRUE, ENTITY_ID_COLUMN, EVENT_MESSAGE_ID_COLUMN, INTERNAL_ENTITY_ID_KEY, }; -use crate::object::model_data::ModelMember; use crate::types::{TypeData, TypeMapping, ValueMapping}; pub mod data; pub mod filter; pub mod order; -pub async fn type_mapping_query( - conn: &mut SqliteConnection, - model_id: &str, -) -> sqlx::Result { - let model_members = fetch_model_members(conn, model_id).await?; - let (root_members, nested_members): (Vec<&ModelMember>, Vec<&ModelMember>) = - model_members.iter().partition(|member| member.model_idx == 0); +pub fn build_type_mapping(namespace: &str, schema: &Ty) -> TypeMapping { + let model = schema.as_struct().unwrap(); - build_type_mapping(&root_members, &nested_members) -} - -async fn fetch_model_members( - conn: &mut SqliteConnection, - model_id: &str, -) -> sqlx::Result> { - sqlx::query_as( - r#" - SELECT - id, - model_id, - model_idx, - name, - type AS ty, - type_enum, - key, - executed_at, - created_at - from model_members WHERE model_id = ? - "#, - ) - .bind(model_id) - .fetch_all(conn) - .await -} - -fn build_type_mapping( - root_members: &[&ModelMember], - nested_members: &[&ModelMember], -) -> sqlx::Result { - let type_mapping: TypeMapping = root_members + model + .children .iter() - .map(|&member| { - let type_data = member_to_type_data(member, nested_members); - Ok((Name::new(&member.name), type_data)) + .map(|member| { + let type_data = member_to_type_data(namespace, &member.ty); + (Name::new(&member.name), type_data) }) - .collect::>()?; - - Ok(type_mapping) + .collect() } -fn member_to_type_data(member: &ModelMember, nested_members: &[&ModelMember]) -> TypeData { +fn member_to_type_data(namespace: &str, schema: &Ty) -> TypeData { // TODO: convert sql -> Ty directly - match member.type_enum.as_str() { - "Primitive" => TypeData::Simple(TypeRef::named(&member.ty)), - "ByteArray" => TypeData::Simple(TypeRef::named("ByteArray")), - "Array" => TypeData::List(Box::new(member_to_type_data( - nested_members - .iter() - .find(|&nested_member| { - nested_member.model_id == member.model_id - && nested_member.id.ends_with(&member.name) - // TEMP FIX: refer to parse_nested_type - && nested_member - .id - .split('$') - .collect::>() - .starts_with(&member.id.split('$').collect::>()) - }) - .expect("Array type should have nested type"), - nested_members, - ))), + match schema { + Ty::Primitive(primitive) => TypeData::Simple(TypeRef::named(primitive.to_string())), + Ty::ByteArray(_) => TypeData::Simple(TypeRef::named("ByteArray")), + Ty::Array(array) => TypeData::List(Box::new(member_to_type_data(namespace, &array[0]))), // Enums that do not have a nested member are considered as a simple Enum - "Enum" - if !nested_members.iter().any(|&nested_member| { - nested_member.model_id == member.model_id - && nested_member.id.ends_with(&member.name) - && nested_member - .id - .split('$') - .collect::>() - .starts_with(&member.id.split('$').collect::>()) - }) => + Ty::Enum(enum_) + if enum_ + .options + .iter() + .all(|o| if let Ty::Tuple(t) = &o.ty { t.is_empty() } else { false }) => { TypeData::Simple(TypeRef::named("Enum")) } - _ => parse_nested_type(member, nested_members), + _ => parse_nested_type(namespace, schema), } } -fn parse_nested_type(member: &ModelMember, nested_members: &[&ModelMember]) -> TypeData { - let nested_mapping: TypeMapping = nested_members - .iter() - .filter_map(|&nested_member| { - if member.model_id == nested_member.model_id - && nested_member.id.ends_with(&member.name) - // TEMP FIX: a nested member that has the same name as another nested member - // and that both have parents that start with the same id (Model$Test1 and Model$Test2) - // will end up being assigned to the wrong parent - && nested_member - .id - .split('$') - .take(nested_member.id.split('$').count() - 1) - .collect::>() - .eq(&member.id.split('$').collect::>()) - { - // if the nested member is an Enum and the member is an Enum, we need to inject the - // Enum type in order to have a "option" field in the nested Enum - // for the enum variant - if nested_member.type_enum == "Enum" - && nested_member.name == "option" - && member.type_enum == "Enum" - { - return Some((Name::new("option"), TypeData::Simple(TypeRef::named("Enum")))); - } +fn parse_nested_type(namespace: &str, schema: &Ty) -> TypeData { + let type_mapping: TypeMapping = match schema { + Ty::Struct(s) => s + .children + .iter() + .map(|member| { + let type_data = member_to_type_data(namespace, &member.ty); + (Name::new(&member.name), type_data) + }) + .collect(), + Ty::Enum(e) => { + let mut type_mapping = e + .options + .iter() + .filter_map(|option| { + // ignore unit type variants + if let Ty::Tuple(t) = &option.ty { + if t.is_empty() { + return None; + } + } - let type_data = member_to_type_data(nested_member, nested_members); - Some((Name::new(&nested_member.name), type_data)) - } else { - None - } - }) - .collect(); + let type_data = member_to_type_data(namespace, &option.ty); + Some((Name::new(&option.name), type_data)) + }) + .collect::(); + + type_mapping.insert(Name::new("option"), TypeData::Simple(TypeRef::named("Enum"))); + type_mapping + } + Ty::Tuple(t) => t + .iter() + .enumerate() + .map(|(i, ty)| (Name::new(format!("_{}", i)), member_to_type_data(namespace, ty))) + .collect(), + _ => return TypeData::Simple(TypeRef::named(schema.name())), + }; - let model_name = member.id.split('$').next().unwrap(); + let name: String = format!("{}_{}", namespace, schema.name()); // sanitizes the member type string // for eg. Position_Array -> Position_ArrayVec2 // Position_(u8, Vec2) -> Position_u8Vec2 let re = Regex::new(r"[, ()<>-]").unwrap(); - let sanitized_model_name = model_name.replace('-', "_"); - let sanitized_member_type_name = re.replace_all(&member.ty, ""); - let namespaced = format!("{}_{}", sanitized_model_name, sanitized_member_type_name); - TypeData::Nested((TypeRef::named(namespaced), nested_mapping)) + let sanitized_member_type_name = re.replace_all(&name, ""); + TypeData::Nested((TypeRef::named(sanitized_member_type_name), type_mapping)) } fn remove_hex_leading_zeros(value: Value) -> Value { @@ -170,37 +116,145 @@ fn remove_hex_leading_zeros(value: Value) -> Value { pub fn value_mapping_from_row( row: &SqliteRow, types: &TypeMapping, - is_external: bool, + is_internal: bool, + snake_case: bool, ) -> sqlx::Result { - let mut value_mapping = types - .iter() - .filter(|(_, type_data)| { - type_data.is_simple() - // ignore Enum fields because the column is not stored in this row. we inejct it later - // && !(type_data.type_ref().to_string() == "Enum") - }) - .map(|(field_name, type_data)| { - let mut value = - fetch_value(row, field_name, &type_data.type_ref().to_string(), is_external)?; - - // handles felt arrays stored as string (ex: keys) - if let (TypeRef::List(_), Value::String(s)) = (&type_data.type_ref(), &value) { - let mut felts: Vec<_> = s.split(SQL_FELT_DELIMITER).map(Value::from).collect(); - felts.pop(); // removes empty item - value = Value::List(felts); - } + // Retrieve entity ID if present + let entity_id = if let Ok(entity_id) = row.try_get::(ENTITY_ID_COLUMN) { + Some(entity_id) + } else if let Ok(event_message_id) = row.try_get::(EVENT_MESSAGE_ID_COLUMN) { + Some(event_message_id) + } else { + None + }; - Ok((Name::new(field_name), value)) - }) - .collect::>()?; + fn build_value_mapping( + row: &SqliteRow, + types: &TypeMapping, + prefix: &str, + is_internal: bool, + snake_case: bool, + entity_id: &Option, + ) -> sqlx::Result { + let mut value_mapping = ValueMapping::new(); + // Add internal entity ID if present + if let Some(entity_id) = entity_id { + value_mapping.insert(Name::new(INTERNAL_ENTITY_ID_KEY), Value::from(entity_id)); + } - // entity_id is not part of a model's type_mapping but needed to relate to parent entity - if let Ok(entity_id) = row.try_get::(ENTITY_ID_COLUMN) { - value_mapping.insert(Name::new(INTERNAL_ENTITY_ID_KEY), Value::from(entity_id)); - } else if let Ok(event_message_id) = row.try_get::(EVENT_MESSAGE_ID_COLUMN) { - value_mapping.insert(Name::new(INTERNAL_ENTITY_ID_KEY), Value::from(event_message_id)); + for (field_name, type_data) in types { + let column_name = if prefix.is_empty() { + field_name.to_string() + } else { + format!("{}.{}", prefix, field_name) + }; + + match type_data { + TypeData::Simple(type_ref) => { + let mut value = fetch_value( + row, + &column_name, + &type_ref.to_string(), + is_internal, + snake_case, + )?; + + // handles felt arrays stored as string (ex: keys) + if let (TypeRef::List(_), Value::String(s)) = (type_ref, &value) { + let mut felts: Vec<_> = + s.split(SQL_FELT_DELIMITER).map(Value::from).collect(); + felts.pop(); // removes empty item + value = Value::List(felts); + } + + value_mapping.insert(Name::new(field_name), value); + } + TypeData::List(_) => { + let value = fetch_value(row, &column_name, "String", is_internal, snake_case)?; + if let Value::String(json_str) = value { + let mut array_value: Value = + serde_json::from_str(&json_str).map_err(|e| { + sqlx::Error::Protocol(format!("JSON parse error: {}", e)) + })?; + + fn populate_value( + value: &mut Value, + type_data: &TypeData, + entity_id: &Option, + ) { + match value { + Value::Object(obj) => { + for (field_name, field_value) in obj.iter_mut() { + populate_value( + field_value, + &type_data.type_mapping().unwrap()[field_name], + entity_id, + ); + } + + if type_data.type_mapping().map_or(false, |mapping| { + mapping.contains_key(&Name::new("option")) + }) { + obj.insert( + Name::new("option"), + Value::String(obj.keys().next().unwrap().to_string()), + ); + } + + // insert $entity_id$ relation + if let Some(entity_id) = entity_id { + obj.insert( + Name::new(INTERNAL_ENTITY_ID_KEY), + Value::from(entity_id), + ); + } + } + Value::List(inner) => match type_data { + TypeData::List(inner_type_data) => { + for item in inner.iter_mut() { + populate_value(item, inner_type_data, entity_id); + } + } + TypeData::Nested((_, mapping)) => { + let mut obj = IndexMap::new(); + for (i, item) in inner.iter_mut().enumerate() { + populate_value( + item, + &mapping[&Name::new(format!("_{}", i))], + entity_id, + ); + obj.insert(Name::new(format!("_{}", i)), item.clone()); + } + *value = Value::Object(obj); + } + _ => {} + }, + _ => {} + } + } + + populate_value(&mut array_value, type_data, entity_id); + value_mapping.insert(Name::new(field_name), array_value); + } + } + TypeData::Nested((_, nested_mapping)) => { + let nested_values = build_value_mapping( + row, + nested_mapping, + &column_name, + is_internal, + snake_case, + entity_id, + )?; + value_mapping.insert(Name::new(field_name), Value::Object(nested_values)); + } + } + } + + Ok(value_mapping) } + let value_mapping = build_value_mapping(row, types, "", is_internal, snake_case, &entity_id)?; Ok(value_mapping) } @@ -208,14 +262,37 @@ fn fetch_value( row: &SqliteRow, field_name: &str, type_name: &str, - is_external: bool, + is_internal: bool, + snake_case: bool, ) -> sqlx::Result { - let column_name = if is_external { - format!("external_{}", field_name) + let mut column_name = if is_internal { + format!("internal_{}", field_name) + } else if snake_case { + field_name.to_case(Case::Snake) } else { - field_name.to_string().to_case(Case::Snake) + field_name.to_string() }; + // Strip _0, _1, etc. from tuple field names + // to get the actual SQL column name which is 0, 1 etc.. + column_name = column_name + .split('.') + .map(|part| { + if part.starts_with('_') && part[1..].parse::().is_ok() { + part[1..].to_string() + } else { + part.to_string() + } + }) + .collect::>() + .join("."); + + // for enum options, remove the ".option" suffix to get the variant + // through the enum itself field name + if type_name == "Enum" && column_name.ends_with(".option") { + column_name = column_name.trim_end_matches(".option").to_string(); + } + match Primitive::from_str(type_name) { // fetch boolean Ok(Primitive::Bool(_)) => { diff --git a/crates/torii/graphql/src/schema.rs b/crates/torii/graphql/src/schema.rs index 79ec29e15d..8e1aba65c3 100644 --- a/crates/torii/graphql/src/schema.rs +++ b/crates/torii/graphql/src/schema.rs @@ -1,5 +1,6 @@ use anyhow::Result; use async_graphql::dynamic::{Object, Scalar, Schema, Subscription, Union}; +use dojo_types::schema::Ty; use sqlx::SqlitePool; use torii_core::types::Model; @@ -22,7 +23,7 @@ use crate::object::metadata::MetadataObject; use crate::object::model::ModelObject; use crate::object::transaction::TransactionObject; use crate::object::ObjectVariant; -use crate::query::type_mapping_query; +use crate::query::build_type_mapping; // The graphql schema is built dynamically at runtime, this is because we won't know the schema of // the models until runtime. There are however, predefined objects such as entities and @@ -139,7 +140,9 @@ async fn build_objects(pool: &SqlitePool) -> Result<(Vec, Vec Result<(Vec, Vec Option<&TypeData> { + match self { + TypeData::List(inner) => Some(inner), + _ => None, + } + } + // pub fn is_enum(&self) -> bool { // matches!(self, TypeData::Enum(_)) // } diff --git a/crates/torii/grpc/src/server/mod.rs b/crates/torii/grpc/src/server/mod.rs index 67084230df..4cfda84941 100644 --- a/crates/torii/grpc/src/server/mod.rs +++ b/crates/torii/grpc/src/server/mod.rs @@ -65,11 +65,11 @@ use crate::types::ComparisonOperator; 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 ENTITIES_ENTITY_RELATION_COLUMN: &str = "internal_entity_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"; +pub(crate) static EVENT_MESSAGES_ENTITY_RELATION_COLUMN: &str = "internal_event_message_id"; pub(crate) static EVENT_MESSAGES_HISTORICAL_TABLE: &str = "event_messages_historical"; @@ -299,35 +299,23 @@ impl DojoWorld { let schemas = self.model_cache.models(&model_ids).await?.into_iter().map(|m| m.schema).collect(); - let (entity_query, arrays_queries, _) = build_sql_query( + let (entity_query, _) = build_sql_query( &schemas, table, entity_relation_column, Some(&format!( "[{table}].id IN (SELECT id FROM temp_entity_ids WHERE model_group = ?)" )), - Some(&format!( - "[{table}].id IN (SELECT id FROM temp_entity_ids WHERE model_group = ?)" - )), None, None, )?; let rows = sqlx::query(&entity_query).bind(&models_str).fetch_all(&mut *tx).await?; - - let mut arrays_rows = HashMap::new(); - for (name, array_query) in arrays_queries { - let array_rows = - sqlx::query(&array_query).bind(&models_str).fetch_all(&mut *tx).await?; - arrays_rows.insert(name, array_rows); - } - - let arrays_rows = Arc::new(arrays_rows); let schemas = Arc::new(schemas); let group_entities: Result, Error> = rows .par_iter() - .map(|row| map_row_to_entity(row, &arrays_rows, &schemas, dont_include_hashed_keys)) + .map(|row| map_row_to_entity(row, &schemas, dont_include_hashed_keys)) .collect(); all_entities.extend(group_entities?); @@ -686,20 +674,15 @@ impl DojoWorld { let schemas = self.model_cache.models(&model_ids).await?.into_iter().map(|m| m.schema).collect(); - let model = member_clause.model.clone(); - let parts: Vec<&str> = member_clause.member.split('.').collect(); - let (table_name, column_name) = if parts.len() > 1 { - let nested_table = parts[..parts.len() - 1].join("$"); - (format!("{model}${nested_table}"), format!("external_{}", parts.last().unwrap())) - } else { - (model, format!("external_{}", member_clause.member)) - }; - let (entity_query, arrays_queries, count_query) = build_sql_query( + // Use the member name directly as the column name since it's already flattened + let (entity_query, count_query) = build_sql_query( &schemas, table, entity_relation_column, - Some(&format!("[{table_name}].{column_name} {comparison_operator} ?")), - None, + Some(&format!( + "[{}].[{}] {comparison_operator} ?", + member_clause.model, member_clause.member + )), limit, offset, )?; @@ -710,21 +693,15 @@ impl DojoWorld { .await? .unwrap_or(0); let db_entities = sqlx::query(&entity_query) - .bind(comparison_value.clone()) + .bind(comparison_value) .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(comparison_value.clone()).fetch_all(&self.pool).await?; - arrays_rows.insert(name, rows); - } let entities_collection: Result, Error> = db_entities .par_iter() - .map(|row| map_row_to_entity(row, &arrays_rows, &schemas, dont_include_hashed_keys)) + .map(|row| map_row_to_entity(row, &schemas, dont_include_hashed_keys)) .collect(); Ok((entities_collection?, total_count)) } @@ -1058,7 +1035,6 @@ fn map_row_to_event(row: &(String, String, String)) -> Result>, schemas: &[Ty], dont_include_hashed_keys: bool, ) -> Result { @@ -1067,7 +1043,7 @@ fn map_row_to_entity( .iter() .map(|schema| { let mut ty = schema.clone(); - map_row_to_ty("", &schema.name(), &mut ty, row, arrays_rows)?; + map_row_to_ty("", &schema.name(), &mut ty, row)?; Ok(ty.as_struct().unwrap().clone().into()) }) .collect::, Error>>()?; @@ -1121,9 +1097,7 @@ fn build_composite_clause( let mut join_clauses = Vec::new(); let mut having_clauses = Vec::new(); let mut bind_values = Vec::new(); - - // HashMap to track the number of joins per model - let mut model_counters: HashMap = HashMap::new(); + let mut seen_models = HashMap::new(); for clause in &composite.clauses { match clause.clause_type.as_ref().unwrap() { @@ -1160,42 +1134,41 @@ fn build_composite_clause( bind_values.push(comparison_value); let model = member.model.clone(); - let parts: Vec<&str> = member.member.split('.').collect(); - let (table_name, column_name) = if parts.len() > 1 { - let nested_table = parts[..parts.len() - 1].join("$"); - ( - format!("[{model}${nested_table}]"), - format!("external_{}", parts.last().unwrap()), - ) - } else { - (format!("[{model}]"), format!("external_{}", member.member)) - }; - - let (namespace, model_name) = member - .model - .split_once('-') - .ok_or(QueryError::InvalidNamespacedModel(member.model.clone()))?; - let model_id = compute_selector_from_names(namespace, model_name); - - // Generate a unique alias for each model - let counter = model_counters.entry(model.clone()).or_insert(0); - *counter += 1; - let alias = - if *counter == 1 { model.clone() } else { format!("{model}_{}", *counter - 1) }; - - join_clauses.push(format!( - "LEFT JOIN {table_name} AS [{alias}] ON [{table}].id = [{alias}].entity_id" - )); - where_clauses.push(format!("[{alias}].{column_name} {comparison_operator} ?")); - having_clauses.push(format!( - "INSTR(group_concat({model_relation_table}.model_id), '{:#x}') > 0", - model_id - )); + // Get or create unique alias for this model + let alias = seen_models.entry(model.clone()).or_insert_with(|| { + let (namespace, model_name) = model + .split_once('-') + .ok_or(QueryError::InvalidNamespacedModel(model.clone())) + .unwrap(); + let model_id = compute_selector_from_names(namespace, model_name); + + // Add model check to having clause + having_clauses.push(format!( + "INSTR(group_concat({model_relation_table}.model_id), '{:#x}') > 0", + model_id + )); + + // Add join clause + join_clauses.push(format!( + "LEFT JOIN [{model}] AS [{model}] ON [{table}].id = \ + [{model}].internal_entity_id" + )); + + model.clone() + }); + + // Use the column name directly since it's already flattened + where_clauses + .push(format!("[{alias}].[{}] {comparison_operator} ?", member.member)); } - ClauseType::Composite(nested_composite) => { + ClauseType::Composite(nested) => { + // Handle nested composite by recursively building the clause let (nested_where, nested_having, nested_join, nested_values) = - build_composite_clause(table, model_relation_table, nested_composite)?; - where_clauses.push(format!("({})", nested_where.trim_start_matches("WHERE "))); + build_composite_clause(table, model_relation_table, nested)?; + + if !nested_where.is_empty() { + where_clauses.push(format!("({})", nested_where.trim_start_matches("WHERE "))); + } if !nested_having.is_empty() { having_clauses.push(nested_having.trim_start_matches("HAVING ").to_string()); } diff --git a/crates/torii/grpc/src/server/subscriptions/entity.rs b/crates/torii/grpc/src/server/subscriptions/entity.rs index f8793b5109..ceed5ac823 100644 --- a/crates/torii/grpc/src/server/subscriptions/entity.rs +++ b/crates/torii/grpc/src/server/subscriptions/entity.rs @@ -114,6 +114,7 @@ impl Service { ) -> Result<(), Error> { let mut closed_stream = Vec::new(); let hashed = Felt::from_str(&entity.id).map_err(ParseError::FromStr)?; + // sometimes for some reason keys isxx empty. investigate the issue let keys = entity .keys .trim_end_matches(SQL_FELT_DELIMITER) diff --git a/crates/torii/grpc/src/server/tests/entities_test.rs b/crates/torii/grpc/src/server/tests/entities_test.rs index 031779da58..47c08e2593 100644 --- a/crates/torii/grpc/src/server/tests/entities_test.rs +++ b/crates/torii/grpc/src/server/tests/entities_test.rs @@ -132,7 +132,7 @@ async fn test_entities_queries(sequencer: &RunnerCtx) { .query_by_keys( "entities", "entity_model", - "entity_id", + "internal_entity_id", &KeysClause { keys: vec![account.address().to_bytes_be().to_vec()], pattern_matching: 0, diff --git a/crates/torii/libp2p/src/server/mod.rs b/crates/torii/libp2p/src/server/mod.rs index 85b81ade16..704cff6170 100644 --- a/crates/torii/libp2p/src/server/mod.rs +++ b/crates/torii/libp2p/src/server/mod.rs @@ -253,7 +253,7 @@ impl Relay

{ // select only identity field, if doesn't exist, empty string let query = format!( - "SELECT external_identity FROM [{}] WHERE id = ?", + "SELECT identity FROM [{}] WHERE internal_id = ?", ty.name() ); let entity_identity: Option = match sqlx::query_scalar(&query) diff --git a/crates/torii/migrations/20241126064130_model_schema.sql b/crates/torii/migrations/20241126064130_model_schema.sql new file mode 100644 index 0000000000..50965b49b5 --- /dev/null +++ b/crates/torii/migrations/20241126064130_model_schema.sql @@ -0,0 +1,3 @@ +-- Adds a new schema column to the models table. +-- The schema is the JSON serialized Ty of the model. +ALTER TABLE models ADD COLUMN schema BLOB NOT NULL; diff --git a/crates/torii/migrations/20241126064421_delete_model_members.sql b/crates/torii/migrations/20241126064421_delete_model_members.sql new file mode 100644 index 0000000000..3c3052d9ba --- /dev/null +++ b/crates/torii/migrations/20241126064421_delete_model_members.sql @@ -0,0 +1,4 @@ +-- Deletes the model_members table. Which is no longer needed since we store the schema in the models table. +PRAGMA foreign_keys = OFF; +DROP TABLE model_members; +PRAGMA foreign_keys = ON; diff --git a/crates/torii/types-test/Scarb.lock b/crates/torii/types-test/Scarb.lock index ffc9ecef4d..e8acab7a4a 100644 --- a/crates/torii/types-test/Scarb.lock +++ b/crates/torii/types-test/Scarb.lock @@ -14,7 +14,7 @@ version = "2.8.4" [[package]] name = "types_test" -version = "1.0.2" +version = "1.0.3" dependencies = [ "dojo", ] diff --git a/crates/torii/types-test/manifest_dev.json b/crates/torii/types-test/manifest_dev.json new file mode 100644 index 0000000000..48092ca366 --- /dev/null +++ b/crates/torii/types-test/manifest_dev.json @@ -0,0 +1,1496 @@ +{ + "world": { + "class_hash": "0x45575a88cc5cef1e444c77ce60b7b4c9e73a01cbbe20926d5a4c72a94011410", + "address": "0x1413cc3c0d35059f2807a2d77b8f61084dcac934144f24b4a72cf1d0ef9c7f6", + "seed": "types_test", + "name": "types test", + "entrypoints": [ + "uuid", + "set_metadata", + "register_namespace", + "register_event", + "register_model", + "register_contract", + "init_contract", + "upgrade_event", + "upgrade_model", + "upgrade_contract", + "emit_event", + "emit_events", + "set_entity", + "set_entities", + "delete_entity", + "delete_entities", + "grant_owner", + "revoke_owner", + "grant_writer", + "revoke_writer", + "upgrade" + ], + "abi": [ + { + "type": "impl", + "name": "World", + "interface_name": "dojo::world::iworld::IWorld" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "enum", + "name": "dojo::world::resource::Resource", + "variants": [ + { + "name": "Model", + "type": "(core::starknet::contract_address::ContractAddress, core::felt252)" + }, + { + "name": "Event", + "type": "(core::starknet::contract_address::ContractAddress, core::felt252)" + }, + { + "name": "Contract", + "type": "(core::starknet::contract_address::ContractAddress, core::felt252)" + }, + { + "name": "Namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "World", + "type": "()" + }, + { + "name": "Unregistered", + "type": "()" + } + ] + }, + { + "type": "struct", + "name": "dojo::model::metadata::ResourceMetadata", + "members": [ + { + "name": "resource_id", + "type": "core::felt252" + }, + { + "name": "metadata_uri", + "type": "core::byte_array::ByteArray" + }, + { + "name": "metadata_hash", + "type": "core::felt252" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::>", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::>" + } + ] + }, + { + "type": "enum", + "name": "dojo::model::definition::ModelIndex", + "variants": [ + { + "name": "Keys", + "type": "core::array::Span::" + }, + { + "name": "Id", + "type": "core::felt252" + }, + { + "name": "MemberId", + "type": "(core::felt252, core::felt252)" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "dojo::meta::layout::FieldLayout", + "members": [ + { + "name": "selector", + "type": "core::felt252" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "enum", + "name": "dojo::meta::layout::Layout", + "variants": [ + { + "name": "Fixed", + "type": "core::array::Span::" + }, + { + "name": "Struct", + "type": "core::array::Span::" + }, + { + "name": "Tuple", + "type": "core::array::Span::" + }, + { + "name": "Array", + "type": "core::array::Span::" + }, + { + "name": "ByteArray", + "type": "()" + }, + { + "name": "Enum", + "type": "core::array::Span::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "enum", + "name": "core::bool", + "variants": [ + { + "name": "False", + "type": "()" + }, + { + "name": "True", + "type": "()" + } + ] + }, + { + "type": "interface", + "name": "dojo::world::iworld::IWorld", + "items": [ + { + "type": "function", + "name": "resource", + "inputs": [ + { + "name": "selector", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "dojo::world::resource::Resource" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "uuid", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "metadata", + "inputs": [ + { + "name": "resource_selector", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "dojo::model::metadata::ResourceMetadata" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_metadata", + "inputs": [ + { + "name": "metadata", + "type": "dojo::model::metadata::ResourceMetadata" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "register_namespace", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "register_event", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "register_model", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "register_contract", + "inputs": [ + { + "name": "salt", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "init_contract", + "inputs": [ + { + "name": "selector", + "type": "core::felt252" + }, + { + "name": "init_calldata", + "type": "core::array::Span::" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "upgrade_event", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "upgrade_model", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "upgrade_contract", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [ + { + "type": "core::starknet::class_hash::ClassHash" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "emit_event", + "inputs": [ + { + "name": "event_selector", + "type": "core::felt252" + }, + { + "name": "keys", + "type": "core::array::Span::" + }, + { + "name": "values", + "type": "core::array::Span::" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "emit_events", + "inputs": [ + { + "name": "event_selector", + "type": "core::felt252" + }, + { + "name": "keys", + "type": "core::array::Span::>" + }, + { + "name": "values", + "type": "core::array::Span::>" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "entity", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "index", + "type": "dojo::model::definition::ModelIndex" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [ + { + "type": "core::array::Span::" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "entities", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "indexes", + "type": "core::array::Span::" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [ + { + "type": "core::array::Span::>" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_entity", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "index", + "type": "dojo::model::definition::ModelIndex" + }, + { + "name": "values", + "type": "core::array::Span::" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "set_entities", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "indexes", + "type": "core::array::Span::" + }, + { + "name": "values", + "type": "core::array::Span::>" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "delete_entity", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "index", + "type": "dojo::model::definition::ModelIndex" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "delete_entities", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "indexes", + "type": "core::array::Span::" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "is_owner", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "grant_owner", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "revoke_owner", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "is_writer", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "grant_writer", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "revoke_writer", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "impl", + "name": "UpgradeableWorld", + "interface_name": "dojo::world::iworld::IUpgradeableWorld" + }, + { + "type": "interface", + "name": "dojo::world::iworld::IUpgradeableWorld", + "items": [ + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [ + { + "name": "world_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::WorldSpawned", + "kind": "struct", + "members": [ + { + "name": "creator", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::WorldUpgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::NamespaceRegistered", + "kind": "struct", + "members": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "hash", + "type": "core::felt252", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ModelRegistered", + "kind": "struct", + "members": [ + { + "name": "name", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "namespace", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::EventRegistered", + "kind": "struct", + "members": [ + { + "name": "name", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "namespace", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ContractRegistered", + "kind": "struct", + "members": [ + { + "name": "name", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "namespace", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "salt", + "type": "core::felt252", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ModelUpgraded", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "prev_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::EventUpgraded", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "prev_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ContractUpgraded", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ContractInitialized", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "init_calldata", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::EventEmitted", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "system_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "keys", + "type": "core::array::Span::", + "kind": "data" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::MetadataUpdate", + "kind": "struct", + "members": [ + { + "name": "resource", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "uri", + "type": "core::byte_array::ByteArray", + "kind": "data" + }, + { + "name": "hash", + "type": "core::felt252", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreSetRecord", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "keys", + "type": "core::array::Span::", + "kind": "data" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreUpdateRecord", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "member_selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreDelRecord", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::WriterUpdated", + "kind": "struct", + "members": [ + { + "name": "resource", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "value", + "type": "core::bool", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::OwnerUpdated", + "kind": "struct", + "members": [ + { + "name": "resource", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "value", + "type": "core::bool", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::Event", + "kind": "enum", + "variants": [ + { + "name": "WorldSpawned", + "type": "dojo::world::world_contract::world::WorldSpawned", + "kind": "nested" + }, + { + "name": "WorldUpgraded", + "type": "dojo::world::world_contract::world::WorldUpgraded", + "kind": "nested" + }, + { + "name": "NamespaceRegistered", + "type": "dojo::world::world_contract::world::NamespaceRegistered", + "kind": "nested" + }, + { + "name": "ModelRegistered", + "type": "dojo::world::world_contract::world::ModelRegistered", + "kind": "nested" + }, + { + "name": "EventRegistered", + "type": "dojo::world::world_contract::world::EventRegistered", + "kind": "nested" + }, + { + "name": "ContractRegistered", + "type": "dojo::world::world_contract::world::ContractRegistered", + "kind": "nested" + }, + { + "name": "ModelUpgraded", + "type": "dojo::world::world_contract::world::ModelUpgraded", + "kind": "nested" + }, + { + "name": "EventUpgraded", + "type": "dojo::world::world_contract::world::EventUpgraded", + "kind": "nested" + }, + { + "name": "ContractUpgraded", + "type": "dojo::world::world_contract::world::ContractUpgraded", + "kind": "nested" + }, + { + "name": "ContractInitialized", + "type": "dojo::world::world_contract::world::ContractInitialized", + "kind": "nested" + }, + { + "name": "EventEmitted", + "type": "dojo::world::world_contract::world::EventEmitted", + "kind": "nested" + }, + { + "name": "MetadataUpdate", + "type": "dojo::world::world_contract::world::MetadataUpdate", + "kind": "nested" + }, + { + "name": "StoreSetRecord", + "type": "dojo::world::world_contract::world::StoreSetRecord", + "kind": "nested" + }, + { + "name": "StoreUpdateRecord", + "type": "dojo::world::world_contract::world::StoreUpdateRecord", + "kind": "nested" + }, + { + "name": "StoreUpdateMember", + "type": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "nested" + }, + { + "name": "StoreDelRecord", + "type": "dojo::world::world_contract::world::StoreDelRecord", + "kind": "nested" + }, + { + "name": "WriterUpdated", + "type": "dojo::world::world_contract::world::WriterUpdated", + "kind": "nested" + }, + { + "name": "OwnerUpdated", + "type": "dojo::world::world_contract::world::OwnerUpdated", + "kind": "nested" + } + ] + } + ] + }, + "contracts": [ + { + "address": "0x5e3b474c077756ba9378903764ba932bed6c9ffe000165f9beba8769e9e8200", + "class_hash": "0x148bf0bb34a0ed998edfbca7f0799e76c1c5bbb87ca972d684b60136550a02", + "abi": [ + { + "type": "impl", + "name": "records__ContractImpl", + "interface_name": "dojo::contract::interface::IContract" + }, + { + "type": "interface", + "name": "dojo::contract::interface::IContract", + "items": [] + }, + { + "type": "impl", + "name": "records__DeployedContractImpl", + "interface_name": "dojo::meta::interface::IDeployedResource" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "interface", + "name": "dojo::meta::interface::IDeployedResource", + "items": [ + { + "type": "function", + "name": "dojo_name", + "inputs": [], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "RecordsImpl", + "interface_name": "types_test::contracts::IRecords" + }, + { + "type": "interface", + "name": "types_test::contracts::IRecords", + "items": [ + { + "type": "function", + "name": "create", + "inputs": [ + { + "name": "num_records", + "type": "core::integer::u8" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "delete", + "inputs": [ + { + "name": "record_id", + "type": "core::integer::u32" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "function", + "name": "dojo_init", + "inputs": [], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "impl", + "name": "WorldProviderImpl", + "interface_name": "dojo::contract::components::world_provider::IWorldProvider" + }, + { + "type": "struct", + "name": "dojo::world::iworld::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "interface", + "name": "dojo::contract::components::world_provider::IWorldProvider", + "items": [ + { + "type": "function", + "name": "world_dispatcher", + "inputs": [], + "outputs": [ + { + "type": "dojo::world::iworld::IWorldDispatcher" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "UpgradeableImpl", + "interface_name": "dojo::contract::components::upgradeable::IUpgradeable" + }, + { + "type": "interface", + "name": "dojo::contract::components::upgradeable::IUpgradeable", + "items": [ + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "enum", + "variants": [ + { + "name": "Upgraded", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "nested" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "types_test::contracts::records::Event", + "kind": "enum", + "variants": [ + { + "name": "UpgradeableEvent", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "nested" + }, + { + "name": "WorldProviderEvent", + "type": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "nested" + } + ] + } + ], + "init_calldata": [], + "tag": "types_test-records", + "selector": "0x11bb34944de6c1b172f8f43624c026a63b2913b825f996c99c85ea3d1f9ac8a", + "systems": [ + "create", + "delete", + "upgrade" + ] + } + ], + "models": [ + { + "members": [], + "class_hash": "0x21569c1c7a59f12d1b1d4c14a4447e21c9783c67327caac8853563bb4cc76d", + "tag": "types_test-Record", + "selector": "0x21aeac4d33b64ae624204430d0c09837f34f654a7fac1cc83270a27a739ec71" + }, + { + "members": [], + "class_hash": "0x55a80dd8db0bc8ead4fc1f4c33c76367dbd63507e71672fe7af7ad8dbeac6af", + "tag": "types_test-RecordSibling", + "selector": "0x298128bd79a29d95f2888d2284ff39e037072dd71dae9313ffee995b12e9c86" + }, + { + "members": [], + "class_hash": "0x28299f3de912dbe3777bb8c3dd7e263a0d7785ac19fda69ca35f7e2392f7854", + "tag": "types_test-Subrecord", + "selector": "0x38871602c0e0b8a9c403655653d0080376ce1706b8b00c63a5f850cd6709689" + } + ], + "events": [ + { + "members": [], + "class_hash": "0x4d455c1ea98a2dcb6af77a01b3b992acfd80478bcb4948a663fdef69ae0b4d0", + "tag": "types_test-RecordLogged", + "selector": "0x26d4239a31cc102ac8fe5df0e0af32e84fa6d8667c699fbb5a41ce39ad713b4" + } + ] +} \ No newline at end of file diff --git a/examples/spawn-and-move/Scarb.lock b/examples/spawn-and-move/Scarb.lock index 1d6835aa49..0537ff21a2 100644 --- a/examples/spawn-and-move/Scarb.lock +++ b/examples/spawn-and-move/Scarb.lock @@ -31,7 +31,7 @@ dependencies = [ [[package]] name = "dojo_examples" -version = "1.0.2" +version = "1.0.3" dependencies = [ "armory", "bestiary", diff --git a/examples/spawn-and-move/src/actions.cairo b/examples/spawn-and-move/src/actions.cairo index 1a8071ada3..95c8d4a4d5 100644 --- a/examples/spawn-and-move/src/actions.cairo +++ b/examples/spawn-and-move/src/actions.cairo @@ -64,7 +64,7 @@ pub mod actions { player: seed.try_into().unwrap(), name: "hello", items: array![], - favorite_item: Option::None + favorite_item: Option::None, }; let mut world = self.world_default(); @@ -127,7 +127,7 @@ pub mod actions { PlayerItem { item_id: 2, quantity: 50, score: -32 } ]; - let config = PlayerConfig { player, name, items, favorite_item: Option::Some(1), }; + let config = PlayerConfig { player, name, items, favorite_item: Option::Some(1) }; world.write_model(@config); }