Test Failed
Push — master ( 11f981...26eaf4 )
by Sergei
31:16 queued 13:50
created

Command   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 186
Duplicated Lines 0 %

Test Coverage

Coverage 98.59%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 23
eloc 66
c 4
b 1
f 0
dl 0
loc 186
ccs 70
cts 71
cp 0.9859
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A queryInternal() 0 36 4
A showDatabases() 0 7 1
A insertWithReturningPks() 0 25 4
A extractUsedParams() 0 16 4
A splitStatements() 0 23 5
A execute() 0 27 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite;
6
7
use Throwable;
8
use Yiisoft\Db\Driver\Pdo\AbstractPdoCommand;
9
use Yiisoft\Db\Exception\Exception;
10
use Yiisoft\Db\Exception\InvalidArgumentException;
11
12
use function array_pop;
13
use function count;
14
use function ltrim;
15
use function preg_match_all;
16
use function strpos;
17
18
/**
19
 * Implements a database command that can be executed with a PDO (PHP Data Object) database connection for SQLite
20
 * Server.
21
 */
22
final class Command extends AbstractPdoCommand
23
{
24
    public function insertWithReturningPks(string $table, array $columns): bool|array
25
    {
26
        $params = [];
27 4
        $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params);
28
        $this->setSql($sql)->bindValues($params);
29 4
30 4
        if (!$this->execute()) {
31 4
            return false;
32
        }
33 4
34
        $tableSchema = $this->db->getSchema()->getTableSchema($table);
35
        $tablePrimaryKeys = $tableSchema?->getPrimaryKey() ?? [];
36
37 4
        $result = [];
38 4
        foreach ($tablePrimaryKeys as $name) {
39
            if ($tableSchema?->getColumn($name)?->isAutoIncrement()) {
40 4
                $result[$name] = $this->db->getLastInsertID((string) $tableSchema?->getSequenceName());
41 4
                continue;
42 4
            }
43 3
44 3
            /** @psalm-var mixed */
45
            $result[$name] = $columns[$name] ?? $tableSchema?->getColumn($name)?->getDefaultValue();
46
        }
47
48 1
        return $result;
49
    }
50
51 4
    public function showDatabases(): array
52
    {
53
        $sql = <<<SQL
54 1
        SELECT name FROM pragma_database_list;
55
        SQL;
56 1
57
        return $this->setSql($sql)->queryColumn();
58 1
    }
59
60 1
    /**
61
     * Executes the SQL statement.
62
     *
63 287
     * 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 287
     *
66
     * @throws Exception
67
     * @throws Throwable The execution failed.
68
     *
69
     * @return int Number of rows affected by the execution.
70
     */
71
    public function execute(): int
72
    {
73
        $sql = $this->getSql();
74
75
        /** @psalm-var array<string, string> $params */
76
        $params = $this->params;
77
78
        $statements = $this->splitStatements($sql, $params);
79 112
80
        if ($statements === false) {
0 ignored issues
show
introduced by
The condition $statements === false is always true.
Loading history...
81 112
            return parent::execute();
82
        }
83
84 112
        $result = 0;
85
86 112
        /** @psalm-var array<array-key, array<array-key, string|array>> $statements */
87
        foreach ($statements as $statement) {
88 112
            [$statementSql, $statementParams] = $statement;
89 100
            $statementSql = is_string($statementSql) ? $statementSql : '';
90
            $statementParams = is_array($statementParams) ? $statementParams : [];
91
            $this->setSql($statementSql)->bindValues($statementParams);
92 12
            $result = parent::execute();
93
        }
94
95 12
        $this->setSql($sql)->bindValues($params);
96 12
97 12
        return $result;
98 12
    }
99 12
100 12
    /**
101
     * Performs the actual DB query of an SQL statement.
102
     *
103 12
     * @param int $queryMode Return results as DataReader
104
     *
105 12
     * @throws Exception
106
     * @throws Throwable If the query causes any problem.
107
     *
108
     * @return mixed The method execution result.
109
     */
110
    protected function queryInternal(int $queryMode): mixed
111
    {
112
        $sql = $this->getSql();
113 254
114
        /** @psalm-var array<string, string> $params */
115 254
        $params = $this->params;
116
117 254
        $statements = $this->splitStatements($sql, $params);
118
119
        if ($statements === false || $statements === []) {
0 ignored issues
show
introduced by
The condition $statements === false is always true.
Loading history...
120 254
            return parent::queryInternal($queryMode);
121 254
        }
122 254
123
        [$lastStatementSql, $lastStatementParams] = array_pop($statements);
124 1
125 1
        /**
126 1
         * @psalm-var array<array-key, array> $statements
127 1
         */
128 1
        foreach ($statements as $statement) {
129 1
            /**
130
             * @psalm-var string $statementSql
131 254
             * @psalm-var array $statementParams
132
             */
133 254
            [$statementSql, $statementParams] = $statement;
134 2
            $this->setSql($statementSql)->bindValues($statementParams);
135 2
            parent::execute();
136 2
        }
137
138 2
        $this->setSql($lastStatementSql)->bindValues($lastStatementParams);
139 2
140
        /** @psalm-var string $result */
141
        $result = parent::queryInternal($queryMode);
142
143
        $this->setSql($sql)->bindValues($params);
144
145
        return $result;
146
    }
147
148
    /**
149
     * Splits the specified SQL into individual SQL statements and returns them or `false` if there's a single
150
     * statement.
151
     *
152
     * @param string $sql SQL to split.
153
     *
154
     * @throws InvalidArgumentException
155 254
     *
156
     * @return array|bool List of SQL statements or `false` if there's a single statement.
157 254
     *
158
     * @psalm-param array<string, string> $params
159
     *
160 254
     * @psalm-return false|list<array{0: string, 1: array}>
161
     */
162 254
    private function splitStatements(string $sql, array $params): bool|array
163
    {
164 254
        $semicolonIndex = strpos($sql, ';');
165 254
166
        if ($semicolonIndex === false || $semicolonIndex === mb_strlen($sql, '8bit') - 1) {
167
            return false;
168 1
        }
169
170
        $tokenizer = new SqlTokenizer($sql);
171
172
        $codeToken = $tokenizer->tokenize();
173 1
174
        if (count($codeToken->getChildren()) === 1) {
175
            return false;
176
        }
177
178 1
        $statements = [];
179 1
180 1
        foreach ($codeToken->getChildren() as $statement) {
181
            $statements[] = [$statement->getSql(), $this->extractUsedParams($statement, $params)];
182
        }
183 1
184
        return $statements;
185
    }
186 1
187
    /**
188 1
     * Returns named bindings used in the specified statement token.
189
     *
190 1
     * @psalm-param array<string, string> $params
191
     */
192
    private function extractUsedParams(SqlToken $statement, array $params): array
193
    {
194
        preg_match_all('/(?P<placeholder>:\w+)/', $statement->getSql(), $matches, PREG_SET_ORDER);
195
196
        $result = [];
197
198
        foreach ($matches as $match) {
199
            $phName = ltrim($match['placeholder'], ':');
200
            if (isset($params[$phName])) {
201
                $result[$phName] = $params[$phName];
202
            } elseif (isset($params[':' . $phName])) {
203
                $result[':' . $phName] = $params[':' . $phName];
204
            }
205
        }
206
207 255
        return $result;
208
    }
209
}
210