kaliop-uk /
ezmigrationbundle
| 1 | <?php |
||||||||
| 2 | |||||||||
| 3 | namespace Kaliop\eZMigrationBundle\Core\StorageHandler\Database; |
||||||||
| 4 | |||||||||
| 5 | use Doctrine\DBAL\Exception\UniqueConstraintViolationException; |
||||||||
| 6 | use Doctrine\DBAL\Schema\Schema; |
||||||||
| 7 | use eZ\Publish\Core\Persistence\Database\DatabaseHandler; |
||||||||
| 8 | use eZ\Publish\Core\Persistence\Database\QueryException; |
||||||||
| 9 | use eZ\Publish\Core\Persistence\Database\SelectQuery; |
||||||||
| 10 | use Kaliop\eZMigrationBundle\API\StorageHandlerInterface; |
||||||||
| 11 | use Kaliop\eZMigrationBundle\API\Collection\MigrationCollection; |
||||||||
| 12 | use Kaliop\eZMigrationBundle\API\Exception\MigrationBundleException; |
||||||||
| 13 | use Kaliop\eZMigrationBundle\API\Value\Migration as APIMigration; |
||||||||
| 14 | use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition; |
||||||||
| 15 | |||||||||
| 16 | use Kaliop\eZMigrationBundle\API\ConfigResolverInterface; |
||||||||
| 17 | |||||||||
| 18 | /** |
||||||||
| 19 | * Database-backed storage for info on executed migrations |
||||||||
| 20 | * |
||||||||
| 21 | * @todo replace all usage of the ezcdb api with the doctrine dbal one, so that we only depend on one |
||||||||
| 22 | */ |
||||||||
| 23 | class Migration extends TableStorage implements StorageHandlerInterface |
||||||||
| 24 | { |
||||||||
| 25 | protected $fieldList = 'migration, md5, path, execution_date, status, execution_error'; |
||||||||
| 26 | |||||||||
| 27 | /** |
||||||||
| 28 | * @param DatabaseHandler $dbHandler |
||||||||
| 29 | * @param string $tableNameParameter |
||||||||
| 30 | * @param ConfigResolverInterface $configResolver |
||||||||
| 31 | * @param array $tableCreationOptions |
||||||||
| 32 | * @throws \Exception |
||||||||
| 33 | */ |
||||||||
| 34 | 149 | public function __construct(DatabaseHandler $dbHandler, $tableNameParameter = 'kaliop_migrations', ConfigResolverInterface $configResolver = null, $tableCreationOptions = array()) |
|||||||
| 35 | { |
||||||||
| 36 | 149 | parent::__construct($dbHandler, $tableNameParameter, $configResolver, $tableCreationOptions); |
|||||||
| 37 | 149 | } |
|||||||
| 38 | |||||||||
| 39 | /** |
||||||||
| 40 | * @param int $limit |
||||||||
| 41 | * @param int $offset |
||||||||
| 42 | * @return MigrationCollection |
||||||||
| 43 | */ |
||||||||
| 44 | 99 | public function loadMigrations($limit = null, $offset = null) |
|||||||
| 45 | { |
||||||||
| 46 | 99 | return $this->loadMigrationsInner(null, null, $limit, $offset); |
|||||||
| 47 | } |
||||||||
| 48 | |||||||||
| 49 | /** |
||||||||
| 50 | * @param int $status |
||||||||
| 51 | * @param int $limit |
||||||||
| 52 | * @param int $offset |
||||||||
| 53 | * @return MigrationCollection |
||||||||
| 54 | */ |
||||||||
| 55 | 2 | public function loadMigrationsByStatus($status, $limit = null, $offset = null) |
|||||||
| 56 | { |
||||||||
| 57 | 2 | return $this->loadMigrationsInner($status, null, $limit, $offset); |
|||||||
| 58 | } |
||||||||
| 59 | |||||||||
| 60 | 47 | public function loadMigrationsByPaths($paths, $limit = null, $offset = null) |
|||||||
| 61 | { |
||||||||
| 62 | 47 | return $this->loadMigrationsInner(null, $paths, $limit, $offset); |
|||||||
| 63 | } |
||||||||
| 64 | |||||||||
| 65 | /** |
||||||||
| 66 | * @param int $status |
||||||||
| 67 | * @param null|string[] $paths |
||||||||
| 68 | * @param int $limit |
||||||||
| 69 | * @param int $offset |
||||||||
| 70 | * @return MigrationCollection |
||||||||
| 71 | */ |
||||||||
| 72 | 144 | protected function loadMigrationsInner($status = null, $paths = array(), $limit = null, $offset = null) |
|||||||
| 73 | { |
||||||||
| 74 | 144 | $this->createTableIfNeeded(); |
|||||||
| 75 | |||||||||
| 76 | /** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */ |
||||||||
| 77 | 144 | $q = $this->dbHandler->createSelectQuery(); |
|||||||
| 78 | 144 | $q->select($this->fieldList) |
|||||||
|
0 ignored issues
–
show
|
|||||||||
| 79 | 144 | ->from($this->tableName) |
|||||||
|
0 ignored issues
–
show
The call to
eZ\Publish\Core\Persiste...ase\SelectQuery::from() has too many arguments starting with $this->tableName.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. Loading history...
|
|||||||||
| 80 | 144 | ->orderBy('migration', SelectQuery::ASC); |
|||||||
| 81 | 144 | if ($status !== null || (is_array($paths) && count($paths))) { |
|||||||
| 82 | 5 | $exps = []; |
|||||||
| 83 | 5 | if ($status !== null) { |
|||||||
| 84 | 2 | $exps[] = $q->expr->eq('status', $q->bindValue($status)); |
|||||||
| 85 | } |
||||||||
| 86 | 5 | if (is_array($paths)) { |
|||||||
| 87 | 3 | foreach ($paths as $i => $path) { |
|||||||
| 88 | 3 | if ($path === '') { |
|||||||
| 89 | unset($paths[$i]); |
||||||||
| 90 | } |
||||||||
| 91 | } |
||||||||
| 92 | 3 | if (count($paths)) { |
|||||||
| 93 | 3 | $pexps = array(); |
|||||||
| 94 | 3 | foreach ($paths as $path) { |
|||||||
| 95 | // NB: this works fine only as long both the paths stored in the db and the ones passed in follow |
||||||||
| 96 | // the same convention regarding path normalization |
||||||||
| 97 | /// @todo use a proper db-aware escaping function |
||||||||
| 98 | 3 | $pexps[] = $q->expr->like('path', "'" . str_replace(array('_', '%', "'"), array('\_', '\%', "''"), $path) . '%' . "'"); |
|||||||
| 99 | } |
||||||||
| 100 | } |
||||||||
| 101 | 3 | $exps[] = $q->expr->lor($pexps); |
|||||||
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
The call to
eZ\Publish\Core\Persiste...abase\Expression::lOr() has too many arguments starting with $pexps.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. Loading history...
|
|||||||||
| 102 | } |
||||||||
| 103 | 5 | $q->where($q->expr->land($exps)); |
|||||||
|
0 ignored issues
–
show
The call to
eZ\Publish\Core\Persiste...se\SelectQuery::where() has too many arguments starting with $q->expr->land($exps).
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. Loading history...
The call to
eZ\Publish\Core\Persiste...base\Expression::lAnd() has too many arguments starting with $exps.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. Loading history...
|
|||||||||
| 104 | } |
||||||||
| 105 | 144 | if ($limit > 0 || $offset > 0) { |
|||||||
| 106 | 1 | if ($limit <= 0) { |
|||||||
| 107 | $limit = null; |
||||||||
| 108 | } |
||||||||
| 109 | 1 | if ($offset == 0) { |
|||||||
|
0 ignored issues
–
show
|
|||||||||
| 110 | 1 | $offset = null; |
|||||||
| 111 | } |
||||||||
| 112 | 1 | $q->limit($limit, $offset); |
|||||||
| 113 | } |
||||||||
| 114 | 144 | $stmt = $q->prepare(); |
|||||||
| 115 | 144 | $stmt->execute(); |
|||||||
| 116 | 144 | $results = $stmt->fetchAll(); |
|||||||
| 117 | |||||||||
| 118 | 144 | $migrations = array(); |
|||||||
| 119 | 144 | foreach ($results as $result) { |
|||||||
| 120 | 84 | $migrations[$result['migration']] = $this->arrayToMigration($result); |
|||||||
| 121 | } |
||||||||
| 122 | |||||||||
| 123 | 144 | return new MigrationCollection($migrations); |
|||||||
| 124 | } |
||||||||
| 125 | |||||||||
| 126 | /** |
||||||||
| 127 | * @param string $migrationName |
||||||||
| 128 | * @return APIMigration|null |
||||||||
| 129 | */ |
||||||||
| 130 | 108 | public function loadMigration($migrationName) |
|||||||
| 131 | { |
||||||||
| 132 | 108 | $this->createTableIfNeeded(); |
|||||||
| 133 | |||||||||
| 134 | /** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */ |
||||||||
| 135 | 108 | $q = $this->dbHandler->createSelectQuery(); |
|||||||
| 136 | 108 | $q->select($this->fieldList) |
|||||||
|
0 ignored issues
–
show
The call to
eZ\Publish\Core\Persiste...e\SelectQuery::select() has too many arguments starting with $this->fieldList.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. Loading history...
|
|||||||||
| 137 | 108 | ->from($this->tableName) |
|||||||
|
0 ignored issues
–
show
The call to
eZ\Publish\Core\Persiste...ase\SelectQuery::from() has too many arguments starting with $this->tableName.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. Loading history...
|
|||||||||
| 138 | 108 | ->where($q->expr->eq('migration', $q->bindValue($migrationName))); |
|||||||
|
0 ignored issues
–
show
The call to
eZ\Publish\Core\Persiste...se\SelectQuery::where() has too many arguments starting with $q->expr->eq('migration'...dValue($migrationName)).
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. Loading history...
|
|||||||||
| 139 | 108 | $stmt = $q->prepare(); |
|||||||
| 140 | 108 | $stmt->execute(); |
|||||||
| 141 | 108 | $result = $stmt->fetch(\PDO::FETCH_ASSOC); |
|||||||
| 142 | |||||||||
| 143 | 108 | if (is_array($result) && !empty($result)) { |
|||||||
| 144 | 108 | return $this->arrayToMigration($result); |
|||||||
| 145 | } |
||||||||
| 146 | |||||||||
| 147 | 104 | return null; |
|||||||
| 148 | } |
||||||||
| 149 | |||||||||
| 150 | /** |
||||||||
| 151 | * Creates and stores a new migration (leaving it in TODO status) |
||||||||
| 152 | * @param MigrationDefinition $migrationDefinition |
||||||||
| 153 | * @return APIMigration |
||||||||
| 154 | * @throws \Exception If the migration exists already (we rely on the PK for that) |
||||||||
| 155 | */ |
||||||||
| 156 | 85 | public function addMigration(MigrationDefinition $migrationDefinition) |
|||||||
| 157 | { |
||||||||
| 158 | 85 | $this->createTableIfNeeded(); |
|||||||
| 159 | |||||||||
| 160 | 85 | $conn = $this->getConnection(); |
|||||||
| 161 | |||||||||
| 162 | 85 | $migration = new APIMigration( |
|||||||
| 163 | 85 | $migrationDefinition->name, |
|||||||
| 164 | 85 | md5($migrationDefinition->rawDefinition), |
|||||||
| 165 | 85 | $migrationDefinition->path, |
|||||||
| 166 | 85 | null, |
|||||||
| 167 | 85 | APIMigration::STATUS_TODO |
|||||||
| 168 | ); |
||||||||
| 169 | try { |
||||||||
| 170 | 85 | $conn->insert($this->tableName, $this->migrationToArray($migration)); |
|||||||
| 171 | 1 | } catch (UniqueConstraintViolationException $e) { |
|||||||
| 172 | 1 | throw new MigrationBundleException("Migration '{$migrationDefinition->name}' already exists"); |
|||||||
| 173 | } |
||||||||
| 174 | |||||||||
| 175 | 85 | return $migration; |
|||||||
| 176 | } |
||||||||
| 177 | |||||||||
| 178 | /** |
||||||||
| 179 | * Starts a migration, given its definition: stores its status in the db, returns the Migration object |
||||||||
| 180 | * |
||||||||
| 181 | * @param MigrationDefinition $migrationDefinition |
||||||||
| 182 | * @param bool $force when true, starts migrations even if they already exist in DONE, SKIPPED status |
||||||||
| 183 | * @return APIMigration |
||||||||
| 184 | * @throws \Exception if migration was already executing or already done |
||||||||
| 185 | * @todo add a parameter to allow re-execution of already-done migrations |
||||||||
| 186 | */ |
||||||||
| 187 | 89 | public function startMigration(MigrationDefinition $migrationDefinition, $force = false) |
|||||||
| 188 | { |
||||||||
| 189 | 89 | return $this->createMigration($migrationDefinition, APIMigration::STATUS_STARTED, 'started', $force); |
|||||||
| 190 | } |
||||||||
| 191 | |||||||||
| 192 | /** |
||||||||
| 193 | * Stops a migration by storing it in the db. Migration status can not be 'started' |
||||||||
| 194 | * |
||||||||
| 195 | * NB: if this call happens within another DB transaction which has already been flagged for rollback, the result |
||||||||
| 196 | * will be that a RuntimeException is thrown, as Doctrine does not allow to call commit() after rollback(). |
||||||||
| 197 | * One way to fix the problem would be not to use a transaction and select-for-update here, but since that is the |
||||||||
| 198 | * best way to insure atomic updates, I am loath to remove it. |
||||||||
| 199 | * A known workaround is to call the Doctrine Connection method setNestTransactionsWithSavepoints(true); this can |
||||||||
| 200 | * be achieved as simply as setting the parameter 'use_savepoints' in the doctrine connection configuration. |
||||||||
| 201 | * |
||||||||
| 202 | * @param APIMigration $migration |
||||||||
| 203 | * @param bool $force When true, the migration will be updated even if it was not in 'started' status |
||||||||
| 204 | * @throws \Exception If the migration was not started (unless $force=true) |
||||||||
| 205 | */ |
||||||||
| 206 | 89 | public function endMigration(APIMigration $migration, $force = false) |
|||||||
| 207 | { |
||||||||
| 208 | 89 | if ($migration->status == APIMigration::STATUS_STARTED) { |
|||||||
| 209 | throw new MigrationBundleException($this->getEntityName($migration)." '{$migration->name}' can not be ended as its status is 'started'..."); |
||||||||
| 210 | } |
||||||||
| 211 | |||||||||
| 212 | 89 | $this->createTableIfNeeded(); |
|||||||
| 213 | |||||||||
| 214 | // select for update |
||||||||
| 215 | |||||||||
| 216 | // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders... |
||||||||
| 217 | // at least the doctrine one allows us to still use parameter binding when we add our sql particle |
||||||||
| 218 | 89 | $conn = $this->getConnection(); |
|||||||
| 219 | |||||||||
| 220 | 89 | $qb = $conn->createQueryBuilder(); |
|||||||
| 221 | 89 | $qb->select('*') |
|||||||
| 222 | 89 | ->from($this->tableName, 'm') |
|||||||
| 223 | 89 | ->where('migration = ?'); |
|||||||
| 224 | 89 | $sql = $qb->getSQL() . ' FOR UPDATE'; |
|||||||
| 225 | |||||||||
| 226 | 89 | $conn->beginTransaction(); |
|||||||
| 227 | |||||||||
| 228 | 89 | $stmt = $conn->executeQuery($sql, array($migration->name)); |
|||||||
| 229 | 89 | $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC); |
|||||||
| 230 | |||||||||
| 231 | // fail if it was not executing |
||||||||
| 232 | |||||||||
| 233 | 89 | if (!is_array($existingMigrationData)) { |
|||||||
| 234 | // commit to release the lock |
||||||||
| 235 | $conn->commit(); |
||||||||
| 236 | throw new MigrationBundleException($this->getEntityName($migration)." '{$migration->name}' can not be ended as it is not found"); |
||||||||
| 237 | } |
||||||||
| 238 | |||||||||
| 239 | 89 | if (($existingMigrationData['status'] != APIMigration::STATUS_STARTED) && !$force) { |
|||||||
| 240 | // commit to release the lock |
||||||||
| 241 | $conn->commit(); |
||||||||
| 242 | throw new MigrationBundleException($this->getEntityName($migration)." '{$migration->name}' can not be ended as it is not executing"); |
||||||||
| 243 | } |
||||||||
| 244 | |||||||||
| 245 | 89 | $conn->update( |
|||||||
| 246 | 89 | $this->tableName, |
|||||||
| 247 | array( |
||||||||
| 248 | 89 | 'status' => $migration->status, |
|||||||
| 249 | /// @todo use mb_substr (if all dbs we support count col length not in bytes but in chars...) |
||||||||
| 250 | 89 | 'execution_error' => substr($migration->executionError, 0, 4000), |
|||||||
| 251 | 89 | 'execution_date' => $migration->executionDate |
|||||||
| 252 | ), |
||||||||
| 253 | 89 | array('migration' => $migration->name) |
|||||||
| 254 | ); |
||||||||
| 255 | |||||||||
| 256 | 89 | $conn->commit(); |
|||||||
| 257 | 89 | } |
|||||||
| 258 | |||||||||
| 259 | /** |
||||||||
| 260 | * Removes a Migration from the table - regardless of its state! |
||||||||
| 261 | * |
||||||||
| 262 | * @param APIMigration $migration |
||||||||
| 263 | */ |
||||||||
| 264 | 108 | public function deleteMigration(APIMigration $migration) |
|||||||
| 265 | { |
||||||||
| 266 | 108 | $this->createTableIfNeeded(); |
|||||||
| 267 | 108 | $conn = $this->getConnection(); |
|||||||
| 268 | 108 | $conn->delete($this->tableName, array('migration' => $migration->name)); |
|||||||
| 269 | 108 | } |
|||||||
| 270 | |||||||||
| 271 | /** |
||||||||
| 272 | * Skips a migration by storing it in the db. Migration status can not be 'started' |
||||||||
| 273 | * |
||||||||
| 274 | * @param MigrationDefinition $migrationDefinition |
||||||||
| 275 | * @return APIMigration |
||||||||
| 276 | * @throws \Exception If the migration was already executed or is executing |
||||||||
| 277 | */ |
||||||||
| 278 | 1 | public function skipMigration(MigrationDefinition $migrationDefinition) |
|||||||
| 279 | { |
||||||||
| 280 | 1 | return $this->createMigration($migrationDefinition, APIMigration::STATUS_SKIPPED, 'skipped'); |
|||||||
| 281 | } |
||||||||
| 282 | |||||||||
| 283 | /** |
||||||||
| 284 | * @param MigrationDefinition $migrationDefinition |
||||||||
| 285 | * @param int $status NB: atm not all statuses are supported the same way. IE. logical checks against the db data |
||||||||
| 286 | * will be done for some statuses but not for others |
||||||||
| 287 | * @param string $action |
||||||||
| 288 | * @param bool $force |
||||||||
| 289 | * @return APIMigration |
||||||||
| 290 | * @throws \Exception |
||||||||
| 291 | */ |
||||||||
| 292 | 90 | protected function createMigration(MigrationDefinition $migrationDefinition, $status, $action, $force = false) |
|||||||
| 293 | { |
||||||||
| 294 | 90 | $this->createTableIfNeeded(); |
|||||||
| 295 | |||||||||
| 296 | // select for update |
||||||||
| 297 | |||||||||
| 298 | // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders... |
||||||||
| 299 | // at least the doctrine one allows us to still use parameter binding when we add our sql particle |
||||||||
| 300 | 90 | $conn = $this->getConnection(); |
|||||||
| 301 | |||||||||
| 302 | 90 | $qb = $conn->createQueryBuilder(); |
|||||||
| 303 | 90 | $qb->select('*') |
|||||||
| 304 | 90 | ->from($this->tableName, 'm') |
|||||||
| 305 | 90 | ->where('migration = ?'); |
|||||||
| 306 | 90 | $sql = $qb->getSQL() . ' FOR UPDATE'; |
|||||||
| 307 | |||||||||
| 308 | 90 | $conn->beginTransaction(); |
|||||||
| 309 | |||||||||
| 310 | 90 | $stmt = $conn->executeQuery($sql, array($migrationDefinition->name)); |
|||||||
| 311 | 90 | $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC); |
|||||||
| 312 | |||||||||
| 313 | 90 | if (is_array($existingMigrationData)) { |
|||||||
| 314 | // migration exists |
||||||||
| 315 | |||||||||
| 316 | // fail if it was already executing |
||||||||
| 317 | 71 | if ($existingMigrationData['status'] == APIMigration::STATUS_STARTED) { |
|||||||
| 318 | // commit to release the lock |
||||||||
| 319 | $conn->commit(); |
||||||||
| 320 | throw new MigrationBundleException("Migration '{$migrationDefinition->name}' can not be $action as it is already executing"); |
||||||||
| 321 | } |
||||||||
| 322 | // fail if it was already already done, unless in 'force' mode |
||||||||
| 323 | 71 | if (!$force) { |
|||||||
| 324 | 71 | if ($existingMigrationData['status'] == APIMigration::STATUS_DONE) { |
|||||||
| 325 | // commit to release the lock |
||||||||
| 326 | $conn->commit(); |
||||||||
| 327 | throw new MigrationBundleException("Migration '{$migrationDefinition->name}' can not be $action as it was already executed"); |
||||||||
| 328 | } |
||||||||
| 329 | 71 | if ($existingMigrationData['status'] == APIMigration::STATUS_SKIPPED) { |
|||||||
| 330 | // commit to release the lock |
||||||||
| 331 | $conn->commit(); |
||||||||
| 332 | throw new MigrationBundleException("Migration '{$migrationDefinition->name}' can not be $action as it was already skipped"); |
||||||||
| 333 | } |
||||||||
| 334 | } |
||||||||
| 335 | |||||||||
| 336 | // do not set migration start date if we are skipping it |
||||||||
| 337 | 71 | $migration = new APIMigration( |
|||||||
| 338 | 71 | $migrationDefinition->name, |
|||||||
| 339 | 71 | md5($migrationDefinition->rawDefinition), |
|||||||
| 340 | 71 | $migrationDefinition->path, |
|||||||
| 341 | 71 | ($status == APIMigration::STATUS_SKIPPED ? null : time()), |
|||||||
| 342 | $status |
||||||||
| 343 | ); |
||||||||
| 344 | 71 | $conn->update( |
|||||||
| 345 | 71 | $this->tableName, |
|||||||
| 346 | array( |
||||||||
| 347 | 71 | 'execution_date' => $migration->executionDate, |
|||||||
| 348 | 71 | 'status' => $status, |
|||||||
| 349 | 'execution_error' => null |
||||||||
| 350 | ), |
||||||||
| 351 | 71 | array('migration' => $migrationDefinition->name) |
|||||||
| 352 | ); |
||||||||
| 353 | 71 | $conn->commit(); |
|||||||
| 354 | |||||||||
| 355 | } else { |
||||||||
| 356 | // migration did not exist. Create it! |
||||||||
| 357 | |||||||||
| 358 | // commit immediately, to release the lock and avoid deadlocks |
||||||||
| 359 | 19 | $conn->commit(); |
|||||||
| 360 | |||||||||
| 361 | 19 | $migration = new APIMigration( |
|||||||
| 362 | 19 | $migrationDefinition->name, |
|||||||
| 363 | 19 | md5($migrationDefinition->rawDefinition), |
|||||||
| 364 | 19 | $migrationDefinition->path, |
|||||||
| 365 | 19 | ($status == APIMigration::STATUS_SKIPPED ? null : time()), |
|||||||
| 366 | $status |
||||||||
| 367 | ); |
||||||||
| 368 | 19 | $conn->insert($this->tableName, $this->migrationToArray($migration)); |
|||||||
| 369 | } |
||||||||
| 370 | |||||||||
| 371 | 90 | return $migration; |
|||||||
| 372 | } |
||||||||
| 373 | |||||||||
| 374 | 1 | public function resumeMigration(APIMigration $migration) |
|||||||
| 375 | { |
||||||||
| 376 | 1 | $this->createTableIfNeeded(); |
|||||||
| 377 | |||||||||
| 378 | // select for update |
||||||||
| 379 | |||||||||
| 380 | // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders... |
||||||||
| 381 | // at least the doctrine one allows us to still use parameter binding when we add our sql particle |
||||||||
| 382 | 1 | $conn = $this->getConnection(); |
|||||||
| 383 | |||||||||
| 384 | 1 | $qb = $conn->createQueryBuilder(); |
|||||||
| 385 | 1 | $qb->select('*') |
|||||||
| 386 | 1 | ->from($this->tableName, 'm') |
|||||||
| 387 | 1 | ->where('migration = ?'); |
|||||||
| 388 | 1 | $sql = $qb->getSQL() . ' FOR UPDATE'; |
|||||||
| 389 | |||||||||
| 390 | 1 | $conn->beginTransaction(); |
|||||||
| 391 | |||||||||
| 392 | 1 | $stmt = $conn->executeQuery($sql, array($migration->name)); |
|||||||
| 393 | 1 | $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC); |
|||||||
| 394 | |||||||||
| 395 | 1 | if (!is_array($existingMigrationData)) { |
|||||||
| 396 | // commit immediately, to release the lock and avoid deadlocks |
||||||||
| 397 | $conn->commit(); |
||||||||
| 398 | throw new MigrationBundleException($this->getEntityName($migration)." '{$migration->name}' can not be resumed as it is not found"); |
||||||||
| 399 | } |
||||||||
| 400 | |||||||||
| 401 | // migration exists |
||||||||
| 402 | |||||||||
| 403 | // fail if it was not suspended |
||||||||
| 404 | 1 | if ($existingMigrationData['status'] != APIMigration::STATUS_SUSPENDED) { |
|||||||
| 405 | // commit to release the lock |
||||||||
| 406 | $conn->commit(); |
||||||||
| 407 | throw new MigrationBundleException($this->getEntityName($migration)." '{$migration->name}' can not be resumed as it is not suspended"); |
||||||||
| 408 | } |
||||||||
| 409 | |||||||||
| 410 | 1 | $migration = new APIMigration( |
|||||||
| 411 | 1 | $migration->name, |
|||||||
| 412 | 1 | $migration->md5, |
|||||||
| 413 | 1 | $migration->path, |
|||||||
| 414 | 1 | time(), |
|||||||
| 415 | 1 | APIMigration::STATUS_STARTED |
|||||||
| 416 | ); |
||||||||
| 417 | |||||||||
| 418 | 1 | $conn->update( |
|||||||
| 419 | 1 | $this->tableName, |
|||||||
| 420 | array( |
||||||||
| 421 | 1 | 'execution_date' => $migration->executionDate, |
|||||||
| 422 | 1 | 'status' => APIMigration::STATUS_STARTED, |
|||||||
| 423 | 'execution_error' => null |
||||||||
| 424 | ), |
||||||||
| 425 | 1 | array('migration' => $migration->name) |
|||||||
| 426 | ); |
||||||||
| 427 | 1 | $conn->commit(); |
|||||||
| 428 | |||||||||
| 429 | 1 | return $migration; |
|||||||
| 430 | } |
||||||||
| 431 | |||||||||
| 432 | /** |
||||||||
| 433 | * Removes all migration from storage (regardless of their status) |
||||||||
| 434 | */ |
||||||||
| 435 | public function deleteMigrations() |
||||||||
| 436 | { |
||||||||
| 437 | $this->drop(); |
||||||||
| 438 | } |
||||||||
| 439 | |||||||||
| 440 | /** |
||||||||
| 441 | * @throws QueryException |
||||||||
| 442 | */ |
||||||||
| 443 | 1 | public function createTable() |
|||||||
| 444 | { |
||||||||
| 445 | /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */ |
||||||||
| 446 | 1 | $sm = $this->getConnection()->getSchemaManager(); |
|||||||
| 447 | 1 | $dbPlatform = $sm->getDatabasePlatform(); |
|||||||
| 448 | |||||||||
| 449 | 1 | $schema = new Schema(); |
|||||||
| 450 | |||||||||
| 451 | 1 | $t = $schema->createTable($this->tableName); |
|||||||
| 452 | 1 | $t->addColumn('migration', 'string', array('length' => 255)); |
|||||||
| 453 | 1 | $t->addColumn('path', 'string', array('length' => 4000)); |
|||||||
| 454 | 1 | $t->addColumn('md5', 'string', array('length' => 32)); |
|||||||
| 455 | 1 | $t->addColumn('execution_date', 'integer', array('notnull' => false)); |
|||||||
| 456 | 1 | $t->addColumn('status', 'integer', array('default ' => APIMigration::STATUS_TODO)); |
|||||||
| 457 | 1 | $t->addColumn('execution_error', 'string', array('length' => 4000, 'notnull' => false)); |
|||||||
| 458 | 1 | $t->setPrimaryKey(array('migration')); |
|||||||
| 459 | // in case users want to look up migrations by their full path |
||||||||
| 460 | // NB: disabled for the moment, as it causes problems on some versions of mysql which limit index length to 767 bytes, |
||||||||
| 461 | // and 767 bytes can be either 255 chars or 191 chars depending on charset utf8 or utf8mb4... |
||||||||
| 462 | //$t->addIndex(array('path')); |
||||||||
| 463 | |||||||||
| 464 | 1 | $this->injectTableCreationOptions($t); |
|||||||
| 465 | |||||||||
| 466 | 1 | foreach ($schema->toSql($dbPlatform) as $sql) { |
|||||||
| 467 | try { |
||||||||
| 468 | 1 | $this->dbHandler->exec($sql); |
|||||||
| 469 | } catch (QueryException $e) { |
||||||||
| 470 | // work around limitations in both Mysql and Doctrine |
||||||||
| 471 | // @see https://github.com/kaliop-uk/ezmigrationbundle/issues/176 |
||||||||
| 472 | if (strpos($e->getMessage(), '1071 Specified key was too long; max key length is 767 bytes') !== false && |
||||||||
| 473 | strpos($sql, 'PRIMARY KEY(migration)') !== false) { |
||||||||
| 474 | $this->dbHandler->exec(str_replace('PRIMARY KEY(migration)', 'PRIMARY KEY(migration(191))', $sql)); |
||||||||
| 475 | } else { |
||||||||
| 476 | throw $e; |
||||||||
| 477 | } |
||||||||
| 478 | } |
||||||||
| 479 | } |
||||||||
| 480 | 1 | } |
|||||||
| 481 | |||||||||
| 482 | 103 | protected function migrationToArray(APIMigration $migration) |
|||||||
| 483 | { |
||||||||
| 484 | return array( |
||||||||
| 485 | 103 | 'migration' => $migration->name, |
|||||||
| 486 | 103 | 'md5' => $migration->md5, |
|||||||
| 487 | 103 | 'path' => $migration->path, |
|||||||
| 488 | 103 | 'execution_date' => $migration->executionDate, |
|||||||
| 489 | 103 | 'status' => $migration->status, |
|||||||
| 490 | 103 | 'execution_error' => $migration->executionError |
|||||||
| 491 | ); |
||||||||
| 492 | } |
||||||||
| 493 | |||||||||
| 494 | 108 | protected function arrayToMigration(array $data) |
|||||||
| 495 | { |
||||||||
| 496 | 108 | return new APIMigration( |
|||||||
| 497 | 108 | $data['migration'], |
|||||||
| 498 | 108 | $data['md5'], |
|||||||
| 499 | 108 | $data['path'], |
|||||||
| 500 | 108 | $data['execution_date'], |
|||||||
| 501 | 108 | $data['status'], |
|||||||
| 502 | 108 | $data['execution_error'] |
|||||||
| 503 | ); |
||||||||
| 504 | } |
||||||||
| 505 | |||||||||
| 506 | protected function getEntityName($migration) |
||||||||
| 507 | { |
||||||||
| 508 | $arr = explode('\\', get_class($migration)); |
||||||||
| 509 | return end($arr); |
||||||||
| 510 | } |
||||||||
| 511 | } |
||||||||
| 512 |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.