Passed
Pull Request — master (#150)
by Def
30:21 queued 17:41
created

DMLQueryBuilder   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 296
Duplicated Lines 0 %

Test Coverage

Coverage 94.12%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 29
eloc 68
c 2
b 0
f 0
dl 0
loc 296
ccs 64
cts 68
cp 0.9412
rs 10

9 Methods

Rating   Name   Duplication   Size   Complexity  
A newUpsert() 0 49 6
B normalizeTableRowData() 0 22 7
A upsert() 0 13 2
A resetSequence() 0 27 5
A truncateTable() 0 3 1
A update() 0 9 2
A insert() 0 3 1
A insertEx() 0 20 4
A __construct() 0 6 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Pgsql;
6
7
use JsonException;
8
use PDO;
9
use Yiisoft\Db\Command\Param;
10
use Yiisoft\Db\Exception\Exception;
11
use Yiisoft\Db\Exception\InvalidArgumentException;
12
use Yiisoft\Db\Exception\InvalidConfigException;
13
use Yiisoft\Db\Exception\NotSupportedException;
14
use Yiisoft\Db\Expression\Expression;
15
use Yiisoft\Db\QueryBuilder\DMLQueryBuilder as AbstractDMLQueryBuilder;
16
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;
17
use Yiisoft\Db\Query\Query;
18
use Yiisoft\Db\Query\QueryInterface;
19
use Yiisoft\Db\Schema\QuoterInterface;
20
use Yiisoft\Db\Schema\Schema;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Yiisoft\Db\Pgsql\Schema. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
21
use Yiisoft\Db\Schema\SchemaInterface;
22
23
use function implode;
24
use function is_array;
25
use function is_string;
26
use function reset;
27
28
final class DMLQueryBuilder extends AbstractDMLQueryBuilder
29
{
30 422
    public function __construct(
31
        private QueryBuilderInterface $queryBuilder,
32
        private QuoterInterface $quoter,
33
        private SchemaInterface $schema
34
    ) {
35 422
        parent::__construct($queryBuilder, $quoter, $schema);
36
    }
37
38 1
    public function insertEx(string $table, QueryInterface|array $columns, array &$params = []): string
39
    {
40 1
        $sql = $this->insert($table, $columns, $params);
41
42 1
        $tableSchema = $this->schema->getTableSchema($table);
43
44 1
        $returnColumns = [];
45 1
        if ($tableSchema !== null) {
46 1
            $returnColumns = $tableSchema->getPrimaryKey();
47
        }
48
49 1
        if (!empty($returnColumns)) {
50 1
            $returning = [];
51 1
            foreach ($returnColumns as $name) {
52 1
                $returning[] = $this->quoter->quoteColumnName($name);
53
            }
54 1
            $sql .= ' RETURNING ' . implode(', ', $returning);
55
        }
56
57 1
        return $sql;
58
    }
59
60
    /**
61
     * Creates an INSERT SQL statement.
62
     *
63
     * For example,
64
     *
65
     * ```php
66
     * $sql = $queryBuilder->insert('user', [
67
     *     'name' => 'Sam',
68
     *     'age' => 30,
69
     * ], $params);
70
     * ```
71
     *
72
     * The method will properly escape the table and column names.
73
     *
74
     * @param string $table the table that new rows will be inserted into.
75
     * @param array|QueryInterface $columns the column data (name => value) to be inserted into the table or instance of
76
     * {@see Query} to perform INSERT INTO ... SELECT SQL statement. Passing of {@see Query}.
77
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the
78
     * DB command later.
79
     *
80
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
81
     *
82
     * @return string the INSERT SQL
83
     *
84
     * @psalm-suppress UndefinedInterfaceMethod
85
     * @psalm-suppress MixedArgument
86
     */
87 50
    public function insert(string $table, QueryInterface|array $columns, array &$params = []): string
88
    {
89 50
        return parent::insert($table, $this->normalizeTableRowData($table, $columns), $params);
90
    }
91
92
    /**
93
     * {@see upsert()} implementation for PostgresSQL 9.5 or higher.
94
     *
95
     * @param string $table
96
     * @param array|QueryInterface $insertColumns
97
     * @param array|bool|QueryInterface $updateColumns
98
     * @param array $params
99
     *
100
     * @throws Exception|InvalidArgumentException|InvalidConfigException|JsonException|NotSupportedException
101
     *
102
     * @return string
103
     */
104 18
    public function newUpsert(
105
        string $table,
106
        QueryInterface|array $insertColumns,
107
        bool|array|QueryInterface $updateColumns,
108
        array &$params = []
109
    ): string {
110 18
        $insertSql = $this->insert($table, $insertColumns, $params);
111
112
        /** @var array $uniqueNames */
113 18
        [$uniqueNames, , $updateNames] = $this->prepareUpsertColumns(
114
            $table,
115
            $insertColumns,
116
            $updateColumns,
117
        );
118
119 18
        if (empty($uniqueNames)) {
120 3
            return $insertSql;
121
        }
122
123 15
        if ($updateNames === []) {
0 ignored issues
show
introduced by
The condition $updateNames === array() is always false.
Loading history...
124
            /** there are no columns to update */
125
            $updateColumns = false;
126
        }
127
128 15
        if ($updateColumns === false) {
0 ignored issues
show
introduced by
The condition $updateColumns === false is always false.
Loading history...
129 5
            return "$insertSql ON CONFLICT DO NOTHING";
130
        }
131
132 10
        if ($updateColumns === true) {
0 ignored issues
show
introduced by
The condition $updateColumns === true is always false.
Loading history...
133 4
            $updateColumns = [];
134
135
            /** @var string $name */
136 4
            foreach ($updateNames as $name) {
137 4
                $updateColumns[$name] = new Expression(
138 4
                    'EXCLUDED.' . $this->quoter->quoteColumnName($name)
139
                );
140
            }
141
        }
142
143
        /**
144
         * @var array $updateColumns
145
         *
146
         * @psalm-var string[] $uniqueNames
147
         * @psalm-var string[] $updates
148
         */
149 10
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
150
151
        return $insertSql
152 10
            . ' ON CONFLICT (' . implode(', ', $uniqueNames) . ') DO UPDATE SET ' . implode(', ', $updates);
153
    }
154
155
    /**
156
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
157
     *
158
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
159
     * or 1.
160
     *
161
     * @param string $tableName the name of the table whose primary key sequence will be reset.
162
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
163
     * row's primary key will have a value 1.
164
     *
165
     * @throws Exception|InvalidArgumentException if the table does not exist or there is no sequence
166
     * associated with the table.
167
     *
168
     * @return string the SQL statement for resetting sequence.
169
     */
170 2
    public function resetSequence(string $tableName, $value = null): string
171
    {
172 2
        $table = $this->schema->getTableSchema($tableName);
173
174 2
        if ($table !== null && ($sequence = $table->getSequenceName()) !== null) {
175
            /**
176
             * {@see http://www.postgresql.org/docs/8.1/static/functions-sequence.html}
177
             */
178 2
            $sequence = $this->quoter->quoteTableName($sequence);
179 2
            $tableName = $this->quoter->quoteTableName($tableName);
180
181 2
            if ($value === null) {
182 2
                $pk = $table->getPrimaryKey();
183 2
                $key = $this->quoter->quoteColumnName(reset($pk));
184 2
                $value = "(SELECT COALESCE(MAX($key),0) FROM $tableName)+1";
185
            } else {
186 2
                $value = (int) $value;
187
            }
188
189 2
            return "SELECT SETVAL('$sequence',$value,false)";
190
        }
191
192
        if ($table === null) {
193
            throw new InvalidArgumentException("Table not found: $tableName");
194
        }
195
196
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.");
197
    }
198
199 1
    public function truncateTable(string $table): string
200
    {
201 1
        return 'TRUNCATE TABLE ' . $this->quoter->quoteTableName($table) . ' RESTART IDENTITY';
202
    }
203
204
    /**
205
     * Creates an UPDATE SQL statement.
206
     *
207
     * For example,
208
     *
209
     * ```php
210
     * $params = [];
211
     * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
212
     * ```
213
     *
214
     * The method will properly escape the table and column names.
215
     *
216
     * @param string $table the table to be updated.
217
     * @param array $columns the column data (name => value) to be updated.
218
     * @param array|string $condition the condition that will be put in the WHERE part. Please refer to
219
     * {@see Query::where()} on how to specify condition.
220
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
221
     * DB command later.
222
     *
223
     * @return string the UPDATE SQL.
224
     *
225
     * @psalm-suppress UndefinedInterfaceMethod
226
     */
227 4
    public function update(string $table, array $columns, array|string $condition, array &$params = []): string
228
    {
229 4
        $normalizeTableRowData = $this->normalizeTableRowData($table, $columns);
230
231 4
        return parent::update(
232
            $table,
233 4
            is_array($normalizeTableRowData) ? $normalizeTableRowData : [],
0 ignored issues
show
introduced by
The condition is_array($normalizeTableRowData) is always true.
Loading history...
234
            $condition,
235
            $params,
236
        );
237
    }
238
239
    /**
240
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
241
     * constraints), or update them if they do.
242
     *
243
     * For example,
244
     *
245
     * ```php
246
     * $sql = $queryBuilder->upsert('pages', [
247
     *     'name' => 'Front page',
248
     *     'url' => 'http://example.com/', // url is unique
249
     *     'visits' => 0,
250
     * ], [
251
     *     'visits' => new \Yiisoft\Db\Expression('visits + 1'),
252
     * ], $params);
253
     * ```
254
     *
255
     * The method will properly escape the table and column names.
256
     *
257
     * @param string $table the table that new rows will be inserted into/updated in.
258
     * @param array|QueryInterface $insertColumns the column data (name => value) to be inserted into the table or
259
     * instance of {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
260
     * @param array|bool|QueryInterface $updateColumns the column data (name => value) to be updated if they already
261
     * exist.
262
     * If `true` is passed, the column data will be updated to match the insert column data.
263
     * If `false` is passed, no update will be performed if the column data already exists.
264
     * @param array $params the binding parameters that will be generated by this method.
265
     * They should be bound to the DB command later.
266
     *
267
     * @throws Exception|InvalidConfigException|JsonException|NotSupportedException if this is not supported by the
268
     * underlying DBMS.
269
     *
270
     * @return string the resulting SQL.
271
     *
272
     * @link https://www.postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
273
     * @link https://stackoverflow.com/questions/1109061/insert-on-duplicate-update-in-postgresql/8702291#8702291
274
     *
275
     * @psalm-suppress UndefinedInterfaceMethod
276
     */
277 18
    public function upsert(
278
        string $table,
279
        QueryInterface|array $insertColumns,
280
        $updateColumns,
281
        array &$params = []
282
    ): string {
283 18
        $insertColumns = $this->normalizeTableRowData($table, $insertColumns);
284
285 18
        if (!is_bool($updateColumns)) {
286 7
            $updateColumns = $this->normalizeTableRowData($table, $updateColumns);
287
        }
288
289 18
        return $this->newUpsert($table, $insertColumns, $updateColumns, $params);
290
    }
291
292
    /**
293
     * Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary.
294
     *
295
     * @param string $table the table that data will be saved into.
296
     * @param array|QueryInterface $columns the column data (name => value) to be saved into the table or instance of
297
     * {@see QueryInterface} to perform INSERT INTO ... SELECT SQL statement. Passing of
298
     * {@see QueryInterface}.
299
     *
300
     * @return array|QueryInterface normalized columns.
301
     */
302 52
    private function normalizeTableRowData(string $table, QueryInterface|array $columns): QueryInterface|array
303
    {
304 52
        if ($columns instanceof QueryInterface) {
0 ignored issues
show
introduced by
$columns is never a sub-type of Yiisoft\Db\Query\QueryInterface.
Loading history...
305 14
            return $columns;
306
        }
307
308 44
        if (($tableSchema = $this->schema->getTableSchema($table)) !== null) {
309 44
            $columnSchemas = $tableSchema->getColumns();
310
            /** @var mixed $value */
311 44
            foreach ($columns as $name => $value) {
312
                if (
313 44
                    isset($columnSchemas[$name]) &&
314 44
                    $columnSchemas[$name]->getType() === Schema::TYPE_BINARY &&
315 44
                    is_string($value)
316
                ) {
317
                    /** explicitly setup PDO param type for binary column */
318 3
                    $columns[$name] = new Param($value, PDO::PARAM_LOB);
319
                }
320
            }
321
        }
322
323 44
        return $columns;
324
    }
325
}
326