Passed
Push — dev ( c872e6...d79752 )
by Def
24:43 queued 20:57
created

CommandPDOSqlite::prepare()   B

Complexity

Conditions 7
Paths 13

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 17
c 1
b 0
f 0
dl 0
loc 30
ccs 16
cts 16
cp 1
rs 8.8333
cc 7
nc 13
nop 1
crap 7

1 Method

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