Skip to content

B4 — declare the journal tables to Doctrine, which wanted to drop them - #274

Open
gplanchat wants to merge 2 commits into
mainfrom
fix/journal-dbal-invisible-de-doctrine
Open

B4 — declare the journal tables to Doctrine, which wanted to drop them#274
gplanchat wants to merge 2 commits into
mainfrom
fix/journal-dbal-invisible-de-doctrine

Conversation

@gplanchat

@gplanchat gplanchat commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Second audit finding. doctrine:migrations:diff was generating DROP TABLE statements against the journal.

The defect

The four tables of the DBAL bridge were declared to Doctrine by no configureSchema and no postGenerateSchema listener. addToSchema() was only ever called by ensure(), that is, by the bridge itself — verified, a single caller in the whole repository.

A docblock claimed otherwise:

Declares the missing tables; also wired to configureSchema on the bundle side.

Consequence: Doctrine's tooling builds the expected schema from entities alone, does not find these tables, and treats them as orphans. The generated migration drops them — the journal, and with it every in-flight execution.

The fix

DurableSchema::configureSchema(Schema $schema, Connection $forConnection, \Closure $isSameDatabase) takes the signature of the three upstream adapters that solve exactly this problem — DoctrineDbalAdapter, DoctrineTransport, DoctrineDbalStore — database probe included. It skips, table by table, the ones the schema already carries.

A listener in the bundle wires it to postGenerateSchema, registered only if the ORM's event class exists: the DBAL bridge works without the ORM, and an application that only has DBAL has no schema to complete.

auto_setup

It accompanies the fix rather than widening it: once migrations own the schema, the bridge's lazy creation writes behind their back. Defaults to true, so nothing changes unless asked. This is the switch the Doctrine transport exposes for the same reason.

Tests

Six cases, RED first (Call to undefined method ...::configureSchema(), Unknown named parameter $autoSetup):

  • the four tables join an empty schema;
  • a table already present is not redeclared, the others still are;
  • nothing is declared when the probe says "different database";
  • everything is declared when it says "same database" on another connection;
  • auto_setup: false creates no table;
  • auto_setup at its default still creates all four — regression net, this case already passed.

Documentation

The bridge README carried "there is no migration to run" without saying what happened if the application did have one. It now carries the declaration, the connection boundary and auto_setup. The docblock that lied is corrected.


Addendum — 2026-09-04, after cross-review

The original text justified a deliberate caution: the listener did not lean on the upstream AbstractSchemaListener, because filterSchemaChanges() "is recent and does not exist across the whole ^6.4 || ^7.0 || ^8.0 range". It therefore declared the tables only when the journal writes on the very Connection object the ORM inspects, passing static fn(): bool => false as the probe.

The premise was wrong, and seven independent review axes reached it. AbstractSchemaListener::getIsSameDatabaseChecker() is declared identically on v6.4.0, 7.2 and 8.0 — checked against the source. What moved is filterSchemaChanges(), the other method. With the justification gone, so was the caution it funded: as soon as the journal has its own named connection — the case this branch's own README describes — nothing was declared and doctrine:migrations:diff went back to generating its DROP TABLE. B4 was closed for the default case and silently reopened for the case it targeted.

The probe is now copied from AbstractSchemaListener — twenty lines that depend only on DBAL — rather than inherited: it is protected there, and extending the class would impose symfony/doctrine-bridge on the bundle.

Two more things the review found, both now fixed:

  • doctrine/orm had been written into require-dev by hand, out of alphabetical order despite sort-packages, and never resolved. The eight jobs that run composer install were red: no test had ever run on this branch. The ^2.19 half of the constraint was unsatisfiable here anyway — ORM 2 wants DBAL ^2.13.1 || ^3.2, the bridge wants ^3.7 || ^4.0, and the lock carries DBAL 4.4.4. Constraint narrowed to ^3.0, lock updated.
  • The only production path had no test. The six cases above call configureSchema() and pass it the probe, which proves the shape of the schema and never the decision to declare. DurableSchemaListenerTest now exercises postGenerateSchema() with the real probe on three cases; the second one fails on the code that preceded this addendum.

Still open, out of scope for this branch: the schema_filter bypass (upstream goes through filterSchemaChanges(), absent in 6.4), and the lack of a supported creation path when auto_setup: false — a durable:setup, along the lines of messenger:setup-transports.

gplanchat and others added 2 commits September 3, 2026 23:41
… supprimer

