Passed
Branch master (6554fc)
by Wilmer
30:37 queued 16:38
created

CommandPDO   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 185
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 28
eloc 67
dl 0
loc 185
ccs 69
cts 69
cp 1
rs 10
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
B internalExecute() 0 25 9
A execute() 0 27 5
A extractUsedParams() 0 16 4
A splitStatements() 0 23 5
A queryBuilder() 0 3 1
A queryInternal() 0 36 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite;
6
7
use PDOException;
8
use Throwable;
9
use Yiisoft\Db\Driver\PDO\CommandPDO as AbstractCommandPDO;
10
use Yiisoft\Db\Driver\PDO\ConnectionPDOInterface;
11
use Yiisoft\Db\Exception\ConvertException;
12
use Yiisoft\Db\Exception\Exception;
13
use Yiisoft\Db\Exception\InvalidArgumentException;
14
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;
15
use Yiisoft\Strings\StringHelper;
16
17
use function array_pop;
18
use function count;
19
use function ltrim;
20
use function preg_match_all;
21
use function strpos;
22
23
final class CommandPDO extends AbstractCommandPDO
24
{
25 227
    public function queryBuilder(): QueryBuilderInterface
26
    {
27 227
        return $this->db->getQueryBuilder();
28
    }
29
30
    /**
31
     * Executes the SQL statement.
32
     *
33
     * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs.
34
     * No result set will be returned.
35
     *
36
     * @throws Exception|Throwable execution failed.
37
     *
38
     * @return int number of rows affected by the execution.
39
     */
40 77
    public function execute(): int
41
    {
42 77
        $sql = $this->getSql();
43
44
        /** @psalm-var array<string, string> $params */
45 77
        $params = $this->params;
46
47 77
        $statements = $this->splitStatements($sql, $params);
48
49 77
        if ($statements === false) {
0 ignored issues
show
introduced by
The condition $statements === false is always true.
Loading history...
50 72
            return parent::execute();
51
        }
52
53 5
        $result = 0;
54
55
        /** @psalm-var array<array-key, array<array-key, string|array>> $statements */
56 5
        foreach ($statements as $statement) {
57 5
            [$statementSql, $statementParams] = $statement;
58 5
            $statementSql = is_string($statementSql) ? $statementSql : '';
59 5
            $statementParams = is_array($statementParams) ? $statementParams : [];
60 5
            $this->setSql($statementSql)->bindValues($statementParams);
61 5
            $result = parent::execute();
62
        }
63
64 5
        $this->setSql($sql)->bindValues($params);
65
66 5
        return $result;
67
    }
68
69
    /**
70
     * @psalm-suppress UnusedClosureParam
71
     *
72
     * @throws Throwable
73
     */
74 204
    protected function internalExecute(string|null $rawSql): void
75
    {
76 204
        $attempt = 0;
77
78 204
        while (true) {
79
            try {
80
                if (
81
                    ++$attempt === 1
82 204
                    && $this->isolationLevel !== null
83 204
                    && $this->db->getTransaction() === null
84
                ) {
85 1
                    $this->db->transaction(
86 1
                        fn (ConnectionPDOInterface $db) => $this->internalExecute($rawSql),
0 ignored issues
show
Unused Code introduced by
The parameter $db is not used and could be removed. ( Ignorable by Annotation )

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

86
                        fn (/** @scrutinizer ignore-unused */ ConnectionPDOInterface $db) => $this->internalExecute($rawSql),

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
87 1
                        $this->isolationLevel,
88
                    );
89
                } else {
90 204
                    $this->pdoStatement?->execute();
91
                }
92 204
                break;
93 2
            } catch (PDOException $e) {
94 2
                $rawSql = $rawSql ?: $this->getRawSql();
95 2
                $e = (new ConvertException($e, $rawSql))->run();
96
97 2
                if ($this->retryHandler === null || !($this->retryHandler)($e, $attempt)) {
98 2
                    throw $e;
99
                }
100
            }
101
        }
102
    }
103
104
    /**
105
     * Performs the actual DB query of a SQL statement.
106
     *
107
     * @param int $queryMode - return results as DataReader
108
     *
109
     * @throws Exception|Throwable if the query causes any problem.
110
     *
111
     * @return mixed the method execution result.
112
     */
113 204
    protected function queryInternal(int $queryMode): mixed
114
    {
115 204
        $sql = $this->getSql();
116
117
        /** @psalm-var array<string, string> $params */
118 204
        $params = $this->params;
119
120 204
        $statements = $this->splitStatements($sql, $params);
121
122 204
        if ($statements === false || $statements === []) {
0 ignored issues
show
introduced by
The condition $statements === false is always true.
Loading history...
123 204
            return parent::queryInternal($queryMode);
124
        }
125
126 1
        [$lastStatementSql, $lastStatementParams] = array_pop($statements);
127
128
        /**
129
         * @psalm-var array<array-key, array> $statements
130
         */
131 1
        foreach ($statements as $statement) {
132
            /**
133
             * @var string $statementSql
134
             * @var array $statementParams
135
             */
136 1
            [$statementSql, $statementParams] = $statement;
137 1
            $this->setSql($statementSql)->bindValues($statementParams);
138 1
            parent::execute();
139
        }
140
141 1
        $this->setSql($lastStatementSql)->bindValues($lastStatementParams);
142
143
        /** @psalm-var string $result */
144 1
        $result = parent::queryInternal($queryMode);
145
146 1
        $this->setSql($sql)->bindValues($params);
147
148 1
        return $result;
149
    }
150
151
    /**
152
     * Splits the specified SQL codes into individual SQL statements and returns them or `false` if there's a single
153
     * statement.
154
     *
155
     * @throws InvalidArgumentException
156
     *
157
     * @return array|bool (array|string)[][]|bool
158
     *
159
     * @psalm-param array<string, string> $params
160
     * @psalm-return false|list<array{0: string, 1: array}>
161
     */
162 205
    private function splitStatements(string $sql, array $params): bool|array
163
    {
164 205
        $semicolonIndex = strpos($sql, ';');
165
166 205
        if ($semicolonIndex === false || $semicolonIndex === StringHelper::byteLength($sql) - 1) {
167 205
            return false;
168
        }
169
170 6
        $tokenizer = new SqlTokenizer($sql);
171
172 6
        $codeToken = $tokenizer->tokenize();
173
174 6
        if (count($codeToken->getChildren()) === 1) {
175 1
            return false;
176
        }
177
178 5
        $statements = [];
179
180 5
        foreach ($codeToken->getChildren() as $statement) {
181 5
            $statements[] = [$statement->getSql(), $this->extractUsedParams($statement, $params)];
182
        }
183
184 5
        return $statements;
185
    }
186
187
    /**
188
     * Returns named bindings used in the specified statement token.
189
     *
190
     * @psalm-param array<string, string> $params
191
     */
192 5
    private function extractUsedParams(SqlToken $statement, array $params): array
193
    {
194 5
        preg_match_all('/(?P<placeholder>[:][a-zA-Z0-9_]+)/', $statement->getSql(), $matches, PREG_SET_ORDER);
195
196 5
        $result = [];
197
198 5
        foreach ($matches as $match) {
199 5
            $phName = ltrim($match['placeholder'], ':');
200 5
            if (isset($params[$phName])) {
201 1
                $result[$phName] = $params[$phName];
202 4
            } elseif (isset($params[':' . $phName])) {
203 4
                $result[':' . $phName] = $params[':' . $phName];
204
            }
205
        }
206
207 5
        return $result;
208
    }
209
}
210