Skip to content

Add option for typecasting when insert or update values. #949

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 23, 2025
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
- Enh #925, #951: Add callback to `Query::all()` and `Query::one()` methods (@Tigrov, @vjik)
- New #954: Add `DbArrayHelper::arrange()` method (@Tigrov)
- Chg #956: Remove nullable from `PdoConnectionInterface::getActivePdo()` result (@vjik)
- New #949: Add option for typecasting when insert or update values (@Tigrov)
- New #945: Add option for typecasting values retrieved from DB (@Tigrov)
- Enh #941: Add the ability for user-defined type casting (@Tigrov)
- Enh #822: Refactor data readers (@Tigrov)
Expand Down
10 changes: 6 additions & 4 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace
- `QueryInterface::resultCallback()` - allows to use a callback, to be called on all rows of the query result;
- `QueryInterface::getResultCallback()` - returns the callback to be called on all rows of the query result or
`null` if the callback is not set;
- `QueryInterface::withTypecasting()` - enables or disables typecasting of values when retrieving records from DB;
- `QueryPartsInterface::setWhere()` - overwrites the `WHERE` part of the query;
- `ConnectionInterface::getColumnFactory()` - returns the column factory object for concrete DBMS;
- `ConnectionInterface::getServerInfo()` - returns `ServerInfoInterface` instance which provides server information;
Expand All @@ -128,17 +129,18 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace
- `QueryBuilderInterface::prepareValue()` - converts a value to its SQL representation;
- `QueryBuilderInterface::getColumnFactory()` - returns the column factory object for concrete DBMS;
- `QueryBuilderInterface::getServerInfo()` - returns `ServerInfoInterface` instance which provides server information;
- `DMLQueryBuilderInterface::withTypecasting()` - enables or disables typecasting of values when inserting or updating
records in DB;
- `LikeConditionInterface::getCaseSensitive()` - returns whether the comparison is case-sensitive;
- `SchemaInterface::hasTable()` - returns whether the specified table exists in database;
- `SchemaInterface::hasSchema()` - returns whether the specified schema exists in database;
- `SchemaInterface::hasView()` - returns whether the specified view exists in database;
- `DbArrayHelper::arrange()` - arranges an array by specified keys;
- `CommandInterface::withDbTypecasting()` - enables or disables typecasting of values when inserting or updating records;
- `CommandInterface::withPhpTypecasting()` - enables or disables typecasting of values when retrieving records from DB;
- `AbstractCommand::withPhpTypecasting()` - enables or disables typecasting of values when retrieving records from DB;
- `QueryInterface::withTypecasting()` - enables or disables typecasting of values when retrieving records from DB;
- `Query::withTypecasting()` - enables or disables typecasting of values when retrieving records from DB;
- `CommandInterface::withTypecasting()` - enables or disables typecasting of values when inserting, updating
or retrieving records from DB;
- `SchemaInterface::getResultColumn()` - returns the column instance for the column metadata received from the query;
- `AbstractSchema::getResultColumn()` - returns the column instance for the column metadata received from the query;
- `AbstractSchema::getResultColumnCacheKey()` - returns the cache key for the column metadata received from the query;
- `AbstractSchema::loadResultColumn()` - creates a new column instance according to the column metadata from the query;

Expand Down
47 changes: 41 additions & 6 deletions docs/guide/en/schema/typecasting.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ flowchart LR
When saving a value to the database, the value must be in the correct type. For example, if saving a value to a column
that is of type `bit`, the value must be an `integer` or `string` depends on DBMS.

To ensure that the value is saved in the correct type, `ColumnInterface::dbTypecast()` method can be used to cast
the value. Majority of the DB library methods, such as `CommandInterface::insert()`, automatically convert the type.
By default `update()`, `upsert()`, `insert()`, `insertBatch()` and `insertWithReturningPks()` methods
of `CommandInterface` and `QueryBuilderInterface` instances cast the values to the correct type.

```php
use Yiisoft\Db\Connection\ConnectionInterface;
Expand All @@ -33,7 +33,45 @@ $command->execute();
```

In the example above, the value of `is_active` is a `boolean`, but the column `is_active` can be of type `bit`.
The `CommandInterface::insert()` method will automatically cast the value to the correct type.
The `CommandInterface::insert()` method by default cast the value to the correct type.

### Using `ColumnInterface::dbTypecast()`

To ensure that the value is saved in the correct type, `ColumnInterface::dbTypecast()` method can be used to cast
the value.

```php
$isActive = true;
// Cast the value to the correct database type
$isActive = $db->getTableSchema('customer')->getColumn('is_active')->phpTypecast($isActive);

$command = $db->createCommand();
$command->setSql('INSERT INTO {{customer}} (name, is_active) VALUES (:name, :is_active)', [
':name' => 'John Doe',
':is_active' => $isActive,
]);
$command->execute();
```

In the example above, the value of `is_active` is casted to the correct database type before it is saved.

### Using `QueryBuilderInterface::withTypecasting()` or `CommandInterface::withDbTypecasting()`

You can also use `QueryBuilderInterface::withTypecasting()` or `CommandInterface::withDbTypecasting()` methods to
enable or disable type casting.

```php
$command = $db->createCommand()->withDbTypecasting(false);
$command->insert('customer', [
'name' => 'John Doe',
'is_active' => 1,
]);
$command->execute();
```

In the example above, the value of `is_active` is not casted to the correct type before it is saved. The value
is saved as an integer `1` instead of a `bit` or a `bool`. This is useful when you want to save the value as is or
the value is already in the correct type.

## Casting values retrieved from the database

Expand All @@ -47,9 +85,6 @@ To ensure that the value is returned in the correct type, you can use `ColumnInt
the value, in the example above, to a `float`.

```php
use Yiisoft\Db\Connection\ConnectionInterface;

/** @var ConnectionInterface $db */
$command = $db->createCommand('SELECT * FROM {{customer}} WHERE id = 1');

$row = $command->queryOne();
Expand Down
30 changes: 23 additions & 7 deletions src/Command/AbstractCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ abstract class AbstractCommand implements CommandInterface
*/
protected string|null $refreshTableName = null;
protected Closure|null $retryHandler = null;
protected bool $dbTypecasting = true;
protected bool $phpTypecasting = false;
/**
* @var string The SQL statement to execute.
Expand Down Expand Up @@ -457,13 +458,6 @@ public function queryScalar(): bool|string|null|int|float
return is_scalar($result) ? $result : null;
}

public function withPhpTypecasting(bool $phpTypecasting = true): static
{
$new = clone $this;
$new->phpTypecasting = $phpTypecasting;
return $new;
}

public function renameColumn(string $table, string $oldName, string $newName): static
{
$sql = $this->getQueryBuilder()->renameColumn($table, $oldName, $newName);
Expand Down Expand Up @@ -529,6 +523,28 @@ public function upsert(
return $this->setSql($sql)->bindValues($params);
}

public function withDbTypecasting(bool $dbTypecasting = true): static
{
$new = clone $this;
$new->dbTypecasting = $dbTypecasting;
return $new;
}

public function withPhpTypecasting(bool $phpTypecasting = true): static
{
$new = clone $this;
$new->phpTypecasting = $phpTypecasting;
return $new;
}

public function withTypecasting(bool $typecasting = true): static
{
$new = clone $this;
$new->dbTypecasting = $typecasting;
$new->phpTypecasting = $typecasting;
return $new;
}

/**
* @return QueryBuilderInterface The query builder instance.
*/
Expand Down
21 changes: 16 additions & 5 deletions src/Command/CommandInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -619,11 +619,6 @@ public function insert(string $table, QueryInterface|array $columns): static;
*/
public function insertWithReturningPks(string $table, array $columns): array|false;

/**
* Returns a copy of the instance with enabled or disabled typecasting of values when retrieving records from DB.
*/
public function withPhpTypecasting(bool $phpTypecasting = true): static;

/**
* Prepares the SQL statement to be executed.
*
Expand Down Expand Up @@ -894,4 +889,20 @@ public function upsert(
bool|array $updateColumns = true,
array $params = []
): static;

/**
* Returns copy of the instance with enabled or disabled typecasting of values when inserting or updating records.
*/
public function withDbTypecasting(bool $dbTypecasting = true): static;

/**
* Returns a copy of the instance with enabled or disabled typecasting of values when retrieving records from DB.
*/
public function withPhpTypecasting(bool $phpTypecasting = true): static;

/**
* Returns a copy of the instance with enabled or disabled typecasting of values when inserting, updating
* or retrieving records from DB.
*/
public function withTypecasting(bool $typecasting = true): static;
}
32 changes: 24 additions & 8 deletions src/Debug/CommandInterfaceProxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -321,14 +321,6 @@ public function insertWithReturningPks(string $table, array $columns): array|fal
return $this->decorated->{__FUNCTION__}(...func_get_args());
}

/**
* @psalm-suppress MixedArgument
*/
public function withPhpTypecasting(bool $phpTypecasting = true): static
{
return new self($this->decorated->{__FUNCTION__}(...func_get_args()), $this->collector);
}

public function prepare(?bool $forRead = null): void
{
$this->decorated->{__FUNCTION__}(...func_get_args());
Expand Down Expand Up @@ -506,6 +498,30 @@ public function upsert(
return new self($this->decorated->{__FUNCTION__}(...func_get_args()), $this->collector);
}

/**
* @psalm-suppress MixedArgument
*/
public function withDbTypecasting(bool $dbTypecasting = true): static
{
return new self($this->decorated->{__FUNCTION__}(...func_get_args()), $this->collector);
}

/**
* @psalm-suppress MixedArgument
*/
public function withPhpTypecasting(bool $phpTypecasting = true): static
{
return new self($this->decorated->{__FUNCTION__}(...func_get_args()), $this->collector);
}

/**
* @psalm-suppress MixedArgument
*/
public function withTypecasting(bool $typecasting = true): static
{
return new self($this->decorated->{__FUNCTION__}(...func_get_args()), $this->collector);
}

private function collectQueryStart(string $id, string $line): void
{
$this->collector->collectQueryStart(
Expand Down
2 changes: 1 addition & 1 deletion src/Driver/Pdo/AbstractPdoCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ protected function bindPendingParams(): void

protected function getQueryBuilder(): QueryBuilderInterface
{
return $this->db->getQueryBuilder();
return $this->db->getQueryBuilder()->withTypecasting($this->dbTypecasting);
}

protected function getQueryMode(int $queryMode): string
Expand Down
15 changes: 12 additions & 3 deletions src/QueryBuilder/AbstractDMLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
*/
abstract class AbstractDMLQueryBuilder implements DMLQueryBuilderInterface
{
protected bool $typecasting = true;

public function __construct(
protected QueryBuilderInterface $queryBuilder,
protected QuoterInterface $quoter,
Expand Down Expand Up @@ -145,6 +147,13 @@ public function upsert(
throw new NotSupportedException(__METHOD__ . ' is not supported by this DBMS.');
}

public function withTypecasting(bool $typecasting = true): static
{
$new = clone $this;
$new->typecasting = $typecasting;
return $new;
}

/**
* Prepare traversable for batch insert.
*
Expand Down Expand Up @@ -186,7 +195,7 @@ protected function prepareBatchInsertValues(string $table, iterable $rows, array
/** @var string[] $names */
$names = array_values($columnNames);
$keys = array_fill_keys($names, false);
$columns = $this->schema->getTableSchema($table)?->getColumns() ?? [];
$columns = $this->typecasting ? $this->schema->getTableSchema($table)?->getColumns() ?? [] : [];

foreach ($rows as $row) {
$i = 0;
Expand Down Expand Up @@ -339,7 +348,7 @@ protected function prepareInsertValues(string $table, array|QueryInterface $colu
$names = [];
$placeholders = [];
$columns = $this->normalizeColumnNames($columns);
$tableColumns = $this->schema->getTableSchema($table)?->getColumns() ?? [];
$tableColumns = $this->typecasting ? $this->schema->getTableSchema($table)?->getColumns() ?? [] : [];

foreach ($columns as $name => $value) {
$names[] = $this->quoter->quoteColumnName($name);
Expand Down Expand Up @@ -373,7 +382,7 @@ protected function prepareUpdateSets(string $table, array $columns, array $param
{
$sets = [];
$columns = $this->normalizeColumnNames($columns);
$tableColumns = $this->schema->getTableSchema($table)?->getColumns() ?? [];
$tableColumns = $this->typecasting ? $this->schema->getTableSchema($table)?->getColumns() ?? [] : [];

foreach ($columns as $name => $value) {
if (isset($tableColumns[$name])) {
Expand Down
7 changes: 7 additions & 0 deletions src/QueryBuilder/AbstractQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,13 @@ public function upsert(
return $this->dmlBuilder->upsert($table, $insertColumns, $updateColumns, $params);
}

public function withTypecasting(bool $typecasting = true): static
{
$new = clone $this;
$new->dmlBuilder = $new->dmlBuilder->withTypecasting($typecasting);
return $new;
}

/**
* Converts a resource value to its SQL representation or throws an exception if conversion is not possible.
*
Expand Down
2 changes: 2 additions & 0 deletions src/QueryBuilder/DMLQueryBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,6 @@ public function upsert(
bool|array $updateColumns,
array &$params
): string;

public function withTypecasting(bool $typecasting = true): static;
}
Loading