From d9b52a59deec5d58838158777e914fc9ee537295 Mon Sep 17 00:00:00 2001
From: Morton Jonuschat <m.jonuschat@mojocode.de>
Date: Sat, 1 Oct 2016 17:02:31 -0700
Subject: [PATCH] [BUGFIX] Doctrine: Consider MySQL index subpart information
 in upgrade wizards

If an index is defined on a table that is stored on a MySQL database
and uses the MySQL specific subpart length feature add the information
to the schema diff so that the upgrade wizards don't show false
positive changes.

Change-Id: I49eb73c18f7b86aad70d11f3e222c44bd1bd827f
Resolves: #78024
Resolves: #79065
Releases: master
Reviewed-on: https://review.typo3.org/50081
Reviewed-by: Markus Klein <markus.klein@typo3.org>
Tested-by: TYPO3com <no-reply@typo3.com>
Tested-by: Markus Klein <markus.klein@typo3.org>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
---
 .../core/Classes/Database/ConnectionPool.php  |   7 +
 .../SchemaIndexDefinitionListener.php         | 121 ++++++++++++++++++
 .../Action/Tool/ImportantActions.php          |  14 +-
 .../Updates/FinalDatabaseSchemaUpdate.php     |   6 +-
 4 files changed, 140 insertions(+), 8 deletions(-)
 create mode 100644 typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaIndexDefinitionListener.php

diff --git a/typo3/sysext/core/Classes/Database/ConnectionPool.php b/typo3/sysext/core/Classes/Database/ConnectionPool.php
index 85700ac1082d..50be13d7f24c 100644
--- a/typo3/sysext/core/Classes/Database/ConnectionPool.php
+++ b/typo3/sysext/core/Classes/Database/ConnectionPool.php
@@ -22,6 +22,7 @@ use Doctrine\DBAL\Types\Type;
 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
 use TYPO3\CMS\Core\Database\Schema\EventListener\SchemaAlterTableListener;
 use TYPO3\CMS\Core\Database\Schema\EventListener\SchemaColumnDefinitionListener;
+use TYPO3\CMS\Core\Database\Schema\EventListener\SchemaIndexDefinitionListener;
 use TYPO3\CMS\Core\Database\Schema\Types\EnumType;
 use TYPO3\CMS\Core\Database\Schema\Types\SetType;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -176,6 +177,12 @@ class ConnectionPool
             GeneralUtility::makeInstance(SchemaColumnDefinitionListener::class)
         );
 
