nautechsystems/nautilus_trader · error · anyhow::Error

Failed to batch insert into pool_swap_event table: {e}

Error message

Failed to batch insert into pool_swap_event table: {e}

What it means

The bulk INSERT of pool swap-event rows into the pool_swap_event table failed at the database level (e.g. constraint violation, connectivity, or type mismatch); the underlying sqlx error is wrapped so callers can see the batch failed and none of the rows in that batch were committed.

Source

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

        .bind(&transaction_hashes[..])
        .bind(&transaction_indices[..])
        .bind(&log_indices[..])
        .bind(&senders[..])
        .bind(&recipients[..])
        .bind(&sqrt_price_x96s[..])
        .bind(&liquidities[..])
        .bind(&ticks[..])
        .bind(&amount0s[..])
        .bind(&amount1s[..])
        .bind(&order_sides[..])
        .bind(&base_quantities[..])
        .bind(&quote_quantities[..])
        .bind(&spot_prices[..])
        .bind(&execution_prices[..])
        .execute(&self.pool)
        .await
        .map(|_| ())
        .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_swap_event table: {e}"))
    }

    /// Inserts multiple pool liquidity updates in a single database operation using UNNEST for optimal performance.
    ///
    /// # Errors
    ///
    /// Returns an error if the database operation fails.
    pub async fn add_pool_liquidity_updates_batch(
        &self,
        chain_id: u32,
        updates: &[PoolLiquidityUpdate],
    ) -> anyhow::Result<()> {
        if updates.is_empty() {
            return Ok(());
        }

        // Prepare vectors for each column
        let len = updates.len();

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Read the wrapped `{e}` for the exact failing constraint or cast
  2. Persist parent rows first (block, pool) before swap events so FKs resolve
  3. Deduplicate events by their natural key or add ON CONFLICT DO NOTHING for replay tolerance
  4. Ensure all bound slices are the same length and numeric values serialize to strings Postgres NUMERIC accepts

Example fix

// before
db.insert_swap_events(&events).await?; // fails: block rows missing
// after
db.insert_blocks(&blocks_for(events)).await?;
db.insert_swap_events(&events).await?;
Defensive patterns

Strategy: validation

Validate before calling

let n = events.len();
anyhow::ensure!(quote_quantities.len() == n && spot_prices.len() == n && execution_prices.len() == n, "swap batch arrays misaligned");
// parents must exist first
anyhow::ensure!(blocks_inserted && pools_inserted, "insert blocks/pools before swap events");

Type guard

fn swap_batch_valid(events: &[SwapEvent]) -> bool {
    events.iter().all(|e| e.log_index >= 0 && !e.tx_hash.is_empty())
}

Try / catch

match db.insert_swap_events_batch(&events).await {
    Err(e) if e.to_string().contains("duplicate key") => tracing::debug!("swap events already persisted"),
    Err(e) => return Err(e.context("swap event batch insert failed")),
    Ok(()) => {}
}

Prevention

When it happens

Trigger: Calling the batch swap insert with misaligned bound arrays (quote_quantities/spot_prices/execution_prices and the rest), numeric strings that fail Postgres numeric/decimal casts, FK violation when the referenced pool or block row is absent, or connection loss during execute.

Common situations: Swap events referencing blocks not yet inserted (FK violation due to out-of-order persistence); price/quantity serialized with precision formats rejected by NUMERIC; replayed events duplicating a unique (pool, block, tx, log_index) key; fresh DB without migrations.

Related errors


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