juanfont/headscale · critical

renaming table %s to %s_old: %w

Error message

renaming table %s to %s_old: %w

What it means

The SQLite schema-recreation migration failed renaming a core table to its _old variant (ALTER TABLE x RENAME TO x_old). This is the first destructive-ish step of rebuilding all tables. Failure typically means another connection holds the table, a previous failed run left an inconsistent state, or triggers/views depend on the table (legacy_sql_alter semantics).

Source

Thrown at hscontrol/db/db.go:303

					}

					for _, table := range tablesToRename {
						// Check if table exists before renaming
						var exists bool

						err := tx.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?", table).Row().Scan(&exists)
						if err != nil {
							return fmt.Errorf("checking if table %s exists: %w", table, err)
						}

						if exists {
							// Drop old table if it exists from previous failed migration
							_ = tx.Exec("DROP TABLE IF EXISTS " + table + "_old").Error

							// Rename current table to _old
							err := tx.Exec("ALTER TABLE " + table + " RENAME TO " + table + "_old").Error
							if err != nil {
								return fmt.Errorf("renaming table %s to %s_old: %w", table, table, err)
							}
						}
					}

					// Create new tables with correct schema
					tableCreationSQL := []string{
						`CREATE TABLE users(
  id integer PRIMARY KEY AUTOINCREMENT,
  name text,
  display_name text,
  email text,
  provider_identifier text,
  provider text,
  profile_pic_url text,
  created_at datetime,
  updated_at datetime,
  deleted_at datetime
)`,

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Guarantee exclusive access: fully stop the old instance before starting the new binary.
  2. If a previous run crashed, restore from the pre-upgrade backup rather than hand-editing half-renamed tables.
  3. Check the wrapped error for 'database is locked' and identify the holder (lsof/fuser).
  4. Retry startup once the environment is quiet - the DROP IF EXISTS x_old cleanup makes reruns safe.
Defensive patterns

Strategy: fallback

Try / catch

if _, err := db.NewHeadscaleDatabase(cfg); err != nil {
    if strings.Contains(err.Error(), "renaming table") {
        // do not hand-patch half-renamed tables: restore the pre-upgrade backup and retry
    }
}

Prevention

When it happens

Trigger: Concurrent reader on the table during RENAME; a prior aborted migration left x_old existing or half-renamed state; SQLite compiled/running with legacy_alter_table where dependent views block renames.

Common situations: Startup race where the old headscale process is still serving requests; retry after a mid-migration crash.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/44bee7cd3cab2efd. Report an issue: GitHub.