Passed
Push — master ( a7a57c...9e99de )
by William
03:11 queued 12s
created

ExplainStatement::build()   B

Complexity

Conditions 11
Paths 108

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 11

Importance

Changes 0
Metric Value
cc 11
eloc 19
nc 108
nop 0
dl 0
loc 35
ccs 20
cts 20
cp 1
crap 11
rs 7.25
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\SqlParser\Statements;
6
7
use PhpMyAdmin\SqlParser\Components\OptionsArray;
8
use PhpMyAdmin\SqlParser\Context;
9
use PhpMyAdmin\SqlParser\Parser;
10
use PhpMyAdmin\SqlParser\Statement;
11
use PhpMyAdmin\SqlParser\Token;
12
use PhpMyAdmin\SqlParser\TokensList;
13
14
use function array_slice;
15
use function count;
16
17
/**
18
 * `EXPLAIN` statement.
19
 */
20
class ExplainStatement extends Statement
21
{
22
    /**
23
     * Options for `EXPLAIN` statements.
24
     *
25
     * @var array<string, int|array<int, int|string>>
26
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
27
     */
28
    public static $OPTIONS = [
29
30
        'EXTENDED' => 1,
31
        'PARTITIONS' => 1,
32
        'FORMAT' => [
33
            1,
34
            'var',
35
        ],
36
    ];
37
38
    /**
39
     * The parser of the statement to be explained
40
     *
41
     * @var Parser|null
42
     */
43
    public $bodyParser = null;
44
45
    /**
46
     * The statement alias, could be any of the following:
47
     * - {EXPLAIN | DESCRIBE | DESC}
48
     * - {EXPLAIN | DESCRIBE | DESC} ANALYZE
49
     * - ANALYZE
50
     *
51
     * @var string
52
     */
53
    public $statementAlias;
54
55
    /**
56
     * The connection identifier, if used.
57
     *
58
     * @var int|null
59
     */
60
    public $connectionId = null;
61
62
    /**
63
     * The explained database for the table's name, if used.
64
     *
65
     * @var string|null
66
     */
67
    public $explainedDatabase = null;
68
69
    /**
70
     * The explained table's name, if used.
71
     *
72
     * @var string|null
73
     */
74
    public $explainedTable = null;
75
76
    /**
77
     * The explained column's name, if used.
78
     *
79
     * @var string|null
80
     */
81
    public $explainedColumn = null;
82
83
    /**
84
     * @param Parser     $parser the instance that requests parsing
85
     * @param TokensList $list   the list of tokens to be parsed
86
     */
87 44
    public function parse(Parser $parser, TokensList $list)
88
    {
89
        /**
90
         * The state of the parser.
91
         *
92
         * Below are the states of the parser.
93
         *
94
         *      0 -------------------[ EXPLAIN/EXPLAIN ANALYZE/ANALYZE ]-----------------------> 1
95
         *
96
         *      0 ------------------------[ EXPLAIN/DESC/DESCRIBE ]----------------------------> 3
97
         *
98
         *      1 ------------------------------[ OPTIONS ]------------------------------------> 2
99
         *
100
         *      2 --------------[ tablename / STATEMENT / FOR CONNECTION ]---------------------> 2
101
         *
102
         *      3 -----------------------------[ tablename ]-----------------------------------> 3
103
         *
104
         * @var int
105
         */
106 44
        $state = 0;
107
108
        /**
109
         * To Differentiate between ANALYZE / EXPLAIN / EXPLAIN ANALYZE
110
         * 0 -> ANALYZE ( used by mariaDB https://mariadb.com/kb/en/analyze-statement)
111
         * 1 -> {EXPLAIN | DESCRIBE | DESC}
112
         * 2 -> {EXPLAIN | DESCRIBE | DESC} ANALYZE
113
         */
114 44
        $miniState = 0;
115
116 44
        for (; $list->idx < $list->count; ++$list->idx) {
117
            /**
118
             * Token parsed at this moment.
119
             */
120 44
            $token = $list->tokens[$list->idx];
121
122
            // End of statement.
123 44
            if ($token->type === Token::TYPE_DELIMITER) {
124 20
                --$list->idx; // Back up one token, no real reasons to document
125 20
                break;
126
            }
127
128
            // Skipping whitespaces and comments.
129 44
            if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) {
130 38
                continue;
131
            }
132
133 44
            if ($state === 0) {
134 44
                if ($token->keyword === 'ANALYZE' && $miniState === 0) {
135 10
                    $state = 1;
136 10
                    $this->statementAlias = 'ANALYZE';
137
                } elseif (
138 36
                    $token->keyword === 'EXPLAIN'
139 20
                    || $token->keyword === 'DESC'
140 36
                    || $token->keyword === 'DESCRIBE'
141
                ) {
142 36
                    $this->statementAlias = $token->keyword;
143
144 36
                    $lastIdx = $list->idx;
145 36
                    $list->idx++; // Ignore the current token
146 36
                    $nextKeyword = $list->getNextOfType(Token::TYPE_KEYWORD);
147 36
                    $list->idx = $lastIdx;
148
149
                    // There is no other keyword, we must be describing a table
150 36
                    if ($nextKeyword === null) {
151 18
                        $state = 3;
152 18
                        continue;
153
                    }
154
155 20
                    $miniState = 1;
156
157 20
                    $lastIdx = $list->idx;
158 20
                    $nextKeyword = $list->getNextOfTypeAndValue(Token::TYPE_KEYWORD, 'ANALYZE');
159 20
                    if ($nextKeyword && $nextKeyword->keyword !== null) {
160 6
                        $miniState = 2;
161 6
                        $this->statementAlias .= ' ANALYZE';
162
                    } else {
163 16
                        $list->idx = $lastIdx;
164
                    }
165
166 28
                    $state = 1;
167
                }
168 40
            } elseif ($state === 1) {
169
                // Parsing options.
170 26
                $this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
171 26
                $state = 2;
172 40
            } elseif ($state === 2) {
173 26
                $currIdx = $list->idx;
174 26
                $list->idx++; // Ignore the current token
175 26
                $nextToken = $list->getNext();
176 26
                $list->idx = $currIdx;
177
178 26
                if ($token->keyword === 'FOR' && $nextToken->keyword === 'CONNECTION') {
179 2
                    $list->idx++; // Ignore the current token
180 2
                    $list->getNext(); // CONNECTION
181 2
                    $nextToken = $list->getNext(); // Identifier
182 2
                    $this->connectionId = $nextToken->value;
183 2
                    break;
184
                }
185
186
                if (
187 26
                    $token->keyword !== 'SELECT'
188 26
                    && $token->keyword !== 'TABLE'
189 26
                    && $token->keyword !== 'INSERT'
190 26
                    && $token->keyword !== 'REPLACE'
191 26
                    && $token->keyword !== 'UPDATE'
192 26
                    && $token->keyword !== 'DELETE'
193
                ) {
194 6
                    $parser->error('Unexpected token.', $token);
195 6
                    break;
196
                }
197
198
                // Index of the last parsed token by default would be the last token in the $list, because we're
199
                // assuming that all remaining tokens at state 2, are related to the to-be-explained statement.
200 20
                $idxOfLastParsedToken = $list->count - 1;
201 20
                $subList = new TokensList(array_slice($list->tokens, $list->idx));
202
203 20
                $this->bodyParser = new Parser($subList);
204 20
                if (count($this->bodyParser->errors)) {
205 4
                    foreach ($this->bodyParser->errors as $error) {
206 4
                        $parser->errors[] = $error;
207
                    }
208
209 4
                    break;
210
                }
211
212 16
                $list->idx = $idxOfLastParsedToken;
213 16
                break;
214 16
            } elseif ($state === 3) {
215 16
                if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '.')) {
216 4
                    continue;
217
                }
218
219 16
                if ($this->explainedDatabase === null) {
220 16
                    $lastIdx = $list->idx;
221 16
                    $nextDot = $list->getNextOfTypeAndValue(Token::TYPE_OPERATOR, '.');
222 16
                    $list->idx = $lastIdx;
223 16
                    if ($nextDot !== null) {// We found a dot, so it must be a db.table name format
224 4
                        $this->explainedDatabase = $token->value;
225 4
                        continue;
226
                    }
227
                }
228
229 16
                if ($this->explainedTable === null) {
230 16
                    $this->explainedTable = $token->value;
231 16
                    continue;
232
                }
233
234 10
                if ($this->explainedColumn === null) {
235 10
                    $this->explainedColumn = $token->value;
236
                }
237
            }
