diff --git a/crates/sui-graphql-e2e-tests/tests/packages/versioning.exp b/crates/sui-graphql-e2e-tests/tests/packages/versioning.exp index 4c37560181402a..7b8ad9c4c32cc4 100644 --- a/crates/sui-graphql-e2e-tests/tests/packages/versioning.exp +++ b/crates/sui-graphql-e2e-tests/tests/packages/versioning.exp @@ -1,4 +1,4 @@ -processed 9 tasks +processed 14 tasks init: A: object(0,0) @@ -9,23 +9,97 @@ created: object(1,0), object(1,1) mutated: object(0,0) gas summary: computation_cost: 1000000, storage_cost: 5076800, storage_rebate: 0, non_refundable_storage_fee: 0 -task 2, lines 11-15: +task 2, line 11: +//# create-checkpoint +Checkpoint created: 1 + +task 3, lines 13-21: +//# run-graphql +Response: { + "data": { + "latestPackage": { + "version": 1, + "module": { + "functions": { + "nodes": [ + { + "name": "f" + } + ] + } + } + } + } +} + +task 4, lines 23-27: //# upgrade --package P0 --upgrade-capability 1,1 --sender A -created: object(2,0) +created: object(4,0) mutated: object(0,0), object(1,1) gas summary: computation_cost: 1000000, storage_cost: 5251600, storage_rebate: 2595780, non_refundable_storage_fee: 26220 -task 3, lines 17-22: +task 5, line 29: +//# create-checkpoint +Checkpoint created: 2 + +task 6, lines 31-39: +//# run-graphql +Response: { + "data": { + "latestPackage": { + "version": 2, + "module": { + "functions": { + "nodes": [ + { + "name": "f" + }, + { + "name": "g" + } + ] + } + } + } + } +} + +task 7, lines 41-46: //# upgrade --package P1 --upgrade-capability 1,1 --sender A -created: object(3,0) +created: object(7,0) mutated: object(0,0), object(1,1) gas summary: computation_cost: 1000000, storage_cost: 5426400, storage_rebate: 2595780, non_refundable_storage_fee: 26220 -task 4, line 24: +task 8, line 48: //# create-checkpoint -Checkpoint created: 1 +Checkpoint created: 3 -task 5, lines 26-45: +task 9, lines 50-58: +//# run-graphql +Response: { + "data": { + "latestPackage": { + "version": 3, + "module": { + "functions": { + "nodes": [ + { + "name": "f" + }, + { + "name": "g" + }, + { + "name": "h" + } + ] + } + } + } + } +} + +task 10, lines 60-97: //# run-graphql Response: { "data": { @@ -38,6 +112,23 @@ Response: { } ] } + }, + "latestPackage": { + "module": { + "functions": { + "nodes": [ + { + "name": "f" + }, + { + "name": "g" + }, + { + "name": "h" + } + ] + } + } } }, "v2": { @@ -52,6 +143,23 @@ Response: { } ] } + }, + "latestPackage": { + "module": { + "functions": { + "nodes": [ + { + "name": "f" + }, + { + "name": "g" + }, + { + "name": "h" + } + ] + } + } } }, "v3": { @@ -69,12 +177,29 @@ Response: { } ] } + }, + "latestPackage": { + "module": { + "functions": { + "nodes": [ + { + "name": "f" + }, + { + "name": "g" + }, + { + "name": "h" + } + ] + } + } } } } } -task 6, lines 47-84: +task 11, lines 99-136: //# run-graphql Response: { "data": { @@ -165,7 +290,7 @@ Response: { } } -task 7, lines 86-141: +task 12, lines 138-193: //# run-graphql Response: { "data": { @@ -304,7 +429,7 @@ Response: { } } -task 8, lines 143-171: +task 13, lines 195-223: //# run-graphql Response: { "data": { diff --git a/crates/sui-graphql-e2e-tests/tests/packages/versioning.move b/crates/sui-graphql-e2e-tests/tests/packages/versioning.move index 72bdc66db632fd..f4646294722b70 100644 --- a/crates/sui-graphql-e2e-tests/tests/packages/versioning.move +++ b/crates/sui-graphql-e2e-tests/tests/packages/versioning.move @@ -8,12 +8,36 @@ module P0::m { public fun f(): u64 { 42 } } +//# create-checkpoint + +//# run-graphql +{ + latestPackage(address: "@{P0}") { + version + module(name: "m") { + functions { nodes { name } } + } + } +} + //# upgrade --package P0 --upgrade-capability 1,1 --sender A module P1::m { public fun f(): u64 { 42 } public fun g(): u64 { 43 } } +//# create-checkpoint + +//# run-graphql +{ + latestPackage(address: "@{P0}") { + version + module(name: "m") { + functions { nodes { name } } + } + } +} + //# upgrade --package P1 --upgrade-capability 1,1 --sender A module P2::m { public fun f(): u64 { 42 } @@ -23,24 +47,52 @@ module P2::m { //# create-checkpoint +//# run-graphql +{ + latestPackage(address: "@{P0}") { + version + module(name: "m") { + functions { nodes { name } } + } + } +} + //# run-graphql { # Test fetching by ID v1: package(address: "@{P0}") { module(name: "m") { functions { nodes { name } } } + + latestPackage { + module(name: "m") { + functions { nodes { name } } + } + } } v2: package(address: "@{P1}") { module(name: "m") { functions { nodes { name } } } + + latestPackage { + module(name: "m") { + functions { nodes { name } } + } + } } v3: package(address: "@{P2}") { module(name: "m") { functions { nodes { name } } } + + latestPackage { + module(name: "m") { + functions { nodes { name } } + } + } } } diff --git a/crates/sui-graphql-rpc/schema/current_progress_schema.graphql b/crates/sui-graphql-rpc/schema/current_progress_schema.graphql index 201b62290eabed..69472d05780455 100644 --- a/crates/sui-graphql-rpc/schema/current_progress_schema.graphql +++ b/crates/sui-graphql-rpc/schema/current_progress_schema.graphql @@ -2173,6 +2173,11 @@ type MovePackage implements IObject & IOwner { """ packageAtVersion(version: Int!): MovePackage """ + Fetch the latest version of this package (the package with the highest `version` that shares + this packages's original ID) + """ + latestPackage: MovePackage! + """ A representation of the module called `name` in this package, including the structs and functions it defines. """ @@ -3058,6 +3063,13 @@ type Query { """ package(address: SuiAddress!, version: UInt53): MovePackage """ + The latest version of the package at `address`. + + This corresponds to the package with the highest `version` that shares its original ID with + the package at `address`. + """ + latestPackage(address: SuiAddress!): MovePackage + """ Look-up an Account by its SuiAddress. """ address(address: SuiAddress!): Address diff --git a/crates/sui-graphql-rpc/src/types/move_package.rs b/crates/sui-graphql-rpc/src/types/move_package.rs index a9dbeb0188f153..1e8490be2465bb 100644 --- a/crates/sui-graphql-rpc/src/types/move_package.rs +++ b/crates/sui-graphql-rpc/src/types/move_package.rs @@ -1,7 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use std::collections::{BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use super::balance::{self, Balance}; use super::base64::Base64; @@ -54,6 +54,10 @@ pub(crate) enum PackageLookup { version: u64, checkpoint_viewed_at: u64, }, + + /// Get the package whose original ID matches the storage ID of the package at the given + /// address, but that has the max version at the given checkpoint. + Latest { checkpoint_viewed_at: u64 }, } /// Information used by a package to link to a specific version of its dependency. @@ -99,6 +103,14 @@ struct PackageVersionKey { version: u64, } +/// DataLoader key for fetching the latest version of a user package: The package with the largest +/// version whose original ID matches the original ID of the package at `address`. +#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] +struct LatestKey { + address: SuiAddress, + checkpoint_viewed_at: u64, +} + /// A MovePackage is a kind of Move object that represents code that has been published on chain. /// It exposes information about its modules, type definitions, functions, and dependencies. #[Object] @@ -304,6 +316,19 @@ impl MovePackage { .extend() } + /// Fetch the latest version of this package (the package with the highest `version` that shares + /// this packages's original ID) + async fn latest_package(&self, ctx: &Context<'_>) -> Result { + Ok(MovePackage::query( + ctx, + self.super_.address, + MovePackage::latest_at(self.checkpoint_viewed_at_impl()), + ) + .await + .extend()? + .ok_or_else(|| Error::Internal("No latest version found".to_string()))?) + } + /// A representation of the module called `name` in this package, including the /// structs and functions it defines. async fn module(&self, name: String) -> Result> { @@ -482,6 +507,14 @@ impl MovePackage { } } + /// Look-up the package that shares the same original ID as the package at `address`, but has + /// the latest version, as of the given checkpoint. + pub(crate) fn latest_at(checkpoint_viewed_at: u64) -> PackageLookup { + PackageLookup::Latest { + checkpoint_viewed_at, + } + } + pub(crate) async fn query( ctx: &Context<'_>, address: SuiAddress, @@ -510,6 +543,27 @@ impl MovePackage { (translation, Object::latest_at(checkpoint_viewed_at)) } } + + PackageLookup::Latest { + checkpoint_viewed_at, + } => { + if is_system_package(address) { + (address, Object::latest_at(checkpoint_viewed_at)) + } else { + let DataLoader(loader) = &ctx.data_unchecked(); + let Some(translation) = loader + .load_one(LatestKey { + address, + checkpoint_viewed_at, + }) + .await? + else { + return Ok(None); + }; + + (translation, Object::latest_at(checkpoint_viewed_at)) + } + } }; let Some(object) = Object::query(ctx, address, key).await? else { @@ -580,6 +634,75 @@ impl Loader for Db { } } +#[async_trait::async_trait] +impl Loader for Db { + type Value = SuiAddress; + type Error = Error; + + async fn load(&self, keys: &[LatestKey]) -> Result, Error> { + use packages::dsl; + let other = diesel::alias!(packages as other); + + let mut ids_by_cursor: BTreeMap<_, BTreeSet<_>> = BTreeMap::new(); + for key in keys { + ids_by_cursor + .entry(key.checkpoint_viewed_at) + .or_default() + .insert(key.address.into_vec()); + } + + // Issue concurrent reads for each group of IDs + let futures = ids_by_cursor + .into_iter() + .map(|(checkpoint_viewed_at, ids)| { + self.execute(move |conn| { + let results: Vec<(Vec, Vec)> = conn.results(|| { + let o_original_id = other.field(dsl::original_id); + let o_package_id = other.field(dsl::package_id); + let o_cp_seq_num = other.field(dsl::checkpoint_sequence_number); + let o_version = other.field(dsl::package_version); + + let query = dsl::packages + .inner_join(other.on(dsl::original_id.eq(o_original_id))) + .select((dsl::package_id, o_package_id)) + .filter(dsl::package_id.eq_any(ids.iter().cloned())) + .filter(o_cp_seq_num.le(checkpoint_viewed_at as i64)) + .order_by((dsl::package_id, dsl::original_id, o_version.desc())) + .distinct_on((dsl::package_id, dsl::original_id)); + query + })?; + + Ok::<_, diesel::result::Error>( + results + .into_iter() + .map(|(p, latest)| (checkpoint_viewed_at, p, latest)) + .collect::>(), + ) + }) + }); + + // Wait for the reads to all finish, and gather them into the result map. + let groups = futures::future::join_all(futures).await; + + let mut results = HashMap::new(); + for group in groups { + for (checkpoint_viewed_at, address, latest) in + group.map_err(|e| Error::Internal(format!("Failed to fetch packages: {e}")))? + { + results.insert( + LatestKey { + address: addr(&address)?, + checkpoint_viewed_at, + }, + addr(&latest)?, + ); + } + } + + Ok(results) + } +} + impl TryFrom<&Object> for MovePackage { type Error = MovePackageDowncastError; diff --git a/crates/sui-graphql-rpc/src/types/query.rs b/crates/sui-graphql-rpc/src/types/query.rs index 46334df30b1fd4..5aa55e73347738 100644 --- a/crates/sui-graphql-rpc/src/types/query.rs +++ b/crates/sui-graphql-rpc/src/types/query.rs @@ -248,6 +248,21 @@ impl Query { MovePackage::query(ctx, address, key).await.extend() } + /// The latest version of the package at `address`. + /// + /// This corresponds to the package with the highest `version` that shares its original ID with + /// the package at `address`. + async fn latest_package( + &self, + ctx: &Context<'_>, + address: SuiAddress, + ) -> Result> { + let Watermark { checkpoint, .. } = *ctx.data()?; + MovePackage::query(ctx, address, MovePackage::latest_at(checkpoint)) + .await + .extend() + } + /// Look-up an Account by its SuiAddress. async fn address(&self, ctx: &Context<'_>, address: SuiAddress) -> Result> { let Watermark { checkpoint, .. } = *ctx.data()?; diff --git a/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap b/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap index 809354db889720..61cabeeb1de138 100644 --- a/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap +++ b/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap @@ -2177,6 +2177,11 @@ type MovePackage implements IObject & IOwner { """ packageAtVersion(version: Int!): MovePackage """ + Fetch the latest version of this package (the package with the highest `version` that shares + this packages's original ID) + """ + latestPackage: MovePackage! + """ A representation of the module called `name` in this package, including the structs and functions it defines. """ @@ -3062,6 +3067,13 @@ type Query { """ package(address: SuiAddress!, version: UInt53): MovePackage """ + The latest version of the package at `address`. + + This corresponds to the package with the highest `version` that shares its original ID with + the package at `address`. + """ + latestPackage(address: SuiAddress!): MovePackage + """ Look-up an Account by its SuiAddress. """ address(address: SuiAddress!): Address