diff --git a/src/Tempest/Database/src/Builder/ModelQueryBuilder.php b/src/Tempest/Database/src/Builder/ModelQueryBuilder.php index b54028343..8dfe2b4a8 100644 --- a/src/Tempest/Database/src/Builder/ModelQueryBuilder.php +++ b/src/Tempest/Database/src/Builder/ModelQueryBuilder.php @@ -4,6 +4,7 @@ namespace Tempest\Database\Builder; +use Closure; use Tempest\Database\DatabaseModel; use Tempest\Database\Id; use Tempest\Database\Query; @@ -22,6 +23,8 @@ final class ModelQueryBuilder private ?int $limit = null; + private ?int $offset = null; + private array $raw = []; private array $relations = []; @@ -67,6 +70,22 @@ public function all(mixed ...$bindings): array return map($this->build($bindings))->collection()->to($this->modelClass); } + /** + * @param \Closure(TModelClass[] $models): void $closure + */ + public function chunk(Closure $closure, int $amountPerChunk = 200): void + { + $offset = 0; + + do { + $data = $this->clone()->limit($amountPerChunk)->offset($offset)->all(); + + $offset += count($data); + + $closure($data); + } while ($data !== []); + } + /** @return self */ public function where(string $where, mixed ...$bindings): self { @@ -93,6 +112,14 @@ public function limit(int $limit): self return $this; } + /** @return self */ + public function offset(int $offset): self + { + $this->offset = $offset; + + return $this; + } + /** @return self */ public function raw(string $raw): self { @@ -177,6 +204,10 @@ private function build(array $bindings): Query $statements[] = sprintf('LIMIT %s', $this->limit); } + if ($this->offset) { + $statements[] = sprintf('OFFSET %s', $this->offset); + } + if ($this->raw !== []) { $statements[] = implode(', ', $this->raw); } @@ -197,4 +228,9 @@ private function getRelations(ModelDefinition $modelDefinition): array return $relations; } + + private function clone(): self + { + return clone $this; + } } diff --git a/tests/Integration/Database/Builder/ModelQueryBuilderTest.php b/tests/Integration/Database/Builder/ModelQueryBuilderTest.php index 1bca3a1de..ac46089ec 100644 --- a/tests/Integration/Database/Builder/ModelQueryBuilderTest.php +++ b/tests/Integration/Database/Builder/ModelQueryBuilderTest.php @@ -71,6 +71,52 @@ public function test_limit(): void $this->assertSame('B', $books[1]->title); } + public function test_offset(): void + { + $this->migrate( + CreateMigrationsTable::class, + CreateAuthorTable::class, + CreateBookTable::class, + ); + + (Book::new(title: 'A'))->save(); + (Book::new(title: 'B'))->save(); + (Book::new(title: 'C'))->save(); + (Book::new(title: 'D'))->save(); + + $books = Book::query()->limit(2)->offset(2)->all(); + + $this->assertCount(2, $books); + $this->assertSame('C', $books[0]->title); + $this->assertSame('D', $books[1]->title); + } + + public function test_chunk(): void + { + $this->migrate( + CreateMigrationsTable::class, + CreateAuthorTable::class, + CreateBookTable::class, + ); + + (Book::new(title: 'A'))->save(); + (Book::new(title: 'B'))->save(); + (Book::new(title: 'C'))->save(); + (Book::new(title: 'D'))->save(); + + $results = []; + Book::query()->chunk(function (array $chunk) use (&$results) { + $results = [...$results, ...$chunk]; + }, 2); + $this->assertCount(4, $results); + + $results = []; + Book::query()->where('title <> "A"')->chunk(function (array $chunk) use (&$results) { + $results = [...$results, ...$chunk]; + }, 2); + $this->assertCount(3, $results); + } + public function test_raw(): void { $this->migrate(