From 76405e117fde300bb7a173ba5f9b795668d39e34 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Tue, 8 Aug 2023 18:33:17 +0530 Subject: [PATCH 01/13] tests : Converted derived-loaders from integration-tests to a runner-test --- .../derived-loaders/abis/Contract.abi | 33 ----- .../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 | 10 +- .../derived-loaders/src/mapping.ts | 57 +++++++++ .../derived-loaders/subgraph.yaml | 6 +- tests/tests/integration_tests.rs | 1 - tests/tests/runner_tests.rs | 91 +++++++++++++ 11 files changed, 175 insertions(+), 216 deletions(-) delete mode 100644 tests/integration-tests/derived-loaders/abis/Contract.abi 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%) rename tests/{integration-tests => runner-tests}/derived-loaders/schema.graphql (54%) 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/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/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/integration-tests/derived-loaders/schema.graphql b/tests/runner-tests/derived-loaders/schema.graphql similarity index 54% rename from tests/integration-tests/derived-loaders/schema.graphql rename to tests/runner-tests/derived-loaders/schema.graphql index 57886a920dd..6bd6e7edb72 100644 --- a/tests/integration-tests/derived-loaders/schema.graphql +++ b/tests/runner-tests/derived-loaders/schema.graphql @@ -1,7 +1,7 @@ type BFoo @entity { id: Bytes! value: Int8! - bar: BBar! @derivedFrom(field: "fooValue") + bar: BBar @derivedFrom(field: "fooValue") } type BBar @entity { @@ -12,10 +12,16 @@ type BBar @entity { type Foo @entity { id: ID! value: Int8! - bar: Bar! @derivedFrom(field: "fooValue") + bar: Bar @derivedFrom(field: "fooValue") } type Bar @entity { id: ID! fooValue: Foo! } + +type TestResult @entity { + id: ID! + barDerived: String + bBarDerived: Bytes +} \ No newline at end of file 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..e85faf5aa57 --- /dev/null +++ b/tests/runner-tests/derived-loaders/src/mapping.ts @@ -0,0 +1,57 @@ +import { Bytes, BigInt, log, store } from "@graphprotocol/graph-ts"; +import { TestEvent } from "../generated/Contract/Contract"; +import { Bar, Foo, BFoo, BBar, TestResult } from "../generated/schema"; + +export function handleTestEvent(event: TestEvent): void { + if (event.params.testCommand == "0") { + let foo = new Foo("0"); + foo.value = 1; + foo.save(); + + let bar = new Bar("0"); + bar.fooValue = "0"; + bar.save(); + + let bFoo = new BFoo(Bytes.fromUTF8("0")); + bFoo.value = 1; + bFoo.save(); + + let bBar = new BBar(Bytes.fromUTF8("0")); + bBar.fooValue = Bytes.fromUTF8("0"); + bBar.save(); + + let fooLoaded = Foo.load("0"); + let barDerived = fooLoaded!.bar.load(); + let bFooLoaded = BFoo.load(Bytes.fromUTF8("0")); + let bBarDerived: BBar[] = bFooLoaded!.bar.load(); + + let testResult = new TestResult("0"); + if (barDerived.length > 0) { + testResult.barDerived = barDerived[0].id; + } + + if (bBarDerived.length > 0) { + testResult.bBarDerived = bBarDerived[0].id; + } + + testResult.save(); + } + + if (event.params.testCommand == "1") { + let fooLoaded = Foo.load("0"); + let barDerived = fooLoaded!.bar.load(); + let bFooLoaded = BFoo.load(Bytes.fromUTF8("0")); + let bBarDerived = bFooLoaded!.bar.load(); + + let testResult1 = new TestResult("1"); + + if (barDerived.length > 0) { + testResult1.barDerived = barDerived[0].id; + } + + if (bBarDerived.length > 0) { + testResult1.bBarDerived = bBarDerived[0].id; + } + testResult1.save(); + } +} \ 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/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..b87b9b69d8c 100644 --- a/tests/tests/runner_tests.rs +++ b/tests/tests/runner_tests.rs @@ -145,6 +145,97 @@ 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, "0"); + let mut block_2 = empty_block(block_1.ptr(), test_ptr(2)); + push_test_log(&mut block_2, "1"); + 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(test_ptr(1)).await; + + // BUG: There is a bug that prevents the derived fields from being loaded correctly + // when the entities are loaded from the cache + // So the following test wont work + // let query_res = ctx + // .query(&format!( + // r#"{{ testResult(id: "0") {{ id barDerived bBarDerived }} }}"#, + // )) + // .await + // .unwrap(); + // assert_json_eq!( + // query_res, + // Some(object! { testResult: object!{ + // id: "0", + // barDerived: "1", + // bBarDerived: "1", + // }}) + // ); + ctx.start_and_sync_to(stop_block).await; + + let query_res = ctx + .query(&format!( + r#"{{ testResult(id: "1") {{ id barDerived bBarDerived }} }}"#, + )) + .await + .unwrap(); + assert_json_eq!( + query_res, + Some(object! { testResult: object!{ + id: "1", + barDerived: "0", + bBarDerived: "0x30", + }}) + ); + + let query_res = ctx + .query(&format!( + r#"{{ bfoos(orderBy: id) {{ id value bar {{ id }} }} }}"#, + )) + .await + .unwrap(); + + assert_json_eq!( + query_res, + Some(object! { bfoos: vec![object!{ + id: "0x30", + value: "1", + bar: object!{ id: "0x30" } + }] }) + ); + + let query_res = ctx + .query(&format!( + r#"{{ foos(orderBy: id) {{ id value bar {{ id }} }} }}"#, + )) + .await + .unwrap(); + + assert_json_eq!( + query_res, + Some(object! { foos: vec![object!{ + id: "0", + value: "1", + bar: object!{ id: "0" } + }] }) + ); +} + #[tokio::test] async fn file_data_sources() { let RunnerTestRecipe { From a64add5d3d787b0cbb6f56594e7fe38727836f1c Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Wed, 9 Aug 2023 15:14:56 +0530 Subject: [PATCH 02/13] graph: Check entity_cache before loading derived entites from store --- graph/src/components/store/entity_cache.rs | 28 +++++++++++++++++++--- graph/src/util/lfu_cache.rs | 10 ++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/graph/src/components/store/entity_cache.rs b/graph/src/components/store/entity_cache.rs index 77e01f04371..6503b96c5f7 100644 --- a/graph/src/components/store/entity_cache.rs +++ b/graph/src/components/store/entity_cache.rs @@ -209,11 +209,33 @@ impl EntityCache { id_is_bytes: id_is_bytes, }; + // Get the entities from the store and insert it into the cache if it's not already there let entities = self.store.get_derived(&query)?; - entities.iter().for_each(|(key, e)| { - self.current.insert(key.clone(), Some(e.clone())); + + entities.iter().for_each(|(k, v)| { + // Only insert to the cache if it's not already there + if !self.current.contains_key(k) { + self.current.insert(k.clone(), Some(v.clone())); + } }); - let entities: Vec = entities.values().cloned().collect(); + + // Get the derived entities from the cache + let entities = self + .current + .filter(|key, entity: &Option| { + key.entity_type == query.entity_type && // Check if entity_type matches + entity.as_ref().map_or(false, |e| { + // Check if value in the field indicated by entity_field matches the value from + // the query + e.get(&query.entity_field.to_camel_case()) + .map(|v| v.to_string()) + .map(|word| word == query.value.to_string()) + .unwrap_or(false) + }) + }) + .filter_map(|entity| entity.clone()) + .collect(); + Ok(entities) } diff --git a/graph/src/util/lfu_cache.rs b/graph/src/util/lfu_cache.rs index 915e793ec59..405d71dd0a7 100644 --- a/graph/src/util/lfu_cache.rs +++ b/graph/src/util/lfu_cache.rs @@ -177,6 +177,16 @@ impl }) } + pub fn filter<'a, F>(&'a self, predicate: F) -> impl Iterator + where + F: Fn(&K, &V) -> bool, + { + self.queue + .iter() + .filter(move |entry| predicate(&entry.0.key, &entry.0.value)) + .map(|entry| &entry.0.value) + } + pub fn get(&mut self, key: &K) -> Option<&V> { self.get_mut(key.clone()).map(|x| &x.value) } From 7e2e7f65a400b02be949142e662be64a01d5d262 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Wed, 9 Aug 2023 16:04:17 +0530 Subject: [PATCH 03/13] tests: Add more tests for derived loaders --- .../derived-loaders/src/mapping.ts | 23 +++++++- tests/tests/runner_tests.rs | 55 ++++++++++++------- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/tests/runner-tests/derived-loaders/src/mapping.ts b/tests/runner-tests/derived-loaders/src/mapping.ts index e85faf5aa57..741b58f27cc 100644 --- a/tests/runner-tests/derived-loaders/src/mapping.ts +++ b/tests/runner-tests/derived-loaders/src/mapping.ts @@ -20,9 +20,10 @@ export function handleTestEvent(event: TestEvent): void { bBar.fooValue = Bytes.fromUTF8("0"); bBar.save(); + // Test loading from entities created in the same handler let fooLoaded = Foo.load("0"); - let barDerived = fooLoaded!.bar.load(); let bFooLoaded = BFoo.load(Bytes.fromUTF8("0")); + let barDerived = fooLoaded!.bar.load(); let bBarDerived: BBar[] = bFooLoaded!.bar.load(); let testResult = new TestResult("0"); @@ -37,6 +38,7 @@ export function handleTestEvent(event: TestEvent): void { testResult.save(); } + // Test loading from entities created in the same block if (event.params.testCommand == "1") { let fooLoaded = Foo.load("0"); let barDerived = fooLoaded!.bar.load(); @@ -54,4 +56,23 @@ export function handleTestEvent(event: TestEvent): void { } testResult1.save(); } + + // Testing loading from entities created in the different blocks + if (event.params.testCommand == "2") { + let fooLoaded = Foo.load("0"); + let barDerived = fooLoaded!.bar.load(); + let bFooLoaded = BFoo.load(Bytes.fromUTF8("0")); + let bBarDerived = bFooLoaded!.bar.load(); + + let testResult1 = new TestResult("2"); + + if (barDerived.length > 0) { + testResult1.barDerived = barDerived[0].id; + } + + if (bBarDerived.length > 0) { + testResult1.bBarDerived = bBarDerived[0].id; + } + testResult1.save(); + } } \ No newline at end of file diff --git a/tests/tests/runner_tests.rs b/tests/tests/runner_tests.rs index b87b9b69d8c..7b1d3ca813e 100644 --- a/tests/tests/runner_tests.rs +++ b/tests/tests/runner_tests.rs @@ -157,8 +157,9 @@ async fn derived_loaders() { let block_0 = genesis(); let mut block_1 = empty_block(block_0.ptr(), test_ptr(1)); push_test_log(&mut block_1, "0"); + push_test_log(&mut block_1, "1"); let mut block_2 = empty_block(block_1.ptr(), test_ptr(2)); - push_test_log(&mut block_2, "1"); + push_test_log(&mut block_2, "2"); vec![block_0, block_1, block_2] }; @@ -167,27 +168,25 @@ async fn derived_loaders() { 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(test_ptr(1)).await; - - // BUG: There is a bug that prevents the derived fields from being loaded correctly - // when the entities are loaded from the cache - // So the following test wont work - // let query_res = ctx - // .query(&format!( - // r#"{{ testResult(id: "0") {{ id barDerived bBarDerived }} }}"#, - // )) - // .await - // .unwrap(); - // assert_json_eq!( - // query_res, - // Some(object! { testResult: object!{ - // id: "0", - // barDerived: "1", - // bBarDerived: "1", - // }}) - // ); ctx.start_and_sync_to(stop_block).await; + // Test loadRelated in the same handler + let query_res = ctx + .query(&format!( + r#"{{ testResult(id: "0") {{ id barDerived bBarDerived }} }}"#, + )) + .await + .unwrap(); + assert_json_eq!( + query_res, + Some(object! { testResult: object!{ + id: "0", + barDerived: "1", + bBarDerived: "1", + }}) + ); + + // Test loadRelated in same block let query_res = ctx .query(&format!( r#"{{ testResult(id: "1") {{ id barDerived bBarDerived }} }}"#, @@ -203,6 +202,22 @@ async fn derived_loaders() { }}) ); + // Test loadRelated in a different block + let query_res = ctx + .query(&format!( + r#"{{ testResult(id: "2") {{ id barDerived bBarDerived }} }}"#, + )) + .await + .unwrap(); + assert_json_eq!( + query_res, + Some(object! { testResult: object!{ + id: "2", + barDerived: "0", + bBarDerived: "0x30", + }}) + ); + let query_res = ctx .query(&format!( r#"{{ bfoos(orderBy: id) {{ id value bar {{ id }} }} }}"#, From d97ca7c6e326f3f4eb032794fd81335006641c17 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Wed, 9 Aug 2023 18:17:18 +0530 Subject: [PATCH 04/13] graph, tests: consider `updates` and `handler_updates` while loading derived_entities --- graph/src/components/store/entity_cache.rs | 54 ++++++++++++++++------ graph/src/util/lfu_cache.rs | 8 +--- tests/tests/runner_tests.rs | 4 +- 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/graph/src/components/store/entity_cache.rs b/graph/src/components/store/entity_cache.rs index 6503b96c5f7..8c76dd0ccba 100644 --- a/graph/src/components/store/entity_cache.rs +++ b/graph/src/components/store/entity_cache.rs @@ -219,21 +219,47 @@ impl EntityCache { } }); - // Get the derived entities from the cache - let entities = self - .current - .filter(|key, entity: &Option| { - key.entity_type == query.entity_type && // Check if entity_type matches - entity.as_ref().map_or(false, |e| { - // Check if value in the field indicated by entity_field matches the value from - // the query - e.get(&query.entity_field.to_camel_case()) - .map(|v| v.to_string()) - .map(|word| word == query.value.to_string()) - .unwrap_or(false) - }) + // Insert `None` for entities that are not in the store and not in the current cache + // but are present in updates or handler_updates + // The updates will be applied before returning the entities + self.updates.iter().for_each(|(k, _)| { + if !self.current.contains_key(k) { + self.current.insert(k.clone(), None); + } + }); + + self.handler_updates.iter().for_each(|(k, _)| { + if !self.current.contains_key(k) { + self.current.insert(k.clone(), None); + } + }); + + // Retrieve the derived entities from the cache by filtering the cached entities. + // Include entities if either a matching value is found in the specified entity field + // as indicated by `query`, or if the entity is None. + let entities = self.current.iter().filter(|(key, entity)| { + key.entity_type == query.entity_type + && entity.as_ref().map_or(true, |e| { + e.get(&query.entity_field.to_camel_case()) + .map(|v| v.to_string() == query.value.to_string()) + .unwrap_or(false) + }) + }); + + let entities = entities + .filter_map(|(key, entity)| { + let mut entity: Option> = entity.to_owned().map(Cow::Owned); + + if let Some(op) = self.updates.get(&key).cloned() { + op.apply_to(&mut entity).ok()?; + } + + if let Some(op) = self.handler_updates.get(&key).cloned() { + op.apply_to(&mut entity).ok()?; + } + + entity.map(|e| e.into_owned()) }) - .filter_map(|entity| entity.clone()) .collect(); Ok(entities) diff --git a/graph/src/util/lfu_cache.rs b/graph/src/util/lfu_cache.rs index 405d71dd0a7..f99d24fc744 100644 --- a/graph/src/util/lfu_cache.rs +++ b/graph/src/util/lfu_cache.rs @@ -177,14 +177,10 @@ impl }) } - pub fn filter<'a, F>(&'a self, predicate: F) -> impl Iterator - where - F: Fn(&K, &V) -> bool, - { + pub fn iter<'a>(&'a self) -> impl Iterator { self.queue .iter() - .filter(move |entry| predicate(&entry.0.key, &entry.0.value)) - .map(|entry| &entry.0.value) + .map(|entry| (&entry.0.key, &entry.0.value)) } pub fn get(&mut self, key: &K) -> Option<&V> { diff --git a/tests/tests/runner_tests.rs b/tests/tests/runner_tests.rs index 7b1d3ca813e..636baf06a21 100644 --- a/tests/tests/runner_tests.rs +++ b/tests/tests/runner_tests.rs @@ -181,8 +181,8 @@ async fn derived_loaders() { query_res, Some(object! { testResult: object!{ id: "0", - barDerived: "1", - bBarDerived: "1", + barDerived: "0", + bBarDerived: "0x30", }}) ); From 98b4a24440dbc252dc0cbe707218c7acfaa33c36 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Thu, 10 Aug 2023 15:34:36 +0530 Subject: [PATCH 05/13] graph : merge `updates` and `handler_updates` for load_related queries --- graph/src/components/store/entity_cache.rs | 90 +++++++++++----------- graph/src/components/store/mod.rs | 17 ++++ store/postgres/src/relational_queries.rs | 3 +- 3 files changed, 62 insertions(+), 48 deletions(-) diff --git a/graph/src/components/store/entity_cache.rs b/graph/src/components/store/entity_cache.rs index 8c76dd0ccba..4028096b8ed 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,65 +202,62 @@ 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, }; - // Get the entities from the store and insert it into the cache if it's not already there - let entities = self.store.get_derived(&query)?; - - entities.iter().for_each(|(k, v)| { - // Only insert to the cache if it's not already there - if !self.current.contains_key(k) { - self.current.insert(k.clone(), Some(v.clone())); + // Merge updates and handler_updates + let mut merged_updates = self.updates.clone(); + for (key, op) in &self.handler_updates { + if let Some(existing_op) = merged_updates.get_mut(key) { + existing_op.accumulate(op.clone()); + } else { + merged_updates.insert(key.clone(), op.clone()); } - }); - - // Insert `None` for entities that are not in the store and not in the current cache - // but are present in updates or handler_updates - // The updates will be applied before returning the entities - self.updates.iter().for_each(|(k, _)| { - if !self.current.contains_key(k) { - self.current.insert(k.clone(), None); - } - }); + } - self.handler_updates.iter().for_each(|(k, _)| { - if !self.current.contains_key(k) { - self.current.insert(k.clone(), None); + // Update the cache with entities from the store that are not in the cache + for (key, entity) in self.store.get_derived(&query)? { + if !self.current.contains_key(&key) { + self.current.insert(key.clone(), Some(entity)); } - }); - - // Retrieve the derived entities from the cache by filtering the cached entities. - // Include entities if either a matching value is found in the specified entity field - // as indicated by `query`, or if the entity is None. - let entities = self.current.iter().filter(|(key, entity)| { - key.entity_type == query.entity_type - && entity.as_ref().map_or(true, |e| { - e.get(&query.entity_field.to_camel_case()) - .map(|v| v.to_string() == query.value.to_string()) - .unwrap_or(false) - }) - }); - - let entities = entities - .filter_map(|(key, entity)| { - let mut entity: Option> = entity.to_owned().map(Cow::Owned); + } - if let Some(op) = self.updates.get(&key).cloned() { - op.apply_to(&mut entity).ok()?; + let mut entity_map = HashMap::new(); + + // Check inside self.current for entities that match the query + for (key, opt_entity) in self.current.iter() { + if let Some(entity) = opt_entity { + if query.matches(key, entity) { + if let Some(op) = merged_updates.get(key).cloned() { + let mut entity_cow = Some(Cow::Borrowed(entity)); + op.apply_to(&mut entity_cow) + .map_err(|e| key.unknown_attribute(e))?; + entity_map.insert(key.clone(), entity_cow.unwrap().into_owned()); + } else { + entity_map.insert(key.clone(), entity.clone()); + } } + } + } - if let Some(op) = self.handler_updates.get(&key).cloned() { - op.apply_to(&mut entity).ok()?; + // Now, check for entities in merged_updates not present in the entity_map and that match the query + for (key, op) in &merged_updates { + if !entity_map.contains_key(key) { + match op { + EntityOp::Update(entity) | EntityOp::Overwrite(entity) => { + if query.matches(key, entity) { + entity_map.insert(key.clone(), entity.clone()); + } + } + EntityOp::Remove => {} } + } + } - entity.map(|e| e.into_owned()) - }) - .collect(); - + let entities = entity_map.into_values().collect(); Ok(entities) } 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/store/postgres/src/relational_queries.rs b/store/postgres/src/relational_queries.rs index 6a60a3ccc4a..2706661c46d 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; @@ -1764,7 +1765,7 @@ impl<'a> QueryFragment for FindDerivedQuery<'a> { } 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("); From 9c41c388dc3439d62499d321cbfc101aed28fbd2 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Thu, 10 Aug 2023 18:39:51 +0530 Subject: [PATCH 06/13] graph: Avoid cloning `self.updates` in `loadRelated` --- graph/src/components/store/entity_cache.rs | 93 +++++++++++++++------- 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/graph/src/components/store/entity_cache.rs b/graph/src/components/store/entity_cache.rs index 4028096b8ed..c8b89c974e3 100644 --- a/graph/src/components/store/entity_cache.rs +++ b/graph/src/components/store/entity_cache.rs @@ -1,6 +1,6 @@ use anyhow::anyhow; use std::borrow::Cow; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt::{self, Debug}; use std::sync::Arc; @@ -208,57 +208,92 @@ impl EntityCache { id_is_bytes: id_is_bytes, }; - // Merge updates and handler_updates - let mut merged_updates = self.updates.clone(); - for (key, op) in &self.handler_updates { - if let Some(existing_op) = merged_updates.get_mut(key) { - existing_op.accumulate(op.clone()); - } else { - merged_updates.insert(key.clone(), op.clone()); - } - } - - // Update the cache with entities from the store that are not in the cache for (key, entity) in self.store.get_derived(&query)? { + // Only insert to the cache if it's not already there + // This is to avoid overwriting updates if !self.current.contains_key(&key) { self.current.insert(key.clone(), Some(entity)); } } - let mut entity_map = HashMap::new(); - - // Check inside self.current for entities that match the query + let mut entity_map = BTreeMap::new(); + // Check inside self.current for entities that match the `DerivedEntityQuery` + // and apply any updates from `updates` and `handler_updates`. for (key, opt_entity) in self.current.iter() { if let Some(entity) = opt_entity { if query.matches(key, entity) { - if let Some(op) = merged_updates.get(key).cloned() { - let mut entity_cow = Some(Cow::Borrowed(entity)); + let mut entity_cow = Some(Cow::Borrowed(entity)); + + if let Some(op) = self.updates.get_mut(key).cloned() { op.apply_to(&mut entity_cow) .map_err(|e| key.unknown_attribute(e))?; - entity_map.insert(key.clone(), entity_cow.unwrap().into_owned()); - } else { - entity_map.insert(key.clone(), entity.clone()); + } + + 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(entity) = entity_cow { + entity_map.insert(key.clone(), entity.into_owned()); + } + } + } + } + + // A helper function to apply an update and return the resulting entity if it matches the query + fn apply_update( + op: &EntityOp, + query: &DerivedEntityQuery, + key: &EntityKey, + ) -> Result, anyhow::Error> { + match op { + EntityOp::Update(entity) | EntityOp::Overwrite(entity) => { + if query.matches(key, entity) { + return Ok(Some(entity.clone())); } } + EntityOp::Remove => {} } + Ok(None) } - // Now, check for entities in merged_updates not present in the entity_map and that match the query - for (key, op) in &merged_updates { + // 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) { - match op { - EntityOp::Update(entity) | EntityOp::Overwrite(entity) => { - if query.matches(key, entity) { - entity_map.insert(key.clone(), entity.clone()); + if let Some(entity) = apply_update(op, &query, key)? { + if let Some(handler_op) = self.handler_updates.get(key) { + // If there's an update from handler_updates, apply it + if let Some(updated_entity) = apply_update(handler_op, &query, key)? { + entity_map.insert(key.clone(), updated_entity); + continue; // Move to the next iteration since the entity is already updated and inserted } } - EntityOp::Remove => {} + // 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) = apply_update(handler_op, &query, key)? { + entity_map.insert(key.clone(), entity); } } } - let entities = entity_map.into_values().collect(); - Ok(entities) + Ok(entity_map.into_values().collect()) } pub fn remove(&mut self, key: EntityKey) { From 6c45058eba14bded6cfbbbcd9bcd59bdc59bf13f Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Thu, 10 Aug 2023 22:22:44 +0530 Subject: [PATCH 07/13] graph: remove iteration over `self.current` in load_related --- graph/src/components/store/entity_cache.rs | 42 ++++++++++------------ 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/graph/src/components/store/entity_cache.rs b/graph/src/components/store/entity_cache.rs index c8b89c974e3..57e7605f29b 100644 --- a/graph/src/components/store/entity_cache.rs +++ b/graph/src/components/store/entity_cache.rs @@ -1,6 +1,6 @@ use anyhow::anyhow; use std::borrow::Cow; -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; use std::fmt::{self, Debug}; use std::sync::Arc; @@ -208,36 +208,32 @@ impl EntityCache { id_is_bytes: id_is_bytes, }; - for (key, entity) in self.store.get_derived(&query)? { + 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 // This is to avoid overwriting updates if !self.current.contains_key(&key) { - self.current.insert(key.clone(), Some(entity)); + self.current.insert(key.clone(), Some(entity.clone())); } } - let mut entity_map = BTreeMap::new(); - // Check inside self.current for entities that match the `DerivedEntityQuery` - // and apply any updates from `updates` and `handler_updates`. - for (key, opt_entity) in self.current.iter() { - if let Some(entity) = opt_entity { - if query.matches(key, entity) { - let mut entity_cow = Some(Cow::Borrowed(entity)); - - if let Some(op) = self.updates.get_mut(key).cloned() { - op.apply_to(&mut entity_cow) - .map_err(|e| key.unknown_attribute(e))?; - } + // 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.handler_updates.get(key).cloned() { - op.apply_to(&mut entity_cow) - .map_err(|e| key.unknown_attribute(e))?; - } + 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(entity) = entity_cow { - entity_map.insert(key.clone(), entity.into_owned()); - } - } + 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(); } } From 5cd68f63a1653b837f0ee0e0cead236c37eefac2 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Fri, 11 Aug 2023 00:41:27 +0530 Subject: [PATCH 08/13] store: Make `get_derived` work with `Bytes` as ID --- store/postgres/src/relational_queries.rs | 11 ++++++++++- store/postgres/src/writable.rs | 10 +++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/store/postgres/src/relational_queries.rs b/store/postgres/src/relational_queries.rs index 2706661c46d..1de09778538 100644 --- a/store/postgres/src/relational_queries.rs +++ b/store/postgres/src/relational_queries.rs @@ -1761,7 +1761,16 @@ 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 "); } 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) } From 626890b34a588da5aebd9998e5ced98feaf4fd8b Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Fri, 11 Aug 2023 00:48:17 +0530 Subject: [PATCH 09/13] graph: refactor `apply_update` in `load_related` --- graph/src/components/store/entity_cache.rs | 50 ++++++++++++++-------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/graph/src/components/store/entity_cache.rs b/graph/src/components/store/entity_cache.rs index 57e7605f29b..8410accc21f 100644 --- a/graph/src/components/store/entity_cache.rs +++ b/graph/src/components/store/entity_cache.rs @@ -218,6 +218,8 @@ impl EntityCache { } } + 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)); @@ -234,24 +236,33 @@ impl EntityCache { 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 to apply an update and return the resulting entity if it matches the query - fn apply_update( + // Remove the marked keys from the map + for key in keys_to_remove { + entity_map.remove(&key); + } + + // 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) { - return Ok(Some(entity.clone())); - } + EntityOp::Update(entity) | EntityOp::Overwrite(entity) + if query.matches(key, entity) => + { + Ok(Some(entity.clone())) } - EntityOp::Remove => {} + EntityOp::Remove => Ok(None), + _ => Ok(None), } - Ok(None) } // Iterate over self.updates to find entities that: @@ -262,16 +273,19 @@ impl EntityCache { // - Add the entity to entity_map. for (key, op) in self.updates.iter() { if !entity_map.contains_key(key) { - if let Some(entity) = apply_update(op, &query, key)? { - if let Some(handler_op) = self.handler_updates.get(key) { - // If there's an update from handler_updates, apply it - if let Some(updated_entity) = apply_update(handler_op, &query, key)? { - entity_map.insert(key.clone(), updated_entity); - continue; // Move to the next iteration since the entity is already updated and inserted - } + 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))?; + entity_map.insert(key.clone(), entity_cow.unwrap().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); } - // 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); } } } @@ -283,7 +297,7 @@ impl EntityCache { // 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) = apply_update(handler_op, &query, key)? { + if let Some(entity) = matches_query(handler_op, &query, key)? { entity_map.insert(key.clone(), entity); } } From c7bce0d6eb8f946b235f318477fd74c1695605d5 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Fri, 11 Aug 2023 17:54:06 +0530 Subject: [PATCH 10/13] graph: handle removed entties in `handler_udpates` during `loadRelated` --- graph/src/components/store/entity_cache.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/graph/src/components/store/entity_cache.rs b/graph/src/components/store/entity_cache.rs index 8410accc21f..c76da4ef1e2 100644 --- a/graph/src/components/store/entity_cache.rs +++ b/graph/src/components/store/entity_cache.rs @@ -243,11 +243,6 @@ impl EntityCache { } } - // Remove the marked keys from the map - for key in keys_to_remove { - entity_map.remove(&key); - } - // A helper function that checks if an update matches the query and returns the updated entity if it does fn matches_query( op: &EntityOp, @@ -281,7 +276,10 @@ impl EntityCache { handler_op .apply_to(&mut entity_cow) .map_err(|e| key.unknown_attribute(e))?; - entity_map.insert(key.clone(), entity_cow.unwrap().into_owned()); + + 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); @@ -303,6 +301,11 @@ impl EntityCache { } } + // Remove keys that were marked for removal + for key in keys_to_remove { + entity_map.remove(&key); + } + Ok(entity_map.into_values().collect()) } From 2701053d9f8fc912bbb90fa99cfc84e7a9ba4569 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Fri, 11 Aug 2023 23:16:19 +0530 Subject: [PATCH 11/13] tests: Add comprehensive tests for `derived-loaders` --- .../derived-loaders/schema.graphql | 28 ++- .../derived-loaders/src/helpers.ts | 160 +++++++++++++++ .../derived-loaders/src/mapping.ts | 180 ++++++++++++----- tests/tests/runner_tests.rs | 185 +++++++++++------- 4 files changed, 436 insertions(+), 117 deletions(-) create mode 100644 tests/runner-tests/derived-loaders/src/helpers.ts diff --git a/tests/runner-tests/derived-loaders/schema.graphql b/tests/runner-tests/derived-loaders/schema.graphql index 6bd6e7edb72..ade664b2f00 100644 --- a/tests/runner-tests/derived-loaders/schema.graphql +++ b/tests/runner-tests/derived-loaders/schema.graphql @@ -1,27 +1,45 @@ type BFoo @entity { id: Bytes! value: Int8! - bar: BBar @derivedFrom(field: "fooValue") + 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") + 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: String - bBarDerived: Bytes -} \ No newline at end of file + 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 index 741b58f27cc..063a25d1ca3 100644 --- a/tests/runner-tests/derived-loaders/src/mapping.ts +++ b/tests/runner-tests/derived-loaders/src/mapping.ts @@ -1,78 +1,166 @@ -import { Bytes, BigInt, log, store } from "@graphprotocol/graph-ts"; +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 { - if (event.params.testCommand == "0") { + 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 = 1; + foo.value = 0; foo.save(); - let bar = new Bar("0"); - bar.fooValue = "0"; - bar.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 = 1; + bFoo.value = 0; bFoo.save(); - let bBar = new BBar(Bytes.fromUTF8("0")); - bBar.fooValue = Bytes.fromUTF8("0"); - bBar.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); - // Test loading from entities created in the same handler - let fooLoaded = Foo.load("0"); let bFooLoaded = BFoo.load(Bytes.fromUTF8("0")); - let barDerived = fooLoaded!.bar.load(); let bBarDerived: BBar[] = bFooLoaded!.bar.load(); - let testResult = new TestResult("0"); - if (barDerived.length > 0) { - testResult.barDerived = barDerived[0].id; - } - - if (bBarDerived.length > 0) { - testResult.bBarDerived = bBarDerived[0].id; - } + saveBBarsToTestResult(bBarDerived, testResult, event.params.testCommand); - testResult.save(); + assertBBarsArrayEqual(bBarDerived, [bBar, bBar1, bBar2]); } - // Test loading from entities created in the same block - if (event.params.testCommand == "1") { - let fooLoaded = Foo.load("0"); - let barDerived = fooLoaded!.bar.load(); + if (event.params.testCommand == "1_1") { let bFooLoaded = BFoo.load(Bytes.fromUTF8("0")); let bBarDerived = bFooLoaded!.bar.load(); - let testResult1 = new TestResult("1"); + let bBarLoaded = BBar.load(Bytes.fromUTF8("0")); + bBarLoaded!.value = 1; + bBarLoaded!.save(); + + store.remove("BBar", Bytes.fromUTF8("1").toHex()); - if (barDerived.length > 0) { - testResult1.barDerived = barDerived[0].id; - } + let bBarDerivedLoaded = bFooLoaded!.bar.load(); + saveBBarsToTestResult( + bBarDerivedLoaded, + testResult, + event.params.testCommand + ); - if (bBarDerived.length > 0) { - testResult1.bBarDerived = bBarDerived[0].id; - } - testResult1.save(); + assert(bBarDerivedLoaded.length == 2, "bBarDerivedLoaded.length != 2"); + assertBBarsEqual(bBarDerivedLoaded[0], bBarLoaded!); } - // Testing loading from entities created in the different blocks - if (event.params.testCommand == "2") { - let fooLoaded = Foo.load("0"); - let barDerived = fooLoaded!.bar.load(); + if (event.params.testCommand == "2_0") { let bFooLoaded = BFoo.load(Bytes.fromUTF8("0")); let bBarDerived = bFooLoaded!.bar.load(); - let testResult1 = new TestResult("2"); + let bBarLoaded = BBar.load(Bytes.fromUTF8("0")); + bBarLoaded!.value = 2; + bBarLoaded!.save(); + + store.remove("BBar", Bytes.fromUTF8("2").toHex()); - if (barDerived.length > 0) { - testResult1.barDerived = barDerived[0].id; - } + let bBarDerivedLoaded = bFooLoaded!.bar.load(); + saveBBarsToTestResult( + bBarDerivedLoaded, + testResult, + event.params.testCommand + ); - if (bBarDerived.length > 0) { - testResult1.bBarDerived = bBarDerived[0].id; - } - testResult1.save(); + assert(bBarDerivedLoaded.length == 1, "bBarDerivedLoaded.length != 1"); + assertBBarsEqual(bBarDerivedLoaded[0], bBarLoaded!); } } \ No newline at end of file diff --git a/tests/tests/runner_tests.rs b/tests/tests/runner_tests.rs index 636baf06a21..1a35e9b48a3 100644 --- a/tests/tests/runner_tests.rs +++ b/tests/tests/runner_tests.rs @@ -156,10 +156,10 @@ async fn derived_loaders() { let blocks = { let block_0 = genesis(); let mut block_1 = empty_block(block_0.ptr(), test_ptr(1)); - push_test_log(&mut block_1, "0"); - push_test_log(&mut block_1, "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"); + push_test_log(&mut block_2, "2_0"); vec![block_0, block_1, block_2] }; @@ -170,84 +170,137 @@ async fn derived_loaders() { ctx.start_and_sync_to(stop_block).await; - // Test loadRelated in the same handler - let query_res = ctx - .query(&format!( - r#"{{ testResult(id: "0") {{ id barDerived bBarDerived }} }}"#, - )) - .await - .unwrap(); - assert_json_eq!( - query_res, - Some(object! { testResult: object!{ - id: "0", - barDerived: "0", - bBarDerived: "0x30", - }}) - ); + // 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. - // Test loadRelated in same block let query_res = ctx - .query(&format!( - r#"{{ testResult(id: "1") {{ id barDerived bBarDerived }} }}"#, - )) - .await - .unwrap(); - assert_json_eq!( - query_res, - Some(object! { testResult: object!{ - id: "1", - barDerived: "0", - bBarDerived: "0x30", - }}) - ); + .query(&format!( + r#"{{ testResult(id:"1_0", block: {{ number: 1 }} ){{ id barDerived{{id value value2}} bBarDerived{{id value value2}} }} }}"#, + )) + .await + .unwrap(); - // Test loadRelated in a different block - let query_res = ctx - .query(&format!( - r#"{{ testResult(id: "2") {{ id barDerived bBarDerived }} }}"#, - )) - .await - .unwrap(); assert_json_eq!( query_res, - Some(object! { testResult: object!{ - id: "2", - barDerived: "0", - bBarDerived: "0x30", - }}) + 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#"{{ bfoos(orderBy: id) {{ id value bar {{ id }} }} }}"#, - )) - .await - .unwrap(); + .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! { bfoos: vec![object!{ - id: "0x30", - value: "1", - bar: object!{ id: "0x30" } - }] }) + 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#"{{ foos(orderBy: id) {{ id value bar {{ id }} }} }}"#, - )) - .await - .unwrap(); - + 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! { foos: vec![object!{ - id: "0", - value: "1", - bar: object!{ id: "0" } - }] }) + 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" + } + ] + } + }) ); } From 28a9b316c18265775f268675a6bfffda4eef9603 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Sat, 12 Aug 2023 00:58:56 +0530 Subject: [PATCH 12/13] graph: Add more comments to `load_related` --- graph/src/components/store/entity_cache.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/graph/src/components/store/entity_cache.rs b/graph/src/components/store/entity_cache.rs index c76da4ef1e2..292ed91699d 100644 --- a/graph/src/components/store/entity_cache.rs +++ b/graph/src/components/store/entity_cache.rs @@ -212,7 +212,6 @@ impl EntityCache { for (key, entity) in entity_map.iter() { // Only insert to the cache if it's not already there - // This is to avoid overwriting updates if !self.current.contains_key(&key) { self.current.insert(key.clone(), Some(entity.clone())); } @@ -301,7 +300,11 @@ impl EntityCache { } } - // Remove keys that were marked for removal + // 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); } From c55ce86de1288f56514e96e79f5adfe7339fc501 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Sat, 12 Aug 2023 01:00:16 +0530 Subject: [PATCH 13/13] tests: Add derived-loaders to yarn workspace --- tests/runner-tests/package.json | 3 +- tests/runner-tests/yarn.lock | 74 +++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) 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"