Passed
Pull Request — master (#140)
by Wilmer
06:13 queued 02:15
created

CommandPDO::queryBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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\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
    /**
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 224
    public function queryBuilder(): QueryBuilderInterface
56
    {
57 224
        return $this->db->getQueryBuilder();
58
    }
59
60
    /**
61
     * Executes the SQL statement.
62
     *
63
     * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs.
64
     * No result set will be returned.
65
     *
66
     * @throws Exception|Throwable execution failed.
67
     *
68
     * @return int number of rows affected by the execution.
69
     */
70 78
    public function execute(): int
71
    {
72 78
        $sql = $this->getSql();
73
74
        /** @var array<string, string> */
75 78
        $params = $this->params;
76
77 78
        $statements = $this->splitStatements($sql, $params);
78
79 78
        if ($statements === false) {
0 ignored issues
show
introduced by
The condition $statements === false is always true.
Loading history...
80 73
            return parent::execute();
81
        }
82
83 5
        $result = 0;
84
85
        /** @psalm-var array<array-key, array<array-key, string|array>> $statements */
86 5
        foreach ($statements as $statement) {
87 5
            [$statementSql, $statementParams] = $statement;
88 5
            $statementSql = is_string($statementSql) ? $statementSql : '';
89 5
            $statementParams = is_array($statementParams) ? $statementParams : [];
90 5
            $this->setSql($statementSql)->bindValues($statementParams);
91 5
            $result = parent::execute();
92
        }
93
94 5
        $this->setSql($sql)->bindValues($params);
95
96 5
        return $result;
97
    }
98
99
    /**
100
     * @psalm-suppress UnusedClosureParam
101
     */
102 201
    protected function internalExecute(string|null $rawSql): void
103
    {
104 201
        $attempt = 0;
105
106 201
        while (true) {
107
            try {
108
                if (
109
                    ++$attempt === 1
110 201
                    && $this->isolationLevel !== null
111 201
                    && $this->db->getTransaction() === null
112
                ) {
113 1
                    $this->db->transaction(
114 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

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