-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathChunkedChangeSafeIterator.php
121 lines (100 loc) · 3.47 KB
/
ChunkedChangeSafeIterator.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
<?php declare(strict_types = 1);
namespace LastDragon_ru\LaraASP\Eloquent\Iterators;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use Override;
use function end;
use function explode;
use function mb_trim;
/**
* The iterator that grabs rows by chunk and safe for changing/deleting rows
* while iteration.
*
* Similar to {@link \Illuminate\Database\Query\Builder::chunkById()} but uses
* generators instead of {@link \Closure}. Although you can modify/delete the
* items while iteration there are few important limitations:
*
* - it is not possible to sort rows, they always will be sorted by `column asc`;
* - the `column` should not be changed while iteration or this may lead to
* repeating row in results;
* - the row inserted while iteration may be skipped if it has `column` with
* the value that is lower than the internal pointer, or it was inserted after
* the last chunk loaded;
* - queries with UNION are not supported.
*
* @see https://github.com/laravel/framework/issues/35400
*
* @template TItem of Model
*
* @extends IteratorImpl<TItem>
*/
class ChunkedChangeSafeIterator extends IteratorImpl {
private string $column;
public function __construct(Builder $builder, ?string $column = null) {
parent::__construct($builder);
$this->column = $column ?? $this->getDefaultColumn($builder);
// Unfortunately the Laravel doesn't correctly work with UNION,
// it just adds conditional to the main query, and this leads to an
// infinite loop.
if ($this->hasUnions()) {
throw new InvalidArgumentException('Query with UNION is not supported.');
}
}
public function getColumn(): string {
return $this->column;
}
#[Override]
protected function getChunk(Builder $builder, int $chunk): Collection {
$column = $this->getColumn();
$builder
->reorder()
->orderBy($column)
->limit($chunk)
->when(
$this->getOffset(),
static function (Builder $builder, string|int|null $offset) use ($column): void {
$builder->where($column, '>', $offset);
},
);
return $builder->get();
}
#[Override]
protected function chunkProcessed(Collection $items): bool {
$last = $this->column($items->last());
if ($last !== null) {
$this->setOffset($last);
}
return parent::chunkProcessed($items)
&& $last !== null;
}
/**
* @param TItem|null $item
*/
protected function column(Model|null $item): mixed {
$value = null;
$column = explode('.', $this->getColumn());
$column = mb_trim(end($column), '`"[]');
if ($item !== null) {
$value = $item->getAttribute($column);
}
return $value;
}
protected function hasUnions(): bool {
return (bool) $this->getBuilder()->getQuery()->unions;
}
#[Override]
protected function getDefaultOffset(): ?int {
// Because Builder contains SQL offset, not column value.
return null;
}
/**
* @param Builder<TItem> $builder
*/
protected function getDefaultColumn(Builder $builder): string {
$column = $builder->getModel()->getKeyName();
$column = $builder->qualifyColumn($column);
return $column;
}
}