Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build Guide: querying source chains #535

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions src/pages/_data/navigation/mainNav.json5
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
{ title: "Identifiers", url: "/build/identifiers/" },
{ title: "Entries", url: "/build/entries/" },
{ title: "Links, Paths, and Anchors", url: "/build/links-paths-and-anchors/" },
{ title: "Querying Source Chains", url: "/build/querying-source-chains/" },
]},
{ title: "Connecting the Parts", url: "/build/connecting-the-parts/", children: [
{ title: "Front End", url: "/build/connecting-a-front-end/" },
Expand Down
1 change: 1 addition & 0 deletions src/pages/build/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Now that you've got some basic concepts and the terms we use for them, it's time
* [Identifiers](/build/identifiers) --- working with hashes and other unique IDs
* [Entries](/build/entries/) --- defining, creating, reading, updating, and deleting data
* [Links, Paths, and Anchors](/build/links-paths-and-anchors/) --- creating relationships between data
* [Querying Source Chains](/build/querying-source-chains/) --- getting data from an agent's history
:::

## Connecting everything together
Expand Down
228 changes: 228 additions & 0 deletions src/pages/build/querying-source-chains.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
---
title: "Querying Source Chains"
---

::: intro
An agent can **query their source chain** for a history of the [records](/build/working-with-data/#entries-actions-and-records-primary-data) they've written, including [**link**](/build/links-paths-and-anchors/) and public and private [**entry**](/build/entries/) data, which includes **capability grants and claims**<!--TODO: link to capabilities -->. They can also query the public portion of another agent's source chain in a [**zome function**](/build/zome-functions/) or [**`validate` callback**](/build/callbacks-and-lifecycle-hooks/#define-a-validate-callback).
:::

An agent's source chain is their record of local state changes. It's a multi-purpose data structure, and can be interpreted in different ways, including as:

* a chronological record of contributions to the shared [**DHT**](/concepts/4_dht/),
* a ledger of changes to a single state,
* a store for private entry data, or
* a store for capability grants and claims.

## Filtering a query

Whether an agent is querying their own source chain or another agent's, you build a query with the [`ChainQueryFilter`](https://docs.rs/holochain_zome_types/latest/holochain_zome_types/query/struct.ChainQueryFilter.html) struct, which has a few filter types:

* <code>sequence_range: <a href="https://docs.rs/holochain_zome_types/latest/holochain_zome_types/query/enum.ChainQueryFilterRange.html">ChainQueryFilterRange</a></code>: A start and end point on the source chain, either:
* `Unbounded`: the beginning and current tip of the chain
* `ActionSeqRange(u32, u32)`: start and end sequence indices, inclusive.
* `ActionHashRange(ActionHash, ActionHash)`: start and end action hashes, inclusive.
* `ActionHashTerminated(ActionHash, u32)`: an action hash plus the _n_th actions preceding it.
* <code>entry_type: Option&lt;Vec&lt;<a href="https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/action/enum.EntryType.html">EntryType</a>&gt;&gt;</code>: Only select the given entry types, which can include both system and app entry types.
* `entry_hashes: Option<HashSet<EntryHash>>`: Only select entry creation actions with the given entry hashes.
* <code>action_type: Option&lt;Vec&lt;<a href="https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/action/enum.ActionType.html">ActionType</a>&gt;&gt;</code>: Only select actions of the given type.
* `include_entries: bool`: Try to retrieve and include entry data for entry creation actions. Private entries will only be included for `query`, not `get_agent_activity`.
* `order_descending: bool`: Return the results in reverse chronological order, newest first and oldest last.

After retrieving the filtered records, you can then further filter them in memory using Rust's standard `Iterator` trait.

### Use the builder interface

Rather than building a struct and having to specify fields you don't need, you can use the builder interface on `ChainQueryFilter`:

```rust
use hdk::prelude::*;
use movies::prelude::*;

let filter_only_movie_updates = ChainQueryFilter::new()
.entry_type(EntryType::App(UnitEntryTypes::Movie.into()))
.action_type(ActionType::Update)
.include_entries(true);

let filter_only_cap_grants_and_claims_newest_first = ChainQueryFilter::new()
.entry_type(EntryType::CapGrant)
.entry_type(EntryType::CapClaim)
.include_entries(true)
.descending();

let filter_first_ten_records = ChainQueryFilter::new()
.sequence_range(ChainQueryFilterRange::ActionSeqRange(0, 9));
```

### Use `ChainQueryFilter` to query a vector of actions or records

If you already have a vector of `Action`s or `Records` in memory, you can apply a `ChainQueryFilter` to them as if you were querying a source chain.

```rust
include hdk::prelude::*;

let actions: Vec<Action> = /* get some actions somehow */;
let movie_update_actions = filter_only_movie_updates.filter_actions(actions);
```

## Query an agent's own source chain

An agent can query their own source chain with the [`query`](https://docs.rs/hdk/latest/hdk/chain/fn.query.html) host function, which takes a `ChainQueryFilter` and returns a `Vec<Record>` wrapped in an `ExternResult`.

```rust
use hdk::prelude::*;

#[hdk_extern]
pub fn get_all_movies_i_authored() -> Vec<Record> {
query(ChainQueryFilter::new()
.entry_type(EntryType::App(UnitEntryTypes::Movie.into()))
.include_entries(true)
)
}
```

## Query another agent's source chain
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a really different topic. You don't get back their source chain in the same way you do locally and it's for quite different purposes.

There's a huge downside, in that you have to fetch and cache their entire chain to do this. Plus it's really for examining an agent's state, not the content of their chain. You see what valid/invalid data they have, the top of their chain, any warrants etc. That's not the same as retrieving your own source chain. I think this would be better saved until we're dealing with warrants and blocking.

This function was actually flagged to be renamed in the run-up to the Holo launch to make it clearer what it does.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed in f71509a


An agent can query another agent's source chain with the [`get_agent_activity`](https://docs.rs/hdk/latest/hdk/chain/fn.get_agent_activity.html) host function.

`get_agent_activity` returns [`AgentActivity`](https://docs.rs/holochain_zome_types/latest/holochain_zome_types/query/struct.AgentActivity.html), a struct with rich info on the agent's source chain status, but it _only returns the action hashes_ of matching records; it's the job of the caller to turn them into records.

```rust
use hdk::prelude::*;

#[hdk_extern]
pub fn get_all_movies_authored_by_other_agent(agent: AgentPubKey) -> ExternResult<Vec<(u32, Option<Record>)>> {
let activity = get_agent_activity(
agent,
ChainQueryFilter::new()
.entry_type(EntryType::App(UnitEntryTypes::Movie.into()))
.include_entries(true),
ActivityRequest::Full
)?;

// Now try to retrieve the records for all of the action hashes, turning
// the whole result into an Err if any record retrieval returns an Err.
let chain_with_entries = activity.valid_activity
.iter()
.map(|a| {
let maybe_record = get(a.1, GetOptions::network())?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could issue thousands of gets and cause them all to be cached. This is so expensive, I'd call it an antipattern. Data that this agent intended to share should be discovered through the DHT with links and the like, ideally not via their chain unless you have a really good reason to audit their activity or something.

Copy link
Collaborator Author

@pdaoust pdaoust Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you suggest a legit but simple use case? The only ones I can think of involve transactions, which are a rabbit hole in the extreme.

Or asked a different way, what would you have called get_agent_activity to make it clearer what it does?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed in f71509a

// Because some records may be irretrievable if no agent is
// currently serving them, remember the action hash so we can try
// retrieving them later.
Ok((a.1, maybe_record))
})
.collect();
Ok(chain_with_entries)
}
```

### Get source chain status summary

`get_agent_activity` is for more than just querying an agent's source chain. It also returns the _status of the agent's source chain_, which includes evidence of [source chain forks](/resources/glossary/#fork-source-chain) and invalid actions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the primary intention of the function, and why I think it's probably not meant for this page.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in f71509a and I've created an issue #537 so we don't miss writing about it in the future.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed in f71509a ; created an issue for documenting get_agent_activity


To quickly check the status without retrieving the source chain's action hashes, pass `ActivityRequest::Status` to `get_agent_activity` and look at the `status` field of the return value:

```rust
use hdk::prelude::*;

#[hdk_extern]
pub fn check_status_of_peer_source_chain(agent: AgentPubKey) -> ExternResult<ChainStatus> {
let activity_summary = get_agent_activity(
agent,
ChainQueryFilter::new()
.sequence_range(ChainQueryFilterRange::Unbounded),
ActivityRequest.Status
)?;
Ok(activity_summary.status)
}
```

[This status can be one of](https://docs.rs/holochain_zome_types/latest/holochain_zome_types/query/enum.ChainStatus.html):

* `Empty`: No source chain activity found for the agent.
* <code>Valid(<a href="https://docs.rs/hdk/latest/hdk/prelude/struct.ChainHead.html">ChainHead</a>)</code>: The source chain is valid, with its newest action's sequence index and hash given.
* <code>Forked(<a href="https://docs.rs/holochain_zome_types/latest/holochain_zome_types/query/struct.ChainFork.html">ChainFork</a>)</code>: The source chain has been [forked](/resources/glossary/#fork-source-chain), with the sequence index and conflicting action hashes of the fork point given.
* <code>Invalid(<a href="https://docs.rs/hdk/latest/hdk/prelude/struct.ChainHead.html">ChainHead</a>)</code>: An invalid record was found at the given sequence index and action hash.

## Query another agent's source chain for validation

Validation imposes an extra constraint on source chain queries. A source chain can grow over time, including branching or forking. That means a source chain, when retrieved by agent public key alone, is a non-deterministic source of data, which you can't use in validation<!-- TODO: link to validation page -->. Instead, you can use [`must_get_agent_activity`](https://docs.rs/hdi/latest/hdi/chain/fn.must_get_agent_activity.html), whose [filter struct](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/chain/struct.ChainFilter.html) and return value remove non-determinism.

`must_get_agent_activity` only allows you to select a contiguous, bounded slice of a source chain, and doesn't return any information about the validity of the actions in that slice or the chain as a whole.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here would be a good place to suggest that this is only used within RegisterAgentActivity validation

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


<!-- TODO: move this to validation page or must_get_* page -->

This example fills out a `validate_update_movie` stub function, generated by the scaffolding tool, to enforce that an agent may only edit a movie listing three times.

```rust
use hdi::prelude::*;

pub fn validate_update_movie(
action: Update,
_movie: Movie,
original_action: EntryCreationAction,
_original_movie: Movie,
) -> ExternResult<ValidateCallbackResult> {
let result = must_get_agent_activity(
// Get the agent hash from the action itself.
action.author,
// You can only validate an action based on the source chain records
// that precede it, so use the previous action hash as the `chain_top`
// argument in this query.
// We don't specify a range here, so it defaults to
// `ChainFilters::ToGenesis`. We also don't specify whether to include
// entries, so it defaults to false.
ChainFilter::new(action.prev_action)
)?;

// The result is a vector of
// holochain_integrity_types::op::RegisterAgentActivity DHT ops, from
// which we can get the actions.
let result_as_actions = result
.iter()
.map(|o| o.action.hashed.content)
.collect();

// Now we build and use a filter to only get updates for `Movie` entries.
let filter_movie_update_actions = ChainQueryFilter::new()
.action_type(ActionType::Update)
.entry_type(EntryType::App(EntryTypes::Movie.into()));
let movie_update_actions: Vec<Update> = filter_movie_update_actions
.filter_actions(result_as_actions)
.iter()
.map(|a| a.try_into().unwrap())
.collect();

// Now find out how many times we've tried to update the same `Movie` as
// we're trying to update now.
let times_I_updated_this_movie = movie_update_actions
.fold(1, |c, a| c + if a.original_action_address == action.original_action_address { 1 } else { 0 });

if times_I_updated_this_movie > 3 {
Ok(ValidateCallbackResult::Invalid("Already tried to update the same movie action 3 times"))
} else {
Ok(ValidateCallbackResult::Valid)
}
}
```

## Reference

* [`holochain_zome_types::query::ChainQueryFilter`](https://docs.rs/holochain_zome_types/latest/holochain_zome_types/query/struct.ChainQueryFilter.html)
* [`holochain_zome_types::query::ChainQueryFilterRange`](https://docs.rs/holochain_zome_types/latest/holochain_zome_types/query/enum.ChainQueryFilterRange.html)
* [`holochain_integrity_types::action::EntryType`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/action/enum.EntryType.html)
* [`holochain_integrity_types::action::ActionType`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/action/enum.ActionType.html)
* [`hdk::chain::query`](https://docs.rs/hdk/latest/hdk/chain/fn.query.html)
* [`holochain_integrity_types::record::Record`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/record/struct.Record.html)
* [`hdk::chain::get_agent_activity`](https://docs.rs/hdk/latest/hdk/chain/fn.get_agent_activity.html)
* [`holochain_zome_types::query::ChainStatus`](https://docs.rs/holochain_zome_types/latest/holochain_zome_types/query/enum.ChainStatus.html)
* [`holochain_zome_types::query::AgentActivity`](https://docs.rs/holochain_zome_types/latest/holochain_zome_types/query/struct.AgentActivity.html)
* [`holochain_zome_types::query::ActivityRequest`](https://docs.rs/holochain_zome_types/latest/holochain_zome_types/query/enum.ActivityRequest.html)
* [`hdi::chain::must_get_agent_activity`](https://docs.rs/hdi/latest/hdi/chain/fn.must_get_agent_activity.html)
* [`holochain_integrity_types::chain::ChainFilter`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/chain/struct.ChainFilter.html)

## Further reading

* [Core Concepts: Source Chain](/concepts/3_source_chain/)
* [Core Concepts: Validation](/concepts/7_validation/)
* [Build Guide: Working With Data: Entries, actions, and records](/build/working-with-data/#entries-actions-and-records-primary-data)
<!-- TODO: add validation build guide -->
1 change: 1 addition & 0 deletions src/pages/build/working-with-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ title: Working With Data
* [Identifiers](/build/identifiers/) --- understanding and using addresses of different types
* [Entries](/build/entries/) --- creating, reading, updating, and deleting
* [Links, Paths, and Anchors](/build/links-paths-and-anchors/) --- creating and deleting
* [Querying Source Chains](/build/querying-source-chains/) --- getting data from an agent's history
:::

::: intro
Expand Down