commit 425c2f721c6cefa4b90c0f4833e2fb080c45c4ec
parent 436d5958715f3b68e179b2dec05325d4bfc5d061
Author: triesap <tyson@radroots.org>
Date: Fri, 26 Jun 2026 09:32:56 +0000
sdk: add private farm location store
Diffstat:
10 files changed, 1194 insertions(+), 19 deletions(-)
diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs
@@ -172,6 +172,9 @@ pub enum RadrootsSdkError {
Outbox {
message: String,
},
+ PrivateStore {
+ message: String,
+ },
GeoNames {
kind: RadrootsSdkGeoNamesErrorKind,
message: String,
@@ -215,6 +218,7 @@ impl RadrootsSdkError {
Self::ListingDraft { .. } => "listing_draft",
Self::ListingMutation { .. } => "listing_mutation",
Self::Outbox { .. } => "outbox",
+ Self::PrivateStore { .. } => "private_store",
Self::GeoNames { kind, .. } => match kind {
RadrootsSdkGeoNamesErrorKind::Configuration => "geonames_configuration",
RadrootsSdkGeoNamesErrorKind::Download => "geonames_download",
@@ -234,6 +238,7 @@ impl RadrootsSdkError {
Self::Io { .. }
| Self::EventStore { .. }
| Self::Outbox { .. }
+ | Self::PrivateStore { .. }
| Self::Projection { .. } => RadrootsSdkErrorClass::Storage,
Self::GeoNames { kind, .. } => match kind {
RadrootsSdkGeoNamesErrorKind::Configuration => RadrootsSdkErrorClass::Configuration,
@@ -279,6 +284,7 @@ impl RadrootsSdkError {
| Self::ProductSyncRelaySetupFailure { .. }
| Self::EventStore { .. }
| Self::Outbox { .. }
+ | Self::PrivateStore { .. }
| Self::GeoNames {
kind: RadrootsSdkGeoNamesErrorKind::Cache
| RadrootsSdkGeoNamesErrorKind::Download,
@@ -297,6 +303,7 @@ impl RadrootsSdkError {
Self::Io { .. }
| Self::EventStore { .. }
| Self::Outbox { .. }
+ | Self::PrivateStore { .. }
| Self::Projection { .. } => vec![RadrootsSdkRecoveryAction::InspectLocalStores],
Self::GeoNames { kind, .. } => match kind {
RadrootsSdkGeoNamesErrorKind::Configuration => {
@@ -415,6 +422,7 @@ impl RadrootsSdkError {
| Self::ListingDraft { message }
| Self::ListingMutation { message }
| Self::Outbox { message }
+ | Self::PrivateStore { message }
| Self::RelayTransport { message }
| Self::Projection { message } => json!({ "message": message }),
Self::GeoNames { kind, message } => json!({ "kind": kind, "message": message }),
@@ -600,6 +608,7 @@ impl fmt::Display for RadrootsSdkError {
write!(f, "sdk listing mutation error: {message}")
}
Self::Outbox { message } => write!(f, "sdk outbox error: {message}"),
+ Self::PrivateStore { message } => write!(f, "sdk private store error: {message}"),
Self::GeoNames { kind, message } => {
write!(f, "sdk GeoNames {kind:?} error: {message}")
}
diff --git a/crates/sdk/src/farms_runtime.rs b/crates/sdk/src/farms_runtime.rs
@@ -4,6 +4,8 @@ use crate::workflow_runtime::enqueue_configured_signed_workflow;
use crate::{
FarmsClient, RadrootsSdkError, RadrootsSdkTimestamp, SdkIdempotencyKey, SdkMutationState,
SdkRelayTargetPolicy, SdkRelayUrlPolicy, farm,
+ geonames::{Geocoder, GeocoderPoint, GeocoderReverseOptions, GeocoderReverseResult},
+ private_store::SdkPrivateFarmLocationRecord,
workflow_runtime::{SdkWorkflowEnqueueRequest, enqueue_signed_workflow},
};
#[cfg(feature = "runtime")]
@@ -12,9 +14,10 @@ use radroots_authority::{RadrootsActorContext, RadrootsEventSigner};
use radroots_events::{
contract::RadrootsActorRole,
draft::RadrootsFrozenEventDraft,
- farm::RadrootsFarm,
+ farm::{RadrootsFarm, RadrootsFarmPublicLocation},
ids::{RadrootsAddressableCoordinate, RadrootsEventId},
kinds::KIND_FARM,
+ listing::RadrootsListingPublicLocation,
};
#[cfg(feature = "runtime")]
use radroots_events_codec::wire::to_frozen_draft;
@@ -23,6 +26,12 @@ pub const FARM_PUBLISH_OPERATION_KIND: &str = "farm.publish.v1";
#[cfg(feature = "runtime")]
const FARM_PROFILE_CONTRACT_ID: &str = "radroots.farm.profile.v1";
+#[cfg(feature = "runtime")]
+const FARM_PRIVATE_LOCATION_OPERATION: &str = "farm.private_location.upsert";
+#[cfg(feature = "runtime")]
+const GEOHASH5_LEN: usize = 5;
+#[cfg(feature = "runtime")]
+const GEOHASH_BASE32: &[u8; 32] = b"0123456789bcdefghjkmnpqrstuvwxyz";
#[cfg(feature = "runtime")]
#[derive(Clone, Debug, serde::Serialize)]
@@ -133,6 +142,101 @@ pub struct FarmEnqueueReceipt {
}
#[cfg(feature = "runtime")]
+#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
+pub struct SdkExactLocation {
+ pub latitude: f64,
+ pub longitude: f64,
+}
+
+#[cfg(feature = "runtime")]
+impl SdkExactLocation {
+ pub fn new(latitude: f64, longitude: f64) -> Self {
+ Self {
+ latitude,
+ longitude,
+ }
+ }
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+pub struct SdkPublicLocality {
+ pub primary: String,
+ pub city: Option<String>,
+ pub region: Option<String>,
+ pub country: Option<String>,
+ pub geohash5: String,
+}
+
+#[cfg(feature = "runtime")]
+impl SdkPublicLocality {
+ pub fn to_farm_public_location(&self) -> RadrootsFarmPublicLocation {
+ RadrootsFarmPublicLocation {
+ primary: self.primary.clone(),
+ city: self.city.clone(),
+ region: self.region.clone(),
+ country: self.country.clone(),
+ geohash: self.geohash5.clone(),
+ }
+ }
+
+ pub fn to_listing_public_location(&self) -> RadrootsListingPublicLocation {
+ RadrootsListingPublicLocation {
+ primary: self.primary.clone(),
+ city: self.city.clone(),
+ region: self.region.clone(),
+ country: self.country.clone(),
+ geohash: self.geohash5.clone(),
+ }
+ }
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug, serde::Serialize)]
+#[non_exhaustive]
+pub struct FarmPrivateLocationUpsertRequest {
+ #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
+ pub actor: RadrootsActorContext,
+ pub farm_d_tag: String,
+ pub exact_location: SdkExactLocation,
+ pub updated_at: Option<RadrootsSdkTimestamp>,
+}
+
+#[cfg(feature = "runtime")]
+impl FarmPrivateLocationUpsertRequest {
+ pub fn new(
+ actor: RadrootsActorContext,
+ farm_d_tag: impl Into<String>,
+ exact_location: SdkExactLocation,
+ ) -> Self {
+ Self {
+ actor,
+ farm_d_tag: farm_d_tag.into(),
+ exact_location,
+ updated_at: None,
+ }
+ }
+
+ pub fn with_updated_at(mut self, updated_at: RadrootsSdkTimestamp) -> Self {
+ self.updated_at = Some(updated_at);
+ self
+ }
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
+pub struct FarmPrivateLocationReceipt {
+ pub farm_addr: RadrootsAddressableCoordinate,
+ pub farm_pubkey: String,
+ pub farm_d_tag: String,
+ pub exact_location: SdkExactLocation,
+ pub public_locality: SdkPublicLocality,
+ pub geonames_feature_id: Option<i64>,
+ pub geonames_country_id: Option<String>,
+ pub updated_at_ms: i64,
+}
+
+#[cfg(feature = "runtime")]
impl<'sdk> FarmsClient<'sdk> {
pub fn prepare_publish(
&self,
@@ -237,6 +341,64 @@ impl<'sdk> FarmsClient<'sdk> {
Ok(farm_enqueue_receipt(plan, enqueue))
}
+ pub async fn upsert_private_location(
+ &self,
+ request: FarmPrivateLocationUpsertRequest,
+ ) -> Result<FarmPrivateLocationReceipt, RadrootsSdkError> {
+ let geocoder = self.sdk.geonames().open_verified()?;
+ self.upsert_private_location_with_geocoder(request, &geocoder)
+ .await
+ }
+
+ pub async fn upsert_private_location_with_geocoder(
+ &self,
+ request: FarmPrivateLocationUpsertRequest,
+ geocoder: &Geocoder,
+ ) -> Result<FarmPrivateLocationReceipt, RadrootsSdkError> {
+ require_farmer_actor(&request.actor, FARM_PRIVATE_LOCATION_OPERATION)?;
+ validate_exact_location(request.exact_location)?;
+ let updated_at_ms = match request.updated_at {
+ Some(updated_at) => sdk_timestamp_ms(updated_at)?,
+ None => crate::runtime::sdk_now_ms(self.sdk)?,
+ };
+ let farm_addr = farm_addr(&request.actor, request.farm_d_tag.as_str())?;
+ let reverse = nearest_geonames_locality(geocoder, request.exact_location)?;
+ let public_locality = public_locality_from_reverse(request.exact_location, &reverse)?;
+ let record = SdkPrivateFarmLocationRecord {
+ farm_addr: farm_addr.clone(),
+ farm_pubkey: request.actor.pubkey().as_str().to_owned(),
+ farm_d_tag: request.farm_d_tag,
+ latitude: request.exact_location.latitude,
+ longitude: request.exact_location.longitude,
+ locality_primary: public_locality.primary.clone(),
+ locality_city: public_locality.city.clone(),
+ locality_region: public_locality.region.clone(),
+ locality_country: public_locality.country.clone(),
+ geohash5: public_locality.geohash5.clone(),
+ geonames_feature_id: Some(reverse.id),
+ geonames_country_id: Some(reverse.country_id.clone()),
+ updated_at_ms,
+ };
+ self.sdk
+ ._private_store
+ .upsert_farm_location(&record)
+ .await?;
+ Ok(private_location_receipt_from_record(record))
+ }
+
+ pub async fn private_location(
+ &self,
+ farm_addr: &RadrootsAddressableCoordinate,
+ ) -> Result<Option<FarmPrivateLocationReceipt>, RadrootsSdkError> {
+ self.sdk
+ ._private_store
+ .farm_location(farm_addr)
+ .await?
+ .map(private_location_receipt_from_record)
+ .map(Ok)
+ .transpose()
+ }
+
fn resolved_created_at(
&self,
created_at: Option<RadrootsSdkTimestamp>,
@@ -323,6 +485,156 @@ fn farm_addr(
)
}
+#[cfg(feature = "runtime")]
+fn validate_exact_location(location: SdkExactLocation) -> Result<(), RadrootsSdkError> {
+ if !location.latitude.is_finite()
+ || !location.longitude.is_finite()
+ || location.latitude < -90.0
+ || location.latitude > 90.0
+ || location.longitude < -180.0
+ || location.longitude > 180.0
+ {
+ return Err(RadrootsSdkError::InvalidRequest {
+ message: "farm exact location coordinates are outside valid latitude/longitude bounds"
+ .to_owned(),
+ });
+ }
+ Ok(())
+}
+
+#[cfg(feature = "runtime")]
+fn sdk_timestamp_ms(timestamp: RadrootsSdkTimestamp) -> Result<i64, RadrootsSdkError> {
+ let seconds = timestamp.unix_seconds();
+ let millis = seconds
+ .checked_mul(1_000)
+ .ok_or(RadrootsSdkError::TimestampOutOfRange { value: seconds })?;
+ i64::try_from(millis).map_err(|_| RadrootsSdkError::TimestampOutOfRange { value: seconds })
+}
+
+#[cfg(feature = "runtime")]
+fn nearest_geonames_locality(
+ geocoder: &Geocoder,
+ exact_location: SdkExactLocation,
+) -> Result<GeocoderReverseResult, RadrootsSdkError> {
+ let mut results = geocoder.reverse(
+ GeocoderPoint {
+ lat: exact_location.latitude,
+ lng: exact_location.longitude,
+ },
+ Some(GeocoderReverseOptions {
+ limit: 1,
+ degree_offset: 0.5,
+ }),
+ )?;
+ results.pop().ok_or_else(|| RadrootsSdkError::GeoNames {
+ kind: crate::RadrootsSdkGeoNamesErrorKind::Lookup,
+ message: "GeoNames reverse lookup returned no public locality".to_owned(),
+ })
+}
+
+#[cfg(feature = "runtime")]
+fn public_locality_from_reverse(
+ exact_location: SdkExactLocation,
+ reverse: &GeocoderReverseResult,
+) -> Result<SdkPublicLocality, RadrootsSdkError> {
+ let primary = required_public_string(reverse.name.as_str(), "GeoNames locality name")?;
+ let country = optional_public_string(reverse.country_name.as_deref())
+ .or_else(|| Some(reverse.country_id.clone()));
+ Ok(SdkPublicLocality {
+ primary: primary.clone(),
+ city: Some(primary),
+ region: optional_public_string(reverse.admin1_name.as_deref()),
+ country,
+ geohash5: geohash5(exact_location)?,
+ })
+}
+
+#[cfg(feature = "runtime")]
+fn required_public_string(value: &str, label: &str) -> Result<String, RadrootsSdkError> {
+ let trimmed = value.trim();
+ if trimmed.is_empty() {
+ return Err(RadrootsSdkError::GeoNames {
+ kind: crate::RadrootsSdkGeoNamesErrorKind::Lookup,
+ message: format!("{label} must not be empty"),
+ });
+ }
+ Ok(trimmed.to_owned())
+}
+
+#[cfg(feature = "runtime")]
+fn optional_public_string(value: Option<&str>) -> Option<String> {
+ value
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .map(str::to_owned)
+}
+
+#[cfg(feature = "runtime")]
+fn geohash5(location: SdkExactLocation) -> Result<String, RadrootsSdkError> {
+ validate_exact_location(location)?;
+ let mut latitude_min = -90.0;
+ let mut latitude_max = 90.0;
+ let mut longitude_min = -180.0;
+ let mut longitude_max = 180.0;
+ let mut even = true;
+ let mut bit_count = 0;
+ let mut value = 0usize;
+ let mut geohash = String::with_capacity(GEOHASH5_LEN);
+ while geohash.len() < GEOHASH5_LEN {
+ value <<= 1;
+ if even {
+ let middle = (longitude_min + longitude_max) / 2.0;
+ if location.longitude >= middle {
+ value |= 1;
+ longitude_min = middle;
+ } else {
+ longitude_max = middle;
+ }
+ } else {
+ let middle = (latitude_min + latitude_max) / 2.0;
+ if location.latitude >= middle {
+ value |= 1;
+ latitude_min = middle;
+ } else {
+ latitude_max = middle;
+ }
+ }
+ even = !even;
+ bit_count += 1;
+ if bit_count == 5 {
+ geohash.push(GEOHASH_BASE32[value] as char);
+ bit_count = 0;
+ value = 0;
+ }
+ }
+ Ok(geohash)
+}
+
+#[cfg(feature = "runtime")]
+fn private_location_receipt_from_record(
+ record: SdkPrivateFarmLocationRecord,
+) -> FarmPrivateLocationReceipt {
+ FarmPrivateLocationReceipt {
+ farm_addr: record.farm_addr,
+ farm_pubkey: record.farm_pubkey,
+ farm_d_tag: record.farm_d_tag,
+ exact_location: SdkExactLocation {
+ latitude: record.latitude,
+ longitude: record.longitude,
+ },
+ public_locality: SdkPublicLocality {
+ primary: record.locality_primary,
+ city: record.locality_city,
+ region: record.locality_region,
+ country: record.locality_country,
+ geohash5: record.geohash5,
+ },
+ geonames_feature_id: record.geonames_feature_id,
+ geonames_country_id: record.geonames_country_id,
+ updated_at_ms: record.updated_at_ms,
+ }
+}
+
#[cfg(all(test, feature = "runtime"))]
#[path = "../tests/unit/farms_runtime_tests.rs"]
mod tests;
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
@@ -33,6 +33,8 @@ mod order;
#[cfg(feature = "runtime")]
mod orders_runtime;
#[cfg(feature = "runtime")]
+mod private_store;
+#[cfg(feature = "runtime")]
mod product_clients;
mod profile;
pub mod protocol;
@@ -56,7 +58,8 @@ pub use crate::error::{
#[cfg(feature = "runtime")]
pub use crate::farms_runtime::{
FARM_PUBLISH_OPERATION_KIND, FarmEnqueuePublishRequest, FarmEnqueueReceipt,
- FarmPreparePublishRequest, FarmPublishPlan,
+ FarmPreparePublishRequest, FarmPrivateLocationReceipt, FarmPrivateLocationUpsertRequest,
+ FarmPublishPlan, SdkExactLocation, SdkPublicLocality,
};
#[cfg(feature = "runtime")]
pub use crate::geonames::{
@@ -111,8 +114,8 @@ pub use crate::runtime::{
RadrootsClientBuilder, RadrootsSdkClock, RadrootsSdkStorageConfig, RadrootsSdkStoragePaths,
RadrootsSdkTimestamp, RestoreArchive, RestoreReceipt, RestoreRequest, SdkBackupManifest,
SdkBackupManifestKind, SdkBackupState, SdkBackupVerification, SdkEventStoreStorageStatus,
- SdkOutboxStorageStatus, SdkPublishTransport, SdkRestoreState, SdkSqliteStoreStatus,
- SdkStorageKind, StorageStatusReceipt, StorageStatusRequest,
+ SdkOutboxStorageStatus, SdkPrivateStoreStorageStatus, SdkPublishTransport, SdkRestoreState,
+ SdkSqliteStoreStatus, SdkStorageKind, StorageStatusReceipt, StorageStatusRequest,
};
#[cfg(all(feature = "runtime", feature = "signer-adapters"))]
pub use crate::signer_provider::{
diff --git a/crates/sdk/src/private_store.rs b/crates/sdk/src/private_store.rs
@@ -0,0 +1,308 @@
+#![cfg(feature = "runtime")]
+
+use crate::RadrootsSdkError;
+use radroots_events::ids::RadrootsAddressableCoordinate;
+use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
+use sqlx::{Row, SqlitePool};
+use std::path::Path;
+use std::str::FromStr;
+
+pub(crate) const SDK_PRIVATE_STORE_SCHEMA_VERSION: i64 = 1;
+
+const PRIVATE_STORE_MIGRATION_UP: &str = r#"
+CREATE TABLE IF NOT EXISTS sdk_private_farm_location (
+ farm_addr TEXT PRIMARY KEY NOT NULL,
+ farm_pubkey TEXT NOT NULL,
+ farm_d_tag TEXT NOT NULL,
+ latitude REAL NOT NULL CHECK(latitude >= -90.0 AND latitude <= 90.0),
+ longitude REAL NOT NULL CHECK(longitude >= -180.0 AND longitude <= 180.0),
+ locality_primary TEXT NOT NULL,
+ locality_city TEXT,
+ locality_region TEXT,
+ locality_country TEXT,
+ geohash5 TEXT NOT NULL CHECK(length(geohash5) = 5),
+ geonames_feature_id INTEGER,
+ geonames_country_id TEXT,
+ updated_at_ms INTEGER NOT NULL
+);
+CREATE INDEX IF NOT EXISTS sdk_private_farm_location_pubkey
+ ON sdk_private_farm_location (farm_pubkey, farm_d_tag);
+"#;
+
+#[derive(Clone)]
+pub(crate) struct SdkPrivateStore {
+ pool: SqlitePool,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub(crate) struct SdkPrivateFarmLocationRecord {
+ pub farm_addr: RadrootsAddressableCoordinate,
+ pub farm_pubkey: String,
+ pub farm_d_tag: String,
+ pub latitude: f64,
+ pub longitude: f64,
+ pub locality_primary: String,
+ pub locality_city: Option<String>,
+ pub locality_region: Option<String>,
+ pub locality_country: Option<String>,
+ pub geohash5: String,
+ pub geonames_feature_id: Option<i64>,
+ pub geonames_country_id: Option<String>,
+ pub updated_at_ms: i64,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub(crate) struct SdkPrivateStoreStatusSummary {
+ pub farm_private_locations: i64,
+}
+
+impl SdkPrivateStore {
+ pub async fn open_memory() -> Result<Self, RadrootsSdkError> {
+ let options =
+ SqliteConnectOptions::from_str("sqlite::memory:").map_err(private_store_error)?;
+ let pool = SqlitePoolOptions::new()
+ .max_connections(1)
+ .connect_with(options)
+ .await
+ .map_err(private_store_error)?;
+ configure_connection(&pool, false).await?;
+ apply_up(&pool).await?;
+ Ok(Self { pool })
+ }
+
+ pub async fn open_file(path: impl AsRef<Path>) -> Result<Self, RadrootsSdkError> {
+ let options = SqliteConnectOptions::new()
+ .filename(path)
+ .create_if_missing(true);
+ let pool = SqlitePoolOptions::new()
+ .max_connections(1)
+ .connect_with(options)
+ .await
+ .map_err(private_store_error)?;
+ configure_connection(&pool, true).await?;
+ apply_up(&pool).await?;
+ Ok(Self { pool })
+ }
+
+ pub fn pool(&self) -> &SqlitePool {
+ &self.pool
+ }
+
+ pub async fn pragma_foreign_keys(&self) -> Result<i64, RadrootsSdkError> {
+ query_i64(&self.pool, "PRAGMA foreign_keys").await
+ }
+
+ pub async fn pragma_busy_timeout(&self) -> Result<i64, RadrootsSdkError> {
+ query_i64(&self.pool, "PRAGMA busy_timeout").await
+ }
+
+ pub async fn pragma_journal_mode(&self) -> Result<String, RadrootsSdkError> {
+ query_string(&self.pool, "PRAGMA journal_mode").await
+ }
+
+ pub async fn status_summary(&self) -> Result<SdkPrivateStoreStatusSummary, RadrootsSdkError> {
+ Ok(SdkPrivateStoreStatusSummary {
+ farm_private_locations: query_i64(
+ &self.pool,
+ "SELECT COUNT(*) FROM sdk_private_farm_location",
+ )
+ .await?,
+ })
+ }
+
+ pub async fn upsert_farm_location(
+ &self,
+ record: &SdkPrivateFarmLocationRecord,
+ ) -> Result<(), RadrootsSdkError> {
+ validate_location_record(record)?;
+ sqlx::query(
+ r#"
+ INSERT INTO sdk_private_farm_location (
+ farm_addr,
+ farm_pubkey,
+ farm_d_tag,
+ latitude,
+ longitude,
+ locality_primary,
+ locality_city,
+ locality_region,
+ locality_country,
+ geohash5,
+ geonames_feature_id,
+ geonames_country_id,
+ updated_at_ms
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
+ ON CONFLICT(farm_addr) DO UPDATE SET
+ farm_pubkey = excluded.farm_pubkey,
+ farm_d_tag = excluded.farm_d_tag,
+ latitude = excluded.latitude,
+ longitude = excluded.longitude,
+ locality_primary = excluded.locality_primary,
+ locality_city = excluded.locality_city,
+ locality_region = excluded.locality_region,
+ locality_country = excluded.locality_country,
+ geohash5 = excluded.geohash5,
+ geonames_feature_id = excluded.geonames_feature_id,
+ geonames_country_id = excluded.geonames_country_id,
+ updated_at_ms = excluded.updated_at_ms
+ "#,
+ )
+ .bind(record.farm_addr.as_str())
+ .bind(record.farm_pubkey.as_str())
+ .bind(record.farm_d_tag.as_str())
+ .bind(record.latitude)
+ .bind(record.longitude)
+ .bind(record.locality_primary.as_str())
+ .bind(record.locality_city.as_deref())
+ .bind(record.locality_region.as_deref())
+ .bind(record.locality_country.as_deref())
+ .bind(record.geohash5.as_str())
+ .bind(record.geonames_feature_id)
+ .bind(record.geonames_country_id.as_deref())
+ .bind(record.updated_at_ms)
+ .execute(&self.pool)
+ .await
+ .map(|_| ())
+ .map_err(private_store_error)
+ }
+
+ pub async fn farm_location(
+ &self,
+ farm_addr: &RadrootsAddressableCoordinate,
+ ) -> Result<Option<SdkPrivateFarmLocationRecord>, RadrootsSdkError> {
+ let row = sqlx::query(
+ r#"
+ SELECT
+ farm_addr,
+ farm_pubkey,
+ farm_d_tag,
+ latitude,
+ longitude,
+ locality_primary,
+ locality_city,
+ locality_region,
+ locality_country,
+ geohash5,
+ geonames_feature_id,
+ geonames_country_id,
+ updated_at_ms
+ FROM sdk_private_farm_location
+ WHERE farm_addr = ?1
+ "#,
+ )
+ .bind(farm_addr.as_str())
+ .fetch_optional(&self.pool)
+ .await
+ .map_err(private_store_error)?;
+ row.map(|row| private_farm_location_from_row(farm_addr.clone(), row))
+ .transpose()
+ }
+}
+
+async fn configure_connection(
+ pool: &SqlitePool,
+ file_backed: bool,
+) -> Result<(), RadrootsSdkError> {
+ sqlx::query("PRAGMA foreign_keys = ON")
+ .execute(pool)
+ .await
+ .map_err(private_store_error)?;
+ sqlx::query("PRAGMA busy_timeout = 5000")
+ .execute(pool)
+ .await
+ .map_err(private_store_error)?;
+ if file_backed {
+ sqlx::query("PRAGMA journal_mode = WAL")
+ .execute(pool)
+ .await
+ .map_err(private_store_error)?;
+ }
+ Ok(())
+}
+
+async fn apply_up(pool: &SqlitePool) -> Result<(), RadrootsSdkError> {
+ sqlx::raw_sql(PRIVATE_STORE_MIGRATION_UP)
+ .execute(pool)
+ .await
+ .map(|_| ())
+ .map_err(private_store_error)
+}
+
+async fn query_i64(pool: &SqlitePool, sql: &str) -> Result<i64, RadrootsSdkError> {
+ let row = sqlx::query(sql)
+ .fetch_one(pool)
+ .await
+ .map_err(private_store_error)?;
+ row.try_get(0).map_err(private_store_error)
+}
+
+async fn query_string(pool: &SqlitePool, sql: &str) -> Result<String, RadrootsSdkError> {
+ let row = sqlx::query(sql)
+ .fetch_one(pool)
+ .await
+ .map_err(private_store_error)?;
+ row.try_get(0).map_err(private_store_error)
+}
+
+fn private_farm_location_from_row(
+ farm_addr: RadrootsAddressableCoordinate,
+ row: sqlx::sqlite::SqliteRow,
+) -> Result<SdkPrivateFarmLocationRecord, RadrootsSdkError> {
+ Ok(SdkPrivateFarmLocationRecord {
+ farm_addr,
+ farm_pubkey: row.try_get("farm_pubkey").map_err(private_store_error)?,
+ farm_d_tag: row.try_get("farm_d_tag").map_err(private_store_error)?,
+ latitude: row.try_get("latitude").map_err(private_store_error)?,
+ longitude: row.try_get("longitude").map_err(private_store_error)?,
+ locality_primary: row
+ .try_get("locality_primary")
+ .map_err(private_store_error)?,
+ locality_city: row.try_get("locality_city").map_err(private_store_error)?,
+ locality_region: row
+ .try_get("locality_region")
+ .map_err(private_store_error)?,
+ locality_country: row
+ .try_get("locality_country")
+ .map_err(private_store_error)?,
+ geohash5: row.try_get("geohash5").map_err(private_store_error)?,
+ geonames_feature_id: row
+ .try_get("geonames_feature_id")
+ .map_err(private_store_error)?,
+ geonames_country_id: row
+ .try_get("geonames_country_id")
+ .map_err(private_store_error)?,
+ updated_at_ms: row.try_get("updated_at_ms").map_err(private_store_error)?,
+ })
+}
+
+fn validate_location_record(record: &SdkPrivateFarmLocationRecord) -> Result<(), RadrootsSdkError> {
+ if !record.latitude.is_finite()
+ || !record.longitude.is_finite()
+ || record.latitude < -90.0
+ || record.latitude > 90.0
+ || record.longitude < -180.0
+ || record.longitude > 180.0
+ {
+ return Err(RadrootsSdkError::InvalidRequest {
+ message: "farm exact location coordinates are outside valid latitude/longitude bounds"
+ .to_owned(),
+ });
+ }
+ if record.locality_primary.trim().is_empty() {
+ return Err(RadrootsSdkError::InvalidRequest {
+ message: "farm public locality primary name must not be empty".to_owned(),
+ });
+ }
+ if record.geohash5.len() != 5 {
+ return Err(RadrootsSdkError::InvalidRequest {
+ message: "farm public locality geohash must be precision 5".to_owned(),
+ });
+ }
+ Ok(())
+}
+
+fn private_store_error(error: impl ToString) -> RadrootsSdkError {
+ RadrootsSdkError::PrivateStore {
+ message: error.to_string(),
+ }
+}
diff --git a/crates/sdk/src/runtime.rs b/crates/sdk/src/runtime.rs
@@ -1,4 +1,6 @@
#[cfg(feature = "runtime")]
+use crate::private_store::{SDK_PRIVATE_STORE_SCHEMA_VERSION, SdkPrivateStore};
+#[cfg(feature = "runtime")]
use crate::{
DvmClient, FarmsClient, GeoNamesClient, ListingsClient, MarketClient, RadrootsGeoNamesConfig,
RadrootsSdkError, SdkRelayTargetSet, SdkRelayUrlPolicy, SyncClient, TradesClient,
@@ -34,10 +36,14 @@ const SDK_EVENT_STORE_SCHEMA_VERSION: i64 = 1;
#[cfg(feature = "runtime")]
const SDK_OUTBOX_SCHEMA_VERSION: i64 = 1;
#[cfg(feature = "runtime")]
+const SDK_PRIVATE_STORE_SCHEMA_VERSION_CURRENT: i64 = SDK_PRIVATE_STORE_SCHEMA_VERSION;
+#[cfg(feature = "runtime")]
const EVENT_STORE_BACKUP_FILE: &str = "event_store.sqlite";
#[cfg(feature = "runtime")]
const OUTBOX_BACKUP_FILE: &str = "outbox.sqlite";
#[cfg(feature = "runtime")]
+const PRIVATE_STORE_BACKUP_FILE: &str = "private.sqlite";
+#[cfg(feature = "runtime")]
const BACKUP_MANIFEST_FILE: &str = "manifest.json";
#[cfg(feature = "runtime")]
@@ -119,6 +125,7 @@ fn sdk_timestamp_from_system_time(
pub struct RadrootsSdkStoragePaths {
pub event_store_path: PathBuf,
pub outbox_path: PathBuf,
+ pub private_store_path: PathBuf,
}
#[cfg(feature = "runtime")]
@@ -140,6 +147,7 @@ pub struct StorageStatusReceipt {
pub paths: Option<RadrootsSdkStoragePaths>,
pub event_store: SdkEventStoreStorageStatus,
pub outbox: SdkOutboxStorageStatus,
+ pub private_store: SdkPrivateStoreStorageStatus,
}
#[cfg(feature = "runtime")]
@@ -190,6 +198,13 @@ pub struct SdkOutboxStorageStatus {
#[cfg(feature = "runtime")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+pub struct SdkPrivateStoreStorageStatus {
+ pub store: SdkSqliteStoreStatus,
+ pub farm_private_locations: i64,
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct BackupRequest {
pub destination: PathBuf,
@@ -218,6 +233,7 @@ pub struct BackupReceipt {
pub state: SdkBackupState,
pub event_store_path: Option<PathBuf>,
pub outbox_path: Option<PathBuf>,
+ pub private_store_path: Option<PathBuf>,
pub manifest_path: Option<PathBuf>,
pub manifest: SdkBackupManifest,
}
@@ -258,8 +274,10 @@ pub struct SdkBackupManifest {
pub struct SdkBackupVerification {
pub event_store_ok: bool,
pub outbox_ok: bool,
+ pub private_store_ok: bool,
pub event_store_events: i64,
pub outbox_events: i64,
+ pub private_farm_locations: i64,
}
#[cfg(feature = "runtime")]
@@ -280,8 +298,10 @@ pub struct IntegrityReceipt {
pub checked_paths: Vec<PathBuf>,
pub event_store_ok: bool,
pub outbox_ok: bool,
+ pub private_store_ok: bool,
pub event_store_result: String,
pub outbox_result: String,
+ pub private_store_result: String,
}
#[cfg(feature = "runtime")]
@@ -368,6 +388,7 @@ pub struct RestoreArchive {
pub source: PathBuf,
pub event_store_path: PathBuf,
pub outbox_path: PathBuf,
+ pub private_store_path: PathBuf,
pub manifest_path: PathBuf,
pub manifest: SdkBackupManifest,
pub verification: SdkBackupVerification,
@@ -382,6 +403,7 @@ pub struct RestoreReceipt {
pub destination_paths: Option<RadrootsSdkStoragePaths>,
pub event_store_path: PathBuf,
pub outbox_path: PathBuf,
+ pub private_store_path: PathBuf,
pub manifest_path: PathBuf,
pub manifest: SdkBackupManifest,
pub verification: SdkBackupVerification,
@@ -477,6 +499,7 @@ impl RadrootsClientBuilder {
Ok(RadrootsClient {
_event_store: storage.event_store,
_outbox: storage.outbox,
+ _private_store: storage.private_store,
storage_paths: storage.paths,
geonames: self.geonames,
clock: self.clock,
@@ -493,6 +516,7 @@ impl RadrootsClientBuilder {
pub struct RadrootsClient {
pub(crate) _event_store: RadrootsEventStore,
pub(crate) _outbox: RadrootsOutbox,
+ pub(crate) _private_store: SdkPrivateStore,
storage_paths: Option<RadrootsSdkStoragePaths>,
geonames: Option<RadrootsGeoNamesConfig>,
clock: RadrootsSdkClock,
@@ -590,8 +614,10 @@ impl RadrootsClient {
let now_ms = sdk_now_ms(self)?;
let event_store_status = event_store_sqlite_status(&self._event_store).await?;
let outbox_store_status = outbox_sqlite_status(&self._outbox).await?;
+ let private_store_status = private_store_sqlite_status(&self._private_store).await?;
let event_summary = event_store_status_summary(&self._event_store).await?;
let outbox_summary = outbox_status_summary(&self._outbox, now_ms).await?;
+ let private_summary = self._private_store.status_summary().await?;
Ok(StorageStatusReceipt {
storage: self.storage_kind(),
paths: self.storage_paths.clone(),
@@ -615,6 +641,10 @@ impl RadrootsClient {
last_attempt_at_ms: outbox_summary.last_attempt_at_ms,
last_error: outbox_summary.last_error,
},
+ private_store: SdkPrivateStoreStorageStatus {
+ store: private_store_status,
+ farm_private_locations: private_summary.farm_private_locations,
+ },
})
}
@@ -624,17 +654,27 @@ impl RadrootsClient {
) -> Result<IntegrityReceipt, RadrootsSdkError> {
let event_store_integrity = sqlite_integrity_result(self._event_store.pool()).await?;
let outbox_integrity = sqlite_integrity_result(self._outbox.pool()).await?;
+ let private_store_integrity =
+ private_store_sqlite_integrity_result(self._private_store.pool()).await?;
let checked_paths = self
.storage_paths
.as_ref()
- .map(|paths| vec![paths.event_store_path.clone(), paths.outbox_path.clone()])
+ .map(|paths| {
+ vec![
+ paths.event_store_path.clone(),
+ paths.outbox_path.clone(),
+ paths.private_store_path.clone(),
+ ]
+ })
.unwrap_or_default();
Ok(IntegrityReceipt {
checked_paths,
event_store_ok: event_store_integrity.ok,
outbox_ok: outbox_integrity.ok,
+ private_store_ok: private_store_integrity.ok,
event_store_result: event_store_integrity.result,
outbox_result: outbox_integrity.result,
+ private_store_result: private_store_integrity.result,
})
}
@@ -649,16 +689,22 @@ impl RadrootsClient {
let backup_paths = RadrootsSdkStoragePaths {
event_store_path: request.destination.join(EVENT_STORE_BACKUP_FILE),
outbox_path: request.destination.join(OUTBOX_BACKUP_FILE),
+ private_store_path: request.destination.join(PRIVATE_STORE_BACKUP_FILE),
};
let manifest_backup_paths = RadrootsSdkStoragePaths {
event_store_path: PathBuf::from(EVENT_STORE_BACKUP_FILE),
outbox_path: PathBuf::from(OUTBOX_BACKUP_FILE),
+ private_store_path: PathBuf::from(PRIVATE_STORE_BACKUP_FILE),
};
let manifest_path = request.destination.join(BACKUP_MANIFEST_FILE);
let source_status = self.storage_status(StorageStatusRequest::new()).await?;
- let backup_verification =
- backup_sqlite_stores(self._event_store.pool(), self._outbox.pool(), &backup_paths)
- .await?;
+ let backup_verification = backup_sqlite_stores(
+ self._event_store.pool(),
+ self._outbox.pool(),
+ self._private_store.pool(),
+ &backup_paths,
+ )
+ .await?;
let manifest = SdkBackupManifest {
manifest_kind: SDK_STORAGE_MANIFEST_KIND,
manifest_version: SDK_STORAGE_MANIFEST_VERSION,
@@ -715,6 +761,7 @@ impl RadrootsClient {
destination_paths: Some(destination_paths),
event_store_path: archive.event_store_path,
outbox_path: archive.outbox_path,
+ private_store_path: archive.private_store_path,
manifest_path: archive.manifest_path,
manifest: archive.manifest,
verification: archive.verification,
@@ -752,6 +799,20 @@ async fn outbox_sqlite_status(
}
#[cfg(feature = "runtime")]
+async fn private_store_sqlite_status(
+ private_store: &SdkPrivateStore,
+) -> Result<SdkSqliteStoreStatus, RadrootsSdkError> {
+ private_sqlite_store_status(
+ private_store.pool(),
+ SDK_PRIVATE_STORE_SCHEMA_VERSION_CURRENT,
+ private_store.pragma_journal_mode().await?,
+ private_store.pragma_foreign_keys().await? != 0,
+ private_store.pragma_busy_timeout().await?,
+ )
+ .await
+}
+
+#[cfg(feature = "runtime")]
async fn event_store_status_summary(
event_store: &RadrootsEventStore,
) -> Result<radroots_event_store::RadrootsEventStoreStatusSummary, RadrootsSdkError> {
@@ -770,6 +831,7 @@ async fn outbox_status_summary(
async fn backup_sqlite_stores(
event_store_pool: &SqlitePool,
outbox_pool: &SqlitePool,
+ private_store_pool: &SqlitePool,
backup_paths: &RadrootsSdkStoragePaths,
) -> Result<SdkBackupVerification, RadrootsSdkError> {
sqlite_vacuum_into(
@@ -779,6 +841,12 @@ async fn backup_sqlite_stores(
)
.await?;
sqlite_vacuum_into(outbox_pool, &backup_paths.outbox_path, "outbox").await?;
+ sqlite_vacuum_into(
+ private_store_pool,
+ &backup_paths.private_store_path,
+ "private store",
+ )
+ .await?;
verify_backup_paths(backup_paths).await
}
@@ -795,6 +863,7 @@ fn write_backup_receipt(
state: SdkBackupState::Completed,
event_store_path: Some(backup_paths.event_store_path),
outbox_path: Some(backup_paths.outbox_path),
+ private_store_path: Some(backup_paths.private_store_path),
manifest_path: Some(manifest_path),
manifest,
})
@@ -848,9 +917,15 @@ async fn inspect_restore_archive(source: PathBuf) -> Result<RestoreArchive, Radr
)?;
let outbox_path =
restore_archive_member_path(&source_root, &manifest.backup_paths.outbox_path, "outbox")?;
+ let private_store_path = restore_archive_member_path(
+ &source_root,
+ &manifest.backup_paths.private_store_path,
+ "private store",
+ )?;
let verification = verify_backup_paths(&RadrootsSdkStoragePaths {
event_store_path: event_store_path.clone(),
outbox_path: outbox_path.clone(),
+ private_store_path: private_store_path.clone(),
})
.await?;
validate_restore_verification(&verification, &manifest.backup_verification)?;
@@ -858,6 +933,7 @@ async fn inspect_restore_archive(source: PathBuf) -> Result<RestoreArchive, Radr
source,
event_store_path,
outbox_path,
+ private_store_path,
manifest_path,
manifest,
verification,
@@ -969,7 +1045,7 @@ fn validate_restore_verification(
actual: &SdkBackupVerification,
manifest: &SdkBackupVerification,
) -> Result<(), RadrootsSdkError> {
- if !actual.event_store_ok || !actual.outbox_ok {
+ if !actual.event_store_ok || !actual.outbox_ok || !actual.private_store_ok {
return Err(RadrootsSdkError::InvalidRequest {
message: "restore backup stores failed integrity checks".to_owned(),
});
@@ -1054,6 +1130,7 @@ fn preflight_restore_destination(
Ok(RadrootsSdkStoragePaths {
event_store_path: destination.join(EVENT_STORE_BACKUP_FILE),
outbox_path: destination.join(OUTBOX_BACKUP_FILE),
+ private_store_path: destination.join(PRIVATE_STORE_BACKUP_FILE),
})
}
@@ -1090,6 +1167,7 @@ async fn restore_archive_to_destination(
let staging_paths = RadrootsSdkStoragePaths {
event_store_path: staging.join(EVENT_STORE_BACKUP_FILE),
outbox_path: staging.join(OUTBOX_BACKUP_FILE),
+ private_store_path: staging.join(PRIVATE_STORE_BACKUP_FILE),
};
if let Err(error) = copy_restore_archive_to_staging(archive, &staging_paths).await {
let _ = remove_existing_restore_path(&staging);
@@ -1152,6 +1230,11 @@ async fn copy_restore_archive_to_staging(
"event store",
)?;
copy_restore_file(&archive.outbox_path, &staging_paths.outbox_path, "outbox")?;
+ copy_restore_file(
+ &archive.private_store_path,
+ &staging_paths.private_store_path,
+ "private store",
+ )?;
let staging_verification = verify_backup_paths(staging_paths).await?;
validate_restore_verification(&staging_verification, &archive.verification)
}
@@ -1265,6 +1348,7 @@ fn rollback_restore_destination(destination: &Path, previous: &Path, previous_in
struct OpenedRuntimeStorage {
event_store: RadrootsEventStore,
outbox: RadrootsOutbox,
+ private_store: SdkPrivateStore,
paths: Option<RadrootsSdkStoragePaths>,
}
@@ -1276,6 +1360,7 @@ async fn open_storage(
RadrootsSdkStorageConfig::Memory => Ok(OpenedRuntimeStorage {
event_store: RadrootsEventStore::open_memory().await?,
outbox: RadrootsOutbox::open_memory().await?,
+ private_store: SdkPrivateStore::open_memory().await?,
paths: None,
}),
RadrootsSdkStorageConfig::Directory(path) => open_directory_storage(path).await,
@@ -1291,10 +1376,12 @@ async fn open_directory_storage(path: &Path) -> Result<OpenedRuntimeStorage, Rad
let paths = RadrootsSdkStoragePaths {
event_store_path: path.join("event_store.sqlite"),
outbox_path: path.join("outbox.sqlite"),
+ private_store_path: path.join("private.sqlite"),
};
Ok(OpenedRuntimeStorage {
event_store: RadrootsEventStore::open_file(&paths.event_store_path).await?,
outbox: RadrootsOutbox::open_file(&paths.outbox_path).await?,
+ private_store: SdkPrivateStore::open_file(&paths.private_store_path).await?,
paths: Some(paths),
})
}
@@ -1325,6 +1412,25 @@ async fn sqlite_store_status(
}
#[cfg(feature = "runtime")]
+async fn private_sqlite_store_status(
+ pool: &SqlitePool,
+ schema_version: i64,
+ journal_mode: String,
+ foreign_keys_enabled: bool,
+ busy_timeout_ms: i64,
+) -> Result<SdkSqliteStoreStatus, RadrootsSdkError> {
+ let integrity = private_store_sqlite_integrity_result(pool).await?;
+ Ok(SdkSqliteStoreStatus {
+ schema_version,
+ journal_mode,
+ foreign_keys_enabled,
+ busy_timeout_ms,
+ integrity_ok: integrity.ok,
+ integrity_result: integrity.result,
+ })
+}
+
+#[cfg(feature = "runtime")]
async fn sqlite_integrity_result(
pool: &SqlitePool,
) -> Result<SqliteIntegrityResult, RadrootsSdkError> {
@@ -1342,6 +1448,23 @@ async fn sqlite_integrity_result(
}
#[cfg(feature = "runtime")]
+async fn private_store_sqlite_integrity_result(
+ pool: &SqlitePool,
+) -> Result<SqliteIntegrityResult, RadrootsSdkError> {
+ let results = sqlx::query_scalar::<_, String>("PRAGMA integrity_check")
+ .fetch_all(pool)
+ .await
+ .map_err(|error| RadrootsSdkError::PrivateStore {
+ message: error.to_string(),
+ })?;
+ let result = results.join("; ");
+ Ok(SqliteIntegrityResult {
+ ok: result == "ok",
+ result,
+ })
+}
+
+#[cfg(feature = "runtime")]
fn prepare_backup_destination(path: &Path, overwrite: bool) -> Result<(), RadrootsSdkError> {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_symlink() => {
@@ -1407,15 +1530,21 @@ async fn verify_backup_paths(
) -> Result<SdkBackupVerification, RadrootsSdkError> {
let event_store = RadrootsEventStore::open_file(&paths.event_store_path).await?;
let outbox = RadrootsOutbox::open_file(&paths.outbox_path).await?;
+ let private_store = SdkPrivateStore::open_file(&paths.private_store_path).await?;
let event_store_integrity = sqlite_integrity_result(event_store.pool()).await?;
let outbox_integrity = sqlite_integrity_result(outbox.pool()).await?;
+ let private_store_integrity =
+ private_store_sqlite_integrity_result(private_store.pool()).await?;
let event_summary = event_store.status_summary().await?;
let outbox_summary = outbox.status_summary(i64::MAX).await?;
+ let private_summary = private_store.status_summary().await?;
Ok(SdkBackupVerification {
event_store_ok: event_store_integrity.ok,
outbox_ok: outbox_integrity.ok,
+ private_store_ok: private_store_integrity.ok,
event_store_events: event_summary.total_events,
outbox_events: outbox_summary.total_events,
+ private_farm_locations: private_summary.farm_private_locations,
})
}
diff --git a/crates/sdk/tests/farms_runtime.rs b/crates/sdk/tests/farms_runtime.rs
@@ -14,11 +14,14 @@ use radroots_outbox::{RadrootsOutbox, RadrootsOutboxEventState};
use radroots_relay_transport::RadrootsMockRelayPublishAdapter;
use radroots_sdk::{
FARM_PUBLISH_OPERATION_KIND, FarmEnqueuePublishRequest, FarmPreparePublishRequest,
- PushOutboxEventState, PushOutboxRelayOutcomeKind, PushOutboxRequest, RadrootsClient,
- RadrootsSdkError, RadrootsSdkPartialLocalMutationFailure, RadrootsSdkRecoveryAction,
- RadrootsSdkTimestamp, SdkIdempotencyKey, SdkMutationState, SdkRelayTargetPolicy,
- SdkRelayTargetSet, SdkRelayUrlPolicy,
+ FarmPrivateLocationUpsertRequest, Geocoder, PushOutboxEventState, PushOutboxRelayOutcomeKind,
+ PushOutboxRequest, RadrootsClient, RadrootsSdkError, RadrootsSdkErrorClass,
+ RadrootsSdkGeoNamesErrorKind, RadrootsSdkPartialLocalMutationFailure,
+ RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, SdkExactLocation, SdkIdempotencyKey,
+ SdkMutationState, SdkRelayTargetPolicy, SdkRelayTargetSet, SdkRelayUrlPolicy,
+ StorageStatusRequest,
};
+use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
#[path = "support/serializer_failure.rs"]
mod serializer_failure;
@@ -127,6 +130,70 @@ async fn directory_sdk_with_relays(relays: &[&str]) -> (tempfile::TempDir, Radro
(tempdir, sdk)
}
+async fn fixture_geocoder(tempdir: &tempfile::TempDir) -> Geocoder {
+ let path = tempdir.path().join("geonames-fixture.db");
+ let options = SqliteConnectOptions::new()
+ .filename(&path)
+ .create_if_missing(true);
+ let pool = SqlitePoolOptions::new()
+ .max_connections(1)
+ .connect_with(options)
+ .await
+ .expect("geonames fixture pool");
+ sqlx::raw_sql(
+ r#"
+ CREATE TABLE countries(
+ id TEXT,
+ name TEXT,
+ PRIMARY KEY (id)
+ );
+ CREATE TABLE admin1(
+ country_id TEXT,
+ id INTEGER,
+ name TEXT,
+ PRIMARY KEY (country_id, id)
+ );
+ CREATE TABLE features(
+ id INTEGER,
+ name TEXT,
+ country_id TEXT,
+ admin1_id INTEGER,
+ PRIMARY KEY (id)
+ );
+ CREATE TABLE coordinates(
+ feature_id INTEGER,
+ latitude REAL,
+ longitude REAL,
+ PRIMARY KEY (feature_id)
+ );
+ CREATE INDEX coordinates_lat_lng ON coordinates (latitude, longitude);
+ CREATE VIEW geonames AS
+ SELECT
+ features.id,
+ features.name,
+ admin1.id AS admin1_id,
+ admin1.name AS admin1_name,
+ countries.id AS country_id,
+ countries.name AS country_name,
+ coordinates.latitude AS latitude,
+ coordinates.longitude AS longitude
+ FROM features
+ LEFT JOIN countries ON features.country_id = countries.id
+ LEFT JOIN admin1 ON features.country_id = admin1.country_id AND features.admin1_id = admin1.id
+ JOIN coordinates ON features.id = coordinates.feature_id;
+ INSERT INTO countries (id, name) VALUES ('FX', 'Fixture Country');
+ INSERT INTO admin1 (country_id, id, name) VALUES ('FX', 1, 'Fixture Region');
+ INSERT INTO features (id, name, country_id, admin1_id) VALUES (1, 'Fixture Town', 'FX', 1);
+ INSERT INTO coordinates (feature_id, latitude, longitude) VALUES (1, 12.25, -34.50);
+ "#,
+ )
+ .execute(&pool)
+ .await
+ .expect("seed geonames fixture");
+ pool.close().await;
+ Geocoder::open_path(path).expect("open geonames fixture")
+}
+
#[tokio::test]
async fn farm_prepare_publish_is_side_effect_free() {
let (_tempdir, sdk) = directory_sdk().await;
@@ -190,6 +257,147 @@ async fn farm_prepare_publish_rejects_non_farmer_actor() {
}
#[tokio::test]
+async fn farm_private_location_upsert_stores_exact_location_and_public_locality_without_events() {
+ let (tempdir, sdk) = directory_sdk().await;
+ let geocoder = fixture_geocoder(&tempdir).await;
+ let request = FarmPrivateLocationUpsertRequest::new(
+ farmer_actor(),
+ FARM_A_D_TAG,
+ SdkExactLocation::new(12.26, -34.51),
+ )
+ .with_updated_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_123));
+
+ let receipt = sdk
+ .farms()
+ .upsert_private_location_with_geocoder(request, &geocoder)
+ .await
+ .expect("upsert private location");
+
+ assert_eq!(
+ receipt.farm_addr.as_str(),
+ format!("{KIND_FARM}:{FARMER}:{FARM_A_D_TAG}")
+ );
+ assert_eq!(receipt.farm_pubkey, FARMER);
+ assert_eq!(receipt.farm_d_tag, FARM_A_D_TAG);
+ assert_eq!(receipt.exact_location, SdkExactLocation::new(12.26, -34.51));
+ assert_eq!(receipt.public_locality.primary, "Fixture Town");
+ assert_eq!(
+ receipt.public_locality.city.as_deref(),
+ Some("Fixture Town")
+ );
+ assert_eq!(
+ receipt.public_locality.region.as_deref(),
+ Some("Fixture Region")
+ );
+ assert_eq!(
+ receipt.public_locality.country.as_deref(),
+ Some("Fixture Country")
+ );
+ assert_eq!(receipt.public_locality.geohash5, "e4pmw");
+ assert_eq!(receipt.geonames_feature_id, Some(1));
+ assert_eq!(receipt.geonames_country_id.as_deref(), Some("FX"));
+ assert_eq!(receipt.updated_at_ms, 1_700_000_123_000);
+ let farm_public = receipt.public_locality.to_farm_public_location();
+ assert_eq!(farm_public.primary, "Fixture Town");
+ assert_eq!(farm_public.geohash, "e4pmw");
+ let listing_public = receipt.public_locality.to_listing_public_location();
+ assert_eq!(listing_public.primary, "Fixture Town");
+ assert_eq!(listing_public.geohash, "e4pmw");
+
+ let stored = sdk
+ .farms()
+ .private_location(&receipt.farm_addr)
+ .await
+ .expect("private location read")
+ .expect("stored private location");
+ assert_eq!(stored, receipt);
+ let status = sdk
+ .storage_status(StorageStatusRequest::new())
+ .await
+ .expect("status");
+ assert_eq!(status.private_store.farm_private_locations, 1);
+ assert_eq!(status.event_store.total_events, 0);
+ assert_eq!(status.outbox.total_events, 0);
+
+ let clock_receipt = sdk
+ .farms()
+ .upsert_private_location_with_geocoder(
+ FarmPrivateLocationUpsertRequest::new(
+ farmer_actor(),
+ FARM_B_D_TAG,
+ SdkExactLocation::new(12.26, -34.51),
+ ),
+ &geocoder,
+ )
+ .await
+ .expect("upsert with sdk clock");
+ assert_eq!(clock_receipt.updated_at_ms, 1_700_000_000_000);
+}
+
+#[tokio::test]
+async fn farm_private_location_requires_farmer_role_and_valid_coordinates() {
+ let (tempdir, sdk) = directory_sdk().await;
+ let geocoder = fixture_geocoder(&tempdir).await;
+ let non_farmer = sdk
+ .farms()
+ .upsert_private_location_with_geocoder(
+ FarmPrivateLocationUpsertRequest::new(
+ non_farmer_actor(),
+ FARM_B_D_TAG,
+ SdkExactLocation::new(12.26, -34.51),
+ ),
+ &geocoder,
+ )
+ .await
+ .expect_err("non farmer");
+ assert!(matches!(
+ non_farmer,
+ RadrootsSdkError::UnauthorizedActor { .. }
+ ));
+
+ let invalid = sdk
+ .farms()
+ .upsert_private_location_with_geocoder(
+ FarmPrivateLocationUpsertRequest::new(
+ farmer_actor(),
+ FARM_B_D_TAG,
+ SdkExactLocation::new(91.0, -34.51),
+ ),
+ &geocoder,
+ )
+ .await
+ .expect_err("invalid coordinates");
+ assert!(matches!(invalid, RadrootsSdkError::InvalidRequest { .. }));
+}
+
+#[tokio::test]
+async fn farm_private_location_requires_configured_geonames_for_default_upsert() {
+ let (_tempdir, sdk) = directory_sdk().await;
+ let error = sdk
+ .farms()
+ .upsert_private_location(FarmPrivateLocationUpsertRequest::new(
+ farmer_actor(),
+ FARM_C_D_TAG,
+ SdkExactLocation::new(12.26, -34.51),
+ ))
+ .await
+ .expect_err("missing geonames config");
+
+ assert!(matches!(
+ error,
+ RadrootsSdkError::GeoNames {
+ kind: RadrootsSdkGeoNamesErrorKind::Configuration,
+ ..
+ }
+ ));
+ assert_eq!(error.class(), RadrootsSdkErrorClass::Configuration);
+ assert_eq!(
+ error.recovery_actions(),
+ vec![RadrootsSdkRecoveryAction::ConfigureGeoNamesCache]
+ );
+}
+
+#[tokio::test]
async fn farm_enqueue_publish_stores_event_and_queues_signed_outbox_without_profile_event() {
let (_tempdir, sdk) = directory_sdk().await;
let request = FarmEnqueuePublishRequest::new(
diff --git a/crates/sdk/tests/runtime_foundation.rs b/crates/sdk/tests/runtime_foundation.rs
@@ -5,9 +5,10 @@ use radroots_sdk::{
RadrootsSdkClock, RadrootsSdkError, RadrootsSdkErrorClass, RadrootsSdkGeoNamesErrorKind,
RadrootsSdkRecoveryAction, RadrootsSdkStorageConfig, RadrootsSdkTimestamp, RestoreRequest,
SDK_IDEMPOTENCY_KEY_MAX_LEN, SDK_RELAY_TARGET_MAX_COUNT, SdkBackupState, SdkBackupVerification,
- SdkEventStoreStorageStatus, SdkIdempotencyKey, SdkOutboxStorageStatus, SdkRelayTargetPolicy,
- SdkRelayTargetSet, SdkRelayUrlPolicy, SdkRestoreState, SdkSqliteStoreStatus, SdkStorageKind,
- StorageStatusReceipt, StorageStatusRequest,
+ SdkEventStoreStorageStatus, SdkIdempotencyKey, SdkOutboxStorageStatus,
+ SdkPrivateStoreStorageStatus, SdkRelayTargetPolicy, SdkRelayTargetSet, SdkRelayUrlPolicy,
+ SdkRestoreState, SdkSqliteStoreStatus, SdkStorageKind, StorageStatusReceipt,
+ StorageStatusRequest,
};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use std::path::{Path, PathBuf};
@@ -149,8 +150,13 @@ async fn sdk_directory_storage_creates_deterministic_sqlite_files() {
paths.outbox_path,
tempdir.path().join("sdk-runtime").join("outbox.sqlite")
);
+ assert_eq!(
+ paths.private_store_path,
+ tempdir.path().join("sdk-runtime").join("private.sqlite")
+ );
assert!(paths.event_store_path.exists());
assert!(paths.outbox_path.exists());
+ assert!(paths.private_store_path.exists());
let event_tables = sqlite_table_names(&paths.event_store_path).await;
assert!(event_tables.iter().any(|name| name == "nostr_events"));
assert!(event_tables.iter().any(|name| name == "nostr_event_tags"));
@@ -162,6 +168,12 @@ async fn sdk_directory_storage_creates_deterministic_sqlite_files() {
let outbox_tables = sqlite_table_names(&paths.outbox_path).await;
assert!(outbox_tables.iter().any(|name| name == "outbox_operations"));
assert!(!outbox_tables.iter().any(|name| name == "outbox_operation"));
+ let private_tables = sqlite_table_names(&paths.private_store_path).await;
+ assert!(
+ private_tables
+ .iter()
+ .any(|name| name == "sdk_private_farm_location")
+ );
}
#[tokio::test]
@@ -180,13 +192,17 @@ async fn sdk_memory_storage_status_and_integrity_report_canonical_stores() {
assert_eq!(status.paths, None);
assert_eq!(status.event_store.store.schema_version, 1);
assert_eq!(status.outbox.store.schema_version, 1);
+ assert_eq!(status.private_store.store.schema_version, 1);
assert!(status.event_store.store.foreign_keys_enabled);
assert!(status.outbox.store.foreign_keys_enabled);
+ assert!(status.private_store.store.foreign_keys_enabled);
assert_eq!(status.event_store.total_events, 0);
assert_eq!(status.outbox.total_events, 0);
+ assert_eq!(status.private_store.farm_private_locations, 0);
assert_eq!(status.outbox.failed_terminal_events, 0);
assert!(status.event_store.store.integrity_ok);
assert!(status.outbox.store.integrity_ok);
+ assert!(status.private_store.store.integrity_ok);
let integrity = sdk
.integrity(IntegrityRequest::new())
@@ -195,8 +211,10 @@ async fn sdk_memory_storage_status_and_integrity_report_canonical_stores() {
assert!(integrity.checked_paths.is_empty());
assert!(integrity.event_store_ok);
assert!(integrity.outbox_ok);
+ assert!(integrity.private_store_ok);
assert_eq!(integrity.event_store_result, "ok");
assert_eq!(integrity.outbox_result, "ok");
+ assert_eq!(integrity.private_store_result, "ok");
}
#[tokio::test]
@@ -658,7 +676,7 @@ fn storage_backup_and_integrity_contract_dtos_serialize() {
last_event_updated_at_ms: Some(1_700_000_000_000),
},
outbox: SdkOutboxStorageStatus {
- store,
+ store: store.clone(),
total_events: 3,
pending_events: 1,
retryable_events: 1,
@@ -669,6 +687,10 @@ fn storage_backup_and_integrity_contract_dtos_serialize() {
last_attempt_at_ms: Some(1_700_000_000_000),
last_error: Some("relay publish incomplete".to_owned()),
},
+ private_store: SdkPrivateStoreStorageStatus {
+ store,
+ farm_private_locations: 4,
+ },
})
.expect("status receipt"),
serde_json::json!({
@@ -707,6 +729,17 @@ fn storage_backup_and_integrity_contract_dtos_serialize() {
"publishing_events": 0,
"last_attempt_at_ms": 1700000000000i64,
"last_error": "relay publish incomplete"
+ },
+ "private_store": {
+ "store": {
+ "schema_version": 1,
+ "journal_mode": "wal",
+ "foreign_keys_enabled": true,
+ "busy_timeout_ms": 5000,
+ "integrity_ok": true,
+ "integrity_result": "ok"
+ },
+ "farm_private_locations": 4
}
})
);
@@ -744,15 +777,19 @@ fn storage_backup_and_integrity_contract_dtos_serialize() {
serde_json::to_value(SdkBackupVerification {
event_store_ok: true,
outbox_ok: true,
+ private_store_ok: true,
event_store_events: 2,
outbox_events: 3,
+ private_farm_locations: 4,
})
.expect("backup verification"),
serde_json::json!({
"event_store_ok": true,
"outbox_ok": true,
+ "private_store_ok": true,
"event_store_events": 2,
- "outbox_events": 3
+ "outbox_events": 3,
+ "private_farm_locations": 4
})
);
assert_eq!(
diff --git a/crates/sdk/tests/sync_runtime.rs b/crates/sdk/tests/sync_runtime.rs
@@ -603,11 +603,13 @@ async fn sdk_directory_backup_creates_verified_canonical_store_copy() {
integrity.checked_paths,
vec![
source_paths.event_store_path.clone(),
- source_paths.outbox_path.clone()
+ source_paths.outbox_path.clone(),
+ source_paths.private_store_path.clone()
]
);
assert!(integrity.event_store_ok);
assert!(integrity.outbox_ok);
+ assert!(integrity.private_store_ok);
let backup_destination = tempdir.path().join("backup");
let backup = sdk
@@ -619,9 +621,14 @@ async fn sdk_directory_backup_creates_verified_canonical_store_copy() {
.as_ref()
.expect("event store backup");
let outbox_path = backup.outbox_path.as_ref().expect("outbox backup");
+ let private_store_path = backup
+ .private_store_path
+ .as_ref()
+ .expect("private store backup");
let manifest_path = backup.manifest_path.as_ref().expect("manifest");
assert!(event_store_path.exists());
assert!(outbox_path.exists());
+ assert!(private_store_path.exists());
assert!(manifest_path.exists());
assert_eq!(
backup.manifest.manifest_kind,
@@ -635,11 +642,28 @@ async fn sdk_directory_backup_creates_verified_canonical_store_copy() {
backup.manifest.backup_paths.outbox_path,
PathBuf::from("outbox.sqlite")
);
+ assert_eq!(
+ backup.manifest.backup_paths.private_store_path,
+ PathBuf::from("private.sqlite")
+ );
assert_eq!(backup.manifest.created_at_ms, 1_700_000_000_000);
assert_eq!(backup.manifest.source_status.event_store.total_events, 1);
assert_eq!(backup.manifest.source_status.outbox.total_events, 1);
+ assert_eq!(
+ backup
+ .manifest
+ .source_status
+ .private_store
+ .farm_private_locations,
+ 0
+ );
assert!(backup.manifest.backup_verification.event_store_ok);
assert!(backup.manifest.backup_verification.outbox_ok);
+ assert!(backup.manifest.backup_verification.private_store_ok);
+ assert_eq!(
+ backup.manifest.backup_verification.private_farm_locations,
+ 0
+ );
let restore_archive = RadrootsClient::inspect_restore_archive(backup_destination.clone())
.await
@@ -657,6 +681,12 @@ async fn sdk_directory_backup_creates_verified_canonical_store_copy() {
restore_archive.outbox_path,
outbox_path.canonicalize().expect("outbox canonical")
);
+ assert_eq!(
+ restore_archive.private_store_path,
+ private_store_path
+ .canonicalize()
+ .expect("private store canonical")
+ );
let backup_event_store = RadrootsEventStore::open_file(event_store_path)
.await
@@ -738,6 +768,13 @@ async fn runtime_backup_rejects_empty_destination_and_overwrites_file_destinatio
.exists()
);
assert!(receipt.outbox_path.as_ref().expect("outbox").exists());
+ assert!(
+ receipt
+ .private_store_path
+ .as_ref()
+ .expect("private store")
+ .exists()
+ );
}
#[cfg(unix)]
diff --git a/crates/sdk/tests/unit/farms_runtime_tests.rs b/crates/sdk/tests/unit/farms_runtime_tests.rs
@@ -139,6 +139,39 @@ fn farm_request_builders_reject_invalid_options_and_timestamp_bounds() {
));
}
+#[test]
+fn farm_public_locality_derivation_covers_country_fallback_and_empty_names() {
+ let reverse = GeocoderReverseResult {
+ id: 1,
+ name: " Fixture Town ".to_owned(),
+ admin1_id: None,
+ admin1_name: None,
+ country_id: "FX".to_owned(),
+ country_name: None,
+ latitude: 12.25,
+ longitude: -34.50,
+ };
+ let locality = public_locality_from_reverse(SdkExactLocation::new(12.26, -34.51), &reverse)
+ .expect("locality");
+ assert_eq!(locality.primary, "Fixture Town");
+ assert_eq!(locality.city.as_deref(), Some("Fixture Town"));
+ assert_eq!(locality.region, None);
+ assert_eq!(locality.country.as_deref(), Some("FX"));
+ assert_eq!(locality.geohash5, "e4pmw");
+
+ let blank_name = GeocoderReverseResult {
+ name: " ".to_owned(),
+ ..reverse
+ };
+ assert!(matches!(
+ public_locality_from_reverse(SdkExactLocation::new(12.26, -34.51), &blank_name),
+ Err(RadrootsSdkError::GeoNames {
+ kind: crate::RadrootsSdkGeoNamesErrorKind::Lookup,
+ ..
+ })
+ ));
+}
+
#[tokio::test]
async fn farm_client_prepare_resolves_default_and_explicit_created_at() {
let sdk = crate::RadrootsClient::builder()
diff --git a/crates/sdk/tests/unit/runtime_tests.rs b/crates/sdk/tests/unit/runtime_tests.rs
@@ -31,6 +31,14 @@ fn assert_outbox_error<T>(result: Result<T, RadrootsSdkError>) {
}
}
+fn assert_private_store_error<T>(result: Result<T, RadrootsSdkError>) {
+ match result {
+ Err(RadrootsSdkError::PrivateStore { .. }) => {}
+ Err(other) => panic!("expected private store error, got {other:?}"),
+ Ok(_) => panic!("expected private store error"),
+ }
+}
+
fn sqlite_status() -> SdkSqliteStoreStatus {
SdkSqliteStoreStatus {
schema_version: 1,
@@ -66,6 +74,10 @@ fn storage_status() -> StorageStatusReceipt {
last_attempt_at_ms: None,
last_error: None,
},
+ private_store: SdkPrivateStoreStorageStatus {
+ store: sqlite_status(),
+ farm_private_locations: 0,
+ },
}
}
@@ -73,8 +85,10 @@ fn verification(event_store_ok: bool, outbox_ok: bool) -> SdkBackupVerification
SdkBackupVerification {
event_store_ok,
outbox_ok,
+ private_store_ok: true,
event_store_events: 0,
outbox_events: 0,
+ private_farm_locations: 0,
}
}
@@ -112,12 +126,75 @@ fn manifest() -> SdkBackupManifest {
backup_paths: RadrootsSdkStoragePaths {
event_store_path: PathBuf::from(EVENT_STORE_BACKUP_FILE),
outbox_path: PathBuf::from(OUTBOX_BACKUP_FILE),
+ private_store_path: PathBuf::from(PRIVATE_STORE_BACKUP_FILE),
},
source_status: storage_status(),
backup_verification: verification(true, true),
}
}
+fn private_farm_location_record() -> crate::private_store::SdkPrivateFarmLocationRecord {
+ crate::private_store::SdkPrivateFarmLocationRecord {
+ farm_addr: radroots_events::ids::RadrootsAddressableCoordinate::parse(format!(
+ "{}:{}:{}",
+ radroots_events::kinds::KIND_FARM,
+ "a".repeat(64),
+ "AAAAAAAAAAAAAAAAAAAAAA"
+ ))
+ .expect("farm addr"),
+ farm_pubkey: "a".repeat(64),
+ farm_d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(),
+ latitude: 12.26,
+ longitude: -34.51,
+ locality_primary: "Fixture Town".to_owned(),
+ locality_city: Some("Fixture Town".to_owned()),
+ locality_region: Some("Fixture Region".to_owned()),
+ locality_country: Some("Fixture Country".to_owned()),
+ geohash5: "e4pmw".to_owned(),
+ geonames_feature_id: Some(1),
+ geonames_country_id: Some("FX".to_owned()),
+ updated_at_ms: 1_700_000_123_000,
+ }
+}
+
+#[tokio::test]
+async fn private_store_validates_location_rows_and_round_trips_valid_records() {
+ let store = SdkPrivateStore::open_memory().await.expect("private store");
+ let record = private_farm_location_record();
+ store
+ .upsert_farm_location(&record)
+ .await
+ .expect("valid private farm location");
+ assert_eq!(
+ store
+ .farm_location(&record.farm_addr)
+ .await
+ .expect("lookup"),
+ Some(record.clone())
+ );
+
+ let mut invalid_coordinates = record.clone();
+ invalid_coordinates.latitude = f64::NAN;
+ assert!(matches!(
+ store.upsert_farm_location(&invalid_coordinates).await,
+ Err(RadrootsSdkError::InvalidRequest { .. })
+ ));
+
+ let mut blank_locality = record.clone();
+ blank_locality.locality_primary = " ".to_owned();
+ assert!(matches!(
+ store.upsert_farm_location(&blank_locality).await,
+ Err(RadrootsSdkError::InvalidRequest { .. })
+ ));
+
+ let mut invalid_geohash = record;
+ invalid_geohash.geohash5 = "abcd".to_owned();
+ assert!(matches!(
+ store.upsert_farm_location(&invalid_geohash).await,
+ Err(RadrootsSdkError::InvalidRequest { .. })
+ ));
+}
+
#[tokio::test]
async fn open_storage_and_storage_kind_cover_memory_directory_and_file_failures() {
let memory = open_storage(&RadrootsSdkStorageConfig::Memory)
@@ -127,6 +204,7 @@ async fn open_storage_and_storage_kind_cover_memory_directory_and_file_failures(
let memory_sdk = RadrootsClient {
_event_store: memory.event_store,
_outbox: memory.outbox,
+ _private_store: memory.private_store,
storage_paths: None,
geonames: None,
clock: RadrootsSdkClock::Fixed(RadrootsSdkTimestamp::from_unix_seconds(1)),
@@ -145,9 +223,11 @@ async fn open_storage_and_storage_kind_cover_memory_directory_and_file_failures(
let directory_paths = directory_storage.paths.expect("directory paths");
assert!(directory_paths.event_store_path.exists());
assert!(directory_paths.outbox_path.exists());
+ assert!(directory_paths.private_store_path.exists());
let directory_sdk = RadrootsClient {
_event_store: directory_storage.event_store,
_outbox: directory_storage.outbox,
+ _private_store: directory_storage.private_store,
storage_paths: Some(directory_paths),
geonames: None,
clock: RadrootsSdkClock::Fixed(RadrootsSdkTimestamp::from_unix_seconds(1)),
@@ -172,6 +252,12 @@ async fn open_storage_and_storage_kind_cover_memory_directory_and_file_failures(
fs::create_dir(&outbox_directory).expect("outbox dir");
fs::create_dir(outbox_directory.join(OUTBOX_BACKUP_FILE)).expect("outbox file slot dir");
assert_outbox_error(open_directory_storage(&outbox_directory).await);
+
+ let private_store_directory = tempdir.path().join("private-store-directory");
+ fs::create_dir(&private_store_directory).expect("private store dir");
+ fs::create_dir(private_store_directory.join(PRIVATE_STORE_BACKUP_FILE))
+ .expect("private store file slot dir");
+ assert_private_store_error(open_directory_storage(&private_store_directory).await);
}
#[tokio::test]
@@ -855,11 +941,13 @@ async fn sqlite_backup_errors_cover_invalid_paths_and_execute_failures() {
let backup_paths = RadrootsSdkStoragePaths {
event_store_path: tempdir.path().join("closed-event-store-backup.sqlite"),
outbox_path: tempdir.path().join("closed-event-store-outbox.sqlite"),
+ private_store_path: tempdir.path().join("closed-event-store-private.sqlite"),
};
assert_event_store_error(
backup_sqlite_stores(
storage.event_store.pool(),
storage.outbox.pool(),
+ storage.private_store.pool(),
&backup_paths,
)
.await,
@@ -872,11 +960,13 @@ async fn sqlite_backup_errors_cover_invalid_paths_and_execute_failures() {
let outbox_closed_paths = RadrootsSdkStoragePaths {
event_store_path: tempdir.path().join("open-event-store-backup.sqlite"),
outbox_path: tempdir.path().join("closed-outbox-backup.sqlite"),
+ private_store_path: tempdir.path().join("outbox-closed-private.sqlite"),
};
assert_event_store_error(
backup_sqlite_stores(
outbox_closed_storage.event_store.pool(),
outbox_closed_storage.outbox.pool(),
+ outbox_closed_storage.private_store.pool(),
&outbox_closed_paths,
)
.await,
@@ -887,6 +977,7 @@ async fn sqlite_backup_errors_cover_invalid_paths_and_execute_failures() {
RadrootsSdkStoragePaths {
event_store_path: tempdir.path().join(EVENT_STORE_BACKUP_FILE),
outbox_path: tempdir.path().join(OUTBOX_BACKUP_FILE),
+ private_store_path: tempdir.path().join(PRIVATE_STORE_BACKUP_FILE),
},
tempdir.path().to_path_buf(),
manifest(),
@@ -925,6 +1016,7 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
source: tempdir.path().join("missing-archive"),
event_store_path: tempdir.path().join("missing-event-store.sqlite"),
outbox_path: tempdir.path().join("missing-outbox.sqlite"),
+ private_store_path: tempdir.path().join("missing-private.sqlite"),
manifest_path: tempdir.path().join(BACKUP_MANIFEST_FILE),
manifest: manifest(),
verification: verification(true, true),
@@ -932,6 +1024,7 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
let staging_paths = RadrootsSdkStoragePaths {
event_store_path: tempdir.path().join("staging-event-store.sqlite"),
outbox_path: tempdir.path().join("staging-outbox.sqlite"),
+ private_store_path: tempdir.path().join("staging-private.sqlite"),
};
assert!(
io_message(copy_restore_archive_to_staging(&missing_archive, &staging_paths).await)
@@ -949,10 +1042,12 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
let corrupt_archive = RestoreArchive {
event_store_path: tempdir.path().join("corrupt-event-store.sqlite"),
outbox_path: tempdir.path().join("corrupt-outbox.sqlite"),
+ private_store_path: tempdir.path().join("corrupt-private.sqlite"),
..missing_archive.clone()
};
fs::write(&corrupt_archive.event_store_path, b"not sqlite").expect("corrupt event store");
fs::write(&corrupt_archive.outbox_path, b"not sqlite").expect("corrupt outbox");
+ fs::write(&corrupt_archive.private_store_path, b"not sqlite").expect("corrupt private store");
assert_event_store_error(
copy_restore_archive_to_staging(&corrupt_archive, &staging_paths).await,
);
@@ -988,6 +1083,7 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
let protected_paths = RadrootsSdkStoragePaths {
event_store_path: protected_destination.join(EVENT_STORE_BACKUP_FILE),
outbox_path: protected_destination.join(OUTBOX_BACKUP_FILE),
+ private_store_path: protected_destination.join(PRIVATE_STORE_BACKUP_FILE),
};
set_mode(&protected_parent, 0o500);
let protected_result =
@@ -1022,6 +1118,7 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
let missing_outbox_paths = RadrootsSdkStoragePaths {
event_store_path: archive.event_store_path.clone(),
outbox_path: tempdir.path().to_path_buf(),
+ private_store_path: archive.private_store_path.clone(),
};
assert!(verify_backup_paths(&missing_outbox_paths).await.is_err());
@@ -1029,6 +1126,7 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
let invalid_paths = RadrootsSdkStoragePaths {
event_store_path: invalid_destination.join(EVENT_STORE_BACKUP_FILE),
outbox_path: invalid_destination.join(OUTBOX_BACKUP_FILE),
+ private_store_path: invalid_destination.join(PRIVATE_STORE_BACKUP_FILE),
};
let invalid_restore_message = io_message(
restore_archive_to_destination(&archive, &invalid_destination, &invalid_paths).await,
@@ -1101,6 +1199,7 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
let wrong_destination_paths = RadrootsSdkStoragePaths {
event_store_path: populated_archive.event_store_path.clone(),
outbox_path: populated_archive.outbox_path.clone(),
+ private_store_path: populated_archive.private_store_path.clone(),
};
assert!(
invalid_request_message(