Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/Db/Adapter/MysqlAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,20 @@ protected function getAddIndexInstructions(TableMetadata $table, Index $index):
$this->getIndexSqlDefinition($index),
);

// FULLTEXT indexes use post-steps (raw SQL) which executeAlterSteps
// does not append algorithm/lock to, so we inline the clause here.
// Setting on instructions as well ensures validation still runs.
if ($index->getAlgorithm() !== null || $index->getLock() !== null) {
if ($index->getAlgorithm() !== null) {
$alter .= ', ALGORITHM=' . strtoupper($index->getAlgorithm());
$instructions->setAlgorithm($index->getAlgorithm());
}
if ($index->getLock() !== null) {
$alter .= ', LOCK=' . strtoupper($index->getLock());
$instructions->setLock($index->getLock());
}
}

$instructions->addPostStep($alter);
} else {
$alter = sprintf(
Expand All @@ -862,6 +876,13 @@ protected function getAddIndexInstructions(TableMetadata $table, Index $index):
);

$instructions->addAlter($alter);

if ($index->getAlgorithm() !== null) {
$instructions->setAlgorithm($index->getAlgorithm());
}
if ($index->getLock() !== null) {
$instructions->setLock($index->getLock());
}
}

return $instructions;
Expand Down
52 changes: 51 additions & 1 deletion src/Db/Table/Index.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ class Index extends DatabaseIndex
* @param array<string>|null $include The included columns for covering indexes.
* @param ?string $where The where clause for partial indexes.
* @param bool $concurrent Whether to create the index concurrently.
* @param ?string $algorithm The ALTER TABLE algorithm (MySQL-specific).
* @param ?string $lock The ALTER TABLE lock mode (MySQL-specific).
*/
public function __construct(
protected string $name = '',
Expand All @@ -57,6 +59,8 @@ public function __construct(
protected ?array $include = null,
protected ?string $where = null,
protected bool $concurrent = false,
protected ?string $algorithm = null,
protected ?string $lock = null,
) {
}

Expand Down Expand Up @@ -149,6 +153,52 @@ public function getConcurrently(): bool
return $this->concurrent;
}

/**
* Sets the ALTER TABLE algorithm (MySQL-specific).
*
* @param string $algorithm Algorithm
* @return $this
*/
public function setAlgorithm(string $algorithm)
{
$this->algorithm = $algorithm;

return $this;
}

/**
* Gets the ALTER TABLE algorithm.
*
* @return string|null
*/
public function getAlgorithm(): ?string
{
return $this->algorithm;
}

/**
* Sets the ALTER TABLE lock mode (MySQL-specific).
*
* @param string $lock Lock mode
* @return $this
*/
public function setLock(string $lock)
{
$this->lock = $lock;

return $this;
}

/**
* Gets the ALTER TABLE lock mode.
*
* @return string|null
*/
public function getLock(): ?string
{
return $this->lock;
}

