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
29 changes: 29 additions & 0 deletions system/BaseModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,19 @@ abstract public function countAllResults(bool $reset = true, bool $test = false)
*/
abstract public function chunk(int $size, Closure $userFunc);

/**
* Loops over records in batches ordered by the primary key.
* This method works only with DB calls.
*
* @param Closure(array<string, string>|object): mixed $userFunc
*
* @return void
*
* @throws DataException
* @throws InvalidArgumentException if $size is not a positive integer or the current query cannot be chunked by ID
*/
abstract public function chunkById(int $size, Closure $userFunc);

/**
* Loops over records in batches, allowing you to operate on each chunk at a time.
* This method works only with DB calls.
Expand All @@ -609,6 +622,22 @@ abstract public function chunk(int $size, Closure $userFunc);
*/
abstract public function chunkRows(int $size, Closure $userFunc);

/**
* Loops over records in batches ordered by the primary key, allowing you to operate on each chunk at a time.
* This method works only with DB calls.
*
* This method calls the `$userFunc` with the chunk, instead of a single record as in `chunkById()`.
* This allows you to operate on multiple records at once, which can be more efficient for certain operations.
*
* @param Closure(list<array<string, string>>|list<object>): mixed $userFunc
*
* @return void
*
* @throws DataException
* @throws InvalidArgumentException if $size is not a positive integer or the current query cannot be chunked by ID
*/
abstract public function chunkRowsById(int $size, Closure $userFunc);

/**
* Fetches the row of database.
*
Expand Down
10 changes: 10 additions & 0 deletions system/Database/BaseBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2034,6 +2034,16 @@ public function offset(int $offset)
return $this;
}

/**
* Checks if the current query has a LIMIT, OFFSET or UNION clause.
*
* @internal This method is for internal Model use only.
*/
public function hasLimitOffsetOrUnion(): bool
{
return $this->QBLimit !== false || $this->QBOffset !== false || $this->QBUnion !== [];
}

/**
* Generates a platform-specific LIMIT clause.
*/
Expand Down
114 changes: 111 additions & 3 deletions system/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ class Model extends BaseModel
'getCompiledInsert',
'getCompiledSelect',
'getCompiledUpdate',
'hasLimitOffsetOrUnion',
];

public function __construct(?ConnectionInterface $db = null, ?ValidationInterface $validation = null)
Expand Down Expand Up @@ -599,9 +600,7 @@ private function prepareSoftDeleteQuery(bool $reset): void
*/
private function iterateChunks(int $size): Generator
{
if ($size <= 0) {
throw new InvalidArgumentException('$size must be a positive integer.');
}
$this->assertValidChunkSize($size);

$total = $this->builder()->countAllResults(false);
$offset = 0;
Expand All @@ -626,6 +625,89 @@ private function iterateChunks(int $size): Generator
}
}

/**
* Iterates over the result set in chunks of the specified size ordered by the primary key.
*
* @param int $size The number of records to retrieve in each chunk.
*
* @return Generator<list<array<string, string>>|list<object>>
*/
private function iterateChunksById(int $size): Generator
{
$this->assertValidChunkSize($size);

if ($this->primaryKey === '') {
throw new InvalidArgumentException('ID-based chunking requires a primary key.');
}

$builder = clone $this->builder();
$qualifiedPrimaryKey = $this->table . '.' . $this->primaryKey;
$lastPrimaryKey = null;
$hasLastPrimaryKey = false;

if ($builder->QBOrderBy !== []) {
throw new InvalidArgumentException('ID-based chunking cannot be used with orderBy().');
}

if ($builder->QBGroupBy !== []) {
throw new InvalidArgumentException('ID-based chunking cannot be used with groupBy().');
}

if ($builder->hasLimitOffsetOrUnion()) {
throw new InvalidArgumentException('ID-based chunking cannot be used with limit(), offset() or union().');
}

while (true) {
$chunkBuilder = clone $builder;

if ($this->tempUseSoftDeletes) {
$chunkBuilder->where($this->table . '.' . $this->deletedField, null);
}

if ($hasLastPrimaryKey) {
$chunkBuilder->where($qualifiedPrimaryKey . ' >', $lastPrimaryKey);
}

$rows = $chunkBuilder
->orderBy($qualifiedPrimaryKey, 'ASC')
->get($size);

if (! $rows) {
throw DataException::forEmptyDataset('chunkById');
}

$rows = $rows->getResult($this->tempReturnType);

if ($rows === []) {
return;
}

$lastPrimaryKey = $this->getIdValue($rows[array_key_last($rows)]);

if ($lastPrimaryKey === null) {
throw new InvalidArgumentException('The primary key must be selected for ID-based chunking.');
}

$hasLastPrimaryKey = true;

yield $rows;

if (count($rows) < $size) {
return;
}
}
}

/**
* Asserts the chunk size is valid.
*/
private function assertValidChunkSize(int $size): void
{
if ($size <= 0) {
throw new InvalidArgumentException('$size must be a positive integer.');
}
}

/**
* {@inheritDoc}
*/
Expand All @@ -640,6 +722,20 @@ public function chunk(int $size, Closure $userFunc)
}
}

/**
* {@inheritDoc}
*/
public function chunkById(int $size, Closure $userFunc)
{
foreach ($this->iterateChunksById($size) as $rows) {
foreach ($rows as $row) {
if ($userFunc($row) === false) {
return;
}
}
}
}

/**
* {@inheritDoc}
*/
Expand All @@ -652,6 +748,18 @@ public function chunkRows(int $size, Closure $userFunc): void
}
}

/**
* {@inheritDoc}
*/
public function chunkRowsById(int $size, Closure $userFunc): void
{
foreach ($this->iterateChunksById($size) as $rows) {
if ($userFunc($rows) === false) {
return;
}
}
}

/**
* Provides a shared instance of the Query Builder.
*
Expand Down
Loading
Loading