Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
301 / 301
100.00% covered (success)
100.00%
62 / 62
CRAP
100.00% covered (success)
100.00%
1 / 1
AlterTable
100.00% covered (success)
100.00%
301 / 301
100.00% covered (success)
100.00%
62 / 62
123
100.00% covered (success)
100.00%
1 / 1
 online
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderOnline
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 ignore
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderIgnore
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 ifExists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderIfExists
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 table
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderTable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 wait
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderWait
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 noWait
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderNoWait
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 add
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 addIfNotExists
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 renderAdd
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 change
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 changeIfExists
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 renderChange
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 modify
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 modifyIfExists
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 renderModify
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 dropColumn
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 dropColumnIfExists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderDropColumns
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 dropPrimaryKey
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderDropPrimaryKey
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 dropKey
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 dropKeyIfExists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderDropKeys
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 dropForeignKey
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 dropForeignKeyIfExists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderDropForeignKeys
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 dropConstraint
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 dropConstraintIfExists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderDropConstraints
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 disableKeys
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderDisableKeys
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 enableKeys
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderEnableKeys
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 renameTo
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderRenameTo
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 orderBy
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 renderOrderBy
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 renameColumn
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderRenameColumns
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 renameKey
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderRenameKeys
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 convertToCharset
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 renderConvertToCharset
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 charset
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderCharset
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 collate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderCollate
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 lock
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderLock
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 force
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderForce
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 algorithm
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderAlgorithm
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 sql
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
2
 joinParts
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 run
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework Database Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\Database\Definition;
11
12use Framework\Database\Definition\Table\TableDefinition;
13use Framework\Database\Definition\Table\TableStatement;
14use InvalidArgumentException;
15use LogicException;
16
17/**
18 * Class AlterTable.
19 *
20 * @see https://mariadb.com/kb/en/alter-table/
21 *
22 * @package database
23 */
24class AlterTable extends TableStatement
25{
26    /**
27     * @var string
28     */
29    public const ALGO_COPY = 'COPY';
30    /**
31     * @var string
32     */
33    public const ALGO_DEFAULT = 'DEFAULT';
34    /**
35     * @var string
36     */
37    public const ALGO_INPLACE = 'INPLACE';
38    /**
39     * @var string
40     */
41    public const ALGO_INSTANT = 'INSTANT';
42    /**
43     * @var string
44     */
45    public const ALGO_NOCOPY = 'NOCOPY';
46    /**
47     * @var string
48     */
49    public const LOCK_DEFAULT = 'DEFAULT';
50    /**
51     * @var string
52     */
53    public const LOCK_EXCLUSIVE = 'EXCLUSIVE';
54    /**
55     * @var string
56     */
57    public const LOCK_NONE = 'NONE';
58    /**
59     * @var string
60     */
61    public const LOCK_SHARED = 'SHARED';
62
63    /**
64     * @return static
65     */
66    public function online() : static
67    {
68        $this->sql['online'] = true;
69        return $this;
70    }
71
72    protected function renderOnline() : ?string
73    {
74        if ( ! isset($this->sql['online'])) {
75            return null;
76        }
77        return ' ONLINE';
78    }
79
80    /**
81     * @return static
82     */
83    public function ignore() : static
84    {
85        $this->sql['ignore'] = true;
86        return $this;
87    }
88
89    protected function renderIgnore() : ?string
90    {
91        if ( ! isset($this->sql['ignore'])) {
92            return null;
93        }
94        return ' IGNORE';
95    }
96
97    public function ifExists() : static
98    {
99        $this->sql['if_exists'] = true;
100        return $this;
101    }
102
103    protected function renderIfExists() : ?string
104    {
105        if ( ! isset($this->sql['if_exists'])) {
106            return null;
107        }
108        return ' IF EXISTS';
109    }
110
111    /**
112     * @param string $tableName
113     *
114     * @return static
115     */
116    public function table(string $tableName) : static
117    {
118        $this->sql['table'] = $tableName;
119        return $this;
120    }
121
122    protected function renderTable() : string
123    {
124        if (isset($this->sql['table'])) {
125            return ' ' . $this->database->protectIdentifier($this->sql['table']);
126        }
127        throw new LogicException('TABLE name must be set');
128    }
129
130    /**
131     * @param int $seconds
132     *
133     * @return static
134     */
135    public function wait(int $seconds) : static
136    {
137        $this->sql['wait'] = $seconds;
138        return $this;
139    }
140
141    protected function renderWait() : ?string
142    {
143        if ( ! isset($this->sql['wait'])) {
144            return null;
145        }
146        if ($this->sql['wait'] < 0) {
147            throw new InvalidArgumentException(
148                "Invalid WAIT value: {$this->sql['wait']}"
149            );
150        }
151        return " WAIT {$this->sql['wait']}";
152    }
153
154    public function noWait() : static
155    {
156        $this->sql['no_wait'] = true;
157        return $this;
158    }
159
160    protected function renderNoWait() : ?string
161    {
162        if ( ! isset($this->sql['no_wait'])) {
163            return null;
164        }
165        if (isset($this->sql['wait'])) {
166            throw new LogicException('WAIT and NOWAIT can not be used together');
167        }
168        return ' NOWAIT';
169    }
170
171    /**
172     * @param callable $definition
173     * @param bool $ifNotExists
174     *
175     * @return static
176     */
177    public function add(callable $definition, bool $ifNotExists = false) : static
178    {
179        $this->sql['add'][] = [
180            'definition' => $definition,
181            'if_not_exists' => $ifNotExists,
182        ];
183        return $this;
184    }
185
186    /**
187     * @param callable $definition
188     *
189     * @return static
190     */
191    public function addIfNotExists(callable $definition) : static
192    {
193        $this->sql['add'][] = [
194            'definition' => $definition,
195            'if_not_exists' => true,
196        ];
197        return $this;
198    }
199
200    protected function renderAdd() : ?string
201    {
202        if ( ! isset($this->sql['add'])) {
203            return null;
204        }
205        $parts = [];
206        foreach ($this->sql['add'] as $add) {
207            $definition = new TableDefinition(
208                $this->database,
209                $add['if_not_exists'] ? 'IF NOT EXISTS' : null
210            );
211            $add['definition']($definition);
212            $part = $definition->sql('ADD');
213            if ($part) {
214                $parts[] = $part;
215            }
216        }
217        return $parts ? \implode(',' . \PHP_EOL, $parts) : null;
218    }
219
220    /**
221     * @param callable $definition
222     * @param bool $ifExists
223     *
224     * @return static
225     */
226    public function change(callable $definition, bool $ifExists = false) : static
227    {
228        $this->sql['change'][] = [
229            'definition' => $definition,
230            'if_exists' => $ifExists,
231        ];
232        return $this;
233    }
234
235    public function changeIfExists(callable $definition) : static
236    {
237        $this->sql['change'][] = [
238            'definition' => $definition,
239            'if_exists' => true,
240        ];
241        return $this;
242    }
243
244    protected function renderChange() : ?string
245    {
246        if ( ! isset($this->sql['change'])) {
247            return null;
248        }
249        $parts = [];
250        foreach ($this->sql['change'] as $change) {
251            $definition = new TableDefinition(
252                $this->database,
253                $change['if_exists'] ? 'IF EXISTS' : null
254            );
255            $change['definition']($definition);
256            $part = $definition->sql('CHANGE');
257            if ($part) {
258                $parts[] = $part;
259            }
260        }
261        return $parts ? \implode(',' . \PHP_EOL, $parts) : null;
262    }
263
264    /**
265     * @param callable $definition
266     * @param bool $ifExists
267     *
268     * @return static
269     */
270    public function modify(callable $definition, bool $ifExists = false) : static
271    {
272        $this->sql['modify'][] = [
273            'definition' => $definition,
274            'if_exists' => $ifExists,
275        ];
276        return $this;
277    }
278
279    public function modifyIfExists(callable $definition) : static
280    {
281        $this->sql['modify'][] = [
282            'definition' => $definition,
283            'if_exists' => true,
284        ];
285        return $this;
286    }
287
288    protected function renderModify() : ?string
289    {
290        if ( ! isset($this->sql['modify'])) {
291            return null;
292        }
293        $parts = [];
294        foreach ($this->sql['modify'] as $modify) {
295            $definition = new TableDefinition(
296                $this->database,
297                $modify['if_exists'] ? 'IF EXISTS' : null
298            );
299            $modify['definition']($definition);
300            $part = $definition->sql('MODIFY');
301            if ($part) {
302                $parts[] = $part;
303            }
304        }
305        return $parts ? \implode(',' . \PHP_EOL, $parts) : null;
306    }
307
308    public function dropColumn(string $name, bool $ifExists = false) : static
309    {
310        $this->sql['drop_columns'][$name] = $ifExists;
311        return $this;
312    }
313
314    public function dropColumnIfExists(string $name) : static
315    {
316        $this->sql['drop_columns'][$name] = true;
317        return $this;
318    }
319
320    protected function renderDropColumns() : ?string
321    {
322        if ( ! isset($this->sql['drop_columns'])) {
323            return null;
324        }
325        $drops = [];
326        foreach ($this->sql['drop_columns'] as $name => $ifExists) {
327            $name = $this->database->protectIdentifier($name);
328            $ifExists = $ifExists ? 'IF EXISTS ' : '';
329            $drops[] = ' DROP COLUMN ' . $ifExists . $name;
330        }
331        return \implode(',' . \PHP_EOL, $drops);
332    }
333
334    public function dropPrimaryKey() : static
335    {
336        $this->sql['drop_primary_key'] = true;
337        return $this;
338    }
339
340    protected function renderDropPrimaryKey() : ?string
341    {
342        if ( ! isset($this->sql['drop_primary_key'])) {
343            return null;
344        }
345        return ' DROP PRIMARY KEY';
346    }
347
348    public function dropKey(string $name, bool $ifExists = false) : static
349    {
350        $this->sql['drop_keys'][$name] = $ifExists;
351        return $this;
352    }
353
354    public function dropKeyIfExists(string $name) : static
355    {
356        $this->sql['drop_keys'][$name] = true;
357        return $this;
358    }
359
360    protected function renderDropKeys() : ?string
361    {
362        if ( ! isset($this->sql['drop_keys'])) {
363            return null;
364        }
365        $drops = [];
366        foreach ($this->sql['drop_keys'] as $name => $ifExists) {
367            $name = $this->database->protectIdentifier($name);
368            $ifExists = $ifExists ? 'IF EXISTS ' : '';
369            $drops[] = ' DROP KEY ' . $ifExists . $name;
370        }
371        return \implode(',' . \PHP_EOL, $drops);
372    }
373
374    public function dropForeignKey(string $name, bool $ifExists = false) : static
375    {
376        $this->sql['drop_foreign_keys'][$name] = $ifExists;
377        return $this;
378    }
379
380    public function dropForeignKeyIfExists(string $name) : static
381    {
382        $this->sql['drop_foreign_keys'][$name] = true;
383        return $this;
384    }
385
386    protected function renderDropForeignKeys() : ?string
387    {
388        if ( ! isset($this->sql['drop_foreign_keys'])) {
389            return null;
390        }
391        $drops = [];
392        foreach ($this->sql['drop_foreign_keys'] as $name => $ifExists) {
393            $name = $this->database->protectIdentifier($name);
394            $ifExists = $ifExists ? 'IF EXISTS ' : '';
395            $drops[] = ' DROP FOREIGN KEY ' . $ifExists . $name;
396        }
397        return \implode(',' . \PHP_EOL, $drops);
398    }
399
400    public function dropConstraint(string $name, bool $ifExists = false) : static
401    {
402        $this->sql['drop_constraints'][$name] = $ifExists;
403        return $this;
404    }
405
406    public function dropConstraintIfExists(string $name) : static
407    {
408        $this->sql['drop_constraints'][$name] = true;
409        return $this;
410    }
411
412    protected function renderDropConstraints() : ?string
413    {
414        if ( ! isset($this->sql['drop_constraints'])) {
415            return null;
416        }
417        $drops = [];
418        foreach ($this->sql['drop_constraints'] as $name => $ifExists) {
419            $name = $this->database->protectIdentifier($name);
420            $ifExists = $ifExists ? 'IF EXISTS ' : '';
421            $drops[] = ' DROP CONSTRAINT ' . $ifExists . $name;
422        }
423        return \implode(',' . \PHP_EOL, $drops);
424    }
425
426    public function disableKeys() : static
427    {
428        $this->sql['disable_keys'] = true;
429        return $this;
430    }
431
432    protected function renderDisableKeys() : ?string
433    {
434        if ( ! isset($this->sql['disable_keys'])) {
435            return null;
436        }
437        return ' DISABLE KEYS';
438    }
439
440    public function enableKeys() : static
441    {
442        $this->sql['enable_keys'] = true;
443        return $this;
444    }
445
446    protected function renderEnableKeys() : ?string
447    {
448        if ( ! isset($this->sql['enable_keys'])) {
449            return null;
450        }
451        return ' ENABLE KEYS';
452    }
453
454    public function renameTo(string $newTableName) : static
455    {
456        $this->sql['rename_to'] = $newTableName;
457        return $this;
458    }
459
460    protected function renderRenameTo() : ?string
461    {
462        if ( ! isset($this->sql['rename_to'])) {
463            return null;
464        }
465        return ' RENAME TO ' . $this->database->protectIdentifier($this->sql['rename_to']);
466    }
467
468    public function orderBy(string $column, string ...$columns) : static
469    {
470        foreach ([$column, ...$columns] as $col) {
471            $this->sql['order_by'][] = $col;
472        }
473        return $this;
474    }
475
476    protected function renderOrderBy() : ?string
477    {
478        if ( ! isset($this->sql['order_by'])) {
479            return null;
480        }
481        $columns = [];
482        foreach ($this->sql['order_by'] as $column) {
483            $columns[] = $this->database->protectIdentifier($column);
484        }
485        return ' ORDER BY ' . \implode(', ', $columns);
486    }
487
488    public function renameColumn(string $name, string $newName) : static
489    {
490        $this->sql['rename_columns'][$name] = $newName;
491        return $this;
492    }
493
494    protected function renderRenameColumns() : ?string
495    {
496        if ( ! isset($this->sql['rename_columns'])) {
497            return null;
498        }
499        $renames = [];
500        foreach ($this->sql['rename_columns'] as $name => $newName) {
501            $name = $this->database->protectIdentifier($name);
502            $newName = $this->database->protectIdentifier($newName);
503            $renames[] = ' RENAME COLUMN ' . $name . ' TO ' . $newName;
504        }
505        return \implode(',' . \PHP_EOL, $renames);
506    }
507
508    public function renameKey(string $name, string $newName) : static
509    {
510        $this->sql['rename_keys'][$name] = $newName;
511        return $this;
512    }
513
514    protected function renderRenameKeys() : ?string
515    {
516        if ( ! isset($this->sql['rename_keys'])) {
517            return null;
518        }
519        $renames = [];
520        foreach ($this->sql['rename_keys'] as $name => $newName) {
521            $name = $this->database->protectIdentifier($name);
522            $newName = $this->database->protectIdentifier($newName);
523            $renames[] = ' RENAME KEY ' . $name . ' TO ' . $newName;
524        }
525        return \implode(',' . \PHP_EOL, $renames);
526    }
527
528    public function convertToCharset(string $charset, string $collation = null) : static
529    {
530        $this->sql['convert_to_charset'] = [
531            'charset' => $charset,
532            'collation' => $collation,
533        ];
534        return $this;
535    }
536
537    protected function renderConvertToCharset() : ?string
538    {
539        if ( ! isset($this->sql['convert_to_charset'])) {
540            return null;
541        }
542        $charset = $this->database->quote($this->sql['convert_to_charset']['charset']);
543        $convert = ' CONVERT TO CHARACTER SET ' . $charset;
544        if (isset($this->sql['convert_to_charset']['collation'])) {
545            $convert .= ' COLLATE ' . $this->database->quote($this->sql['convert_to_charset']['collation']);
546        }
547        return $convert;
548    }
549
550    public function charset(?string $charset) : static
551    {
552        $this->sql['charset'] = $charset ?? 'DEFAULT';
553        return $this;
554    }
555
556    protected function renderCharset() : ?string
557    {
558        if ( ! isset($this->sql['charset'])) {
559            return null;
560        }
561        $charset = \strtolower($this->sql['charset']);
562        if ($charset === 'default') {
563            return ' DEFAULT CHARACTER SET';
564        }
565        return ' CHARACTER SET = ' . $this->database->quote($charset);
566    }
567
568    public function collate(?string $collation) : static
569    {
570        $this->sql['collate'] = $collation ?? 'DEFAULT';
571        return $this;
572    }
573
574    protected function renderCollate() : ?string
575    {
576        if ( ! isset($this->sql['collate'])) {
577            return null;
578        }
579        $collate = \strtolower($this->sql['collate']);
580        if ($collate === 'default') {
581            return ' DEFAULT COLLATE';
582        }
583        return ' COLLATE = ' . $this->database->quote($collate);
584    }
585
586    /**
587     * @param string $type
588     *
589     * @see https://mariadb.com/kb/en/alter-table/#lock
590     * @see AlterTable::LOCK_DEFAULT
591     * @see AlterTable::LOCK_EXCLUSIVE
592     * @see AlterTable::LOCK_NONE
593     * @see AlterTable::LOCK_SHARED
594     *
595     * @return static
596     */
597    public function lock(string $type) : static
598    {
599        $this->sql['lock'] = $type;
600        return $this;
601    }
602
603    protected function renderLock() : ?string
604    {
605        if ( ! isset($this->sql['lock'])) {
606            return null;
607        }
608        $lock = \strtoupper($this->sql['lock']);
609        if ( ! \in_array($lock, [
610            static::LOCK_DEFAULT,
611            static::LOCK_EXCLUSIVE,
612            static::LOCK_NONE,
613            static::LOCK_SHARED,
614        ], true)) {
615            throw new InvalidArgumentException("Invalid LOCK value: {$this->sql['lock']}");
616        }
617        return ' LOCK = ' . $lock;
618    }
619
620    public function force() : static
621    {
622        $this->sql['force'] = true;
623        return $this;
624    }
625
626    protected function renderForce() : ?string
627    {
628        if ( ! isset($this->sql['force'])) {
629            return null;
630        }
631        return ' FORCE';
632    }
633
634    /**
635     * @param string $algo
636     *
637     * @see https://mariadb.com/kb/en/innodb-online-ddl-overview/#algorithm
638     * @see AlterTable::ALGO_COPY
639     * @see AlterTable::ALGO_DEFAULT
640     * @see AlterTable::ALGO_INPLACE
641     * @see AlterTable::ALGO_INSTANT
642     * @see AlterTable::ALGO_NOCOPY
643     *
644     * @return static
645     */
646    public function algorithm(string $algo) : static
647    {
648        $this->sql['algorithm'] = $algo;
649        return $this;
650    }
651
652    protected function renderAlgorithm() : ?string
653    {
654        if ( ! isset($this->sql['algorithm'])) {
655            return null;
656        }
657        $algo = \strtoupper($this->sql['algorithm']);
658        if ( ! \in_array($algo, [
659            static::ALGO_COPY,
660            static::ALGO_DEFAULT,
661            static::ALGO_INPLACE,
662            static::ALGO_INSTANT,
663            static::ALGO_NOCOPY,
664        ], true)) {
665            throw new InvalidArgumentException("Invalid ALGORITHM value: {$this->sql['algorithm']}");
666        }
667        return ' ALGORITHM = ' . $algo;
668    }
669
670    public function sql() : string
671    {
672        $sql = 'ALTER' . $this->renderOnline() . $this->renderIgnore();
673        $sql .= ' TABLE' . $this->renderIfExists();
674        $sql .= $this->renderTable() . \PHP_EOL;
675        $part = $this->renderWait() . $this->renderNoWait();
676        if ($part) {
677            $sql .= $part . \PHP_EOL;
678        }
679        $sql .= $this->joinParts([
680            $this->renderOptions(),
681            $this->renderAdd(),
682            $this->renderChange(),
683            $this->renderModify(),
684            $this->renderDropColumns(),
685            $this->renderDropPrimaryKey(),
686            $this->renderDropKeys(),
687            $this->renderDropForeignKeys(),
688            $this->renderDropConstraints(),
689            $this->renderDisableKeys(),
690            $this->renderEnableKeys(),
691            $this->renderRenameTo(),
692            $this->renderOrderBy(),
693            $this->renderRenameColumns(),
694            $this->renderRenameKeys(),
695            $this->renderConvertToCharset(),
696            $this->renderCharset(),
697            $this->renderCollate(),
698            $this->renderAlgorithm(),
699            $this->renderLock(),
700            $this->renderForce(),
701        ]);
702        return $sql;
703    }
704
705    /**
706     * @param array<string|null> $parts
707     *
708     * @return string
709     */
710    protected function joinParts(array $parts) : string
711    {
712        $result = '';
713        $hasBefore = false;
714        foreach ($parts as $part) {
715            if ($part !== null) {
716                $result .= $hasBefore ? ',' . \PHP_EOL : '';
717                $result .= $part;
718                $hasBefore = true;
719            }
720        }
721        return $result;
722    }
723
724    /**
725     * Runs the ALTER TABLE statement.
726     *
727     * @return int|string The number of affected rows
728     */
729    public function run() : int | string
730    {
731        return $this->database->exec($this->sql());
732    }
733}