/**
* Utility method that maps an array of index options to this object's methods.
*
Expand All @@ -159,7 +209,7 @@ public function getConcurrently(): bool
public function setOptions(array $options)
{
// Valid Options
$validOptions = ['concurrently', 'type', 'unique', 'name', 'limit', 'order', 'include', 'where'];
$validOptions = ['concurrently', 'type', 'unique', 'name', 'limit', 'order', 'include', 'where', 'algorithm', 'lock'];
foreach ($options as $option => $value) {
if (!in_array($option, $validOptions, true)) {
throw new RuntimeException(sprintf('"%s" is not a valid index option.', $option));
Expand Down
276 changes: 276 additions & 0 deletions tests/TestCase/Db/Adapter/MysqlAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3037,6 +3037,282 @@ public function testAlgorithmWithMixedCase()
$this->assertTrue($this->adapter->hasColumn('mixed_case', 'col2'));
}

public function testAddColumnWithAlgorithmAndLockSqlContainsClause()
{
$table = new Table('col_sql_verify', [], $this->adapter);
$table->addColumn('col1', 'string')
->create();

$this->io->level(ConsoleIo::VERBOSE);
$this->out->clear();

$table->addColumn('col2', 'string', [
'null' => true,
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
'lock' => MysqlAdapter::LOCK_NONE,
])->update();

$output = $this->out->output();
$this->assertStringContainsString('ALGORITHM=INPLACE', $output);
$this->assertStringContainsString('LOCK=NONE', $output);
$this->assertTrue($this->adapter->hasColumn('col_sql_verify', 'col2'));
}

public function testAddColumnWithFluentColumnBuilder()
{
$table = new Table('col_fluent', [], $this->adapter);
$table->addColumn('col1', 'string')
->create();

$column = new Column();
$column->setName('col2')
->setType('string')
->setNull(true)
->setAlgorithm(MysqlAdapter::ALGORITHM_INPLACE)
->setLock(MysqlAdapter::LOCK_NONE);

$this->io->level(ConsoleIo::VERBOSE);
$this->out->clear();

$table->addColumn($column)->update();

$output = $this->out->output();
$this->assertStringContainsString('ALGORITHM=INPLACE', $output);
$this->assertStringContainsString('LOCK=NONE', $output);
$this->assertTrue($this->adapter->hasColumn('col_fluent', 'col2'));
}

public function testAddIndexWithAlgorithm()
{
$table = new Table('index_algo', [], $this->adapter);
$table->addColumn('email', 'string')
->create();

$table->addIndex('email', [
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
])->update();

$this->assertTrue($this->adapter->hasIndex('index_algo', ['email']));
}

public function testAddIndexWithAlgorithmAndLock()
{
$table = new Table('index_algo_lock', [], $this->adapter);
$table->addColumn('email', 'string')
->create();

$table->addIndex('email', [
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
'lock' => MysqlAdapter::LOCK_NONE,
])->update();

$this->assertTrue($this->adapter->hasIndex('index_algo_lock', ['email']));
}

public function testAddIndexWithAlgorithmCopy()
{
$table = new Table('index_copy', [], $this->adapter);
$table->addColumn('email', 'string')
->create();

$table->addIndex('email', [
'algorithm' => MysqlAdapter::ALGORITHM_COPY,
])->update();

$this->assertTrue($this->adapter->hasIndex('index_copy', ['email']));
}

public function testAddIndexWithAlgorithmMixedCase()
{
$table = new Table('index_case', [], $this->adapter);
$table->addColumn('email', 'string')
->create();

$table->addIndex('email', [
'algorithm' => 'inplace',
'lock' => 'none',
])->update();

$this->assertTrue($this->adapter->hasIndex('index_case', ['email']));
}

public function testAddIndexWithInvalidAlgorithmThrowsException()
{
$table = new Table('index_invalid_algo', [], $this->adapter);
$table->addColumn('email', 'string')
->create();

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid algorithm');

$table->addIndex('email', [
'algorithm' => 'INVALID',
])->update();
}

public function testAddIndexWithInvalidLockThrowsException()
{
$table = new Table('index_invalid_lock', [], $this->adapter);
$table->addColumn('email', 'string')
->create();

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid lock');

$table->addIndex('email', [
'lock' => 'INVALID',
])->update();
}

public function testAddIndexWithAlgorithmInstantAndExplicitLockThrowsException()
{
$table = new Table('index_instant_lock', [], $this->adapter);
$table->addColumn('email', 'string')
->create();

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('ALGORITHM=INSTANT cannot be combined with LOCK=NONE');

$table->addIndex('email', [
'algorithm' => MysqlAdapter::ALGORITHM_INSTANT,
'lock' => MysqlAdapter::LOCK_NONE,
])->update();
}

public function testBatchedIndexesWithSameAlgorithm()
{
$table = new Table('index_batch', [], $this->adapter);
$table->addColumn('email', 'string')
->addColumn('name', 'string')
->create();

$table->addIndex('email', [
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
'lock' => MysqlAdapter::LOCK_NONE,
])
->addIndex('name', [
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
'lock' => MysqlAdapter::LOCK_NONE,
])
->update();

$this->assertTrue($this->adapter->hasIndex('index_batch', ['email']));
$this->assertTrue($this->adapter->hasIndex('index_batch', ['name']));
}

public function testBatchedIndexesWithConflictingAlgorithmsThrowsException()
{
$table = new Table('index_batch_conflict', [], $this->adapter);
$table->addColumn('email', 'string')
->addColumn('name', 'string')
->create();

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Conflicting algorithm specifications');

$table->addIndex('email', [
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
])
->addIndex('name', [
'algorithm' => MysqlAdapter::ALGORITHM_COPY,
])
->update();
}

public function testBatchedIndexesWithConflictingLocksThrowsException()
{
$table = new Table('index_lock_conflict', [], $this->adapter);
$table->addColumn('email', 'string')
->addColumn('name', 'string')
->create();

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Conflicting lock specifications');

$table->addIndex('email', [
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
'lock' => MysqlAdapter::LOCK_NONE,
])
->addIndex('name', [
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
'lock' => MysqlAdapter::LOCK_SHARED,
])
->update();
}

public function testAddFulltextIndexWithAlgorithmAndLock()
{
$table = new Table('index_fulltext_algo', [], $this->adapter);
$table->addColumn('content', 'text')
->create();

$table->addIndex('content', [
'type' => 'fulltext',
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
'lock' => MysqlAdapter::LOCK_SHARED,
])->update();

$this->assertTrue($this->adapter->hasIndex('index_fulltext_algo', ['content']));
}

public function testAddFulltextIndexWithInstantAndLockThrowsException()
{
$table = new Table('index_fulltext_instant', [], $this->adapter);
$table->addColumn('content', 'text')
->create();

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('ALGORITHM=INSTANT cannot be combined with LOCK=NONE');

$table->addIndex('content', [
'type' => 'fulltext',
'algorithm' => MysqlAdapter::ALGORITHM_INSTANT,
'lock' => MysqlAdapter::LOCK_NONE,
])->update();
}

public function testAddIndexWithAlgorithmAndLockSqlContainsClause()
{
$table = new Table('idx_sql_verify', [], $this->adapter);
$table->addColumn('email', 'string')
->create();

$this->io->level(ConsoleIo::VERBOSE);
$this->out->clear();

$table->addIndex('email', [
'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
'lock' => MysqlAdapter::LOCK_NONE,
])->update();

$output = $this->out->output();
$this->assertStringContainsString('ALGORITHM=INPLACE', $output);
$this->assertStringContainsString('LOCK=NONE', $output);
$this->assertTrue($this->adapter->hasIndex('idx_sql_verify', ['email']));
}

public function testAddIndexWithFluentIndexBuilder()
{
$table = new Table('idx_fluent', [], $this->adapter);
$table->addColumn('email', 'string')
->create();

$index = new Index();
$index->setColumns('email')
->setAlgorithm(MysqlAdapter::ALGORITHM_INPLACE)
->setLock(MysqlAdapter::LOCK_NONE);

$this->io->level(ConsoleIo::VERBOSE);
$this->out->clear();

$table->addIndex($index)->update();

$output = $this->out->output();
$this->assertStringContainsString('ALGORITHM=INPLACE', $output);
$this->assertStringContainsString('LOCK=NONE', $output);
$this->assertTrue($this->adapter->hasIndex('idx_fluent', ['email']));
}

public function testInsertOrUpdateWithDuplicates()
{
$table = new Table('currencies', [], $this->adapter);
Expand Down
Loading