Completed
Pull Request — master (#3738)
by
unknown
61:48 queued 11s
created

OCI8Statement::columnCount()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 1
nop 0
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Driver\OCI8;
6
7
use Doctrine\DBAL\Driver\Statement;
8
use Doctrine\DBAL\Driver\StatementIterator;
9
use Doctrine\DBAL\Exception\InvalidColumnIndex;
10
use Doctrine\DBAL\FetchMode;
11
use Doctrine\DBAL\ParameterType;
12
use InvalidArgumentException;
13
use IteratorAggregate;
14
use const OCI_ASSOC;
15
use const OCI_B_BIN;
16
use const OCI_B_BLOB;
17
use const OCI_BOTH;
18
use const OCI_D_LOB;
19
use const OCI_FETCHSTATEMENT_BY_COLUMN;
20
use const OCI_FETCHSTATEMENT_BY_ROW;
21
use const OCI_NUM;
22
use const OCI_RETURN_LOBS;
23
use const OCI_RETURN_NULLS;
24
use const OCI_TEMP_BLOB;
25
use const PREG_OFFSET_CAPTURE;
26
use const SQLT_CHR;
27
use function array_key_exists;
28
use function assert;
29
use function count;
30
use function implode;
31
use function is_int;
32
use function is_resource;
33
use function oci_bind_by_name;
34
use function oci_cancel;
35
use function oci_error;
36
use function oci_execute;
37
use function oci_fetch_all;
38
use function oci_fetch_array;
39
use function oci_fetch_object;
40
use function oci_new_descriptor;
41
use function oci_num_fields;
42
use function oci_num_rows;
43
use function oci_parse;
44
use function preg_match;
45
use function preg_quote;
46
use function sprintf;
47
use function substr;
48
49
/**
50
 * The OCI8 implementation of the Statement interface.
51
 */
52
class OCI8Statement implements IteratorAggregate, Statement
53
{
54
    /** @var resource */
55
    protected $_dbh;
56
57
    /** @var resource */
58
    protected $_sth;
59
60
    /** @var OCI8Connection */
61
    protected $_conn;
62
63
    /** @var int[] */
64
    protected static $fetchModeMap = [
65
        FetchMode::MIXED       => OCI_BOTH,
66
        FetchMode::ASSOCIATIVE => OCI_ASSOC,
67
        FetchMode::NUMERIC     => OCI_NUM,
68
        FetchMode::COLUMN      => OCI_NUM,
69
    ];
70
71
    /** @var int */
72
    protected $_defaultFetchMode = FetchMode::MIXED;
73
74
    /** @var string[] */
75
    protected $_paramMap = [];
76
77
    /**
78
     * Holds references to bound parameter values.
79
     *
80
     * This is a new requirement for PHP7's oci8 extension that prevents bound values from being garbage collected.
81
     *
82
     * @var mixed[]
83
     */
84
    private $boundValues = [];
85
86
    /**
87
     * Indicates whether the statement is in the state when fetching results is possible
88
     *
89
     * @var bool
90
     */
91
    private $result = false;
92
93
    /**
94
     * @param resource $dbh The connection handle.
95
     *
96
     * @return resource
97
     */
98 356
    protected function prepareQuery($dbh, string $query)
99
    {
100 356
        $stmt = oci_parse($dbh, $query);
101 356
        assert(is_resource($stmt));
102
103 356
        return $stmt;
104
    }
105
106
    /**
107
     * Creates a new OCI8Statement that uses the given connection handle and SQL statement.
108
     *
109
     * @param resource $dbh   The connection handle.
110
     * @param string   $query The SQL query.
111
     */
112 356
    public function __construct($dbh, string $query, OCI8Connection $conn)
113
    {
114 356
        [$query, $paramMap] = self::convertPositionalToNamedPlaceholders($query);
115
116 356
        $this->_sth      = $this->prepareQuery($dbh, $query);
117 356
        $this->_dbh      = $dbh;
118 356
        $this->_paramMap = $paramMap;
119 356
        $this->_conn     = $conn;
120 356
    }
121
122
    /**
123
     * Converts positional (?) into named placeholders (:param<num>).
124
     *
125
     * Oracle does not support positional parameters, hence this method converts all
126
     * positional parameters into artificially named parameters. Note that this conversion
127
     * is not perfect. All question marks (?) in the original statement are treated as
128
     * placeholders and converted to a named parameter.
129
     *
130
     * The algorithm uses a state machine with two possible states: InLiteral and NotInLiteral.
131
     * Question marks inside literal strings are therefore handled correctly by this method.
132
     * This comes at a cost, the whole sql statement has to be looped over.
133
     *
134
     * @param string $statement The SQL statement to convert.
135
     *
136
     * @return mixed[] [0] => the statement value (string), [1] => the paramMap value (array).
137
     *
138
     * @throws OCI8Exception
139
     *
140
     * @todo extract into utility class in Doctrine\DBAL\Util namespace
141
     * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements.
142
     */
143 495
    public static function convertPositionalToNamedPlaceholders(string $statement) : array
144
    {
145 495
        $fragmentOffset          = $tokenOffset = 0;
146 495
        $fragments               = $paramMap = [];
147 495
        $currentLiteralDelimiter = null;
148
149
        do {
150 495
            if ($currentLiteralDelimiter === null) {
151 495
                $result = self::findPlaceholderOrOpeningQuote(
152 495
                    $statement,
153
                    $tokenOffset,
154
                    $fragmentOffset,
155
                    $fragments,
156
                    $currentLiteralDelimiter,
157
                    $paramMap
158
                );
159
            } else {
160 265
                $result = self::findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
0 ignored issues
show
Bug introduced by
$currentLiteralDelimiter of type null is incompatible with the type string expected by parameter $currentLiteralDelimiter of Doctrine\DBAL\Driver\OCI...ent::findClosingQuote(). ( Ignorable by Annotation )

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

160
                $result = self::findClosingQuote($statement, $tokenOffset, /** @scrutinizer ignore-type */ $currentLiteralDelimiter);
Loading history...
161
            }
162 495
        } while ($result);
163
164 495
        if ($currentLiteralDelimiter) {
165 3
            throw new OCI8Exception(sprintf(
166 3
                'The statement contains non-terminated string literal starting at offset %d.',
167 3
                $tokenOffset - 1
168
            ));
169
        }
170
171 492
        $fragments[] = substr($statement, $fragmentOffset);
172 492
        $statement   = implode('', $fragments);
173
174 492
        return [$statement, $paramMap];
175
    }
176
177
    /**
178
     * Finds next placeholder or opening quote.
179
     *
180
     * @param string      $statement               The SQL statement to parse
181
     * @param int         $tokenOffset             The offset to start searching from
182
     * @param int         $fragmentOffset          The offset to build the next fragment from
183
     * @param string[]    $fragments               Fragments of the original statement not containing placeholders
184
     * @param string|null $currentLiteralDelimiter The delimiter of the current string literal
185
     *                                             or NULL if not currently in a literal
186
     * @param string[]    $paramMap                Mapping of the original parameter positions to their named replacements
187
     *
188
     * @return bool Whether the token was found
189
     */
190 495
    private static function findPlaceholderOrOpeningQuote(
191
        string $statement,
192
        int &$tokenOffset,
193
        int &$fragmentOffset,
194
        array &$fragments,
195
        ?string &$currentLiteralDelimiter,
196
        array &$paramMap
197
    ) : bool {
198 495
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');
199
200 495
        if (! $token) {
201 492
            return false;
202
        }
203
204 411
        if ($token === '?') {
205 286
            $position            = count($paramMap) + 1;
206 286
            $param               = ':param' . $position;
207 286
            $fragments[]         = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
208 286
            $fragments[]         = $param;
209 286
            $paramMap[$position] = $param;
210 286
            $tokenOffset        += 1;
211 286
            $fragmentOffset      = $tokenOffset;
212
213 286
            return true;
214
        }
215
216 265
        $currentLiteralDelimiter = $token;
217 265
        ++$tokenOffset;
218
219 265
        return true;
220
    }
221
222
    /**
223
     * Finds closing quote
224
     *
225
     * @param string $statement               The SQL statement to parse
226
     * @param int    $tokenOffset             The offset to start searching from
227
     * @param string $currentLiteralDelimiter The delimiter of the current string literal
228
     *
229
     * @return bool Whether the token was found
230
     */
231 265
    private static function findClosingQuote(
232
        string $statement,
233
        int &$tokenOffset,
234
        string &$currentLiteralDelimiter
235
    ) : bool {
236 265
        $token = self::findToken(
237 181
            $statement,
238
            $tokenOffset,
239 265
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
240
        );
241
242 265
        if (! $token) {
243 3
            return false;
244
        }
245
246 263
        $currentLiteralDelimiter = null;
247 263
        ++$tokenOffset;
248
249 263
        return true;
250
    }
251
252
    /**
253
     * Finds the token described by regex starting from the given offset. Updates the offset with the position
254
     * where the token was found.
255
     *
256
     * @param string $statement The SQL statement to parse
257
     * @param int    $offset    The offset to start searching from
258
     * @param string $regex     The regex containing token pattern
259
     *
260
     * @return string|null Token or NULL if not found
261
     */
262 495
    private static function findToken(string $statement, int &$offset, string $regex) : ?string
263
    {
264 495
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset)) {
265 411
            $offset = $matches[0][1];
266
267 411
            return $matches[0][0];
268
        }
269
270 495
        return null;
271
    }
