Passed
Push — master ( 1be5dd...997ae8 )
by Sergei
27:53 queued 14:03
created

DMLQueryBuilder::upsert()   B

Complexity

Conditions 10
Paths 37

Size

Total Lines 70
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 10

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 35
c 1
b 0
f 0
nc 37
nop 4
dl 0
loc 70
ccs 37
cts 37
cp 1
crap 10
rs 7.6666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mssql;
6
7
use JsonException;
8
use Yiisoft\Db\Constraint\Constraint;
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\Query\QueryInterface;
15
use Yiisoft\Db\QueryBuilder\AbstractDMLQueryBuilder;
16
17
use function implode;
18
use function in_array;
19
20
/**
21
 * Implements a DML (Data Manipulation Language) SQL statements for MSSQL Server.
22
 */
23
final class DMLQueryBuilder extends AbstractDMLQueryBuilder
24
{
25
    /**
26
     * @throws Exception
27
     * @throws InvalidArgumentException
28
     * @throws InvalidConfigException
29
     * @throws NotSupportedException
30
     */
31 13
    public function insertWithReturningPks(string $table, QueryInterface|array $columns, array &$params = []): string
32
    {
33 13
        [$names, $placeholders, $values, $params] = $this->prepareInsertValues($table, $columns, $params);
34
35 13
        $createdCols = [];
36 13
        $insertedCols = [];
37 13
        $returnColumns = $this->schema->getTableSchema($table)?->getColumns() ?? [];
38
39 13
        foreach ($returnColumns as $returnColumn) {
40 13
            if ($returnColumn->isComputed()) {
41 1
                continue;
42
            }
43
44 13
            $dbType = $returnColumn->getDbType();
45
46 13
            if (in_array($dbType, ['char', 'varchar', 'nchar', 'nvarchar', 'binary', 'varbinary'], true)) {
47 1
                $dbType .= '(MAX)';
48 13
            } elseif ($dbType === 'timestamp') {
49 3
                $dbType = $returnColumn->isAllowNull() ? 'varbinary(8)' : 'binary(8)';
50
            }
51
52 13
            $quotedName = $this->quoter->quoteColumnName($returnColumn->getName());
53 13
            $createdCols[] = $quotedName . ' ' . (string) $dbType . ' ' . ($returnColumn->isAllowNull() ? 'NULL' : '');
54 13
            $insertedCols[] = 'INSERTED.' . $quotedName;
55
        }
56
57 13
        $sql = 'INSERT INTO ' . $this->quoter->quoteTableName($table)
58 13
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
59 13
            . ' OUTPUT ' . implode(',', $insertedCols) . ' INTO @temporary_inserted'
60 13
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : ' ' . $values);
61
62 13
        return 'SET NOCOUNT ON;DECLARE @temporary_inserted TABLE (' . implode(', ', $createdCols) . ');'
63 13
            . $sql . ';SELECT * FROM @temporary_inserted;';
64
    }
65
66
    /**
67
     * @throws InvalidArgumentException
68
     */
69 5
    public function resetSequence(string $table, int|string $value = null): string
70
    {
71 5
        $tableSchema = $this->schema->getTableSchema($table);
72
73 5
        if ($tableSchema === null) {
74 1
            throw new InvalidArgumentException("Table not found: '$table'.");
75
        }
76
77 4
        $sequenceName = $tableSchema->getSequenceName();
78
79 4
        if ($sequenceName === null) {
80 1
            throw new InvalidArgumentException("There is not sequence associated with table '$table'.'");
81
        }
82
83 3
        $tableName = $this->quoter->quoteTableName($table);
84
85 3
        if ($value === null) {
86 3
            return "DBCC CHECKIDENT ('$tableName', RESEED, 0) WITH NO_INFOMSGS;DBCC CHECKIDENT ('$tableName', RESEED)";
87
        }
88
89 1
        return "DBCC CHECKIDENT ('$tableName', RESEED, $value)";
90
    }
91
92
    /**
93
     * @throws Exception
94
     * @throws InvalidArgumentException
95
     * @throws InvalidConfigException
96
     * @throws JsonException
97
     * @throws NotSupportedException
98
     */
99 37
    public function upsert(
100
        string $table,
101
        QueryInterface|array $insertColumns,
102
        bool|array $updateColumns,
103
        array &$params = []
104
    ): string {
105
        /** @psalm-var Constraint[] $constraints */
106 37
        $constraints = [];
107
108 37
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
109 37
            $table,
110 37
            $insertColumns,
111 37
            $updateColumns,
112 37
            $constraints
113 37
        );
114
115 37
        if (empty($uniqueNames)) {
116 2
            return $this->insert($table, $insertColumns, $params);
117
        }
118
119 35
        $onCondition = ['or'];
120 35
        $quotedTableName = $this->quoter->quoteTableName($table);
121
122 35
        foreach ($constraints as $constraint) {
123 35
            $constraintCondition = ['and'];
124 35
            $columnNames = (array) $constraint->getColumnNames();
125
126
            /** @psalm-var string[] $columnNames */
127 35
            foreach ($columnNames as $name) {
128 35
                $quotedName = $this->quoter->quoteColumnName($name);
129 35
                $constraintCondition[] = "$quotedTableName.$quotedName=[EXCLUDED].$quotedName";
130
            }
131
132 35
            $onCondition[] = $constraintCondition;
133
        }
134
135 35
        $on = $this->queryBuilder->buildCondition($onCondition, $params);
136
137 35
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
138
139 35
        $mergeSql = 'MERGE ' . $quotedTableName . ' WITH (HOLDLOCK) USING ('
140 35
            . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : $values)
141 35
            . ') AS [EXCLUDED] (' . implode(', ', $insertNames) . ') ' . "ON ($on)";
142
143 35
        $insertValues = [];
144
145 35
        foreach ($insertNames as $quotedName) {
146 35
            $insertValues[] = '[EXCLUDED].' . $quotedName;
147
        }
148
149 35
        $insertSql = 'INSERT (' . implode(', ', $insertNames) . ') VALUES (' . implode(', ', $insertValues) . ')';
150
151 35
        if ($updateColumns === false || $updateNames === []) {
0 ignored issues
show
introduced by
The condition $updateNames === array() is always false.
Loading history...
152
            /** there are no columns to update */
153 14
            return "$mergeSql WHEN NOT MATCHED THEN $insertSql;";
154
        }
155
156 21
        if ($updateColumns === true) {
0 ignored issues
show
introduced by
The condition $updateColumns === true is always false.
Loading history...
157 10
            $updateColumns = [];
158
159
            /** @psalm-var string[] $updateNames */
160 10
            foreach ($updateNames as $quotedName) {
161 10
                $updateColumns[$quotedName] = new Expression('[EXCLUDED].' . $quotedName);
162
            }
163
        }
164
165 21
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
166
167 21
        return "$mergeSql WHEN MATCHED THEN UPDATE SET " . implode(', ', $updates)
168 21
            . " WHEN NOT MATCHED THEN $insertSql;";
169
    }
170
}
171