Skip to content

Commit 89f8918

Browse files
Add Support for PostgreSQL Binary Dumps (#107)
* Use database_dump_file_extension * Add Binary Dump * Add Support for Binary Import * Run PostgreSQL Tests on GitHub Actions (#108) * Add Postgres Container in GitHub Actions workflow * Remove Coverage from phpunit.xml.dist * Add PGPASSWORD env when restoring Postgres --------- Co-authored-by: stefanzweifel <1080923+stefanzweifel@users.noreply.github.com>
1 parent 28c9bca commit 89f8918

File tree

10 files changed

+102
-36
lines changed

10 files changed

+102
-36
lines changed

.github/workflows/phpstan.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ jobs:
77
update_release_draft:
88
uses: stefanzweifel/reusable-workflows/.github/workflows/phpstan.yml@main
99
with:
10-
php_version: '8.3'
10+
php_version: '8.5'

.github/workflows/run-tests.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ on:
44
push:
55
branches: [main]
66
pull_request:
7-
branches: [main]
87

98
jobs:
109
test:
@@ -33,6 +32,19 @@ jobs:
3332
ports:
3433
- 3306:3306
3534
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
35+
postgres:
36+
image: postgres:latest
37+
env:
38+
POSTGRES_DB: laravel_backup_restore
39+
POSTGRES_USER: postgres
40+
POSTGRES_PASSWORD: postgres
41+
options: >-
42+
--health-cmd pg_isready
43+
--health-interval 10s
44+
--health-timeout 5s
45+
--health-retries 5
46+
ports:
47+
- 5432:5432
3648

3749
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}
3850

@@ -67,13 +79,12 @@ jobs:
6779
touch database/database.sqlite
6880
6981
- name: Execute tests
70-
run: vendor/bin/pest --exclude-group=pgsql --coverage --min=70
82+
run: vendor/bin/pest --coverage --min=70
7183
env:
7284
MYSQL_PORT: 3306
7385
MYSQL_USERNAME: root
7486
MYSQL_PASSWORD: 'password'
7587
MYSQL_DATABASE: laravel_backup_restore
76-
PGSQL_HOST: localhost
7788
PGSQL_PORT: 5432
7889
PGSQL_USERNAME: postgres
7990
PGSQL_PASSWORD: postgres

phpunit.xml.dist

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,6 @@
55
<directory>tests</directory>
66
</testsuite>
77
</testsuites>
8-
<coverage>
9-
<report>
10-
<html outputDirectory="build/coverage"/>
11-
<text outputFile="build/coverage.txt"/>
12-
<clover outputFile="build/logs/clover.xml"/>
13-
</report>
14-
</coverage>
158
<logging>
169
<junit outputFile="build/report.junit.xml"/>
1710
</logging>

src/Databases/DbImporter.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ protected function checkIfImportWasSuccessful(ProcessResult $process, string $du
3434
*/
3535
public function importToDatabase(string $dumpFile, string $connection): void
3636
{
37-
$process = Process::forever()->run($this->getImportCommand($dumpFile, $connection));
37+
$driver = config("database.connections.{$connection}.driver");
38+
$password = config("database.connections.{$connection}.password");
39+
40+
$process = Process::forever()->env([
41+
$driver === 'pgsql' ? 'PGPASSWORD' : '' => $password,
42+
])->run($this->getImportCommand($dumpFile, $connection));
3843

3944
$this->checkIfImportWasSuccessful($process, $dumpFile);
4045
}

src/Databases/PostgreSql.php

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,35 @@ public function getImportCommand(string $dumpFile, string $connection): string
2424
$dumper = DbDumperFactory::createFromConnection($connection);
2525
$dumper->getContentsOfCredentialsFile();
2626

27+
$username = config("database.connections.{$connection}.username");
28+
$password = config("database.connections.{$connection}.password");
29+
$host = config("database.connections.{$connection}.host");
30+
$port = config("database.connections.{$connection}.port");
31+
$database = config("database.connections.{$connection}.database");
2732
if (str($dumpFile)->endsWith('sql')) {
2833
return collect([
2934
$this->dumpBinaryPath.'psql',
3035
'postgresql://'.
31-
urldecode(config("database.connections.{$connection}.username")).':'.
32-
urlencode(config("database.connections.{$connection}.password")).'@'.
33-
config("database.connections.{$connection}.host").':'.
34-
config("database.connections.{$connection}.port").'/'.
35-
config("database.connections.{$connection}.database"),
36+
urldecode($username).':'.
37+
urlencode($password).'@'.
38+
$host.':'.
39+
$port.'/'.
40+
$database,
3641
'< '.$dumpFile,
3742
])->implode(' ');
3843
}
3944

45+
if ($this->isBinaryDump($dumpFile)) {
46+
return sprintf(
47+
'pg_restore --verbose --no-owner --host=%s --port=%s --username=%s --dbname=%s %s',
48+
escapeshellarg($host),
49+
escapeshellarg($port),
50+
escapeshellarg($username),
51+
escapeshellarg($database),
52+
escapeshellarg($dumpFile)
53+
);
54+
}
55+
4056
// @todo: Improve detection of compressed files
4157
$decompressCommand = match (File::extension($dumpFile)) {
4258
'gz' => "gunzip -c {$dumpFile}",
@@ -49,16 +65,23 @@ public function getImportCommand(string $dumpFile, string $connection): string
4965
'|',
5066
$this->dumpBinaryPath.'psql',
5167
'postgresql://'.
52-
urldecode(config("database.connections.{$connection}.username")).':'.
53-
urldecode(config("database.connections.{$connection}.password")).'@'.
54-
config("database.connections.{$connection}.host").':'.
55-
config("database.connections.{$connection}.port").'/'.
56-
config("database.connections.{$connection}.database"),
68+
urldecode($username).':'.
69+
urldecode($password).'@'.
70+
$host.':'.
71+
$port.'/'.
72+
$database,
5773
])->implode(' ');
5874
}
5975

