GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Pull Request — master (#61)
by Brendan
02:21
created

IndexDefinition::expectIndexColumns()   A

Complexity

Conditions 6
Paths 13

Size

Total Lines 28
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6.3949

Importance

Changes 0
Metric Value
cc 6
eloc 19
nc 13
nop 1
dl 0
loc 28
ccs 14
cts 18
cp 0.7778
crap 6.3949
rs 9.0111
c 0
b 0
f 0
1
<?php
2
namespace Graze\Morphism\Parse;
3
4
use LogicException;
5
use RuntimeException;
6
7
/**
8
 * Represents the definition of an index.
9
 */
10
class IndexDefinition
11
{
12
    /** @var string|null */
13
    public $constraint = null;
14
15
    /** @var string */
16
    public $type = '';
17
18
    /** @var string|null */
19
    public $name = null;
20
21
    /**
22
     * @var array[] the columns which are indexed.
23
     *
24
     * Each entry is an array of the form:
25
     *
26
     * 'name'   => string
27
     * 'length' => int|null
28
     * 'sort'   => 'ASC'|'DESC'
29
     */
30
    public $columns = [];
31
32
    /** @var mixed[] Associative array of index options
33
     *
34
     * 'USING'          => string 'BTREE' | 'HASH'
35
     * 'KEY_BLOCK_SIZE' => integer
36
     * 'WITH PARSER'    => string;
37
     * 'COMMENT'        => string
38
     */
39
    public $options = [];
40
41
    /**
42
     * @var array Only present for FOREIGN KEYS
43
     *
44
     * 'table'     => string name of foreign table
45
     * 'columns'   => [ 'name' => string, 'length' => int|null, 'sort' => string][] columns referenced in foreign table
46
     * 'ON DELETE' => string 'RESTRICT' | 'CASCADE' | 'SET NULL' | 'NO ACTION'
47
     * 'ON UPDATE' => string 'RESTRICT' | 'CASCADE' | 'SET NULL' | 'NO ACTION'
48
     */
49
    public $reference = [];
50
51
    /**
52
     * Parses an index definition from $stream
53
     *
54
     * The type of key (PRIMARY KEY, UNIQUE KEY, etc) should already have
55
     * been parsed from the stream. If the optional preceding CONSTRAINT clause
56
     * was parsed, you should supply its optional name in $constraint.
57
     *
58
     * @param TokenStream $stream
59
     * @param string $type 'PRIMARY KEY' | 'UNIQUE KEY' | 'KEY' | 'FULLTEXT KEY' | 'FOREIGN KEY'
60
     * @param string|null $constraint name supplied in optional CONSTRAINT clause
61
     */
62 37
    public function parse(TokenStream $stream, $type, $constraint = null)
63
    {
64 37
        $this->type = $type;
65
66
        switch ($type) {
67 37
            case 'PRIMARY KEY':
68 11
                $this->parseOptionalIndexType($stream);
69 11
                $this->parseIndexColumns($stream);
70 11
                $this->parseIndexOptions($stream);
71 11
                break;
72
73 27
            case 'UNIQUE KEY':
74 22
            case 'KEY':
75 15
            case 'FULLTEXT KEY':
76 20
                $this->parseOptionalIndexType($stream);
77 20
                if (!isset($this->options['USING'])) {
78 20
                    $this->parseOptionalIndexName($stream);
79 20
                    $this->parseOptionalIndexType($stream);
80
                }
81 20
                if (!is_null($constraint)) {
82 1
                    $this->name = $constraint;
83
                }
84 20
                $this->parseIndexColumns($stream);
85 20
                $this->parseIndexOptions($stream);
86 20
                break;
87
88 12
            case 'FOREIGN KEY':
89 12
                $this->constraint = $constraint;
90 12
                $this->parseOptionalIndexName($stream);
91 12
                $this->parseIndexColumns($stream);
92 12
                $this->parseReferenceDefinition($stream);
93 12
                break;
94
95
            default:
96
                throw new LogicException("Internal error - unknown index type '$type'");
97
        }
98 37
    }
99
100
    /**
101
     * @param TokenStream $stream
102
     */
103 27
    private function parseOptionalIndexName(TokenStream $stream)
104
    {
105 27
        $mark = $stream->getMark();
106 27
        $token = $stream->nextToken();
107 27
        if ($token->type === Token::IDENTIFIER) {
108 8
            $this->name = $token->text;
109
        } else {
110 24
            $stream->rewind($mark);
111
        }
112 27
    }
113
114
    /**
115
     * @param TokenStream $stream
116
     */
117 30
    private function parseOptionalIndexType(TokenStream $stream)
118
    {
119 30
        if ($stream->consume('USING')) {
120
            $this->parseIndexType($stream);
121
        }
122 30
    }
123
124
    /**
125
     * @param TokenStream $stream
126
     */
127
    private function parseIndexType(TokenStream $stream)
128
    {
129
        if ($stream->consume('BTREE')) {
130
            $using = 'BTREE';
131
        } elseif ($stream->consume('HASH')) {
132
            $using = 'HASH';
133
        } else {
134
            throw new RuntimeException("Expected BTREE or HASH");
135
        }
136
        $this->options['USING'] = $using;
137
    }
138
139
    /**
140
     * @param TokenStream $stream
141
     */
142 37
    private function parseIndexColumns(TokenStream $stream)
143
    {
144 37
        $this->columns = $this->expectIndexColumns($stream);
145 37
    }
146
147
    /**
148
     * @param TokenStream $stream
149
     * @return array
150
     */
151 37
    private function expectIndexColumns(TokenStream $stream)
152
    {
153 37
        $columns = [];
154 37
        $stream->expect(Token::SYMBOL, '(');
155 37
        while (true) {
156
            $column = [
157 37
                'name'   => $stream->expectName(),
158
                'length' => null,
159 37
                'sort'   => 'ASC',
160
            ];
161 37
            if ($stream->consume([[Token::SYMBOL, '(']])) {
162
                $column['length'] = $stream->expectNumber();
163
                $stream->expect(Token::SYMBOL, ')');
164
            }
165 37
            if ($stream->consume('ASC')) {
166
                $column['sort'] = 'ASC';
167 37
            } elseif ($stream->consume('DESC')) {
168
                $column['sort'] = 'DESC';
169
            }
170 37
            $columns[] = $column;
171 37
            if (!$stream->consume([[Token::SYMBOL, ',']])) {
172 37
                break;
173
            }
174
        }
175
176 37
        $stream->expect(Token::SYMBOL, ')');
177
178 37
        return $columns;
179
    }
180
181
    /**
182
     * @param TokenStream $stream
183
     */
184 30
    private function parseIndexOptions(TokenStream $stream)
185
    {
186 30
        while (true) {
187 30
            if ($stream->consume('KEY_BLOCK_SIZE')) {
188
                $stream->consume([[Token::SYMBOL, '=']]);
189
                $this->options['KEY_BLOCK_SIZE'] = $stream->expectNumber();
190 30
            } elseif ($stream->consume('WITH PARSER')) {
191
                $this->options['WITH PARSER'] = $stream->expectName();
192 30
            } elseif ($stream->consume('COMMENT')) {
193
                $this->options['COMMENT'] = $stream->expectString();
194 30
            } elseif ($stream->consume('USING')) {
195
                $this->parseIndexType($stream);
196
            } else {
197 30
                break;
198
            }
199
        }
200 30
    }
201
202
    /**
203
     * @param TokenStream $stream
204
     */
205 12
    private function parseReferenceDefinition(TokenStream $stream)
206
    {
207 12
        $stream->expect(Token::IDENTIFIER, 'REFERENCES');
208
209 12
        $tableOrSchema = $stream->expectName();
210 12
        if ($stream->consume([[Token::SYMBOL, '.']])) {
211
            $schema = $tableOrSchema;
212
            $table = $stream->expectName();
213
        } else {
214 12
            $schema = null;
215 12
            $table = $tableOrSchema;
216
        }
217
218 12
        $this->reference['schema'] = $schema;
219 12
        $this->reference['table'] = $table;
220 12
        $this->reference['columns'] = $this->expectIndexColumns($stream);
221 12
        $this->reference['ON DELETE'] = 'RESTRICT';
222 12
        $this->reference['ON UPDATE'] = 'RESTRICT';
223
224 12
        while (true) {
225 12
            if ($stream->consume('MATCH')) {
226
                throw new RuntimeException("MATCH clause is not supported in this tool, or in MySQL itself!");
227 12
            } elseif ($stream->consume('ON DELETE')) {
228
                $this->parseReferenceOption($stream, 'ON DELETE');
229 12
            } elseif ($stream->consume('ON UPDATE')) {
230
                $this->parseReferenceOption($stream, 'ON UPDATE');
231
            } else {
232 12
                break;
233
            }
234
        }
235 12
    }
236
237
    /**
238
     * @param TokenStream $stream
239
     * @param string $clause
240
     */
241
    private function parseReferenceOption(TokenStream $stream, $clause)
242
    {
243
        $availableOptions = ['RESTRICT', 'CASCADE', 'SET NULL', 'NO ACTION'];
244
        foreach ($availableOptions as $option) {
245
            if ($stream->consume($option)) {
246
                $this->reference[$clause] = $option;
247
                return;
248
            }
249
        }
250
        throw new RuntimeException("Expected one of: " . implode(", ", $availableOptions));
251
    }
252
253
    /**
254
     * Returns an array of all sequences of columns covered by this index. Includes optional lengths.
255
     *
256
     * E.g. the index ```KEY idx (x,y(12),z)``` will give ```[['x', 'y(12)', 'z'], ['x', 'y(12)'], ['x']]```.
257
     *
258
     * @return string[][]
259
     */
260 35
    public function getCovers()
261
    {
262
        // TODO - we should really have separate IndexPart objects so we can
263
        // correctly cope with pathological cases like:
264
        //
265
        //      CREATE TABLE evil (
266
        //          `bad(16)` varchar(32) not null,
267
        //          KEY (`bad(16)`(8))
268
        //      )
269
        //
270 35
        $covers = [];
271 35
        $cover = [];
272 35
        foreach ($this->columns as $column) {
273 35
            $name = $column['name'];
274 35
            if ($column['length']) {
275
                $name .= '(' . $column['length'] . ')';
276
            }
277 35
            $cover[] = $name;
278 35
            $covers[] = $cover;
279
        }
280 35
        return $covers;
281
    }
282
283
    /**
284
     * Returns the sequence of indexed columns, including optional lengths.
285
     *
286
     * E.g. the index ```KEY idx (x, y(12))``` will give ```['x', 'y(12)']```
287
     *
288
     * @return string[]
289
     */
290 12
    public function getColumns()
291
    {
292 12
        $columns = [];
293 12
        foreach ($this->columns as $column) {
294 12
            $name = $column['name'];
295 12
            if ($column['length']) {
296
                $name .= '(' . $column['length'] . ')';
297
            }
298 12
            $columns[] = $name;
299
        }
300 12
        return $columns;
301
    }
302
303
    /**
304
     * Returns an SQL fragment for declaring this index as part of a table definition.
305
     *
306
     * @return string
307
     */
308 38
    public function toString()
309
    {
310 38
        $line = '';
311 38
        if ($this->type === 'FOREIGN KEY') {
312 12
            $line = "CONSTRAINT " . Token::escapeIdentifier($this->constraint) . " ";
313
        }
314 38
        $line .= $this->type;
315 38
        if (!in_array($this->type, ['PRIMARY KEY', 'FOREIGN KEY'])) {
316 24
            $line .= " " . Token::escapeIdentifier($this->name);
317
        }
318 38
        $cols = [];
319 38
        foreach ($this->columns as $column) {
320 38
            $col = Token::escapeIdentifier($column['name']);
321 38
            if (!is_null($column['length'])) {
322
                $col .= "(" . $column['length'] . ")";
323
            }
324 38
            $cols[] = $col;
325
        }
326 38
        $line .= " (" . implode(',', $cols) . ")";
327
328 38
        if (isset($this->options['USING'])) {
329
            $line .= " USING " . $this->options['USING'];
330
        }
331 38
        if (isset($this->options['KEY_BLOCK_SIZE']) && $this->options['KEY_BLOCK_SIZE'] !== 0) {
332
            $line .= " KEY_BLOCK_SIZE=" . $this->options['KEY_BLOCK_SIZE'];
333
        }
334 38
        if (isset($this->options['COMMENT']) && $this->options['COMMENT'] !== '') {
335
            $line .= " COMMENT " . Token::escapeString($this->options['COMMENT']);
336
        }
337 38
        if (isset($this->options['WITH PARSER'])) {
338
            $line .= " WITH PARSER " . $this->options['WITH PARSER'];
339
        }
340
341 38
        if ($this->type === 'FOREIGN KEY') {
342 12
            $reference = Token::escapeIdentifier($this->reference['table']);
343 12
            if (!is_null($this->reference['schema'])) {
344
                $reference = Token::escapeIdentifier($this->reference['schema']) . '.' . $reference;
345
            }
346 12
            $line .= " REFERENCES $reference";
347 12
            $cols = [];
348 12
            foreach ($this->reference['columns'] as $column) {
349 12
                $col = Token::escapeIdentifier($column['name']);
350 12
                if (!is_null($column['length'])) {
351
                    $col .= "(" . $column['length'] . ")";
352
                }
353 12
                $cols[] = $col;
354
            }
355 12
            $line .= " (" . implode(',', $cols) . ")";
356 12
            foreach (['ON DELETE', 'ON UPDATE'] as $clause) {
357 12
                $action = $this->reference[$clause];
358 12
                if ($action !== 'RESTRICT') {
359 12
                    $line .= " $clause $action";
360
                }
361
            }
362
        }
363 38
        return $line;
364
    }
365
}
366