{"record":{"id":"6471c3139b5ad266","repo":"vitessio/vitess","slug":"v-has-an-extra-v-v","errorCode":null,"errorMessage":"%v has an extra %v %v","messagePattern":"(.+?) has an extra (.+?) (.+?)","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"go/vt/mysqlctl/tmutils/permissions.go","lineNumber":178,"sourceCode":"\treturn result\n}\n\n// PermissionsString pretty-prints Permissions\nfunc PermissionsString(permissions *tabletmanagerdatapb.Permissions) string {\n\treturn printPermissions(\"User\", userPermissionList(permissions.UserPermissions)) +\n\t\tprintPermissions(\"Db\", dbPermissionList(permissions.DbPermissions))\n}\n\nfunc diffPermissions(name, leftName string, left permissionList, rightName string, right permissionList, er concurrency.ErrorRecorder) {\n\tleftIndex := 0\n\trightIndex := 0\n\tfor leftIndex < left.Len() && rightIndex < right.Len() {\n\t\tlpk, lval := left.Get(leftIndex)\n\t\trpk, rval := right.Get(rightIndex)\n\n\t\t// extra value on the left side\n\t\tif lpk < rpk {\n\t\t\ter.RecordError(fmt.Errorf(\"%v has an extra %v %v\", leftName, name, lpk))\n\t\t\tleftIndex++\n\t\t\tcontinue\n\t\t}\n\n\t\t// extra value on the right side\n\t\tif lpk > rpk {\n\t\t\ter.RecordError(fmt.Errorf(\"%v has an extra %v %v\", rightName, name, rpk))\n\t\t\trightIndex++\n\t\t\tcontinue\n\t\t}\n\n\t\t// same name, let's see content\n\t\tif lval != rval {\n\t\t\ter.RecordError(fmt.Errorf(\"permissions differ on %v %v:\\n%s: %v\\n differs from:\\n%s: %v\", name, lpk, leftName, lval, rightName, rval))\n\t\t}\n\t\tleftIndex++\n\t\trightIndex++\n\t}","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/vitessio/vitess/blob/01a25a7d176f94613b8d59d799f438380a8760e4/go/vt/mysqlctl/tmutils/permissions.go#L160-L196","documentation":"diffPermissions compares two Permission sets (left vs right) by sorted primary key. When the left side contains a permission entry (a user or a table in a permission record) that the right side lacks, this error records a drift on the left (leftName).","triggerScenarios":"Running DiffPermissions (tablet-manager permission sync/checks) when the left source (e.g. local mysqld) has an extra user/DB/table permission entry absent from the right (e.g. expected topo copy).","commonSituations":"A GRANT was applied directly on one tablet but not replicated to the other; permissions drift after manual dbadmin operations; comparing freshly-restored tablets against topo records.","solutions":["Diff the permission lists on both sides and apply the missing/extra GRANT/REVOKE so both match","Re-sync permissions from the source of truth (topo or target tablet)","Check for manual GRANT statements run on only one MySQL instance","Re-run DiffPermissions to confirm drift is resolved"],"exampleFix":"// before: manual GRANT on left tablet only\n// after: apply the same GRANT on both, or REVOKE on left\n// REVOKE ... ON db.* FROM 'user'@'host';","handlingStrategy":"validation","validationCode":"// compare permission key sets before invoking diff\nleftKeys, rightKeys := keySet(left), keySet(right)\nfor k := range leftKeys {\n    if !rightKeys[k] {\n        log.Warnf(\"left has extra permission %v; will be flagged\", k)\n    }\n}","typeGuard":null,"tryCatchPattern":"er := tmutils.DiffPermissions(\"local\", permsLocal, \"topo\", permsTopo)\nif er.HasErrors() {\n    for _, err := range er.Errors {\n        log.Warn(\"permission drift detected\", slog.Any(\"error\", err))\n    }\n}","preventionTips":["Apply GRANT/REVOKE changes to all tablets uniformly","Use the topo as the source of truth for permissions","Audit manual dbadmin GRANTs after incidents"],"tags":["permissions","drift","mysqlctl"],"backgroundTag":"permission-drift","analyzedSha":"01a25a7d176f94613b8d59d799f438380a8760e4","analyzedAt":"2026-09-01T17:28:30.605Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}