Skip to main content

sui_indexer_alt_reader/
object_versions.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::BTreeMap;
5use std::collections::HashMap;
6
7use anyhow::Context as _;
8use async_graphql::dataloader::Loader;
9use diesel::sql_types::Array;
10use diesel::sql_types::BigInt;
11use diesel::sql_types::Bytea;
12use sui_indexer_alt_schema::objects::StoredObjVersion;
13use sui_types::base_types::ObjectID;
14
15use crate::error::Error;
16use crate::pg_reader::PgReader;
17
18/// Key for fetching the latest version of an object. If the object has been deleted or wrapped,
19/// the latest version will return the version it was deleted/wrapped at.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct LatestObjectVersionKey(pub ObjectID);
22
23/// Key for fetching the latest version of an object, with an inclusive version upperbound.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
25pub struct VersionBoundedObjectVersionKey(pub ObjectID, pub u64);
26
27/// Key for fetching the latest version of an object, as of a given checkpoint.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
29pub struct CheckpointBoundedObjectVersionKey(pub ObjectID, pub u64);
30
31/// Key for fetching a [StoredObjVersion] by its ID and version (used to determine the checkpoint
32/// this object version was modified in).
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct VersionedObjectVersionKey(pub ObjectID, pub u64);
35
36#[async_trait::async_trait]
37impl Loader<LatestObjectVersionKey> for PgReader {
38    type Value = StoredObjVersion;
39    type Error = Error;
40
41    async fn load(
42        &self,
43        keys: &[LatestObjectVersionKey],
44    ) -> Result<HashMap<LatestObjectVersionKey, StoredObjVersion>, Error> {
45        if keys.is_empty() {
46            return Ok(HashMap::new());
47        }
48
49        let mut conn = self.connect().await?;
50
51        let ids: Vec<_> = keys.iter().map(|k| k.0.into_bytes()).collect();
52        let query = diesel::sql_query(
53            r#"
54                SELECT
55                    k.object_id,
56                    v.object_version,
57                    v.object_digest,
58                    v.cp_sequence_number
59                FROM (
60                    SELECT UNNEST($1) object_id
61                ) k
62                CROSS JOIN LATERAL (
63                    SELECT
64                        object_version,
65                        object_digest,
66                        cp_sequence_number
67                    FROM
68                        obj_versions
69                    WHERE
70                        obj_versions.object_id = k.object_id
71                    ORDER BY
72                        object_version DESC
73                    LIMIT
74                        1
75                ) v
76            "#,
77        )
78        .bind::<Array<Bytea>, _>(ids);
79
80        let obj_versions: Vec<StoredObjVersion> = conn.results(query).await?;
81        let id_to_stored: HashMap<_, _> = obj_versions
82            .into_iter()
83            .map(|stored| (stored.object_id.clone(), stored))
84            .collect();
85
86        Ok(keys
87            .iter()
88            .filter_map(|key| {
89                let slice: &[u8] = key.0.as_ref();
90                Some((*key, id_to_stored.get(slice).cloned()?))
91            })
92            .collect())
93    }
94}
95
96#[async_trait::async_trait]
97impl Loader<VersionBoundedObjectVersionKey> for PgReader {
98    type Value = StoredObjVersion;
99    type Error = Error;
100
101    async fn load(
102        &self,
103        keys: &[VersionBoundedObjectVersionKey],
104    ) -> Result<HashMap<VersionBoundedObjectVersionKey, StoredObjVersion>, Error> {
105        if keys.is_empty() {
106            return Ok(HashMap::new());
107        }
108
109        let mut conn = self.connect().await?;
110
111        let ids: Vec<_> = keys.iter().map(|k| k.0.into_bytes()).collect();
112        let versions: Vec<_> = keys.iter().map(|k| k.1 as i64).collect();
113        let query = diesel::sql_query(
114            r#"
115                SELECT
116                    k.object_id,
117                    v.object_version,
118                    v.object_digest,
119                    v.cp_sequence_number
120                FROM (
121                    SELECT
122                        UNNEST($1) object_id,
123                        UNNEST($2) object_version
124                ) k
125                CROSS JOIN LATERAL (
126                    SELECT
127                        object_version,
128                        object_digest,
129                        cp_sequence_number
130                    FROM
131                        obj_versions
132                    WHERE
133                        obj_versions.object_id = k.object_id
134                    AND obj_versions.object_version <= k.object_version
135                    ORDER BY
136                        object_version DESC
137                    LIMIT
138                        1
139                ) v
140            "#,
141        )
142        .bind::<Array<Bytea>, _>(ids)
143        .bind::<Array<BigInt>, _>(versions);
144
145        let obj_versions: Vec<StoredObjVersion> = conn.results(query).await?;
146
147        // A single data loader request may contain multiple keys for the same object ID. Store
148        // them in an ordered map, so that we can find the latest version for each key.
149        let mut key_to_stored = BTreeMap::new();
150        for obj_version in obj_versions {
151            let id = ObjectID::from_bytes(&obj_version.object_id)
152                .context("Failed to deserialize ObjectID")?;
153
154            let version = obj_version.object_version as u64;
155
156            key_to_stored.insert(VersionBoundedObjectVersionKey(id, version), obj_version);
157        }
158
159        Ok(keys
160            .iter()
161            .filter_map(|key| {
162                let (bound, stored) = key_to_stored.range(..=key).last()?;
163                (key.0 == bound.0).then(|| (*key, stored.clone()))
164            })
165            .collect())
166    }
167}
168
169#[async_trait::async_trait]
170impl Loader<CheckpointBoundedObjectVersionKey> for PgReader {
171    type Value = StoredObjVersion;
172    type Error = Error;
173
174    async fn load(
175        &self,
176        keys: &[CheckpointBoundedObjectVersionKey],
177    ) -> Result<HashMap<CheckpointBoundedObjectVersionKey, StoredObjVersion>, Error> {
178        if keys.is_empty() {
179            return Ok(HashMap::new());
180        }
181
182        let mut conn = self.connect().await?;
183
184        let ids: Vec<_> = keys.iter().map(|k| k.0.into_bytes()).collect();
185        let cps: Vec<_> = keys.iter().map(|k| k.1 as i64).collect();
186        let query = diesel::sql_query(
187            r#"
188                SELECT
189                    k.object_id,
190                    v.object_version,
191                    v.object_digest,
192                    v.cp_sequence_number
193                FROM (
194                    SELECT
195                        UNNEST($1) object_id,
196                        UNNEST($2) cp_sequence_number
197                ) k
198                CROSS JOIN LATERAL (
199                    SELECT
200                        object_version,
201                        object_digest,
202                        cp_sequence_number
203                    FROM
204                        obj_versions
205                    WHERE
206                        obj_versions.object_id = k.object_id
207                    AND obj_versions.cp_sequence_number <= k.cp_sequence_number
208                    ORDER BY
209                        cp_sequence_number DESC,
210                        object_version DESC
211                    LIMIT
212                        1
213                ) v
214            "#,
215        )
216        .bind::<Array<Bytea>, _>(ids)
217        .bind::<Array<BigInt>, _>(cps);
218
219        let obj_versions: Vec<StoredObjVersion> = conn.results(query).await?;
220
221        // A single data loader request may contain multiple keys for the same object ID. Store
222        // them in an ordered map, so that we can find the latest version for each key.
223        let mut key_to_stored = BTreeMap::new();
224        for obj_version in obj_versions {
225            let id = ObjectID::from_bytes(&obj_version.object_id)
226                .context("Failed to deserialize ObjectID")?;
227
228            let cp_sequence_number = obj_version.cp_sequence_number as u64;
229
230            key_to_stored.insert(
231                CheckpointBoundedObjectVersionKey(id, cp_sequence_number),
232                obj_version,
233            );
234        }
235
236        Ok(keys
237            .iter()
238            .filter_map(|key| {
239                let (bound, stored) = key_to_stored.range(..=key).last()?;
240                (key.0 == bound.0).then(|| (*key, stored.clone()))
241            })
242            .collect())
243    }
244}
245
246#[async_trait::async_trait]
247impl Loader<VersionedObjectVersionKey> for PgReader {
248    type Value = StoredObjVersion;
249    type Error = Error;
250
251    async fn load(
252        &self,
253        keys: &[VersionedObjectVersionKey],
254    ) -> Result<HashMap<VersionedObjectVersionKey, StoredObjVersion>, Error> {
255        if keys.is_empty() {
256            return Ok(HashMap::new());
257        }
258
259        let mut conn = self.connect().await?;
260
261        let ids: Vec<_> = keys.iter().map(|k| k.0.into_bytes()).collect();
262        let versions: Vec<_> = keys.iter().map(|k| k.1 as i64).collect();
263        let query = diesel::sql_query(
264            r#"
265                SELECT
266                    k.object_id,
267                    v.object_version,
268                    v.object_digest,
269                    v.cp_sequence_number
270                FROM (
271                    SELECT
272                        UNNEST($1) object_id,
273                        UNNEST($2) object_version
274                ) k
275                CROSS JOIN LATERAL (
276                    SELECT
277                        object_version,
278                        object_digest,
279                        cp_sequence_number
280                    FROM
281                        obj_versions
282                    WHERE
283                        obj_versions.object_id = k.object_id
284                    AND obj_versions.object_version = k.object_version
285                    LIMIT
286                        1
287                ) v
288            "#,
289        )
290        .bind::<Array<Bytea>, _>(ids)
291        .bind::<Array<BigInt>, _>(versions);
292
293        let obj_versions: Vec<StoredObjVersion> = conn.results(query).await?;
294        let key_to_stored: HashMap<_, _> = obj_versions
295            .iter()
296            .map(|stored| {
297                let id = &stored.object_id[..];
298                let version = stored.object_version as u64;
299                ((id, version), stored)
300            })
301            .collect();
302
303        Ok(keys
304            .iter()
305            .filter_map(|key| {
306                let slice: &[u8] = key.0.as_ref();
307                let stored = *key_to_stored.get(&(slice, key.1))?;
308                Some((*key, stored.clone()))
309            })
310            .collect())
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use async_graphql::dataloader::Loader;
317    use diesel_async::RunQueryDsl as _;
318    use prometheus::Registry;
319    use sui_indexer_alt_schema::MIGRATIONS;
320    use sui_indexer_alt_schema::schema::obj_versions;
321    use sui_pg_db::Db;
322    use sui_pg_db::DbArgs;
323    use sui_pg_db::temp::TempDb;
324    use sui_types::digests::ObjectDigest;
325
326    use super::*;
327
328    /// Create a temporary database, and return a connection pool that can write to it, and a
329    /// reader to it.
330    async fn setup() -> (TempDb, Db, PgReader) {
331        let registry = Registry::new();
332        let temp_db = TempDb::new().unwrap();
333        let url = temp_db.database().url();
334
335        let writer = Db::for_write(url.clone(), DbArgs::default()).await.unwrap();
336        let reader = PgReader::new(None, Some(url.clone()), DbArgs::default(), &registry)
337            .await
338            .unwrap();
339
340        writer.run_migrations(Some(&MIGRATIONS)).await.unwrap();
341        (temp_db, writer, reader)
342    }
343
344    fn stored(id: ObjectID, d: ObjectDigest, v: u64, cp: u64) -> StoredObjVersion {
345        StoredObjVersion {
346            object_id: id.into_bytes().to_vec(),
347            object_version: v as i64,
348            object_digest: Some(d.into_inner().to_vec()),
349            cp_sequence_number: cp as i64,
350        }
351    }
352
353    #[tokio::test]
354    async fn test_version_bounded() {
355        use obj_versions::dsl as v;
356
357        let (_temp_db, writer, reader) = setup().await;
358
359        let o0 = ObjectID::random();
360        let d0 = ObjectDigest::random();
361
362        let o1 = ObjectID::random();
363        let d1 = ObjectDigest::random();
364
365        let o2 = ObjectID::random();
366
367        {
368            // Set-up the table with a couple of records.
369            let mut conn = writer.connect().await.unwrap();
370
371            diesel::insert_into(v::obj_versions)
372                .values(vec![
373                    stored(o0, d0, 1, 1),
374                    stored(o1, d1, 2, 1),
375                    stored(o0, d0, 2, 2),
376                    stored(o0, d0, 4, 2),
377                ])
378                .execute(&mut conn)
379                .await
380                .unwrap();
381        }
382
383        use VersionBoundedObjectVersionKey as K;
384
385        // Exact match on the first version of the object.
386        assert_eq!(
387            Loader::load(&reader, &[K(o0, 1)]).await.unwrap(),
388            HashMap::from_iter([(K(o0, 1), stored(o0, d0, 1, 1))]),
389        );
390
391        // Exact match on the last version of the object.
392        assert_eq!(
393            Loader::load(&reader, &[K(o0, 4)]).await.unwrap(),
394            HashMap::from_iter([(K(o0, 4), stored(o0, d0, 4, 2))]),
395        );
396
397        // Inexact match on the middle version of the object.
398        assert_eq!(
399            Loader::load(&reader, &[K(o0, 3)]).await.unwrap(),
400            HashMap::from_iter([(K(o0, 3), stored(o0, d0, 2, 2))]),
401        );
402
403        // Inexact match on the last version of the object.
404        assert_eq!(
405            Loader::load(&reader, &[K(o0, 100)]).await.unwrap(),
406            HashMap::from_iter([(K(o0, 100), stored(o0, d0, 4, 2))]),
407        );
408
409        // No matching object version.
410        assert_eq!(
411            Loader::load(&reader, &[K(o1, 1)]).await.unwrap(),
412            HashMap::new(),
413        );
414
415        // No matching object.
416        assert_eq!(
417            Loader::load(&reader, &[K(o2, 1)]).await.unwrap(),
418            HashMap::new(),
419        );
420
421        // Multiple requests that map to the same record.
422        assert_eq!(
423            Loader::load(&reader, &[K(o0, 2), K(o0, 3)]).await.unwrap(),
424            HashMap::from_iter([
425                (K(o0, 2), stored(o0, d0, 2, 2)),
426                (K(o0, 3), stored(o0, d0, 2, 2))
427            ]),
428        );
429
430        // Multiple requests, one of them not matching.
431        assert_eq!(
432            Loader::load(&reader, &[K(o0, 1), K(o1, 1)]).await.unwrap(),
433            HashMap::from_iter([(K(o0, 1), stored(o0, d0, 1, 1))]),
434        );
435
436        // Same again, but with ObjectIDs swapped.
437        assert_eq!(
438            Loader::load(&reader, &[K(o0, 0), K(o1, 2)]).await.unwrap(),
439            HashMap::from_iter([(K(o1, 2), stored(o1, d1, 2, 1))]),
440        );
441
442        // All the requests in one.
443        assert_eq!(
444            Loader::load(
445                &reader,
446                &[
447                    K(o0, 0),
448                    K(o0, 1),
449                    K(o0, 2),
450                    K(o0, 3),
451                    K(o0, 4),
452                    K(o0, 5),
453                    K(o1, 1),
454                    K(o1, 2),
455                    K(o2, 1),
456                ]
457            )
458            .await
459            .unwrap(),
460            HashMap::from_iter([
461                (K(o0, 1), stored(o0, d0, 1, 1)),
462                (K(o0, 2), stored(o0, d0, 2, 2)),
463                (K(o0, 3), stored(o0, d0, 2, 2)),
464                (K(o0, 4), stored(o0, d0, 4, 2)),
465                (K(o0, 5), stored(o0, d0, 4, 2)),
466                (K(o1, 2), stored(o1, d1, 2, 1)),
467            ])
468        );
469    }
470
471    #[tokio::test]
472    async fn test_checkpoint_bounded() {
473        use obj_versions::dsl as v;
474
475        let (_temp_db, writer, reader) = setup().await;
476
477        let o0 = ObjectID::random();
478        let d0 = ObjectDigest::random();
479
480        let o1 = ObjectID::random();
481        let d1 = ObjectDigest::random();
482
483        let o2 = ObjectID::random();
484
485        {
486            // Set-up the table with a couple of records.
487            let mut conn = writer.connect().await.unwrap();
488
489            diesel::insert_into(v::obj_versions)
490                .values(vec![
491                    stored(o0, d0, 1, 1),
492                    stored(o1, d1, 2, 1),
493                    stored(o0, d0, 2, 2),
494                    stored(o0, d0, 4, 2),
495                    stored(o1, d1, 3, 3),
496                ])
497                .execute(&mut conn)
498                .await
499                .unwrap();
500        }
501
502        use CheckpointBoundedObjectVersionKey as K;
503
504        // Exact match on the first checkpoint including an object.
505        assert_eq!(
506            Loader::load(&reader, &[K(o0, 1)]).await.unwrap(),
507            HashMap::from_iter([(K(o0, 1), stored(o0, d0, 1, 1))]),
508        );
509
510        // Exact match on the last checkpoint including an object.
511        assert_eq!(
512            Loader::load(&reader, &[K(o0, 2)]).await.unwrap(),
513            HashMap::from_iter([(K(o0, 2), stored(o0, d0, 4, 2))]),
514        );
515
516        // Inexact match on the first checkpoint including an object.
517        assert_eq!(
518            Loader::load(&reader, &[K(o1, 2)]).await.unwrap(),
519            HashMap::from_iter([(K(o1, 2), stored(o1, d1, 2, 1))]),
520        );
521
522        // Inexact match on the last checkpoint including an object.
523        assert_eq!(
524            Loader::load(&reader, &[K(o0, 3)]).await.unwrap(),
525            HashMap::from_iter([(K(o0, 3), stored(o0, d0, 4, 2))]),
526        );
527
528        // No matching checkpoint.
529        assert_eq!(
530            Loader::load(&reader, &[K(o1, 0)]).await.unwrap(),
531            HashMap::new(),
532        );
533
534        // No matching object.
535        assert_eq!(
536            Loader::load(&reader, &[K(o2, 1)]).await.unwrap(),
537            HashMap::new(),
538        );
539
540        // Multiple requests that map to the same record.
541        assert_eq!(
542            Loader::load(&reader, &[K(o0, 2), K(o0, 3)]).await.unwrap(),
543            HashMap::from_iter([
544                (K(o0, 2), stored(o0, d0, 4, 2)),
545                (K(o0, 3), stored(o0, d0, 4, 2)),
546            ])
547        );
548
549        // Multiple requests, one of them not matching.
550        assert_eq!(
551            Loader::load(&reader, &[K(o0, 1), K(o1, 0)]).await.unwrap(),
552            HashMap::from_iter([(K(o0, 1), stored(o0, d0, 1, 1))])
553        );
554
555        // Same again, but with ObjectIDs swapped.
556        assert_eq!(
557            Loader::load(&reader, &[K(o0, 0), K(o1, 1)]).await.unwrap(),
558            HashMap::from_iter([(K(o1, 1), stored(o1, d1, 2, 1))])
559        );
560
561        // All the requests in one.
562        assert_eq!(
563            Loader::load(
564                &reader,
565                &[
566                    K(o0, 0),
567                    K(o0, 1),
568                    K(o0, 2),
569                    K(o0, 3),
570                    K(o1, 0),
571                    K(o1, 1),
572                    K(o1, 2),
573                    K(o2, 1),
574                ]
575            )
576            .await
577            .unwrap(),
578            HashMap::from_iter([
579                (K(o0, 1), stored(o0, d0, 1, 1)),
580                (K(o0, 2), stored(o0, d0, 4, 2)),
581                (K(o0, 3), stored(o0, d0, 4, 2)),
582                (K(o1, 1), stored(o1, d1, 2, 1)),
583                (K(o1, 2), stored(o1, d1, 2, 1)),
584            ])
585        );
586    }
587}