Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 177 additions & 2 deletions src/Db/Adapter/MysqlAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,77 @@ class MysqlAdapter extends AbstractAdapter

public const FIRST = 'FIRST';

/**
* MySQL ALTER TABLE ALGORITHM options
*
* These constants control how MySQL performs ALTER TABLE operations:
* - ALGORITHM_DEFAULT: Let MySQL choose the best algorithm
* - ALGORITHM_INSTANT: Instant operation (no table copy, MySQL 8.0+ / MariaDB 10.3+)
* - ALGORITHM_INPLACE: In-place operation (no full table copy)
* - ALGORITHM_COPY: Traditional table copy algorithm
*
* Usage:
* ```php
* use Migrations\Db\Adapter\MysqlAdapter;
*
* // ALGORITHM=INSTANT alone (recommended)
* $table->addColumn('status', 'string', [
* 'null' => true,
* 'algorithm' => MysqlAdapter::ALGORITHM_INSTANT,
* ]);
*
* // Or with ALGORITHM=INPLACE and explicit LOCK
* $table->addColumn('status', 'string', [
* 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
* 'lock' => MysqlAdapter::LOCK_NONE,
* ]);
* ```
*
* Important: ALGORITHM=INSTANT cannot be combined with LOCK=NONE, LOCK=SHARED,
* or LOCK=EXCLUSIVE (MySQL restriction). Use ALGORITHM=INSTANT alone or with
* LOCK=DEFAULT only.
*
* Note: ALGORITHM_INSTANT requires MySQL 8.0+ or MariaDB 10.3+ and only works for
* compatible operations (adding nullable columns, dropping columns, etc.).
* If the operation cannot be performed instantly, MySQL will return an error.
*
* @see https://dev.mysql.com/doc/refman/8.0/en/alter-table.html
* @see https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-operations.html
* @see https://mariadb.com/kb/en/alter-table/#algorithm
*/
public const ALGORITHM_DEFAULT = 'DEFAULT';
public const ALGORITHM_INSTANT = 'INSTANT';
public const ALGORITHM_INPLACE = 'INPLACE';
public const ALGORITHM_COPY = 'COPY';

/**
* MySQL ALTER TABLE LOCK options
*
* These constants control the locking behavior during ALTER TABLE operations:
* - LOCK_DEFAULT: Let MySQL choose the appropriate lock level
* - LOCK_NONE: Allow concurrent reads and writes (least restrictive)
* - LOCK_SHARED: Allow concurrent reads, block writes
* - LOCK_EXCLUSIVE: Block all concurrent access (most restrictive)
*
* Usage:
* ```php
* use Migrations\Db\Adapter\MysqlAdapter;
*
* $table->changeColumn('name', 'string', [
* 'limit' => 500,
* 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
* 'lock' => MysqlAdapter::LOCK_NONE,
* ]);
* ```
*
* @see https://dev.mysql.com/doc/refman/8.0/en/alter-table.html
* @see https://mariadb.com/kb/en/alter-table/#lock
*/
public const LOCK_DEFAULT = 'DEFAULT';
public const LOCK_NONE = 'NONE';
public const LOCK_SHARED = 'SHARED';
public const LOCK_EXCLUSIVE = 'EXCLUSIVE';

/**
* @inheritDoc
*/
Expand Down Expand Up @@ -577,7 +648,16 @@ protected function getAddColumnInstructions(TableMetadata $table, Column $column

$alter .= $this->afterClause($column);

return new AlterInstructions([$alter]);
$instructions = new AlterInstructions([$alter]);

if ($column->getAlgorithm() !== null) {
$instructions->setAlgorithm($column->getAlgorithm());
}
if ($column->getLock() !== null) {
$instructions->setLock($column->getLock());
}

return $instructions;
}

/**
Expand Down Expand Up @@ -677,7 +757,16 @@ protected function getChangeColumnInstructions(string $tableName, string $column
$this->afterClause($newColumn),
);

return new AlterInstructions([$alter]);
$instructions = new AlterInstructions([$alter]);

if ($newColumn->getAlgorithm() !== null) {
$instructions->setAlgorithm($newColumn->getAlgorithm());
}
if ($newColumn->getLock() !== null) {
$instructions->setLock($newColumn->getLock());
}

return $instructions;
}

/**
Expand Down Expand Up @@ -1164,4 +1253,90 @@ protected function isMariaDb(): bool

return stripos($version, 'mariadb') !== false;
}

/**
* {@inheritDoc}
*
* Overridden to support ALGORITHM and LOCK clauses from AlterInstructions.
*
* @param string $tableName The table name
* @param \Migrations\Db\AlterInstructions $instructions The alter instructions
* @throws \InvalidArgumentException
* @return void
*/
protected function executeAlterSteps(string $tableName, AlterInstructions $instructions): void
{
$algorithm = $instructions->getAlgorithm();
$lock = $instructions->getLock();

if ($algorithm === null && $lock === null) {
parent::executeAlterSteps($tableName, $instructions);

return;
}

$algorithmLockClause = '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't we be missing lock/algorithm when $adapter->addColumn() and changeColumn() are used because of where this logic is?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a commit. Please check if that makes more sense now.

$upperAlgorithm = null;
$upperLock = null;

if ($algorithm !== null) {
$upperAlgorithm = strtoupper($algorithm);
$validAlgorithms = [
self::ALGORITHM_DEFAULT,
self::ALGORITHM_INSTANT,
self::ALGORITHM_INPLACE,
self::ALGORITHM_COPY,
];
if (!in_array($upperAlgorithm, $validAlgorithms, true)) {
throw new InvalidArgumentException(sprintf(
'Invalid algorithm "%s". Valid options: %s',
$algorithm,
implode(', ', $validAlgorithms),
));
}
$algorithmLockClause .= ', ALGORITHM=' . $upperAlgorithm;
}

if ($lock !== null) {
$upperLock = strtoupper($lock);
$validLocks = [
self::LOCK_DEFAULT,
self::LOCK_NONE,
self::LOCK_SHARED,
self::LOCK_EXCLUSIVE,
];
if (!in_array($upperLock, $validLocks, true)) {
throw new InvalidArgumentException(sprintf(
'Invalid lock "%s". Valid options: %s',
$lock,
implode(', ', $validLocks),
));
}
$algorithmLockClause .= ', LOCK=' . $upperLock;
}

if ($upperAlgorithm === self::ALGORITHM_INSTANT && $upperLock !== null && $upperLock !== self::LOCK_DEFAULT) {
throw new InvalidArgumentException(
'ALGORITHM=INSTANT cannot be combined with LOCK=NONE, LOCK=SHARED, or LOCK=EXCLUSIVE. ' .
'Either use ALGORITHM=INSTANT alone, or use ALGORITHM=INSTANT with LOCK=DEFAULT.',
);
}

$alterTemplate = sprintf('ALTER TABLE %s %%s', $this->quoteTableName($tableName));

if ($instructions->getAlterParts()) {
$alter = sprintf($alterTemplate, implode(', ', $instructions->getAlterParts()) . $algorithmLockClause);
$this->execute($alter);
}

$state = [];
foreach ($instructions->getPostSteps() as $instruction) {
if (is_callable($instruction)) {
$state = $instruction($state);
continue;
}

$this->execute($instruction);
}
}
}
78 changes: 78 additions & 0 deletions src/Db/AlterInstructions.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

