test(schema): dbDelta re-ALTERs forever when a CREATE TABLE lies, and nothing measured it (#1087 passo 7) - #1095
Merged
Conversation
… nothing measured it (#1087 passo 7) `dbDelta()` ALTERs away the difference between a `CREATE TABLE` and what MySQL actually stored. When the two disagree in a way the statement can never satisfy, it re-ALTERs on every single run, forever, in silence. No gate saw that class. `ActivatorSqlTest` reads the statement as text, with no database. The `fresh-install` job starts from an empty schema, so it only ever watches the CREATE succeed — never the second pass. The post-deploy smoke asserts the tables exist, not that the schema still matches. #997 measured it exactly once, by hand, against a real MariaDB (41 database errors on the self-scheduling tables, 1 after the cleanup) and never again — while two CREATE statements changed in #1091 and #1093. The new step runs in `fresh-install`, the only job with a real MariaDB AND a freshly created schema, and replays every CREATE through `dbDelta( $sql, false )` against the table it just built. The change list must be empty; anything in it is an ALTER on every future activation. The extraction moves to `.github/scripts/ffc-create-statements.php` so this gate and `ActivatorSqlTest` measure the same 37 statements — the rule `uninstall.php` already follows as one manifest, and the direct lesson of steps 3 and 6: two private scans of one thing is how a denominator goes wrong unnoticed (the row ruler saw 45 of 53 classes). The gate fails on blindness rather than passing: an empty scan, a count that diverges from a wider net (single quotes, heredoc), or a table name it cannot resolve all abort instead of counting as clean. Recorded along the way: `ffc_device_signals` is the only table whose dbDelta is deliberately not guarded by `table_exists()`, so it already runs against an existing table on every activation in every install — the #997 class, live and never measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XWx9qJdjZdAq8crxM9GCU
rpgmem
marked this pull request as ready for review
September 7, 2026 02:38
rpgmem
enabled auto-merge (squash)
September 7, 2026 02:38
…s=1) is fatal The gate failed on its first run for a reason that has nothing to do with the schema: `wp eval-file` reads the file and `eval()`s it, and a `declare(strict_types=1)` is only legal as the first statement of a script — inside an eval it is a fatal error, so the step died before reaching a single CREATE TABLE. Load WordPress the way `fresh-install-check.php` does in this same job instead: take the wp root as an argument and `require` its `wp-load.php`. Same invocation shape as its sibling, the declaration stays, and the command that eval()s files is out of the loop. Verified: the original failure reproduces exactly (`eval()` of this file before the change → "strict_types declaration must be the very first statement in the script"), and after it the file parses whole and stops at its own usage guard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XWx9qJdjZdAq8crxM9GCU
…Delta re-ALTERs (#1087 passo 7) The gate ran for real and did not come back empty. Against tables it had just created from those very statements, `dbDelta()` wanted 33 changes across 14 `CREATE TABLE` literals — an ALTER on every future activation, in every install. Three families, and only one is a schema question: 1. Display width the server supplies. The statement writes `int unsigned` and MariaDB stores `int(10) unsigned`; dbDelta compares text and ALTERs the difference forever. 9 statements. 2. `json`, which MariaDB implements as `longtext` plus a CHECK, so the statement asks for `json` and `SHOW CREATE TABLE` answers `longtext`. 6 statements. 3. A column or index the live table does not have — `ffc_reregistrations.audience_id` and a `KEY auth_code` on two tables. Not cosmetic: statement and table genuinely disagree, and three files declare `ffc_custom_fields` while three declare `ffc_reregistration_submissions`. So the gate blocks against a frozen baseline rather than against zero, in the shape this repository already uses four times over (module boundary, vacuous tests, superglobal casts, the row ruler): a change not listed fails, and a listed change that stops happening also fails, so a fix is locked in the moment it lands. Fixing the 14 is follow-up work on #1087, not something to fold into the pull request that adds the measurement. Two things the run settled. `ffc_device_signals` — the only table whose dbDelta is not guarded by `table_exists()`, so it re-runs on every activation in every install — is NOT among the 14. It was the obvious suspect and it is clean; the drift is in tables where the repeated ALTER is latent instead. And there is no local way to regenerate the baseline, since it needs a live MariaDB, so the gate prints the exact block to paste and dumps `SHOW CREATE TABLE` for the statements whose disagreement is not a type spelling — the data family 3 needs. `CreateStatementsParserTest` gains a check that every baseline key still names a file that declares that table: an entry whose statement moved would otherwise match nothing and fail nothing, which is the same silent exemption this issue has spent three steps on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XWx9qJdjZdAq8crxM9GCU
Coverage Report for CI Build 34077374481Warning No base build found for commit Coverage: 89.969%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsRequires a base build to compare against. How to fix this → Coverage Stats
💛 - Coveralls |
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
dbDelta()compara umCREATE TABLEcom o que o MySQL efetivamente gravou e emiteALTERpara a diferença. Quando os dois discordam de um jeito que a declaração nunca satisfaz, ele re-ALTERa em toda ativação, para sempre, em silêncio.ActivatorSqlTestlê a declaração como texto, sem banco; o jobfresh-installsó vê oCREATEda primeira vez; o smoke pós-deploy confere que a tabela existe, não que o schema ainda bate. O Test audit by mocked boundary (stages 1–3) #997 mediu essa classe uma vez, à mão (41 erros de banco nas três tabelas de self-scheduling, 1 depois da limpeza) e nunca mais — e duas declarações mudaram desde então, no fix(schema): a fresh install of ffc_submissions was born with 7 of its 25 columns (#1087) #1091 e no fix(schema): a fresh install of ffc_reregistration_submissions lacked auth_code and magic_token (#1087 passo 5) #1093..github/scripts/dbdelta-idempotence-check.phproda no jobfresh-install(o único com MariaDB real e schema recém-criado) e repassa cadaCREATE TABLEpelodbDelta()contra a tabela que ele mesmo acabou de criar.dbDeltadevolve as mudanças que fez; contra a tabela criada por aquela mesma declaração, a lista tem de vir vazia. O que sobrar é umALTERem toda execução futura..github/scripts/ffc-create-statements.phppassa a ser a fonte única das 37 declarações: oActivatorSqlTest(regras de texto do dbDelta) e o gate novo leem o mesmo conjunto. É a regra que o repositório já aplica aouninstall.php— um manifesto, vários consumidores — e a lição direta dos passos 3 e 6 desta issue: duas varreduras privadas da mesma coisa é como um denominador fica errado sem ninguém notar (a régua de linhas via 45 de 53 classes).Achado registrado no caminho
ffc_device_signalsé a única tabela cujodbDeltadeliberadamente não é protegido portable_exists()(documentado emclass-ffc-rate-limit-activator.php, para o mesmo caminho servir instalação nova e a atualização 6.3.1→6.3.2). Ou seja: ela já rodadbDeltacontra tabela existente em toda ativação, em toda instalação — exatamente a classe do #997, viva em produção e nunca medida. É a primeira coisa que este gate deveria ser confiado a dizer.Type of change
Nenhuma mudança de runtime: o diff é um gate de CI, uma extração compartilhada e um teste.
Test plan
vendor/bin/phpunit— suíte completa verde (7666 testes, 21971 asserções)vendor/bin/phpunit --filter 'ActivatorSql|SchemaAgreement|CreateStatementsParser'— 10 testes, 18 asserções$sobrando no SQL finalfresh-installentre a checagem de ativação e a desinstalaçãoABSPATH,dbDelta()e um MySQL vivo, e o contêiner de desenvolvimento não tem banco. Quem o executa pela primeira vez é o jobfresh-installdesta PR (MariaDB 11.8) — e é ele que dirá seffc_device_signalsestá limpa.Testes novos
tests/Unit/CreateStatementsParserTest.phpcobre o parser nas duas coisas que podem dar errado — enxergar menos do que existe, e não resolver um nome — em vez do caminho feliz. Os quatro idiomas de nomeação são verificados por nome, não por contagem: cada um derrotou uma versão anterior do resolvedor (a janela de 25 linhas, depois a de 40), e uma regressão apareceria só como um total um pouco menor que ninguém lê.Checklist
CHANGELOG.mdatualizado ([Unreleased]→Added)Referencia #1087 (passo 7). A issue segue aberta.
🤖 Generated with Claude Code
https://claude.ai/code/session_012XWx9qJdjZdAq8crxM9GCU
Generated by Claude Code