6076
public function getCliName(): string
6177
{
6278
return 'psql';
6379
}
80+
81+
public function isBinaryDump(string $dumpFile): bool
82+
{
83+
return str($dumpFile)->endsWith([
84+
'.backup',
85+
]);
86+
}
6487
}

src/PendingRestore.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ public function getAvailableFilesInDbDumpsDirectory(): Collection
7979

8080
public function getAvailableDbDumps(): Collection
8181
{
82+
$backupDatabaseDumpFileExtension = config('backup.backup.database_dump_file_extension', 'sql');
83+
$backupDatabaseDumpFileExtensionWithLeadingDot = ".{$backupDatabaseDumpFileExtension}";
84+
8285
return $this->getAvailableFilesInDbDumpsDirectory()
83-
->filter(fn ($file) => Str::endsWith($file, ['.sql', '.sql.gz', '.sql.bz2']));
86+
->filter(fn ($file) => Str::endsWith($file, ['.sql', '.sql.gz', '.sql.bz2', $backupDatabaseDumpFileExtensionWithLeadingDot]));
8487
}
8588
}

tests/Commands/RestoreCommandTest.php

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44

55
use Illuminate\Support\Facades\DB;
66
use Illuminate\Support\Facades\Event;
7+
use Illuminate\Support\Facades\Storage;
78
use Wnx\LaravelBackupRestore\Commands\RestoreCommand;
89
use Wnx\LaravelBackupRestore\Events\DatabaseReset;
910
use Wnx\LaravelBackupRestore\Events\LocalBackupRemoved;
1011
use Wnx\LaravelBackupRestore\Exceptions\NoBackupsFound;
1112

