diff --git a/crates/sui-graphql-e2e-tests/tests/packages/versioning.exp b/crates/sui-graphql-e2e-tests/tests/packages/versioning.exp index 7a0e6ec3faeed..7f0e8a7153b98 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 15 tasks +processed 17 tasks init: A: object(0,0) @@ -13,7 +13,7 @@ task 2, line 11: //# create-checkpoint Checkpoint created: 1 -task 3, lines 13-28: +task 3, lines 13-50: //# run-graphql Response: { "data": { @@ -27,6 +27,35 @@ Response: { } ] } + }, + "packageVersions": { + "nodes": [ + { + "address": "0x175ae86f2df1eb652d57fbe9e44c7f2d67870d2b6776a4356f30930221b63b88", + "version": 1 + } + ] + } + }, + "firstPackage": { + "address": "0x175ae86f2df1eb652d57fbe9e44c7f2d67870d2b6776a4356f30930221b63b88", + "version": 1, + "module": { + "functions": { + "nodes": [ + { + "name": "f" + } + ] + } + }, + "packageVersions": { + "nodes": [ + { + "address": "0x175ae86f2df1eb652d57fbe9e44c7f2d67870d2b6776a4356f30930221b63b88", + "version": 1 + } + ] } }, "packages": { @@ -56,17 +85,17 @@ Response: { } } -task 4, lines 30-34: +task 4, lines 52-56: //# upgrade --package P0 --upgrade-capability 1,1 --sender A 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 5, line 36: +task 5, line 58: //# create-checkpoint Checkpoint created: 2 -task 6, lines 38-53: +task 6, lines 60-97: //# run-graphql Response: { "data": { @@ -83,6 +112,43 @@ Response: { } ] } + }, + "packageVersions": { + "nodes": [ + { + "address": "0x175ae86f2df1eb652d57fbe9e44c7f2d67870d2b6776a4356f30930221b63b88", + "version": 1 + }, + { + "address": "0x351bc614b36f0f522a64334e4c278d4bfe200234958870c084e0a005f041d681", + "version": 2 + } + ] + } + }, + "firstPackage": { + "address": "0x175ae86f2df1eb652d57fbe9e44c7f2d67870d2b6776a4356f30930221b63b88", + "version": 1, + "module": { + "functions": { + "nodes": [ + { + "name": "f" + } + ] + } + }, + "packageVersions": { + "nodes": [ + { + "address": "0x175ae86f2df1eb652d57fbe9e44c7f2d67870d2b6776a4356f30930221b63b88", + "version": 1 + }, + { + "address": "0x351bc614b36f0f522a64334e4c278d4bfe200234958870c084e0a005f041d681", + "version": 2 + } + ] } }, "packages": { @@ -116,17 +182,17 @@ Response: { } } -task 7, lines 55-60: +task 7, lines 99-104: //# upgrade --package P1 --upgrade-capability 1,1 --sender A 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 8, line 62: +task 8, line 106: //# create-checkpoint Checkpoint created: 3 -task 9, lines 64-79: +task 9, lines 108-145: //# run-graphql Response: { "data": { @@ -146,6 +212,51 @@ Response: { } ] } + }, + "packageVersions": { + "nodes": [ + { + "address": "0x175ae86f2df1eb652d57fbe9e44c7f2d67870d2b6776a4356f30930221b63b88", + "version": 1 + }, + { + "address": "0x351bc614b36f0f522a64334e4c278d4bfe200234958870c084e0a005f041d681", + "version": 2 + }, + { + "address": "0x0eae57b7a07b0548b1f6b0c309f0692828ff994e9159b541334b25582980631c", + "version": 3 + } + ] + } + }, + "firstPackage": { + "address": "0x175ae86f2df1eb652d57fbe9e44c7f2d67870d2b6776a4356f30930221b63b88", + "version": 1, + "module": { + "functions": { + "nodes": [ + { + "name": "f" + } + ] + } + }, + "packageVersions": { + "nodes": [ + { + "address": "0x175ae86f2df1eb652d57fbe9e44c7f2d67870d2b6776a4356f30930221b63b88", + "version": 1 + }, + { + "address": "0x351bc614b36f0f522a64334e4c278d4bfe200234958870c084e0a005f041d681", + "version": 2 + }, + { + "address": "0x0eae57b7a07b0548b1f6b0c309f0692828ff994e9159b541334b25582980631c", + "version": 3 + } + ] } }, "packages": { @@ -183,7 +294,7 @@ Response: { } } -task 10, lines 81-118: +task 10, lines 147-184: //# run-graphql Response: { "data": { @@ -283,7 +394,7 @@ Response: { } } -task 11, lines 120-157: +task 11, lines 186-223: //# run-graphql Response: { "data": { @@ -374,7 +485,7 @@ Response: { } } -task 12, lines 159-214: +task 12, lines 225-280: //# run-graphql Response: { "data": { @@ -513,7 +624,7 @@ Response: { } } -task 13, lines 216-244: +task 13, lines 282-310: //# run-graphql Response: { "data": { @@ -526,7 +637,7 @@ Response: { } } -task 14, lines 246-277: +task 14, lines 312-343: //# run-graphql Response: { "data": { @@ -621,3 +732,83 @@ Response: { } } } + +task 15, lines 345-380: +//# run-graphql +Response: { + "data": { + "packageVersions": { + "nodes": [ + { + "address": "0x175ae86f2df1eb652d57fbe9e44c7f2d67870d2b6776a4356f30930221b63b88", + "version": 1 + }, + { + "address": "0x351bc614b36f0f522a64334e4c278d4bfe200234958870c084e0a005f041d681", + "version": 2 + }, + { + "address": "0x0eae57b7a07b0548b1f6b0c309f0692828ff994e9159b541334b25582980631c", + "version": 3 + } + ] + }, + "after": { + "nodes": [ + { + "address": "0x351bc614b36f0f522a64334e4c278d4bfe200234958870c084e0a005f041d681", + "version": 2 + }, + { + "address": "0x0eae57b7a07b0548b1f6b0c309f0692828ff994e9159b541334b25582980631c", + "version": 3 + } + ] + }, + "before": { + "nodes": [ + { + "address": "0x175ae86f2df1eb652d57fbe9e44c7f2d67870d2b6776a4356f30930221b63b88", + "version": 1 + }, + { + "address": "0x351bc614b36f0f522a64334e4c278d4bfe200234958870c084e0a005f041d681", + "version": 2 + } + ] + }, + "between": { + "nodes": [ + { + "address": "0x351bc614b36f0f522a64334e4c278d4bfe200234958870c084e0a005f041d681", + "version": 2 + } + ] + } + } +} + +task 16, lines 382-400: +//# run-graphql +Response: { + "data": { + "packageVersions": { + "nodes": [ + { + "address": "0x0000000000000000000000000000000000000000000000000000000000000001", + "version": 1 + } + ] + }, + "package": { + "packageVersions": { + "nodes": [ + { + "address": "0x0000000000000000000000000000000000000000000000000000000000000001", + "version": 1 + } + ] + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/packages/versioning.move b/crates/sui-graphql-e2e-tests/tests/packages/versioning.move index 694072fb9c445..b0e0900bbcb59 100644 --- a/crates/sui-graphql-e2e-tests/tests/packages/versioning.move +++ b/crates/sui-graphql-e2e-tests/tests/packages/versioning.move @@ -17,6 +17,28 @@ module P0::m { module(name: "m") { functions { nodes { name } } } + + packageVersions { + nodes { + address + version + } + } + } + + firstPackage: package(address: "@{P0}", version: 1) { + address + version + module(name: "m") { + functions { nodes { name } } + } + + packageVersions { + nodes { + address + version + } + } } packages(first: 10) { @@ -42,6 +64,28 @@ module P1::m { module(name: "m") { functions { nodes { name } } } + + packageVersions { + nodes { + address + version + } + } + } + + firstPackage: package(address: "@{P1}", version: 1) { + address + version + module(name: "m") { + functions { nodes { name } } + } + + packageVersions { + nodes { + address + version + } + } } packages(first: 10) { @@ -68,6 +112,28 @@ module P2::m { module(name: "m") { functions { nodes { name } } } + + packageVersions { + nodes { + address + version + } + } + } + + firstPackage: package(address: "@{P2}", version: 1) { + address + version + module(name: "m") { + functions { nodes { name } } + } + + packageVersions { + nodes { + address + version + } + } } packages(first: 10) { @@ -275,3 +341,60 @@ module P2::m { } } } + +//# run-graphql +{ # Query for versions of a user package + packageVersions(address: "@{P0}") { + nodes { + address + version + } + } + + after: packageVersions(address: "@{P0}", filter: { afterVersion: 1 }) { + nodes { + address + version + } + } + + before: packageVersions(address: "@{P0}", filter: { beforeVersion: 3 }) { + nodes { + address + version + } + } + + between: packageVersions( + address: "@{P0}", + filter: { + afterVersion: 1, + beforeVersion: 3, + }, + ) { + nodes { + address + version + } + } +} + +//# run-graphql +{ # Query for versions of a system package (there will be only one because we + # don't have a way to upgrade system packages in these tests.) + packageVersions(address: "0x1") { + nodes { + address + version + } + } + + package(address: "0x1") { + packageVersions { + nodes { + address + version + } + } + } +} diff --git a/crates/sui-graphql-rpc/schema/current_progress_schema.graphql b/crates/sui-graphql-rpc/schema/current_progress_schema.graphql index 95fca9ce535b8..9a134382a7a70 100644 --- a/crates/sui-graphql-rpc/schema/current_progress_schema.graphql +++ b/crates/sui-graphql-rpc/schema/current_progress_schema.graphql @@ -2173,6 +2173,12 @@ type MovePackage implements IObject & IOwner { """ packageAtVersion(version: Int!): MovePackage """ + Fetch all versions of this package (packages that share this package's original ID), + optionally bounding the versions exclusively from below with `afterVersion`, or from above + with `beforeVersion`. + """ + packageVersions(first: Int, after: String, last: Int, before: String, filter: MovePackageVersionFilter): MovePackageConnection! + """ Fetch the latest version of this package (the package with the highest `version` that shares this packages's original ID) """ @@ -2246,6 +2252,22 @@ type MovePackageEdge { cursor: String! } +""" +Filter for paginating versions of a given `MovePackage`. +""" +input MovePackageVersionFilter { + """ + Fetch versions of this package that are strictly newer than this version. Omitting this + fetches versions since the original version. + """ + afterVersion: UInt53 + """ + Fetch versions of this package that are strictly older than this version. Omitting this + fetches versions up to the latest version (inclusive). + """ + beforeVersion: UInt53 +} + """ Description of a struct type, defined in a Move module. """ @@ -3139,6 +3161,12 @@ type Query { """ packages(first: Int, after: String, last: Int, before: String, filter: MovePackageCheckpointFilter): MovePackageConnection! """ + Fetch all versions of package at `address` (packages that share this package's original ID), + optionally bounding the versions exclusively from below with `afterVersion`, or from above + with `beforeVersion`. + """ + packageVersions(first: Int, after: String, last: Int, before: String, address: SuiAddress!, filter: MovePackageVersionFilter): MovePackageConnection! + """ Fetch the protocol config by protocol version (defaults to the latest protocol version known to the GraphQL service). """ diff --git a/crates/sui-graphql-rpc/src/types/move_package.rs b/crates/sui-graphql-rpc/src/types/move_package.rs index d29612246c305..aba7259ff1a22 100644 --- a/crates/sui-graphql-rpc/src/types/move_package.rs +++ b/crates/sui-graphql-rpc/src/types/move_package.rs @@ -58,6 +58,18 @@ pub(crate) struct MovePackageCheckpointFilter { pub before_checkpoint: Option, } +/// Filter for paginating versions of a given `MovePackage`. +#[derive(InputObject, Debug, Default, Clone)] +pub(crate) struct MovePackageVersionFilter { + /// Fetch versions of this package that are strictly newer than this version. Omitting this + /// fetches versions since the original version. + pub after_version: Option, + + /// Fetch versions of this package that are strictly older than this version. Omitting this + /// fetches versions up to the latest version (inclusive). + pub before_version: Option, +} + /// Filter for a point query of a MovePackage, supporting querying different versions of a package /// by their version. Note that different versions of the same user package exist at different IDs /// to each other, so this is different from looking up the historical version of an object. @@ -355,6 +367,31 @@ impl MovePackage { .extend() } + /// Fetch all versions of this package (packages that share this package's original ID), + /// optionally bounding the versions exclusively from below with `afterVersion`, or from above + /// with `beforeVersion`. + async fn package_versions( + &self, + ctx: &Context<'_>, + first: Option, + after: Option, + last: Option, + before: Option, + filter: Option, + ) -> Result> { + let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?; + + MovePackage::paginate_by_version( + ctx.data_unchecked(), + page, + self.super_.address, + filter, + self.checkpoint_viewed_at_impl(), + ) + .await + .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 { @@ -690,6 +727,55 @@ impl MovePackage { Ok(conn) } + /// Query the database for a `page` of Move packages. The Page uses the checkpoint sequence + /// number the package was created at, its original ID, and its version as the cursor. The query + /// is filtered by the ID of a package and will only return packages from the same family + /// (sharing the same original ID as the package whose ID was given), and can optionally be + /// filtered by an upper and lower bound on package version. + /// + /// The `checkpoint_viewed_at` parameter represents the checkpoint sequence number at which this + /// page was queried. Each entity returned in the connection will inherit this checkpoint, so + /// that when viewing that entity's state, it will be as if it is being viewed at this + /// checkpoint. + /// + /// The cursors in `page` may also include checkpoint viewed at fields. If these are set, they + /// take precedence over the checkpoint that pagination is being conducted in. + pub(crate) async fn paginate_by_version( + db: &Db, + page: Page, + package: SuiAddress, + filter: Option, + checkpoint_viewed_at: u64, + ) -> Result, Error> { + let cursor_viewed_at = page.validate_cursor_consistency()?; + let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(checkpoint_viewed_at); + let (prev, next, results) = db + .execute(move |conn| { + page.paginate_raw_query::( + conn, + checkpoint_viewed_at, + if is_system_package(package) { + system_package_version_query(package, filter) + } else { + user_package_version_query(package, filter) + }, + ) + }) + .await?; + + let mut conn = Connection::new(prev, next); + + // The "checkpoint viewed at" sets a consistent upper bound for the nested queries. + for stored in results { + let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor(); + let package = + MovePackage::try_from_stored_history_object(stored.object, checkpoint_viewed_at)?; + conn.edges.push(Edge::new(cursor, package)); + } + + Ok(conn) + } + /// `checkpoint_viewed_at` points to the checkpoint snapshot that this `MovePackage` came from. /// This is stored in the `MovePackage` so that related fields from the package are read from /// the same checkpoint (consistently). @@ -719,9 +805,9 @@ impl RawPaginated for StoredHistoryPackage { format!( "o.checkpoint_sequence_number > {cp} OR (\ o.checkpoint_sequence_number = {cp} AND - p.original_id > '\\x{id}'::bytea OR (\ - p.original_id = '\\x{id}'::bytea AND \ - p.package_version >= {pv}\ + original_id > '\\x{id}'::bytea OR (\ + original_id = '\\x{id}'::bytea AND \ + o.object_version >= {pv}\ ))", cp = cursor.checkpoint_sequence_number, id = hex::encode(&cursor.original_id), @@ -736,9 +822,9 @@ impl RawPaginated for StoredHistoryPackage { format!( "o.checkpoint_sequence_number < {cp} OR (\ o.checkpoint_sequence_number = {cp} AND - p.original_id < '\\x{id}'::bytea OR (\ - p.original_id = '\\x{id}'::bytea AND \ - p.package_version <= {pv}\ + original_id < '\\x{id}'::bytea OR (\ + original_id = '\\x{id}'::bytea AND \ + o.object_version <= {pv}\ ))", cp = cursor.checkpoint_sequence_number, id = hex::encode(&cursor.original_id), @@ -751,13 +837,13 @@ impl RawPaginated for StoredHistoryPackage { if asc { query .order_by("o.checkpoint_sequence_number ASC") - .order_by("p.original_id ASC") - .order_by("p.package_version ASC") + .order_by("original_id ASC") + .order_by("o.object_version ASC") } else { query .order_by("o.checkpoint_sequence_number DESC") - .order_by("p.original_id DESC") - .order_by("p.package_version DESC") + .order_by("original_id DESC") + .order_by("o.object_version DESC") } } } @@ -918,3 +1004,94 @@ impl TryFrom<&Object> for MovePackage { } } } + +/// Query for fetching all the versions of a system package (assumes that `package` has already been +/// verified as a system package). This is an `objects_history` query disguised as a package query. +fn system_package_version_query( + package: SuiAddress, + filter: Option, +) -> RawQuery { + // Query uses a left join to force the query planner to use `objects_version` in the outer loop. + let mut q = query!( + r#" + SELECT + o.object_id AS original_id, + o.* + FROM + objects_version v + LEFT JOIN + objects_history o + ON + v.object_id = o.object_id + AND v.object_version = o.object_version + AND v.cp_sequence_number = o.checkpoint_sequence_number + "# + ); + + q = filter!( + q, + format!( + "v.object_id = '\\x{}'::bytea", + hex::encode(package.into_vec()) + ) + ); + + if let Some(after) = filter.as_ref().and_then(|f| f.after_version) { + let a: u64 = after.into(); + q = filter!(q, format!("v.object_version > {a}")); + } + + if let Some(before) = filter.as_ref().and_then(|f| f.before_version) { + let b: u64 = before.into(); + q = filter!(q, format!("v.object_version < {b}")); + } + + q +} + +/// Query for fetching all the versions of a non-system package (assumes that `package` has already +/// been verified as a system package) +fn user_package_version_query( + package: SuiAddress, + filter: Option, +) -> RawQuery { + let mut q = query!( + r#" + SELECT + p.original_id, + o.* + FROM + packages q + INNER JOIN + packages p + ON + q.original_id = p.original_id + INNER JOIN + objects_history o + ON + p.package_id = o.object_id + AND p.package_version = o.object_version + AND p.checkpoint_sequence_number = o.checkpoint_sequence_number + "# + ); + + q = filter!( + q, + format!( + "q.package_id = '\\x{}'::bytea", + hex::encode(package.into_vec()) + ) + ); + + if let Some(after) = filter.as_ref().and_then(|f| f.after_version) { + let a: u64 = after.into(); + q = filter!(q, format!("p.package_version > {a}")); + } + + if let Some(before) = filter.as_ref().and_then(|f| f.before_version) { + let b: u64 = before.into(); + q = filter!(q, format!("p.package_version < {b}")); + } + + q +} diff --git a/crates/sui-graphql-rpc/src/types/query.rs b/crates/sui-graphql-rpc/src/types/query.rs index 0970a046749c3..1ff209d351a55 100644 --- a/crates/sui-graphql-rpc/src/types/query.rs +++ b/crates/sui-graphql-rpc/src/types/query.rs @@ -12,7 +12,9 @@ use sui_sdk::SuiClient; use sui_types::transaction::{TransactionData, TransactionKind}; use sui_types::{gas_coin::GAS, transaction::TransactionDataAPI, TypeTag}; -use super::move_package::{self, MovePackage, MovePackageCheckpointFilter}; +use super::move_package::{ + self, MovePackage, MovePackageCheckpointFilter, MovePackageVersionFilter, +}; use super::suins_registration::NameService; use super::uint53::UInt53; use super::{ @@ -457,6 +459,27 @@ impl Query { .extend() } + /// Fetch all versions of package at `address` (packages that share this package's original ID), + /// optionally bounding the versions exclusively from below with `afterVersion`, or from above + /// with `beforeVersion`. + async fn package_versions( + &self, + ctx: &Context<'_>, + first: Option, + after: Option, + last: Option, + before: Option, + address: SuiAddress, + filter: Option, + ) -> Result> { + let Watermark { checkpoint, .. } = *ctx.data()?; + + let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?; + MovePackage::paginate_by_version(ctx.data_unchecked(), page, address, filter, checkpoint) + .await + .extend() + } + /// Fetch the protocol config by protocol version (defaults to the latest protocol /// version known to the GraphQL service). async fn protocol_config( 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 3499ea88329cc..87f5a27061f06 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,12 @@ type MovePackage implements IObject & IOwner { """ packageAtVersion(version: Int!): MovePackage """ + Fetch all versions of this package (packages that share this package's original ID), + optionally bounding the versions exclusively from below with `afterVersion`, or from above + with `beforeVersion`. + """ + packageVersions(first: Int, after: String, last: Int, before: String, filter: MovePackageVersionFilter): MovePackageConnection! + """ Fetch the latest version of this package (the package with the highest `version` that shares this packages's original ID) """ @@ -2250,6 +2256,22 @@ type MovePackageEdge { cursor: String! } +""" +Filter for paginating versions of a given `MovePackage`. +""" +input MovePackageVersionFilter { + """ + Fetch versions of this package that are strictly newer than this version. Omitting this + fetches versions since the original version. + """ + afterVersion: UInt53 + """ + Fetch versions of this package that are strictly older than this version. Omitting this + fetches versions up to the latest version (inclusive). + """ + beforeVersion: UInt53 +} + """ Description of a struct type, defined in a Move module. """ @@ -3143,6 +3165,12 @@ type Query { """ packages(first: Int, after: String, last: Int, before: String, filter: MovePackageCheckpointFilter): MovePackageConnection! """ + Fetch all versions of package at `address` (packages that share this package's original ID), + optionally bounding the versions exclusively from below with `afterVersion`, or from above + with `beforeVersion`. + """ + packageVersions(first: Int, after: String, last: Int, before: String, address: SuiAddress!, filter: MovePackageVersionFilter): MovePackageConnection! + """ Fetch the protocol config by protocol version (defaults to the latest protocol version known to the GraphQL service). """