Skip to content

Commit

Permalink
Support existence queries on PostgreSQL
Browse files Browse the repository at this point in the history
  • Loading branch information
staudenmeir committed Dec 18, 2018
1 parent f26da67 commit 2d9dd76
Show file tree
Hide file tree
Showing 20 changed files with 474 additions and 17 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class Locale extends Model

Remember to use the `HasJsonRelationships` trait in both the parent and the related model.

**Limitations:** On PostgreSQL, existence queries (`Locale::has('users')`) and `HasManyThrough` relationships don't work with integer keys.
**Limitations:** On PostgreSQL, `HasManyThrough` relationships don't work with integer keys.

### Many-To-Many Relationships

Expand Down
103 changes: 103 additions & 0 deletions src/HasJsonRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Support\Str;
use Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson;
use Staudenmeir\EloquentJsonRelations\Relations\HasManyJson;
use Staudenmeir\EloquentJsonRelations\Relations\Postgres\BelongsTo as BelongsToPostgres;
use Staudenmeir\EloquentJsonRelations\Relations\Postgres\HasMany as HasManyPostgres;
use Staudenmeir\EloquentJsonRelations\Relations\Postgres\HasOne as HasOnePostgres;
use Staudenmeir\EloquentJsonRelations\Relations\Postgres\MorphMany as MorphManyPostgres;
use Staudenmeir\EloquentJsonRelations\Relations\Postgres\MorphOne as MorphOnePostgres;

trait HasJsonRelationships
{
Expand Down Expand Up @@ -52,6 +62,99 @@ public function getAttributeValue($key)
return parent::getAttributeValue($key);
}

/**
* Instantiate a new HasOne relationship.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $foreignKey
* @param string $localKey
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
protected function newHasOne(Builder $query, Model $parent, $foreignKey, $localKey)
{
if ($query->getConnection()->getDriverName() === 'pgsql') {
return new HasOnePostgres($query, $parent, $foreignKey, $localKey);
}

return new HasOne($query, $parent, $foreignKey, $localKey);
}

/**
* Instantiate a new MorphOne relationship.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $type
* @param string $id
* @param string $localKey
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
*/
protected function newMorphOne(Builder $query, Model $parent, $type, $id, $localKey)
{
if ($query->getConnection()->getDriverName() === 'pgsql') {
return new MorphOnePostgres($query, $parent, $type, $id, $localKey);
}

return new MorphOne($query, $parent, $type, $id, $localKey);
}

/**
* Instantiate a new BelongsTo relationship.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $child
* @param string $foreignKey
* @param string $ownerKey
* @param string $relation
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
protected function newBelongsTo(Builder $query, Model $child, $foreignKey, $ownerKey, $relation)
{
if ($query->getConnection()->getDriverName() === 'pgsql') {
return new BelongsToPostgres($query, $child, $foreignKey, $ownerKey, $relation);
}

return new BelongsTo($query, $child, $foreignKey, $ownerKey, $relation);
}

/**
* Instantiate a new HasMany relationship.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $foreignKey
* @param string $localKey
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
protected function newHasMany(Builder $query, Model $parent, $foreignKey, $localKey)
{
if ($query->getConnection()->getDriverName() === 'pgsql') {
return new HasManyPostgres($query, $parent, $foreignKey, $localKey);
}

return new HasMany($query, $parent, $foreignKey, $localKey);
}

/**
* Instantiate a new MorphMany relationship.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $type
* @param string $id
* @param string $localKey
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
protected function newMorphMany(Builder $query, Model $parent, $type, $id, $localKey)
{
if ($query->getConnection()->getDriverName() === 'pgsql') {
return new MorphManyPostgres($query, $parent, $type, $id, $localKey);
}

return new MorphMany($query, $parent, $type, $id, $localKey);
}

/**
* Define an inverse one-to-one or many JSON relationship.
*
Expand Down
55 changes: 55 additions & 0 deletions src/Relations/Postgres/BelongsTo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace Staudenmeir\EloquentJsonRelations\Relations\Postgres;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo as Base;

class BelongsTo extends Base
{
use IsPostgresRelation;

/**
* Add the constraints for a relationship query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
* @param array|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
if ($parentQuery->getQuery()->from == $query->getQuery()->from) {
return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns);
}

$first = $this->jsonColumn($query, $this->related, $this->getQualifiedForeignKey(), $this->ownerKey);

return $query->select($columns)->whereColumn(
$first, '=', $query->qualifyColumn($this->ownerKey)
);
}

/**
* Add the constraints for a relationship query on the same table.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
* @param array|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
{
$query->select($columns)->from(
$query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash()
);

$query->getModel()->setTable($hash);

$first = $this->jsonColumn($query, $this->related, $this->getQualifiedForeignKey(), $this->ownerKey);

return $query->whereColumn(
$first, $hash.'.'.$this->ownerKey
);
}
}
10 changes: 10 additions & 0 deletions src/Relations/Postgres/HasMany.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Staudenmeir\EloquentJsonRelations\Relations\Postgres;

use Illuminate\Database\Eloquent\Relations\HasMany as Base;

class HasMany extends Base
{
use HasOneOrMany;
}
10 changes: 10 additions & 0 deletions src/Relations/Postgres/HasOne.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Staudenmeir\EloquentJsonRelations\Relations\Postgres;

use Illuminate\Database\Eloquent\Relations\HasOne as Base;

class HasOne extends Base
{
use HasOneOrMany;
}
65 changes: 65 additions & 0 deletions src/Relations/Postgres/HasOneOrMany.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Staudenmeir\EloquentJsonRelations\Relations\Postgres;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

trait HasOneOrMany
{
use IsPostgresRelation;

/**
* Add the constraints for a relationship query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
* @param array|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
if ($query->getQuery()->from == $parentQuery->getQuery()->from) {
return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns);
}

$second = $this->jsonColumn($query, $this->parent, $this->getExistenceCompareKey(), $this->localKey);

return $query->select($columns)->whereColumn(
$this->getQualifiedParentKeyName(), '=', $second
);
}

/**
* Add the constraints for a relationship query on the same table.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
* @param array|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
{
$query->from($query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash());

$query->getModel()->setTable($hash);

$second = $this->jsonColumn($query, $this->parent, $hash.'.'.$this->getForeignKeyName(), $this->localKey);

return $query->select($columns)->whereColumn(
$this->getQualifiedParentKeyName(), '=', $second
);
}

/**
* Get the name of the "where in" method for eager loading.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @return string
*/
protected function whereInMethod(Model $model, $key)
{
return 'whereIn';
}
}
30 changes: 30 additions & 0 deletions src/Relations/Postgres/IsPostgresRelation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Staudenmeir\EloquentJsonRelations\Relations\Postgres;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Expression;

