Passed
Push — main ( 4889b2...808ce0 )
by Michiel
07:43
created

PgsqlPDOQuerySplitter   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 266
Duplicated Lines 0 %

Test Coverage

Coverage 86.67%

Importance

Changes 0
Metric Value
wmc 56
eloc 137
dl 0
loc 266
ccs 104
cts 120
cp 0.8667
rs 5.5199
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
F nextQuery() 0 137 42
A getc() 0 12 4
A ungetc() 0 3 1
B checkDollarQuote() 0 33 9

How to fix   Complexity   

Complex Class

Complex classes like PgsqlPDOQuerySplitter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PgsqlPDOQuerySplitter, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 *
19
 */
20
21
namespace Phing\Task\System\Pdo;
22
23
/**
24
 * Splits PostgreSQL's dialect of SQL into separate queries
25
 *
26
 * Unlike DefaultPDOQuerySplitter this uses a lexer instead of regular
27
 * expressions. This allows handling complex constructs like C-style comments
28
 * (including nested ones) and dollar-quoted strings.
29
 *
30
 * @author  Alexey Borzov <[email protected]>
31
 * @link    http://www.phing.info/trac/ticket/499
32
 * @link    http://www.postgresql.org/docs/current/interactive/sql-syntax-lexical.html#SQL-SYNTAX-DOLLAR-QUOTING
33
 */
