Test Failed
Pull Request — master (#194)
by Def
18:32 queued 14:55
created

DMLQueryBuilder::upsert()   B

Complexity

Conditions 7
Paths 20

Size

Total Lines 48
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 20
c 1
b 0
f 0
nc 20
nop 4
dl 0
loc 48
ccs 19
cts 19
cp 1
crap 7
rs 8.6666
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mysql;
6
7
use JsonException;
8
use Throwable;
9
use Yiisoft\Db\Exception\Exception;
10
use Yiisoft\Db\Exception\InvalidArgumentException;
11
use Yiisoft\Db\Exception\InvalidConfigException;
12
use Yiisoft\Db\Exception\NotSupportedException;
13
use Yiisoft\Db\Expression\Expression;
14
use Yiisoft\Db\Expression\ExpressionInterface;
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\SchemaInterface;
21
22
use function implode;
23
use function reset;
24
25
final class DMLQueryBuilder extends AbstractDMLQueryBuilder
26
{
27 484
    public function __construct(
28
        QueryBuilderInterface $queryBuilder,
29
        private QuoterInterface $quoter,
30
        private SchemaInterface $schema
31
    ) {
32 484
        parent::__construct($queryBuilder, $quoter, $schema);
33
    }
34
35
    /**
36
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
37
     *
38
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
39
     * or 1.
40
     *
41
     * @param string $tableName the name of the table whose primary key sequence will be reset.
42
     * @param int|string|null $value the value for the primary key of the next new row inserted. If this is not set, the
43
     * next new row's primary key will have a value 1.
44
     *
45
     * @throws Exception|InvalidArgumentException|Throwable
46
     *
47
     * @return string the SQL statement for resetting sequence.
48
     */
49 3
    public function resetSequence(string $tableName, int|string $value = null): string
50
    {
51 3
        $table = $this->schema->getTableSchema($tableName);
52
53 3
        if ($table === null) {
54 1
            throw new InvalidArgumentException("Table not found: '$tableName'.");
55
        }
56
57 2
        $sequenceName = $table->getSequenceName();
58 2
        if ($sequenceName === null) {
59 1
            throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.");
60
        }
61
62 1
        $tableName = $this->quoter->quoteTableName($tableName);
63
64 1
        if ($value !== null) {
65 1
            return 'ALTER TABLE ' . $tableName . ' AUTO_INCREMENT=' . $value . ';';
66
        }
67
68 1
        $pk = $table->getPrimaryKey();
69 1
        $key = (string) reset($pk);
70
71 1
        return "SET @new_autoincrement_value := (SELECT MAX(`$key`) + 1 FROM $tableName);
72 1
SET @sql = CONCAT('ALTER TABLE $tableName AUTO_INCREMENT =', @new_autoincrement_value);
73
PREPARE autoincrement_stmt FROM @sql;
74 1
EXECUTE autoincrement_stmt";
75
    }
76
77
    /**
78
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
79
     * constraints), or update them if they do.
80
     *
81
     * For example,
82
     *
83
     * ```php
84
     * $sql = $queryBuilder->upsert('pages', [
85
     *     'name' => 'Front page',
86
     *     'url' => 'http://example.com/', // url is unique
87
     *     'visits' => 0,
88
     * ], [
89
     *     'visits' => new Expression('visits + 1'),
90
     * ], $params);
91
     * ```
92
     *
93
     * The method will properly escape the table and column names.
94
     *
95
     * @param string $table the table that new rows will be inserted into/updated in.
96
     * @param array|QueryInterface $insertColumns the column data (name => value) to be inserted into the table or
97
     * instance of {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
98
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist. If `true`
99
     * is passed, the column data will be updated to match the insert column data. If `false` is passed, no update will
100
     * be performed if the column data already exists.
101
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the DB
102
     * command later.
103
     *
104
     * @throws Exception|InvalidConfigException|JsonException|NotSupportedException if this is not supported by the
105
     * underlying DBMS.
106
     *
107
     * @return string the resulting SQL.
108
     */
109 34
    public function upsert(
110
        string $table,
111
        QueryInterface|array $insertColumns,
112
        bool|array $updateColumns,
113
        array &$params
114
    ): string {
115 34
        if (!$insertColumns instanceof QueryInterface) {
0 ignored issues
show
introduced by
$insertColumns is never a sub-type of Yiisoft\Db\Query\QueryInterface.
Loading history...
116
            $insertColumns = $this->normalizeColumnNames($table, $insertColumns);
0 ignored issues
show
Bug introduced by
The method normalizeColumnNames() does not exist on Yiisoft\Db\Mysql\DMLQueryBuilder. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

116
            /** @scrutinizer ignore-call */ 
117
            $insertColumns = $this->normalizeColumnNames($table, $insertColumns);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
117
        }
118 34
119 34
        if (!is_bool($updateColumns)) {
0 ignored issues
show
introduced by
The condition is_bool($updateColumns) is always false.
Loading history...
120 34
            $updateColumns = $this->normalizeColumnNames($table, $updateColumns);
121 34
        }
122 34
123
        $insertSql = $this->insert($table, $insertColumns, $params);
124 34
125 2
        /** @var array $uniqueNames */
126
        [$uniqueNames, , $updateNames] = $this->prepareUpsertColumns(
127
            $table,
128 32
            $insertColumns,
129 10
            $updateColumns,
130
        );
131 10
132 8
        if (empty($uniqueNames)) {
133 8
            return $insertSql;
134 8
        }
135
136
        if ($updateColumns === true) {
137
            $updateColumns = [];
138 32
            /** @var string $name */
139 14
            foreach ($updateNames as $name) {
0 ignored issues
show
Bug introduced by
The expression $updateNames of type null is not traversable.
Loading history...
140
                $updateColumns[$name] = new Expression(
141
                    'VALUES(' . $this->quoter->quoteColumnName($name) . ')'
142
                );
143
            }
144
        }
145
146 18
        if (empty($updateColumns)) {
147
            return str_replace('INSERT INTO', 'INSERT IGNORE INTO', $insertSql);
148 18
        }
149
150
        /**
151
         *  @psalm-var array<array-key, string> $updates
152
         *  @psalm-var array<string, ExpressionInterface|string> $updateColumns
153
         */
154
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
155
156
        return $insertSql . ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates);
157
    }
158
159
    /**
160
     * Prepares a `VALUES` part for an `INSERT` SQL statement.
161
     *
162
     * @param string $table the table that new rows will be inserted into.
163
     * @param array|QueryInterface $columns the column data (name => value) to be inserted into the table or instance of
164 72
     * {@see Query|Query} to perform INSERT INTO ... SELECT SQL statement.
165
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the DB
166
     * command later.
167
     *
168
     * @throws Exception|InvalidArgumentException|InvalidConfigException|JsonException|NotSupportedException
169
     *
170 72
     * @return array array of column names, placeholders, values and params.
171
     */
172 69
    protected function prepareInsertValues(string $table, QueryInterface|array $columns, array $params = []): array
173 2
    {
174
        /**
175 2
         * @var array $names
176 2
         * @var array $placeholders
177 2
         */
178 2
        [$names, $placeholders, $values, $params] = parent::prepareInsertValues($table, $columns, $params);
179 2
180 2
        if (!$columns instanceof QueryInterface && empty($names)) {
0 ignored issues
show
introduced by
$columns is never a sub-type of Yiisoft\Db\Query\QueryInterface.
Loading history...
181 2
            $tableSchema = $this->schema->getTableSchema($table);
182
183
            if ($tableSchema !== null) {
184
                $columns = $tableSchema->getColumns();
185
                $columns = !empty($tableSchema->getPrimaryKey())
186 69
                    ? $tableSchema->getPrimaryKey() : [reset($columns)->getName()];
187
                foreach ($columns as $name) {
188
                    $names[] = $this->quoter->quoteColumnName($name);
189
                    $placeholders[] = 'DEFAULT';
190
                }
191
            }
192
        }
193
194
        return [$names, $placeholders, $values, $params];
195
    }
196
}
197