Passed
Branch master (deb69d)
by Wilmer
14:01 queued 01:23
created

CommandPDO::internalExecute()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 9.0294

Importance

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