272
273
    /**
274
     * {@inheritdoc}
275
     */
276 131
    public function bindValue($param, $value, int $type = ParameterType::STRING) : void
277
    {
278 131
        $this->bindParam($param, $value, $type, null);
279 130
    }
280
281
    /**
282
     * {@inheritdoc}
283
     */
284 152
    public function bindParam($param, &$variable, int $type = ParameterType::STRING, ?int $length = null) : void
285
    {
286 152
        $param = $this->_paramMap[$param] ?? (string) $param;
287
288 152
        if ($type === ParameterType::LARGE_OBJECT) {
289 4
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
290
291 4
            $class = 'OCI-Lob';
292 4
            assert($lob instanceof $class);
293
294 4
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);
295
296 4
            $variable =& $lob;
297
        }
298
299 152
        $this->boundValues[$param] =& $variable;
300
301 152
        if (! oci_bind_by_name(
302 152
            $this->_sth,
303 152
            $param,
304 152
            $variable,
305 152
            $length ?? -1,
306 152
            $this->convertParameterType($type)
307
        )) {
308 1
            throw OCI8Exception::fromErrorInfo(oci_error($this->_sth));
309
        }
310 151
    }
311
312
    /**
313
     * Converts DBAL parameter type to oci8 parameter type
314
     */
