Passed
Pull Request — dev (#107)
by Def
06:03 queued 01:55
created

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