Skip to content

Commit 34b6a76

Browse files
committed
Optimize code / add tests
1 parent 9bba682 commit 34b6a76

File tree

2 files changed

+197
-58
lines changed

2 files changed

+197
-58
lines changed

src/Snapshot.php

+91-58
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44

55
use Carbon\Carbon;
66
use Illuminate\Contracts\Filesystem\Factory;
7-
use Illuminate\Filesystem\FilesystemAdapter as Disk;
7+
use Illuminate\Filesystem\FilesystemAdapter;
88
use Illuminate\Support\Facades\DB;
9-
use Illuminate\Support\LazyCollection;
109
use Spatie\DbSnapshots\Events\DeletedSnapshot;
1110
use Spatie\DbSnapshots\Events\DeletingSnapshot;
1211
use Spatie\DbSnapshots\Events\LoadedSnapshot;
@@ -15,7 +14,7 @@
1514

1615
class Snapshot
1716
{
18-
public Disk $disk;
17+
public FilesystemAdapter $disk;
1918

2019
public string $fileName;
2120

@@ -29,10 +28,9 @@ class Snapshot
2928

3029
protected Factory $filesystemFactory;
3130

32-
public function __construct(Disk $disk, string $fileName)
31+
public function __construct(FilesystemAdapter $disk, string $fileName)
3332
{
3433
$this->disk = $disk;
35-
3634
$this->fileName = $fileName;
3735

3836
$pathinfo = pathinfo($fileName);
@@ -43,14 +41,12 @@ public function __construct(Disk $disk, string $fileName)
4341
}
4442

4543
$this->name = pathinfo($fileName, PATHINFO_FILENAME);
46-
4744
$this->filesystemFactory = app(Factory::class);
4845
}
4946

5047
public function useStream(): self
5148
{
5249
$this->useStream = true;
53-
5450
return $this;
5551
}
5652

@@ -79,6 +75,10 @@ protected function loadAsync(?string $connectionName = null): void
7975
$dbDumpContents = gzdecode($dbDumpContents);
8076
}
8177

78+
if (empty(trim($dbDumpContents))) {
79+
return;
80+
}
81+
8282
DB::connection($connectionName)->unprepared($dbDumpContents);
8383
}
8484

@@ -90,83 +90,116 @@ protected function isASqlComment(string $line): bool
9090
protected function shouldIgnoreLine(string $line): bool
9191
{
9292
$line = trim($line);
93-
9493
return empty($line) || $this->isASqlComment($line);
9594
}
9695

9796
protected function loadStream(?string $connectionName = null): void
9897
{
99-
$directory = (new TemporaryDirectory(config('db-snapshots.temporary_directory_path')))->create();
98+
$temporaryDirectory = (new TemporaryDirectory(config('db-snapshots.temporary_directory_path')))->create();
99+
100+
$this->configureFilesystemDisk($temporaryDirectory->path());
101+
102+
$localDisk = $this->filesystemFactory->disk(self::class);
103+
104+
try {
105+
$this->processStream($localDisk, $connectionName);
106+
} finally {
107+
$temporaryDirectory->delete();
108+
}
109+
}
100110

111+
private function configureFilesystemDisk(string $path): void
112+
{
101113
config([
102114
'filesystems.disks.' . self::class => [
103115
'driver' => 'local',
104-
'root' => $directory->path(),
116+
'root' => $path,
105117
'throw' => false,
106-
]
118+
],
107119
]);
120+
}
108121

