diff --git a/resources/sql/autopatches/20180418.almanac.network.unique.php b/resources/sql/autopatches/20180418.almanac.network.unique.php new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20180418.almanac.network.unique.php @@ -0,0 +1,46 @@ +establishConnection('w'); + +queryfx( + $conn, + 'LOCK TABLES %T WRITE', + $table->getTableName()); + +$seen = array(); +foreach (new LiskMigrationIterator($table) as $network) { + $name = $network->getName(); + + // If this is the first copy of this row we've seen, mark it as seen and + // move on. + if (empty($seen[$name])) { + $seen[$name] = 1; + continue; + } + + // Otherwise, rename this row. + while (true) { + $new_name = $name.'-'.$seen[$name]; + if (empty($seen[$new_name])) { + $network->setName($new_name); + try { + $network->save(); + break; + } catch (AphrontDuplicateKeyQueryException $ex) { + // New name is a dupe of a network we haven't seen yet. + } + } + $seen[$name]++; + } + $seen[$new_name] = 1; +} + +queryfx( + $conn, + 'ALTER TABLE %T ADD UNIQUE KEY `key_name` (name)', + $table->getTableName()); + +queryfx( + $conn, + 'UNLOCK TABLES'); diff --git a/src/applications/almanac/query/AlmanacNetworkQuery.php b/src/applications/almanac/query/AlmanacNetworkQuery.php --- a/src/applications/almanac/query/AlmanacNetworkQuery.php +++ b/src/applications/almanac/query/AlmanacNetworkQuery.php @@ -5,6 +5,7 @@ private $ids; private $phids; + private $names; public function withIDs(array $ids) { $this->ids = $ids; @@ -20,6 +21,11 @@ return new AlmanacNetwork(); } + public function withNames(array $names) { + $this->names = $names; + return $this; + } + public function withNameNgrams($ngrams) { return $this->withNgramsConstraint( new AlmanacNetworkNameNgrams(), @@ -47,6 +53,13 @@ $this->phids); } + if ($this->names !== null) { + $where[] = qsprintf( + $conn, + 'network.name IN (%Ls)', + $this->names); + } + return $where; } diff --git a/src/applications/almanac/storage/AlmanacNetwork.php b/src/applications/almanac/storage/AlmanacNetwork.php --- a/src/applications/almanac/storage/AlmanacNetwork.php +++ b/src/applications/almanac/storage/AlmanacNetwork.php @@ -24,8 +24,15 @@ return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( - 'name' => 'text128', + 'name' => 'sort128', 'mailKey' => 'bytes20', + + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_name' => array( + 'columns' => array('name'), + 'unique' => true, + ), ), ) + parent::getConfiguration(); } diff --git a/src/applications/almanac/util/AlmanacNames.php b/src/applications/almanac/util/AlmanacNames.php --- a/src/applications/almanac/util/AlmanacNames.php +++ b/src/applications/almanac/util/AlmanacNames.php @@ -6,57 +6,58 @@ if (strlen($name) < 3) { throw new Exception( pht( - 'Almanac service, device, property and namespace names must be '. - 'at least 3 characters long.')); + 'Almanac service, device, property, network and namespace names '. + 'must be at least 3 characters long.')); } if (strlen($name) > 100) { throw new Exception( pht( - 'Almanac service, device, property and namespace names may not '. - 'be more than 100 characters long.')); + 'Almanac service, device, property, network and namespace names '. + 'may not be more than 100 characters long.')); } if (!preg_match('/^[a-z0-9.-]+\z/', $name)) { throw new Exception( pht( - 'Almanac service, device, property and namespace names may only '. - 'contain lowercase letters, numbers, hyphens, and periods.')); + 'Almanac service, device, property, network and namespace names '. + 'may only contain lowercase letters, numbers, hyphens, and '. + 'periods.')); } if (preg_match('/(^|\\.)\d+(\z|\\.)/', $name)) { throw new Exception( pht( - 'Almanac service, device, property and namespace names may not '. - 'have any segments containing only digits.')); + 'Almanac service, device, network, property and namespace names '. + 'may not have any segments containing only digits.')); } if (preg_match('/\.\./', $name)) { throw new Exception( pht( - 'Almanac service, device, property and namespace names may not '. - 'contain multiple consecutive periods.')); + 'Almanac service, device, property, network and namespace names '. + 'may not contain multiple consecutive periods.')); } if (preg_match('/\\.-|-\\./', $name)) { throw new Exception( pht( - 'Almanac service, device, property and namespace names may not '. - 'contain hyphens adjacent to periods.')); + 'Almanac service, device, property, network and namespace names '. + 'may not contain hyphens adjacent to periods.')); } if (preg_match('/--/', $name)) { throw new Exception( pht( - 'Almanac service, device, property and namespace names may not '. - 'contain multiple consecutive hyphens.')); + 'Almanac service, device, property, network and namespace names '. + 'may not contain multiple consecutive hyphens.')); } if (!preg_match('/^[a-z0-9].*[a-z0-9]\z/', $name)) { throw new Exception( pht( - 'Almanac service, device, property and namespace names must begin '. - 'and end with a letter or number.')); + 'Almanac service, device, property, network and namespace names '. + 'must begin and end with a letter or number.')); } } diff --git a/src/applications/almanac/xaction/AlmanacNetworkNameTransaction.php b/src/applications/almanac/xaction/AlmanacNetworkNameTransaction.php --- a/src/applications/almanac/xaction/AlmanacNetworkNameTransaction.php +++ b/src/applications/almanac/xaction/AlmanacNetworkNameTransaction.php @@ -38,6 +38,37 @@ pht('Network name is required.')); } + foreach ($xactions as $xaction) { + $name = $xaction->getNewValue(); + + $message = null; + try { + AlmanacNames::validateName($name); + } catch (Exception $ex) { + $message = $ex->getMessage(); + } + + if ($message !== null) { + $errors[] = $this->newInvalidError($message, $xaction); + continue; + } + + if ($name === $object->getName()) { + continue; + } + + $other = id(new AlmanacNetworkQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames(array($name)) + ->executeOne(); + if ($other && ($other->getID() != $object->getID())) { + $errors[] = $this->newInvalidError( + pht('Almanac networks must have unique names.'), + $xaction); + continue; + } + } + return $errors; }