phacility/phabricator · error · AphrontDuplicateKeyQueryException

1062

1062

Error message

#%d: %s

What it means

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.

Source

Thrown at src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php:346

      case 2013: // Connection Dropped
        throw new AphrontConnectionLostQueryException($message);
      case 2006: // Gone Away
        $more = pht(
          'This error may occur if your configured MySQL "wait_timeout" or '.
          '"max_allowed_packet" values are too small. This may also indicate '.
          'that something used the MySQL "KILL <process>" command to kill '.
          'the connection running the query.');
        throw new AphrontConnectionLostQueryException("{$message}\n\n{$more}");
      case 1213: // Deadlock
        throw new AphrontDeadlockQueryException($message);
      case 1205: // Lock wait timeout exceeded
        throw new AphrontLockTimeoutQueryException($message);
      case 1062: // Duplicate Key
        // NOTE: In some versions of MySQL we get a key name back here, but
        // older versions just give us a key index ("key 2") so it's not
        // portable to parse the key out of the error and attach it to the
        // exception.
        throw new AphrontDuplicateKeyQueryException($message);
      case 1044: // Access denied to database
      case 1142: // Access denied to table
      case 1143: // Access denied to column
      case 1227: // Access denied (e.g., no SUPER for SHOW SLAVE STATUS).

        // See T13622. Try to help users figure out that this is a GRANT
        // problem.

        $more = pht(
          'This error usually indicates that you need to "GRANT" the '.
          'MySQL user additional permissions. See "GRANT" in the MySQL '.
          'manual for help.');

        throw new AphrontAccessDeniedQueryException("{$message}\n\n{$more}");
      case 1045: // Access denied (auth)
        throw new AphrontInvalidCredentialsQueryException($message);
      case 1146: // No such table
      case 1049: // No such database

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Catch AphrontDuplicateKeyQueryException and treat it as 'already exists': load the existing row with loadOneWhere() and continue instead of failing
  2. 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
  3. Deduplicate existing data before applying a patch that adds a unique index
  4. Only if the upsert semantics are intended, use INSERT ... ON DUPLICATE KEY UPDATE / REPLACE deliberately, not as a band-aid

Example fix

// before: naive create, dies on concurrent insert
$obj = MyDAO::initializeNewObject($name);
$obj->save();

// after: adopt the row the winner created
try {
  $obj->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
  $obj = id(new MyDAO())->loadOneWhere('objectName = %s', $name);
  if (!$obj) {
    throw $ex;
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Advisory pre-check only — the UNIQUE index is the real arbiter under concurrency:
$existing = id(new MyDAO())->loadOneWhere(
  '%C = %s',
  $unique_column,
  $value);
if ($existing) {
  return $existing;
}

Try / catch

try {
  $object->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
  // Lost the race: adopt the winner's row instead of failing.
  $object = id(new MyDAO())->loadOneWhere(
    '%C = %s',
    $unique_column,
    $value);
  if (!$object) {
    throw $ex; // row vanished again — surface it
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/9d1a575d5405184a. Report an issue: GitHub.