phpmyadmin/phpmyadmin

View on GitHub
src/Index.php

Summary

Maintainability
D
1 day
Test Coverage
<?php

declare(strict_types=1);

namespace PhpMyAdmin;

use function __;
use function array_pop;
use function count;
use function htmlspecialchars;

/**
 * Index manipulation class
 */
class Index
{
    public const PRIMARY = 1;
    public const UNIQUE = 2;
    public const INDEX = 4;
    public const SPATIAL = 8;
    public const FULLTEXT = 16;

    /**
     * Class-wide storage container for indexes (caching, singleton)
     *
     * @var array<string, array<string, array<string, Index>>>
     */
    private static array $registry = [];

    /** @var string The name of the schema */
    private string $schema = '';

    /** @var string The name of the table */
    private string $table = '';

    /** @var string The name of the index */
    private string $name = '';

    /**
     * Columns in index
     *
     * @var array<string|int, IndexColumn>
     */
    private array $columns = [];

    /**
     * The index method used (BTREE, HASH, RTREE).
     */
    private string $type = '';

    /**
     * The index choice (PRIMARY, UNIQUE, INDEX, SPATIAL, FULLTEXT)
     */
    private string $choice = '';

    /**
     * Various remarks.
     */
    private string $remarks = '';

    /**
     * Any comment provided for the index with a COMMENT attribute when the
     * index was created.
     */
    private string $comment = '';

    /** @var bool false if the index cannot contain duplicates, true if it can. */
    private bool $nonUnique = false;

    /**
     * Indicates how the key is packed. NULL if it is not.
     */
    private string|null $packed = null;

    /**
     * Block size for the index
     */
    private int $keyBlockSize = 0;

    /**
     * Parser option for the index
     */
    private string $parser = '';

    /** @param mixed[] $params parameters */
    public function __construct(array $params = [])
    {
        $this->set($params);
    }

    /**
     * Creates (if not already created) and returns the corresponding Index object
     *
     * @return Index corresponding Index object
     */
    public static function singleton(
        DatabaseInterface $dbi,
        string $schema,
        string $table,
        string $indexName = '',
    ): Index {
        self::loadIndexes($dbi, $table, $schema);
        if (isset(self::$registry[$schema][$table][$indexName])) {
            return self::$registry[$schema][$table][$indexName];
        }

        $index = new Index();
        if ($indexName !== '') {
            $index->setName($indexName);
            self::$registry[$schema][$table][$index->getName()] = $index;
        }

        return $index;
    }

    /**
     * returns an array with all indexes from the given table
     *
     * @return Index[]
     */
    public static function getFromTable(DatabaseInterface $dbi, string $table, string $schema): array
    {
        self::loadIndexes($dbi, $table, $schema);

        return self::$registry[$schema][$table] ?? [];
    }

    /**
     * Returns an array with all indexes from the given table of the requested types
     *
     * @param string $table   table
     * @param string $schema  schema
     * @param int    $choices choices
     *
     * @return Index[] array of indexes
     */
    public static function getFromTableByChoice(string $table, string $schema, int $choices = 31): array
    {
        $indexes = [];
        foreach (self::getFromTable(DatabaseInterface::getInstance(), $table, $schema) as $index) {
            if (($choices & self::PRIMARY) && $index->getChoice() === 'PRIMARY') {
                $indexes[] = $index;
            }

            if (($choices & self::UNIQUE) && $index->getChoice() === 'UNIQUE') {
                $indexes[] = $index;
            }

            if (($choices & self::INDEX) && $index->getChoice() === 'INDEX') {
                $indexes[] = $index;
            }

            if (($choices & self::SPATIAL) && $index->getChoice() === 'SPATIAL') {
                $indexes[] = $index;
            }

            if ((($choices & self::FULLTEXT) === 0) || $index->getChoice() !== 'FULLTEXT') {
                continue;
            }

            $indexes[] = $index;
        }

        return $indexes;
    }

    public static function getPrimary(DatabaseInterface $dbi, string $table, string $schema): Index|null
    {
        self::loadIndexes($dbi, $table, $schema);

        return self::$registry[$schema][$table]['PRIMARY'] ?? null;
    }

