›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-hold
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Holdables and holders
  • Capacity and slots
  • Acquiring holds
  • Releasing and extending
  • Expiry
  • Hold state

Operations

  • Events and listeners
  • Scheduling expiry
  • Pruning history
  • Concurrency and databases

Reference

  • Configuration
  • Console commands
  • Database schema
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Holdables and holders
  • Capacity and slots
  • Acquiring holds
  • Releasing and extending
  • Expiry
  • Hold state

Operations

  • Events and listeners
  • Scheduling expiry
  • Pruning history
  • Concurrency and databases

Reference

  • Configuration
  • Console commands
  • Database schema
  • Testing
  • Troubleshooting

byrcsc/laravel-hold · 1.x

Database schema.

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.

The table

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.

The columns

ColumnNullableWritten by
idnoThe database
holdable_typenoAcquisition
holdable_idnoAcquisition
holder_typenoAcquisition
holder_idnoAcquisition
released_by_typeyesrelease(by: ...)
released_by_idyesrelease(by: ...)
expires_atyesAcquisition, then extend()
released_atyesrelease()
expired_atyeshold:expire
metadatayesAcquisition, then release()
created_atnoAcquisition
updated_atnoAny 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.

Identity columns follow config

holdable_key_type shapes holdable_id. holder_key_type shapes holder_id and released_by_id.

Config valueColumn produced
intunsignedBigInteger
uuiduuid
ulidulid
stringstring

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.

The two indexes

{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.

Index names

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.

No foreign keys

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.

Growth

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:

  • Indefinite holds are never pruned. Their expires_at is null, so no retention window reaches them. Release them deliberately.
  • The hot index carries every row, including dead ones. Pruning keeps acquisition's count cheap as much as it keeps the table small.

What to read next

  • Configuration for the three keys that shape this table.
  • Hold state for what the timestamps mean when you query them directly.
  • Concurrency and databases for how acquisition uses the hot index.
PreviousConsole commandsNextTesting
View source

On this page

  1. The table
  2. The columns
  3. Identity columns follow config
  4. The two indexes
  5. Index names
  6. No foreign keys
  7. Growth
  8. What to read next