From 257222a8a46083a2b3a84a696a0b26a626341536 Mon Sep 17 00:00:00 2001 From: Saeed Vaziry Date: Thu, 28 May 2026 21:08:54 +0200 Subject: [PATCH 1/4] Add db:vacuum command to reclaim SQLite space Adds a driver-guarded db:vacuum command that compacts the SQLite database, with a free-disk-space pre-check, and schedules it daily after metrics pruning. --- .../Commands/VacuumDatabaseCommand.php | 62 +++++++++++++++++++ app/Console/Kernel.php | 1 + 2 files changed, 63 insertions(+) create mode 100644 app/Console/Commands/VacuumDatabaseCommand.php diff --git a/app/Console/Commands/VacuumDatabaseCommand.php b/app/Console/Commands/VacuumDatabaseCommand.php new file mode 100644 index 000000000..3219f979e --- /dev/null +++ b/app/Console/Commands/VacuumDatabaseCommand.php @@ -0,0 +1,62 @@ +getDriverName() !== 'sqlite') { + $this->warn('VACUUM is only supported on SQLite connections. Skipping.'); + + return self::SUCCESS; + } + + $database = $connection->getDatabaseName(); + + if (! is_string($database) || ! is_file($database)) { + $this->warn('Could not locate the SQLite database file. Skipping.'); + + return self::SUCCESS; + } + + $required = filesize($database) * 2; + $available = disk_free_space(dirname($database)); + + if ($available !== false && $available < $required) { + $this->warn(sprintf( + 'Not enough free disk space to vacuum safely (need ~%s, have %s). Skipping.', + $this->formatBytes($required), + $this->formatBytes((int) $available), + )); + + return self::SUCCESS; + } + + $this->info('Vacuuming the database...'); + + $connection->statement('VACUUM'); + + $this->info('Database vacuumed!'); + + return self::SUCCESS; + } + + private function formatBytes(int $bytes): string + { + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $power = $bytes > 0 ? (int) floor(log($bytes, 1024)) : 0; + $power = min($power, count($units) - 1); + + return round($bytes / (1024 ** $power), 2).' '.$units[$power]; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e19b35757..f966cece2 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -17,6 +17,7 @@ protected function schedule(Schedule $schedule): void $schedule->command('backups:run "0 0 * * 0"')->weekly(); $schedule->command('backups:run "0 0 1 * *"')->monthly(); $schedule->command('metrics:delete-older-metrics')->daily(); + $schedule->command('db:vacuum')->daily(); $schedule->command('metrics:get')->everyMinute(); $schedule->command('servers:check')->everyFiveMinutes(); $schedule->command('servers:check-updates')->dailyAt('02:00'); From 5eec7f3878cd4ed3d62be1c9323a67976d831312 Mon Sep 17 00:00:00 2001 From: Saeed Vaziry Date: Thu, 28 May 2026 21:11:28 +0200 Subject: [PATCH 2/4] Drop redundant is_string check in db:vacuum --- app/Console/Commands/VacuumDatabaseCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Console/Commands/VacuumDatabaseCommand.php b/app/Console/Commands/VacuumDatabaseCommand.php index 3219f979e..cbd9ee4ce 100644 --- a/app/Console/Commands/VacuumDatabaseCommand.php +++ b/app/Console/Commands/VacuumDatabaseCommand.php @@ -23,7 +23,7 @@ public function handle(): int $database = $connection->getDatabaseName(); - if (! is_string($database) || ! is_file($database)) { + if (! is_file($database)) { $this->warn('Could not locate the SQLite database file. Skipping.'); return self::SUCCESS; From e962a2c57cbbda652c0d4d0b5c4aad576cb5cbb1 Mon Sep 17 00:00:00 2001 From: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com> Date: Thu, 28 May 2026 23:01:06 +0200 Subject: [PATCH 3/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- app/Console/Commands/VacuumDatabaseCommand.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/Console/Commands/VacuumDatabaseCommand.php b/app/Console/Commands/VacuumDatabaseCommand.php index cbd9ee4ce..3e45e4729 100644 --- a/app/Console/Commands/VacuumDatabaseCommand.php +++ b/app/Console/Commands/VacuumDatabaseCommand.php @@ -29,7 +29,15 @@ public function handle(): int return self::SUCCESS; } - $required = filesize($database) * 2; + $databaseSize = filesize($database); + + if ($databaseSize === false) { + $this->warn('Could not determine the SQLite database file size. Skipping.'); + + return self::SUCCESS; + } + + $required = $databaseSize * 2; $available = disk_free_space(dirname($database)); if ($available !== false && $available < $required) { From f3cc8ec0b5bd6f76a083b4becdbd88c827be3b4d Mon Sep 17 00:00:00 2001 From: Saeed Vaziry Date: Thu, 28 May 2026 23:34:36 +0200 Subject: [PATCH 4/4] review --- .../Commands/VacuumDatabaseCommand.php | 8 ++- .../Commands/VacuumDatabaseCommandTest.php | 58 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/Commands/VacuumDatabaseCommandTest.php diff --git a/app/Console/Commands/VacuumDatabaseCommand.php b/app/Console/Commands/VacuumDatabaseCommand.php index 3e45e4729..7ccfc024c 100644 --- a/app/Console/Commands/VacuumDatabaseCommand.php +++ b/app/Console/Commands/VacuumDatabaseCommand.php @@ -40,7 +40,13 @@ public function handle(): int $required = $databaseSize * 2; $available = disk_free_space(dirname($database)); - if ($available !== false && $available < $required) { + if ($available === false) { + $this->warn('Could not determine the available disk space. Skipping.'); + + return self::SUCCESS; + } + + if ($available < $required) { $this->warn(sprintf( 'Not enough free disk space to vacuum safely (need ~%s, have %s). Skipping.', $this->formatBytes($required), diff --git a/tests/Unit/Commands/VacuumDatabaseCommandTest.php b/tests/Unit/Commands/VacuumDatabaseCommandTest.php new file mode 100644 index 000000000..f3de3837b --- /dev/null +++ b/tests/Unit/Commands/VacuumDatabaseCommandTest.php @@ -0,0 +1,58 @@ +shouldReceive('getDriverName')->andReturn('mysql'); + $connection->shouldNotReceive('statement'); + + DB::shouldReceive('connection')->andReturn($connection); + + $this->artisan('db:vacuum') + ->expectsOutput('VACUUM is only supported on SQLite connections. Skipping.') + ->assertSuccessful(); + } + + public function test_skips_when_database_file_is_missing(): void + { + $connection = Mockery::mock(Connection::class); + $connection->shouldReceive('getDriverName')->andReturn('sqlite'); + $connection->shouldReceive('getDatabaseName')->andReturn('/path/that/does/not/exist.sqlite'); + $connection->shouldNotReceive('statement'); + + DB::shouldReceive('connection')->andReturn($connection); + + $this->artisan('db:vacuum') + ->expectsOutput('Could not locate the SQLite database file. Skipping.') + ->assertSuccessful(); + } + + public function test_vacuums_when_disk_space_is_sufficient(): void + { + $database = tempnam(sys_get_temp_dir(), 'vito-vacuum-test-'); + file_put_contents($database, 'x'); + + $connection = Mockery::mock(Connection::class); + $connection->shouldReceive('getDriverName')->andReturn('sqlite'); + $connection->shouldReceive('getDatabaseName')->andReturn($database); + $connection->shouldReceive('statement')->once()->with('VACUUM'); + + DB::shouldReceive('connection')->andReturn($connection); + + $this->artisan('db:vacuum') + ->expectsOutput('Vacuuming the database...') + ->expectsOutput('Database vacuumed!') + ->assertSuccessful(); + + unlink($database); + } +}