+        // Handler for enhanced index definitions in the SchemaManager
+        $conn->getDatabasePlatform()->getEventManager()->addEventListener(
+            Events::onSchemaIndexDefinition,
+            GeneralUtility::makeInstance(SchemaIndexDefinitionListener::class)
+        );
+
         // Handler for adding custom database platform options to ALTER TABLE
         // requests in the SchemaManager
         $conn->getDatabasePlatform()->getEventManager()->addEventListener(
diff --git a/typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaIndexDefinitionListener.php b/typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaIndexDefinitionListener.php
new file mode 100644
index 000000000000..94fedc9036c2
--- /dev/null
+++ b/typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaIndexDefinitionListener.php
@@ -0,0 +1,121 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\EventListener;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Doctrine\DBAL\Event\SchemaIndexDefinitionEventArgs;
+use Doctrine\DBAL\Schema\Index;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Event listener to handle additional processing for index definitions to integrate
+ * MySQL index sub parts.
+ */
+class SchemaIndexDefinitionListener
+{
+    /**
+     * Listener for index definition events. This intercepts definitions
+     * for indexes and builds the appropriate Index Object taking the sub
+     * part length into account when a MySQL platform has been detected.
+     *
+     * @param \Doctrine\DBAL\Event\SchemaIndexDefinitionEventArgs $event
+     * @throws \Doctrine\DBAL\DBALException
+     * @throws \InvalidArgumentException
+     */
+    public function onSchemaIndexDefinition(SchemaIndexDefinitionEventArgs $event)
+    {
+        if (strpos($event->getConnection()->getServerVersion(), 'MySQL') !== 0) {
+            return;
+        }
+
+        $connection = $event->getConnection();
+        $indexName = $event->getTableIndex()['name'];
+        $sql = $event->getDatabasePlatform()->getListTableIndexesSQL(
+            $event->getTable(),
+            $event->getConnection()->getDatabase()
+        );
+        $sql .= ' AND ' . $connection->quoteIdentifier('INDEX_NAME') . ' = ' . $connection->quote($indexName);
+        $tableIndexes = $event->getConnection()->fetchAll($sql);
+
+        $subPartColumns = array_filter(
+            $tableIndexes,
+            function ($column) {
+                return $column['Sub_Part'];
+            }
+        );
+
+        if (!empty($subPartColumns)) {
+            $event->setIndex($this->buildIndex($tableIndexes));
+            $event->preventDefault();
+        }
+    }
+
+    /**
+     * Build a Doctrine Index Object based on the information
+     * gathered from the MySQL information schema.
+     *
+     * @param array $tableIndexRows
+     * @return \Doctrine\DBAL\Schema\Index
+     * @throws \InvalidArgumentException
+     */
+    protected function buildIndex(array $tableIndexRows): Index
+    {
+        $data = null;
+        foreach ($tableIndexRows as $tableIndex) {
+            $tableIndex = array_change_key_case($tableIndex, CASE_LOWER);
+
+            $tableIndex['primary'] = $tableIndex['key_name'] === 'PRIMARY';
+
+            if (strpos($tableIndex['index_type'], 'FULLTEXT') !== false) {
+                $tableIndex['flags'] = ['FULLTEXT'];
+            } elseif (strpos($tableIndex['index_type'], 'SPATIAL') !== false) {
+                $tableIndex['flags'] = ['SPATIAL'];
+            }
+
+            $indexName = $tableIndex['key_name'];
+            $columnName = $tableIndex['column_name'];
+
+            if ($tableIndex['sub_part'] !== null) {
+                $columnName .= '(' . $tableIndex['sub_part'] . ')';
+            }
+
+            if ($data === null) {
+                $data = [
+                    'name' => $indexName,
+                    'columns' => [$columnName],
+                    'unique' => !$tableIndex['non_unique'],
+                    'primary' => $tableIndex['primary'],
+                    'flags' => $tableIndex['flags'] ?? [],
+                    'options' => isset($tableIndex['where']) ? ['where' => $tableIndex['where']] : [],
+                ];
+            } else {
+                $data['columns'][] = $columnName;
+            }
+        }
+
+        $index = GeneralUtility::makeInstance(
+            Index::class,
+            $data['name'],
+            $data['columns'],
+            $data['unique'],
+            $data['primary'],
+            $data['flags'],
+            $data['options']
+        );
+
+        return $index;
+    }
+}
diff --git a/typo3/sysext/install/Classes/Controller/Action/Tool/ImportantActions.php b/typo3/sysext/install/Classes/Controller/Action/Tool/ImportantActions.php
index d990bd4204c9..6b3ba02ba2fb 100644
--- a/typo3/sysext/install/Classes/Controller/Action/Tool/ImportantActions.php
+++ b/typo3/sysext/install/Classes/Controller/Action/Tool/ImportantActions.php
@@ -381,7 +381,7 @@ class ImportantActions extends Action\AbstractAction
         // Aggregate the per-connection statements into one flat array
         $addCreateChange = array_merge_recursive(...array_values($addCreateChange));
 
-        if (isset($addCreateChange['create_table'])) {
+        if (!empty($addCreateChange['create_table'])) {
             $databaseAnalyzerSuggestion['addTable'] = [];
             foreach ($addCreateChange['create_table'] as $hash => $statement) {
                 $databaseAnalyzerSuggestion['addTable'][$hash] = [
@@ -390,7 +390,7 @@ class ImportantActions extends Action\AbstractAction
                 ];
             }
         }
-        if (isset($addCreateChange['add'])) {
+        if (!empty($addCreateChange['add'])) {
             $databaseAnalyzerSuggestion['addField'] = [];
             foreach ($addCreateChange['add'] as $hash => $statement) {
                 $databaseAnalyzerSuggestion['addField'][$hash] = [
@@ -399,7 +399,7 @@ class ImportantActions extends Action\AbstractAction
                 ];
             }
         }
-        if (isset($addCreateChange['change'])) {
+        if (!empty($addCreateChange['change'])) {
             $databaseAnalyzerSuggestion['change'] = [];
             foreach ($addCreateChange['change'] as $hash => $statement) {
                 $databaseAnalyzerSuggestion['change'][$hash] = [
@@ -416,7 +416,7 @@ class ImportantActions extends Action\AbstractAction
         $dropRename = $schemaMigrationService->getUpdateSuggestions($sqlStatements, true);
         // Aggregate the per-connection statements into one flat array
         $dropRename = array_merge_recursive(...array_values($dropRename));
-        if (isset($dropRename['change_table'])) {
+        if (!empty($dropRename['change_table'])) {
             $databaseAnalyzerSuggestion['renameTableToUnused'] = [];
             foreach ($dropRename['change_table'] as $hash => $statement) {
                 $databaseAnalyzerSuggestion['renameTableToUnused'][$hash] = [
@@ -428,7 +428,7 @@ class ImportantActions extends Action\AbstractAction
                 }
             }
         }
-        if (isset($dropRename['change'])) {
+        if (!empty($dropRename['change'])) {
             $databaseAnalyzerSuggestion['renameTableFieldToUnused'] = [];
             foreach ($dropRename['change'] as $hash => $statement) {
                 $databaseAnalyzerSuggestion['renameTableFieldToUnused'][$hash] = [
@@ -437,7 +437,7 @@ class ImportantActions extends Action\AbstractAction
                 ];
             }
         }
-        if (isset($dropRename['drop'])) {
+        if (!empty($dropRename['drop'])) {
             $databaseAnalyzerSuggestion['deleteField'] = [];
             foreach ($dropRename['drop'] as $hash => $statement) {
                 $databaseAnalyzerSuggestion['deleteField'][$hash] = [
@@ -446,7 +446,7 @@ class ImportantActions extends Action\AbstractAction
                 ];
             }
         }
-        if (isset($dropRename['drop_table'])) {
+        if (!empty($dropRename['drop_table'])) {
             $databaseAnalyzerSuggestion['deleteTable'] = [];
             foreach ($dropRename['drop_table'] as $hash => $statement) {
                 $databaseAnalyzerSuggestion['deleteTable'][$hash] = [
diff --git a/typo3/sysext/install/Classes/Updates/FinalDatabaseSchemaUpdate.php b/typo3/sysext/install/Classes/Updates/FinalDatabaseSchemaUpdate.php
index 701857717fdc..cff63a79955f 100644
--- a/typo3/sysext/install/Classes/Updates/FinalDatabaseSchemaUpdate.php
+++ b/typo3/sysext/install/Classes/Updates/FinalDatabaseSchemaUpdate.php
@@ -62,7 +62,11 @@ class FinalDatabaseSchemaUpdate extends AbstractDatabaseSchemaUpdate
         foreach ($databaseDifferences as $schemaDiff) {
             // A change for a table is required
             if (count($schemaDiff->changedTables) !== 0) {
-                return true;
+                foreach ($schemaDiff->changedTables as $changedTable) {
+                    if (!empty($changedTable->addedColumns) || !empty($changedTable->changedColumns)) {
+                        return true;
+                    }
+                }
             }
         }
 
-- 
GitLab