{"record":{"id":"9d1a575d5405184a","repo":"phacility/phabricator","slug":"1062","errorCode":"1062","errorMessage":"#%d: %s","messagePattern":"#%d: %s","errorType":"exception","errorClass":"AphrontDuplicateKeyQueryException","httpStatus":null,"severity":"error","filePath":"src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php","lineNumber":346,"sourceCode":"      case 2013: // Connection Dropped\n        throw new AphrontConnectionLostQueryException($message);\n      case 2006: // Gone Away\n        $more = pht(\n          'This error may occur if your configured MySQL \"wait_timeout\" or '.\n          '\"max_allowed_packet\" values are too small. This may also indicate '.\n          'that something used the MySQL \"KILL <process>\" command to kill '.\n          'the connection running the query.');\n        throw new AphrontConnectionLostQueryException(\"{$message}\\n\\n{$more}\");\n      case 1213: // Deadlock\n        throw new AphrontDeadlockQueryException($message);\n      case 1205: // Lock wait timeout exceeded\n        throw new AphrontLockTimeoutQueryException($message);\n      case 1062: // Duplicate Key\n        // NOTE: In some versions of MySQL we get a key name back here, but\n        // older versions just give us a key index (\"key 2\") so it's not\n        // portable to parse the key out of the error and attach it to the\n        // exception.\n        throw new AphrontDuplicateKeyQueryException($message);\n      case 1044: // Access denied to database\n      case 1142: // Access denied to table\n      case 1143: // Access denied to column\n      case 1227: // Access denied (e.g., no SUPER for SHOW SLAVE STATUS).\n\n        // See T13622. Try to help users figure out that this is a GRANT\n        // problem.\n\n        $more = pht(\n          'This error usually indicates that you need to \"GRANT\" the '.\n          'MySQL user additional permissions. See \"GRANT\" in the MySQL '.\n          'manual for help.');\n\n        throw new AphrontAccessDeniedQueryException(\"{$message}\\n\\n{$more}\");\n      case 1045: // Access denied (auth)\n        throw new AphrontInvalidCredentialsQueryException($message);\n      case 1146: // No such table\n      case 1049: // No such database","sourceCodeStart":328,"sourceCodeEnd":364,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php#L328-L364","documentation":"MySQL returned error 1062 (duplicate entry for a UNIQUE or PRIMARY key) while executing a query, and the connection layer maps it to a typed AphrontDuplicateKeyQueryException so callers can distinguish constraint collisions from other failures. The key name is deliberately not parsed out of the message because older MySQL servers only report a key index like 'key 2'. This exception is a plain AphrontQueryException, not AphrontRecoverableQueryException, so blindly retrying the identical INSERT will fail again.","triggerScenarios":"Calling $dao->save()/insert() or a raw INSERT/UPDATE that collides with an existing unique value; two workers concurrently inserting the same PHID/username/token (check-then-insert without a transaction); replaying a data script that assumes a row is absent; a newly added unique index colliding with pre-existing duplicate rows.","commonSituations":"Race conditions where parallel daemons or web requests create the same record; migrations that add a UNIQUE constraint over dirty data; re-running import scripts that are not idempotent; generating identifiers externally and inserting them without deduplication.","solutions":["Catch AphrontDuplicateKeyQueryException and treat it as 'already exists': load the existing row with loadOneWhere() and continue instead of failing","Make the writer idempotent: do the check-then-insert inside a transaction, or rely on the catch as the arbiter rather than a pre-check","Deduplicate existing data before applying a patch that adds a unique index","Only if the upsert semantics are intended, use INSERT ... ON DUPLICATE KEY UPDATE / REPLACE deliberately, not as a band-aid"],"exampleFix":"// before: naive create, dies on concurrent insert\n$obj = MyDAO::initializeNewObject($name);\n$obj->save();\n\n// after: adopt the row the winner created\ntry {\n  $obj->save();\n} catch (AphrontDuplicateKeyQueryException $ex) {\n  $obj = id(new MyDAO())->loadOneWhere('objectName = %s', $name);\n  if (!$obj) {\n    throw $ex;\n  }\n}","handlingStrategy":"try-catch","validationCode":"// Advisory pre-check only — the UNIQUE index is the real arbiter under concurrency:\n$existing = id(new MyDAO())->loadOneWhere(\n  '%C = %s',\n  $unique_column,\n  $value);\nif ($existing) {\n  return $existing;\n}","typeGuard":null,"tryCatchPattern":"try {\n  $object->save();\n} catch (AphrontDuplicateKeyQueryException $ex) {\n  // Lost the race: adopt the winner's row instead of failing.\n  $object = id(new MyDAO())->loadOneWhere(\n    '%C = %s',\n    $unique_column,\n    $value);\n  if (!$object) {\n    throw $ex; // row vanished again — surface it\n  }\n}","preventionTips":["Model create-or-get as INSERT + catch AphrontDuplicateKeyQueryException, never as bare check-then-insert outside a transaction","Keep UNIQUE indexes on every column whose uniqueness the code assumes, so races cannot create duplicates","Deduplicate data before deploying a patch that adds a unique index","Remember this exception is not AphrontRecoverableQueryException — do not blind-retry the same INSERT"],"tags":["mysql","duplicate-key","unique-constraint","lisk","storage"],"backgroundTag":"duplicate-key-violation","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}