From 46c0714aabf30dd772716adf2dd5d2dc4b1ba6e5 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Thu, 30 May 2024 04:37:29 +1000 Subject: [PATCH] [1.x] Extract insert all (#105) * Extracts `insertMany` method * add tests * Fix code styling * Formatting * reuse method logic --------- Co-authored-by: timacdonald --- src/Drivers/DatabaseDriver.php | 41 ++++++++----- tests/Feature/DatabaseDriverTest.php | 89 ++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 15 deletions(-) diff --git a/src/Drivers/DatabaseDriver.php b/src/Drivers/DatabaseDriver.php index 7f2ebca..0e92052 100644 --- a/src/Drivers/DatabaseDriver.php +++ b/src/Drivers/DatabaseDriver.php @@ -168,8 +168,8 @@ public function getAll($features): array $inserts[] = [ 'name' => $feature, - 'scope' => Feature::serializeScope($scope), - 'value' => json_encode($value, flags: JSON_THROW_ON_ERROR), + 'scope' => $scope, + 'value' => $value, ]; return $value; @@ -177,14 +177,8 @@ public function getAll($features): array })->all())->all(); if ($inserts->isNotEmpty()) { - $now = Carbon::now(); - try { - $this->newQuery()->insert($inserts->map(fn ($insert) => [ - ...$insert, - static::CREATED_AT => $now, - static::UPDATED_AT => $now, - ])->all()); + $this->insertMany($inserts->all()); } catch (UniqueConstraintViolationException $e) { if ($this->retryDepth === 2) { throw new RuntimeException('Unable to insert feature values into the database.', previous: $e); @@ -222,7 +216,7 @@ public function get($feature, $scope): mixed $this->insert($feature, $scope, $value); } catch (UniqueConstraintViolationException $e) { if ($this->retryDepth === 1) { - throw new RuntimeException('Unable to insert feature value from the database.', previous: $e); + throw new RuntimeException('Unable to insert feature value into the database.', previous: $e); } $this->retryDepth++; @@ -332,13 +326,30 @@ protected function update($feature, $scope, $value) */ protected function insert($feature, $scope, $value) { - return $this->newQuery()->insert([ + return $this->insertMany([[ 'name' => $feature, - 'scope' => Feature::serializeScope($scope), - 'value' => json_encode($value, flags: JSON_THROW_ON_ERROR), - static::CREATED_AT => $now = Carbon::now(), + 'scope' => $scope, + 'value' => $value, + ]]); + } + + /** + * Insert the given feature values into storage. + * + * @param array $inserts + * @return bool + */ + protected function insertMany($inserts) + { + $now = Carbon::now(); + + return $this->newQuery()->insert(array_map(fn ($insert) => [ + 'name' => $insert['name'], + 'scope' => Feature::serializeScope($insert['scope']), + 'value' => json_encode($insert['value'], flags: JSON_THROW_ON_ERROR), + static::CREATED_AT => $now, static::UPDATED_AT => $now, - ]); + ], $inserts)); } /** diff --git a/tests/Feature/DatabaseDriverTest.php b/tests/Feature/DatabaseDriverTest.php index 9de3f33..764006f 100644 --- a/tests/Feature/DatabaseDriverTest.php +++ b/tests/Feature/DatabaseDriverTest.php @@ -2,12 +2,16 @@ namespace Tests\Feature; +use Exception; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Database\Events\QueryExecuted; +use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Str; use InvalidArgumentException; use Laravel\Pennant\Contracts\FeatureScopeable; use Laravel\Pennant\Events\AllFeaturesPurged; @@ -19,6 +23,7 @@ use Laravel\Pennant\Events\FeatureUpdatedForAllScopes; use Laravel\Pennant\Events\UnknownFeatureResolved; use Laravel\Pennant\Feature; +use RuntimeException; use Tests\TestCase; use Workbench\App\Models\User; use Workbench\Database\Factories\UserFactory; @@ -1293,6 +1298,90 @@ public function test_it_can_list_stored_features() $this->assertSame(Feature::stored(), ['bar', 'baz']); } + + public function test_it_retries_3_times_and_then_fails() + { + Feature::define('foo', fn () => true); + Feature::define('bar', fn () => true); + $insertAttempts = 0; + DB::listen(function (QueryExecuted $event) use (&$insertAttempts) { + if (Str::startsWith($event->sql, 'insert into "features"')) { + $insertAttempts++; + DB::table('features')->delete(); + throw new UniqueConstraintViolationException($event->connectionName, $event->sql, $event->bindings, new RuntimeException()); + } + }); + + try { + Feature::for('tim')->loadMissing(['foo', 'bar']); + $this->fail('Should have failed.'); + } catch (Exception $e) { + $this->assertInstanceOf(RuntimeException::class, $e); + $this->assertSame('Unable to insert feature values into the database.', $e->getMessage()); + $this->assertInstanceOf(UniqueConstraintViolationException::class, $e->getPrevious()); + } + + $this->assertSame(3, $insertAttempts); + } + + public function test_it_only_retries_on_conflicts() + { + Feature::define('foo', fn () => true); + Feature::define('bar', fn () => true); + $insertAttempts = 0; + DB::listen(function (QueryExecuted $event) use (&$insertAttempts) { + if (Str::startsWith($event->sql, 'insert into "features"')) { + $insertAttempts++; + throw new UniqueConstraintViolationException($event->connectionName, $event->sql, $event->bindings, new RuntimeException()); + } + }); + + Feature::for('tim')->loadMissing(['foo', 'bar']); + + $this->assertSame(1, $insertAttempts); + } + + public function test_it_retries_2_times_and_then_fails_for_individual_queries() + { + Feature::define('foo', fn () => true); + Feature::define('bar', fn () => true); + $insertAttempts = 0; + DB::listen(function (QueryExecuted $event) use (&$insertAttempts) { + if (Str::startsWith($event->sql, 'insert into "features"')) { + $insertAttempts++; + DB::table('features')->delete(); + throw new UniqueConstraintViolationException($event->connectionName, $event->sql, $event->bindings, new RuntimeException()); + } + }); + + try { + Feature::driver('database')->get('foo', 'tim'); + $this->fail('Should have failed.'); + } catch (Exception $e) { + $this->assertInstanceOf(RuntimeException::class, $e); + $this->assertSame('Unable to insert feature value into the database.', $e->getMessage()); + $this->assertInstanceOf(UniqueConstraintViolationException::class, $e->getPrevious()); + } + + $this->assertSame(2, $insertAttempts); + } + + public function test_it_only_retries_on_conflicts_for_individual_queries() + { + Feature::define('foo', fn () => true); + Feature::define('bar', fn () => true); + $insertAttempts = 0; + DB::listen(function (QueryExecuted $event) use (&$insertAttempts) { + if (Str::startsWith($event->sql, 'insert into "features"')) { + $insertAttempts++; + throw new UniqueConstraintViolationException($event->connectionName, $event->sql, $event->bindings, new RuntimeException()); + } + }); + + Feature::driver('database')->get('foo', 'tim'); + + $this->assertSame(1, $insertAttempts); + } } class UnregisteredFeature