Database
Two migrations ship with the package and are registered automatically, so php artisan migrate is enough. See Installation.
polyslug_slugs
Every slug every model has ever had. The current one is flagged is_current; the rest are
kept as history so no published URL goes dead.
| Column | Type | Notes |
|---|---|---|
id | auto-increment | |
sluggable_type | string | Morph type. |
sluggable_id | string | Morph key, stored as a string so any host key type fits — integer, UUID, ULID, token. |
locale | string(16) | One slug per locale. |
scope | string | The uniqueness scope; '' means global per type and locale. |
slug | string | The slug itself. |
is_current | boolean | true for the live slug, false for history. |
enforce_unique | boolean | Defaults to true, so every existing slug keeps its guarantee. false for rows written by a unique: false model, which excludes them from the uniqueness index. Id-less models always stay true — they resolve by slug. |
created_at / updated_at | timestamps | |
deleted_at | soft delete |
The two unique indexes
polyslug_slugs_current_uniqueover(sluggable_type, locale, scope, lower(slug))for current, non-deleted, uniqueness-enforcing rows. This is the slug-uniqueness guarantee, and it is case-insensitive.polyslug_slugs_one_currentover(sluggable_type, sluggable_id, locale, scope)for current, non-deleted rows. This is the one-current-slug-per-record guarantee — without it a concurrent rename could leave two live rows and flap the canonical URL.
They are independent, which is why unique: false can relax the first without touching
the second.
The mechanism differs per engine, the guarantee does not:
- PostgreSQL and SQLite use a functional partial unique index — the same statement runs on both.
- MySQL 8.4 has neither partial nor functional-partial indexes, so the migration adds
VIRTUALgenerated key columns that areNULLfor non-current or soft-deleted rows (nulls never collide in a unique index), hashed to stay inside the index key-length limit, withLOWER(slug)mirroring the other engines'lower(slug).
polyslug_tokens
The key-to-token store for the stored-token encoders,
RandomTokenEncoder and
SequentialTokenEncoder. One row
per record, with (key_type, key_value) and token both unique — that uniqueness is what
makes a record's short opaque URL stable.
key_type is the morph type that owns the token, so Page#1 and Wishlist#1 hold separate
rows and separate tokens. An empty key_type is the untyped lane: tokens issued before
0.11.0 that the upgrade could not attribute, plus anything an encoder written against the
older IdentityEncoder contract stores. Reads fall back to it, so those URLs keep resolving;
the store adopts such a row the first time the record it belongs to renders a URL.
The column is NOT NULL with an empty default rather than nullable, and that is deliberate:
nulls do not collide in a unique index on any of the three engines, so a nullable owner would
let two rows hold the same key_value and reintroduce — inside the untyped lane — exactly
the ambiguity the column removes.
Tokens are stored rather than computed, which is what makes a length or scheme change safe on a live application: existing rows keep resolving unchanged, so the table legitimately holds a mix of widths.
Empty and inert unless a model uses one of those encoders.
polyslug_short_links
The /go short links: one row per (model, locale) with a
unique token, plus a unique constraint on the target triple so a model and locale can
never end up with two competing tokens.
Empty and inert until shortLink() is called.
Rolling back
Both migrations are reversible. The second one restores the unconditional
current_unique index before dropping the enforce_unique column it referenced; the
first drops the three tables in dependency order.