public function setKeys(array $keys, $ttl = null)
 {
     if (PhabricatorEnv::isReadOnly()) {
         return;
     }
     if ($keys) {
         $map = $this->digestKeys(array_keys($keys));
         $conn_w = $this->establishConnection('w');
         $sql = array();
         foreach ($map as $key => $hash) {
             $value = $keys[$key];
             list($format, $storage_value) = $this->willWriteValue($key, $value);
             $sql[] = qsprintf($conn_w, '(%s, %s, %s, %B, %d, %nd)', $hash, $key, $format, $storage_value, time(), $ttl ? time() + $ttl : null);
         }
         $guard = AphrontWriteGuard::beginScopedUnguardedWrites();
         foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
             queryfx($conn_w, 'INSERT INTO %T
           (cacheKeyHash, cacheKey, cacheFormat, cacheData,
             cacheCreated, cacheExpires) VALUES %Q
           ON DUPLICATE KEY UPDATE
             cacheKey = VALUES(cacheKey),
             cacheFormat = VALUES(cacheFormat),
             cacheData = VALUES(cacheData),
             cacheCreated = VALUES(cacheCreated),
             cacheExpires = VALUES(cacheExpires)', $this->getTableName(), $chunk);
         }
         unset($guard);
     }
     return $this;
 }
예제 #2
0
 public static function writeCaches(array $values)
 {
     if (PhabricatorEnv::isReadOnly()) {
         return;
     }
     if (!$values) {
         return;
     }
     $table = new self();
     $conn_w = $table->establishConnection('w');
     $sql = array();
     foreach ($values as $value) {
         $key = $value['key'];
         $sql[] = qsprintf($conn_w, '(%s, %s, %s, %s, %s)', $value['userPHID'], PhabricatorHash::digestForIndex($key), $key, $value['value'], $value['type']->getUserCacheType());
     }
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
     foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
         queryfx($conn_w, 'INSERT INTO %T (userPHID, cacheIndex, cacheKey, cacheData, cacheType)
       VALUES %Q
       ON DUPLICATE KEY UPDATE
         cacheData = VALUES(cacheData),
         cacheType = VALUES(cacheType)', $table->getTableName(), $chunk);
     }
     unset($unguarded);
 }
 protected function execute(ConduitAPIRequest $request)
 {
     $viewer = $request->getUser();
     $repository_phid = $request->getValue('repositoryPHID');
     $repository = id(new PhabricatorRepositoryQuery())->setViewer($viewer)->withPHIDs(array($repository_phid))->executeOne();
     if (!$repository) {
         throw new Exception(pht('No repository exists with PHID "%s".', $repository_phid));
     }
     $commit_name = $request->getValue('commit');
     $commit = id(new DiffusionCommitQuery())->setViewer($viewer)->withRepository($repository)->withIdentifiers(array($commit_name))->executeOne();
     if (!$commit) {
         throw new Exception(pht('No commit exists with identifier "%s".', $commit_name));
     }
     $branch = PhabricatorRepositoryBranch::loadOrCreateBranch($repository->getID(), $request->getValue('branch'));
     $coverage = $request->getValue('coverage');
     $path_map = id(new DiffusionPathIDQuery(array_keys($coverage)))->loadPathIDs();
     $conn = $repository->establishConnection('w');
     $sql = array();
     foreach ($coverage as $path => $coverage_info) {
         $sql[] = qsprintf($conn, '(%d, %d, %d, %s)', $branch->getID(), $path_map[$path], $commit->getID(), $coverage_info);
     }
     $table_name = 'repository_coverage';
     $conn->openTransaction();
     queryfx($conn, 'DELETE FROM %T WHERE branchID = %d', $table_name, $branch->getID());
     foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
         queryfx($conn, 'INSERT INTO %T (branchID, pathID, commitID, coverage) VALUES %Q', $table_name, $chunk);
     }
     $conn->saveTransaction();
 }
