1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct LatestObjectVersionKey(pub ObjectID);
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
25pub struct VersionBoundedObjectVersionKey(pub ObjectID, pub u64);
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
29pub struct CheckpointBoundedObjectVersionKey(pub ObjectID, pub u64);
30
31#[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 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 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 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(), ®istry)
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 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 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 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 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 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 assert_eq!(
411 Loader::load(&reader, &[K(o1, 1)]).await.unwrap(),
412 HashMap::new(),
413 );
414
415 assert_eq!(
417 Loader::load(&reader, &[K(o2, 1)]).await.unwrap(),
418 HashMap::new(),
419 );
420
421 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 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 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 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 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 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 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 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 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 assert_eq!(
530 Loader::load(&reader, &[K(o1, 0)]).await.unwrap(),
531 HashMap::new(),
532 );
533
534 assert_eq!(
536 Loader::load(&reader, &[K(o2, 1)]).await.unwrap(),
537 HashMap::new(),
538 );
539
540 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 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 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 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}