Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
radicle: Fix panic when reading from SQLite database fails
Merged lorenz opened 8 months ago

I was greeted by rad patch redact with

called `Result::unwrap()` on an `Err` value:

  Error { code: None, message: Some("failed to convert") }

stack backtrace:

[…]
   3: core::result::Result<T,E>::unwrap
         at …/rust-1.88.0/lib/rustlib/src/rust/library/core/src/result.rs:1137:23
   4: sqlite::cursor::Row::read
         at …/index.crates.io-1949cf8c6b5b557f/sqlite-0.32.0/src/cursor.rs:136:9
   5: radicle::cob::patch::cache::query::find_by_revision
         at ./crates/radicle/src/cob/patch/cache.rs:624:65
   6: <… as radicle::cob::patch::cache::Patches>::find_by_revision
         at ./crates/radicle/src/cob/patch/cache.rs:553:9
   7: radicle_cli::commands::rad_patch::redact::run
         at ./crates/radicle-cli/src/commands/patch/redact.rs:23:9
   8: radicle_cli::commands::rad_patch::run
         at ./crates/radicle-cli/src/commands/patch.rs:1026:13
[…]

It turns out that sqlite::cursor::Row::read is the panicky version of sqlite::cursor::Row::try_read (which returns a Result).

While it is somewhat rare that SQLite reads fail, it is not unheard of. In radicle-cli it might not be critical, but also radicle-protocol and radicle-fetch are affected, and they could potentially panic a radicle-node process.

Use try_read instead, and propagate down the error handling.

15 files changed +242 -144 a568e7f4 192cc993
modified crates/radicle-cli/src/commands/follow.rs
@@ -147,41 +147,45 @@ pub fn following(profile: &Profile, alias: Option<Alias>) -> anyhow::Result<()>
        term::format::default(String::from("Policy")),
    ]);
    t.divider();
-

-
    match alias {
-
        None => push_policies(&mut t, &aliases, store.follow_policies()?),
-
        Some(alias) => push_policies(
-
            &mut t,
-
            &aliases,
-
            store
-
                .follow_policies()?
-
                .filter(|p| p.alias.as_ref().is_some_and(|alias_| *alias_ == alias)),
-
        ),
-
    };
+
    push_policies(&mut t, &aliases, store.follow_policies()?, &alias);
    t.print();
-

    Ok(())
}

fn push_policies(
    t: &mut Table<3, Paint<String>>,
    aliases: &impl AliasStore,
-
    policies: impl Iterator<Item = policy::FollowPolicy>,
+
    policies: impl Iterator<Item = Result<policy::FollowPolicy, policy::store::Error>>,
+
    filter: &Option<Alias>,
) {
-
    for policy::FollowPolicy {
-
        nid: id,
-
        alias,
-
        policy,
-
    } in policies
-
    {
-
        t.push([
-
            term::format::highlight(Did::from(id).to_string()),
-
            match alias {
-
                None => term::format::secondary(fallback_alias(&id, aliases)),
-
                Some(alias) => term::format::secondary(alias.to_string()),
-
            },
-
            term::format::secondary(policy.to_string()),
-
        ]);
+
    for policy in policies {
+
        match policy {
+
            Ok(policy::FollowPolicy {
+
                nid: id,
+
                alias,
+
                policy,
+
            }) => {
+
                if match (filter, &alias) {
+
                    (None, _) => false,
+
                    (Some(filter), Some(alias)) => *filter != *alias,
+
                    (Some(_), None) => true,
+
                } {
+
                    continue;
+
                }
+

+
                t.push([
+
                    term::format::highlight(Did::from(id).to_string()),
+
                    match alias {
+
                        None => term::format::secondary(fallback_alias(&id, aliases)),
+
                        Some(alias) => term::format::secondary(alias.to_string()),
+
                    },
+
                    term::format::secondary(policy.to_string()),
+
                ]);
+
            }
+
            Err(err) => {
+
                term::error(format!("Failed to read a follow policy: {err}"));
+
            }
+
        }
    }
}

modified crates/radicle-cli/src/commands/seed.rs
@@ -203,21 +203,28 @@ pub fn seeding(profile: &Profile) -> anyhow::Result<()> {
    ]);
    t.divider();

