-
-
Notifications
You must be signed in to change notification settings - Fork 0
β‘ Optimize config version pruning to avoid N+1 queries #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
60eeca2
d04bfd1
7b2eb6a
90d54c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -295,16 +295,28 @@ public function deleteVersion(int $versionId): void | |||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| protected function pruneOldVersions(int $profileId): void | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| $versions = ConfigVersion::where('profile_id', $profileId) | ||||||||||||||||||||||||||
| // Find the oldest version we want to keep (the Nth newest) | ||||||||||||||||||||||||||
| $cutoff = ConfigVersion::where('profile_id', $profileId) | ||||||||||||||||||||||||||
| ->orderByDesc('created_at') | ||||||||||||||||||||||||||
| ->get(); | ||||||||||||||||||||||||||
| ->orderByDesc('id') | ||||||||||||||||||||||||||
| ->skip($this->maxVersions - 1) | ||||||||||||||||||||||||||
| ->take(1) | ||||||||||||||||||||||||||
| ->first(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if ($versions->count() > $this->maxVersions) { | ||||||||||||||||||||||||||
| $toDelete = $versions->slice($this->maxVersions); | ||||||||||||||||||||||||||
| foreach ($toDelete as $version) { | ||||||||||||||||||||||||||
| $version->delete(); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if ($cutoff === null) { | ||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Delete versions older than the cutoff | ||||||||||||||||||||||||||
| ConfigVersion::where('profile_id', $profileId) | ||||||||||||||||||||||||||
| ->where(function ($query) use ($cutoff) { | ||||||||||||||||||||||||||
| $query->where('created_at', '<', $cutoff->created_at) | ||||||||||||||||||||||||||
| ->orWhere(function ($q) use ($cutoff) { | ||||||||||||||||||||||||||
| $q->where('created_at', $cutoff->created_at) | ||||||||||||||||||||||||||
| ->where('id', '<', $cutoff->id); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
Comment on lines
+312
to
+317
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§© Analysis chainπ Script executed: #!/bin/bash
# First, check the actual file and the specific lines mentioned
echo "=== Checking file existence and content ==="
if [ -f "src/Core/Config/ConfigVersioning.php" ]; then
echo "File exists. Checking lines 310-320:"
sed -n '310,320p' "src/Core/Config/ConfigVersioning.php"
else
echo "File not found"
fiRepository: host-uk/core-php Length of output: 627 π Script executed: #!/bin/bash
# Search for existing patterns of where() with typed closures in src/Core
echo "=== Searching for query builder closure patterns ==="
rg -n "where\(function" src/Core --type php -A 2 -B 1 | head -100Repository: host-uk/core-php Length of output: 5981 π Script executed: #!/bin/bash
# Check if Illuminate\Database\Query\Builder is already imported in the file
echo "=== Checking imports in ConfigVersioning.php ==="
head -40 src/Core/Config/ConfigVersioning.php | grep -E "^use|^namespace"Repository: host-uk/core-php Length of output: 242 Add type hints to the nested query closures. Both closures accept untyped parameters and have no return type, violating the coding guideline that "all parameters and return types must have type hints in PHP". Type-hint the builder parameter as Proposed fix+use Illuminate\Database\Query\Builder;- ->where(function ($query) use ($cutoff) {
+ ->where(function (Builder $query) use ($cutoff): void {
$query->where('created_at', '<', $cutoff->created_at)
- ->orWhere(function ($q) use ($cutoff) {
- $q->where('created_at', $cutoff->created_at)
+ ->orWhere(function (Builder $q) use ($cutoff): void {
+ $q->where('created_at', $cutoff->created_at)
->where('id', '<', $cutoff->id);
});
})
->delete();π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| ->delete(); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,77 @@ | ||||||||||||||||||
| <?php | ||||||||||||||||||
|
|
||||||||||||||||||
| namespace Core\Config\Tests\Feature; | ||||||||||||||||||
|
Comment on lines
+1
to
+3
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§© Analysis chainπ Script executed: #!/bin/bash
# Check if file exists and read its content
if [ -f "src/Core/Config/Tests/Feature/ConfigVersioningPerformanceTest.php" ]; then
echo "=== File exists. Reading content: ==="
head -40 "src/Core/Config/Tests/Feature/ConfigVersioningPerformanceTest.php"
else
echo "File not found"
fiRepository: host-uk/core-php Length of output: 1280 π Script executed: #!/bin/bash
# Check other files in the same directory for strict_types pattern
echo "=== Checking strict_types in same directory: ==="
find src/Core/Config/Tests/Feature -name "*.php" -type f | head -5 | while read f; do
echo "--- $f ---"
head -5 "$f"
doneRepository: host-uk/core-php Length of output: 354 π Script executed: #!/bin/bash
# Find the test_pruning_performance method definition
sed -n '25,30p' src/Core/Config/Tests/Feature/ConfigVersioningPerformanceTest.phpRepository: host-uk/core-php Length of output: 314 Add strict types declaration and type hint test method. This file is missing π‘ Proposed fixes <?php
+
+declare(strict_types=1);
namespace Core\Config\Tests\Feature;- public function test_pruning_performance()
+ public function test_pruning_performance(): void
{π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| use Core\Config\ConfigExporter; | ||||||||||||||||||
| use Core\Config\ConfigService; | ||||||||||||||||||
| use Core\Config\ConfigVersioning; | ||||||||||||||||||
| use Core\Config\Models\ConfigProfile; | ||||||||||||||||||
| use Core\Config\Models\ConfigVersion; | ||||||||||||||||||
| use Core\Tests\TestCase; | ||||||||||||||||||
| use Illuminate\Foundation\Testing\RefreshDatabase; | ||||||||||||||||||
| use Illuminate\Support\Facades\Config; | ||||||||||||||||||
| use Mockery; | ||||||||||||||||||
|
|
||||||||||||||||||
| class ConfigVersioningPerformanceTest extends TestCase | ||||||||||||||||||
| { | ||||||||||||||||||
| use RefreshDatabase; | ||||||||||||||||||
|
|
||||||||||||||||||
| protected function setUp(): void | ||||||||||||||||||
| { | ||||||||||||||||||
| parent::setUp(); | ||||||||||||||||||
| $this->loadMigrationsFrom(__DIR__.'/../../Migrations'); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| public function test_pruning_performance() | ||||||||||||||||||
| { | ||||||||||||||||||
|
Comment on lines
+25
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§© Analysis chainπ Script executed: cat -n src/Core/Config/Tests/Feature/ConfigVersioningPerformanceTest.php | head -40Repository: host-uk/core-php Length of output: 1522 π Script executed: rg -n "function test_pruning_performance" src/Core/Config/Tests/Feature/ConfigVersioningPerformanceTest.php -A 5Repository: host-uk/core-php Length of output: 332 π Script executed: rg -n "public function test_" src/Core/Config/Tests/Feature/ConfigVersioningPerformanceTest.phpRepository: host-uk/core-php Length of output: 109 π Script executed: rg -n "public function " src/Core/Config/Tests/Feature/ConfigVersioningPerformanceTest.php | head -20Repository: host-uk/core-php Length of output: 109 π Script executed: rg -n "public function " src/Core/Config/Tests/Feature/ConfigVersioningPerformanceTest.php -A 1Repository: host-uk/core-php Length of output: 118 π Script executed: # Check other test files to see if test methods have return types
rg -n "public function test_" src/Core/ -A 1 | head -50Repository: host-uk/core-php Length of output: 3293 Add a return type to the test method. Test methods must declare return types to comply with the coding guideline requiring type hints on all parameters and return types in PHP files. π‘ Proposed fix- public function test_pruning_performance()
+ public function test_pruning_performance(): voidπ Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||
| // 1. Setup dependencies | ||||||||||||||||||
| $mockExporter = Mockery::mock(ConfigExporter::class); | ||||||||||||||||||
| $mockExporter->shouldReceive('exportJson') | ||||||||||||||||||
| ->andReturn(json_encode(['values' => []])); | ||||||||||||||||||
|
|
||||||||||||||||||
| $configService = app(ConfigService::class); | ||||||||||||||||||
|
|
||||||||||||||||||
| $versioning = new ConfigVersioning($configService, $mockExporter); | ||||||||||||||||||
|
|
||||||||||||||||||
| // set max versions to a small number to force pruning | ||||||||||||||||||
| Config::set('core.config.max_versions', 50); | ||||||||||||||||||
| // Re-instantiate to pick up config or use setter if available | ||||||||||||||||||
| $versioning->setMaxVersions(50); | ||||||||||||||||||
|
|
||||||||||||||||||
| // 2. Create a profile | ||||||||||||||||||
| $profile = ConfigProfile::ensureSystem(); | ||||||||||||||||||
|
|
||||||||||||||||||
| // 3. Seed many versions (e.g., 500) | ||||||||||||||||||
| // We insert them directly to avoid triggering pruning during setup | ||||||||||||||||||
| $now = now(); | ||||||||||||||||||
| $versions = []; | ||||||||||||||||||
| for ($i = 0; $i < 500; $i++) { | ||||||||||||||||||
| $versions[] = [ | ||||||||||||||||||
| 'profile_id' => $profile->id, | ||||||||||||||||||
| 'workspace_id' => null, | ||||||||||||||||||
| 'label' => "Version $i", | ||||||||||||||||||
| 'snapshot' => '{}', | ||||||||||||||||||
| 'author' => 'tester', | ||||||||||||||||||
| 'created_at' => $now->subMinutes(500 - $i), // Oldest first | ||||||||||||||||||
| ]; | ||||||||||||||||||
| } | ||||||||||||||||||
| ConfigVersion::insert($versions); | ||||||||||||||||||
|
|
||||||||||||||||||
| $initialCount = ConfigVersion::count(); | ||||||||||||||||||
| $this->assertEquals(500, $initialCount); | ||||||||||||||||||
|
|
||||||||||||||||||
| // 4. Measure pruning time | ||||||||||||||||||
| $start = microtime(true); | ||||||||||||||||||
|
|
||||||||||||||||||
| // createVersion triggers pruneOldVersions | ||||||||||||||||||
| $versioning->createVersion(null, 'New Version'); | ||||||||||||||||||
|
|
||||||||||||||||||
| $duration = microtime(true) - $start; | ||||||||||||||||||
|
|
||||||||||||||||||
| // 5. Verify | ||||||||||||||||||
| $finalCount = ConfigVersion::count(); | ||||||||||||||||||
| $this->assertEquals(50, $finalCount, 'Should have pruned down to 50 versions'); | ||||||||||||||||||
|
|
||||||||||||||||||
| echo "\nPerformance: Pruning took ".number_format($duration * 1000, 2)."ms\n"; | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against maxVersions < 1 before computing the cutoff.
If configuration sets maxVersions to 0 or a negative value,
skip($this->maxVersions - 1)becomes a negative offset and can yield invalid SQL. Clamping locally keeps pruning robust even with misconfiguration.π‘ Proposed fix
π€ Prompt for AI Agents