feat(database): add replace-existing option for PostgreSQL restores

Allow single-database PostgreSQL imports to drop matching objects before restore. The API and import form accept replace_existing, which adds --clean --if-exists to pg_restore, and pg_restore now uses --exit-on-error.
This commit is contained in:
Andras Bacsai
2026-09-09 11:48:17 +02:00
parent ab3b3926aa
commit 4accae95b5
13 changed files with 134 additions and 18 deletions
@@ -61,13 +61,14 @@ trait HandlesDatabaseImportsApi
{
$this->authorize('update', $resource);
$payload = $request->json()->all() ?: $request->request->all();
$allowed = ['source', 'upload_id', 's3_storage_uuid', 'path', 'dump_all'];
$allowed = ['source', 'upload_id', 's3_storage_uuid', 'path', 'dump_all', 'replace_existing'];
$validator = Validator::make($payload, [
'source' => ['required', Rule::in(['upload', 's3', 'server'])],
'upload_id' => ['required_if:source,upload', 'prohibited_unless:source,upload', 'uuid'],
's3_storage_uuid' => ['required_if:source,s3', 'prohibited_unless:source,s3', 'string'],
'path' => ['required_if:source,s3,server', 'prohibited_if:source,upload', 'string', 'max:4096'],
'dump_all' => ['sometimes', 'boolean'],
'replace_existing' => ['sometimes', 'boolean'],
]);
foreach (array_diff(array_keys($payload), $allowed) as $field) {
$validator->errors()->add($field, 'This field is not allowed.');
@@ -77,7 +78,7 @@ trait HandlesDatabaseImportsApi
}
try {
$source = new DatabaseImportSource((string) $payload['source'], $payload['upload_id'] ?? null, $payload['path'] ?? null, $payload['s3_storage_uuid'] ?? null, (bool) ($payload['dump_all'] ?? false));
$source = new DatabaseImportSource((string) $payload['source'], $payload['upload_id'] ?? null, $payload['path'] ?? null, $payload['s3_storage_uuid'] ?? null, (bool) ($payload['dump_all'] ?? false), (bool) ($payload['replace_existing'] ?? false));
$activity = app(StartDatabaseImport::class)->handle($resource, $source, $teamId);
} catch (DatabaseImportException $exception) {
return response()->json(['message' => $exception->getMessage()], $exception->status);
@@ -87,6 +88,7 @@ trait HandlesDatabaseImportsApi
'database_uuid' => $resource->uuid,
'database_name' => $resource->name,
'source' => $source->type,
'replace_existing' => $source->replaceExisting,
'activity_id' => $activity->id,
]);
$url = route($statusRoute, [...$routeParameters, 'activity_id' => $activity->id], false);
+3 -3
View File
@@ -16,9 +16,9 @@ use OpenApi\Attributes as OA;
new OA\Schema(
schema: 'DatabaseImportRequest',
oneOf: [
new OA\Schema(required: ['source', 'upload_id'], properties: [new OA\Property(property: 'source', type: 'string', enum: ['upload']), new OA\Property(property: 'upload_id', type: 'string', format: 'uuid'), new OA\Property(property: 'dump_all', type: 'boolean', default: false)]),
new OA\Schema(required: ['source', 's3_storage_uuid', 'path'], properties: [new OA\Property(property: 'source', type: 'string', enum: ['s3']), new OA\Property(property: 's3_storage_uuid', type: 'string'), new OA\Property(property: 'path', type: 'string'), new OA\Property(property: 'dump_all', type: 'boolean', default: false)]),
new OA\Schema(required: ['source', 'path'], properties: [new OA\Property(property: 'source', type: 'string', enum: ['server']), new OA\Property(property: 'path', type: 'string', example: '/var/backups/database.sql.gz'), new OA\Property(property: 'dump_all', type: 'boolean', default: false)]),
new OA\Schema(required: ['source', 'upload_id'], properties: [new OA\Property(property: 'source', type: 'string', enum: ['upload']), new OA\Property(property: 'upload_id', type: 'string', format: 'uuid'), new OA\Property(property: 'dump_all', type: 'boolean', default: false), new OA\Property(property: 'replace_existing', description: 'Drop matching PostgreSQL objects before restoring a single-database archive.', type: 'boolean', default: false)]),
new OA\Schema(required: ['source', 's3_storage_uuid', 'path'], properties: [new OA\Property(property: 'source', type: 'string', enum: ['s3']), new OA\Property(property: 's3_storage_uuid', type: 'string'), new OA\Property(property: 'path', type: 'string'), new OA\Property(property: 'dump_all', type: 'boolean', default: false), new OA\Property(property: 'replace_existing', description: 'Drop matching PostgreSQL objects before restoring a single-database archive.', type: 'boolean', default: false)]),
new OA\Schema(required: ['source', 'path'], properties: [new OA\Property(property: 'source', type: 'string', enum: ['server']), new OA\Property(property: 'path', type: 'string', example: '/var/backups/database.sql.gz'), new OA\Property(property: 'dump_all', type: 'boolean', default: false), new OA\Property(property: 'replace_existing', description: 'Drop matching PostgreSQL objects before restoring a single-database archive.', type: 'boolean', default: false)]),
],
type: 'object',
additionalProperties: false,