-
    for policy::SeedPolicy { rid, policy } in store.seed_policies()? {
-
        let id = rid.to_string();
-
        let name = storage
-
            .repository(rid)
-
            .and_then(|repo| repo.project().map(|proj| proj.name().to_string()))
-
            .unwrap_or_default();
-
        let scope = policy.scope().unwrap_or_default().to_string();
-
        let policy = term::format::policy(&Policy::from(policy));
-

-
        t.push([
-
            term::format::tertiary(id),
-
            name.into(),
-
            policy,
-
            term::format::dim(scope),
-
        ])
+
    for policy in store.seed_policies()? {
+
        match policy {
+
            Ok(policy::SeedPolicy { rid, policy }) => {
+
                let id = rid.to_string();
+
                let name = storage
+
                    .repository(rid)
+
                    .and_then(|repo| repo.project().map(|proj| proj.name().to_string()))
+
                    .unwrap_or_default();
+
                let scope = policy.scope().unwrap_or_default().to_string();
+
                let policy = term::format::policy(&Policy::from(policy));
+

+
                t.push([
+
                    term::format::tertiary(id),
+
                    name.into(),
+
                    policy,
+
                    term::format::dim(scope),
+
                ])
+
            }
+
            Err(err) => {
+
                term::error(format!("Failed to read a seeding policy: {err}"));
+
            }
+
        }
    }

    if t.is_empty() {
modified crates/radicle-cli/src/commands/stats.rs
@@ -126,7 +126,7 @@ pub fn run(_options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
            .into_iter()
            .next()
            .unwrap()?;
-
        let count = row.read::<i64, _>(0) as usize;
+
        let count = row.try_read::<i64, _>(0)? as usize;

        stats.repos.unique = count;
    }
@@ -141,7 +141,7 @@ pub fn run(_options: Options, ctx: impl term::Context) -> anyhow::Result<()> {

        // SAFETY: `COUNT` always returns a row.
        let row = stmt.iter().next().unwrap()?;
-
        stats.nodes.online_daily = row.read::<i64, _>(0) as usize;
+
        stats.nodes.online_daily = row.try_read::<i64, _>(0)? as usize;

        let since = now - LocalDuration::from_mins(60 * 24 * 7); // 1 week.
        stmt.reset()?;
@@ -149,7 +149,7 @@ pub fn run(_options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
        stmt.bind((2, now.as_millis() as i64))?;

        let row = stmt.iter().next().unwrap()?;
-
        stats.nodes.online_weekly = row.read::<i64, _>(0) as usize;
+
        stats.nodes.online_weekly = row.try_read::<i64, _>(0)? as usize;
    }

    {
@@ -168,7 +168,7 @@ pub fn run(_options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
            .next()
            // SAFETY: `COUNT` always returns a row.
            .unwrap()?;
-
        let count = row.read::<i64, _>(0) as usize;
+
        let count = row.try_read::<i64, _>(0)? as usize;

        stats.nodes.public_daily = count;
    }
@@ -187,7 +187,7 @@ pub fn run(_options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
            .next()
            // SAFETY: `COUNT` always returns a row.
            .unwrap()?;
-
        let count = row.read::<i64, _>(0) as usize;
+
        let count = row.try_read::<i64, _>(0)? as usize;

        stats.nodes.seeding_weekly = count;
    }
modified crates/radicle-fetch/src/policy.rs
@@ -30,9 +30,22 @@ impl Allowed {
                let nodes = config
                    .follow_policies()
                    .map_err(|err| error::Policy::FailedNodes { rid, err })?;
-
                let followed: HashSet<_> = nodes
-
                    .filter_map(|node| (node.policy == Policy::Allow).then_some(node.nid))
-
                    .collect();
+

+
                let mut followed = HashSet::new();
+

+
                for node in nodes {
+
                    let node = match node {
+
                        Ok(policy) => policy,
+
                        Err(err) => {
+
                            log::error!(target: "fetch", "Failed to read follow policy for {rid}: {err}");
+
                            continue;
+
                        }
+
                    };
+

+
                    if node.policy == Policy::Allow {
+
                        followed.insert(node.nid);
+
                    }
+
                }

                Ok(Allowed::Followed { remotes: followed })
            }
@@ -62,10 +75,23 @@ impl BlockList {
    }

    pub fn from_config(config: &Config<Read>) -> Result<BlockList, error::Blocked> {
-
        Ok(config
-
            .follow_policies()?
-
            .filter_map(|entry| (entry.policy == Policy::Block).then_some(entry.nid))
-
            .collect())
+
        let mut blocked = HashSet::new();
+

+
        for entry in config.follow_policies()? {
+
            let entry = match entry {
+
                Ok(entry) => entry,
+
                Err(err) => {
+
                    log::error!(target: "fetch", "Failed to read follow policy: {err}");
+
                    continue;
+
                }
+
            };
+

+
            if entry.policy == Policy::Block {
+
                blocked.insert(entry.nid);
+
            }
+
        }
+

+
        Ok(BlockList(blocked))
    }
}

modified crates/radicle-protocol/src/service.rs
@@ -570,13 +570,7 @@ where
            // Nb. This is potentially slow if we have lots of repos. We should probably
            // only re-compute the filter when we've unseeded a certain amount of repos
            // and the filter is really out of date.
-
            //
-
            // TODO: Share this code with initialization code.
-
            self.filter = Filter::new(
-
                self.policies
-
                    .seed_policies()?
-
                    .filter_map(|t| (t.policy.is_allow()).then_some(t.rid)),
-
            );
+
            self.filter = Filter::allowed_by(self.policies.seed_policies()?);
            // Update and announce new inventory.
            if let Err(e) = self.remove_inventory(id) {
                error!(target: "service", "Error updating inventory after unseed: {e}");
@@ -750,11 +744,7 @@ where
            .remove_inventories(private.iter(), &nid)?;

        // Setup subscription filter for seeded repos.
-
        self.filter = Filter::new(
-
            self.policies
-
                .seed_policies()?
-
                .filter_map(|t| (t.policy.is_allow()).then_some(t.rid)),
-
        );
+
        self.filter = Filter::allowed_by(self.policies.seed_policies()?);
        // Connect to configured peers.
        let addrs = self.config.connect.clone();
        for (id, addr) in addrs.into_iter().map(|ca| ca.into()) {
@@ -2510,10 +2500,16 @@ where

    /// Fetch all repositories that are seeded but missing from storage.
    fn fetch_missing_repositories(&mut self) -> Result<(), Error> {
-
        // TODO(finto): could filter the policies based on the continue checks
-
        // below, but `storage.contains` is fallible
        let policies = self.policies.seed_policies()?.collect::<Vec<_>>();
        for policy in policies {
+
            let policy = match policy {
+
                Ok(policy) => policy,
+
                Err(err) => {
+
                    log::error!(target: "protocol::filter", "Failed to read seed policy: {err}");
+
                    continue;
+
                }
+
            };
+

            let rid = policy.rid;

            if !policy.is_allow() {
modified crates/radicle-protocol/src/service/filter.rs
@@ -59,6 +59,30 @@ impl Filter {
        Self(bloom)
    }

+
    pub fn allowed_by(
+
        policies: impl Iterator<
+
            Item = Result<radicle::node::policy::SeedPolicy, radicle::node::policy::store::Error>,
+
        >,
+
    ) -> Self {
+
        let mut ids = Vec::new();
+

+
        for seed in policies {
+
            let seed = match seed {
+
                Ok(seed) => seed,
+
                Err(err) => {
+
                    log::error!(target: "protocol::filter", "Failed to read seed policy: {err}");
+
                    continue;
+
                }
+
            };
+

+
            if seed.policy.is_allow() {
+
                ids.push(seed.rid);
+
            }
+
        }
+

+
        Self::new(ids)
+
    }
+

    /// Empty filter with nothing set.
    pub fn empty() -> Self {
        Self(BloomFilter::from(vec![0x0; FILTER_SIZE_S]))
modified crates/radicle-protocol/src/service/gossip/store.rs
@@ -135,7 +135,7 @@ impl Store for Database {

        if let Some(row) = stmt.into_iter().next() {
            let row = row?;
-
            let id = row.read::<i64, _>("rowid");
+
            let id = row.try_read::<i64, _>("rowid")?;

            Ok(Some(id as AnnouncementId))
        } else {
@@ -360,9 +360,9 @@ mod parse {
    use super::*;

    pub fn announcement(row: sql::Row) -> Result<(AnnouncementId, Announcement), Error> {
-
        let id = row.read::<i64, _>("rowid") as AnnouncementId;
-
        let node = row.read::<NodeId, _>("node");
-
        let gt = row.read::<GossipType, _>("type");
+
        let id = row.try_read::<i64, _>("rowid")? as AnnouncementId;
+
        let node = row.try_read::<NodeId, _>("node")?;
+
        let gt = row.try_read::<GossipType, _>("type")?;
        let message = match gt {
            GossipType::Refs => {
                let ann = row.try_read::<RefsAnnouncement, _>("message")?;
@@ -377,8 +377,8 @@ mod parse {
                AnnouncementMessage::Node(ann)
            }
        };
-
        let signature = row.read::<Signature, _>("signature");
-
        let timestamp = row.read::<Timestamp, _>("timestamp");
+
        let signature = row.try_read::<Signature, _>("signature")?;
+
        let timestamp = row.try_read::<Timestamp, _>("timestamp")?;

        debug_assert_eq!(timestamp, message.timestamp());

modified crates/radicle/src/cob/cache/migrations/2.rs
@@ -27,8 +27,8 @@ pub fn run(

    for row in rows {
        let row = row?;
-
        let id = row.read::<&str, _>("id");
-
        let mut patch = json::from_str::<json::Value>(row.read::<&str, _>("patch"))
+
        let id = row.try_read::<&str, _>("id")?;
+
        let mut patch = json::from_str::<json::Value>(row.try_read::<&str, _>("patch")?)
            .map_err(Error::MalformedJson)?;
        let patch = patch.as_object_mut().ok_or(Error::MalformedJsonSchema)?;
        let revisions = patch["revisions"]
@@ -117,7 +117,7 @@ mod tests {
            })
            .unwrap();

-
        let patch = row.read::<&str, _>("patch");
+
        let patch = row.try_read::<&str, _>("patch").unwrap();
        let actual: serde_json::Value = serde_json::from_str(patch).unwrap();
        let expected: serde_json::Value = serde_json::from_str(PATCH_V2).unwrap();

modified crates/radicle/src/cob/issue/cache.rs
@@ -440,8 +440,8 @@ pub struct IssuesIter<'a> {

impl IssuesIter<'_> {
    fn parse_row(row: sql::Row) -> Result<(IssueId, Issue), Error> {
-
        let id = IssueId::from_str(row.read::<&str, _>("id"))?;
-
        let issue = serde_json::from_str::<Issue>(row.read::<&str, _>("issue"))?;
+
        let id = IssueId::from_str(row.try_read::<&str, _>("id")?)?;
+
        let issue = serde_json::from_str::<Issue>(row.try_read::<&str, _>("issue")?)?;
        Ok((id, issue))
    }
}
@@ -537,7 +537,7 @@ mod query {
        match stmt.into_iter().next().transpose()? {
            None => Ok(None),
            Some(row) => {
-
                let issue = row.read::<&str, _>("issue");
+
                let issue = row.try_read::<&str, _>("issue")?;
                let issue = serde_json::from_str(issue)?;
                Ok(Some(issue))
            }
@@ -597,8 +597,8 @@ mod query {
        stmt.into_iter()
            .try_fold(IssueCounts::default(), |mut counts, row| {
                let row = row?;
-
                let count = row.read::<i64, _>("count") as usize;
-
                let status = serde_json::from_str::<State>(row.read::<&str, _>("state"))?;
+
                let count = row.try_read::<i64, _>("count")? as usize;
+
                let status = serde_json::from_str::<State>(row.try_read::<&str, _>("state")?)?;
                match status {
                    State::Closed { .. } => counts.closed += count,
                    State::Open => counts.open += count,
modified crates/radicle/src/cob/patch/cache.rs
@@ -427,8 +427,8 @@ pub struct PatchesIter<'a> {

impl PatchesIter<'_> {
    fn parse_row(row: sql::Row) -> Result<(PatchId, Patch), Error> {
-
        let id = PatchId::from_str(row.read::<&str, _>("id"))?;
-
        let patch = serde_json::from_str::<Patch>(row.read::<&str, _>("patch"))
+
        let id = PatchId::from_str(row.try_read::<&str, _>("id")?)?;
+
        let patch = serde_json::from_str::<Patch>(row.try_read::<&str, _>("patch")?)
            .map_err(|e| Error::Object(id, e))?;
        Ok((id, patch))
    }
@@ -592,7 +592,7 @@ mod query {
        match stmt.into_iter().next().transpose()? {
            None => Ok(None),
            Some(row) => {
-
                let patch = row.read::<&str, _>("patch");
+
                let patch = row.try_read::<&str, _>("patch")?;
                let patch = serde_json::from_str(patch).map_err(|e| Error::Object(*id, e))?;
                Ok(Some(patch))
            }
@@ -618,10 +618,11 @@ mod query {
        match stmt.into_iter().next().transpose()? {
            None => Ok(None),
            Some(row) => {
-
                let id = PatchId::from_str(row.read::<&str, _>("id"))?;
-
                let patch = serde_json::from_str::<Patch>(row.read::<&str, _>("patch"))
+
                let id = PatchId::from_str(row.try_read::<&str, _>("id")?)?;
+
                let patch = serde_json::from_str::<Patch>(row.try_read::<&str, _>("patch")?)
                    .map_err(|e| Error::Object(id, e))?;
-
                let revision = serde_json::from_str::<Revision>(row.read::<&str, _>("revision"))?;
+
                let revision =
+
                    serde_json::from_str::<Revision>(row.try_read::<&str, _>("revision")?)?;
                Ok(Some(ByRevision {
                    id,
                    patch,
@@ -686,8 +687,8 @@ mod query {
        stmt.into_iter()
            .try_fold(PatchCounts::default(), |mut counts, row| {
                let row = row?;
-
                let count = row.read::<i64, _>("count") as usize;
-
                let status = serde_json::from_str::<State>(row.read::<&str, _>("state"))?;
+
                let count = row.try_read::<i64, _>("count")? as usize;
+
                let status = serde_json::from_str::<State>(row.try_read::<&str, _>("state")?)?;
                match status {
                    State::Draft => counts.draft += count,
                    State::Open { .. } => counts.open += count,
modified crates/radicle/src/node/address/store.rs
@@ -115,15 +115,15 @@ impl Store for Database {
        stmt.bind((1, node))?;

        if let Some(Ok(row)) = stmt.into_iter().next() {
-
            let version = row.read::<i64, _>("version").try_into()?;
-
            let features = row.read::<node::Features, _>("features");
-
            let alias = Alias::from_str(row.read::<&str, _>("alias"))?;
-
            let timestamp = row.read::<Timestamp, _>("timestamp");
-
            let pow = row.read::<i64, _>("pow") as u32;
-
            let agent = row.read::<UserAgent, _>("agent");
-
            let penalty = row.read::<i64, _>("penalty").min(u8::MAX as i64);
+
            let version = row.try_read::<i64, _>("version")?.try_into()?;
+
            let features = row.try_read::<node::Features, _>("features")?;
+
            let alias = Alias::from_str(row.try_read::<&str, _>("alias")?)?;
+
            let timestamp = row.try_read::<Timestamp, _>("timestamp")?;
+
            let pow = row.try_read::<i64, _>("pow")? as u32;
+
            let agent = row.try_read::<UserAgent, _>("agent")?;
+
            let penalty = row.try_read::<i64, _>("penalty")?.min(u8::MAX as i64);
            let penalty = Penalty(penalty as u8);
-
            let banned = row.read::<i64, _>("banned").is_positive();
+
            let banned = row.try_read::<i64, _>("banned")?.is_positive();
            let addrs = self.addresses_of(node)?;

            Ok(Some(Node {
@@ -154,8 +154,8 @@ impl Store for Database {

        if let Some(row) = stmt.into_iter().next() {
            let row = row?;
-
            let addr_banned = row.read::<i64, _>(0).is_positive();
-
            let node_banned = row.read::<i64, _>(1).is_positive();
+
            let addr_banned = row.try_read::<i64, _>(0)?.is_positive();
+
            let node_banned = row.try_read::<i64, _>(1)?.is_positive();

            Ok(node_banned || addr_banned)
        } else {
@@ -183,16 +183,16 @@ impl Store for Database {

        for row in stmt.into_iter() {
            let row = row?;
-
            let _typ = row.read::<AddressType, _>("type");
-
            let addr = row.read::<Address, _>("value");
-
            let source = row.read::<Source, _>("source");
+
            let _typ = row.try_read::<AddressType, _>("type")?;
+
            let addr = row.try_read::<Address, _>("value")?;
+
            let source = row.try_read::<Source, _>("source")?;
            let last_attempt = row
                .read::<Option<i64>, _>("last_attempt")
                .map(|t| LocalTime::from_millis(t as u128));
            let last_success = row
                .read::<Option<i64>, _>("last_success")
                .map(|t| LocalTime::from_millis(t as u128));
-
            let banned = row.read::<i64, _>("banned").is_positive();
+
            let banned = row.try_read::<i64, _>("banned")?.is_positive();

            addrs.push(KnownAddress {
                addr,
@@ -212,7 +212,7 @@ impl Store for Database {
            .into_iter()
            .next()
            .ok_or(Error::NoRows)??;
-
        let count = row.read::<i64, _>(0) as usize;
+
        let count = row.try_read::<i64, _>(0)? as usize;

        Ok(count)
    }
@@ -224,7 +224,7 @@ impl Store for Database {
            .into_iter()
            .next()
            .ok_or(Error::NoRows)??;
-
        let count = row.read::<i64, _>(0) as usize;
+
        let count = row.try_read::<i64, _>(0)? as usize;

        Ok(count)
    }
@@ -299,17 +299,17 @@ impl Store for Database {
        let mut entries = Vec::new();

        while let Some(Ok(row)) = stmt.next() {
-
            let node = row.read::<NodeId, _>("node");
-
            let _typ = row.read::<AddressType, _>("type");
-
            let addr = row.read::<Address, _>("value");
-
            let source = row.read::<Source, _>("source");
-
            let last_success = row.read::<Option<i64>, _>("last_success");
-
            let last_attempt = row.read::<Option<i64>, _>("last_attempt");
+
            let node = row.try_read::<NodeId, _>("node")?;
+
            let _typ = row.try_read::<AddressType, _>("type")?;
+
            let addr = row.try_read::<Address, _>("value")?;
+
            let source = row.try_read::<Source, _>("source")?;
+
            let last_success = row.try_read::<Option<i64>, _>("last_success")?;
+
            let last_attempt = row.try_read::<Option<i64>, _>("last_attempt")?;
            let last_success = last_success.map(|t| LocalTime::from_millis(t as u128));
            let last_attempt = last_attempt.map(|t| LocalTime::from_millis(t as u128));
-
            let version = row.read::<i64, _>("version").try_into()?;
-
            let banned = row.read::<i64, _>("banned").is_positive();
-
            let penalty = row.read::<i64, _>("penalty");
+
            let version = row.try_read::<i64, _>("version")?.try_into()?;
+
            let banned = row.try_read::<i64, _>("banned")?.is_positive();
+
            let penalty = row.try_read::<i64, _>("penalty")?;
            let penalty = Penalty(penalty as u8); // Clamped at `u8::MAX`.

            entries.push(AddressEntry {
modified crates/radicle/src/node/policy/config.rs
@@ -106,9 +106,20 @@ impl<T> Config<T> {
                let nodes = self
                    .follow_policies()
                    .map_err(|err| FailedNodes { rid: *rid, err })?;
-
                let mut followed: HashSet<_> = nodes
-
                    .filter_map(|node| (node.policy == Policy::Allow).then_some(node.nid))
-
                    .collect();
+

+
                let mut followed: HashSet<_> = HashSet::new();
+
                for node in nodes {
+
                    let node = match node {
+
                        Ok(node) => node,
+
                        Err(err) => {
+
                            log::warn!(target: "service", "Failed to read follow policy: {err}");
+
                            continue;
+
                        }
+
                    };
+
                    if node.policy == Policy::Allow {
+
                        followed.insert(node.nid);
+
                    }
+
                }

                if let Ok(repo) = storage.repository(*rid) {
                    let delegates = repo
modified crates/radicle/src/node/policy/store.rs
@@ -257,13 +257,13 @@ impl<T> Store<T> {
        stmt.bind((1, id))?;

        if let Some(Ok(row)) = stmt.into_iter().next() {
-
            let alias = row.read::<&str, _>("alias");
+
            let alias = row.try_read::<&str, _>("alias")?;
            let alias = alias
                .is_empty()
                .not()
                .then_some(alias.to_owned())
                .and_then(|s| Alias::from_str(&s).ok());
-
            let policy = row.read::<Policy, _>("policy");
+
            let policy = row.try_read::<Policy, _>("policy")?;

            return Ok(Some(FollowPolicy {
                nid: *id,
@@ -283,9 +283,9 @@ impl<T> Store<T> {
        stmt.bind((1, id))?;

        if let Some(Ok(row)) = stmt.into_iter().next() {
-
            let policy = match row.read::<Policy, _>("policy") {
+
            let policy = match row.try_read::<Policy, _>("policy")? {
                Policy::Allow => SeedingPolicy::Allow {
-
                    scope: row.read::<Scope, _>("scope"),
+
                    scope: row.try_read::<Scope, _>("scope")?,
                },
                Policy::Block => SeedingPolicy::Block,
            };
@@ -329,25 +329,38 @@ pub struct FollowPolicies<'a> {
}

impl Iterator for FollowPolicies<'_> {
-
    type Item = FollowPolicy;
+
    type Item = Result<FollowPolicy, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        let row = self.inner.next()?;
        let Ok(row) = row else { return self.next() };
-
        let id = row.read("id");
-
        let alias = row.read::<&str, _>("alias").to_owned();
+

+
        let id = match row.try_read("id") {
+
            Ok(id) => id,
+
            Err(err) => return Some(Err(err.into())),
+
        };
+

+
        let alias = match row.try_read::<&str, _>("alias") {
+
            Ok(alias) => alias.to_owned(),
+
            Err(err) => return Some(Err(err.into())),
+
        };
+

        let alias = alias
            .is_empty()
            .not()
            .then_some(alias.to_owned())
            .and_then(|s| Alias::from_str(&s).ok());
-
        let policy = row.read::<Policy, _>("policy");

-
        Some(FollowPolicy {
+
        let policy = match row.try_read::<Policy, _>("policy") {
+
            Ok(policy) => policy,
+
            Err(err) => return Some(Err(err.into())),
+
        };
+

+
        Some(Ok(FollowPolicy {
            nid: id,
            alias,
            policy,
-
        })
+
        }))
    }
}

@@ -356,19 +369,35 @@ pub struct SeedPolicies<'a> {
}

impl Iterator for SeedPolicies<'_> {
-
    type Item = SeedPolicy;
+
    type Item = Result<SeedPolicy, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        let row = self.inner.next()?;
        let Ok(row) = row else { return self.next() };
-
        let id = row.read("id");
-
        let policy = match row.read::<Policy, _>("policy") {
-
            Policy::Allow => SeedingPolicy::Allow {
-
                scope: row.read::<Scope, _>("scope"),
-
            },
-
            Policy::Block => SeedingPolicy::Block,
+

+
        let id = match row.try_read("id") {
+
            Ok(id) => id,
+
            Err(err) => return Some(Err(err.into())),
+
        };
+

+
        let policy = match row.try_read::<Policy, _>("policy") {
+
            Ok(policy) => policy,
+
            Err(err) => return Some(Err(err.into())),
        };
-
        Some(SeedPolicy { rid: id, policy })
+

+
        match policy {
+
            Policy::Allow => match row.try_read::<Scope, _>("scope") {
+
                Ok(scope) => Some(Ok(SeedPolicy {
+
                    rid: id,
+
                    policy: SeedingPolicy::Allow { scope },
+
                })),
+
                Err(err) => Some(Err(err.into())),
+
            },
+
            Policy::Block => Some(Ok(SeedPolicy {
+
                rid: id,
+
                policy: SeedingPolicy::Block,
+
            })),
+
        }
    }
}

@@ -457,9 +486,9 @@ mod test {
            assert!(db.follow(id, None).unwrap());
        }
        let mut entries = db.follow_policies().unwrap();
-
        assert_matches!(entries.next(), Some(FollowPolicy { nid, .. }) if nid == ids[0]);
-
        assert_matches!(entries.next(), Some(FollowPolicy { nid, .. }) if nid == ids[1]);
-
        assert_matches!(entries.next(), Some(FollowPolicy { nid, .. }) if nid == ids[2]);
+
        assert_matches!(entries.next(), Some(Ok(FollowPolicy { nid, .. })) if nid == ids[0]);
+
        assert_matches!(entries.next(), Some(Ok(FollowPolicy { nid, .. })) if nid == ids[1]);
+
        assert_matches!(entries.next(), Some(Ok(FollowPolicy { nid, .. })) if nid == ids[2]);
    }

    #[test]
@@ -471,9 +500,9 @@ mod test {
            assert!(db.seed(id, Scope::All).unwrap());
        }
        let mut entries = db.seed_policies().unwrap();
-
        assert_matches!(entries.next(), Some(SeedPolicy { rid, .. }) if rid == ids[0]);
-
        assert_matches!(entries.next(), Some(SeedPolicy { rid, .. }) if rid == ids[1]);
-
        assert_matches!(entries.next(), Some(SeedPolicy { rid, .. }) if rid == ids[2]);
+
        assert_matches!(entries.next(), Some(Ok(SeedPolicy { rid, .. })) if rid == ids[0]);
+
        assert_matches!(entries.next(), Some(Ok(SeedPolicy { rid, .. })) if rid == ids[1]);
+
        assert_matches!(entries.next(), Some(Ok(SeedPolicy { rid, .. })) if rid == ids[2]);
    }

    #[test]
modified crates/radicle/src/node/refs/store.rs
@@ -154,7 +154,7 @@ impl Store for Database {
            .into_iter()
            .next()
            .ok_or(Error::NoRows)??;
-
        let count = row.read::<i64, _>(0) as usize;
+
        let count = row.try_read::<i64, _>(0)? as usize;

        Ok(count)
    }
modified crates/radicle/src/node/routing.rs
@@ -108,7 +108,7 @@ impl Store for Database {
        stmt.bind((2, node))?;

        if let Some(Ok(row)) = stmt.into_iter().next() {
-
            return Ok(Some(row.read::<Timestamp, _>("timestamp")));
+
            return Ok(Some(row.try_read::<Timestamp, _>("timestamp")?));
        }
        Ok(None)
    }