Passed
Branch dev (914540)
by Wilmer
06:24
created

CommandPDOSqlite::getCacheKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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