109-
$localDisk = $this->filesystemFactory->disk(self::class);
122+
private function processStream(FilesystemAdapter $localDisk, ?string $connectionName): void
123+
{
124+
$this->copyStreamToLocalDisk($localDisk);
125+
126+
$stream = $this->openStream($localDisk);
110127

111128
try {
112-
LazyCollection::make(function () use ($localDisk) {
113-
$localDisk->writeStream($this->fileName, $this->disk->readStream($this->fileName));
114-
115-
$stream = $this->compressionExtension === 'gz'
116-
? gzopen($localDisk->path($this->fileName), 'r')
117-
: $localDisk->readStream($this->fileName);
118-
119-
$statement = '';
120-
while (!feof($stream)) {
121-
$chunk = $this->compressionExtension === 'gz'
122-
? gzread($stream, self::STREAM_BUFFER_SIZE)
123-
: fread($stream, self::STREAM_BUFFER_SIZE);
124-
125-
$lines = explode("\n", $chunk);
126-
foreach ($lines as $idx => $line) {
127-
if ($this->shouldIgnoreLine($line)) {
128-
continue;
129-
}
130-
131-
$statement .= $line;
132-
133-
// Carry-over the last line to the next chunk since it
134-
// is possible that this chunk finished mid-line right on
135-
// a semi-colon.
136-
if (count($lines) == $idx + 1) {
137-
break;
138-
}
139-
140-
if (str_ends_with(trim($statement), ';')) {
141-
yield $statement;
142-
$statement = '';
143-
}
144-
}
129+
$this->processStatements($stream, $connectionName);
130+
} finally {
131+
$this->closeStream($stream);
132+
}
133+
}
134+
135+
private function copyStreamToLocalDisk(FilesystemAdapter $localDisk): void
136+
{
137+
$localDisk->writeStream($this->fileName, $this->disk->readStream($this->fileName));
138+
}
139+
140+
private function openStream(FilesystemAdapter $localDisk): mixed
141+
{
142+
return $this->compressionExtension === 'gz'
143+
? gzopen($localDisk->path($this->fileName), 'r')
144+
: $localDisk->readStream($this->fileName);
145+
}
146+
147+
private function closeStream(mixed $stream): void
148+
{
149+
$this->compressionExtension === 'gz' ? gzclose($stream) : fclose($stream);
150+
}
151+
152+
private function processStatements(mixed $stream, ?string $connectionName): void
153+
{
154+
$statement = '';
155+
while (!feof($stream)) {
156+
$chunk = $this->readChunk($stream);
157+
$lines = explode("\n", $chunk);
158+
159+
foreach ($lines as $idx => $line) {
160+
if ($this->shouldIgnoreLine($line)) {
161+
continue;
145162
}
146163

147-
if (str_ends_with(trim($statement), ';')) {
148-
yield $statement;
164+
$statement .= $line;
165+
166+
if ($this->isLastLineOfChunk($lines, $idx)) {
167+
break;
149168
}
150169

151-
if ($this->compressionExtension === 'gz') {
152-
gzclose($stream);
153-
} else {
154-
fclose($stream);
170+
if ($this->isCompleteStatement($statement)) {
171+
DB::connection($connectionName)->unprepared($statement);
172+
$statement = '';
155173
}
156-
})->each(function (string $statement) use ($connectionName) {
157-
DB::connection($connectionName)->unprepared($statement);
158-
});
159-
} finally {
160-
$directory->delete();
174+
}
175+
}
176+
177+
if ($this->isCompleteStatement($statement)) {
178+
DB::connection($connectionName)->unprepared($statement);
161179
}
162180
}
163181

182+
private function readChunk(mixed $stream): string
183+
{
184+
return $this->compressionExtension === 'gz'
185+
? gzread($stream, self::STREAM_BUFFER_SIZE)
186+
: fread($stream, self::STREAM_BUFFER_SIZE);
187+
}
188+
189+
private function isLastLineOfChunk(array $lines, int $idx): bool
190+
{
191+
return count($lines) === $idx + 1;
192+
}
193+
194+
private function isCompleteStatement(string $statement): bool
195+
{
196+
return str_ends_with(trim($statement), ';');
197+
}
198+
164199
public function delete(): void
165200
{
166201
event(new DeletingSnapshot($this));
167-
168202
$this->disk->delete($this->fileName);
169-
170203
event(new DeletedSnapshot($this->fileName, $this->disk));
171204
}
172205

tests/Commands/LoadTest.php

+106
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
<?php
22

3+
use Carbon\Carbon;
4+
use Illuminate\Filesystem\FilesystemAdapter;
35
use Illuminate\Support\Facades\Artisan;
46
use Illuminate\Support\Facades\DB;
7+
use Illuminate\Support\Facades\Event;
58
use Mockery as m;
69

10+
use Spatie\DbSnapshots\Events\DeletedSnapshot;
11+
use Spatie\DbSnapshots\Snapshot;
712
use function Pest\Laravel\assertDatabaseCount;
813
use function PHPUnit\Framework\assertEquals;
914
use function PHPUnit\Framework\assertNotEquals;
@@ -143,3 +148,104 @@ function getNameOfLoadedSnapshot(): string
143148

144149
assertSnapshotLoaded('snapshot4');
145150
});
151+
152+
it('throws an error when snapshot does not exist', function () {
153+
$this->expectException(Exception::class);
154+
155+
$disk = m::mock(FilesystemAdapter::class);
156+
$disk->shouldReceive('exists')
157+
->with('nonexistent.sql')
158+
->andReturn(false);
159+
160+
$snapshot = new Snapshot($disk, 'nonexistent.sql');
161+
$snapshot->load();
162+
});
163+
164+
it('throws an error for invalid SQL in snapshot', function () {
165+
$disk = m::mock(FilesystemAdapter::class);
166+
$disk->shouldReceive('get')
167+
->andReturn("INVALID SQL;\n");
168+
169+
$snapshot = new Snapshot($disk, 'invalid.sql');
170+
171+
$this->expectException(Exception::class);
172+
$snapshot->load();
173+
});
174+
175+
it('deletes the snapshot and triggers event', function () {
176+
Event::fake();
177+
178+
$disk = m::mock(FilesystemAdapter::class);
179+
$disk->shouldReceive('delete')
180+
->once()
181+
->with('snapshot.sql')
182+
->andReturn(true);
183+
184+
$snapshot = new Snapshot($disk, 'snapshot.sql');
185+
$snapshot->delete();
186+
187+
Event::assertDispatched(DeletedSnapshot::class, function ($event) use ($snapshot) {
188+
return $event->fileName === $snapshot->fileName && $event->disk === $snapshot->disk;
189+
});
190+
});
191+
192+
it('returns the correct size of the snapshot', function () {
193+
$disk = m::mock(FilesystemAdapter::class);
194+
$disk->shouldReceive('size')
195+
->andReturn(2048);
196+
197+
$snapshot = new Snapshot($disk, 'snapshot.sql');
198+
199+
assertEquals(2048, $snapshot->size());
200+
});
201+
202+
it('returns the correct creation date of the snapshot', function () {
203+
$timestamp = Carbon::now()->timestamp;
204+
205+
$disk = m::mock(FilesystemAdapter::class);
206+
$disk->shouldReceive('lastModified')
207+
->andReturn($timestamp);
208+
209+
$snapshot = new Snapshot($disk, 'snapshot.sql');
210+
211+
assertEquals(Carbon::createFromTimestamp($timestamp), $snapshot->createdAt());
212+
});
213+
214+
it('handles empty snapshots gracefully', function () {
215+
$disk = m::mock(FilesystemAdapter::class);
216+
$disk->shouldReceive('get')
217+
->andReturn("");
218+
219+
$snapshot = new Snapshot($disk, 'empty.sql');
220+
221+
$snapshot->load();
222+
223+
// Expect no SQL to be executed
224+
DB::shouldReceive('unprepared')
225+
->never();
226+
});
227+
228+
it('drops all current tables when requested', function () {
229+
// Mock SchemaBuilder
230+
$schemaBuilderMock = m::mock();
231+
$schemaBuilderMock->shouldReceive('dropAllTables')->once();
232+
233+
// Mock DB facade
234+
DB::shouldReceive('connection')
235+
->andReturnSelf(); // Returns the DB connection
236+
DB::shouldReceive('getSchemaBuilder')
237+
->andReturn($schemaBuilderMock); // Returns the mocked schema builder
238+
DB::shouldReceive('getDefaultConnection')
239+
->andReturn('testing'); // Returns a mock default connection
240+
DB::shouldReceive('reconnect')->once();
241+
242+
// Instance of Snapshot
243+
$snapshot = new Snapshot(m::mock(FilesystemAdapter::class), 'snapshot.sql');
244+
245+
// Access protected method via Reflection
246+
$reflection = new ReflectionMethod(Snapshot::class, 'dropAllCurrentTables');
247+
$reflection->setAccessible(true);
248+
249+
// Invoke the protected method
250+
$reflection->invoke($snapshot);
251+
});

0 commit comments

Comments
 (0)