@@ -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 *
0 commit comments