{"record":{"id":"9649ad11fae1c109","repo":"benbjohnson/litestream","slug":"last-page-match-w","errorCode":null,"errorMessage":"last page match: %w","messagePattern":"last page match: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"db.go","lineNumber":1790,"sourceCode":"\tprevWALOffset := info.offset - frameSize\n\tdb.Logger.Debug(\"verify\", \"saltMatch\", saltMatch, \"prevWALOffset\", prevWALOffset)\n\n\tif prevWALOffset == WALHeaderSize {\n\t\tif saltMatch { // No writes occurred since last sync, salt still matches\n\t\t\tinfo.snapshotting = false\n\t\t\treturn info, nil\n\t\t}\n\t\t// Salt has changed but we don't know if writes occurred since last sync\n\t\tinfo.reason = \"wal header salt reset, snapshotting\"\n\t\treturn info, nil\n\t} else if prevWALOffset < WALHeaderSize {\n\t\treturn info, fmt.Errorf(\"prev WAL offset is less than the header size: %d\", prevWALOffset)\n\t}\n\n\t// If we can't verify the last page is in the last LTX file, then we need to snapshot.\n\tlastPageMatch, err := db.lastPageMatch(ctx, dec, prevWALOffset, frameSize)\n\tif err != nil {\n\t\treturn info, fmt.Errorf(\"last page match: %w\", err)\n\t} else if !lastPageMatch {\n\t\tinfo.reason = \"last page does not exist in last ltx file, wal overwritten by another process\"\n\t\treturn info, nil\n\t}\n\n\tdb.Logger.Debug(\"verify.2\", \"lastPageMatch\", lastPageMatch)\n\n\t// Salt has changed which could indicate a FULL checkpoint.\n\t// If we have a last page match, then we can assume that the WAL has not been overwritten.\n\tif !saltMatch {\n\t\tdb.Logger.Log(ctx, internal.LevelTrace, \"wal restarted\",\n\t\t\t\"salt1\", salt1,\n\t\t\t\"salt2\", salt2)\n\n\t\tinfo.offset = WALHeaderSize\n\t\tinfo.salt1, info.salt2 = salt1, salt2\n\n\t\tif detected, err := db.detectFullCheckpoint(ctx, [][2]uint32{{salt1, salt2}, {dec.Header().WALSalt1, dec.Header().WALSalt2}}); err != nil {","sourceCodeStart":1772,"sourceCodeEnd":1808,"githubUrl":"https://github.com/benbjohnson/litestream/blob/4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3/db.go#L1772-L1808","documentation":"Wraps any error from db.lastPageMatch(), which reads the WAL page at the previously synced offset and checks that the same page exists in the last LTX file. If this check fails, litestream cannot prove the WAL wasn't overwritten by another process and conservatively snapshots.","triggerScenarios":"lastPageMatch returns an error — typically the underlying readWALFileAt or LTX page decode failing (see errors 278/279) while comparing WAL vs LTX content.","commonSituations":"WAL being rewritten concurrently by another SQLite writer on the same file; IO errors; corrupted LTX payloads.","solutions":["Ensure only one process writes the SQLite database (litestream assumes it owns replication)","Check the wrapped inner cause: WAL read errors vs LTX decode errors need different fixes","If state is unrecoverable, `litestream reset` to force a clean snapshot"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// verify single-writer assumption and WAL readability before sync\nif _, err := os.Stat(dbPath + \"-wal\"); err != nil { /* fix before syncing */ }","typeGuard":null,"tryCatchPattern":"if err := db.Sync(ctx); err != nil {\n    if strings.Contains(err.Error(), \"last page match\") {\n        // unwrap cause: WAL read vs LTX decode; snapshot fallback will kick in on next cycle\n    }\n}","preventionTips":["Only one process should write the SQLite DB","Watch for 'wal overwritten by another process' reasons in logs","Keep LTX files on healthy storage"],"tags":["wal","ltx","verification","page-match"],"backgroundTag":"file-read-failed","analyzedSha":"4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3","analyzedAt":"2026-09-06T18:29:25.564Z","contentChangedAt":"2026-09-06T18:29:25.564Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}