Skip to content

Commit d5d2d34

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

File tree

2 files changed

+195
-48
lines changed

2 files changed

+195
-48
lines changed

src/Snapshot.php

+89-48
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ protected function loadAsync(?string $connectionName = null): void
7979
$dbDumpContents = gzdecode($dbDumpContents);
8080
}
8181

82+
if (empty(trim($dbDumpContents))) {
83+
// Ignoriere leeren Snapshot
84+
return;
85+
}
86+
8287
DB::connection($connectionName)->unprepared($dbDumpContents);
8388
}
8489

@@ -96,69 +101,105 @@ protected function shouldIgnoreLine(string $line): bool
96101

97102
protected function loadStream(?string $connectionName = null): void
98103
{
99-
$directory = (new TemporaryDirectory(config('db-snapshots.temporary_directory_path')))->create();
104+
$temporaryDirectory = (new TemporaryDirectory(config('db-snapshots.temporary_directory_path')))->create();
105+
106+
$this->configureFilesystemDisk($temporaryDirectory->path());
100107

108+
$localDisk = $this->filesystemFactory->disk(self::class);
109+
110+
try {
111+
$this->processStream($localDisk, $connectionName);
112+
} finally {
113+
$temporaryDirectory->delete();
114+
}
115+
}
116+
117+
private function configureFilesystemDisk(string $path): void
118+
{
101119
config([
102120
'filesystems.disks.' . self::class => [
103121
'driver' => 'local',
104-
'root' => $directory->path(),
122+
'root' => $path,
105123
'throw' => false,
106-
]
124+
],
107125
]);
126+
}
108127

109-
$localDisk = $this->filesystemFactory->disk(self::class);
128+
private function processStream($localDisk, ?string $connectionName): void
129+
{
130+
$this->copyStreamToLocalDisk($localDisk);
131+
132+
$stream = $this->openStream($localDisk);
110133

111134
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-
}
135+
$this->processStatements($stream, $connectionName);
136+
} finally {
137+
$this->closeStream($stream);
138+
}
139+
}
140+
141+
private function copyStreamToLocalDisk($localDisk): void
142+
{
143+
$localDisk->writeStream($this->fileName, $this->disk->readStream($this->fileName));
144+
}
145+
146+
private function openStream($localDisk)
147+
{
148+
return $this->compressionExtension === 'gz'
149+
? gzopen($localDisk->path($this->fileName), 'r')
150+
: $localDisk->readStream($this->fileName);
151+
}
152+
153+
private function closeStream($stream): void
154+
{
155+
$this->compressionExtension === 'gz' ? gzclose($stream) : fclose($stream);
156+
}
157+
158+
private function processStatements($stream, ?string $connectionName): void
159+
{
160+
$statement = '';
161+
while (!feof($stream)) {
162+
$chunk = $this->readChunk($stream);
163+
$lines = explode("\n", $chunk);
164+
165+
foreach ($lines as $idx => $line) {
166+
if ($this->shouldIgnoreLine($line)) {
167+
continue;
145168
}
146169

147-
if (str_ends_with(trim($statement), ';')) {
148-
yield $statement;
170+
$statement .= $line;
171+
172+
if ($this->isLastLineOfChunk($lines, $idx)) {
173+
break;
149174
}
150175

151-
if ($this->compressionExtension === 'gz') {
152-
gzclose($stream);
153-
} else {
154-
fclose($stream);
176+
if ($this->isCompleteStatement($statement)) {
177+
DB::connection($connectionName)->unprepared($statement);
178+
$statement = '';
155179
}
156-
})->each(function (string $statement) use ($connectionName) {
157-
DB::connection($connectionName)->unprepared($statement);
158-
});
159-
} finally {
160-
$directory->delete();
180+
}
161181
}
182+
183+
if ($this->isCompleteStatement($statement)) {
184+
DB::connection($connectionName)->unprepared($statement);
185+
}
186+
}
187+
188+
private function readChunk($stream): string
189+
{
190+
return $this->compressionExtension === 'gz'
191+
? gzread($stream, self::STREAM_BUFFER_SIZE)
192+
: fread($stream, self::STREAM_BUFFER_SIZE);
193+
}
194+
195+
private function isLastLineOfChunk(array $lines, int $idx): bool
196+
{
197+
return count($lines) === $idx + 1;
198+
}
199+
200+
private function isCompleteStatement(string $statement): bool
201+
{
202+
return str_ends_with(trim($statement), ';');
162203
}
163204

164205
public function delete(): void

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)