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
- Always echo back the exact server_revision string from the previous /pull-updates response; it is always a transaction boundary.
- Send an empty client_revision to restart from offset 0 and receive the full logical range.
- 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
- Store server_revision verbatim from each response; it is always a transaction boundary.
- Never compute resume offsets from byte positions.
- Do not swap .db-log files between runs.
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
- MVCC logical pull revision is from the future: client_offset
- sync_server supports only single-generation MVCC logical pul
- invalid MVCC pull revision generation: {revision}
- invalid MVCC pull revision generation: {revision}: {err}
- invalid MVCC pull revision offset: {revision}: {err}
AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16).
Data as JSON: /api/errors/af32677d29180367.
Report an issue: GitHub.