trait IsPostgresRelation
{
/**
* Get the wrapped and cast JSON column.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $column
* @param string $key
* @return \Illuminate\Database\Query\Expression
*/
protected function jsonColumn(Builder $query, Model $model, $column, $key)
{
$sql = $query->getQuery()->getGrammar()->wrap($column);

if ($model->getKeyName() === $key && in_array($model->getKeyType(), ['int', 'integer'])) {
$sql = '('.$sql.')::int';
}

return new Expression($sql);
}
}
10 changes: 10 additions & 0 deletions src/Relations/Postgres/MorphMany.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Staudenmeir\EloquentJsonRelations\Relations\Postgres;

use Illuminate\Database\Eloquent\Relations\MorphMany as Base;

class MorphMany extends Base
{
use MorphOneOrMany;
}
10 changes: 10 additions & 0 deletions src/Relations/Postgres/MorphOne.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Staudenmeir\EloquentJsonRelations\Relations\Postgres;

use Illuminate\Database\Eloquent\Relations\MorphOne as Base;

class MorphOne extends Base
{
use MorphOneOrMany;
}
26 changes: 26 additions & 0 deletions src/Relations/Postgres/MorphOneOrMany.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Staudenmeir\EloquentJsonRelations\Relations\Postgres;

use Illuminate\Database\Eloquent\Builder;

trait MorphOneOrMany
{
use HasOneOrMany {
getRelationExistenceQuery as getRelationExistenceQueryParent;
}

/**
* Add the constraints for a relationship query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
* @param array|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
return $this->getRelationExistenceQueryParent($query, $parentQuery, $columns)
->where($this->morphType, $this->morphClass);
}
}
8 changes: 8 additions & 0 deletions tests/BelongsToTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tests;

use Tests\Models\Comment;
use Tests\Models\User;

class BelongsToTest extends TestCase
Expand Down Expand Up @@ -34,6 +35,13 @@ public function testExistenceQuery()
$this->assertEquals([1], $users->pluck('id')->all());
}

public function testExistenceQueryForSelfRelation()
{
$comments = Comment::has('parent')->get();

$this->assertEquals([2], $comments->pluck('id')->all());
}

public function testAssociate()
{
$user = (new User)->locale()->associate(1);
Expand Down
8 changes: 8 additions & 0 deletions tests/HasManyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tests;

use Tests\Models\Comment;
use Tests\Models\Locale;
use Tests\Models\User;

Expand Down Expand Up @@ -35,6 +36,13 @@ public function testExistenceQuery()
$this->assertEquals([1], $locales->pluck('id')->all());
}

public function testExistenceQueryForSelfRelation()
{
$comments = Comment::has('children')->get();

$this->assertEquals([1], $comments->pluck('id')->all());
}

public function testSave()
{
$user = Locale::first()->users()->save(User::find(2));
Expand Down
Loading

0 comments on commit 2d9dd76

Please sign in to comment.