Passed
Push — master ( d40a47...c8de2b )
by Wilmer
12:04
created

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

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