Skip to content

Commit

Permalink
feat(Database): add ChunkWriteService
Browse files Browse the repository at this point in the history
  • Loading branch information
h4kuna committed Jul 30, 2024
1 parent ee86da6 commit f85cc63
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 0 deletions.
18 changes: 18 additions & 0 deletions src/Database/Contracts/ChunkWriteServiceContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace LaraStrict\Database\Contracts;

use Closure;
use Generator;
use Illuminate\Database\Eloquent\Model;
use LaraStrict\Database\Entities\ChunkWriteStateEntity;

interface ChunkWriteServiceContract
{
/**
* @param Closure(): Generator<int, Model> $closure
*/
public function write(Closure $closure): ChunkWriteStateEntity;
}
23 changes: 23 additions & 0 deletions src/Database/Entities/ChunkWriteStateEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace LaraStrict\Database\Entities;

use Illuminate\Database\Eloquent\Model;

class ChunkWriteStateEntity
{
/**
* @param class-string<Model>|null $modelClass
* @param array<array<string, string|int|bool|float>> $toWrite
*/
public function __construct(
public int $batchSize = 0,
public ?string $modelClass = null,
public int $insertedCount = 0,
public int $attributesCount = 0,
public array $toWrite = [],
) {
}
}
82 changes: 82 additions & 0 deletions src/Database/Services/ChunkWriteService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace LaraStrict\Database\Services;

use Closure;
use Illuminate\Database\Eloquent\Model;
use LaraStrict\Database\Contracts\ChunkWriteServiceContract;
use LaraStrict\Database\Entities\ChunkWriteStateEntity;
use LogicException;

final class ChunkWriteService implements ChunkWriteServiceContract
{
public function write(Closure $closure, int $batchSize = 0): ChunkWriteStateEntity
{
$writeState = new ChunkWriteStateEntity(batchSize: $batchSize);

foreach ($closure() as $model) {
$this->add($model, $writeState);
}

$this->finish($writeState);

return $writeState;
}

private function add(Model $model, ChunkWriteStateEntity $state): void
{
if ($model->usesTimestamps()) {
$model->updateTimestamps();
}

/** @var array<string, string|int|bool|float> $attributes */
$attributes = $model->getAttributes();
$attributesCount = count($attributes);

$modelClass = $model::class;

if ($state->modelClass === null) {
$state->modelClass = $modelClass;
} elseif ($state->modelClass !== $modelClass) {
throw new LogicException(sprintf(
'Batch insert must contain items with same class <%s> got <%s>',
$state->modelClass,
$modelClass
));
}

// We need to prevent insert max statements by limiting number of insert
if ($state->batchSize === 0) {
$state->batchSize = (int) (65536 / $attributesCount);
}

if ($state->attributesCount !== 0 && $state->attributesCount !== $attributesCount) {
throw new LogicException('Batch insert must contain items with same attributes count' . print_r(
$attributes,
true
));
}

$state->toWrite[] = $attributes;
$state->attributesCount = $attributesCount;

if ($state->batchSize === count($state->toWrite)) {
$this->finish($state);
}
}

private function finish(ChunkWriteStateEntity $state): void
{
if ($state->toWrite === [] || $state->modelClass === null) {
return;
}

// Do not fail on duplicated entries.
$count = $state->modelClass::insertOrIgnore($state->toWrite);

$state->insertedCount += $count;
$state->toWrite = [];
}
}
58 changes: 58 additions & 0 deletions tests/Unit/Database/Services/ChunkWriteServiceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace Tests\LaraStrict\Unit\Database\Services;

use Closure;
use LaraStrict\Database\Entities\ChunkWriteStateEntity;
use LaraStrict\Database\Services\ChunkWriteService;
use LaraStrict\Tests\Traits\SqlTestEnable;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;

final class ChunkWriteServiceTest extends TestCase
{
use SqlTestEnable;

/**
* @return array<string|int, array{0: Closure(static):void}>
*/
public static function data(): array
{
return [
[
'empty' => static function (self $self) {
$self->assert(new ChunkWriteStateEntity(), static function () {
yield from [];
},);
},
],
[
static function (self $self) {
$self->assert(
new ChunkWriteStateEntity(32768, TestModel::class, 3, 2),
static function () {
yield from [new TestModel(), new TestModel(), new TestModel()];
},
);
},
],
];
}

/**
* @param Closure(static):void $assert
* @dataProvider data
*/
public function test(Closure $assert): void
{
$assert($this);
}

public function assert(ChunkWriteStateEntity $expected, Closure $data): void
{
$state = (new ChunkWriteService())->write($data);
Assert::assertEquals($expected, $state);
}
}
15 changes: 15 additions & 0 deletions tests/Unit/Database/Services/TestModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Tests\LaraStrict\Unit\Database\Services;

use Illuminate\Database\Eloquent\Model;

final class TestModel extends Model
{
public static function insertOrIgnore(array $data): int
{
return count($data);
}
}

0 comments on commit f85cc63

Please sign in to comment.