From c350e4f35c49bcf8a8b521851f790234ba2c0295 Mon Sep 17 00:00:00 2001 From: Krishnanand V P <44740264+incrypto32@users.noreply.github.com> Date: Mon, 14 Aug 2023 13:26:45 +0530 Subject: [PATCH] graph, tests : Fix `loadDerived` not taking entity cache into consideration when loading derived entities (#4799) - graph: Consider `udpates` and `handler_updates` in `load_related` - Add more tests for derived loaders --- graph/src/components/store/entity_cache.rs | 111 +++++++++++- graph/src/components/store/mod.rs | 17 ++ graph/src/util/lfu_cache.rs | 6 + store/postgres/src/relational_queries.rs | 14 +- store/postgres/src/writable.rs | 10 +- .../derived-loaders/abis/Contract.abi | 33 ---- .../derived-loaders/schema.graphql | 21 --- .../derived-loaders/src/mapping.ts | 34 ---- .../derived-loaders/test/test.js | 120 ------------- .../derived-loaders/truffle.js | 22 --- .../derived-loaders/abis/Contract.abi | 15 ++ .../derived-loaders/package.json | 2 +- .../derived-loaders/schema.graphql | 45 +++++ .../derived-loaders/src/helpers.ts | 160 +++++++++++++++++ .../derived-loaders/src/mapping.ts | 166 ++++++++++++++++++ .../derived-loaders/subgraph.yaml | 6 +- tests/runner-tests/package.json | 3 +- tests/runner-tests/yarn.lock | 74 ++++++++ tests/tests/integration_tests.rs | 1 - tests/tests/runner_tests.rs | 159 +++++++++++++++++ 20 files changed, 772 insertions(+), 247 deletions(-) delete mode 100644 tests/integration-tests/derived-loaders/abis/Contract.abi delete mode 100644 tests/integration-tests/derived-loaders/schema.graphql delete mode 100644 tests/integration-tests/derived-loaders/src/mapping.ts delete mode 100644 tests/integration-tests/derived-loaders/test/test.js delete mode 100644 tests/integration-tests/derived-loaders/truffle.js create mode 100644 tests/runner-tests/derived-loaders/abis/Contract.abi rename tests/{integration-tests => runner-tests}/derived-loaders/package.json (95%) create mode 100644 tests/runner-tests/derived-loaders/schema.graphql create mode 100644 tests/runner-tests/derived-loaders/src/helpers.ts create mode 100644 tests/runner-tests/derived-loaders/src/mapping.ts rename tests/{integration-tests => runner-tests}/derived-loaders/subgraph.yaml (76%) diff --git a/graph/src/components/store/entity_cache.rs b/graph/src/components/store/entity_cache.rs index 77e01f04371..292ed91699d 100644 --- a/graph/src/components/store/entity_cache.rs +++ b/graph/src/components/store/entity_cache.rs @@ -1,5 +1,4 @@ use anyhow::anyhow; -use inflector::Inflector; use std::borrow::Cow; use std::collections::HashMap; use std::fmt::{self, Debug}; @@ -203,18 +202,114 @@ impl EntityCache { let query = DerivedEntityQuery { entity_type: EntityType::new(base_type.to_string()), - entity_field: field.name.clone().to_snake_case().into(), + entity_field: field.name.clone().into(), value: eref.entity_id.clone(), causality_region: eref.causality_region, id_is_bytes: id_is_bytes, }; - let entities = self.store.get_derived(&query)?; - entities.iter().for_each(|(key, e)| { - self.current.insert(key.clone(), Some(e.clone())); - }); - let entities: Vec = entities.values().cloned().collect(); - Ok(entities) + let mut entity_map = self.store.get_derived(&query)?; + + for (key, entity) in entity_map.iter() { + // Only insert to the cache if it's not already there + if !self.current.contains_key(&key) { + self.current.insert(key.clone(), Some(entity.clone())); + } + } + + let mut keys_to_remove = Vec::new(); + + // Apply updates from `updates` and `handler_updates` directly to entities in `entity_map` that match the query + for (key, entity) in entity_map.iter_mut() { + let mut entity_cow = Some(Cow::Borrowed(entity)); + + if let Some(op) = self.updates.get(key).cloned() { + op.apply_to(&mut entity_cow) + .map_err(|e| key.unknown_attribute(e))?; + } + + if let Some(op) = self.handler_updates.get(key).cloned() { + op.apply_to(&mut entity_cow) + .map_err(|e| key.unknown_attribute(e))?; + } + + if let Some(updated_entity) = entity_cow { + *entity = updated_entity.into_owned(); + } else { + // if entity_cow is None, it means that the entity was removed by an update + // mark the key for removal from the map + keys_to_remove.push(key.clone()); + } + } + + // A helper function that checks if an update matches the query and returns the updated entity if it does + fn matches_query( + op: &EntityOp, + query: &DerivedEntityQuery, + key: &EntityKey, + ) -> Result, anyhow::Error> { + match op { + EntityOp::Update(entity) | EntityOp::Overwrite(entity) + if query.matches(key, entity) => + { + Ok(Some(entity.clone())) + } + EntityOp::Remove => Ok(None), + _ => Ok(None), + } + } + + // Iterate over self.updates to find entities that: + // - Aren't already present in the entity_map + // - Match the query + // If these conditions are met: + // - Check if there's an update for the same entity in handler_updates and apply it. + // - Add the entity to entity_map. + for (key, op) in self.updates.iter() { + if !entity_map.contains_key(key) { + if let Some(entity) = matches_query(op, &query, key)? { + if let Some(handler_op) = self.handler_updates.get(key).cloned() { + // If there's a corresponding update in handler_updates, apply it to the entity + // and insert the updated entity into entity_map + let mut entity_cow = Some(Cow::Borrowed(&entity)); + handler_op + .apply_to(&mut entity_cow) + .map_err(|e| key.unknown_attribute(e))?; + + if let Some(updated_entity) = entity_cow { + entity_map.insert(key.clone(), updated_entity.into_owned()); + } + } else { + // If there isn't a corresponding update in handler_updates or the update doesn't match the query, just insert the entity from self.updates + entity_map.insert(key.clone(), entity); + } + } + } + } + + // Iterate over handler_updates to find entities that: + // - Aren't already present in the entity_map. + // - Aren't present in self.updates. + // - Match the query. + // If these conditions are met, add the entity to entity_map. + for (key, handler_op) in self.handler_updates.iter() { + if !entity_map.contains_key(key) && !self.updates.contains_key(key) { + if let Some(entity) = matches_query(handler_op, &query, key)? { + entity_map.insert(key.clone(), entity); + } + } + } + + // Remove entities that are in the store but have been removed by an update. + // We do this last since the loops over updates and handler_updates are only + // concerned with entities that are not in the store yet and by leaving removed + // keys in entity_map we avoid processing these updates a second time when we + // already looked at them when we went through entity_map + for key in keys_to_remove { + entity_map.remove(&key); + } + + Ok(entity_map.into_values().collect()) } pub fn remove(&mut self, key: EntityKey) { diff --git a/graph/src/components/store/mod.rs b/graph/src/components/store/mod.rs index a6dfabcb7e5..5bd27c65ab0 100644 --- a/graph/src/components/store/mod.rs +++ b/graph/src/components/store/mod.rs @@ -20,6 +20,7 @@ use std::borrow::Borrow; use std::collections::btree_map::Entry; use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::fmt::Display; +use std::str::FromStr; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, RwLock}; use std::time::Duration; @@ -199,6 +200,22 @@ pub struct DerivedEntityQuery { pub causality_region: CausalityRegion, } +impl DerivedEntityQuery { + /// Checks if a given key and entity match this query. + pub fn matches(&self, key: &EntityKey, entity: &Entity) -> bool { + key.entity_type == self.entity_type + && entity + .get(&self.entity_field) + .map(|v| match v { + Value::String(s) => s.as_str() == self.value.as_str(), + Value::Bytes(b) => Bytes::from_str(self.value.as_str()) + .map_or(false, |bytes_value| &bytes_value == b), + _ => false, + }) + .unwrap_or(false) + } +} + impl EntityKey { // For use in tests only #[cfg(debug_assertions)] diff --git a/graph/src/util/lfu_cache.rs b/graph/src/util/lfu_cache.rs index 915e793ec59..f99d24fc744 100644 --- a/graph/src/util/lfu_cache.rs +++ b/graph/src/util/lfu_cache.rs @@ -177,6 +177,12 @@ impl }) } + pub fn iter<'a>(&'a self) -> impl Iterator { + self.queue + .iter() + .map(|entry| (&entry.0.key, &entry.0.value)) + } + pub fn get(&mut self, key: &K) -> Option<&V> { self.get_mut(key.clone()).map(|x| &x.value) } diff --git a/store/postgres/src/relational_queries.rs b/store/postgres/src/relational_queries.rs index 6a60a3ccc4a..1de09778538 100644 --- a/store/postgres/src/relational_queries.rs +++ b/store/postgres/src/relational_queries.rs @@ -27,6 +27,7 @@ use graph::{ components::store::{AttributeNames, EntityType}, data::store::scalar, }; +use inflector::Inflector; use itertools::Itertools; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::convert::TryFrom; @@ -1760,11 +1761,20 @@ impl<'a> QueryFragment for FindDerivedQuery<'a> { if i > 0 { out.push_sql(", "); } - out.push_bind_param::(&value.entity_id.as_str())?; + + if *id_is_bytes { + out.push_sql("decode("); + out.push_bind_param::( + &value.entity_id.as_str().strip_prefix("0x").unwrap(), + )?; + out.push_sql(", 'hex')"); + } else { + out.push_bind_param::(&value.entity_id.as_str())?; + } } out.push_sql(") and "); } - out.push_identifier(entity_field.as_str())?; + out.push_identifier(entity_field.to_snake_case().as_str())?; out.push_sql(" = "); if *id_is_bytes { out.push_sql("decode("); diff --git a/store/postgres/src/writable.rs b/store/postgres/src/writable.rs index 081c5514ea4..3e8e589c2ca 100644 --- a/store/postgres/src/writable.rs +++ b/store/postgres/src/writable.rs @@ -1,5 +1,6 @@ use std::collections::BTreeSet; use std::ops::Deref; +use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Mutex, RwLock, TryLockError as RwLockError}; use std::time::{Duration, Instant}; @@ -10,6 +11,8 @@ use graph::components::store::{ Batch, DeploymentCursorTracker, DerivedEntityQuery, EntityKey, ReadStore, }; use graph::constraint_violation; +use graph::data::store::scalar::Bytes; +use graph::data::store::Value; use graph::data::subgraph::schema; use graph::data_source::CausalityRegion; use graph::prelude::{ @@ -1100,7 +1103,12 @@ impl Queue { fn is_related(derived_query: &DerivedEntityQuery, entity: &Entity) -> bool { entity .get(&derived_query.entity_field) - .map(|related_id| related_id.as_str() == Some(&derived_query.value)) + .map(|v| match v { + Value::String(s) => s.as_str() == derived_query.value.as_str(), + Value::Bytes(b) => Bytes::from_str(derived_query.value.as_str()) + .map_or(false, |bytes_value| &bytes_value == b), + _ => false, + }) .unwrap_or(false) } diff --git a/tests/integration-tests/derived-loaders/abis/Contract.abi b/tests/integration-tests/derived-loaders/abis/Contract.abi deleted file mode 100644 index 02da1a9e7f3..00000000000 --- a/tests/integration-tests/derived-loaders/abis/Contract.abi +++ /dev/null @@ -1,33 +0,0 @@ -[ - { - "inputs": [], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint16", - "name": "x", - "type": "uint16" - } - ], - "name": "Trigger", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "uint16", - "name": "x", - "type": "uint16" - } - ], - "name": "emitTrigger", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } -] diff --git a/tests/integration-tests/derived-loaders/schema.graphql b/tests/integration-tests/derived-loaders/schema.graphql deleted file mode 100644 index 57886a920dd..00000000000 --- a/tests/integration-tests/derived-loaders/schema.graphql +++ /dev/null @@ -1,21 +0,0 @@ -type BFoo @entity { - id: Bytes! - value: Int8! - bar: BBar! @derivedFrom(field: "fooValue") -} - -type BBar @entity { - id: Bytes! - fooValue: BFoo! -} - -type Foo @entity { - id: ID! - value: Int8! - bar: Bar! @derivedFrom(field: "fooValue") -} - -type Bar @entity { - id: ID! - fooValue: Foo! -} diff --git a/tests/integration-tests/derived-loaders/src/mapping.ts b/tests/integration-tests/derived-loaders/src/mapping.ts deleted file mode 100644 index 4d5c5cd408b..00000000000 --- a/tests/integration-tests/derived-loaders/src/mapping.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Bytes, store } from "@graphprotocol/graph-ts"; -import { Trigger } from "../generated/Contract/Contract"; -import { Bar, Foo, BFoo, BBar } from "../generated/schema"; - -export function handleTrigger(event: Trigger): void { - let foo = new Foo("0"); - foo.value = 1; - foo.save(); - - let bar = new Bar("1"); - bar.fooValue = "0"; - bar.save(); - - let fooLoaded = Foo.load("0"); - - let barDerived = fooLoaded!.bar.load(); - - assert(barDerived !== null, "barDerived should not be null"); - - let bFoo = new BFoo(Bytes.fromUTF8("0")); - bFoo.value = 1; - bFoo.save(); - - let bBar = new BBar(Bytes.fromUTF8("1")); - bBar.fooValue = Bytes.fromUTF8("0"); - bBar.save(); - - let bFooLoaded = BFoo.load(Bytes.fromUTF8("0")); - let bBarDerived = changetype( - store.loadRelated("BFoo", bFooLoaded!.id.toHexString(), "bar") - ); - - assert(bBarDerived !== null, "bBarDerived should not be null"); -} diff --git a/tests/integration-tests/derived-loaders/test/test.js b/tests/integration-tests/derived-loaders/test/test.js deleted file mode 100644 index b3f04865759..00000000000 --- a/tests/integration-tests/derived-loaders/test/test.js +++ /dev/null @@ -1,120 +0,0 @@ -const path = require("path"); -const execSync = require("child_process").execSync; -const { system, patching } = require("gluegun"); -const { createApolloFetch } = require("apollo-fetch"); - -const Contract = artifacts.require("./Contract.sol"); - -const srcDir = path.join(__dirname, ".."); - -const httpPort = process.env.GRAPH_NODE_HTTP_PORT || 18000; -const indexPort = process.env.GRAPH_NODE_INDEX_PORT || 18030; - -const fetchSubgraphs = createApolloFetch({ - uri: `http://localhost:${indexPort}/graphql`, -}); -const fetchSubgraph = createApolloFetch({ - uri: `http://localhost:${httpPort}/subgraphs/name/test/derived-loaders`, -}); - -const exec = (cmd) => { - try { - return execSync(cmd, { cwd: srcDir, stdio: "inherit" }); - } catch (e) { - throw new Error(`Failed to run command \`${cmd}\``); - } -}; - -const waitForSubgraphToBeSynced = async () => - new Promise((resolve, reject) => { - // Wait for 60s - let deadline = Date.now() + 60 * 1000; - - // Function to check if the subgraph is synced - const checkSubgraphSynced = async () => { - try { - let result = await fetchSubgraphs({ - query: `{ indexingStatuses { synced, health } }`, - }); - - if (result.data.indexingStatuses[0].synced) { - resolve(); - } else if (result.data.indexingStatuses[0].health != "healthy") { - reject(new Error(`Subgraph failed`)); - } else { - throw new Error("reject or retry"); - } - } catch (e) { - if (Date.now() > deadline) { - reject(new Error(`Timed out waiting for the subgraph to sync`)); - } else { - setTimeout(checkSubgraphSynced, 1000); - } - } - }; - - // Periodically check whether the subgraph has synced - setTimeout(checkSubgraphSynced, 0); - }); - -contract("Contract", (accounts) => { - // Deploy the subgraph once before all tests - before(async () => { - // Deploy the contract - const contract = await Contract.deployed(); - - // Insert its address into subgraph manifest - await patching.replace( - path.join(srcDir, "subgraph.yaml"), - "0x0000000000000000000000000000000000000000", - contract.address - ); - - // Create and deploy the subgraph - exec(`yarn codegen`); - exec(`yarn create:test`); - exec(`yarn deploy:test`); - - // Wait for the subgraph to be indexed - await waitForSubgraphToBeSynced(); - }); - - it("should return correct BFoos", async () => { - let result = await fetchSubgraph({ - query: `{ - bfoos(orderBy: id) { id value bar { id } } - }`, - }); - - expect(result.errors).to.be.undefined; - expect(result.data).to.deep.equal({ - bfoos: [ - { - id: "0x30", - value: "1", - bar: { id: "0x31" }, - }, - ], - }); - }); - - - it("should return correct Foos", async () => { - let result = await fetchSubgraph({ - query: `{ - foos(orderBy: id) { id value bar { id } } - }`, - }); - - expect(result.errors).to.be.undefined; - expect(result.data).to.deep.equal({ - foos: [ - { - id: "0", - value: "1", - bar: { id: "1" }, - }, - ], - }); - }); -}); diff --git a/tests/integration-tests/derived-loaders/truffle.js b/tests/integration-tests/derived-loaders/truffle.js deleted file mode 100644 index 27a9675b4d7..00000000000 --- a/tests/integration-tests/derived-loaders/truffle.js +++ /dev/null @@ -1,22 +0,0 @@ -require("babel-register"); -require("babel-polyfill"); - -module.exports = { - contracts_directory: "../../common", - migrations_directory: "../../common", - contracts_build_directory: "./truffle_output", - networks: { - test: { - host: "localhost", - port: process.env.GANACHE_TEST_PORT || 18545, - network_id: "*", - gas: "100000000000", - gasPrice: "1" - } - }, - compilers: { - solc: { - version: "0.8.2" - } - } -}; diff --git a/tests/runner-tests/derived-loaders/abis/Contract.abi b/tests/runner-tests/derived-loaders/abis/Contract.abi new file mode 100644 index 00000000000..9d9f56b9263 --- /dev/null +++ b/tests/runner-tests/derived-loaders/abis/Contract.abi @@ -0,0 +1,15 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "testCommand", + "type": "string" + } + ], + "name": "TestEvent", + "type": "event" + } +] diff --git a/tests/integration-tests/derived-loaders/package.json b/tests/runner-tests/derived-loaders/package.json similarity index 95% rename from tests/integration-tests/derived-loaders/package.json rename to tests/runner-tests/derived-loaders/package.json index d9488cef457..3d681dd86c7 100644 --- a/tests/integration-tests/derived-loaders/package.json +++ b/tests/runner-tests/derived-loaders/package.json @@ -9,7 +9,7 @@ "deploy:test": "graph deploy test/derived-loaders --version-label v0.0.1 --ipfs $IPFS_URI --node $GRAPH_NODE_ADMIN_URI" }, "devDependencies": { - "@graphprotocol/graph-cli": "0.51.0", + "@graphprotocol/graph-cli": "0.53.0", "@graphprotocol/graph-ts": "0.31.0", "solc": "^0.8.2" }, diff --git a/tests/runner-tests/derived-loaders/schema.graphql b/tests/runner-tests/derived-loaders/schema.graphql new file mode 100644 index 00000000000..ade664b2f00 --- /dev/null +++ b/tests/runner-tests/derived-loaders/schema.graphql @@ -0,0 +1,45 @@ +type BFoo @entity { + id: Bytes! + value: Int8! + bar: [BBar!]! @derivedFrom(field: "fooValue") +} + +type BBar @entity { + id: Bytes! + value: Int8! + value2: Int8! + fooValue: BFoo! +} + +type BBarTestResult @entity { + id: Bytes! + value: Int8! + value2: Int8! + fooValue: BFoo! +} + +type Foo @entity { + id: ID! + value: Int8! + bar: [Bar!]! @derivedFrom(field: "fooValue") +} + +type Bar @entity { + id: ID! + value: Int8! + value2: Int8! + fooValue: Foo! +} + +type BarTestResult @entity { + id: ID! + value: Int8! + value2: Int8! + fooValue: Foo! +} + +type TestResult @entity { + id: ID! + barDerived: [BarTestResult!] + bBarDerived: [BBarTestResult!] +} diff --git a/tests/runner-tests/derived-loaders/src/helpers.ts b/tests/runner-tests/derived-loaders/src/helpers.ts new file mode 100644 index 00000000000..72bc2e8f959 --- /dev/null +++ b/tests/runner-tests/derived-loaders/src/helpers.ts @@ -0,0 +1,160 @@ +import { Bytes, log } from "@graphprotocol/graph-ts"; +import { + BBar, + BBarTestResult, + Bar, + BarTestResult, + TestResult, +} from "../generated/schema"; + +/** + * Asserts that two `Bar` instances are equal. + */ +export function assertBarsEqual(a: Bar, b: Bar): void { + assert( + a.id == b.id && + a.value == b.value && + a.value2 == b.value2 && + a.fooValue == b.fooValue, + "Bar instances are not equal" + ); +} + +/** + * Asserts that two `BBar` instances are equal. + */ +export function assertBBarsEqual( + a: BBar, + b: BBar, + message: string = "BBar instances are not equal" +): void { + assert( + a.id.toHex() == b.id.toHex() && + a.value == b.value && + a.value2 == b.value2 && + a.fooValue.toHex() == b.fooValue.toHex(), + message + ); +} + +/** + * Creates a new `Bar` entity and saves it. + */ +export function createBar( + id: string, + fooValue: string, + value: i64, + value2: i64 +): Bar { + let bar = new Bar(id); + bar.fooValue = fooValue; + bar.value = value; + bar.value2 = value2; + bar.save(); + return bar; +} + +/** + * Creates a new `BBar` entity and saves it. + */ +export function createBBar( + id: Bytes, + fooValue: Bytes, + value: i64, + value2: i64 +): BBar { + let bBar = new BBar(id); + bBar.fooValue = fooValue; + bBar.value = value; + bBar.value2 = value2; + bBar.save(); + return bBar; +} + +/** + * A function to loop over an array of `Bar` instances and assert that the values are equal. + */ +export function assertBarsArrayEqual(bars: Bar[], expected: Bar[]): void { + assert(bars.length == expected.length, "bars.length != expected.length"); + for (let i = 0; i < bars.length; i++) { + assertBarsEqual(bars[i], expected[i]); + } +} + +/** + * A function to loop over an array of `BBar` instances and assert that the values are equal. + */ +export function assertBBarsArrayEqual(bBars: BBar[], expected: BBar[]): void { + assert(bBars.length == expected.length, "bBars.length != expected.length"); + for (let i = 0; i < bBars.length; i++) { + assertBBarsEqual(bBars[i], expected[i]); + } +} + +export function convertBarToBarTestResult( + barInstance: Bar, + testId: string +): BarTestResult { + const barTestResult = new BarTestResult(barInstance.id + "_" + testId); + + barTestResult.value = barInstance.value; + barTestResult.value2 = barInstance.value2; + barTestResult.fooValue = barInstance.fooValue; + barTestResult.save(); + + return barTestResult; +} + +export function convertbBarToBBarTestResult( + bBarInstance: BBar, + testId: string +): BBarTestResult { + const bBarTestResult = new BBarTestResult( + Bytes.fromUTF8(bBarInstance.id.toString() + "_" + testId) + ); + + bBarTestResult.value = bBarInstance.value; + bBarTestResult.value2 = bBarInstance.value2; + bBarTestResult.fooValue = bBarInstance.fooValue; + bBarTestResult.save(); + + return bBarTestResult; +} + +// convertBarArrayToBarTestResultArray +export function saveBarsToTestResult( + barArray: Bar[], + testResult: TestResult, + testID: string +): void { + let result: string[] = []; + for (let i = 0; i < barArray.length; i++) { + result.push(convertBarToBarTestResult(barArray[i], testID).id); + } + testResult.barDerived = result; +} + +// convertBBarArrayToBBarTestResultArray +export function saveBBarsToTestResult( + bBarArray: BBar[], + testResult: TestResult, + testID: string +): void { + let result: Bytes[] = []; + for (let i = 0; i < bBarArray.length; i++) { + result.push(convertbBarToBBarTestResult(bBarArray[i], testID).id); + } + testResult.bBarDerived = result; +} + +export function logTestResult(testResult: TestResult): void { + log.info("TestResult with ID: {} has barDerived: {} and bBarDerived: {}", [ + testResult.id, + testResult.barDerived ? testResult.barDerived!.join(", ") : "null", + testResult.bBarDerived + ? testResult + .bBarDerived!.map((b) => b.toHex()) + .join(", ") + : "null", + ]); +} diff --git a/tests/runner-tests/derived-loaders/src/mapping.ts b/tests/runner-tests/derived-loaders/src/mapping.ts new file mode 100644 index 00000000000..063a25d1ca3 --- /dev/null +++ b/tests/runner-tests/derived-loaders/src/mapping.ts @@ -0,0 +1,166 @@ +import { Bytes, store } from "@graphprotocol/graph-ts"; +import { TestEvent } from "../generated/Contract/Contract"; +import { Bar, Foo, BFoo, BBar, TestResult } from "../generated/schema"; +import { + assertBBarsArrayEqual, + assertBBarsEqual, + assertBarsArrayEqual, + assertBarsEqual, + createBBar, + createBar, + saveBBarsToTestResult, + saveBarsToTestResult, + logTestResult, +} from "./helpers"; + +export function handleTestEvent(event: TestEvent): void { + let testResult = new TestResult(event.params.testCommand); + handleTestEventForID(event, testResult); + handleTestEventForBytesAsIDs(event, testResult); + logTestResult(testResult); + testResult.save(); +} + +function handleTestEventForID(event: TestEvent, testResult: TestResult): void { + // This test is to check that the derived entities are loaded correctly + // in the case where the derived entities are created in the same handler call + // ie: updates are coming from `entity_cache.handler_updates` + if (event.params.testCommand == "1_0") { + let foo = new Foo("0"); + foo.value = 0; + foo.save(); + + let bar = createBar("0", "0", 0, 0); + let bar1 = createBar("1", "0", 0, 0); + let bar2 = createBar("2", "0", 0, 0); + + let fooLoaded = Foo.load("0"); + let barDerived = fooLoaded!.bar.load(); + + saveBarsToTestResult(barDerived, testResult, event.params.testCommand); + + // bar0, bar1, bar2 should be loaded + assertBarsArrayEqual(barDerived, [bar, bar1, bar2]); + } + + // This test is to check that the derived entities are loaded correctly + // in the case where the derived entities are created in the same block + // ie: updates are coming from `entity_cache.updates` + if (event.params.testCommand == "1_1") { + let fooLoaded = Foo.load("0"); + + let barLoaded = Bar.load("0"); + barLoaded!.value = 1; + barLoaded!.save(); + + // remove bar1 to test that it is not loaded + // This tests the case where the entity is present in `entity_cache.updates` but is removed by + // An update from `entity_cache.handler_updates` + store.remove("Bar", "1"); + + let barDerivedLoaded = fooLoaded!.bar.load(); + saveBarsToTestResult( + barDerivedLoaded, + testResult, + event.params.testCommand + ); + + // bar1 should not be loaded as it was removed + assert(barDerivedLoaded.length == 2, "barDerivedLoaded.length != 2"); + // bar0 should be loaded with the updated value + assertBarsEqual(barDerivedLoaded[0], barLoaded!); + } + + if (event.params.testCommand == "2_0") { + let fooLoaded = Foo.load("0"); + let barDerived = fooLoaded!.bar.load(); + + // update bar0 + // This tests the case where the entity is present in `store` but is updated by + // An update from `entity_cache.handler_updates` + let barLoaded = Bar.load("0"); + barLoaded!.value = 2; + barLoaded!.save(); + + // remove bar2 to test that it is not loaded + // This tests the case where the entity is present in store but is removed by + // An update from `entity_cache.handler_updates` + store.remove("Bar", "2"); + + let barDerivedLoaded = fooLoaded!.bar.load(); + assert(barDerivedLoaded.length == 1, "barDerivedLoaded.length != 1"); + // bar0 should be loaded with the updated value + assertBarsEqual(barDerivedLoaded[0], barLoaded!); + + saveBarsToTestResult( + barDerivedLoaded, + testResult, + event.params.testCommand + ); + } +} + +// Same as handleTestEventForID but uses Bytes as IDs +function handleTestEventForBytesAsIDs( + event: TestEvent, + testResult: TestResult +): void { + if (event.params.testCommand == "1_0") { + let bFoo = new BFoo(Bytes.fromUTF8("0")); + bFoo.value = 0; + bFoo.save(); + + let bBar = createBBar(Bytes.fromUTF8("0"), Bytes.fromUTF8("0"), 0, 0); + let bBar1 = createBBar(Bytes.fromUTF8("1"), Bytes.fromUTF8("0"), 0, 0); + let bBar2 = createBBar(Bytes.fromUTF8("2"), Bytes.fromUTF8("0"), 0, 0); + + let bFooLoaded = BFoo.load(Bytes.fromUTF8("0")); + let bBarDerived: BBar[] = bFooLoaded!.bar.load(); + + saveBBarsToTestResult(bBarDerived, testResult, event.params.testCommand); + + assertBBarsArrayEqual(bBarDerived, [bBar, bBar1, bBar2]); + } + + if (event.params.testCommand == "1_1") { + let bFooLoaded = BFoo.load(Bytes.fromUTF8("0")); + let bBarDerived = bFooLoaded!.bar.load(); + + let bBarLoaded = BBar.load(Bytes.fromUTF8("0")); + bBarLoaded!.value = 1; + bBarLoaded!.save(); + + store.remove("BBar", Bytes.fromUTF8("1").toHex()); + + let bBarDerivedLoaded = bFooLoaded!.bar.load(); + saveBBarsToTestResult( + bBarDerivedLoaded, + testResult, + event.params.testCommand + ); + + assert(bBarDerivedLoaded.length == 2, "bBarDerivedLoaded.length != 2"); + assertBBarsEqual(bBarDerivedLoaded[0], bBarLoaded!); + } + + if (event.params.testCommand == "2_0") { + let bFooLoaded = BFoo.load(Bytes.fromUTF8("0")); + let bBarDerived = bFooLoaded!.bar.load(); + + let bBarLoaded = BBar.load(Bytes.fromUTF8("0")); + bBarLoaded!.value = 2; + bBarLoaded!.save(); + + store.remove("BBar", Bytes.fromUTF8("2").toHex()); + + let bBarDerivedLoaded = bFooLoaded!.bar.load(); + saveBBarsToTestResult( + bBarDerivedLoaded, + testResult, + event.params.testCommand + ); + + assert(bBarDerivedLoaded.length == 1, "bBarDerivedLoaded.length != 1"); + assertBBarsEqual(bBarDerivedLoaded[0], bBarLoaded!); + } +} \ No newline at end of file diff --git a/tests/integration-tests/derived-loaders/subgraph.yaml b/tests/runner-tests/derived-loaders/subgraph.yaml similarity index 76% rename from tests/integration-tests/derived-loaders/subgraph.yaml rename to tests/runner-tests/derived-loaders/subgraph.yaml index 0ab4801ce44..99d5a09db46 100644 --- a/tests/integration-tests/derived-loaders/subgraph.yaml +++ b/tests/runner-tests/derived-loaders/subgraph.yaml @@ -6,7 +6,7 @@ dataSources: name: Contract network: test source: - address: "0xCfEB869F69431e42cdB54A4F4f105C19C080A601" + address: "0x0000000000000000000000000000000000000000" abi: Contract mapping: kind: ethereum/events @@ -18,6 +18,6 @@ dataSources: entities: - Call eventHandlers: - - event: Trigger(uint16) - handler: handleTrigger + - event: TestEvent(string) + handler: handleTestEvent file: ./src/mapping.ts diff --git a/tests/runner-tests/package.json b/tests/runner-tests/package.json index c35ae39ad3d..ad842148243 100644 --- a/tests/runner-tests/package.json +++ b/tests/runner-tests/package.json @@ -6,6 +6,7 @@ "dynamic-data-source", "fatal-error", "file-data-sources", - "typename" + "typename", + "derived-loaders" ] } diff --git a/tests/runner-tests/yarn.lock b/tests/runner-tests/yarn.lock index 870fe1e8932..fbd1c3003d8 100644 --- a/tests/runner-tests/yarn.lock +++ b/tests/runner-tests/yarn.lock @@ -903,6 +903,38 @@ which "2.0.2" yaml "1.10.2" +"@graphprotocol/graph-cli@0.53.0": + version "0.53.0" + resolved "https://registry.yarnpkg.com/@graphprotocol/graph-cli/-/graph-cli-0.53.0.tgz#7a7a6b6197ec28d6da661f9dc7da8a3198ce2f77" + integrity sha512-K6mvEgaHrNlldbNfAL7DKQhJnp8mSuHkDwBXZm4tbdEbwKneItRObjIOEtgAcfK+Gei1mT6pTO4I+8N7tIg9XA== + dependencies: + "@float-capital/float-subgraph-uncrashable" "^0.0.0-alpha.4" + "@oclif/core" "2.8.6" + "@whatwg-node/fetch" "^0.8.4" + assemblyscript "0.19.23" + binary-install-raw "0.0.13" + chalk "3.0.0" + chokidar "3.5.3" + debug "4.3.4" + docker-compose "0.23.19" + dockerode "2.5.8" + fs-extra "9.1.0" + glob "9.3.5" + gluegun "5.1.2" + graphql "15.5.0" + immutable "4.2.1" + ipfs-http-client "55.0.0" + jayson "4.0.0" + js-yaml "3.14.1" + prettier "1.19.1" + request "2.88.2" + semver "7.4.0" + sync-request "6.1.0" + tmp-promise "3.0.3" + web3-eth-abi "1.7.0" + which "2.0.2" + yaml "1.10.2" + "@graphprotocol/graph-ts@0.30.0": version "0.30.0" resolved "https://registry.yarnpkg.com/@graphprotocol/graph-ts/-/graph-ts-0.30.0.tgz#591dee3c7d9fc236ad57ce0712779e94aef9a50a" @@ -910,6 +942,13 @@ dependencies: assemblyscript "0.19.10" +"@graphprotocol/graph-ts@0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@graphprotocol/graph-ts/-/graph-ts-0.31.0.tgz#730668c0369828b31bef81e8d9bc66b9b48e3480" + integrity sha512-xreRVM6ho2BtolyOh2flDkNoGZximybnzUnF53zJVp0+Ed0KnAlO1/KOCUYw06euVI9tk0c9nA2Z/D5SIQV2Rg== + dependencies: + assemblyscript "0.19.10" + "@graphql-tools/batch-delegate@^6.2.4", "@graphql-tools/batch-delegate@^6.2.6": version "6.2.6" resolved "https://registry.yarnpkg.com/@graphql-tools/batch-delegate/-/batch-delegate-6.2.6.tgz#fbea98dc825f87ef29ea5f3f371912c2a2aa2f2c" @@ -1307,6 +1346,41 @@ wordwrap "^1.0.0" wrap-ansi "^7.0.0" +"@oclif/core@2.8.6": + version "2.8.6" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-2.8.6.tgz#7eb6984108f471ad0d719d3c07cde14c47ab17c5" + integrity sha512-1QlPaHMhOORySCXkQyzjsIsy2GYTilOw3LkjeHkCgsPJQjAT4IclVytJusWktPbYNys9O+O4V23J44yomQvnBQ== + dependencies: + "@types/cli-progress" "^3.11.0" + ansi-escapes "^4.3.2" + ansi-styles "^4.3.0" + cardinal "^2.1.1" + chalk "^4.1.2" + clean-stack "^3.0.1" + cli-progress "^3.12.0" + debug "^4.3.4" + ejs "^3.1.8" + fs-extra "^9.1.0" + get-package-type "^0.1.0" + globby "^11.1.0" + hyperlinker "^1.0.0" + indent-string "^4.0.0" + is-wsl "^2.2.0" + js-yaml "^3.14.1" + natural-orderby "^2.0.3" + object-treeify "^1.1.33" + password-prompt "^1.1.2" + semver "^7.3.7" + string-width "^4.2.3" + strip-ansi "^6.0.1" + supports-color "^8.1.1" + supports-hyperlinks "^2.2.0" + ts-node "^10.9.1" + tslib "^2.5.0" + widest-line "^3.1.0" + wordwrap "^1.0.0" + wrap-ansi "^7.0.0" + "@peculiar/asn1-schema@^2.3.6": version "2.3.6" resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.6.tgz#3dd3c2ade7f702a9a94dfb395c192f5fa5d6b922" diff --git a/tests/tests/integration_tests.rs b/tests/tests/integration_tests.rs index 7d2a00e2cb2..7c4a682e4ae 100644 --- a/tests/tests/integration_tests.rs +++ b/tests/tests/integration_tests.rs @@ -40,7 +40,6 @@ pub const INTEGRATION_TEST_DIRS: &[&str] = &[ "remove-then-update", "value-roundtrip", "int8", - "derived-loaders", ]; #[derive(Debug, Clone)] diff --git a/tests/tests/runner_tests.rs b/tests/tests/runner_tests.rs index 5e61a81e5e4..1a35e9b48a3 100644 --- a/tests/tests/runner_tests.rs +++ b/tests/tests/runner_tests.rs @@ -145,6 +145,165 @@ async fn typename() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn derived_loaders() { + let RunnerTestRecipe { + stores, + subgraph_name, + hash, + } = RunnerTestRecipe::new("derived-loaders").await; + + let blocks = { + let block_0 = genesis(); + let mut block_1 = empty_block(block_0.ptr(), test_ptr(1)); + push_test_log(&mut block_1, "1_0"); + push_test_log(&mut block_1, "1_1"); + let mut block_2 = empty_block(block_1.ptr(), test_ptr(2)); + push_test_log(&mut block_2, "2_0"); + vec![block_0, block_1, block_2] + }; + + let stop_block = blocks.last().unwrap().block.ptr(); + + let chain = chain(blocks, &stores, None).await; + let ctx = fixture::setup(subgraph_name.clone(), &hash, &stores, &chain, None, None).await; + + ctx.start_and_sync_to(stop_block).await; + + // This test tests that derived loaders work correctly. + // The test fixture has 2 entities, `Bar` and `BBar`, which are derived from `Foo` and `BFoo`. + // Where `Foo` and `BFoo` are the same entity, but `BFoo` uses Bytes as the ID type. + // This test tests multiple edge cases of derived loaders: + // - The derived loader is used in the same handler as the entity is created. + // - The derived loader is used in the same block as the entity is created. + // - The derived loader is used in a later block than the entity is created. + // This is to test the cases where the entities are loaded from the store, `EntityCache.updates` and `EntityCache.handler_updates` + // It also tests cases where derived entities are updated and deleted when + // in same handler, same block and later block as the entity is created/updated. + // For more details on the test cases, see `tests/runner-tests/derived-loaders/src/mapping.ts` + // Where the test cases are documented in the code. + + let query_res = ctx + .query(&format!( + r#"{{ testResult(id:"1_0", block: {{ number: 1 }} ){{ id barDerived{{id value value2}} bBarDerived{{id value value2}} }} }}"#, + )) + .await + .unwrap(); + + assert_json_eq!( + query_res, + Some(object! { + testResult: object! { + id: "1_0", + barDerived: vec![ + object! { + id: "0_1_0", + value: "0", + value2: "0" + }, + object! { + id: "1_1_0", + value: "0", + value2: "0" + }, + object! { + id: "2_1_0", + value: "0", + value2: "0" + } + ], + bBarDerived: vec![ + object! { + id: "0x305f315f30", + value: "0", + value2: "0" + }, + object! { + id: "0x315f315f30", + value: "0", + value2: "0" + }, + object! { + id: "0x325f315f30", + value: "0", + value2: "0" + } + ] + } + }) + ); + + let query_res = ctx + .query(&format!( + r#"{{ testResult(id:"1_1", block: {{ number: 1 }} ){{ id barDerived{{id value value2}} bBarDerived{{id value value2}} }} }}"#, + )) + .await + .unwrap(); + + assert_json_eq!( + query_res, + Some(object! { + testResult: object! { + id: "1_1", + barDerived: vec![ + object! { + id: "0_1_1", + value: "1", + value2: "0" + }, + object! { + id: "2_1_1", + value: "0", + value2: "0" + } + ], + bBarDerived: vec![ + object! { + id: "0x305f315f31", + value: "1", + value2: "0" + }, + object! { + id: "0x325f315f31", + value: "0", + value2: "0" + } + ] + } + }) + ); + + let query_res = ctx.query( + &format!( + r#"{{ testResult(id:"2_0" ){{ id barDerived{{id value value2}} bBarDerived{{id value value2}} }} }}"# + ) +) +.await +.unwrap(); + assert_json_eq!( + query_res, + Some(object! { + testResult: object! { + id: "2_0", + barDerived: vec![ + object! { + id: "0_2_0", + value: "2", + value2: "0" + } + ], + bBarDerived: vec![ + object! { + id: "0x305f325f30", + value: "2", + value2: "0" + } + ] + } + }) + ); +} + #[tokio::test] async fn file_data_sources() { let RunnerTestRecipe {