13+
use function Pest\Laravel\artisan;
14+
1215
// MySQL
1316
it('restores mysql database', function (string $backup, ?string $password = null) {
1417
$this->artisan(RestoreCommand::class, [
@@ -76,14 +79,16 @@
7679

7780
// pgsql
7881
it('restores pgsql database', function (string $backup, ?string $password = null) {
82+
$connection = config('database.connections.pgsql-restore');
83+
7984
$this->artisan(RestoreCommand::class, [
8085
'--disk' => 'remote',
8186
'--backup' => $backup,
8287
'--connection' => 'pgsql-restore',
8388
'--password' => $password,
8489
'--no-interaction' => true,
8590
])
86-
->expectsQuestion("Proceed to restore \"{$backup}\" using the \"pgsql-restore\" database connection. (Database: laravel_backup_restore, Host: 127.0.0.1, username: root)", true)
91+
->expectsQuestion("Proceed to restore \"{$backup}\" using the \"pgsql-restore\" database connection. (Database: laravel_backup_restore, Host: {$connection['host']}, username: {$connection['username']})", true)
8792
->assertSuccessful();
8893

8994
$result = DB::connection('pgsql')->table('users')->count();
@@ -192,8 +197,41 @@
192197
->assertSuccessful();
193198

194199
Event::assertNotDispatched(LocalBackupRemoved::class);
195-
$files = \Illuminate\Support\Facades\Storage::disk('local')->allFiles('backup-restore-temp');
200+
$files = Storage::disk('local')->allFiles('backup-restore-temp');
196201

197202
expect($files)->not->toBeEmpty();
198203

199204
})->group('sqlite');
205+
206+
it('restores pgsql database with binary dump', function (string $backup, ?string $password = null) {
207+
config([
208+
'backup.backup.database_dump_file_extension' => 'backup',
209+
]);
210+
artisan('db:wipe', [
211+
'--database' => 'pgsql-restore',
212+
]);
213+
214+
$connection = config('database.connections.pgsql-restore');
215+
216+
$this->artisan(RestoreCommand::class, [
217+
'--disk' => 'remote',
218+
'--backup' => $backup,
219+
'--connection' => 'pgsql-restore',
220+
'--password' => $password,
221+
'--no-interaction' => true,
222+
])
223+
->expectsQuestion("Proceed to restore \"{$backup}\" using the \"pgsql-restore\" database connection. (Database: laravel_backup_restore, Host: {$connection['host']}, username: {$connection['username']})", true)
224+
->assertSuccessful();
225+
226+
$result = DB::connection('pgsql-restore')->table('users')->count();
227+
228+
expect($result)->toBe(1);
229+
230+
artisan('db:wipe', [
231+
'--database' => 'pgsql-restore',
232+
]);
233+
})->with([
234+
[
235+
'backup' => 'Laravel/2025-12-26-pgsql-no-compression-custom-extension-binary-dump.zip',
236+
],
237+
])->group('pgsql');

tests/Databases/PostgreSqlTest.php

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,6 @@
4747
Event::assertDispatched(function (DatabaseDumpImportWasSuccessful $event) use ($dumpFile) {
4848
return $event->absolutePathToDump === $dumpFile;
4949
});
50-
51-
$result = DB::connection('pgsql')->table('users')->count();
52-
expect($result)->toBe(10);
5350
})->group('pgsql');
5451

5552
it('uses custom binary to import pgsql dump', function () {
@@ -68,9 +65,6 @@
6865
Event::assertDispatched(function (DatabaseDumpImportWasSuccessful $event) use ($dumpFile) {
6966
return $event->absolutePathToDump === $dumpFile;
7067
});
71-
72-
$result = DB::connection('pgsql')->table('users')->count();
73-
expect($result)->toBe(10);
7468
})->group('pgsql');
7569

7670
it('uses custom binary to import compressed pgsql dump', function () {
@@ -90,9 +84,6 @@
9084
Event::assertDispatched(function (DatabaseDumpImportWasSuccessful $event) use ($dumpFile) {
9185
return $event->absolutePathToDump === $dumpFile;
9286
});
93-
94-
$result = DB::connection('pgsql')->table('users')->count();
95-
expect($result)->toBe(10);
9687
})->group('pgsql');
9788

9889
it('throws import failed exception if pgsql dump could not be imported')

tests/Pest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
// Wipe all databases before each test
1616
artisan('db:wipe', ['--database' => 'mysql']);
1717
artisan('db:wipe', ['--database' => 'sqlite']);
18-
// artisan('db:wipe', ['--database' => 'pgsql']);
18+
artisan('db:wipe', ['--database' => 'pgsql']);
19+
artisan('db:wipe', ['--database' => 'pgsql-restore']);
1920
})
2021
->afterEach(function () {
2122
// Wipe all databases after each test
2223
artisan('db:wipe', ['--database' => 'mysql']);
2324
artisan('db:wipe', ['--database' => 'sqlite']);
24-
// artisan('db:wipe', ['--database' => 'pgsql']);
25+
artisan('db:wipe', ['--database' => 'pgsql']);
26+
artisan('db:wipe', ['--database' => 'pgsql-restore']);
2527

2628
// Delete all files in the temp directory
2729
Storage::disk('local')->deleteDirectory('backup-restore-temp');
Binary file not shown.

0 commit comments

Comments
 (0)