{"record":{"id":"ddd9e12526cc31b1","repo":"juanfont/headscale","slug":"dropping-routes-table-w","errorCode":null,"errorMessage":"dropping routes table: %w","messagePattern":"dropping routes table: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/db/db.go","lineNumber":269,"sourceCode":"\t\t\t\t\t\tlog.Info().Msg(\"skipping schema migration on non-SQLite database\")\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tlog.Info().Msg(\"starting schema recreation with table renaming\")\n\n\t\t\t\t\t// Rename existing tables to _old versions\n\t\t\t\t\ttablesToRename := []string{\"users\", \"pre_auth_keys\", \"api_keys\", \"nodes\", \"policies\"}\n\n\t\t\t\t\t// Check if routes table exists and drop it (should have been migrated already)\n\t\t\t\t\tvar routesExists bool\n\n\t\t\t\t\terr := tx.Raw(\"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='routes'\").Row().Scan(&routesExists)\n\t\t\t\t\tif err == nil && routesExists {\n\t\t\t\t\t\tlog.Info().Msg(\"dropping leftover routes table\")\n\n\t\t\t\t\t\terr := tx.Exec(\"DROP TABLE routes\").Error\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"dropping routes table: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Drop all indexes first to avoid conflicts\n\t\t\t\t\tindexesToDrop := []string{\n\t\t\t\t\t\t\"idx_users_deleted_at\",\n\t\t\t\t\t\t\"idx_provider_identifier\",\n\t\t\t\t\t\t\"idx_name_provider_identifier\",\n\t\t\t\t\t\t\"idx_name_no_provider_identifier\",\n\t\t\t\t\t\t\"idx_api_keys_prefix\",\n\t\t\t\t\t\t\"idx_policies_deleted_at\",\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, index := range indexesToDrop {\n\t\t\t\t\t\t_ = tx.Exec(\"DROP INDEX IF EXISTS \" + index).Error\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, table := range tablesToRename {","sourceCodeStart":251,"sourceCodeEnd":287,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/db.go#L251-L287","documentation":"During the v0.27.0 SQLite-only schema recreation migration, DROP TABLE routes failed. The migration drops a leftover routes table (normally already removed by migration 202502131714) before renaming and rebuilding all core tables. Failure means SQLite rejected the DROP: database locked by another connection, read-only file, or insufficient permissions.","triggerScenarios":"Another process (old headscale instance, CLI, sqlite3 shell) holds the database while startup migration runs; SQLite file or directory not writable (journal creation fails).","commonSituations":"Restarting headscale while the previous instance has not fully exited; database on NFS without proper locking; container running with a read-only mounted volume.","solutions":["Ensure no other process has the SQLite file open: stop old containers/instances and CLI commands.","Verify write permission on both the database file and its directory.","Retry startup - the existence check and DROP are idempotent.","If it persists, open the DB with the sqlite3 shell and DROP TABLE routes manually (data was already migrated), then restart."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if _, err := db.NewHeadscaleDatabase(cfg); err != nil {\n    if strings.Contains(err.Error(), \"dropping routes table\") {\n        // SQLite lock or read-only file: stop other sqlite users, fix perms, restart\n    }\n}","preventionTips":["Stop the old headscale process completely before starting the upgraded one.","Mount the SQLite volume read-write; the migration needs journal + DDL access.","Never open the production DB in an editor/shell while headscale starts."],"tags":["database","migration","sqlite","ddl","locking"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}