Test Failed
Pull Request — dev (#104)
by Wilmer
03:26
created

CommandPDOSqlite::schema()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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