Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.21% covered (warning)
81.21%
281 / 346
38.46% covered (danger)
38.46%
15 / 39
CRAP
0.00% covered (danger)
0.00%
0 / 1
doctrine
81.21% covered (warning)
81.21%
281 / 346
38.46% covered (danger)
38.46%
15 / 39
185.33
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_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
 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
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
4
 sql_create_index
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 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
 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
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
4.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%
55 / 55
100.00% covered (success)
100.00%
1 / 1
9
 schema_create_table
90.32% covered (success)
90.32%
28 / 31
0.00% covered (danger)
0.00%
0 / 1
12.13
 schema_drop_table
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 schema_column_add
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 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
85.00% covered (warning)
85.00%
17 / 20
0.00% covered (danger)
0.00%
0 / 1
8.22
 schema_create_index
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 schema_create_unique_index
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 schema_index_drop
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 schema_create_primary_key
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 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     * Database tools constructors.
54     *
55     * @param Connection $connection
56     * @param bool       $return_statements
57     */
58    public function __construct(Connection $connection, bool $return_statements = false)
59    {
60        $this->return_statements = $return_statements;
61        $this->connection = $connection;
62    }
63
64    /**
65     * @return AbstractSchemaManager
66     *
67     * @throws Exception
68     */
69    protected function get_schema_manager(): AbstractSchemaManager
70    {
71        if ($this->schema_manager == null)
72        {
73            $this->schema_manager = $this->connection->createSchemaManager();
74        }
75
76        return $this->schema_manager;
77    }
78
79    /**
80     * @return Schema
81     *
82     * @throws Exception
83     */
84    protected function get_schema(): Schema
85    {
86        return $this->get_schema_manager()->createSchema();
87    }
88
89    /**
90     * {@inheritDoc}
91     */
92    public function sql_list_tables(): array
93    {
94        try
95        {
96            $tables = array_map('strtolower', $this->get_schema_manager()->listTableNames());
97            return array_combine($tables, $tables);
98        }
99        catch (Exception $e)
100        {
101            return [];
102        }
103    }
104
105    /**
106     * {@inheritDoc}
107     */
108    public function sql_table_exists(string $table_name): bool
109    {
110        try
111        {
112            return $this->get_schema_manager()->tablesExist([$table_name]);
113        }
114        catch (Exception $e)
115        {
116            return false;
117        }
118    }
119
120    /**
121     * {@inheritDoc}
122     */
123    public function sql_list_columns(string $table_name): array
124    {
125        try
126        {
127            return $this->get_asset_names($this->get_schema_manager()->listTableColumns($table_name));
128        }
129        catch (Exception $e)
130        {
131            return [];
132        }
133    }
134
135    /**
136     * {@inheritDoc}
137     */
138    public function sql_column_exists(string $table_name, string $column_name): bool
139    {
140        try
141        {
142            return $this->asset_exists($column_name, $this->get_schema_manager()->listTableColumns($table_name));
143        }
144        catch (Exception $e)
145        {
146            return false;
147        }
148    }
149
150    /**
151     * {@inheritDoc}
152     */
153    public function sql_list_index(string $table_name): array
154    {
155        return $this->get_asset_names($this->get_filtered_index_list($table_name, true));
156    }
157
158    /**
159     * {@inheritDoc}
160     */
161    public function sql_index_exists(string $table_name, string $index_name): bool
162    {
163        return $this->asset_exists($index_name, $this->get_filtered_index_list($table_name, true));
164    }
165
166    /**
167     * {@inheritDoc}
168     */
169    public function sql_unique_index_exists(string $table_name, string $index_name): bool
170    {
171        return $this->asset_exists($index_name, $this->get_filtered_index_list($table_name, false));
172    }
173
174    /**
175     * {@inheritDoc}
176     */
177    public function perform_schema_changes(array $schema_changes)
178    {
179        if (empty($schema_changes))
180        {
181            return true;
182        }
183
184        return $this->alter_schema(
185            function (Schema $schema) use ($schema_changes): void
186            {
187                $this->schema_perform_changes($schema, $schema_changes);
188            }
189        );
190    }
191
192    /**
193     * {@inheritDoc}
194     */
195    public function sql_create_table(string $table_name, array $table_data)
196    {
197        return $this->alter_schema(
198            function (Schema $schema) use ($table_name, $table_data): void
199            {
200                $this->schema_create_table($schema, $table_name, $table_data, true);
201            }
202        );
203    }
204
205    /**
206     * {@inheritDoc}
207     */
208    public function sql_table_drop(string $table_name)
209    {
210        return $this->alter_schema(
211            function (Schema $schema) use ($table_name): void
212            {
213                $this->schema_drop_table($schema, $table_name, true);
214            }
215        );
216    }
217
218    /**
219     * {@inheritDoc}
220     */
221    public function sql_column_add(string $table_name, string $column_name, array $column_data)
222    {
223        return $this->alter_schema(
224            function (Schema $schema) use ($table_name, $column_name, $column_data): void
225            {
226                $this->schema_column_add($schema, $table_name, $column_name, $column_data);
227            }
228        );
229    }
230
231    /**
232     * {@inheritDoc}
233     */
234    public function sql_column_change(string $table_name, string $column_name, array $column_data)
235    {
236        $column_indexes = $this->get_filtered_index_list($table_name, true);
237
238        $column_indexes = array_filter($column_indexes, function($index) use ($column_name) {
239            $index_columns = array_map('strtolower', $index->getUnquotedColumns());
240            return in_array($column_name, $index_columns, true);
241        });
242
243        if (count($column_indexes))
244        {
245            $ret = $this->alter_schema(
246                function (Schema $schema) use ($table_name, $column_name, $column_data, $column_indexes): void
247                {
248                    foreach ($column_indexes as $index)
249                    {
250                        $this->schema_index_drop($schema, $table_name, $index->getName());
251                    }
252                }
253            );
254
255            if ($ret !== true)
256            {
257                return $ret;
258            }
259        }
260
261        return $this->alter_schema(
262            function (Schema $schema) use ($table_name, $column_name, $column_data, $column_indexes): void
263            {
264                $this->schema_column_change($schema, $table_name, $column_name, $column_data);
265
266                if (count($column_indexes))
267                {
268                    foreach ($column_indexes as $index)
269                    {
270                        $this->schema_create_index($schema, $table_name, $index->getName(), $index->getColumns());
271                    }
272                }
273            }
274        );
275    }
276
277    /**
278     * {@inheritDoc}
279     */
280    public function sql_column_remove(string $table_name, string $column_name)
281    {
282        // Check if this column is part of a primary key. If yes, remove the primary key.
283        $primary_key_indexes = $this->get_filtered_index_list($table_name, false);
284
285        $primary_key_indexes = array_filter($primary_key_indexes, function($index) use ($column_name) {
286            $index_columns = array_map('strtolower', $index->getUnquotedColumns());
287            return in_array($column_name, $index_columns, true) && $index->isPrimary();
288        });
289
290        if (count($primary_key_indexes))
291        {
292            $ret = $this->alter_schema(
293                function (Schema $schema) use ($table_name, $column_name): void
294                {
295                    $table = $schema->getTable($table_name);
296                    $table->dropPrimaryKey();
297                }
298            );
299
300            if ($ret !== true)
301            {
302                return $ret;
303            }
304        }
305
306        return $this->alter_schema(
307            function (Schema $schema) use ($table_name, $column_name): void
308            {
309                $this->schema_column_remove($schema, $table_name, $column_name);
310            }
311        );
312    }
313
314    /**
315     * {@inheritDoc}
316     */
317    public function sql_create_index(string $table_name, string $index_name, $column)
318    {
319        return $this->alter_schema(
320            function (Schema $schema) use ($table_name, $index_name, $column): void
321            {
322                $this->schema_create_index($schema, $table_name, $index_name, $column);
323            }
324        );
325    }
326
327    /**
328     * {@inheritDoc}
329     */
330    public function sql_index_drop(string $table_name, string $index_name)
331    {
332        return $this->alter_schema(
333            function (Schema $schema) use ($table_name, $index_name): void
334            {
335                $this->schema_index_drop($schema, $table_name, $index_name);
336            }
337        );
338    }
339
340    /**
341     * {@inheritDoc}
342     */
343    public function sql_create_unique_index(string $table_name, string $index_name, $column)
344    {
345        return $this->alter_schema(
346            function (Schema $schema) use ($table_name, $index_name, $column): void
347            {
348                $this->schema_create_unique_index($schema, $table_name, $index_name, $column);
349            }
350        );
351    }
352
353    /**
354     * {@inheritDoc}
355     */
356    public function sql_create_primary_key(string $table_name, $column)
357    {
358        return $this->alter_schema(
359            function (Schema $schema) use ($table_name, $column): void
360            {
361                $this->schema_create_primary_key($schema, $column, $table_name);
362            }
363        );
364    }
365
366    /**
367     * {@inheritDoc}
368     */
369    public function sql_truncate_table(string $table_name): void
370    {
371        try
372        {
373            $this->connection->executeQuery($this->get_schema_manager()->getDatabasePlatform()->getTruncateTableSQL($table_name));
374        }
375        catch (Exception $e)
376        {
377            return;
378        }
379    }
380
381    /**
382     * Returns an array of indices for either unique and primary keys, or simple indices.
383     *
384     * @param string $table_name    The name of the table.
385     * @param bool   $is_non_unique Whether to return simple indices or primary and unique ones.
386     *
387     * @return Index[] The filtered index array.
388     */
389    protected function get_filtered_index_list(string $table_name, bool $is_non_unique): array
390    {
391        try
392        {
393            $indices = $this->get_schema_manager()->listTableIndexes($table_name);
394        }
395        catch (Exception $e)
396        {
397            return [];
398        }
399
400        if ($is_non_unique)
401        {
402            return array_filter($indices, function (Index $index)
403            {
404                return $index->isSimpleIndex();
405            });
406        }
407
408        return array_filter($indices, function (Index $index)
409        {
410            return !$index->isSimpleIndex();
411        });
412    }
413
414    /**
415     * Returns an array of lowercase asset names.
416     *
417     * @param array $assets Array of assets.
418     *
419     * @return array An array of lowercase asset names.
420     */
421    protected function get_asset_names(array $assets): array
422    {
423        return array_map(
424            function (AbstractAsset $asset)
425            {
426                return strtolower($asset->getName());
427            },
428            $assets
429        );
430    }
431
432    /**
433     * Returns whether an asset name exists in a list of assets (case insensitive).
434     *
435     * @param string $needle The asset name to search for.
436     * @param array  $assets The array of assets.
437     *
438     * @return bool Whether the asset name exists in a list of assets.
439     */
440    protected function asset_exists(string $needle, array $assets): bool
441    {
442        return in_array(strtolower($needle), $this->get_asset_names($assets), true);
443    }
444
445    /**
446     * Alter the current database representation using a callback and execute the changes.
447     * Returns false in case of error.
448     *
449     * @param callable $callback Callback taking the schema as parameters and returning it altered (or null in case of error)
450     *
451     * @return bool|string[]
452     */
453    protected function alter_schema(callable $callback)
454    {
455        try
456        {
457            $current_schema = $this->get_schema();
458            $new_schema = clone $current_schema;
459            call_user_func($callback, $new_schema);
460
461            $comparator = new comparator();
462            $schemaDiff = $comparator->compareSchemas($current_schema, $new_schema);
463            $queries = $schemaDiff->toSql($this->get_schema_manager()->getDatabasePlatform());
464
465            if ($this->return_statements)
466            {
467                return $queries;
468            }
469
470            foreach ($queries as $query)
471            {
472                // executeQuery() must be used here because $query might return a result set, for instance REPAIR does
473                $this->connection->executeQuery($query);
474            }
475
476            return true;
477        }
478        catch (Exception $e)
479        {
480            // @todo: check if it makes sense to properly handle the exception
481            return false;
482        }
483    }
484
485    /**
486     * Alter table.
487     *
488     * @param string   $table_name Table name.
489     * @param callable $callback   Callback function to modify the table.
490     *
491     * @throws SchemaException
492     */
493    protected function alter_table(Schema $schema, string $table_name, callable $callback): void
494    {
495        $table = $schema->getTable($table_name);
496        call_user_func($callback, $table);
497    }
498
499    /**
500     * Perform schema changes
501     *
502     * @param Schema $schema
503     * @param array $schema_changes
504     */
505    protected function schema_perform_changes(Schema $schema, array $schema_changes): void
506    {
507        $mapping = [
508            'drop_tables' => [
509                'method' => 'schema_drop_table',
510                'use_key' => false,
511            ],
512            'add_tables' => [
513                'method' => 'schema_create_table',
514                'use_key' => true,
515            ],
516            'change_columns' => [
517                'method' => 'schema_column_change_add',
518                'use_key' => true,
519                'per_table' => true,
520            ],
521            'add_columns' => [
522                'method' => 'schema_column_add',
523                'use_key' => true,
524                'per_table' => true,
525            ],
526            'drop_columns' => [
527                'method' => 'schema_column_remove',
528                'use_key' => false,
529                'per_table' => true,
530            ],
531            'drop_keys' => [
532                'method' => 'schema_index_drop',
533                'use_key' => false,
534                'per_table' => true,
535            ],
536            'add_primary_keys' => [
537                'method' => 'schema_create_primary_key',
538                'use_key' => true,
539            ],
540            'add_unique_index' => [
541                'method' => 'schema_create_unique_index',
542                'use_key' => true,
543                'per_table' => true,
544            ],
545            'add_index' => [
546                'method' => 'schema_create_index',
547                'use_key' => true,
548                'per_table' => true,
549            ],
550        ];
551
552        foreach ($mapping as $action => $params)
553        {
554            if (array_key_exists($action, $schema_changes))
555            {
556                foreach ($schema_changes[$action] as $table_name => $table_data)
557                {
558                    if (array_key_exists('per_table', $params) && $params['per_table'])
559                    {
560                        foreach ($table_data as $key => $data)
561                        {
562                            if ($params['use_key'] == false)
563                            {
564                                $this->{$params['method']}($schema, $table_name, $data, true);
565                            }
566                            else
567                            {
568                                $this->{$params['method']}($schema, $table_name, $key, $data, true);
569                            }
570                        }
571                    }
572                    else
573                    {
574                        if ($params['use_key'] == false)
575                        {
576                            $this->{$params['method']}($schema, $table_data, true);
577                        }
578                        else
579                        {
580                            $this->{$params['method']}($schema, $table_name, $table_data, true);
581                        }
582                    }
583                }
584            }
585        }
586    }
587
588    /**
589     * Update the schema representation with a new table.
590     * Returns null in case of errors
591     *
592     * @param Schema $schema
593     * @param string $table_name
594     * @param array  $table_data
595     * @param bool   $safe_check
596     *
597     * @throws SchemaException
598     */
599    protected function schema_create_table(Schema $schema, string $table_name, array $table_data, bool $safe_check = false): void
600    {
601        if ($safe_check && $this->sql_table_exists($table_name))
602        {
603            return;
604        }
605
606        $table = $schema->createTable($table_name);
607        $dbms_name = $this->get_schema_manager()->getDatabasePlatform()->getName();
608
609        foreach ($table_data['COLUMNS'] as $column_name => $column_data)
610        {
611            list($type, $options) = table_helper::convert_column_data(
612                $column_data,
613                $dbms_name
614            );
615            $table->addColumn($column_name, $type, $options);
616        }
617
618        if (array_key_exists('PRIMARY_KEY', $table_data))
619        {
620            $table_data['PRIMARY_KEY'] = (!is_array($table_data['PRIMARY_KEY']))
621                ? [$table_data['PRIMARY_KEY']]
622                : $table_data['PRIMARY_KEY'];
623
624            $table->setPrimaryKey($table_data['PRIMARY_KEY']);
625        }
626
627        if (array_key_exists('KEYS', $table_data))
628        {
629            foreach ($table_data['KEYS'] as $key_name => $key_data)
630            {
631                $columns = (is_array($key_data[1])) ? $key_data[1] : [$key_data[1]];
632
633                // Supports key columns defined with there length
634                $columns = array_map(function (string $column)
635                {
636                    if (strpos($column, ':') !== false)
637                    {
638                        $parts = explode(':', $column, 2);
639                        return $parts[0];
640                    }
641                    return $column;
642                }, $columns);
643
644                if ($key_data[0] === 'UNIQUE')
645                {
646                    $table->addUniqueIndex($columns, $key_name);
647                }
648                else
649                {
650                    $table->addIndex($columns, $key_name);
651                }
652            }
653        }
654
655        switch ($dbms_name)
656        {
657            case 'mysql':
658                $table->addOption('collate', 'utf8_bin');
659            break;
660        }
661    }
662
663    /**
664     * @param Schema $schema
665     * @param string $table_name
666     * @param bool   $safe_check
667     *
668     * @throws SchemaException
669     */
670    protected function schema_drop_table(Schema $schema, string $table_name, bool $safe_check = false): void
671    {
672        if ($safe_check && !$schema->hasTable($table_name))
673        {
674            return;
675        }
676
677        $schema->dropTable($table_name);
678    }
679
680    /**
681     * @param Schema $schema
682     * @param string $table_name
683     * @param string $column_name
684     * @param array  $column_data
685     * @param bool   $safe_check
686     *
687     * @throws SchemaException
688     */
689    protected function schema_column_add(Schema $schema, string $table_name, string $column_name, array $column_data, bool $safe_check = false): void
690    {
691        $this->alter_table(
692            $schema,
693            $table_name,
694            function (Table $table) use ($column_name, $column_data, $safe_check)
695            {
696                if ($safe_check && $table->hasColumn($column_name))
697                {
698                    return false;
699                }
700
701                $dbms_name = $this->get_schema_manager()->getDatabasePlatform()->getName();
702
703                list($type, $options) = table_helper::convert_column_data($column_data, $dbms_name);
704                $table->addColumn($column_name, $type, $options);
705                return $table;
706            }
707        );
708    }
709
710    /**
711     * @param Schema $schema
712     * @param string $table_name
713     * @param string $column_name
714     * @param array  $column_data
715     * @param bool   $safe_check
716     *
717     * @throws SchemaException
718     */
719    protected function schema_column_change(Schema $schema, string $table_name, string $column_name, array $column_data, bool $safe_check = false): void
720    {
721        $this->alter_table(
722            $schema,
723            $table_name,
724            function (Table $table) use ($column_name, $column_data, $safe_check): void
725            {
726                if ($safe_check && !$table->hasColumn($column_name))
727                {
728                    return;
729                }
730
731                $dbms_name = $this->get_schema_manager()->getDatabasePlatform()->getName();
732
733                list($type, $options) = table_helper::convert_column_data($column_data, $dbms_name);
734                $options['type'] = Type::getType($type);
735                $table->changeColumn($column_name, $options);
736            }
737        );
738    }
739
740    /**
741     * @param Schema $schema
742     * @param string $table_name
743     * @param string $column_name
744     * @param array  $column_data
745     * @param bool   $safe_check
746     *
747     * @throws SchemaException
748     */
749    protected function schema_column_change_add(Schema $schema, string $table_name, string $column_name, array $column_data, bool $safe_check = false): void
750    {
751        $table = $schema->getTable($table_name);
752        if ($table->hasColumn($column_name))
753        {
754            $this->schema_column_change($schema, $table_name, $column_name, $column_data, $safe_check);
755        }
756        else
757        {
758            $this->schema_column_add($schema, $table_name, $column_name, $column_data, $safe_check);
759        }
760    }
761
762    /**
763     * @param Schema $schema
764     * @param string $table_name
765     * @param string $column_name
766     * @param bool   $safe_check
767     *
768     * @throws SchemaException
769     */
770    protected function schema_column_remove(Schema $schema, string $table_name, string $column_name, bool $safe_check = false): void
771    {
772        $this->alter_table(
773            $schema,
774            $table_name,
775            function (Table $table) use ($schema, $table_name, $column_name, $safe_check): void
776            {
777                if ($safe_check && !$table->hasColumn($column_name))
778                {
779                    return;
780                }
781
782                /*
783                 * As our sequences does not have the same name as these generated
784                 * by default by doctrine or the DBMS, we have to manage them ourselves.
785                 */
786                if ($table->getColumn($column_name)->getAutoincrement())
787                {
788                    foreach ($schema->getSequences() as $sequence)
789                    {
790                        if ($this->isSequenceAutoIncrementsFor($sequence, $table))
791                        {
792                            $schema->dropSequence($sequence->getName());
793                        }
794                    }
795                }
796
797                // Re-create / delete the indices using this column
798                foreach ($table->getIndexes() as $index)
799                {
800                    $index_columns = array_map('strtolower', $index->getUnquotedColumns());
801                    $key = array_search($column_name, $index_columns, true);
802                    if ($key !== false)
803                    {
804                        unset($index_columns[$key]);
805                        $this->recreate_index($table, $index, $index_columns);
806                    }
807                }
808
809                $table->dropColumn($column_name);
810            }
811        );
812    }
813
814    /**
815     * @param Schema $schema
816     * @param string $table_name
817     * @param string $index_name
818     * @param string|array $column
819     * @param bool   $safe_check
820     *
821     * @throws SchemaException
822     */
823    protected function schema_create_index(Schema $schema, string $table_name, string $index_name, $column, bool $safe_check = false): void
824    {
825        $columns = (is_array($column)) ? $column : [$column];
826        $table = $schema->getTable($table_name);
827
828        if ($safe_check && $table->hasIndex($index_name))
829        {
830            return;
831        }
832
833        $table->addIndex($columns, $index_name);
834    }
835
836    /**
837     * @param Schema $schema
838     * @param string $table_name
839     * @param string $index_name
840     * @param string|array $column
841     * @param bool   $safe_check
842     *
843     * @throws SchemaException
844     */
845    protected function schema_create_unique_index(Schema $schema, string $table_name, string $index_name, $column, bool $safe_check = false): void
846    {
847        $columns = (is_array($column)) ? $column : [$column];
848        $table = $schema->getTable($table_name);
849
850        if ($safe_check && $table->hasIndex($index_name))
851        {
852            return;
853        }
854
855        $table->addUniqueIndex($columns, $index_name);
856    }
857
858    /**
859     * @param Schema $schema
860     * @param string $table_name
861     * @param string $index_name
862     * @param bool   $safe_check
863     *
864     * @throws SchemaException
865     */
866    protected function schema_index_drop(Schema $schema, string $table_name, string $index_name, bool $safe_check = false): void
867    {
868        $table = $schema->getTable($table_name);
869
870        if ($safe_check && !$table->hasIndex($index_name))
871        {
872            return;
873        }
874
875        $table->dropIndex($index_name);
876    }
877
878    /**
879     * @param        $column
880     * @param Schema $schema
881     * @param string $table_name
882     * @param bool   $safe_check
883     *
884     * @throws SchemaException
885     */
886    protected function schema_create_primary_key(Schema $schema, $column, string $table_name, bool $safe_check = false): void
887    {
888        $columns = (is_array($column)) ? $column : [$column];
889        $table = $schema->getTable($table_name);
890        $table->dropPrimaryKey();
891        $table->setPrimaryKey($columns);
892    }
893
894    /**
895     * Recreate an index of a table
896     *
897     * @param Table $table
898     * @param Index $index
899     * @param array  Columns to use in the new (recreated) index
900     *
901     * @throws SchemaException
902     */
903    protected function recreate_index(Table $table, Index $index, array $new_columns): void
904    {
905        if ($index->isPrimary())
906        {
907            $table->dropPrimaryKey();
908        }
909        else
910        {
911            $table->dropIndex($index->getName());
912        }
913
914        if (count($new_columns) > 0)
915        {
916            if ($index->isPrimary())
917            {
918                $table->setPrimaryKey(
919                    $new_columns,
920                    $index->getName(),
921                );
922            }
923            else if ($index->isUnique())
924            {
925                $table->addUniqueIndex(
926                    $new_columns,
927                    $index->getName(),
928                    $index->getOptions(),
929                );
930            }
931            else
932            {
933                $table->addIndex(
934                    $new_columns,
935                    $index->getName(),
936                    $index->getFlags(),
937                    $index->getOptions(),
938                );
939            }
940        }
941    }
942
943    /**
944     * @param Sequence $sequence
945     * @param Table    $table
946     *
947     * @return bool
948     * @throws SchemaException
949     *
950     * @see Sequence
951     */
952    private function isSequenceAutoIncrementsFor(Sequence $sequence, Table $table): bool
953    {
954        $primaryKey = $table->getPrimaryKey();
955
956        if ($primaryKey === null)
957        {
958            return false;
959        }
960
961        $pkColumns = $primaryKey->getColumns();
962
963        if (count($pkColumns) !== 1)
964        {
965            return false;
966        }
967
968        $column = $table->getColumn($pkColumns[0]);
969
970        if (! $column->getAutoincrement())
971        {
972            return false;
973        }
974
975        $sequenceName      = $sequence->getShortestName($table->getNamespaceName());
976        $tableName         = $table->getShortestName($table->getNamespaceName());
977        $tableSequenceName = sprintf('%s_seq', $tableName);
978
979        return $tableSequenceName === $sequenceName;
980    }
981}