Completed
Push — master ( a49fb0...9d47fe )
by Edgard
32:03
created

QueryBuilder::upsert()   C

Complexity

Conditions 13
Paths 109

Size

Total Lines 61

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 54
CRAP Score 13

Importance

Changes 0
Metric Value
dl 0
loc 61
ccs 54
cts 54
cp 1
rs 6.5416
c 0
b 0
f 0
cc 13
nc 109
nop 4
crap 13

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
/**
4
 * @link http://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license http://www.yiiframework.com/license/
7
 */
8
9
namespace edgardmessias\db\ibm\db2;
10
11
use yii\base\InvalidParamException;
12
use yii\db\Constraint;
13
use yii\db\Expression;
14
use yii\db\Query;
15
16
/**
17
 * QueryBuilder is the query builder for DB2 databases.
18
 * 
19
 * @property Connection $db Connetion
20
 *
21
 * @author Edgard Lorraine Messias <[email protected]>
22
 * @author Nikita Verkhovin <[email protected]>
23
 */
24
class QueryBuilder extends \yii\db\QueryBuilder
25
{
26
    public $typeMap = [
27
        Schema::TYPE_PK => 'integer NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1) PRIMARY KEY',
28
        Schema::TYPE_BIGPK => 'bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1) PRIMARY KEY',
29
        Schema::TYPE_STRING => 'varchar(255)',
30
        Schema::TYPE_TEXT => 'clob',
31
        Schema::TYPE_SMALLINT => 'smallint',
32
        Schema::TYPE_INTEGER => 'integer',
33
        Schema::TYPE_BIGINT => 'bigint',
34
        Schema::TYPE_FLOAT => 'float',
35
        Schema::TYPE_DOUBLE => 'double',
36
        Schema::TYPE_DECIMAL => 'decimal(10,0)',
37
        Schema::TYPE_DATETIME => 'timestamp',
38
        Schema::TYPE_TIMESTAMP => 'timestamp',
39
        Schema::TYPE_TIME => 'time',
40
        Schema::TYPE_DATE => 'date',
41
        Schema::TYPE_BINARY => 'blob',
42
        Schema::TYPE_BOOLEAN => 'smallint',
43
        Schema::TYPE_MONEY => 'decimal(19,4)',
44
    ];
45
46 337
    protected function defaultExpressionBuilders()
47
    {
48 337
        return array_merge(parent::defaultExpressionBuilders(), [
49 337
            'yii\db\conditions\InCondition' => 'edgardmessias\db\ibm\db2\conditions\InConditionBuilder',
50 337
            'yii\db\conditions\LikeCondition' => 'edgardmessias\db\ibm\db2\conditions\LikeConditionBuilder',
51 337
        ]);
52
    }
53
54
    /**
55
     * Builds a SQL statement for truncating a DB table.
56
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
57
     * @return string the SQL statement for truncating a DB table.
58
     */
59 1
    public function truncateTable($table)
60
    {
61 1
        return 'TRUNCATE TABLE ' . $this->db->quoteTableName($table) . ' IMMEDIATE';
62
    }
63
64
    /**
65
     * @inheritdoc
66
     */
67 5
    public function resetSequence($tableName, $value = null)
68
    {
69 5
        $table = $this->db->getTableSchema($tableName);
70
71 5
        if ($table !== null && isset($table->columns[$table->sequenceName])) {
72 5
            if ($value === null) {
73
                $sql = 'SELECT MAX("'. $table->sequenceName .'") FROM "'. $tableName . '"';
74
                $value = $this->db->createCommand($sql)->queryScalar() + 1;
75
            } else {
76 5
                $value = (int) $value;
77
            }
78 5
            return 'ALTER TABLE "' . $tableName . '" ALTER COLUMN "'.$table->sequenceName.'" RESTART WITH ' . $value;
79
        } elseif ($table === null) {
80
            throw new InvalidParamException("Table not found: $tableName");
0 ignored issues
show
Deprecated Code introduced by
The class yii\base\InvalidParamException has been deprecated with message: since 2.0.14. Use [[InvalidArgumentException]] instead.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
81
        } else {
82
            throw new InvalidParamException("There is no sequence associated with table '$tableName'.");
0 ignored issues
show
Deprecated Code introduced by
The class yii\base\InvalidParamException has been deprecated with message: since 2.0.14. Use [[InvalidArgumentException]] instead.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
83
        }
84
    }
85
86
    /**
87
     * Builds a SQL statement for enabling or disabling integrity check.
88
     * @param boolean $check whether to turn on or off the integrity check.
89
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
90
     * @param string $table the table name. Defaults to empty string, meaning that no table will be changed.
91
     * @return string the SQL statement for checking integrity
92
     * @throws \yii\base\NotSupportedException if this is not supported by the underlying DBMS
93
     * @see http://www-01.ibm.com/support/knowledgecenter/SSEPGG_10.5.0/com.ibm.db2.luw.sql.ref.doc/doc/r0000998.html?cp=SSEPGG_10.5.0%2F2-12-7-227
94
     */
95
    public function checkIntegrity($check = true, $schema = '', $table = '')
96
    {
97
        if ($table) {
98
            $tableNames = [$table];
99
        } else {
100
            if (!$schema) {
101
                $schema = $this->db->defaultSchema;
102
            }
103
104
            //Return only tables
105
            $sql = "SELECT t.tabname FROM syscat.tables AS t"
106
                    . " WHERE t.type in ('T') AND t.ownertype != 'S'";
107
108
            /**
109
             * Filter by integrity pending
110
             * @see http://www-01.ibm.com/support/knowledgecenter/SSEPGG_9.7.0/com.ibm.db2.luw.sql.ref.doc/doc/r0001063.html
111
             */
112
            if ($check) {
113
                $sql .= " AND t.status = 'C'";
114
            }
115
            if ($schema) {
116
                $sql .= ' AND t.tabschema = :schema';
117
            }
118
            
119
            $command = $this->db->createCommand($sql);
120
            if ($schema) {
121
                $command->bindValue(':schema', $schema);
122
            }
123
124
            $tableNames = $command->queryColumn();
125
        }
126
127
        if (empty($tableNames)) {
128
            return '';
129
        }
130
131
        $quotedTableNames = [];
132
        foreach ($tableNames as $tableName) {
133
            $quotedTableNames[] = $this->db->quoteTableName($tableName) . ($check? '' : ' ALL');
134
        }
135
136
        $enable = $check ? 'CHECKED' : 'UNCHECKED';
137
        return 'SET INTEGRITY FOR ' . implode(', ', $quotedTableNames) . ' IMMEDIATE ' . $enable. ';';
138
    }
139
140
    /**
141
     * @inheritdoc
142
     */
143 263
    public function buildOrderByAndLimit($sql, $orderBy, $limit, $offset)
144
    {
145 263
        $limitOffsetStatment = $this->buildLimit($limit, $offset);
146 263
        if ($limitOffsetStatment != '') {
147 25
            $sql = str_replace(':query', $sql, $limitOffsetStatment);
148
149
            //convert "item"."id" to "id" to use in OVER()
150 25
            $newOrderBy = [];
151
152 25
            if(!empty($orderBy)){
153 9
                foreach ($orderBy as $name => $direction) {
154 9
                    if(is_string($name)){
155 9
                        $e = explode('.', $name);
156 9
                        $name = array_pop($e);
157 9
                    }
158 9
                    $newOrderBy[$name] = $direction;
159 9
                }
160 9
            }
161
162 25
            $orderByStatment = $this->buildOrderBy($newOrderBy);
163
164 25
            $sql = str_replace(':order', $orderByStatment,$sql);
165 25
        }else{
166 256
            $orderByStatment = $this->buildOrderBy($orderBy);
167 256
            if ($orderByStatment !== '') {
168 36
                $sql .= $this->separator . $orderByStatment;
169 36
            }
170
        }
171 263
        return $sql;
172
    }
173
174
    /**
175
     * @inheritdoc
176
     */
177 263
    public function buildLimit($limit, $offset)
178
    {
179 263
        if (!$this->hasLimit($limit) && !$this->hasOffset($offset)) {
180 256
            return '';
181
        }
182
183 25
        if (!$this->hasOffset($offset)) {
184 23
            return ':query FETCH FIRST ' . $limit . ' ROWS ONLY';
185
        }
186
187
        /**
188
         * @todo Need remote the `RN_` from result to use in "INSERT" query
189
         */
190 3
        $limitOffsetStatment = 'SELECT * FROM (SELECT SUBQUERY_.*, ROW_NUMBER() OVER(:order) AS RN_ FROM ( :query ) AS SUBQUERY_) as t WHERE :offset :limit';
191
192 3
        $replacement = $this->hasOffset($offset) ? 't.RN_ > ' . $offset : 't.RN_ > 0';
193 3
        $limitOffsetStatment = str_replace(':offset', $replacement, $limitOffsetStatment);
194
195 3
        $replacement = '';
196
        
197 3
        if ($this->hasLimit($limit)) {
198 2
            if ($limit instanceof \yii\db\ExpressionInterface || $offset instanceof \yii\db\ExpressionInterface) {
199 1
                $replacement = 'AND t.RN_ <= (' . $limit  . ' + ' . $offset . ')';
200 1
            } else {
201 1
                $replacement = 'AND t.RN_ <= ' . ($limit + $offset);
202
            }
203 2
        }
204 3
        $limitOffsetStatment = str_replace(':limit', $replacement, $limitOffsetStatment);
205
206 3
        return $limitOffsetStatment;
207
    }
208
209
    /**
210
     * @inheritdoc
211
     */
212 1
    public function alterColumn($table, $column, $type)
213
    {
214 1
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN '
215 1
        . $this->db->quoteColumnName($column) . ' SET DATA TYPE '
216 1
        . $this->getColumnType($type);
217
    }
218
219
    /**
220
     * @inheritdoc
221
     */
222 62
    public function prepareInsertValues($table, $columns, $params = [])
223
    {
224 62
        $result = parent::prepareInsertValues($table, $columns, $params);
225
226
        // Empty placeholders, replace for (DEFAULT, DEFAULT, ...)
227 59
        if (empty($result[1]) && $result[2] === ' DEFAULT VALUES') {
228 1
            $schema = $this->db->getSchema();
229 1
            if (($tableSchema = $schema->getTableSchema($table)) !== null) {
230 1
                $columnSchemas = $tableSchema->columns;
231 1
            } else {
232
                $columnSchemas = [];
233
            }
234 1
            $result[1] = array_fill(0, count($columnSchemas), 'DEFAULT');
235 1
        }
236
237 59
        return $result;
238
    }
239
    
240
    /**
241
     * @inheritdoc
242
     */
243 17
    public function upsert($table, $insertColumns, $updateColumns, &$params)
244
    {
245
        /** @var Constraint[] $constraints */
246 17
        list($uniqueNames, $insertNames, $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
247 17
        if (empty($uniqueNames)) {
248 3
            return $this->insert($table, $insertColumns, $params);
249
        }
250
251 14
        $onCondition = ['or'];
252 14
        $quotedTableName = $this->db->quoteTableName($table);
253 14
        foreach ($constraints as $constraint) {
254 14
            $constraintCondition = ['and'];
255 14
            foreach ($constraint->columnNames as $name) {
0 ignored issues
show
Bug introduced by
The expression $constraint->columnNames of type array<integer,string>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
256 14
                $quotedName = $this->db->quoteColumnName($name);
257 14
                $constraintCondition[] = "$quotedTableName.$quotedName=\"EXCLUDED\".$quotedName";
258 14
            }
259 14
            $onCondition[] = $constraintCondition;
260 14
        }
261 14
        $on = $this->buildCondition($onCondition, $params);
262 14
        list(, $placeholders, $values, $params) = $this->prepareInsertValues($table, $insertColumns, $params);
263 14
        if (!empty($placeholders)) {
264 6
            $usingSelectValues = [];
265 6
            foreach ($insertNames as $index => $name) {
266 6
                $usingSelectValues[$name] = new Expression($placeholders[$index]);
267 6
            }
268 6
            $usingSubQuery = (new Query())
269 6
                ->select($usingSelectValues)
270 6
                ->from('SYSIBM.SYSDUMMY1');
271 6
            list($usingValues, $params) = $this->build($usingSubQuery, $params);
272 6
        }
273 14
        $mergeSql = 'MERGE INTO ' . $this->db->quoteTableName($table) . ' '
274 14
            . 'USING (' . (isset($usingValues) ? $usingValues : ltrim($values, ' ')) . ') "EXCLUDED" '
275 14
            . "ON ($on)";
276 14
        $insertValues = [];
277 14
        foreach ($insertNames as $name) {
278 14
            $quotedName = $this->db->quoteColumnName($name);
279 14
            if (strrpos($quotedName, '.') === false) {
280 14
                $quotedName = '"EXCLUDED".' . $quotedName;
281 14
            }
282 14
            $insertValues[] = $quotedName;
283 14
        }
284 14
        $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')'
285 14
            . ' VALUES (' . implode(', ', $insertValues) . ')';
286 14
        if ($updateColumns === false) {
287 4
            return "$mergeSql WHEN NOT MATCHED THEN $insertSql";
288
        }
289
290 10
        if ($updateColumns === true) {
291 4
            $updateColumns = [];
292 4
            foreach ($updateNames as $name) {
293 4
                $quotedName = $this->db->quoteColumnName($name);
294 4
                if (strrpos($quotedName, '.') === false) {
295 4
                    $quotedName = '"EXCLUDED".' . $quotedName;
296 4
                }
297 4
                $updateColumns[$name] = new Expression($quotedName);
298 4
            }
299 4
        }
300 10
        list($updates, $params) = $this->prepareUpdateSets($table, $updateColumns, $params);
301 10
        $updateSql = 'UPDATE SET ' . implode(', ', $updates);
302 10
        return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql";
303
    }
304
305
    /**
306
     * Creates a SELECT EXISTS() SQL statement.
307
     * @param string $rawSql the subquery in a raw form to select from.
308
     * @return string the SELECT EXISTS() SQL statement.
309
     *
310
     * @since 2.0.8
311
     */
312 21
    public function selectExists($rawSql)
313
    {
314 21
        return 'SELECT CASE WHEN COUNT(*)>0 THEN 1 ELSE 0 END FROM (' . $rawSql . ') CHECKEXISTS';;
315
    }
316
317
    /**
318
     * Builds a SQL command for adding comment to column
319
     *
320
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
321
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the method.
322
     * @return string the SQL statement for adding comment on column
323
     * @since 2.0.8
324
     */
325 2
    public function dropCommentFromColumn($table, $column)
326
    {
327 2
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column) . " IS ''";
328
    }
329
330
    /**
331
     * Builds a SQL command for adding comment to table
332
     *
333
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
334
     * @return string the SQL statement for adding comment on column
335
     * @since 2.0.8
336
     */
337 1
    public function dropCommentFromTable($table)
338
    {
339 1
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . " IS ''";
340
    }
341
342
    /**
343
     * @inheritdoc
344
     */
345 2
    public function dropIndex($name, $table)
346
    {
347 2
        return 'DROP INDEX ' . $this->db->quoteTableName($name);
348
    }
349
350
    /**
351
     * {@inheritdoc}
352
     */
353 2
    public function addDefaultValue($name, $table, $column, $value)
354
    {
355 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) 
356 2
            . ' ALTER COLUMN ' . $this->db->quoteColumnName($column)
357 2
            . ' SET DEFAULT ' . $this->db->quoteValue($value);
358
    }
359
360
    /**
361
     * {@inheritdoc}
362
     */
363 2
    public function dropDefaultValue($name, $table)
364
    {
365 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) 
366 2
            . ' ALTER COLUMN ' . $this->db->quoteColumnName($name)
367 2
            . ' DROP DEFAULT';
368
    }
369
}
370