commit ab432b108686980036251f268d060b8674d71f44 parent 89493babf4162744b9911ac24e1c67eae69a7720 Author: triesap <tyson@radroots.org> Date: Fri, 12 Jun 2026 22:30:03 -0700 replica: replace event state with event heads - rename the active replica event-state table, schema, APIs, and wasm surface to event-head names - drive replica ingest from contract-derived event-head candidates and NIP-01 tie-breaks - make profile metadata replaceable and keep order events out of head selection - update replica tests for deterministic hex pubkeys and same-timestamp lower-id winners Diffstat:
31 files changed, 927 insertions(+), 765 deletions(-)
diff --git a/crates/events/src/contract.rs b/crates/events/src/contract.rs @@ -589,7 +589,7 @@ static ALL_KIND_CONTRACTS: &[RadrootsKindContract] = &[ KIND_PROFILE, "KIND_PROFILE", "Profile Metadata", - RadrootsEventClass::Regular, + RadrootsEventClass::Replaceable, RadrootsNostrStandard::Nip01, ["radroots.profile.metadata.v1"] ), @@ -1320,7 +1320,7 @@ static ALL_EVENT_CONTRACTS: &[RadrootsEventContract] = &[ KIND_PROFILE, "Profile Metadata", "RadrootsProfile", - RadrootsEventClass::Regular, + RadrootsEventClass::Replaceable, RadrootsEventPrivacy::Public, RadrootsActorRole::Any, RadrootsContentSchema::JsonObject, diff --git a/crates/events/src/event_head.rs b/crates/events/src/event_head.rs @@ -417,7 +417,7 @@ mod tests { } #[test] - fn contract_bridge_keeps_regular_events_out_of_head_selection() { + fn contract_bridge_uses_profile_replaceable_heads() { let profile = event_with_content( KIND_PROFILE, &hex_64('3'), @@ -426,11 +426,22 @@ mod tests { Vec::new(), r#"{"name":"Alice"}"#, ); + let RadrootsEventHeadCandidateResult::Candidate(candidate) = + event_head_candidate_for_event(&profile).expect("profile contract") + else { + panic!("expected candidate") + }; assert_eq!( - event_head_candidate_for_event(&profile).expect("profile contract"), - RadrootsEventHeadCandidateResult::NotHeadSelected + candidate.coordinate, + RadrootsEventHeadCoordinate::Replaceable { + kind: KIND_PROFILE, + pubkey: RadrootsPublicKey::parse(hex_64('c')).unwrap() + } ); + } + #[test] + fn contract_bridge_keeps_order_events_out_of_head_selection() { let order = event_with_content( KIND_ORDER_REQUEST, &hex_64('4'), diff --git a/crates/replica_db/migrations/0018_nostr_event_head.down.sql b/crates/replica_db/migrations/0018_nostr_event_head.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS nostr_event_head; diff --git a/crates/replica_db/migrations/0018_nostr_event_head.up.sql b/crates/replica_db/migrations/0018_nostr_event_head.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS nostr_event_head ( + id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), + created_at DATETIME NOT NULL CHECK(length(created_at) = 24), + updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), + key TEXT NOT NULL UNIQUE, + kind INTEGER NOT NULL, + pubkey CHAR(64) NOT NULL CHECK(length(pubkey) = 64), + d_tag TEXT NOT NULL, + last_event_id CHAR(64) NOT NULL CHECK(length(last_event_id) = 64), + last_created_at INTEGER NOT NULL, + content_hash TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS nostr_event_head_kind_idx ON nostr_event_head(kind); diff --git a/crates/replica_db/migrations/0018_nostr_event_state.down.sql b/crates/replica_db/migrations/0018_nostr_event_state.down.sql @@ -1 +0,0 @@ -DROP TABLE IF EXISTS nostr_event_state; diff --git a/crates/replica_db/migrations/0018_nostr_event_state.up.sql b/crates/replica_db/migrations/0018_nostr_event_state.up.sql @@ -1,14 +0,0 @@ -CREATE TABLE IF NOT EXISTS nostr_event_state ( - id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), - created_at DATETIME NOT NULL CHECK(length(created_at) = 24), - updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), - key TEXT NOT NULL UNIQUE, - kind INTEGER NOT NULL, - pubkey CHAR(64) NOT NULL CHECK(length(pubkey) = 64), - d_tag TEXT NOT NULL, - last_event_id CHAR(64) NOT NULL CHECK(length(last_event_id) = 64), - last_created_at INTEGER NOT NULL, - content_hash TEXT NOT NULL -); - -CREATE INDEX IF NOT EXISTS nostr_event_state_kind_idx ON nostr_event_state(kind); diff --git a/crates/replica_db/migrations/0019_repair_missing_indexes.down.sql b/crates/replica_db/migrations/0019_repair_missing_indexes.down.sql @@ -1,4 +1,4 @@ -DROP INDEX IF EXISTS nostr_event_state_kind_idx; +DROP INDEX IF EXISTS nostr_event_head_kind_idx; DROP INDEX IF EXISTS plot_farm_d_tag_idx; DROP INDEX IF EXISTS gcs_location_geohash_idx; DROP INDEX IF EXISTS farm_pubkey_d_tag_idx; diff --git a/crates/replica_db/migrations/0019_repair_missing_indexes.up.sql b/crates/replica_db/migrations/0019_repair_missing_indexes.up.sql @@ -1,4 +1,4 @@ CREATE UNIQUE INDEX IF NOT EXISTS farm_pubkey_d_tag_idx ON farm(pubkey, d_tag); CREATE INDEX IF NOT EXISTS gcs_location_geohash_idx ON gcs_location(geohash); CREATE UNIQUE INDEX IF NOT EXISTS plot_farm_d_tag_idx ON plot(farm_id, d_tag); -CREATE INDEX IF NOT EXISTS nostr_event_state_kind_idx ON nostr_event_state(kind); +CREATE INDEX IF NOT EXISTS nostr_event_head_kind_idx ON nostr_event_head(kind); diff --git a/crates/replica_db/src/lib.rs b/crates/replica_db/src/lib.rs @@ -60,11 +60,11 @@ use radroots_replica_db_schema::nostr_profile::{ INostrProfileUpdateResolve, }; -use radroots_replica_db_schema::nostr_event_state::{ - INostrEventStateCreate, INostrEventStateCreateResolve, INostrEventStateDelete, - INostrEventStateDeleteResolve, INostrEventStateFindMany, INostrEventStateFindManyResolve, - INostrEventStateFindOne, INostrEventStateFindOneResolve, INostrEventStateUpdate, - INostrEventStateUpdateResolve, +use radroots_replica_db_schema::nostr_event_head::{ + INostrEventHeadCreate, INostrEventHeadCreateResolve, INostrEventHeadDelete, + INostrEventHeadDeleteResolve, INostrEventHeadFindMany, INostrEventHeadFindManyResolve, + INostrEventHeadFindOne, INostrEventHeadFindOneResolve, INostrEventHeadUpdate, + INostrEventHeadUpdateResolve, }; use radroots_replica_db_schema::nostr_relay::{ @@ -577,39 +577,39 @@ impl<E: SqlExecutor> ReplicaSql<E> { models::nostr_profile::delete(self.executor(), opts) } - pub fn nostr_event_state_create( + pub fn nostr_event_head_create( &self, - opts: &INostrEventStateCreate, - ) -> Result<INostrEventStateCreateResolve, IError<SqlError>> { - models::nostr_event_state::create(self.executor(), opts) + opts: &INostrEventHeadCreate, + ) -> Result<INostrEventHeadCreateResolve, IError<SqlError>> { + models::nostr_event_head::create(self.executor(), opts) } - pub fn nostr_event_state_find_many( + pub fn nostr_event_head_find_many( &self, - opts: &INostrEventStateFindMany, - ) -> Result<INostrEventStateFindManyResolve, IError<SqlError>> { - models::nostr_event_state::find_many(self.executor(), opts) + opts: &INostrEventHeadFindMany, + ) -> Result<INostrEventHeadFindManyResolve, IError<SqlError>> { + models::nostr_event_head::find_many(self.executor(), opts) } - pub fn nostr_event_state_find_one( + pub fn nostr_event_head_find_one( &self, - opts: &INostrEventStateFindOne, - ) -> Result<INostrEventStateFindOneResolve, IError<SqlError>> { - models::nostr_event_state::find_one(self.executor(), opts) + opts: &INostrEventHeadFindOne, + ) -> Result<INostrEventHeadFindOneResolve, IError<SqlError>> { + models::nostr_event_head::find_one(self.executor(), opts) } - pub fn nostr_event_state_update( + pub fn nostr_event_head_update( &self, - opts: &INostrEventStateUpdate, - ) -> Result<INostrEventStateUpdateResolve, IError<SqlError>> { - models::nostr_event_state::update(self.executor(), opts) + opts: &INostrEventHeadUpdate, + ) -> Result<INostrEventHeadUpdateResolve, IError<SqlError>> { + models::nostr_event_head::update(self.executor(), opts) } - pub fn nostr_event_state_delete( + pub fn nostr_event_head_delete( &self, - opts: &INostrEventStateDelete, - ) -> Result<INostrEventStateDeleteResolve, IError<SqlError>> { - models::nostr_event_state::delete(self.executor(), opts) + opts: &INostrEventHeadDelete, + ) -> Result<INostrEventHeadDeleteResolve, IError<SqlError>> { + models::nostr_event_head::delete(self.executor(), opts) } pub fn nostr_relay_create( diff --git a/crates/replica_db/src/migrations.rs b/crates/replica_db/src/migrations.rs @@ -94,9 +94,9 @@ pub static MIGRATIONS: &[Migration] = &[ down_sql: include_str!("../migrations/0017_farm_member_claim.down.sql"), }, Migration { - name: "0018_nostr_event_state", - up_sql: include_str!("../migrations/0018_nostr_event_state.up.sql"), - down_sql: include_str!("../migrations/0018_nostr_event_state.down.sql"), + name: "0018_nostr_event_head", + up_sql: include_str!("../migrations/0018_nostr_event_head.up.sql"), + down_sql: include_str!("../migrations/0018_nostr_event_head.down.sql"), }, Migration { name: "0019_repair_missing_indexes", diff --git a/crates/replica_db/src/models/mod.rs b/crates/replica_db/src/models/mod.rs @@ -6,7 +6,7 @@ pub mod farm_tag; pub mod gcs_location; pub mod log_error; pub mod media_image; -pub mod nostr_event_state; +pub mod nostr_event_head; pub mod nostr_profile; pub mod nostr_profile_relay; pub mod nostr_relay; diff --git a/crates/replica_db/src/models/nostr_event_head.rs b/crates/replica_db/src/models/nostr_event_head.rs @@ -0,0 +1,150 @@ +use radroots_replica_db_schema::nostr_event_head::{ + INostrEventHeadCreate, INostrEventHeadCreateResolve, INostrEventHeadDelete, + INostrEventHeadDeleteResolve, INostrEventHeadFieldsFilter, INostrEventHeadFindMany, + INostrEventHeadFindManyResolve, INostrEventHeadFindOne, INostrEventHeadFindOneResolve, + INostrEventHeadUpdate, INostrEventHeadUpdateResolve, NostrEventHead, + NostrEventHeadQueryBindValues, +}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{SqlExecutor, utils}; +use radroots_types::types::{IError, IResult, IResultList}; +use serde_json::Value; + +const TABLE_NAME: &str = "nostr_event_head"; + +pub fn create( + exec: &dyn SqlExecutor, + opts: &INostrEventHeadCreate, +) -> Result<INostrEventHeadCreateResolve, IError<SqlError>> { + let field_map = utils::to_object_map(opts).expect("serialize object map"); + let id = utils::uuidv4(); + let now = utils::time_created_on(); + let meta: [(&str, Value); 3] = [ + ("id", Value::from(id.clone())), + ("created_at", Value::from(now.clone())), + ("updated_at", Value::from(now.clone())), + ]; + let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); + let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); + let _ = exec.exec(&sql, ¶ms_json)?; + let on = NostrEventHeadQueryBindValues::Id { id: id.clone() }; + let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; + Ok(IResult { result }) +} + +pub fn find_one( + exec: &dyn SqlExecutor, + opts: &INostrEventHeadFindOne, +) -> Result<INostrEventHeadFindOneResolve, IError<SqlError>> { + let result = match opts { + INostrEventHeadFindOne::On(args) => find_one_by_on(exec, &args.on)?, + }; + Ok(IResult { result }) +} + +pub fn find_many( + exec: &dyn SqlExecutor, + opts: &INostrEventHeadFindMany, +) -> Result<INostrEventHeadFindManyResolve, IError<SqlError>> { + let results = find_many_filter(exec, &opts.filter)?; + Ok(IResultList { results }) +} + +fn find_many_filter( + exec: &dyn SqlExecutor, + filter: &Option<INostrEventHeadFieldsFilter>, +) -> Result<Vec<NostrEventHead>, IError<SqlError>> { + let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); + let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let rows: Vec<NostrEventHead> = utils::parse_json(&json)?; + Ok(rows) +} + +fn find_one_by_on( + exec: &dyn SqlExecutor, + on: &NostrEventHeadQueryBindValues, +) -> Result<Option<NostrEventHead>, IError<SqlError>> { + let (column, value) = on.to_filter_param(); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); + let params_json = utils::to_params_json(vec![value]).expect("serialize bind params"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<NostrEventHead> = utils::parse_json(&json)?; + Ok(rows.pop()) +} + +fn select_by_id(exec: &dyn SqlExecutor, id: &str) -> Result<NostrEventHead, IError<SqlError>> { + let params_json = + utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); + let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); + let json = exec.query_raw(&sql, ¶ms_json)?; + let mut rows: Vec<NostrEventHead> = utils::parse_json(&json)?; + rows.pop() + .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) +} + +pub fn update( + exec: &dyn SqlExecutor, + opts: &INostrEventHeadUpdate, +) -> Result<INostrEventHeadUpdateResolve, IError<SqlError>> { + let mut updates = + utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); + if updates.is_empty() { + return Err(IError::from(SqlError::InvalidArgument(String::from( + "no fields to update", + )))); + } + updates.insert( + String::from("updated_at"), + Value::from(utils::time_created_on()), + ); + let mut set_parts = Vec::with_capacity(updates.len()); + let mut bind_values = Vec::with_capacity(updates.len() + 1); + for (column, value) in updates { + set_parts.push(format!("{column} = ?")); + bind_values.push(utils::to_db_bind_value(&value)); + } + let id_for_lookup = match opts.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &opts.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; + model.id + } + }; + bind_values.push(Value::from(id_for_lookup.clone())); + let sql = format!( + "UPDATE {TABLE_NAME} SET {} WHERE id = ?;", + set_parts.join(", ") + ); + let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); + let _ = exec.exec(&sql, ¶ms_json)?; + let updated = select_by_id(exec, &id_for_lookup)?; + Ok(IResult { result: updated }) +} + +pub fn delete( + exec: &dyn SqlExecutor, + opts: &INostrEventHeadDelete, +) -> Result<INostrEventHeadDeleteResolve, IError<SqlError>> { + let id_for_lookup = match opts { + INostrEventHeadDelete::On(args) => match args.on.primary_key() { + Some(id) => id, + None => { + let found = find_one_by_on(exec, &args.on)?; + let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; + model.id + } + }, + }; + let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) + .expect("serialize bind params"); + let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); + let outcome = exec.exec(&sql, ¶ms_json)?; + if outcome.changes == 0 { + return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); + } + Ok(IResult { + result: id_for_lookup, + }) +} diff --git a/crates/replica_db/src/models/nostr_event_state.rs b/crates/replica_db/src/models/nostr_event_state.rs @@ -1,150 +0,0 @@ -use radroots_replica_db_schema::nostr_event_state::{ - INostrEventStateCreate, INostrEventStateCreateResolve, INostrEventStateDelete, - INostrEventStateDeleteResolve, INostrEventStateFieldsFilter, INostrEventStateFindMany, - INostrEventStateFindManyResolve, INostrEventStateFindOne, INostrEventStateFindOneResolve, - INostrEventStateUpdate, INostrEventStateUpdateResolve, NostrEventState, - NostrEventStateQueryBindValues, -}; -use radroots_sql_core::error::SqlError; -use radroots_sql_core::{SqlExecutor, utils}; -use radroots_types::types::{IError, IResult, IResultList}; -use serde_json::Value; - -const TABLE_NAME: &str = "nostr_event_state"; - -pub fn create( - exec: &dyn SqlExecutor, - opts: &INostrEventStateCreate, -) -> Result<INostrEventStateCreateResolve, IError<SqlError>> { - let field_map = utils::to_object_map(opts).expect("serialize object map"); - let id = utils::uuidv4(); - let now = utils::time_created_on(); - let meta: [(&str, Value); 3] = [ - ("id", Value::from(id.clone())), - ("created_at", Value::from(now.clone())), - ("updated_at", Value::from(now.clone())), - ]; - let (sql, bind_values) = utils::build_insert_query_with_meta(TABLE_NAME, &meta, &field_map); - let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); - let _ = exec.exec(&sql, ¶ms_json)?; - let on = NostrEventStateQueryBindValues::Id { id: id.clone() }; - let result = find_one_by_on(exec, &on)?.ok_or(IError::from(SqlError::NotFound(id.clone())))?; - Ok(IResult { result }) -} - -pub fn find_one( - exec: &dyn SqlExecutor, - opts: &INostrEventStateFindOne, -) -> Result<INostrEventStateFindOneResolve, IError<SqlError>> { - let result = match opts { - INostrEventStateFindOne::On(args) => find_one_by_on(exec, &args.on)?, - }; - Ok(IResult { result }) -} - -pub fn find_many( - exec: &dyn SqlExecutor, - opts: &INostrEventStateFindMany, -) -> Result<INostrEventStateFindManyResolve, IError<SqlError>> { - let results = find_many_filter(exec, &opts.filter)?; - Ok(IResultList { results }) -} - -fn find_many_filter( - exec: &dyn SqlExecutor, - filter: &Option<INostrEventStateFieldsFilter>, -) -> Result<Vec<NostrEventState>, IError<SqlError>> { - let (sql, bind_values) = utils::build_select_query_with_meta(TABLE_NAME, filter.as_ref()); - let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let rows: Vec<NostrEventState> = utils::parse_json(&json)?; - Ok(rows) -} - -fn find_one_by_on( - exec: &dyn SqlExecutor, - on: &NostrEventStateQueryBindValues, -) -> Result<Option<NostrEventState>, IError<SqlError>> { - let (column, value) = on.to_filter_param(); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE {column} = ? LIMIT 1;"); - let params_json = utils::to_params_json(vec![value]).expect("serialize bind params"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<NostrEventState> = utils::parse_json(&json)?; - Ok(rows.pop()) -} - -fn select_by_id(exec: &dyn SqlExecutor, id: &str) -> Result<NostrEventState, IError<SqlError>> { - let params_json = - utils::to_params_json(vec![Value::from(id.to_owned())]).expect("serialize bind params"); - let sql = format!("SELECT * FROM {TABLE_NAME} WHERE id = ?;"); - let json = exec.query_raw(&sql, ¶ms_json)?; - let mut rows: Vec<NostrEventState> = utils::parse_json(&json)?; - rows.pop() - .ok_or(IError::from(SqlError::NotFound(id.to_owned()))) -} - -pub fn update( - exec: &dyn SqlExecutor, - opts: &INostrEventStateUpdate, -) -> Result<INostrEventStateUpdateResolve, IError<SqlError>> { - let mut updates = - utils::to_partial_object_map(&opts.fields).expect("serialize partial object map"); - if updates.is_empty() { - return Err(IError::from(SqlError::InvalidArgument(String::from( - "no fields to update", - )))); - } - updates.insert( - String::from("updated_at"), - Value::from(utils::time_created_on()), - ); - let mut set_parts = Vec::with_capacity(updates.len()); - let mut bind_values = Vec::with_capacity(updates.len() + 1); - for (column, value) in updates { - set_parts.push(format!("{column} = ?")); - bind_values.push(utils::to_db_bind_value(&value)); - } - let id_for_lookup = match opts.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &opts.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(opts.on.lookup_key())))?; - model.id - } - }; - bind_values.push(Value::from(id_for_lookup.clone())); - let sql = format!( - "UPDATE {TABLE_NAME} SET {} WHERE id = ?;", - set_parts.join(", ") - ); - let params_json = utils::to_params_json(bind_values).expect("serialize bind params"); - let _ = exec.exec(&sql, ¶ms_json)?; - let updated = select_by_id(exec, &id_for_lookup)?; - Ok(IResult { result: updated }) -} - -pub fn delete( - exec: &dyn SqlExecutor, - opts: &INostrEventStateDelete, -) -> Result<INostrEventStateDeleteResolve, IError<SqlError>> { - let id_for_lookup = match opts { - INostrEventStateDelete::On(args) => match args.on.primary_key() { - Some(id) => id, - None => { - let found = find_one_by_on(exec, &args.on)?; - let model = found.ok_or(IError::from(SqlError::NotFound(args.on.lookup_key())))?; - model.id - } - }, - }; - let params_json = utils::to_params_json(vec![Value::from(id_for_lookup.clone())]) - .expect("serialize bind params"); - let sql = format!("DELETE FROM {TABLE_NAME} WHERE id = ?;"); - let outcome = exec.exec(&sql, ¶ms_json)?; - if outcome.changes == 0 { - return Err(IError::from(SqlError::NotFound(id_for_lookup.clone()))); - } - Ok(IResult { - result: id_for_lookup, - }) -} diff --git a/crates/replica_db/src/query.rs b/crates/replica_db/src/query.rs @@ -118,7 +118,7 @@ impl<E: SqlExecutor> ReplicaSql<E> { pub fn nostr_event_last_created_at(&self) -> Result<Option<u64>, SqlError> { let json = self.executor().query_raw( - "SELECT MAX(last_created_at) AS last_created_at FROM nostr_event_state WHERE last_created_at IS NOT NULL", + "SELECT MAX(last_created_at) AS last_created_at FROM nostr_event_head WHERE last_created_at IS NOT NULL", "[]", )?; let rows: Vec<ReplicaEventFreshnessRow> = diff --git a/crates/replica_db/tests/error_paths.rs b/crates/replica_db/tests/error_paths.rs @@ -28,9 +28,9 @@ use radroots_replica_db_schema::media_image::{ IMediaImageCreate, IMediaImageDelete, IMediaImageFindMany, IMediaImageFindOne, IMediaImageUpdate, }; -use radroots_replica_db_schema::nostr_event_state::{ - INostrEventStateCreate, INostrEventStateDelete, INostrEventStateFindMany, - INostrEventStateFindOne, INostrEventStateUpdate, +use radroots_replica_db_schema::nostr_event_head::{ + INostrEventHeadCreate, INostrEventHeadDelete, INostrEventHeadFindMany, INostrEventHeadFindOne, + INostrEventHeadUpdate, }; use radroots_replica_db_schema::nostr_profile::{ INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, @@ -832,29 +832,29 @@ fn log_error_error_paths_cover_regions() { } #[test] -fn nostr_event_state_error_paths_cover_regions() { +fn nostr_event_head_error_paths_cover_regions() { let db = open_db(); - let update_missing: INostrEventStateUpdate = parse_json(json!({ + let update_missing: INostrEventHeadUpdate = parse_json(json!({ "on": { "key": "state-a" }, "fields": { "content_hash": "hash-x" } })); - assert_not_found(db.nostr_event_state_update(&update_missing)); + assert_not_found(db.nostr_event_head_update(&update_missing)); - let update_missing_id: INostrEventStateUpdate = parse_json(json!({ + let update_missing_id: INostrEventHeadUpdate = parse_json(json!({ "on": { "id": "missing-id" }, "fields": { "content_hash": "hash-y" } })); - assert_not_found(db.nostr_event_state_update(&update_missing_id)); + assert_not_found(db.nostr_event_head_update(&update_missing_id)); - let delete_missing_on: INostrEventStateDelete = parse_json(json!({ + let delete_missing_on: INostrEventHeadDelete = parse_json(json!({ "on": { "key": "state-a" } })); - assert_not_found(db.nostr_event_state_delete(&delete_missing_on)); + assert_not_found(db.nostr_event_head_delete(&delete_missing_on)); - drop_table(&db, "nostr_event_state"); + drop_table(&db, "nostr_event_head"); - let create_opts: INostrEventStateCreate = parse_json(json!({ + let create_opts: INostrEventHeadCreate = parse_json(json!({ "key": "state-a", "kind": 30023, "pubkey": hex64('a'), @@ -863,28 +863,28 @@ fn nostr_event_state_error_paths_cover_regions() { "last_created_at": 1, "content_hash": "hash-a" })); - assert_invalid_query(db.nostr_event_state_create(&create_opts)); + assert_invalid_query(db.nostr_event_head_create(&create_opts)); - let find_many_filter: INostrEventStateFindMany = parse_json(json!({ + let find_many_filter: INostrEventHeadFindMany = parse_json(json!({ "filter": { "id": "id-1" } })); - assert_invalid_query(db.nostr_event_state_find_many(&find_many_filter)); + assert_invalid_query(db.nostr_event_head_find_many(&find_many_filter)); - let find_one_on: INostrEventStateFindOne = parse_json(json!({ + let find_one_on: INostrEventHeadFindOne = parse_json(json!({ "on": { "id": "id-1" } })); - assert_invalid_query(db.nostr_event_state_find_one(&find_one_on)); + assert_invalid_query(db.nostr_event_head_find_one(&find_one_on)); - let update_id: INostrEventStateUpdate = parse_json(json!({ + let update_id: INostrEventHeadUpdate = parse_json(json!({ "on": { "id": "id-1" }, "fields": { "content_hash": "hash-z" } })); - assert_invalid_query(db.nostr_event_state_update(&update_id)); + assert_invalid_query(db.nostr_event_head_update(&update_id)); - let delete_id: INostrEventStateDelete = parse_json(json!({ + let delete_id: INostrEventHeadDelete = parse_json(json!({ "on": { "id": "id-1" } })); - assert_invalid_query(db.nostr_event_state_delete(&delete_id)); + assert_invalid_query(db.nostr_event_head_delete(&delete_id)); } #[test] diff --git a/crates/replica_db/tests/full_mode.rs b/crates/replica_db/tests/full_mode.rs @@ -29,9 +29,9 @@ use radroots_replica_db_schema::media_image::{ IMediaImageCreate, IMediaImageDelete, IMediaImageFindMany, IMediaImageFindOne, IMediaImageUpdate, MediaImageFindManyRel, MediaImageTradeProductArgs, }; -use radroots_replica_db_schema::nostr_event_state::{ - INostrEventStateCreate, INostrEventStateDelete, INostrEventStateFindMany, - INostrEventStateFindOne, INostrEventStateUpdate, +use radroots_replica_db_schema::nostr_event_head::{ + INostrEventHeadCreate, INostrEventHeadDelete, INostrEventHeadFindMany, INostrEventHeadFindOne, + INostrEventHeadUpdate, }; use radroots_replica_db_schema::nostr_profile::{ INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, @@ -157,7 +157,7 @@ fn full_mode_shaped_query_helpers_cover_cli_reads() { db.trade_product_location_set(&product_location_rel) .expect("product location set"); - let nostr_event_state: INostrEventStateCreate = parse_json(json!({ + let nostr_event_head: INostrEventHeadCreate = parse_json(json!({ "key": "state-a", "kind": 30023, "pubkey": hex64('d'), @@ -166,7 +166,7 @@ fn full_mode_shaped_query_helpers_cover_cli_reads() { "last_created_at": 42, "content_hash": "hash-a" })); - db.nostr_event_state_create(&nostr_event_state) + db.nostr_event_head_create(&nostr_event_head) .expect("nostr event state create"); let rows = db @@ -342,7 +342,7 @@ fn full_mode_crud_and_relation_paths() { .expect("nostr relay create") .result; - let nostr_event_state: INostrEventStateCreate = parse_json(json!({ + let nostr_event_head: INostrEventHeadCreate = parse_json(json!({ "key": "state-a", "kind": 30023, "pubkey": hex64('d'), @@ -351,8 +351,8 @@ fn full_mode_crud_and_relation_paths() { "last_created_at": 1, "content_hash": "hash-a" })); - let nostr_event_state_created = db - .nostr_event_state_create(&nostr_event_state) + let nostr_event_head_created = db + .nostr_event_head_create(&nostr_event_head) .expect("nostr event state create") .result; @@ -1032,45 +1032,45 @@ fn full_mode_crud_and_relation_paths() { parse_json(json!({ "on": { "id": nostr_profile_created.id }, "fields": {} })); assert_invalid_argument(db.nostr_profile_update(&nostr_profile_update_empty)); - let nostr_event_state_find_many: INostrEventStateFindMany = - parse_json(json!({ "filter": { "id": nostr_event_state_created.id } })); + let nostr_event_head_find_many: INostrEventHeadFindMany = + parse_json(json!({ "filter": { "id": nostr_event_head_created.id } })); assert_eq!( - db.nostr_event_state_find_many(&nostr_event_state_find_many) + db.nostr_event_head_find_many(&nostr_event_head_find_many) .expect("nostr event state find many") .results .len(), 1 ); - let nostr_event_state_find_one: INostrEventStateFindOne = - parse_json(json!({ "on": { "id": nostr_event_state_created.id } })); + let nostr_event_head_find_one: INostrEventHeadFindOne = + parse_json(json!({ "on": { "id": nostr_event_head_created.id } })); assert!( - db.nostr_event_state_find_one(&nostr_event_state_find_one) + db.nostr_event_head_find_one(&nostr_event_head_find_one) .expect("nostr event state find one") .result .is_some() ); - let nostr_event_state_update_alt: INostrEventStateUpdate = + let nostr_event_head_update_alt: INostrEventHeadUpdate = parse_json(json!({ "on": { "key": "state-a" }, "fields": { "content_hash": "hash-b" } })); assert_eq!( - db.nostr_event_state_update(&nostr_event_state_update_alt) + db.nostr_event_head_update(&nostr_event_head_update_alt) .expect("nostr event state update") .result .content_hash, "hash-b" ); - let nostr_event_state_update_id: INostrEventStateUpdate = parse_json( - json!({ "on": { "id": nostr_event_state_created.id }, "fields": { "content_hash": "hash-c" } }), + let nostr_event_head_update_id: INostrEventHeadUpdate = parse_json( + json!({ "on": { "id": nostr_event_head_created.id }, "fields": { "content_hash": "hash-c" } }), ); assert_eq!( - db.nostr_event_state_update(&nostr_event_state_update_id) + db.nostr_event_head_update(&nostr_event_head_update_id) .expect("nostr event state update id") .result .content_hash, "hash-c" ); - let nostr_event_state_update_empty: INostrEventStateUpdate = - parse_json(json!({ "on": { "id": nostr_event_state_created.id }, "fields": {} })); - assert_invalid_argument(db.nostr_event_state_update(&nostr_event_state_update_empty)); + let nostr_event_head_update_empty: INostrEventHeadUpdate = + parse_json(json!({ "on": { "id": nostr_event_head_created.id }, "fields": {} })); + assert_invalid_argument(db.nostr_event_head_update(&nostr_event_head_update_empty)); for opts in [ INostrRelayFindMany::Rel { @@ -1292,13 +1292,13 @@ fn full_mode_crud_and_relation_paths() { let _ = db.nostr_relay_delete(&opts); } - let nostr_event_state_delete: INostrEventStateDelete = + let nostr_event_head_delete: INostrEventHeadDelete = parse_json(json!({ "on": { "key": "state-a" } })); - db.nostr_event_state_delete(&nostr_event_state_delete) + db.nostr_event_head_delete(&nostr_event_head_delete) .expect("nostr event state delete"); - let nostr_event_state_delete_missing: INostrEventStateDelete = - parse_json(json!({ "on": { "id": nostr_event_state_created.id } })); - assert_not_found(db.nostr_event_state_delete(&nostr_event_state_delete_missing)); + let nostr_event_head_delete_missing: INostrEventHeadDelete = + parse_json(json!({ "on": { "id": nostr_event_head_created.id } })); + assert_not_found(db.nostr_event_head_delete(&nostr_event_head_delete_missing)); let log_error_delete: ILogErrorDelete = parse_json(json!({ "on": { "nostr_pubkey": hex64('c') } })); diff --git a/crates/replica_db/tests/migration_repairs.rs b/crates/replica_db/tests/migration_repairs.rs @@ -14,7 +14,7 @@ fn create_legacy_schema_without_secondary_indexes(exec: &SqliteExecutor) { "CREATE TABLE gcs_location (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), d_tag TEXT NOT NULL, lat REAL NOT NULL, lng REAL NOT NULL, geohash TEXT NOT NULL, point TEXT NOT NULL, polygon TEXT NOT NULL, accuracy REAL, altitude REAL, tag_0 TEXT, label TEXT, area REAL, elevation INTEGER, soil TEXT, climate TEXT, gc_id TEXT, gc_name TEXT, gc_admin1_id TEXT, gc_admin1_name TEXT, gc_country_id TEXT, gc_country_name TEXT)", "CREATE TABLE trade_product (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), key TEXT NOT NULL, category TEXT NOT NULL, title TEXT NOT NULL, summary TEXT NOT NULL, process TEXT NOT NULL, lot TEXT NOT NULL, profile TEXT NOT NULL, year INTEGER NOT NULL, qty_amt INTEGER NOT NULL, qty_unit CHAR(4) NOT NULL, qty_label TEXT, qty_avail INTEGER, price_amt REAL NOT NULL, price_currency CHAR(3) NOT NULL, price_qty_amt INTEGER NOT NULL, price_qty_unit CHAR(4) NOT NULL, notes TEXT)", "CREATE TABLE plot (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), d_tag TEXT NOT NULL, farm_id CHAR(36) NOT NULL, name TEXT NOT NULL, about TEXT, location_primary TEXT, location_city TEXT, location_region TEXT, location_country TEXT, FOREIGN KEY (farm_id) REFERENCES farm(id) ON DELETE CASCADE)", - "CREATE TABLE nostr_event_state (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), key TEXT NOT NULL UNIQUE, kind INTEGER NOT NULL, pubkey CHAR(64) NOT NULL CHECK(length(pubkey) = 64), d_tag TEXT NOT NULL, last_event_id CHAR(64) NOT NULL CHECK(length(last_event_id) = 64), last_created_at INTEGER NOT NULL, content_hash TEXT NOT NULL)", + "CREATE TABLE nostr_event_head (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), key TEXT NOT NULL UNIQUE, kind INTEGER NOT NULL, pubkey CHAR(64) NOT NULL CHECK(length(pubkey) = 64), d_tag TEXT NOT NULL, last_event_id CHAR(64) NOT NULL CHECK(length(last_event_id) = 64), last_created_at INTEGER NOT NULL, content_hash TEXT NOT NULL)", ]; for sql in schema { @@ -41,7 +41,7 @@ fn create_legacy_schema_without_secondary_indexes(exec: &SqliteExecutor) { "0015_plot_tag", "0016_farm_member", "0017_farm_member_claim", - "0018_nostr_event_state", + "0018_nostr_event_head", ] { let sql = format!("INSERT INTO __migrations(name) VALUES ('{name}')"); exec.exec(&sql, "[]") @@ -56,7 +56,7 @@ fn run_all_up_repairs_missing_indexes_in_legacy_sqlite_dbs() { let before = query_rows( &exec, - "SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('farm_pubkey_d_tag_idx', 'gcs_location_geohash_idx', 'plot_farm_d_tag_idx', 'nostr_event_state_kind_idx') ORDER BY name", + "SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('farm_pubkey_d_tag_idx', 'gcs_location_geohash_idx', 'plot_farm_d_tag_idx', 'nostr_event_head_kind_idx') ORDER BY name", ); assert!(before.is_empty()); @@ -64,7 +64,7 @@ fn run_all_up_repairs_missing_indexes_in_legacy_sqlite_dbs() { let after = query_rows( &exec, - "SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('farm_pubkey_d_tag_idx', 'gcs_location_geohash_idx', 'plot_farm_d_tag_idx', 'nostr_event_state_kind_idx') ORDER BY name", + "SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('farm_pubkey_d_tag_idx', 'gcs_location_geohash_idx', 'plot_farm_d_tag_idx', 'nostr_event_head_kind_idx') ORDER BY name", ); let names = after .iter() @@ -80,7 +80,7 @@ fn run_all_up_repairs_missing_indexes_in_legacy_sqlite_dbs() { vec![ "farm_pubkey_d_tag_idx".to_string(), "gcs_location_geohash_idx".to_string(), - "nostr_event_state_kind_idx".to_string(), + "nostr_event_head_kind_idx".to_string(), "plot_farm_d_tag_idx".to_string(), ] ); diff --git a/crates/replica_db/tests/region_scripted_paths.rs b/crates/replica_db/tests/region_scripted_paths.rs @@ -33,9 +33,9 @@ use radroots_replica_db_schema::media_image::{ IMediaImageFindOneRelArgs, IMediaImageUpdate, MediaImageFindManyRel, MediaImageTradeProductArgs, }; -use radroots_replica_db_schema::nostr_event_state::{ - INostrEventStateCreate, INostrEventStateDelete, INostrEventStateFindMany, - INostrEventStateFindOne, INostrEventStateUpdate, +use radroots_replica_db_schema::nostr_event_head::{ + INostrEventHeadCreate, INostrEventHeadDelete, INostrEventHeadFindMany, INostrEventHeadFindOne, + INostrEventHeadUpdate, }; use radroots_replica_db_schema::nostr_profile::{ INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, @@ -530,8 +530,8 @@ assert_secondary_model_paths!( ); assert_secondary_model_paths!( - nostr_event_state_scripted_region_paths, - INostrEventStateCreate, + nostr_event_head_scripted_region_paths, + INostrEventHeadCreate, json!({ "key": "state-a", "kind": 30023, @@ -541,20 +541,20 @@ assert_secondary_model_paths!( "last_created_at": 1, "content_hash": "hash-a" }), - nostr_event_state_create, - INostrEventStateFindMany, + nostr_event_head_create, + INostrEventHeadFindMany, json!({ "filter": { "id": "id-1" } }), - nostr_event_state_find_many, - INostrEventStateFindOne, + nostr_event_head_find_many, + INostrEventHeadFindOne, json!({ "on": { "id": "id-1" } }), - nostr_event_state_find_one, - INostrEventStateUpdate, + nostr_event_head_find_one, + INostrEventHeadUpdate, json!({ "on": { "id": "id-1" }, "fields": { "content_hash": "hash-z" } }), json!({ "on": { "key": "state-a" }, "fields": { "content_hash": "hash-y" } }), - nostr_event_state_update, - INostrEventStateDelete, + nostr_event_head_update, + INostrEventHeadDelete, json!({ "on": { "key": "state-a" } }), - nostr_event_state_delete + nostr_event_head_delete ); assert_rel_model_paths!( diff --git a/crates/replica_db_schema/src/models/mod.rs b/crates/replica_db_schema/src/models/mod.rs @@ -6,7 +6,7 @@ pub mod farm_tag; pub mod gcs_location; pub mod log_error; pub mod media_image; -pub mod nostr_event_state; +pub mod nostr_event_head; pub mod nostr_profile; pub mod nostr_profile_relay; pub mod nostr_relay; diff --git a/crates/replica_db_schema/src/models/nostr_event_head.rs b/crates/replica_db_schema/src/models/nostr_event_head.rs @@ -0,0 +1,119 @@ +use radroots_types::types::{IResult, IResultList}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Serialize, Deserialize)] +pub struct NostrEventHead { + pub id: String, + pub created_at: String, + pub updated_at: String, + pub key: String, + pub kind: u32, + pub pubkey: String, + pub d_tag: String, + pub last_event_id: String, + pub last_created_at: u32, + pub content_hash: String, +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct INostrEventHeadFields { + pub key: String, + pub kind: u32, + pub pubkey: String, + pub d_tag: String, + pub last_event_id: String, + pub last_created_at: u32, + pub content_hash: String, +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct INostrEventHeadFieldsPartial { + pub key: Option<serde_json::Value>, + pub kind: Option<serde_json::Value>, + pub pubkey: Option<serde_json::Value>, + pub d_tag: Option<serde_json::Value>, + pub last_event_id: Option<serde_json::Value>, + pub last_created_at: Option<serde_json::Value>, + pub content_hash: Option<serde_json::Value>, +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct INostrEventHeadFieldsFilter { + pub id: Option<String>, + pub created_at: Option<String>, + pub updated_at: Option<String>, + pub key: Option<String>, + pub kind: Option<u32>, + pub pubkey: Option<String>, + pub d_tag: Option<String>, + pub last_event_id: Option<String>, + pub last_created_at: Option<u32>, + pub content_hash: Option<String>, +} + +#[derive(Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum NostrEventHeadQueryBindValues { + Id { id: String }, + Key { key: String }, +} +impl NostrEventHeadQueryBindValues { + pub fn to_filter_param(&self) -> (&'static str, Value) { + match self { + Self::Id { id } => ("id", Value::from(id.clone())), + Self::Key { key } => ("key", Value::from(key.clone())), + } + } + + pub fn primary_key(&self) -> Option<String> { + match self { + Self::Id { id } => Some(id.clone()), + _ => None, + } + } + + pub fn lookup_key(&self) -> String { + match self { + Self::Id { id } => id.clone(), + Self::Key { key } => key.clone(), + } + } +} + +pub struct INostrEventHeadCreateTs; +pub type INostrEventHeadCreate = INostrEventHeadFields; +pub struct INostrEventHeadCreateResolveTs; +pub type INostrEventHeadCreateResolve = IResult<NostrEventHead>; +#[derive(Deserialize, Serialize)] +pub struct INostrEventHeadFindOneArgs { + pub on: NostrEventHeadQueryBindValues, +} + +#[derive(Deserialize, Serialize)] +#[serde(untagged)] +pub enum INostrEventHeadFindOne { + On(INostrEventHeadFindOneArgs), +} + +pub struct INostrEventHeadFindOneResolveTs; +pub type INostrEventHeadFindOneResolve = IResult<Option<NostrEventHead>>; +#[derive(Deserialize, Serialize)] +pub struct INostrEventHeadFindManyArgs { + pub filter: Option<INostrEventHeadFieldsFilter>, +} +pub type INostrEventHeadFindMany = INostrEventHeadFindManyArgs; +pub struct INostrEventHeadFindManyResolveTs; +pub type INostrEventHeadFindManyResolve = IResultList<NostrEventHead>; +pub struct INostrEventHeadDeleteTs; +pub type INostrEventHeadDelete = INostrEventHeadFindOne; +pub struct INostrEventHeadDeleteResolveTs; +pub type INostrEventHeadDeleteResolve = IResult<String>; +#[derive(Deserialize, Serialize)] +pub struct INostrEventHeadUpdateArgs { + pub on: NostrEventHeadQueryBindValues, + pub fields: INostrEventHeadFieldsPartial, +} +pub type INostrEventHeadUpdate = INostrEventHeadUpdateArgs; +pub struct INostrEventHeadUpdateResolveTs; +pub type INostrEventHeadUpdateResolve = IResult<NostrEventHead>; diff --git a/crates/replica_db_schema/src/models/nostr_event_state.rs b/crates/replica_db_schema/src/models/nostr_event_state.rs @@ -1,119 +0,0 @@ -use radroots_types::types::{IResult, IResultList}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -#[derive(Serialize, Deserialize)] -pub struct NostrEventState { - pub id: String, - pub created_at: String, - pub updated_at: String, - pub key: String, - pub kind: u32, - pub pubkey: String, - pub d_tag: String, - pub last_event_id: String, - pub last_created_at: u32, - pub content_hash: String, -} - -#[derive(Clone, Deserialize, Serialize)] -pub struct INostrEventStateFields { - pub key: String, - pub kind: u32, - pub pubkey: String, - pub d_tag: String, - pub last_event_id: String, - pub last_created_at: u32, - pub content_hash: String, -} - -#[derive(Clone, Deserialize, Serialize)] -pub struct INostrEventStateFieldsPartial { - pub key: Option<serde_json::Value>, - pub kind: Option<serde_json::Value>, - pub pubkey: Option<serde_json::Value>, - pub d_tag: Option<serde_json::Value>, - pub last_event_id: Option<serde_json::Value>, - pub last_created_at: Option<serde_json::Value>, - pub content_hash: Option<serde_json::Value>, -} - -#[derive(Clone, Deserialize, Serialize)] -pub struct INostrEventStateFieldsFilter { - pub id: Option<String>, - pub created_at: Option<String>, - pub updated_at: Option<String>, - pub key: Option<String>, - pub kind: Option<u32>, - pub pubkey: Option<String>, - pub d_tag: Option<String>, - pub last_event_id: Option<String>, - pub last_created_at: Option<u32>, - pub content_hash: Option<String>, -} - -#[derive(Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum NostrEventStateQueryBindValues { - Id { id: String }, - Key { key: String }, -} -impl NostrEventStateQueryBindValues { - pub fn to_filter_param(&self) -> (&'static str, Value) { - match self { - Self::Id { id } => ("id", Value::from(id.clone())), - Self::Key { key } => ("key", Value::from(key.clone())), - } - } - - pub fn primary_key(&self) -> Option<String> { - match self { - Self::Id { id } => Some(id.clone()), - _ => None, - } - } - - pub fn lookup_key(&self) -> String { - match self { - Self::Id { id } => id.clone(), - Self::Key { key } => key.clone(), - } - } -} - -pub struct INostrEventStateCreateTs; -pub type INostrEventStateCreate = INostrEventStateFields; -pub struct INostrEventStateCreateResolveTs; -pub type INostrEventStateCreateResolve = IResult<NostrEventState>; -#[derive(Deserialize, Serialize)] -pub struct INostrEventStateFindOneArgs { - pub on: NostrEventStateQueryBindValues, -} - -#[derive(Deserialize, Serialize)] -#[serde(untagged)] -pub enum INostrEventStateFindOne { - On(INostrEventStateFindOneArgs), -} - -pub struct INostrEventStateFindOneResolveTs; -pub type INostrEventStateFindOneResolve = IResult<Option<NostrEventState>>; -#[derive(Deserialize, Serialize)] -pub struct INostrEventStateFindManyArgs { - pub filter: Option<INostrEventStateFieldsFilter>, -} -pub type INostrEventStateFindMany = INostrEventStateFindManyArgs; -pub struct INostrEventStateFindManyResolveTs; -pub type INostrEventStateFindManyResolve = IResultList<NostrEventState>; -pub struct INostrEventStateDeleteTs; -pub type INostrEventStateDelete = INostrEventStateFindOne; -pub struct INostrEventStateDeleteResolveTs; -pub type INostrEventStateDeleteResolve = IResult<String>; -#[derive(Deserialize, Serialize)] -pub struct INostrEventStateUpdateArgs { - pub on: NostrEventStateQueryBindValues, - pub fields: INostrEventStateFieldsPartial, -} -pub type INostrEventStateUpdate = INostrEventStateUpdateArgs; -pub struct INostrEventStateUpdateResolveTs; -pub type INostrEventStateUpdateResolve = IResult<NostrEventState>; diff --git a/crates/replica_db_schema/tests/query_bind_values.rs b/crates/replica_db_schema/tests/query_bind_values.rs @@ -6,7 +6,7 @@ use radroots_replica_db_schema::farm_tag::FarmTagQueryBindValues; use radroots_replica_db_schema::gcs_location::GcsLocationQueryBindValues; use radroots_replica_db_schema::log_error::LogErrorQueryBindValues; use radroots_replica_db_schema::media_image::MediaImageQueryBindValues; -use radroots_replica_db_schema::nostr_event_state::NostrEventStateQueryBindValues; +use radroots_replica_db_schema::nostr_event_head::NostrEventHeadQueryBindValues; use radroots_replica_db_schema::nostr_profile::NostrProfileQueryBindValues; use radroots_replica_db_schema::nostr_relay::NostrRelayQueryBindValues; use radroots_replica_db_schema::plot::PlotQueryBindValues; @@ -221,14 +221,14 @@ assert_query_bind_values!( ); assert_query_bind_values!( - nostr_event_state_query_bind_values_cover_all_variants, - NostrEventStateQueryBindValues::Id { + nostr_event_head_query_bind_values_cover_all_variants, + NostrEventHeadQueryBindValues::Id { id: "nostr-event-state-id".to_string() }, "id", "nostr-event-state-id", [( - NostrEventStateQueryBindValues::Key { + NostrEventHeadQueryBindValues::Key { key: "event-key".to_string() }, "key", diff --git a/crates/replica_db_wasm/src/wasm_impl.rs b/crates/replica_db_wasm/src/wasm_impl.rs @@ -51,9 +51,9 @@ use radroots_replica_db_schema::nostr_profile::{ INostrProfileUpdate, }; -use radroots_replica_db_schema::nostr_event_state::{ - INostrEventStateCreate, INostrEventStateDelete, INostrEventStateFindMany, - INostrEventStateFindOne, INostrEventStateUpdate, +use radroots_replica_db_schema::nostr_event_head::{ + INostrEventHeadCreate, INostrEventHeadDelete, INostrEventHeadFindMany, INostrEventHeadFindOne, + INostrEventHeadUpdate, }; use radroots_replica_db_schema::nostr_relay::{ @@ -704,48 +704,48 @@ pub fn replica_db_nostr_profile_delete(opts_json: &str) -> Result<JsValue, JsVal value_to_js(out) } -#[wasm_bindgen(js_name = replica_db_nostr_event_state_create)] -pub fn replica_db_nostr_event_state_create(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventStateCreate = parse_json(opts_json).map_err(err_js)?; +#[wasm_bindgen(js_name = replica_db_nostr_event_head_create)] +pub fn replica_db_nostr_event_head_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventHeadCreate = parse_json(opts_json).map_err(err_js)?; let exec = WasmSqlExecutor::new(); let out = - radroots_replica_db::nostr_event_state::create(&exec, &opts).map_err(|e| err_js(e.err))?; + radroots_replica_db::nostr_event_head::create(&exec, &opts).map_err(|e| err_js(e.err))?; value_to_js(out) } -#[wasm_bindgen(js_name = replica_db_nostr_event_state_find_one)] -pub fn replica_db_nostr_event_state_find_one(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventStateFindOne = parse_json(opts_json).map_err(err_js)?; +#[wasm_bindgen(js_name = replica_db_nostr_event_head_find_one)] +pub fn replica_db_nostr_event_head_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventHeadFindOne = parse_json(opts_json).map_err(err_js)?; let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::nostr_event_state::find_one(&exec, &opts) - .map_err(|e| err_js(e.err))?; + let out = + radroots_replica_db::nostr_event_head::find_one(&exec, &opts).map_err(|e| err_js(e.err))?; value_to_js(out) } -#[wasm_bindgen(js_name = replica_db_nostr_event_state_find_many)] -pub fn replica_db_nostr_event_state_find_many(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventStateFindMany = parse_json(opts_json).map_err(err_js)?; +#[wasm_bindgen(js_name = replica_db_nostr_event_head_find_many)] +pub fn replica_db_nostr_event_head_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventHeadFindMany = parse_json(opts_json).map_err(err_js)?; let exec = WasmSqlExecutor::new(); - let out = radroots_replica_db::nostr_event_state::find_many(&exec, &opts) + let out = radroots_replica_db::nostr_event_head::find_many(&exec, &opts) .map_err(|e| err_js(e.err))?; value_to_js(out) } -#[wasm_bindgen(js_name = replica_db_nostr_event_state_update)] -pub fn replica_db_nostr_event_state_update(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventStateUpdate = parse_json(opts_json).map_err(err_js)?; +#[wasm_bindgen(js_name = replica_db_nostr_event_head_update)] +pub fn replica_db_nostr_event_head_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventHeadUpdate = parse_json(opts_json).map_err(err_js)?; let exec = WasmSqlExecutor::new(); let out = - radroots_replica_db::nostr_event_state::update(&exec, &opts).map_err(|e| err_js(e.err))?; + radroots_replica_db::nostr_event_head::update(&exec, &opts).map_err(|e| err_js(e.err))?; value_to_js(out) } -#[wasm_bindgen(js_name = replica_db_nostr_event_state_delete)] -pub fn replica_db_nostr_event_state_delete(opts_json: &str) -> Result<JsValue, JsValue> { - let opts: INostrEventStateDelete = parse_json(opts_json).map_err(err_js)?; +#[wasm_bindgen(js_name = replica_db_nostr_event_head_delete)] +pub fn replica_db_nostr_event_head_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventHeadDelete = parse_json(opts_json).map_err(err_js)?; let exec = WasmSqlExecutor::new(); let out = - radroots_replica_db::nostr_event_state::delete(&exec, &opts).map_err(|e| err_js(e.err))?; + radroots_replica_db::nostr_event_head::delete(&exec, &opts).map_err(|e| err_js(e.err))?; value_to_js(out) } diff --git a/crates/replica_sync/src/emit.rs b/crates/replica_sync/src/emit.rs @@ -1178,7 +1178,7 @@ mod tests { exec, &IFarmMemberFields { farm_id: farm.id.clone(), - member_pubkey: "m".repeat(64), + member_pubkey: "6".repeat(64), role: "member".to_string(), }, ) @@ -1187,7 +1187,7 @@ mod tests { exec, &IFarmMemberFields { farm_id: farm.id.clone(), - member_pubkey: "o".repeat(64), + member_pubkey: "8".repeat(64), role: "owner".to_string(), }, ) @@ -1196,7 +1196,7 @@ mod tests { exec, &IFarmMemberFields { farm_id: farm.id.clone(), - member_pubkey: "u".repeat(64), + member_pubkey: "e".repeat(64), role: "worker".to_string(), }, ) @@ -1205,7 +1205,7 @@ mod tests { exec, &IFarmMemberFields { farm_id: farm.id.clone(), - member_pubkey: "x".repeat(64), + member_pubkey: "1".repeat(64), role: "member".to_string(), }, ) @@ -1214,7 +1214,7 @@ mod tests { let _ = farm_member_claim::create( exec, &IFarmMemberClaimFields { - member_pubkey: "m".repeat(64), + member_pubkey: "6".repeat(64), farm_pubkey: farm.pubkey.clone(), }, ) @@ -1222,7 +1222,7 @@ mod tests { let _ = farm_member_claim::create( exec, &IFarmMemberClaimFields { - member_pubkey: "x".repeat(64), + member_pubkey: "1".repeat(64), farm_pubkey: farm.pubkey.clone(), }, ) @@ -1248,7 +1248,7 @@ mod tests { let _ = nostr_profile::create( exec, &INostrProfileFields { - public_key: "m".repeat(64), + public_key: "6".repeat(64), profile_type: "legacy".to_string(), name: "member profile".to_string(), display_name: Some("member".to_string()), @@ -1478,7 +1478,7 @@ mod tests { .is_some() ); assert!( - load_profile(&exec, &"z".repeat(64)) + load_profile(&exec, &"3".repeat(64)) .expect("missing profile") .is_none() ); @@ -1505,12 +1505,12 @@ mod tests { .expect("profile farm"); assert!(!profile_event_farm.tags.is_empty()); let profile_event_unknown = profile_event( - &"m".repeat(64), + &"6".repeat(64), radroots_replica_db_schema::nostr_profile::NostrProfile { id: "00000000-0000-0000-0000-000000000002".to_string(), created_at: "2024-01-01T00:00:00.000Z".to_string(), updated_at: "2024-01-01T00:00:00.000Z".to_string(), - public_key: "m".repeat(64), + public_key: "6".repeat(64), profile_type: "legacy".to_string(), name: "legacy".to_string(), display_name: None, @@ -1548,7 +1548,7 @@ mod tests { let claims = load_member_claims(&exec, &farm_row.pubkey).expect("claims"); assert!(!claims.is_empty()); let member_claims = - load_member_claims_for_member(&exec, &"m".repeat(64)).expect("claims by member"); + load_member_claims_for_member(&exec, &"6".repeat(64)).expect("claims by member"); assert!(!member_claims.is_empty()); let profile_events = radroots_replica_profile_events(&exec, &farm_row).expect("profiles"); @@ -1697,7 +1697,7 @@ mod tests { assert_eq!(plot_events.len(), 1); let claims = - radroots_replica_membership_claim_events(&exec, &"z".repeat(64)).expect("empty claims"); + radroots_replica_membership_claim_events(&exec, &"3".repeat(64)).expect("empty claims"); assert!(claims.is_empty()); let by_pair = resolve_farm( @@ -1721,7 +1721,7 @@ mod tests { &exec, &IFarmMemberFields { farm_id: farm_row.id.clone(), - member_pubkey: "z".repeat(64), + member_pubkey: "3".repeat(64), role: ROLE_MEMBER.to_string(), }, ) @@ -1800,7 +1800,7 @@ mod tests { created_at: "now".to_string(), updated_at: "now".to_string(), d_tag: "d".to_string(), - pubkey: "p".repeat(64), + pubkey: "9".repeat(64), name: "farm".to_string(), about: None, website: None, @@ -2315,7 +2315,7 @@ mod tests { let _ = farm_member_claim::create( &exec, &IFarmMemberClaimFields { - member_pubkey: "q".repeat(64), + member_pubkey: "a".repeat(64), farm_pubkey: " ".repeat(64), }, ) diff --git a/crates/replica_sync/src/event_head.rs b/crates/replica_sync/src/event_head.rs @@ -0,0 +1,125 @@ +#[cfg(not(feature = "std"))] +use alloc::format; +#[cfg(not(feature = "std"))] +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; +#[cfg(feature = "std")] +use std::{string::String, vec::Vec}; + +use serde_json::Value; +use sha2::{Digest, Sha256}; + +#[cfg(test)] +use crate::error::RadrootsReplicaEventsError; + +#[cfg(test)] +mod failpoints { + use std::cell::Cell; + + thread_local! { + static FORCE_ERROR: Cell<bool> = const { Cell::new(false) }; + } + + pub(crate) fn set_error() { + FORCE_ERROR.with(|flag| flag.set(true)); + } + + pub(crate) fn take_error() -> bool { + FORCE_ERROR.with(|flag| { + let value = flag.get(); + flag.set(false); + value + }) + } +} + +pub fn event_head_key(kind: u32, pubkey: &str, d_tag: &str) -> String { + format!("{kind}:{pubkey}:{d_tag}") +} + +fn event_content_hash_value(content: &str, tags: &[Vec<String>]) -> String { + let tags_json = Value::Array( + tags.iter() + .map(|tag| Value::Array(tag.iter().cloned().map(Value::String).collect())) + .collect(), + ) + .to_string(); + let mut hasher = Sha256::new(); + hasher.update(content.as_bytes()); + hasher.update(tags_json.as_bytes()); + hex::encode(hasher.finalize()) +} + +#[cfg(test)] +pub fn event_content_hash( + content: &str, + tags: &[Vec<String>], +) -> Result<String, RadrootsReplicaEventsError> { + #[cfg(test)] + if failpoints::take_error() { + return Err(RadrootsReplicaEventsError::InvalidData( + "content_hash".to_string(), + )); + } + Ok(event_content_hash_value(content, tags)) +} + +#[cfg(not(test))] +pub fn event_content_hash(content: &str, tags: &[Vec<String>]) -> String { + event_content_hash_value(content, tags) +} + +#[cfg(test)] +pub(crate) fn event_content_hash_fail_next() { + failpoints::set_error(); +} + +pub fn tag_value<'a>(tags: &'a [Vec<String>], key: &str) -> Option<&'a str> { + tags.iter() + .find(|tag| tag.get(0).map(|v| v.as_str()) == Some(key)) + .and_then(|tag| tag.get(1)) + .map(|value| value.as_str()) +} + +#[cfg(test)] +mod tests { + use super::{event_content_hash, event_head_key, tag_value}; + + #[test] + fn event_head_key_formats_consistently() { + let key = event_head_key(30000, "author", "d-tag"); + assert_eq!(key, "30000:author:d-tag"); + } + + #[test] + fn event_content_hash_is_stable_for_same_inputs() { + let tags = vec![vec!["d".to_string(), "tag".to_string()]]; + let first = event_content_hash("content", &tags).expect("hash first"); + let second = event_content_hash("content", &tags).expect("hash second"); + assert_eq!(first, second); + assert_eq!(first.len(), 64); + } + + #[test] + fn event_content_hash_reports_failpoint_errors() { + super::failpoints::set_error(); + let tags = vec![vec!["d".to_string(), "tag".to_string()]]; + let err = event_content_hash("content", &tags).expect_err("failpoint"); + assert!(err.to_string().contains("content_hash")); + } + + #[test] + fn tag_value_finds_and_misses_keys() { + let tags = vec![ + vec!["p".to_string(), "member".to_string()], + vec!["d".to_string(), "farm".to_string()], + vec!["x".to_string()], + ]; + assert_eq!(tag_value(&tags, "p"), Some("member")); + assert_eq!(tag_value(&tags, "d"), Some("farm")); + assert_eq!(tag_value(&tags, "x"), None); + assert_eq!(tag_value(&tags, "missing"), None); + } +} diff --git a/crates/replica_sync/src/event_state.rs b/crates/replica_sync/src/event_state.rs @@ -1,125 +0,0 @@ -#[cfg(not(feature = "std"))] -use alloc::format; -#[cfg(not(feature = "std"))] -use alloc::{ - string::{String, ToString}, - vec::Vec, -}; -#[cfg(feature = "std")] -use std::{string::String, vec::Vec}; - -use serde_json::Value; -use sha2::{Digest, Sha256}; - -#[cfg(test)] -use crate::error::RadrootsReplicaEventsError; - -#[cfg(test)] -mod failpoints { - use std::cell::Cell; - - thread_local! { - static FORCE_ERROR: Cell<bool> = const { Cell::new(false) }; - } - - pub(crate) fn set_error() { - FORCE_ERROR.with(|flag| flag.set(true)); - } - - pub(crate) fn take_error() -> bool { - FORCE_ERROR.with(|flag| { - let value = flag.get(); - flag.set(false); - value - }) - } -} - -pub fn event_state_key(kind: u32, pubkey: &str, d_tag: &str) -> String { - format!("{kind}:{pubkey}:{d_tag}") -} - -fn event_content_hash_value(content: &str, tags: &[Vec<String>]) -> String { - let tags_json = Value::Array( - tags.iter() - .map(|tag| Value::Array(tag.iter().cloned().map(Value::String).collect())) - .collect(), - ) - .to_string(); - let mut hasher = Sha256::new(); - hasher.update(content.as_bytes()); - hasher.update(tags_json.as_bytes()); - hex::encode(hasher.finalize()) -} - -#[cfg(test)] -pub fn event_content_hash( - content: &str, - tags: &[Vec<String>], -) -> Result<String, RadrootsReplicaEventsError> { - #[cfg(test)] - if failpoints::take_error() { - return Err(RadrootsReplicaEventsError::InvalidData( - "content_hash".to_string(), - )); - } - Ok(event_content_hash_value(content, tags)) -} - -#[cfg(not(test))] -pub fn event_content_hash(content: &str, tags: &[Vec<String>]) -> String { - event_content_hash_value(content, tags) -} - -#[cfg(test)] -pub(crate) fn event_content_hash_fail_next() { - failpoints::set_error(); -} - -pub fn tag_value<'a>(tags: &'a [Vec<String>], key: &str) -> Option<&'a str> { - tags.iter() - .find(|tag| tag.get(0).map(|v| v.as_str()) == Some(key)) - .and_then(|tag| tag.get(1)) - .map(|value| value.as_str()) -} - -#[cfg(test)] -mod tests { - use super::{event_content_hash, event_state_key, tag_value}; - - #[test] - fn event_state_key_formats_consistently() { - let key = event_state_key(30000, "author", "d-tag"); - assert_eq!(key, "30000:author:d-tag"); - } - - #[test] - fn event_content_hash_is_stable_for_same_inputs() { - let tags = vec![vec!["d".to_string(), "tag".to_string()]]; - let first = event_content_hash("content", &tags).expect("hash first"); - let second = event_content_hash("content", &tags).expect("hash second"); - assert_eq!(first, second); - assert_eq!(first.len(), 64); - } - - #[test] - fn event_content_hash_reports_failpoint_errors() { - super::failpoints::set_error(); - let tags = vec![vec!["d".to_string(), "tag".to_string()]]; - let err = event_content_hash("content", &tags).expect_err("failpoint"); - assert!(err.to_string().contains("content_hash")); - } - - #[test] - fn tag_value_finds_and_misses_keys() { - let tags = vec![ - vec!["p".to_string(), "member".to_string()], - vec!["d".to_string(), "farm".to_string()], - vec!["x".to_string()], - ]; - assert_eq!(tag_value(&tags, "p"), Some("member")); - assert_eq!(tag_value(&tags, "d"), Some("farm")); - assert_eq!(tag_value(&tags, "x"), None); - assert_eq!(tag_value(&tags, "missing"), None); - } -} diff --git a/crates/replica_sync/src/ingest.rs b/crates/replica_sync/src/ingest.rs @@ -13,6 +13,12 @@ use base64::engine::general_purpose::URL_SAFE_NO_PAD; use radroots_core::RadrootsCoreDecimal; use radroots_events::RadrootsNostrEvent; +use radroots_events::event_head::{ + RadrootsCurrentEventHead, RadrootsEventHeadCandidateResult, RadrootsEventHeadCoordinate, + RadrootsEventHeadDecision as ProtocolEventHeadDecision, event_head_candidate_for_event, + select_event_head, +}; +use radroots_events::ids::RadrootsEventId; use radroots_events::kinds::{ KIND_FARM, KIND_LISTING, KIND_PLOT, KIND_PROFILE, is_nip51_list_set_kind, }; @@ -26,7 +32,7 @@ use radroots_events_codec::plot::decode as plot_decode; use radroots_events_codec::profile::decode as profile_decode; use radroots_replica_db::{ farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, - nostr_event_state, nostr_profile, plot, plot_gcs_location, plot_tag, trade_product, + nostr_event_head, nostr_profile, plot, plot_gcs_location, plot_tag, trade_product, }; use radroots_replica_db_schema::farm::{ FarmQueryBindValues, IFarmFields, IFarmFieldsFilter, IFarmFieldsPartial, IFarmFindMany, @@ -49,9 +55,10 @@ use radroots_replica_db_schema::farm_tag::{ IFarmTagFindOneArgs, }; use radroots_replica_db_schema::gcs_location::IGcsLocationFields; -use radroots_replica_db_schema::nostr_event_state::{ - INostrEventStateFields, INostrEventStateFieldsPartial, INostrEventStateFindOne, - INostrEventStateFindOneArgs, INostrEventStateUpdate, NostrEventStateQueryBindValues, +use radroots_replica_db_schema::nostr_event_head::{ + INostrEventHeadFields, INostrEventHeadFieldsPartial, INostrEventHeadFindOne, + INostrEventHeadFindOneArgs, INostrEventHeadUpdate, NostrEventHead, + NostrEventHeadQueryBindValues, }; use radroots_replica_db_schema::nostr_profile::{ INostrProfileFields, INostrProfileFieldsPartial, INostrProfileFindOne, @@ -79,7 +86,7 @@ use radroots_sql_core::error::SqlError; use serde_json::{Value, json}; use crate::error::RadrootsReplicaEventsError; -use crate::event_state::{event_content_hash, event_state_key}; +use crate::event_head::{event_content_hash, event_head_key}; const ROLE_PRIMARY: &str = "primary"; const ROLE_MEMBER: &str = "member"; const ROLE_OWNER: &str = "owner"; @@ -218,8 +225,7 @@ fn ingest_profile_event( } }; - let d_tag = "".to_string(); - let decision = event_state_decision(exec, event, &d_tag)?; + let decision = event_head_decision(exec, event)?; if !decision.apply { return Ok(RadrootsReplicaIngestOutcome::Skipped); } @@ -284,7 +290,7 @@ fn ingest_profile_event( } } - radroots_replica_ingest_event_state(exec, event, &d_tag, &decision.content_hash)?; + upsert_event_head(exec, &decision)?; Ok(RadrootsReplicaIngestOutcome::Applied) } @@ -294,7 +300,7 @@ fn ingest_farm_event( factory: &dyn RadrootsReplicaIdFactory, ) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { let farm = farm_decode::farm_from_event(event.kind, &event.tags, &event.content)?; - let decision = event_state_decision(exec, event, &farm.d_tag)?; + let decision = event_head_decision(exec, event)?; if !decision.apply { return Ok(RadrootsReplicaIngestOutcome::Skipped); } @@ -368,7 +374,7 @@ fn ingest_farm_event( upsert_farm_tags(exec, &farm_id, farm.tags)?; upsert_farm_location(exec, &farm_id, location, factory)?; - radroots_replica_ingest_event_state(exec, event, &farm.d_tag, &decision.content_hash)?; + upsert_event_head(exec, &decision)?; Ok(RadrootsReplicaIngestOutcome::Applied) } @@ -378,7 +384,7 @@ fn ingest_plot_event( factory: &dyn RadrootsReplicaIdFactory, ) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { let plot = plot_decode::plot_from_event(event.kind, &event.tags, &event.content)?; - let decision = event_state_decision(exec, event, &plot.d_tag)?; + let decision = event_head_decision(exec, event)?; if !decision.apply { return Ok(RadrootsReplicaIngestOutcome::Skipped); } @@ -444,7 +450,7 @@ fn ingest_plot_event( upsert_plot_tags(exec, &plot_id, plot.tags)?; upsert_plot_location(exec, &plot_id, location, factory)?; - radroots_replica_ingest_event_state(exec, event, &plot.d_tag, &decision.content_hash)?; + upsert_event_head(exec, &decision)?; Ok(RadrootsReplicaIngestOutcome::Applied) } @@ -453,7 +459,7 @@ fn ingest_listing_event( event: &RadrootsNostrEvent, ) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { let listing = listing_decode::listing_from_event(event.kind, &event.tags, &event.content)?; - let decision = event_state_decision(exec, event, &listing.d_tag)?; + let decision = event_head_decision(exec, event)?; if !decision.apply { return Ok(RadrootsReplicaIngestOutcome::Skipped); } @@ -466,7 +472,7 @@ fn ingest_listing_event( delete_trade_products_for_listing_addr(exec, &listing_addr)?; } - radroots_replica_ingest_event_state(exec, event, &listing.d_tag, &decision.content_hash)?; + upsert_event_head(exec, &decision)?; Ok(RadrootsReplicaIngestOutcome::Applied) } @@ -495,28 +501,36 @@ fn ingest_list_set_event( } let d_tag = list_set.d_tag.clone(); - let decision = event_state_decision(exec, event, &d_tag)?; - if !decision.apply { - return Ok(RadrootsReplicaIngestOutcome::Skipped); - } if d_tag == "member_of.farms" { ensure_list_set_entries_tag(&list_set, "p", "member_of.farms")?; + let decision = event_head_decision(exec, event)?; + if !decision.apply { + return Ok(RadrootsReplicaIngestOutcome::Skipped); + } upsert_member_claims(exec, &event.author, &list_set)?; - radroots_replica_ingest_event_state(exec, event, &d_tag, &decision.content_hash)?; + upsert_event_head(exec, &decision)?; return Ok(RadrootsReplicaIngestOutcome::Applied); } if let Some((farm_d_tag, role)) = parse_farm_list_set_d_tag(&d_tag) { if role == ListSetRole::Plots { ensure_list_set_entries_tag(&list_set, "a", "farm plots")?; - radroots_replica_ingest_event_state(exec, event, &d_tag, &decision.content_hash)?; + let decision = event_head_decision(exec, event)?; + if !decision.apply { + return Ok(RadrootsReplicaIngestOutcome::Skipped); + } + upsert_event_head(exec, &decision)?; return Ok(RadrootsReplicaIngestOutcome::Applied); } ensure_list_set_entries_tag(&list_set, "p", "farm members")?; + let decision = event_head_decision(exec, event)?; + if !decision.apply { + return Ok(RadrootsReplicaIngestOutcome::Skipped); + } let farm = find_farm_by_ref(exec, &event.author, &farm_d_tag)?; upsert_farm_members(exec, &farm.id, role, &list_set)?; - radroots_replica_ingest_event_state(exec, event, &d_tag, &decision.content_hash)?; + upsert_event_head(exec, &decision)?; return Ok(RadrootsReplicaIngestOutcome::Applied); } @@ -606,8 +620,8 @@ fn trade_product_fields_from_listing( price_qty_amt_exact, price_qty_unit, listing_addr: Some(listing_addr.to_string()), - primary_bin_id: Some(listing.primary_bin_id.clone()), - verified_primary_bin_id: Some(listing.primary_bin_id.clone()), + primary_bin_id: Some(listing.primary_bin_id.to_string()), + verified_primary_bin_id: Some(listing.primary_bin_id.to_string()), notes: trade_product_notes_from_listing(listing)?, }) } @@ -792,97 +806,183 @@ fn trade_product_partial_from_fields(fields: &ITradeProductFields) -> ITradeProd } } -pub fn radroots_replica_ingest_event_state( +pub fn radroots_replica_ingest_event_head( exec: &dyn SqlExecutor, event: &RadrootsNostrEvent, - d_tag: &str, - content_hash: &str, +) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> { + let decision = event_head_decision(exec, event)?; + if !decision.apply { + return Ok(RadrootsReplicaIngestOutcome::Skipped); + } + upsert_event_head(exec, &decision)?; + Ok(RadrootsReplicaIngestOutcome::Applied) +} + +fn upsert_event_head( + exec: &dyn SqlExecutor, + decision: &EventHeadDecision, ) -> Result<(), RadrootsReplicaEventsError> { - let key = event_state_key(event.kind, &event.author, d_tag); - let existing_result = nostr_event_state::find_one( + let existing_result = nostr_event_head::find_one( exec, - &INostrEventStateFindOne::On(INostrEventStateFindOneArgs { - on: NostrEventStateQueryBindValues::Key { key: key.clone() }, + &INostrEventHeadFindOne::On(INostrEventHeadFindOneArgs { + on: NostrEventHeadQueryBindValues::Key { + key: decision.key.clone(), + }, }), ); let existing = existing_result?.result; match existing { Some(state) => { - let fields = INostrEventStateFieldsPartial { + let fields = INostrEventHeadFieldsPartial { key: None, kind: None, pubkey: None, d_tag: None, - last_event_id: Some(Value::from(event.id.clone())), - last_created_at: Some(Value::from(event.created_at)), - content_hash: Some(Value::from(content_hash.to_string())), + last_event_id: Some(Value::from(decision.last_event_id.clone())), + last_created_at: Some(Value::from(decision.last_created_at)), + content_hash: Some(Value::from(decision.content_hash.clone())), }; - let update_result = nostr_event_state::update( + let update_result = nostr_event_head::update( exec, - &INostrEventStateUpdate { - on: NostrEventStateQueryBindValues::Id { id: state.id }, + &INostrEventHeadUpdate { + on: NostrEventHeadQueryBindValues::Id { id: state.id }, fields, }, ); let _updated = update_result?; } None => { - let fields = INostrEventStateFields { - key, - kind: event.kind, - pubkey: event.author.clone(), - d_tag: d_tag.to_string(), - last_event_id: event.id.clone(), - last_created_at: event.created_at, - content_hash: content_hash.to_string(), + let fields = INostrEventHeadFields { + key: decision.key.clone(), + kind: decision.kind, + pubkey: decision.pubkey.clone(), + d_tag: decision.d_tag.clone(), + last_event_id: decision.last_event_id.clone(), + last_created_at: decision.last_created_at, + content_hash: decision.content_hash.clone(), }; - let _ = nostr_event_state::create(exec, &fields)?; + let _ = nostr_event_head::create(exec, &fields)?; } } Ok(()) } -fn event_state_decision( +fn event_head_decision( exec: &dyn SqlExecutor, event: &RadrootsNostrEvent, - d_tag: &str, -) -> Result<EventStateDecision, RadrootsReplicaEventsError> { - let key = event_state_key(event.kind, &event.author, d_tag); +) -> Result<EventHeadDecision, RadrootsReplicaEventsError> { + let candidate = match event_head_candidate_for_event(event).map_err(|err| { + RadrootsReplicaEventsError::InvalidData(format!("event head contract mismatch: {err:?}")) + })? { + RadrootsEventHeadCandidateResult::Candidate(candidate) => candidate, + RadrootsEventHeadCandidateResult::NotHeadSelected => { + return Err(RadrootsReplicaEventsError::InvalidData( + "event is not head-selected".to_string(), + )); + } + RadrootsEventHeadCandidateResult::NotPersisted => { + return Ok(EventHeadDecision { + apply: false, + key: String::new(), + kind: event.kind, + pubkey: event.author.clone(), + d_tag: String::new(), + last_event_id: event.id.clone(), + last_created_at: event.created_at, + content_hash: String::new(), + }); + } + RadrootsEventHeadCandidateResult::Malformed(err) => { + return Err(RadrootsReplicaEventsError::InvalidData(format!( + "malformed event head: {err:?}" + ))); + } + }; + let (key, kind, pubkey, d_tag) = event_head_coordinate_fields(&candidate.coordinate); #[cfg(test)] let content_hash = event_content_hash(&event.content, &event.tags)?; #[cfg(not(test))] let content_hash = event_content_hash(&event.content, &event.tags); - let existing_result = nostr_event_state::find_one( + let existing_result = nostr_event_head::find_one( exec, - &INostrEventStateFindOne::On(INostrEventStateFindOneArgs { - on: NostrEventStateQueryBindValues::Key { key }, + &INostrEventHeadFindOne::On(INostrEventHeadFindOneArgs { + on: NostrEventHeadQueryBindValues::Key { key: key.clone() }, }), ); let existing = existing_result?.result; + let current = existing + .as_ref() + .map(|state| current_event_head_from_row(state, &candidate.coordinate)) + .transpose()?; - if let Some(state) = existing { - if event.created_at < state.last_created_at { - return Ok(EventStateDecision { - apply: false, - content_hash, - }); - } - if event.created_at == state.last_created_at && content_hash == state.content_hash { - return Ok(EventStateDecision { - apply: false, - content_hash, - }); + let decision = select_event_head(candidate, current.as_ref()); + let apply = match decision { + ProtocolEventHeadDecision::Applied(_) => true, + ProtocolEventHeadDecision::SkippedDuplicate + | ProtocolEventHeadDecision::SkippedOlder + | ProtocolEventHeadDecision::SkippedSameTimestampHigherEventId => false, + ProtocolEventHeadDecision::CoordinateMismatch => { + return Err(RadrootsReplicaEventsError::InvalidData( + "event head coordinate mismatch".to_string(), + )); } - } + }; - Ok(EventStateDecision { - apply: true, + Ok(EventHeadDecision { + apply, + key, + kind, + pubkey, + d_tag, + last_event_id: event.id.clone(), + last_created_at: event.created_at, content_hash, }) } +fn current_event_head_from_row( + row: &NostrEventHead, + coordinate: &RadrootsEventHeadCoordinate, +) -> Result<RadrootsCurrentEventHead, RadrootsReplicaEventsError> { + let event_id = RadrootsEventId::parse(&row.last_event_id).map_err(|err| { + RadrootsReplicaEventsError::InvalidData(format!( + "nostr event head last_event_id invalid: {err}" + )) + })?; + Ok(RadrootsCurrentEventHead { + coordinate: coordinate.clone(), + event_id, + created_at: row.last_created_at, + }) +} + +fn event_head_coordinate_fields( + coordinate: &RadrootsEventHeadCoordinate, +) -> (String, u32, String, String) { + match coordinate { + RadrootsEventHeadCoordinate::Replaceable { kind, pubkey } => { + let pubkey = pubkey.to_string(); + ( + event_head_key(*kind, &pubkey, ""), + *kind, + pubkey, + String::new(), + ) + } + RadrootsEventHeadCoordinate::Addressable { + kind, + pubkey, + d_tag, + } => { + let pubkey = pubkey.to_string(); + let d_tag = d_tag.to_string(); + (event_head_key(*kind, &pubkey, &d_tag), *kind, pubkey, d_tag) + } + } +} + fn find_farm_by_ref( exec: &dyn SqlExecutor, pubkey: &str, @@ -1392,8 +1492,14 @@ fn to_value_opt(value: Option<String>) -> Option<Value> { }) } -struct EventStateDecision { +struct EventHeadDecision { apply: bool, + key: String, + kind: u32, + pubkey: String, + d_tag: String, + last_event_id: String, + last_created_at: u32, content_hash: String, } @@ -1421,7 +1527,7 @@ mod tests { use radroots_events_codec::plot::encode as plot_encode; use radroots_replica_db::{ ReplicaSql, farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, - gcs_location, migrations, nostr_event_state, plot, plot_gcs_location, plot_tag, + gcs_location, migrations, nostr_event_head, plot, plot_gcs_location, plot_tag, trade_product, }; use radroots_replica_db_schema::farm::IFarmFields; @@ -1890,7 +1996,7 @@ mod tests { exec, &IFarmMemberFields { farm_id: farm_row.id.clone(), - member_pubkey: "m".repeat(64), + member_pubkey: "6".repeat(64), role: "member".to_string(), }, ) @@ -1898,7 +2004,7 @@ mod tests { let _ = farm_member_claim::create( exec, &IFarmMemberClaimFields { - member_pubkey: "m".repeat(64), + member_pubkey: "6".repeat(64), farm_pubkey: farm_row.pubkey.clone(), }, ) @@ -1994,7 +2100,7 @@ mod tests { let factory = RadrootsReplicaDefaultIdFactory; assert_eq!(factory.new_d_tag().len(), 22); - let profile_pubkey = "p".repeat(64); + let profile_pubkey = "9".repeat(64); let profile = profile_event( 10, &profile_pubkey, @@ -2030,20 +2136,27 @@ mod tests { Some(RadrootsProfileType::Individual), "alice-old", ); - let decision_old = event_state_decision(&exec, &profile_older, "").expect("decision old"); + let decision_old = event_head_decision(&exec, &profile_older).expect("decision old"); assert!(!decision_old.apply); - let decision_same = - event_state_decision(&exec, &profile_update, "").expect("decision same"); + let decision_same = event_head_decision(&exec, &profile_update).expect("decision same"); assert!(!decision_same.apply); - let profile_same_time_diff_hash = profile_event( + let profile_same_time_higher_id = profile_event( 12, &profile_pubkey, 2, Some(RadrootsProfileType::Individual), "alice-3", ); - let decision = - event_state_decision(&exec, &profile_same_time_diff_hash, "").expect("decision"); + let decision = event_head_decision(&exec, &profile_same_time_higher_id).expect("decision"); + assert!(!decision.apply); + let profile_same_time_lower_id = profile_event( + 10, + &profile_pubkey, + 2, + Some(RadrootsProfileType::Individual), + "alice-0", + ); + let decision = event_head_decision(&exec, &profile_same_time_lower_id).expect("decision"); assert!(decision.apply); let farm_pubkey = "f".repeat(64); @@ -2135,11 +2248,11 @@ mod tests { RadrootsReplicaIngestOutcome::Skipped ); - let members = farm_list_sets::farm_members_list_set(farm_d_tag, vec!["m".repeat(64)]) + let members = farm_list_sets::farm_members_list_set(farm_d_tag, vec!["6".repeat(64)]) .expect("members"); let owners = - farm_list_sets::farm_owners_list_set(farm_d_tag, vec!["o".repeat(64)]).expect("owners"); - let workers = farm_list_sets::farm_workers_list_set(farm_d_tag, vec!["w".repeat(64)]) + farm_list_sets::farm_owners_list_set(farm_d_tag, vec!["8".repeat(64)]).expect("owners"); + let workers = farm_list_sets::farm_workers_list_set(farm_d_tag, vec!["0".repeat(64)]) .expect("workers"); let plots = farm_list_sets::farm_plots_list_set( farm_d_tag, @@ -2317,7 +2430,7 @@ mod tests { let exec = SqliteExecutor::open_memory().expect("db"); migrations::run_all_up(&exec).expect("migrations"); - let seller_pubkey = "s".repeat(64); + let seller_pubkey = "c".repeat(64); let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ"; let listing_addr = format!("{}:{}:{}", KIND_LISTING, seller_pubkey, listing_d_tag); @@ -2434,11 +2547,11 @@ mod tests { .results; assert!(product_rows.is_empty()); - let state = nostr_event_state::find_one( + let state = nostr_event_head::find_one( &exec, - &INostrEventStateFindOne::On(INostrEventStateFindOneArgs { - on: NostrEventStateQueryBindValues::Key { - key: event_state_key(KIND_LISTING, &seller_pubkey, listing_d_tag), + &INostrEventHeadFindOne::On(INostrEventHeadFindOneArgs { + on: NostrEventHeadQueryBindValues::Key { + key: event_head_key(KIND_LISTING, &seller_pubkey, listing_d_tag), }, }), ) @@ -2475,7 +2588,7 @@ mod tests { let exec = SqliteExecutor::open_memory().expect("db"); migrations::run_all_up(&exec).expect("migrations"); - let seller_pubkey = "s".repeat(64); + let seller_pubkey = "c".repeat(64); let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAg"; let listing_addr = format!("{}:{}:{}", KIND_LISTING, seller_pubkey, listing_d_tag); @@ -2695,7 +2808,7 @@ mod tests { ); let members_list_set = - farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["n".repeat(64)]) + farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["7".repeat(64)]) .expect("members"); assert!( upsert_farm_members(&exec, &farm_id, ListSetRole::Members, &members_list_set).is_ok() @@ -2706,7 +2819,7 @@ mod tests { err: SqlError::NotFound("farm_member".to_string()), }; let not_found_members_list_set = - farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["q".repeat(64)]) + farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["a".repeat(64)]) .expect("not found members"); assert!( upsert_farm_members( @@ -2728,17 +2841,17 @@ mod tests { ); let member_claims = - farm_list_sets::member_of_farms_list_set(vec!["z".repeat(64)]).expect("claims"); - assert!(upsert_member_claims(&exec, &"m".repeat(64), &member_claims).is_ok()); + farm_list_sets::member_of_farms_list_set(vec!["3".repeat(64)]).expect("claims"); + assert!(upsert_member_claims(&exec, &"6".repeat(64), &member_claims).is_ok()); let not_found_claims = DeleteErrorExecutor { inner: &exec, table_name: "farm_member_claim", err: SqlError::NotFound("farm_member_claim".to_string()), }; let not_found_member_claims = - farm_list_sets::member_of_farms_list_set(vec!["y".repeat(64)]).expect("claims nf"); + farm_list_sets::member_of_farms_list_set(vec!["2".repeat(64)]).expect("claims nf"); assert!( - upsert_member_claims(¬_found_claims, &"m".repeat(64), ¬_found_member_claims) + upsert_member_claims(¬_found_claims, &"6".repeat(64), ¬_found_member_claims) .is_ok() ); assert!(not_found_claims.begin().is_ok()); @@ -2834,7 +2947,7 @@ mod tests { table_name: "farm_member_claim", err: SqlError::Internal, }; - assert!(upsert_member_claims(&internal_claims, &"m".repeat(64), &member_claims).is_err()); + assert!(upsert_member_claims(&internal_claims, &"6".repeat(64), &member_claims).is_err()); } #[test] @@ -2854,7 +2967,7 @@ mod tests { migrations::run_all_up(&exec).expect("migrations"); let pass = PassExecutor { inner: &exec }; - let profile_pubkey = "p".repeat(64); + let profile_pubkey = "9".repeat(64); let farm_pubkey = "f".repeat(64); let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; let plot_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ"; @@ -2921,7 +3034,7 @@ mod tests { ); let members = - farm_list_sets::farm_members_list_set(farm_d_tag, vec!["m".repeat(64)]).expect("list"); + farm_list_sets::farm_members_list_set(farm_d_tag, vec!["6".repeat(64)]).expect("list"); let members_event = list_set_event(503, &farm_pubkey, 53, KIND_LIST_SET_GENERIC, &members); assert_eq!( ingest_list_set_event(&pass, &members_event).expect("members list set"), @@ -2947,7 +3060,7 @@ mod tests { }, RadrootsListEntry { tag: "p".to_string(), - values: vec!["m".repeat(64)], + values: vec!["6".repeat(64)], }, ], title: None, @@ -3095,7 +3208,7 @@ mod tests { rollback_count: Arc::new(AtomicUsize::new(0)), }; - let profile_pubkey = "p".repeat(64); + let profile_pubkey = "9".repeat(64); let profile_event_row = profile_event( 700, &profile_pubkey, @@ -3108,17 +3221,9 @@ mod tests { RadrootsReplicaIngestOutcome::Applied ); let profile_decision = - event_state_decision(&pass_txn, &profile_event_row, "").expect("profile decision"); + event_head_decision(&pass_txn, &profile_event_row).expect("profile decision"); assert!(!profile_decision.apply); - assert!( - radroots_replica_ingest_event_state( - &pass_txn, - &profile_event_row, - "", - &profile_decision.content_hash, - ) - .is_ok() - ); + assert!(radroots_replica_ingest_event_head(&pass_txn, &profile_event_row).is_ok()); assert_eq!( radroots_replica_ingest_event_with_factory( &pass_txn, @@ -3219,14 +3324,14 @@ mod tests { .is_ok() ); let members_list = - farm_list_sets::farm_members_list_set(farm_d_tag, vec!["m".repeat(64)]).expect("list"); + farm_list_sets::farm_members_list_set(farm_d_tag, vec!["6".repeat(64)]).expect("list"); assert!( upsert_farm_members(&pass_txn, &farm_row.id, ListSetRole::Members, &members_list) .is_ok() ); let member_of_list = farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member_of"); - assert!(upsert_member_claims(&pass_txn, &"m".repeat(64), &member_of_list).is_ok()); + assert!(upsert_member_claims(&pass_txn, &"6".repeat(64), &member_of_list).is_ok()); let rollback_count = Arc::new(AtomicUsize::new(0)); let txn = TxnExecutor { @@ -3237,8 +3342,8 @@ mod tests { }; assert!(ingest_profile_event(&txn, &profile_event_row).is_err()); - assert!(event_state_decision(&txn, &profile_event_row, "").is_err()); - assert!(radroots_replica_ingest_event_state(&txn, &profile_event_row, "", "hash").is_err()); + assert!(event_head_decision(&txn, &profile_event_row).is_err()); + assert!(radroots_replica_ingest_event_head(&txn, &profile_event_row).is_err()); assert!( radroots_replica_ingest_event_with_factory(&txn, &profile_event_row, &FixedFactory) .is_err() @@ -3285,7 +3390,7 @@ mod tests { .is_err() ); assert!(upsert_farm_members(&txn, "farm-id", ListSetRole::Members, &members_list).is_err()); - assert!(upsert_member_claims(&txn, &"m".repeat(64), &member_of_list).is_err()); + assert!(upsert_member_claims(&txn, &"6".repeat(64), &member_of_list).is_err()); } #[test] @@ -3313,7 +3418,7 @@ mod tests { let farm_pubkey = "f".repeat(64); let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; let plot_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ"; - let profile_pubkey = "p".repeat(64); + let profile_pubkey = "9".repeat(64); let profile = profile_event( 800, @@ -3358,7 +3463,7 @@ mod tests { }; let profile_new = profile_event( 802, - &"n".repeat(64), + &"7".repeat(64), 82, Some(RadrootsProfileType::Individual), "profile-new", @@ -3367,12 +3472,12 @@ mod tests { let profile_state_fail = QueryFailExecutor { inner: &exec, - needle: "nostr_event_state", + needle: "nostr_event_head", err: SqlError::Internal, }; let profile_state_event = profile_event( 803, - &"s".repeat(64), + &"c".repeat(64), 83, Some(RadrootsProfileType::Individual), "profile-state", @@ -3410,7 +3515,7 @@ mod tests { }; let farm_query_event = farm_event( 811, - &"q".repeat(64), + &"a".repeat(64), 91, farm_d_tag, "farm-query", @@ -3458,7 +3563,7 @@ mod tests { }; let farm_tag_event = farm_event( 814, - &"t".repeat(64), + &"d".repeat(64), 94, farm_d_tag, "farm-tag", @@ -3474,7 +3579,7 @@ mod tests { }; let farm_gcs_event = farm_event( 815, - &"g".repeat(64), + &"0".repeat(64), 95, farm_d_tag, "farm-gcs", @@ -3496,7 +3601,7 @@ mod tests { }; let farm_rel_event = farm_event( 816, - &"r".repeat(64), + &"b".repeat(64), 96, farm_d_tag, "farm-rel", @@ -3513,12 +3618,12 @@ mod tests { let farm_state_fail = QueryFailExecutor { inner: &exec, - needle: "nostr_event_state", + needle: "nostr_event_head", err: SqlError::Internal, }; let farm_state_event = farm_event( 817, - &"w".repeat(64), + &"0".repeat(64), 97, farm_d_tag, "farm-state", @@ -3531,7 +3636,7 @@ mod tests { bad_point.point.coordinates = [f64::NAN, 13.0]; let farm_bad_point = farm_event( 818, - &"x".repeat(64), + &"1".repeat(64), 98, farm_d_tag, "farm-bad-point", @@ -3550,7 +3655,7 @@ mod tests { bad_polygon.polygon.coordinates[0][1][0] = f64::NAN; let farm_bad_polygon = farm_event( 819, - &"y".repeat(64), + &"2".repeat(64), 99, farm_d_tag, "farm-bad-polygon", @@ -3727,7 +3832,7 @@ mod tests { let plot_state_fail = QueryFailExecutor { inner: &exec, - needle: "nostr_event_state", + needle: "nostr_event_head", err: SqlError::Internal, }; let plot_state_event = plot_event( @@ -3757,13 +3862,13 @@ mod tests { list_decode_fail.tags = Vec::new(); assert!(ingest_list_set_event(&exec, &list_decode_fail).is_err()); - let members_list = farm_list_sets::farm_members_list_set(farm_d_tag, vec!["m".repeat(64)]) + let members_list = farm_list_sets::farm_members_list_set(farm_d_tag, vec!["6".repeat(64)]) .expect("members list"); let member_event = list_set_event(831, &farm_pubkey, 109, KIND_LIST_SET_GENERIC, &members_list); let list_decision_fail = QueryFailExecutor { inner: &exec, - needle: "nostr_event_state", + needle: "nostr_event_head", err: SqlError::Internal, }; assert!(ingest_list_set_event(&list_decision_fail, &member_event).is_err()); @@ -3771,7 +3876,7 @@ mod tests { let member_of = farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member-of"); let member_of_event = - list_set_event(832, &"m".repeat(64), 110, KIND_LIST_SET_GENERIC, &member_of); + list_set_event(832, &"6".repeat(64), 110, KIND_LIST_SET_GENERIC, &member_of); let claims_fail = QueryFailExecutor { inner: &exec, needle: "farm_member_claim", @@ -3781,7 +3886,7 @@ mod tests { let claims_state_fail = QueryFailExecutor { inner: &exec, - needle: "nostr_event_state", + needle: "nostr_event_head", err: SqlError::Internal, }; assert!(ingest_list_set_event(&claims_state_fail, &member_of_event).is_err()); @@ -3796,16 +3901,16 @@ mod tests { list_set_event(833, &farm_pubkey, 111, KIND_LIST_SET_GENERIC, &plots_list); let plots_state_fail = QueryFailExecutor { inner: &exec, - needle: "nostr_event_state", + needle: "nostr_event_head", err: SqlError::Internal, }; assert!(ingest_list_set_event(&plots_state_fail, &plots_event).is_err()); let missing_farm_members = - farm_list_sets::farm_members_list_set(farm_d_tag, vec!["n".repeat(64)]).expect("list"); + farm_list_sets::farm_members_list_set(farm_d_tag, vec!["7".repeat(64)]).expect("list"); let missing_farm_event = list_set_event( 834, - &"z".repeat(64), + &"3".repeat(64), 112, KIND_LIST_SET_GENERIC, &missing_farm_members, @@ -3821,7 +3926,7 @@ mod tests { let members_state_fail = QueryFailExecutor { inner: &exec, - needle: "nostr_event_state", + needle: "nostr_event_head", err: SqlError::Internal, }; assert!(ingest_list_set_event(&members_state_fail, &member_event).is_err()); @@ -3831,22 +3936,25 @@ mod tests { let state_create_fail = QueryFailExecutor { inner: &exec, - needle: "nostr_event_state", + needle: "nostr_event_head", err: SqlError::Internal, }; - assert!( - radroots_replica_ingest_event_state(&state_create_fail, &profile, "", "hash").is_err() - ); + assert!(radroots_replica_ingest_event_head(&state_create_fail, &profile).is_err()); - radroots_replica_ingest_event_state(&exec, &profile, "", "hash").expect("seed state"); + radroots_replica_ingest_event_head(&exec, &profile).expect("seed state"); let state_update_fail = QueryFailExecutor { inner: &exec, - needle: "update nostr_event_state", + needle: "update nostr_event_head", err: SqlError::Internal, }; - assert!( - radroots_replica_ingest_event_state(&state_update_fail, &profile, "", "hash2").is_err() + let profile_update = profile_event( + 808, + &"9".repeat(64), + 101, + Some(RadrootsProfileType::Individual), + "profile-update-error", ); + assert!(radroots_replica_ingest_event_head(&state_update_fail, &profile_update).is_err()); } #[test] @@ -3856,14 +3964,14 @@ mod tests { let profile = profile_event( 900, - &"u".repeat(64), + &"e".repeat(64), 120, Some(RadrootsProfileType::Individual), "profile-state-insert", ); let state_insert_fail = QueryFailExecutor { inner: &exec, - needle: "insert into nostr_event_state", + needle: "insert into nostr_event_head", err: SqlError::Internal, }; assert!(ingest_profile_event(&state_insert_fail, &profile).is_err()); @@ -3894,7 +4002,7 @@ mod tests { ); assert!(ingest_plot_event(&state_insert_fail, &plot_state, &FixedFactory).is_err()); - let members_set = farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["n".repeat(64)]) + let members_set = farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["7".repeat(64)]) .expect("members"); let members_event = list_set_event(903, &farm_pubkey, 123, KIND_LIST_SET_GENERIC, &members_set); @@ -3913,7 +4021,7 @@ mod tests { farm_list_sets::member_of_farms_list_set(vec![farm_pubkey.clone()]).expect("member_of"); let member_of_event = list_set_event( 905, - &"n".repeat(64), + &"7".repeat(64), 125, KIND_LIST_SET_GENERIC, &member_of_set, @@ -3922,16 +4030,13 @@ mod tests { let state_insert_only_fail = QueryFailExecutor { inner: &exec, - needle: "insert into nostr_event_state", + needle: "insert into nostr_event_head", err: SqlError::Internal, }; - assert!( - radroots_replica_ingest_event_state(&state_insert_only_fail, &profile, "", "hash") - .is_err() - ); + assert!(radroots_replica_ingest_event_head(&state_insert_only_fail, &profile).is_err()); - crate::event_state::event_content_hash_fail_next(); - assert!(event_state_decision(&exec, &profile, "").is_err()); + crate::event_head::event_content_hash_fail_next(); + assert!(event_head_decision(&exec, &profile).is_err()); let farm_tag_insert_fail = QueryFailExecutor { inner: &exec, @@ -4071,7 +4176,7 @@ mod tests { err: SqlError::Internal, }; assert!( - upsert_member_claims(&claims_insert_fail, &"n".repeat(64), &member_of_set).is_err() + upsert_member_claims(&claims_insert_fail, &"7".repeat(64), &member_of_set).is_err() ); super::failpoints::set_gcs_point_serialize_error(); @@ -4088,7 +4193,7 @@ mod tests { let exec = SqliteExecutor::open_memory().expect("db"); let (farm_id, farm_pubkey, _, _) = seed_rows(&exec); - let member_pubkey = "m".repeat(64); + let member_pubkey = "6".repeat(64); let member_list_set = RadrootsListSet { d_tag: "farm:AAAAAAAAAAAAAAAAAAAAAQ:members".to_string(), content: String::new(), @@ -4133,7 +4238,7 @@ mod tests { upsert_farm_members(&exec, &farm_id, ListSetRole::Plots, &member_list_set) .expect("plots is no-op"); - let claimant_pubkey = "n".repeat(64); + let claimant_pubkey = "7".repeat(64); let claims_list_set = RadrootsListSet { d_tag: "member_of.farms".to_string(), content: String::new(), @@ -4199,7 +4304,7 @@ mod tests { bad_member_of.entries[0].tag = "x".to_string(); let bad_member_of_event = list_set_event( 951, - &"n".repeat(64), + &"7".repeat(64), 221, KIND_LIST_SET_GENERIC, &bad_member_of, @@ -4218,7 +4323,7 @@ mod tests { assert!(ingest_list_set_event(&exec, &bad_plots_event).is_err()); let mut bad_members = - farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["m".repeat(64)]) + farm_list_sets::farm_members_list_set(&farm_d_tag, vec!["6".repeat(64)]) .expect("members"); bad_members.entries[0].tag = "a".to_string(); let bad_members_event = diff --git a/crates/replica_sync/src/lib.rs b/crates/replica_sync/src/lib.rs @@ -7,7 +7,7 @@ extern crate alloc; mod canonical; pub mod emit; pub mod error; -mod event_state; +mod event_head; mod geo; pub mod ingest; pub mod sync_state; @@ -21,7 +21,7 @@ pub use emit::{ }; pub use error::RadrootsReplicaEventsError; pub use ingest::{ - RadrootsReplicaIdFactory, RadrootsReplicaIngestOutcome, radroots_replica_ingest_event_state, + RadrootsReplicaIdFactory, RadrootsReplicaIngestOutcome, radroots_replica_ingest_event_head, radroots_replica_ingest_event_with_factory, }; pub use sync_state::{ diff --git a/crates/replica_sync/src/sync_state.rs b/crates/replica_sync/src/sync_state.rs @@ -4,11 +4,11 @@ use alloc::{collections::BTreeMap, string::String, string::ToString, vec::Vec}; use std::collections::BTreeMap; use radroots_replica_db_schema::farm::IFarmFindMany; -use radroots_replica_db_schema::nostr_event_state::INostrEventStateFindMany; +use radroots_replica_db_schema::nostr_event_head::INostrEventHeadFindMany; use radroots_sql_core::SqlExecutor; use crate::error::RadrootsReplicaEventsError; -use crate::event_state::{event_content_hash, event_state_key, tag_value}; +use crate::event_head::{event_content_hash, event_head_key, tag_value}; use crate::types::{RadrootsReplicaEventDraft, RadrootsReplicaFarmSelector}; #[derive(Clone, Debug)] @@ -60,7 +60,7 @@ pub fn radroots_replica_pending_publish_batch<E: SqlExecutor>( let bundle = crate::emit::radroots_replica_sync_all_with_options(exec, &selector, None)?; for event in bundle.events { let d_tag = tag_value(&event.tags, "d").unwrap_or(""); - let key = event_state_key(event.kind, &event.author, d_tag); + let key = event_head_key(event.kind, &event.author, d_tag); let content_hash = draft_content_hash(&event)?; expected .entry(key.clone()) @@ -75,9 +75,9 @@ pub fn radroots_replica_pending_publish_batch<E: SqlExecutor>( } } - let states_query = radroots_replica_db::nostr_event_state::find_many( + let states_query = radroots_replica_db::nostr_event_head::find_many( exec, - &INostrEventStateFindMany { filter: None }, + &INostrEventHeadFindMany { filter: None }, ); let states_result = states_query?; let states = states_result.results; @@ -119,13 +119,13 @@ fn draft_content_hash( mod tests { use super::{radroots_replica_pending_publish_batch, radroots_replica_sync_status}; use crate::emit::radroots_replica_sync_all_with_options; - use crate::event_state::{ - event_content_hash, event_content_hash_fail_next, event_state_key, tag_value, + use crate::event_head::{ + event_content_hash, event_content_hash_fail_next, event_head_key, tag_value, }; use crate::types::RadrootsReplicaFarmSelector; - use radroots_replica_db::{farm, migrations, nostr_event_state}; + use radroots_replica_db::{farm, migrations, nostr_event_head}; use radroots_replica_db_schema::farm::IFarmFields; - use radroots_replica_db_schema::nostr_event_state::INostrEventStateFields; + use radroots_replica_db_schema::nostr_event_head::INostrEventHeadFields; use radroots_sql_core::{SqlExecutor, SqliteExecutor}; #[test] @@ -171,9 +171,9 @@ mod tests { let expected_count = bundle.events.len(); let first = bundle.events.first().expect("event"); let d_tag = tag_value(&first.tags, "d").unwrap_or(""); - let key = event_state_key(first.kind, &first.author, d_tag); + let key = event_head_key(first.kind, &first.author, d_tag); let content_hash = event_content_hash(&first.content, &first.tags).expect("hash"); - let fields = INostrEventStateFields { + let fields = INostrEventHeadFields { key, kind: first.kind, pubkey: first.author.clone(), @@ -182,7 +182,7 @@ mod tests { last_created_at: 1, content_hash, }; - let _ = nostr_event_state::create(&exec, &fields).expect("state"); + let _ = nostr_event_head::create(&exec, &fields).expect("state"); let status = radroots_replica_sync_status(&exec).expect("status"); assert_eq!(status.expected_count, expected_count); @@ -222,9 +222,9 @@ mod tests { radroots_replica_sync_all_with_options(&exec, &selector, None).expect("bundle"); let first = bundle.events.first().expect("event"); let d_tag = tag_value(&first.tags, "d").unwrap_or(""); - let key = event_state_key(first.kind, &first.author, d_tag); + let key = event_head_key(first.kind, &first.author, d_tag); let content_hash = event_content_hash(&first.content, &first.tags).expect("hash"); - let fields = INostrEventStateFields { + let fields = INostrEventHeadFields { key: key.clone(), kind: first.kind, pubkey: first.author.clone(), @@ -233,7 +233,7 @@ mod tests { last_created_at: 1, content_hash, }; - let _ = nostr_event_state::create(&exec, &fields).expect("state"); + let _ = nostr_event_head::create(&exec, &fields).expect("state"); let batch = radroots_replica_pending_publish_batch(&exec).expect("batch"); @@ -313,8 +313,8 @@ mod tests { let exec = SqliteExecutor::open_memory().expect("db"); migrations::run_all_up(&exec).expect("migrations"); let _ = exec - .exec("DROP TABLE nostr_event_state;", "[]") - .expect("drop nostr_event_state"); + .exec("DROP TABLE nostr_event_head;", "[]") + .expect("drop nostr_event_head"); let err = radroots_replica_sync_status(&exec).expect_err("state query error"); assert!(err.to_string().contains("invalid query")); } diff --git a/crates/replica_sync/src/tests.rs b/crates/replica_sync/src/tests.rs @@ -141,7 +141,7 @@ fn sync_all_emits_expected_order() { "plot_tag", ); - let owner_pubkey = "o".repeat(64); + let owner_pubkey = "8".repeat(64); let _ = unwrap_sql( farm_member::create( &exec, diff --git a/crates/replica_sync/tests/ingest_roundtrip.rs b/crates/replica_sync/tests/ingest_roundtrip.rs @@ -391,7 +391,7 @@ fn seed_source( "plot_tag", ); - let owner_pubkey = "o".repeat(64); + let owner_pubkey = "8".repeat(64); let _ = unwrap_sql( farm_member::create( exec, @@ -629,7 +629,7 @@ fn ingest_reports_parse_and_state_error_paths_for_all_kinds() { let exec = SqliteExecutor::open_memory().expect("db"); migrations::run_all_up(&exec).expect("migrations"); - let profile_pubkey = "q".repeat(64); + let profile_pubkey = "a".repeat(64); let profile_ok = profile_event( 9_201, &profile_pubkey, @@ -647,7 +647,7 @@ fn ingest_reports_parse_and_state_error_paths_for_all_kinds() { ); assert!(radroots_replica_ingest_event(&exec, &profile_parse_error).is_err()); - let farm_pubkey = "r".repeat(64); + let farm_pubkey = "b".repeat(64); let farm_seed_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; let farm_seed = farm_event( 9_203, @@ -708,7 +708,7 @@ fn ingest_reports_parse_and_state_error_paths_for_all_kinds() { let state_query_fail = QueryFailExecutor { inner: &exec, - needle: "nostr_event_state", + needle: "nostr_event_head", err: SqlError::Internal, }; assert!(radroots_replica_ingest_event(&state_query_fail, &profile_ok).is_err()); @@ -728,12 +728,12 @@ fn ingest_reports_parse_and_state_error_paths_for_all_kinds() { let state_insert_fail = QueryFailExecutor { inner: &exec, - needle: "insert into nostr_event_state", + needle: "insert into nostr_event_head", err: SqlError::Internal, }; let profile_insert_state_error = profile_event( 9_209, - &"s".repeat(64), + &"c".repeat(64), 18, Some(RadrootsProfileType::Individual), "profile-state-insert", @@ -787,7 +787,7 @@ fn ingest_reports_query_fail_paths_for_profile_farm_plot_and_list_sets() { ); }; - let profile_pubkey = "t".repeat(64); + let profile_pubkey = "d".repeat(64); let profile_create = profile_event( 9_301, &profile_pubkey, @@ -810,7 +810,7 @@ fn ingest_reports_query_fail_paths_for_profile_farm_plot_and_list_sets() { ); assert_query_fail("update nostr_profile", &profile_update); - let farm_pubkey = "u".repeat(64); + let farm_pubkey = "e".repeat(64); let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA"; let farm_create = farm_event( 9_303, @@ -902,21 +902,21 @@ fn ingest_reports_query_fail_paths_for_profile_farm_plot_and_list_sets() { assert_query_fail("insert into farm_member_claim", &member_of_event); let members_set = - farm_list_sets::farm_members_list_set(farm_d_tag, vec!["m".repeat(64)]).expect("members"); + farm_list_sets::farm_members_list_set(farm_d_tag, vec!["6".repeat(64)]).expect("members"); let members_event = list_set_event(9_308, &farm_pubkey, 17, KIND_LIST_SET_GENERIC, &members_set); assert_query_fail("insert into farm_member", &members_event); assert_query_fail("select * from farm where", &members_event); - assert_query_fail("select * from nostr_event_state", &members_event); - assert_query_fail("insert into nostr_event_state", &members_event); + assert_query_fail("select * from nostr_event_head", &members_event); + assert_query_fail("insert into nostr_event_head", &members_event); assert_eq!( radroots_replica_ingest_event(&exec, &members_event).expect("seed members"), RadrootsReplicaIngestOutcome::Applied ); let members_update = list_set_event(9_309, &farm_pubkey, 18, KIND_LIST_SET_GENERIC, &members_set); - assert_query_fail("update nostr_event_state", &members_update); + assert_query_fail("update nostr_event_head", &members_update); } fn event_with_parts( @@ -1092,7 +1092,7 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { let exec = SqliteExecutor::open_memory().expect("db"); migrations::run_all_up(&exec).expect("migrations"); - let profile_pubkey = "p".repeat(64); + let profile_pubkey = "9".repeat(64); let profile_create = profile_event( 101, &profile_pubkey, @@ -1119,7 +1119,7 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { radroots_replica_ingest_event(&exec, &profile_older).expect("profile skip older"), RadrootsReplicaIngestOutcome::Skipped ); - let profile_same_time_new_hash = profile_event( + let profile_same_time_higher_id = profile_event( 103, &profile_pubkey, 10, @@ -1127,8 +1127,20 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { "alice-updated", ); assert_eq!( - radroots_replica_ingest_event(&exec, &profile_same_time_new_hash) - .expect("profile apply same timestamp different hash"), + radroots_replica_ingest_event(&exec, &profile_same_time_higher_id) + .expect("profile skip same timestamp higher id"), + RadrootsReplicaIngestOutcome::Skipped + ); + let profile_same_time_lower_id = profile_event( + 100, + &profile_pubkey, + 10, + Some(RadrootsProfileType::Individual), + "alice-lower-id", + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &profile_same_time_lower_id) + .expect("profile apply same timestamp lower id"), RadrootsReplicaIngestOutcome::Applied ); let profile_missing_type = profile_event(104, &profile_pubkey, 11, None, "missing-type"); @@ -1204,7 +1216,7 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { radroots_replica_ingest_event(&exec, &farm_older).expect("farm skip older"), RadrootsReplicaIngestOutcome::Skipped ); - let farm_update_same_time = farm_event( + let farm_update_same_time_higher_id = farm_event( 202, &farm_pubkey, 100, @@ -1214,7 +1226,22 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { Some(vec!["market".to_string()]), ); assert_eq!( - radroots_replica_ingest_event(&exec, &farm_update_same_time).expect("farm update"), + radroots_replica_ingest_event(&exec, &farm_update_same_time_higher_id) + .expect("farm skip same timestamp higher id"), + RadrootsReplicaIngestOutcome::Skipped + ); + let farm_update_same_time_lower_id = farm_event( + 199, + &farm_pubkey, + 100, + farm_d_tag, + "farm-a-updated", + None, + Some(vec!["market".to_string()]), + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &farm_update_same_time_lower_id) + .expect("farm update same timestamp lower id"), RadrootsReplicaIngestOutcome::Applied ); @@ -1316,7 +1343,7 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { radroots_replica_ingest_event(&exec, &plot_older).expect("plot skip older"), RadrootsReplicaIngestOutcome::Skipped ); - let plot_update = plot_event( + let plot_update_higher_id = plot_event( 302, &farm_pubkey, 200, @@ -1330,7 +1357,26 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { Some(vec!["updated".to_string()]), ); assert_eq!( - radroots_replica_ingest_event(&exec, &plot_update).expect("plot update"), + radroots_replica_ingest_event(&exec, &plot_update_higher_id) + .expect("plot skip same timestamp higher id"), + RadrootsReplicaIngestOutcome::Skipped + ); + let plot_update_lower_id = plot_event( + 299, + &farm_pubkey, + 200, + plot_d_tag, + RadrootsFarmRef { + pubkey: farm_pubkey.clone(), + d_tag: farm_d_tag.to_string(), + }, + "plot-a-updated", + None, + Some(vec!["updated".to_string()]), + ); + assert_eq!( + radroots_replica_ingest_event(&exec, &plot_update_lower_id) + .expect("plot update same timestamp lower id"), RadrootsReplicaIngestOutcome::Applied ); let plot_missing_farm = plot_event( @@ -1339,7 +1385,7 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { 201, "AAAAAAAAAAAAAAAAAAAAAg", RadrootsFarmRef { - pubkey: "z".repeat(64), + pubkey: "3".repeat(64), d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), }, "plot-missing-farm", @@ -1614,7 +1660,7 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { ); let members_valid = - farm_list_sets::farm_members_list_set(farm_d_tag, vec!["m".repeat(64), "m".repeat(64)]) + farm_list_sets::farm_members_list_set(farm_d_tag, vec!["6".repeat(64), "6".repeat(64)]) .expect("members list"); let members_event = list_set_event( 406, @@ -1647,14 +1693,14 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { RadrootsReplicaIngestOutcome::Applied ); let owners_valid = - farm_list_sets::farm_owners_list_set(farm_d_tag, vec!["o".repeat(64)]).expect("owners"); + farm_list_sets::farm_owners_list_set(farm_d_tag, vec!["8".repeat(64)]).expect("owners"); let owners_event = list_set_event(407, &farm_pubkey, 307, KIND_LIST_SET_GENERIC, &owners_valid); assert_eq!( radroots_replica_ingest_event(&exec, &owners_event).expect("owners apply"), RadrootsReplicaIngestOutcome::Applied ); let workers_valid = - farm_list_sets::farm_workers_list_set(farm_d_tag, vec!["w".repeat(64)]).expect("workers"); + farm_list_sets::farm_workers_list_set(farm_d_tag, vec!["0".repeat(64)]).expect("workers"); let workers_event = list_set_event( 408, &farm_pubkey, @@ -1808,7 +1854,7 @@ fn sync_status_reports_pending_when_not_all_events_are_ingested() { } target .exec( - "UPDATE nostr_event_state SET content_hash = ? WHERE id = (SELECT id FROM nostr_event_state LIMIT 1)", + "UPDATE nostr_event_head SET content_hash = ? WHERE id = (SELECT id FROM nostr_event_head LIMIT 1)", "[\"invalid_hash\"]", ) .expect("mutate state hash"); @@ -1856,7 +1902,7 @@ fn sync_all_rejects_invalid_selectors_and_resolves_unique_pair() { assert!(missing_id_err.to_string().contains("farm not found")); let duplicate_d_tag = "AAAAAAAAAAAAAAAAAAAAAA".to_string(); - let duplicate_pubkey = "u".repeat(64); + let duplicate_pubkey = "e".repeat(64); let fields = IFarmFields { d_tag: duplicate_d_tag.clone(), pubkey: duplicate_pubkey.clone(), @@ -1893,7 +1939,7 @@ fn sync_emit_handles_invalid_geojson_and_unknown_profile_type() { let exec = SqliteExecutor::open_memory().expect("db"); migrations::run_all_up(&exec).expect("migrations"); - let farm_pubkey = "g".repeat(64); + let farm_pubkey = "0".repeat(64); let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA".to_string(); let farm_row = unwrap_sql( farm::create( @@ -1986,7 +2032,7 @@ fn sync_emit_handles_invalid_geojson_and_unknown_profile_type() { "plot gcs", ); - let member_pubkey = "m".repeat(64); + let member_pubkey = "6".repeat(64); let _ = unwrap_sql( farm_member::create( &exec, @@ -2093,7 +2139,7 @@ fn sync_emit_reports_encode_error_for_invalid_farm_record() { &exec, &IFarmFields { d_tag: String::new(), - pubkey: "v".repeat(64), + pubkey: "f".repeat(64), name: "invalid farm".to_string(), about: None, website: None,