Skip to content

Commit 1259617

Browse files
[2323] Add Native Support for ALGORITHM=INSTANT in Migrations for MYSQL
1 parent 9e1cd7d commit 1259617

6 files changed

Lines changed: 509 additions & 5 deletions

File tree

CONTRIBUTING.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,16 @@ install [docker-compose](https://docs.docker.com/compose/) for your platform.
7777
docker-compose run --rm phinx
7878
```
7979
80-
1. Install dependencies:
80+
If you use Mac with Apple Silicon add `platform: linux/amd64` for `mysql` and `postgres` services first. Otherwise,
81+
you might have an error `no matching manifest for linux/arm64/v8 in the manifest list entries`
82+
83+
2. Install dependencies:
8184
8285
```
8386
composer update
8487
```
8588
86-
1. Run unittest:
89+
3. Run unittest:
8790
8891
```
8992
vendor/bin/phpunit

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM php:7.3
1+
FROM php:8.1
22

33
# system dependecies
44
RUN apt-get update && apt-get install -y \

src/Phinx/Db/Adapter/MysqlAdapter.php

Lines changed: 177 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,77 @@ class MysqlAdapter extends PdoAdapter
9393

9494
public const FIRST = 'FIRST';
9595

96+
/**
97+
* MySQL ALTER TABLE ALGORITHM options
98+
*
99+
* These constants control how MySQL performs ALTER TABLE operations:
100+
* - ALGORITHM_DEFAULT: Let MySQL choose the best algorithm
101+
* - ALGORITHM_INSTANT: Instant operation (no table copy, MySQL 8.0+ / MariaDB 10.3+)
102+
* - ALGORITHM_INPLACE: In-place operation (no full table copy)
103+
* - ALGORITHM_COPY: Traditional table copy algorithm
104+
*
105+
* Usage:
106+
* ```php
107+
* use Migrations\Db\Adapter\MysqlAdapter;
108+
*
109+
* // ALGORITHM=INSTANT alone (recommended)
110+
* $table->addColumn('status', 'string', [
111+
* 'null' => true,
112+
* 'algorithm' => MysqlAdapter::ALGORITHM_INSTANT,
113+
* ]);
114+
*
115+
* // Or with ALGORITHM=INPLACE and explicit LOCK
116+
* $table->addColumn('status', 'string', [
117+
* 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
118+
* 'lock' => MysqlAdapter::LOCK_NONE,
119+
* ]);
120+
* ```
121+
*
122+
* Important: ALGORITHM=INSTANT cannot be combined with LOCK=NONE, LOCK=SHARED,
123+
* or LOCK=EXCLUSIVE (MySQL restriction). Use ALGORITHM=INSTANT alone or with
124+
* LOCK=DEFAULT only.
125+
*
126+
* Note: ALGORITHM_INSTANT requires MySQL 8.0+ or MariaDB 10.3+ and only works for
127+
* compatible operations (adding nullable columns, dropping columns, etc.).
128+
* If the operation cannot be performed instantly, MySQL will return an error.
129+
*
130+
* @see https://dev.mysql.com/doc/refman/8.0/en/alter-table.html
131+
* @see https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-operations.html
132+
* @see https://mariadb.com/kb/en/alter-table/#algorithm
133+
*/
134+
public const ALGORITHM_DEFAULT = 'DEFAULT';
135+
public const ALGORITHM_INSTANT = 'INSTANT';
136+
public const ALGORITHM_INPLACE = 'INPLACE';
137+
public const ALGORITHM_COPY = 'COPY';
138+
139+
/**
140+
* MySQL ALTER TABLE LOCK options
141+
*
142+
* These constants control the locking behavior during ALTER TABLE operations:
143+
* - LOCK_DEFAULT: Let MySQL choose the appropriate lock level
144+
* - LOCK_NONE: Allow concurrent reads and writes (least restrictive)
145+
* - LOCK_SHARED: Allow concurrent reads, block writes
146+
* - LOCK_EXCLUSIVE: Block all concurrent access (most restrictive)
147+
*
148+
* Usage:
149+
* ```php
150+
* use Migrations\Db\Adapter\MysqlAdapter;
151+
*
152+
* $table->changeColumn('name', 'string', [
153+
* 'limit' => 500,
154+
* 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
155+
* 'lock' => MysqlAdapter::LOCK_NONE,
156+
* ]);
157+
* ```
158+
*
159+
* @see https://dev.mysql.com/doc/refman/8.0/en/alter-table.html
160+
* @see https://mariadb.com/kb/en/alter-table/#lock
161+
*/
162+
public const LOCK_DEFAULT = 'DEFAULT';
163+
public const LOCK_NONE = 'NONE';
164+
public const LOCK_SHARED = 'SHARED';
165+
public const LOCK_EXCLUSIVE = 'EXCLUSIVE';
166+
96167
/**
97168
* {@inheritDoc}
98169
*
@@ -533,7 +604,16 @@ protected function getAddColumnInstructions(Table $table, Column $column): Alter
533604

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

536-
return new AlterInstructions([$alter]);
607+
$instructions = new AlterInstructions([$alter]);
608+
609+
if ($column->getAlgorithm() !== null) {
610+
$instructions->setAlgorithm($column->getAlgorithm());
611+
}
612+
if ($column->getLock() !== null) {
613+
$instructions->setLock($column->getLock());
614+
}
615+
616+
return $instructions;
537617
}
538618

539619
/**
@@ -616,7 +696,16 @@ protected function getChangeColumnInstructions(string $tableName, string $column
616696
$this->afterClause($newColumn),
617697
);
618698

619-
return new AlterInstructions([$alter]);
699+
$instructions = new AlterInstructions([$alter]);
700+
701+
if ($newColumn->getAlgorithm() !== null) {
702+
$instructions->setAlgorithm($newColumn->getAlgorithm());
703+
}
704+
if ($newColumn->getLock() !== null) {
705+
$instructions->setLock($newColumn->getLock());
706+
}
707+
708+
return $instructions;
620709
}
621710

622711
/**
@@ -1510,6 +1599,92 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
15101599
return $def;
15111600
}
15121601

1602+
/**
1603+
* {@inheritDoc}
1604+
*
1605+
* Overridden to support ALGORITHM and LOCK clauses from AlterInstructions.
1606+
*
1607+
* @param string $tableName The table name
1608+
* @param \Phinx\Db\Util\AlterInstructions $instructions The alter instructions
1609+
* @throws \InvalidArgumentException
1610+
* @return void
1611+
*/
1612+
protected function executeAlterSteps(string $tableName, AlterInstructions $instructions): void
1613+
{
1614+
$algorithm = $instructions->getAlgorithm();
1615+
$lock = $instructions->getLock();
1616+
1617+
if ($algorithm === null && $lock === null) {
1618+
parent::executeAlterSteps($tableName, $instructions);
1619+
1620+
return;
1621+
}
1622+
1623+
$algorithmLockClause = '';
1624+
$upperAlgorithm = null;
1625+
$upperLock = null;
1626+
1627+
if ($algorithm !== null) {
1628+
$upperAlgorithm = strtoupper($algorithm);
1629+
$validAlgorithms = [
1630+
self::ALGORITHM_DEFAULT,
1631+
self::ALGORITHM_INSTANT,
1632+
self::ALGORITHM_INPLACE,
1633+
self::ALGORITHM_COPY,
1634+
];
1635+
if (!in_array($upperAlgorithm, $validAlgorithms, true)) {
1636+
throw new InvalidArgumentException(sprintf(
1637+
'Invalid algorithm "%s". Valid options: %s',
1638+
$algorithm,
1639+
implode(', ', $validAlgorithms),
1640+
));
1641+
}
1642+
$algorithmLockClause .= ', ALGORITHM=' . $upperAlgorithm;
1643+
}
1644+
1645+
if ($lock !== null) {
1646+
$upperLock = strtoupper($lock);
1647+
$validLocks = [
1648+
self::LOCK_DEFAULT,
1649+
self::LOCK_NONE,
1650+
self::LOCK_SHARED,
1651+
self::LOCK_EXCLUSIVE,
1652+
];
1653+
if (!in_array($upperLock, $validLocks, true)) {
1654+
throw new InvalidArgumentException(sprintf(
1655+
'Invalid lock "%s". Valid options: %s',
1656+
$lock,
1657+
implode(', ', $validLocks),
1658+
));
1659+
}
1660+
$algorithmLockClause .= ', LOCK=' . $upperLock;
1661+
}
1662+
1663+
if ($upperAlgorithm === self::ALGORITHM_INSTANT && $upperLock !== null && $upperLock !== self::LOCK_DEFAULT) {
1664+
throw new InvalidArgumentException(
1665+
'ALGORITHM=INSTANT cannot be combined with LOCK=NONE, LOCK=SHARED, or LOCK=EXCLUSIVE. ' .
1666+
'Either use ALGORITHM=INSTANT alone, or use ALGORITHM=INSTANT with LOCK=DEFAULT.',
1667+
);
1668+
}
1669+
1670+
$alterTemplate = sprintf('ALTER TABLE %s %%s', $this->quoteTableName($tableName));
1671+
1672+
if ($instructions->getAlterParts()) {
1673+
$alter = sprintf($alterTemplate, implode(', ', $instructions->getAlterParts()) . $algorithmLockClause);
1674+
$this->execute($alter);
1675+
}
1676+
1677+
$state = [];
1678+
foreach ($instructions->getPostSteps() as $instruction) {
1679+
if (is_callable($instruction)) {
1680+
$state = $instruction($state);
1681+
continue;
1682+
}
1683+
1684+
$this->execute($instruction);
1685+
}
1686+
}
1687+
15131688
/**
15141689
* Describes a database table. This is a MySQL adapter specific method.
15151690
*

src/Phinx/Db/Table/Column.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,16 @@ class Column
162162
*/
163163
protected ?array $values = null;
164164

165+
/**
166+
* @var string|null
167+
*/
168+
protected ?string $algorithm = null;
169+
170+
/**
171+
* @var string|null
172+
*/
173+
protected ?string $lock = null;
174+
165175
/**
166176
* Column constructor
167177
*/
@@ -708,6 +718,52 @@ public function getEncoding(): ?string
708718
return $this->encoding;
709719
}
710720

