Passed
Branch dev (ea35c5)
by Wilmer
17:29 queued 12:43
created

CommandPDOSqlite::execute()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 14
dl 0
loc 27
ccs 15
cts 15
cp 1
rs 9.4888
c 1
b 0
f 0
cc 5
nc 6
nop 0
crap 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite\PDO;
6
7
use PDOException;
8
use Throwable;
9
use Yiisoft\Db\Cache\QueryCache;
10
use Yiisoft\Db\Command\Command;
11
use Yiisoft\Db\Connection\ConnectionPDOInterface;
12
use Yiisoft\Db\Exception\ConvertException;
13
use Yiisoft\Db\Exception\Exception;
14
use Yiisoft\Db\Exception\InvalidArgumentException;
15
use Yiisoft\Db\Query\QueryBuilderInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Query\QueryBuilderInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use Yiisoft\Db\Sqlite\SqlToken;
17
use Yiisoft\Db\Sqlite\SqlTokenizer;
18
use Yiisoft\Strings\StringHelper;
19
20
use function array_pop;
21
use function count;
22
use function ltrim;
23
use function preg_match_all;
24
use function strpos;
25
26
final class CommandPDOSqlite extends Command
27
{
28 323
    public function __construct(private ConnectionPDOInterface $db, QueryCache $queryCache)
29
    {
30 323
        parent::__construct($queryCache);
31
    }
32
33 172
    public function queryBuilder(): QueryBuilderInterface
34
    {
35 172
        return $this->db->getQueryBuilder();
36
    }
37
38 158
    public function prepare(?bool $forRead = null): void
39
    {
40 158
        if (isset($this->pdoStatement)) {
41 4
            $this->bindPendingParams();
42
43 4
            return;
44
        }
45
46 158
        $sql = $this->getSql() ?? '';
47
48 158
        if ($this->db->getTransaction()) {
49
            /** master is in a transaction. use the same connection. */
50 5
            $forRead = false;
51
        }
52
53 158
        if ($forRead || ($forRead === null && $this->db->getSchema()->isReadQuery($sql))) {
54 153
            $pdo = $this->db->getSlavePdo();
55
        } else {
56 47
            $pdo = $this->db->getMasterPdo();
57
        }
58
59
        try {
60 158
            $this->pdoStatement = $pdo?->prepare($sql);
61 158
            $this->bindPendingParams();
62 4
        } catch (PDOException $e) {
63 4
            $message = $e->getMessage() . "\nFailed to prepare SQL: $sql";
64
            /** @var array|null */
65 4
            $errorInfo = $e->errorInfo ?? null;
66
67 4
            throw new Exception($message, $errorInfo, $e);
68
        }
69
    }
70
71
    /**
72
     * Executes the SQL statement.
73
     *
74
     * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs.
75
     * No result set will be returned.
76
     *
77
     * @throws Exception|Throwable execution failed.
78
     *
79
     * @return int number of rows affected by the execution.
80
     */
81 46
    public function execute(): int
82
    {
83 46
        $sql = $this->getSql() ?? '';
84
85
        /** @var array<string, string> */
86 46
        $params = $this->params;
87
88 46
        $statements = $this->splitStatements($sql, $params);
89
90 46
        if ($statements === false) {
0 ignored issues
show
introduced by
The condition $statements === false is always true.
Loading history...
91 41
            return parent::execute();
92
        }
93
94 5
        $result = 0;
95
96
        /** @psalm-var array<array-key, array<array-key, string|array>> $statements */
97 5
        foreach ($statements as $statement) {
98 5
            [$statementSql, $statementParams] = $statement;
99 5
            $statementSql = is_string($statementSql) ? $statementSql : '';
100 5
            $statementParams = is_array($statementParams) ? $statementParams : [];
101 5
            $this->setSql($statementSql)->bindValues($statementParams);
102 5
            $result = parent::execute();
103
        }
104
105 5
        $this->setSql($sql)->bindValues($params);
106
107 5
        return $result;
108
    }
109
110 2
    protected function getCacheKey(string $method, ?int $fetchMode, string $rawSql): array
111
    {
112
        return [
113
            __CLASS__,
114
            $method,
115
            $fetchMode,
116 2
            $this->db->getDriver()->getDsn(),
117 2
            $this->db->getDriver()->getUsername(),
118
            $rawSql,
119
        ];
120
    }
121
122 157
    protected function internalExecute(?string $rawSql): void
123
    {
124 157
        $attempt = 0;
125
126 157
        while (true) {
127
            try {
128
                if (
129 157
                    ++$attempt === 1
130 157
                    && $this->isolationLevel !== null
131 157
                    && $this->db->getTransaction() === null
132
                ) {
133
                    $this->db->transaction(fn (?string $rawSql) => $this->internalExecute($rawSql), $this->isolationLevel);
134
                } else {
135 157
                    $this->pdoStatement?->execute();
136
                }
137 157
                break;
138 2
            } catch (PDOException $e) {
139 2
                $rawSql = $rawSql ?: $this->getRawSql();
140 2
                $e = (new ConvertException($e, $rawSql))->run();
141
142 2
                if ($this->retryHandler === null || !($this->retryHandler)($e, $attempt)) {
143 2
                    throw $e;
144
                }
145
            }
146
        }
147
    }
148
149
    /**
150
     * Performs the actual DB query of a SQL statement.
151
     *
152
     * @param string $method method of PDOStatement to be called
153
     * @param array|int|null $fetchMode the result fetch mode.
154
     * Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) for valid fetch
155
     * modes. If this parameter is null, the value set in {@see fetchMode} will be used.
156
     *
157
     * @throws Exception|Throwable if the query causes any problem.
158
     *
159
     * @return mixed the method execution result.
160
     */
161 153
    protected function queryInternal(string $method, array|int $fetchMode = null): mixed
162
    {
163 153
        $sql = $this->getSql() ?? '';
164
165
        /** @var array<string, string> */
166 153
        $params = $this->params;
167
168 153
        $statements = $this->splitStatements($sql, $params);
169
170 153
        if ($statements === false || $statements === []) {
0 ignored issues
show
introduced by
The condition $statements === false is always true.
Loading history...
171 153
            return parent::queryInternal($method, $fetchMode);
172
        }
173
174 1
        [$lastStatementSql, $lastStatementParams] = array_pop($statements);
175
176
        /**
177
         * @psalm-var array<array-key, array> $statements
178
         */
179 1
        foreach ($statements as $statement) {
180
            /**
181
             * @var string $statementSql
182
             * @var array $statementParams
183
             */
184 1
            [$statementSql, $statementParams] = $statement;
185 1
            $this->setSql($statementSql)->bindValues($statementParams);
186 1
            parent::execute();
187
        }
188
189 1
        $this->setSql($lastStatementSql)->bindValues($lastStatementParams);
190
191
        /** @var string */
192 1
        $result = parent::queryInternal($method, $fetchMode);
193
194 1
        $this->setSql($sql)->bindValues($params);
195
196 1
        return $result;
197
    }
198
199
    /**
200
     * Splits the specified SQL codes into individual SQL statements and returns them or `false` if there's a single
201
     * statement.
202
     *
203
     * @param string $sql
204
     * @param array $params
205
     *
206
     * @throws InvalidArgumentException
207
     *
208
     * @return array|bool (array|string)[][]|bool
209
     *
210
     * @psalm-param array<string, string> $params
211
     * @psalm-return false|list<array{0: string, 1: array}>
212
     */
213 157
    private function splitStatements(string $sql, array $params): bool|array
214
    {
215 157
        $semicolonIndex = strpos($sql, ';');
216
217 157
        if ($semicolonIndex === false || $semicolonIndex === StringHelper::byteLength($sql) - 1) {
218 157
            return false;
219
        }
220
221 5
        $tokenizer = new SqlTokenizer($sql);
222
223 5
        $codeToken = $tokenizer->tokenize();
224
225 5
        if (count($codeToken->getChildren()) === 1) {
226
            return false;
227
        }
228
229 5
        $statements = [];
230
231 5
        foreach ($codeToken->getChildren() as $statement) {
232 5
            $statements[] = [$statement->getSql(), $this->extractUsedParams($statement, $params)];
233
        }
234
235 5
        return $statements;
236
    }
237
238
    /**
239
     * Returns named bindings used in the specified statement token.
240
     *
241
     * @param SqlToken $statement
242
     * @param array $params
243
     *
244
     * @return array
245
     *
246
     * @psalm-param array<string, string> $params
247
     */
248 5
    private function extractUsedParams(SqlToken $statement, array $params): array
249
    {
250 5
        preg_match_all('/(?P<placeholder>[:][a-zA-Z0-9_]+)/', $statement->getSql(), $matches, PREG_SET_ORDER);
251
252 5
        $result = [];
253
254 5
        foreach ($matches as $match) {
255 5
            $phName = ltrim($match['placeholder'], ':');
256 5
            if (isset($params[$phName])) {
257 1
                $result[$phName] = $params[$phName];
258 4
            } elseif (isset($params[':' . $phName])) {
259 4
                $result[':' . $phName] = $params[':' . $phName];
260
            }
261
        }
262
263 5
        return $result;
264
    }
265
}
266