Passed
Branch master (c9bbe5)
by Wilmer
23:28 queued 14:50
created

CommandPDO::splitStatements()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 23
ccs 12
cts 12
cp 1
rs 9.6111
c 0
b 0
f 0
cc 5
nc 4
nop 2
crap 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\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\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 3
    public function insertWithReturningPks(string $table, array $columns): bool|array
26
    {
27 3
        $params = [];
28 3
        $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params);
29 3
        $this->setSql($sql)->bindValues($params);
30
31 3
        if (!$this->execute()) {
32
            return false;
33
        }
34
35 3
        $tableSchema = $this->db->getSchema()->getTableSchema($table);
36 3
        $tablePrimaryKeys = $tableSchema?->getPrimaryKey() ?? [];
37
38 3
        $result = [];
39 3
        foreach ($tablePrimaryKeys as $name) {
40 3
            if ($tableSchema?->getColumn($name)?->isAutoIncrement()) {
41 2
                $result[$name] = $this->db->getLastInsertID((string) $tableSchema?->getSequenceName());
42 2
                continue;
43
            }
44
45
            /** @psalm-var mixed */
46 1
            $result[$name] = $columns[$name] ?? $tableSchema?->getColumn($name)?->getDefaultValue();
47
        }
48
49 3
        return $result;
50
    }
51
52 266
    public function queryBuilder(): QueryBuilderInterface
53
    {
54 266
        return $this->db->getQueryBuilder();
55
    }
56
57
    /**
58
     * Executes the SQL statement.
59
     *
60
     * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs.
61
     * No result set will be returned.
62
     *
63
     * @throws Exception|Throwable execution failed.
64
     *
65
     * @return int number of rows affected by the execution.
66
     */
67 98
    public function execute(): int
68
    {
69 98
        $sql = $this->getSql();
70
71
        /** @psalm-var array<string, string> $params */
72 98
        $params = $this->params;
73
74 98
        $statements = $this->splitStatements($sql, $params);
75
76 98
        if ($statements === false) {
0 ignored issues
show
introduced by
The condition $statements === false is always true.
Loading history...
77 86
            return parent::execute();
78
        }
79
80 12
        $result = 0;
81
82
        /** @psalm-var array<array-key, array<array-key, string|array>> $statements */
83 12
        foreach ($statements as $statement) {
84 12
            [$statementSql, $statementParams] = $statement;
85 12
            $statementSql = is_string($statementSql) ? $statementSql : '';
86 12
            $statementParams = is_array($statementParams) ? $statementParams : [];
87 12
            $this->setSql($statementSql)->bindValues($statementParams);
88 12
            $result = parent::execute();
89
        }
90
91 12
        $this->setSql($sql)->bindValues($params);
92
93 12
        return $result;
94
    }
95
96
    /**
97
     * @psalm-suppress UnusedClosureParam
98
     *
99
     * @throws Throwable
100
     */
101 234
    protected function internalExecute(string|null $rawSql): void
102
    {
103 234
        $attempt = 0;
104
105 234
        while (true) {
106
            try {
107
                if (
108 234
                    ++$attempt === 1
109 234
                    && $this->isolationLevel !== null
110 234
                    && $this->db->getTransaction() === null
111
                ) {
112 1
                    $this->db->transaction(
113 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

113
                        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...
114 1
                        $this->isolationLevel,
115 1
                    );
116
                } else {
117 234
                    $this->pdoStatement?->execute();
118
                }
119 234
                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 234
    protected function queryInternal(int $queryMode): mixed
141
    {
142 234
        $sql = $this->getSql();
143
144
        /** @psalm-var array<string, string> $params */
145 234
        $params = $this->params;
146
147 234
        $statements = $this->splitStatements($sql, $params);
148
149 234
        if ($statements === false || $statements === []) {
0 ignored issues
show
introduced by
The condition $statements === false is always true.
Loading history...
150 234
            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
        /** @psalm-var string $result */
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
     * @throws InvalidArgumentException
183
     *
184
     * @return array|bool (array|string)[][]|bool
185
     *
186
     * @psalm-param array<string, string> $params
187
     * @psalm-return false|list<array{0: string, 1: array}>
188
     */
189 235
    private function splitStatements(string $sql, array $params): bool|array
190
    {
191 235
        $semicolonIndex = strpos($sql, ';');
192
193 235
        if ($semicolonIndex === false || $semicolonIndex === StringHelper::byteLength($sql) - 1) {
194 235
            return false;
195
        }
196
197 13
        $tokenizer = new SqlTokenizer($sql);
198
199 13
        $codeToken = $tokenizer->tokenize();
200
201 13
        if (count($codeToken->getChildren()) === 1) {
202 1
            return false;
203
        }
204
205 12
        $statements = [];
206
207 12
        foreach ($codeToken->getChildren() as $statement) {
208 12
            $statements[] = [$statement->getSql(), $this->extractUsedParams($statement, $params)];
209
        }
210
211 12
        return $statements;
212
    }
213
214
    /**
215
     * Returns named bindings used in the specified statement token.
216
     *
217
     * @psalm-param array<string, string> $params
218
     */
219 12
    private function extractUsedParams(SqlToken $statement, array $params): array
220
    {
221 12
        preg_match_all('/(?P<placeholder>[:][a-zA-Z0-9_]+)/', $statement->getSql(), $matches, PREG_SET_ORDER);
222
223 12
        $result = [];
224
225 12
        foreach ($matches as $match) {
226 12
            $phName = ltrim($match['placeholder'], ':');
227 12
            if (isset($params[$phName])) {
228 1
                $result[$phName] = $params[$phName];
229 11
            } elseif (isset($params[':' . $phName])) {
230 11
                $result[':' . $phName] = $params[':' . $phName];
231
            }
232
        }
233
234 12
        return $result;
235
    }
236
}
237