Les quatre tables du pont n'étaient déclarées à Doctrine par aucun `configureSchema`
ni écouteur `postGenerateSchema` — alors qu'un docbloc de `DurableSchema` affirmait
« branché aussi sur configureSchema côté bundle ». `doctrine:schema:update` et
`doctrine:migrations:diff` construisent le schéma attendu depuis les seules entités,
n'y trouvent pas ces tables, et les traitent en orphelines : la migration générée les
supprime, avec le journal et toutes les exécutions en vol.

`DurableSchema::configureSchema()` reprend la signature des adaptateurs amont qui
résolvent le même problème — `DoctrineDbalAdapter`, `DoctrineTransport`, `DoctrineDbalStore` —
y compris le paramètre de sonde `$isSameDatabase`, et saute par table celles que le
schéma porte déjà.

L'écouteur du bundle ne s'adosse volontairement pas à `AbstractSchemaListener` :
`filterSchemaChanges()` n'existe pas sur toute la plage ^6.4 à ^8.0 que le bundle
accepte, et un appel fatal sur la version basse coûterait plus que ce qu'il apporte.
Il ne déclare donc que sur la connexion même que l'ORM inspecte — ne rien déclarer
laisse l'exploitant gérer ce schéma, là où déclarer à tort créerait des tables dans la
mauvaise base. Le paramètre de sonde reste offert à qui sait trancher.

`dbal.auto_setup` accompagne le correctif : dès que les migrations tiennent le schéma,
les deux mécanismes écriraient l'un derrière l'autre. Défaut `true`, donc rien ne change
sans le demander. C'est aussi l'interrupteur que le transport Doctrine expose.

`doctrine/orm` est déclaré — require-dev à la racine comme illuminate/*, suggest sur le
bundle — plutôt que référencé en douce : l'écouteur ne s'enregistre que si la classe
d'événement existe.

Suite unit : 1079 tests contre 1073 sur main, mêmes 4 erreurs d'environnement. PHPStan
signale localement trois class.notFound sur Doctrine\ORM, de même nature que les deux
sur Illuminate\Cache déjà présents sur main : le paquet est déclaré, il n'est pas
installé sur ce poste.

Refs: B4 de documentation/audit/

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oduction est testé

La relecture croisée a montré que le correctif se refermait sur lui-même : l'écouteur passait
`static fn(): bool => false` à `configureSchema()`, au motif écrit en docbloc que l'API
d'`AbstractSchemaListener` aurait bougé entre Symfony ^6.4 et ^8.0. Vérification faite sur
`v6.4.0`, `7.2` et `8.0`, `getIsSameDatabaseChecker()` y est déclarée à l'identique ; ce qui a
bougé est `filterSchemaChanges()`, l'autre méthode. La justification étant fausse, la prudence
qu'elle fondait ne l'était pas : dès que le journal a sa propre connexion — le cas que le README
de cette branche décrit lui-même — plus rien n'était déclaré et `doctrine:migrations:diff`
regénérait ses `DROP TABLE`. B4 était fermé pour le cas par défaut, rouvert pour son cas d'usage.

La sonde est donc recopiée depuis `AbstractSchemaListener`, vingt lignes qui ne dépendent que de
la DBAL, plutôt qu'héritée : elle y est `protected`, et l'étendre imposerait
`symfony/doctrine-bridge` au bundle.

`doctrine/orm` était écrit à la main dans `require-dev`, hors ordre alphabétique malgré
`sort-packages`, et jamais résolu : les huit jobs qui font un `composer install` étaient rouges,
aucun test n'avait tourné sur cette branche. La moitié `^2.19` de la contrainte était de toute
façon insatisfiable ici — ORM 2 veut DBAL `^2.13.1 || ^3.2`, le pont veut `^3.7 || ^4.0` et le
lock porte DBAL 4.4.4. Contrainte ramenée à `^3.0`, lock à jour.

Le seul chemin de production n'avait aucun test : les cas existants appellent `configureSchema()`
en lui passant la sonde, ce qui prouve la forme du schéma et jamais la décision de déclarer.
`DurableSchemaListenerTest` exerce `postGenerateSchema()` avec la vraie sonde sur trois cas — même
connexion, deux connexions sur le même fichier SQLite, deux bases distinctes. Le deuxième échoue
sur le code d'avant ce commit (0 table déclarée au lieu de 4).

Le README annonçait une abstention que le code ne pratique plus.

Restent ouverts, hors périmètre de cette branche : le contournement de `schema_filter` (l'amont
passe par `filterSchemaChanges()`, absente en 6.4), et l'absence de chemin de création supporté
quand `auto_setup: false` — un `durable:setup`, à l'image de `messenger:setup-transports`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gplanchat gplanchat changed the title B4 — déclarer les tables du journal à Doctrine, qui voulait les supprimer B4 — declare the journal tables to Doctrine, which wanted to drop them Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant