mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-10 06:43:18 +01:00
Add owner organization and owner user to audits
- Rename the audit actor columns user_type/user_id to actor_type/actor_id - Add owner_organization_id and owner_user_id to the audits table as foreign keys with cascade on delete, so that the audits of an organization or user are deleted together with it. The indexes and foreign keys are created without blocking writes on the large table. - Fill the owner columns for new audits via CustomAuditable. Organizations and users no longer record their own deletion audit, since it would reference the already deleted owner.
This commit is contained in:
committed by
Constantin Graf
parent
af54b0db73
commit
135984f9ef
@@ -18,7 +18,8 @@ class CreateAuditsTable extends Migration
|
||||
|
||||
Schema::connection($connection)->create($table, function (Blueprint $table): void {
|
||||
|
||||
$morphPrefix = config('audit.user.morph_prefix', 'user');
|
||||
// Note: The morph prefix is hardcoded, since the columns are renamed in a later migration
|
||||
$morphPrefix = 'user';
|
||||
|
||||
$table->bigIncrements('id');
|
||||
$table->string($morphPrefix.'_type')->nullable();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* Note: Renaming columns and adding nullable columns without default are metadata-only operations in PostgreSQL,
|
||||
* so this migration is fast even for a large audits table.
|
||||
* The indexes and foreign keys for the new columns are added in a separate non-transactional migration.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('audits', function (Blueprint $table): void {
|
||||
$table->renameColumn('user_type', 'actor_type');
|
||||
$table->renameColumn('user_id', 'actor_id');
|
||||
$table->renameIndex('audits_user_id_user_type_index', 'audits_actor_id_actor_type_index');
|
||||
$table->uuid('owner_user_id')->nullable();
|
||||
$table->uuid('owner_organization_id')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('audits', function (Blueprint $table): void {
|
||||
$table->dropColumn('owner_user_id');
|
||||
$table->dropColumn('owner_organization_id');
|
||||
$table->renameIndex('audits_actor_id_actor_type_index', 'audits_user_id_user_type_index');
|
||||
$table->renameColumn('actor_type', 'user_type');
|
||||
$table->renameColumn('actor_id', 'user_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* PostgreSQL cannot build an index concurrently inside a transaction.
|
||||
* Keeping this migration non-transactional prevents long write locks on the (large) audits table in production.
|
||||
* Every step is idempotent, so the migration can be re-run if it fails halfway.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $withinTransaction = false;
|
||||
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$this->createIndex('audits_owner_user_id_index', 'owner_user_id');
|
||||
$this->createIndex('audits_owner_organization_id_index', 'owner_organization_id');
|
||||
|
||||
$this->createForeignKey('audits_owner_user_id_foreign', 'owner_user_id', 'users');
|
||||
$this->createForeignKey('audits_owner_organization_id_foreign', 'owner_organization_id', 'organizations');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement('ALTER TABLE audits DROP CONSTRAINT IF EXISTS audits_owner_user_id_foreign');
|
||||
DB::statement('ALTER TABLE audits DROP CONSTRAINT IF EXISTS audits_owner_organization_id_foreign');
|
||||
DB::statement('DROP INDEX'.$this->concurrently().' IF EXISTS audits_owner_user_id_index');
|
||||
DB::statement('DROP INDEX'.$this->concurrently().' IF EXISTS audits_owner_organization_id_index');
|
||||
}
|
||||
|
||||
private function createIndex(string $index, string $column): void
|
||||
{
|
||||
$state = DB::selectOne(
|
||||
<<<'SQL'
|
||||
SELECT pg_index.indisvalid::int AS valid
|
||||
FROM pg_index
|
||||
JOIN pg_class ON pg_class.oid = pg_index.indexrelid
|
||||
JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
|
||||
WHERE pg_namespace.nspname = current_schema()
|
||||
AND pg_class.relname = ?
|
||||
SQL,
|
||||
[$index],
|
||||
);
|
||||
|
||||
if ($state !== null && (bool) $state->valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A failed concurrent index build leaves an invalid index behind, that needs to be dropped before rebuilding
|
||||
if ($state !== null) {
|
||||
DB::statement('DROP INDEX'.$this->concurrently().' '.$index);
|
||||
}
|
||||
|
||||
DB::statement('CREATE INDEX'.$this->concurrently().' '.$index.' ON audits ('.$column.')');
|
||||
}
|
||||
|
||||
private function createForeignKey(string $constraint, string $column, string $referencedTable): void
|
||||
{
|
||||
$exists = DB::selectOne(
|
||||
<<<'SQL'
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
JOIN pg_namespace ON pg_namespace.oid = pg_constraint.connamespace
|
||||
WHERE pg_namespace.nspname = current_schema()
|
||||
AND pg_constraint.conname = ?
|
||||
SQL,
|
||||
[$constraint],
|
||||
) !== null;
|
||||
|
||||
// Note: Adding the constraint as NOT VALID only needs a short lock, the validation of the existing rows
|
||||
// afterward does not block reads or writes on the audits table.
|
||||
if (! $exists) {
|
||||
DB::statement('ALTER TABLE audits ADD CONSTRAINT '.$constraint.' FOREIGN KEY ('.$column.') REFERENCES '.$referencedTable.' (id) ON DELETE CASCADE NOT VALID');
|
||||
}
|
||||
|
||||
DB::statement('ALTER TABLE audits VALIDATE CONSTRAINT '.$constraint);
|
||||
}
|
||||
|
||||
private function concurrently(): string
|
||||
{
|
||||
return DB::transactionLevel() === 0 ? ' CONCURRENTLY' : '';
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user