{"record":{"id":"6b4d3ca3cb8a01ea","repo":"v2rayA/v2rayA","slug":"subscription-count-mismatch-boltdb-d-sqlite-d","errorCode":null,"errorMessage":"subscription count mismatch: BoltDB=%d, SQLite=%d","messagePattern":"subscription count mismatch: BoltDB=(.+?), SQLite=(.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"service/db/migrate.go","lineNumber":556,"sourceCode":"\t\t\tserversData := touchBkt.Get([]byte(\"servers\"))\n\t\t\tif serversData != nil {\n\t\t\t\tboltServerCount := len(gjson.ParseBytes(serversData).Array())\n\t\t\t\tvar sqlServerCount int\n\t\t\t\tsqldb.QueryRow(\"SELECT COUNT(*) FROM servers WHERE type = 'server'\").Scan(&sqlServerCount)\n\t\t\t\tif boltServerCount != sqlServerCount {\n\t\t\t\t\treturn fmt.Errorf(\"server count mismatch: BoltDB=%d, SQLite=%d\", boltServerCount, sqlServerCount)\n\t\t\t\t}\n\t\t\t\tlog.Info(\"Servers: %d entries verified\", boltServerCount)\n\t\t\t}\n\n\t\t\t// Verify subscriptions\n\t\t\tsubsData := touchBkt.Get([]byte(\"subscriptions\"))\n\t\t\tif subsData != nil {\n\t\t\t\tboltSubCount := len(gjson.ParseBytes(subsData).Array())\n\t\t\t\tvar sqlSubCount int\n\t\t\t\tsqldb.QueryRow(\"SELECT COUNT(*) FROM subscriptions\").Scan(&sqlSubCount)\n\t\t\t\tif boltSubCount != sqlSubCount {\n\t\t\t\t\treturn fmt.Errorf(\"subscription count mismatch: BoltDB=%d, SQLite=%d\", boltSubCount, sqlSubCount)\n\t\t\t\t}\n\t\t\t\tlog.Info(\"Subscriptions: %d entries verified\", boltSubCount)\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Warn(\"Migration verification passed!\")\n\treturn nil\n}\n","sourceCodeStart":538,"sourceCodeEnd":572,"githubUrl":"https://github.com/v2rayA/v2rayA/blob/71e5442fc548c05680ee55eae943e6c0afe9ac51/service/db/migrate.go#L538-L572","documentation":"This error is thrown during the one-time BoltDB→SQLite migration when a post-migration verification step compares the number of subscription entries serialized inside the BoltDB 'subscriptions' JSON blob against the row count of the new SQLite 'subscriptions' table. A mismatch means some (or all) subscription records were not carried over, or extra rows exist, so the migration aborts rather than silently lose data.","triggerScenarios":"Running the migration on a database where the BoltDB subscriptions JSON array contains a different number of elements than the SQLite subscriptions table has rows — e.g. a previous partial/failed migration left rows inserted, subscription rows were manually deleted/added after migration, or the importer skipped subscriptions that failed to parse.","commonSituations":"Re-running a migration after an earlier interrupted run; v2rayA upgrade where an old BoltDB file is imported into a SQLite DB that already has subscription rows; corrupt or hand-edited subscription JSON in BoltDB; DB file copied/restored from backups of mixed versions.","solutions":["Inspect the SQLite table: SELECT COUNT(*) FROM subscriptions; and compare with the count of elements in the BoltDB 'subscriptions' JSON blob to see which side is stale.","If a previous partial migration duplicated rows, start from a clean SQLite DB (delete/rename the SQLite database file) and re-run the migration from the original BoltDB file.","If BoltDB is the stale side (extra/obsolete subscriptions), prune the BoltDB subscriptions blob or accept the current SQLite state and skip re-import by not pointing at the old BoltDB file.","Check the migration/importer logs for per-subscription insert failures; fix the malformed subscription entries that were skipped, then re-migrate.","Back up both database files before any of the above."],"exampleFix":"// before: verifying against an already-populated SQLite DB\nerr := migrate.FromBoltDB(boltPath)\n// after: re-run migration against a fresh SQLite DB\nos.Rename(\"v2raya.db\", \"v2raya.db.bak\") // move old SQLite DB aside\nerr := migrate.FromBoltDB(boltPath) // clean import, counts now match","handlingStrategy":"validation","validationCode":"// Before migrating, snapshot and compare expected counts\nvar sqlCount int\nif err := sqldb.QueryRow(\"SELECT COUNT(*) FROM subscriptions\").Scan(&sqlCount); err != nil {\n    return fmt.Errorf(\"count subscriptions: %w\", err)\n}\nboltCount := len(gjson.GetBytes(subsBlob, \"subscriptions\").Array())\nif boltCount != sqlCount {\n    return fmt.Errorf(\"pre-check mismatch: BoltDB=%d, SQLite=%d — restore a clean SQLite DB before migrating\", boltCount, sqlCount)\n}","typeGuard":"func isCountMismatch(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"count mismatch\")\n}","tryCatchPattern":"// Go: detect the mismatch sentinel and offer recovery instead of crashing\nif err := migrate.FromBoltDB(boltPath); err != nil {\n    if isCountMismatch(err) {\n        log.Warn(\"migration verification failed: %v — keeping backup at %s\", err, backupPath)\n        os.Rename(backupPath, dbPath) // roll back to pre-migration DB\n    }\n    return err\n}","preventionTips":["Always back up the BoltDB and SQLite files before running a migration.","Never re-run migration against a SQLite DB that already contains data; start from an empty database.","Run only one v2rayA instance per config directory during migration.","Log per-subscription insert failures so skipped rows are caught before the count check.","Automate the BoltDB-vs-SQLite count comparison in CI for migration code changes."],"tags":["database","migration","sqlite","boltdb","data-loss"],"backgroundTag":"migration-count-mismatch","analyzedSha":"71e5442fc548c05680ee55eae943e6c0afe9ac51","analyzedAt":"2026-09-05T20:04:37.459Z","contentChangedAt":"2026-09-05T20:04:37.459Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}