Skip to content

Commit 3e07ddf

Browse files
committed
Added support of functional indexes for MySQL and Postgres
1 parent 0b77350 commit 3e07ddf

14 files changed

+332
-56
lines changed

Diff for: src/Driver/AbstractMySQLDriver.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Doctrine\DBAL\Platforms\MariaDB1052Platform;
1313
use Doctrine\DBAL\Platforms\MariaDB1060Platform;
1414
use Doctrine\DBAL\Platforms\MariaDBPlatform;
15+
use Doctrine\DBAL\Platforms\MySQL8013Platform;
1516
use Doctrine\DBAL\Platforms\MySQL80Platform;
1617
use Doctrine\DBAL\Platforms\MySQLPlatform;
1718
use Doctrine\DBAL\ServerVersionProvider;
@@ -46,7 +47,14 @@ public function getDatabasePlatform(ServerVersionProvider $versionProvider): Abs
4647
return new MariaDBPlatform();
4748
}
4849

49-
if (version_compare($version, '8.0.0', '>=')) {
50+
if (version_compare($version, '8.0.13', '>=')) {
51+
return new MySQL8013Platform();
52+
}
53+
54+
if (
55+
version_compare($version, '8.0.0', '>=')
56+
&& version_compare($version, '8.0.13', '<')
57+
) {
5058
return new MySQL80Platform();
5159
}
5260

Diff for: src/Platforms/AbstractMySQLPlatform.php

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ abstract class AbstractMySQLPlatform extends AbstractPlatform
4343
final public const LENGTH_LIMIT_BLOB = 65535;
4444
final public const LENGTH_LIMIT_MEDIUMBLOB = 16777215;
4545

46+
public function getColumnNameForIndexFetch(): string
47+
{
48+
return 'COLUMN_NAME';
49+
}
50+
4651
protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string
4752
{
4853
if ($limit !== null) {

Diff for: src/Platforms/AbstractPlatform.php

+22
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@
5555
use function str_contains;
5656
use function str_replace;
5757
use function strlen;
58+
use function strrpos;
5859
use function strtolower;
5960
use function strtoupper;
61+
use function substr;
6062

6163
/**
6264
* Base class for all DatabasePlatforms. The DatabasePlatforms are the central
@@ -1082,6 +1084,18 @@ public function getCreateIndexSQL(Index $index, string $table): string
10821084
));
10831085
}
10841086

1087+
foreach ($columns as $column) {
1088+
if (Index::isFunctionalIndex($column) && ! $this->supportsFunctionalIndex()) {
1089+
throw new InvalidArgumentException(sprintf(
1090+
'Index "%s" on table "%s" contains a functional part, ' .
1091+
'but platform "%s" does not support functional indexes.',
1092+
$name,
1093+
$table,
1094+
substr(static::class, (int) strrpos(static::class, '\\') + 1),
1095+
));
1096+
}
1097+
}
1098+
10851099
if ($index->isPrimary()) {
10861100
return $this->getCreatePrimaryKeySQL($index, $table);
10871101
}
@@ -1974,6 +1988,14 @@ public function supportsColumnCollation(): bool
19741988
return false;
19751989
}
19761990

1991+
/**
1992+
* A flag that indicates whether the platform supports functional indexes.
1993+
*/
1994+
public function supportsFunctionalIndex(): bool
1995+
{
1996+
return false;
1997+
}
1998+
19771999
/**
19782000
* Gets the format string, as accepted by the date() function, that describes
19792001
* the format of a stored datetime value of this platform.

Diff for: src/Platforms/MySQL8013Platform.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Platforms;
6+
7+
/**
8+
* Provides features of the MySQL since 8.0.13 database platform.
9+
*
10+
* Note: Should not be used with versions prior to 8.0.13.
11+
*/
12+
class MySQL8013Platform extends MySQL80Platform
13+
{
14+
public function getColumnNameForIndexFetch(): string
15+
{
16+
return "COALESCE(COLUMN_NAME, CONCAT('(', REPLACE(EXPRESSION, '\\\''', ''''), ')'))";
17+
}
18+
19+
public function supportsFunctionalIndex(): bool
20+
{
21+
return true;
22+
}
23+
}

Diff for: src/Platforms/PostgreSQLPlatform.php

+5
Original file line numberDiff line numberDiff line change
@@ -781,4 +781,9 @@ public function createSchemaManager(Connection $connection): PostgreSQLSchemaMan
781781
{
782782
return new PostgreSQLSchemaManager($connection, $this);
783783
}
784+
785+
public function supportsFunctionalIndex(): bool
786+
{
787+
return true;
788+
}
784789
}

Diff for: src/Platforms/SQLitePlatform.php

+13
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use function sprintf;
3636
use function str_replace;
3737
use function strpos;
38+
use function strrpos;
3839
use function strtolower;
3940
use function substr;
4041
use function trim;
@@ -561,6 +562,18 @@ public function getCreateIndexSQL(Index $index, string $table): string
561562
));
562563
}
563564

