Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.46% covered (warning)
82.46%
315 / 382
46.81% covered (danger)
46.81%
22 / 47
CRAP
0.00% covered (danger)
0.00%
0 / 1
doctrine
82.46% covered (warning)
82.46%
315 / 382
46.81% covered (danger)
46.81%
22 / 47
216.40
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_connection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_schema_manager
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_schema
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_table_prefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sql_list_tables
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 sql_table_exists
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
3.19
 sql_list_columns
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
3.19
 sql_column_exists
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
3.19
 sql_list_index
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sql_index_exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sql_unique_index_exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 perform_schema_changes
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 sql_create_table
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 sql_table_drop
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 sql_column_add
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 sql_column_change
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
6
 sql_column_remove
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 sql_create_index
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 sql_rename_index
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 sql_index_drop
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 sql_create_unique_index
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 sql_create_primary_key
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 sql_truncate_table
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
3.19
 add_prefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 remove_prefix
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
4
 sql_get_table_index_data
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 get_filtered_index_list
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
3.04
 get_asset_names
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 asset_exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 alter_schema
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
 alter_table
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 schema_perform_changes
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
1 / 1
9
 schema_create_table
89.29% covered (warning)
89.29%
25 / 28
0.00% covered (danger)
0.00%
0 / 1
12.18
 schema_drop_table
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 schema_column_add
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 schema_column_change
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 schema_column_change_add
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 schema_column_remove
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
8.06
 schema_create_index
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
 schema_rename_index
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
6.07
 schema_create_unique_index
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
 schema_index_drop
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
 schema_create_primary_key
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 schema_get_index_key_data
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 recreate_index
57.14% covered (warning)
57.14%
12 / 21
0.00% covered (danger)
0.00%
0 / 1
6.97
 isSequenceAutoIncrementsFor
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 *
4 * This file is part of the phpBB Forum Software package.
5 *
6 * @copyright (c) phpBB Limited <https://www.phpbb.com>
7 * @license GNU General Public License, version 2 (GPL-2.0)
8 *
9 * For full copyright and license information, please see
10 * the docs/CREDITS.txt file.
11 *
12 */
13
14namespace phpbb\db\tools;
15
16use Doctrine\DBAL\Connection;
17use Doctrine\DBAL\Exception;
18use Doctrine\DBAL\Schema\AbstractAsset;
19use Doctrine\DBAL\Schema\AbstractSchemaManager;
20use Doctrine\DBAL\Schema\Index;
21use Doctrine\DBAL\Schema\Schema;
22use Doctrine\DBAL\Schema\SchemaException;
23use Doctrine\DBAL\Schema\Sequence;
24use Doctrine\DBAL\Schema\Table;
25use Doctrine\DBAL\Types\Type;
26use phpbb\db\doctrine\comparator;
27use phpbb\db\doctrine\table_helper;
28
29/**
30 * BC layer for database tools.
31 *
32 * In general, it is recommended to use Doctrine directly instead of this class as this
33 * implementation is only a BC layer.
34 */
35class doctrine implements tools_interface
36{
37    /**
38     * @var AbstractSchemaManager
39     */
40    private $schema_manager;
41
42    /**
43     * @var Connection
44     */
45    private $connection;
46
47    /**
48     * @var bool
49     */
50    private $return_statements;
51
52    /**
53     * @var string
54     */
55    private $table_prefix;
56
57    /**
58     * Database tools constructors.
59     *
60     * @param Connection $connection
61     * @param bool       $return_statements
62     */
63    public function __construct(Connection $connection, bool $return_statements = false)
64    {
65        $this->return_statements = $return_statements;
66        $this->connection = $connection;
67    }
68
69    /**
70     * {@inheritDoc}
71     */
72    public function get_connection(): Connection
73    {
74        return $this->connection;
75    }
76
77    /**
78     * @return AbstractSchemaManager
79     *
80     * @throws Exception
81     */
82    protected function get_schema_manager(): AbstractSchemaManager
83    {
84        if ($this->schema_manager == null)
85        {
86            $this->schema_manager = $this->connection->createSchemaManager();
87        }
88
89        return $this->schema_manager;
90    }
91
92    /**
93     * @return Schema
94     *
95     * @throws Exception
96     */
97    protected function get_schema(): Schema
98    {
99        return $this->get_schema_manager()->introspectSchema();
100    }
101
102    /**
103     * {@inheritDoc}
104     */
105    public function set_table_prefix($table_prefix): void
106    {
107        $this->table_prefix = $table_prefix;
108    }
109
110    /**
111     * {@inheritDoc}
112     */
113    public function sql_list_tables(): array
114    {
115        try
116        {
117            $tables = array_map('strtolower', $this->get_schema_manager()->listTableNames());
118            return array_combine($tables, $tables);
119        }
120        catch (Exception $e)
121        {
122            return [];
123        }
124    }
125
126    /**
127     * {@inheritDoc}
128     */
129    public function sql_table_exists(string $table_name): bool
130    {
131        try
132        {
133            return $this->get_schema_manager()->tablesExist([$table_name]);
134        }
135        catch (Exception $e)
136        {
137            return false;
138        }
139    }
140
141    /**
142     * {@inheritDoc}
143     */
144    public function sql_list_columns(string $table_name): array
145    {
146        try
147        {
148            return $this->get_asset_names($this->get_schema_manager()->listTableColumns($table_name));
149        }
150        catch (Exception $e)
151        {
152            return [];
153        }
154    }
155
156    /**
157     * {@inheritDoc}
158     */
159    public function sql_column_exists(string $table_name, string $column_name): bool
160    {
161        try
162        {
163            return $this->asset_exists($column_name, $this->get_schema_manager()->listTableColumns($table_name));
164        }
165        catch (Exception $e)
166        {
167            return false;
168        }
169    }
170
171    /**
172     * {@inheritDoc}
173     */
174    public function sql_list_index(string $table_name): array
175    {
176        return $this->get_asset_names($this->get_filtered_index_list($table_name, true));
177    }
178
179    /**
180     * {@inheritDoc}
181     */
182    public function sql_index_exists(string $table_name, string $index_name): bool
183    {
184        return $this->asset_exists($index_name, $this->get_filtered_index_list($table_name, true));
185    }
186
187    /**
188     * {@inheritDoc}
189     */
190    public function sql_unique_index_exists(string $table_name, string $index_name): bool
191    {
192        return $this->asset_exists($index_name, $this->get_filtered_index_list($table_name, false));
193    }
194
195    /**
196     * {@inheritDoc}
197     */
198    public function perform_schema_changes(array $schema_changes)
199    {
200        if (empty($schema_changes))
201        {
202            return true;
203        }
204
205        return $this->alter_schema(
206            function (Schema $schema) use ($schema_changes): void
207            {
208                $this->schema_perform_changes($schema, $schema_changes);
209            }
210        );
211    }
212
213    /**
214     * {@inheritDoc}
215     */
216    public function sql_create_table(string $table_name, array $table_data)
217    {
218        return $this->alter_schema(
219            function (Schema $schema) use ($table_name, $table_data): void
220            {
221                $this->schema_create_table($schema, $table_name, $table_data, true);
222            }
223        );
224    }
225
226    /**
227     * {@inheritDoc}
228     */
229    public function sql_table_drop(string $table_name)
230    {
231        return $this->alter_schema(
232            function (Schema $schema) use ($table_name): void
233            {
234                $this->schema_drop_table($schema, $table_name, true);
235            }
236        );
237    }
238
239    /**
240     * {@inheritDoc}
241     */
242    public function sql_column_add(string $table_name, string $column_name, array $column_data)
243    {
244        return $this->alter_schema(
245            function (Schema $schema) use ($table_name, $column_name, $column_data): void
246            {
247                $this->schema_column_add($schema, $table_name, $column_name, $column_data);
248            }
249        );
250    }
251
252    /**
253     * {@inheritDoc}
254     */
255    public function sql_column_change(string $table_name, string $column_name, array $column_data)
256    {
257        $column_indexes = $this->get_filtered_index_list($table_name, true);
258
259        $column_indexes = array_filter($column_indexes, function($index) use ($column_name) {
260            $index_columns = array_map('strtolower', $index->getUnquotedColumns());
261            return in_array($column_name, $index_columns, true);
262        });
263
264        if (count($column_indexes))
265        {
266            $ret = $this->alter_schema(
267                function (Schema $schema) use ($table_name, $column_name, $column_data, $column_indexes): void
268                {
269                    foreach ($column_indexes as $index)
270                    {
271                        $this->schema_index_drop($schema, $table_name, $index->getName());
272                    }
273                }
274            );
275
276            if ($ret !== true)
277            {
278                return $ret;
279            }
280        }
281
282        return $this->alter_schema(
283            function (Schema $schema) use ($table_name, $column_name, $column_data, $column_indexes): void
284            {
285                $this->schema_column_change($schema, $table_name, $column_name, $column_data);
286
287                if (count($column_indexes))
288                {
289                    foreach ($column_indexes as $index)
290                    {
291                        $this->schema_create_index($schema, $table_name, $index->getName(), $index->getColumns());
292                    }
293                }
294            }
295        );
296    }
297
298    /**
299     * {@inheritDoc}
300     */
301    public function sql_column_remove(string $table_name, string $column_name)
302    {
303        return $this->alter_schema(
304            function (Schema $schema) use ($table_name, $column_name): void
305            {
306                $this->schema_column_remove($schema, $table_name, $column_name);
307            }
308        );
309    }
310
311    /**
312     * {@inheritDoc}
313     */
314    public function sql_create_index(string $table_name, string $index_name, $column)
315    {
316        return $this->alter_schema(
317            function (Schema $schema) use ($table_name, $index_name, $column): void
318            {
319                $this->schema_create_index($schema, $table_name, $index_name, $column);
320            }
321        );
322    }
323
324    /**
325     * {@inheritDoc}
326     */
327    public function sql_rename_index(string $table_name, string $index_name_old, string $index_name_new)
328    {
329        return $this->alter_schema(
330            function (Schema $schema) use ($table_name, $index_name_old, $index_name_new): void
331            {
332                $this->schema_rename_index($schema, $table_name, $index_name_old, $index_name_new);
333            }
334        );
335    }
336
337    /**
338     * {@inheritDoc}
339     */
340    public function sql_index_drop(string $table_name, string $index_name)
341    {
342        return $this->alter_schema(
343            function (Schema $schema) use ($table_name, $index_name): void
344            {
345                $this->schema_index_drop($schema, $table_name, $index_name);
346            }
347        );
348    }
349
350    /**
351     * {@inheritDoc}
352     */
353    public function sql_create_unique_index(string $table_name, string $index_name, $column)
354    {
355        return $this->alter_schema(
356            function (Schema $schema) use ($table_name, $index_name, $column): void
357            {
358                $this->schema_create_unique_index($schema, $table_name, $index_name, $column);
359            }
360        );
361    }
362
363    /**
364     * {@inheritDoc}
365     */
366    public function sql_create_primary_key(string $table_name, $column)
367    {
368        return $this->alter_schema(
369            function (Schema $schema) use ($table_name, $column): void
370            {
371                $this->schema_create_primary_key($schema, $table_name, $column);
372            }
373        );
374    }
375
376    /**
377     * {@inheritDoc}
378     */
379    public function sql_truncate_table(string $table_name): void
380    {
381        try
382        {
383            $this->connection->executeQuery($this->connection->getDatabasePlatform()->getTruncateTableSQL($table_name));
384        }
385        catch (Exception $e)
386        {
387            return;
388        }
389    }
390
391    /**
392     * {@inheritDoc}
393     */
394    public static function add_prefix(string $name, string $prefix): string
395    {
396        return str_ends_with($prefix, '_') ? $prefix . $name : $prefix . '_' . $name;
397    }
398
399    /**
400     * {@inheritDoc}
401     */
402    public static function remove_prefix(string $name, string $prefix = ''): string
403    {
404        $prefix = str_ends_with($prefix, '_') ? $prefix : $prefix . '_';
405        return $prefix && str_starts_with($name, $prefix) ? substr($name, strlen($prefix)) : $name;
406    }
407
408    /**
409     * Returns an array of the table index names and relevant data in format
410     * [
411     *        [$index_name] = [
412     *            'columns'    => (array) $index_columns,
413     *            'flags'        => (array) $index_flags,
414     *            'options'    => (array) $index_options,
415     *            'is_primary'=> (bool) $isPrimary,
416     *            'is_unique'    => (bool) $isUnique,
417     *            'is_simple'    => (bool) $isSimple,
418     *        ]
419     *
420     * @param string $table_name
421     *
422     * @return array
423     */
424    public function sql_get_table_index_data(string $table_name): array
425    {
426        $schema = $this->get_schema();
427        $table = $schema->getTable($table_name);
428        $indexes = [];
429        foreach ($table->getIndexes() as $index)
430        {
431            $indexes[$index->getName()] = [
432                'columns'    => array_map('strtolower', $index->getUnquotedColumns()),
433                'flags'        => $index->getFlags(),
434                'options'    => $index->getOptions(),
435                'is_primary'=> $index->isPrimary(),
436                'is_unique'    => $index->isUnique(),
437                'is_simple'    => $index->isSimpleIndex(),
438            ];
439        }
440
441        return $indexes;
442    }
443
444    /**
445     * Returns an array of indices for either unique and primary keys, or simple indices.
446     *
447     * @param string $table_name    The name of the table.
448     * @param bool   $is_non_unique Whether to return simple indices or primary and unique ones.
449     *
450     * @return Index[] The filtered index array.
451     */
452    protected function get_filtered_index_list(string $table_name, bool $is_non_unique): array
453    {
454        try
455        {
456            $indices = $this->get_schema_manager()->listTableIndexes($table_name);
457        }
458        catch (Exception $e)
459        {
460            return [];
461        }
462
463        if ($is_non_unique)
464        {
465            return array_filter($indices, function (Index $index)
466            {
467                return $index->isSimpleIndex();
468            });
469        }
470
471        return array_filter($indices, function (Index $index)
472        {
473            return !$index->isSimpleIndex();
474        });
475    }
476
477    /**
478     * Returns an array of lowercase asset names.
479     *
480     * @param array $assets Array of assets.
481     *
482     * @return array An array of lowercase asset names.
483     */
484    protected function get_asset_names(array $assets): array
485    {
486        return array_map(
487            function (AbstractAsset $asset)
488            {
489                return strtolower($asset->getName());
490            },
491            $assets
492        );
493    }
494
495    /**
496     * Returns whether an asset name exists in a list of assets (case insensitive).
497     *
498     * @param string $needle The asset name to search for.
499     * @param array  $assets The array of assets.
500     *
501     * @return bool Whether the asset name exists in a list of assets.
502     */
503    protected function asset_exists(string $needle, array $assets): bool
504    {
505        return in_array(strtolower($needle), $this->get_asset_names($assets), true);
506    }
507
508    /**
509     * Alter the current database representation using a callback and execute the changes.
510     * Returns false in case of error.
511     *
512     * @param callable $callback Callback taking the schema as parameters and returning it altered (or null in case of error)
513     *
514     * @return bool|string[]
515     */
516    protected function alter_schema(callable $callback)
517    {
518        $current_schema = $this->get_schema();
519        $new_schema = clone $current_schema;
520        call_user_func($callback, $new_schema);
521
522        $comparator = new comparator();
523        $schemaDiff = $comparator->compareSchemas($current_schema, $new_schema);
524        $queries = $schemaDiff->toSql($this->connection->getDatabasePlatform());
525
526        if ($this->return_statements)
527        {
528            return $queries;
529        }
530
531        foreach ($queries as $query)
532        {
533            // executeQuery() must be used here because $query might return a result set, for instance REPAIR does
534            $this->connection->executeQuery($query);
535        }
536        return true;
537    }
538
539    /**
540     * Alter table.
541     *
542     * @param string   $table_name Table name.
543     * @param callable $callback   Callback function to modify the table.
544     *
545     * @throws SchemaException
546     */
547    protected function alter_table(Schema $schema, string $table_name, callable $callback): void
548    {
549        $table = $schema->getTable($table_name);
550        call_user_func($callback, $table);
551    }
552
553    /**
554     * Perform schema changes
555     *
556     * @param Schema $schema
557     * @param array $schema_changes
558     */
559    protected function schema_perform_changes(Schema $schema, array $schema_changes): void
560    {
561        $mapping = [
562            'drop_tables' => [
563                'method' => 'schema_drop_table',
564                'use_key' => false,
565            ],
566            'add_tables' => [
567                'method' => 'schema_create_table',
568                'use_key' => true,
569            ],
570            'change_columns' => [
571                'method' => 'schema_column_change_add',
572                'use_key' => true,
573                'per_table' => true,
574            ],
575            'add_columns' => [
576                'method' => 'schema_column_add',
577                'use_key' => true,
578                'per_table' => true,
579            ],
580            'drop_columns' => [
581                'method' => 'schema_column_remove',
582                'use_key' => false,
583                'per_table' => true,
584            ],
585            'drop_keys' => [
586                'method' => 'schema_index_drop',
587                'use_key' => false,
588                'per_table' => true,
589            ],
590            'add_primary_keys' => [
591                'method' => 'schema_create_primary_key',
592                'use_key' => true,
593            ],
594            'add_unique_index' => [
595                'method' => 'schema_create_unique_index',
596                'use_key' => true,
597                'per_table' => true,
598            ],
599            'add_index' => [
600                'method' => 'schema_create_index',
601                'use_key' => true,
602                'per_table' => true,
603            ],
604            'rename_index' => [
605                'method' => 'schema_rename_index',
606                'use_key' => true,
607                'per_table' => true,
608            ],
609        ];
610
611        foreach ($mapping as $action => $params)
612        {
613            if (array_key_exists($action, $schema_changes))
614            {
615                foreach ($schema_changes[$action] as $table_name => $table_data)
616                {
617                    if (array_key_exists('per_table', $params) && $params['per_table'])
618                    {
619                        foreach ($table_data as $key => $data)
620                        {
621                            if ($params['use_key'] == false)
622                            {
623                                $this->{$params['method']}($schema, $table_name, $data, true);
624                            }
625                            else
626                            {
627                                $this->{$params['method']}($schema, $table_name, $key, $data, true);
628                            }
629                        }
630                    }
631                    else
632                    {
633                        if ($params['use_key'] == false)
634                        {
635                            $this->{$params['method']}($schema, $table_data, true);
636                        }
637                        else
638                        {
639                            $this->{$params['method']}($schema, $table_name, $table_data, true);
640                        }
641                    }
642                }
643            }
644        }
645    }
646
647    /**
648     * Update the schema representation with a new table.
649     * Returns null in case of errors
650     *
651     * @param Schema $schema
652     * @param string $table_name
653     * @param array  $table_data
654     * @param bool   $safe_check
655     *
656     * @throws SchemaException
657     */
658    protected function schema_create_table(Schema $schema, string $table_name, array $table_data, bool $safe_check = false): void
659    {
660        if ($safe_check && $this->sql_table_exists($table_name))
661        {
662            return;
663        }
664
665        $table = $schema->createTable($table_name);
666        $short_table_name = table_helper::generate_shortname(self::remove_prefix($table_name, $this->table_prefix));
667        $dbms_name = $this->connection->getDatabasePlatform()->getName();
668
669        foreach ($table_data['COLUMNS'] as $column_name => $column_data)
670        {
671            list($type, $options) = table_helper::convert_column_data(
672                $column_data,
673                $dbms_name
674            );
675            $table->addColumn($column_name, $type, $options);
676        }
677
678        if (array_key_exists('PRIMARY_KEY', $table_data))
679        {
680            $table_data['PRIMARY_KEY'] = (!is_array($table_data['PRIMARY_KEY']))
681                ? [$table_data['PRIMARY_KEY']]
682                : $table_data['PRIMARY_KEY'];
683
684            $table->setPrimaryKey($table_data['PRIMARY_KEY']);
685        }
686
687        if (array_key_exists('KEYS', $table_data))
688        {
689            foreach ($table_data['KEYS'] as $key_name => $key_data)
690            {
691                $columns = (is_array($key_data[1])) ? $key_data[1] : [$key_data[1]];
692                $key_name = !str_starts_with($key_name, $short_table_name) ? self::add_prefix($key_name, $short_table_name) : $key_name;
693
694                $options = [];
695                $this->schema_get_index_key_data($columns, $options);
696
697                if ($key_data[0] === 'UNIQUE')
698                {
699                    $table->addUniqueIndex($columns, $key_name, $options);
700                }
701                else
702                {
703                    $table->addIndex($columns, $key_name, [], $options);
704                }
705            }
706        }
707
708        switch ($dbms_name)
709        {
710            case 'mysql':
711                $table->addOption('collate', 'utf8_bin');
712            break;
713        }
714    }
715
716    /**
717     * Removes a table
718     *
719     * @param Schema $schema
720     * @param string $table_name
721     * @param bool   $safe_check
722     *
723     * @throws SchemaException
724     */
725    protected function schema_drop_table(Schema $schema, string $table_name, bool $safe_check = false): void
726    {
727        if ($safe_check && !$schema->hasTable($table_name))
728        {
729            return;
730        }
731
732        $schema->dropTable($table_name);
733    }
734
735    /**
736     * Adds column to a table
737     *
738     * @param Schema $schema
739     * @param string $table_name
740     * @param string $column_name
741     * @param array  $column_data
742     * @param bool   $safe_check
743     *
744     * @throws SchemaException
745     */
746    protected function schema_column_add(Schema $schema, string $table_name, string $column_name, array $column_data, bool $safe_check = false): void
747    {
748        $this->alter_table(
749            $schema,
750            $table_name,
751            function (Table $table) use ($column_name, $column_data, $safe_check)
752            {
753                if ($safe_check && $table->hasColumn($column_name))
754                {
755                    return false;
756                }
757
758                $dbms_name = $this->connection->getDatabasePlatform()->getName();
759
760                list($type, $options) = table_helper::convert_column_data($column_data, $dbms_name);
761                $table->addColumn($column_name, $type, $options);
762                return $table;
763            }
764        );
765    }
766
767    /**
768     * Alters column properties
769     *
770     * @param Schema $schema
771     * @param string $table_name
772     * @param string $column_name
773     * @param array  $column_data
774     * @param bool   $safe_check
775     *
776     * @throws SchemaException
777     */
778    protected function schema_column_change(Schema $schema, string $table_name, string $column_name, array $column_data, bool $safe_check = false): void
779    {
780        $this->alter_table(
781            $schema,
782            $table_name,
783            function (Table $table) use ($column_name, $column_data, $safe_check): void
784            {
785                if ($safe_check && !$table->hasColumn($column_name))
786                {
787                    return;
788                }
789
790                $dbms_name = $this->connection->getDatabasePlatform()->getName();
791
792                list($type, $options) = table_helper::convert_column_data($column_data, $dbms_name);
793                $options['type'] = Type::getType($type);
794                $table->changeColumn($column_name, $options);
795            }
796        );
797    }
798
799    /**
800     * Alters column properties or adds a column
801     *
802     * @param Schema $schema
803     * @param string $table_name
804     * @param string $column_name
805     * @param array  $column_data
806     * @param bool   $safe_check
807     *
808     * @throws SchemaException
809     */
810    protected function schema_column_change_add(Schema $schema, string $table_name, string $column_name, array $column_data, bool $safe_check = false): void
811    {
812        $table = $schema->getTable($table_name);
813        if ($table->hasColumn($column_name))
814        {
815            $this->schema_column_change($schema, $table_name, $column_name, $column_data, $safe_check);
816        }
817        else
818        {
819            $this->schema_column_add($schema, $table_name, $column_name, $column_data, $safe_check);
820        }
821    }
822
823    /**
824     * Removes a column in a table
825     *
826     * @param Schema $schema
827     * @param string $table_name
828     * @param string $column_name
829     * @param bool   $safe_check
830     *
831     * @throws SchemaException
832     */
833    protected function schema_column_remove(Schema $schema, string $table_name, string $column_name, bool $safe_check = false): void
834    {
835        $this->alter_table(
836            $schema,
837            $table_name,
838            function (Table $table) use ($schema, $table_name, $column_name, $safe_check): void
839            {
840                if ($safe_check && !$table->hasColumn($column_name))
841                {
842                    return;
843                }
844
845                /*
846                 * As our sequences does not have the same name as these generated
847                 * by default by doctrine or the DBMS, we have to manage them ourselves.
848                 */
849                if ($table->getColumn($column_name)->getAutoincrement())
850                {
851                    foreach ($schema->getSequences() as $sequence)
852                    {
853                        if ($this->isSequenceAutoIncrementsFor($sequence, $table))
854                        {
855                            $schema->dropSequence($sequence->getName());
856                        }
857                    }
858                }
859
860                // Re-create / delete the indices using this column
861                foreach ($table->getIndexes() as $index)
862                {
863                    $index_columns = array_map('strtolower', $index->getUnquotedColumns());
864                    $key = array_search($column_name, $index_columns, true);
865                    if ($key !== false)
866                    {
867                        unset($index_columns[$key]);
868                        $this->recreate_index($table, $index, $index_columns);
869                    }
870                }
871
872                $table->dropColumn($column_name);
873            }
874        );
875    }
876
877    /**
878     * Creates non-unique index for a table
879     *
880     * @param Schema $schema
881     * @param string $table_name
882     * @param string $index_name
883     * @param string|array $column
884     * @param bool   $safe_check
885     *
886     * @throws SchemaException
887     */
888    protected function schema_create_index(Schema $schema, string $table_name, string $index_name, $column, bool $safe_check = false): void
889    {
890        $columns = (is_array($column)) ? $column : [$column];
891        $table = $schema->getTable($table_name);
892        $short_table_name = table_helper::generate_shortname(self::remove_prefix($table_name, $this->table_prefix));
893        $index_name = !str_starts_with($index_name, $short_table_name) ? self::add_prefix($index_name, $short_table_name) : $index_name;
894
895        if ($safe_check && $table->hasIndex($index_name))
896        {
897            return;
898        }
899
900        $options = [];
901        $this->schema_get_index_key_data($columns, $options);
902
903        $table->addIndex($columns, $index_name, [], $options);
904    }
905
906    /**
907     * Renames table index
908     *
909     * @param Schema $schema
910     * @param string $table_name
911     * @param string $index_name_old
912     * @param string $index_name_new
913     * @param bool   $safe_check
914     *
915     * @throws SchemaException
916     */
917    protected function schema_rename_index(Schema $schema, string $table_name, string $index_name_old, string $index_name_new, bool $safe_check = false): void
918    {
919        $table = $schema->getTable($table_name);
920        $short_table_name = table_helper::generate_shortname(self::remove_prefix($table_name, $this->table_prefix));
921
922        if (!$table->hasIndex($index_name_old))
923        {
924            $index_name_old = !str_starts_with($index_name_old, $short_table_name) ? self::add_prefix($index_name_old, $short_table_name) : self::remove_prefix($index_name_old, $short_table_name);
925        }
926        $index_name_new = !str_starts_with($index_name_new, $short_table_name) ? self::add_prefix($index_name_new, $short_table_name) : $index_name_new;
927
928        if ($safe_check && !$table->hasIndex($index_name_old))
929        {
930            return;
931        }
932
933        $table->renameIndex($index_name_old, $index_name_new);
934    }
935
936    /**
937     * Creates unique (non-primary) index for a table
938     *
939     * @param Schema $schema
940     * @param string $table_name
941     * @param string $index_name
942     * @param string|array $column
943     * @param bool   $safe_check
944     *
945     * @throws SchemaException
946     */
947    protected function schema_create_unique_index(Schema $schema, string $table_name, string $index_name, $column, bool $safe_check = false): void
948    {
949        $columns = (is_array($column)) ? $column : [$column];
950        $table = $schema->getTable($table_name);
951        $short_table_name = table_helper::generate_shortname(self::remove_prefix($table_name, $this->table_prefix));
952        $index_name = !str_starts_with($index_name, $short_table_name) ? self::add_prefix($index_name, $short_table_name) : $index_name;
953
954        if ($safe_check && $table->hasIndex($index_name))
955        {
956            return;
957        }
958
959        $options = [];
960        $this->schema_get_index_key_data($columns, $options);
961
962        $table->addUniqueIndex($columns, $index_name, $options);
963    }
964
965    /**
966     * Removes table index
967     *
968     * @param Schema $schema
969     * @param string $table_name
970     * @param string $index_name
971     * @param bool   $safe_check
972     *
973     * @throws SchemaException
974     */
975    protected function schema_index_drop(Schema $schema, string $table_name, string $index_name, bool $safe_check = false): void
976    {
977        $table = $schema->getTable($table_name);
978        $short_table_name = table_helper::generate_shortname(self::remove_prefix($table_name, $this->table_prefix));
979
980        if (!$table->hasIndex($index_name))
981        {
982            $index_name = !str_starts_with($index_name, $short_table_name) ? self::add_prefix($index_name, $short_table_name) : self::remove_prefix($index_name, $short_table_name);
983        }
984
985        if ($safe_check && !$table->hasIndex($index_name))
986        {
987            return;
988        }
989
990        $table->dropIndex($index_name);
991    }
992
993    /**
994     * Creates primary key for a table
995     *
996     * @param Schema $schema
997     * @param string $table_name
998     * @param array|string $column_name
999     * @param bool   $safe_check
1000     *
1001     * @throws SchemaException
1002     */
1003    protected function schema_create_primary_key(Schema $schema, string $table_name, array|string $column_name, bool $safe_check = false): void
1004    {
1005        $columns = (is_array($column_name)) ? $column_name : [$column_name];
1006        $table = $schema->getTable($table_name);
1007        $table->dropPrimaryKey();
1008        $table->setPrimaryKey($columns);
1009    }
1010
1011    /**
1012     * Checks if index data contains key length
1013     * and put it into $options['lengths'] array.
1014     * Handles key length in formats of 'keyname:123' or 'keyname(123)'
1015     *
1016     * @param array  $columns
1017     * @param array  $options
1018     */
1019    protected function schema_get_index_key_data(array &$columns, array &$options): void
1020    {
1021        if (!empty($columns))
1022        {
1023            $columns = array_map(function (string $column) use (&$options)
1024            {
1025                if (preg_match('/^([a-zA-Z0-9_]+)(?:(?:\:|\()([0-9]{1,3})\)?)?$/', $column, $parts))
1026                {
1027                    $options['lengths'][] = $parts[2] ?? null;
1028                    return $parts[1];
1029                }
1030                return $column;
1031            }, $columns);
1032        }
1033    }
1034
1035    /**
1036     * Recreate an index of a table
1037     *
1038     * @param Table $table
1039     * @param Index $index
1040     * @param array  Columns to use in the new (recreated) index
1041     *
1042     * @throws SchemaException
1043     */
1044    protected function recreate_index(Table $table, Index $index, array $new_columns): void
1045    {
1046        if ($index->isPrimary())
1047        {
1048            $table->dropPrimaryKey();
1049        }
1050        else
1051        {
1052            $table->dropIndex($index->getName());
1053        }
1054
1055        if (count($new_columns) > 0)
1056        {
1057            if ($index->isPrimary())
1058            {
1059                $table->setPrimaryKey(
1060                    $new_columns,
1061                    $index->getName(),
1062                );
1063            }
1064            else if ($index->isUnique())
1065            {
1066                $table->addUniqueIndex(
1067                    $new_columns,
1068                    $index->getName(),
1069                    $index->getOptions(),
1070                );
1071            }
1072            else
1073            {
1074                $table->addIndex(
1075                    $new_columns,
1076                    $index->getName(),
1077                    $index->getFlags(),
1078                    $index->getOptions(),
1079                );
1080            }
1081        }
1082    }
1083
1084    /**
1085     * @param Sequence $sequence
1086     * @param Table    $table
1087     *
1088     * @return bool
1089     * @throws SchemaException
1090     *
1091     * @see Sequence
1092     */
1093    private function isSequenceAutoIncrementsFor(Sequence $sequence, Table $table): bool
1094    {
1095        $primaryKey = $table->getPrimaryKey();
1096
1097        if ($primaryKey === null)
1098        {
1099            return false;
1100        }
1101
1102        $pkColumns = $primaryKey->getColumns();
1103
1104        if (count($pkColumns) !== 1)
1105        {
1106            return false;
1107        }
1108
1109        $column = $table->getColumn($pkColumns[0]);
1110
1111        if (! $column->getAutoincrement())
1112        {
1113            return false;
1114        }
1115
1116        $sequenceName      = $sequence->getShortestName($table->getNamespaceName());
1117        $tableName         = $table->getShortestName($table->getNamespaceName());
1118        $tableSequenceName = sprintf('%s_seq', $tableName);
1119
1120        return $tableSequenceName === $sequenceName;
1121    }
1122}