1use std::{
6 collections::{BTreeMap, BTreeSet, HashMap},
7 fmt::Write,
8};
9
10use async_graphql::{
11 connection::{Connection, CursorType, Edge},
12 dataloader::Loader,
13 *,
14};
15use diesel::{BoolExpressionMethods, ExpressionMethods, QueryDsl, SelectableHelper, sql_types};
16use iota_indexer::{
17 ingestion::common::CommitterTables,
18 models::objects::{StoredHistoryObject, StoredObject},
19 schema::objects,
20 types::{ObjectStatus as NativeObjectStatus, OwnerType},
21};
22use iota_sdk_types::{
23 MoveStruct as NativeMoveStruct, ObjectId, Owner as NativeOwner, StructTag, TypeTag, Version,
24};
25use iota_types::object::{Object as NativeObject, bounded_visitor::BoundedVisitor};
26use move_core_types::annotated_value::{MoveStruct, MoveTypeLayout};
27use serde::{Deserialize, Serialize};
28
29use crate::{
30 backward_view::{HistoricalFilter, consistent, historical},
31 config::DEFAULT_PAGE_SIZE,
32 connection::ScanConnection,
33 consistency::Checkpointed,
34 data::{DataLoader, Db, DbConnection, QueryExecutor, package_resolver::PackageResolver},
35 error::Error,
36 filter, or_filter,
37 raw_query::RawQuery,
38 types::{
39 available_range::AvailableRange,
40 balance::{self, Balance},
41 base64::Base64,
42 big_int::BigInt,
43 coin::Coin,
44 coin_metadata::CoinMetadata,
45 cursor::{self, Page, RawPaginated, ScanLimited, Target},
46 digest::Digest,
47 display::{Display, DisplayEntry},
48 dynamic_field::{DynamicField, DynamicFieldName},
49 intersect,
50 iota_address::{IotaAddress, addr},
51 iota_names_registration::{NameFormat, NameRegistration},
52 move_object::MoveObject,
53 move_package::MovePackage,
54 owner::{Owner, OwnerImpl},
55 stake::StakedIota,
56 transaction_block,
57 transaction_block::{TransactionBlock, TransactionBlockFilter},
58 type_filter::{ExactTypeFilter, TypeFilter},
59 uint53::UInt53,
60 },
61};
62
63#[derive(Clone, Debug)]
64pub(crate) struct Object {
65 pub address: IotaAddress,
66 pub inner: ActiveObject,
67 pub checkpoint_viewed_at: u64,
69 root_version: u64,
83}
84
85pub(crate) struct ObjectImpl<'o>(pub &'o Object);
87
88#[derive(Clone, Debug)]
89pub(crate) struct ActiveObject {
90 native: NativeObject,
92 status: ObjectStatus,
94 bcs: Option<Vec<u8>>,
99}
100
101#[derive(Enum, Copy, Clone, Eq, PartialEq, Debug)]
102#[graphql(name = "ObjectKind")]
103pub enum ObjectStatus {
104 NotIndexed,
107 Indexed,
109}
110
111#[derive(Clone, Debug, PartialEq, Eq, InputObject)]
112pub(crate) struct ObjectRef {
113 pub address: IotaAddress,
115 pub version: UInt53,
117 pub digest: Digest,
119}
120
121#[derive(InputObject, Default, Debug, Clone, Eq, PartialEq)]
129pub(crate) struct ObjectFilter {
130 pub type_: Option<TypeFilter>,
137
138 pub owner: Option<IotaAddress>,
140
141 pub object_ids: Option<Vec<IotaAddress>>,
143
144 pub object_keys: Option<Vec<ObjectKey>>,
147}
148
149#[derive(InputObject, Debug, Clone, Eq, PartialEq)]
150pub(crate) struct ObjectKey {
151 pub object_id: IotaAddress,
152 pub version: UInt53,
153}
154
155#[derive(Union, Clone)]
157pub(crate) enum ObjectOwner {
158 Immutable(Immutable),
159 Shared(Shared),
160 Parent(Box<Parent>),
161 Address(AddressOwner),
162}
163
164#[derive(SimpleObject, Clone)]
167pub(crate) struct Immutable {
168 #[graphql(name = "_")]
169 dummy: Option<bool>,
170}
171
172#[derive(SimpleObject, Clone)]
176pub(crate) struct Shared {
177 initial_shared_version: UInt53,
178}
179
180#[derive(SimpleObject, Clone)]
185pub(crate) struct Parent {
186 parent: Option<Object>,
187}
188
189#[derive(SimpleObject, Clone)]
194pub(crate) struct AddressOwner {
195 owner: Option<Owner>,
196}
197
198pub(crate) enum ObjectLookup {
200 LatestAt {
201 checkpoint_viewed_at: u64,
203 },
204
205 UnderParent {
206 parent_version: u64,
210 checkpoint_viewed_at: u64,
212 },
213
214 VersionAt {
215 version: u64,
217 checkpoint_viewed_at: u64,
219 },
220
221 OptimisticVersion {
225 version: u64,
227 },
228}
229
230pub(crate) type Cursor = cursor::BcsCursor<HistoricalObjectCursor>;
231
232#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
236pub(crate) struct HistoricalObjectCursor {
237 #[serde(rename = "o")]
238 object_id: Vec<u8>,
239 #[serde(rename = "c")]
241 checkpoint_viewed_at: u64,
242}
243
244#[expect(clippy::duplicated_attributes)]
247#[derive(Interface)]
248#[graphql(
249 name = "IObject",
250 field(name = "version", ty = "UInt53"),
251 field(
252 name = "status",
253 ty = "ObjectStatus",
254 desc = r#"
255 The current status of the object as read from the off-chain store. The
256 possible states are:
257 - NOT_INDEXED: The object is loaded from serialized data, such as the
258 contents of a genesis or system package upgrade transaction.
259 - INDEXED: The object is retrieved from the off-chain index and
260 represents the most recent or historical state of the object.
261 "#
262 ),
263 field(
264 name = "digest",
265 ty = "Option<String>",
266 desc = "32-byte hash that identifies the object's current contents, encoded as a Base58 \
267 string."
268 ),
269 field(
270 name = "owner",
271 ty = "Option<ObjectOwner>",
272 desc = "The owner type of this object: Immutable, Shared, Parent, Address\n\
273 Immutable and Shared Objects do not have owners."
274 ),
275 field(
276 name = "previous_transaction_block",
277 ty = "Option<TransactionBlock>",
278 desc = "The transaction block that created this version of the object."
279 ),
280 field(name = "storage_rebate", ty = "Option<BigInt>", desc = "",),
281 field(
282 name = "received_transaction_blocks",
283 arg(name = "first", ty = "Option<u64>"),
284 arg(name = "after", ty = "Option<transaction_block::Cursor>"),
285 arg(name = "last", ty = "Option<u64>"),
286 arg(name = "before", ty = "Option<transaction_block::Cursor>"),
287 arg(name = "filter", ty = "Option<TransactionBlockFilter>"),
288 arg(
289 name = "scan_limit",
290 ty = "Option<u64>",
291 deprecation = "`scanLimit` will be removed with v1.38, along with the support for combining complex filters."
292 ),
293 ty = "ScanConnection<String, TransactionBlock>",
294 desc = "The transaction blocks that sent objects to this object."
295 ),
296 field(
297 name = "bcs",
298 ty = "Option<Base64>",
299 desc = "The Base64-encoded BCS serialization of the object's content."
300 )
301)]
302pub(crate) enum IObject {
303 Object(Object),
304 MovePackage(MovePackage),
305 MoveObject(MoveObject),
306 Coin(Coin),
307 CoinMetadata(CoinMetadata),
308 StakedIota(StakedIota),
309}
310
311#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
315struct HistoricalKey {
316 id: IotaAddress,
317 version: u64,
318 checkpoint_viewed_at: u64,
319}
320
321#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
325struct OptimisticKey {
326 id: IotaAddress,
327 version: u64,
328}
329
330#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
336struct ParentVersionKey {
337 id: IotaAddress,
338 parent_version: u64,
339 checkpoint_viewed_at: u64,
340}
341
342#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
345struct LatestAtKey {
346 id: IotaAddress,
347 checkpoint_viewed_at: u64,
348}
349
350#[Object]
355impl Object {
356 pub(crate) async fn address(&self) -> IotaAddress {
357 OwnerImpl::from(self).address().await
358 }
359
360 pub(crate) async fn objects(
362 &self,
363 ctx: &Context<'_>,
364 first: Option<u64>,
365 after: Option<Cursor>,
366 last: Option<u64>,
367 before: Option<Cursor>,
368 filter: Option<ObjectFilter>,
369 ) -> Result<Connection<String, MoveObject>> {
370 OwnerImpl::from(self)
371 .objects(ctx, first, after, last, before, filter)
372 .await
373 }
374
375 pub(crate) async fn balance(
378 &self,
379 ctx: &Context<'_>,
380 type_: Option<ExactTypeFilter>,
381 ) -> Result<Option<Balance>> {
382 OwnerImpl::from(self).balance(ctx, type_).await
383 }
384
385 pub(crate) async fn balances(
387 &self,
388 ctx: &Context<'_>,
389 first: Option<u64>,
390 after: Option<balance::Cursor>,
391 last: Option<u64>,
392 before: Option<balance::Cursor>,
393 ) -> Result<Connection<String, Balance>> {
394 OwnerImpl::from(self)
395 .balances(ctx, first, after, last, before)
396 .await
397 }
398
399 pub(crate) async fn coins(
404 &self,
405 ctx: &Context<'_>,
406 first: Option<u64>,
407 after: Option<Cursor>,
408 last: Option<u64>,
409 before: Option<Cursor>,
410 type_: Option<ExactTypeFilter>,
411 ) -> Result<Connection<String, Coin>> {
412 OwnerImpl::from(self)
413 .coins(ctx, first, after, last, before, type_)
414 .await
415 }
416
417 pub(crate) async fn staked_iotas(
419 &self,
420 ctx: &Context<'_>,
421 first: Option<u64>,
422 after: Option<Cursor>,
423 last: Option<u64>,
424 before: Option<Cursor>,
425 ) -> Result<Connection<String, StakedIota>> {
426 OwnerImpl::from(self)
427 .staked_iotas(ctx, first, after, last, before)
428 .await
429 }
430
431 pub(crate) async fn iota_names_default_name(
434 &self,
435 ctx: &Context<'_>,
436 format: Option<NameFormat>,
437 ) -> Result<Option<String>> {
438 OwnerImpl::from(self)
439 .iota_names_default_name(ctx, format)
440 .await
441 }
442
443 pub(crate) async fn iota_names_registrations(
446 &self,
447 ctx: &Context<'_>,
448 first: Option<u64>,
449 after: Option<Cursor>,
450 last: Option<u64>,
451 before: Option<Cursor>,
452 ) -> Result<Connection<String, NameRegistration>> {
453 OwnerImpl::from(self)
454 .iota_names_registrations(ctx, first, after, last, before)
455 .await
456 }
457
458 pub(crate) async fn version(&self) -> UInt53 {
459 ObjectImpl(self).version().await
460 }
461
462 pub(crate) async fn status(&self) -> ObjectStatus {
469 ObjectImpl(self).status().await
470 }
471
472 pub(crate) async fn digest(&self) -> Option<String> {
475 ObjectImpl(self).digest().await
476 }
477
478 pub(crate) async fn owner(&self, ctx: &Context<'_>) -> Option<ObjectOwner> {
481 ObjectImpl(self).owner(ctx).await
482 }
483
484 pub(crate) async fn previous_transaction_block(
486 &self,
487 ctx: &Context<'_>,
488 ) -> Result<Option<TransactionBlock>> {
489 ObjectImpl(self).previous_transaction_block(ctx).await
490 }
491
492 pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
496 ObjectImpl(self).storage_rebate().await
497 }
498
499 #[graphql(
530 complexity = "first.or(last).unwrap_or(DEFAULT_PAGE_SIZE as u64) as usize * child_complexity"
531 )]
532 pub(crate) async fn received_transaction_blocks(
533 &self,
534 ctx: &Context<'_>,
535 first: Option<u64>,
536 after: Option<transaction_block::Cursor>,
537 last: Option<u64>,
538 before: Option<transaction_block::Cursor>,
539 filter: Option<TransactionBlockFilter>,
540 #[graphql(
541 deprecation = "`scanLimit` will be removed with v1.38, along with the support for combining complex filters."
542 )]
543 scan_limit: Option<u64>,
544 ) -> Result<ScanConnection<String, TransactionBlock>> {
545 ObjectImpl(self)
546 .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit)
547 .await
548 }
549
550 pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
552 ObjectImpl(self).bcs().await
553 }
554
555 async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
559 ObjectImpl(self).display(ctx).await
560 }
561
562 async fn dynamic_field(
569 &self,
570 ctx: &Context<'_>,
571 name: DynamicFieldName,
572 ) -> Result<Option<DynamicField>> {
573 OwnerImpl::from(self)
574 .dynamic_field(ctx, name, Some(self.root_version()))
575 .await
576 }
577
578 async fn dynamic_object_field(
587 &self,
588 ctx: &Context<'_>,
589 name: DynamicFieldName,
590 ) -> Result<Option<DynamicField>> {
591 OwnerImpl::from(self)
592 .dynamic_object_field(ctx, name, Some(self.root_version()))
593 .await
594 }
595
596 async fn dynamic_fields(
601 &self,
602 ctx: &Context<'_>,
603 first: Option<u64>,
604 after: Option<Cursor>,
605 last: Option<u64>,
606 before: Option<Cursor>,
607 ) -> Result<Connection<String, DynamicField>> {
608 OwnerImpl::from(self)
609 .dynamic_fields(ctx, first, after, last, before, Some(self.root_version()))
610 .await
611 }
612
613 async fn as_move_object(&self) -> Option<MoveObject> {
615 MoveObject::try_from(self).ok()
616 }
617
618 async fn as_move_package(&self) -> Option<MovePackage> {
620 MovePackage::try_from(self).ok()
621 }
622}
623
624impl ObjectImpl<'_> {
625 pub(crate) async fn version(&self) -> UInt53 {
626 self.0.version_impl().into()
627 }
628
629 pub(crate) async fn status(&self) -> ObjectStatus {
630 ObjectStatus::from(&self.0.inner)
631 }
632
633 pub(crate) async fn digest(&self) -> Option<String> {
634 Some(self.0.native_impl().digest().to_base58())
635 }
636
637 pub(crate) async fn owner(&self, ctx: &Context<'_>) -> Option<ObjectOwner> {
638 use NativeOwner as O;
639
640 let native = self.0.native_impl();
641
642 match native.owner {
643 O::Address(address) => {
644 let address = IotaAddress::from(address);
645 Some(ObjectOwner::Address(AddressOwner {
646 owner: Some(Owner {
647 address,
648 checkpoint_viewed_at: self.0.checkpoint_viewed_at,
649 root_version: None,
650 }),
651 }))
652 }
653 O::Immutable => Some(ObjectOwner::Immutable(Immutable { dummy: None })),
654 O::Object(address) => {
655 let parent = Object::query(
656 ctx,
657 address.into(),
658 Object::under_parent(self.0.root_version, self.0.checkpoint_viewed_at),
659 )
660 .await
661 .ok()
662 .flatten();
663
664 Some(ObjectOwner::Parent(Box::new(Parent { parent })))
665 }
666 O::Shared(initial_shared_version) => Some(ObjectOwner::Shared(Shared {
667 initial_shared_version: initial_shared_version.as_u64().into(),
668 })),
669 _ => unimplemented!("a new Owner enum variant was added and needs to be handled"),
670 }
671 }
672
673 pub(crate) async fn previous_transaction_block(
674 &self,
675 ctx: &Context<'_>,
676 ) -> Result<Option<TransactionBlock>> {
677 let native = self.0.native_impl();
678 let digest = native.previous_transaction;
679 let key = transaction_block::DigestKey::new(digest.into(), self.0.checkpoint_viewed_at);
680
681 TransactionBlock::query(ctx, key).await.extend()
682 }
683
684 pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
685 Some(self.0.native_impl().storage_rebate.into())
686 }
687
688 pub(crate) async fn received_transaction_blocks(
689 &self,
690 ctx: &Context<'_>,
691 first: Option<u64>,
692 after: Option<transaction_block::Cursor>,
693 last: Option<u64>,
694 before: Option<transaction_block::Cursor>,
695 filter: Option<TransactionBlockFilter>,
696 scan_limit: Option<u64>,
697 ) -> Result<ScanConnection<String, TransactionBlock>> {
698 let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
699
700 let Some(filter) = filter
701 .unwrap_or_default()
702 .intersect(TransactionBlockFilter {
703 recv_address: Some(self.0.address),
704 ..Default::default()
705 })
706 else {
707 return Ok(ScanConnection::new(false, false));
708 };
709
710 TransactionBlock::paginate(ctx, page, filter, self.0.checkpoint_viewed_at, scan_limit)
711 .await
712 .extend()
713 }
714
715 pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
716 let inner = &self.0.inner;
717 Ok(match &inner.bcs {
718 Some(serialized) => Some(Base64::from(serialized)),
719
720 None => {
721 let bytes = bcs::to_bytes(&inner.native)
722 .map_err(|e| {
723 Error::Internal(format!(
724 "Failed to serialize object at {}: {e}",
725 self.0.address
726 ))
727 })
728 .extend()?;
729 Some(Base64::from(&bytes))
730 }
731 })
732 }
733
734 pub(crate) async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
737 let native = self.0.native_impl();
738
739 let move_object = native
740 .data
741 .as_opt_struct()
742 .ok_or_else(|| Error::Internal("Failed to convert object into MoveObject".to_string()))
743 .extend()?;
744
745 let (struct_tag, move_struct) = deserialize_move_struct(move_object, ctx.data_unchecked())
746 .await
747 .extend()?;
748
749 let Some(display) = Display::query(ctx.data_unchecked(), struct_tag.into())
750 .await
751 .extend()?
752 else {
753 return Ok(None);
754 };
755
756 Ok(Some(display.render(&move_struct).extend()?))
757 }
758}
759
760impl Object {
761 pub(crate) fn from_native(
776 address: IotaAddress,
777 native: NativeObject,
778 checkpoint_viewed_at: u64,
779 root_version: Option<u64>,
780 ) -> Object {
781 let root_version = root_version.unwrap_or_else(|| version_for_dynamic_fields(&native));
782 Object {
783 address,
784 inner: ActiveObject {
785 native,
786 status: ObjectStatus::NotIndexed,
787 bcs: None,
788 },
789 checkpoint_viewed_at,
790 root_version,
791 }
792 }
793
794 pub(crate) fn native_impl(&self) -> &NativeObject {
795 &self.inner.native
796 }
797
798 pub(crate) fn version_impl(&self) -> u64 {
799 self.native_impl().version().as_u64()
800 }
801
802 pub(crate) fn root_version(&self) -> u64 {
806 self.root_version
807 }
808
809 pub(crate) async fn paginate(
816 db: &Db,
817 page: Page<Cursor>,
818 filter: ObjectFilter,
819 checkpoint_viewed_at: u64,
820 ) -> Result<Connection<String, Object>, Error> {
821 Self::paginate_subtype(db, page, filter, checkpoint_viewed_at, Ok).await
822 }
823
824 pub(crate) async fn paginate_subtype<T: OutputType>(
841 db: &Db,
842 page: Page<Cursor>,
843 filter: ObjectFilter,
844 checkpoint_viewed_at: u64,
845 downcast: impl Fn(Object) -> Result<T, Error>,
846 ) -> Result<Connection<String, T>, Error> {
847 let cursor_viewed_at = page.validate_cursor_consistency()?;
852 let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(checkpoint_viewed_at);
853
854 let max_available_range = db.max_available_range;
855
856 let Some((prev, next, results)) = db
857 .execute_repeatable(move |conn| {
858 if !AvailableRange::is_checkpoint_in_backward_history_range(
859 conn,
860 checkpoint_viewed_at,
861 max_available_range,
862 )? {
863 return Ok::<_, diesel::result::Error>(None);
864 };
865
866 let (prev, next, results_iter) = page.paginate_raw_query::<StoredBackwardObject>(
867 conn,
868 checkpoint_viewed_at,
869 backward_objects_query(&filter, checkpoint_viewed_at, &page),
870 )?;
871 let results: Vec<StoredBackwardObject> = results_iter.collect();
872 Ok(Some((prev, next, results)))
873 })
874 .await?
875 else {
876 return Err(Error::Client(
877 "Requested data is outside the available range".to_string(),
878 ));
879 };
880
881 let mut conn: Connection<String, T> = Connection::new(prev, next);
882
883 for stored in results {
884 let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor();
887 let stored_history = stored.into_stored_history(checkpoint_viewed_at);
888 let active_object = ActiveObject::try_from(stored_history)?;
889 let object = Object::from_active_object(active_object, checkpoint_viewed_at, None);
890 conn.edges.push(Edge::new(cursor, downcast(object)?));
891 }
892
893 Ok(conn)
894 }
895
896 pub(crate) fn latest_at(checkpoint_viewed_at: u64) -> ObjectLookup {
898 ObjectLookup::LatestAt {
899 checkpoint_viewed_at,
900 }
901 }
902
903 pub(crate) fn under_parent(parent_version: u64, checkpoint_viewed_at: u64) -> ObjectLookup {
906 ObjectLookup::UnderParent {
907 parent_version,
908 checkpoint_viewed_at,
909 }
910 }
911
912 pub(crate) fn at_version(version: u64, checkpoint_viewed_at: u64) -> ObjectLookup {
914 ObjectLookup::VersionAt {
915 version,
916 checkpoint_viewed_at,
917 }
918 }
919
920 pub(crate) fn at_optimistic_version(version: u64) -> ObjectLookup {
922 ObjectLookup::OptimisticVersion { version }
923 }
924
925 pub(crate) async fn query(
926 ctx: &Context<'_>,
927 id: IotaAddress,
928 key: ObjectLookup,
929 ) -> Result<Option<Self>, Error> {
930 let DataLoader(loader) = &ctx.data_unchecked();
931
932 match key {
933 ObjectLookup::VersionAt {
934 version,
935 checkpoint_viewed_at,
936 } => loader
937 .load_one(HistoricalKey {
938 id,
939 version,
940 checkpoint_viewed_at,
941 })
942 .await?
943 .transpose(),
944
945 ObjectLookup::OptimisticVersion { version } => loader
946 .load_one(OptimisticKey { id, version })
947 .await?
948 .transpose(),
949
950 ObjectLookup::UnderParent {
951 parent_version,
952 checkpoint_viewed_at,
953 } => loader
954 .load_one(ParentVersionKey {
955 id,
956 parent_version,
957 checkpoint_viewed_at,
958 })
959 .await?
960 .transpose(),
961
962 ObjectLookup::LatestAt {
963 checkpoint_viewed_at,
964 } => {
965 loader
966 .load_one(LatestAtKey {
967 id,
968 checkpoint_viewed_at,
969 })
970 .await
971 }
972 }
973 }
974
975 pub(crate) async fn query_singleton(
979 db: &Db,
980 type_: StructTag,
981 checkpoint_viewed_at: u64,
982 ) -> Result<Option<Object>, Error> {
983 let filter = ObjectFilter {
984 type_: Some(TypeFilter::ByType(type_)),
985 ..Default::default()
986 };
987
988 let connection = Self::paginate(db, Page::bounded(1), filter, checkpoint_viewed_at).await?;
989
990 Ok(connection.edges.into_iter().next().map(|edge| edge.node))
991 }
992
993 pub(crate) fn from_active_object(
1008 active_object: ActiveObject,
1009 checkpoint_viewed_at: u64,
1010 root_version: Option<u64>,
1011 ) -> Self {
1012 let address = IotaAddress::from(active_object.native.id());
1013 let root_version =
1014 root_version.unwrap_or_else(|| version_for_dynamic_fields(&active_object.native));
1015 Self {
1016 address,
1017 inner: active_object,
1018 checkpoint_viewed_at,
1019 root_version,
1020 }
1021 }
1022
1023 pub(crate) fn try_from_stored_object(
1024 stored_object: StoredObject,
1025 checkpoint_viewed_at: u64,
1026 root_version: Option<u64>,
1027 ) -> Result<Self, Error> {
1028 let active_object = ActiveObject {
1029 native: NativeObject::try_from(&stored_object)?,
1030 status: ObjectStatus::Indexed,
1031 bcs: Some(stored_object.serialized_object),
1032 };
1033 Ok(Self::from_active_object(
1034 active_object,
1035 checkpoint_viewed_at,
1036 root_version,
1037 ))
1038 }
1039}
1040
1041impl TryFrom<StoredHistoryObject> for ActiveObject {
1042 type Error = Error;
1043
1044 fn try_from(stored: StoredHistoryObject) -> Result<Self, Self::Error> {
1055 if !matches!(
1056 NativeObjectStatus::try_from(stored.object_status),
1057 Ok(NativeObjectStatus::Active)
1058 ) {
1059 return Err(Error::Internal(format!(
1060 "Expected active object 0x{} at version {}, but found status {}",
1061 hex::encode(&stored.object_id),
1062 stored.object_version,
1063 stored.object_status,
1064 )));
1065 }
1066
1067 let native = NativeObject::try_from(&stored)?;
1068 Ok(ActiveObject {
1069 native,
1070 status: ObjectStatus::Indexed,
1071 bcs: stored.serialized_object,
1072 })
1073 }
1074}
1075
1076fn version_for_dynamic_fields(native: &NativeObject) -> u64 {
1087 native.as_inner().version().as_u64()
1088}
1089
1090impl ObjectFilter {
1091 pub(crate) fn intersect(self, other: ObjectFilter) -> Option<Self> {
1097 macro_rules! intersect {
1098 ($field:ident, $body:expr) => {
1099 intersect::field(self.$field, other.$field, $body)
1100 };
1101 }
1102
1103 let keys = intersect::field(self.keys(), other.keys(), |k, l| {
1106 let mut combined = BTreeMap::new();
1107
1108 for (id, v) in k {
1109 if let Some(w) = l.get(&id).copied() {
1110 combined.insert(id, intersect::field(v, w, intersect::by_eq)?);
1111 }
1112 }
1113
1114 (!combined.is_empty()).then_some(combined)
1118 })?;
1119
1120 let object_ids = {
1127 let partition: Vec<_> = keys
1128 .iter()
1129 .flatten()
1130 .filter_map(|(id, v)| v.is_none().then_some(*id))
1131 .collect();
1132
1133 (!partition.is_empty()).then_some(partition)
1134 };
1135
1136 let object_keys = {
1137 let partition: Vec<_> = keys
1138 .iter()
1139 .flatten()
1140 .filter_map(|(id, v)| {
1141 Some(ObjectKey {
1142 object_id: *id,
1143 version: (*v)?.into(),
1144 })
1145 })
1146 .collect();
1147
1148 (!partition.is_empty()).then_some(partition)
1149 };
1150
1151 Some(Self {
1152 type_: intersect!(type_, TypeFilter::intersect)?,
1153 owner: intersect!(owner, intersect::by_eq)?,
1154 object_ids,
1155 object_keys,
1156 })
1157 }
1158
1159 fn keys(&self) -> Option<BTreeMap<IotaAddress, Option<u64>>> {
1163 if self.object_keys.is_none() && self.object_ids.is_none() {
1164 return None;
1165 }
1166
1167 Some(BTreeMap::from_iter(
1168 self.object_keys
1169 .iter()
1170 .flatten()
1171 .map(|key| (key.object_id, Some(key.version.into())))
1172 .chain(self.object_ids.iter().flatten().map(|id| (*id, None))),
1175 ))
1176 }
1177
1178 pub(crate) fn apply(&self, mut query: RawQuery) -> RawQuery {
1181 if let Some(object_ids) = &self.object_ids {
1184 if object_ids.is_empty() {
1186 query = or_filter!(query, "1=0");
1187 } else {
1188 let mut inner = String::new();
1189 let mut prefix = "object_id IN (";
1190 for id in object_ids {
1191 write!(
1193 &mut inner,
1194 "{prefix}'\\x{}'::bytea",
1195 hex::encode(id.into_vec())
1196 )
1197 .unwrap();
1198 prefix = ", ";
1199 }
1200 inner.push(')');
1201 query = or_filter!(query, inner);
1202 }
1203 }
1204
1205 if let Some(object_keys) = &self.object_keys {
1206 if object_keys.is_empty() {
1208 query = or_filter!(query, "1=0");
1209 } else {
1210 let mut inner = String::new();
1211 let mut prefix = "(";
1212 for ObjectKey { object_id, version } in object_keys {
1213 write!(
1215 &mut inner,
1216 "{prefix}(object_id = '\\x{}'::bytea AND object_version = {})",
1217 hex::encode(object_id.into_vec()),
1218 version
1219 )
1220 .unwrap();
1221 prefix = " OR ";
1222 }
1223 inner.push(')');
1224 query = or_filter!(query, inner);
1225 }
1226 }
1227
1228 if let Some(owner) = self.owner {
1229 query = filter!(
1230 query,
1231 format!(
1232 "owner_id = '\\x{}'::bytea AND owner_type = {}",
1233 hex::encode(owner.into_vec()),
1234 OwnerType::Address as i16
1235 )
1236 );
1237 }
1238
1239 if let Some(type_) = &self.type_ {
1240 return type_.apply_raw(
1241 query,
1242 "object_type",
1243 "object_type_package",
1244 "object_type_module",
1245 "object_type_name",
1246 );
1247 }
1248
1249 query
1250 }
1251}
1252
1253impl HistoricalObjectCursor {
1254 pub(crate) fn new(object_id: Vec<u8>, checkpoint_viewed_at: u64) -> Self {
1255 Self {
1256 object_id,
1257 checkpoint_viewed_at,
1258 }
1259 }
1260}
1261
1262impl Checkpointed for Cursor {
1263 fn checkpoint_viewed_at(&self) -> u64 {
1264 self.checkpoint_viewed_at
1265 }
1266}
1267
1268impl ScanLimited for Cursor {}
1269
1270impl RawPaginated<Cursor> for StoredHistoryObject {
1271 fn filter_ge(cursor: &Cursor, query: RawQuery) -> RawQuery {
1272 filter!(
1273 query,
1274 format!(
1275 "candidates.object_id >= '\\x{}'::bytea",
1276 hex::encode(cursor.object_id.clone())
1277 )
1278 )
1279 }
1280
1281 fn filter_le(cursor: &Cursor, query: RawQuery) -> RawQuery {
1282 filter!(
1283 query,
1284 format!(
1285 "candidates.object_id <= '\\x{}'::bytea",
1286 hex::encode(cursor.object_id.clone())
1287 )
1288 )
1289 }
1290
1291 fn order(asc: bool, query: RawQuery) -> RawQuery {
1292 if asc {
1293 query.order_by("candidates.object_id ASC")
1294 } else {
1295 query.order_by("candidates.object_id DESC")
1296 }
1297 }
1298}
1299
1300impl Target<Cursor> for StoredHistoryObject {
1301 fn cursor(&self, checkpoint_viewed_at: u64) -> Cursor {
1302 Cursor::new(HistoricalObjectCursor::new(
1303 self.object_id.clone(),
1304 checkpoint_viewed_at,
1305 ))
1306 }
1307}
1308
1309#[derive(diesel::QueryableByName, Clone, Debug)]
1314pub(crate) struct StoredBackwardObject {
1315 #[diesel(sql_type = sql_types::Binary)]
1316 pub object_id: Vec<u8>,
1317 #[diesel(sql_type = sql_types::BigInt)]
1318 pub object_version: i64,
1319 #[diesel(sql_type = sql_types::SmallInt)]
1320 pub object_status: i16,
1321 #[diesel(sql_type = sql_types::Nullable<sql_types::Binary>)]
1322 pub object_digest: Option<Vec<u8>>,
1323 #[diesel(sql_type = sql_types::Nullable<sql_types::SmallInt>)]
1324 pub owner_type: Option<i16>,
1325 #[diesel(sql_type = sql_types::Nullable<sql_types::Binary>)]
1326 pub owner_id: Option<Vec<u8>>,
1327 #[diesel(sql_type = sql_types::Nullable<sql_types::Text>)]
1328 pub object_type: Option<String>,
1329 #[diesel(sql_type = sql_types::Nullable<sql_types::Binary>)]
1330 pub object_type_package: Option<Vec<u8>>,
1331 #[diesel(sql_type = sql_types::Nullable<sql_types::Text>)]
1332 pub object_type_module: Option<String>,
1333 #[diesel(sql_type = sql_types::Nullable<sql_types::Text>)]
1334 pub object_type_name: Option<String>,
1335 #[diesel(sql_type = sql_types::Nullable<sql_types::Binary>)]
1336 pub serialized_object: Option<Vec<u8>>,
1337 #[diesel(sql_type = sql_types::Nullable<sql_types::Text>)]
1338 pub coin_type: Option<String>,
1339 #[diesel(sql_type = sql_types::Nullable<sql_types::BigInt>)]
1340 pub coin_balance: Option<i64>,
1341 #[diesel(sql_type = sql_types::Nullable<sql_types::SmallInt>)]
1342 pub df_kind: Option<i16>,
1343}
1344
1345impl StoredBackwardObject {
1346 pub(crate) fn into_stored_history(self, checkpoint_viewed_at: u64) -> StoredHistoryObject {
1351 StoredHistoryObject {
1352 object_id: self.object_id,
1353 object_version: self.object_version,
1354 object_status: self.object_status,
1355 object_digest: self.object_digest,
1356 checkpoint_sequence_number: checkpoint_viewed_at as i64,
1357 owner_type: self.owner_type,
1358 owner_id: self.owner_id,
1359 object_type: self.object_type,
1360 object_type_package: self.object_type_package,
1361 object_type_module: self.object_type_module,
1362 object_type_name: self.object_type_name,
1363 serialized_object: self.serialized_object,
1364 coin_type: self.coin_type,
1365 coin_balance: self.coin_balance,
1366 df_kind: self.df_kind,
1367 }
1368 }
1369}
1370
1371impl RawPaginated<Cursor> for StoredBackwardObject {
1372 fn filter_ge(cursor: &Cursor, query: RawQuery) -> RawQuery {
1373 filter!(
1374 query,
1375 format!(
1376 "candidates.object_id >= '\\x{}'::bytea",
1377 hex::encode(cursor.object_id.clone())
1378 )
1379 )
1380 }
1381
1382 fn filter_le(cursor: &Cursor, query: RawQuery) -> RawQuery {
1383 filter!(
1384 query,
1385 format!(
1386 "candidates.object_id <= '\\x{}'::bytea",
1387 hex::encode(cursor.object_id.clone())
1388 )
1389 )
1390 }
1391
1392 fn order(asc: bool, query: RawQuery) -> RawQuery {
1393 if asc {
1394 query.order_by("candidates.object_id ASC")
1395 } else {
1396 query.order_by("candidates.object_id DESC")
1397 }
1398 }
1399}
1400
1401impl Target<Cursor> for StoredBackwardObject {
1402 fn cursor(&self, checkpoint_viewed_at: u64) -> Cursor {
1403 Cursor::new(HistoricalObjectCursor::new(
1404 self.object_id.clone(),
1405 checkpoint_viewed_at,
1406 ))
1407 }
1408}
1409
1410const OBJECT_STATUS_PRUNED: i16 = -1;
1414
1415const OBJECT_STATUS_WRAPPED_OR_DELETED: i16 = NativeObjectStatus::WrappedOrDeleted as i16;
1416
1417const OBJECT_STATUS_ACTIVE: i16 = NativeObjectStatus::Active as i16;
1418
1419trait FallbackObjectKey: Eq + std::hash::Hash {
1423 fn checkpoint_viewed_at(&self) -> u64;
1425 fn root_version(&self) -> Option<u64>;
1427}
1428
1429impl FallbackObjectKey for HistoricalKey {
1430 fn checkpoint_viewed_at(&self) -> u64 {
1431 self.checkpoint_viewed_at
1432 }
1433 fn root_version(&self) -> Option<u64> {
1434 None
1435 }
1436}
1437
1438impl FallbackObjectKey for ParentVersionKey {
1439 fn checkpoint_viewed_at(&self) -> u64 {
1440 self.checkpoint_viewed_at
1441 }
1442 fn root_version(&self) -> Option<u64> {
1443 Some(self.parent_version)
1444 }
1445}
1446
1447fn objects_version_complete(db: &Db) -> bool {
1451 db.inner
1452 .ensure_data_not_pruned_for_checkpoint(0, &[CommitterTables::ObjectsVersion])
1453 .is_ok()
1454}
1455
1456async fn fill_missing_objects_from_fallback<K: FallbackObjectKey>(
1464 db: &Db,
1465 keys_with_refs: Vec<(K, (ObjectId, Version))>,
1466 results: &mut HashMap<K, Result<Object, Error>>,
1467) -> Result<(), Error> {
1468 if keys_with_refs.is_empty() {
1469 return Ok(());
1470 }
1471
1472 let object_refs: Vec<(ObjectId, Version)> =
1473 keys_with_refs.iter().map(|(_, obj_ref)| *obj_ref).collect();
1474
1475 let fetched: Vec<Result<Option<StoredObject>, Error>> = if db.inner.is_fallback_enabled() {
1476 db.inner
1477 .multi_get_fallback_objects(&object_refs, false)
1478 .await
1479 .map_err(Error::from)?
1480 .into_iter()
1481 .map(Ok)
1482 .collect()
1483 } else {
1484 object_refs
1485 .iter()
1486 .map(|(id, _)| {
1487 Err(Error::DataPruned(format!(
1488 "data for object {id} potentially pruned"
1489 )))
1490 })
1491 .collect()
1492 };
1493
1494 for ((key, _), fetched) in keys_with_refs.into_iter().zip(fetched) {
1495 match fetched {
1496 Ok(Some(stored)) => {
1497 let object = Object::try_from_stored_object(
1498 stored,
1499 key.checkpoint_viewed_at(),
1500 key.root_version(),
1501 )?;
1502 results.insert(key, Ok(object));
1503 }
1504 Ok(None) => {}
1506 Err(e) => {
1508 results.insert(key, Err(e));
1509 }
1510 }
1511 }
1512
1513 Ok(())
1514}
1515
1516impl Loader<HistoricalKey> for Db {
1517 type Value = Result<Object, Error>;
1519 type Error = Error;
1520
1521 async fn load(
1522 &self,
1523 keys: &[HistoricalKey],
1524 ) -> Result<HashMap<HistoricalKey, Result<Object, Error>>, Error> {
1525 if keys.is_empty() {
1526 return Ok(HashMap::new());
1527 }
1528
1529 let (ids, versions): (Vec<Vec<u8>>, Vec<i64>) = keys
1530 .iter()
1531 .map(|key| (key.id.into_vec(), key.version as i64))
1532 .collect::<BTreeSet<_>>()
1533 .into_iter()
1534 .unzip();
1535
1536 let sql = "SELECT \
1549 v.object_id, \
1550 v.object_version, \
1551 v.cp_sequence_number AS checkpoint_sequence_number, \
1552 COALESCE(co.object_status, bh.object_status, \
1553 CASE WHEN v.cp_sequence_number >= COALESCE((\
1554 SELECT min_available_cp FROM watermarks \
1555 WHERE entity = 'objects_backward_history'), 0) \
1556 THEN $3 ELSE $4 END) AS object_status, \
1557 COALESCE(co.object_digest, bh.object_digest) AS object_digest, \
1558 COALESCE(co.owner_type, bh.owner_type) AS owner_type, \
1559 COALESCE(co.owner_id, bh.owner_id) AS owner_id, \
1560 COALESCE(co.object_type, bh.object_type) AS object_type, \
1561 COALESCE(co.object_type_package, bh.object_type_package) AS object_type_package, \
1562 COALESCE(co.object_type_module, bh.object_type_module) AS object_type_module, \
1563 COALESCE(co.object_type_name, bh.object_type_name) AS object_type_name, \
1564 COALESCE(co.serialized_object, bh.serialized_object) AS serialized_object, \
1565 COALESCE(co.coin_type, bh.coin_type) AS coin_type, \
1566 COALESCE(co.coin_balance, bh.coin_balance) AS coin_balance, \
1567 COALESCE(co.df_kind, bh.df_kind) AS df_kind \
1568 FROM unnest($1::bytea[], $2::bigint[]) AS pairs(object_id, object_version) \
1569 INNER JOIN objects_version v \
1570 ON v.object_id = pairs.object_id \
1571 AND v.object_version = pairs.object_version \
1572 LEFT JOIN checkpointed_objects co \
1573 ON co.object_id = v.object_id \
1574 AND co.object_version = v.object_version \
1575 LEFT JOIN objects_backward_history bh \
1576 ON bh.object_id = v.object_id \
1577 AND bh.object_version = v.object_version";
1578
1579 let objects: Vec<StoredHistoryObject> = self
1580 .execute(move |conn| {
1581 conn.results(move || {
1582 diesel::sql_query(sql)
1583 .bind::<sql_types::Array<sql_types::Binary>, _>(ids.clone())
1584 .bind::<sql_types::Array<sql_types::BigInt>, _>(versions.clone())
1585 .bind::<sql_types::SmallInt, _>(OBJECT_STATUS_WRAPPED_OR_DELETED)
1586 .bind::<sql_types::SmallInt, _>(OBJECT_STATUS_PRUNED)
1587 })
1588 })
1589 .await
1590 .map_err(|e| Error::Internal(format!("Failed to fetch objects: {e}")))?;
1591
1592 let mut id_version_to_stored = BTreeMap::new();
1593 for stored in objects {
1594 let key = (addr(&stored.object_id)?, stored.object_version as u64);
1595 id_version_to_stored.insert(key, stored);
1596 }
1597
1598 let mut result = HashMap::new();
1599 let mut fallback_keys = Vec::new();
1600 let mut unresolved_keys = Vec::new();
1602 for key in keys {
1603 let Some(stored) = id_version_to_stored.get(&(key.id, key.version)) else {
1604 unresolved_keys.push(*key);
1605 continue;
1606 };
1607
1608 if key.checkpoint_viewed_at < stored.checkpoint_sequence_number as u64 {
1612 continue;
1613 }
1614
1615 let active_object = match stored.object_status {
1616 OBJECT_STATUS_PRUNED => {
1617 fallback_keys.push(*key);
1618 continue;
1619 }
1620 OBJECT_STATUS_ACTIVE => ActiveObject::try_from(stored.clone())?,
1621 _ => continue,
1623 };
1624 let object = Object::from_active_object(active_object, key.checkpoint_viewed_at, None);
1627 result.insert(*key, Ok(object));
1628 }
1629
1630 if !objects_version_complete(self) {
1634 fallback_keys.append(&mut unresolved_keys);
1635 }
1636
1637 let keys_with_refs = fallback_keys
1638 .into_iter()
1639 .map(|key| (key, (key.id.into(), Version::from(key.version))))
1640 .collect();
1641 fill_missing_objects_from_fallback(self, keys_with_refs, &mut result).await?;
1642
1643 Ok(result)
1644 }
1645}
1646
1647impl Loader<OptimisticKey> for Db {
1648 type Value = Result<Object, Error>;
1650 type Error = Error;
1651
1652 async fn load(
1653 &self,
1654 keys: &[OptimisticKey],
1655 ) -> Result<HashMap<OptimisticKey, Result<Object, Error>>, Error> {
1656 use objects::dsl as o;
1657
1658 if keys.is_empty() {
1659 return Ok(HashMap::new());
1660 }
1661
1662 let id_versions: BTreeSet<_> = keys
1663 .iter()
1664 .map(|key| (key.id.into_vec(), key.version as i64))
1665 .collect();
1666
1667 let objects: Vec<StoredObject> = self
1668 .execute(move |conn| {
1669 conn.results(move || {
1670 let mut query = o::objects.select(StoredObject::as_select()).into_boxed();
1671 for (id, version) in id_versions.iter().cloned() {
1672 query =
1673 query.or_filter(o::object_id.eq(id).and(o::object_version.eq(version)));
1674 }
1675 query
1676 })
1677 })
1678 .await
1679 .map_err(|e| Error::Internal(format!("Failed to fetch optimistic objects: {e}")))?;
1680
1681 let mut result = HashMap::new();
1682 let id_version_to_stored = objects
1683 .into_iter()
1684 .map(|stored| {
1685 addr(&stored.object_id).map(|id| ((id, stored.object_version as u64), stored))
1686 })
1687 .collect::<Result<BTreeMap<_, _>, _>>()?;
1688
1689 let mut missing_keys = Vec::new();
1691 for key in keys {
1692 if let Some(stored) = id_version_to_stored.get(&(key.id, key.version)) {
1693 let object = Object::try_from_stored_object(stored.clone(), u64::MAX, None)?;
1694 result.insert(*key, Ok(object));
1695 } else {
1696 missing_keys.push(*key);
1697 }
1698 }
1699
1700 if !missing_keys.is_empty() {
1702 let historical_keys: Vec<HistoricalKey> = missing_keys
1703 .iter()
1704 .map(|key| HistoricalKey {
1705 id: key.id,
1706 version: key.version,
1707 checkpoint_viewed_at: u64::MAX,
1708 })
1709 .collect();
1710
1711 let historical_result: HashMap<HistoricalKey, Result<Object, Error>> =
1712 self.load(&historical_keys).await?;
1713
1714 for (historical_key, object) in historical_result {
1715 let optimistic_key = OptimisticKey {
1716 id: historical_key.id,
1717 version: historical_key.version,
1718 };
1719 result.insert(optimistic_key, object);
1720 }
1721 }
1722
1723 Ok(result)
1724 }
1725}
1726
1727impl Loader<ParentVersionKey> for Db {
1728 type Value = Result<Object, Error>;
1730 type Error = Error;
1731
1732 async fn load(
1733 &self,
1734 keys: &[ParentVersionKey],
1735 ) -> Result<HashMap<ParentVersionKey, Result<Object, Error>>, Error> {
1736 #[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy)]
1739 struct GroupKey {
1740 checkpoint_viewed_at: u64,
1741 parent_version: u64,
1742 }
1743
1744 let mut keys_by_cursor_and_parent_version: BTreeMap<_, BTreeSet<_>> = BTreeMap::new();
1745 for key in keys {
1746 let group_key = GroupKey {
1747 checkpoint_viewed_at: key.checkpoint_viewed_at,
1748 parent_version: key.parent_version,
1749 };
1750
1751 keys_by_cursor_and_parent_version
1752 .entry(group_key)
1753 .or_default()
1754 .insert(key.id.into_vec());
1755 }
1756
1757 let sql = "WITH ids AS (SELECT unnest($1::bytea[]) AS object_id), \
1770 latest_per_id AS (\
1771 SELECT i.object_id, o.object_version, o.cp_sequence_number \
1772 FROM ids i \
1773 JOIN LATERAL (\
1774 SELECT object_version, cp_sequence_number \
1775 FROM objects_version \
1776 WHERE object_id = i.object_id \
1777 AND object_version <= $2::bigint \
1778 ORDER BY object_version DESC \
1779 LIMIT 1) o ON TRUE) \
1780 SELECT \
1781 v.object_id, \
1782 v.object_version, \
1783 v.cp_sequence_number AS checkpoint_sequence_number, \
1784 COALESCE(co.object_status, bh.object_status, \
1785 CASE WHEN v.cp_sequence_number >= COALESCE((\
1786 SELECT min_available_cp FROM watermarks \
1787 WHERE entity = 'objects_backward_history'), 0) \
1788 THEN $3 ELSE $4 END) AS object_status, \
1789 COALESCE(co.object_digest, bh.object_digest) AS object_digest, \
1790 COALESCE(co.owner_type, bh.owner_type) AS owner_type, \
1791 COALESCE(co.owner_id, bh.owner_id) AS owner_id, \
1792 COALESCE(co.object_type, bh.object_type) AS object_type, \
1793 COALESCE(co.object_type_package, bh.object_type_package) AS object_type_package, \
1794 COALESCE(co.object_type_module, bh.object_type_module) AS object_type_module, \
1795 COALESCE(co.object_type_name, bh.object_type_name) AS object_type_name, \
1796 COALESCE(co.serialized_object, bh.serialized_object) AS serialized_object, \
1797 COALESCE(co.coin_type, bh.coin_type) AS coin_type, \
1798 COALESCE(co.coin_balance, bh.coin_balance) AS coin_balance, \
1799 COALESCE(co.df_kind, bh.df_kind) AS df_kind \
1800 FROM latest_per_id v \
1801 LEFT JOIN checkpointed_objects co \
1802 ON co.object_id = v.object_id \
1803 AND co.object_version = v.object_version \
1804 LEFT JOIN objects_backward_history bh \
1805 ON bh.object_id = v.object_id \
1806 AND bh.object_version = v.object_version";
1807
1808 let futures = keys_by_cursor_and_parent_version
1810 .into_iter()
1811 .map(|(group_key, ids)| {
1812 let parent_version = group_key.parent_version as i64;
1813 let ids: Vec<Vec<u8>> = ids.into_iter().collect();
1814
1815 self.execute(move |conn| {
1816 let stored: Vec<StoredHistoryObject> = conn.results(move || {
1817 diesel::sql_query(sql)
1818 .bind::<sql_types::Array<sql_types::Binary>, _>(ids.clone())
1819 .bind::<sql_types::BigInt, _>(parent_version)
1820 .bind::<sql_types::SmallInt, _>(OBJECT_STATUS_WRAPPED_OR_DELETED)
1821 .bind::<sql_types::SmallInt, _>(OBJECT_STATUS_PRUNED)
1822 })?;
1823
1824 Ok::<_, diesel::result::Error>(
1825 stored
1826 .into_iter()
1827 .map(|stored| (group_key, stored))
1828 .collect::<Vec<_>>(),
1829 )
1830 })
1831 });
1832
1833 let groups = futures::future::join_all(futures).await;
1835
1836 let mut key_to_stored = HashMap::new();
1837 for group in groups {
1838 for (group_key, stored) in
1839 group.map_err(|e| Error::Internal(format!("Failed to fetch objects: {e}")))?
1840 {
1841 let key = ParentVersionKey {
1842 id: addr(&stored.object_id)?,
1843 checkpoint_viewed_at: group_key.checkpoint_viewed_at,
1844 parent_version: group_key.parent_version,
1845 };
1846 key_to_stored.insert(key, stored);
1847 }
1848 }
1849
1850 let mut results = HashMap::new();
1851 let mut fallback_keys = Vec::new();
1852 let mut unresolved_keys = Vec::new();
1854 for key in keys {
1855 let Some(stored) = key_to_stored.get(key) else {
1856 unresolved_keys.push(*key);
1857 continue;
1858 };
1859
1860 if key.checkpoint_viewed_at < stored.checkpoint_sequence_number as u64 {
1862 continue;
1863 }
1864
1865 let active_object = match stored.object_status {
1866 OBJECT_STATUS_PRUNED => {
1867 let object_ref = (key.id.into(), Version::from(stored.object_version as u64));
1868 fallback_keys.push((*key, object_ref));
1869 continue;
1870 }
1871 OBJECT_STATUS_ACTIVE => ActiveObject::try_from(stored.clone())?,
1872 _ => continue,
1874 };
1875 let object = Object::from_active_object(
1878 active_object,
1879 key.checkpoint_viewed_at,
1880 Some(key.parent_version),
1881 );
1882
1883 results.insert(*key, Ok(object));
1884 }
1885
1886 if !objects_version_complete(self) {
1892 for key in unresolved_keys {
1893 let id = key.id;
1894 results.insert(
1895 key,
1896 Err(Error::DataPruned(format!(
1897 "data for object {id} potentially pruned"
1898 ))),
1899 );
1900 }
1901 }
1902
1903 fill_missing_objects_from_fallback(self, fallback_keys, &mut results).await?;
1904
1905 Ok(results)
1906 }
1907}
1908
1909impl Loader<LatestAtKey> for Db {
1910 type Value = Object;
1911 type Error = Error;
1912
1913 async fn load(&self, keys: &[LatestAtKey]) -> Result<HashMap<LatestAtKey, Object>, Error> {
1914 let mut keys_by_cursor_and_parent_version: BTreeMap<_, BTreeSet<_>> = BTreeMap::new();
1917
1918 for key in keys {
1919 keys_by_cursor_and_parent_version
1920 .entry(key.checkpoint_viewed_at)
1921 .or_default()
1922 .insert(key.id);
1923 }
1924
1925 let max_available_range = self.max_available_range;
1926
1927 let futures =
1929 keys_by_cursor_and_parent_version
1930 .into_iter()
1931 .map(|(checkpoint_viewed_at, ids)| {
1932 self.execute_repeatable(move |conn| {
1933 if !AvailableRange::is_checkpoint_in_backward_history_range(
1934 conn,
1935 checkpoint_viewed_at,
1936 max_available_range,
1937 )? {
1938 return Ok::<Vec<(u64, StoredHistoryObject)>, diesel::result::Error>(
1939 vec![],
1940 );
1941 };
1942
1943 let filter = ObjectFilter {
1944 object_ids: Some(ids.iter().cloned().collect()),
1945 ..Default::default()
1946 };
1947
1948 let results: Vec<StoredBackwardObject> = conn.results(move || {
1949 consistent::query(
1950 checkpoint_viewed_at,
1951 &Page::bounded(ids.len() as u64),
1952 |q| filter.apply(q),
1953 )
1954 .into_boxed()
1955 })?;
1956
1957 Ok(results
1958 .into_iter()
1959 .map(|r| {
1960 (
1961 checkpoint_viewed_at,
1962 r.into_stored_history(checkpoint_viewed_at),
1963 )
1964 })
1965 .collect())
1966 })
1967 });
1968
1969 let groups = futures::future::join_all(futures).await;
1971
1972 let mut results = HashMap::new();
1973 for group in groups {
1974 for (checkpoint_viewed_at, stored) in
1975 group.map_err(|e| Error::Internal(format!("Failed to fetch objects: {e}")))?
1976 {
1977 let active_object = ActiveObject::try_from(stored)?;
1978 let object = Object::from_active_object(active_object, checkpoint_viewed_at, None);
1979
1980 let key = LatestAtKey {
1981 id: object.address,
1982 checkpoint_viewed_at,
1983 };
1984
1985 results.insert(key, object);
1986 }
1987 }
1988
1989 Ok(results)
1990 }
1991}
1992
1993impl From<&ActiveObject> for ObjectStatus {
1994 fn from(active_object: &ActiveObject) -> Self {
1995 active_object.status
1996 }
1997}
1998
1999impl From<&Object> for OwnerImpl {
2000 fn from(object: &Object) -> Self {
2001 OwnerImpl {
2002 address: object.address,
2003 checkpoint_viewed_at: object.checkpoint_viewed_at,
2004 }
2005 }
2006}
2007
2008pub(crate) async fn deserialize_move_struct(
2009 move_object: &NativeMoveStruct,
2010 resolver: &PackageResolver,
2011) -> Result<(StructTag, MoveStruct), Error> {
2012 let struct_tag = move_object.struct_tag().clone();
2013 let contents = move_object.contents();
2014 let move_type_layout = resolver
2015 .type_layout(TypeTag::from(struct_tag.clone()))
2016 .await
2017 .map_err(|e| {
2018 Error::Internal(format!(
2019 "Error fetching layout for type {}: {e}",
2020 struct_tag.to_canonical_string(true)
2021 ))
2022 })?;
2023
2024 let MoveTypeLayout::Struct(layout) = move_type_layout else {
2025 return Err(Error::Internal("Object is not a move struct".to_string()));
2026 };
2027
2028 let move_struct = BoundedVisitor::deserialize_struct(contents, &layout).map_err(|e| {
2032 Error::Internal(format!(
2033 "Error deserializing move struct for type {}: {e}",
2034 struct_tag.to_canonical_string(true)
2035 ))
2036 })?;
2037
2038 Ok((struct_tag, move_struct))
2039}
2040
2041fn backward_objects_query(
2049 filter: &ObjectFilter,
2050 checkpoint_viewed_at: u64,
2051 page: &Page<Cursor>,
2052) -> RawQuery {
2053 if let (Some(_), Some(_)) = (&filter.object_ids, &filter.object_keys) {
2054 let ids_only_filter = ObjectFilter {
2057 object_keys: None,
2058 ..filter.clone()
2059 };
2060 let (id_query, id_bindings) = consistent::query(checkpoint_viewed_at, page, move |query| {
2061 ids_only_filter.apply(query)
2062 })
2063 .finish();
2064
2065 let keys_filter: HistoricalFilter = ObjectFilter {
2066 object_ids: None,
2067 ..filter.clone()
2068 }
2069 .try_into()
2070 .expect("object_keys is Some by match-arm guard");
2071 let (key_query, key_bindings) = historical::query(page, &keys_filter).finish();
2072
2073 RawQuery::new(
2074 format!("SELECT * FROM (({id_query}) UNION ALL ({key_query})) AS candidates",),
2075 id_bindings.into_iter().chain(key_bindings).collect(),
2076 )
2077 .order_by("object_id")
2078 .limit(page.limit() as i64)
2079 } else if let Ok(keys_filter) = HistoricalFilter::try_from(filter.clone()) {
2080 historical::query(page, &keys_filter)
2081 } else {
2082 consistent::query(checkpoint_viewed_at, page, move |query| filter.apply(query))
2083 }
2084}
2085
2086#[cfg(test)]
2087mod tests {
2088 use std::str::FromStr;
2089
2090 use super::*;
2091
2092 #[test]
2093 fn test_owner_filter_intersection() {
2094 let f0 = ObjectFilter {
2095 owner: Some(IotaAddress::from_str("0x1").unwrap()),
2096 ..Default::default()
2097 };
2098
2099 let f1 = ObjectFilter {
2100 owner: Some(IotaAddress::from_str("0x2").unwrap()),
2101 ..Default::default()
2102 };
2103
2104 assert_eq!(f0.clone().intersect(f0.clone()), Some(f0.clone()));
2105 assert_eq!(f0.intersect(f1), None);
2106 }
2107
2108 #[test]
2109 fn test_key_filter_intersection() {
2110 let i1 = IotaAddress::from_str("0x1").unwrap();
2111 let i2 = IotaAddress::from_str("0x2").unwrap();
2112 let i3 = IotaAddress::from_str("0x3").unwrap();
2113 let i4 = IotaAddress::from_str("0x4").unwrap();
2114
2115 let f0 = ObjectFilter {
2116 object_ids: Some(vec![i1, i3]),
2117 object_keys: Some(vec![
2118 ObjectKey {
2119 object_id: i2,
2120 version: 1.into(),
2121 },
2122 ObjectKey {
2123 object_id: i4,
2124 version: 2.into(),
2125 },
2126 ]),
2127 ..Default::default()
2128 };
2129
2130 let f1 = ObjectFilter {
2131 object_ids: Some(vec![i1, i2]),
2132 object_keys: Some(vec![ObjectKey {
2133 object_id: i4,
2134 version: 2.into(),
2135 }]),
2136 ..Default::default()
2137 };
2138
2139 let f2 = ObjectFilter {
2140 object_ids: Some(vec![i1, i3]),
2141 ..Default::default()
2142 };
2143
2144 let f3 = ObjectFilter {
2145 object_keys: Some(vec![
2146 ObjectKey {
2147 object_id: i2,
2148 version: 2.into(),
2149 },
2150 ObjectKey {
2151 object_id: i4,
2152 version: 2.into(),
2153 },
2154 ]),
2155 ..Default::default()
2156 };
2157
2158 assert_eq!(
2159 f0.clone().intersect(f1.clone()),
2160 Some(ObjectFilter {
2161 object_ids: Some(vec![i1]),
2162 object_keys: Some(vec![
2163 ObjectKey {
2164 object_id: i2,
2165 version: 1.into(),
2166 },
2167 ObjectKey {
2168 object_id: i4,
2169 version: 2.into(),
2170 },
2171 ]),
2172 ..Default::default()
2173 })
2174 );
2175
2176 assert_eq!(
2177 f1.clone().intersect(f2.clone()),
2178 Some(ObjectFilter {
2179 object_ids: Some(vec![i1]),
2180 ..Default::default()
2181 })
2182 );
2183
2184 assert_eq!(
2185 f1.intersect(f3.clone()),
2186 Some(ObjectFilter {
2187 object_keys: Some(vec![
2188 ObjectKey {
2189 object_id: i2,
2190 version: 2.into(),
2191 },
2192 ObjectKey {
2193 object_id: i4,
2194 version: 2.into(),
2195 },
2196 ]),
2197 ..Default::default()
2198 })
2199 );
2200
2201 assert_eq!(f0.intersect(f3.clone()), None);
2203
2204 assert_eq!(f2.intersect(f3), None);
2206 }
2207}