From f85cc63cff351ffac68daf4f9429caab5fffa884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Mat=C4=9Bj=C4=8Dek?= Date: Tue, 30 Jul 2024 07:35:20 +0200 Subject: [PATCH] feat(Database): add ChunkWriteService --- .../Contracts/ChunkWriteServiceContract.php | 18 ++++ .../Entities/ChunkWriteStateEntity.php | 23 ++++++ src/Database/Services/ChunkWriteService.php | 82 +++++++++++++++++++ .../Services/ChunkWriteServiceTest.php | 58 +++++++++++++ tests/Unit/Database/Services/TestModel.php | 15 ++++ 5 files changed, 196 insertions(+) create mode 100644 src/Database/Contracts/ChunkWriteServiceContract.php create mode 100644 src/Database/Entities/ChunkWriteStateEntity.php create mode 100644 src/Database/Services/ChunkWriteService.php create mode 100644 tests/Unit/Database/Services/ChunkWriteServiceTest.php create mode 100644 tests/Unit/Database/Services/TestModel.php diff --git a/src/Database/Contracts/ChunkWriteServiceContract.php b/src/Database/Contracts/ChunkWriteServiceContract.php new file mode 100644 index 00000000..48106823 --- /dev/null +++ b/src/Database/Contracts/ChunkWriteServiceContract.php @@ -0,0 +1,18 @@ + $closure + */ + public function write(Closure $closure): ChunkWriteStateEntity; +} diff --git a/src/Database/Entities/ChunkWriteStateEntity.php b/src/Database/Entities/ChunkWriteStateEntity.php new file mode 100644 index 00000000..cf51faf7 --- /dev/null +++ b/src/Database/Entities/ChunkWriteStateEntity.php @@ -0,0 +1,23 @@ +|null $modelClass + * @param array> $toWrite + */ + public function __construct( + public int $batchSize = 0, + public ?string $modelClass = null, + public int $insertedCount = 0, + public int $attributesCount = 0, + public array $toWrite = [], + ) { + } +} diff --git a/src/Database/Services/ChunkWriteService.php b/src/Database/Services/ChunkWriteService.php new file mode 100644 index 00000000..ec2aa3ca --- /dev/null +++ b/src/Database/Services/ChunkWriteService.php @@ -0,0 +1,82 @@ +add($model, $writeState); + } + + $this->finish($writeState); + + return $writeState; + } + + private function add(Model $model, ChunkWriteStateEntity $state): void + { + if ($model->usesTimestamps()) { + $model->updateTimestamps(); + } + + /** @var array $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 = []; + } +} diff --git a/tests/Unit/Database/Services/ChunkWriteServiceTest.php b/tests/Unit/Database/Services/ChunkWriteServiceTest.php new file mode 100644 index 00000000..c8e4442a --- /dev/null +++ b/tests/Unit/Database/Services/ChunkWriteServiceTest.php @@ -0,0 +1,58 @@ + + */ + 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); + } +} diff --git a/tests/Unit/Database/Services/TestModel.php b/tests/Unit/Database/Services/TestModel.php new file mode 100644 index 00000000..79ea47be --- /dev/null +++ b/tests/Unit/Database/Services/TestModel.php @@ -0,0 +1,15 @@ +