Skip to content

Commit

Permalink
[11.x] Support eager loading with limit (#49695)
Browse files Browse the repository at this point in the history
* Support eager loading with limit

* Fix style

* formatting

---------

Co-authored-by: Taylor Otwell <[email protected]>
  • Loading branch information
staudenmeir and taylorotwell authored Jan 15, 2024
1 parent ec6e169 commit 8f29cc9
Show file tree
Hide file tree
Showing 11 changed files with 534 additions and 1 deletion.
37 changes: 37 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot;
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable;
use Illuminate\Database\Query\Grammars\MySqlGrammar;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Support\Str;
use InvalidArgumentException;
Expand Down Expand Up @@ -1347,6 +1348,42 @@ public function getRelationExistenceQueryForSelfJoin(Builder $query, Builder $pa
return parent::getRelationExistenceQuery($query, $parentQuery, $columns);
}

/**
* Alias to set the "limit" value of the query.
*
* @param int $value
* @return $this
*/
public function take($value)
{
return $this->limit($value);
}

/**
* Set the "limit" value of the query.
*
* @param int $value
* @return $this
*/
public function limit($value)
{
if ($this->parent->exists) {
$this->query->limit($value);
} else {
$column = $this->getExistenceCompareKey();

$grammar = $this->query->getQuery()->getGrammar();

if ($grammar instanceof MySqlGrammar && $grammar->useLegacyGroupLimit($this->query->getQuery())) {
$column = 'pivot_'.last(explode('.', $column));
}

$this->query->groupLimit($value, $column);
}

return $this;
}

/**
* Get the key for comparing against the parent key in "has" query.
*
Expand Down
37 changes: 37 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Grammars\MySqlGrammar;
use Illuminate\Database\UniqueConstraintViolationException;

class HasManyThrough extends Relation
Expand Down Expand Up @@ -762,6 +763,42 @@ public function getRelationExistenceQueryForThroughSelfRelation(Builder $query,
);
}

/**
* Alias to set the "limit" value of the query.
*
* @param int $value
* @return $this
*/
public function take($value)
{
return $this->limit($value);
}

/**
* Set the "limit" value of the query.
*
* @param int $value
* @return $this
*/
public function limit($value)
{
if ($this->farParent->exists) {
$this->query->limit($value);
} else {
$column = $this->getQualifiedFirstKeyName();

$grammar = $this->query->getQuery()->getGrammar();

if ($grammar instanceof MySqlGrammar && $grammar->useLegacyGroupLimit($this->query->getQuery())) {
$column = 'laravel_through_key';
}

$this->query->groupLimit($value, $column);
}

return $this;
}

/**
* Get the qualified foreign key on the related model.
*
Expand Down
28 changes: 28 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,34 @@ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder
);
}

/**
* Alias to set the "limit" value of the query.
*
* @param int $value
* @return $this
*/
public function take($value)
{
return $this->limit($value);
}

/**
* Set the "limit" value of the query.
*
* @param int $value
* @return $this
*/
public function limit($value)
{
if ($this->parent->exists) {
$this->query->limit($value);
} else {
$this->query->groupLimit($value, $this->getExistenceCompareKey());
}

return $this;
}

/**
* Get the key for comparing against the parent key in "has" query.
*
Expand Down
55 changes: 54 additions & 1 deletion src/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ class Builder implements BuilderContract
*/
public $limit;

/**
* The maximum number of records to return per group.
*
* @var array
*/
public $groupLimit;

/**
* The number of records to skip.
*
Expand Down Expand Up @@ -2444,6 +2451,22 @@ public function limit($value)
return $this;
}

/**
* Add a "group limit" clause to the query.
*
* @param int $value
* @param string $column
* @return $this
*/
public function groupLimit($value, $column)
{
if ($value >= 0) {
$this->groupLimit = compact('value', 'column');
}

return $this;
}

/**
* Set the limit and offset for a given page.
*
Expand Down Expand Up @@ -2737,9 +2760,13 @@ public function soleValue($column)
*/
public function get($columns = ['*'])
{
return collect($this->onceWithColumns(Arr::wrap($columns), function () {
$items = collect($this->onceWithColumns(Arr::wrap($columns), function () {
return $this->processor->processSelect($this, $this->runSelect());
}));

return isset($this->groupLimit)
? $this->withoutGroupLimitKeys($items)
: $items;
}

/**
Expand All @@ -2754,6 +2781,32 @@ protected function runSelect()
);
}

/**
* Remove the group limit keys from the results in the collection.
*
* @param \Illuminate\Support\Collection $items
* @return \Illuminate\Support\Collection
*/
protected function withoutGroupLimitKeys($items)
{
$keysToRemove = ['laravel_row'];

if (is_string($this->groupLimit['column'])) {
$column = last(explode('.', $this->groupLimit['column']));

$keysToRemove[] = '@laravel_group := '.$this->grammar->wrap($column);
$keysToRemove[] = '@laravel_group := '.$this->grammar->wrap('pivot_'.$column);
}

$items->each(function ($item) use ($keysToRemove) {
foreach ($keysToRemove as $key) {
unset($item->$key);
}
});

return $items;
}

/**
* Paginate the given query into a simple paginator.
*
Expand Down
71 changes: 71 additions & 0 deletions src/Illuminate/Database/Query/Grammars/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ public function compileSelect(Builder $query)
return $this->compileUnionAggregate($query);
}

// If a "group limit" is in place, we will need to compile the SQL to use a
// different syntax. This primarily supports limits on eager loads using
// Eloquent. We'll also set the columns if they have not been defined.
if (isset($query->groupLimit)) {
if (is_null($query->columns)) {
$query->columns = ['*'];
}

return $this->compileGroupLimit($query);
}

// If the query does not have any columns set, we'll set the columns to the
// * character to just get all of the columns from the database. Then we
// can build the query and concatenate all the pieces together as one.
Expand Down Expand Up @@ -917,6 +928,66 @@ protected function compileLimit(Builder $query, $limit)
return 'limit '.(int) $limit;
}

/**
* Compile a group limit clause.
*
* @param \Illuminate\Database\Query\Builder $query
* @return string
*/
protected function compileGroupLimit(Builder $query)
{
$selectBindings = array_merge($query->getRawBindings()['select'], $query->getRawBindings()['order']);

$query->setBindings($selectBindings, 'select');
$query->setBindings([], 'order');

$limit = (int) $query->groupLimit['value'];
$offset = $query->offset;

if (isset($offset)) {
$offset = (int) $offset;
$limit += $offset;

$query->offset = null;
}

$components = $this->compileComponents($query);

$components['columns'] .= $this->compileRowNumber(
$query->groupLimit['column'],
$components['orders'] ?? ''
);

unset($components['orders']);

$table = $this->wrap('laravel_table');
$row = $this->wrap('laravel_row');

$sql = $this->concatenate($components);

$sql = 'select * from ('.$sql.') as '.$table.' where '.$row.' <= '.$limit;

if (isset($offset)) {
$sql .= ' and '.$row.' > '.$offset;
}

return $sql.' order by '.$row;
}

/**
* Compile a row number clause.
*
* @param string $partition
* @param string $orders
* @return string
*/
protected function compileRowNumber($partition, $orders)
{
$over = trim('partition by '.$this->wrap($partition).' '.$orders);

return ', row_number() over ('.$over.') as '.$this->wrap('laravel_row');
}

/**
* Compile the "offset" portions of the query.
*
Expand Down
77 changes: 77 additions & 0 deletions src/Illuminate/Database/Query/Grammars/MySqlGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Database\Query\Builder;
use Illuminate\Support\Str;
use PDO;

class MySqlGrammar extends Grammar
{
Expand Down Expand Up @@ -94,6 +95,82 @@ protected function compileIndexHint(Builder $query, $indexHint)
};
}

/**
* Compile a group limit clause.
*
* @param \Illuminate\Database\Query\Builder $query
* @return string
*/
protected function compileGroupLimit(Builder $query)
{
return $this->useLegacyGroupLimit($query)
? $this->compileLegacyGroupLimit($query)
: parent::compileGroupLimit($query);
}

/**
* Determine whether to use a legacy group limit clause for MySQL < 8.0.
*
* @param \Illuminate\Database\Query\Builder $query
* @return bool
*/
public function useLegacyGroupLimit(Builder $query)
{
$version = $query->getConnection()->getReadPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);

return ! $query->getConnection()->isMaria() && version_compare($version, '8.0.11') < 0;
}

/**
* Compile a group limit clause for MySQL < 8.0.
*
* Derived from https://softonsofa.com/tweaking-eloquent-relations-how-to-get-n-related-models-per-parent/.
*
* @param \Illuminate\Database\Query\Builder $query
* @return string
*/
protected function compileLegacyGroupLimit(Builder $query)
{
$limit = (int) $query->groupLimit['value'];
$offset = $query->offset;

if (isset($offset)) {
$offset = (int) $offset;
$limit += $offset;

$query->offset = null;
}

$column = last(explode('.', $query->groupLimit['column']));
$column = $this->wrap($column);

$partition = ', @laravel_row := if(@laravel_group = '.$column.', @laravel_row + 1, 1) as `laravel_row`';
$partition .= ', @laravel_group := '.$column;

$orders = (array) $query->orders;

array_unshift($orders, [
'column' => $query->groupLimit['column'],
'direction' => 'asc'
]);

$query->orders = $orders;

$components = $this->compileComponents($query);

$sql = $this->concatenate($components);

$from = '(select @laravel_row := 0, @laravel_group := 0) as `laravel_vars`, ('.$sql.') as `laravel_table`';

$sql = 'select `laravel_table`.*'.$partition.' from '.$from.' having `laravel_row` <= '.$limit;

if (isset($offset)) {
$sql .= ' and `laravel_row` > '.$offset;
}

return $sql.' order by `laravel_row`';
}

/**
* Compile an insert ignore statement into SQL.
*
Expand Down
Loading

0 comments on commit 8f29cc9

Please sign in to comment.