238
        }
239
240 44
        if ($state !== 3 || $this->explainedTable !== null) {
0 ignored issues
show
introduced by
The condition $state !== 3 is always true.
Loading history...
241 42
            return;
242
        }
243
244
        // We reached end of the state 3 and no table name was found
245
        /** Token parsed at this moment. */
246 2
        $token = $list->tokens[$list->idx];
247 2
        $parser->error('Expected a table name.', $token);
248
    }
249
250 2
    public function build(): string
251
    {
252 2
        $str = $this->statementAlias;
253
254 2
        if ($this->options !== null) {
255 2
            if (count($this->options->options)) {
256 2
                $str .= ' ';
257
            }
258
259 2
            $str .= OptionsArray::build($this->options) . ' ';
260
        }
261
262 2
        if ($this->options === null) {
263 2
            $str .= ' ';
264
        }
265
266 2
        if ($this->bodyParser) {
267 2
            foreach ($this->bodyParser->statements as $statement) {
268 2
                $str .= $statement->build();
269
            }
270 2
        } elseif ($this->connectionId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->connectionId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
271 2
            $str .= 'FOR CONNECTION ' . $this->connectionId;
272
        }
273
274 2
        if ($this->explainedDatabase !== null && $this->explainedTable !== null) {
275 2
            $str .= Context::escape($this->explainedDatabase) . '.' . Context::escape($this->explainedTable);
276 2
        } elseif ($this->explainedTable !== null) {
277 2
            $str .= Context::escape($this->explainedTable);
278
        }
279
280 2
        if ($this->explainedColumn !== null) {
281 2
            $str .= ' ' . Context::escape($this->explainedColumn);
282
        }
283
284 2
        return $str;
285
    }
286
}
287