Skip to content

Commit 54a44b8

Browse files
committed
feat(migration): USING modifier for column type changes
1 parent 55182aa commit 54a44b8

File tree

5 files changed

+96
-10
lines changed

5 files changed

+96
-10
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ composer require tpetry/laravel-postgresql-enhanced
4040
- [Column Options](#column-options)
4141
- [Compression](#compression)
4242
- [Initial](#initial)
43+
- [Using](#using)
4344
- [Column Types](#column-types)
4445
- [Arrays](#arrays)
4546
- [Ranges](#ranges)
@@ -644,6 +645,24 @@ Schema::table('users', function (Blueprint $table): void {
644645
});
645646
```
646647

648+
#### Using
649+
650+
PostgreSQL forbids some data type changes in migrations when they violate the type system.
651+
You can't, e.g., change a `varchar` column storing one email address into a `jsonb` array storing multiple email addresses, as PostgreSQL doesn't know how to convert between these types automatically.
652+
You would get this error:
653+
654+
```
655+
SQLSTATE[42804]: Datatype mismatch: 7 ERROR: column "email" cannot be cast automatically to type jsonb
656+
```
657+
658+
You can specify an expression how the current value has to be transformed to the new type with the `using()` modifier:
659+
660+
```php
661+
Schema::table('users', function (Blueprint $table): void {
662+
$table->jsonb('email')->using('jsonb_build_array(email)')->change();
663+
});
664+
```
665+
647666
### Column Types
648667

649668
#### Arrays

src/Query/Grammar.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class Grammar extends PostgresGrammar
3333
/**
3434
* Compile a delete statement into SQL.
3535
*
36-
* @param \Tpetry\PostgresqlEnhanced\Query\Builder $query
36+
* @param Builder $query
3737
*/
3838
public function compileDelete(BaseBuilder $query): string
3939
{
@@ -43,7 +43,7 @@ public function compileDelete(BaseBuilder $query): string
4343
/**
4444
* Compile an insert statement into SQL.
4545
*
46-
* @param \Tpetry\PostgresqlEnhanced\Query\Builder $query
46+
* @param Builder $query
4747
*/
4848
public function compileInsert(BaseBuilder $query, array $values): string
4949
{
@@ -53,7 +53,7 @@ public function compileInsert(BaseBuilder $query, array $values): string
5353
/**
5454
* Compile an insert statement using a subquery into SQL.
5555
*
56-
* @param \Tpetry\PostgresqlEnhanced\Query\Builder $query
56+
* @param Builder $query
5757
*/
5858
public function compileInsertUsing(BaseBuilder $query, array $columns, string $sql): string
5959
{
@@ -63,7 +63,7 @@ public function compileInsertUsing(BaseBuilder $query, array $columns, string $s
6363
/**
6464
* Compile a select query into SQL.
6565
*
66-
* @param \Tpetry\PostgresqlEnhanced\Query\Builder $query
66+
* @param Builder $query
6767
*/
6868
public function compileSelect(BaseBuilder $query): string
6969
{
@@ -73,7 +73,7 @@ public function compileSelect(BaseBuilder $query): string
7373
/**
7474
* Compile an update statement into SQL.
7575
*
76-
* @param \Tpetry\PostgresqlEnhanced\Query\Builder $query
76+
* @param Builder $query
7777
*/
7878
public function compileUpdate(BaseBuilder $query, array $values): string
7979
{
@@ -83,7 +83,7 @@ public function compileUpdate(BaseBuilder $query, array $values): string
8383
/**
8484
* Compile an update from statement into SQL.
8585
*
86-
* @param \Tpetry\PostgresqlEnhanced\Query\Builder $query
86+
* @param Builder $query
8787
*/
8888
public function compileUpdateFrom(BaseBuilder $query, $values): string
8989
{

src/Schema/Grammars/GrammarTypes.php

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,40 @@
77
use Illuminate\Database\Connection;
88
use Illuminate\Database\Schema\Blueprint as BaseBlueprint;
99
use Illuminate\Support\Fluent;
10+
use Illuminate\Support\Str;
1011
use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
1112

1213
trait GrammarTypes
1314
{
14-
public function compileChange(BaseBlueprint $blueprint, Fluent $command, Connection $connection)
15+
/**
16+
* Compile a change column command into a series of SQL statements.
17+
*/
18+
public function compileChange(BaseBlueprint $blueprint, Fluent $command, Connection $connection): array
1519
{
16-
$queries = parent::compileChange($blueprint, $command, $connection);
20+
$queries = [];
21+
22+
// The table prefix is accessed differently based on Laravel version. In old version the $prefix was public,
23+
// while with new ones the $blueprint->prefix() method should be used. The issue is solved by invading the
24+
// object and getting the property directly.
25+
$prefix = (fn () => $this->prefix)->call($blueprint);
26+
1727
foreach ($blueprint->getChangedColumns() as $changedColumn) {
28+
$blueprintColumn = new BaseBlueprint($blueprint->getTable(), null, $prefix);
29+
$blueprintColumn->addColumn($changedColumn['type'], $changedColumn['name'], $changedColumn->toArray());
30+
31+
foreach (parent::compileChange($blueprintColumn, $command, $connection) as $sql) {
32+
if (filled($changedColumn['using']) && Str::is('ALTER TABLE * ALTER * TYPE *', $sql)) {
33+
$using = match ($connection->getSchemaGrammar()->isExpression($changedColumn['using'])) {
34+
true => $connection->getSchemaGrammar()->getValue($changedColumn['using']),
35+
false => $changedColumn['using'],
36+
};
37+
38+
$queries[] = "{$sql} USING {$using}";
39+
} else {
40+
$queries[] = $sql;
41+
}
42+
}
43+
1844
if (filled($changedColumn['compression'])) {
1945
$queries[] = sprintf(
2046
'ALTER TABLE %s ALTER %s SET COMPRESSION %s',

src/Support/Phpstan/SchemaColumnDefinitionExtension.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Tpetry\PostgresqlEnhanced\Support\Phpstan;
66

7+
use Illuminate\Contracts\Database\Query\Expression as ExpressionContract;
78
use Illuminate\Database\Schema\ColumnDefinition;
89
use PHPStan\Reflection\ClassReflection;
910
use PHPStan\Reflection\FunctionVariant;
@@ -19,13 +20,14 @@
1920
class SchemaColumnDefinitionExtension implements MethodsClassReflectionExtension
2021
{
2122
/**
22-
* @param 'initial'|'compression' $methodName
23+
* @param 'compression'|'initial'|'using' $methodName
2324
*/
2425
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
2526
{
2627
return match ($methodName) {
2728
'initial' => $this->getInitialMethod($classReflection),
2829
'compression' => $this->getCompressionMethod($classReflection),
30+
'using' => $this->getUsingMethod($classReflection),
2931
};
3032
}
3133

@@ -35,7 +37,7 @@ public function hasMethod(ClassReflection $classReflection, string $methodName):
3537
return false;
3638
}
3739

38-
return \in_array($methodName, ['compression', 'initial']);
40+
return \in_array($methodName, ['compression', 'initial', 'using']);
3941
}
4042

4143
private function getCompressionMethod(ClassReflection $classReflection): MethodReflection
@@ -65,4 +67,20 @@ classReflection: $classReflection,
6567
],
6668
);
6769
}
70+
71+
private function getUsingMethod(ClassReflection $classReflection): MethodReflection
72+
{
73+
$parametersExpression = [new ReflectedParameter('expression', new ObjectType(ExpressionContract::class))];
74+
$parametersString = [new ReflectedParameter('expression', new StringType())];
75+
$returnType = new ObjectType(ColumnDefinition::class);
76+
77+
return new ReflectedMethod(
78+
classReflection: $classReflection,
79+
name: 'using',
80+
variants: [
81+
new FunctionVariant(TemplateTypeMap::createEmpty(), null, $parametersExpression, false, $returnType),
82+
new FunctionVariant(TemplateTypeMap::createEmpty(), null, $parametersString, false, $returnType),
83+
],
84+
);
85+
}
6886
}

tests/Migration/TypesTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Tpetry\PostgresqlEnhanced\Tests\Migration;
66

77
use Closure;
8+
use Illuminate\Database\Query\Expression;
89
use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
910
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;
1011
use Tpetry\PostgresqlEnhanced\Tests\TestCase;
@@ -64,6 +65,28 @@ public function testColumnModifierCompressionIsSupported(): void
6465
$this->assertEquals('ALTER TABLE "test" ALTER "col" SET COMPRESSION "lz4"', $queries[1]['query'] ?? null);
6566
}
6667

68+
public function testColumnModifierUsingIsSupported(): void
69+
{
70+
$queries = $this->runMigrations(
71+
fnCreate: fn (Blueprint $table) => $table->string('col'),
72+
fnChange: fn (Blueprint $table) => $table->json('col')->using('json_build_array(col)')->change(),
73+
);
74+
75+
$this->assertEquals('create table "test" ("col" varchar(255) not null)', $queries[0]['query'] ?? null);
76+
$this->assertEquals('ALTER TABLE test ALTER col TYPE JSON USING json_build_array(col)', $queries[1]['query'] ?? null);
77+
}
78+
79+
public function testColumnModifierWithExpressionUsingIsSupported(): void
80+
{
81+
$queries = $this->runMigrations(
82+
fnCreate: fn (Blueprint $table) => $table->string('col'),
83+
fnChange: fn (Blueprint $table) => $table->json('col')->using(new Expression('json_build_array(col)'))->change(),
84+
);
85+
86+
$this->assertEquals('create table "test" ("col" varchar(255) not null)', $queries[0]['query'] ?? null);
87+
$this->assertEquals('ALTER TABLE test ALTER col TYPE JSON USING json_build_array(col)', $queries[1]['query'] ?? null);
88+
}
89+
6790
public function testDateMultiRangeTypeIsSupported(): void
6891
{
6992
$queries = $this->runMigrations(

0 commit comments

Comments
 (0)