{"record":{"id":"44bee7cd3cab2efd","repo":"juanfont/headscale","slug":"renaming-table-s-to-s-old-w","errorCode":null,"errorMessage":"renaming table %s to %s_old: %w","messagePattern":"renaming table (.+?) to (.+?)_old: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/db/db.go","lineNumber":303,"sourceCode":"\t\t\t\t\t}\n\n\t\t\t\t\tfor _, table := range tablesToRename {\n\t\t\t\t\t\t// Check if table exists before renaming\n\t\t\t\t\t\tvar exists bool\n\n\t\t\t\t\t\terr := tx.Raw(\"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?\", table).Row().Scan(&exists)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"checking if table %s exists: %w\", table, err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif exists {\n\t\t\t\t\t\t\t// Drop old table if it exists from previous failed migration\n\t\t\t\t\t\t\t_ = tx.Exec(\"DROP TABLE IF EXISTS \" + table + \"_old\").Error\n\n\t\t\t\t\t\t\t// Rename current table to _old\n\t\t\t\t\t\t\terr := tx.Exec(\"ALTER TABLE \" + table + \" RENAME TO \" + table + \"_old\").Error\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn fmt.Errorf(\"renaming table %s to %s_old: %w\", table, table, err)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Create new tables with correct schema\n\t\t\t\t\ttableCreationSQL := []string{\n\t\t\t\t\t\t`CREATE TABLE users(\n  id integer PRIMARY KEY AUTOINCREMENT,\n  name text,\n  display_name text,\n  email text,\n  provider_identifier text,\n  provider text,\n  profile_pic_url text,\n  created_at datetime,\n  updated_at datetime,\n  deleted_at datetime\n)`,","sourceCodeStart":285,"sourceCodeEnd":321,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/db.go#L285-L321","documentation":"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).","triggerScenarios":"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.","commonSituations":"Startup race where the old headscale process is still serving requests; retry after a mid-migration crash.","solutions":["Guarantee exclusive access: fully stop the old instance before starting the new binary.","If a previous run crashed, restore from the pre-upgrade backup rather than hand-editing half-renamed tables.","Check the wrapped error for 'database is locked' and identify the holder (lsof/fuser).","Retry startup once the environment is quiet - the DROP IF EXISTS x_old cleanup makes reruns safe."],"exampleFix":null,"handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"if _, err := db.NewHeadscaleDatabase(cfg); err != nil {\n    if strings.Contains(err.Error(), \"renaming table\") {\n        // do not hand-patch half-renamed tables: restore the pre-upgrade backup and retry\n    }\n}","preventionTips":["Guarantee a fresh file-level backup before the v0.27.0 upgrade (this migration rebuilds all tables).","Ensure single-writer access; systemd 'ExecStartPre' checking no other instance holds the DB.","Never kill headscale mid-migration; let it finish or roll back."],"tags":["database","sqlite","migration","ddl","locking"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}