Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 60 additions & 20 deletions crates/tracedecay-runtime-core/src/db/retrieval_anchor_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,28 +332,40 @@ async fn validate_legacy_alias_ownership(
Ok(())
}

/// The two directions an owner-unbound alias row can collide with a canonical
/// one: same owner key under a different anchor, or same anchor key under a
/// different owner.
///
/// These live outside the loop below because an array literal iterated in
/// place leaves a `std::array::IntoIter` alive across the query `await`, and
/// that iterator's `MaybeDangling`/`ManuallyDrop`/`MaybeUninit` layers are
/// re-entered for every auto-trait obligation raised on the enclosing future —
/// at the deepest point of the schema-install chain that daemon project-open
/// awaits. Borrowing a promoted slice keeps a plain `slice::Iter` there.
const ALIAS_COPY_CONFLICT_QUERIES: &[&str] = &[
"SELECT 1
FROM retrieval_anchor_aliases_owner_unbound_v1 AS legacy
JOIN retrieval_anchor_aliases AS current
ON current.owner_json = legacy.owner_json
AND current.alias_kind = legacy.alias_kind
AND current.locator_digest = legacy.locator_digest
WHERE current.anchor_id <> legacy.anchor_id
LIMIT 1",
"SELECT 1
FROM retrieval_anchor_aliases_owner_unbound_v1 AS legacy
JOIN retrieval_anchor_aliases AS current
ON current.anchor_id = legacy.anchor_id
AND current.alias_kind = legacy.alias_kind
AND current.locator_digest = legacy.locator_digest
WHERE current.owner_json <> legacy.owner_json
LIMIT 1",
];

async fn validate_alias_copy_conflicts(
conn: &(impl Executor + Sync),
operation: &str,
) -> Result<()> {
for sql in [
"SELECT 1
FROM retrieval_anchor_aliases_owner_unbound_v1 AS legacy
JOIN retrieval_anchor_aliases AS current
ON current.owner_json = legacy.owner_json
AND current.alias_kind = legacy.alias_kind
AND current.locator_digest = legacy.locator_digest
WHERE current.anchor_id <> legacy.anchor_id
LIMIT 1",
"SELECT 1
FROM retrieval_anchor_aliases_owner_unbound_v1 AS legacy
JOIN retrieval_anchor_aliases AS current
ON current.anchor_id = legacy.anchor_id
AND current.alias_kind = legacy.alias_kind
AND current.locator_digest = legacy.locator_digest
WHERE current.owner_json <> legacy.owner_json
LIMIT 1",
] {
for &sql in ALIAS_COPY_CONFLICT_QUERIES {
let mut rows = conn
.query(sql, ())
.await
Expand Down Expand Up @@ -752,6 +764,11 @@ async fn upgrade_dispositions_if_needed(
/// Existing one-column alias foreign keys are upgraded with a resumable,
/// validated copy; conflicting or ownerless rows are retained and reported
/// rather than discarded.
///
/// The two upgrade phases are awaited through a box. Each is a deep tree of
/// nested `async fn` validators, and async lowering would otherwise expand
/// those trees into the future of every migration, admission, and store-open
/// path that installs this schema.
#[hotpath::measure(label = "runtime_core.db.anchor_schema_install")]
pub async fn install_retrieval_anchor_schema(
conn: &(impl Executor + Sync),
Expand All @@ -761,8 +778,8 @@ pub async fn install_retrieval_anchor_schema(
.await
.map_err(|error| database_error(operation, error))?;
validate_anchor_table_columns(conn, operation).await?;
upgrade_aliases_if_needed(conn, operation).await?;
upgrade_dispositions_if_needed(conn, operation).await?;
Box::pin(upgrade_aliases_if_needed(conn, operation)).await?;
Box::pin(upgrade_dispositions_if_needed(conn, operation)).await?;
conn.execute_batch(
"DROP TRIGGER IF EXISTS retrieval_anchors_no_update;
DROP TRIGGER IF EXISTS retrieval_anchors_no_delete;
Expand Down Expand Up @@ -810,6 +827,29 @@ mod tests {
.expect("insert anchor");
}

/// Async lowering embeds an awaited future inside its caller, so the alias
/// and disposition upgrade phases would expand into every migration,
/// admission, and store-open future that installs this schema. Both phases
/// stay behind pointers at this boundary; the probe fails if either is
/// inlined back into the install future.
///
/// Measured under `--features hotpath`, where each measured `async fn`
/// embeds its body a second time: 2,144 B with both phases boxed, 7,664 B
/// with either one inlined.
#[test]
fn schema_install_holds_its_upgrade_phases_behind_pointers() {
const CEILING: usize = 4 * 1024;
let directory = tempfile::tempdir().expect("create anchor future probe fixture");
let connection = TestConnection::open(&directory.path().join("anchors.db"));
let install = install_retrieval_anchor_schema(&connection, "probe anchor schema future");
let size = std::mem::size_of_val(&install);
drop(install);
assert!(
size <= CEILING,
"install_retrieval_anchor_schema future is {size} B; ceiling {CEILING} B"
);
}

#[tokio::test]
async fn installs_owner_bound_aliases_and_immutable_records() {
let (_directory, conn) = connection().await;
Expand Down
Loading