Skip to content

Commit b31ee2e

Browse files
Add ALGORITHM/LOCK support to Index operations (#1057)
* Add ALGORITHM/LOCK support to Index operations Extend the ALGORITHM and LOCK clause support added in PR #955 for Column operations to also cover Index operations. This allows addIndex to pass algorithm and lock options through the fluent API on MySQL. Changes: - Add algorithm/lock properties, getters/setters to Index class - Wire algorithm/lock through getAddIndexInstructions in MysqlAdapter - Handle FULLTEXT indexes which use post-steps (inline the clause) - Add 9 tests mirroring the Column algorithm/lock test coverage * Add tests for FULLTEXT index and batched lock conflict paths * Add SQL validation and fluent builder tests for ALGORITHM/LOCK Verify that ALGORITHM and LOCK clauses appear in generated SQL using verbose logging, and exercise the fluent Column/Index builder path for both column and index operations.
1 parent 743a893 commit b31ee2e

File tree

3 files changed

+348
-1
lines changed

3 files changed

+348
-1
lines changed

src/Db/Adapter/MysqlAdapter.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,20 @@ protected function getAddIndexInstructions(TableMetadata $table, Index $index):
899899
$this->getIndexSqlDefinition($index),
900900
);
901901

902+
// FULLTEXT indexes use post-steps (raw SQL) which executeAlterSteps
903+
// does not append algorithm/lock to, so we inline the clause here.
904+
// Setting on instructions as well ensures validation still runs.
905+
if ($index->getAlgorithm() !== null || $index->getLock() !== null) {
906+
if ($index->getAlgorithm() !== null) {
907+
$alter .= ', ALGORITHM=' . strtoupper($index->getAlgorithm());
908+
$instructions->setAlgorithm($index->getAlgorithm());
909+
}
910+
if ($index->getLock() !== null) {
911+
$alter .= ', LOCK=' . strtoupper($index->getLock());
912+
$instructions->setLock($index->getLock());
913+
}
914+
}
915+
902916
$instructions->addPostStep($alter);
903917
} else {
904918
$alter = sprintf(
@@ -907,6 +921,13 @@ protected function getAddIndexInstructions(TableMetadata $table, Index $index):
907921
);
908922

909923
$instructions->addAlter($alter);
924+
925+
if ($index->getAlgorithm() !== null) {
926+
$instructions->setAlgorithm($index->getAlgorithm());
927+
}
928+
if ($index->getLock() !== null) {
929+
$instructions->setLock($index->getLock());
930+
}
910931
}
911932

912933
return $instructions;

src/Db/Table/Index.php

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ class Index extends DatabaseIndex
4747
* @param array<string>|null $include The included columns for covering indexes.
4848
* @param ?string $where The where clause for partial indexes.
4949
* @param bool $concurrent Whether to create the index concurrently.
50+
* @param ?string $algorithm The ALTER TABLE algorithm (MySQL-specific).
51+
* @param ?string $lock The ALTER TABLE lock mode (MySQL-specific).
5052
*/
5153
public function __construct(
5254
protected string $name = '',
@@ -57,6 +59,8 @@ public function __construct(
5759
protected ?array $include = null,
5860
protected ?string $where = null,
5961
protected bool $concurrent = false,
62+
protected ?string $algorithm = null,
63+
protected ?string $lock = null,
6064
) {
6165
}
6266

@@ -149,6 +153,52 @@ public function getConcurrently(): bool
149153
return $this->concurrent;
150154
}
151155

