Passed
Pull Request — master (#133)
by Wilmer
17:16 queued 12:38
created

CommandPDO::extractUsedParams()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

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