Skip to content

[5.8][WIP] Support eager loading with limit #26035

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
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
28 changes: 28 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,34 @@ 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 {
$this->query->partitionLimit($value, $this->getQualifiedForeignPivotKeyName());
}

return $this;
}

/**
* Get the key for comparing against the parent key in "has" query.
*
Expand Down
28 changes: 28 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,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->farParent->exists) {
$this->query->limit($value);
} else {
$this->query->partitionLimit($value, $this->getQualifiedFirstKeyName());
}

return $this;
}

/**
* Get a relationship join table hash.
*
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 @@ -359,6 +359,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->partitionLimit($value, $this->getQualifiedForeignKeyName());
}

return $this;
}

/**
* Get a relationship join table hash.
*
Expand Down
23 changes: 23 additions & 0 deletions src/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ class Builder
*/
public $limit;

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

/**
* The number of records to skip.
*
Expand Down Expand Up @@ -1900,6 +1907,22 @@ protected function removeExistingOrdersFor($column)
})->values()->all();
}

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

return $this;
}

/**
* Add a union statement to the query.
*
Expand Down
57 changes: 57 additions & 0 deletions src/Illuminate/Database/Query/Grammars/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ class Grammar extends BaseGrammar
*/
public function compileSelect(Builder $query)
{
if ($query->partitionLimit) {
if (is_null($query->columns)) {
$query->columns = ['*'];
}

return $this->compilePartitionLimit($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 @@ -693,6 +701,55 @@ protected function compileOffset(Builder $query, $offset)
return 'offset '.(int) $offset;
}

/**
* Compile a partition limit clause for the query.
*
* @param \Illuminate\Database\Query\Builder $query
* @return string
*/
protected function compilePartitionLimit(Builder $query)
{
$components = $this->compileComponents($query);

$partition = 'partition by '.$this->wrap($query->partitionLimit['column']);

$components['columns'] .= $this->compileOver($partition, $components['orders'] ?? '');

unset($components['orders']);

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

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

return $this->compileTableExpression($sql, '<= '.$limit);
}

/**
* Compile the over statement for a table expression.
*
* @param string $partition
* @param string $orderings
* @return string
*/
protected function compileOver($partition, $orderings)
{
$over = trim($partition.' '.$orderings);

return ', row_number() over ('.$over.') as row_num';
}

/**
* Compile a common table expression for the query.
*
* @param string $sql
* @param string $constraint
* @return string
*/
protected function compileTableExpression($sql, $constraint)
{
return 'select * from ('.$sql.') as temp_table where row_num '.$constraint.' order by row_num';
}

/**
* Compile the "union" queries attached to the main query.
*
Expand Down
56 changes: 56 additions & 0 deletions src/Illuminate/Database/Query/Grammars/MySqlGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Illuminate\Database\Query\Grammars;

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

Expand Down Expand Up @@ -80,6 +82,60 @@ protected function compileJsonLength($column, $operator, $value)
return 'json_length('.$field.$path.') '.$operator.' '.$value;
}

/**
* Compile a partition limit clause for the query.
*
* @param \Illuminate\Database\Query\Builder $query
* @return string
*/
protected function compilePartitionLimit(Builder $query)
{
$version = $query->getConnection()->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);

if (version_compare($version, '8.0.11') >= 0) {
return parent::compilePartitionLimit($query);
}

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

/**
* Compile a partition limit clause for the query on 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 compileLegacyPartitionLimit(Builder $query)
{
$column = Str::after($query->partitionLimit['column'], '.');

if ($query->joins && Str::contains(end($query->columns), ' as pivot_')) {
$column = 'pivot_'.$column;
}

$column = $this->wrap($column);

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

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

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

$query->orders = $orders;

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

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

$from = '(select @row_num := 0, @partition := 0) as laravel_vars, ('.$sql.') as temp_table';

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

return 'select temp_table.*'.$partition.' from '.$from.' having row_num <= '.$limit.' order by row_num';
}

/**
* Compile a single union statement.
*
Expand Down
20 changes: 20 additions & 0 deletions src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Illuminate\Database\Query\Grammars;

use PDO;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Database\Query\Builder;
Expand Down Expand Up @@ -55,6 +56,25 @@ public function compileSelect(Builder $query)
return $sql;
}

/**
* Compile a partition limit clause for the query.
*
* @param \Illuminate\Database\Query\Builder $query
* @return string
*/
protected function compilePartitionLimit(Builder $query)
{
$version = $query->getConnection()->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);

if (version_compare($version, '3.25.0') >= 0) {
return parent::compilePartitionLimit($query);
}

$query->partitionLimit = null;

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

/**
* Compile a single union statement.
*
Expand Down
37 changes: 13 additions & 24 deletions src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,17 +166,10 @@ protected function compileJsonLength($column, $operator, $value)
*/
protected function compileAnsiOffset(Builder $query, $components)
{
// An ORDER BY clause is required to make this offset query work, so if one does
// not exist we'll just create a dummy clause to trick the database and so it
// does not complain about the queries for not having an "order by" clause.
if (empty($components['orders'])) {
$components['orders'] = 'order by (select 0)';
}

// We need to add the row number to the query so we can compare it to the offset
// and limit values given for the statements. So we will add an expression to
// the "select" that will give back the row numbers on each of the records.
$components['columns'] .= $this->compileOver($components['orders']);
$components['columns'] .= $this->compileOver('', $components['orders'] ?? '');

unset($components['orders']);

Expand All @@ -185,32 +178,28 @@ protected function compileAnsiOffset(Builder $query, $components)
// set we will just handle the offset only since that is all that matters.
$sql = $this->concatenate($components);

return $this->compileTableExpression($sql, $query);
$constraint = $this->compileRowConstraint($query);

return $this->compileTableExpression($sql, $constraint);
}

/**
* Compile the over statement for a table expression.
*
* @param string $partition
* @param string $orderings
* @return string
*/
protected function compileOver($orderings)
{
return ", row_number() over ({$orderings}) as row_num";
}

/**
* Compile a common table expression for a query.
*
* @param string $sql
* @param \Illuminate\Database\Query\Builder $query
* @return string
*/
protected function compileTableExpression($sql, $query)
protected function compileOver($partition, $orderings)
{
$constraint = $this->compileRowConstraint($query);
// An ORDER BY clause is required to make this offset query work, so if one does
// not exist we'll just create a dummy clause to trick the database and so it
// does not complain about the queries for not having an "order by" clause.
if (empty($orderings)) {
$orderings = 'order by (select 0)';
}

return "select * from ({$sql}) as temp_table where row_num {$constraint} order by row_num";
return parent::compileOver($partition, $orderings);
}

/**
Expand Down
Loading