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
 * @package phing.tasks.ext.pdo
20
 */
21

22
/**
23
 * Splits PostgreSQL's dialect of SQL into separate queries
24
 *
25
 * Unlike DefaultPDOQuerySplitter this uses a lexer instead of regular
26
 * expressions. This allows handling complex constructs like C-style comments
27
 * (including nested ones) and dollar-quoted strings.
28
 *
29
 * @author  Alexey Borzov <avb@php.net>
30
 * @package phing.tasks.ext.pdo
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
    }
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 0
                for ($i = 0, $tagLength = strlen($tag); $i < $tagLength; $i++) {
150 0
                    $this->ungetc();
151
                }
152

153 0
                return false;
154
            }
155
        }
156
    }
157

158
    /**
159
     * @return null|string
160
     */
161 1
    public function nextQuery()
162
    {
163 1
        $sql = '';
164 1
        $delimiter = $this->parent->getDelimiter();
165 1
        $openParens = 0;
166

167 1
        while (false !== ($ch = $this->getc())) {
168 1
            switch ($this->state) {
169
                case self::STATE_NORMAL:
170
                    switch ($ch) {
171 1
                        case '-':
172 1
                            if ('-' == $this->getc()) {
173 1
                                $this->state = self::STATE_COMMENT_LINEEND;
174
                            } else {
175 0
                                $this->ungetc();
176
                            }
177 1
                            break;
178 1
                        case '"':
179 1
                            $this->state = self::STATE_DOUBLE_QUOTED;
180 1
                            break;
181 1
                        case "'":
182 1
                            $this->state = self::STATE_SINGLE_QUOTED;
183 1
                            break;
184 1
                        case '/':
185 1
                            if ('*' == $this->getc()) {
186 1
                                $this->state = self::STATE_COMMENT_MULTILINE;
187 1
                                $this->commentDepth = 1;
188
                            } else {
189 1
                                $this->ungetc();
190
                            }
191 1
                            break;
192 1
                        case '$':
193 1
                            if (false !== ($tag = $this->checkDollarQuote())) {
194 1
                                $this->state = self::STATE_DOLLAR_QUOTED;
195 1
                                $this->quotingTag = $tag;
196 1
                                $sql .= '$' . $tag . '$';
197 1
                                continue 3;
198
                            }
199 0
                            break;
200 1
                        case '(':
201 1
                            $openParens++;
202 1
                            break;
203 1
                        case ')':
204 1
                            $openParens--;
205 1
                            break;
206
                        // technically we can use e.g. psql's \g command as delimiter
207 1
                        case $delimiter[0]:
208
                            // special case to allow "create rule" statements
209
                            // http://www.postgresql.org/docs/current/interactive/sql-createrule.html
210 1
                            if (';' == $delimiter && 0 < $openParens) {
211 1
                                break;
212
                            }
213 1
                            $hasQuery = true;
214 1
                            for ($i = 1, $delimiterLength = strlen($delimiter); $i < $delimiterLength; $i++) {
215 0
                                if ($delimiter[$i] != $this->getc()) {
216 0
                                    $hasQuery = false;
217
                                }
218
                            }
219 1
                            if ($hasQuery) {
220 1
                                return $sql;
221
                            }
222

223 0
                            for ($j = 1; $j < $i; $j++) {
224 0
                                $this->ungetc();
225
                            }
226
                    }
227 1
                    break;
228

229
                case self::STATE_COMMENT_LINEEND:
230 1
                    if ("\n" == $ch) {
231 1
                        $this->state = self::STATE_NORMAL;
232
                    }
233 1
                    break;
234

235
                case self::STATE_COMMENT_MULTILINE:
236 1
                    switch ($ch) {
237 1
                        case '/':
238 1
                            if ('*' != $this->getc()) {
239 0
                                $this->ungetc();
240
                            } else {
241 1
                                $this->commentDepth++;
242
                            }
243 1
                            break;
244

245 1
                        case '*':
246 1
                            if ('/' != $this->getc()) {
247 0
                                $this->ungetc();
248
                            } else {
249 1
                                $this->commentDepth--;
250 1
                                if (0 == $this->commentDepth) {
251 1
                                    $this->state = self::STATE_NORMAL;
252 1
                                    continue 3;
253
                                }
254
                            }
255
                    }
256

257
                // no break
258
                case self::STATE_SINGLE_QUOTED:
259
                case self::STATE_DOUBLE_QUOTED:
260 1
                    if ($this->escape) {
261 0
                        $this->escape = false;
262 0
                        break;
263
                    }
264 1
                    $quote = $this->state == self::STATE_SINGLE_QUOTED ? "'" : '"';
265
                    switch ($ch) {
266 1
                        case '\\':
267 0
                            $this->escape = true;
268 0
                            break;
269 1
                        case $quote:
270 1
                            if ($quote == $this->getc()) {
271 1
                                $sql .= $quote;
272
                            } else {
273 1
                                $this->ungetc();
274 1
                                $this->state = self::STATE_NORMAL;
275
                            }
276
                    }
277

278
                // no break
279
                case self::STATE_DOLLAR_QUOTED:
280 1
                    if ('$' == $ch && false !== ($tag = $this->checkDollarQuote())) {
281 1
                        if ($tag == $this->quotingTag) {
282 1
                            $this->state = self::STATE_NORMAL;
283
                        }
284 1
                        $sql .= '$' . $tag . '$';
285 1
                        continue 2;
286
                    }
287
            }
288

289 1
            if ($this->state != self::STATE_COMMENT_LINEEND && $this->state != self::STATE_COMMENT_MULTILINE) {
290 1
                $sql .= $ch;
291
            }
292
        }
293 1
        if ('' !== $sql) {
294 1
            return $sql;
295
        }
296

297 1
        return null;
298
    }
299
}

Read our documentation on viewing source code .

Loading