    /**
     * Load index data for table
     */
    private static function loadIndexes(DatabaseInterface $dbi, string $table, string $schema): void
    {
        if (isset(self::$registry[$schema][$table])) {
            return;
        }

        $rawIndexes = $dbi->getTableIndexes($schema, $table);
        foreach ($rawIndexes as $eachIndex) {
            $eachIndex['Schema'] = $schema;
            $keyName = $eachIndex['Key_name'];
            if (! isset(self::$registry[$schema][$table][$keyName])) {
                $key = new Index($eachIndex);
                self::$registry[$schema][$table][$keyName] = $key;
            } else {
                $key = self::$registry[$schema][$table][$keyName];
            }

            $key->addColumn($eachIndex);
        }
    }

    /**
     * Add column to index
     *
     * @param array<string, string|null> $params column params
     */
    public function addColumn(array $params): void
    {
        $key = $params['Column_name'] ?? $params['Expression'] ?? '';
        if (isset($params['Expression'])) {
            // The Expression only does not make the key unique, add a sequence number
            $key .= $params['Seq_in_index'];
        }

        if ($key === '') {
            return;
        }

        $this->columns[$key] = new IndexColumn($params);
    }

    /**
     * Adds a list of columns to the index
     *
     * @param mixed[] $columns array containing details about the columns
     */
    public function addColumns(array $columns): void
    {
        $addedColumns = [];

        if (isset($columns['names'])) {
            // coming from form
            // $columns[names][]
            // $columns[sub_parts][]
            foreach ($columns['names'] as $key => $name) {
                $subPart = $columns['sub_parts'][$key] ?? '';
                $addedColumns[] = ['Column_name' => $name, 'Sub_part' => $subPart];
            }
        } else {
            // coming from SHOW INDEXES
            // $columns[][name]
            // $columns[][sub_part]
            // ...
            $addedColumns = $columns;
        }

        foreach ($addedColumns as $column) {
            $this->addColumn($column);
        }
    }

    /**
     * Returns true if $column indexed in this index
     *
     * @param string $column the column
     */
    public function hasColumn(string $column): bool
    {
        return isset($this->columns[$column]);
    }

    /**
     * Sets index details
     *
     * @param mixed[] $params index details
     */
    public function set(array $params): void
    {
        if (isset($params['columns'])) {
            $this->addColumns($params['columns']);
        }

        if (isset($params['Schema'])) {
            $this->schema = $params['Schema'];
        }

        if (isset($params['Table'])) {
            $this->table = $params['Table'];
        }

        if (isset($params['Key_name'])) {
            $this->name = $params['Key_name'];
        }

        if (isset($params['Index_type'])) {
            $this->type = $params['Index_type'];
        }

        if (isset($params['Comment'])) {
            $this->remarks = $params['Comment'];
        }

        if (isset($params['Index_comment'])) {
            $this->comment = $params['Index_comment'];
        }

        if (isset($params['Non_unique'])) {
            $this->nonUnique = (bool) $params['Non_unique'];
        }

        if (isset($params['Packed'])) {
            $this->packed = $params['Packed'];
        }

        if (isset($params['Index_choice'])) {
            $this->choice = $params['Index_choice'];
        } elseif ($this->name === 'PRIMARY') {
            $this->choice = 'PRIMARY';
        } elseif ($this->type === 'FULLTEXT') {
            $this->choice = 'FULLTEXT';
            $this->type = '';
        } elseif ($this->type === 'SPATIAL') {
            $this->choice = 'SPATIAL';
            $this->type = '';
        } elseif (! $this->nonUnique) {
            $this->choice = 'UNIQUE';
        } else {
            $this->choice = 'INDEX';
        }

        if (isset($params['Key_block_size'])) {
            $this->keyBlockSize = (int) $params['Key_block_size'];
        }

        if (! isset($params['Parser'])) {
            return;
        }

        $this->parser = $params['Parser'];
    }

    /**
     * Returns the number of columns of the index
     *
     * @return int the number of the columns
     */
    public function getColumnCount(): int
    {
        return count($this->columns);
    }

    /**
     * Returns the index comment
     *
     * @return string index comment
     */
    public function getComment(): string
    {
        return $this->comment;
    }

    /**
     * Returns index remarks
     *
     * @return string index remarks
     */
    public function getRemarks(): string
    {
        return $this->remarks;
    }

    /**
     * Return the key block size
     */
    public function getKeyBlockSize(): int
    {
        return $this->keyBlockSize;
    }

