Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/Database/Relations/Concerns/AttachOneOrMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ public function addConstraints()
if (static::$constraints) {
$this->query->where($this->morphType, $this->morphClass);

$this->query->where($this->foreignKey, '=', $this->getParentKey());
// Bind the parent key as a string to match the string `attachment_id` column. A numeric
// binding forces the database to coerce the column for every row, which prevents the
// column's index from being used.
$parentKey = $this->getParentKey();

$this->query->where($this->foreignKey, '=', is_null($parentKey) ? $parentKey : (string) $parentKey);

$this->query->where('field', $this->getFieldName());

Expand Down Expand Up @@ -116,7 +121,19 @@ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder
*/
public function addEagerConstraints(array $models)
{
parent::addEagerConstraints($models);
// Bind the parent keys as strings to match the string `attachment_id` column, rather than
// deferring to the parent implementation, which compares integer keys against the string
// column and prevents the column's index from being used. Null keys are preserved as-is
// so they match nothing, instead of being cast to an empty string.
$this->query->whereIn(
$this->foreignKey,
array_map(
fn ($key) => is_null($key) ? $key : (string) $key,
$this->getKeys($models, $this->localKey)
)
);

$this->query->where($this->morphType, $this->morphClass);

$this->query->where('field', $this->fieldName);
}
Expand Down
75 changes: 75 additions & 0 deletions tests/Database/Relations/AttachOneTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Winter\Storm\Tests\Database\Relations;

use Illuminate\Database\Eloquent\Relations\Relation;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Winter\Storm\Database\Attach\File;
use Winter\Storm\Database\Model;
Expand Down Expand Up @@ -95,6 +96,80 @@ public function testSetRelationValueLaravelRelation()
$this->assertEquals('avatar.png', $user2->displayPicture->file_name);
}

public function testConstraintsBindParentKeyAsString()
{
Model::unguard();
$user = User::create(['name' => 'Stevie', 'email' => 'stevie@example.com']);
Model::reguard();

$bindings = $user->avatar()->getBaseQuery()->getBindings();

// The attachment_id column is a string; the key must be bound as a string so the
// database can use the column's index instead of coercing every row to a number.
$this->assertContains((string) $user->id, $bindings);
$this->assertNotContains($user->id, $bindings);
}

public function testEagerConstraintsBindParentKeysAsStrings()
{
Model::unguard();
$user = User::create(['name' => 'Stevie', 'email' => 'stevie@example.com']);
$user2 = User::create(['name' => 'Joe', 'email' => 'joe@example.com']);
Model::reguard();

$relation = Relation::noConstraints(function () use ($user) {
return $user->avatar();
});
$relation->addEagerConstraints([$user, $user2]);

$bindings = $relation->getBaseQuery()->getBindings();

$this->assertContains((string) $user->id, $bindings);
$this->assertContains((string) $user2->id, $bindings);
$this->assertNotContains($user->id, $bindings);
$this->assertNotContains($user2->id, $bindings);
}

public function testEagerConstraintsPreserveNullParentKeys()
{
Model::unguard();
$user = User::create(['name' => 'Stevie', 'email' => 'stevie@example.com']);
Model::reguard();

$unsavedUser = new User;

$relation = Relation::noConstraints(function () use ($user) {
return $user->avatar();
});
$relation->addEagerConstraints([$user, $unsavedUser]);

$bindings = $relation->getBaseQuery()->getBindings();

// A parent without a key must not be cast to an empty string, which could match
// orphaned rows where attachment_id = ''
$this->assertNotContains('', $bindings);
$this->assertContains((string) $user->id, $bindings);
}

public function testEagerLoadMatchesAttachmentsToParents()
{
Model::unguard();
$user = User::create(['name' => 'Stevie', 'email' => 'stevie@example.com']);
$user2 = User::create(['name' => 'Joe', 'email' => 'joe@example.com']);
Model::reguard();

$user->avatar()->create(['data' => dirname(dirname(__DIR__)) . '/fixtures/attach/avatar.png']);
$user2->avatar()->create(['data' => dirname(dirname(__DIR__)) . '/fixtures/attach/avatar.png']);

$results = User::with('avatar')->whereIn('id', [$user->id, $user2->id])->get();

foreach ($results as $result) {
$this->assertTrue($result->relationLoaded('avatar'));
$this->assertNotNull($result->avatar);
$this->assertEquals($result->id, $result->avatar->attachment_id);
}
}

public function testDeleteFlagDestroyRelationship()
{
Model::unguard();
Expand Down
Loading