Skip to content

Commit fa0498b

Browse files
committed
graphql-alt: Fetch latest Epoch
## Description Support calling `Query.epoch` without an ID to fetch the latest epoch. ## Test plan Updated E2E tests: ``` sui$ cargo nextest run \ -p sui-indexer-alt-e2e-tests \ -- graphql/epochs ```
1 parent 1f9734e commit fa0498b

File tree

10 files changed

+150
-38
lines changed

10 files changed

+150
-38
lines changed

crates/sui-indexer-alt-e2e-tests/tests/graphql/epochs/query.move

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
//# run-graphql
1717
{
18+
latest: epoch { ...E }
19+
1820
e0: epoch(epochId: 0) { ...E }
1921
e1: epoch(epochId: 1) { ...E }
2022
e2: epoch(epochId: 2) { ...E }
@@ -31,16 +33,21 @@ fragment E on Epoch {
3133
}
3234

3335
//# run-graphql
34-
{ # This checkpoint is half way through the epoch, so the epoch should return
35-
# its starting information, but not its ending information.
36+
{ # This checkpoint is half way through an earlier epoch, which should be
37+
# reflected in the latest epoch we get a start time for and an end time for.
3638
checkpoint(sequenceNumber: 2) {
3739
query {
38-
epoch(epochId: 1) {
39-
epochId
40-
referenceGasPrice
41-
startTimestamp
42-
endTimestamp
43-
}
40+
latest: epoch { ...E }
41+
e0: epoch(epochId: 0) { ...E }
42+
e1: epoch(epochId: 1) { ...E }
43+
e2: epoch(epochId: 2) { ...E }
4444
}
4545
}
4646
}
47+
48+
fragment E on Epoch {
49+
epochId
50+
referenceGasPrice
51+
startTimestamp
52+
endTimestamp
53+
}

crates/sui-indexer-alt-e2e-tests/tests/graphql/epochs/query.snap

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@ task 5, line 14:
1818
//# advance-epoch
1919
Epoch advanced: 2
2020

21-
task 6, lines 16-31:
21+
task 6, lines 16-33:
2222
//# run-graphql
2323
Response: {
2424
"data": {
25+
"latest": {
26+
"epochId": 2,
27+
"referenceGasPrice": "1000",
28+
"startTimestamp": "1970-01-01T00:00:00.444Z",
29+
"endTimestamp": null
30+
},
2531
"e0": {
2632
"epochId": 0,
2733
"referenceGasPrice": "1000",
@@ -44,18 +50,31 @@ Response: {
4450
}
4551
}
4652

47-
task 7, lines 33-46:
53+
task 7, lines 35-53:
4854
//# run-graphql
4955
Response: {
5056
"data": {
5157
"checkpoint": {
5258
"query": {
53-
"epoch": {
59+
"latest": {
60+
"epochId": 1,
61+
"referenceGasPrice": "1000",
62+
"startTimestamp": "1970-01-01T00:00:00.123Z",
63+
"endTimestamp": null
64+
},
65+
"e0": {
66+
"epochId": 0,
67+
"referenceGasPrice": "1000",
68+
"startTimestamp": "1970-01-01T00:00:00Z",
69+
"endTimestamp": "1970-01-01T00:00:00.123Z"
70+
},
71+
"e1": {
5472
"epochId": 1,
5573
"referenceGasPrice": "1000",
5674
"startTimestamp": "1970-01-01T00:00:00.123Z",
5775
"endTimestamp": null
58-
}
76+
},
77+
"e2": null
5978
}
6079
}
6180
}

crates/sui-indexer-alt-graphql/schema.graphql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,11 +373,11 @@ type Query {
373373
"""
374374
checkpoint(sequenceNumber: UInt53): Checkpoint
375375
"""
376-
Fetch an epoch by its ID.
376+
Fetch an epoch by its ID, or fetch the latest epoch if no ID is provided.
377377
378378
Returns `null` if the epoch does not exist yet, or was pruned.
379379
"""
380-
epoch(epochId: UInt53!): Epoch
380+
epoch(epochId: UInt53): Epoch
381381
"""
382382
Fetch checkpoints by their sequence numbers.
383383

crates/sui-indexer-alt-graphql/src/api/query.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,14 @@ impl Query {
5252
Ok(Checkpoint::with_sequence_number(scope, sequence_number))
5353
}
5454

55-
/// Fetch an epoch by its ID.
55+
/// Fetch an epoch by its ID, or fetch the latest epoch if no ID is provided.
5656
///
5757
/// Returns `null` if the epoch does not exist yet, or was pruned.
58-
async fn epoch(&self, ctx: &Context<'_>, epoch_id: UInt53) -> Result<Option<Epoch>, RpcError> {
58+
async fn epoch(
59+
&self,
60+
ctx: &Context<'_>,
61+
epoch_id: Option<UInt53>,
62+
) -> Result<Option<Epoch>, RpcError> {
5963
let scope = self.scope(ctx)?;
6064
Epoch::fetch(ctx, scope, epoch_id).await
6165
}
@@ -86,7 +90,7 @@ impl Query {
8690
let scope = self.scope(ctx)?;
8791
let epochs = keys
8892
.into_iter()
89-
.map(|k| Epoch::fetch(ctx, scope.clone(), k));
93+
.map(|k| Epoch::fetch(ctx, scope.clone(), Some(k)));
9094

9195
try_join_all(epochs).await
9296
}

crates/sui-indexer-alt-graphql/src/api/types/epoch.rs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::sync::Arc;
66
use anyhow::Context as _;
77
use async_graphql::{dataloader::DataLoader, Context, Object};
88
use sui_indexer_alt_reader::{
9-
epochs::{EpochEndKey, EpochStartKey},
9+
epochs::{CheckpointBoundedEpochStartKey, EpochEndKey, EpochStartKey},
1010
pg_reader::PgReader,
1111
};
1212
use sui_indexer_alt_schema::epochs::{StoredEpochEnd, StoredEpochStart};
@@ -58,7 +58,7 @@ impl Epoch {
5858

5959
#[graphql(flatten)]
6060
async fn start(&self, ctx: &Context<'_>) -> Result<EpochStart, RpcError> {
61-
self.start.fetch(ctx, self.epoch_id).await
61+
self.start.fetch(ctx, Some(self.epoch_id)).await
6262
}
6363

6464
#[graphql(flatten)]
@@ -143,14 +143,18 @@ impl Epoch {
143143
}
144144

145145
/// Load the epoch from the store, and return it fully inflated (with contents already
146-
/// fetched). Returns `None` if the epoch does not exist (or started after the checkpoint being
147-
/// viewed).
146+
/// fetched). If `epoch_id` is provided, the epoch with that ID is loaded. Otherwise, the
147+
/// latest epoch for the current checkpoint is loaded.
148+
///
149+
/// Returns `None` if the epoch does not exist (or started after the checkpoint being viewed).
148150
pub(crate) async fn fetch(
149151
ctx: &Context<'_>,
150152
scope: Scope,
151-
epoch_id: UInt53,
153+
epoch_id: Option<UInt53>,
152154
) -> Result<Option<Self>, RpcError> {
153-
let start = EpochStart::empty(scope).fetch(ctx, epoch_id.into()).await?;
155+
let start = EpochStart::empty(scope)
156+
.fetch(ctx, epoch_id.map(|id| id.into()))
157+
.await?;
154158

155159
let Some(contents) = &start.contents else {
156160
return Ok(None);
@@ -175,17 +179,22 @@ impl EpochStart {
175179
/// otherwise attempts to fetch from the store. The resulting value may still have an empty
176180
/// contents field, because it could not be found in the store, or the epoch started after the
177181
/// checkpoint being viewed.
178-
async fn fetch(&self, ctx: &Context<'_>, epoch_id: u64) -> Result<Self, RpcError> {
182+
async fn fetch(&self, ctx: &Context<'_>, epoch_id: Option<u64>) -> Result<Self, RpcError> {
179183
if self.contents.is_some() {
180184
return Ok(self.clone());
181185
}
182186

183187
let pg_loader: &Arc<DataLoader<PgReader>> = ctx.data()?;
184-
let Some(stored) = pg_loader
185-
.load_one(EpochStartKey(epoch_id))
186-
.await
187-
.context("Failed to fetch epoch start information")?
188-
else {
188+
189+
let load = if let Some(id) = epoch_id {
190+
pg_loader.load_one(EpochStartKey(id)).await
191+
} else {
192+
let cp = self.scope.checkpoint_viewed_at();
193+
pg_loader.load_one(CheckpointBoundedEpochStartKey(cp)).await
194+
}
195+
.context("Failed to fetch epoch start information")?;
196+
197+
let Some(stored) = load else {
189198
return Ok(self.clone());
190199
};
191200

crates/sui-indexer-alt-graphql/src/snapshots/sui_indexer_alt_graphql__tests__schema.graphql.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,11 +377,11 @@ type Query {
377377
"""
378378
checkpoint(sequenceNumber: UInt53): Checkpoint
379379
"""
380-
Fetch an epoch by its ID.
380+
Fetch an epoch by its ID, or fetch the latest epoch if no ID is provided.
381381

382382
Returns `null` if the epoch does not exist yet, or was pruned.
383383
"""
384-
epoch(epochId: UInt53!): Epoch
384+
epoch(epochId: UInt53): Epoch
385385
"""
386386
Fetch checkpoints by their sequence numbers.
387387

crates/sui-indexer-alt-graphql/src/snapshots/sui_indexer_alt_graphql__tests__staging.graphql.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,11 +377,11 @@ type Query {
377377
"""
378378
checkpoint(sequenceNumber: UInt53): Checkpoint
379379
"""
380-
Fetch an epoch by its ID.
380+
Fetch an epoch by its ID, or fetch the latest epoch if no ID is provided.
381381

382382
Returns `null` if the epoch does not exist yet, or was pruned.
383383
"""
384-
epoch(epochId: UInt53!): Epoch
384+
epoch(epochId: UInt53): Epoch
385385
"""
386386
Fetch checkpoints by their sequence numbers.
387387

crates/sui-indexer-alt-graphql/staging.graphql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,11 +373,11 @@ type Query {
373373
"""
374374
checkpoint(sequenceNumber: UInt53): Checkpoint
375375
"""
376-
Fetch an epoch by its ID.
376+
Fetch an epoch by its ID, or fetch the latest epoch if no ID is provided.
377377
378378
Returns `null` if the epoch does not exist yet, or was pruned.
379379
"""
380-
epoch(epochId: UInt53!): Epoch
380+
epoch(epochId: UInt53): Epoch
381381
"""
382382
Fetch checkpoints by their sequence numbers.
383383

crates/sui-indexer-alt-reader/src/epochs.rs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
// Copyright (c) Mysten Labs, Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
use std::{collections::HashMap, sync::Arc};
4+
use std::{
5+
collections::{BTreeMap, HashMap},
6+
sync::Arc,
7+
};
58

69
use async_graphql::dataloader::Loader;
7-
use diesel::{ExpressionMethods, QueryDsl};
10+
use diesel::{
11+
sql_types::{Array, BigInt},
12+
ExpressionMethods, QueryDsl,
13+
};
814
use sui_indexer_alt_schema::{
915
epochs::{StoredEpochEnd, StoredEpochStart},
1016
schema::{kv_epoch_ends, kv_epoch_starts},
@@ -16,6 +22,10 @@ use crate::{error::Error, pg_reader::PgReader};
1622
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1723
pub struct EpochStartKey(pub u64);
1824

25+
/// Key for fetching information about the latest epoch to have started as of a given checkpoint.
26+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27+
pub struct CheckpointBoundedEpochStartKey(pub u64);
28+
1929
/// Key for fetching information about the end of an epoch (which must already be finished).
2030
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2131
pub struct EpochEndKey(pub u64);
@@ -50,6 +60,69 @@ impl Loader<EpochStartKey> for PgReader {
5060
}
5161
}
5262

63+
#[async_trait::async_trait]
64+
impl Loader<CheckpointBoundedEpochStartKey> for PgReader {
65+
type Value = StoredEpochStart;
66+
type Error = Arc<Error>;
67+
68+
async fn load(
69+
&self,
70+
keys: &[CheckpointBoundedEpochStartKey],
71+
) -> Result<HashMap<CheckpointBoundedEpochStartKey, Self::Value>, Self::Error> {
72+
if keys.is_empty() {
73+
return Ok(HashMap::new());
74+
}
75+
76+
let mut conn = self.connect().await.map_err(Arc::new)?;
77+
78+
let cps: Vec<_> = keys.iter().map(|e| e.0 as i64).collect();
79+
let query = diesel::sql_query(
80+
r#"
81+
SELECT
82+
v.*
83+
FROM (
84+
SELECT UNNEST($1) cp_sequence_number
85+
) k
86+
CROSS JOIN LATERAL (
87+
SELECT
88+
epoch,
89+
protocol_version,
90+
cp_lo,
91+
start_timestamp_ms,
92+
reference_gas_price,
93+
system_state
94+
FROM
95+
kv_epoch_starts
96+
WHERE
97+
kv_epoch_starts.cp_lo <= k.cp_sequence_number
98+
ORDER BY
99+
kv_epoch_starts.cp_lo DESC
100+
LIMIT
101+
1
102+
) v
103+
"#,
104+
)
105+
.bind::<Array<BigInt>, _>(cps);
106+
107+
let stored_epochs: Vec<StoredEpochStart> = conn.results(query).await.map_err(Arc::new)?;
108+
109+
// A single data loader request may contain multiple keys for the same epoch. Store them in
110+
// an ordered map, so that we can find the latest version for each key.
111+
let cp_to_stored: BTreeMap<_, _> = stored_epochs
112+
.into_iter()
113+
.map(|epoch| (epoch.cp_lo as u64, epoch))
114+
.collect();
115+
116+
Ok(keys
117+
.iter()
118+
.filter_map(|key| {
119+
let stored = cp_to_stored.range(..=key.0).last()?.1;
120+
Some((*key, stored.clone()))
121+
})
122+
.collect())
123+
}
124+
}
125+
53126
#[async_trait::async_trait]
54127
impl Loader<EpochEndKey> for PgReader {
55128
type Value = StoredEpochEnd;

crates/sui-indexer-alt-schema/src/epochs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub struct StoredEpochEnd {
2727
pub epoch_commitments: Vec<u8>,
2828
}
2929

30-
#[derive(Insertable, Queryable, Debug, Clone, FieldCount)]
30+
#[derive(Insertable, Queryable, QueryableByName, Debug, Clone, FieldCount)]
3131
#[diesel(table_name = kv_epoch_starts)]
3232
pub struct StoredEpochStart {
3333
pub epoch: i64,

0 commit comments

Comments
 (0)