commit 963dd8dc99fc5f16ef7d7eea11f8d964770a696e
parent c9d75ca260a9191dd91a44581c1daa0460e366ab
Author: triesap <tyson@radroots.org>
Date: Mon, 14 Sep 2026 20:43:08 +0000
storage: add bounded author-wide draft inventory
- Select an explicit author and schema across application scopes
- Bind distinct continuations while preserving exact-scope cursor bytes
- Retain query bounds and corruption isolation across SQLite restart
- Verify API compatibility and unchanged owner coverage gates
Diffstat:
7 files changed, 397 insertions(+), 14 deletions(-)
diff --git a/contracts/api_baselines/radroots_storage.txt b/contracts/api_baselines/radroots_storage.txt
@@ -526,6 +526,8 @@ impl radroots_storage::authored_draft_query::AuthoredDraftQuery
pub const fn radroots_storage::authored_draft_query::AuthoredDraftQuery::after(&self) -> core::option::Option<[u8; 16]>
pub const fn radroots_storage::authored_draft_query::AuthoredDraftQuery::author(&self) -> &[u8; 32]
pub fn radroots_storage::authored_draft_query::AuthoredDraftQuery::cursor_after(&self, [u8; 16]) -> radroots_storage::authored_draft_query::AuthoredDraftCursor
+pub fn radroots_storage::authored_draft_query::AuthoredDraftQuery::for_author([u8; 32], impl core::convert::AsRef<str>, u16) -> core::result::Result<Self, radroots_storage::Error>
+pub const fn radroots_storage::authored_draft_query::AuthoredDraftQuery::is_author_wide(&self) -> bool
pub const fn radroots_storage::authored_draft_query::AuthoredDraftQuery::limit(&self) -> u16
pub fn radroots_storage::authored_draft_query::AuthoredDraftQuery::matches(&self, &radroots_storage::authored_draft::AuthoredDraft) -> bool
pub fn radroots_storage::authored_draft_query::AuthoredDraftQuery::new([u8; 32], impl core::convert::AsRef<str>, core::option::Option<radroots_storage::authored_draft_query::AuthoredDraftScope>, u16) -> core::result::Result<Self, radroots_storage::Error>
@@ -541,6 +543,7 @@ pub fn [u8; 32]::from(radroots_storage::authored_draft_query::AuthoredDraftScope
impl core::convert::TryFrom<[u8; 32]> for radroots_storage::authored_draft_query::AuthoredDraftScope
pub type radroots_storage::authored_draft_query::AuthoredDraftScope::Error = radroots_storage::Error
pub fn radroots_storage::authored_draft_query::AuthoredDraftScope::try_from([u8; 32]) -> core::result::Result<Self, radroots_storage::Error>
+pub const radroots_storage::authored_draft_query::AUTHORED_DRAFT_AUTHOR_CURSOR_SCHEMA_VERSION: u16
pub const radroots_storage::authored_draft_query::AUTHORED_DRAFT_CURSOR_SCHEMA_VERSION: u16
pub const radroots_storage::authored_draft_query::AUTHORED_DRAFT_PAGE_PAYLOAD_MAX_BYTES: usize
pub const radroots_storage::authored_draft_query::AUTHORED_DRAFT_PAGE_SNAPSHOT_MAX_BYTES: usize
diff --git a/contracts/architecture/decisions/authored_draft_inventory.v1.json b/contracts/architecture/decisions/authored_draft_inventory.v1.json
@@ -0,0 +1,13 @@
+{
+ "schema": "radroots.authored-draft-inventory.v1",
+ "status": "implemented",
+ "owners": ["radroots_storage", "radroots_storage_sqlite"],
+ "authority": "Supplement authored_draft_submission.v1 query mechanics without changing its exact optional-scope behavior or historical serialized records.",
+ "selection": "AuthoredDraftQuery::for_author explicitly selects one independently supplied author and exact payload schema across scoped and unscoped current heads. AuthoredDraftQuery::new still selects one exact optional scope; None means unscoped only. Neither constructor infers or changes author or schema from a continuation.",
+ "cursor": "Exact-scope cursors retain byte-identical schema version 1. Author-wide cursors require schema version 2, null scope and the explicit selection marker author_schema. Reject missing or conflicting selection markers, scope on an author-wide cursor, unknown fields, unsupported versions and invalid existing fields. Continuations cannot move between exact-scope and author-wide queries, authors or schemas. Callers retain independent authority selection on every page.",
+ "bounds": {"page_records": 256, "metadata_lookahead": 1, "decoded_page_payload_bytes": 4194304, "serialized_page_snapshot_bytes": 16777216},
+ "backend": "Retain stable ascending draft ID ordering, latest revisions, one released SQLite read snapshot per page, bounded memory retention and explicit continuation after page or byte exhaustion. Existing corrupt-row locators and unknown historical schema policy remain author-bound. Known foreign authors and payload schemas do not enter results. No migration, SQL ownership, dependency, private snapshot or signed operation identity changes.",
+ "consistency": "This is a live bounded traversal, not a frozen multi-page inventory. Updates do not move stable IDs. Callers must serialize their own application mutation admission or resweep for new earlier IDs; storage installs no task, lock outside the read snapshot, or application policy.",
+ "verification": ["old exact-scope and cursor assertions unchanged", "new cursor roundtrip and authority rejection", "memory and SQLite multi-scope parity", "1,000 records across page boundaries with concurrent revisions", "corrupt and foreign records and SQLite reopen", "existing byte bounds", "public API, full coverage thresholds, workspace and release preflight"],
+ "non_goals": ["publication policy", "application schema interpretation", "author enumeration", "unbounded result collections", "new database", "signing or transport", "release qualification"]
+}
diff --git a/crates/storage/README.md b/crates/storage/README.md
@@ -175,6 +175,14 @@ SQLite status requires its governed lock, WAL, and busy-timeout contract.
## Serialization
+`AuthoredDraftQuery::new` selects an exact author, schema and optional scope;
+an absent scope selects only unscoped drafts. `AuthoredDraftQuery::for_author`
+explicitly traverses that author's schema across all scopes using the same
+bounded pages. Its version-2 continuation includes an explicit selection marker
+and cannot be used for an exact-scope query or another author/schema. Existing
+version-1 continuations keep their original bytes and meaning. Both traversals
+are live views: callers must revisit earlier IDs when new work can be inserted.
+
The optional `serde` feature serializes passive identities, requests, records,
receipts, status values, manifests, and coordination metadata. Deserialization
revalidates invariants rather than trusting encoded revisions, digests, paths,
diff --git a/crates/storage/src/authored_draft_query.rs b/crates/storage/src/authored_draft_query.rs
@@ -12,6 +12,8 @@ pub const AUTHORED_DRAFT_PAGE_PAYLOAD_MAX_BYTES: usize = 4 * 1024 * 1024;
/// Maximum serialized snapshot bytes read by one native query page.
pub const AUTHORED_DRAFT_PAGE_SNAPSHOT_MAX_BYTES: usize = 16 * 1024 * 1024;
pub const AUTHORED_DRAFT_CURSOR_SCHEMA_VERSION: u16 = 1;
+/// Explicit author/schema traversal has a distinct continuation authority.
+pub const AUTHORED_DRAFT_AUTHOR_CURSOR_SCHEMA_VERSION: u16 = 2;
/// An opaque, stable application-selected scope digest; never a credential.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
@@ -48,6 +50,7 @@ pub struct AuthoredDraftQuery {
author: [u8; 32],
payload_schema: String,
scope: Option<AuthoredDraftScope>,
+ author_wide: bool,
limit: u16,
after: Option<[u8; 16]>,
}
@@ -73,12 +76,38 @@ impl AuthoredDraftQuery {
author,
payload_schema: schema.to_owned(),
scope,
+ author_wide: false,
limit,
after: None,
})
}
+ /// Selects this author's exact payload schema across all application scopes.
+ /// This does not broaden `new(..., None, ...)`, which remains unscoped only.
+ pub fn for_author(
+ author: [u8; 32],
+ payload_schema: impl AsRef<str>,
+ limit: u16,
+ ) -> Result<Self, Error> {
+ let mut query = Self::new(author, payload_schema, None, limit)?;
+ query.author_wide = true;
+ Ok(query)
+ }
+
+ /// Whether scope filtering was explicitly omitted by `for_author`.
+ pub const fn is_author_wide(&self) -> bool {
+ self.author_wide
+ }
+
+ const fn cursor_version(&self) -> u16 {
+ if self.author_wide {
+ AUTHORED_DRAFT_AUTHOR_CURSOR_SCHEMA_VERSION
+ } else {
+ AUTHORED_DRAFT_CURSOR_SCHEMA_VERSION
+ }
+ }
+
pub fn with_cursor(mut self, cursor: &AuthoredDraftCursor) -> Result<Self, Error> {
- if cursor.schema_version != AUTHORED_DRAFT_CURSOR_SCHEMA_VERSION
+ if cursor.schema_version != self.cursor_version()
|| cursor.author != self.author
|| cursor.payload_schema != self.payload_schema
|| cursor.scope != self.scope
@@ -94,6 +123,8 @@ impl AuthoredDraftQuery {
pub fn payload_schema(&self) -> &str {
&self.payload_schema
}
+ /// Exact optional scope for ordinary queries. Author-wide queries return
+ /// `None`; backends must also honor `is_author_wide` when selecting rows.
pub const fn scope(&self) -> Option<AuthoredDraftScope> {
self.scope
}
@@ -106,11 +137,11 @@ impl AuthoredDraftQuery {
pub fn matches(&self, draft: &AuthoredDraft) -> bool {
draft.author() == &self.author
&& draft.payload_schema() == self.payload_schema
- && draft.scope() == self.scope
+ && (self.author_wide || draft.scope() == self.scope)
}
pub fn cursor_after(&self, after_id: [u8; 16]) -> AuthoredDraftCursor {
AuthoredDraftCursor {
- schema_version: AUTHORED_DRAFT_CURSOR_SCHEMA_VERSION,
+ schema_version: self.cursor_version(),
author: self.author,
payload_schema: self.payload_schema.clone(),
scope: self.scope,
@@ -133,8 +164,16 @@ pub struct AuthoredDraftCursor {
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(untagged)]
+enum CursorWire {
+ Exact(ExactCursorWire),
+ Author(AuthorCursorWire),
+}
+
+#[cfg(feature = "serde")]
+#[derive(serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
-struct CursorWire {
+struct ExactCursorWire {
schema_version: u16,
author: [u8; 32],
payload_schema: String,
@@ -142,28 +181,68 @@ struct CursorWire {
after_id: [u8; 16],
}
#[cfg(feature = "serde")]
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(deny_unknown_fields)]
+struct AuthorCursorWire {
+ schema_version: u16,
+ author: [u8; 32],
+ payload_schema: String,
+ scope: (),
+ after_id: [u8; 16],
+ selection: CursorSelection,
+}
+#[cfg(feature = "serde")]
+#[derive(serde::Serialize, serde::Deserialize)]
+enum CursorSelection {
+ #[serde(rename = "author_schema")]
+ AuthorSchema,
+}
+#[cfg(feature = "serde")]
impl TryFrom<CursorWire> for AuthoredDraftCursor {
type Error = Error;
fn try_from(value: CursorWire) -> Result<Self, Error> {
- if value.schema_version != AUTHORED_DRAFT_CURSOR_SCHEMA_VERSION {
- return Err(Error::InvalidAuthoredDraft);
- }
- Ok(
- AuthoredDraftQuery::new(value.author, value.payload_schema, value.scope, 1)?
- .cursor_after(value.after_id),
- )
+ let (query, after) = match value {
+ CursorWire::Exact(value)
+ if value.schema_version == AUTHORED_DRAFT_CURSOR_SCHEMA_VERSION =>
+ {
+ (
+ AuthoredDraftQuery::new(value.author, value.payload_schema, value.scope, 1)?,
+ value.after_id,
+ )
+ }
+ CursorWire::Author(value)
+ if value.schema_version == AUTHORED_DRAFT_AUTHOR_CURSOR_SCHEMA_VERSION =>
+ {
+ (
+ AuthoredDraftQuery::for_author(value.author, value.payload_schema, 1)?,
+ value.after_id,
+ )
+ }
+ _ => return Err(Error::InvalidAuthoredDraft),
+ };
+ Ok(query.cursor_after(after))
}
}
#[cfg(feature = "serde")]
impl From<AuthoredDraftCursor> for CursorWire {
fn from(value: AuthoredDraftCursor) -> Self {
- Self {
+ if value.schema_version == AUTHORED_DRAFT_AUTHOR_CURSOR_SCHEMA_VERSION {
+ return Self::Author(AuthorCursorWire {
+ schema_version: value.schema_version,
+ author: value.author,
+ payload_schema: value.payload_schema,
+ scope: (),
+ after_id: value.after_id,
+ selection: CursorSelection::AuthorSchema,
+ });
+ }
+ Self::Exact(ExactCursorWire {
schema_version: value.schema_version,
author: value.author,
payload_schema: value.payload_schema,
scope: value.scope,
after_id: value.after_id,
- }
+ })
}
}
diff --git a/crates/storage/tests/authored_draft_query.rs b/crates/storage/tests/authored_draft_query.rs
@@ -35,6 +35,159 @@ fn query(scope: Option<AuthoredDraftScope>, limit: u16) -> AuthoredDraftQuery {
}
#[test]
+fn author_wide_cursor_requires_explicit_selection_and_independent_authority() {
+ let q = AuthoredDraftQuery::for_author([9; 32], "fixture.composer.v1", 256).unwrap();
+ assert!(q.is_author_wide());
+ assert_eq!(q.scope(), None);
+ assert!(!query(None, 1).is_author_wide());
+ assert!(AuthoredDraftQuery::for_author([0; 32], "fixture.composer.v1", 1).is_err());
+ let cursor = q.cursor_after([3; 16]);
+ let bytes = serde_json::to_vec(&cursor).unwrap();
+ let decoded: AuthoredDraftCursor = serde_json::from_slice(&bytes).unwrap();
+ assert_eq!(decoded, cursor);
+ assert_eq!(
+ q.clone().with_cursor(&decoded).unwrap().after(),
+ Some([3; 16])
+ );
+ assert_eq!(serde_json::to_vec(&decoded).unwrap(), bytes);
+ let wire = serde_json::to_value(&cursor).unwrap();
+ assert_eq!(wire["schema_version"], 2);
+ assert_eq!(wire["selection"], "author_schema");
+ assert!(wire["scope"].is_null());
+ for changed in [
+ query(None, 1),
+ query(Some(AuthoredDraftScope::new([7; 32]).unwrap()), 1),
+ AuthoredDraftQuery::for_author([8; 32], q.payload_schema(), 1).unwrap(),
+ AuthoredDraftQuery::for_author([9; 32], "fixture.other.v1", 1).unwrap(),
+ ] {
+ assert!(changed.with_cursor(&decoded).is_err());
+ }
+ assert!(
+ q.with_cursor(&query(None, 1).cursor_after([3; 16]))
+ .is_err()
+ );
+ for (field, value) in [
+ ("schema_version", serde_json::json!(1)),
+ ("schema_version", serde_json::json!(3)),
+ ("selection", serde_json::Value::Null),
+ ("selection", serde_json::json!("other")),
+ ("scope", serde_json::json!([7; 32].to_vec())),
+ ("author", serde_json::json!([0; 32].to_vec())),
+ ("payload_schema", serde_json::json!("")),
+ ("unknown", serde_json::json!(true)),
+ ] {
+ let mut forged = wire.clone();
+ forged[field] = value;
+ assert!(
+ serde_json::from_value::<AuthoredDraftCursor>(forged).is_err(),
+ "{field}"
+ );
+ }
+ let mut absent = wire;
+ let mut absent_scope = absent.clone();
+ absent_scope.as_object_mut().unwrap().remove("scope");
+ assert!(serde_json::from_value::<AuthoredDraftCursor>(absent_scope).is_err());
+ absent.as_object_mut().unwrap().remove("selection");
+ assert!(serde_json::from_value::<AuthoredDraftCursor>(absent).is_err());
+ let old = query(None, 1).cursor_after([3; 16]);
+ let mut forbidden = serde_json::to_value(&old).unwrap();
+ forbidden["selection"] = serde_json::Value::Null;
+ assert!(serde_json::from_value::<AuthoredDraftCursor>(forbidden).is_err());
+ let expected = format!(
+ "{{\"schema_version\":1,\"author\":{},\"payload_schema\":\"fixture.composer.v1\",\"scope\":null,\"after_id\":{}}}",
+ serde_json::to_string(&[9; 32]).unwrap(),
+ serde_json::to_string(&[3; 16]).unwrap()
+ );
+ assert_eq!(serde_json::to_string(&old).unwrap(), expected);
+}
+
+#[test]
+fn author_wide_memory_pages_cover_a_thousand_scopes_and_preserve_bounds() {
+ let store = MemoryStorage::default();
+ let scope = AuthoredDraftScope::new([7; 32]).unwrap();
+ for id in 1_u128..=1000 {
+ let value = AuthoredDraft::initial(
+ AuthoredDraftId::new(id.to_be_bytes()).unwrap(),
+ [9; 32],
+ "fixture.composer.v1",
+ vec![1],
+ AuthoredDraftStage::Draft,
+ None,
+ 10,
+ )
+ .unwrap();
+ let value = if id % 2 == 0 {
+ value.with_scope(scope).unwrap()
+ } else {
+ value
+ };
+ block_on(store.append_authored_draft(value, None)).unwrap();
+ }
+ for (id, author, schema) in [
+ (1001_u128, [8; 32], "fixture.composer.v1"),
+ (1002, [9; 32], "fixture.other.v1"),
+ ] {
+ let value = AuthoredDraft::initial(
+ AuthoredDraftId::new(id.to_be_bytes()).unwrap(),
+ author,
+ schema,
+ vec![1],
+ AuthoredDraftStage::Draft,
+ None,
+ 10,
+ )
+ .unwrap();
+ block_on(store.append_authored_draft(value, None)).unwrap();
+ }
+ let q = AuthoredDraftQuery::for_author([9; 32], "fixture.composer.v1", 37).unwrap();
+ let mut cursor = None;
+ let mut expected = 1_u128;
+ loop {
+ let query = cursor.as_ref().map_or_else(
+ || q.clone(),
+ |cursor| q.clone().with_cursor(cursor).unwrap(),
+ );
+ let page = block_on(store.query_authored_drafts(query)).unwrap();
+ assert!(page.records().len() <= 37);
+ for record in page.records() {
+ assert_eq!(record.draft_key(), expected.to_be_bytes());
+ expected += 1;
+ }
+ cursor = page.next_cursor().cloned();
+ if cursor.is_none() {
+ break;
+ }
+ }
+ assert_eq!(expected, 1001);
+ for id in 1003_u128..=1005 {
+ let value = AuthoredDraft::initial(
+ AuthoredDraftId::new(id.to_be_bytes()).unwrap(),
+ [9; 32],
+ "fixture.large.v1",
+ vec![1; 2 * 1024 * 1024],
+ AuthoredDraftStage::Draft,
+ None,
+ 10,
+ )
+ .unwrap();
+ let value = if id % 2 == 0 {
+ value.with_scope(scope).unwrap()
+ } else {
+ value
+ };
+ block_on(store.append_authored_draft(value, None)).unwrap();
+ }
+ let q = AuthoredDraftQuery::for_author([9; 32], "fixture.large.v1", 256).unwrap();
+ let first = block_on(store.query_authored_drafts(q.clone())).unwrap();
+ assert_eq!(first.records().len(), 2);
+ let second =
+ block_on(store.query_authored_drafts(q.with_cursor(first.next_cursor().unwrap()).unwrap()))
+ .unwrap();
+ assert_eq!(second.records().len(), 1);
+ assert!(second.next_cursor().is_none());
+}
+
+#[test]
fn scoped_pages_preserve_revisions_and_do_not_mix_other_schemas_or_scopes() {
let store = MemoryStorage::default();
let scope = AuthoredDraftScope::new([7; 32]).unwrap();
diff --git a/crates/storage_sqlite/src/authored_draft_query.rs b/crates/storage_sqlite/src/authored_draft_query.rs
@@ -36,13 +36,14 @@ async fn read_page(
revisions.payload_schema = '' AS unknown_schema
FROM radroots_runtime_authored_draft_revisions AS revisions
WHERE revisions.author = ?
- AND ((revisions.payload_schema = ? AND revisions.payload_scope IS ?)
+ AND ((revisions.payload_schema = ? AND (? OR revisions.payload_scope IS ?))
OR revisions.payload_schema = '')
AND (? IS NULL OR revisions.draft_id > ?)
AND revisions.revision = (SELECT MAX(head.revision)
FROM radroots_runtime_authored_draft_revisions AS head WHERE head.draft_id = revisions.draft_id)
ORDER BY revisions.draft_id LIMIT ?"
).bind(query.author().as_slice()).bind(query.payload_schema())
+ .bind(query.is_author_wide())
.bind(query.scope().map(|value| value.as_bytes().to_vec()))
.bind(&after).bind(after).bind(i64::from(query.limit()) + 1)
.fetch_all(&mut **transaction).await.map_err(map_backend)?;
diff --git a/crates/storage_sqlite/src/authored_draft_query_tests.rs b/crates/storage_sqlite/src/authored_draft_query_tests.rs
@@ -30,6 +30,132 @@ fn draft(
fn query(scope: Option<AuthoredDraftScope>, limit: u16) -> AuthoredDraftQuery {
AuthoredDraftQuery::new([7; 32], "fixture.composer.v1", scope, limit).unwrap()
}
+
+#[tokio::test]
+async fn author_wide_sqlite_pages_preserve_scope_isolation_corruption_and_restart() {
+ let temp = TempDir::new().unwrap();
+ let store = open_store(&temp).await;
+ let scope = AuthoredDraftScope::new([9; 32]).unwrap();
+ for id in 1_u128..=1000 {
+ let value = AuthoredDraft::initial(
+ AuthoredDraftId::new(id.to_be_bytes()).unwrap(),
+ [7; 32],
+ "fixture.composer.v1",
+ vec![1],
+ AuthoredDraftStage::Draft,
+ None,
+ 10,
+ )
+ .unwrap();
+ let value = if id % 2 == 0 {
+ value.with_scope(scope).unwrap()
+ } else {
+ value
+ };
+ store.append_authored_draft(value, None).await.unwrap();
+ }
+ corrupt(&store, 0, 7, "fixture.composer.v1", Some(scope)).await;
+ corrupt(&store, 2, 8, "fixture.composer.v1", Some(scope)).await;
+ corrupt(&store, 3, 7, "fixture.other.v1", Some(scope)).await;
+ let q = AuthoredDraftQuery::for_author([7; 32], "fixture.composer.v1", 37).unwrap();
+ let first = store.query_authored_drafts(q.clone()).await.unwrap();
+ assert_eq!(first.records().len(), 37);
+ assert!(matches!(
+ first.records()[0],
+ AuthoredDraftQueryRecord::Corrupt {
+ draft_key: [0, ..],
+ ..
+ }
+ ));
+ for (index, record) in first.records().iter().enumerate().skip(1) {
+ assert_eq!(record.draft_key(), (index as u128).to_be_bytes());
+ }
+ let mut cursor = first.next_cursor().cloned();
+ // Revisions of both a visited and an unvisited ID do not move position.
+ for id in [1_u128, 999] {
+ let original = store
+ .authored_draft_head(AuthoredDraftId::new(id.to_be_bytes()).unwrap())
+ .await
+ .unwrap()
+ .unwrap();
+ let next = original
+ .successor(vec![2], AuthoredDraftStage::Draft, None, 11)
+ .unwrap();
+ store
+ .append_authored_draft(next, Some(original.revision()))
+ .await
+ .unwrap();
+ }
+ store.close().await.unwrap();
+ let store = open_store(&temp).await;
+ let mut expected = 37_u128;
+ while let Some(current) = cursor {
+ let bytes = serde_json::to_vec(¤t).unwrap();
+ let decoded = serde_json::from_slice(&bytes).unwrap();
+ let page = store
+ .query_authored_drafts(q.clone().with_cursor(&decoded).unwrap())
+ .await
+ .unwrap();
+ assert!(page.records().len() <= 37);
+ for record in page.records() {
+ assert_eq!(record.draft_key(), expected.to_be_bytes());
+ if expected == 999 {
+ assert_eq!(record.revision().get(), 2);
+ }
+ expected += 1;
+ }
+ cursor = page.next_cursor().cloned();
+ }
+ assert_eq!(expected, 1001);
+ let unscoped = store.query_authored_drafts(query(None, 256)).await.unwrap();
+ assert!(
+ unscoped
+ .records()
+ .iter()
+ .all(|record| u128::from_be_bytes(record.draft_key()) % 2 == 1)
+ );
+ let fresh = store.query_authored_drafts(q).await.unwrap();
+ assert_eq!(fresh.records()[1].revision().get(), 2);
+ store.close().await.unwrap();
+}
+
+#[tokio::test]
+async fn author_wide_sqlite_pages_preserve_snapshot_and_payload_budgets() {
+ for byte in [0, 99] {
+ let temp = TempDir::new().unwrap();
+ let store = open_store(&temp).await;
+ let scope = AuthoredDraftScope::new([9; 32]).unwrap();
+ for (id, scope) in [(1, None), (2, Some(scope))] {
+ store
+ .append_authored_draft(
+ draft(
+ id,
+ "fixture.composer.v1",
+ scope,
+ vec![byte; 3 * 1024 * 1024],
+ ),
+ None,
+ )
+ .await
+ .unwrap();
+ }
+ let q = AuthoredDraftQuery::for_author([7; 32], "fixture.composer.v1", 256).unwrap();
+ let first = store.query_authored_drafts(q.clone()).await.unwrap();
+ assert_eq!(first.records().len(), 1);
+ let next = store
+ .query_authored_drafts(q.with_cursor(first.next_cursor().unwrap()).unwrap())
+ .await
+ .unwrap();
+ assert_eq!(next.records().len(), 1);
+ assert_eq!(next.records()[0].draft_key(), [2; 16]);
+ assert!(matches!(
+ next.records()[0],
+ AuthoredDraftQueryRecord::Draft(_)
+ ));
+ assert!(next.next_cursor().is_none());
+ store.close().await.unwrap();
+ }
+}
async fn corrupt(
store: &SqliteStorage,
id: u8,