Completed
Pull Request — 2.1 (#15718)
by Alex
17:00
created

QueryBuilder::resetSequence()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.3073

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 10
cts 13
cp 0.7692
rs 8.7624
c 0
b 0
f 0
cc 5
eloc 14
nc 4
nop 2
crap 5.3073
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db\pgsql;
9
10
use yii\base\InvalidArgumentException;
11
use yii\db\Constraint;
12
use yii\db\Expression;
13
use yii\db\PdoValue;
14
use yii\db\Query;
15
use yii\helpers\StringHelper;
16
17
/**
18
 * QueryBuilder is the query builder for PostgreSQL databases.
19
 *
20
 * @author Gevik Babakhani <[email protected]>
21
 * @since 2.0
22
 */
23
class QueryBuilder extends \yii\db\QueryBuilder
24
{
25
    /**
26
     * Defines a UNIQUE index for [[createIndex()]].
27
     * @since 2.0.6
28
     */
29
    const INDEX_UNIQUE = 'unique';
30
    /**
31
     * Defines a B-tree index for [[createIndex()]].
32
     * @since 2.0.6
33
     */
34
    const INDEX_B_TREE = 'btree';
35
    /**
36
     * Defines a hash index for [[createIndex()]].
37
     * @since 2.0.6
38
     */
39
    const INDEX_HASH = 'hash';
40
    /**
41
     * Defines a GiST index for [[createIndex()]].
42
     * @since 2.0.6
43
     */
44
    const INDEX_GIST = 'gist';
45
    /**
46
     * Defines a GIN index for [[createIndex()]].
47
     * @since 2.0.6
48
     */
49
    const INDEX_GIN = 'gin';
50
51
    /**
52
     * @var array mapping from abstract column types (keys) to physical column types (values).
53
     */
54
    public $typeMap = [
55
        Schema::TYPE_PK => 'serial NOT NULL PRIMARY KEY',
56
        Schema::TYPE_UPK => 'serial NOT NULL PRIMARY KEY',
57
        Schema::TYPE_BIGPK => 'bigserial NOT NULL PRIMARY KEY',
58
        Schema::TYPE_UBIGPK => 'bigserial NOT NULL PRIMARY KEY',
59
        Schema::TYPE_CHAR => 'char(1)',
60
        Schema::TYPE_STRING => 'varchar(255)',
61
        Schema::TYPE_TEXT => 'text',
62
        Schema::TYPE_TINYINT => 'smallint',
63
        Schema::TYPE_SMALLINT => 'smallint',
64
        Schema::TYPE_INTEGER => 'integer',
65
        Schema::TYPE_BIGINT => 'bigint',
66
        Schema::TYPE_FLOAT => 'double precision',
67
        Schema::TYPE_DOUBLE => 'double precision',
68
        Schema::TYPE_DECIMAL => 'numeric(10,0)',
69
        Schema::TYPE_DATETIME => 'timestamp(0)',
70
        Schema::TYPE_TIMESTAMP => 'timestamp(0)',
71
        Schema::TYPE_TIME => 'time(0)',
72
        Schema::TYPE_DATE => 'date',
73
        Schema::TYPE_BINARY => 'bytea',
74
        Schema::TYPE_BOOLEAN => 'boolean',
75
        Schema::TYPE_MONEY => 'numeric(19,4)',
76
        Schema::TYPE_JSON => 'jsonb',
77
    ];
78
79
    /**
80
     * {@inheritdoc}
81
     */
82 523
    protected function defaultConditionClasses()
83
    {
84 523
        return array_merge(parent::defaultConditionClasses(), [
85 523
            'ILIKE' => \yii\db\conditions\LikeCondition::class,
86
            'NOT ILIKE' => \yii\db\conditions\LikeCondition::class,
87
            'OR ILIKE' => \yii\db\conditions\LikeCondition::class,
88
            'OR NOT ILIKE' => \yii\db\conditions\LikeCondition::class,
89
        ]);
90
    }
91
92
    /**
93
     * {@inheritdoc}
94
     */
95 523
    protected function defaultExpressionBuilders()
96
    {
97 523
        return array_merge(parent::defaultExpressionBuilders(), [
98 523
            \yii\db\ArrayExpression::class => ArrayExpressionBuilder::class,
99
            \yii\db\JsonExpression::class => JsonExpressionBuilder::class,
100
        ]);
101
    }
102
103
    /**
104
     * Builds a SQL statement for creating a new index.
105
     * @param string $name the name of the index. The name will be properly quoted by the method.
106
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method.
107
     * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
108
     * separate them with commas or use an array to represent them. Each column name will be properly quoted
109
     * by the method, unless a parenthesis is found in the name.
110
     * @param bool|string $unique whether to make this a UNIQUE index constraint. You can pass `true` or [[INDEX_UNIQUE]] to create
111
     * a unique index, `false` to make a non-unique index using the default index type, or one of the following constants to specify
112
     * the index method to use: [[INDEX_B_TREE]], [[INDEX_HASH]], [[INDEX_GIST]], [[INDEX_GIN]].
113
     * @return string the SQL statement for creating a new index.
114
     * @see http://www.postgresql.org/docs/8.2/static/sql-createindex.html
115
     */
116 6
    public function createIndex($name, $table, $columns, $unique = false)
117
    {
118 6
        if ($unique === self::INDEX_UNIQUE || $unique === true) {
119 4
            $index = false;
120 4
            $unique = true;
121
        } else {
122 3
            $index = $unique;
123 3
            $unique = false;
124
        }
125
126 6
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ') .
127 6
        $this->db->quoteTableName($name) . ' ON ' .
128 6
        $this->db->quoteTableName($table) .
129 6
        ($index !== false ? " USING $index" : '') .
130 6
        ' (' . $this->buildColumns($columns) . ')';
131
    }
132
133
    /**
134
     * Builds a SQL statement for dropping an index.
135
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
136
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
137
     * @return string the SQL statement for dropping an index.
138
     */
139 2
    public function dropIndex($name, $table)
140
    {
141 2
        return 'DROP INDEX ' . $this->db->quoteTableName($name);
142
    }
143
144
    /**
145
     * Builds a SQL statement for renaming a DB table.
146
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
147
     * @param string $newName the new table name. The name will be properly quoted by the method.
148
     * @return string the SQL statement for renaming a DB table.
149
     */
150 1
    public function renameTable($oldName, $newName)
151
    {
152 1
        return 'ALTER TABLE ' . $this->db->quoteTableName($oldName) . ' RENAME TO ' . $this->db->quoteTableName($newName);
153
    }
154
155
    /**
156
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
157
     * The sequence will be reset such that the primary key of the next new row inserted
158
     * will have the specified value or 1.
159
     * @param string $tableName the name of the table whose primary key sequence will be reset
160
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set,
161
     * the next new row's primary key will have a value 1.
162
     * @return string the SQL statement for resetting sequence
163
     * @throws InvalidArgumentException if the table does not exist or there is no sequence associated with the table.
164
     */
165 5
    public function resetSequence($tableName, $value = null)
166
    {
167 5
        $table = $this->db->getTableSchema($tableName);
168 5
        if ($table !== null && $table->sequenceName !== null) {
169
            // c.f. http://www.postgresql.org/docs/8.1/static/functions-sequence.html
170 5
            $sequence = $this->db->quoteTableName($table->sequenceName);
171 5
            $tableName = $this->db->quoteTableName($tableName);
172 5
            if ($value === null) {
173 1
                $key = $this->db->quoteColumnName(reset($table->primaryKey));
0 ignored issues
show
Security Bug introduced by
It seems like reset($table->primaryKey) targeting reset() can also be of type false; however, yii\db\Connection::quoteColumnName() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
174 1
                $value = "(SELECT COALESCE(MAX({$key}),0) FROM {$tableName})+1";
175
            } else {
176 5
                $value = (int) $value;
177
            }
178
179 5
            return "SELECT SETVAL('$sequence',$value,false)";
180
        } elseif ($table === null) {
181
            throw new InvalidArgumentException("Table not found: $tableName");
182
        }
183
184
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.");
185
    }
186
187
    /**
188
     * Builds a SQL statement for enabling or disabling integrity check.
189
     * @param bool $check whether to turn on or off the integrity check.
190
     * @param string $schema the schema of the tables.
191
     * @param string $table the table name.
192
     * @return string the SQL statement for checking integrity
193
     */
194
    public function checkIntegrity($check = true, $schema = '', $table = '')
195
    {
196
        $enable = $check ? 'ENABLE' : 'DISABLE';
197
        $schema = $schema ?: $this->db->getSchema()->defaultSchema;
198
        $tableNames = $table ? [$table] : $this->db->getSchema()->getTableNames($schema);
199
        $viewNames = $this->db->getSchema()->getViewNames($schema);
0 ignored issues
show
Documentation Bug introduced by
The method getViewNames does not exist on object<yii\db\Schema>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
200
        $tableNames = array_diff($tableNames, $viewNames);
201
        $command = '';
202
203
        foreach ($tableNames as $tableName) {
204
            $tableName = $this->db->quoteTableName("{$schema}.{$tableName}");
205
            $command .= "ALTER TABLE $tableName $enable TRIGGER ALL; ";
206
        }
207
208
        // enable to have ability to alter several tables
209
        $this->db->getMasterPdo()->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true);
210
211
        return $command;
212
    }
213
214
    /**
215
     * Builds a SQL statement for truncating a DB table.
216
     * Explicitly restarts identity for PGSQL to be consistent with other databases which all do this by default.
217
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
218
     * @return string the SQL statement for truncating a DB table.
219
     */
220 1
    public function truncateTable($table)
221
    {
222 1
        return 'TRUNCATE TABLE ' . $this->db->quoteTableName($table) . ' RESTART IDENTITY';
223
    }
224
225
    /**
226
     * Builds a SQL statement for changing the definition of a column.
227
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
228
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
229
     * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract
230
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
231
     * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
232
     * will become 'varchar(255) not null'. You can also use PostgreSQL-specific syntax such as `SET NOT NULL`.
233
     * @return string the SQL statement for changing the definition of a column.
234
     */
235 2
    public function alterColumn($table, $column, $type)
236
    {
237
        // https://github.com/yiisoft/yii2/issues/4492
238
        // http://www.postgresql.org/docs/9.1/static/sql-altertable.html
239 2
        if (!preg_match('/^(DROP|SET|RESET)\s+/i', $type)) {
240 2
            $type = 'TYPE ' . $this->getColumnType($type);
241
        }
242
243 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN '
244 2
            . $this->db->quoteColumnName($column) . ' ' . $type;
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250 190
    public function insert($table, $columns, &$params)
251
    {
252 190
        return parent::insert($table, $this->normalizeTableRowData($table, $columns), $params);
253
    }
254
255
    /**
256
     * @inheritdoc
257
     * @see https://www.postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
258
     * @see https://stackoverflow.com/questions/1109061/insert-on-duplicate-update-in-postgresql/8702291#8702291
259
     */
260 22
    public function upsert($table, $insertColumns, $updateColumns, &$params)
261
    {
262 22
        $insertColumns = $this->normalizeTableRowData($table, $insertColumns);
263 22
        if (!is_bool($updateColumns)) {
264 7
            $updateColumns = $this->normalizeTableRowData($table, $updateColumns);
265
        }
266 22
        if (version_compare($this->db->getServerVersion(), '9.5', '<')) {
267
            return $this->oldUpsert($table, $insertColumns, $updateColumns, $params);
268
        }
269
270 22
        return $this->newUpsert($table, $insertColumns, $updateColumns, $params);
271
    }
272
273
    /**
274
     * [[upsert()]] implementation for PostgreSQL 9.5 or higher.
275
     * @param string $table
276
     * @param array|Query $insertColumns
277
     * @param array|bool $updateColumns
278
     * @param array $params
279
     * @return string
280
     */
281 22
    private function newUpsert($table, $insertColumns, $updateColumns, &$params)
282
    {
283 22
        $insertSql = $this->insert($table, $insertColumns, $params);
284 22
        [$uniqueNames, , $updateNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns);
0 ignored issues
show
Bug introduced by
The variable $uniqueNames does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $updateNames does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
285 22
        if (empty($uniqueNames)) {
286 3
            return $insertSql;
287
        }
288
289 19
        if ($updateColumns === false) {
290 4
            return "$insertSql ON CONFLICT DO NOTHING";
291
        }
292
293 15
        if ($updateColumns === true) {
294 9
            $updateColumns = [];
295 9
            foreach ($updateNames as $name) {
296 9
                $updateColumns[$name] = new Expression('EXCLUDED.' . $this->db->quoteColumnName($name));
297
            }
298
        }
299 15
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
0 ignored issues
show
Bug introduced by
The variable $updates does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
It seems like $updateColumns defined by parameter $updateColumns on line 281 can also be of type boolean; however, yii\db\QueryBuilder::prepareUpdateSets() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
300 15
        return $insertSql . ' ON CONFLICT (' . implode(', ', $uniqueNames) . ') DO UPDATE SET ' . implode(', ', $updates);
301
    }
302
303
    /**
304
     * [[upsert()]] implementation for PostgreSQL older than 9.5.
305
     * @param string $table
306
     * @param array|Query $insertColumns
307
     * @param array|bool $updateColumns
308
     * @param array $params
309
     * @return string
310
     */
311
    private function oldUpsert($table, $insertColumns, $updateColumns, &$params)
312
    {
313
        /** @var Constraint[] $constraints */
314
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
0 ignored issues
show
Bug introduced by
The variable $uniqueNames does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $insertNames does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $updateNames does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
315
        if (empty($uniqueNames)) {
316
            return $this->insert($table, $insertColumns, $params);
317
        }
318
319
        /** @var Schema $schema */
320
        $schema = $this->db->getSchema();
321
        if (!$insertColumns instanceof Query) {
322
            $tableSchema = $schema->getTableSchema($table);
323
            $columnSchemas = $tableSchema !== null ? $tableSchema->columns : [];
324
            foreach ($insertColumns as $name => $value) {
325
                // NULLs and numeric values must be type hinted in order to be used in SET assigments
326
                // NVM, let's cast them all
327
                if (isset($columnSchemas[$name])) {
328
                    $phName = self::PARAM_PREFIX . count($params);
329
                    $params[$phName] = $value;
330
                    $insertColumns[$name] = new Expression("CAST($phName AS {$columnSchemas[$name]->dbType})");
331
                }
332
            }
333
        }
334
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
0 ignored issues
show
Bug introduced by
The variable $placeholders does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $values does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
335
        $updateCondition = ['or'];
336
        $insertCondition = ['or'];
337
        $quotedTableName = $schema->quoteTableName($table);
338
        foreach ($constraints as $constraint) {
339
            $constraintUpdateCondition = ['and'];
340
            $constraintInsertCondition = ['and'];
341
            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...
342
                $quotedName = $schema->quoteColumnName($name);
343
                $constraintUpdateCondition[] = "$quotedTableName.$quotedName=\"EXCLUDED\".$quotedName";
344
                $constraintInsertCondition[] = "\"upsert\".$quotedName=\"EXCLUDED\".$quotedName";
345
            }
346
            $updateCondition[] = $constraintUpdateCondition;
347
            $insertCondition[] = $constraintInsertCondition;
348
        }
349
        $withSql = 'WITH "EXCLUDED" (' . implode(', ', $insertNames)
350
            . ') AS (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ')';
351
        if ($updateColumns === false) {
352
            $selectSubQuery = (new Query())
353
                ->select(new Expression('1'))
354
                ->from($table)
355
                ->where($updateCondition);
356
            $insertSelectSubQuery = (new Query())
357
                ->select($insertNames)
358
                ->from('EXCLUDED')
359
                ->where(['not exists', $selectSubQuery]);
360
            $insertSql = $this->insert($table, $insertSelectSubQuery, $params);
361
            return "$withSql $insertSql";
362
        }
363
364
        if ($updateColumns === true) {
365
            $updateColumns = [];
366
            foreach ($updateNames as $name) {
367
                $quotedName = $this->db->quoteColumnName($name);
368
                if (strrpos($quotedName, '.') === false) {
369
                    $quotedName = '"EXCLUDED".' . $quotedName;
370
                }
371
                $updateColumns[$name] = new Expression($quotedName);
372
            }
373
        }
374
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
0 ignored issues
show
Bug introduced by
The variable $updates does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
It seems like $updateColumns defined by parameter $updateColumns on line 311 can also be of type boolean; however, yii\db\QueryBuilder::prepareUpdateSets() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
375
        $updateSql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $updates)
