tursodatabase/turso · error · ToSqlConversionFailure

only finite floating-point values can be bound

Error message

only finite floating-point values can be bound

What it means

The Rust batch API validates parameters before executing a batch and rejects any bound Value::Real that is NaN-adjacent infinity (f64::INFINITY or NEG_INFINITY). SQLite has no way to store IEEE infinity in its wire/file format, so binding one would silently corrupt or fail later; the library fails fast with ToSqlConversionFailure wrapping an InvalidInput io::Error.

Source

Thrown at bindings/rust/src/batch.rs:60

impl BatchStatement {
    /// Create a batch statement from SQL text and parameters, accepting the
    /// same parameter forms as [`Connection::execute`](crate::Connection::execute).
    pub fn new(sql: impl Into<String>, params: impl IntoParams) -> Result<Self> {
        Ok(Self {
            sql: sql.into(),
            params: params.into_params()?,
        })
    }

    pub(crate) fn validate_params(&self) -> Result<()> {
        let has_infinity = match &self.params {
            Params::None => false,
            Params::Positional(values) => values.iter().any(is_infinite),
            Params::Named(values) => values.iter().any(|(_, value)| is_infinite(value)),
        };
        if has_infinity {
            return Err(Error::ToSqlConversionFailure(Box::new(
                std::io::Error::new(
                    std::io::ErrorKind::InvalidInput,
                    "only finite floating-point values can be bound",
                ),
            )));
        }
        Ok(())
    }

    pub(crate) fn controls_transaction(&self) -> bool {
        matches!(
            first_sql_keyword(&self.sql).as_deref(),
            Some("BEGIN" | "COMMIT" | "END" | "ROLLBACK" | "SAVEPOINT" | "RELEASE")
        )
    }
}

fn is_infinite(value: &Value) -> bool {
    matches!(value, Value::Real(number) if number.is_infinite())

View on GitHub (pinned to c1e5928725)

Solutions

  1. Check parameter values with f64::is_finite() before constructing the BatchStatement and clamp, store NULL, or return an application error instead.
  2. Use Option<f64> (Params None/Null) for values that may be infinite, mapping non-finite to NULL.
  3. If infinity must be persisted, store a sentinel TEXT/REAL value (e.g. 'Infinity') and convert on read.

Example fix

// before
let ratio = numerator / denominator; // may be inf
conn.batch([BatchStatement::new("INSERT INTO t(v) VALUES (?1)", (ratio,))?])?;
// after
let ratio = if denominator != 0.0 { numerator / denominator } else { f64::NAN };
let v = if ratio.is_finite() { Some(ratio) } else { None };
conn.batch([BatchStatement::new("INSERT INTO t(v) VALUES (?1)", (v,))?])?;
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_bindable(params: &[f64]) -> Result<(), String> {
    params.iter().find(|v| !v.is_finite())
        .map_or(Ok(()), |v| Err(format!("non-finite param: {v}")))
}

Type guard

fn is_bindable_real(v: f64) -> bool { v.is_finite() }

Try / catch

match stmt_result {
    Err(turso::Error::ToSqlConversionFailure(e)) if e.to_string().contains("finite") => {
        eprintln!("non-finite bind value: {e}"); // sanitize params and retry
    }
    r => r.expect("batch failed"),
}

Prevention

When it happens

Trigger: Calling BatchStatement::new with params containing an infinite f64 (e.g. computed via 1.0/0.0, f64::INFINITY, or overflowed arithmetic) in either positional or named Params, when the batch is later validated by validate_params during batch execution.

Common situations: Dividing by zero or accumulating overflow in Rust code that feeds values into a batch INSERT/UPDATE; deserializing JSON numbers like 1e999 into f64 infinity; log/metric aggregation producing inf and passing it straight to the database.

Related errors


AI-assisted analysis of tursodatabase/turso@c1e5928725 (2026-08-31). Data as JSON: /api/errors/f94dccd033042aac. Report an issue: GitHub.