private function updateWorkboardColumns($object, $const, $old, $new)
 {
     // If an object is removed from a project, remove it from any proxy
     // columns for that project. This allows a task which is moved up from a
     // milestone to the parent to move back into the "Backlog" column on the
     // parent workboard.
     if ($const != PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) {
         return;
     }
     // TODO: This should likely be some future WorkboardInterface.
     $appears_on_workboards = $object instanceof ManiphestTask;
     if (!$appears_on_workboards) {
         return;
     }
     $removed_phids = array_keys(array_diff_key($old, $new));
     if (!$removed_phids) {
         return;
     }
     // Find any proxy columns for the removed projects.
     $proxy_columns = id(new PhabricatorProjectColumnQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withProxyPHIDs($removed_phids)->execute();
     if (!$proxy_columns) {
         return array();
     }
     $proxy_phids = mpull($proxy_columns, 'getPHID');
     $position_table = new PhabricatorProjectColumnPosition();
     $conn_w = $position_table->establishConnection('w');
     queryfx($conn_w, 'DELETE FROM %T WHERE objectPHID = %s AND columnPHID IN (%Ls)', $position_table->getTableName(), $object->getPHID(), $proxy_phids);
 }
 protected function applyCustomExternalTransaction(PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction)
 {
     switch ($xaction->getTransactionType()) {
         case ManiphestTransaction::TYPE_PROJECT_COLUMN:
             $board_phid = idx($xaction->getNewValue(), 'projectPHID');
             if (!$board_phid) {
                 throw new Exception(pht("Expected '%s' in column transaction.", 'projectPHID'));
             }
             $old_phids = idx($xaction->getOldValue(), 'columnPHIDs', array());
             $new_phids = idx($xaction->getNewValue(), 'columnPHIDs', array());
             if (count($new_phids) !== 1) {
                 throw new Exception(pht("Expected exactly one '%s' in column transaction.", 'columnPHIDs'));
             }
             $columns = id(new PhabricatorProjectColumnQuery())->setViewer($this->requireActor())->withPHIDs($new_phids)->execute();
             $columns = mpull($columns, null, 'getPHID');
             $positions = id(new PhabricatorProjectColumnPositionQuery())->setViewer($this->requireActor())->withObjectPHIDs(array($object->getPHID()))->withBoardPHIDs(array($board_phid))->execute();
             $before_phid = idx($xaction->getNewValue(), 'beforePHID');
             $after_phid = idx($xaction->getNewValue(), 'afterPHID');
             if (!$before_phid && !$after_phid && $old_phids == $new_phids) {
                 // If we are not moving the object between columns and also not
                 // reordering the position, this is a move on some other order
                 // (like priority). We can leave the positions untouched and just
                 // bail, there's no work to be done.
                 return;
             }
             // Otherwise, we're either moving between columns or adjusting the
             // object's position in the "natural" ordering, so we do need to update
             // some rows.
             // Remove all existing column positions on the board.
             foreach ($positions as $position) {
                 $position->delete();
             }
             // Add the new column positions.
             foreach ($new_phids as $phid) {
                 $column = idx($columns, $phid);
                 if (!$column) {
                     throw new Exception(pht('No such column "%s" exists!', $phid));
                 }
                 // Load the other object positions in the column. Note that we must
                 // skip implicit column creation to avoid generating a new position
                 // if the target column is a backlog column.
                 $other_positions = id(new PhabricatorProjectColumnPositionQuery())->setViewer($this->requireActor())->withColumns(array($column))->withBoardPHIDs(array($board_phid))->setSkipImplicitCreate(true)->execute();
                 $other_positions = msort($other_positions, 'getOrderingKey');
                 // Set up the new position object. We're going to figure out the
                 // right sequence number and then persist this object with that
                 // sequence number.
                 $new_position = id(new PhabricatorProjectColumnPosition())->setBoardPHID($board_phid)->setColumnPHID($column->getPHID())->setObjectPHID($object->getPHID());
                 $updates = array();
                 $sequence = 0;
                 // If we're just dropping this into the column without any specific
                 // position information, put it at the top.
                 if (!$before_phid && !$after_phid) {
                     $new_position->setSequence($sequence)->save();
                     $sequence++;
                 }
                 foreach ($other_positions as $position) {
                     $object_phid = $position->getObjectPHID();
                     // If this is the object we're moving before and we haven't
                     // saved yet, insert here.
                     if ($before_phid == $object_phid && !$new_position->getID()) {
                         $new_position->setSequence($sequence)->save();
                         $sequence++;
                     }
                     // This object goes here in the sequence; we might need to update
                     // the row.
                     if ($sequence != $position->getSequence()) {
                         $updates[$position->getID()] = $sequence;
                     }
                     $sequence++;
                     // If this is the object we're moving after and we haven't saved
                     // yet, insert here.
                     if ($after_phid == $object_phid && !$new_position->getID()) {
                         $new_position->setSequence($sequence)->save();
                         $sequence++;
                     }
                 }
                 // We should have found a place to put it.
                 if (!$new_position->getID()) {
                     throw new Exception(pht('Unable to find a place to insert object on column!'));
                 }
                 // If we changed other objects' column positions, bulk reorder them.
                 if ($updates) {
                     $position = new PhabricatorProjectColumnPosition();
                     $conn_w = $position->establishConnection('w');
                     $pairs = array();
                     foreach ($updates as $id => $sequence) {
                         // This is ugly because MySQL gets upset with us if it is
                         // configured strictly and we attempt inserts which can't work.
                         // We'll never actually do these inserts since they'll always
                         // collide (triggering the ON DUPLICATE KEY logic), so we just
                         // provide dummy values in order to get there.
                         $pairs[] = qsprintf($conn_w, '(%d, %d, "", "", "")', $id, $sequence);
                     }
                     queryfx($conn_w, 'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID)
             VALUES %Q ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)', $position->getTableName(), implode(', ', $pairs));
                 }
             }
             break;
         default:
             break;
     }
 }
 public function applyPositionUpdates()
 {
     foreach ($this->remQueue as $position) {
         if ($position->getID()) {
             $position->delete();
         }
     }
     $this->remQueue = array();
     $adds = array();
     $updates = array();
     foreach ($this->addQueue as $position) {
         $id = $position->getID();
         if ($id) {
             $updates[$id] = $position;
         } else {
             $adds[] = $position;
         }
     }
     $this->addQueue = array();
     $table = new PhabricatorProjectColumnPosition();
     $conn_w = $table->establishConnection('w');
     $pairs = array();
     foreach ($updates as $id => $position) {
         // This is ugly because MySQL gets upset with us if it is configured
         // strictly and we attempt inserts which can't work. We'll never actually
         // do these inserts since they'll always collide (triggering the ON
         // DUPLICATE KEY logic), so we just provide dummy values in order to get
         // there.
         $pairs[] = qsprintf($conn_w, '(%d, %d, "", "", "")', $id, $position->getSequence());
     }
     if ($pairs) {
         queryfx($conn_w, 'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID)
       VALUES %Q ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)', $table->getTableName(), implode(', ', $pairs));
     }
     foreach ($adds as $position) {
         $position->save();
     }
     return $this;
 }