721+
/**
722+
* Sets the ALTER TABLE algorithm (MySQL-specific).
723+
*
724+
* @param string $algorithm Algorithm
725+
* @return $this
726+
*/
727+
public function setAlgorithm(string $algorithm)
728+
{
729+
$this->algorithm = $algorithm;
730+
731+
return $this;
732+
}
733+
734+
/**
735+
* Gets the ALTER TABLE algorithm.
736+
*
737+
* @return string|null
738+
*/
739+
public function getAlgorithm(): ?string
740+
{
741+
return $this->algorithm;
742+
}
743+
744+
/**
745+
* Sets the ALTER TABLE lock mode (MySQL-specific).
746+
*
747+
* @param string $lock Lock mode
748+
* @return $this
749+
*/
750+
public function setLock(string $lock)
751+
{
752+
$this->lock = $lock;
753+
754+
return $this;
755+
}
756+
757+
/**
758+
* Gets the ALTER TABLE lock mode.
759+
*
760+
* @return string|null
761+
*/
762+
public function getLock(): ?string
763+
{
764+
return $this->lock;
765+
}
766+
711767
/**
712768
* Sets the column SRID.
713769
*
@@ -757,6 +813,8 @@ protected function getValidOptions(): array
757813
'seed',
758814
'increment',
759815
'generated',
816+
'algorithm',
817+
'lock',
760818
];
761819
}
762820

0 commit comments

Comments
 (0)