tursodatabase/turso · error

MVCC logical pull offset is not a transaction boundary: {off

Error message

MVCC logical pull offset is not a transaction boundary: {offset}

What it means

Incremental logical pulls from a non-zero offset need the crc32c seed recorded at that exact transaction boundary; MvccLogSnapshot.crc_by_offset only contains offsets where a complete transaction ends (the header offset plus each frame end). A client revision pointing into the middle of a transaction frame cannot be resumed — the receiver's CRC chain would not match — so crc_seed_at fails and the pull returns HTTP 500.

Source

Thrown at cli/sync_server.rs:891

struct HttpResponse {
    status: u16,
    content_type: String,
    body: Vec<u8>,
}

struct MvccLogSnapshot {
    end_offset: u64,
    crc_by_offset: Vec<(u64, u32)>,
}

impl MvccLogSnapshot {
    fn crc_seed_at(&self, offset: u64) -> Result<u32> {
        self.crc_by_offset
            .iter()
            .find_map(|(boundary, crc)| (*boundary == offset).then_some(*crc))
            .ok_or_else(|| {
                anyhow!("MVCC logical pull offset is not a transaction boundary: {offset}")
            })
    }
}

fn logical_log_path(db_path: &str) -> Result<PathBuf> {
    Ok(db_file_path(db_path)?.with_extension("db-log"))
}

fn is_in_memory_db_path(db_path: &str) -> bool {
    db_path == ":memory:"
}

fn db_file_path(db_path: &str) -> Result<PathBuf> {
    if is_in_memory_db_path(db_path) {
        return Err(anyhow!(
            "MVCC logical pull is not supported for in-memory sync server databases"
        ));
    }

View on GitHub (pinned to bad083fafb)

Solutions

  1. Always echo back the exact server_revision string from the previous /pull-updates response; it is always a transaction boundary.
  2. Send an empty client_revision to restart from offset 0 and receive the full logical range.
  3. If client or server state is stale, remove the server's .db-log: the server then falls back to replace-base pages and clients re-bootstrap.

Example fix

// before: guessed offset that may sit inside a transaction
req.client_revision = format!("g1:o{}", arbitrary_byte_offset);

// after: use the server-provided revision verbatim
req.client_revision = last_response.server_revision.clone(); // e.g. "g1:o1048576"
Defensive patterns

Strategy: fallback

Validate before calling

fn is_known_boundary(rev: &str, boundaries: &[u64]) -> bool {
    match rev.strip_prefix("g1:o").and_then(|o| o.parse::<u64>().ok()) {
        Some(off) => off == 0 || boundaries.contains(&off),
        None => rev.is_empty(),
    }
}

Try / catch

On 500 'offset is not a transaction boundary', drop the persisted revision and re-pull with an empty client_revision (offset 0), or trigger a fresh bootstrap; never re-derive offsets yourself.

Prevention

When it happens

Trigger: client_revision g1:oN where N is not a frame end recorded by scan_mvcc_log: hand-crafted revisions, offsets computed by a different tool, or a .db-log that was rewritten or compacted so a previously valid boundary no longer is.

Common situations: Unit tests using arbitrary byte offsets; persisting a hand-computed offset instead of the server's revision string; swapping .db-log files between runs.

Related errors


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