예제 #4
0
 public function testSQLChunking()
 {
     $fragments = array('a', 'a', 'b', 'b', 'ccc', 'dd', 'e');
     $this->assertEqual(array('aa', 'bb', 'ccc', 'dd', 'e'), PhabricatorLiskDAO::chunkSQL($fragments, '', 2));
     $fragments = array('a', 'a', 'a', 'XX', 'a', 'a', 'a', 'a');
     $this->assertEqual(array('a, a, a', 'XX, a, a', 'a, a'), PhabricatorLiskDAO::chunkSQL($fragments, ', ', 8));
     $fragments = array('xxxxxxxxxx', 'yyyyyyyyyy', 'a', 'b', 'c', 'zzzzzzzzzz');
     $this->assertEqual(array('xxxxxxxxxx', 'yyyyyyyyyy', 'a, b, c', 'zzzzzzzzzz'), PhabricatorLiskDAO::chunkSQL($fragments, ', ', 8));
 }
예제 #5
0
 protected function deleteDocumentsByHash(array $hashes)
 {
     $atom_table = new DivinerLiveAtom();
     $symbol_table = new DivinerLiveSymbol();
     $conn_w = $symbol_table->establishConnection('w');
     $strings = array();
     foreach ($hashes as $hash) {
         $strings[] = qsprintf($conn_w, '%s', $hash);
     }
     foreach (PhabricatorLiskDAO::chunkSQL($strings, ', ') as $chunk) {
         queryfx($conn_w, 'UPDATE %T SET graphHash = NULL, nodeHash = NULL
       WHERE graphHash IN (%Q)', $symbol_table->getTableName(), $chunk);
     }
     queryfx($conn_w, 'DELETE a FROM %T a LEFT JOIN %T s
     ON a.symbolPHID = s.phid
     WHERE s.graphHash IS NULL', $atom_table->getTableName(), $symbol_table->getTableName());
 }
 protected function execute(ConduitAPIRequest $request)
 {
     $viewer = $request->getUser();
     $repository_phid = $request->getValue('repositoryPHID');
     $repository = id(new PhabricatorRepositoryQuery())->setViewer($viewer)->withPHIDs(array($repository_phid))->executeOne();
     if (!$repository) {
         throw new Exception(pht('No repository exists with PHID "%s".', $repository_phid));
     }
     $commit_name = $request->getValue('commit');
     $commit = id(new DiffusionCommitQuery())->setViewer($viewer)->withRepository($repository)->withIdentifiers(array($commit_name))->executeOne();
     if (!$commit) {
         throw new Exception(pht('No commit exists with identifier "%s".', $commit_name));
     }
     $branch = PhabricatorRepositoryBranch::loadOrCreateBranch($repository->getID(), $request->getValue('branch'));
     $coverage = $request->getValue('coverage');
     $path_map = id(new DiffusionPathIDQuery(array_keys($coverage)))->loadPathIDs();
     $conn = $repository->establishConnection('w');
     $sql = array();
     foreach ($coverage as $path => $coverage_info) {
         $sql[] = qsprintf($conn, '(%d, %d, %d, %s)', $branch->getID(), $path_map[$path], $commit->getID(), $coverage_info);
     }
     $table_name = 'repository_coverage';
     $conn->openTransaction();
     $mode = $request->getValue('mode');
     switch ($mode) {
         case '':
         case 'overwrite':
             // sets the coverage for the whole branch, deleting all previous
             // coverage information
             queryfx($conn, 'DELETE FROM %T WHERE branchID = %d', $table_name, $branch->getID());
             break;
         case 'update':
             // sets the coverage for the provided files on the specified commit
             break;
         default:
             $conn->killTransaction();
             throw new Exception(pht('Invalid mode "%s".', $mode));
     }
     foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
         queryfx($conn, 'INSERT INTO %T (branchID, pathID, commitID, coverage) VALUES %Q' . ' ON DUPLICATE KEY UPDATE coverage=VALUES(coverage)', $table_name, $chunk);
     }
     $conn->saveTransaction();
 }
 private function writeCommitChanges(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, array $changes)
 {
     $repository_id = (int) $repository->getID();
     $commit_id = (int) $commit->getID();
     // NOTE: This SQL is being built manually instead of with qsprintf()
     // because some SVN changes affect an enormous number of paths (millions)
     // and this showed up as significantly slow on a profile at some point.
     $changes_sql = array();
     foreach ($changes as $change) {
         $values = array($repository_id, (int) $change->getPathID(), $commit_id, nonempty((int) $change->getTargetPathID(), 'null'), nonempty((int) $change->getTargetCommitID(), 'null'), (int) $change->getChangeType(), (int) $change->getFileType(), (int) $change->getIsDirect(), (int) $change->getCommitSequence());
         $changes_sql[] = '(' . implode(', ', $values) . ')';
     }
     $conn_w = $repository->establishConnection('w');
     queryfx($conn_w, 'DELETE FROM %T WHERE commitID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit_id);
     foreach (PhabricatorLiskDAO::chunkSQL($changes_sql) as $chunk) {
         queryfx($conn_w, 'INSERT INTO %T
       (repositoryID, pathID, commitID, targetPathID, targetCommitID,
         changeType, fileType, isDirect, commitSequence)
       VALUES %Q', PhabricatorRepository::TABLE_PATHCHANGE, $chunk);
     }
 }
<?php

$diff_table = new DifferentialDiff();
$conn_w = $diff_table->establishConnection('w');
$size = 1000;
$row_iter = id(new LiskMigrationIterator($diff_table))->setPageSize($size);
$chunk_iter = new PhutilChunkedIterator($row_iter, $size);
foreach ($chunk_iter as $chunk) {
    $sql = array();
    foreach ($chunk as $diff) {
        $id = $diff->getID();
        echo pht('Migrating diff ID %d...', $id) . "\n";
        $phid = $diff->getPHID();
        if (strlen($phid)) {
            continue;
        }
        $type_diff = DifferentialDiffPHIDType::TYPECONST;
        $new_phid = PhabricatorPHID::generateNewPHID($type_diff);
        $sql[] = qsprintf($conn_w, '(%d, %s)', $id, $new_phid);
    }
    if (!$sql) {
        continue;
    }
    foreach (PhabricatorLiskDAO::chunkSQL($sql, ', ') as $sql_chunk) {
        queryfx($conn_w, 'INSERT IGNORE INTO %T (id, phid) VALUES %Q
        ON DUPLICATE KEY UPDATE phid = VALUES(phid)', $diff_table->getTableName(), $sql_chunk);
    }
}
echo pht('Done.') . "\n";
예제 #9
0
 public function updateDatasourceTokens()
 {
     $table = self::TABLE_DATASOURCE_TOKEN;
     $conn_w = $this->establishConnection('w');
     $id = $this->getID();
     $slugs = queryfx_all($conn_w, 'SELECT * FROM %T WHERE projectPHID = %s', id(new PhabricatorProjectSlug())->getTableName(), $this->getPHID());
     $all_strings = ipull($slugs, 'slug');
     $all_strings[] = $this->getName();
     $all_strings = implode(' ', $all_strings);
     $tokens = PhabricatorTypeaheadDatasource::tokenizeString($all_strings);
     $sql = array();
     foreach ($tokens as $token) {
         $sql[] = qsprintf($conn_w, '(%d, %s)', $id, $token);
     }
     $this->openTransaction();
     queryfx($conn_w, 'DELETE FROM %T WHERE projectID = %d', $table, $id);
     foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
         queryfx($conn_w, 'INSERT INTO %T (projectID, token) VALUES %Q', $table, $chunk);
     }
     $this->saveTransaction();
 }
예제 #10
0
<?php

// Was PhabricatorEdgeConfig::TYPE_COLUMN_HAS_OBJECT
$type_has_object = 44;
$column = new PhabricatorProjectColumn();
$conn_w = $column->establishConnection('w');
$rows = queryfx_all($conn_w, 'SELECT src, dst FROM %T WHERE type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, $type_has_object);
$cols = array();
foreach ($rows as $row) {
    $cols[$row['src']][] = $row['dst'];
}
$sql = array();
foreach ($cols as $col_phid => $obj_phids) {
    echo pht("Migrating column '%s'...", $col_phid) . "\n";
    $column = id(new PhabricatorProjectColumn())->loadOneWhere('phid = %s', $col_phid);
    if (!$column) {
        echo pht("Column '%s' does not exist.", $col_phid) . "\n";
        continue;
    }
    $sequence = 0;
    foreach ($obj_phids as $obj_phid) {
        $sql[] = qsprintf($conn_w, '(%s, %s, %s, %d)', $column->getProjectPHID(), $column->getPHID(), $obj_phid, $sequence++);
    }
}
echo pht('Inserting rows...') . "\n";
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
    queryfx($conn_w, 'INSERT INTO %T (boardPHID, columnPHID, objectPHID, sequence)
      VALUES %Q', id(new PhabricatorProjectColumnPosition())->getTableName(), $chunk);
}
echo pht('Done.') . "\n";
 private function sendNotifications()
 {
     $cursor = $this->getCursor();
     $window_min = $cursor - phutil_units('16 hours in seconds');
     $window_max = $cursor + phutil_units('16 hours in seconds');
     $viewer = PhabricatorUser::getOmnipotentUser();
     $events = id(new PhabricatorCalendarEventQuery())->setViewer($viewer)->withDateRange($window_min, $window_max)->withIsCancelled(false)->withIsImported(false)->setGenerateGhosts(true)->execute();
     if (!$events) {
         // No events are starting soon in any timezone, so there is nothing
         // left to be done.
         return;
     }
     $attendee_map = array();
     foreach ($events as $key => $event) {
         $notifiable_phids = array();
         foreach ($event->getInvitees() as $invitee) {
             if (!$invitee->isAttending()) {
                 continue;
             }
             $notifiable_phids[] = $invitee->getInviteePHID();
         }
         if (!$notifiable_phids) {
             unset($events[$key]);
         }
         $attendee_map[$key] = array_fuse($notifiable_phids);
     }
     if (!$attendee_map) {
         // None of the events have any notifiable attendees, so there is no
         // one to notify of anything.
         return;
     }
     $all_attendees = array();
     foreach ($attendee_map as $key => $attendee_phids) {
         foreach ($attendee_phids as $attendee_phid) {
             $all_attendees[$attendee_phid] = $attendee_phid;
         }
     }
     $user_map = id(new PhabricatorPeopleQuery())->setViewer($viewer)->withPHIDs($all_attendees)->withIsDisabled(false)->needUserSettings(true)->execute();
     $user_map = mpull($user_map, null, 'getPHID');
     if (!$user_map) {
         // None of the attendees are valid users: they're all imported users
         // or projects or invalid or some other kind of unnotifiable entity.
         return;
     }
     $all_event_phids = array();
     foreach ($events as $key => $event) {
         foreach ($event->getNotificationPHIDs() as $phid) {
             $all_event_phids[$phid] = $phid;
         }
     }
     $table = new PhabricatorCalendarNotification();
     $conn = $table->establishConnection('w');
     $rows = queryfx_all($conn, 'SELECT * FROM %T WHERE eventPHID IN (%Ls) AND targetPHID IN (%Ls)', $table->getTableName(), $all_event_phids, $all_attendees);
     $sent_map = array();
     foreach ($rows as $row) {
         $event_phid = $row['eventPHID'];
         $target_phid = $row['targetPHID'];
         $initial_epoch = $row['utcInitialEpoch'];
         $sent_map[$event_phid][$target_phid][$initial_epoch] = $row;
     }
     $now = PhabricatorTime::getNow();
     $notify_min = $now;
     $notify_max = $now + $this->getNotifyWindow();
     $notify_map = array();
     foreach ($events as $key => $event) {
         $initial_epoch = $event->getUTCInitialEpoch();
         $event_phids = $event->getNotificationPHIDs();
         // Select attendees who actually exist, and who we have not sent any
         // notifications to yet.
         $attendee_phids = $attendee_map[$key];
         $users = array_select_keys($user_map, $attendee_phids);
         foreach ($users as $user_phid => $user) {
             foreach ($event_phids as $event_phid) {
                 if (isset($sent_map[$event_phid][$user_phid][$initial_epoch])) {
                     unset($users[$user_phid]);
                     continue 2;
                 }
             }
         }
         if (!$users) {
             continue;
         }
         // Discard attendees for whom the event start time isn't soon. Events
         // may start at different times for different users, so we need to
         // check every user's start time.
         foreach ($users as $user_phid => $user) {
             $user_datetime = $event->newStartDateTime()->setViewerTimezone($user->getTimezoneIdentifier());
             $user_epoch = $user_datetime->getEpoch();
             if ($user_epoch < $notify_min || $user_epoch > $notify_max) {
                 unset($users[$user_phid]);
                 continue;
             }
             $view = id(new PhabricatorCalendarEventNotificationView())->setViewer($user)->setEvent($event)->setDateTime($user_datetime)->setEpoch($user_epoch);
             $notify_map[$user_phid][] = $view;
         }
     }
     $mail_list = array();
     $mark_list = array();
     $now = PhabricatorTime::getNow();
     foreach ($notify_map as $user_phid => $events) {
         $user = $user_map[$user_phid];
         $locale = PhabricatorEnv::beginScopedLocale($user->getTranslation());
         $caught = null;
         try {
             $mail_list[] = $this->newMailMessage($user, $events);
         } catch (Exception $ex) {
             $caught = $ex;
         }
         unset($locale);
         if ($caught) {
             throw $ex;
         }
         foreach ($events as $view) {
             $event = $view->getEvent();
             foreach ($event->getNotificationPHIDs() as $phid) {
                 $mark_list[] = qsprintf($conn, '(%s, %s, %d, %d)', $phid, $user_phid, $event->getUTCInitialEpoch(), $now);
             }
         }
     }
     // Mark all the notifications we're about to send as delivered so we
     // do not double-notify.
     foreach (PhabricatorLiskDAO::chunkSQL($mark_list) as $chunk) {
         queryfx($conn, 'INSERT IGNORE INTO %T
       (eventPHID, targetPHID, utcInitialEpoch, didNotifyEpoch)
       VALUES %Q', $table->getTableName(), $chunk);
     }
     foreach ($mail_list as $mail) {
         $mail->saveAndSend();
     }
 }
 /**
  * Publish field indexes into index tables, so ApplicationSearch can search
  * them.
  *
  * @return void
  */
 public function rebuildIndexes(PhabricatorCustomFieldInterface $object)
 {
     $indexes = array();
     $index_keys = array();
     $phid = $object->getPHID();
     $role = PhabricatorCustomField::ROLE_APPLICATIONSEARCH;
     foreach ($this->fields as $field) {
         if (!$field->shouldEnableForRole($role)) {
             continue;
         }
         $index_keys[$field->getFieldIndex()] = true;
         foreach ($field->buildFieldIndexes() as $index) {
             $index->setObjectPHID($phid);
             $indexes[$index->getTableName()][] = $index;
         }
     }
     if (!$indexes) {
         return;
     }
     $any_index = head(head($indexes));
     $conn_w = $any_index->establishConnection('w');
     foreach ($indexes as $table => $index_list) {
         $sql = array();
         foreach ($index_list as $index) {
             $sql[] = $index->formatForInsert($conn_w);
         }
         $indexes[$table] = $sql;
     }
     $any_index->openTransaction();
     foreach ($indexes as $table => $sql_list) {
         queryfx($conn_w, 'DELETE FROM %T WHERE objectPHID = %s AND indexKey IN (%Ls)', $table, $phid, array_keys($index_keys));
         if (!$sql_list) {
             continue;
         }
         foreach (PhabricatorLiskDAO::chunkSQL($sql_list) as $chunk) {
             queryfx($conn_w, 'INSERT INTO %T (objectPHID, indexKey, indexValue) VALUES %Q', $table, $chunk);
         }
     }
     $any_index->saveTransaction();
 }
 private function rebuildRepository(PhabricatorRepository $repo)
 {
     $console = PhutilConsole::getConsole();
     $console->writeOut("%s\n", pht('Rebuilding "%s"...', $repo->getMonogram()));
     $refs = id(new PhabricatorRepositoryRefCursorQuery())->setViewer($this->getViewer())->withRefTypes(array(PhabricatorRepositoryRefCursor::TYPE_BRANCH))->withRepositoryPHIDs(array($repo->getPHID()))->execute();
     $graph = array();
     foreach ($refs as $ref) {
         if (!$repo->shouldTrackBranch($ref->getRefName())) {
             continue;
         }
         $console->writeOut("%s\n", pht('Rebuilding branch "%s"...', $ref->getRefName()));
         $commit = $ref->getCommitIdentifier();
         if ($repo->isGit()) {
             $stream = new PhabricatorGitGraphStream($repo, $commit);
         } else {
             $stream = new PhabricatorMercurialGraphStream($repo, $commit);
         }
         $discover = array($commit);
         while ($discover) {
             $target = array_pop($discover);
             if (isset($graph[$target])) {
                 continue;
             }
             $graph[$target] = $stream->getParents($target);
             foreach ($graph[$target] as $parent) {
                 $discover[] = $parent;
             }
         }
     }
     $console->writeOut("%s\n", pht('Found %s total commit(s); updating...', phutil_count($graph)));
     $commit_table = id(new PhabricatorRepositoryCommit());
     $commit_table_name = $commit_table->getTableName();
     $conn_w = $commit_table->establishConnection('w');
     $bar = id(new PhutilConsoleProgressBar())->setTotal(count($graph));
     $need = array();
     foreach ($graph as $child => $parents) {
         foreach ($parents as $parent) {
             $need[$parent] = $parent;
         }
         $need[$child] = $child;
     }
     $map = array();
     foreach (array_chunk($need, 2048) as $chunk) {
         $rows = queryfx_all($conn_w, 'SELECT id, commitIdentifier FROM %T
       WHERE commitIdentifier IN (%Ls) AND repositoryID = %d', $commit_table_name, $chunk, $repo->getID());
         foreach ($rows as $row) {
             $map[$row['commitIdentifier']] = $row['id'];
         }
     }
     $insert_sql = array();
     $delete_sql = array();
     foreach ($graph as $child => $parents) {
         $names = $parents;
         $names[] = $child;
         foreach ($names as $name) {
             if (empty($map[$name])) {
                 throw new Exception(pht('Unknown commit "%s"!', $name));
             }
         }
         if (!$parents) {
             // Write an explicit 0 to indicate "no parents" instead of "no data".
             $insert_sql[] = qsprintf($conn_w, '(%d, 0)', $map[$child]);
         } else {
             foreach ($parents as $parent) {
                 $insert_sql[] = qsprintf($conn_w, '(%d, %d)', $map[$child], $map[$parent]);
             }
         }
         $delete_sql[] = $map[$child];
         $bar->update(1);
     }
     $commit_table->openTransaction();
     foreach (PhabricatorLiskDAO::chunkSQL($delete_sql) as $chunk) {
         queryfx($conn_w, 'DELETE FROM %T WHERE childCommitID IN (%Q)', PhabricatorRepository::TABLE_PARENTS, $chunk);
     }
     foreach (PhabricatorLiskDAO::chunkSQL($insert_sql) as $chunk) {
         queryfx($conn_w, 'INSERT INTO %T (childCommitID, parentCommitID) VALUES %Q', PhabricatorRepository::TABLE_PARENTS, $chunk);
     }
     $commit_table->saveTransaction();
     $bar->done();
 }
 private static function recordAction(array $actors, PhabricatorSystemAction $action, $score)
 {
     $log = new PhabricatorSystemActionLog();
     $conn_w = $log->establishConnection('w');
     $sql = array();
     foreach ($actors as $actor) {
         $sql[] = qsprintf($conn_w, '(%s, %s, %s, %f, %d)', PhabricatorHash::digestForIndex($actor), $actor, $action->getActionConstant(), $score, time());
     }
     foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
         queryfx($conn_w, 'INSERT INTO %T (actorHash, actorIdentity, action, score, epoch)
       VALUES %Q', $log->getTableName(), $chunk);
     }
 }
<?php

// Destroy duplicate drafts before storage adjustment adds a unique key to this
// table. See T1191. We retain the newest draft.
// (We can't easily do this in a single SQL statement because MySQL won't let us
// modify a table that's joined in a subquery.)
$table = new DifferentialDraft();
$conn_w = $table->establishConnection('w');
$duplicates = queryfx_all($conn_w, 'SELECT DISTINCT u.id id FROM %T u
    JOIN %T v
      ON u.objectPHID = v.objectPHID
      AND u.authorPHID = v.authorPHID
      AND u.draftKey = v.draftKey
      AND u.id < v.id', $table->getTableName(), $table->getTableName());
$duplicates = ipull($duplicates, 'id');
foreach (PhabricatorLiskDAO::chunkSQL($duplicates) as $chunk) {
    queryfx($conn_w, 'DELETE FROM %T WHERE id IN (%Q)', $table->getTableName(), $chunk);
}