Passed
Pull Request — master (#265)
by Sergei
21:17 queued 16:59
created

Command   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 228
Duplicated Lines 0 %

Test Coverage

Coverage 98.91%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 33
eloc 86
c 3
b 1
f 0
dl 0
loc 228
ccs 91
cts 92
cp 0.9891
rs 9.76

8 Methods

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