Skip to content

Commit a03014f

Browse files
lutdevalutskevychcheck24MasterOdin
authored
Add algorithm/lock Support for alter columns in mysql (#2399)
Co-authored-by: Andrii Lutskevych <andrii.lutskevych@check24.de> Co-authored-by: Matthew Peveler <matt.peveler@gmail.com>
1 parent 52f1bc5 commit a03014f

File tree

7 files changed

+564
-5
lines changed

7 files changed

+564
-5
lines changed

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
*
@@ -537,7 +608,16 @@ protected function getAddColumnInstructions(Table $table, Column $column): Alter
537608

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

540-
return new AlterInstructions([$alter]);
611+
$instructions = new AlterInstructions([$alter]);
612+
613+
if ($column->getAlgorithm() !== null) {
614+
$instructions->setAlgorithm($column->getAlgorithm());
615+
}
616+
if ($column->getLock() !== null) {
617+
$instructions->setLock($column->getLock());
618+
}
619+
620+
return $instructions;
541621
}
542622

543623
/**
@@ -620,7 +700,16 @@ protected function getChangeColumnInstructions(string $tableName, string $column
620700
$this->afterClause($newColumn),
621701
);
622702

623-
return new AlterInstructions([$alter]);
703+
$instructions = new AlterInstructions([$alter]);
704+
705+
if ($newColumn->getAlgorithm() !== null) {
706+
$instructions->setAlgorithm($newColumn->getAlgorithm());
707+
}
708+
if ($newColumn->getLock() !== null) {
709+
$instructions->setLock($newColumn->getLock());
710+
}
711+
712+
return $instructions;
624713
}
625714

626715
/**
@@ -1514,6 +1603,92 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
15141603
return $def;
15151604
}
15161605

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

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)