Passed
Pull Request — master (#245)
by Wilmer
04:02
created

Command::queryInternal()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 36
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 36
ccs 15
cts 15
cp 1
rs 9.7998
c 0
b 0
f 0
cc 4
nc 3
nop 1
crap 4
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\Driver\PDO\AbstractCommandPDO;
10
use Yiisoft\Db\Driver\PDO\ConnectionPDOInterface;
11
use Yiisoft\Db\Exception\ConvertException;
12
use Yiisoft\Db\Exception\Exception;
13
use Yiisoft\Db\Exception\InvalidArgumentException;
14
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;
15
use Yiisoft\Db\Sqlite\SqlToken;
16
use Yiisoft\Db\Sqlite\SqlTokenizer;
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
/**
25
 * Implements a database command that can be executed with a PDO (PHP Data Object) database connection for SQLite
26
 * Server.
27
 */
28
final class Command extends AbstractCommandPDO
29
{
30 4
    public function insertWithReturningPks(string $table, array $columns): bool|array
31
    {
32 4
        $params = [];
33 4
        $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params);
34 4
        $this->setSql($sql)->bindValues($params);
35
36 4
        if (!$this->execute()) {
37
            return false;
38
        }
39
40 4
        $tableSchema = $this->db->getSchema()->getTableSchema($table);
41 4
        $tablePrimaryKeys = $tableSchema?->getPrimaryKey() ?? [];
42
43 4
        $result = [];
44 4
        foreach ($tablePrimaryKeys as $name) {
45 4
            if ($tableSchema?->getColumn($name)?->isAutoIncrement()) {
46 3
                $result[$name] = $this->db->getLastInsertID((string) $tableSchema?->getSequenceName());
47 3
                continue;
48
            }
49
50
            /** @psalm-var mixed */
51 1
            $result[$name] = $columns[$name] ?? $tableSchema?->getColumn($name)?->getDefaultValue();
52
        }
53
54 4
        return $result;
55
    }
56
57 1
    public function showDatabases(): array
58
    {
59 1
        $sql = <<<SQL
60
        SELECT name FROM pragma_database_list;
61 1
        SQL;
62
63 1
        return $this->setSql($sql)->queryColumn();
64
    }
65
66 281
    protected function getQueryBuilder(): QueryBuilderInterface
67
    {
68 281
        return $this->db->getQueryBuilder();
69
    }
70
71
    /**
72
     * Executes the SQL statement.
73
     *
74
     * This method should only be used for executing a non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs.
75
     * No result set will be returned.
76
     *
77
     * @throws Exception
78
     * @throws Throwable The execution failed.
79
     *
80
     * @return int Number of rows affected by the execution.
81
     */
82 109
    public function execute(): int
83
    {
84 109
        $sql = $this->getSql();
85
86
        /** @psalm-var array<string, string> $params */
87 109
        $params = $this->params;
88
89 109
        $statements = $this->splitStatements($sql, $params);
90
91 109
        if ($statements === false) {
0 ignored issues
show
introduced by
The condition $statements === false is always true.
Loading history...
92 97
            return parent::execute();
93
        }
94
95 12
        $result = 0;
96
97
        /** @psalm-var array<array-key, array<array-key, string|array>> $statements */
98 12
        foreach ($statements as $statement) {
99 12
            [$statementSql, $statementParams] = $statement;
100 12
            $statementSql = is_string($statementSql) ? $statementSql : '';
101 12
            $statementParams = is_array($statementParams) ? $statementParams : [];
102 12
            $this->setSql($statementSql)->bindValues($statementParams);
103 12
            $result = parent::execute();
104
        }
105
106 12
        $this->setSql($sql)->bindValues($params);
107
108 12
        return $result;
109
    }
110
111
    /**
112
     * @psalm-suppress UnusedClosureParam
113
     *
114
     * @throws Throwable
115
     */
116 248
    protected function internalExecute(string|null $rawSql): void
117
    {
118 248
        $attempt = 0;
119
120 248
        while (true) {
121
            try {
122
                if (
123 248
                    ++$attempt === 1
124 248
                    && $this->isolationLevel !== null
125 248
                    && $this->db->getTransaction() === null
126
                ) {
127 1
                    $this->db->transaction(
128 1
                        fn (ConnectionPDOInterface $db) => $this->internalExecute($rawSql),
0 ignored issues
show
Unused Code introduced by
The parameter $db is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

128
                        fn (/** @scrutinizer ignore-unused */ ConnectionPDOInterface $db) => $this->internalExecute($rawSql),

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
129 1
                        $this->isolationLevel,
130 1
                    );
131
                } else {
132 248
                    $this->pdoStatement?->execute();
133
                }
134 248
                break;
135 2
            } catch (PDOException $e) {
136 2
                $rawSql = $rawSql ?: $this->getRawSql();
137 2
                $e = (new ConvertException($e, $rawSql))->run();
138
139 2
                if ($this->retryHandler === null || !($this->retryHandler)($e, $attempt)) {
140 2
                    throw $e;
141
                }
142
            }
143
        }
144
    }
145
146
    /**
147
     * Performs the actual DB query of an SQL statement.
148
     *
149
     * @param int $queryMode Return results as DataReader
150
     *
151
     * @throws Exception
152
     * @throws Throwable If the query causes any problem.
153
     *
154
     * @return mixed The method execution result.
155
     */
156 248
    protected function queryInternal(int $queryMode): mixed
157
    {
158 248
        $sql = $this->getSql();
159
160
        /** @psalm-var array<string, string> $params */
161 248
        $params = $this->params;
162
163 248
        $statements = $this->splitStatements($sql, $params);
164
165 248
        if ($statements === false || $statements === []) {
0 ignored issues
show
introduced by
The condition $statements === false is always true.
Loading history...
166 248
            return parent::queryInternal($queryMode);
167
        }
168
169 1
        [$lastStatementSql, $lastStatementParams] = array_pop($statements);
170
171
        /**
172
         * @psalm-var array<array-key, array> $statements
173
         */
174 1
        foreach ($statements as $statement) {
175
            /**
176
             * @psalm-var string $statementSql
177
             * @psalm-var array $statementParams
178
             */
179 1
            [$statementSql, $statementParams] = $statement;
180 1
            $this->setSql($statementSql)->bindValues($statementParams);
181 1
            parent::execute();
182
        }
183
184 1
        $this->setSql($lastStatementSql)->bindValues($lastStatementParams);
185
186
        /** @psalm-var string $result */
187 1
        $result = parent::queryInternal($queryMode);
188
189 1
        $this->setSql($sql)->bindValues($params);
190
191 1
        return $result;
192
    }
193
194
    /**
195
     * Splits the specified SQL into individual SQL statements and returns them or `false` if there's a single
196
     * statement.
197
     *
198
     * @param string $sql SQL to split.
199
     *
200
     * @throws InvalidArgumentException
201
     *
202
     * @return array|bool List of SQL statements or `false` if there's a single statement.
203
     *
204
     * @psalm-param array<string, string> $params
205
     *
206
     * @psalm-return false|list<array{0: string, 1: array}>
207
     */
208 249
    private function splitStatements(string $sql, array $params): bool|array
209
    {
210 249
        $semicolonIndex = strpos($sql, ';');
211
212 249
        if ($semicolonIndex === false || $semicolonIndex === mb_strlen($sql, '8bit') - 1) {
213 249
            return false;
214
        }
215
216 13
        $tokenizer = new SqlTokenizer($sql);
217
218 13
        $codeToken = $tokenizer->tokenize();
219
220 13
        if (count($codeToken->getChildren()) === 1) {
221 1
            return false;
222
        }
223
224 12
        $statements = [];
225
226 12
        foreach ($codeToken->getChildren() as $statement) {
227 12
            $statements[] = [$statement->getSql(), $this->extractUsedParams($statement, $params)];
228
        }
229
230 12
        return $statements;
231
    }
232
233
    /**
234
     * Returns named bindings used in the specified statement token.
235
     *
236
     * @psalm-param array<string, string> $params
237
     */
238 12
    private function extractUsedParams(SqlToken $statement, array $params): array
239
    {
240 12
        preg_match_all('/(?P<placeholder>:\w+)/', $statement->getSql(), $matches, PREG_SET_ORDER);
241
242 12
        $result = [];
243
244 12
        foreach ($matches as $match) {
245 12
            $phName = ltrim($match['placeholder'], ':');
246 12
            if (isset($params[$phName])) {
247 1
                $result[$phName] = $params[$phName];
248 11
            } elseif (isset($params[':' . $phName])) {
249 11
                $result[':' . $phName] = $params[':' . $phName];
250
            }
251
        }
252
253 12
        return $result;
254
    }
255
}
256