Issues (265)

src/Driver/SQLite/SQLiteHandler.php (4 issues)

1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Driver\SQLite;
13
14
use Cycle\Database\Driver\Handler;
15
use Cycle\Database\Driver\SQLite\Schema\SQLiteTable;
16
use Cycle\Database\Exception\DBALException;
17
use Cycle\Database\Exception\HandlerException;
18
use Cycle\Database\Schema\AbstractColumn;
0 ignored issues
show
The type Cycle\Database\Schema\AbstractColumn was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use Cycle\Database\Schema\AbstractForeignKey;
20
use Cycle\Database\Schema\AbstractTable;
21
22
class SQLiteHandler extends Handler
23
{
24
    /**
25
     * @return string[]
26
     */
27 904
    public function getTableNames(string $prefix = ''): array
28
    {
29 904
        $query = $this->driver->query(
0 ignored issues
show
The method query() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

29
        /** @scrutinizer ignore-call */ 
30
        $query = $this->driver->query(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
30 904
            "SELECT name FROM 'sqlite_master' WHERE type = 'table'",
31
        );
32
33 904
        $tables = [];
34 904
        foreach ($query as $table) {
35 448
            if ($table['name'] === 'sqlite_sequence') {
36
                continue;
37
            }
38
39 448
            if ($prefix !== '' && !\str_starts_with($table['name'], $prefix)) {
40 2
                continue;
41
            }
42
43 448
            $tables[] = $table['name'];
44
        }
45
46 904
        return $tables;
47
    }
48
49
    /**
50
     * @psalm-param non-empty-string $table
51
     */
52 482
    public function hasTable(string $table): bool
53
    {
54 482
        $query = "SELECT COUNT('sql') FROM 'sqlite_master' WHERE type = 'table' and name = ?";
55
56 482
        return (bool) $this->driver->query($query, [$table])->fetchColumn();
57
    }
58
59 482
    public function getSchema(string $table, ?string $prefix = null): AbstractTable
60
    {
61 482
        return new SQLiteTable($this->driver, $table, $prefix ?? '');
0 ignored issues
show
It seems like $this->driver can also be of type null; however, parameter $driver of Cycle\Database\Driver\SQ...iteTable::__construct() does only seem to accept Cycle\Database\Driver\DriverInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

61
        return new SQLiteTable(/** @scrutinizer ignore-type */ $this->driver, $table, $prefix ?? '');
Loading history...
62
    }
63
64 4
    public function eraseTable(AbstractTable $table): void
65
    {
66 4
        $this->driver->execute(
67 4
            "DELETE FROM {$this->driver->identifier($table->getFullName())}",
0 ignored issues
show
The method identifier() does not exist on Cycle\Database\Driver\DriverInterface. It seems like you code against a sub-type of Cycle\Database\Driver\DriverInterface such as Cycle\Database\Driver\Driver. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

67
            "DELETE FROM {$this->driver->/** @scrutinizer ignore-call */ identifier($table->getFullName())}",
Loading history...
68
        );
69 2
    }
70
71 158
    public function syncTable(AbstractTable $table, int $operation = self::DO_ALL): void
72
    {
73 158
        if (!$this->requiresRebuild($table)) {
74
            //Nothing special, can be handled as usually
75 50
            parent::syncTable($table, $operation);
76
77 50
            return;
78
        }
79
80 116
        if ($table->getComparator()->isPrimaryChanged()) {
81
            throw new DBALException('Unable to change primary keys for existed table');
82
        }
83
84 116
        $initial = clone $table;
85 116
        $initial->resetState();
86
87
        //Temporary table is required to copy data over
88 116
        $temporary = $this->createTemporary($table);
89
90
        //Moving data over
91 116
        $this->copyData(
92 116
            $initial->getFullName(),
93 116
            $temporary->getFullName(),
94 116
            $this->createMapping($initial, $temporary),
95
        );
96
97
        //We can drop initial table now
98 116
        $this->dropTable($table);
99
100
        //Renaming temporary table (should automatically handle table renaming)
101 116
        $this->renameTable($temporary->getFullName(), $initial->getFullName());
102
103
        //Not all databases support adding index while table creation, so we can do it after
104 116
        foreach ($table->getIndexes() as $index) {
105 64
            $this->createIndex($table, $index);
106
        }
107 116
    }
108
109
    public function createColumn(AbstractTable $table, AbstractColumn $column): void
110
    {
111
        //Not supported
112
    }
113
114
    public function dropColumn(AbstractTable $table, AbstractColumn $column): void
115
    {
116
        //Not supported
117
    }
118
119
    public function alterColumn(
120
        AbstractTable $table,
121
        AbstractColumn $initial,
122
        AbstractColumn $column,
123
    ): void {
124
        //Not supported
125
    }
126
127
    public function createForeignKey(AbstractTable $table, AbstractForeignKey $foreignKey): void
128
    {
129
        //Not supported
130
    }
131
132
    public function dropForeignKey(AbstractTable $table, AbstractForeignKey $foreignKey): void
133
    {
134
        //Not supported
135
    }
136
137
    public function alterForeignKey(
138
        AbstractTable $table,
139
        AbstractForeignKey $initial,
140
        AbstractForeignKey $foreignKey,
141
    ): void {
142
        //Not supported
143
    }
144
145
    public function enableForeignKeyConstraints(): void
146
    {
147
        $this->run('PRAGMA foreign_keys = ON;');
148 116
    }
149
150
    public function disableForeignKeyConstraints(): void
151 116
    {
152 116
        $this->run('PRAGMA foreign_keys = OFF;');
153 116
    }
154
155
    /**
156
     * Temporary table based on parent.
157 116
     */
158 64
    protected function createTemporary(AbstractTable $table): AbstractTable
159
    {
160
        //Temporary table is required to copy data over
161 116
        $temporary = clone $table;
162
        $temporary->setName(
163 116
            'spiral_temp_' . $table->getFullName() . '_' . \uniqid(),
164
        );
165
166
        //We don't need any indexes in temporary table
167
        foreach ($temporary->getIndexes() as $index) {
168
            $temporary->dropIndex($index->getColumnsWithSort());
169 158
        }
170
171 158
        $this->createTable($temporary);
172
173 158
        return $temporary;
174 158
    }
175 158
176 158
    /**
177
     * Rebuild is required when columns or foreign keys are altered.
178 158
     */
179 158
    private function requiresRebuild(AbstractTable $table): bool
180 158
    {
181
        $comparator = $table->getComparator();
182
183 158
        $difference = [
184
            \count($comparator->addedColumns()),
185
            \count($comparator->droppedColumns()),
186
            \count($comparator->alteredColumns()),
187
188
            \count($comparator->addedForeignKeys()),
189
            \count($comparator->droppedForeignKeys()),
190
            \count($comparator->alteredForeignKeys()),
191
        ];
192
193
        return \array_sum($difference) !== 0;
194
    }
195
196
    /**
197
     * Copy table data to another location.
198 116
     *
199
     * @see http://stackoverflow.com/questions/4007014/alter-column-in-sqlite
200 116
     *
201 116
     * @psalm-param non-empty-string $source
202
     * @psalm-param non-empty-string $to
203
     *
204 116
     * @param array  $mapping (destination => source)
205 116
     *
206
     * @throws HandlerException
207 116
     */
208 116
    private function copyData(string $source, string $to, array $mapping): void
209 116
    {
210 116
        $sourceColumns = \array_keys($mapping);
211 116
        $targetColumns = \array_values($mapping);
212 116
213
        //Preparing mapping
214
        $sourceColumns = \array_map([$this, 'identify'], $sourceColumns);
215 116
        $targetColumns = \array_map([$this, 'identify'], $targetColumns);
216 116
217
        $query = \sprintf(
218
            'INSERT INTO %s (%s) SELECT %s FROM %s',
219
            $this->identify($to),
220
            \implode(', ', $targetColumns),
221 116
            \implode(', ', $sourceColumns),
222
            $this->identify($source),
223 116
        );
224 116
225 116
        $this->run($query);
226 116
    }
227
228
    /**
229
     * Get mapping between new and initial columns.
230 116
     */
231
    private function createMapping(AbstractTable $source, AbstractTable $target): array
232
    {
233
        $mapping = [];
234
        foreach ($target->getColumns() as $name => $column) {
235
            if ($source->hasColumn($name)) {
236
                $mapping[$name] = $column->getName();
237
            }
238
        }
239
240
        return $mapping;
241
    }
242
}
243