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

TableOptions::parseString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
ccs 0
cts 3
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
namespace Graze\Morphism\Parse;
3
4
use RuntimeException;
5
6
/**
7
 * Represents a set of table options - MIN_ROWS, PACK_KEYS, COMMENT, etc.
8
 */
9
class TableOptions
10
{
11
    /** @var string|null */
12
    public $engine = null;
13
14
    /** @var CollationInfo */
15
    public $collation = null;
16
17
    /**
18
     * @var array
19
     * maps option names to values (string|int|null)
20
     */
21
    public $options = [];
22
23
    /** @var string */
24
    private $defaultEngine = null;
25
    /** @var CollationInfo|null */
26
    private $defaultCollation = null;
27
    /** @var array */
28
    private $defaultOptions = [
29
        'AUTO_INCREMENT'  => null,
30
        'MIN_ROWS'        => 0,
31
        'MAX_ROWS'        => 0,
32
        'AVG_ROW_LENGTH'  => 0,
33
        'PACK_KEYS'       => 'DEFAULT',
34
        'CHECKSUM'        => '0',
35
        'DELAY_KEY_WRITE' => '0',
36
        'ROW_FORMAT'      => 'DEFAULT',
37
        'KEY_BLOCK_SIZE'  => 0,
38
        'COMMENT'         => '',
39
        'CONNECTION'      => '',
40
    ];
41
42
    /**
43
     * Constructor
44
     * @param CollationInfo $databaseCollation
45
     */
46 86
    public function __construct(CollationInfo $databaseCollation)
47
    {
48 86
        $this->collation = new CollationInfo;
49 86
        $this->defaultCollation = clone $databaseCollation;
50 86
    }
51
52
    /**
53
     * Set the default storage engine for the table to use in case the
54
     * ENGINE option is not supplied.
55
     *
56
     * @param string $engine e.g. 'InnoDB"
57
     */
58 84
    public function setDefaultEngine($engine)
59
    {
60 84
        $this->defaultEngine = self::normaliseEngine($engine);
61 84
    }
62
63
    /**
64
     * Parses table options from $stream.
65
     * @param TokenStream $stream
66
     */
67 66
    public function parse(TokenStream $stream)
68
    {
69 66
        $this->engine = $this->defaultEngine;
70 66
        $this->options = $this->defaultOptions;
71
72 66
        while (true) {
73 66
            $mark = $stream->getMark();
74 66
            $token = $stream->nextToken();
75 66
            if ($token->type !== Token::IDENTIFIER) {
76 66
                $stream->rewind($mark);
77 66
                break;
78
            }
79
80
            if ($token->eq(Token::IDENTIFIER, 'DEFAULT')) {
81
                $token = $stream->nextToken();
82
                if (!($token->type === Token::IDENTIFIER &&
83
                      in_array(strtoupper($token->text), ['CHARSET', 'CHARACTER', 'COLLATE']))
84
                ) {
85
                    throw new RuntimeException("Expected CHARSET, CHARACTER SET or COLLATE");
86
                }
87
            }
88
89
            $this->parseOption($stream, strtoupper($token->text));
90
        }
91
92 66
        if (!$this->collation->isSpecified()) {
93 66
            $this->collation = clone $this->defaultCollation;
94
        }
95 66
    }
96
97
    /**
98
     * @param TokenStream $stream
99
     * @param string $option
100
     */
101
    private function parseOption(TokenStream $stream, $option)
102
    {
103
        switch ($option) {
104
            case 'ENGINE':
105
            case 'COLLATE':
106
            case 'CHARSET':
107
                $this->parseIdentifier($stream, $option);
108
                break;
109
110
            case 'CHARACTER':
111
                $stream->expect(Token::IDENTIFIER, 'SET');
112
                $this->parseIdentifier($stream, 'CHARSET');
113
                break;
114
115
            case 'AUTO_INCREMENT':
116
            case 'AVG_ROW_LENGTH':
117
            case 'KEY_BLOCK_SIZE':
118
            case 'MAX_ROWS':
119
            case 'MIN_ROWS':
120
                $this->parseNumber($stream, $option);
121
                break;
122
123
            case 'CHECKSUM':
124
            case 'DELAY_KEY_WRITE':
125
                $this->parseEnum($stream, $option, ['0', '1']);
126
                break;
127
128
            case 'PACK_KEYS':
129
                $this->parseEnum($stream, $option, ['DEFAULT', '0', '1']);
130
                break;
131
132
            case 'DATA':
133
            case 'INDEX':
134
                $stream->expect(Token::IDENTIFIER, 'DIRECTORY');
135
                // fall through //
136
            case 'COMMENT':
137
            case 'CONNECTION':
138
            case 'PASSWORD':
139
                $this->parseString($stream, $option);
140
                break;
141
142
            case 'INSERT_METHOD':
143
                $this->parseEnum($stream, $option, ['NO', 'FIRST', 'LAST']);
144
                throw new RuntimeException("$option is not currently supported by this tool");
145
146
            case 'ROW_FORMAT':
147
                $this->parseEnum($stream, $option, ['DEFAULT', 'DYNAMIC', 'FIXED', 'COMPRESSED', 'REDUNDANT', 'COMPACT']);
148
                break;
149
150
            case 'PARTITION':
151
            case 'STATS_AUTO_RECALC':
152
            case 'STATS_PERSISTENT':
153
            case 'STATS_SAMPLE_PAGES':
154
            case 'TABLESPACE':
155
            case 'UNION':
156
                throw new RuntimeException("$option is not currently supported by this tool");
157
158
            default:
159
                throw new RuntimeException("Unknown table option: $option");
160
        }
161
    }
162
163
    /**
164
     * @param string $engine
165
     * @return string
166
     */
167 84
    private static function normaliseEngine($engine)
168
    {
169 84
        $engine = strtoupper($engine);
170
        switch ($engine) {
171 84
            case 'INNODB':
172 84
                return 'InnoDB';
173
            case 'MYISAM':
174
                return 'MyISAM';
175
            default:
176
                return $engine;
177
        }
178
    }
179
180
    /**
181
     * @param string $option
182
     * @param string $value
183
     */
184
    private function setOption($option, $value)
185
    {
186
        switch ($option) {
187
            case 'ENGINE':
188
                $this->engine = self::normaliseEngine($value);
189
                break;
190
191
            case 'CHARSET':
192
                if (strtoupper($value) === 'DEFAULT') {
193
                    $this->collation = new CollationInfo();
194
                } else {
195
                    $this->collation->setCharset($value);
196
                }
197
                break;
198
199
            case 'COLLATE':
200
                if (strtoupper($value) === 'DEFAULT') {
201
                    $this->collation = new CollationInfo();
202
                } else {
203
                    $this->collation->setCollation($value);
204
                }
205
                break;
206
207
            default:
208
                $this->options[$option] = $value;
209
                break;
210
        }
211
    }
212
213
    /**
214
     * @param TokenStream $stream
215
     * @param string $option
216
     */
217
    private function parseIdentifier(TokenStream $stream, $option)
218
    {
219
        $stream->consume([[Token::SYMBOL, '=']]);
220
        $token = $stream->nextToken();
221
        if ($token->isEof()) {
222
            throw new RuntimeException("Unexpected end-of-file");
223
        }
224
        if (!in_array($token->type, [Token::IDENTIFIER, Token::STRING])) {
225
            throw new RuntimeException("Bad table option value: '$token->text'");
226
        }
227
        $this->setOption($option, strtolower($token->text));
228
    }
229
230
    /**
231
     * @param TokenStream $stream
232
     * @param string $option
233
     */
234
    private function parseNumber(TokenStream $stream, $option)
235
    {
236
        $stream->consume([[Token::SYMBOL, '=']]);
237
        $this->setOption($option, $stream->expectNumber());
238
    }
239
240
    /**
241
     * @param TokenStream $stream
242
     * @param string $option
243
     * @param array $enums
244
     */
245
    private function parseEnum(TokenStream $stream, $option, array $enums)
246
    {
247
        $stream->consume([[Token::SYMBOL, '=']]);
248
        $token = $stream->nextToken();
249
        if (!in_array($token->type, [Token::IDENTIFIER, Token::NUMBER])) {
250
            throw new RuntimeException("Bad table option value");
251
        }
252
        $value = strtoupper($token->text);
253
        if (!in_array($value, $enums)) {
254
            throw new RuntimeException("Invalid option value, expected " . implode(' | ', $enums));
255
        }
256
        $this->setOption($option, $value);
257
    }
258
259
    /**
260
     * @param TokenStream $stream
261
     * @param string $option
262
     */
263
    private function parseString(TokenStream $stream, $option)
264
    {
265
        $stream->consume([[Token::SYMBOL, '=']]);
266
        $this->setOption($option, $stream->expectString());
267
    }
268
269
    /**
270
     * Returns an SQL fragment to set the options as part of a CREATE TABLE statement.
271
     * Note that the AUTO_INCREMENT option is explicitly *not* included in the output.
272
     */
273 53
    public function toString()
274
    {
275 53
        $items = [];
276
277 53
        $items[] = "ENGINE=" . $this->engine;
278
279
        // (omit AUTO_INCREMENT)
280
281 53
        $collation = $this->collation;
282 53
        if ($collation->isSpecified()) {
283
            $items[] = "DEFAULT CHARSET=" . $collation->getCharset();
284
            if (!$collation->isDefaultCollation()) {
285
                $items[] = "COLLATE=" . $collation->getCollation();
286
            }
287
        }
288
289
        foreach ([
290 53
            'MIN_ROWS',
291
            'MAX_ROWS',
292
            'AVG_ROW_LENGTH',
293
            'PACK_KEYS',
294
            'CHECKSUM',
295
            'DELAY_KEY_WRITE',
296
            'ROW_FORMAT',
297
            'KEY_BLOCK_SIZE',
298
            'COMMENT',
299
            'CONNECTION',
300
        ] as $option) {
301 53
            if ($this->options[$option] !== $this->defaultOptions[$option]) {
302
                $value = $this->options[$option];
303
                if (in_array($option, ['COMMENT', 'CONNECTION'])) {
304
                    $value = Token::escapeString($value);
305
                }
306 53
                $items[] = "$option=$value";
307
            }
308
        }
309
310 53
        return implode(' ', $items);
311
    }
312
313
    /**
314
     * Returns an SQL fragment to transform these table options into those
315
     * specified by $that as part of an ALTER TABLE statement.
316
     *
317
     * The empty string is returned if nothing needs to be done.
318
     *
319
     * $flags           |
320
     * :----------------|
321
     * 'alterEngine'    | (bool) include 'ALTER TABLE ... ENGINE=' [default: true]
322
     *
323
     * @param TableOptions $that
324
     * @param array $flags
325
     * @return string
326
     */
327 13
    public function diff(TableOptions $that, array $flags = [])
328
    {
329
        $flags += [
330 13
            'alterEngine' => true,
331
        ];
332
333 13
        $alters = [];
334 13
        if ($flags['alterEngine']) {
335 13
            if (strcasecmp($this->engine, $that->engine) !== 0) {
336
                $alters[] = "ENGINE=" . $that->engine;
337
            }
338
        }
339
340 13
        $thisCollation = $this->collation->isSpecified()
341
            ? $this->collation->getCollation()
342 13
            : null;
343 13
        $thatCollation = $that->collation->isSpecified()
344
            ? $that->collation->getCollation()
345 13
            : null;
346 13
        if ($thisCollation !== $thatCollation) {
347
            // TODO - what if !$that->collation->isSpecified()
348
            if (!is_null($thatCollation)) {
349
                $alters[] = "DEFAULT CHARSET=" . $that->collation->getCharset();
350
                if (!$that->collation->isDefaultCollation()) {
351
                    $alters[] = "COLLATE=" . $thatCollation;
352
                }
353
            }
354
        }
355
356
        foreach ([
357 13
            'MIN_ROWS',
358
            'MAX_ROWS',
359
            'AVG_ROW_LENGTH',
360
            'PACK_KEYS',
361
            'CHECKSUM',
362
            'DELAY_KEY_WRITE',
363
364
            // The storage engine may pick a different row format when
365
            // ROW_FORMAT=DEFAULT (or no ROW_FORMAT)/ is specified, depending
366
            // on whether any variable length columns are present. Since we
367
            // don't (currently) explicitly specify ROW_FORMAT in any of our
368
            // tables, I'm choosing to ignore it for the time being...
369
        //  'ROW_FORMAT',
370
            'KEY_BLOCK_SIZE',
371
            'COMMENT',
372
            'CONNECTION',
373
        ] as $option) {
374 13
            $thisValue = $this->options[$option];
375 13
            $thatValue = $that->options[$option];
376 13
            if (in_array($option, ['COMMENT', 'CONNECTION'])) {
377 13
                $thisValue = Token::escapeString($thisValue);
378 13
                $thatValue = Token::escapeString($thatValue);
379
            }
380
381 13
            if ($thisValue !== $thatValue) {
382 13
                $alters[] = "$option=$thatValue";
383
            }
384
        }
385
386 13
        return implode(' ', $alters);
387
    }
388
}
389