From 09c5cbcbb0d8d6c0d63425138756ca41890c5cfa Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:05:18 +0200 Subject: [PATCH] feat(database): record which sqlite database a connected volume comes from --- .../Database/Sqlite/ConnectApplication.php | 4 ++- app/Models/LocalPersistentVolume.php | 21 +++++++++++---- app/Models/StandaloneSqlite.php | 8 ++++++ ...e_id_to_local_persistent_volumes_table.php | 26 +++++++++++++++++++ .../Feature/SqliteConnectApplicationTest.php | 5 +++- 5 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 database/migrations/2026_09_26_152908_add_standalone_sqlite_id_to_local_persistent_volumes_table.php diff --git a/app/Livewire/Project/Database/Sqlite/ConnectApplication.php b/app/Livewire/Project/Database/Sqlite/ConnectApplication.php index 88c49b3bec..eabbcd5de3 100644 --- a/app/Livewire/Project/Database/Sqlite/ConnectApplication.php +++ b/app/Livewire/Project/Database/Sqlite/ConnectApplication.php @@ -80,13 +80,15 @@ class ConnectApplication extends Component throw new \Exception('Docker Compose applications are not supported: Coolify renames named volumes in compose files.'); } - if ($application->persistentStorages()->where('name', $this->volumeName)->exists()) { + if ($application->persistentStorages()->where('standalone_sqlite_id', $this->database->id)->exists()) { throw new \Exception("{$application->name} already mounts this database volume."); } + LocalPersistentVolume::create([ 'name' => $this->volumeName, 'mount_path' => $this->mountPath, 'host_path' => null, + 'standalone_sqlite_id' => $this->database->id, 'resource_id' => $application->id, 'resource_type' => $application->getMorphClass(), 'is_preview_suffix_enabled' => false, diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php index 0b5cc908f4..ccd843d119 100644 --- a/app/Models/LocalPersistentVolume.php +++ b/app/Models/LocalPersistentVolume.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Support\Str; use Symfony\Component\Yaml\Yaml; @@ -22,6 +23,7 @@ class LocalPersistentVolume extends BaseModel 'name', 'mount_path', 'host_path', + 'standalone_sqlite_id', 'container_id', 'resource_type', 'resource_id', @@ -57,6 +59,14 @@ class LocalPersistentVolume extends BaseModel return $this->morphMany(ScheduledVolumeBackup::class, 'backupable'); } + /** + * The SQLite database this volume was connected from, if any. + */ + public function standaloneSqlite(): BelongsTo + { + return $this->belongsTo(StandaloneSqlite::class); + } + public function abortIfScheduledBackupsExist(): void { if ($this->scheduledBackups()->exists()) { @@ -69,16 +79,17 @@ class LocalPersistentVolume extends BaseModel */ public function isSharedWithAnotherResource(): bool { - if (filled($this->host_path)) { + if ($this->standalone_sqlite_id !== null) { + return true; + } + + if ($this->resource_type !== StandaloneSqlite::class) { return false; } return static::query() + ->where('standalone_sqlite_id', $this->resource_id) ->where('name', $this->name) - ->where(function ($query): void { - $query->where('resource_type', '!=', $this->resource_type) - ->orWhere('resource_id', '!=', $this->resource_id); - }) ->exists(); } diff --git a/app/Models/StandaloneSqlite.php b/app/Models/StandaloneSqlite.php index 797254bc8c..0f1da4f7f7 100644 --- a/app/Models/StandaloneSqlite.php +++ b/app/Models/StandaloneSqlite.php @@ -306,6 +306,14 @@ class StandaloneSqlite extends BaseModel return $this->morphMany(LocalPersistentVolume::class, 'resource'); } + /** + * Volumes of other resources that mount this database's data volume. + */ + public function connectedVolumes() + { + return $this->hasMany(LocalPersistentVolume::class, 'standalone_sqlite_id'); + } + public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); diff --git a/database/migrations/2026_09_26_152908_add_standalone_sqlite_id_to_local_persistent_volumes_table.php b/database/migrations/2026_09_26_152908_add_standalone_sqlite_id_to_local_persistent_volumes_table.php new file mode 100644 index 0000000000..b5e1b40656 --- /dev/null +++ b/database/migrations/2026_09_26_152908_add_standalone_sqlite_id_to_local_persistent_volumes_table.php @@ -0,0 +1,26 @@ +foreignId('standalone_sqlite_id')->nullable()->after('host_path')->constrained('standalone_sqlites')->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('local_persistent_volumes', function (Blueprint $table) { + $table->dropConstrainedForeignId('standalone_sqlite_id'); + }); + } +}; diff --git a/tests/Feature/SqliteConnectApplicationTest.php b/tests/Feature/SqliteConnectApplicationTest.php index eac3789032..3b2a3c37fc 100644 --- a/tests/Feature/SqliteConnectApplicationTest.php +++ b/tests/Feature/SqliteConnectApplicationTest.php @@ -96,7 +96,9 @@ it('mounts the data volume into the selected application and redirects to its st expect($volume->name)->toBe('sqlite-data-'.$this->sqlite->uuid) ->and($volume->mount_path)->toBe('/app/database') - ->and($volume->is_preview_suffix_enabled)->toBeFalse(); + ->and($volume->is_preview_suffix_enabled)->toBeFalse() + ->and($volume->standalone_sqlite_id)->toBe($this->sqlite->id) + ->and($this->sqlite->connectedVolumes()->sole()->resource->is($this->application))->toBeTrue(); }); it('refuses to mount the volume into compose applications', function () { @@ -148,6 +150,7 @@ it('keeps a docker volume shared with another resource when the application volu 'name' => 'sqlite-data-'.$this->sqlite->uuid, 'mount_path' => StandaloneSqlite::DATA_DIRECTORY, 'host_path' => null, + 'standalone_sqlite_id' => $this->sqlite->id, 'resource_id' => $this->application->id, 'resource_type' => $this->application->getMorphClass(), 'is_preview_suffix_enabled' => false,