Completed
Branch feature/pre-split (d89158)
by Anton
04:43
created

SQLiteHandler::syncTable()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 38
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 17
nc 3
nop 2
dl 0
loc 38
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\Database\Drivers\SQLite;
8
9
use Spiral\Database\Entities\AbstractHandler;
10
use Spiral\Database\Exceptions\HandlerException;
11
use Spiral\Database\Schemas\Prototypes\AbstractColumn;
12
use Spiral\Database\Schemas\Prototypes\AbstractReference;
13
use Spiral\Database\Schemas\Prototypes\AbstractTable;
14
15
class SQLiteHandler extends AbstractHandler
16
{
17
    public function syncTable(AbstractTable $table, int $behaviour = self::DO_ALL)
18
    {
19
        if (!$this->requiresRebuild($table)) {
20
            //Nothing special, can be handled as usually
21
            parent::syncTable($table, $behaviour);
22
23
            return;
24
        }
25
26
        //Now we have to work with temporary table in order to perform every change
27
        $this->log('Rebuilding table {table} to apply required modifications.', [
28
            'table' => $this->identify($table)
29
        ]);
30
31
        $initial = clone $table;
32
        $initial->resetState();
33
34
        //Temporary table is required to copy data over
35
        $temporary = $this->createTemporary($table);
36
37
        //Moving data over
38
        $this->copyData(
39
            $initial->getName(),
40
            $temporary->getName(),
41
            $this->createMapping($initial, $temporary)
42
        );
43
44
        //We can drop initial table now
45
        $this->dropTable($table);
46
47
        //Renaming temporary table (should automatically handle table renaming)
48
        $this->renameTable($temporary->getName(), $initial->getName());
49
50
        //Not all databases support adding index while table creation, so we can do it after
51
        foreach ($table->getIndexes() as $index) {
52
            $this->createIndex($table, $index);
0 ignored issues
show
Compatibility introduced by
$index of type object<Spiral\Database\Schemas\IndexInterface> is not a sub-type of object<Spiral\Database\S...ototypes\AbstractIndex>. It seems like you assume a concrete implementation of the interface Spiral\Database\Schemas\IndexInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
53
        }
54
    }
55
56
    /**
57
     * {@inheritdoc}
58
     */
59
    public function createColumn(AbstractTable $table, AbstractColumn $column)
60
    {
61
        //Not supported
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67
    public function dropColumn(AbstractTable $table, AbstractColumn $column)
68
    {
69
        //Not supported
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75
    public function alterColumn(
76
        AbstractTable $table,
77
        AbstractColumn $initial,
78
        AbstractColumn $column
79
    ) {
80
        //Not supported
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86
    public function createForeign(AbstractTable $table, AbstractReference $foreign)
87
    {
88
        //Not supported
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94
    public function dropForeign(AbstractTable $table, AbstractReference $foreign)
95
    {
96
        //Not supported
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function alterForeign(
103
        AbstractTable $table,
104
        AbstractReference $initial,
105
        AbstractReference $foreign
106
    ) {
107
        //Not supported
108
    }
109
110
    /**
111
     * Rebuild is required when columns or foreign keys are altered.
112
     *
113
     * @param AbstractTable $table
114
     *
115
     * @return bool
116
     */
117
    private function requiresRebuild(AbstractTable $table): bool
118
    {
119
        $comparator = $table->getComparator();
120
121
        $difference = [
122
            count($comparator->addedColumns()),
123
            count($comparator->droppedColumns()),
124
            count($comparator->alteredColumns()),
125
            count($comparator->addedForeigns()),
126
            count($comparator->droppedForeigns()),
127
            count($comparator->alteredForeigns()),
128
        ];
129
130
        return array_sum($difference) != 0;
131
    }
132
133
    /**
134
     * Temporary table.
135
     *
136
     * @return AbstractTable
137
     */
138
    protected function createTemporary(AbstractTable $table): AbstractTable
139
    {
140
        //Temporary table is required to copy data over
141
        $temporary = clone $table;
142
        $temporary->setName('spiral_temp_' . $table->getName() . '_' . uniqid());
143
144
        //We don't need any index in temporary table
145
        foreach ($temporary->getIndexes() as $index) {
146
            $temporary->dropIndex($index->getColumns());
147
        }
148
149
        $this->createTable($temporary);
150
151
        return $temporary;
152
    }
153
154
    /**
155
     * Copy table data to another location.
156
     *
157
     * @see http://stackoverflow.com/questions/4007014/alter-column-in-sqlite
158
     *
159
     * @param string $source
160
     * @param string $to
161
     * @param array  $mapping (destination => source)
162
     *
163
     * @throws HandlerException
164
     */
165
    private function copyData(string $source, string $to, array $mapping)
166
    {
167
        $sourceColumns = array_keys($mapping);
168
        $targetColumns = array_values($mapping);
169
170
        $this->log(
171
            'Copying table data from {source} to {to} using mapping ({columns}) => ({target}).',
172
            [
173
                'source'  => $this->identify($source),
174
                'to'      => $this->identify($to),
175
                'columns' => implode(', ', $sourceColumns),
176
                'target'  => implode(', ', $targetColumns),
177
            ]
178
        );
179
180
        //Preparing mapping
181
        $sourceColumns = array_map([$this, 'identify'], $sourceColumns);
182
        $targetColumns = array_map([$this, 'identify'], $targetColumns);
183
184
        $query = \Spiral\interpolate('INSERT INTO {to} ({target}) SELECT {columns} FROM {source}',
185
            [
186
                'source'  => $this->identify($source),
187
                'to'      => $this->identify($to),
188
                'columns' => implode(', ', $sourceColumns),
189
                'target'  => implode(', ', $targetColumns),
190
            ]
191
        );
192
193
        $this->run($query);
194
    }
195
196
    /**
197
     * Get mapping between new and initial columns.
198
     *
199
     * @param AbstractTable $source
200
     * @param AbstractTable $target
201
     *
202
     * @return array
203
     */
204
    private function createMapping(AbstractTable $source, AbstractTable $target)
205
    {
206
        $mapping = [];
207
        foreach ($target->getColumns() as $name => $column) {
208
            if ($source->hasColumn($name)) {
209
                $mapping[$name] = $column->getName();
210
            }
211
        }
212
213
        return $mapping;
214
    }
215
}