festbench-web/app/Models/BenchmarkRun.php

109 lines
3.4 KiB
PHP

<?php
namespace App\Models;
use Database\Factories\BenchmarkRunFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class BenchmarkRun extends Model
{
/** @use HasFactory<BenchmarkRunFactory> */
use HasFactory;
public const VISIBILITY_PUBLIC = 'public';
public const VISIBILITY_PRIVATE = 'private';
/**
* Columns written only at submission/import time. Everything here is
* immutable once stored; user updates may only touch $overlay.
*/
protected $fillable = [
'run_id', 'ran_at', 'suite', 'run_key',
'model_id', 'model_quant', 'model_quant_raw', 'model_path',
'engine', 'engine_build', 'n_ctx', 'slots',
'gpu', 'gpu_count', 'gpu_source', 'host', 'client', 'endpoint',
'label', 'config', 'image_digest', 'validators', 'suite_versions',
'harness', 'harness_ver', 'harness_mode', 'user_id',
'visibility', 'notes',
];
/** The only fields a user may change after submission. */
public const OVERLAY_FIELDS = ['visibility', 'notes'];
protected function casts(): array
{
return [
'ran_at' => 'datetime',
'validators' => 'array',
'suite_versions' => 'array',
'n_ctx' => 'integer',
'slots' => 'integer',
'gpu_count' => 'integer',
];
}
/**
* Submitted metrics are immutable: any update that touches a column
* outside OVERLAY_FIELDS is silently reverted before it hits the
* database. This is the safety net for every user-facing update path.
*/
protected static function booted(): void
{
static::updating(function (self $run): void {
foreach ($run->getDirty() as $key => $_) {
if (! in_array($key, self::OVERLAY_FIELDS, true)) {
$run->setAttribute($key, $run->getOriginal($key));
}
}
});
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function results(): HasMany
{
return $this->hasMany(RunResult::class);
}
public function isOwnedBy(?User $user): bool
{
return $user !== null && $this->user_id === $user->id;
}
/**
* Runs visible to $user: everything public, plus the user's own private
* runs. Applied as a local scope so every listing query starts from it.
*/
public function scopeVisibleTo(Builder $query, ?User $user): Builder
{
return $query->where(self::visibleClause($user));
}
/**
* The visibility predicate as a standalone closure, so joined queries
* (e.g. the flat results table) can apply the exact same rule without
* duplicating the logic.
*
* @return \Closure(Builder): Builder
*/
public static function visibleClause(?User $user): \Closure
{
return function (Builder $q) use ($user) {
$q->where('visibility', self::VISIBILITY_PUBLIC);
if ($user !== null) {
$q->orWhere(fn (Builder $own) => $own
->where('visibility', self::VISIBILITY_PRIVATE)
->where('user_id', $user->id));
}
};
}
}