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