DEPLOY.LEGACY.OSC_ARTIFACT — What an online-schema-change tool left behind
- Category: safety
- Severity:
lowfor a leftover table,mediumfor a leftover trigger — the axis earns the difference: a table costs storage, a trigger costs every write on a live table - Level: 0
- Downtime class:
online— the artifacts exist and, apart from the triggers, do nothing - Stability: stable
- Suites: deploy
- Command:
sqlens:postdeployonly - Applies to: MySQL 8.4 and newer
- Not transferable to MariaDB: this page describes MySQL 8.4 behavior and the schemes of the tools that run against it
What it reads
information_schema.TABLES and information_schema.TRIGGERS, for names a tool generates:
| pattern | tool | what it was |
|---|---|---|
_<table>_gho | gh-ost | the ghost table the new schema is built in |
_<table>_ghc | gh-ost | the changelog table it tracks its own progress in |
_<table>_del | gh-ost | the original, renamed out of the way at cut-over |
_<table>_new | pt-osc | the new table it copies into |
_<table>_old | pt-osc | the original after the atomic rename |
#sql-…, #sql2-…, #sql-ib… | InnoDB | the temporary table an ALGORITHM=COPY rebuild works in |
pt_osc_<db>_<table>_ins|upd|del | pt-osc | the triggers that keep its copy in step |
This is not a suffix hunt, and the leading underscore is why
Its PostgreSQL sibling DEPLOY.LEGACY.ORPHAN_TRANSITION_OBJECT matches shapes a person writes
when they mean "temporary", and says out loud that a name is not evidence. This list is narrower: no
project writes _orders_gho by hand, and #sql-1234_a cannot even be created without backticks
because the # makes the name unquotable in ordinary SQL.
The discriminator is the leading underscore. photo_gho ends in _gho and is an ordinary
permanent name; _orders_gho is a tool's output. Both are in the test fixture, and only one is
reported.
It is still undetermined, for a different reason
The sibling is undetermined because a name proves nothing. Here the name genuinely is a tool's
output — but a run still in flight looks exactly like one that died. gh-ost creates _t_gho and
works in it for hours; catching that mid-migration and calling it wreckage reports the healthy case
as the broken one, which is the more expensive of the two mistakes.
So: confirm nothing is running, then remove it.
--expect-shadow can settle it for you
The one thing that separates a dead artifact from a live one, without asking you to check by hand, is whether any migration describes it. It never should — an online-schema-change tool creates these itself — so an artifact that is also absent from a full replay of your migrations is wreckage rather than work in progress:
php artisan sqlens:postdeploy --expect-shadow
That replays your migrations into a throwaway shadow database and compares. When the comparison
reports the same object as unexpected_in_database, the finding is raised from undetermined to
fail. It never goes the other way: a comparison that could not run, or that accounted for the
object, leaves the finding at the rung it earned.
The option is off by default because it creates a database and therefore falls under the production
guard — and a run without it says so, on stderr and in the report's run.expectation block, so a
green post-deploy can never quietly mean nobody looked.
The trigger is the one to look at today
Everything else on this list costs storage and backup time — real, and payable at leisure. A
leftover pt_osc_* trigger fires on every INSERT, UPDATE and DELETE on the real table,
for as long as it exists, writing into a table nothing reads.
That is why it carries medium where the tables carry low, and why its message says so in its own
sentence rather than being folded into "an artifact was found".
DEPLOY.LEGACY.INVALID_INDEX on MySQL answers NOT APPLICABLE, out loud
InnoDB has no invalid-index state: a build is either committed or rolled back, and there is no
equivalent of PostgreSQL's indisvalid = false.
The tempting shape is no check at all. That is exactly the silent green this package refuses — a user who read the PostgreSQL documentation, ran the same command against MySQL and saw nothing has been told two things at once, there is no wreckage and this question was never asked, with no way to tell which. On the engine where somebody is most likely to be comparing two databases, that is the worst place to leave the distinction unmade.
So the check runs, answers undetermined, names the reason, and points at the case that is real
on this engine: the #sql- temporary table above. It reads nothing — no query, no session, no
privilege — because the answer is a property of the engine rather than of the instance.
Primum non nocere
Reports; never drops. A #sql- table in particular may be one InnoDB is using right now, and the
message says so rather than leaving the reader to find out.
Sources
- MySQL 8.4 — INFORMATION_SCHEMA — the two views this check reads, and the privilege filtering that makes an under-privileged read come back empty rather than fail
- gh-ost — the
_gho/_ghc/_delscheme - pt-online-schema-change —
the
_new/_oldtables and thept_osc_*triggers