Skip to content
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

[Backport] chunkById Database query builder method #7

Merged
merged 14 commits into from
Sep 22, 2023
Merged
Prev Previous commit
Next Next commit
Moving method closer to chunk
abdulhp committed Sep 22, 2023
commit 195ded510dc2d51b40644924157c534505010354
186 changes: 93 additions & 93 deletions src/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
@@ -1522,6 +1522,99 @@ public function chunk($count, callable $callback)
}
}

/**
* Chunk the results of a query by comparing IDs.
*
* @param int $count
* @param callable $callback
* @param string|null $column
* @param string|null $alias
* @return bool
*/
public function chunkById(int $count, callable $callback, string|null $column = null, string|null $alias = null): bool
{
$column ??= $this->defaultKeyName();

$alias ??= $column;

$lastId = null;

$page = 1;

do {
$clone = clone $this;

// We'll execute the query for the given page and get the results. If there are
// no results we can just break and return from here. When there are results
// we will call the callback with the current chunk of these results here.
$results = $clone->forPageAfterId($count, $lastId, $column)->get();

$countResults = count($results);

if ($countResults === 0) {
break;
}

// On each chunk result set, we will pass them to the callback and then let the
// developer take care of everything within the callback, which allows us to
// keep the memory low for spinning through large result sets for working.
if ($callback($results, $page) === false) {
return false;
}

$lastId = data_get(end($results), $alias);

if ($lastId === null) {
throw new RuntimeException("The chunkById operation was aborted because the [{$alias}] column is not present in the query result.");
}

unset($results);

$page++;
} while ($countResults === $count);

return true;
}

/**
* Constrain the query to the next "page" of results after a given ID.
*
* @param int $perPage
* @param int|null $lastId
* @param string $column
* @return $this
*/
public function forPageAfterId(int $perPage = 15, int|null $lastId = 0, string $column = 'id'): Builder
{
$this->orders = $this->removeExistingOrdersFor($column);

if (! is_null($lastId)) {
$this->where($column, '>', $lastId);
}

return $this->orderBy($column, 'asc')
->limit($perPage);
}

/**
* Get an array with all orders with a given column removed.
*
* @param string $column
* @return array
*/
protected function removeExistingOrdersFor(string $column): array
{
return Collection::make($this->orders)
->reject(function ($order) use ($column) {
return isset($order['column']) && $order['column'] === $column;
})->values()->all();
}

private function defaultKeyName(): string
{
return 'id';
}

/**
* Get an array with the values of a given column.
*
@@ -2185,97 +2278,4 @@ public function __call($method, $parameters)
throw new \BadMethodCallException("Call to undefined method {$className}::{$method}()");
}

/**
* Chunk the results of a query by comparing IDs.
*
* @param int $count
* @param callable $callback
* @param string|null $column
* @param string|null $alias
* @return bool
*/
public function chunkById(int $count, callable $callback, string|null $column = null, string|null $alias = null): bool
{
$column ??= $this->defaultKeyName();

$alias ??= $column;

$lastId = null;

$page = 1;

do {
$clone = clone $this;

// We'll execute the query for the given page and get the results. If there are
// no results we can just break and return from here. When there are results
// we will call the callback with the current chunk of these results here.
$results = $clone->forPageAfterId($count, $lastId, $column)->get();

$countResults = count($results);

if ($countResults === 0) {
break;
}

// On each chunk result set, we will pass them to the callback and then let the
// developer take care of everything within the callback, which allows us to
// keep the memory low for spinning through large result sets for working.
if ($callback($results, $page) === false) {
return false;
}

$lastId = data_get(end($results), $alias);

if ($lastId === null) {
throw new RuntimeException("The chunkById operation was aborted because the [{$alias}] column is not present in the query result.");
}

unset($results);

$page++;
} while ($countResults === $count);

return true;
}

/**
* Constrain the query to the next "page" of results after a given ID.
*
* @param int $perPage
* @param int|null $lastId
* @param string $column
* @return $this
*/
public function forPageAfterId(int $perPage = 15, int|null $lastId = 0, string $column = 'id'): Builder
{
$this->orders = $this->removeExistingOrdersFor($column);

if (! is_null($lastId)) {
$this->where($column, '>', $lastId);
}

return $this->orderBy($column, 'asc')
->limit($perPage);
}

/**
* Get an array with all orders with a given column removed.
*
* @param string $column
* @return array
*/
protected function removeExistingOrdersFor(string $column): array
{
return Collection::make($this->orders)
->reject(function ($order) use ($column) {
return isset($order['column']) && $order['column'] === $column;
})->values()->all();
}

private function defaultKeyName(): string
{
return 'id';
}

}