tursodatabase/turso · error · anyhow::Error

record header out of bounds on page

Error message

record header out of bounds on page {root_page}

What it means

After locating the payload, the generator parses the record header size and checks that the whole header fits inside the page bytes. If `payload_start + header_size` exceeds the page length, the page bytes don't match the expected SQLite record format and patching would read garbage, so it aborts for that root page.

Solutions

  1. Verify page_size matches the actual database page size.
  2. Ensure the target row's record fits entirely on the leaf page (no overflow).
  3. Re-check that `root_page` points at the table's leaf page containing the first row.

Example fix

// before
let page_size = 4096; // db actually uses 512
// after
let page_size = db_page_size(&db_path)?;
Defensive patterns

Strategy: validation

Validate before calling

let (header_size, _) = parse_sqlite_varint(&page, payload_start)?;
assert!(payload_start + header_size as usize <= page.len(), "record header spills past page {root_page}; check page_size and overflow");

Prevention

When it happens

Trigger: `patch_first_row_serial_type` computes `header_end = payload_start + header_size` from the parsed varint and it exceeds `bytes.len()` — caused by a malformed or non-leaf page, wrong page size, or overflow payload spilling off the page.

Common situations: Patching a row whose record spills to overflow pages, wrong page_size passed in, or the target page isn't the table leaf containing the first row.

Related errors


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

Appendix: source

Thrown at testing/sqltest/src/generator/mod.rs:1080

    let payload_start = cell_start + payload_varint_len + rowid_varint_len;
    anyhow::ensure!(
        payload_start < bytes.len(),
        "payload start out of bounds on page {root_page}"
    );

    let (header_size, header_size_varint_len) = parse_sqlite_varint(&bytes, payload_start)?;
    let header_end = payload_start + header_size as usize;
    anyhow::ensure!(
        header_end <= bytes.len(),
        "record header out of bounds on page {root_page}"
    );

    let mut serial_offset = payload_start + header_size_varint_len;
    for _ in 0..patch.column_index {
        let (_, serial_len) = parse_sqlite_varint(&bytes, serial_offset)?;
        serial_offset += serial_len;
    }
    anyhow::ensure!(
        serial_offset < header_end,
        "record has no column {} on page {root_page}",
        patch.column_index
    );
    anyhow::ensure!(
        bytes[serial_offset] == patch.expected_serial_type,
        "unexpected serial type {} for column {} of the fixture row, expected {}",
        bytes[serial_offset],
        patch.column_index,
        patch.expected_serial_type
    );
    bytes[serial_offset] = patch.new_serial_type;

    std::fs::write(db_path, bytes)
        .with_context(|| format!("failed to write fixture '{}'", db_path.display()))?;
    Ok(())
}

View on GitHub (pinned to 8d4a589f8d)