315 152
    private function convertParameterType(int $type) : int
316
    {
317 152
        switch ($type) {
318
            case ParameterType::BINARY:
319 1
                return OCI_B_BIN;
320
321
            case ParameterType::LARGE_OBJECT:
322 4
                return OCI_B_BLOB;
323
324
            default:
325 150
                return SQLT_CHR;
326
        }
327
    }
328
329
    /**
330
     * {@inheritdoc}
331
     */
332 19
    public function closeCursor() : void
333
    {
334
        // not having the result means there's nothing to close
335 19
        if (! $this->result) {
336 3
            return;
337
        }
338
339 16
        oci_cancel($this->_sth);
340
341 16
        $this->result = false;
342 16
    }
343
344
    /**
345
     * {@inheritdoc}
346
     */
347 4
    public function columnCount() : int
348
    {
349 4
        return oci_num_fields($this->_sth) ?: 0;
350
    }
351
352
    /**
353
     * {@inheritdoc}
354
     */
355 357
    public function execute(?array $params = null) : void
356
    {
357 357
        if ($params) {
358 103
            $hasZeroIndex = array_key_exists(0, $params);
359
360 103
            foreach ($params as $key => $val) {
361 103
                if ($hasZeroIndex && is_int($key)) {
362 102
                    $param = $key + 1;
363
                } else {
364 1
                    $param = $key;
365
                }
366
367 103
                $this->bindValue($param, $val);
368
            }
369
        }
370
371 353
        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
372 353
        if (! $ret) {
373 150
            throw OCI8Exception::fromErrorInfo(oci_error($this->_sth));
374
        }
375
376 344
        $this->result = true;
377 344
    }
