benbjohnson/litestream · error

delete rows: %w

Error message

delete rows: %w

What it means

This error wraps failure of the batched `DELETE FROM <table> WHERE id IN (...)` statement that `deleteFromTable` executes after computing the rows to delete. It means SQLite could not execute the delete — commonly a lock conflict, a disk I/O error, or a query that grew beyond SQLite's variable/SQL-length limits. The shrink run aborts with the underlying driver error wrapped via `%w`.

Source

Thrown at cmd/litestream-test/shrink.go:233

				ORDER BY RANDOM()
				LIMIT %d
			)
		`, table, table, rowsToDelete)
	} else {
		deleteQuery = fmt.Sprintf(`
			DELETE FROM %s
			WHERE rowid IN (
				SELECT rowid FROM %s
				ORDER BY RANDOM()
				LIMIT %d
			)
		`, table, table, rowsToDelete)
	}

	startTime := time.Now()
	result, err := db.Exec(deleteQuery)
	if err != nil {
		return 0, fmt.Errorf("delete rows: %w", err)
	}

	rowsDeleted, _ := result.RowsAffected()
	duration := time.Since(startTime)

	slog.Debug("Deleted rows from table",
		"table", table,
		"rows_deleted", rowsDeleted,
		"duration", duration,
	)

	return rowsDeleted, nil
}

func (c *ShrinkCommand) runCheckpoint(db *sql.DB) error {
	slog.Info("Running checkpoint", "mode", c.CheckpointMode)

	startTime := time.Now()

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Chunk the ID list into smaller batches (e.g. 500 per statement)
  2. Ensure no other process/connection is writing to the DB during shrink
  3. Check wrapped error for 'database is locked' / 'disk I/O error' and address that cause
  4. Ensure the database file and its directory are writable

Example fix

// before
deleteQuery := fmt.Sprintf("DELETE FROM %s WHERE id IN (%s)", table, strings.Join(ids, ","))
_, err := db.Exec(deleteQuery)
// after
for chunk := range slices.Chunk(ids, 500) {
	deleteQuery := fmt.Sprintf("DELETE FROM %s WHERE id IN (%s)", table, strings.Join(chunk, ","))
	if _, err := db.Exec(deleteQuery); err != nil {
		return 0, fmt.Errorf("delete rows: %w", err)
	}
}
Defensive patterns

Strategy: validation

Validate before calling

// pre-check locks and batch size
if len(rowsToDelete) > 500 { return errors.New("delete batch too large; chunk it") }

Try / catch

if _, err := db.Exec(deleteQuery); err != nil {
	var sqliteErr *sqlite.Error
	if errors.As(err, &sqliteErr) && strings.Contains(err.Error(), "locked") {
		// retry after backoff or ensure single writer
	}
	return fmt.Errorf("delete rows: %w", err)
}

Prevention

When it happens

Trigger: Deleting from a table while another connection holds a write transaction; building a DELETE with thousands of IDs exceeding SQLITE_MAX_VARIABLE_NUMBER; table name interpolated with characters producing invalid SQL; disk full during the delete.

Common situations: Shrinking a large production-like table in a test where litestream is also replicating and holds the WAL; very wide delete batches on older SQLite builds with low variable limits; read-only database file.

Related errors


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