Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions app/Console/Commands/VacuumDatabaseCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class VacuumDatabaseCommand extends Command
{
protected $signature = 'db:vacuum';

protected $description = 'Reclaim unused space in the SQLite database';

public function handle(): int
{
$connection = DB::connection();

if ($connection->getDriverName() !== 'sqlite') {
Comment on lines +14 to +18
$this->warn('VACUUM is only supported on SQLite connections. Skipping.');

return self::SUCCESS;
}

$database = $connection->getDatabaseName();

if (! is_file($database)) {
$this->warn('Could not locate the SQLite database file. Skipping.');

return self::SUCCESS;
}

$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) {
$this->warn('Could not determine the available disk space. Skipping.');

return self::SUCCESS;
}

if ($available < $required) {
$this->warn(sprintf(
Comment on lines +41 to +50
'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];
}
}
1 change: 1 addition & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
58 changes: 58 additions & 0 deletions tests/Unit/Commands/VacuumDatabaseCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Tests\Unit\Commands;

use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;

class VacuumDatabaseCommandTest extends TestCase
{
public function test_skips_when_connection_is_not_sqlite(): void
{
$connection = Mockery::mock(Connection::class);
$connection->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);
}
}
Loading