Skip to main content

iota_graphql_rpc/types/
object.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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    /// The checkpoint sequence number at which this was viewed at.
68    pub checkpoint_viewed_at: u64,
69    /// Root parent object version for dynamic fields.
70    ///
71    /// This enables consistent dynamic field reads in the case of chained
72    /// dynamic object fields, e.g., `Parent -> DOF1 -> DOF2`. In such
73    /// cases, the object versions may end up like `Parent >= DOF1, DOF2`
74    /// but `DOF1 < DOF2`. Thus, database queries for dynamic fields must
75    /// bound the object versions by the version of the root object of the tree.
76    ///
77    /// Essentially, lamport timestamps of objects are updated for all top-level
78    /// mutable objects provided as inputs to a transaction as well as any
79    /// mutated dynamic child objects. However, any dynamic child objects
80    /// that were loaded but not actually mutated don't end up having
81    /// their versions updated.
82    root_version: u64,
83}
84
85/// Type to implement GraphQL fields that are shared by all Objects.
86pub(crate) struct ObjectImpl<'o>(pub &'o Object);
87
88#[derive(Clone, Debug)]
89pub(crate) struct ActiveObject {
90    /// The deserialized object.
91    native: NativeObject,
92    /// Where the object's state was read from.
93    status: ObjectStatus,
94    /// The serialized object as stored in the index. `None` for `NotIndexed`
95    /// objects.
96    ///
97    /// Avoids the re-serialization of `native` for `Indexed` objects.
98    bcs: Option<Vec<u8>>,
99}
100
101#[derive(Enum, Copy, Clone, Eq, PartialEq, Debug)]
102#[graphql(name = "ObjectKind")]
103pub enum ObjectStatus {
104    /// The object is loaded from serialized data, such as the contents of a
105    /// transaction that hasn't been indexed yet.
106    NotIndexed,
107    /// The object is fetched from the index.
108    Indexed,
109}
110
111#[derive(Clone, Debug, PartialEq, Eq, InputObject)]
112pub(crate) struct ObjectRef {
113    /// ID of the object.
114    pub address: IotaAddress,
115    /// Version or sequence number of the object.
116    pub version: UInt53,
117    /// Digest of the object.
118    pub digest: Digest,
119}
120
121/// Constrains the set of objects returned. All filters are optional, and the
122/// resulting set of objects are ones whose
123///
124/// - Type matches the `type` filter,
125/// - AND, whose owner matches the `owner` filter,
126/// - AND, whose ID is in `objectIds` OR whose ID and version is in
127///   `objectKeys`.
128#[derive(InputObject, Default, Debug, Clone, Eq, PartialEq)]
129pub(crate) struct ObjectFilter {
130    /// Filter objects by their type's `package`, `package::module`, or their
131    /// fully qualified type name.
132    ///
133    /// Generic types can be queried by either the generic type name, e.g.
134    /// `0x2::coin::Coin`, or by the full type name, such as
135    /// `0x2::coin::Coin<0x2::iota::IOTA>`.
136    pub type_: Option<TypeFilter>,
137
138    /// Filter for live objects by their current owners.
139    pub owner: Option<IotaAddress>,
140
141    /// Filter for live objects by their IDs.
142    pub object_ids: Option<Vec<IotaAddress>>,
143
144    /// Filter for live or potentially historical objects by their ID and
145    /// version.
146    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/// The object's owner type: Immutable, Shared, Parent, or Address.
156#[derive(Union, Clone)]
157pub(crate) enum ObjectOwner {
158    Immutable(Immutable),
159    Shared(Shared),
160    Parent(Box<Parent>),
161    Address(AddressOwner),
162}
163
164/// An immutable object is an object that can't be mutated, transferred, or
165/// deleted. Immutable objects have no owner, so anyone can use them.
166#[derive(SimpleObject, Clone)]
167pub(crate) struct Immutable {
168    #[graphql(name = "_")]
169    dummy: Option<bool>,
170}
171
172/// A shared object is an object that is shared using the
173/// 0x2::transfer::share_object function. Unlike owned objects, once an object
174/// is shared, it stays mutable and is accessible by anyone.
175#[derive(SimpleObject, Clone)]
176pub(crate) struct Shared {
177    initial_shared_version: UInt53,
178}
179
180/// If the object's owner is a Parent, this object is part of a dynamic field
181/// (it is the value of the dynamic field, or the intermediate Field object
182/// itself). Also note that if the owner is a parent, then it's guaranteed to be
183/// an object.
184#[derive(SimpleObject, Clone)]
185pub(crate) struct Parent {
186    parent: Option<Object>,
187}
188
189/// An address-owned object is owned by a specific 32-byte address that is
190/// either an account address (derived from a particular signature scheme) or
191/// an object ID. An address-owned object is accessible only to its owner and no
192/// others.
193#[derive(SimpleObject, Clone)]
194pub(crate) struct AddressOwner {
195    owner: Option<Owner>,
196}
197
198/// Filter for a point query of an Object.
199pub(crate) enum ObjectLookup {
200    LatestAt {
201        /// The checkpoint sequence number at which this was viewed at.
202        checkpoint_viewed_at: u64,
203    },
204
205    UnderParent {
206        /// The parent version to be used as an upper bound for the query. Look
207        /// for the latest version of a child object whose version is
208        /// less than or equal to this upper bound.
209        parent_version: u64,
210        /// The checkpoint sequence number at which this was viewed at.
211        checkpoint_viewed_at: u64,
212    },
213
214    VersionAt {
215        /// The exact version of the object to be fetched.
216        version: u64,
217        /// The checkpoint sequence number at which this was viewed at.
218        checkpoint_viewed_at: u64,
219    },
220
221    /// Variant analogous to [`VersionAt`](Self::VersionAt) but for optimistic
222    /// transactions, using the most recent not checkpointed data,
223    /// not bound by any checkpoint sequence number.
224    OptimisticVersion {
225        /// The exact version of the object to be fetched.
226        version: u64,
227    },
228}
229
230pub(crate) type Cursor = cursor::BcsCursor<HistoricalObjectCursor>;
231
232/// The inner struct for the `Object`'s cursor. The `object_id` is used as the
233/// cursor, while the `checkpoint_viewed_at` sets the consistent upper bound for
234/// the cursor.
235#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
236pub(crate) struct HistoricalObjectCursor {
237    #[serde(rename = "o")]
238    object_id: Vec<u8>,
239    /// The checkpoint sequence number this was viewed at.
240    #[serde(rename = "c")]
241    checkpoint_viewed_at: u64,
242}
243
244/// Interface implemented by on-chain values that are addressable by an ID (also
245/// referred to as its address). This includes Move objects and packages.
246#[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/// `DataLoader` key for fetching an `Object` at a specific version, constrained
312/// by a consistency cursor (if that version was created after the checkpoint
313/// the query is viewing at, then it will fail).
314#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
315struct HistoricalKey {
316    id: IotaAddress,
317    version: u64,
318    checkpoint_viewed_at: u64,
319}
320
321/// `DataLoader` key for fetching objects that haven't been checkpointed yet.
322/// This is used specifically for loading objects
323/// that are part of optimistic transaction effects.
324#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
325struct OptimisticKey {
326    id: IotaAddress,
327    version: u64,
328}
329
330/// `DataLoader` key for fetching the latest version of an object whose parent
331/// object has version `parent_version`, as of `checkpoint_viewed_at`. This
332/// look-up can fail to find a valid object if the key is not self-consistent,
333/// for example if the `parent_version` is set to a higher version
334/// than the object's actual parent as of `checkpoint_viewed_at`.
335#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
336struct ParentVersionKey {
337    id: IotaAddress,
338    parent_version: u64,
339    checkpoint_viewed_at: u64,
340}
341
342/// `DataLoader` key for fetching the latest version of an object as of a given
343/// checkpoint.
344#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
345struct LatestAtKey {
346    id: IotaAddress,
347    checkpoint_viewed_at: u64,
348}
349
350/// An object in IOTA is a package (set of Move bytecode modules) or object
351/// (typed data structure with fields) with additional metadata detailing its
352/// id, version, transaction digest, owner field indicating how this object can
353/// be accessed.
354#[Object]
355impl Object {
356    pub(crate) async fn address(&self) -> IotaAddress {
357        OwnerImpl::from(self).address().await
358    }
359
360    /// Objects owned by this object, optionally `filter`-ed.
361    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    /// Total balance of all coins with marker type owned by this object. If
376    /// type is not supplied, it defaults to `0x2::iota::IOTA`.
377    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    /// The balances of all coin types owned by this object.
386    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    /// The coin objects for this object.
400    ///
401    /// `type` is a filter on the coin's type parameter, defaulting to
402    /// `0x2::iota::IOTA`.
403    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    /// The `0x3::staking_pool::StakedIota` objects owned by this object.
418    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    /// The name explicitly configured as the default name pointing to this
432    /// address.
433    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    /// The NameRegistration NFTs owned by this address. These grant the
444    /// owner the capability to manage the associated name.
445    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    /// The current status of the object as read from the off-chain store. The
463    /// possible states are:
464    /// - NOT_INDEXED: The object is loaded from serialized data, such as the
465    ///   contents of a genesis or system package upgrade transaction.
466    /// - INDEXED: The object is retrieved from the off-chain index and
467    ///   represents the most recent or historical state of the object.
468    pub(crate) async fn status(&self) -> ObjectStatus {
469        ObjectImpl(self).status().await
470    }
471
472    /// 32-byte hash that identifies the object's current contents, encoded as a
473    /// Base58 string.
474    pub(crate) async fn digest(&self) -> Option<String> {
475        ObjectImpl(self).digest().await
476    }
477
478    /// The owner type of this object: Immutable, Shared, Parent, Address
479    /// Immutable and Shared Objects do not have owners.
480    pub(crate) async fn owner(&self, ctx: &Context<'_>) -> Option<ObjectOwner> {
481        ObjectImpl(self).owner(ctx).await
482    }
483
484    /// The transaction block that created this version of the object.
485    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    /// The amount of IOTA we would rebate if this object gets deleted or
493    /// mutated. This number is recalculated based on the present storage
494    /// gas price.
495    pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
496        ObjectImpl(self).storage_rebate().await
497    }
498
499    /// The transaction blocks that sent objects to this object.
500    ///
501    /// `scanLimit` restricts the number of candidate transactions scanned when
502    /// gathering a page of results. It is required for queries that apply two
503    /// or more complex filters (on function, affected address, recipient, input
504    /// object, changed object, or wrapped or deleted object), and can be at
505    /// most `serviceConfig.maxScanLimit`. A `kind` filter cannot be
506    /// combined with any of them.
507    ///
508    /// When the scan limit is reached the page will be returned even if it has
509    /// fewer than `first` results when paginating forward (`last` when
510    /// paginating backwards). If there are more transactions to scan,
511    /// `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to
512    /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set
513    /// to the last transaction that was scanned as opposed to the last (or
514    /// first) transaction in the page.
515    ///
516    /// Requesting the next (or previous) page after this cursor will resume the
517    /// search, scanning the next `scanLimit` many transactions in the
518    /// direction of pagination, and so on until all transactions in the
519    /// scanning range have been visited.
520    ///
521    /// By default, the scanning range includes all transactions known to
522    /// GraphQL, but it can be restricted by the `after` and `before`
523    /// cursors, and the `beforeCheckpoint`, `afterCheckpoint` and
524    /// `atCheckpoint` filters.
525    ///
526    /// DEPRECATION NOTICE: Support for the combination of two or more complex
527    /// filters as discussed above will stop with the v1.38 release. `scanLimit`
528    /// will thus become obsolete and will be removed as well.
529    #[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    /// The Base64-encoded BCS serialization of the object's content.
551    pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
552        ObjectImpl(self).bcs().await
553    }
554
555    /// The set of named templates defined on-chain for the type of this object,
556    /// to be handled off-chain. The server substitutes data from the object
557    /// into these templates to generate a display string per template.
558    async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
559        ObjectImpl(self).display(ctx).await
560    }
561
562    /// Access a dynamic field on an object using its name. Names are arbitrary
563    /// Move values whose type have `copy`, `drop`, and `store`, and are
564    /// specified using their type, and their BCS contents, Base64 encoded.
565    ///
566    /// Dynamic fields on wrapped objects can be accessed by using the same API
567    /// under the Owner type.
568    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    /// Access a dynamic object field on an object using its name. Names are
579    /// arbitrary Move values whose type have `copy`, `drop`, and `store`,
580    /// and are specified using their type, and their BCS contents, Base64
581    /// encoded. The value of a dynamic object field can also be accessed
582    /// off-chain directly via its address (e.g. using `Query.object`).
583    ///
584    /// Dynamic fields on wrapped objects can be accessed by using the same API
585    /// under the Owner type.
586    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    /// The dynamic fields and dynamic object fields on an object.
597    ///
598    /// Dynamic fields on wrapped objects can be accessed by using the same API
599    /// under the Owner type.
600    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    /// Attempts to convert the object into a MoveObject
614    async fn as_move_object(&self) -> Option<MoveObject> {
615        MoveObject::try_from(self).ok()
616    }
617
618    /// Attempts to convert the object into a MovePackage
619    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    /// `display` is part of the `IMoveObject` interface, but is implemented on
735    /// `ObjectImpl` to allow for a convenience function on `Object`.
736    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    /// Construct a GraphQL object from a native object, without its stored
762    /// (indexed) counterpart.
763    ///
764    /// `checkpoint_viewed_at` represents the checkpoint sequence number at
765    /// which this `Object` was constructed in. This is stored on `Object`
766    /// so that when viewing that entity's state, it will be as if it was
767    /// read at the same checkpoint.
768    ///
769    /// `root_version` represents the version of the root object in some nested
770    /// chain of dynamic fields. This should typically be left `None`,
771    /// unless the object(s) being resolved is a dynamic field, or if
772    /// `root_version` has been explicitly set for this object. If None, then
773    /// we use [`version_for_dynamic_fields`] to infer a root version to then
774    /// propagate from this object down to its dynamic fields.
775    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    /// Root parent object version for dynamic fields.
803    ///
804    /// Check [`Object::root_version`] for details.
805    pub(crate) fn root_version(&self) -> u64 {
806        self.root_version
807    }
808
809    /// Query the database for a `page` of objects, optionally `filter`-ed.
810    ///
811    /// `checkpoint_viewed_at` represents the checkpoint sequence number at
812    /// which this page was queried for. Each entity returned in the
813    /// connection will inherit this checkpoint, so that when viewing that
814    /// entity's state, it will be as if it was read at the same checkpoint.
815    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    /// Query the database for a `page` of some sub-type of Object. The page
825    /// uses the bytes of an Object ID and the checkpoint when the query was
826    /// made as the cursor, and can optionally be further `filter`-ed. The
827    /// subtype is created using the `downcast` function, which is allowed
828    /// to fail, if the downcast has failed.
829    ///
830    /// `checkpoint_viewed_at` represents the checkpoint sequence number at
831    /// which this page was queried for. Each entity returned in the
832    /// connection will inherit this checkpoint, so that when viewing that
833    /// entity's state, it will be as if it was read at the same checkpoint.
834    ///
835    /// If a `Page<Cursor>` is also provided, then this function will defer to
836    /// the `checkpoint_viewed_at` in the cursors. Otherwise, use the value
837    /// from the parameter, or set to None. This is so that paginated
838    /// queries are consistent with the previous query that created the
839    /// cursor.
840    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        // If cursors are provided, defer to the `checkpoint_viewed_at` in the cursor if
848        // they are consistent. Otherwise, use the value from the parameter, or
849        // set to None. This is so that paginated queries are consistent with
850        // the previous query that created the cursor.
851        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            // To maintain consistency, the returned cursor should have the same upper-bound
885            // as the checkpoint found on the cursor.
886            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    /// Look-up the latest version of the object as of a given checkpoint.
897    pub(crate) fn latest_at(checkpoint_viewed_at: u64) -> ObjectLookup {
898        ObjectLookup::LatestAt {
899            checkpoint_viewed_at,
900        }
901    }
902
903    /// Look-up the latest version of an object whose version is less than or
904    /// equal to its parent's version, as of a given checkpoint.
905    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    /// Look-up a specific version of the object, as of a given checkpoint.
913    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    /// Look-up a specific version of the object from optimistic transactions.
921    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    /// Query for a singleton object identified by its type. Note: the object is
976    /// assumed to be a singleton (we either find at least one object with
977    /// this type and then return it, or return nothing).
978    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    /// Builds an `Object` from an active object, viewed at
994    /// `checkpoint_viewed_at`.
995    ///
996    /// `checkpoint_viewed_at` represents the checkpoint sequence number at
997    /// which this `Object` was constructed in. This is stored on `Object`
998    /// so that when viewing that entity's state, it will be as if it was
999    /// read at the same checkpoint.
1000    ///
1001    /// `root_version` represents the version of the root object in some nested
1002    /// chain of dynamic fields. This should typically be left `None`,
1003    /// unless the object(s) being resolved is a dynamic field, or if
1004    /// `root_version` has been explicitly set for this object. If None, then
1005    /// we use [`version_for_dynamic_fields`] to infer a root version to then
1006    /// propagate from this object down to its dynamic fields.
1007    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    /// Builds a value from an active stored history row by
1045    /// deserializing its native object.
1046    ///
1047    /// # Errors
1048    ///
1049    /// Fails in the following cases:
1050    ///
1051    /// - The row is not active (a wrapped or deleted tombstone, or an
1052    ///   unrecognized status).
1053    /// - The row has no serialized object, or it fails to deserialize.
1054    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
1076/// We're deliberately choosing to use a child object's version as the root
1077/// here, and letting the caller override it with the actual root object's
1078/// version if it has access to it.
1079///
1080/// Using the child object's version as the root means that we're seeing the
1081/// dynamic field tree under this object at the state resulting from the
1082/// transaction that produced this version.
1083///
1084/// See [`Object::root_version`] for more details on parent/child object version
1085/// mechanics.
1086fn version_for_dynamic_fields(native: &NativeObject) -> u64 {
1087    native.as_inner().version().as_u64()
1088}
1089
1090impl ObjectFilter {
1091    /// Try to create a filter whose results are the intersection of objects in
1092    /// `self`'s results and objects in `other`'s results. This may not be
1093    /// possible if the resulting filter is inconsistent in some way (e.g. a
1094    /// filter that requires one field to be two different values
1095    /// simultaneously).
1096    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        // Treat `object_ids` and `object_keys` as a single filter on IDs, and
1104        // optionally versions, and compute the intersection of that.
1105        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            // If the intersection is empty, it means, there were some ID or Key filters in
1115            // both `self` and `other`, but they don't overlap, so the final
1116            // result is inconsistent.
1117            (!combined.is_empty()).then_some(combined)
1118        })?;
1119
1120        // Extract the ID and Key filters back out. At this point, we know that if there
1121        // were ID/Key filters in both `self` and `other`, then they intersected
1122        // to form a consistent set of constraints, so it is safe to interpret
1123        // the lack of any ID/Key filters respectively as a lack of that kind of
1124        // constraint, rather than a constraint on the empty set.
1125
1126        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    /// Extract the Object ID and Key filters into one combined map from Object
1160    /// IDs in this filter, to the versions they should have (or None if the
1161    /// filter mentions the ID but no version for it).
1162    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 ID filters after Key filters so if there is overlap, we overwrite the key
1173                // filter with the ID filter.
1174                .chain(self.object_ids.iter().flatten().map(|id| (*id, None))),
1175        ))
1176    }
1177
1178    /// Applies ObjectFilter to the input `RawQuery` and returns a new
1179    /// `RawQuery`.
1180    pub(crate) fn apply(&self, mut query: RawQuery) -> RawQuery {
1181        // Start by applying the filters on IDs and/or keys because they are combined as
1182        // a disjunction, while the remaining queries are conjunctions.
1183        if let Some(object_ids) = &self.object_ids {
1184            // Maximally strict - match a vec of 0 elements
1185            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                    // SAFETY: Writing to a `String` cannot fail.
1192                    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            // Maximally strict - match a vec of 0 elements
1207            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                    // SAFETY: Writing to a `String` cannot fail.
1214                    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/// Query-result struct for the backward diff read path. Used by
1310/// `build_backward_objects_query` which unions `checkpointed_objects` and
1311/// `objects_backward_history`. Has explicit `sql_type` annotations because
1312/// there is no single backing Diesel table definition.
1313#[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    /// Convert into a `StoredHistoryObject`, filling in
1347    /// `checkpoint_sequence_number` with the checkpoint the object was
1348    /// viewed at. The backward diff query guarantees the result is valid at
1349    /// this checkpoint, so it's the most accurate value we have.
1350    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
1410/// Synthetic `object_status` for object versions that still exist in
1411/// `objects_version` table, but are already pruned from
1412/// `objects_backward_history`.
1413const 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
1419/// Supplies the parameters needed to build an [`Object`] from a row fetched
1420/// from the historical fallback: the checkpoint it is viewed at, and the root
1421/// version to record on it.
1422trait FallbackObjectKey: Eq + std::hash::Hash {
1423    /// The checkpoint the object is viewed at.
1424    fn checkpoint_viewed_at(&self) -> u64;
1425    /// The root version to record on the resulting object, if any.
1426    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
1447/// Checks if `objects_version` table covers the whole chain history.
1448///
1449/// This function currently returns false for indexer restored from snapshot.
1450fn objects_version_complete(db: &Db) -> bool {
1451    db.inner
1452        .ensure_data_not_pruned_for_checkpoint(0, &[CommitterTables::ObjectsVersion])
1453        .is_ok()
1454}
1455
1456/// Fills the missing `results` entries with objects fetched from the
1457/// historical fallback.
1458///
1459/// It stores nothing when the object does not exist at the requested version
1460/// (e.g. when the object is wrapped or deleted). It stores
1461/// [`Error::DataPruned`] per key when the historical fallback is not
1462/// configured.
1463async 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            // The object does not exist at the requested version.
1505            Ok(None) => {}
1506            // Only this key cannot be served.
1507            Err(e) => {
1508                results.insert(key, Err(e));
1509            }
1510        }
1511    }
1512
1513    Ok(())
1514}
1515
1516impl Loader<HistoricalKey> for Db {
1517    /// Error stored per key, e.g. when object version is pruned.
1518    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        // For each `(object_id, object_version)` pair, locate the row content
1537        // in `checkpointed_objects` (current state of the object) or
1538        // `objects_backward_history` (a superseded prior state). The
1539        // `objects_version` join confirms the version is real and supplies the
1540        // checkpoint at which it became current.
1541        //
1542        // A row is returned for every version found in `objects_version`.
1543        // Wrapped-or-deleted status is reported for versions without row
1544        // content that are in the retention window of
1545        // `objects_backward_history`. Versions outside of this retention
1546        // window are returned with the synthetic `OBJECT_STATUS_PRUNED`
1547        // status.
1548        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        // Keys with no `objects_version` entry at all.
1601        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            // Filter by key's checkpoint viewed at here. Doing this in memory because it
1609            // should be quite rare that this query actually filters something,
1610            // but encoding it in SQL is complicated.
1611            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                // Wrapped, deleted, or not yet created: resolve as non-existent.
1622                _ => continue,
1623            };
1624            // This conversion will use the object's own version as the
1625            // `Object::root_version`.
1626            let object = Object::from_active_object(active_object, key.checkpoint_viewed_at, None);
1627            result.insert(*key, Ok(object));
1628        }
1629
1630        // When version history is complete, unresolved key = nonexisting object
1631        // version. When history is incomplete we have to query the KV to know
1632        // if object exists or not.
1633        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    /// Error stored per key, e.g. when object version is pruned.
1649    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        // Collect keys that were not found in objects table
1690        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        // For missing keys, fallback to the backward-history loader
1701        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    /// Error stored per key, e.g. when object version is pruned.
1729    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        // Group keys by checkpoint viewed at and parent version -- we'll issue a
1737        // separate query for each group.
1738        #[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        // For each id, pick the largest version `≤ parent_version` from
1758        // `objects_version` (versions table contains all current and past versions of
1759        // objects), then locate the row content in `checkpointed_objects`
1760        // (current state of the object) or `objects_backward_history` (a
1761        // superseded prior state).
1762        //
1763        // A row is returned for the largest version `≤ parent_version` found
1764        // in `objects_version`. Wrapped-or-deleted status is reported for
1765        // versions without row content that are in the retention window of
1766        // `objects_backward_history`. Versions outside of this retention
1767        // window are returned with the synthetic `OBJECT_STATUS_PRUNED`
1768        // status.
1769        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        // Issue concurrent reads for each group of keys.
1809        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        // Wait for the reads to all finish, and gather them into the result map.
1834        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        // Keys with no version `≤ parent_version` in `objects_version` at all.
1853        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            // This version didn't exist at the checkpoint we are viewing at.
1861            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                // Wrapped, deleted, or not yet created: resolve as non-existent.
1873                _ => continue,
1874            };
1875            // If `LatestAtKey::parent_version` is set, it must have been correctly
1876            // propagated from the `Object::root_version` of some object.
1877            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        // When version history is complete, unresolved key = nonexisting object
1887        // version. When history is incomplete we return DataPruned for such
1888        // cases, since this cannot be handled by KV currently: before_version
1889        // query will not work properly since we don't store wrapped-or-deleted
1890        // version in the KV.
1891        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        // Group keys by checkpoint viewed at -- we'll issue a separate query for each
1915        // group.
1916        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        // Issue concurrent reads for each group of keys.
1928        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        // Wait for the reads to all finish, and gather them into the result map.
1970        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(/* with_prefix */ 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    // TODO (annotated-visitor): Use custom visitors for extracting a dynamic field,
2029    // and for creating a GraphQL MoveValue directly (not via an annotated
2030    // visitor).
2031    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(/* with_prefix */ true)
2035        ))
2036    })?;
2037
2038    Ok((struct_tag, move_struct))
2039}
2040
2041/// Constructs a backward diff query for objects.
2042///
2043/// Uses consistent view for most queries to ensure point-in-time correctness.
2044/// Falls back to historical view only for object-key lookups (specific
2045/// id+version pairs) which don't need consistency filtering. When both
2046/// `object_ids` and `object_keys` are provided, the results from both views
2047/// are unioned.
2048fn 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        // If both object IDs and object keys are specified, then we need to query in
2055        // both historical and consistent views, and then union the results.
2056        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        // i2 got a conflicting version assignment
2202        assert_eq!(f0.intersect(f3.clone()), None);
2203
2204        // No overlap between these two.
2205        assert_eq!(f2.intersect(f3), None);
2206    }
2207}