Skip to content

Commit a572b26

Browse files
authored
fix(database): saving nullable BelongsTo relations (#1584)
1 parent 83320ab commit a572b26

File tree

2 files changed

+55
-4
lines changed

2 files changed

+55
-4
lines changed

packages/database/src/Mappers/SelectModelMapper.php

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22

33
namespace Tempest\Database\Mappers;
44

5-
use Exception;
65
use Tempest\Database\BelongsTo;
76
use Tempest\Database\Builder\ModelInspector;
8-
use Tempest\Database\Exceptions\ModelDidNotHavePrimaryColumn;
97
use Tempest\Database\HasMany;
108
use Tempest\Database\HasOne;
119
use Tempest\Discovery\SkipDiscovery;
@@ -35,7 +33,31 @@ public function map(mixed $from, mixed $to): array
3533
->map(fn (array $rows) => $this->normalizeFields($model, $rows))
3634
->values();
3735

38-
return map($parsed->toArray())->collection()->to($to);
36+
$objects = map($parsed->toArray())->collection()->to($to);
37+
38+
foreach ($objects as $i => $object) {
39+
foreach ($model->getRelations() as $relation) {
40+
// When a nullable BelongsTo relation wasn't loaded, we need to make sure to unset it if it has a default value.
41+
// If we wouldn't do this, the default value would overwrite the "unloaded" value on the next time saving the model
42+
if (! $relation instanceof BelongsTo) {
43+
continue;
44+
}
45+
46+
if (! $relation->property->isNullable()) {
47+
continue;
48+
}
49+
50+
if (! $relation->property->hasDefaultValue()) {
51+
continue;
52+
}
53+
54+
if (! array_key_exists($relation->name, $parsed[$i] ?? [])) {
55+
$relation->property->unset($object);
56+
}
57+
}
58+
}
59+
60+
return $objects;
3961
}
4062

4163
private function normalizeFields(ModelInspector $model, array $rows): array

tests/Integration/Database/Builder/IsDatabaseModelTest.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,12 +626,38 @@ public function test_nullable_relations(): void
626626
CreateANullableTable::class,
627627
);
628628

629-
$a = ANullableModel::create();
629+
$a = ANullableModel::create(
630+
name: 'a',
631+
);
630632

631633
$a->load('b');
632634

633635
$this->assertNull($a->b);
634636
}
637+
638+
public function test_nullable_relation_save(): void
639+
{
640+
$this->migrate(
641+
CreateMigrationsTable::class,
642+
CreateBNullableTable::class,
643+
CreateANullableTable::class,
644+
);
645+
646+
ANullableModel::create(
647+
name: 'a',
648+
b: BNullableModel::new(
649+
name: 'b',
650+
),
651+
);
652+
653+
$a = ANullableModel::select()->first();
654+
$a->save();
655+
656+
$a = ANullableModel::select()->with('b')->first();
657+
658+
$this->assertNotNull($a->b);
659+
$this->assertSame('b', $a->b->name);
660+
}
635661
}
636662

637663
final class Foo
@@ -987,6 +1013,7 @@ public function up(): QueryStatement
9871013
{
9881014
return new CreateTableStatement('a')
9891015
->primary()
1016+
->string('name')
9901017
->belongsTo('a.b_id', 'b.id', nullable: true);
9911018
}
9921019
}
@@ -1009,6 +1036,8 @@ final class ANullableModel
10091036
use IsDatabaseModel;
10101037

10111038
public ?BNullableModel $b = null;
1039+
1040+
public string $name;
10121041
}
10131042

10141043
#[Table('b')]

0 commit comments

Comments
 (0)