Skip to content

Commit 4928a74

Browse files
committed
feat: casting to numeric types (resolves #8)
1 parent a12e85a commit 4928a74

File tree

4 files changed

+178
-0
lines changed

4 files changed

+178
-0
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,20 @@ User::select([
9898
> The `Alias` class in isolation is not that usefull because Eloquent can already do this.
9999
> But it will be used more in the next examples.
100100
101+
#### Cast
102+
103+
```php
104+
use Illuminate\Contracts\Database\Query\Expression;
105+
use Tpetry\QueryExpressions\Language\Alias;
106+
use Tpetry\QueryExpressions\Language\Cast;
107+
108+
new Cast(string|Expression $expression, 'int'|'bigint'|'float'|'double' $type)
109+
110+
Invoice::select([
111+
new Alias(new Cast('invoice_number', 'int')),
112+
])->get();
113+
```
114+
101115
#### Case-When
102116

103117
```php

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"pestphp/pest": "^2.28.1",
3131
"pestphp/pest-plugin-laravel": "^2.2.0",
3232
"phpstan/extension-installer": "^1.1",
33+
"phpstan/phpstan": "^1.11",
3334
"phpstan/phpstan-deprecation-rules": "^1.0",
3435
"phpstan/phpstan-phpunit": "^1.0",
3536
"phpunit/phpunit": "^10.5.3",

src/Language/Cast.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tpetry\QueryExpressions\Language;
6+
7+
use Illuminate\Contracts\Database\Query\Expression;
8+
use Illuminate\Database\Grammar;
9+
use RuntimeException;
10+
use Tpetry\QueryExpressions\Concerns\IdentifiesDriver;
11+
use Tpetry\QueryExpressions\Concerns\StringizeExpression;
12+
13+
/**
14+
* @phpstan-type CastType 'bigint'|'double'|'float'|'int'
15+
*/
16+
class Cast implements Expression
17+
{
18+
use IdentifiesDriver;
19+
use StringizeExpression;
20+
21+
/**
22+
* @param CastType $type
23+
*/
24+
public function __construct(
25+
private readonly string|Expression $expression,
26+
private readonly string $type,
27+
) {
28+
}
29+
30+
public function getValue(Grammar $grammar): string
31+
{
32+
$expression = $this->stringize($grammar, $this->expression);
33+
34+
return match ($this->identify($grammar)) {
35+
'mariadb', 'mysql' => $this->castMysql($expression),
36+
'pgsql' => $this->castPgsql($expression),
37+
'sqlite' => $this->castSqlite($expression),
38+
'sqlsrv' => $this->castSqlsrv($expression),
39+
};
40+
}
41+
42+
private function castMysql(float|int|string $expression): string
43+
{
44+
// MySQL 5.7 does not support casting to floating-point numbers. So the workaround is to multiply with one to
45+
// trigger MySQL's automatic type conversion. Technically, this will always produce a double value and never a
46+
// float one, but it will be silently downsized to a float when stored in a table.
47+
return match ($this->type) {
48+
'bigint', 'int' => "cast({$expression} as signed)",
49+
'float', 'double' => "(({$expression})*1.0)",
50+
default => throw new RuntimeException("Unknown cast type '{$this->type}'."), // @phpstan-ignore match.unreachable
51+
};
52+
}
53+
54+
private function castPgsql(float|int|string $expression): string
55+
{
56+
return match ($this->type) {
57+
'bigint' => "cast({$expression} as bigint)",
58+
'float' => "cast({$expression} as real)",
59+
'double' => "cast({$expression} as double precision)",
60+
'int' => "cast({$expression} as int)",
61+
default => throw new RuntimeException("Unknown cast type '{$this->type}'."), // @phpstan-ignore match.unreachable
62+
};
63+
}
64+
65+
private function castSqlite(float|int|string $expression): string
66+
{
67+
return match ($this->type) {
68+
'bigint', 'int' => "cast({$expression} as integer)",
69+
'float', 'double' => "cast({$expression} as real)",
70+
default => throw new RuntimeException("Unknown cast type '{$this->type}'."), // @phpstan-ignore match.unreachable
71+
};
72+
}
73+
74+
private function castSqlsrv(float|int|string $expression): string
75+
{
76+
return match ($this->type) {
77+
'bigint' => "cast({$expression} as bigint)",
78+
'float' => "cast({$expression} as float(24))",
79+
'double' => "cast({$expression} as float(53))",
80+
'int' => "(({$expression})*1)",
81+
default => throw new RuntimeException("Unknown cast type '{$this->type}'."), // @phpstan-ignore match.unreachable
82+
};
83+
}
84+
}

tests/Language/CastTest.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Database\Query\Expression;
6+
use Illuminate\Database\Schema\Blueprint;
7+
use Tpetry\QueryExpressions\Language\Cast;
8+
9+
it('can cast a column to an int')
10+
->expect(new Cast('val', 'int'))
11+
->toBeExecutable(function (Blueprint $table) {
12+
$table->string('val');
13+
})
14+
->toBeMysql('cast(`val` as signed)')
15+
->toBePgsql('cast("val" as int)')
16+
->toBeSqlite('cast("val" as integer)')
17+
->toBeSqlsrv('(([val])*1)');
18+
19+
it('can cast an expression to an int')
20+
->expect(new Cast(new Expression("'42'"), 'int'))
21+
->toBeExecutable()
22+
->toBeMysql("cast('42' as signed)")
23+
->toBePgsql("cast('42' as int)")
24+
->toBeSqlite("cast('42' as integer)")
25+
->toBeSqlsrv("(('42')*1)");
26+
27+
it('can cast a column to a bigint')
28+
->expect(new Cast('val', 'bigint'))
29+
->toBeExecutable(function (Blueprint $table) {
30+
$table->string('val');
31+
})
32+
->toBeMysql('cast(`val` as signed)')
33+
->toBePgsql('cast("val" as bigint)')
34+
->toBeSqlite('cast("val" as integer)')
35+
->toBeSqlsrv('cast([val] as bigint)');
36+
37+
it('can cast an expression to a bigint')
38+
->expect(new Cast(new Expression("'42'"), 'bigint'))
39+
->toBeExecutable()
40+
->toBeMysql("cast('42' as signed)")
41+
->toBePgsql("cast('42' as bigint)")
42+
->toBeSqlite("cast('42' as integer)")
43+
->toBeSqlsrv("cast('42' as bigint)");
44+
45+
it('can cast a column to a float')
46+
->expect(new Cast('val', 'float'))
47+
->toBeExecutable(function (Blueprint $table) {
48+
$table->string('val');
49+
})
50+
->toBeMysql('((`val`)*1.0)')
51+
->toBePgsql('cast("val" as real)')
52+
->toBeSqlite('cast("val" as real)')
53+
->toBeSqlsrv('cast([val] as float(24))');
54+
55+
it('can cast an expression to a float')
56+
->expect(new Cast(new Expression("'42.42'"), 'float'))
57+
->toBeExecutable()
58+
->toBeMysql("(('42.42')*1.0)")
59+
->toBePgsql("cast('42.42' as real)")
60+
->toBeSqlite("cast('42.42' as real)")
61+
->toBeSqlsrv("cast('42.42' as float(24))");
62+
63+
it('can cast a column to a double')
64+
->expect(new Cast('val', 'double'))
65+
->toBeExecutable(function (Blueprint $table) {
66+
$table->string('val');
67+
})
68+
->toBeMysql('((`val`)*1.0)')
69+
->toBePgsql('cast("val" as double precision)')
70+
->toBeSqlite('cast("val" as real)')
71+
->toBeSqlsrv('cast([val] as float(53))');
72+
73+
it('can cast an expression to a double')
74+
->expect(new Cast(new Expression("'42.42'"), 'double'))
75+
->toBeExecutable()
76+
->toBeMysql("(('42.42')*1.0)")
77+
->toBePgsql("cast('42.42' as double precision)")
78+
->toBeSqlite("cast('42.42' as real)")
79+
->toBeSqlsrv("cast('42.42' as float(53))");

0 commit comments

Comments
 (0)