565+
foreach ($columns as $column) {
566+
if (Index::isFunctionalIndex($column) && ! $this->supportsFunctionalIndex()) {
567+
throw new InvalidArgumentException(sprintf(
568+
'Index "%s" on table "%s" contains a functional part, ' .
569+
'but platform "%s" does not support functional indexes.',
570+
$name,
571+
$table,
572+
substr(static::class, (int) strrpos(static::class, '\\') + 1),
573+
));
574+
}
575+
}
576+
564577
if ($index->isPrimary()) {
565578
return $this->getCreatePrimaryKeySQL($index, $table);
566579
}

Diff for: src/Schema/Index.php

+25-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use function array_search;
1313
use function array_shift;
1414
use function count;
15+
use function str_ends_with;
16+
use function str_starts_with;
1517
use function strtolower;
1618

1719
class Index extends AbstractAsset
@@ -27,6 +29,8 @@ class Index extends AbstractAsset
2729

2830
protected bool $_isPrimary = false;
2931

32+
protected bool $_isFunctional = false;
33+
3034
/**
3135
* Platform specific flags for indexes.
3236
*
@@ -58,6 +62,10 @@ public function __construct(
5862

5963
foreach ($columns as $column) {
6064
$this->_addColumn($column);
65+
66+
$this->_isFunctional = $this->_isFunctional === true
67+
? $this->_isFunctional
68+
: self::isFunctionalIndex($column);
6169
}
6270

6371
foreach ($flags as $flag) {
@@ -101,10 +109,14 @@ public function getQuotedColumns(AbstractPlatform $platform): array
101109
foreach ($this->_columns as $column) {
102110
$length = array_shift($subParts);
103111

104-
$quotedColumn = $column->getQuotedName($platform);
112+
if ($this->isFunctional()) {
113+
$quotedColumn = $column->getName();
114+
} else {
115+
$quotedColumn = $column->getQuotedName($platform);
105116

106-
if ($length !== null) {
107-
$quotedColumn .= '(' . $length . ')';
117+
if ($length !== null) {
118+
$quotedColumn .= '(' . $length . ')';
119+
}
108120
}
109121

110122
$columns[] = $quotedColumn;
@@ -137,6 +149,11 @@ public function isPrimary(): bool
137149
return $this->_isPrimary;
138150
}
139151

152+
public function isFunctional(): bool
153+
{
154+
return $this->_isFunctional;
155+
}
156+
140157
public function hasColumnAtPosition(string $name, int $pos = 0): bool
141158
{
142159
$name = $this->trimQuotes(strtolower($name));
@@ -283,6 +300,11 @@ public function getOptions(): array
283300
return $this->options;
284301
}
285302

303+
public static function isFunctionalIndex(string $name): bool
304+
{
305+
return str_starts_with($name, '(') && str_ends_with($name, ')');
306+
}
307+
286308
/**
287309
* Return whether the two indexes have the same partial index
288310
*/

Diff for: src/Schema/MySQLSchemaManager.php

+17-2
Original file line numberDiff line numberDiff line change
@@ -390,10 +390,12 @@ protected function selectIndexColumns(string $databaseName, ?string $tableName =
390390
$sql .= ' TABLE_NAME,';
391391
}
392392

393-
$sql .= <<<'SQL'
393+
$columnName = $this->getColumnNameForIndexFetch();
394+
395+
$sql .= <<<SQL
394396
NON_UNIQUE AS Non_Unique,
395397
INDEX_NAME AS Key_name,
396-
COLUMN_NAME AS Column_Name,
398+
{$columnName},
397399
SUB_PART AS Sub_Part,
398400
INDEX_TYPE AS Index_Type
399401
FROM information_schema.STATISTICS
@@ -539,4 +541,17 @@ private function getDefaultTableOptions(): DefaultTableOptions
539541

540542
return $this->defaultTableOptions;
541543
}
544+
545+
/**
546+
* EXPRESSION
547+
*
548+
* MySQL 8.0.13 and higher supports functional key parts (see Functional Key Parts), which affects both
549+
* the COLUMN_NAME and EXPRESSION columns:
550+
* For a nonfunctional key part, COLUMN_NAME indicates the column indexed by the key part and EXPRESSION is NULL.
551+
* For a functional key part, COLUMN_NAME column is NULL and EXPRESSION indicates the expression for the key part.
552+
*/
553+
private function getColumnNameForIndexFetch(): string
554+
{
555+
return $this->platform->getColumnNameForIndexFetch() . ' as Column_Name';
556+
}
542557
}

Diff for: src/Schema/PostgreSQLSchemaManager.php

