tursodatabase/turso · error

row count mismatch for

Error message

row count mismatch for {case:?}

What it means

For all non-ranked query cases (And, Or, Phrase), validate_result asserts the returned row count equals the full expected match count times the number of queries executed. ensure! at perf/fts/src/main.rs:338 fails when the engine returned a different number of rows than the seeded-data predicate predicts for that case.

Solutions

  1. Verify the FTS index is fully built/populated before running validation queries
  2. Run with --queries 1 to see whether a single query already returns the wrong count
  3. Compare against sqlite3 FTS5 output for the same corpus (differential check)
  4. Align the predicate in validate_result with the actual query being executed

Example fix

// before
ensure!(rows == expected_rows, "row count mismatch");
// after
ensure!(rows == expected_rows * queries, "row count mismatch for {case:?}");
Defensive patterns

Strategy: try-catch

Validate before calling

let expected: usize = (0..n).filter(|&id| predicate(id)).count() * queries;

Try / catch

if let Err(e) = validate_result(...) {
    if e.to_string().starts_with("row count mismatch") {
        eprintln!("got {rows} rows, expected {expected}; check FTS index population");
    }
    return Err(e);
}

Prevention

When it happens

Trigger: validate_result invoked with case And/Or/Phrase where rows != expected_rows * queries — the engine under- or over-returned rows for the repeated query, e.g. wrong tokenization, missing index entries, or visibility differences.

Common situations: FTS index not fully populated before querying; predicate in validate_result out of sync with the actual query text; MVCC/WAL snapshot hiding committed rows; a regression in the FTS match implementation.

Related errors


AI-assisted analysis of tursodatabase/turso@8d4a589f8d (2026-09-20). Data as JSON: /api/errors/64745f06e236b324. Report an issue: GitHub.

Appendix: source

Thrown at perf/fts/src/main.rs:338

    queries: usize,
    rows: usize,
    sum: i64,
) -> Result<()> {
    let ids = (0..documents).filter(|id| match case {
        QueryCase::Rare => id % 100 == 0,
        QueryCase::Common => true,
        QueryCase::And => id % 6 == 0,
        QueryCase::Or | QueryCase::Ranked => id % 2 == 0 || id % 3 == 0,
        QueryCase::Phrase => id % 200 == 0,
    });
    let expected_rows = ids.clone().count();
    if matches!(case, QueryCase::Ranked) {
        ensure!(
            rows == expected_rows.min(10) * queries,
            "ranked row count mismatch"
        );
    } else {
        ensure!(
            rows == expected_rows * queries,
            "row count mismatch for {case:?}"
        );
        ensure!(
            sum == ids.map(|id| id as i64).sum::<i64>() * queries as i64,
            "ID sum mismatch for {case:?}"
        );
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn rejects_mixing_search_latency_and_throughput_options() -> Result<()> {
        for options in [

View on GitHub (pinned to 8d4a589f8d)