namespace Migrations\Db;

use InvalidArgumentException;

/**
* Contains all the information for running an ALTER command for a table,
* and any post-steps required after the fact.
Expand All @@ -24,6 +26,16 @@ class AlterInstructions
*/
protected array $postSteps = [];

/**
* @var string|null MySQL-specific: ALGORITHM clause
*/
protected ?string $algorithm = null;

/**
* @var string|null MySQL-specific: LOCK clause
*/
protected ?string $lock = null;

/**
* Constructor
*
Expand Down Expand Up @@ -87,12 +99,78 @@ public function getPostSteps(): array
* Merges another AlterInstructions object to this one
*
* @param \Migrations\Db\AlterInstructions $other The other collection of instructions to merge in
* @throws \InvalidArgumentException When algorithm or lock specifications conflict
* @return void
*/
public function merge(AlterInstructions $other): void
{
$this->alterParts = array_merge($this->alterParts, $other->getAlterParts());
$this->postSteps = array_merge($this->postSteps, $other->getPostSteps());

if ($other->getAlgorithm() !== null) {
if ($this->algorithm !== null && $this->algorithm !== $other->getAlgorithm()) {
throw new InvalidArgumentException(sprintf(
'Conflicting algorithm specifications in batched operations: "%s" and "%s". ' .
'All operations in a batch must use the same algorithm, or specify it on only one operation.',
$this->algorithm,
$other->getAlgorithm(),
));
}
$this->algorithm = $other->getAlgorithm();
}
if ($other->getLock() !== null) {
if ($this->lock !== null && $this->lock !== $other->getLock()) {
throw new InvalidArgumentException(sprintf(
'Conflicting lock specifications in batched operations: "%s" and "%s". ' .
'All operations in a batch must use the same lock mode, or specify it on only one operation.',
$this->lock,
$other->getLock(),
));
}
$this->lock = $other->getLock();
}
}

/**
* Sets the ALGORITHM clause (MySQL-specific)
*
* @param string|null $algorithm The algorithm to use
* @return void
*/
public function setAlgorithm(?string $algorithm): void
{
$this->algorithm = $algorithm;
}

/**
* Gets the ALGORITHM clause (MySQL-specific)
*
* @return string|null
*/
public function getAlgorithm(): ?string
{
return $this->algorithm;
}

/**
* Sets the LOCK clause (MySQL-specific)
*
* @param string|null $lock The lock mode to use
* @return void
*/
public function setLock(?string $lock): void
{
$this->lock = $lock;
}

/**
* Gets the LOCK clause (MySQL-specific)
*
* @return string|null
*/
public function getLock(): ?string
{
return $this->lock;
}

/**
Expand Down
58 changes: 58 additions & 0 deletions src/Db/Table/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ class Column extends DatabaseColumn
*/
protected ?array $values = null;

/**
* @var string|null
*/
protected ?string $algorithm = null;

/**
* @var string|null
*/
protected ?string $lock = null;

/**
* Column constructor
*
Expand Down Expand Up @@ -650,6 +660,52 @@ public function getEncoding(): ?string
return $this->encoding;
}

/**
* Sets the ALTER TABLE algorithm (MySQL-specific).
*
* @param string $algorithm Algorithm
* @return $this
*/
public function setAlgorithm(string $algorithm)
{
$this->algorithm = $algorithm;

return $this;
}

/**
* Gets the ALTER TABLE algorithm.
*
* @return string|null
*/
public function getAlgorithm(): ?string
{
return $this->algorithm;
}

/**
* Sets the ALTER TABLE lock mode (MySQL-specific).
*
* @param string $lock Lock mode
* @return $this
*/
public function setLock(string $lock)
{
$this->lock = $lock;

return $this;
}

/**
* Gets the ALTER TABLE lock mode.
*
* @return string|null
*/
public function getLock(): ?string
{
return $this->lock;
}

/**
* Gets all allowed options. Each option must have a corresponding `setFoo` method.
*
Expand Down Expand Up @@ -677,6 +733,8 @@ protected function getValidOptions(): array
'seed',
'increment',
'generated',
'algorithm',
'lock',
];
}

Expand Down
Loading
Loading