+54-49
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
use function implode;
2020
use function in_array;
2121
use function is_string;
22+
use function json_decode;
2223
use function preg_match;
23-
use function sprintf;
2424
use function str_contains;
2525
use function str_replace;
2626
use function strtolower;
@@ -161,30 +161,16 @@ protected function _getPortableTableIndexesList(array $tableIndexes, string $tab
161161
{
162162
$buffer = [];
163163
foreach ($tableIndexes as $row) {
164-
$colNumbers = array_map('intval', explode(' ', $row['indkey']));
165-
$columnNameSql = sprintf(
166-
'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC',
167-
$row['indrelid'],
168-
implode(' ,', $colNumbers),
169-
);
170-
171-
$indexColumns = $this->connection->fetchAllAssociative($columnNameSql);
172-
173-
// required for getting the order of the columns right.
174-
foreach ($colNumbers as $colNum) {
175-
foreach ($indexColumns as $colRow) {
176-
if ($colNum !== $colRow['attnum']) {
177-
continue;
178-
}
179-
180-
$buffer[] = [
181-
'key_name' => $row['relname'],
182-
'column_name' => trim($colRow['attname']),
183-
'non_unique' => ! $row['indisunique'],
184-
'primary' => $row['indisprimary'],
185-
'where' => $row['where'],
186-
];
187-
}
164+
$indexColumns = json_decode($row['index_columns'], true);
165+
166+
foreach ($indexColumns as $colRow) {
167+
$buffer[] = [
168+
'key_name' => $row['relname'],
169+
'column_name' => trim($colRow),
170+
'non_unique' => ! $row['indisunique'],
171+
'primary' => $row['indisprimary'],
172+
'where' => $row['where'],
173+
];
188174
}
189175
}
190176

@@ -468,34 +454,53 @@ protected function selectTableColumns(string $databaseName, ?string $tableName =
468454

469455
protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
470456
{
471-
$sql = 'SELECT';
472-
457+
$tableNameSql = '';
473458
if ($tableName === null) {
474-
$sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,';
459+
$tableNameSql = <<<'SQL'
460+
tc.relname AS table_name,
461+
tn.nspname AS schema_name,
462+
SQL;
475463
}
476464

477-
$sql .= <<<'SQL'
478-
quote_ident(ic.relname) AS relname,
479-
i.indisunique,
480-
i.indisprimary,
481-
i.indkey,
482-
i.indrelid,
483-
pg_get_expr(indpred, indrelid) AS "where"
484-
FROM pg_index i
485-
JOIN pg_class AS tc ON tc.oid = i.indrelid
486-
JOIN pg_namespace tn ON tn.oid = tc.relnamespace
487-
JOIN pg_class AS ic ON ic.oid = i.indexrelid
488-
WHERE ic.oid IN (
489-
SELECT indexrelid
490-
FROM pg_index i, pg_class c, pg_namespace n
491-
SQL;
492-
493-
$conditions = array_merge([
494-
'c.oid = i.indrelid',
495-
'c.relnamespace = n.oid',
496-
], $this->buildQueryConditions($tableName));
465+
$whereConditions = array_merge(
466+
[
467+
'c.oid = i.indrelid',
468+
'c.relnamespace = n.oid',
469+
],
470+
$this->buildQueryConditions($tableName),
471+
);
497472

498-
$sql .= ' WHERE ' . implode(' AND ', $conditions) . ')';
473+
$whereSql = implode(' AND ', $whereConditions);
474+
475+
$sql = <<<SQL
476+
SELECT
477+
{$tableNameSql}
478+
quote_ident(ic.relname) AS relname,
479+
i.indisunique,
480+
i.indisprimary,
481+
i.indkey,
482+
i.indrelid,
483+
pg_get_expr(indpred, indrelid) AS "where",
484+
(
485+
SELECT
486+
json_agg(
487+
pg_get_indexdef(i.indexrelid, a.attnum, true)
488+
) as index_columns
489+
FROM pg_attribute AS a
490+
WHERE a.attrelid = ic.oid
491+
)
492+
FROM pg_index i
493+
JOIN pg_class AS tc ON tc.oid = i.indrelid
494+
JOIN pg_namespace tn ON tn.oid = tc.relnamespace
495+
JOIN pg_class AS ic ON ic.oid = i.indexrelid
496+
WHERE ic.oid IN (
497+
SELECT indexrelid
498+
FROM pg_index i,
499+
pg_class c,
500+
pg_namespace n
501+
WHERE {$whereSql}
502+
)
503+
SQL;
499504

500505
return $this->connection->executeQuery($sql);
501506
}

Diff for: src/Schema/Table.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,7 @@ private function _createIndex(
743743
}
744744

745745
foreach ($columns as $columnName) {
746-
if (! $this->hasColumn($columnName)) {
746+
if (! $this->hasColumn($columnName) && ! Index::isFunctionalIndex($columnName)) {
747747
throw ColumnDoesNotExist::new($columnName, $this->_name);
748748
}
749749
}

0 commit comments

Comments
 (0)