From 5e8f6c76c5edf47fd2fda39da9a31deafcca9270 Mon Sep 17 00:00:00 2001 From: dawn <90008@klbr.net> Date: Thu, 8 Oct 2026 03:43:00 +0300 Subject: [PATCH] [types] RepoState v15 with identity_resolved_at, v15 fence, ttl epoch repo rows get a trailing identity_resolved_at (unix secs of the doc fetch behind handle, pds and key), so the relay can trust a stored key for a ttl. it's skipped while None, so an unstamped row is byte for byte a v7 row and nothing is rewritten. v15 notes the ttl epoch in counts, and a row without a stamp reads as resolved at epoch - hash(did) % ttl, spreading the first round of refreshes over one ttl. v15 is a fence, and unlike v14 it has to fence relay-only builds too: they skip v8 for good, so their stored version stayed at 7 and an older relay build would open the db and fail on every stamped row (a v7 reader rejects the longer array, tested). once the fence is applied, markers are authoritative and the stored version is 1 + the newest applied migration, holes or not, so every later migration a relay build applies keeps the builds before it out too, and no build infers that a migration it skipped ran. --- src/db/counts.rs | 13 ++++ src/db/migration/mod.rs | 93 +++++++++++++++++----- src/db/migration/v13.rs | 11 ++- src/db/migration/v14.rs | 1 - src/db/migration/v15.rs | 105 +++++++++++++++++++++++++ src/db/migration/v7.rs | 10 +-- src/types.rs | 59 +++++++++++++- src/types/v14.rs | 2 +- src/types/v15.rs | 119 +++++++++++++++++++++++++++++ tests/representation_inventory.tsv | 7 ++ 10 files changed, 388 insertions(+), 32 deletions(-) create mode 100644 src/db/migration/v15.rs create mode 100644 src/types/v15.rs diff --git a/src/db/counts.rs b/src/db/counts.rs index d1ab0dc..c8f4597 100644 --- a/src/db/counts.rs +++ b/src/db/counts.rs @@ -166,6 +166,19 @@ pub(crate) fn apply_count_delta(current: u64, delta: i64) -> u64 { } } +/// unix secs of the first open at v15. rows from before it carry no resolve time, see +/// [`crate::types::RepoState::identity_resolved_or_spread`] +pub(crate) const IDENTITY_TTL_EPOCH: &str = "identity_ttl_epoch"; + +#[allow(dead_code)] +pub(crate) fn identity_ttl_epoch(db: &Db) -> Result { + db.counts + .get(keys::count_keyspace_key(IDENTITY_TTL_EPOCH)) + .into_diagnostic()? + .map(|value| read_u64_counter(&value).map(|epoch| epoch as i64)) + .unwrap_or_else(|| Ok(chrono::Utc::now().timestamp())) +} + pub fn set_ks_count(batch: &mut OwnedWriteBatch, db: &Db, name: &str, count: u64) { let key = keys::count_keyspace_key(name); batch.insert(&db.counts, key, count.to_be_bytes()); diff --git a/src/db/migration/mod.rs b/src/db/migration/mod.rs index 03eaddd..cff1b76 100644 --- a/src/db/migration/mod.rs +++ b/src/db/migration/mod.rs @@ -68,6 +68,7 @@ mod v11; mod v12; mod v13; mod v14; +mod v15; mod v2; mod v3; mod v4; @@ -260,8 +261,31 @@ const MIGRATIONS: &[(&str, Migration)] = &[ "repo_metadata_v14", Migration::Atomic(v14::repo_metadata_v14), ), + ( + FENCE, + Migration::Atomic(v15::repo_state_identity_resolved_at), + ), ]; +/// from this migration on, the applied markers are the whole truth. a relay-only build skips v8 +/// and the indexer passes for good, so a contiguous version stays at 7 there and would never +/// keep an older build out of rows it can't read +const FENCE: &str = "repo_state_identity_resolved_at"; + +/// the version to store, from which migrations are applied in order. before the fence it's the +/// end of the unbroken run from the start, like it always was. once fenced it's the newest +/// applied one, holes or not, so every migration a build applies keeps the builds before it out +fn schema_version(applied: &[bool], fenced: bool) -> u64 { + let version = match fenced { + true => applied + .iter() + .rposition(|applied| *applied) + .map_or(0, |i| i + 1), + false => applied.iter().take_while(|applied| **applied).count(), + }; + version as u64 +} + pub(crate) const LATEST_VERSION: u64 = MIGRATIONS.len() as u64; /// resume cursors for chunked passes, in the `counts` keyspace: @@ -306,7 +330,13 @@ pub(crate) fn rewind_version_for_test(db: &Db, version: u64) -> Result<()> { batch.commit().into_diagnostic() } -fn is_migration_applied(db: &Db, index: usize, name: &str, stored_version: u64) -> Result { +fn is_migration_applied( + db: &Db, + index: usize, + name: &str, + stored_version: u64, + fenced: bool, +) -> Result { let key = migration_applied_key(name); if db.counts.contains_key(&key).into_diagnostic()? { return Ok(true); @@ -315,8 +345,9 @@ fn is_migration_applied(db: &Db, index: usize, name: &str, stored_version: u64) // Legacy versions predate per-migration applied keys. Their global version // proves ordinary migrations ran, while feature-gated migrations need a // cheap migration-specific proof: reduced-feature builds advanced the old - // version even when they skipped the work. - if (index as u64) < stored_version { + // version even when they skipped the work. a fenced database had markers + // for everything already, and its version may be past what it applied. + if !fenced && (index as u64) < stored_version { return match name { "rebuild_lifecycle_counts" => v8::legacy_migration_applied(db), // The old v9 marker is trusted; v10 safely and boundedly finishes @@ -485,8 +516,12 @@ pub(super) fn run(db: &Db) -> Result<()> { "database schema version {stored_version} is newer than supported version {LATEST_VERSION}" ); } + let fenced = db + .counts + .contains_key(migration_applied_key(FENCE)) + .into_diagnostic()?; for (i, (name, migration)) in MIGRATIONS.iter().enumerate() { - if is_migration_applied(db, i, name, stored_version)? { + if is_migration_applied(db, i, name, stored_version, fenced)? { let key = migration_applied_key(name); if !db.counts.contains_key(&key).into_diagnostic()? { db.counts.insert(key, b"1").into_diagnostic()?; @@ -541,22 +576,19 @@ pub(super) fn run(db: &Db) -> Result<()> { } } - // Advance VERSIONING_KEY to highest contiguous applied migration version - let mut max_contiguous_version = 0u64; - for (i, (name, _)) in MIGRATIONS.iter().enumerate() { - if is_migration_applied(db, i, name, stored_version)? { - max_contiguous_version = (i + 1) as u64; - } else { - break; - } - } - if max_contiguous_version != stored_version { + let applied = MIGRATIONS + .iter() + .enumerate() + .map(|(i, (name, _))| is_migration_applied(db, i, name, stored_version, fenced)) + .collect::>>()?; + let fenced = db + .counts + .contains_key(migration_applied_key(FENCE)) + .into_diagnostic()?; + let version = schema_version(&applied, fenced); + if version != stored_version { let mut batch = db.inner.batch(); - batch.insert( - &db.counts, - VERSIONING_KEY, - max_contiguous_version.to_be_bytes(), - ); + batch.insert(&db.counts, VERSIONING_KEY, version.to_be_bytes()); batch.commit().into_diagnostic()?; } @@ -836,6 +868,29 @@ mod tests { Ok(()) } + #[test] + fn the_stored_version_follows_the_markers_once_fenced() { + // a legacy database, everything applied in order up to v12 + let legacy = [[true; 12].as_slice(), &[false; 3]].concat(); + assert_eq!(schema_version(&legacy, false), 12); + // unfenced holes still stop the version, so a skipped migration runs before anything + // treats the database as past it + assert_eq!(schema_version(&[true, true, false, true], false), 2); + + // a relay-only build: v8 and the indexer passes never apply, the fence (v15) does + let mut relay = [true; 15]; + for skipped in [7, 9, 10, 11] { + relay[skipped] = false; + } + assert_eq!(schema_version(&relay, false), 7); + assert_eq!(schema_version(&relay, true), 15); + // and a v16 every build runs moves it on, so a v15 build is kept out of its changes + let after = [relay.as_slice(), &[true]].concat(); + assert_eq!(schema_version(&after, true), 16); + let not_yet = [relay.as_slice(), &[false]].concat(); + assert_eq!(schema_version(¬_yet, true), 15); + } + #[test] fn chunked_pass_rewrites_every_entry_across_many_chunks() -> Result<()> { let (_tmp, db) = open_db()?; diff --git a/src/db/migration/v13.rs b/src/db/migration/v13.rs index 619f85b..a9a4b38 100644 --- a/src/db/migration/v13.rs +++ b/src/db/migration/v13.rs @@ -59,13 +59,20 @@ mod tests { for (did, pds, active) in rows { let row = v7::RepoState { active, + status: v7::RepoStatus::Desynchronized, + root: None, + last_message_time: None, + last_identity_time: None, + last_account_time: None, + last_updated_at: 0, + signing_key: None, pds: Some(CowStr::Borrowed(pds)), - ..v7::RepoState::backfilling() + handle: None, }; batch.insert( &db.repos, keys::repo_key(&Did::new_static(did).into_diagnostic()?), - crate::db::ser_repo_state(&row)?, + rmp_serde::to_vec(&row).into_diagnostic()?, ); } // what the writers that never counted left behind: one short, one host whose diff --git a/src/db/migration/v14.rs b/src/db/migration/v14.rs index d00200d..c878008 100644 --- a/src/db/migration/v14.rs +++ b/src/db/migration/v14.rs @@ -52,7 +52,6 @@ mod tests { .get(VERSIONING_KEY) .into_diagnostic()? .expect("schema version"); - assert_eq!(LATEST_VERSION, 14); assert_eq!( u64::from_be_bytes(version.as_ref().try_into().into_diagnostic()?), LATEST_VERSION diff --git a/src/db/migration/v15.rs b/src/db/migration/v15.rs new file mode 100644 index 0000000..74ff32c --- /dev/null +++ b/src/db/migration/v15.rs @@ -0,0 +1,105 @@ +//! v15 is a fence for the `types::v15::RepoState` shape. old rows decode with no resolve time +//! through the serde default, but an older build can't read a stamped row, so it has to refuse +//! the database at open. it also notes when the ttl epoch started. don't drop it as a no-op + +use fjall::OwnedWriteBatch; +use miette::Result; + +use crate::db::Db; +use crate::db::counts::{IDENTITY_TTL_EPOCH, set_ks_count}; + +pub(super) fn repo_state_identity_resolved_at( + db: &Db, + batch: &mut OwnedWriteBatch, +) -> Result { + set_ks_count( + batch, + db, + IDENTITY_TTL_EPOCH, + chrono::Utc::now().timestamp() as u64, + ); + Ok(true) +} + +#[cfg(test)] +mod tests { + use crate::config::Config; + use crate::db::keys::{self, VERSIONING_KEY}; + use crate::db::migration::{LATEST_VERSION, rewind_version_for_test}; + use crate::db::{Db, deser_repo_state}; + use crate::types::v7; + use jacquard_common::types::did::Did; + use miette::{IntoDiagnostic, Result}; + use tempfile::tempdir; + + fn version(db: &Db) -> Result { + let bytes = db + .counts + .get(VERSIONING_KEY) + .into_diagnostic()? + .expect("schema version"); + Ok(u64::from_be_bytes( + bytes.as_ref().try_into().into_diagnostic()?, + )) + } + + fn cfg(path: &std::path::Path) -> Config { + Config { + database_path: path.to_path_buf(), + ..Default::default() + } + } + + /// relay-only builds never apply v8, so without the fence their version would stay at 7, + /// and a build from before v15 would open the db and fail on every stamped row + #[test] + fn every_build_fences_older_ones_out() -> Result<()> { + let tmp = tempdir().into_diagnostic()?; + let did = Did::new_static("did:plc:ewvi7nxzyoun6zhxrhs64oiz").into_diagnostic()?; + let old_row = rmp_serde::to_vec(&v7::RepoState { + active: true, + status: v7::RepoStatus::Synced, + root: None, + last_message_time: Some(1), + last_identity_time: None, + last_account_time: None, + last_updated_at: 2, + signing_key: None, + pds: Some("https://pds.example/".into()), + handle: None, + }) + .into_diagnostic()?; + { + let db = Db::open(&cfg(tmp.path()))?; + db.repos + .insert(keys::repo_key(&did), &old_row) + .into_diagnostic()?; + rewind_version_for_test(&db, 7)?; + db.persist()?; + } + + let db = Db::open(&cfg(tmp.path()))?; + assert_eq!(LATEST_VERSION, 15); + assert_eq!(version(&db)?, LATEST_VERSION); + let epoch = crate::db::counts::identity_ttl_epoch(&db)?; + assert!((chrono::Utc::now().timestamp() - epoch).abs() < 60); + let row = db + .repos + .get(keys::repo_key(&did)) + .into_diagnostic()? + .expect("repo row"); + assert_eq!( + row.as_ref(), + old_row.as_slice(), + "the fence rewrites nothing" + ); + assert_eq!(deser_repo_state(&row)?.identity_resolved_at, None); + drop(db); + + // and opening it again doesn't move anything, nor trip over what this build skipped + let db = Db::open(&cfg(tmp.path()))?; + assert_eq!(version(&db)?, LATEST_VERSION); + assert_eq!(crate::db::counts::identity_ttl_epoch(&db)?, epoch); + Ok(()) + } +} diff --git a/src/db/migration/v7.rs b/src/db/migration/v7.rs index 1ed45b3..b39448c 100644 --- a/src/db/migration/v7.rs +++ b/src/db/migration/v7.rs @@ -2,7 +2,8 @@ use fjall::OwnedWriteBatch; use miette::{Context, IntoDiagnostic, Result}; use crate::db::Db; -use crate::types::{RepoState, v4}; +use crate::types::v4; +use crate::types::v7::RepoState; pub(crate) fn repo_state_event_clocks(db: &Db, batch: &mut OwnedWriteBatch) -> Result { for guard in db.repos.iter() { @@ -101,11 +102,8 @@ mod tests { .get(keys::VERSIONING_KEY) .into_diagnostic()? .expect("db version should be set"); - let expected_version = if cfg!(feature = "indexer") { - crate::db::migration::LATEST_VERSION - } else { - 7 - }; + // relay-only builds skip v8 for good, but v15's fence still lifts them to the latest + let expected_version = crate::db::migration::LATEST_VERSION; assert_eq!( u64::from_be_bytes(version_bytes.as_ref().try_into().into_diagnostic()?), expected_version diff --git a/src/types.rs b/src/types.rs index 72eabbc..4b8d2b2 100644 --- a/src/types.rs +++ b/src/types.rs @@ -13,6 +13,7 @@ use crate::resolver::MiniDoc; #[cfg(any(feature = "indexer_stream", feature = "relay", feature = "jetstream"))] pub(crate) mod event; pub(crate) mod v14; +pub(crate) mod v15; pub(crate) mod v2; pub(crate) mod v4; pub(crate) mod v7; @@ -20,6 +21,7 @@ pub(crate) mod v7; #[cfg(any(feature = "indexer_stream", feature = "relay", feature = "jetstream"))] pub(crate) use event::*; pub(crate) use v14::*; +pub(crate) use v15::RepoState; /// which record-scoped body copies an operator redaction removes. #[cfg(feature = "indexer")] @@ -214,6 +216,7 @@ impl<'i> RepoState<'i> { last_message_time: None, last_identity_time: None, last_account_time: None, + identity_resolved_at: None, } } @@ -328,10 +331,20 @@ impl<'i> RepoState<'i> { } else { (current.active, current.status) }; - let (handle, pds, signing_key) = if identity_changed { - (self.handle, self.pds, self.signing_key) + let (handle, pds, signing_key, identity_resolved_at) = if identity_changed { + ( + self.handle, + self.pds, + self.signing_key, + self.identity_resolved_at, + ) } else { - (current.handle, current.pds, current.signing_key) + ( + current.handle, + current.pds, + current.signing_key, + current.identity_resolved_at, + ) }; let root = match (self.root, current.root) { (_, theirs) if !root_changed => theirs, @@ -349,8 +362,20 @@ impl<'i> RepoState<'i> { signing_key, pds, handle, + identity_resolved_at, } } + + /// when the identity was last resolved. a row from before v15 has no time, and reads as + /// resolved at a point in the ttl before `epoch` picked by its did, so the rows that were + /// there at the upgrade come due for a refresh spread over one ttl instead of all at once + #[allow(dead_code)] + pub(crate) fn identity_resolved_or_spread(&self, did: &Did, epoch: i64, ttl: i64) -> i64 { + self.identity_resolved_at.unwrap_or_else(|| { + let spread = (crate::util::hash(&did.as_str()) % ttl.max(1) as u64) as i64; + epoch - spread + }) + } } impl<'i> IntoStatic for RepoState<'i> { @@ -368,6 +393,7 @@ impl<'i> IntoStatic for RepoState<'i> { last_message_time: self.last_message_time, last_identity_time: self.last_identity_time, last_account_time: self.last_account_time, + identity_resolved_at: self.identity_resolved_at, } } } @@ -610,6 +636,33 @@ mod tests { assert_eq!(merged.last_identity_time, Some(20)); } + #[test] + fn rows_without_a_resolve_time_spread_over_one_ttl() -> miette::Result<()> { + let ttl = 86_400; + let epoch = 1_700_000_000; + let ages = (0..1000) + .map(|i| { + let did = Did::new_owned(format!("did:plc:{i:0>24}")).into_diagnostic()?; + let state = RepoState::backfilling(); + let at = state.identity_resolved_or_spread(&did, epoch, ttl); + assert_eq!(at, state.identity_resolved_or_spread(&did, epoch, ttl)); + Ok(epoch - at) + }) + .collect::>>()?; + assert!(ages.iter().all(|age| (0..ttl).contains(age))); + // a tenth of the ttl each, give or take + for slot in 0..10 { + let in_slot = ages.iter().filter(|age| **age * 10 / ttl == slot).count(); + assert!((50..150).contains(&in_slot), "slot {slot} has {in_slot}"); + } + + let mut stamped = RepoState::backfilling(); + stamped.identity_resolved_at = Some(42); + let did = Did::new_static("did:plc:ewvi7nxzyoun6zhxrhs64oiz").into_diagnostic()?; + assert_eq!(stamped.identity_resolved_or_spread(&did, epoch, ttl), 42); + Ok(()) + } + #[test] fn commit_and_repo_status_conversion_contracts() { let did: Did = Did::new_static("did:plc:ewvi7nxzyoun6zhxrhs64oiz").unwrap(); diff --git a/src/types/v14.rs b/src/types/v14.rs index 07be4f1..715ed75 100644 --- a/src/types/v14.rs +++ b/src/types/v14.rs @@ -1,4 +1,4 @@ -pub(crate) use super::v7::{Commit, RepoState, RepoStatus}; +pub(crate) use super::v7::{Commit, RepoStatus}; use crate::db::types::DbTid; use serde::{Deserialize, Serialize}; diff --git a/src/types/v15.rs b/src/types/v15.rs new file mode 100644 index 0000000..48d4625 --- /dev/null +++ b/src/types/v15.rs @@ -0,0 +1,119 @@ +pub(crate) use super::v7::{Commit, RepoStatus}; +use crate::db::types::DidKey; +use jacquard_common::CowStr; +use jacquard_common::types::string::Handle; +use serde::{Deserialize, Serialize}; + +/// v7's row plus when its identity was last resolved. rows are msgpack arrays, so a v7 row +/// decodes as this one with the new field `None`, and nothing gets rewritten +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(bound(deserialize = "'i: 'de"))] +pub(crate) struct RepoState<'i> { + pub active: bool, + pub status: RepoStatus, + pub root: Option, + pub last_message_time: Option, + pub last_identity_time: Option, + pub last_account_time: Option, + pub last_updated_at: i64, // unix timestamp + #[serde(borrow)] + pub signing_key: Option>, + #[serde(borrow)] + pub pds: Option>, + pub handle: Option, + /// unix secs of the doc fetch that set handle, pds and key. `None` on rows from before v15, + /// which read as resolved somewhere in the ttl before the db first opened at v15. it's the + /// last field and left out when `None`, so a row nobody stamped yet stays a v7 row + #[serde(default, skip_serializing_if = "Option::is_none")] + pub identity_resolved_at: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn v7_row() -> super::super::v7::RepoState<'static> { + super::super::v7::RepoState { + active: true, + status: RepoStatus::Synced, + root: None, + last_message_time: Some(7), + last_identity_time: Some(8), + last_account_time: Some(9), + last_updated_at: 42, + signing_key: Some(DidKey(std::borrow::Cow::Borrowed(&[0xed, 1, 2, 3]))), + pds: Some("https://pds.example".into()), + handle: Some(Handle::new_static("alice.test").unwrap()), + } + } + + #[test] + fn v15_persisted_schema_golden_fixture() { + let old = v7_row(); + let state = RepoState { + active: old.active, + status: old.status, + root: old.root, + last_message_time: old.last_message_time, + last_identity_time: old.last_identity_time, + last_account_time: old.last_account_time, + last_updated_at: old.last_updated_at, + signing_key: old.signing_key, + pds: old.pds, + handle: old.handle, + identity_resolved_at: Some(1_700_000_000), + }; + let encoded = rmp_serde::to_vec(&state).unwrap(); + assert_eq!( + hex::encode(&encoded), + "9bc3a653796e636564c00708092ac404ed010203b368747470733a2f2f7064732e6578616d706c65aa616c6963652e74657374ce6553f100" + ); + let decoded: RepoState<'_> = rmp_serde::from_slice(&encoded).unwrap(); + assert_eq!(decoded.identity_resolved_at, Some(1_700_000_000)); + assert_eq!(decoded.pds.as_deref(), Some("https://pds.example")); + } + + #[test] + fn an_unstamped_row_is_written_as_a_v7_row() { + let old = v7_row(); + let state = RepoState { + active: old.active, + status: old.status.clone(), + root: old.root.clone(), + last_message_time: old.last_message_time, + last_identity_time: old.last_identity_time, + last_account_time: old.last_account_time, + last_updated_at: old.last_updated_at, + signing_key: old.signing_key.clone(), + pds: old.pds.clone(), + handle: old.handle.clone(), + identity_resolved_at: None, + }; + assert_eq!( + rmp_serde::to_vec(&state).unwrap(), + rmp_serde::to_vec(&old).unwrap() + ); + } + + #[test] + fn a_v7_row_decodes_with_no_resolve_time() { + let encoded = rmp_serde::to_vec(&v7_row()).unwrap(); + let decoded: RepoState<'_> = rmp_serde::from_slice(&encoded).unwrap(); + assert_eq!(decoded.identity_resolved_at, None); + assert_eq!(decoded.last_account_time, Some(9)); + assert_eq!( + decoded.handle.as_ref().map(Handle::as_str), + Some("alice.test") + ); + } + + /// why v15 needs a fence: an older build can't read a row once it's stamped + #[test] + fn a_v7_reader_rejects_a_stamped_v15_row() { + let mut encoded = rmp_serde::to_vec(&v7_row()).unwrap(); + encoded[0] = 0x9b; + encoded.extend_from_slice(&[0xce, 0x65, 0x53, 0xf1, 0x00]); + assert!(rmp_serde::from_slice::>(&encoded).is_ok()); + assert!(rmp_serde::from_slice::>(&encoded).is_err()); + } +} diff --git a/tests/representation_inventory.tsv b/tests/representation_inventory.tsv index a3732c1..f6ba367 100644 --- a/tests/representation_inventory.tsv +++ b/tests/representation_inventory.tsv @@ -472,6 +472,7 @@ migration-transform:src/db/migration/mod.rs::read_version rust:src/db/migration/ migration-transform:src/db/migration/mod.rs::run rust:src/db/migration/mod.rs::a_chunk_that_fails_partway_commits_nothing migration-transform:src/db/migration/mod.rs::run_chunk rust:src/db/migration/mod.rs::a_chunk_that_fails_partway_commits_nothing migration-transform:src/db/migration/mod.rs::run_pass rust:src/db/migration/mod.rs::a_chunk_that_fails_partway_commits_nothing +migration-transform:src/db/migration/mod.rs::schema_version rust:src/db/migration/mod.rs::the_stored_version_follows_the_markers_once_fenced migration-transform:src/db/migration/v1.rs::stable_firehose_cursors rust:src/db/migration/mod.rs::v0_database_migrates_all_early_representation_boundaries migration-transform:src/db/migration/v10.rs::is_current_key rust:src/db/migration/v10.rs::v10_rewrites_records_and_retires_legacy_storage migration-transform:src/db/migration/v10.rs::parse_legacy_key rust:src/db/migration/v10.rs::v10_rewrites_records_and_retires_legacy_storage @@ -490,6 +491,7 @@ migration-transform:src/db/migration/v11.rs::status rust:src/db/migration/v11.rs migration-transform:src/db/migration/v12.rs::copy_pending_entry rust:src/db/migration/v12.rs::copies_a_queue_that_has_no_did_order_copy_yet migration-transform:src/db/migration/v13.rs::recount_pds_account_counts rust:src/db/migration/v13.rs::repairs_drifted_per_pds_counts_from_the_rows migration-transform:src/db/migration/v14.rs::repo_metadata_v14 rust:src/db/migration/v14.rs::old_database_opens_at_v14_with_its_metadata_rows_untouched +migration-transform:src/db/migration/v15.rs::repo_state_identity_resolved_at rust:src/db/migration/v15.rs::every_build_fences_older_ones_out migration-transform:src/db/migration/v2.rs::repo_state_root_commit rust:src/db/migration/mod.rs::v0_database_migrates_all_early_representation_boundaries migration-transform:src/db/migration/v3.rs::firehose_source_is_pds rust:src/db/migration/mod.rs::v0_database_migrates_all_early_representation_boundaries migration-transform:src/db/migration/v3.rs::is_known_relay rust:src/db/migration/mod.rs::v0_database_migrates_all_early_representation_boundaries @@ -695,6 +697,10 @@ serde-attribute:src/types/event/stream.rs::StoredEvent::serde(bound(deserialize= serde-attribute:src/types/v14.rs::RepoMetadata::backfilled_rev::serde(default) rust:src/types/v14.rs::v4_repo_metadata_rows_decode_with_no_retries serde-attribute:src/types/v14.rs::RepoMetadata::relay_inactive::serde(default) rust:src/types/v14.rs::v4_repo_metadata_rows_decode_with_no_retries serde-attribute:src/types/v14.rs::RepoMetadata::retry_count::serde(default) rust:src/types/v14.rs::v4_repo_metadata_rows_decode_with_no_retries +serde-attribute:src/types/v15.rs::RepoState::identity_resolved_at::serde(default,skip_serializing_if="Option::is_none") rust:src/types/v15.rs::an_unstamped_row_is_written_as_a_v7_row +serde-attribute:src/types/v15.rs::RepoState::pds::serde(borrow) rust:src/types/v15.rs::v15_persisted_schema_golden_fixture +serde-attribute:src/types/v15.rs::RepoState::serde(bound(deserialize="'i:'de")) rust:src/types/v15.rs::v15_persisted_schema_golden_fixture +serde-attribute:src/types/v15.rs::RepoState::signing_key::serde(borrow) rust:src/types/v15.rs::v15_persisted_schema_golden_fixture serde-attribute:src/types/v2.rs::Commit::sig::serde(with="jacquard_common::serde_bytes_helper") rust:src/types/v2.rs::v2_persisted_schema_golden_fixture serde-attribute:src/types/v2.rs::RepoState::pds::serde(borrow) rust:src/types/v2.rs::v2_persisted_schema_golden_fixture serde-attribute:src/types/v2.rs::RepoState::serde(bound(deserialize="'i:'de")) rust:src/types/v2.rs::v2_persisted_schema_golden_fixture @@ -838,6 +844,7 @@ serde-type:src/types/event/stream.rs::RecordEvt rust:src/types/event/stream.rs:: serde-type:src/types/event/stream.rs::StoredData rust:src/types/event/stream.rs::stored_data_persisted_golden_fixtures serde-type:src/types/event/stream.rs::StoredEvent rust:src/control/stream/indexer.rs::persisted_eight_byte_string_rkey_replays_verbatim serde-type:src/types/v14.rs::RepoMetadata rust:src/types/v14.rs::v14_repo_metadata_golden_fixtures +serde-type:src/types/v15.rs::RepoState rust:src/types/v15.rs::v15_persisted_schema_golden_fixture serde-type:src/types/v2.rs::Commit rust:src/types/v2.rs::v2_persisted_schema_golden_fixture serde-type:src/types/v2.rs::RepoState rust:src/types/v2.rs::v2_persisted_schema_golden_fixture serde-type:src/types/v2.rs::RepoStatus rust:src/types/v2.rs::v2_persisted_schema_golden_fixture -- 2.51.2