34
class PgsqlPDOQuerySplitter extends PDOQuerySplitter
35
{
36
    /**#@+
37
     * Lexer states
38
     */
39
    public const STATE_NORMAL = 0;
40
    public const STATE_SINGLE_QUOTED = 1;
41
    public const STATE_DOUBLE_QUOTED = 2;
42
    public const STATE_DOLLAR_QUOTED = 3;
43
    public const STATE_COMMENT_LINEEND = 4;
44
    public const STATE_COMMENT_MULTILINE = 5;
45
    public const STATE_BACKSLASH = 6;
46
    /**#@-*/
47
48
    /**
49
     * Nesting depth of current multiline comment
50
     *
51
     * @var int
52
     */
53
    protected $commentDepth = 0;
54
55
    /**
56
     * Current dollar-quoting "tag"
57
     *
58
     * @var string
59
     */
60
    protected $quotingTag = '';
61
62
    /**
63
     * Current lexer state, one of STATE_* constants
64
     *
65
     * @var int
66
     */
67
    protected $state = self::STATE_NORMAL;
68
69
    /**
70
     * Whether a backslash was just encountered in quoted string
71
     *
72
     * @var bool
73
     */
74
    protected $escape = false;
75
76
    /**
77
     * Current source line being examined
78
     *
79
     * @var string
80
     */
81
    protected $line = '';
82
83
    /**
84
     * Position in current source line
85
     *
86
     * @var int
87
     */
88
    protected $inputIndex;
89
90
    /**
91
     * Gets next symbol from the input, false if at end
92
     *
93
     * @return string|bool
94
     */
95 1
    public function getc()
96
    {
97 1
        if (!strlen($this->line) || $this->inputIndex >= strlen($this->line)) {
98 1
            if (null === ($line = $this->sqlReader->readLine())) {
99 1
                return false;
100
            }
101 1
            $project = $this->parent->getOwningTarget()->getProject();
102 1
            $this->line = $project->replaceProperties($line) . "\n";
103 1
            $this->inputIndex = 0;
104
        }
105
106 1
        return $this->line[$this->inputIndex++];
107
    }
108
109
    /**
110
     * Bactracks one symbol on the input
111
     *
112
     * NB: we don't need ungetc() at the start of the line, so this case is
113
     * not handled.
114
     */
115 1
    public function ungetc()
116
    {
117 1
        $this->inputIndex--;
118 1
    }
119
120
    /**
121
     * Checks whether symbols after $ are a valid dollar-quoting tag
122
     *
123
     * @return string|bool Dollar-quoting "tag" if it is present, false otherwise
124
     */
125 1
    protected function checkDollarQuote()
126
    {
127 1
        $ch = $this->getc();
128 1
        if ('$' === $ch) {
129
            // empty tag
130 1
            return '';
131
        }
132
133 1
        if (!ctype_alpha($ch) && '_' !== $ch) {
134
            // not a delimiter
135 1
            $this->ungetc();
136
137 1
            return false;
138
        }
139
140 1
        $tag = $ch;
141 1
        while (false !== ($ch = $this->getc())) {
142 1
            if ('$' === $ch) {
143 1
                return $tag;
144
            }
145
146 1
            if (ctype_alnum($ch) || '_' === $ch) {
147 1
                $tag .= $ch;
148
            } else {
149
                for ($i = 0, $tagLength = strlen($tag); $i < $tagLength; $i++) {
0 ignored issues
show
Bug introduced by
It seems like $tag can also be of type boolean; however, parameter $string of strlen() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

149
                for ($i = 0, $tagLength = strlen(/** @scrutinizer ignore-type */ $tag); $i < $tagLength; $i++) {
Loading history...
150
                    $this->ungetc();
151
                }
152
153
                return false;
154
            }
155
        }
156
157
        return $tag;
158
    }
159
160
    /**
161
     * @return null|string
162
     */
163 1
    public function nextQuery()
164
    {
165 1
        $sql = '';
166 1
        $delimiter = $this->parent->getDelimiter();
167 1
        $openParens = 0;
168
169 1
        while (false !== ($ch = $this->getc())) {
170 1
            switch ($this->state) {
171 1
                case self::STATE_NORMAL:
172
                    switch ($ch) {
173 1
                        case '-':
174 1
                            if ('-' == $this->getc()) {
175 1
                                $this->state = self::STATE_COMMENT_LINEEND;
176
                            } else {
177
                                $this->ungetc();
178
                            }
179 1
                            break;
180 1
                        case '"':
181 1
                            $this->state = self::STATE_DOUBLE_QUOTED;
182 1
                            break;
183 1
                        case "'":
184 1
                            $this->state = self::STATE_SINGLE_QUOTED;
185 1
                            break;
186 1
                        case '/':
187 1
                            if ('*' === $this->getc()) {
188 1
                                $this->state = self::STATE_COMMENT_MULTILINE;
189 1
                                $this->commentDepth = 1;
190
                            } else {
191 1
                                $this->ungetc();
192
                            }
193 1
                            break;
194 1
                        case '$':
195 1
                            if (false !== ($tag = $this->checkDollarQuote())) {
196 1
                                $this->state = self::STATE_DOLLAR_QUOTED;
197 1
                                $this->quotingTag = $tag;
0 ignored issues
show
Documentation Bug introduced by
It seems like $tag can also be of type true. However, the property $quotingTag is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
198 1
                                $sql .= '$' . $tag . '$';
0 ignored issues
show
Bug introduced by
Are you sure $tag of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

198
                                $sql .= '$' . /** @scrutinizer ignore-type */ $tag . '$';
Loading history...
199 1
                                continue 3;
200
                            }
201
                            break;
202 1
                        case '(':
203 1
                            $openParens++;
204 1
                            break;
205 1
                        case ')':
206 1
                            $openParens--;
207 1
                            break;
208
                        // technically we can use e.g. psql's \g command as delimiter
209 1
                        case $delimiter[0]:
210
                            // special case to allow "create rule" statements
211
                            // http://www.postgresql.org/docs/current/interactive/sql-createrule.html
212 1
                            if (';' === $delimiter && 0 < $openParens) {
213 1
                                break;
214
                            }
215 1
                            $hasQuery = true;
216 1
                            for ($i = 1, $delimiterLength = strlen($delimiter); $i < $delimiterLength; $i++) {
217
                                if ($delimiter[$i] != $this->getc()) {
218
                                    $hasQuery = false;
219
                                }
220
                            }
221 1
                            if ($hasQuery) {
222 1
                                return $sql;
223
                            }
224
225
                            for ($j = 1; $j < $i; $j++) {
226
                                $this->ungetc();
227
                            }
228
                    }
229 1
                    break;
230
231 1
                case self::STATE_COMMENT_LINEEND:
232 1
                    if ("\n" === $ch) {
233 1
                        $this->state = self::STATE_NORMAL;
234
                    }
235 1
                    break;
236
237 1
                case self::STATE_COMMENT_MULTILINE:
238 1
                    switch ($ch) {
239 1
                        case '/':
240 1
                            if ('*' != $this->getc()) {
241
                                $this->ungetc();
242
                            } else {
243 1
                                $this->commentDepth++;
244
                            }
245 1
                            break;
246
247 1
                        case '*':
248 1
                            if ('/' != $this->getc()) {
249
                                $this->ungetc();
250
                            } else {
251 1
                                $this->commentDepth--;
252 1
                                if (0 == $this->commentDepth) {
253 1
                                    $this->state = self::STATE_NORMAL;
254 1
                                    continue 3;
255
                                }
256
                            }
257
                    }
258
259
                // no break
260 1
                case self::STATE_SINGLE_QUOTED:
261 1
                case self::STATE_DOUBLE_QUOTED:
262 1
                    if ($this->escape) {
263
                        $this->escape = false;
264
                        break;
265
                    }
266 1
                    $quote = $this->state == self::STATE_SINGLE_QUOTED ? "'" : '"';
267
                    switch ($ch) {
268 1
                        case '\\':
269
                            $this->escape = true;
270
                            break;
271 1
                        case $quote:
272 1
                            if ($quote == $this->getc()) {
273 1
                                $sql .= $quote;
274
                            } else {
275 1
                                $this->ungetc();
276 1
                                $this->state = self::STATE_NORMAL;
277
                            }
278
                    }
279
280
                // no break
281 1
                case self::STATE_DOLLAR_QUOTED:
282 1
                    if ('$' == $ch && false !== ($tag = $this->checkDollarQuote())) {
283 1
                        if ($tag == $this->quotingTag) {
284 1
                            $this->state = self::STATE_NORMAL;
285
                        }
286 1
                        $sql .= '$' . $tag . '$';
287 1
                        continue 2;
288
                    }
289
            }
290
291 1
            if ($this->state != self::STATE_COMMENT_LINEEND && $this->state != self::STATE_COMMENT_MULTILINE) {
292 1
                $sql .= $ch;
293
            }
294
        }
295 1
        if ('' !== $sql) {
296 1
            return $sql;
297
        }
298
299 1
        return null;
300
    }
301
}
302