benbjohnson/litestream · error

FileControlPersistWAL: %w

Error message

FileControlPersistWAL: %w

What it means

Litestream sets the PERSIST_WAL file control on the SQLite 'main' database so the -wal file survives connection close; without it, SQLite deletes the WAL and litestream cannot replicate. This error wraps a failure returned by fc.FileControlPersistWAL("main", 1) itself — the type assertion already succeeded, but the driver/SQLite rejected the operation or returned SQLITE_NOTFOUND-style errors (e.g. the schema name is wrong or the operation is unsupported for the attached database).

Source

Thrown at db.go:1016

// setPersistWAL sets the PERSIST_WAL file control on the database connection.
// This prevents SQLite from removing the WAL file when connections close.
func (db *DB) setPersistWAL(ctx context.Context) error {
	conn, err := db.db.Conn(ctx)
	if err != nil {
		return fmt.Errorf("get connection: %w", err)
	}
	defer conn.Close()

	return conn.Raw(func(driverConn interface{}) error {
		fc, ok := driverConn.(sqlite.FileControl)
		if !ok {
			return fmt.Errorf("driver does not implement FileControl")
		}

		_, err := fc.FileControlPersistWAL("main", 1)
		if err != nil {
			return fmt.Errorf("FileControlPersistWAL: %w", err)
		}

		return nil
	})
}

// init initializes the connection to the database.
// Skipped if already initialized or if the database file does not exist.
func (db *DB) init(ctx context.Context) (err error) {
	// Exit if already initialized.
	if db.db != nil {
		return nil
	}

	// Exit if no database file exists.
	fi, err := os.Stat(db.path)
	if os.IsNotExist(err) {
		return nil

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Inspect the wrapped inner error (%w) to identify the SQLite result code and address that root cause first
  2. Verify the database file opens cleanly outside litestream: `sqlite3 /path/to/db 'PRAGMA integrity_check;'` and that it can enter WAL mode
  3. Upgrade modernc.org/sqlite to the latest release and rebuild litestream to rule out driver regressions
  4. Retry litestream after ensuring no other process holds the database in a conflicting state (e.g. another tool mid-checkpoint)

Example fix

// diagnose the wrapped cause instead of the opaque wrapper
_, err := fc.FileControlPersistWAL("main", 1)
if err != nil {
    return fmt.Errorf("FileControlPersistWAL: %w", err) // read the inner sqlite error
}
Defensive patterns

Strategy: try-catch

Validate before calling

// before init, check the db is healthy and writable:
// sqlite3 /path/to/db 'PRAGMA integrity_check;'
// sqlite3 /path/to/db 'PRAGMA journal_mode=wal;'
if st, err := os.Stat(dbPath); err != nil || st.IsDir() { return err }

Try / catch

if err := db.setPersistWAL(ctx); err != nil {
    var inner string
    fmt.Sscanf(err.Error(), "FileControlPersistWAL: %s", &inner) // inspect wrapped cause
    log.Printf("persist-WAL setup failed: %v", err)
    // retry init on next sync
}

Prevention

When it happens

Trigger: DB.init -> setPersistWAL -> conn.Raw: calling fc.FileControlPersistWAL("main", 1) returns a non-nil error. Typical causes: the driver build lacks file-control support compiled in, SQLite returns SQLITE_NOTFOUND/SQLITE_ERROR for the requested op, or the database is in a state (e.g. already closed, corrupted header) where the file control cannot run.

Common situations: Operating on a database whose file is corrupt or unreadable mid-init; using a custom build of modernc.org/sqlite with file-control stubs; an unusual DSN causing the connection to attach something other than a normal main database; rare driver regressions after upgrading modernc.org/sqlite.

Understand the failure class

Background: Database query failed: Internal Server Error 500s wrapping SQL, Prisma, and connection failures — what to check first — this error's family across 16 libraries.

Related errors


AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06). Data as JSON: /api/errors/256a1c0be21b5d17. Report an issue: GitHub.