diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dd107291a3..f79fe64254 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -267,8 +267,6 @@ jobs: target: arm64 build-contexts: | artifacts=artifacts - # cache-from: type=gha - # cache-to: type=gha,mode=max docker-build-and-push-linux-amd64: name: Build and push linux-amd64 docker image @@ -305,5 +303,3 @@ jobs: tags: ghcr.io/${{ github.repository }}:latest,ghcr.io/${{ github.repository }}:${{ needs.prepare.outputs.tag_name }} platforms: linux/amd64 target: amd64 - # cache-from: type=gha - # cache-to: type=gha,mode=max diff --git a/Cargo.lock b/Cargo.lock index e5157272aa..e83a521a57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2265,7 +2265,7 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dojo-lang" -version = "0.3.12" +version = "0.3.13" dependencies = [ "anyhow", "cairo-lang-compiler", @@ -2312,7 +2312,7 @@ dependencies = [ [[package]] name = "dojo-languge-server" -version = "0.3.12" +version = "0.3.13" dependencies = [ "anyhow", "cairo-lang-compiler", @@ -2334,7 +2334,7 @@ dependencies = [ [[package]] name = "dojo-signers" -version = "0.3.12" +version = "0.3.13" dependencies = [ "anyhow", "starknet", @@ -2342,7 +2342,7 @@ dependencies = [ [[package]] name = "dojo-test-utils" -version = "0.3.12" +version = "0.3.13" dependencies = [ "anyhow", "assert_fs", @@ -2373,7 +2373,7 @@ dependencies = [ [[package]] name = "dojo-types" -version = "0.3.12" +version = "0.3.13" dependencies = [ "crypto-bigint", "hex", @@ -2388,7 +2388,7 @@ dependencies = [ [[package]] name = "dojo-world" -version = "0.3.12" +version = "0.3.13" dependencies = [ "anyhow", "assert_fs", @@ -4816,7 +4816,7 @@ dependencies = [ [[package]] name = "katana" -version = "0.3.12" +version = "0.3.13" dependencies = [ "assert_matches", "clap", @@ -4834,7 +4834,7 @@ dependencies = [ [[package]] name = "katana-core" -version = "0.3.12" +version = "0.3.13" dependencies = [ "anyhow", "assert_matches", @@ -4865,7 +4865,7 @@ dependencies = [ [[package]] name = "katana-rpc" -version = "0.3.12" +version = "0.3.13" dependencies = [ "anyhow", "assert_matches", @@ -7236,7 +7236,7 @@ dependencies = [ [[package]] name = "sozo" -version = "0.3.12" +version = "0.3.13" dependencies = [ "anyhow", "assert_fs", @@ -8267,7 +8267,7 @@ dependencies = [ [[package]] name = "torii-client" -version = "0.3.12" +version = "0.3.13" dependencies = [ "async-trait", "camino", @@ -8293,7 +8293,7 @@ dependencies = [ [[package]] name = "torii-core" -version = "0.3.12" +version = "0.3.13" dependencies = [ "anyhow", "async-trait", @@ -8329,7 +8329,7 @@ dependencies = [ [[package]] name = "torii-graphql" -version = "0.3.12" +version = "0.3.13" dependencies = [ "anyhow", "async-graphql", @@ -8367,7 +8367,7 @@ dependencies = [ [[package]] name = "torii-grpc" -version = "0.3.12" +version = "0.3.13" dependencies = [ "bytes", "dojo-types", @@ -8404,7 +8404,7 @@ dependencies = [ [[package]] name = "torii-server" -version = "0.3.12" +version = "0.3.13" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 957b0458b5..ecb68c435d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ edition = "2021" license = "Apache-2.0" license-file = "LICENSE" repository = "https://github.com/dojoengine/dojo/" -version = "0.3.12" +version = "0.3.13" [profile.performance] codegen-units = 1 diff --git a/crates/dojo-core/Scarb.lock b/crates/dojo-core/Scarb.lock index 13286a04f2..c7222cdada 100644 --- a/crates/dojo-core/Scarb.lock +++ b/crates/dojo-core/Scarb.lock @@ -3,12 +3,12 @@ version = 1 [[package]] name = "dojo" -version = "0.3.12" +version = "0.3.13" dependencies = [ "dojo_plugin", ] [[package]] name = "dojo_plugin" -version = "0.3.12" -source = "git+https://github.com/dojoengine/dojo?tag=v0.3.12#12d58f29ec53454317f1f6d265007a053d279288" +version = "0.3.11" +source = "git+https://github.com/dojoengine/dojo?tag=v0.3.11#1e651b5d4d3b79b14a7d8aa29a92062fcb9e6659" diff --git a/crates/dojo-core/Scarb.toml b/crates/dojo-core/Scarb.toml index d1f9ec3818..a53a2aec80 100644 --- a/crates/dojo-core/Scarb.toml +++ b/crates/dojo-core/Scarb.toml @@ -2,8 +2,8 @@ cairo-version = "2.3.1" description = "The Dojo Core library for autonomous worlds." name = "dojo" -version = "0.3.12" +version = "0.3.13" [dependencies] -dojo_plugin = { git = "https://github.com/dojoengine/dojo", tag = "v0.3.12" } +dojo_plugin = { git = "https://github.com/dojoengine/dojo", tag = "v0.3.11" } starknet = "2.3.1" diff --git a/crates/dojo-core/src/benchmarks.cairo b/crates/dojo-core/src/benchmarks.cairo index 62f21918ef..de8e389494 100644 --- a/crates/dojo-core/src/benchmarks.cairo +++ b/crates/dojo-core/src/benchmarks.cairo @@ -8,7 +8,8 @@ use starknet::SyscallResultTrait; use starknet::{contract_address_const, ContractAddress, ClassHash, get_caller_address}; use dojo::database; -use dojo::database::{storage, index}; +use dojo::database::{storage, index, Clause}; +use dojo::packing::{shl, shr}; use dojo::model::Model; use dojo::world_test::Foo; use dojo::test_utils::end; @@ -113,30 +114,30 @@ fn bench_native_storage_offset() { fn bench_index() { let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - let no_query = index::query(0, 69, Option::None(())); + let no_query = index::get(0, 69, 0); end(gas, 'idx empty'); assert(no_query.len() == 0, 'entity indexed'); let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - index::create(0, 69, 420); + index::create(0, 69, 420, 0); end(gas, 'idx create 1st'); let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - let query = index::query(0, 69, Option::None(())); + let query = index::get(0, 69, 0); end(gas, 'idx query one'); assert(query.len() == 1, 'entity not indexed'); assert(*query.at(0) == 420, 'entity value incorrect'); - + let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - index::create(0, 69, 1337); + index::create(0, 69, 1337, 0); end(gas, 'idx query 2nd'); let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - let two_query = index::query(0, 69, Option::None(())); + let two_query = index::get(0, 69, 0); end(gas, 'idx query two'); assert(two_query.len() == 2, 'index should have two query'); assert(*two_query.at(1) == 1337, 'entity value incorrect'); @@ -171,18 +172,18 @@ fn bench_big_index() { if i == 1000 { break; } - index::create(0, 69, i); + index::create(0, 69, i, 0); i += 1; }; end(gas, 'idx create 1000'); let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - let query = index::query(0, 69, Option::None(())); + let query = index::get(0, 69, 0); end(gas, 'idx query 1000'); assert(query.len() == 1000, 'entity not indexed'); assert(*query.at(420) == 420, 'entity value incorrect'); - + let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); index::exists(0, 69, 999); @@ -249,27 +250,25 @@ fn bench_indexed_database_array() { let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - database::set_with_index('table', 'even', 0, even, layout); + database::set_with_index('table', 'even', array![].span(), 0, even, layout); end(gas, 'dbi set arr 1st'); let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - let (keys, values) = database::scan('table', Option::None(()), 2, layout); + let values = database::scan(Clause::All('table'), 2, layout); end(gas, 'dbi scan arr 1'); let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - database::set_with_index('table', 'odd', 0, odd, layout); + database::set_with_index('table', 'odd', array![].span(), 0, odd, layout); end(gas, 'dbi set arr 2nd'); let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - let (keys, values) = database::scan('table', Option::None(()), 2, layout); + let values = database::scan(Clause::All('table'), 2, layout); end(gas, 'dbi scan arr 2'); - assert(keys.len() == 2, 'Wrong number of keys!'); assert(values.len() == 2, 'Wrong number of values!'); - assert(*keys.at(0) == 'even', 'Wrong key at index 0!'); assert(*(*values.at(0)).at(0) == 2, 'Wrong value at index 0!'); assert(*(*values.at(0)).at(1) == 4, 'Wrong value at index 1!'); } @@ -282,11 +281,7 @@ fn bench_simple_struct() { let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - let mut foo = Foo { - caller, - a: 0x123456789abcdef, - b: 0x123456789abcdef, - }; + let mut foo = Foo { caller, a: 0x123456789abcdef, b: 0x123456789abcdef, }; end(gas, 'foo init'); let gas = testing::get_available_gas(); @@ -402,18 +397,12 @@ fn bench_nested_struct() { gas::withdraw_gas().unwrap(); let mut case = Case { - owner: caller, - sword: Sword { - swordsmith: caller, - damage: 0x12345678, - }, - material: 'wooden', + owner: caller, sword: Sword { swordsmith: caller, damage: 0x12345678, }, material: 'wooden', }; end(gas, 'case init'); let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); let mut serialized = ArrayTrait::new(); @@ -516,16 +505,16 @@ fn bench_complex_struct() { finished: true, romances: 0x1234, }, - weapon: Weapon::DualWield(( - Sword { - swordsmith: starknet::contract_address_const::<0x69>(), - damage: 0x12345678, - }, - Sword { - swordsmith: starknet::contract_address_const::<0x69>(), - damage: 0x12345678, - } - )), + weapon: Weapon::DualWield( + ( + Sword { + swordsmith: starknet::contract_address_const::<0x69>(), damage: 0x12345678, + }, + Sword { + swordsmith: starknet::contract_address_const::<0x69>(), damage: 0x12345678, + } + ) + ), gold: 0x12345678, }; end(gas, 'chars init'); @@ -566,4 +555,4 @@ fn bench_complex_struct() { gas::withdraw_gas().unwrap(); database::get('chars', '42', 0, char.packed_size(), char.layout()); end(gas, 'chars db get'); -} \ No newline at end of file +} diff --git a/crates/dojo-core/src/database.cairo b/crates/dojo-core/src/database.cairo index 86b6ec6136..e23264cee1 100644 --- a/crates/dojo-core/src/database.cairo +++ b/crates/dojo-core/src/database.cairo @@ -19,79 +19,152 @@ mod utils_test; use index::WhereCondition; -fn get(table: felt252, key: felt252, offset: u8, length: usize, layout: Span) -> Span { +#[derive(Copy, Drop, Serde)] +struct MemberClause { + model: felt252, + member: felt252, // positon of the member in the model + value: felt252, +} + +#[derive(Copy, Drop, Serde)] +struct CompositeClause { + operator: LogicalOperator, + clauses: Span, +} + +#[derive(Copy, Drop, Serde)] +enum LogicalOperator { + And, +} + +#[derive(Copy, Drop, Serde)] +enum Clause { + Member: MemberClause, + Composite: CompositeClause, + All: felt252, +} + +fn get(model: felt252, key: felt252, offset: u8, length: usize, layout: Span) -> Span { let mut keys = ArrayTrait::new(); keys.append('dojo_storage'); - keys.append(table); + keys.append(model); keys.append(key); storage::get_many(0, keys.span(), offset, length, layout) } -fn set(table: felt252, key: felt252, offset: u8, value: Span, layout: Span) { +fn set(model: felt252, key: felt252, offset: u8, value: Span, layout: Span) { let mut keys = ArrayTrait::new(); keys.append('dojo_storage'); - keys.append(table); + keys.append(model); keys.append(key); storage::set_many(0, keys.span(), offset, value, layout); } +/// Creates an entry in the database and adds it to appropriate indexes. +/// # Arguments +/// * `model` - The model to create the entry in. +/// * `key` - key of the created entry. +/// * `members` - The members to create an index on. +/// * `offset` - The offset of the entry. +/// * `value` - The value of the entry. +/// * `layout` - The layout of the entry. fn set_with_index( - table: felt252, key: felt252, offset: u8, value: Span, layout: Span + model: felt252, + key: felt252, + members: Span, + offset: u8, + values: Span, + layout: Span ) { - set(table, key, offset, value, layout); - index::create(0, table, key); + set(model, key, offset, values, layout); + index::create(0, model, key, 0); // create a record in index of all records + + let mut idx = 0; + loop { + if idx == members.len() { + break; // Iterating over all members of the model with `#[key]` attribute + } + + // The position of the member in the model identifies the index + let index = poseidon_hash_span(array![model, idx.into()].span()); + index::create(0, index, key, *members.at(idx)); // create a record for each of the indexes + idx += 1; + }; } -fn del(table: felt252, key: felt252) { - index::delete(0, table, key); +fn del(model: felt252, key: felt252) { + index::delete(0, model, key); + + let len_keys = array!['dojo_storage_keys_len', model, key].span(); + let len = storage::get(0, len_keys); + + let mut idx = 0; // Iterating over all members of the model... + loop { + let index = poseidon_hash_span(array![model, idx].span()); + + if !index::exists(0, index, key) { + break; // ...until we find a member without `#[key]` attribute + } + + index::delete(0, index, key); // deleting all inbetween + idx += 1; + }; } // Query all entities that meet a criteria. If no index is defined, // Returns a tuple of spans, first contains the entity IDs, // second the deserialized entities themselves. -fn scan( - model: felt252, where: Option, values_length: usize, values_layout: Span -) -> (Span, Span>) { - let all_ids = scan_ids(model, where); - (all_ids, get_by_ids(model, all_ids, values_length, values_layout)) +fn scan(where: Clause, values_length: usize, values_layout: Span) -> Span> { + match where { + Clause::Member(clause) => { + // The position of the member in the model identifies the index + let index = poseidon_hash_span(array![clause.model, clause.member].span()); + let keys = index::get(0, index, clause.value); + get_by_keys(clause.model, keys, values_length, values_layout) + }, + Clause::Composite(clause) => { + assert(false, 'unimplemented'); + array![array![].span()].span() + }, + Clause::All(model) => { + let keys = index::get(0, model, 0); + get_by_keys(model, keys, values_length, values_layout) + } + } } -/// Analogous to `scan`, but returns only the IDs of the entities. -fn scan_ids(model: felt252, where: Option) -> Span { +/// Analogous to `scan`, but returns only the keys of the entities. +fn scan_keys(where: Clause) -> Span { match where { - Option::Some(clause) => { - let mut serialized = ArrayTrait::new(); - model.serialize(ref serialized); - clause.key.serialize(ref serialized); - let index = poseidon_hash_span(serialized.span()); - - index::get_by_key(0, index, clause.value).span() + Clause::Member(clause) => { + // The position of the member in the model identifies the index + let i = poseidon_hash_span(array![clause.model, clause.member].span()); + index::get(0, i, clause.value) + }, + Clause::Composite(clause) => { + assert(false, 'unimplemented'); + array![].span() }, - // If no `where` clause is defined, we return all values. - Option::None(_) => { - index::query(0, model, Option::None) + Clause::All(model) => { + index::get(0, model, 0) } } } -/// Returns entries on the given ids. +/// Returns entries on the given keys. /// # Arguments -/// * `class_hash` - The class hash of the contract. -/// * `table` - The table to get the entries from. -/// * `all_ids` - The ids of the entries to get. +/// * `model` - The model to get the entries from. +/// * `keys` - The keys of the entries to get. /// * `length` - The length of the entries. -fn get_by_ids( - table: felt252, all_ids: Span, length: u32, layout: Span +fn get_by_keys( + model: felt252, mut keys: Span, length: u32, layout: Span ) -> Span> { let mut entities: Array> = ArrayTrait::new(); - let mut ids = all_ids; + loop { - match ids.pop_front() { - Option::Some(id) => { - let mut keys = ArrayTrait::new(); - keys.append('dojo_storage'); - keys.append(table); - keys.append(*id); + match keys.pop_front() { + Option::Some(key) => { + let keys = array!['dojo_storage', model, *key]; let value: Span = storage::get_many(0, keys.span(), 0_u8, length, layout); entities.append(value); }, diff --git a/crates/dojo-core/src/database/index.cairo b/crates/dojo-core/src/database/index.cairo index 4a1a514fc4..cd000b1c13 100644 --- a/crates/dojo-core/src/database/index.cairo +++ b/crates/dojo-core/src/database/index.cairo @@ -12,16 +12,25 @@ struct WhereCondition { value: felt252, } -fn create(address_domain: u32, index: felt252, id: felt252) { - if exists(address_domain, index, id) { +fn create(address_domain: u32, index: felt252, key: felt252, value: felt252) { + if exists(address_domain, index, key) { return (); } - let index_len_key = build_index_len_key(index); + let index_len_key = build_index_len_key(index, value); let index_len = storage::get(address_domain, index_len_key); - storage::set(address_domain, build_index_item_key(index, id), index_len + 1); + + if value == 0 { + storage::set(address_domain, build_index_item_key(index, key), index_len + 1); + } else { + let data = array![index_len + 1, value].span(); // index and value of the created entry + storage::set_many( + address_domain, build_index_item_key(index, key), 0, data, array![250, 252].span() + ); + } + storage::set(address_domain, index_len_key, index_len + 1); - storage::set(address_domain, build_index_key(index, index_len), id); + storage::set(address_domain, build_index_key(index, value, index_len), key); } /// Deletes an entry from the main index, as well as from each of the keys. @@ -35,120 +44,55 @@ fn delete(address_domain: u32, index: felt252, id: felt252) { return (); } - let index_len_key = build_index_len_key(index); - let replace_item_idx = storage::get(address_domain, index_len_key) - 1; - let index_item_key = build_index_item_key(index, id); - let delete_item_idx = storage::get(address_domain, index_item_key) - 1; + let index_item_layout = array![250, 252].span(); + let delete_item = storage::get_many(address_domain, index_item_key, 0, 2, index_item_layout); + let delete_item_idx = *delete_item.at(0) - 1; + let value = *delete_item.at(1); + + let index_len_key = build_index_len_key(index, value); + let replace_item_idx = storage::get(address_domain, index_len_key) - 1; storage::set(address_domain, index_item_key, 0); storage::set(address_domain, index_len_key, replace_item_idx); // Replace the deleted element with the last element. // NOTE: We leave the last element set as to not produce an unncessary state diff. - let replace_item_value = storage::get(address_domain, build_index_key(index, replace_item_idx)); - storage::set(address_domain, build_index_key(index, delete_item_idx), replace_item_value); + let replace_item_value = storage::get( + address_domain, build_index_key(index, value, replace_item_idx) + ); + storage::set( + address_domain, build_index_key(index, value, delete_item_idx), replace_item_value + ); } fn exists(address_domain: u32, index: felt252, id: felt252) -> bool { storage::get(address_domain, build_index_item_key(index, id)) != 0 } -fn query(address_domain: u32, table: felt252, where: Option) -> Span { +fn get(address_domain: u32, index: felt252, value: felt252) -> Span { let mut res = ArrayTrait::new(); - - match where { - Option::Some(clause) => { - let mut serialized = ArrayTrait::new(); - table.serialize(ref serialized); - clause.key.serialize(ref serialized); - let index = poseidon_hash_span(serialized.span()); - - let index_len_key = build_index_len_key(index); - let index_len = storage::get(address_domain, index_len_key); - let mut idx = 0; - - loop { - if idx == index_len { - break (); - } - let id = storage::get(address_domain, build_index_key(index, idx)); - res.append(id); - } - }, - - // If no `where` clause is defined, we return all values. - Option::None(_) => { - let index_len_key = build_index_len_key(table); - let index_len = storage::get(address_domain, index_len_key); - let mut idx = 0; - - loop { - if idx == index_len { - break (); - } - - res.append(storage::get(address_domain, build_index_key(table, idx))); - idx += 1; - }; - } - } - - res.span() -} - -/// Returns all the entries that hold a given key -/// # Arguments -/// * address_domain - The address domain to write to. -/// * index - The index to read from. -/// * key - The key return values from. -fn get_by_key(address_domain: u32, index: felt252, key: felt252) -> Array { - let mut res = ArrayTrait::new(); - let specific_len_key = build_index_specific_key_len(index, key); - let index_len = storage::get(address_domain, specific_len_key); - + let index_len_key = build_index_len_key(index, value); + let index_len = storage::get(address_domain, index_len_key); let mut idx = 0; - loop { if idx == index_len { - break (); + break res.span(); } - let specific_key = build_index_specific_key(index, key, idx); - let id = storage::get(address_domain, specific_key); - res.append(id); - + res.append(storage::get(address_domain, build_index_key(index, value, idx))); idx += 1; - }; - - res -} - -fn build_index_len_key(index: felt252) -> Span { - array!['dojo_index_lens', index].span() + } } -fn build_index_key(index: felt252, idx: felt252) -> Span { - array!['dojo_indexes', index, idx].span() +fn build_index_len_key(index: felt252, value: felt252) -> Span { + array!['dojo_index_lens', index, value].span() } -fn build_index_item_key(index: felt252, id: felt252) -> Span { - array!['dojo_index_ids', index, id].span() +fn build_index_key(index: felt252, value: felt252, idx: felt252) -> Span { + array!['dojo_indexes', index, value, idx].span() } -/// Key for a length of index for a given key. -/// # Arguments -/// * index - The index to write to. -/// * key - The key to write. -fn build_index_specific_key_len(index: felt252, key: felt252) -> Span { - array!['dojo_index_key_len', index, key].span() +fn build_index_item_key(table: felt252, id: felt252) -> Span { + array!['dojo_index_ids', table, id].span() } - -/// Key for an index of a given key. -/// # Arguments -/// * index - The index to write to. -/// * key - The key to write. -/// * idx - The position in the index. -fn build_index_specific_key(index: felt252, key: felt252, idx: felt252) -> Span { - array!['dojo_index_key', index, key, idx].span() -} \ No newline at end of file diff --git a/crates/dojo-core/src/database/index_test.cairo b/crates/dojo-core/src/database/index_test.cairo index 2d3aa22a6b..4f15bc5268 100644 --- a/crates/dojo-core/src/database/index_test.cairo +++ b/crates/dojo-core/src/database/index_test.cairo @@ -8,61 +8,98 @@ use dojo::database::index; #[test] #[available_gas(2000000)] -fn test_index_entity() { - let no_query = index::query(0, 69, Option::None(())); - assert(no_query.len() == 0, 'entity indexed'); - - index::create(0, 69, 420); - let query = index::query(0, 69, Option::None(())); - assert(query.len() == 1, 'entity not indexed'); - assert(*query.at(0) == 420, 'entity value incorrect'); - - index::create(0, 69, 420); - let noop_query = index::query(0, 69, Option::None(())); - assert(noop_query.len() == 1, 'index should be noop'); - - index::create(0, 69, 1337); - let two_query = index::query(0, 69, Option::None(())); - assert(two_query.len() == 2, 'index should have two query'); - assert(*two_query.at(1) == 1337, 'entity value incorrect'); +fn test_index_same_values() { + let no_get = index::get(0, 69, 0); + assert(no_get.len() == 0, 'entity indexed'); + + index::create(0, 69, 420, 0); + let get = index::get(0, 69, 0); + assert(get.len() == 1, 'entity not indexed'); + assert(*get.at(0) == 420, 'entity value incorrect'); + + index::create(0, 69, 420, 0); + let noop_get = index::get(0, 69, 0); + assert(noop_get.len() == 1, 'index should be noop'); + + index::create(0, 69, 1337, 0); + let two_get = index::get(0, 69, 0); + assert(two_get.len() == 2, 'index should have two get'); + assert(*two_get.at(1) == 1337, 'entity value incorrect'); } #[test] #[available_gas(2000000)] +fn test_index_different_values() { + index::create(0, 69, 420, 1); + let get = index::get(0, 69, 1); + assert(get.len() == 1, 'entity not indexed'); + assert(*get.at(0) == 420, 'entity value incorrect'); + + let noop_get = index::get(0, 69, 3); + assert(noop_get.len() == 0, 'index should be noop'); + + index::create(0, 69, 1337, 2); + index::create(0, 69, 1337, 2); + index::create(0, 69, 1338, 2); + let two_get = index::get(0, 69, 2); + assert(two_get.len() == 2, 'index should have two get'); + assert(*two_get.at(1) == 1338, 'two get value incorrect'); +} + +#[test] +#[available_gas(100000000)] fn test_entity_delete_basic() { - index::create(0, 69, 420); - let query = index::query(0, 69, Option::None(())); - assert(query.len() == 1, 'entity not indexed'); - assert(*query.at(0) == 420, 'entity value incorrect'); + index::create(0, 69, 420, 1); + let get = index::get(0, 69, 1); + assert(get.len() == 1, 'entity not indexed'); + assert(*get.at(0) == 420, 'entity value incorrect'); assert(index::exists(0, 69, 420), 'entity should exist'); index::delete(0, 69, 420); assert(!index::exists(0, 69, 420), 'entity should not exist'); - let no_query = index::query(0, 69, Option::None(())); - assert(no_query.len() == 0, 'index should have no query'); + let no_get = index::get(0, 69, 1); + assert(no_get.len() == 0, 'index should have no get'); } #[test] -#[available_gas(20000000)] -fn test_entity_query_delete_shuffle() { +#[available_gas(100000000)] +fn test_entity_get_delete_shuffle() { let table = 1; - index::create(0, table, 10); - index::create(0, table, 20); - index::create(0, table, 30); - assert(index::query(0, table, Option::None(())).len() == 3, 'wrong size'); + index::create(0, table, 10, 1); + index::create(0, table, 20, 1); + index::create(0, table, 30, 1); + assert(index::get(0, table, 1).len() == 3, 'wrong size'); index::delete(0, table, 10); - let entities = index::query(0, table, Option::None(())); + let entities = index::get(0, table, 1); assert(entities.len() == 2, 'wrong size'); assert(*entities.at(0) == 30, 'idx 0 not 30'); assert(*entities.at(1) == 20, 'idx 1 not 20'); } #[test] -#[available_gas(20000000)] -fn test_entity_query_delete_non_existing() { - assert(index::query(0, 69, Option::None(())).len() == 0, 'table len != 0'); +#[available_gas(100000000)] +fn test_entity_get_delete_non_existing() { + assert(index::get(0, 69, 1).len() == 0, 'table len != 0'); index::delete(0, 69, 999); // deleting non-existing should not panic } + +#[test] +#[available_gas(100000000)] +fn test_entity_delete_right_value() { + let table = 1; + index::create(0, table, 10, 1); + index::create(0, table, 20, 2); + index::create(0, table, 30, 2); + assert(index::get(0, table, 2).len() == 2, 'wrong size'); + + index::delete(0, table, 20); + assert(index::exists(0, table, 20) == false, 'deleted value exists'); + let entities = index::get(0, table, 2); + assert(entities.len() == 1, 'wrong size'); + assert(*entities.at(0) == 30, 'idx 0 not 30'); + + assert(index::get(0, table, 1).len() == 1, 'wrong size'); +} diff --git a/crates/dojo-core/src/database_test.cairo b/crates/dojo-core/src/database_test.cairo index c1f760bb06..2c9bd69366 100644 --- a/crates/dojo-core/src/database_test.cairo +++ b/crates/dojo-core/src/database_test.cairo @@ -4,13 +4,12 @@ use option::OptionTrait; use serde::Serde; use array::SpanTrait; use traits::{Into, TryInto}; +use debug::PrintTrait; use starknet::syscalls::deploy_syscall; -use starknet::class_hash::{Felt252TryIntoClassHash, ClassHash}; use dojo::world::{IWorldDispatcher}; use dojo::executor::executor; -use dojo::database::{get, set, set_with_index, del, scan}; -use dojo::database::index::WhereCondition; +use dojo::database::{get, set, set_with_index, del, scan, Clause, MemberClause}; #[test] #[available_gas(1000000)] @@ -113,17 +112,84 @@ fn test_database_del() { #[test] #[available_gas(10000000)] fn test_database_scan() { - let even = array![2, 4].span(); - let odd = array![1, 3].span(); - let layout = array![251, 251].span(); + let even = array![2, 4, 6].span(); + let odd = array![1, 3, 5].span(); + let layout = array![251, 251, 251].span(); - set_with_index('table', 'even', 0, even, layout); - set_with_index('table', 'odd', 0, odd, layout); + set_with_index('table', 'even', array!['x'].span(), 0, even, layout); + set_with_index('table', 'odd', array!['x'].span(), 0, odd, layout); - let (keys, values) = scan('table', Option::None(()), 2, layout); - assert(keys.len() == 2, 'Wrong number of keys!'); + let values = scan(Clause::All('table'), 3, layout); assert(values.len() == 2, 'Wrong number of values!'); - assert(*keys.at(0) == 'even', 'Wrong key at index 0!'); + (*(*values.at(0)).at(0)).print(); assert(*(*values.at(0)).at(0) == 2, 'Wrong value at index 0!'); assert(*(*values.at(0)).at(1) == 4, 'Wrong value at index 1!'); + assert(*(*values.at(0)).at(2) == 6, 'Wrong value at index 2!'); + + let where = MemberClause { model: 'table', member: 0, value: 'x' }; + + let values = scan(Clause::Member(where), 32, layout); + assert(values.len() == 2, 'Wrong number of values clause!'); +} + +#[test] +#[available_gas(10000000)] +fn test_database_scan_where() { + let some = array![1, 4].span(); + let same = array![1, 3].span(); + let other = array![5, 5].span(); + let layout = array![251, 251].span(); + + set_with_index('table', 'some', array!['p', 'x'].span(), 0, some, layout); + set_with_index('table', 'same', array!['p', 'x'].span(), 0, same, layout); + set_with_index('table', 'other', array!['p', 'x'].span(), 0, other, layout); + + let values = scan(Clause::All('table'), 2, layout); + assert(values.len() == 3, 'Wrong number of values!'); + assert(*(*values.at(0)).at(0) != 0, 'value is not set'); + + let mut where = MemberClause { model: 'table', member: 0, value: 'x' }; + + let values = scan(Clause::Member(where), 2, layout); + assert(values.len() == 1, 'Wrong len for x = 5'); + assert(*(*values.at(0)).at(0) == 5, 'Wrong value 0 for x = 5'); + assert(*(*values.at(0)).at(1) == 5, 'Wrong value 1 for x = 5'); + + where.value = 'p'; + let values = scan(Clause::Member(where), 2, layout); + assert(values.len() == 2, 'Wrong len for x = 1'); + + where.value = 'q'; + let values = scan(Clause::Member(where), 2, layout); + assert(values.len() == 0, 'Wrong len for x = 6'); +} + +#[test] +#[available_gas(20000000)] +fn test_database_scan_where_deletion() { + let layout = array![251, 251].span(); + + set_with_index('model', 'some', array!['a', 'x'].span(), 0, array![2, 3].span(), layout); + set_with_index('model', 'same', array!['a', 'y'].span(), 0, array![1, 3].span(), layout); + set_with_index('model', 'other', array!['b', 'x'].span(), 0, array![5, 3].span(), layout); + + del('model', 'same'); + + let where = MemberClause { model: 'model', member: 0, value: 'b' }; + let values = scan(Clause::Member(where), 1, layout); + assert(values.len() == 1, 'Wrong len a at 0'); + assert(*(*values.at(0)).at(0) == 5, 'Wrong value for b at 0'); + + let where = MemberClause { model: 'model', member: 1, value: 'x' }; + let values = scan(Clause::Member(where), 2, layout); + assert(values.len() == 2, 'Wrong len for x at 1'); + + del('model', 'some'); + del('model', 'other'); + + let values = scan(Clause::Member(where), 2, layout); + assert(values.len() == 0, 'Wrong len for del y'); + + let values = scan(Clause::All('model'), 2, layout); + assert(values.len() == 0, 'Wrong len for scan'); } diff --git a/crates/dojo-core/src/world.cairo b/crates/dojo-core/src/world.cairo index 53094323c8..ba37a368fe 100644 --- a/crates/dojo-core/src/world.cairo +++ b/crates/dojo-core/src/world.cairo @@ -1,6 +1,7 @@ use starknet::{ContractAddress, ClassHash, StorageBaseAddress, SyscallResult}; use traits::{Into, TryInto}; use option::OptionTrait; +use dojo::database::Clause; #[starknet::interface] trait IWorld { @@ -24,14 +25,9 @@ trait IWorld { layout: Span ); fn entities( - self: @T, - model: felt252, - index: Option, - values: Span, - values_length: usize, - values_layout: Span - ) -> (Span, Span>); - fn entity_ids(self: @T, model: felt252) -> Span; + self: @T, where: Clause, values_length: usize, values_layout: Span + ) -> Span>; + fn entity_ids(self: @T, where: Clause) -> Span; fn set_executor(ref self: T, contract_address: ContractAddress); fn executor(self: @T) -> ContractAddress; fn base(self: @T) -> ClassHash; @@ -45,6 +41,11 @@ trait IWorld { fn revoke_writer(ref self: T, model: felt252, system: ContractAddress); } +#[starknet::interface] +trait IUpgradeableWorld { + fn upgrade(ref self: T, new_class_hash : ClassHash); +} + #[starknet::interface] trait IWorldProvider { fn world(self: @T) -> IWorldDispatcher; @@ -65,14 +66,14 @@ mod world { use starknet::{ get_caller_address, get_contract_address, get_tx_info, contract_address::ContractAddressIntoFelt252, ClassHash, Zeroable, ContractAddress, - syscalls::{deploy_syscall, emit_event_syscall}, SyscallResult, SyscallResultTrait, + syscalls::{deploy_syscall, emit_event_syscall, replace_class_syscall}, SyscallResult, SyscallResultTrait, SyscallResultTraitImpl }; use dojo::database; - use dojo::database::index::WhereCondition; + use dojo::database::Clause; use dojo::executor::{IExecutorDispatcher, IExecutorDispatcherTrait}; - use dojo::world::{IWorldDispatcher, IWorld}; + use dojo::world::{IWorldDispatcher, IWorld, IUpgradeableWorld}; use dojo::components::upgradeable::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; @@ -87,6 +88,7 @@ mod world { WorldSpawned: WorldSpawned, ContractDeployed: ContractDeployed, ContractUpgraded: ContractUpgraded, + WorldUpgraded: WorldUpgraded, MetadataUpdate: MetadataUpdate, ModelRegistered: ModelRegistered, StoreSetRecord: StoreSetRecord, @@ -102,6 +104,11 @@ mod world { creator: ContractAddress } + #[derive(Drop, starknet::Event)] + struct WorldUpgraded { + class_hash: ClassHash, + } + #[derive(Drop, starknet::Event)] struct ContractDeployed { salt: felt252, @@ -253,7 +260,9 @@ mod world { self.metadata_uri.write(i, *item); i += 1; }, - Option::None(_) => { break; } + Option::None(_) => { + break; + } }; }; } @@ -484,6 +493,7 @@ mod world { assert_can_write(@self, model, get_caller_address()); let key = poseidon::poseidon_hash_span(keys); + // database::set_with_index(model, key, keys, offset, values, layout); database::set(model, key, offset, values, layout); EventEmitter::emit(ref self, StoreSetRecord { table: model, keys, offset, values }); @@ -547,42 +557,27 @@ mod world { } /// Returns entity IDs and entities that contain the model state. - /// /// # Arguments - /// - /// * `model` - The name of the model to be retrieved. - /// * `index` - The index to be retrieved. - /// * `values` - The query to be used to find the entity. - /// * `length` - The length of the model values. + /// * `where` - The query to be used to find the entity. + /// * `values_length` - The length of the model values. + /// * `values_layout` - The layout of the model values. /// /// # Returns - /// - /// * `Span` - The entity IDs. /// * `Span>` - The entities. fn entities( - self: @ContractState, - model: felt252, - index: Option, - values: Span, - values_length: usize, - values_layout: Span - ) -> (Span, Span>) { - assert(values.len() == 0, 'Queries by values not impl'); - database::scan(model, Option::None(()), values_length, values_layout) + self: @ContractState, where: Clause, values_length: usize, values_layout: Span + ) -> Span> { + database::scan(where, values_length, values_layout) } /// Returns only the entity IDs that contain the model state. /// # Arguments - /// * `model` - The name of the model to be retrieved. - /// * `index` - The index to be retrieved. - /// * `values` - The query to be used to find the entity. - /// * `length` - The length of the model values. + /// * `where` - The query to be used to find the entity. /// /// # Returns /// * `Span` - The entity IDs. - /// * `Span>` - The entities. - fn entity_ids(self: @ContractState, model: felt252) -> Span { - database::scan_ids(model, Option::None(())) + fn entity_ids(self: @ContractState, where: Clause) -> Span { + database::scan_keys(where) } /// Sets the executor contract address. @@ -616,12 +611,34 @@ mod world { /// /// # Returns /// - /// * `ContractAddress` - The address of the contract_base contract. + /// * `ClassHash` - The class_hash of the contract_base contract. fn base(self: @ContractState) -> ClassHash { self.contract_base.read() } } + + #[external(v0)] + impl UpgradeableWorld of IUpgradeableWorld { + /// Upgrade world with new_class_hash + /// + /// # Arguments + /// + /// * `new_class_hash` - The new world class hash. + fn upgrade(ref self: ContractState, new_class_hash : ClassHash){ + assert(new_class_hash.is_non_zero(), 'invalid class_hash'); + assert(IWorld::is_owner(@self, get_tx_info().unbox().account_contract_address, WORLD), 'only owner can upgrade'); + + // upgrade to new_class_hash + replace_class_syscall(new_class_hash).unwrap(); + + // emit Upgrade Event + EventEmitter::emit( + ref self, WorldUpgraded {class_hash: new_class_hash } + ); + } + } + /// Asserts that the current caller can write to the model. /// /// # Arguments diff --git a/crates/dojo-core/src/world_test.cairo b/crates/dojo-core/src/world_test.cairo index de6ced047f..06d827206a 100644 --- a/crates/dojo-core/src/world_test.cairo +++ b/crates/dojo-core/src/world_test.cairo @@ -6,11 +6,13 @@ use option::OptionTrait; use starknet::class_hash::Felt252TryIntoClassHash; use starknet::{contract_address_const, ContractAddress, ClassHash, get_caller_address}; use starknet::syscalls::deploy_syscall; +use debug::PrintTrait; use dojo::benchmarks; use dojo::executor::executor; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, world}; use dojo::database::introspect::Introspect; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, world}; +use dojo::database::{Clause, MemberClause}; use dojo::test_utils::{spawn_test_world, deploy_with_world_address}; use dojo::benchmarks::{Character, end}; @@ -25,8 +27,10 @@ struct Foo { #[derive(Model, Copy, Drop, Serde)] struct Fizz { #[key] + a: felt252, + #[key] + b: felt252, caller: ContractAddress, - a: felt252 } #[starknet::interface] @@ -34,11 +38,12 @@ trait Ibar { fn set_foo(self: @TContractState, a: felt252, b: u128); fn delete_foo(self: @TContractState); fn set_char(self: @TContractState, a: felt252, b: u32); + fn set_fizz(self: @TContractState, a: felt252, b: felt252); } #[starknet::contract] mod bar { - use super::{Foo, IWorldDispatcher, IWorldDispatcherTrait, Introspect}; + use super::{Foo, Fizz, IWorldDispatcher, IWorldDispatcherTrait, Introspect}; use super::benchmarks::{Character, Abilities, Stats, Weapon, Sword}; use traits::Into; use starknet::{get_caller_address, ContractAddress}; @@ -102,6 +107,10 @@ mod bar { } ); } + + fn set_fizz(self: @ContractState, a: felt252, b: felt252) { + set!(self.world.read(), Fizz { caller: get_caller_address(), a, b }); + } } } @@ -238,6 +247,50 @@ fn deploy_world() -> IWorldDispatcher { spawn_test_world(array![]) } +#[test] +#[available_gas(60000000)] +fn test_entities() { + // Deploy world contract + let world = spawn_test_world(array![fizz::TEST_CLASS_HASH],); + + let bar_contract = IbarDispatcher { + contract_address: deploy_with_world_address(bar::TEST_CLASS_HASH, world) + }; + + let alice = starknet::contract_address_const::<0x1337>(); + starknet::testing::set_contract_address(alice); + bar_contract.set_fizz(1337, 1337); + bar_contract.set_fizz(7331, 7331); + let bob = starknet::contract_address_const::<0x420>(); + starknet::testing::set_contract_address(bob); + bar_contract.set_fizz(1337, 420); + + let layout = array![251].span(); + + // Get all in the index + let values = world.entities(Clause::All('Fizz'), 1, layout); + // assert(keys.len() == 3, 'Not all found for any!'); + // assert(values.len() == 3, 'Number of values does not match'); + // assert(*(*values.at(0)).at(0) == 0x1337, 'Caller at 0 not valid'); + // assert(*(*values.at(1)).at(0) == 0x1337, 'Caller at 1 not valid'); + // assert(*(*values.at(2)).at(0) == 0x420, 'Caller at 2 not valid'); + + let mut where = MemberClause { model: 'Fizz', member: 0, value: 1337, }; + + // Get all with a == 1337 + let values = world.entities(Clause::Member(where), 1, layout); + // assert(keys.len() == 2, 'Not all keys found for 1337!'); + // assert(*(*values.at(0)).at(0) == 0x1337, 'Caller at 0 not valid'); + // assert(*(*values.at(1)).at(0) == 0x420, 'Caller at 1 not valid'); + + // Get all with b == 420 + where.member = 1; + where.value = 420; + let values = world.entities(Clause::Member(where), 1, layout); +// assert(keys.len() == 1, 'Not all keys found for 420!'); +// assert(*(*values.at(0)).at(0) == 0x420, 'Caller at 1 not valid'); +} + #[test] #[available_gas(60000000)] fn test_metadata_uri() { @@ -268,39 +321,6 @@ fn test_set_metadata_uri_reverts_for_not_owner() { world.set_metadata_uri(0, array!['new_uri', 'longer'].span()); } -#[test] -#[available_gas(60000000)] -fn test_entities() { - // Deploy world contract - let world = spawn_test_world(array![foo::TEST_CLASS_HASH],); - - let bar_contract = IbarDispatcher { - contract_address: deploy_with_world_address(bar::TEST_CLASS_HASH, world) - }; - - let alice = starknet::contract_address_const::<0x1337>(); - starknet::testing::set_contract_address(alice); - bar_contract.set_foo(1337, 1337); - - let mut keys = ArrayTrait::new(); - keys.append(0); - - let mut query_keys = ArrayTrait::new(); - let layout = array![251].span(); - let (keys, values) = world.entities('Foo', Option::None, query_keys.span(), 2, layout); - let ids = world.entity_ids('Foo'); - assert(keys.len() == ids.len(), 'result differs in entity_ids'); - assert(keys.len() == 0, 'found value for unindexed'); -// query_keys.append(0x1337); -// let (keys, values) = world.entities('Foo', 42, query_keys.span(), 2, layout); -// assert(keys.len() == 1, 'No keys found!'); - -// let mut query_keys = ArrayTrait::new(); -// query_keys.append(0x1338); -// let (keys, values) = world.entities('Foo', 42, query_keys.span(), 2, layout); -// assert(keys.len() == 0, 'Keys found!'); -} - #[test] #[available_gas(6000000)] fn test_owner() { @@ -405,36 +425,6 @@ fn test_set_writer_fails_for_non_owner() { } -#[starknet::interface] -trait IOrigin { - fn assert_origin(self: @TContractState); -} - -#[starknet::contract] -mod origin { - use super::{IWorldDispatcher, ContractAddress}; - - #[storage] - struct Storage { - world: IWorldDispatcher, - } - - #[constructor] - fn constructor(ref self: ContractState, world: ContractAddress) { - self.world.write(IWorldDispatcher { contract_address: world }) - } - - #[external(v0)] - impl IOriginImpl of super::IOrigin { - fn assert_origin(self: @ContractState) { - assert( - starknet::get_caller_address() == starknet::contract_address_const::<0x1337>(), - 'should be equal' - ); - } - } -} - #[test] #[available_gas(60000000)] fn test_execute_multiple_worlds() { @@ -458,6 +448,9 @@ fn test_execute_multiple_worlds() { bar1_contract.set_foo(1337, 1337); bar2_contract.set_foo(7331, 7331); + let mut keys = ArrayTrait::new(); + keys.append(0); + let data1 = get!(world1, alice, Foo); let data2 = get!(world2, alice, Foo); assert(data1.a == 1337, 'data1 not stored'); @@ -511,3 +504,80 @@ fn bench_execute_complex() { assert(data.heigth == 1337, 'data not stored'); } + + +#[starknet::interface] +trait IWorldUpgrade { + fn hello(self: @TContractState) -> felt252; +} + +#[starknet::contract] +mod worldupgrade { + use super::{IWorldUpgrade, IWorldDispatcher, ContractAddress}; + + #[storage] + struct Storage { + world: IWorldDispatcher, + } + + #[external(v0)] + impl IWorldUpgradeImpl of super::IWorldUpgrade { + fn hello(self: @ContractState) -> felt252{ + 'dojo' + } + } +} + + +#[test] +#[available_gas(60000000)] +fn test_upgradeable_world() { + + // Deploy world contract + let world = deploy_world(); + + let mut upgradeable_world_dispatcher = IUpgradeableWorldDispatcher { + contract_address: world.contract_address + }; + upgradeable_world_dispatcher.upgrade(worldupgrade::TEST_CLASS_HASH.try_into().unwrap()); + + let res = (IWorldUpgradeDispatcher { + contract_address: world.contract_address + }).hello(); + + assert(res == 'dojo', 'should return dojo'); +} + +#[test] +#[available_gas(60000000)] +#[should_panic(expected:('invalid class_hash', 'ENTRYPOINT_FAILED'))] +fn test_upgradeable_world_with_class_hash_zero() { + + // Deploy world contract + let world = deploy_world(); + + starknet::testing::set_contract_address(starknet::contract_address_const::<0x1337>()); + + let mut upgradeable_world_dispatcher = IUpgradeableWorldDispatcher { + contract_address: world.contract_address + }; + upgradeable_world_dispatcher.upgrade(0.try_into().unwrap()); +} + +#[test] +#[available_gas(60000000)] +#[should_panic( expected: ('only owner can upgrade', 'ENTRYPOINT_FAILED'))] +fn test_upgradeable_world_from_non_owner() { + + // Deploy world contract + let world = deploy_world(); + + let not_owner = starknet::contract_address_const::<0x1337>(); + starknet::testing::set_contract_address(not_owner); + starknet::testing::set_account_contract_address(not_owner); + + let mut upgradeable_world_dispatcher = IUpgradeableWorldDispatcher { + contract_address: world.contract_address + }; + upgradeable_world_dispatcher.upgrade(worldupgrade::TEST_CLASS_HASH.try_into().unwrap()); +} \ No newline at end of file diff --git a/crates/dojo-erc/Scarb.lock b/crates/dojo-erc/Scarb.lock new file mode 100644 index 0000000000..18594fb80d --- /dev/null +++ b/crates/dojo-erc/Scarb.lock @@ -0,0 +1,20 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "dojo" +version = "0.3.2" +dependencies = [ + "dojo_plugin", +] + +[[package]] +name = "dojo_erc" +version = "0.3.2" +dependencies = [ + "dojo", +] + +[[package]] +name = "dojo_plugin" +version = "0.3.2" diff --git a/crates/dojo-lang/Scarb.toml b/crates/dojo-lang/Scarb.toml index 0f7e1b1532..d6108e0735 100644 --- a/crates/dojo-lang/Scarb.toml +++ b/crates/dojo-lang/Scarb.toml @@ -1,5 +1,5 @@ [package] name = "dojo_plugin" -version = "0.3.12" +version = "0.3.11" [cairo-plugin] diff --git a/crates/dojo-lang/build.rs b/crates/dojo-lang/build.rs deleted file mode 100644 index 109f547faf..0000000000 --- a/crates/dojo-lang/build.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::env; -use std::process::Command; - -fn main() { - let version = env!("CARGO_PKG_VERSION"); - let output = Command::new("git") - .args(["rev-list", "-n", "1", &format!("v{version}")]) - .output() - .expect("Failed to execute command"); - - let git_hash = String::from_utf8(output.stdout).unwrap().trim().to_string(); - println!("cargo:rustc-env=GIT_HASH={}", git_hash); -} diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest index 393e808895..05b3814632 100644 --- a/crates/dojo-lang/src/manifest_test_data/manifest +++ b/crates/dojo-lang/src/manifest_test_data/manifest @@ -8,7 +8,7 @@ test_manifest_file "world": { "name": "world", "address": null, - "class_hash": "0xb3e374b8087dca92601afbb9881fed855ac0d568e3bf878a876fca5ffcb479", + "class_hash": "0x254e8bab09cdebe6ff66e8ea4b63db54a176861c7bd0ba4006890b274c64198", "abi": [ { "type": "impl", @@ -36,19 +36,75 @@ test_manifest_file ] }, { - "type": "enum", - "name": "core::option::Option::", - "variants": [ + "type": "struct", + "name": "dojo::database::MemberClause", + "members": [ + { + "name": "model", + "type": "core::felt252" + }, { - "name": "Some", + "name": "member", "type": "core::felt252" }, { - "name": "None", + "name": "value", + "type": "core::felt252" + } + ] + }, + { + "type": "enum", + "name": "dojo::database::LogicalOperator", + "variants": [ + { + "name": "And", "type": "()" } ] }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::CompositeClause", + "members": [ + { + "name": "operator", + "type": "dojo::database::LogicalOperator" + }, + { + "name": "clauses", + "type": "core::array::Span::" + } + ] + }, + { + "type": "enum", + "name": "dojo::database::Clause", + "variants": [ + { + "name": "Member", + "type": "dojo::database::MemberClause" + }, + { + "name": "Composite", + "type": "dojo::database::CompositeClause" + }, + { + "name": "All", + "type": "core::felt252" + } + ] + }, { "type": "struct", "name": "core::array::Span::>", @@ -269,16 +325,8 @@ test_manifest_file "name": "entities", "inputs": [ { - "name": "model", - "type": "core::felt252" - }, - { - "name": "index", - "type": "core::option::Option::" - }, - { - "name": "values", - "type": "core::array::Span::" + "name": "where", + "type": "dojo::database::Clause" }, { "name": "values_length", @@ -291,7 +339,7 @@ test_manifest_file ], "outputs": [ { - "type": "(core::array::Span::, core::array::Span::>)" + "type": "core::array::Span::>" } ], "state_mutability": "view" @@ -301,8 +349,8 @@ test_manifest_file "name": "entity_ids", "inputs": [ { - "name": "model", - "type": "core::felt252" + "name": "where", + "type": "dojo::database::Clause" } ], "outputs": [ @@ -472,6 +520,29 @@ test_manifest_file } ] }, + { + "type": "impl", + "name": "UpgradeableWorld", + "interface_name": "dojo::world::IUpgradeableWorld" + }, + { + "type": "interface", + "name": "dojo::world::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", @@ -542,6 +613,18 @@ test_manifest_file } ] }, + { + "type": "event", + "name": "dojo::world::world::WorldUpgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, { "type": "event", "name": "dojo::world::world::MetadataUpdate", @@ -706,6 +789,11 @@ test_manifest_file "type": "dojo::world::world::ContractUpgraded", "kind": "nested" }, + { + "name": "WorldUpgraded", + "type": "dojo::world::world::WorldUpgraded", + "kind": "nested" + }, { "name": "MetadataUpdate", "type": "dojo::world::world::MetadataUpdate", diff --git a/crates/dojo-lang/src/manifest_test_data/simple_crate/Scarb.toml b/crates/dojo-lang/src/manifest_test_data/simple_crate/Scarb.toml index 3acc1969d9..c5d4cd4175 100644 --- a/crates/dojo-lang/src/manifest_test_data/simple_crate/Scarb.toml +++ b/crates/dojo-lang/src/manifest_test_data/simple_crate/Scarb.toml @@ -1,7 +1,7 @@ [package] cairo-version = "2.3.1" name = "test_crate" -version = "0.3.12" +version = "0.3.13" [cairo] sierra-replace-ids = true diff --git a/crates/dojo-lang/src/plugin.rs b/crates/dojo-lang/src/plugin.rs index 8ade21a985..0c350b829d 100644 --- a/crates/dojo-lang/src/plugin.rs +++ b/crates/dojo-lang/src/plugin.rs @@ -188,8 +188,8 @@ impl BuiltinDojoPlugin { impl CairoPlugin for BuiltinDojoPlugin { fn id(&self) -> PackageId { let url = Url::parse("https://github.com/dojoengine/dojo").unwrap(); - let version = env!("CARGO_PKG_VERSION"); - let rev = env!("GIT_HASH"); + let version = "0.3.11"; + let rev = "1e651b5d4d3b79b14a7d8aa29a92062fcb9e6659"; let source_id = SourceId::for_git(&url, &scarb::core::GitReference::Tag(format!("v{version}").into())) diff --git a/crates/dojo-lang/src/plugin_test_data/introspect b/crates/dojo-lang/src/plugin_test_data/introspect index 7ee8be69f3..5ce2d7e588 100644 --- a/crates/dojo-lang/src/plugin_test_data/introspect +++ b/crates/dojo-lang/src/plugin_test_data/introspect @@ -50,6 +50,515 @@ struct GenericStruct { t: T, } +impl Vec2SchemaIntrospection of dojo::database::schema::SchemaIntrospection { + #[inline(always)] + fn size() -> usize { + 2 + } + + #[inline(always)] + fn layout(ref layout: Array) { + layout.append(32); + layout.append(32); + } + + #[inline(always)] + fn ty() -> dojo::database::schema::Ty { + dojo::database::schema::Ty::Struct( + dojo::database::schema::Struct { + name: 'Vec2', + attrs: array![].span(), + children: array![ + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'x', + ty: dojo::database::schema::Ty::Primitive('u32'), + attrs: array![].span() + } + ), + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'y', + ty: dojo::database::schema::Ty::Primitive('u32'), + attrs: array![].span() + } + ) + ] + .span() + } + ) + } +} + + + +#[derive(Serde, Copy, Drop, Introspect)] +enum PlainEnum { + Left: (), + Right: (), +} + +impl PlainEnumSchemaIntrospection of dojo::database::schema::SchemaIntrospection { + #[inline(always)] + fn size() -> usize { + 1 + } + + #[inline(always)] + fn layout(ref layout: Array) { + layout.append(8); + } + + #[inline(always)] + fn ty() -> dojo::database::schema::Ty { + dojo::database::schema::Ty::Enum( + dojo::database::schema::Enum { + name: 'Direction', + attrs: array![].span(), + children: array![ + ( + 'Left', + dojo::database::schema::serialize_member_type( + @dojo::database::schema::Ty::Tuple(array![].span()) + ) + ), + ( + 'Right', + dojo::database::schema::serialize_member_type( + @dojo::database::schema::Ty::Tuple(array![].span()) + ) + ) + ] + .span() + } + ) + } +} + + + +#[derive(Serde, Copy, Drop, Introspect)] +enum EnumPrimitive { + Left: (u16,), + Right: (u16,), +} + +impl EnumPrimitiveSchemaIntrospection of dojo::database::schema::SchemaIntrospection { + #[inline(always)] + fn size() -> usize { + 2 + } + + #[inline(always)] + fn layout(ref layout: Array) { + layout.append(8); + layout.append(16); + } + + #[inline(always)] + fn ty() -> dojo::database::schema::Ty { + dojo::database::schema::Ty::Enum( + dojo::database::schema::Enum { + name: 'Direction', + attrs: array![].span(), + children: array![ + ( + 'Left', + dojo::database::schema::serialize_member_type( + @dojo::database::schema::Ty::Tuple( + array![ + dojo::database::schema::serialize_member_type( + @dojo::database::schema::Ty::Primitive('u16') + ) + ] + .span() + ) + ) + ), + ( + 'Right', + dojo::database::schema::serialize_member_type( + @dojo::database::schema::Ty::Tuple( + array![ + dojo::database::schema::serialize_member_type( + @dojo::database::schema::Ty::Primitive('u16') + ) + ] + .span() + ) + ) + ) + ] + .span() + } + ) + } +} + + + +#[derive(Serde, Copy, Drop, Introspect)] +enum EnumTuple { + Left: (u8, u8), + Right: (u8, u8), +} + +impl EnumTupleSchemaIntrospection of dojo::database::schema::SchemaIntrospection { + #[inline(always)] + fn size() -> usize { + 3 + } + + #[inline(always)] + fn layout(ref layout: Array) { + layout.append(8); + layout.append(8); + layout.append(8); + } + + #[inline(always)] + fn ty() -> dojo::database::schema::Ty { + dojo::database::schema::Ty::Enum( + dojo::database::schema::Enum { + name: 'Direction', + attrs: array![].span(), + children: array![ + ( + 'Left', + dojo::database::schema::serialize_member_type( + @dojo::database::schema::Ty::Tuple( + array![ + dojo::database::schema::serialize_member_type( + @dojo::database::schema::Ty::Primitive('u8') + ), + dojo::database::schema::serialize_member_type( + @dojo::database::schema::Ty::Primitive('u8') + ) + ] + .span() + ) + ) + ), + ( + 'Right', + dojo::database::schema::serialize_member_type( + @dojo::database::schema::Ty::Tuple( + array![ + dojo::database::schema::serialize_member_type( + @dojo::database::schema::Ty::Primitive('u8') + ), + dojo::database::schema::serialize_member_type( + @dojo::database::schema::Ty::Primitive('u8') + ) + ] + .span() + ) + ) + ) + ] + .span() + } + ) + } +} + + + +#[derive(Serde, Copy, Drop, Introspect)] +enum EnumCustom { + Left: Vec2, + Right: Vec2, +} + +impl EnumCustomSchemaIntrospection of dojo::database::schema::SchemaIntrospection { + #[inline(always)] + fn size() -> usize { + dojo::database::schema::SchemaIntrospection::::size() + 1 + } + + #[inline(always)] + fn layout(ref layout: Array) { + layout.append(8); + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + } + + #[inline(always)] + fn ty() -> dojo::database::schema::Ty { + dojo::database::schema::Ty::Enum( + dojo::database::schema::Enum { + name: 'Direction', + attrs: array![].span(), + children: array![ + ( + 'Left', + dojo::database::schema::serialize_member_type( + @dojo::database::schema::Ty::Tuple( + array![ + dojo::database::schema::serialize_member_type( + @dojo::database::schema::SchemaIntrospection::::ty() + ) + ] + .span() + ) + ) + ), + ( + 'Right', + dojo::database::schema::serialize_member_type( + @dojo::database::schema::Ty::Tuple( + array![ + dojo::database::schema::serialize_member_type( + @dojo::database::schema::SchemaIntrospection::::ty() + ) + ] + .span() + ) + ) + ) + ] + .span() + } + ) + } +} + + + +#[derive(Model, Copy, Drop, Introspect)] +struct Position { + #[key] + player: ContractAddress, + before: u8, + vec: Vec2, + after: u16, +} +impl PositionModel of dojo::model::Model { + #[inline(always)] + fn name(self: @Position) -> felt252 { + 'Position' + } + + #[inline(always)] + fn keys(self: @Position) -> Span { + let mut serialized = ArrayTrait::new(); + serde::Serde::serialize(self.player, ref serialized); + array::ArrayTrait::span(@serialized) + } + + #[inline(always)] + fn values(self: @Position) -> Span { + let mut serialized = ArrayTrait::new(); + serde::Serde::serialize(self.before, ref serialized); + serde::Serde::serialize(self.vec, ref serialized); + serde::Serde::serialize(self.after, ref serialized); + array::ArrayTrait::span(@serialized) + } + + #[inline(always)] + fn layout(self: @Position) -> Span { + let mut layout = ArrayTrait::new(); + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + array::ArrayTrait::span(@layout) + } + + #[inline(always)] + fn packed_size(self: @Position) -> usize { + let mut layout = self.layout(); + dojo::packing::calculate_packed_size(ref layout) + } +} + + +impl PositionSchemaIntrospection of dojo::database::schema::SchemaIntrospection { + #[inline(always)] + fn size() -> usize { + dojo::database::schema::SchemaIntrospection::::size() + 2 + } + + #[inline(always)] + fn layout(ref layout: Array) { + layout.append(8); + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + layout.append(16); + } + + #[inline(always)] + fn ty() -> dojo::database::schema::Ty { + dojo::database::schema::Ty::Struct( + dojo::database::schema::Struct { + name: 'Position', + attrs: array![].span(), + children: array![ + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'player', + ty: dojo::database::schema::Ty::Primitive('ContractAddress'), + attrs: array!['key'].span() + } + ), + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'before', + ty: dojo::database::schema::Ty::Primitive('u8'), + attrs: array![].span() + } + ), + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'vec', + ty: dojo::database::schema::SchemaIntrospection::::ty(), + attrs: array![].span() + } + ), + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'after', + ty: dojo::database::schema::Ty::Primitive('u16'), + attrs: array![].span() + } + ) + ] + .span() + } + ) + } +} + + +#[starknet::interface] +trait IPosition { + fn name(self: @T) -> felt252; +} + +#[starknet::contract] +mod position { + use super::Position; + + #[storage] + struct Storage {} + + #[external(v0)] + fn name(self: @ContractState) -> felt252 { + 'Position' + } + + #[external(v0)] + fn unpacked_size(self: @ContractState) -> usize { + dojo::database::schema::SchemaIntrospection::::size() + } + + #[external(v0)] + fn packed_size(self: @ContractState) -> usize { + let mut layout = ArrayTrait::new(); + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + let mut layout_span = layout.span(); + dojo::packing::calculate_packed_size(ref layout_span) + } + + #[external(v0)] + fn layout(self: @ContractState) -> Span { + let mut layout = ArrayTrait::new(); + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + array::ArrayTrait::span(@layout) + } + + #[external(v0)] + fn schema(self: @ContractState) -> dojo::database::schema::Ty { + dojo::database::schema::SchemaIntrospection::::ty() + } +} + +impl PositionSchemaIntrospection of dojo::database::schema::SchemaIntrospection { + #[inline(always)] + fn size() -> usize { + dojo::database::schema::SchemaIntrospection::::size() + 2 + } + + #[inline(always)] + fn layout(ref layout: Array) { + layout.append(8); + dojo::database::schema::SchemaIntrospection::::layout(ref layout); + layout.append(16); + } + + #[inline(always)] + fn ty() -> dojo::database::schema::Ty { + dojo::database::schema::Ty::Struct( + dojo::database::schema::Struct { + name: 'Position', + attrs: array![].span(), + children: array![ + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'player', + ty: dojo::database::schema::Ty::Primitive('ContractAddress'), + attrs: array!['key'].span() + } + ), + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'before', + ty: dojo::database::schema::Ty::Primitive('u8'), + attrs: array![].span() + } + ), + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'vec', + ty: dojo::database::schema::SchemaIntrospection::::ty(), + attrs: array![].span() + } + ), + dojo::database::schema::serialize_member( + @dojo::database::schema::Member { + name: 'after', + ty: dojo::database::schema::Ty::Primitive('u16'), + attrs: array![].span() + } + ) + ] + .span() + } + ) + } +} + +//! > expected_diagnostics +error: Unsupported attribute. + --> test_src/lib.cairo[Position]:96:13 + #[starknet::contract] + ^*******************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[Position]:100:17 + #[storage] + ^********^ + +error: Unsupported attribute. + --> test_src/lib.cairo[Position]:103:17 + #[external(v0)] + ^*************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[Position]:108:17 + #[external(v0)] + ^*************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[Position]:113:17 + #[external(v0)] + ^*************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[Position]:121:17 + #[external(v0)] + ^*************^ + +error: Unsupported attribute. + --> test_src/lib.cairo[Position]:128:17 + #[external(v0)] + ^*************^ + //! > expanded_cairo_code use serde::Serde; diff --git a/crates/dojo-primitives/Scarb.lock b/crates/dojo-primitives/Scarb.lock new file mode 100644 index 0000000000..ba15d91ca8 --- /dev/null +++ b/crates/dojo-primitives/Scarb.lock @@ -0,0 +1,20 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "dojo" +version = "0.3.2" +dependencies = [ + "dojo_plugin", +] + +[[package]] +name = "dojo_plugin" +version = "0.3.2" + +[[package]] +name = "dojo_primitives" +version = "0.3.2" +dependencies = [ + "dojo", +] diff --git a/crates/dojo-test-utils/build.rs b/crates/dojo-test-utils/build.rs index 7160be1859..64b0e8db1f 100644 --- a/crates/dojo-test-utils/build.rs +++ b/crates/dojo-test-utils/build.rs @@ -1,7 +1,6 @@ #[cfg(feature = "build-examples")] fn main() { use std::env; - use std::process::Command; use camino::{Utf8Path, Utf8PathBuf}; use dojo_lang::compiler::DojoCompiler; @@ -11,15 +10,6 @@ fn main() { use scarb::ops::{self, CompileOpts}; use scarb_ui::Verbosity; - let version = env!("CARGO_PKG_VERSION"); - let output = Command::new("git") - .args(["rev-list", "-n", "1", &format!("v{version}")]) - .output() - .expect("Failed to execute command"); - - let git_hash = String::from_utf8(output.stdout).unwrap().trim().to_string(); - println!("cargo:rustc-env=GIT_HASH={}", git_hash); - let project_paths = ["../../examples/spawn-and-move", "../torii/graphql/src/tests/types-test"]; project_paths.iter().for_each(|path| compile(path)); diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index f62c977d16..b56ae45e84 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -123,29 +123,19 @@ impl Sql { }; let entity_id = format!("{:#x}", poseidon_hash_many(&keys)); - let existing: Option<(String,)> = - sqlx::query_as("SELECT model_names FROM entities WHERE id = ?") - .bind(&entity_id) - .fetch_optional(&self.pool) - .await?; - - let model_names = match existing { - Some((existing_names,)) if existing_names.contains(&entity.name()) => { - existing_names.to_string() - } - Some((existing_names,)) => format!("{},{}", existing_names, entity.name()), - None => entity.name().to_string(), - }; + self.query_queue.enqueue( + "INSERT INTO entity_model (entity_id, model_id) VALUES (?, ?) ON CONFLICT(entity_id, \ + model_id) DO NOTHING", + vec![Argument::String(entity_id.clone()), Argument::String(entity.name())], + ); let keys_str = felts_sql_string(&keys); - let insert_entities = "INSERT INTO entities (id, keys, model_names, event_id) VALUES (?, \ - ?, ?, ?) ON CONFLICT(id) DO UPDATE SET \ - model_names=EXCLUDED.model_names, updated_at=CURRENT_TIMESTAMP, \ + let insert_entities = "INSERT INTO entities (id, keys, event_id) VALUES (?, ?, ?) ON \ + CONFLICT(id) DO UPDATE SET updated_at=CURRENT_TIMESTAMP, \ event_id=EXCLUDED.event_id RETURNING *"; let entity_updated: EntityUpdated = sqlx::query_as(insert_entities) .bind(&entity_id) .bind(&keys_str) - .bind(&model_names) .bind(event_id) .fetch_one(&self.pool) .await?; diff --git a/crates/torii/core/src/types.rs b/crates/torii/core/src/types.rs index 41383eef2c..cfca4769bb 100644 --- a/crates/torii/core/src/types.rs +++ b/crates/torii/core/src/types.rs @@ -34,7 +34,6 @@ pub struct Entity { pub id: String, pub keys: String, pub event_id: String, - pub model_names: String, pub created_at: DateTime, pub updated_at: DateTime, } diff --git a/crates/torii/graphql/src/mapping.rs b/crates/torii/graphql/src/mapping.rs index b378146787..c780369fd9 100644 --- a/crates/torii/graphql/src/mapping.rs +++ b/crates/torii/graphql/src/mapping.rs @@ -11,7 +11,6 @@ lazy_static! { pub static ref ENTITY_TYPE_MAPPING: TypeMapping = IndexMap::from([ (Name::new("id"), TypeData::Simple(TypeRef::named(TypeRef::ID))), (Name::new("keys"), TypeData::Simple(TypeRef::named_list(TypeRef::STRING))), - (Name::new("model_names"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), (Name::new("event_id"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), ( Name::new("created_at"), diff --git a/crates/torii/graphql/src/object/entity.rs b/crates/torii/graphql/src/object/entity.rs index 4d9751573e..898bb5fdbd 100644 --- a/crates/torii/graphql/src/object/entity.rs +++ b/crates/torii/graphql/src/object/entity.rs @@ -115,7 +115,6 @@ impl EntityObject { IndexMap::from([ (Name::new("id"), Value::from(entity.id)), (Name::new("keys"), Value::from(keys)), - (Name::new("model_names"), Value::from(entity.model_names)), (Name::new("event_id"), Value::from(entity.event_id)), ( Name::new("created_at"), @@ -135,14 +134,15 @@ fn model_union_field() -> Field { match ctx.parent_value.try_to_value()? { Value::Object(indexmap) => { let mut conn = ctx.data::>()?.acquire().await?; - let model_names: Vec = extract::(indexmap, "model_names")? - .split(',') - .map(|s| s.to_string()) - .collect(); - let entity_id = extract::(indexmap, "id")?; + let model_ids: Vec<(String,)> = + sqlx::query_as("SELECT model_id from entity_model WHERE entity_id = ?") + .bind(&entity_id) + .fetch_all(&mut conn) + .await?; + let mut results: Vec> = Vec::new(); - for name in model_names { + for (name,) in model_ids { let type_mapping = type_mapping_query(&mut conn, &name).await?; let mut path_array = vec![name.clone()]; diff --git a/crates/torii/graphql/src/object/inputs/where_input.rs b/crates/torii/graphql/src/object/inputs/where_input.rs index ba0e5d7522..b7acfe7e6d 100644 --- a/crates/torii/graphql/src/object/inputs/where_input.rs +++ b/crates/torii/graphql/src/object/inputs/where_input.rs @@ -1,7 +1,9 @@ use std::str::FromStr; -use async_graphql::dynamic::{Field, InputObject, InputValue, ResolverContext, TypeRef}; -use async_graphql::Name; +use async_graphql::dynamic::{ + Field, InputObject, InputValue, ResolverContext, TypeRef, ValueAccessor, +}; +use async_graphql::{Error as GqlError, Name, Result}; use dojo_types::primitive::{Primitive, SqlType}; use strum::IntoEnumIterator; @@ -21,26 +23,25 @@ impl WhereInputObject { pub fn new(type_name: &str, object_types: &TypeMapping) -> Self { let where_mapping = object_types .iter() - .filter_map(|(type_name, type_data)| { + .filter(|(_, type_data)| !type_data.is_nested()) + .flat_map(|(type_name, type_data)| { // TODO: filter on nested and enum objects - if type_data.is_nested() { - return None; - } else if type_data.type_ref() == TypeRef::named("Enum") { - return Some(vec![(Name::new(type_name), type_data.clone())]); + if type_data.type_ref() == TypeRef::named("Enum") + || type_data.type_ref() == TypeRef::named("bool") + { + return vec![(Name::new(type_name), type_data.clone())]; } - let mut comparators = Comparator::iter() - .map(|comparator| { + Comparator::iter().fold( + vec![(Name::new(type_name), type_data.clone())], + |mut acc, comparator| { let name = format!("{}{}", type_name, comparator.as_ref()); - (Name::new(name), type_data.clone()) - }) - .collect::>(); - - comparators.push((Name::new(type_name), type_data.clone())); + acc.push((Name::new(name), type_data.clone())); - Some(comparators) + acc + }, + ) }) - .flatten() .collect(); Self { type_name: format!("{}WhereInput", type_name), type_mapping: where_mapping } @@ -70,26 +71,46 @@ pub fn where_argument(field: Field, type_name: &str) -> Field { pub fn parse_where_argument( ctx: &ResolverContext<'_>, where_mapping: &TypeMapping, -) -> Option> { - let where_input = ctx.args.get("where")?; - let input_object = where_input.object().ok()?; - - where_mapping - .iter() - .filter_map(|(type_name, type_data)| { - input_object.get(type_name).map(|input_filter| { - let filter_value = match Primitive::from_str(&type_data.type_ref().to_string()) { - Ok(primitive) => match primitive.to_sql_type() { - SqlType::Integer => FilterValue::Int(input_filter.i64().ok()?), - SqlType::Text => { - FilterValue::String(input_filter.string().ok()?.to_string()) - } - }, - _ => FilterValue::String(input_filter.string().ok()?.to_string()), - }; +) -> Result>> { + ctx.args.get("where").map_or(Ok(None), |where_input| { + let input_object = where_input.object()?; + where_mapping + .iter() + .filter_map(|(type_name, type_data)| { + input_object.get(type_name).map(|input| { + let primitive = Primitive::from_str(&type_data.type_ref().to_string())?; + let filter_value = match primitive.to_sql_type() { + SqlType::Integer => parse_integer(input, type_name, primitive)?, + SqlType::Text => parse_string(input, type_name)?, + }; - Some(parse_filter(type_name, filter_value)) + Ok(Some(parse_filter(type_name, filter_value))) + }) }) - }) - .collect::>>() + .collect::>>>() + }) +} + +fn parse_integer( + input: ValueAccessor<'_>, + type_name: &str, + primitive: Primitive, +) -> Result { + match primitive { + Primitive::Bool(_) => input + .boolean() + .map(|b| FilterValue::Int(b as i64)) // treat bool as int per sqlite + .map_err(|_| GqlError::new(format!("Expected boolean on field {}", type_name))), + _ => input + .i64() + .map(FilterValue::Int) + .map_err(|_| GqlError::new(format!("Expected integer on field {}", type_name))), + } +} + +fn parse_string(input: ValueAccessor<'_>, type_name: &str) -> Result { + input + .string() + .map(|i| FilterValue::String(i.to_string())) + .map_err(|_| GqlError::new(format!("Expected string on field {}", type_name))) } diff --git a/crates/torii/graphql/src/object/model_data.rs b/crates/torii/graphql/src/object/model_data.rs index aadd7ec784..642dfc7e2d 100644 --- a/crates/torii/graphql/src/object/model_data.rs +++ b/crates/torii/graphql/src/object/model_data.rs @@ -86,7 +86,7 @@ impl ObjectTrait for ModelDataObject { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; let order = parse_order_argument(&ctx); - let filters = parse_where_argument(&ctx, &where_mapping); + let filters = parse_where_argument(&ctx, &where_mapping)?; let connection = parse_connection_arguments(&ctx)?; let id_column = "event_id"; diff --git a/crates/torii/graphql/src/query/filter.rs b/crates/torii/graphql/src/query/filter.rs index 2d3da160c6..dd6aef3f2e 100644 --- a/crates/torii/graphql/src/query/filter.rs +++ b/crates/torii/graphql/src/query/filter.rs @@ -2,7 +2,7 @@ use core::fmt; use async_graphql::Name; use strum::IntoEnumIterator; -use strum_macros::{AsRefStr, EnumIter}; +use strum_macros::{AsRefStr, Display, EnumIter}; #[derive(AsRefStr, Debug, Clone, PartialEq, EnumIter)] #[strum(serialize_all = "UPPERCASE")] @@ -28,7 +28,7 @@ impl fmt::Display for Comparator { } } -#[derive(Debug)] +#[derive(Debug, Display)] pub enum FilterValue { Int(i64), String(String), diff --git a/crates/torii/graphql/src/tests/entities_test.rs b/crates/torii/graphql/src/tests/entities_test.rs index ac42333c8d..007096dfec 100644 --- a/crates/torii/graphql/src/tests/entities_test.rs +++ b/crates/torii/graphql/src/tests/entities_test.rs @@ -21,7 +21,6 @@ mod tests { cursor node {{ keys - model_names }} }} page_info {{ @@ -46,7 +45,6 @@ mod tests { {{ entity (id: "{:#x}") {{ keys - model_names models {{ ... on Record {{ __typename @@ -93,12 +91,8 @@ mod tests { // default without params let entities = entities_query(&schema, "").await; let connection: Connection = serde_json::from_value(entities).unwrap(); - let first_entity = connection.edges.first().unwrap(); - let last_entity = connection.edges.last().unwrap(); assert_eq!(connection.edges.len(), 10); assert_eq!(connection.total_count, 20); - assert_eq!(&first_entity.node.model_names, "Subrecord"); - assert_eq!(&last_entity.node.model_names, "Record,RecordSibling"); // first key param - returns all entities with `0x0` as first key let entities = entities_query(&schema, "(keys: [\"0x0\"])").await; diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index 3a07eff19a..175da5635d 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -51,7 +51,6 @@ pub struct Edge { #[derive(Deserialize, Debug, PartialEq)] pub struct Entity { - pub model_names: String, pub keys: Option>, pub created_at: Option, } @@ -270,7 +269,7 @@ pub async fn spinup_types_test() -> Result { execute_strategy(&ws, &migration, &account, None).await.unwrap(); // Execute `create` and insert 10 records into storage - let records_contract = "0x27f701de7d71a2a6ee670bc1ff47a901fdc671cca26fe234ca1a42273aa7f7d"; + let records_contract = "0x50684e3c60b9fe91665aec1acce14ef088a94afb5e0417bcc196f73a92cc389"; let InvokeTransactionResult { transaction_hash } = account .execute(vec![Call { calldata: vec![FieldElement::from_str("0xa").unwrap()], diff --git a/crates/torii/graphql/src/tests/models_test.rs b/crates/torii/graphql/src/tests/models_test.rs index c0942937bc..4678013f9f 100644 --- a/crates/torii/graphql/src/tests/models_test.rs +++ b/crates/torii/graphql/src/tests/models_test.rs @@ -54,7 +54,6 @@ mod tests { }} entity {{ keys - model_names }} }} }} @@ -92,7 +91,6 @@ mod tests { assert_eq!(connection.total_count, 10); assert_eq!(connection.edges.len(), 10); assert_eq!(&record.node.__typename, "Record"); - assert_eq!(&entity.model_names, "Record,RecordSibling"); assert_eq!(entity.keys.clone().unwrap(), vec!["0x0"]); assert_eq!(record.node.depth, "Zero"); assert_eq!(nested.depth, "One"); @@ -169,15 +167,14 @@ mod tests { assert_eq!(last_record.node.type_u256, "0x0"); // where filter on true bool - // TODO: use bool values on input instead of 0 or 1 - let records = records_model_query(&schema, "(where: { type_bool: 1 })").await; + let records = records_model_query(&schema, "(where: { type_bool: true })").await; let connection: Connection = serde_json::from_value(records).unwrap(); let first_record = connection.edges.first().unwrap(); assert_eq!(connection.total_count, 5); assert!(first_record.node.type_bool, "should be true"); // where filter on false bool - let records = records_model_query(&schema, "(where: { type_bool: 0 })").await; + let records = records_model_query(&schema, "(where: { type_bool: false })").await; let connection: Connection = serde_json::from_value(records).unwrap(); let first_record = connection.edges.first().unwrap(); assert_eq!(connection.total_count, 5); diff --git a/crates/torii/graphql/src/tests/subscription_test.rs b/crates/torii/graphql/src/tests/subscription_test.rs index 4c48dc93ba..89b851674e 100644 --- a/crates/torii/graphql/src/tests/subscription_test.rs +++ b/crates/torii/graphql/src/tests/subscription_test.rs @@ -30,7 +30,6 @@ mod tests { "entityUpdated": { "id": entity_id, "keys":vec![keys_str], - "model_names": model_name, "models" : [{ "__typename": model_name, "depth": "Zero", @@ -115,7 +114,6 @@ mod tests { entityUpdated { id keys - model_names models { __typename ... on Record { @@ -153,7 +151,6 @@ mod tests { "entityUpdated": { "id": entity_id, "keys":vec![keys_str], - "model_names": model_name, "models" : [{ "__typename": model_name, "depth": "Zero", @@ -220,7 +217,6 @@ mod tests { entityUpdated(id: "0x579e8877c7755365d5ec1ec7d3a94a457eff5d1f40482bbe9729c064cdead2") { id keys - model_names models { __typename ... on Record { diff --git a/crates/torii/grpc/proto/types.proto b/crates/torii/grpc/proto/types.proto index 80d9ae8da6..873cabd4b7 100644 --- a/crates/torii/grpc/proto/types.proto +++ b/crates/torii/grpc/proto/types.proto @@ -94,7 +94,7 @@ message EntityQuery { message Clause { oneof clause_type { KeysClause keys = 1; - AttributeClause attribute = 2; + MemberClause member = 2; CompositeClause composite = 3; } } @@ -104,7 +104,7 @@ message KeysClause { repeated bytes keys = 2; } -message AttributeClause { +message MemberClause { string model = 1; string member = 2; ComparisonOperator operator = 3; diff --git a/crates/torii/grpc/src/server/mod.rs b/crates/torii/grpc/src/server/mod.rs index 8b7d484817..398343b9c7 100644 --- a/crates/torii/grpc/src/server/mod.rs +++ b/crates/torii/grpc/src/server/mod.rs @@ -165,7 +165,7 @@ impl DojoWorld { async fn entities_by_attribute( &self, - _attribute: proto::types::AttributeClause, + _attribute: proto::types::MemberClause, _limit: u32, _offset: u32, ) -> Result, Error> { @@ -249,7 +249,7 @@ impl DojoWorld { ClauseType::Keys(keys) => { self.entities_by_keys(keys, query.limit, query.offset).await? } - ClauseType::Attribute(attribute) => { + ClauseType::Member(attribute) => { self.entities_by_attribute(attribute, query.limit, query.offset).await? } ClauseType::Composite(composite) => { diff --git a/crates/torii/grpc/src/types.rs b/crates/torii/grpc/src/types.rs index 00efe50358..c2c834cf42 100644 --- a/crates/torii/grpc/src/types.rs +++ b/crates/torii/grpc/src/types.rs @@ -20,7 +20,7 @@ pub struct Query { #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] pub enum Clause { Keys(KeysClause), - Attribute(AttributeClause), + Member(MemberClause), Composite(CompositeClause), } @@ -31,7 +31,7 @@ pub struct KeysClause { } #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] -pub struct AttributeClause { +pub struct MemberClause { pub model: String, pub member: String, pub operator: ComparisonOperator, @@ -117,9 +117,9 @@ impl From for proto::types::Clause { Clause::Keys(clause) => { Self { clause_type: Some(proto::types::clause::ClauseType::Keys(clause.into())) } } - Clause::Attribute(clause) => Self { - clause_type: Some(proto::types::clause::ClauseType::Attribute(clause.into())), - }, + Clause::Member(clause) => { + Self { clause_type: Some(proto::types::clause::ClauseType::Member(clause.into())) } + } Clause::Composite(clause) => Self { clause_type: Some(proto::types::clause::ClauseType::Composite(clause.into())), }, @@ -150,8 +150,8 @@ impl TryFrom for KeysClause { } } -impl From for proto::types::AttributeClause { - fn from(value: AttributeClause) -> Self { +impl From for proto::types::MemberClause { + fn from(value: MemberClause) -> Self { Self { model: value.model, member: value.member, diff --git a/crates/torii/migrations/20231127235011_entity_model.sql b/crates/torii/migrations/20231127235011_entity_model.sql new file mode 100644 index 0000000000..ed1838c9ac --- /dev/null +++ b/crates/torii/migrations/20231127235011_entity_model.sql @@ -0,0 +1,40 @@ +-- NOTE: sqlite does not support deleteing columns. Workaround is to create new table, copy, and delete old. + +-- Create new table without model_names column +CREATE TABLE entities_new ( + id TEXT NOT NULL PRIMARY KEY, + keys TEXT, + event_id TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Copy from old entities +INSERT INTO entities_new (id, keys, event_id, created_at, updated_at) +SELECT id, keys, event_id, created_at, updated_at +FROM entities; + +-- Disable foreign keys constraint so we can delete entities +PRAGMA foreign_keys = OFF; + +-- Drop old entities +DROP TABLE entities; + +-- Rename table and recreate indexes +ALTER TABLE entities_new RENAME TO entities; +CREATE INDEX idx_entities_keys ON entities (keys); +CREATE INDEX idx_entities_event_id ON entities (event_id); + +-- Renable foreign keys +PRAGMA foreign_keys = ON; + +-- New table to track entity to model relationships +CREATE TABLE entity_model ( + entity_id TEXT NOT NULL, + model_id TEXT NOT NULL, + UNIQUE (entity_id, model_id), + FOREIGN KEY (entity_id) REFERENCES entities (id), + FOREIGN KEY (model_id) REFERENCES models (id) +); +CREATE INDEX idx_entity_model_entity_id ON entity_model (entity_id); +CREATE INDEX idx_entity_model_model_id ON entity_model (model_id); \ No newline at end of file diff --git a/examples/spawn-and-move/README.md b/examples/spawn-and-move/README.md index 2571aeb003..75e0ff346f 100644 --- a/examples/spawn-and-move/README.md +++ b/examples/spawn-and-move/README.md @@ -12,7 +12,7 @@ sozo build sozo migrate # Get the class hash of the Moves model by name -sozo model get --world 0x26065106fa319c3981618e7567480a50132f23932226a51c219ffb8e47daa84 Moves +sozo model class-hash --world 0x26065106fa319c3981618e7567480a50132f23932226a51c219ffb8e47daa84 Moves > 0x2b97f0b24be59ecf4504a27ac2301179be7df44c4c7d9482cd7b36137bc0fa4 # Get the schema of the Moves model @@ -24,14 +24,14 @@ sozo model schema --world 0x26065106fa319c3981618e7567480a50132f23932226a51c219f # Get the value of the Moves model for an entity. (in this example, # 0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 is # the calling account. -sozo model entity --world 0x26065106fa319c3981618e7567480a50132f23932226a51c219ffb8e47daa84 Moves 0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 +sozo model get --world 0x26065106fa319c3981618e7567480a50132f23932226a51c219ffb8e47daa84 Moves 0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 > 0x0 -# The returned value is 0 since we haven't spawned yet. Let's spawn -# a player for the caller -sozo execute --world 0x26065106fa319c3981618e7567480a50132f23932226a51c219ffb8e47daa84 spawn +# The returned value is 0 since we haven't spawned yet. +# We can spawn a player using the actions contract address +sozo execute 0x31571485922572446df9e3198a891e10d3a48e544544317dbcbb667e15848cd spawn # Fetch the updated entity -sozo model entity --world 0x26065106fa319c3981618e7567480a50132f23932226a51c219ffb8e47daa84 Moves 0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 +sozo model get --world 0x26065106fa319c3981618e7567480a50132f23932226a51c219ffb8e47daa84 Moves 0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 > 0xa ``` diff --git a/examples/spawn-and-move/Scarb.lock b/examples/spawn-and-move/Scarb.lock index 40830134bd..87ccac7512 100644 --- a/examples/spawn-and-move/Scarb.lock +++ b/examples/spawn-and-move/Scarb.lock @@ -3,19 +3,19 @@ version = 1 [[package]] name = "dojo" -version = "0.3.12" +version = "0.3.13" dependencies = [ "dojo_plugin", ] [[package]] name = "dojo_examples" -version = "0.3.12" +version = "0.3.13" dependencies = [ "dojo", ] [[package]] name = "dojo_plugin" -version = "0.3.12" -source = "git+https://github.com/dojoengine/dojo?tag=v0.3.12#12d58f29ec53454317f1f6d265007a053d279288" +version = "0.3.11" +source = "git+https://github.com/dojoengine/dojo?tag=v0.3.11#1e651b5d4d3b79b14a7d8aa29a92062fcb9e6659" diff --git a/examples/spawn-and-move/Scarb.toml b/examples/spawn-and-move/Scarb.toml index 01191e1edb..1e76728e61 100644 --- a/examples/spawn-and-move/Scarb.toml +++ b/examples/spawn-and-move/Scarb.toml @@ -1,7 +1,7 @@ [package] cairo-version = "2.3.1" name = "dojo_examples" -version = "0.3.12" +version = "0.3.13" [cairo] sierra-replace-ids = true