›
byrcsc/laravel-hold · 1.x
Inspect the holds table, indexes, and configurable identity columns.
The package adds one table and changes none of yours. Neither trait adds a column to the model that uses it.
Schema::create('holds', function (Blueprint $table) {
$table->id();
$table->string('holdable_type');
$table->unsignedBigInteger('holdable_id');
$table->string('holder_type');
$table->unsignedBigInteger('holder_id');
$table->string('released_by_type')->nullable();
$table->unsignedBigInteger('released_by_id')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamp('released_at')->nullable();
$table->timestamp('expired_at')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(
['holdable_type', 'holdable_id', 'released_at', 'expires_at'],
'holds_holdable_active_index',
);
$table->index(['holder_type', 'holder_id'], 'holds_holder_index');
});That is the shipped migration with the default int key types substituted in.
The published file builds the identity columns from config instead.
| Column | Nullable | Written by |
|---|---|---|
id | no | The database |
holdable_type | no | Acquisition |
holdable_id | no | Acquisition |
holder_type | no | Acquisition |
holder_id | no | Acquisition |
released_by_type | yes | release(by: ...) |
released_by_id | yes | release(by: ...) |
expires_at | yes | Acquisition, then extend() |
released_at | yes | release() |
expired_at | yes | hold:expire |
metadata | yes | Acquisition, then release() |
created_at | no | Acquisition |
updated_at | no | Any write |
Three of them carry the whole state model:
expires_at null means the hold is indefinite.released_at set means released, and it wins over expiry.expired_at is bookkeeping. It appears in no availability rule, and the
expired() scope ignores it.See hold state for the truth table these derive.
holdable_key_type shapes holdable_id. holder_key_type shapes holder_id
and released_by_id.
| Config value | Column produced |
|---|---|
int | unsignedBigInteger |
uuid | uuid |
ulid | ulid |
string | string |
The migration reads config at migration time and throws
InvalidArgumentException on any other value:
Unsupported hold key type [bigint]. Use int, uuid, ulid, or string.The *_type columns are always strings, because they store class names or
morph map aliases.
{table}_holdable_active_index covers
(holdable_type, holdable_id, released_at, expires_at).
That is the hot query. Counting one resource's active holds filters on exactly these columns in this order, and acquisition runs that count inside a lock on every attempt.
{table}_holder_index covers (holder_type, holder_id).
That is the holder side, backing $user->holds and $user->activeHolds.
Both are named explicitly rather than derived. Laravel would name the first one
from every column in it, producing
holds_holdable_type_holdable_id_released_at_expires_at_index: 60 characters
on the default table name, and past MySQL's 64-character identifier limit as
soon as anyone renames the table to something longer. PostgreSQL truncates at
63 silently.
Renaming the table through config renames both indexes on a fresh install,
since each is built as {table}_holdable_active_index and
{table}_holder_index.
The identity columns are polymorphic, so there is nothing to constrain them to.
A hold outlives its resource. Deleting a holdable leaves its hold rows in
place, and $hold->holdable returns null for them. If you want holds removed
with their resource, do it in your own model's deleting hook or with a
scheduled clean-up.
The table grows by one row per acquisition and never shrinks on its own.
Released and expired rows stay as history until
hold:prune removes them.
Two things to size for:
expires_at is null, so no
retention window reaches them. Release them deliberately.