156+
/**
157+
* Sets the ALTER TABLE algorithm (MySQL-specific).
158+
*
159+
* @param string $algorithm Algorithm
160+
* @return $this
161+
*/
162+
public function setAlgorithm(string $algorithm)
163+
{
164+
$this->algorithm = $algorithm;
165+
166+
return $this;
167+
}
168+
169+
/**
170+
* Gets the ALTER TABLE algorithm.
171+
*
172+
* @return string|null
173+
*/
174+
public function getAlgorithm(): ?string
175+
{
176+
return $this->algorithm;
177+
}
178+
179+
/**
180+
* Sets the ALTER TABLE lock mode (MySQL-specific).
181+
*
182+
* @param string $lock Lock mode
183+
* @return $this
184+
*/
185+
public function setLock(string $lock)
186+
{
187+
$this->lock = $lock;
188+
189+
return $this;
190+
}
191+
192+
/**
193+
* Gets the ALTER TABLE lock mode.
194+
*
195+
* @return string|null
196+
*/
197+
public function getLock(): ?string
198+
{
199+
return $this->lock;
200+
}
201+
152202
/**
153203
* Utility method that maps an array of index options to this object's methods.
154204
*
@@ -159,7 +209,7 @@ public function getConcurrently(): bool
159209
public function setOptions(array $options)
160210
{
161211
// Valid Options
162-
$validOptions = ['concurrently', 'type', 'unique', 'name', 'limit', 'order', 'include', 'where'];
212+
$validOptions = ['concurrently', 'type', 'unique', 'name', 'limit', 'order', 'include', 'where', 'algorithm', 'lock'];
163213
foreach ($options as $option => $value) {
164214
if (!in_array($option, $validOptions, true)) {
165215
throw new RuntimeException(sprintf('"%s" is not a valid index option.', $option));

tests/TestCase/Db/Adapter/MysqlAdapterTest.php

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3127,6 +3127,282 @@ public function testAlgorithmWithMixedCase(): void
31273127
$this->assertTrue($this->adapter->hasColumn('mixed_case', 'col2'));
31283128
}
31293129

3130+
public function testAddColumnWithAlgorithmAndLockSqlContainsClause(): void
3131+
{
3132+
$table = new Table('col_sql_verify', [], $this->adapter);
3133+
$table->addColumn('col1', 'string')
3134+
->create();
3135+
3136+
$this->io->level(ConsoleIo::VERBOSE);
3137+
$this->out->clear();
3138+
3139+
$table->addColumn('col2', 'string', [
3140+
'null' => true,
3141+
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
3142+
'lock' => MysqlAdapter::LOCK_NONE,
3143+
])->update();
3144+
3145+
$output = $this->out->output();
3146+
$this->assertStringContainsString('ALGORITHM=INPLACE', $output);
3147+
$this->assertStringContainsString('LOCK=NONE', $output);
3148+
$this->assertTrue($this->adapter->hasColumn('col_sql_verify', 'col2'));
3149+
}
3150+
3151+
public function testAddColumnWithFluentColumnBuilder(): void
3152+
{
3153+
$table = new Table('col_fluent', [], $this->adapter);
3154+
$table->addColumn('col1', 'string')
3155+
->create();
3156+
3157+
$column = new Column();
3158+
$column->setName('col2')
3159+
->setType('string')
3160+
->setNull(true)
3161+
->setAlgorithm(MysqlAdapter::ALGORITHM_INPLACE)
3162+
->setLock(MysqlAdapter::LOCK_NONE);
3163+
3164+
$this->io->level(ConsoleIo::VERBOSE);
3165+
$this->out->clear();
3166+
3167+
$table->addColumn($column)->update();
3168+
3169+
$output = $this->out->output();
3170+
$this->assertStringContainsString('ALGORITHM=INPLACE', $output);
3171+
$this->assertStringContainsString('LOCK=NONE', $output);
3172+
$this->assertTrue($this->adapter->hasColumn('col_fluent', 'col2'));
3173+
}
3174+
3175+
public function testAddIndexWithAlgorithm(): void
3176+
{
3177+
$table = new Table('index_algo', [], $this->adapter);
3178+
$table->addColumn('email', 'string')
3179+
->create();
3180+
3181+
$table->addIndex('email', [
3182+
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
3183+
])->update();
3184+
3185+
$this->assertTrue($this->adapter->hasIndex('index_algo', ['email']));
3186+
}
3187+
3188+
public function testAddIndexWithAlgorithmAndLock(): void
3189+
{
3190+
$table = new Table('index_algo_lock', [], $this->adapter);
3191+
$table->addColumn('email', 'string')
3192+
->create();
3193+
3194+
$table->addIndex('email', [
3195+
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
3196+
'lock' => MysqlAdapter::LOCK_NONE,
3197+
])->update();
3198+
3199+
$this->assertTrue($this->adapter->hasIndex('index_algo_lock', ['email']));
3200+
}
3201+
3202+
public function testAddIndexWithAlgorithmCopy(): void
3203+
{
3204+
$table = new Table('index_copy', [], $this->adapter);
3205+
$table->addColumn('email', 'string')
3206+
->create();
3207+
3208+
$table->addIndex('email', [
3209+
'algorithm' => MysqlAdapter::ALGORITHM_COPY,
3210+
])->update();
3211+
3212+
$this->assertTrue($this->adapter->hasIndex('index_copy', ['email']));
3213+
}
3214+
3215+
public function testAddIndexWithAlgorithmMixedCase(): void
3216+
{
3217+
$table = new Table('index_case', [], $this->adapter);
3218+
$table->addColumn('email', 'string')
3219+
->create();
3220+
3221+
$table->addIndex('email', [
3222+
'algorithm' => 'inplace',
3223+
'lock' => 'none',
3224+
])->update();
3225+
3226+
$this->assertTrue($this->adapter->hasIndex('index_case', ['email']));
3227+
}
3228+
3229+
public function testAddIndexWithInvalidAlgorithmThrowsException(): void
3230+
{
3231+
$table = new Table('index_invalid_algo', [], $this->adapter);
3232+
$table->addColumn('email', 'string')
3233+
->create();
3234+
3235+
$this->expectException(InvalidArgumentException::class);
3236+
$this->expectExceptionMessage('Invalid algorithm');
3237+
3238+
$table->addIndex('email', [
3239+
'algorithm' => 'INVALID',
3240+
])->update();
3241+
}
3242+
3243+
public function testAddIndexWithInvalidLockThrowsException(): void
3244+
{
3245+
$table = new Table('index_invalid_lock', [], $this->adapter);
3246+
$table->addColumn('email', 'string')
3247+
->create();
3248+
3249+
$this->expectException(InvalidArgumentException::class);
3250+
$this->expectExceptionMessage('Invalid lock');
3251+
3252+
$table->addIndex('email', [
3253+
'lock' => 'INVALID',
3254+
])->update();
3255+
}
3256+
3257+
public function testAddIndexWithAlgorithmInstantAndExplicitLockThrowsException(): void
3258+
{
3259+
$table = new Table('index_instant_lock', [], $this->adapter);
3260+
$table->addColumn('email', 'string')
3261+
->create();
3262+
3263+
$this->expectException(InvalidArgumentException::class);
3264+
$this->expectExceptionMessage('ALGORITHM=INSTANT cannot be combined with LOCK=NONE');
3265+
3266+
$table->addIndex('email', [
3267+
'algorithm' => MysqlAdapter::ALGORITHM_INSTANT,
3268+
'lock' => MysqlAdapter::LOCK_NONE,
3269+
])->update();
3270+
}
3271+
3272+
public function testBatchedIndexesWithSameAlgorithm(): void
3273+
{
3274+
$table = new Table('index_batch', [], $this->adapter);
3275+
$table->addColumn('email', 'string')
3276+
->addColumn('name', 'string')
3277+
->create();
3278+
3279+
$table->addIndex('email', [
3280+
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
3281+
'lock' => MysqlAdapter::LOCK_NONE,
3282+
])
3283+
->addIndex('name', [
3284+
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
3285+
'lock' => MysqlAdapter::LOCK_NONE,
3286+
])
3287+
->update();
3288+
3289+
$this->assertTrue($this->adapter->hasIndex('index_batch', ['email']));
3290+
$this->assertTrue($this->adapter->hasIndex('index_batch', ['name']));
3291+
}
3292+
3293+
public function testBatchedIndexesWithConflictingAlgorithmsThrowsException(): void
3294+
{
3295+
$table = new Table('index_batch_conflict', [], $this->adapter);
3296+
$table->addColumn('email', 'string')
3297+
->addColumn('name', 'string')
3298+
->create();
3299+
3300+
$this->expectException(InvalidArgumentException::class);
3301+
$this->expectExceptionMessage('Conflicting algorithm specifications');
3302+
3303+
$table->addIndex('email', [
3304+
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
3305+
])
3306+
->addIndex('name', [
3307+
'algorithm' => MysqlAdapter::ALGORITHM_COPY,
3308+
])
3309+
->update();
3310+
}
3311+
3312+
public function testBatchedIndexesWithConflictingLocksThrowsException(): void
3313+
{
3314+
$table = new Table('index_lock_conflict', [], $this->adapter);
3315+
$table->addColumn('email', 'string')
3316+
->addColumn('name', 'string')
3317+
->create();
3318+
3319+
$this->expectException(InvalidArgumentException::class);
3320+
$this->expectExceptionMessage('Conflicting lock specifications');
3321+
3322+
$table->addIndex('email', [
3323+
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
3324+
'lock' => MysqlAdapter::LOCK_NONE,
3325+
])
3326+
->addIndex('name', [
3327+
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
3328+
'lock' => MysqlAdapter::LOCK_SHARED,
3329+
])
3330+
->update();
3331+
}
3332+
3333+
public function testAddFulltextIndexWithAlgorithmAndLock(): void
3334+
{
3335+
$table = new Table('index_fulltext_algo', [], $this->adapter);
3336+
$table->addColumn('content', 'text')
3337+
->create();
3338+
3339+
$table->addIndex('content', [
3340+
'type' => 'fulltext',
3341+
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
3342+
'lock' => MysqlAdapter::LOCK_SHARED,
3343+
])->update();
3344+
3345+
$this->assertTrue($this->adapter->hasIndex('index_fulltext_algo', ['content']));
3346+
}
3347+
3348+
public function testAddFulltextIndexWithInstantAndLockThrowsException(): void
3349+
{
3350+
$table = new Table('index_fulltext_instant', [], $this->adapter);
3351+
$table->addColumn('content', 'text')
3352+
->create();
3353+
3354+
$this->expectException(InvalidArgumentException::class);
3355+
$this->expectExceptionMessage('ALGORITHM=INSTANT cannot be combined with LOCK=NONE');
3356+
3357+
$table->addIndex('content', [
3358+
'type' => 'fulltext',
3359+
'algorithm' => MysqlAdapter::ALGORITHM_INSTANT,
3360+
'lock' => MysqlAdapter::LOCK_NONE,
3361+
])->update();
3362+
}
3363+
3364+
public function testAddIndexWithAlgorithmAndLockSqlContainsClause(): void
3365+
{
3366+
$table = new Table('idx_sql_verify', [], $this->adapter);
3367+
$table->addColumn('email', 'string')
3368+
->create();
3369+
3370+
$this->io->level(ConsoleIo::VERBOSE);
3371+
$this->out->clear();
3372+
3373+
$table->addIndex('email', [
3374+
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
3375+
'lock' => MysqlAdapter::LOCK_NONE,
3376+
])->update();
3377+
3378+
$output = $this->out->output();
3379+
$this->assertStringContainsString('ALGORITHM=INPLACE', $output);
3380+
$this->assertStringContainsString('LOCK=NONE', $output);
3381+
$this->assertTrue($this->adapter->hasIndex('idx_sql_verify', ['email']));
3382+
}
3383+
3384+
public function testAddIndexWithFluentIndexBuilder(): void
3385+
{
3386+
$table = new Table('idx_fluent', [], $this->adapter);
3387+
$table->addColumn('email', 'string')
3388+
->create();
3389+
3390+
$index = new Index();
3391+
$index->setColumns('email')
3392+
->setAlgorithm(MysqlAdapter::ALGORITHM_INPLACE)
3393+
->setLock(MysqlAdapter::LOCK_NONE);
3394+
3395+
$this->io->level(ConsoleIo::VERBOSE);
3396+
$this->out->clear();
3397+
3398+
$table->addIndex($index)->update();
3399+
3400+
$output = $this->out->output();
3401+
$this->assertStringContainsString('ALGORITHM=INPLACE', $output);
3402+
$this->assertStringContainsString('LOCK=NONE', $output);
3403+
$this->assertTrue($this->adapter->hasIndex('idx_fluent', ['email']));
3404+
}
3405+
31303406
public function testInsertOrUpdateWithDuplicates(): void
31313407
{
31323408
$table = new Table('currencies', [], $this->adapter);

0 commit comments

Comments
 (0)