Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.70% covered (success)
95.70%
89 / 93
87.50% covered (success)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Alter
95.70% covered (success)
95.70%
89 / 93
87.50% covered (success)
87.50%
7 / 8
38
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
5
 modifyColumn
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 dropColumn
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 dropIndex
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 dropConstraint
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 after
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 render
90.91% covered (success)
90.91%
40 / 44
0.00% covered (danger)
0.00%
0 / 1
18.24
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Db\Sql\Schema;
16
17use Pop\Db\Adapter\AbstractAdapter;
18
19/**
20 * Schema ALTER table class
21 *
22 * @category   Pop
23 * @package    Pop\Db
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    7.0.0
28 */
29class Alter extends AbstractStructure
30{
31
32    /**
33     * Existing columns in the table
34     * @var array
35     */
36    protected array $existingColumns = [];
37
38    /**
39     * Columns to be dropped
40     * @var array
41     */
42    protected array $dropColumns = [];
43
44    /**
45     * Indices to be dropped
46     * @var array
47     */
48    protected array $dropIndices = [];
49
50    /**
51     * Constraints to be dropped
52     * @var array
53     */
54    protected array $dropConstraints = [];
55
56    /**
57     * Constructor
58     *
59     * Instantiate the ALTER table object
60     *
61     * @param  string          $table
62     * @param  AbstractAdapter $db
63     */
64    public function __construct(string $table, $db)
65    {
66        parent::__construct($table, $db);
67
68        if (count($this->info['columns']) > 0) {
69            foreach ($this->info['columns'] as $name => $column) {
70                $size      = null;
71                $precision = null;
72                if (str_contains($column['type'], '(')) {
73                    $type = substr($column['type'], 0, strpos($column['type'], '('));
74                    if (str_contains($column['type'], ',')) {
75                        $size = substr($column['type'], (strpos($column['type'], '(') + 1));
76                        $size = substr($size, 0, strpos($size, ','));
77                        $precision = substr($column['type'], (strpos($column['type'], ',') + 1));
78                        $precision = trim(substr($precision, 0, strpos($precision, ')')));
79                    } else {
80                        $size = substr($column['type'], (strpos($column['type'], '(') + 1));
81                        $size = substr($size, 0, strpos($size, ')'));
82                    }
83                } else {
84                    $type = $column['type'];
85                }
86
87                $this->existingColumns[$name] = [
88                    'type'       => $type,
89                    'size'       => $size,
90                    'precision'  => $precision,
91                    'nullable'   => $column['null'],
92                    'default'    => null,
93                    'increment'  => false,
94                    'primary'    => $column['primary'],
95                    'unsigned'   => false,
96                    'attributes' => [],
97                    'modify'     => null
98                ];
99            }
100        }
101    }
102
103    /**
104     * Modify a column
105     *
106     * @param  string  $oldName
107     * @param  string  $newName
108     * @param  ?string $type
109     * @param  mixed   $size
110     * @param  mixed   $precision
111     * @return Alter
112     */
113    public function modifyColumn(string $oldName, string $newName, ?string $type = null, mixed $size = null, mixed $precision = null): Alter
114    {
115        if (isset($this->existingColumns[$oldName])) {
116            if ($type !== null) {
117                $this->existingColumns[$oldName]['type'] = $type;
118            }
119            if ($size !== null) {
120                $this->existingColumns[$oldName]['size'] = $size;
121            }
122            if ($precision !== null) {
123                $this->existingColumns[$oldName]['precision'] = $precision;
124            }
125
126            $this->existingColumns[$oldName]['modify'] = $newName;
127        }
128
129        return $this;
130    }
131
132    /**
133     * Drop a column
134     *
135     * @param  string $name
136     * @return Alter
137     */
138    public function dropColumn(string $name): Alter
139    {
140        if (!in_array($name, $this->dropColumns)) {
141            $this->dropColumns[] = $name;
142        }
143        return $this;
144    }
145
146    /**
147     * Drop an index
148     *
149     * @param  string $name
150     * @return Alter
151     */
152    public function dropIndex(string $name): Alter
153    {
154        if (!in_array($name, $this->dropIndices)) {
155            $this->dropIndices[] = $name;
156        }
157        return $this;
158    }
159
160    /**
161     * Drop a constraint
162     *
163     * @param  string $name
164     * @return Alter
165     */
166    public function dropConstraint(string $name): Alter
167    {
168        if (!in_array($name, $this->dropConstraints)) {
169            $this->dropConstraints[] = $name;
170        }
171        return $this;
172    }
173
174    /**
175     * Set the AFTER column (MySQL-only))
176     *
177     * @param  string $column
178     * @return Alter
179     */
180    public function after(string $column): Alter
181    {
182        if (($this->currentColumn !== null) && isset($this->columns[$this->currentColumn])) {
183            $this->columns[$this->currentColumn]['after'] = $column;
184        }
185        return $this;
186    }
187
188    /**
189     * Render the table schema
190     *
191     * @return string
192     */
193    public function render(): string
194    {
195        $schema = '';
196
197        // Modify existing columns
198        foreach ($this->existingColumns as $name => $column) {
199            if ($column['modify'] !== null) {
200                if ($this->isMysql()) {
201                    $schema .= 'ALTER TABLE ' . $this->quoteId($this->table) .
202                        ' CHANGE COLUMN ' . $this->quoteId($name) . ' ' .
203                        $this->getColumnSchema($column['modify'], $column) . ';' . PHP_EOL;
204                } else {
205                    if ($column['modify'] == $name) {
206                        $schema .= 'ALTER TABLE ' . $this->quoteId($this->table) . ' ALTER COLUMN ' .
207                            $this->getColumnSchema($column['modify'], $column) . ';' . PHP_EOL;
208                    } else {
209                        $schema .= 'ALTER TABLE ' . $this->quoteId($this->table) . ' RENAME COLUMN ' .
210                            $this->quoteId($name) . ' ' . $this->quoteId($column['modify']) . ';' . PHP_EOL;
211                    }
212                }
213            }
214        }
215
216        // Add new columns
217        foreach ($this->columns as $name => $column) {
218            $schema .= 'ALTER TABLE ' . $this->quoteId($this->table) . ' ADD ' . $this->getColumnSchema($name, $column);
219            if (($this->isMysql()) && !empty($column['after'])) {
220                $schema .= ' AFTER ' . $this->quoteId($column['after']);
221            }
222            $schema .= ';' . PHP_EOL;
223        }
224
225        // Drop columns
226        foreach ($this->dropColumns as $name => $column) {
227            $schema .= 'ALTER TABLE ' . $this->quoteId($this->table) . ' DROP COLUMN ' . $this->quoteId($column) . ';' . PHP_EOL;
228        }
229
230        // Drop indices
231        foreach ($this->dropIndices as $index) {
232            if ($this->isMysql()) {
233                $schema .= 'ALTER TABLE ' . $this->quoteId($this->table) . ' DROP INDEX ' . $this->quoteId($index) . ';' . PHP_EOL;
234            } else {
235                $schema .= 'DROP INDEX ' . $this->quoteId($this->table . '.' . $index) . ';' . PHP_EOL;
236            }
237        }
238
239        // Drop constraints
240        if ((count($this->dropConstraints) > 0) && ($this->isSqlite())) {
241            throw new Exception(
242                'Error: SQLite does not support dropping a FOREIGN KEY constraint via ALTER TABLE. ' .
243                'The table must be recreated without the constraint instead.'
244            );
245        }
246        foreach ($this->dropConstraints as $constraint) {
247            if ($this->isMysql()) {
248                $schema .= 'ALTER TABLE ' . $this->quoteId($this->table) . ' DROP FOREIGN KEY ' .
249                    $this->quoteId($constraint) . ';' . PHP_EOL;
250            } else {
251                $schema .= 'ALTER TABLE ' . $this->quoteId($this->table) . ' DROP CONSTRAINT ' .
252                    $this->quoteId($constraint) . ';' . PHP_EOL;
253            }
254        }
255
256        // Add indices
257        if (count($this->indices) > 0) {
258            $schema .= Formatter\Table::createIndices($this->indices, $this->table, $this);
259        }
260
261        // Add constraints
262        if (count($this->constraints) > 0) {
263            if ($this->isSqlite()) {
264                throw new Exception(
265                    'Error: SQLite does not support adding a FOREIGN KEY constraint to an existing table via ' .
266                    'ALTER TABLE. The constraint must be declared inline when the table is created instead.'
267                );
268            }
269            $schema .= Formatter\Table::createConstraints($this->constraints, $this->table, $this);
270        }
271
272        return $schema . PHP_EOL;
273    }
274
275    /**
276     * Render the table schema to string
277     *
278     * @return string
279     */
280    public function __toString(): string
281    {
282        return $this->render();
283    }
284
285}