378
379
    /**
380
     * {@inheritdoc}
381
     */
382 319
    public function setFetchMode(int $fetchMode, ...$args) : void
383
    {
384 319
        $this->_defaultFetchMode = $fetchMode;
385 319
    }
386
387
    /**
388
     * {@inheritdoc}
389
     */
390 3
    public function getIterator()
391
    {
392 3
        return new StatementIterator($this);
393
    }
394
395
    /**
396
     * {@inheritdoc}
397
     */
398 125
    public function fetch(?int $fetchMode = null, ...$args)
399
    {
400
        // do not try fetching from the statement if it's not expected to contain result
401
        // in order to prevent exceptional situation
402 125
        if (! $this->result) {
403 3
            return false;
404
        }
405
406 122
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
407
408 122
        if ($fetchMode === FetchMode::COLUMN) {
409 1
            return $this->fetchColumn();
410
        }
411
412 121
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
413 1
            return oci_fetch_object($this->_sth);
414
        }
415
416 120
        if (! isset(self::$fetchModeMap[$fetchMode])) {
417
            throw new InvalidArgumentException(sprintf('Invalid fetch mode %d.', $fetchMode));
418
        }
419
420 120
        return oci_fetch_array(
421 120
            $this->_sth,
422 120
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
423
        );
424
    }
425
426
    /**
427
     * {@inheritdoc}
428
     */
429 109
    public function fetchAll(?int $fetchMode = null, ...$args) : array
430
    {
431 109
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
432
433 109
        $result = [];
434
435 109
        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
436 1
            while ($row = $this->fetch($fetchMode)) {
437 1
                $result[] = $row;
438
            }
439
440 1
            return $result;
441
        }
442
443 108
        if (! isset(self::$fetchModeMap[$fetchMode])) {
444
            throw new InvalidArgumentException(sprintf('Invalid fetch mode %d.', $fetchMode));
445
        }
446
447 108
        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
448 1
            while ($row = $this->fetch($fetchMode)) {
449 1
                $result[] = $row;
450
            }
451
        } else {
452 107
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;
453
454 107
            if ($fetchMode === FetchMode::COLUMN) {
455 7
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
456
            }
457
458
            // do not try fetching from the statement if it's not expected to contain result
459
            // in order to prevent exceptional situation
460 107
            if (! $this->result) {
461 3
                return [];
462
            }
463
464 104
            oci_fetch_all(
465 104
                $this->_sth,
466 104
                $result,
467 104
                0,
468 104
                -1,
469 104
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS
470
            );
471
472 104
            if ($fetchMode === FetchMode::COLUMN) {
473 7
                $result = $result[0];
474
            }
475
        }
476
477 105
        return $result;
478
    }
479
480
    /**
481
     * {@inheritdoc}
482
     */
483 184
    public function fetchColumn(int $columnIndex = 0)
484
    {
485
        // do not try fetching from the statement if it's not expected to contain result
486
        // in order to prevent exceptional situation
487 184
        if (! $this->result) {
488 3
            return false;
489
        }
490
491 181
        $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
492
493 181
        if ($row === false) {
494 3
            return false;
495
        }
496
497 178
        if (! array_key_exists($columnIndex, $row)) {
498 2
            throw InvalidColumnIndex::new($columnIndex, count($row));
499
        }
500
501 176
        return $row[$columnIndex];
502
    }
503
504
    /**
505
     * {@inheritdoc}
506
     */
507 174
    public function rowCount() : int
508
    {
509 174
        return oci_num_rows($this->_sth) ?: 0;
510
    }
511
}
512