nautechsystems/nautilus_trader · error · anyhow::Error

Failed to persist migration evidence: {e}

Error message

Failed to persist migration evidence: {e}

What it means

This error wraps any sqlx failure that occurs while inserting a row into the `execution_verification_decision` table during migration of prior execution records. The migration routine replays each recorded decision (read class, height range, normalized value digest, etc.) as an 'migration'-classified evidence row inside a transaction; if that INSERT fails — schema mismatch, constraint violation, type conversion, or connection loss — the underlying sqlx error is wrapped via anyhow::anyhow! with this message and the whole migration transaction aborts.

Source

Thrown at crates/adapters/blockchain/src/cache/database.rs:4362

                            )
                            ",
                        )
                        .bind(record.intent_id)
                        .bind(nonce)
                        .bind(decision.read_class)
                        .bind(height_start)
                        .bind(height_end)
                        .bind(bootstrap.manifest_version)
                        .bind(bootstrap.manifest_digest)
                        .bind(bootstrap.provider_ids)
                        .bind(bootstrap.operator_ids)
                        .bind(bootstrap.failure_domain_ids)
                        .bind(&decision.normalized_value_digest)
                        .bind(format!("migration:{}:{index}", record.intent_id))
                        .execute(&mut *transaction)
                        .await
                        .map_err(|e| {
                            anyhow::anyhow!("Failed to persist migration evidence: {e}")
                        })?;
                    }
                }
            }
            0
        };

        sqlx::query(
            "
            INSERT INTO execution_verified_finalized_header (
                chain_id, wallet_address, number, hash, parent_hash, timestamp,
                base_fee_per_gas, manifest_digest
            )
            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
            ON CONFLICT (chain_id, wallet_address, number) DO NOTHING
            ",
        )
        .bind(chain_id)

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Run the latest schema migrations for this crate so `execution_verification_decision` matches the columns the INSERT expects (intent_id, nonce, decision_class, read_class, height_start, height_end, manifest_version, manifest_digest, provider_ids, operator_ids, failure_domain_ids, response_class, normalized_value_digest, nonce_revision, outcome, transition_key).
  2. Inspect the wrapped sqlx error after the colon — it names the exact column/constraint that failed (e.g. duplicate transition_key, null value, FK violation on intent_id).
  3. Verify the intent record was inserted successfully in the same transaction before its decisions; fix ordering or data in the migration source record.
  4. Ensure the decision's height_start/height_end and nonce fit in i64 (u64 values above i64::MAX must be rejected before migration).
  5. Check database connectivity, pool limits, and statement timeouts; retry the migration after the transient issue is resolved — the transaction rolls back atomically.

Example fix

// before
.execute(&mut *transaction)
.await
.map_err(|e| {
    anyhow::anyhow!("Failed to persist migration evidence: {e}")
})?;

// after — surface constraint details and context for diagnosis
.execute(&mut *transaction)
.await
.map_err(|e| {
    anyhow::anyhow!(
        "Failed to persist migration evidence for intent {} decision {index}: {e:#}",
        record.intent_id
    )
})?;
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check before migrating a record's decisions
if let Some(nonce) = record.nonce {
    anyhow::ensure!(nonce <= i64::MAX as u64, "nonce exceeds BIGINT");
}
for (i, d) in record.decisions.iter().enumerate() {
    if let Some(h) = d.height_start { anyhow::ensure!(h <= i64::MAX as u64, "decision {i} height_start exceeds BIGINT"); }
    if let Some(h) = d.height_end { anyhow::ensure!(h <= i64::MAX as u64, "decision {i} height_end exceeds BIGINT"); }
}
// Confirm the table exists with expected shape:
// SELECT 1 FROM information_schema.columns
//  WHERE table_name='execution_verification_decision' AND column_name='normalized_value_digest';

Try / catch

match migration_result {
    Err(e) if e.to_string().contains("duplicate key") => warn!("decision already migrated; continuing idempotently"),
    Err(e) => return Err(anyhow!("migration aborted: {e:#}")),
    Ok(v) => info!("migrated {v} records"),
}

Prevention

When it happens

Trigger: Calling the migration/bootstrap path that persists `record.decisions` when: the `execution_verification_decision` table is missing or has a different schema (stale migration state), a bound value violates a NOT NULL/UNIQUE/FK constraint (e.g. intent_id not yet inserted, null normalized_value_digest), height values fall outside i64 range, or the PostgreSQL connection drops mid-transaction.

Common situations: Running the blockchain cache adapter against a database whose schema was created by an older version of the crate; migrating a record with decision rows referencing an intent that failed its own INSERT earlier in the transaction; DB connectivity blips or connection-pool exhaustion during long migrations; misconfigured `database_url` pointing at the wrong database.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/e3a8f2fc5e946b9c. Report an issue: GitHub.