{"record":{"id":"6e99a0e00deaf28f","repo":"nautechsystems/nautilus_trader","slug":"failed-to-persist-replacement-hash-transaction-ha","errorCode":null,"errorMessage":"Failed to persist replacement hash {transaction_hash}: {e}","messagePattern":"Failed to persist replacement hash (.+?): (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/adapters/blockchain/src/cache/database.rs","lineNumber":3837,"sourceCode":"            \"\n            INSERT INTO execution_transaction_hash (\n                intent_id, chain_id, transaction_hash, status, current\n            ) VALUES ($1, $2, $3, 'replaced', TRUE)\n            ON CONFLICT (chain_id, transaction_hash) DO UPDATE\n            SET current = TRUE, updated_at = NOW()\n            WHERE execution_transaction_hash.intent_id = EXCLUDED.intent_id\n            RETURNING\n                id, intent_id, chain_id, transaction_hash, raw_transaction, status,\n                block_number, block_hash, receipt_success, gas_used,\n                effective_gas_price, current\n            \",\n        )\n        .bind(intent_id)\n        .bind(chain_id_db)\n        .bind(transaction_hash)\n        .fetch_optional(&mut *transaction)\n        .await\n        .map_err(|e| anyhow::anyhow!(\"Failed to persist replacement hash {transaction_hash}: {e}\"))?\n        .ok_or_else(|| {\n            anyhow::anyhow!(\"Replacement hash {transaction_hash} conflicts with another intent\")\n        })?;\n\n        sqlx::query(\n            \"UPDATE execution_intent SET status = 'replaced', updated_at = NOW() WHERE id = $1\",\n        )\n        .bind(intent_id)\n        .execute(&mut *transaction)\n        .await\n        .map_err(|e| anyhow::anyhow!(\"Failed to mark execution intent replaced: {e}\"))?;\n        sqlx::query(\n            \"\n            INSERT INTO execution_transaction_transition (\n                intent_id, transaction_hash_id, transition_key, from_status, to_status\n            ) VALUES ($1, $2, $3, $4, 'replaced')\n            ON CONFLICT (intent_id, transition_key) DO NOTHING\n            \",","sourceCodeStart":3819,"sourceCodeEnd":3855,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/2114cf6f761429e0adb5ca9596fcd7b895b16011/crates/adapters/blockchain/src/cache/database.rs#L3819-L3855","documentation":"Wrapped sqlx error from the upsert INSERT INTO execution_transaction_hash ... ON CONFLICT (chain_id, transaction_hash) DO UPDATE SET current = TRUE ... WHERE execution_transaction_hash.intent_id = EXCLUDED.intent_id RETURNING ... statement in add_execution_replacement_hash (database.rs:3818-3837). The failure is a database-level fault during the upsert; the intentional conflict-skip case (row owned by another intent) is surfaced separately as error 93.","triggerScenarios":"Connection loss or statement timeout while executing the upsert; a NOT NULL violation because raw_transaction is not provided for the new row (it is inserted with only intent_id, chain_id, transaction_hash, status, current); a foreign-key violation if intent_id does not exist in execution_intent; schema drift on the RETURNING column list.","commonSituations":"The replacement row is deliberately created without raw bytes (standard JSON-RPC block responses cannot expose the signed envelope, per the function's doc comment), so any NOT NULL constraint on raw_transaction breaks this insert; Postgres restart mid-call.","solutions":["Downcast to sqlx::Error and inspect as_database_error(): NULL constraint on raw_transaction means the schema must allow NULL raw bytes for replacement rows","Apply the project migrations that created replacement support (nullable raw_transaction, the (chain_id, transaction_hash) unique index)","Retry on transient connectivity/timeout classes with backoff","Verify the intent row still exists if an FK violation is reported"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"fn is_transient_db_error(err: &anyhow::Error) -> bool {\n    err.downcast_ref::<sqlx::Error>().map_or(false, |e| {\n        matches!(e, sqlx::Error::PoolTimedOut | sqlx::Error::PoolClosed | sqlx::Error::Io(_))\n            || e.as_database_error().and_then(|d| d.code()).map_or(false, |c| {\n                matches!(c.as_ref(), \"40001\" | \"40P01\" | \"55P03\" | \"57014\" | \"08000\" | \"08003\" | \"08006\")\n            })\n    })\n}","tryCatchPattern":"match db.add_execution_replacement_hash(intent_id, chain_id, &hash).await {\n    Ok(row) => Ok(row),\n    Err(e) if is_transient_db_error(&e) => retry_with_backoff(e),\n    Err(e) => return Err(e), // NOT NULL/FK causes: fix schema or referential data\n}","preventionTips":["Keep raw_transaction nullable - replacement rows are created without raw bytes by design","Maintain the (chain_id, transaction_hash) unique index expected by the ON CONFLICT clause","Test the upsert path in CI against the migrated schema whenever hash-row columns change"],"tags":["rust","sqlx","postgres","blockchain","upsert","replacement"],"backgroundTag":"database-insert-failed","analyzedSha":"2114cf6f761429e0adb5ca9596fcd7b895b16011","analyzedAt":"2026-08-21T11:28:30.864Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}