    /**
     * Return the parser
     */
    public function getParser(): string
    {
        return $this->parser;
    }

    /**
     * Returns concatenated remarks and comment
     *
     * @return string concatenated remarks and comment
     */
    public function getComments(): string
    {
        $comments = $this->getRemarks();
        if ($comments !== '') {
            $comments .= "\n";
        }

        $comments .= $this->getComment();

        return $comments;
    }

    /**
     * Returns index type (BTREE, HASH, RTREE)
     *
     * @return string index type
     */
    public function getType(): string
    {
        return $this->type;
    }

    /**
     * Returns index choice (PRIMARY, UNIQUE, INDEX, SPATIAL, FULLTEXT)
     *
     * @return string index choice
     */
    public function getChoice(): string
    {
        return $this->choice;
    }

    /**
     * Returns a lit of all index types
     *
     * @return string[] index types
     */
    public static function getIndexTypes(): array
    {
        return ['BTREE', 'HASH'];
    }

    public function hasPrimary(): bool
    {
        return self::getPrimary(DatabaseInterface::getInstance(), $this->table, $this->schema) !== null;
    }

    /**
     * Returns how the index is packed
     *
     * @return string|null how the index is packed
     */
    public function getPacked(): string|null
    {
        return $this->packed;
    }

    /**
     * Returns 'No' if the index is not packed,
     * how the index is packed if packed
     */
    public function isPacked(): string
    {
        if ($this->packed === null) {
            return __('No');
        }

        return htmlspecialchars($this->packed);
    }

    /**
     * Returns bool false if the index cannot contain duplicates, true if it can
     *
     * @return bool false if the index cannot contain duplicates, true if it can
     */
    public function getNonUnique(): bool
    {
        return $this->nonUnique;
    }

    /**
     * Returns whether the index is a 'Unique' index
     *
     * @param bool $asText whether to output should be in text
     *
     * @return string|bool whether the index is a 'Unique' index
     */
    public function isUnique(bool $asText = false): string|bool
    {
        if ($asText) {
            return $this->nonUnique ? __('No') : __('Yes');
        }

        return ! $this->nonUnique;
    }

    /**
     * Returns the name of the index
     *
     * @return string the name of the index
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * Sets the name of the index
     */
    public function setName(string $name): void
    {
        $this->name = $name;
    }

    /**
     * Returns the columns of the index
     *
     * @return IndexColumn[]
     */
    public function getColumns(): array
    {
        return $this->columns;
    }

    /**
     * Gets the properties in an array for comparison purposes
     *
     * @return array<string, array<int, array<string, int|string|null>>|string|null>
     * @psalm-return array{
     *   Packed: string|null,
     *   Index_choice: string,
     *   columns?: list<array{
     *     Column_name: string,
     *     Seq_in_index: int,
     *     Collation: string|null,
     *     Sub_part: int|null,
     *     Null: string
     *   }>
     * }
     */
    public function getCompareData(): array
    {
        $data = ['Packed' => $this->packed, 'Index_choice' => $this->choice];

        foreach ($this->columns as $column) {
            $data['columns'][] = $column->getCompareData();
        }

        return $data;
    }

    /**
     * Function to check over array of indexes and look for common problems
     *
     * @param string $table  table name
     * @param string $schema schema name
     *
     * @return string  Output HTML
     */
    public static function findDuplicates(string $table, string $schema): string
    {
        $indexes = self::getFromTable(DatabaseInterface::getInstance(), $table, $schema);

        $output = '';

        // count($indexes) < 2:
        //   there is no need to check if there less than two indexes
        if (count($indexes) < 2) {
            return $output;
        }

        // remove last index from stack and ...
        while ($whileIndex = array_pop($indexes)) {
            // ... compare with every remaining index in stack
            foreach ($indexes as $eachIndex) {
                if ($eachIndex->getCompareData() !== $whileIndex->getCompareData()) {
                    continue;
                }

                // did not find any difference
                // so it makes no sense to have this two equal indexes

                $message = Message::notice(
                    __(
                        'The indexes %1$s and %2$s seem to be equal and one of them could possibly be removed.',
                    ),
                );
                $message->addParam($eachIndex->getName());
                $message->addParam($whileIndex->getName());
                $output .= $message->getDisplay();

                // there is no need to check any further indexes if we have already
                // found that this one has a duplicate
                continue 2;
            }
        }

        return $output;
    }
}