nautechsystems/nautilus_trader · error · anyhow::Error

Failed to batch insert into pool_fee_protocol_update_event t

Error message

Failed to batch insert into pool_fee_protocol_update_event table: {e}

What it means

Raised when the batched UNNEST INSERT of pool fee-protocol update events into the `pool_fee_protocol_update_event` table fails at the sqlx `.execute` step. The driver error is wrapped with this message. The fee-protocol-update batch is not persisted.

Source

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

            ) AS t(chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
                   log_index, fee_protocol0_new, fee_protocol1_new)
            ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
           ",
        )
        .bind(&chain_ids[..])
        .bind(&dex_names[..])
        .bind(&pool_identifiers[..])
        .bind(&blocks[..])
        .bind(&transaction_hashes[..])
        .bind(&transaction_indices[..])
        .bind(&log_indices[..])
        .bind(&fee_protocol0s[..])
        .bind(&fee_protocol1s[..])
        .execute(&self.pool)
        .await
        .map(|_| ())
        .map_err(|e| {
            anyhow::anyhow!("Failed to batch insert into pool_fee_protocol_update_event table: {e}")
        })
    }

    /// Inserts multiple pool protocol-fee withdrawal events in a single database operation using UNNEST.
    ///
    /// # Errors
    ///
    /// Returns an error if the database operation fails.
    pub async fn add_pool_fee_protocol_collect_batch(
        &self,
        chain_id: u32,
        collects: &[PoolFeeProtocolCollect],
    ) -> anyhow::Result<()> {
        if collects.is_empty() {
            return Ok(());
        }

        // Prepare vectors for each column

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Read the `{e}` suffix for the exact sqlx/PostgreSQL error and fix that cause
  2. Ensure arrays bound via UNNEST (identifiers, blocks, hashes, indices, fee protocols) have equal lengths
  3. Apply pending migrations so the table schema matches the bound columns
  4. Use ON CONFLICT DO NOTHING/UPDATE for idempotent re-insertion of replayed logs
  5. Verify database connectivity and pool health

Example fix

// before
.map_err(|e| {
    anyhow::anyhow!("Failed to batch insert into pool_fee_protocol_update_event table: {e}")
})
// after: make the write idempotent and add context
// INSERT ... ON CONFLICT (block, transaction_hash, log_index) DO NOTHING
.map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_fee_protocol_update_event table (rows={}): {e}", fee_protocol0s.len()))
Defensive patterns

Strategy: try-catch

Validate before calling

fn validate_fee_protocol_update_batch(updates: &[FeeProtocolUpdate]) -> anyhow::Result<()> {
    for u in updates {
        anyhow::ensure!(u.fee_protocol0_new <= 255 && u.fee_protocol1_new <= 255, "fee protocol out of range");
        anyhow::ensure!(!u.transaction_hash.is_empty(), "missing tx hash");
    }
    Ok(())
}

Type guard

fn is_valid_update(u: &FeeProtocolUpdate) -> bool {
    u.fee_protocol0_new <= 255 && u.fee_protocol1_new <= 255
}

Try / catch

match insert_pool_fee_protocol_updates(&updates).await {
    Ok(()) => {},
    Err(e) if e.to_string().contains("duplicate key") => tracing::debug!("already persisted"),
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Executing the batch insert after successfully converting fee_protocol0/1 values, when Postgres rejects the statement: constraint violation (PK/unique on block/tx/log index), array-length mismatch, column type mismatch, or lost connection.

Common situations: Replaying the same logs twice causing unique conflicts; schema drift after migrations; pool connections killed by idle timeouts or failover during indexing runs.

Related errors


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