Completed
Pull Request — dev (#89)
by Def
13:35 queued 13:35
created

CommandPDOSqlite::queryBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 1
b 0
f 0
cc 1
nc 1
nop 0
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\CommandPDO;
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 CommandPDO
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 151
    protected function getCacheKey(int $queryMode, string $rawSql): array
141
    {
142
        return [
143 151
            __CLASS__,
144
            $queryMode,
145 151
            $this->db->getDriver()->getDsn(),
146 151
            $this->db->getDriver()->getUsername(),
147
            $rawSql,
148
        ];
149
    }
150
151 158
    protected function internalExecute(?string $rawSql): void
152
    {
153 158
        $attempt = 0;
154
155 158
        while (true) {
156
            try {
157
                if (
158 158
                    ++$attempt === 1
159 158
                    && $this->isolationLevel !== null
160 158
                    && $this->db->getTransaction() === null
161
                ) {
162
                    $this->db->transaction(fn (?string $rawSql) => $this->internalExecute($rawSql), $this->isolationLevel);
163
                } else {
164 158
                    $this->pdoStatement?->execute();
165
                }
166 158
                break;
167 2
            } catch (PDOException $e) {
168 2
                $rawSql = $rawSql ?: $this->getRawSql();
169 2
                $e = (new ConvertException($e, $rawSql))->run();
170
171 2
                if ($this->retryHandler === null || !($this->retryHandler)($e, $attempt)) {
172 2
                    throw $e;
173
                }
174
            }
175
        }
176
    }
177
178
    /**
179
     * Performs the actual DB query of a SQL statement.
180
     *
181
     * @param int $queryMode - return results as DataReader
182
     *
183
     * @throws Exception|Throwable if the query causes any problem.
184
     *
185
     * @return mixed the method execution result.
186
     */
187 158
    protected function queryInternal(int $queryMode): mixed
188
    {
189 158
        $sql = $this->getSql();
190
191
        /** @var array<string, string> */
192 158
        $params = $this->params;
193
194 158
        $statements = $this->splitStatements($sql, $params);
195
196 158
        if ($statements === false || $statements === []) {
0 ignored issues
show
introduced by
The condition $statements === false is always true.
Loading history...
197 158
            return parent::queryInternal($queryMode);
198
        }
199
200 1
        [$lastStatementSql, $lastStatementParams] = array_pop($statements);
201
202
        /**
203
         * @psalm-var array<array-key, array> $statements
204
         */
205 1
        foreach ($statements as $statement) {
206
            /**
207
             * @var string $statementSql
208
             * @var array $statementParams
209
             */
210 1
            [$statementSql, $statementParams] = $statement;
211 1
            $this->setSql($statementSql)->bindValues($statementParams);
212 1
            parent::execute();
213
        }
214
215 1
        $this->setSql($lastStatementSql)->bindValues($lastStatementParams);
216
217
        /** @var string */
218 1
        $result = parent::queryInternal($queryMode);
219
220 1
        $this->setSql($sql)->bindValues($params);
221
222 1
        return $result;
223
    }
224
225
    /**
226
     * Splits the specified SQL codes into individual SQL statements and returns them or `false` if there's a single
227
     * statement.
228
     *
229
     * @param string $sql
230
     * @param array $params
231
     *
232
     * @throws InvalidArgumentException
233
     *
234
     * @return array|bool (array|string)[][]|bool
235
     *
236
     * @psalm-param array<string, string> $params
237
     * @psalm-return false|list<array{0: string, 1: array}>
238
     */
239 158
    private function splitStatements(string $sql, array $params): bool|array
240
    {
241 158
        $semicolonIndex = strpos($sql, ';');
242
243 158
        if ($semicolonIndex === false || $semicolonIndex === StringHelper::byteLength($sql) - 1) {
244 158
            return false;
245
        }
246
247 5
        $tokenizer = new SqlTokenizer($sql);
248
249 5
        $codeToken = $tokenizer->tokenize();
250
251 5
        if (count($codeToken->getChildren()) === 1) {
252
            return false;
253
        }
254
255 5
        $statements = [];
256
257 5
        foreach ($codeToken->getChildren() as $statement) {
258 5
            $statements[] = [$statement->getSql(), $this->extractUsedParams($statement, $params)];
259
        }
260
261 5
        return $statements;
262
    }
263
264
    /**
265
     * Returns named bindings used in the specified statement token.
266
     *
267
     * @param SqlToken $statement
268
     * @param array $params
269
     *
270
     * @return array
271
     *
272
     * @psalm-param array<string, string> $params
273
     */
274 5
    private function extractUsedParams(SqlToken $statement, array $params): array
275
    {
276 5
        preg_match_all('/(?P<placeholder>[:][a-zA-Z0-9_]+)/', $statement->getSql(), $matches, PREG_SET_ORDER);
277
278 5
        $result = [];
279
280 5
        foreach ($matches as $match) {
281 5
            $phName = ltrim($match['placeholder'], ':');
282 5
            if (isset($params[$phName])) {
283 1
                $result[$phName] = $params[$phName];
284 4
            } elseif (isset($params[':' . $phName])) {
285 4
                $result[':' . $phName] = $params[':' . $phName];
286
            }
287
        }
288
289 5
        return $result;
290
    }
291
}
292