This commit is contained in:
Nicolai 2026-06-10 22:38:09 +02:00 committed by GitHub
commit 76e89f3421
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 178 additions and 4 deletions

View File

@ -163,6 +163,7 @@ function addColumnToIndex (sourceArray, arrayIndex, indexChoice, colIndex): void
const keyBlockSize = $('input[name="index[Key_block_size]"]').val();
const parser = $('input[name="index[Parser]"]').val();
const indexType = $('select[name="index[Index_type]"]').val();
const indexVisible = $('select[name="index[Visible]"]').val();
const columns = [];
$('#index_columns').find('tbody').find('tr').each(function () {
// Get columns in particular order.
@ -182,6 +183,7 @@ function addColumnToIndex (sourceArray, arrayIndex, indexChoice, colIndex): void
'Key_block_size': keyBlockSize,
'Parser': parser,
'Index_type': indexType,
'Visible': indexVisible,
'columns': columns
};

View File

@ -75,6 +75,7 @@
<th>{{ t('Type') }}</th>
<th>{{ t('Unique') }}</th>
<th>{{ t('Packed') }}</th>
<th>{{ t('Visible') }}</th>
<th>{{ t('Column') }}</th>
<th>{{ t('Cardinality') }}</th>
<th>{{ t('Collation') }}</th>
@ -91,6 +92,7 @@
<td rowspan="{{ columns_count }}">{{ index.getType()|default(index.getChoice()) }}</td>
<td rowspan="{{ columns_count }}">{{ index.isUnique() ? t('Yes') : t('No') }}</td>
<td rowspan="{{ columns_count }}">{{ index.isPacked()|raw }}</td>
<td rowspan="{{ columns_count }}">{{ index.isVisible() ? t('Yes') : t('No') }}</td>
{% for column in index.getColumns() %}
{% if column.getSeqInIndex() > 1 %}

View File

@ -18,6 +18,7 @@
<th>{{ t('Type') }}</th>
<th>{{ t('Unique') }}</th>
<th>{{ t('Packed') }}</th>
<th>{{ t('Visible') }}</th>
<th>{{ t('Column') }}</th>
<th>{{ t('Cardinality') }}</th>
<th>{{ t('Collation') }}</th>
@ -65,6 +66,7 @@
<td rowspan="{{ columns_count }}">{{ index.getType()|default(index.getChoice()) }}</td>
<td rowspan="{{ columns_count }}">{{ index.isUnique() ? t('Yes') : t('No') }}</td>
<td rowspan="{{ columns_count }}">{{ index.isPacked()|raw }}</td>
<td rowspan="{{ columns_count }}">{{ index.isVisible() ? t('Yes') : t('No') }}</td>
{% for column in index.getColumns() %}
{% if column.getSeqInIndex() > 1 %}

View File

@ -127,6 +127,22 @@
value="{{ index.getComment() }}">
</div>
<div>
<div class="label">
<strong>
<label for="select_index_visible">
{{ t('Visibility:') }}
{{ show_mysql_docu('invisible-indexes') }}
</label>
</strong>
</div>
<select name="index[Visible]" id="select_index_visible">
<option value="YES"{{ index.isVisible() ? ' selected' }}>{{ t('Visible') }}</option>
<option value="NO"{{ not index.isVisible() ? ' selected' }}>{{ t('Invisible') }}</option>
</select>
</div>
{% if default_sliders_state != 'disabled' %}
</div>
{% endif %}

View File

@ -473,6 +473,7 @@
<th>{{ t('Type') }}</th>
<th>{{ t('Unique') }}</th>
<th>{{ t('Packed') }}</th>
<th>{{ t('Visible') }}</th>
<th>{{ t('Column') }}</th>
<th>{{ t('Cardinality') }}</th>
<th>{{ t('Collation') }}</th>
@ -528,6 +529,7 @@
<td rowspan="{{ columns_count }}">{{ index.getType()|default(index.getChoice()) }}</td>
<td rowspan="{{ columns_count }}">{{ index.isUnique() ? t('Yes') : t('No') }}</td>
<td rowspan="{{ columns_count }}">{{ index.isPacked()|raw }}</td>
<td rowspan="{{ columns_count }}">{{ index.isVisible() ? t('Yes') : t('No') }}</td>
{% for column in index.getColumns() %}
{% if column.getSeqInIndex() > 1 %}

View File

@ -132,7 +132,13 @@ final readonly class IndexesController implements InvocableController
return $this->response->response();
}
$this->dbi->query($sqlQuery);
// The SQL may contain multiple statements (e.g. ALTER TABLE ...; ALTER TABLE ... ALTER INDEX ... VISIBLE;)
// when toggling index visibility, so use multi-query and drain any
// additional result sets so subsequent queries on the same connection don't fail.
$this->dbi->tryMultiQuery($sqlQuery);
do {
$next = $this->dbi->nextResult();
} while ($next !== false);
if ($request->isAjax()) {
$message = Message::success(

View File

@ -85,6 +85,13 @@ class Index
*/
private string $parser = '';
/**
* Whether the index is visible to the optimizer.
* Invisible indexes (MySQL 8.0+) are not used by the optimizer unless
* explicitly forced with USE/FORCE INDEX.
*/
private bool $visible = true;
/** @param mixed[] $params parameters */
public function __construct(array $params = [])
{
@ -323,11 +330,17 @@ class Index
$this->keyBlockSize = (int) $params['Key_block_size'];
}
if (! isset($params['Parser'])) {
if (isset($params['Parser'])) {
$this->parser = $params['Parser'];
}
if (! isset($params['Visible'])) {
return;
}
$this->parser = $params['Parser'];
// SHOW INDEXES on MySQL 8.0+ returns 'YES' / 'NO'.
// The form posts the same string values from the dropdown.
$this->visible = $params['Visible'] !== 'NO';
}
/**
@ -376,6 +389,15 @@ class Index
return $this->parser;
}
/**
* Whether the index is visible to the optimizer (MySQL 8.0+).
* Defaults to true; only false when the index was created with INVISIBLE.
*/
public function isVisible(): bool
{
return $this->visible;
}
/**
* Returns concatenated remarks and comment
*

View File

@ -153,6 +153,25 @@ final class Indexes
$sqlQuery .= ';';
// For non-PRIMARY indexes, append a second statement to set the
// index's visibility explicitly. We can't reliably set it inline in
// the compound `DROP INDEX x, ADD INDEX x ...` above: MySQL silently
// ignores `VISIBLE` in that position (and depending on option order
// even `INVISIBLE` may be ignored), so the new index would inherit
// the visibility of the dropped one. A separate `ALTER TABLE ...
// ALTER INDEX x VISIBLE/INVISIBLE` always applies cleanly. Primary
// keys cannot be invisible, so we never emit the keyword for them.
$indexName = $index->getName();
if ($index->getChoice() !== 'PRIMARY' && $indexName !== '') {
$sqlQuery .= sprintf(
' ALTER TABLE %s.%s ALTER INDEX %s %s;',
Util::backquote($dbName),
Util::backquote($tableName),
Util::backquote($indexName),
$index->isVisible() ? 'VISIBLE' : 'INVISIBLE',
);
}
return $sqlQuery;
}

View File

@ -72,6 +72,7 @@
<th>Type</th>
<th>Unique</th>
<th>Packed</th>
<th>Visible</th>
<th>Column</th>
<th>Cardinality</th>
<th>Collation</th>
@ -86,6 +87,7 @@
<td rowspan="1">PRIMARY</td>
<td rowspan="1">Yes</td>
<td rowspan="1">No</td>
<td rowspan="1">Yes</td>
<td>
id

View File

@ -91,7 +91,7 @@ class IndexRenameControllerTest extends AbstractTestCase
// phpcs:disable Generic.Files.LineLength.TooLong
$expected = <<<'HTML'
<div class="preview_sql">
<pre><code class="sql" dir="ltr">ALTER TABLE `test_db`.`test_table_index_rename` DROP INDEX `old_name`, ADD INDEX `new_name` (`name`) USING BTREE;</code></pre>
<pre><code class="sql" dir="ltr">ALTER TABLE `test_db`.`test_table_index_rename` DROP INDEX `old_name`, ADD INDEX `new_name` (`name`) USING BTREE; ALTER TABLE `test_db`.`test_table_index_rename` ALTER INDEX `new_name` VISIBLE;</code></pre>
</div>
HTML;

View File

@ -128,6 +128,19 @@ class IndexTest extends AbstractTestCase
);
}
public function testIsVisibleDefaultsToTrue(): void
{
$index = new Index();
self::assertTrue($index->isVisible());
}
public function testIsVisibleReadsFromShowIndexes(): void
{
// SHOW INDEXES on MySQL 8.0+ returns 'YES' or 'NO' for the Visible column.
self::assertFalse((new Index(['Visible' => 'NO']))->isVisible());
self::assertTrue((new Index(['Visible' => 'YES']))->isVisible());
}
/**
* Test for get Name & set Name
*/

View File

@ -86,4 +86,92 @@ class IndexesTest extends AbstractTestCase
$indexes->getSqlQueryForIndexCreateOrEdit('PRIMARY', $index, $db, $table);
self::assertInstanceOf(Message::class, $indexes->getError());
}
public function testGetSqlQueryForInvisibleIndex(): void
{
$table = $this->getMockBuilder(Table::class)
->disableOriginalConstructor()
->getMock();
$table->expects(self::any())->method('isEngine')->willReturn(false);
$this->dbi->expects(self::any())->method('getTable')->willReturn($table);
$indexes = new Indexes($this->dbi);
$index = new Index([
'Key_name' => 'idx_email',
'Index_choice' => 'INDEX',
'Visible' => 'NO',
'columns' => [['Column_name' => 'email']],
]);
$expected = 'ALTER TABLE `pma_db`.`pma_table` ADD INDEX `idx_email` (`email`);'
. ' ALTER TABLE `pma_db`.`pma_table` ALTER INDEX `idx_email` INVISIBLE;';
self::assertSame($expected, $indexes->getSqlQueryForIndexCreateOrEdit(null, $index, 'pma_db', 'pma_table'));
}
public function testGetSqlQueryForEditIndexToInvisible(): void
{
$table = $this->getMockBuilder(Table::class)
->disableOriginalConstructor()
->getMock();
$table->expects(self::any())->method('isEngine')->willReturn(false);
$this->dbi->expects(self::any())->method('getTable')->willReturn($table);
$indexes = new Indexes($this->dbi);
$index = new Index([
'Key_name' => 'idx_email',
'Index_choice' => 'INDEX',
'Visible' => 'NO',
'columns' => [['Column_name' => 'email']],
]);
$expected = 'ALTER TABLE `pma_db`.`pma_table` DROP INDEX `idx_email`,'
. ' ADD INDEX `idx_email` (`email`);'
. ' ALTER TABLE `pma_db`.`pma_table` ALTER INDEX `idx_email` INVISIBLE;';
$sql = $indexes->getSqlQueryForIndexCreateOrEdit('idx_email', $index, 'pma_db', 'pma_table');
self::assertSame($expected, $sql);
}
public function testGetSqlQueryForVisibleIndexEmitsVisible(): void
{
// The visibility keyword is set via a separate `ALTER INDEX` statement
// because MySQL silently ignores `VISIBLE` inside compound
// `DROP INDEX x, ADD INDEX x` clauses (the new index inherits the
// previous visibility otherwise).
$table = $this->getMockBuilder(Table::class)
->disableOriginalConstructor()
->getMock();
$table->expects(self::any())->method('isEngine')->willReturn(false);
$this->dbi->expects(self::any())->method('getTable')->willReturn($table);
$indexes = new Indexes($this->dbi);
$index = new Index([
'Key_name' => 'idx_email',
'Index_choice' => 'INDEX',
'columns' => [['Column_name' => 'email']],
]);
$expected = 'ALTER TABLE `pma_db`.`pma_table` DROP INDEX `idx_email`,'
. ' ADD INDEX `idx_email` (`email`);'
. ' ALTER TABLE `pma_db`.`pma_table` ALTER INDEX `idx_email` VISIBLE;';
$sql = $indexes->getSqlQueryForIndexCreateOrEdit('idx_email', $index, 'pma_db', 'pma_table');
self::assertSame($expected, $sql);
}
public function testGetSqlQueryForPrimaryDoesNotEmitVisibilityKeyword(): void
{
// Primary keys cannot be invisible — never emit the keyword for them.
$table = $this->getMockBuilder(Table::class)
->disableOriginalConstructor()
->getMock();
$this->dbi->expects(self::any())->method('getTable')->willReturn($table);
$indexes = new Indexes($this->dbi);
$index = new Index([
'Key_name' => 'PRIMARY',
'columns' => [['Column_name' => 'id']],
]);
$sql = $indexes->getSqlQueryForIndexCreateOrEdit(null, $index, 'pma_db', 'pma_table');
self::assertSame('ALTER TABLE `pma_db`.`pma_table` ADD PRIMARY KEY (`id`);', $sql);
}
}