radicle: Fix panic when reading from SQLite database fails

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.
This commit is contained in:
Lorenz Leutgeb 2025-08-23 21:34:28 +02:00 committed by Fintan Halpenny
parent a568e7f484
commit 192cc993a8
15 changed files with 241 additions and 143 deletions

View File

@ -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}"));
}
}
}
}

View File

@ -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));
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),
])
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() {

View File

@ -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;
}

View File

@ -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))
}
}

View File

@ -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() {

View File

@ -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]))

View File

@ -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 {
@ -342,9 +342,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")?;
@ -359,8 +359,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());

View File

@ -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();

View File

@ -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,

View File

@ -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,

View File

@ -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 {

View File

@ -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

View File

@ -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())),
};
Some(SeedPolicy { rid: id, policy })
let policy = match row.try_read::<Policy, _>("policy") {
Ok(policy) => policy,
Err(err) => return Some(Err(err.into())),
};
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]

View File

@ -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)
}

View File

@ -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)
}