376
            . ' FROM "EXCLUDED" ' . $this->buildWhere($updateCondition, $params)
377
            . ' RETURNING ' . $this->db->quoteTableName($table) . '.*';
378
        $selectUpsertSubQuery = (new Query())
379
            ->select(new Expression('1'))
380
            ->from('upsert')
381
            ->where($insertCondition);
382
        $insertSelectSubQuery = (new Query())
383
            ->select($insertNames)
384
            ->from('EXCLUDED')
385
            ->where(['not exists', $selectUpsertSubQuery]);
386
        $insertSql = $this->insert($table, $insertSelectSubQuery, $params);
387
        return "$withSql, \"upsert\" AS ($updateSql) $insertSql";
388
    }
389
390
    /**
391
     * {@inheritdoc}
392
     */
393 43
    public function update($table, $columns, $condition, &$params)
394
    {
395 43
        return parent::update($table, $this->normalizeTableRowData($table, $columns), $condition, $params);
0 ignored issues
show
Bug introduced by
It seems like $this->normalizeTableRowData($table, $columns) targeting yii\db\pgsql\QueryBuilder::normalizeTableRowData() can also be of type object<yii\db\Query>; however, yii\db\QueryBuilder::update() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
396
    }
397
398
    /**
399
     * Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary.
400
     *
401
     * @param string $table the table that data will be saved into.
402
     * @param array|Query $columns the column data (name => value) to be saved into the table or instance
403
     * of [[yii\db\Query|Query]] to perform INSERT INTO ... SELECT SQL statement.
404
     * Passing of [[yii\db\Query|Query]] is available since version 2.0.11.
405
     * @return array normalized columns
406
     * @since 2.0.9
407
     */
