{"record":{"id":"5502079db8372f34","repo":"juanfont/headscale","slug":"renaming-forced-tags-to-tags-w","errorCode":null,"errorMessage":"renaming forced_tags to tags: %w","messagePattern":"renaming forced_tags to tags: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/db/db.go","lineNumber":591,"sourceCode":"\t\t\t\t\t\terr := tx.Exec(createSQL).Error\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"creating index: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tRollback: func(db *gorm.DB) error { return nil },\n\t\t\t},\n\t\t\t{\n\t\t\t\t// Rename forced_tags column to tags in nodes table.\n\t\t\t\t// This must run after migration 202505141324 which creates tables with forced_tags.\n\t\t\t\tID: \"202511131445-node-forced-tags-to-tags\",\n\t\t\t\tMigrate: func(tx *gorm.DB) error {\n\t\t\t\t\t// Rename the column from forced_tags to tags\n\t\t\t\t\terr := tx.Migrator().RenameColumn(&types.Node{}, \"forced_tags\", \"tags\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"renaming forced_tags to tags: %w\", err)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tRollback: func(db *gorm.DB) error { return nil },\n\t\t\t},\n\t\t\t{\n\t\t\t\t// Migrate RequestTags from host_info JSON to tags column.\n\t\t\t\t// In 0.27.x, tags from --advertise-tags (ValidTags) were stored only in\n\t\t\t\t// host_info.RequestTags, not in the tags column (formerly forced_tags).\n\t\t\t\t// This migration validates RequestTags against the policy's tagOwners\n\t\t\t\t// and merges validated tags into the tags column.\n\t\t\t\t// Fixes: https://github.com/juanfont/headscale/issues/3006\n\t\t\t\tID: \"202601121700-migrate-hostinfo-request-tags\",\n\t\t\t\tMigrate: func(tx *gorm.DB) error {\n\t\t\t\t\t// 1. Load policy from file or database based on configuration\n\t\t\t\t\tpolicyData, err := PolicyBytes(tx, cfg)\n\t\t\t\t\tif err != nil {","sourceCodeStart":573,"sourceCodeEnd":609,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/db.go#L573-L609","documentation":"Migration '202511131445-node-forced-tags-to-tags' fails when GORM Migrator().RenameColumn cannot rename nodes.forced_tags to nodes.tags. Typical causes: the forced_tags column does not exist (schema already has tags, e.g. from a newer AutoMigrate run or a partially applied upgrade), the DB lacks ALTER rights, or the table is locked. Unlike the AddColumn guards above, RenameColumn here has no HasColumn check and is not idempotent.","triggerScenarios":"Starting a headscale binary whose embedded schema (AutoMigrate-created nodes.tags) raced ahead of the migration history - for example restoring a migrations table from a newer backup onto an older schema, or running headscale versions out of order (downgrade then upgrade). Also ALTER privilege missing or SQLite lock held by another process.","commonSituations":"Rolling back to an old headscale version and then jumping forward again, leaving columns renamed but migrations unrecorded; restoring a database dump where the migrations table and schema disagree; two instances of different versions sharing one DB.","solutions":["Verify the actual schema: \\d nodes (Postgres) or PRAGMA table_info(nodes) (SQLite) - if 'tags' exists and 'forced_tags' does not, the rename is already done; repair the migrations bookkeeping or pre-rename manually so the call succeeds","If both columns exist, copy data: UPDATE nodes SET tags = forced_tags; then drop forced_tags, and restart","Check the wrapped error for permissions/locks (grant ALTER, stop other instances)","Never downgrade headscale across this boundary - restore from a backup taken before the version jump if state is inconsistent"],"exampleFix":"-- before: migrations table expects rename but schema already has tags\nALTER TABLE nodes RENAME COLUMN forced_tags TO tags; -- fails: no such column\n-- after: reconcile schema with history, or complete the rename manually\nUPDATE nodes SET tags = forced_tags WHERE tags IS NULL;\nALTER TABLE nodes DROP COLUMN forced_tags;","handlingStrategy":"type-guard","validationCode":"// Verify column state before upgrading across this boundary\ncols := map[string]bool{}\nrows, _ := db.Query(\"PRAGMA table_info(nodes)\") // or information_schema.columns on Postgres\nfor rows.Next() {\n\tvar cid int; var name, typ string; var notnull, pk int; var dflt any\n\trows.Scan(&cid, &name, &typ, &notnull, &dflt, &pk)\n\tcols[name] = true\n}\nif cols[\"forced_tags\"] && cols[\"tags\"] {\n\tlog.Fatal(\"both forced_tags and tags exist; reconcile manually before upgrading\")\n}\nif !cols[\"forced_tags\"] && !cols[\"tags\"] {\n\tlog.Fatal(\"nodes has neither forced_tags nor tags; schema/history mismatch - restore from backup\")\n}","typeGuard":"// Go helper to assert the rename is possible\nfunc columnExists(db *sql.DB, table, column string) bool {\n\tvar n int\n\tdb.QueryRow(`SELECT count(*) FROM pragma_table_info(?)`, table).Scan(&n) // SQLite\n\treturn n > 0\n}\n// guard: columnExists(db, \"nodes\", \"forced_tags\") must be true before this migration runs","tryCatchPattern":null,"preventionTips":["Never run an older headscale binary against a newer database (downgrade then upgrade causes exactly this)","Keep migrations-table backups in sync with schema backups when dumping/restoring","Check release notes for schema-changing releases before jumping multiple versions"],"tags":["database","migration","rename","gorm","schema-drift"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}