[TASK] Extract per-connection concerns from SchemaMigrator
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Database / Schema / SchemaMigrator.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Core\Database\Schema;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Doctrine\DBAL\DBALException;
19 use Doctrine\DBAL\Schema\Schema;
20 use Doctrine\DBAL\Schema\SchemaDiff;
21 use Doctrine\DBAL\Schema\Table;
22 use TYPO3\CMS\Core\Database\ConnectionPool;
23 use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
24 use TYPO3\CMS\Core\Database\Schema\Parser\Parser;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26
27 /**
28 * Helper methods to handle SQL files and transform them into individual statements
29 * for further processing.
30 *
31 * @internal
32 */
33 class SchemaMigrator
34 {
35 /**
36 * @var Schema[]
37 */
38 protected $schema = [];
39
40 /**
41 * Compare current and expected schema definitions and provide updates suggestions in the form
42 * of SQL statements.
43 *
44 * @param string[] $statements The CREATE TABLE statements
45 * @param bool $remove TRUE for RENAME/DROP table and column suggestions, FALSE for ADD/CHANGE suggestions
46 * @return array[] SQL statements to migrate the database to the expected schema, indexed by performed operation
47 * @throws \Doctrine\DBAL\DBALException
48 * @throws \Doctrine\DBAL\Schema\SchemaException
49 * @throws \InvalidArgumentException
50 * @throws \RuntimeException
51 * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
52 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
53 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
54 * @throws StatementException
55 */
56 public function getUpdateSuggestions(array $statements, bool $remove = false): array
57 {
58 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
59 $tables = $this->parseCreateTableStatements($statements);
60
61 $updateSuggestions = [];
62
63 foreach ($connectionPool->getConnectionNames() as $connectionName) {
64 $connectionMigrator = ConnectionMigrator::create(
65 $connectionName,
66 $tables
67 );
68
69 $updateSuggestions[$connectionName] =
70 $connectionMigrator->getUpdateSuggestions($remove);
71 }
72
73 return $updateSuggestions;
74 }
75
76 /**
77 * Return the raw Doctrine SchemaDiff objects for each connection. This diff contains
78 * all changes without any pre-processing.
79 *
80 * @param array $statements
81 * @return SchemaDiff[]
82 * @throws \Doctrine\DBAL\DBALException
83 * @throws \Doctrine\DBAL\Schema\SchemaException
84 * @throws \InvalidArgumentException
85 * @throws \RuntimeException
86 * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
87 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
88 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
89 * @throws StatementException
90 */
91 public function getSchemaDiffs(array $statements): array
92 {
93 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
94 $tables = $this->parseCreateTableStatements($statements);
95
96 $schemaDiffs = [];
97
98 foreach ($connectionPool->getConnectionNames() as $connectionName) {
99 $connectionMigrator = ConnectionMigrator::create(
100 $connectionName,
101 $tables
102 );
103 $schemaDiffs[$connectionName] = $connectionMigrator->getSchemaDiff();
104 }
105
106 return $schemaDiffs;
107 }
108
109 /**
110 * This method executes statements from the update suggestions, or a subset of them
111 * filtered by the statements hashes, one by one.
112 *
113 * @param string[] $statements The CREATE TABLE statements
114 * @param string[] $selectedStatements The hashes of the update suggestions to execute
115 * @return array
116 * @throws \Doctrine\DBAL\DBALException
117 * @throws \Doctrine\DBAL\Schema\SchemaException
118 * @throws \InvalidArgumentException
119 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
120 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
121 * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
122 * @throws StatementException
123 * @throws \RuntimeException
124 */
125 public function migrate(array $statements, array $selectedStatements): array
126 {
127 $result = [];
128 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
129 $updateSuggestionsPerConnection = array_merge_recursive(
130 $this->getUpdateSuggestions($statements),
131 $this->getUpdateSuggestions($statements, true)
132 );
133
134 foreach ($updateSuggestionsPerConnection as $connectionName => $updateSuggestions) {
135 unset($updateSuggestions['tables_count'], $updateSuggestions['change_currentValue']);
136 $updateSuggestions = array_merge(...array_values($updateSuggestions));
137 $statementsToExecute = array_intersect_key($updateSuggestions, $selectedStatements);
138 if (count($statementsToExecute) === 0) {
139 continue;
140 }
141
142 $connection = $connectionPool->getConnectionByName($connectionName);
143 foreach ($statementsToExecute as $hash => $statement) {
144 try {
145 $connection->executeUpdate($statement);
146 } catch (DBALException $e) {
147 $result[$hash] = $e->getPrevious()->getMessage();
148 }
149 }
150 }
151
152 return $result;
153 }
154
155 /**
156 * Perform add/change/create operations on tables and fields in an optimized,
157 * non-interactive, mode using the original doctrine SchemaManager ->toSaveSql()
158 * method.
159 *
160 * @param string[] $statements The CREATE TABLE statements
161 * @param bool $createOnly Only perform changes that add fields or create tables
162 * @return array[] Error messages for statements that occurred during the installation procedure.
163 * @throws \Doctrine\DBAL\DBALException
164 * @throws \Doctrine\DBAL\Schema\SchemaException
165 * @throws \InvalidArgumentException
166 * @throws \RuntimeException
167 * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
168 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
169 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
170 * @throws StatementException
171 */
172 public function install(array $statements, bool $createOnly = false): array
173 {
174 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
175 $tables = $this->parseCreateTableStatements($statements);
176 $result = [];
177
178 foreach ($connectionPool->getConnectionNames() as $connectionName) {
179 $connectionMigrator = ConnectionMigrator::create(
180 $connectionName,
181 $tables
182 );
183
184 $lastResult = $connectionMigrator->install($createOnly);
185 $result = array_merge($result, $lastResult);
186 }
187
188 return $result;
189 }
190
191 /**
192 * Import static data (INSERT statements)
193 *
194 * @param array $statements
195 * @param bool $truncate
196 * @return array
197 */
198 public function importStaticData(array $statements, bool $truncate = false): array
199 {
200 $result = [];
201 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
202 $insertStatements = [];
203
204 foreach ($statements as $statement) {
205 // Only handle insert statements and extract the table at the same time. Extracting
206 // the table name is required to perform the inserts on the right connection.
207 if (preg_match('/^INSERT\s+INTO\s+`?(\w+)`?(.*)/i', $statement, $matches)) {
208 list(, $tableName, $sqlFragment) = $matches;
209 $insertStatements[$tableName][] = sprintf(
210 'INSERT INTO %s %s',
211 $connectionPool->getConnectionForTable($tableName)->quoteIdentifier($tableName),
212 rtrim($sqlFragment, ';')
213 );
214 }
215 }
216
217 foreach ($insertStatements as $tableName => $perTableStatements) {
218 $connection = $connectionPool->getConnectionForTable($tableName);
219
220 if ($truncate) {
221 $connection->truncate($tableName);
222 }
223
224 foreach ((array)$perTableStatements as $statement) {
225 try {
226 $connection->executeUpdate($statement);
227 $result[$statement] = '';
228 } catch (DBALException $e) {
229 $result[$statement] = $e->getPrevious()->getMessage();
230 }
231 }
232 }
233
234 return $result;
235 }
236
237 /**
238 * Parse CREATE TABLE statements into Doctrine Table objects.
239 *
240 * @param string[] $statements The SQL CREATE TABLE statements
241 * @return Table[]
242 * @throws \Doctrine\DBAL\Schema\SchemaException
243 * @throws \InvalidArgumentException
244 * @throws \RuntimeException
245 * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
246 * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
247 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
248 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
249 */
250 public function parseCreateTableStatements(array $statements): array
251 {
252 $tables = [];
253 foreach ($statements as $statement) {
254 $createTableParser = GeneralUtility::makeInstance(Parser::class, $statement);
255
256 // We need to keep multiple table definitions at this point so
257 // that Extensions can modify existing tables.
258 try {
259 $tables[] = $createTableParser->parse();
260 } catch (StatementException $statementException) {
261 // Enrich the error message with the full invalid statement
262 throw new StatementException(
263 $statementException->getMessage() . ' in statement ' . $statement,
264 1476171315,
265 $statementException
266 );
267 }
268 }
269
270 // Flatten the array of arrays by one level
271 return array_merge(...$tables);
272 }
273 }