{"record":{"id":"c7bf2b3df4204f43","repo":"juanfont/headscale","slug":"validating-schema-w","errorCode":null,"errorMessage":"validating schema: %w","messagePattern":"validating schema: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/db/db.go","lineNumber":1042,"sourceCode":"\t\tsqlConn.SetMaxOpenConns(maxOpenConns)\n\t\tdefer sqlConn.SetMaxIdleConns(1)\n\t\tdefer sqlConn.SetMaxOpenConns(1)\n\n\t\tctx, cancel := context.WithTimeout(context.Background(), contextTimeout)\n\t\tdefer cancel()\n\n\t\topts := squibble.DigestOptions{\n\t\t\tIgnoreTables: []string{\n\t\t\t\t// Litestream tables, these are inserted by\n\t\t\t\t// litestream and not part of our schema\n\t\t\t\t// https://litestream.io/how-it-works\n\t\t\t\t\"_litestream_lock\",\n\t\t\t\t\"_litestream_seq\",\n\t\t\t},\n\t\t}\n\n\t\tif err := squibble.Validate(ctx, sqlConn, dbSchema, &opts); err != nil { //nolint:noinlineerr\n\t\t\treturn nil, fmt.Errorf(\"validating schema: %w\", err)\n\t\t}\n\t}\n\n\tdb := HSDatabase{\n\t\tDB:  dbConn,\n\t\tcfg: cfg,\n\t}\n\n\treturn &db, err\n}\n\nfunc openDB(cfg types.DatabaseConfig) (*gorm.DB, error) {\n\t// TODO(kradalby): Integrate this with zerolog\n\tvar dbLogger logger.Interface\n\tif cfg.Debug {\n\t\tdbLogger = util.NewDBLogWrapper(&log.Logger, cfg.Gorm.SlowThreshold, cfg.Gorm.SkipErrRecordNotFound, cfg.Gorm.ParameterizedQueries)\n\t} else {\n\t\tdbLogger = logger.Default.LogMode(logger.Silent)","sourceCodeStart":1024,"sourceCodeEnd":1060,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/db.go#L1024-L1060","documentation":"After migrations, headscale validates the sqlite schema digest against the expected schema using squbble (with litestream's _litestream_lock/_litestream_seq tables ignored). A mismatch means the actual table/column layout in the file differs from what this headscale version expects — schema drift between code and database.","triggerScenarios":"Schema was modified outside the migration path: AutoMigrate created extra/altered columns in a prior dev build, a binary downgrade left newer columns in place, or the DB file was hand-edited. Litestream-ignored tables are excluded, so only real headscale tables count.","commonSituations":"Running a dev build (which skips version stamping and may have run AutoMigrate) and then switching back to a release binary; restoring a replica restored at a different schema version; editing the sqlite file with an external tool.","solutions":["Diff the reported digest details against the expected schema to find the offending table/column.","If a dev build mutated the schema, dump data, recreate the database with the release binary, and re-import nodes.","If this follows a downgrade, upgrade back to the binary version whose migrations produced the current schema.","Never let dev builds write to the production database — dev builds skip setDatabaseVersion precisely to avoid this class of drift."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Pre-flight before pointing a release binary at a DB a dev build touched:\n// compare table digests or simply never share DBs between build types.\n// Quick structural check:\n//   sqlite3 headscale.db '.schema nodes' | head -40\n// and diff against the release binary's expected nodes schema.","typeGuard":null,"tryCatchPattern":"// Not catchable meaningfully — squbble validation is a hard gate.\n// On failure, restore a schema-consistent database (backup/replica) or\n// recreate and re-import. Do not bypass by deleting tables.","preventionTips":["Never run dev builds against production databases.","Always migrate upward only; downgrades leave schema residue that fails digest validation.","Restore backups taken with the same binary lineage."],"tags":["database","sqlite","schema-validation","squibble"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}