Passed
Push — master ( 6b91b6...1f7653 )
by William
07:14
created

ExplainStatement   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 187
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 29
eloc 78
c 1
b 1
f 0
dl 0
loc 187
ccs 69
cts 69
cp 1
rs 10

2 Methods

Rating   Name   Duplication   Size   Complexity  
D parse() 0 109 23
A build() 0 21 6
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\Parser;
9
use PhpMyAdmin\SqlParser\Statement;
10
use PhpMyAdmin\SqlParser\Token;
11
use PhpMyAdmin\SqlParser\TokensList;
12
13
use function array_slice;
14
use function count;
15
16
/**
17
 * `EXPLAIN` statement.
18
 */
19
class ExplainStatement extends Statement
20
{
21
    /**
22
     * Options for `EXPLAIN` statements.
23
     *
24
     * @var array<string, int|array<int, int|string>>
25
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
26
     */
27
    public static $OPTIONS = [
28
29
        'EXTENDED' => 1,
30
        'PARTITIONS' => 1,
31
        'FORMAT' => [
32
            1,
33
            'var',
34
        ],
35
    ];
36
37
    /**
38
     * The parser of the statement to be explained
39
     *
40
     * @var Parser|null
41
     */
42
    public $bodyParser = null;
43
44
    /**
45
     * The statement alias, could be any of the following:
46
     * - {EXPLAIN | DESCRIBE | DESC}
47
     * - {EXPLAIN | DESCRIBE | DESC} ANALYZE
48
     * - ANALYZE
49
     *
50
     * @var string
51
     */
52
    public $statemenetAlias;
53
54
    /**
55
     * The connection identifier, if used.
56
     *
57
     * @var int|null
58
     */
59
    public $connectionId = null;
60
61
    /**
62
     * The explained table's name, if used.
63
     *
64
     * @var string|null
65
     */
66
    public $explainedTable = null;
67
68
    /**
69
     * @param Parser     $parser the instance that requests parsing
70
     * @param TokensList $list   the list of tokens to be parsed
71
     */
72 48
    public function parse(Parser $parser, TokensList $list)
73
    {
74
        /**
75
         * The state of the parser.
76
         *
77
         * Below are the states of the parser.
78
         *
79
         *      0 -------------------[ EXPLAIN/EXPLAIN ANALYZE/ANALYZE ]-----------------------> 1
80
         *
81
         *      1 ------------------------------[ OPTIONS ]------------------------------------> 2
82
         *
83
         *      2 --------------[ tablename / STATEMENT / FOR CONNECTION ]---------------------> 2
84
         *
85
         * @var int
86
         */
87 48
        $state = 0;
88
89
        /**
90
         * To Differentiate between ANALYZE / EXPLAIN / EXPLAIN ANALYZE
91
         * 0 -> ANALYZE ( used by mariaDB https://mariadb.com/kb/en/analyze-statement)
92
         * 1 -> {EXPLAIN | DESCRIBE | DESC}
93
         * 2 -> {EXPLAIN | DESCRIBE | DESC} ANALYZE
94
         */
95 48
        $miniState = 0;
96
97 48
        for (; $list->idx < $list->count; ++$list->idx) {
98
            /**
99
             * Token parsed at this moment.
100
             */
101 48
            $token = $list->tokens[$list->idx];
102
103
            // Skipping whitespaces and comments.
104 48
            if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) {
105 40
                continue;
106
            }
107
108 48
            if ($state === 0) {
109 48
                if ($token->keyword === 'ANALYZE' && $miniState === 0) {
110 12
                    $state = 1;
111 12
                    $this->statemenetAlias = 'ANALYZE';
112
                } elseif (
113 40
                    $token->keyword === 'EXPLAIN'
114 12
                    || $token->keyword === 'DESC'
115 40
                    || $token->keyword === 'DESCRIBE'
116
                ) {
117 40
                    $miniState = 1;
118 40
                    $this->statemenetAlias = $token->keyword;
119
120 40
                    $lastIdx = $list->idx;
121 40
                    $nextKeyword = $list->getNextOfTypeAndValue(Token::TYPE_KEYWORD, 'ANALYZE');
122 40
                    if ($nextKeyword && $nextKeyword->keyword !== null) {
123 12
                        $miniState = 2;
124 12
                        $this->statemenetAlias .= ' ANALYZE';
125
                    } else {
126 32
                        $list->idx = $lastIdx;
127
                    }
128
129 48
                    $state = 1;
130
                }
131 48
            } elseif ($state === 1) {
132
                // Parsing options.
133 48
                $this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
134 48
                $state = 2;
135 48
            } elseif ($state === 2) {
136 48
                $currIdx = $list->idx;
137 48
                $currToken = $list->getNext();
0 ignored issues
show
Unused Code introduced by
The assignment to $currToken is dead and can be removed.
Loading history...
138 48
                $nextToken = $list->getNext();
139 48
                $list->idx = $currIdx;
140
141 48
                if ($token->keyword === 'FOR' && $nextToken->keyword === 'CONNECTION') {
142 4
                    $forToken = $list->getNext(); // FOR
0 ignored issues
show
Unused Code introduced by
The assignment to $forToken is dead and can be removed.
Loading history...
143 4
                    $connectionToken = $list->getNext(); // CONNECTION
0 ignored issues
show
Unused Code introduced by
The assignment to $connectionToken is dead and can be removed.
Loading history...
144 4
                    $nextToken = $list->getNext(); // Identifier
145 4
                    $this->connectionId = $nextToken->value;
146 4
                    break;
147
                }
148
149
                // To support EXPLAIN tablename
150 48
                if ($token->type === Token::TYPE_NONE) {
151 16
                    $this->explainedTable = $token->value;
152 16
                    break;
153
                }
154
155
                if (
156 36
                    $token->keyword !== 'SELECT'
157 36
                    && $token->keyword !== 'INSERT'
158 36
                    && $token->keyword !== 'UPDATE'
159 36
                    && $token->keyword !== 'DELETE'
160
                ) {
161 12
                    $parser->error('Unexpected token.', $token);
162 12
                    break;
163
                }
164
165
                // Index of the last parsed token by default would be the last token in the $list, because we're
166
                // assuming that all remaining tokens at state 2, are related to the to-be-explained statement.
167 24
                $idxOfLastParsedToken = $list->count - 1;
168 24
                $subList = new TokensList(array_slice($list->tokens, $list->idx));
169
170 24
                $this->bodyParser = new Parser($subList);
171 24
                if (count($this->bodyParser->errors)) {
172 4
                    foreach ($this->bodyParser->errors as $error) {
173 4
                        $parser->errors[] = $error;
174
                    }
175
176 4
                    break;
177
                }
178
179 20
                $list->idx = $idxOfLastParsedToken;
180 20
                break;
181
            }
182
        }
183 12
    }
184
185 4
    public function build(): string
186
    {
187 4
        $str = $this->statemenetAlias;
188
189 4
        if (count($this->options->options)) {
190 4
            $str .= ' ';
191
        }
192
193 4
        $str .= OptionsArray::build($this->options) . ' ';
194
195 4
        if ($this->bodyParser) {
196 4
            foreach ($this->bodyParser->statements as $statement) {
197 4
                $str .= $statement->build();
198
            }
199 4
        } 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...
200 4
            $str .= 'FOR CONNECTION ' . $this->connectionId;
201 4
        } elseif ($this->explainedTable) {
202 4
            $str .= $this->explainedTable;
203
        }
204
205 4
        return $str;
206
    }
207
}
208