Skip to content

Commit bb6e6f2

Browse files
authored
Add Builder@lazy() and Builder@lazyById() methods (#36699)
1 parent baa48bf commit bb6e6f2

File tree

2 files changed

+184
-0
lines changed

2 files changed

+184
-0
lines changed

src/Illuminate/Database/Concerns/BuildsQueries.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Illuminate\Pagination\LengthAwarePaginator;
99
use Illuminate\Pagination\Paginator;
1010
use Illuminate\Support\Collection;
11+
use Illuminate\Support\LazyCollection;
12+
use InvalidArgumentException;
1113

1214
trait BuildsQueries
1315
{
@@ -159,6 +161,76 @@ public function eachById(callable $callback, $count = 1000, $column = null, $ali
159161
}, $column, $alias);
160162
}
161163

164+
/**
165+
* Query lazily, by chunks of the given size.
166+
*
167+
* @param int $chunkSize
168+
* @return \Illuminate\Support\LazyCollection
169+
*/
170+
public function lazy($chunkSize = 1000)
171+
{
172+
if ($chunkSize < 1) {
173+
throw new InvalidArgumentException('The chunk size should be at least 1');
174+
}
175+
176+
$this->enforceOrderBy();
177+
178+
return LazyCollection::make(function () use ($chunkSize) {
179+
$page = 1;
180+
181+
while (true) {
182+
$results = $this->forPage($page++, $chunkSize)->get();
183+
184+
foreach ($results as $result) {
185+
yield $result;
186+
}
187+
188+
if ($results->count() < $chunkSize) {
189+
return;
190+
}
191+
}
192+
});
193+
}
194+
195+
/**
196+
* Query lazily, by chunking the results of a query by comparing IDs.
197+
*
198+
* @param int $count
199+
* @param string|null $column
200+
* @param string|null $alias
201+
* @return \Illuminate\Support\LazyCollection
202+
*/
203+
public function lazyById($chunkSize = 1000, $column = null, $alias = null)
204+
{
205+
if ($chunkSize < 1) {
206+
throw new InvalidArgumentException('The chunk size should be at least 1');
207+
}
208+
209+
$column = $column ?? $this->defaultKeyName();
210+
211+
$alias = $alias ?? $column;
212+
213+
return LazyCollection::make(function () use ($chunkSize, $column, $alias) {
214+
$lastId = null;
215+
216+
while (true) {
217+
$clone = clone $this;
218+
219+
$results = $clone->forPageAfterId($chunkSize, $lastId, $column)->get();
220+
221+
foreach ($results as $result) {
222+
yield $result;
223+
}
224+
225+
if ($results->count() < $chunkSize) {
226+
return;
227+
}
228+
229+
$lastId = $results->last()->{$alias};
230+
}
231+
});
232+
}
233+
162234
/**
163235
* Execute the query and get the first result.
164236
*

tests/Database/DatabaseEloquentBuilderTest.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,118 @@ public function testChunkPaginatesUsingIdWithCountZero()
382382
}, 'someIdField');
383383
}
384384

385+
public function testLazyWithLastChunkComplete()
386+
{
387+
$builder = m::mock(Builder::class.'[forPage,get]', [$this->getMockQueryBuilder()]);
388+
$builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc'];
389+
390+
$builder->shouldReceive('forPage')->once()->with(1, 2)->andReturnSelf();
391+
$builder->shouldReceive('forPage')->once()->with(2, 2)->andReturnSelf();
392+
$builder->shouldReceive('forPage')->once()->with(3, 2)->andReturnSelf();
393+
$builder->shouldReceive('get')->times(3)->andReturn(
394+
new Collection(['foo1', 'foo2']),
395+
new Collection(['foo3', 'foo4']),
396+
new Collection([])
397+
);
398+
399+
$this->assertEquals(
400+
['foo1', 'foo2', 'foo3', 'foo4'],
401+
$builder->lazy(2)->all()
402+
);
403+
}
404+
405+
public function testLazyWithLastChunkPartial()
406+
{
407+
$builder = m::mock(Builder::class.'[forPage,get]', [$this->getMockQueryBuilder()]);
408+
$builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc'];
409+
410+
$builder->shouldReceive('forPage')->once()->with(1, 2)->andReturnSelf();
411+
$builder->shouldReceive('forPage')->once()->with(2, 2)->andReturnSelf();
412+
$builder->shouldReceive('get')->times(2)->andReturn(
413+
new Collection(['foo1', 'foo2']),
414+
new Collection(['foo3'])
415+
);
416+
417+
$this->assertEquals(
418+
['foo1', 'foo2', 'foo3'],
419+
$builder->lazy(2)->all()
420+
);
421+
}
422+
423+
public function testLazyIsLazy()
424+
{
425+
$builder = m::mock(Builder::class.'[forPage,get]', [$this->getMockQueryBuilder()]);
426+
$builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc'];
427+
428+
$builder->shouldReceive('forPage')->once()->with(1, 2)->andReturnSelf();
429+
$builder->shouldReceive('get')->once()->andReturn(new Collection(['foo1', 'foo2']));
430+
431+
$this->assertEquals(['foo1', 'foo2'], $builder->lazy(2)->take(2)->all());
432+
}
433+
434+
public function testLazyByIdWithLastChunkComplete()
435+
{
436+
$builder = m::mock(Builder::class.'[forPageAfterId,get]', [$this->getMockQueryBuilder()]);
437+
$builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc'];
438+
439+
$chunk1 = new Collection([(object) ['someIdField' => 1], (object) ['someIdField' => 2]]);
440+
$chunk2 = new Collection([(object) ['someIdField' => 10], (object) ['someIdField' => 11]]);
441+
$chunk3 = new Collection([]);
442+
$builder->shouldReceive('forPageAfterId')->once()->with(2, 0, 'someIdField')->andReturnSelf();
443+
$builder->shouldReceive('forPageAfterId')->once()->with(2, 2, 'someIdField')->andReturnSelf();
444+
$builder->shouldReceive('forPageAfterId')->once()->with(2, 11, 'someIdField')->andReturnSelf();
445+
$builder->shouldReceive('get')->times(3)->andReturn($chunk1, $chunk2, $chunk3);
446+
447+
$this->assertEquals(
448+
[
449+
(object) ['someIdField' => 1],
450+
(object) ['someIdField' => 2],
451+
(object) ['someIdField' => 10],
452+
(object) ['someIdField' => 11],
453+
],
454+
$builder->lazyById(2, 'someIdField')->all()
455+
);
456+
}
457+
458+
public function testLazyByIdWithLastChunkPartial()
459+
{
460+
$builder = m::mock(Builder::class.'[forPageAfterId,get]', [$this->getMockQueryBuilder()]);
461+
$builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc'];
462+
463+
$chunk1 = new Collection([(object) ['someIdField' => 1], (object) ['someIdField' => 2]]);
464+
$chunk2 = new Collection([(object) ['someIdField' => 10]]);
465+
$builder->shouldReceive('forPageAfterId')->once()->with(2, 0, 'someIdField')->andReturnSelf();
466+
$builder->shouldReceive('forPageAfterId')->once()->with(2, 2, 'someIdField')->andReturnSelf();
467+
$builder->shouldReceive('get')->times(2)->andReturn($chunk1, $chunk2);
468+
469+
$this->assertEquals(
470+
[
471+
(object) ['someIdField' => 1],
472+
(object) ['someIdField' => 2],
473+
(object) ['someIdField' => 10],
474+
],
475+
$builder->lazyById(2, 'someIdField')->all()
476+
);
477+
}
478+
479+
public function testLazyByIdIsLazy()
480+
{
481+
$builder = m::mock(Builder::class.'[forPageAfterId,get]', [$this->getMockQueryBuilder()]);
482+
$builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc'];
483+
484+
$chunk1 = new Collection([(object) ['someIdField' => 1], (object) ['someIdField' => 2]]);
485+
$builder->shouldReceive('forPageAfterId')->once()->with(2, 0, 'someIdField')->andReturnSelf();
486+
$builder->shouldReceive('get')->once()->andReturn($chunk1);
487+
488+
$this->assertEquals(
489+
[
490+
(object) ['someIdField' => 1],
491+
(object) ['someIdField' => 2],
492+
],
493+
$builder->lazyById(2, 'someIdField')->take(2)->all()
494+
);
495+
}
496+
385497
public function testPluckReturnsTheMutatedAttributesOfAModel()
386498
{
387499
$builder = $this->getBuilder();

0 commit comments

Comments
 (0)