diff --git a/appinfo/info.xml b/appinfo/info.xml
index 51242c1aa6..2002e46f96 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -35,6 +35,7 @@
OCA\Polls\Migration\RepairSteps\UpdateHashes
+ OCA\Polls\Migration\RepairSteps\MigratePublicToOpen
diff --git a/composer.json b/composer.json
index 98eb1a3dbb..70849a3db5 100644
--- a/composer.json
+++ b/composer.json
@@ -23,7 +23,7 @@
"allow-plugins": {
"bamarni/composer-bin-plugin": true
}
- },
+ },
"autoload": {
"psr-4": {
"OCA\\Polls\\": "lib/"
@@ -31,7 +31,8 @@
},
"autoload-dev": {
"psr-4": {
- "OCA\\Polls\\Tests\\": "tests/"
+ "OCA\\Polls\\Tests\\": "tests/",
+ "OCP\\": "vendor/nextcloud/ocp/OCP/"
}
},
"require-dev": {
@@ -66,4 +67,4 @@
"league/commonmark": "^2.1",
"rlanvin/php-rrule": "^2.3"
}
-}
+}
\ No newline at end of file
diff --git a/lib/Db/V4/DbManager.php b/lib/Db/V4/DbManager.php
index 9bb1e31f4c..de9f587c06 100644
--- a/lib/Db/V4/DbManager.php
+++ b/lib/Db/V4/DbManager.php
@@ -10,10 +10,14 @@
use Doctrine\DBAL\Schema\Schema;
use Exception;
-use OCA\Polls\Exceptions\InvalidClassException;
+use OCA\Polls\Exceptions\PreconditionColumnIsMissingException;
+use OCA\Polls\Exceptions\PreconditionException;
+use OCA\Polls\Exceptions\PreconditionTableIsMissingException;
+use OCA\Polls\Exceptions\SchemaMissmatchException;
use OCP\DB\ISchemaWrapper;
use OCP\IConfig;
use OCP\IDBConnection;
+use Psr\Log\LoggerInterface;
abstract class DbManager {
@@ -24,6 +28,7 @@ abstract class DbManager {
public function __construct(
protected IConfig $config,
protected IDBConnection $connection,
+ protected LoggerInterface $logger,
) {
$this->dbPrefix = $this->config->getSystemValue('dbtableprefix', 'oc_');
}
@@ -56,12 +61,14 @@ public function createSchema(): void {
* This method is used to apply the schema changes to the database.
* It must be called after the schema is set.
*
- * @throws InvalidClassException if the schema is not an instance of Schema class
+ * @throws SchemaMissmatchException if the schema is not an instance of Schema class
*/
public function migrateToSchema() : void {
// Schema must be of class Schema
$this->needsSchema(allowISchemWrapperClass: false);
- $this->connection->migrateToSchema($this->schema);
+ if ($this->schema instanceof Schema) {
+ $this->connection->migrateToSchema($this->schema);
+ }
}
/**
@@ -80,9 +87,9 @@ public function setConnection(IDBConnection &$connection): void {
* ISchemaWrapper already uses the prefixed table name, but Schema does not.
*
* @param string $tableName without prefix
- * @return string|null
+ * @return string tableName with prefix
*/
- protected function getTableName(string $tableName): ?string {
+ protected function getTableName(string $tableName): string {
if ($this->schema instanceof Schema) {
// If the schema is an instance of Schema, we need to prefix the table name
return $this->dbPrefix . $tableName;
@@ -93,9 +100,10 @@ protected function getTableName(string $tableName): ?string {
/**
* Use this as a predetermined breaking point to ensure if a method needs a schema to be set.
*
- * @param bool $allowSchemaClass allow schema to be an instance of Schema (default is true)
- * @param bool $allowISchemWrapperClass allow schema to be an instance of ISchemaWrapper (default is true)
- * @throws InvalidClassException if the schema is not set or not of a required class
+ * @param bool $allowSchemaClass allow schema to be an instance of Schema (default is true). table names are not prefixed
+ * @param bool $allowISchemWrapperClass allow schema to be an instance of ISchemaWrapper (default is true), tablen ames are prefixed
+ *
+ * @throws SchemaMissmatchException if the schema is not set or not of a required class
*/
protected function needsSchema(bool $allowSchemaClass = true, bool $allowISchemWrapperClass = true): void {
if (($this->schema instanceof Schema) && $allowSchemaClass) {
@@ -108,17 +116,58 @@ protected function needsSchema(bool $allowSchemaClass = true, bool $allowISchemW
if ($allowSchemaClass && $allowISchemWrapperClass) {
// If the schema is not set or not an instance of Schema or ISchemaWrapper, throw an exception
- throw new InvalidClassException('Schema is not set or not an instance of Schema or ISchemaWrapper (caller: ' . self::formatCaller() . ')');
+ throw new SchemaMissmatchException('Schema is not set or not an instance of Schema or ISchemaWrapper (caller: ' . self::formatCaller() . ')');
}
if ($allowSchemaClass) {
// If the schema is not set or not an instance of Schema, throw an exception
- throw new InvalidClassException('Schema is not set or not an instance of Schema (caller: ' . self::formatCaller() . ')');
+ throw new SchemaMissmatchException('Schema is not set or not an instance of Schema (caller: ' . self::formatCaller() . ')');
}
if ($allowISchemWrapperClass) {
// If the schema is not set or not an instance of ISchemaWrapper, throw an exception
- throw new InvalidClassException('Schema is not set or not an instance of ISchemaWrapper(caller: ' . self::formatCaller() . ')');
+ throw new SchemaMissmatchException('Schema is not set or not an instance of ISchemaWrapper(caller: ' . self::formatCaller() . ')');
+ }
+ throw new SchemaMissmatchException('Unexpected. Schema is an instance of ' . get_class($this->schema) . '(caller: ' . self::formatCaller() . ')');
+ }
+
+ /**
+ * Check if the table and columns exist
+ * If columnNames is empty only the table is checked
+ *
+ * @param string $tableName Unprefixed tablename
+ * @param string[]|string $columnNames Column name or array of column names to check
+ *
+ * @throws PreconditionException on any precondition failure
+ * @throws PreconditionTableIsMissingException if the table does not exist
+ * @throws PreconditionColumnIsMissingException if a column does not exist
+ *
+ */
+ protected function checkPrecondition(string $tableName, array|string $columnNames = []): void {
+ $prefixedTableName = $this->dbPrefix . $tableName;
+
+ if (!$this->connection->tableExists($tableName)) {
+ $this->logger->error('{db} is missing', [ 'db' => $prefixedTableName]);
+ throw new PreconditionTableIsMissingException('Table ' . $prefixedTableName . ' does not exist');
+ }
+
+ if (empty($columnNames)) {
+ return; // Only the table is checked
+ }
+
+ $schema = $this->connection->createSchema();
+ $table = $schema->getTable($prefixedTableName);
+
+ if (is_string($columnNames)) {
+ $columnNames = [$columnNames];
}
- throw new InvalidClassException('Unexpected. Schema is an instance of ' . get_class($this->schema) . '(caller: ' . self::formatCaller() . ')');
+
+ foreach ($columnNames as $columnName) {
+ if (!$table->hasColumn($columnName)) {
+ $this->logger->error('{db} is missing column \'{column}\'', [ 'db' => $prefixedTableName, 'column' => $columnName]);
+ throw new PreconditionColumnIsMissingException('Column ' . $columnName . ' does not exist in ' . $prefixedTableName);
+ }
+ }
+
+ return;
}
private static function formatCaller(int $skip = 1): string {
diff --git a/lib/Db/V4/IndexManager.php b/lib/Db/V4/IndexManager.php
index 29f3a92e73..f57a55cfc8 100644
--- a/lib/Db/V4/IndexManager.php
+++ b/lib/Db/V4/IndexManager.php
@@ -9,24 +9,22 @@
namespace OCA\Polls\Db\V4;
use Doctrine\DBAL\Schema\Exception\IndexDoesNotExist;
-use Doctrine\DBAL\Schema\Schema;
use Exception;
use OCA\Polls\Migration\V4\TableSchema;
-use OCP\DB\ISchemaWrapper;
use OCP\IConfig;
use OCP\IDBConnection;
+use Psr\Log\LoggerInterface;
/** @psalm-suppress UnusedClass */
class IndexManager extends DbManager {
- // private Schema|ISchemaWrapper $schema;
-
/** @psalm-suppress PossiblyUnusedMethod */
public function __construct(
protected IConfig $config,
protected IDBConnection $connection,
+ protected LoggerInterface $logger,
) {
- parent::__construct($config, $connection);
+ parent::__construct($config, $connection, $logger);
}
/**
diff --git a/lib/Db/V4/TableManager.php b/lib/Db/V4/TableManager.php
index 4003694410..614ab5f327 100644
--- a/lib/Db/V4/TableManager.php
+++ b/lib/Db/V4/TableManager.php
@@ -8,7 +8,6 @@
namespace OCA\Polls\Db\V4;
-use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Type;
use Exception;
use OCA\Polls\AppConstants;
@@ -19,6 +18,7 @@
use OCA\Polls\Db\Share;
use OCA\Polls\Db\VoteMapper;
use OCA\Polls\Db\Watch;
+use OCA\Polls\Exceptions\PreconditionException;
use OCA\Polls\Helper\Hash;
use OCA\Polls\Migration\V4\TableSchema;
use OCP\DB\QueryBuilder\IQueryBuilder;
@@ -34,17 +34,17 @@ class TableManager extends DbManager {
public function __construct(
protected IConfig $config,
protected IDBConnection $connection,
- private LoggerInterface $logger,
+ protected LoggerInterface $logger,
private OptionMapper $optionMapper,
private VoteMapper $voteMapper,
) {
- parent::__construct($config, $connection);
+ parent::__construct($config, $connection, $logger);
}
/**
- * @return string[]
+ * Purge all tables and all data
*
- * @psalm-return non-empty-list
+ * @return string[] Messages as array
*/
public function purgeTables(): array {
$messages = [];
@@ -111,7 +111,10 @@ public function purgeTables(): array {
}
/**
- * @return string[]
+ * Remove the watch table if it exists
+ * Used as shorthand to reset the primary key autoincrement value
+ *
+ * @return string[] Messages as array
*/
public function removeWatch(): array {
$messages = [];
@@ -125,9 +128,9 @@ public function removeWatch(): array {
}
/**
- * @return string[]
+ * Create or update a table defined in TableSchema::TABLES
*
- * @psalm-return non-empty-list
+ * @return string[] Messages as array
*/
public function createTable(string $tableName): array {
$this->needsSchema();
@@ -172,9 +175,9 @@ public function createTable(string $tableName): array {
}
/**
- * @return string[]
+ * Create all tables defined in TableSchema::TABLES
*
- * @psalm-return non-empty-list
+ * @return string[] Messages as array
*/
public function createTables(): array {
$this->needsSchema();
@@ -188,6 +191,8 @@ public function createTables(): array {
/**
* Remove obsolete tables if they still exist
+ *
+ * @return string[] Messages as array
*/
public function removeObsoleteTables(): array {
$dropped = false;
@@ -207,23 +212,28 @@ public function removeObsoleteTables(): array {
return $messages;
}
+ /**
+ * Remove obsolete columns if they still exist
+ *
+ * @return string[] Messages as array
+ */
public function removeObsoleteColumns(): array {
$messages = [];
$dropped = false;
foreach (TableSchema::GONE_COLUMNS as $tableName => $columns) {
- $tableName = $this->dbPrefix . $tableName;
- if (!$this->schema->hasTable($tableName)) {
+ $prefixedTableName = $this->dbPrefix . $tableName;
+ if (!$this->schema->hasTable($prefixedTableName)) {
continue;
}
- $table = $this->schema->getTable($tableName);
+ $table = $this->schema->getTable($prefixedTableName);
foreach ($columns as $columnName) {
if ($table->hasColumn($columnName)) {
$dropped = true;
$table->dropColumn($columnName);
- $messages[] = 'Dropped ' . $columnName . ' from ' . $tableName;
+ $messages[] = 'Dropped ' . $columnName . ' from ' . $prefixedTableName;
}
}
}
@@ -244,8 +254,11 @@ public function removeObsoleteColumns(): array {
*
* This method is used to clean up orphaned entries in the database and
* is used by the occ command `occ polls:db:rebuild and while updating
+ *
+ * @return string[] Messages as array
*/
public function removeOrphaned(): array {
+ $orphanedCount = [];
// collects all pollIds
$subqueryPolls = $this->connection->getQueryBuilder();
$subqueryPolls->selectDistinct('id')->from(Poll::TABLE);
@@ -266,10 +279,10 @@ public function removeOrphaned(): array {
)
);
$executed = $query->executeStatement();
- if (isset($orphaned[$tableName])) {
- $orphaned[$tableName] += $executed;
+ if (isset($orphanedCount[$tableName])) {
+ $orphanedCount[$tableName] += $executed;
} else {
- $orphaned[$tableName] = $executed;
+ $orphanedCount[$tableName] = $executed;
}
}
}
@@ -289,7 +302,7 @@ public function removeOrphaned(): array {
$query->expr()->isNull('group_id')
)
);
- $orphaned[Share::TABLE] = $query->executeStatement();
+ $orphanedCount[Share::TABLE] = $query->executeStatement();
// delete all orphaned entries from the poll-group-relation (group_id or poll_id are NULL or not in the polls or poll groups table)
$query = $this->connection->getQueryBuilder();
@@ -306,20 +319,19 @@ public function removeOrphaned(): array {
$query->expr()->isNull('group_id')
)
);
- $orphaned[PollGroup::RELATION_TABLE] = $query->executeStatement();
-
+ $orphanedCount[PollGroup::RELATION_TABLE] = $query->executeStatement();
// finally delete all polls with id === null
$query = $this->connection->getQueryBuilder();
$query->delete(Poll::TABLE)
->where($query->expr()->isNull('id'));
- $orphaned[Poll::TABLE] = $query->executeStatement();
+ $orphanedCount[Poll::TABLE] = $query->executeStatement();
$messages = [];
- foreach ($orphaned as $type => $count) {
+ foreach ($orphanedCount as $tableName => $count) {
if ($count > 0) {
$this->logger->info(
- 'Purged ' . $count . ' orphaned record(s) from ' . $type,
- ['count' => $count, 'type' => $type]
+ 'Purged {count} orphaned record(s) from {tableName}',
+ ['count' => $count, 'tableName' => $tableName]
);
}
}
@@ -328,9 +340,9 @@ public function removeOrphaned(): array {
}
/**
- * @return string[]
+ * Delete all duplicate entries in all tables based on the unique indices defined in TableSchema::UNIQUE_INDICES
*
- * @psalm-return list
+ * @return string[] Messages as array
*/
public function deleteAllDuplicates(?IOutput $output = null): array {
$messages = [];
@@ -355,6 +367,8 @@ public function deleteAllDuplicates(?IOutput $output = null): array {
/**
* Delete entries per timestamp
+ *
+ * @return string Message
*/
public function tidyWatchTable(int $offset): string {
$query = $this->connection->getQueryBuilder();
@@ -373,14 +387,22 @@ public function tidyWatchTable(int $offset): string {
return 'Watch table is clean';
}
+ /**
+ * Delete duplicate entries in $table based on $columns
+ * Keep the entry with the lowest id
+ *
+ * @param string $table
+ * @param array $columns
+ * @return int number of deleted entries
+ */
private function deleteDuplicates(string $table, array $columns):int {
$this->needsSchema();
- $qb = $this->connection->getQueryBuilder();
-
if (!$this->schema->hasTable($this->dbPrefix . $table)) {
return 0;
}
+ $qb = $this->connection->getQueryBuilder();
+
// identify duplicates
$selection = $qb->selectDistinct('t1.id')
->from($table, 't1')
@@ -409,6 +431,8 @@ private function deleteDuplicates(string $table, array $columns):int {
/**
* Tidy migrations table and remove obsolete migration entries.
+ *
+ * @return string[] Messages as array
*/
public function removeObsoleteMigrations(): array {
$messages = [];
@@ -425,6 +449,14 @@ public function removeObsoleteMigrations(): array {
return $messages;
}
+ /**
+ * Fix all votes with option text not matching the option text in the options table
+ * Precondition: The options table has to have a duration column
+ * This method is used to fix votes which were cast while the option text was changing
+ * because of a duration change.
+ *
+ * This method is used by the occ command `occ polls:db:rebuild` and while updating
+ */
public function fixVotes(): void {
if (!$this->schema->hasTable($this->dbPrefix . OptionMapper::TABLE)) {
return;
@@ -447,89 +479,103 @@ public function fixVotes(): void {
}
}
+ /**
+ * Fix all shares with nullish group_id or poll_id
+ * Precondition have to be checked before
+ *
+ * @return string[] Messages as array
+ */
public function fixNullishShares(): array {
$messages = [];
- $query = $this->connection->getQueryBuilder();
- $schema = $this->connection->createSchema();
-
- if (!$schema->hasTable($this->dbPrefix . Share::TABLE)) {
- $messages[] = 'Table ' . $this->dbPrefix . Share::TABLE . ' does not exist';
- return $messages;
- }
- $table = $schema->getTable($this->dbPrefix . Share::TABLE);
+ try {
+ $tableName = Share::TABLE;
+ $affectedColumns = ['group_id', 'poll_id'];
+ $this->checkPrecondition($tableName, $affectedColumns);
- if ($table->hasColumn('group_id')) {
- // replace all nullish group_ids with 0 in share table
- $query->update(Share::TABLE)
- ->set('group_id', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))
- ->where($query->expr()->isNull('group_id'));
- $count = $query->executeStatement();
+ // set all nullish group_id and poll_id to 0
+ foreach ($affectedColumns as $affectedColumn) {
+ $count = $this->migrateNullishColumnToZero($tableName, $affectedColumn);
- if ($count > 0) {
- $messages[] = 'Updated ' . $count . ' shares with nullish group_id and set group_id to 0';
+ if ($count > 0) {
+ $messages[] = 'Updated ' . $count . ' shares with nullish ' . $affectedColumn . ' to 0';
+ }
}
- }
- // replace all nullish poll_id with 0 in share table
- $query = $this->connection->getQueryBuilder();
- $query->update(Share::TABLE)
- ->set('poll_id', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))
- ->where($query->expr()->isNull('poll_id'));
-
- $count = $query->executeStatement();
-
- if ($count > 0) {
- $messages[] = 'Updated ' . $count . ' shares and set poll_id to 0 for nullish values';
+ } catch (PreconditionException $e) {
+ $messages[] = $e->getMessage() . ' - aborted fix nullish shares';
+ return $messages;
}
if (empty($messages)) {
- return ['All shares are valid'];
+ $messages[] = 'All shares are valid';
}
return $messages;
}
+ /**
+ * Fix all poll group relations with nullish group_id or poll_id
+ * Precondition have to be checked before
+ *
+ * @return string[] Messages as array
+ */
public function fixNullishPollGroupRelations(): array {
$messages = [];
- $query = $this->connection->getQueryBuilder();
- $schema = $this->connection->createSchema();
-
- if (!$schema->hasTable($this->dbPrefix . PollGroup::RELATION_TABLE)) {
- $messages[] = 'Table ' . $this->dbPrefix . PollGroup::RELATION_TABLE . ' does not exist';
- return $messages;
- }
-
- // replace all nullish group_ids with 0 in share table
- $query->update(PollGroup::RELATION_TABLE)
- ->set('group_id', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))
- ->where($query->expr()->isNull('group_id'));
-
- $count = $query->executeStatement();
- if ($count > 0) {
- $messages[] = 'Updated ' . $count . ' pollgroup relations and set group_id to 0 for nullish values';
- }
+ try {
+ $tableName = PollGroup::RELATION_TABLE;
+ $affectedColumns = ['group_id', 'poll_id'];
+ $this->checkPrecondition($tableName, $affectedColumns);
- // replace all nullish poll_id with 0 in share table
- $query = $this->connection->getQueryBuilder();
- $query->update(PollGroup::RELATION_TABLE)
- ->set('poll_id', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))
- ->where($query->expr()->isNull('poll_id'));
+ $countAll = 0;
+ // set all nullish group_id and poll_id to 0
+ foreach ($affectedColumns as $affectedColumn) {
+ $updateCount = $this->migrateNullishColumnToZero($tableName, $affectedColumn);
- $count = $query->executeStatement();
+ if ($updateCount > 0) {
+ $countAll += $updateCount;
+ $messages[] = 'Updated ' . $updateCount . ' pollgroup relations and set ' . $affectedColumn . ' to 0 for nullish values';
+ }
+ }
- if ($count > 0) {
- $messages[] = 'Updated ' . $count . ' poll group relations and set poll_id to 0 for nullish values';
+ } catch (PreconditionException $e) {
+ $messages[] = $e->getMessage() . ' - aborted fix nullish poll group relations';
+ return $messages;
}
- if (empty($messages)) {
- return ['All poll group relations are valid'];
+ if ($countAll === 0) {
+ $messages[] = 'All poll group relations are valid';
}
return $messages;
}
+ /**
+ * Migrate all nullish values in $columnName of $tableName to 0
+ *
+ * @param string $tableName Unprefixed tablename
+ * @param string $columnName Column name to update
+ *
+ * @return int number of updated entries
+ */
+ private function migrateNullishColumnToZero(string $tableName, string $columnName): int {
+ $query = $this->connection->getQueryBuilder();
+ $query->update($tableName)
+ ->set($columnName, $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))
+ ->where($query->expr()->isNull($columnName));
+
+ $count = $query->executeStatement();
+ return $count;
+ }
+
+ /**
+ * Set last interaction to current timestamp for all polls
+ * where last interaction is 0
+ *
+ * @param int|null $timestamp
+ * @return string
+ */
public function setLastInteraction(?int $timestamp = null): string {
$timestamp = $timestamp ?? time();
$query = $this->connection->getQueryBuilder();
@@ -550,29 +596,33 @@ public function setLastInteraction(?int $timestamp = null): string {
}
/**
- * @return string[]
+ * Update all option and vote hashes
+ * Ensures the preconditions are met
*
- * @psalm-return list{0?: string,...}
+ * @return string[] Messages as array
*/
- private function updateVoteHashes(Schema &$schema): array {
- $messages = [];
- if (!$schema->hasTable($this->dbPrefix . VoteMapper::TABLE)) {
- $this->logger->error('{db} is missing- aborted recalculating hashes', [
- 'db' => $this->dbPrefix . VoteMapper::TABLE
- ]);
- $messages[] = 'Table ' . $this->dbPrefix . VoteMapper::TABLE . ' does not exist';
- return $messages;
- }
+ public function updateHashes(): array {
+ // Do not catch any exceptions but let any operation break to ensure hash updates can be performed
+ // Otherwise data loss of votes can occur
+ $this->checkPrecondition(OptionMapper::TABLE, ['poll_id', 'poll_option_text', 'poll_option_hash']);
+ $this->checkPrecondition(VoteMapper::TABLE, ['poll_id', 'vote_option_text', 'vote_option_hash']);
- $table = $schema->getTable($this->dbPrefix . VoteMapper::TABLE);
+ $messages = $this->updateOptionHashes();
+ $messages = array_merge($messages, $this->updateVoteHashes());
+ return $messages;
+ }
- if (!$table->hasColumn('vote_option_hash')) {
- $this->logger->error('{db} is missing column \'poll_option_hash\' - aborted recalculating hashes', [
- 'db' => $this->dbPrefix . VoteMapper::TABLE
- ]);
- $messages[] = 'Column \'vote_option_hash\' does not exist in ' . $this->dbPrefix . VoteMapper::TABLE;
- return $messages;
- }
+ /**
+ * Update all vote hashes
+ * Precondition have to be checked before
+ *
+ * @return string[] Messages as array
+ */
+ private function updateVoteHashes(): array {
+ $messages = [];
+
+ $tableName = VoteMapper::TABLE;
+ $prefixedTableName = $this->dbPrefix . $tableName;
$count = 0;
$updated = 0;
@@ -600,7 +650,7 @@ private function updateVoteHashes(Schema &$schema): array {
if ($updated === 0) {
$this->logger->info('Verified {count} vote hashes in {db}', [
'count' => $count,
- 'db' => $this->dbPrefix . VoteMapper::TABLE
+ 'db' => $prefixedTableName
]);
$messages[] = 'No vote hashes to update';
@@ -608,7 +658,7 @@ private function updateVoteHashes(Schema &$schema): array {
$this->logger->info('Updated {updated} hashes of {count} votes in {db}', [
'updated' => $updated,
'count' => $count,
- 'db' => $this->dbPrefix . VoteMapper::TABLE
+ 'db' => $prefixedTableName
]);
$messages[] = 'Updated ' . $updated . ' vote hashes';
@@ -618,25 +668,16 @@ private function updateVoteHashes(Schema &$schema): array {
}
/**
- * @return string[]
+ * Update all option hashes
+ * Precondition have to be checked before
*
- * @psalm-return list{0?: string,...}
+ * @return string[] Messages as array
*/
- private function updateOptionHashes(Schema &$schema): array {
+ private function updateOptionHashes(): array {
$messages = [];
- if (!$schema->hasTable($this->dbPrefix . OptionMapper::TABLE)) {
- $this->logger->error('{db} is missing - aborted recalculating hashes', [ 'db' => $this->dbPrefix . OptionMapper::TABLE]);
- $messages[] = 'Table ' . $this->dbPrefix . OptionMapper::TABLE . ' does not exist';
- return $messages;
- }
- $table = $schema->getTable($this->dbPrefix . OptionMapper::TABLE);
-
- if (!$table->hasColumn('poll_option_hash')) {
- $this->logger->error('{db} is missing column \'poll_option_hash\' - aborted recalculating hashes', [ 'db' => $this->dbPrefix . OptionMapper::TABLE]);
- $messages[] = 'Column \'poll_option_hash\' does not exist in ' . $this->dbPrefix . OptionMapper::TABLE;
- return $messages;
- }
+ $tableName = OptionMapper::TABLE;
+ $prefixedTableName = $this->dbPrefix . $tableName;
$count = 0;
$updated = 0;
@@ -661,7 +702,7 @@ private function updateOptionHashes(Schema &$schema): array {
if ($updated === 0) {
$this->logger->info('Verified {count} option hashes in {db}', [
'count' => $count,
- 'db' => $this->dbPrefix . OptionMapper::TABLE
+ 'db' => $prefixedTableName
]);
$messages[] = 'No option hashes to update';
@@ -669,7 +710,7 @@ private function updateOptionHashes(Schema &$schema): array {
$this->logger->info('Updated {updated} hashes of {count} options in {db}', [
'updated' => $updated,
'count' => $count,
- 'db' => $this->dbPrefix . OptionMapper::TABLE
+ 'db' => $prefixedTableName
]);
$messages[] = 'Updated ' . $updated . ' option hashes';
@@ -678,55 +719,90 @@ private function updateOptionHashes(Schema &$schema): array {
return $messages;
}
- public function updateHashes(): array {
- $schema = $this->connection->createSchema();
- $messages = $this->updateOptionHashes($schema);
- $messages = array_merge($messages, $this->updateVoteHashes($schema));
- return $messages;
- }
-
/**
- * @return string[]
+ * Migrate all share labels to display_name
+ *
+ * @return string[] Messages as array
*
- * @psalm-return list{0?: string,...}
*/
public function migrateShareLabels(): array {
- $schema = $this->connection->createSchema();
$messages = [];
- if (!$schema->hasTable($this->dbPrefix . Share::TABLE)) {
- $this->logger->error('{db} is missing - aborted migrating labels', [ 'db' => $this->dbPrefix . Share::TABLE]);
- $messages[] = 'Table ' . $this->dbPrefix . Share::TABLE . ' does not exist';
- return $messages;
- }
- $table = $schema->getTable($this->dbPrefix . Share::TABLE);
+ $tableName = Share::TABLE;
+ $affectedColumn = 'label';
- if (!$table->hasColumn('label')) {
- $this->logger->error('{db} is missing column \'label\' - aborted migrating labels', [ 'db' => $this->dbPrefix . Share::TABLE]);
- $messages[] = 'Column \'label\' does not exist in ' . $this->dbPrefix . Share::TABLE;
+ try {
+ $this->checkPrecondition($tableName, $affectedColumn);
+ } catch (PreconditionException $e) {
+ $messages[] = $e->getMessage() . ' - aborted migrating labels';
return $messages;
}
+ $prefixedTableName = $this->dbPrefix . $tableName;
$qb = $this->connection->getQueryBuilder();
- $qb->update(Share::TABLE)
- ->set('display_name', 'label') // safe: assigns column B's value into A
- ->andWhere($qb->expr()->isNotNull(Share::TABLE . '.label'))
- ->andWhere($qb->expr()->eq(Share::TABLE . '.label', $qb->expr()->literal('')));
+ $qb->update($tableName)
+ ->set('display_name', $affectedColumn)
+ ->andWhere($qb->expr()->isNotNull($tableName . '.' . $affectedColumn))
+ ->andWhere($qb->expr()->eq($tableName . '.' . $affectedColumn, $qb->expr()->literal('')));
$updated = $qb->executeStatement();
if ($updated === 0) {
$this->logger->info('Verified all share labels in {db}', [
- 'db' => $this->dbPrefix . Share::TABLE
+ 'db' => $prefixedTableName
]);
$messages[] = 'No share labels to update';
} else {
- $this->logger->info('Updated {updated} labels in {db}', [
+ $this->logger->info('Updated {updated} share labels in {db}', [
'updated' => $updated,
- 'db' => $this->dbPrefix . Share::TABLE
+ 'db' => $prefixedTableName
]);
- $messages[] = 'Updated ' . $updated . ' option hashes';
+ $messages[] = 'Updated ' . $updated . ' labels';
+ }
+
+ return $messages;
+ }
+
+ /**
+ * Migrate all polls with access 'public' to access 'open'
+ *
+ * @return string[] Messages as array
+ *
+ */
+ public function migratePublicToOpen(): array {
+ $messages = [];
+
+ $tableName = Poll::TABLE;
+ $affectedColumn = 'access';
+ $prefixedTableName = $this->dbPrefix . $tableName;
+
+ try {
+ $this->checkPrecondition($tableName, $affectedColumn);
+ } catch (PreconditionException $e) {
+ $messages[] = $e->getMessage() . ' - aborted migrating public to open';
+ return $messages;
+ }
+
+ $qb = $this->connection->getQueryBuilder();
+
+ $qb->update($tableName)
+ ->set('access', $qb->expr()->literal(Poll::ACCESS_OPEN))
+ ->where($qb->expr()->eq($tableName . '.' . $affectedColumn, $qb->expr()->literal(Poll::ACCESS_PUBLIC)));
+ $updated = $qb->executeStatement();
+
+ if ($updated === 0) {
+ $this->logger->info('Verified poll access to be \'open\' instead of \'public\' in {db}', [
+ 'db' => $prefixedTableName
+ ]);
+ $messages[] = 'No poll access to update';
+
+ } else {
+ $this->logger->info('Updated {updated} access in {db}', [
+ 'updated' => $updated,
+ 'db' => $prefixedTableName
+ ]);
+ $messages[] = 'Updated ' . $updated . ' poll accesses';
}
diff --git a/lib/Exceptions/PreconditionColumnIsMissingException.php b/lib/Exceptions/PreconditionColumnIsMissingException.php
new file mode 100644
index 0000000000..a8f417e4c3
--- /dev/null
+++ b/lib/Exceptions/PreconditionColumnIsMissingException.php
@@ -0,0 +1,17 @@
+connection->tableExists(Poll::TABLE)) {
try {
$this->tableManager->updateHashes();
+ $this->tableManager->migratePublicToOpen();
+ $this->tableManager->migrateShareLabels();
$this->tableManager->removeOrphaned();
$this->tableManager->deleteAllDuplicates();
$this->tableManager->tidyWatchTable(time());
diff --git a/lib/Migration/RepairSteps/MigratePublicToOpen.php b/lib/Migration/RepairSteps/MigratePublicToOpen.php
new file mode 100644
index 0000000000..670bfbb617
--- /dev/null
+++ b/lib/Migration/RepairSteps/MigratePublicToOpen.php
@@ -0,0 +1,37 @@
+tableManager->setConnection($this->connection);
+
+ $messages = $this->tableManager->migratePublicToOpen();
+ foreach ($messages as $message) {
+ $output->info($message);
+ }
+
+ }
+}