mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-18 04:42:40 +01:00
Move repository URIs to a dedicated index
Summary: Ref T4705 (there are also some other adjacent related tasks dealing with URIs). Currently, we issue a "get repositories matching URIs: ..." query by loading every possible repository and then checking their URIs in PHP. Instead, put URIs in a separate table. I plan for each repository to potentially have multiple URIs soon, so this prepares for that. Test Plan: - Ran migrations. - Looked at index table, made sure it appeared sensible. - Ran some queries by `uri` to find repositories, found the repositories I expected. - Updated the remote URI of a repository, saw queries / index update appropriately. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4705 Differential Revision: https://secure.phabricator.com/D15005
This commit is contained in:
parent
d57c1d6ce2
commit
1eab16c395
10 changed files with 175 additions and 30 deletions
7
resources/sql/autopatches/20160112.repo.01.uri.sql
Normal file
7
resources/sql/autopatches/20160112.repo.01.uri.sql
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE {$NAMESPACE}_repository.repository_uriindex (
|
||||||
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
repositoryPHID VARBINARY(64) NOT NULL,
|
||||||
|
repositoryURI LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
|
||||||
|
KEY `key_repository` (repositoryPHID),
|
||||||
|
KEY `key_uri` (repositoryURI(128))
|
||||||
|
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
|
7
resources/sql/autopatches/20160112.repo.02.uri.index.php
Normal file
7
resources/sql/autopatches/20160112.repo.02.uri.index.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$table = new PhabricatorRepository();
|
||||||
|
|
||||||
|
foreach (new LiskMigrationIterator($table) as $repo) {
|
||||||
|
$repo->updateURIIndex();
|
||||||
|
}
|
|
@ -734,6 +734,7 @@ phutil_register_library_map(array(
|
||||||
'DiffusionRepositorySymbolsController' => 'applications/diffusion/controller/DiffusionRepositorySymbolsController.php',
|
'DiffusionRepositorySymbolsController' => 'applications/diffusion/controller/DiffusionRepositorySymbolsController.php',
|
||||||
'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php',
|
'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php',
|
||||||
'DiffusionRepositoryTestAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryTestAutomationController.php',
|
'DiffusionRepositoryTestAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryTestAutomationController.php',
|
||||||
|
'DiffusionRepositoryURIsIndexEngineExtension' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsIndexEngineExtension.php',
|
||||||
'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php',
|
'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php',
|
||||||
'DiffusionResolveRefsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php',
|
'DiffusionResolveRefsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php',
|
||||||
'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php',
|
'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php',
|
||||||
|
@ -3009,6 +3010,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php',
|
'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php',
|
||||||
'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php',
|
'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php',
|
||||||
'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php',
|
'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php',
|
||||||
|
'PhabricatorRepositoryURIIndex' => 'applications/repository/storage/PhabricatorRepositoryURIIndex.php',
|
||||||
'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php',
|
'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php',
|
||||||
'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php',
|
'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php',
|
||||||
'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php',
|
'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php',
|
||||||
|
@ -4707,6 +4709,7 @@ phutil_register_library_map(array(
|
||||||
'DiffusionRepositorySymbolsController' => 'DiffusionRepositoryEditController',
|
'DiffusionRepositorySymbolsController' => 'DiffusionRepositoryEditController',
|
||||||
'DiffusionRepositoryTag' => 'Phobject',
|
'DiffusionRepositoryTag' => 'Phobject',
|
||||||
'DiffusionRepositoryTestAutomationController' => 'DiffusionRepositoryEditController',
|
'DiffusionRepositoryTestAutomationController' => 'DiffusionRepositoryEditController',
|
||||||
|
'DiffusionRepositoryURIsIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
|
||||||
'DiffusionRequest' => 'Phobject',
|
'DiffusionRequest' => 'Phobject',
|
||||||
'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
|
'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
|
||||||
'DiffusionResolveUserQuery' => 'Phobject',
|
'DiffusionResolveUserQuery' => 'Phobject',
|
||||||
|
@ -7415,6 +7418,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction',
|
'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction',
|
||||||
'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||||
'PhabricatorRepositoryType' => 'Phobject',
|
'PhabricatorRepositoryType' => 'Phobject',
|
||||||
|
'PhabricatorRepositoryURIIndex' => 'PhabricatorRepositoryDAO',
|
||||||
'PhabricatorRepositoryURINormalizer' => 'Phobject',
|
'PhabricatorRepositoryURINormalizer' => 'Phobject',
|
||||||
'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase',
|
'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase',
|
||||||
'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase',
|
'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase',
|
||||||
|
|
|
@ -71,7 +71,7 @@ final class DiffusionLintSaveRunner extends Phobject {
|
||||||
} else if ($uuid) {
|
} else if ($uuid) {
|
||||||
$repository_query->withUUIDs(array($uuid));
|
$repository_query->withUUIDs(array($uuid));
|
||||||
} else if ($remote_uri) {
|
} else if ($remote_uri) {
|
||||||
$repository_query->withRemoteURIs(array($remote_uri));
|
$repository_query->withURIs(array($remote_uri));
|
||||||
}
|
}
|
||||||
|
|
||||||
$repository = $repository_query->executeOne();
|
$repository = $repository_query->executeOne();
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class DiffusionRepositoryURIsIndexEngineExtension
|
||||||
|
extends PhabricatorIndexEngineExtension {
|
||||||
|
|
||||||
|
const EXTENSIONKEY = 'diffusion.repositories.uri';
|
||||||
|
|
||||||
|
public function getExtensionName() {
|
||||||
|
return pht('Repository URIs');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shouldIndexObject($object) {
|
||||||
|
return ($object instanceof PhabricatorRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function indexObject(
|
||||||
|
PhabricatorIndexEngine $engine,
|
||||||
|
$object) {
|
||||||
|
$object->updateURIIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -63,7 +63,7 @@ final class RepositoryQueryConduitAPIMethod
|
||||||
|
|
||||||
$remote_uris = $request->getValue('remoteURIs', array());
|
$remote_uris = $request->getValue('remoteURIs', array());
|
||||||
if ($remote_uris) {
|
if ($remote_uris) {
|
||||||
$query->withRemoteURIs($remote_uris);
|
$query->withURIs($remote_uris);
|
||||||
}
|
}
|
||||||
|
|
||||||
$uuids = $request->getValue('uuids', array());
|
$uuids = $request->getValue('uuids', array());
|
||||||
|
|
|
@ -518,4 +518,8 @@ final class PhabricatorRepositoryEditor
|
||||||
throw new PhabricatorApplicationTransactionValidationException($errors);
|
throw new PhabricatorApplicationTransactionValidationException($errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function supportsSearch() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ final class PhabricatorRepositoryQuery
|
||||||
private $types;
|
private $types;
|
||||||
private $uuids;
|
private $uuids;
|
||||||
private $nameContains;
|
private $nameContains;
|
||||||
private $remoteURIs;
|
private $uris;
|
||||||
private $datasourceQuery;
|
private $datasourceQuery;
|
||||||
private $slugs;
|
private $slugs;
|
||||||
|
|
||||||
|
@ -118,8 +118,8 @@ final class PhabricatorRepositoryQuery
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function withRemoteURIs(array $uris) {
|
public function withURIs(array $uris) {
|
||||||
$this->remoteURIs = $uris;
|
$this->uris = $uris;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,17 +263,6 @@ final class PhabricatorRepositoryQuery
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Denormalize this, too.
|
|
||||||
if ($this->remoteURIs) {
|
|
||||||
$try_uris = $this->getNormalizedPaths();
|
|
||||||
$try_uris = array_fuse($try_uris);
|
|
||||||
foreach ($repositories as $key => $repository) {
|
|
||||||
if (!isset($try_uris[$repository->getNormalizedPath()])) {
|
|
||||||
unset($repositories[$key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the identifierMap
|
// Build the identifierMap
|
||||||
if ($this->numericIdentifiers) {
|
if ($this->numericIdentifiers) {
|
||||||
foreach ($this->numericIdentifiers as $id) {
|
foreach ($this->numericIdentifiers as $id) {
|
||||||
|
@ -445,6 +434,8 @@ final class PhabricatorRepositoryQuery
|
||||||
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
|
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
|
||||||
$parts = parent::buildSelectClauseParts($conn);
|
$parts = parent::buildSelectClauseParts($conn);
|
||||||
|
|
||||||
|
$parts[] = 'r.*';
|
||||||
|
|
||||||
if ($this->shouldJoinSummaryTable()) {
|
if ($this->shouldJoinSummaryTable()) {
|
||||||
$parts[] = 's.*';
|
$parts[] = 's.*';
|
||||||
}
|
}
|
||||||
|
@ -462,9 +453,28 @@ final class PhabricatorRepositoryQuery
|
||||||
PhabricatorRepository::TABLE_SUMMARY);
|
PhabricatorRepository::TABLE_SUMMARY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->shouldJoinURITable()) {
|
||||||
|
$joins[] = qsprintf(
|
||||||
|
$conn,
|
||||||
|
'LEFT JOIN %T uri ON r.phid = uri.repositoryPHID',
|
||||||
|
id(new PhabricatorRepositoryURIIndex())->getTableName());
|
||||||
|
}
|
||||||
|
|
||||||
return $joins;
|
return $joins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function shouldGroupQueryResultRows() {
|
||||||
|
if ($this->shouldJoinURITable()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::shouldGroupQueryResultRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shouldJoinURITable() {
|
||||||
|
return ($this->uris !== null);
|
||||||
|
}
|
||||||
|
|
||||||
private function shouldJoinSummaryTable() {
|
private function shouldJoinSummaryTable() {
|
||||||
if ($this->needCommitCounts) {
|
if ($this->needCommitCounts) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -592,7 +602,7 @@ final class PhabricatorRepositoryQuery
|
||||||
if (strlen($this->nameContains)) {
|
if (strlen($this->nameContains)) {
|
||||||
$where[] = qsprintf(
|
$where[] = qsprintf(
|
||||||
$conn,
|
$conn,
|
||||||
'name LIKE %~',
|
'r.name LIKE %~',
|
||||||
$this->nameContains);
|
$this->nameContains);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -619,6 +629,16 @@ final class PhabricatorRepositoryQuery
|
||||||
$this->slugs);
|
$this->slugs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->uris !== null) {
|
||||||
|
$try_uris = $this->getNormalizedPaths();
|
||||||
|
$try_uris = array_fuse($try_uris);
|
||||||
|
|
||||||
|
$where[] = qsprintf(
|
||||||
|
$conn,
|
||||||
|
'uri.repositoryURI IN (%Ls)',
|
||||||
|
$try_uris);
|
||||||
|
}
|
||||||
|
|
||||||
return $where;
|
return $where;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -635,7 +655,7 @@ final class PhabricatorRepositoryQuery
|
||||||
// or an `svn+ssh` URI, we could deduce how to normalize it. However, this
|
// or an `svn+ssh` URI, we could deduce how to normalize it. However, this
|
||||||
// would be more complicated and it's not clear if it matters in practice.
|
// would be more complicated and it's not clear if it matters in practice.
|
||||||
|
|
||||||
foreach ($this->remoteURIs as $uri) {
|
foreach ($this->uris as $uri) {
|
||||||
$normalized_uris[] = new PhabricatorRepositoryURINormalizer(
|
$normalized_uris[] = new PhabricatorRepositoryURINormalizer(
|
||||||
PhabricatorRepositoryURINormalizer::TYPE_GIT,
|
PhabricatorRepositoryURINormalizer::TYPE_GIT,
|
||||||
$uri);
|
$uri);
|
||||||
|
|
|
@ -823,30 +823,40 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
return $uri;
|
return $uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getNormalizedPath() {
|
public function updateURIIndex() {
|
||||||
$uri = (string)$this->getCloneURIObject();
|
$uris = array(
|
||||||
|
(string)$this->getCloneURIObject(),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($uris as $key => $uri) {
|
||||||
|
$uris[$key] = $this->getNormalizedURI($uri)
|
||||||
|
->getNormalizedPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
PhabricatorRepositoryURIIndex::updateRepositoryURIs(
|
||||||
|
$this->getPHID(),
|
||||||
|
$uris);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getNormalizedURI($uri) {
|
||||||
switch ($this->getVersionControlSystem()) {
|
switch ($this->getVersionControlSystem()) {
|
||||||
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
|
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
|
||||||
$normalized_uri = new PhabricatorRepositoryURINormalizer(
|
return new PhabricatorRepositoryURINormalizer(
|
||||||
PhabricatorRepositoryURINormalizer::TYPE_GIT,
|
PhabricatorRepositoryURINormalizer::TYPE_GIT,
|
||||||
$uri);
|
$uri);
|
||||||
break;
|
|
||||||
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
|
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
|
||||||
$normalized_uri = new PhabricatorRepositoryURINormalizer(
|
return new PhabricatorRepositoryURINormalizer(
|
||||||
PhabricatorRepositoryURINormalizer::TYPE_SVN,
|
PhabricatorRepositoryURINormalizer::TYPE_SVN,
|
||||||
$uri);
|
$uri);
|
||||||
break;
|
|
||||||
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
|
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
|
||||||
$normalized_uri = new PhabricatorRepositoryURINormalizer(
|
return new PhabricatorRepositoryURINormalizer(
|
||||||
PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL,
|
PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL,
|
||||||
$uri);
|
$uri);
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
throw new Exception(pht('Unrecognized version control system.'));
|
throw new Exception(pht('Unrecognized version control system.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $normalized_uri->getNormalizedPath();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isTracked() {
|
public function isTracked() {
|
||||||
|
@ -2231,13 +2241,17 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
public function destroyObjectPermanently(
|
public function destroyObjectPermanently(
|
||||||
PhabricatorDestructionEngine $engine) {
|
PhabricatorDestructionEngine $engine) {
|
||||||
|
|
||||||
|
$phid = $this->getPHID();
|
||||||
|
|
||||||
$this->openTransaction();
|
$this->openTransaction();
|
||||||
|
|
||||||
$this->delete();
|
$this->delete();
|
||||||
|
|
||||||
|
PhabricatorRepositoryURIIndex::updateRepositoryURIs($phid, array());
|
||||||
|
|
||||||
$books = id(new DivinerBookQuery())
|
$books = id(new DivinerBookQuery())
|
||||||
->setViewer($engine->getViewer())
|
->setViewer($engine->getViewer())
|
||||||
->withRepositoryPHIDs(array($this->getPHID()))
|
->withRepositoryPHIDs(array($phid))
|
||||||
->execute();
|
->execute();
|
||||||
foreach ($books as $book) {
|
foreach ($books as $book) {
|
||||||
$engine->destroyObject($book);
|
$engine->destroyObject($book);
|
||||||
|
@ -2245,7 +2259,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
|
|
||||||
$atoms = id(new DivinerAtomQuery())
|
$atoms = id(new DivinerAtomQuery())
|
||||||
->setViewer($engine->getViewer())
|
->setViewer($engine->getViewer())
|
||||||
->withRepositoryPHIDs(array($this->getPHID()))
|
->withRepositoryPHIDs(array($phid))
|
||||||
->execute();
|
->execute();
|
||||||
foreach ($atoms as $atom) {
|
foreach ($atoms as $atom) {
|
||||||
$engine->destroyObject($atom);
|
$engine->destroyObject($atom);
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorRepositoryURIIndex
|
||||||
|
extends PhabricatorRepositoryDAO {
|
||||||
|
|
||||||
|
protected $repositoryPHID;
|
||||||
|
protected $repositoryURI;
|
||||||
|
|
||||||
|
protected function getConfiguration() {
|
||||||
|
return array(
|
||||||
|
self::CONFIG_TIMESTAMPS => false,
|
||||||
|
self::CONFIG_COLUMN_SCHEMA => array(
|
||||||
|
'repositoryURI' => 'text',
|
||||||
|
),
|
||||||
|
self::CONFIG_KEY_SCHEMA => array(
|
||||||
|
'key_repository' => array(
|
||||||
|
'columns' => array('repositoryPHID'),
|
||||||
|
),
|
||||||
|
'key_uri' => array(
|
||||||
|
'columns' => array('repositoryURI(128)'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) + parent::getConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function updateRepositoryURIs(
|
||||||
|
$repository_phid,
|
||||||
|
array $uris) {
|
||||||
|
|
||||||
|
$table = new self();
|
||||||
|
$conn_w = $table->establishConnection('w');
|
||||||
|
|
||||||
|
$sql = array();
|
||||||
|
foreach ($uris as $key => $uri) {
|
||||||
|
if (!strlen($uri)) {
|
||||||
|
unset($uris[$key]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql[] = qsprintf(
|
||||||
|
$conn_w,
|
||||||
|
'(%s, %s)',
|
||||||
|
$repository_phid,
|
||||||
|
$uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->openTransaction();
|
||||||
|
|
||||||
|
queryfx(
|
||||||
|
$conn_w,
|
||||||
|
'DELETE FROM %T WHERE repositoryPHID = %s',
|
||||||
|
$table->getTableName(),
|
||||||
|
$repository_phid);
|
||||||
|
|
||||||
|
if ($sql) {
|
||||||
|
queryfx(
|
||||||
|
$conn_w,
|
||||||
|
'INSERT INTO %T (repositoryPHID, repositoryURI) VALUES %Q',
|
||||||
|
$table->getTableName(),
|
||||||
|
implode(', ', $sql));
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->saveTransaction();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue