Skip to content

Commit

Permalink
feat: casting to numeric types (resolves #8)
Browse files Browse the repository at this point in the history
  • Loading branch information
tpetry committed May 31, 2024
1 parent a12e85a commit 4928a74
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 0 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@ User::select([
> The `Alias` class in isolation is not that usefull because Eloquent can already do this.
> But it will be used more in the next examples.
#### Cast

```php
use Illuminate\Contracts\Database\Query\Expression;
use Tpetry\QueryExpressions\Language\Alias;
use Tpetry\QueryExpressions\Language\Cast;

new Cast(string|Expression $expression, 'int'|'bigint'|'float'|'double' $type)

Invoice::select([
new Alias(new Cast('invoice_number', 'int')),
])->get();
```

#### Case-When

```php
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"pestphp/pest": "^2.28.1",
"pestphp/pest-plugin-laravel": "^2.2.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.11",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^10.5.3",
Expand Down
84 changes: 84 additions & 0 deletions src/Language/Cast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

namespace Tpetry\QueryExpressions\Language;

use Illuminate\Contracts\Database\Query\Expression;
use Illuminate\Database\Grammar;
use RuntimeException;
use Tpetry\QueryExpressions\Concerns\IdentifiesDriver;
use Tpetry\QueryExpressions\Concerns\StringizeExpression;

/**
* @phpstan-type CastType 'bigint'|'double'|'float'|'int'
*/
class Cast implements Expression
{
use IdentifiesDriver;
use StringizeExpression;

/**
* @param CastType $type
*/
public function __construct(
private readonly string|Expression $expression,
private readonly string $type,
) {
}

public function getValue(Grammar $grammar): string
{
$expression = $this->stringize($grammar, $this->expression);

return match ($this->identify($grammar)) {
'mariadb', 'mysql' => $this->castMysql($expression),
'pgsql' => $this->castPgsql($expression),
'sqlite' => $this->castSqlite($expression),
'sqlsrv' => $this->castSqlsrv($expression),
};
}

private function castMysql(float|int|string $expression): string
{
// MySQL 5.7 does not support casting to floating-point numbers. So the workaround is to multiply with one to
// trigger MySQL's automatic type conversion. Technically, this will always produce a double value and never a
// float one, but it will be silently downsized to a float when stored in a table.
return match ($this->type) {
'bigint', 'int' => "cast({$expression} as signed)",
'float', 'double' => "(({$expression})*1.0)",
default => throw new RuntimeException("Unknown cast type '{$this->type}'."), // @phpstan-ignore match.unreachable
};
}

private function castPgsql(float|int|string $expression): string
{
return match ($this->type) {
'bigint' => "cast({$expression} as bigint)",
'float' => "cast({$expression} as real)",
'double' => "cast({$expression} as double precision)",
'int' => "cast({$expression} as int)",
default => throw new RuntimeException("Unknown cast type '{$this->type}'."), // @phpstan-ignore match.unreachable
};
}

private function castSqlite(float|int|string $expression): string
{
return match ($this->type) {
'bigint', 'int' => "cast({$expression} as integer)",
'float', 'double' => "cast({$expression} as real)",
default => throw new RuntimeException("Unknown cast type '{$this->type}'."), // @phpstan-ignore match.unreachable
};
}

private function castSqlsrv(float|int|string $expression): string
{
return match ($this->type) {
'bigint' => "cast({$expression} as bigint)",
'float' => "cast({$expression} as float(24))",
'double' => "cast({$expression} as float(53))",
'int' => "(({$expression})*1)",
default => throw new RuntimeException("Unknown cast type '{$this->type}'."), // @phpstan-ignore match.unreachable
};
}
}
79 changes: 79 additions & 0 deletions tests/Language/CastTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Query\Expression;
use Illuminate\Database\Schema\Blueprint;
use Tpetry\QueryExpressions\Language\Cast;

it('can cast a column to an int')
->expect(new Cast('val', 'int'))
->toBeExecutable(function (Blueprint $table) {
$table->string('val');
})
->toBeMysql('cast(`val` as signed)')
->toBePgsql('cast("val" as int)')
->toBeSqlite('cast("val" as integer)')
->toBeSqlsrv('(([val])*1)');

it('can cast an expression to an int')
->expect(new Cast(new Expression("'42'"), 'int'))
->toBeExecutable()
->toBeMysql("cast('42' as signed)")
->toBePgsql("cast('42' as int)")
->toBeSqlite("cast('42' as integer)")
->toBeSqlsrv("(('42')*1)");

it('can cast a column to a bigint')
->expect(new Cast('val', 'bigint'))
->toBeExecutable(function (Blueprint $table) {
$table->string('val');
})
->toBeMysql('cast(`val` as signed)')
->toBePgsql('cast("val" as bigint)')
->toBeSqlite('cast("val" as integer)')
->toBeSqlsrv('cast([val] as bigint)');

it('can cast an expression to a bigint')
->expect(new Cast(new Expression("'42'"), 'bigint'))
->toBeExecutable()
->toBeMysql("cast('42' as signed)")
->toBePgsql("cast('42' as bigint)")
->toBeSqlite("cast('42' as integer)")
->toBeSqlsrv("cast('42' as bigint)");

it('can cast a column to a float')
->expect(new Cast('val', 'float'))
->toBeExecutable(function (Blueprint $table) {
$table->string('val');
})
->toBeMysql('((`val`)*1.0)')
->toBePgsql('cast("val" as real)')
->toBeSqlite('cast("val" as real)')
->toBeSqlsrv('cast([val] as float(24))');

it('can cast an expression to a float')
->expect(new Cast(new Expression("'42.42'"), 'float'))
->toBeExecutable()
->toBeMysql("(('42.42')*1.0)")
->toBePgsql("cast('42.42' as real)")
->toBeSqlite("cast('42.42' as real)")
->toBeSqlsrv("cast('42.42' as float(24))");

it('can cast a column to a double')
->expect(new Cast('val', 'double'))
->toBeExecutable(function (Blueprint $table) {
$table->string('val');
})
->toBeMysql('((`val`)*1.0)')
->toBePgsql('cast("val" as double precision)')
->toBeSqlite('cast("val" as real)')
->toBeSqlsrv('cast([val] as float(53))');

it('can cast an expression to a double')
->expect(new Cast(new Expression("'42.42'"), 'double'))
->toBeExecutable()
->toBeMysql("(('42.42')*1.0)")
->toBePgsql("cast('42.42' as double precision)")
->toBeSqlite("cast('42.42' as real)")
->toBeSqlsrv("cast('42.42' as float(53))");

0 comments on commit 4928a74

Please sign in to comment.