408 201
    private function normalizeTableRowData($table, $columns)
409
    {
410 201
        if ($columns instanceof Query) {
411 14
            return $columns;
412
        }
413
414 193
        if (($tableSchema = $this->db->getSchema()->getTableSchema($table)) !== null) {
415 193
            $columnSchemas = $tableSchema->columns;
416 193
            foreach ($columns as $name => $value) {
417 192
                if (isset($columnSchemas[$name]) && $columnSchemas[$name]->type === Schema::TYPE_BINARY && is_string($value)) {
418 192
                    $columns[$name] = new PdoValue($value, \PDO::PARAM_LOB); // explicitly setup PDO param type for binary column
419
                }
420
            }
421
        }
422
423 193
        return $columns;
424
    }
425
426
    /**
427
     * {@inheritdoc}
428
     */
429 16
    public function batchInsert($table, $columns, $rows)
430
    {
431 16
        if (empty($rows)) {
432 2
            return '';
433
        }
434
435 15
        $schema = $this->db->getSchema();
436 15
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
437 12
            $columnSchemas = $tableSchema->columns;
438
        } else {
439 3
            $columnSchemas = [];
440
        }
441
442 15
        $values = [];
443 15
        foreach ($rows as $row) {
444 14
            $vs = [];
445 14
            foreach ($row as $i => $value) {
446 14
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
447 9
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
448
                }
449 14
                if (is_string($value)) {
450 8
                    $value = $schema->quoteValue($value);
451 9
                } elseif (is_float($value)) {
452
                    // ensure type cast always has . as decimal separator in all locales
453 1
                    $value = StringHelper::floatToString($value);
454 9
                } elseif ($value === true) {
455 4
                    $value = 'TRUE';
456 9
                } elseif ($value === false) {
457 7
                    $value = 'FALSE';
458 6
                } elseif ($value === null) {
459 4
                    $value = 'NULL';
460
                }
461 14
                $vs[] = $value;
462
            }
463 14
            $values[] = '(' . implode(', ', $vs) . ')';
464
        }
465 15
        if (empty($values)) {
466 1
            return '';
467
        }
468
469 14
        foreach ($columns as $i => $name) {
470 13
            $columns[$i] = $schema->quoteColumnName($name);
471
        }
472
473 14
        return 'INSERT INTO ' . $schema->quoteTableName($table)
474 14
        . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
475
    }
476
}
477