Completed
Branch develop (c2aa4c)
by Anton
05:17
created

TableSchema::loadIndexes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 13
rs 9.4286
cc 3
eloc 7
nc 3
nop 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\Database\Drivers\SQLite\Schemas;
9
10
use Spiral\Database\Entities\Schemas\AbstractTable;
11
12
/**
13
 * SQLIte specific table schema, some alter operations emulated using temporary tables.
14
 */
15
class TableSchema extends AbstractTable
16
{
17
    /**
18
     * {@inheritdoc}
19
     */
20
    protected function loadColumns()
21
    {
22
        $tableSQL = $this->driver->query(
23
            "SELECT sql FROM sqlite_master WHERE type = 'table' and name = ?", [$this->getName()]
24
        )->fetchColumn();
25
26
        /**
27
         * There is not really many ways to get extra information about column in SQLite, let's parse
28
         * table schema. As mention, spiral SQLite schema reader will support fully only tables created
29
         * by spiral as we expecting every column definition be on new line.
30
         */
31
        $tableStatement = explode("\n", $tableSQL);
32
33
        $columnsQuery = $this->driver->query("PRAGMA TABLE_INFO({$this->getName(true)})");
34
35
        $primaryKeys = [];
36
        foreach ($columnsQuery as $column) {
37
            if (!empty($column['pk'])) {
38
                $primaryKeys[] = $column['name'];
39
            }
40
41
            $column['tableStatement'] = $tableStatement;
42
            $this->registerColumn($this->columnSchema($column['name'], $column));
43
        }
44
45
        $this->setPrimaryKeys($primaryKeys);
46
47
        return $this;
48
    }
49
50
    /**
51
     * {@inheritdoc}
52
     */
53
    protected function loadIndexes()
54
    {
55
        $indexesQuery = $this->driver->query("PRAGMA index_list({$this->getName(true)})");
56
        foreach ($indexesQuery as $index) {
57
            $index = $this->registerIndex($this->indexSchema($index['name'], $index));
58
59
            if ($index->getColumns() == $this->getPrimaryKeys()) {
60
                $this->forgetIndex($index);
61
            }
62
        }
63
64
        return $this;
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70 View Code Duplication
    protected function loadReferences()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
71
    {
72
        $foreignsQuery = $this->driver->query("PRAGMA foreign_key_list({$this->getName(true)})");
73
        foreach ($foreignsQuery as $reference) {
74
            $this->registerReference($this->referenceSchema($reference['id'], $reference));
75
        }
76
77
        return $this;
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     */
83
    protected function synchroniseSchema()
84
    {
85
        if (!$this->requiresRebuild()) {
86
            //Probably some index changed or table renamed
87
            return parent::synchroniseSchema();
88
        }
89
90
        $this->logger()->debug("Rebuilding table {table} to apply required modifications.", [
91
            'table' => $this->getName(true)
92
        ]);
93
94
        //Temporary table is required to copy data over
95
        $temporary = $this->createTemporary();
96
97
        //Moving data over
98
        $this->copyData($temporary, $this->columnsMapping(true));
99
100
        //Dropping current table
101
        $this->commander->dropTable($this->initial->getName());
102
103
        //Renaming temporary table (should automatically handle table renaming)
104
        $this->commander->renameTable($temporary->getName(), $this->getName());
105
106
        //We can create needed indexes now
107
        foreach ($this->getIndexes() as $index) {
108
            $this->commander->addIndex($this, $index);
109
        }
110
111
        return $this;
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117
    protected function columnSchema($name, $schema = null)
118
    {
119
        return new ColumnSchema($this, $name, $schema);
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125
    protected function indexSchema($name, $schema = null)
126
    {
127
        return new IndexSchema($this, $name, $schema);
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133
    protected function referenceSchema($name, $schema = null)
134
    {
135
        return new ReferenceSchema($this, $name, $schema);
136
    }
137
138
    /**
139
     * Rebuild is required when columns or foreign keys are altered.
140
     *
141
     * @return bool
142
     */
143
    private function requiresRebuild()
144
    {
145
        $difference = [
146
            count($this->comparator->addedColumns()),
147
            count($this->comparator->droppedColumns()),
148
            count($this->comparator->alteredColumns()),
149
            count($this->comparator->addedForeigns()),
150
            count($this->comparator->droppedForeigns()),
151
            count($this->comparator->alteredForeigns())
152
        ];
153
154
        return array_sum($difference) != 0;
155
    }
156
157
    /**
158
     * Temporary table.
159
     *
160
     * @return TableSchema
161
     */
162
    private function createTemporary()
163
    {
164
        //Temporary table is required to copy data over
165
        $temporary = clone $this;
166
        $temporary->setName('spiral_temp_' . $this->getName() . '_' . uniqid());
167
168
        //We don't need any index in temporary table
169
        foreach ($temporary->getIndexes() as $index) {
170
            $temporary->forgetIndex($index);
171
        }
172
173
        $this->commander->createTable($temporary);
174
175
        return $temporary;
176
    }
177
178
    /**
179
     * Copy table data to another location.
180
     *
181
     * @see http://stackoverflow.com/questions/4007014/alter-column-in-sqlite
182
     * @param AbstractTable $temporary
183
     * @param array         $mapping Association between old and new columns (quoted).
184
     */
185
    private function copyData(AbstractTable $temporary, array $mapping)
186
    {
187
        $this->logger()->debug(
188
            "Copying table data from {source} to {table} using mapping ({columns}) => ({target}).",
189
            [
190
                'source'  => $this->driver->identifier($this->initial->getName()),
191
                'table'   => $temporary->getName(true),
192
                'columns' => join(', ', $mapping),
193
                'target'  => join(', ', array_keys($mapping))
194
            ]
195
        );
196
197
        $query = \Spiral\interpolate(
198
            "INSERT INTO {table} ({target}) SELECT {columns} FROM {source}",
199
            [
200
                'source'  => $this->driver->identifier($this->initial->getName()),
201
                'table'   => $temporary->getName(true),
202
                'columns' => join(', ', $mapping),
203
                'target'  => join(', ', array_keys($mapping))
204
            ]
205
        );
206
207
        //Let's go
208
        $this->driver->statement($query);
209
    }
210
211
    /**
212
     * Get mapping between new and initial columns.
213
     *
214
     * @param bool $quoted
215
     * @return array
216
     */
217
    private function columnsMapping($quoted = false)
218
    {
219
        $current = $this->getColumns();
220
        $initial = $this->initial->getColumns();
221
222
        $mapping = [];
223
        foreach ($current as $name => $column) {
224
            if (isset($initial[$name])) {
225
                $mapping[$column->getName($quoted)] = $initial[$name]->getName($quoted);
226
            }
227
        }
228
229
        return $mapping;
230
    }
231
}