История коммитов

.
test(functional): run the functional suite against SQLite instead of the local stand
The suite used to drive requests against the database of the local stand: without one every test skipped itself, tests looked for an account the installation happened to have, and each of them had to remove what it wrote. It now boots the application against SQLite in memory, builds the schema with the migrations of the core and of every module, seeds the system roles, and rolls back the transaction of every test.

FunctionalUserFactory writes the accounts, so a test says which permissions its visitor holds instead of borrowing a role from an installation, and Fixture fills in the NOT NULL columns the legacy tables are full of. The comments pages create what each of them asks for and are really rendered now, rather than skipping for want of a row.
.
feat(database): let the schema adapter write to a database with global index names
Index names come from 9.x, where a table names its key after the column it covers, so a dozen tables have an index called user_id. MySQL keeps such names per table; SQLite keeps them per database, so the baseline migrations could not be run on it at all. SchemaPlatform collects what differs between databases — the word index and the scope of an index name — and prefixes the table where the namespace is shared. MySQL is unaffected.
.
refactor(auth): split the upgrade to roles by what may be repeated
The four commands an administrator had to run in the right order become two, told apart
by whether running them again is safe.

auth:sync-roles creates the built-in roles this version declares and grants them the
default permissions they are missing. Both halves only add, so it is meant to be run
after every update - a release or a module that declares a new role or permission has no
other way of reaching a site that is already installed. Only a fresh installation used to
get this, from the installer.

auth:migrate-legacy-access does what has to happen exactly once: it seeds the roles,
gives every member of staff the one their access level stood for, and turns the mod_*
settings into permissions. The second half revokes as well as grants - it brings the
roles back to what those settings say - so repeating it would undo whatever was arranged
in the panel afterwards.

Whether there is anything left to do is read off the database: while users.rights is
there the conversion is unfinished, and once the column has gone it can never be needed
again. That replaces the flag in a local config file, so OneTimeTaskTracker and
one_time_tasks.local.php are gone, and the guard of auth:drop-legacy-columns is now the
thing it actually cares about - nobody left with an access level and no role.

This also restores something the schema move had dropped: auth:upgrade-schema used to
seed the built-in roles, and after it was deleted nothing did that for an upgrading site.
It would have failed on the first command of the bridge, telling the administrator to run
a command that no longer exists.
.
refactor(forum): drop the legacy link converter
forum:normalize-message-links converted the posts of a site moving from 9.8 to 9.9:
it unwrapped the /redirect/ interstitial around the site's own links and rewrote the
query-string links of the older forum. 10.0 can only be reached from 9.9, where that
work is behind everyone.

ForumMessageLinkNormalizer goes with it - it existed to serve that command and nothing
else called it. The static analysis baseline loses the entries of both, and the
documentation of the upgrade no longer promises that migrate fills in slugs.
.
refactor(database): drop the slug migrations, the conversion belongs to 9.9
They could never do anything. The installer of 9.9 already created the slug columns and
their unique keys, and a site that came to 9.9 from 9.8 had them filled by the very
commands this replaced - and 10.0 can only be reached from 9.9. On a fresh installation
the baseline creates the columns and there are no rows; on an upgrading one everything
is already there.

The commands stay deleted: they converted 9.8 to 9.9, which is a path 10.0 no longer
has, the same reason the pre-9.9 install scripts were dropped.

The parity test keeps running only the baseline of a source. The fixture is a record of
what the old installer built, and the first migration written after the baseline would
otherwise break it with a confusing failure.
.
docs(database): write down the migration rules and bump the documentation
A guide for whoever writes the next migration: where the files live, what a migration
is given and what it must never reach for, and why - it is a historical record that has
to run unchanged on a site upgrading years from now. The module scaffold gains its
migrations directory, and the documentation submodule follows.
.
refactor(database): turn the slug converters into migrations
library:generate-slugs and downloads:generate-slugs were one-time commands an
administrator had to know about, find in the changelog and run by hand. They are
migrations of their modules now: the upgrade runs them along with everything else, and
the journal is what remembers they are done - not a flag in a local config file that a
restored dump knows nothing about.

They fill only what is empty, where the commands regenerated everything. A slug is the
address a page already has, so one that is set - by the old command or by hand - is
kept, and the ones generated around it reserve it and step out of its way.

forum:normalize-message-links stays a command. It asks which hosts count as internal,
falling back to the configured homeurl, and it works through two services of the module
and a model. A migration may depend on none of those: it has to run unchanged years
from now, when all three have moved on.

The parity test now runs the baseline of a source rather than everything in it. The
fixture is a record of what the old installer built, and a migration written after the
baseline is meant to take the schema somewhere that code never went.
.
feat(admin): say when the database is behind the code
An administrator who unpacked a new version had no way of learning that the database
still had to be brought up to it, short of reading the changelog and reaching for a
terminal. The admin panel now says so on every page, and offers the page that puts it
right.

The notice is shown only to whoever may open the maintenance page - to anybody else it
would be about something they cannot act on - and only while something is actually
waiting. What it counts is cached for a minute and dropped the moment a run finishes,
so it goes away with the migration rather than with the expiry, and a page of the panel
does not pay for a directory listing and a query.

A migration nobody can read is logged and counted as none: this is a notice on a page,
and taking down every page of the panel with it would hide the problem behind a worse
one.

migrate itself joins the maintenance page as a background task, so a site with no shell
can update its database from the browser. Background because on a large site the schema
takes longer than a web server waits, and a queued task cannot be started twice by a
reloaded page.

The language templates are regenerated, which also catches up with the line references
the schema move left behind.
.
refactor(database): move the schema into baseline migrations
The installer no longer builds the tables itself. A fresh installation and an
existing site upgrading to 10.0 now take the same path: the migrations of the core
and of every module. The four places the schema used to be described in - the
installer, AuthSchema, MailSchema and the install() of each module - are gone.

The baseline is a snapshot rather than a step, and the one place idempotence is
allowed: it creates what is missing and leaves the rest alone, so a site upgrading
from 9.9 arrives at the schema a fresh one is built with. Every migration after it is
a plain step forward.

BaselineSchemaParityTest holds the old code to account: the fixture is the exact MySQL
the installer and the module installers produced on a fresh database, recorded from
that code before it was deleted, and the migrations have to produce the same, table by
table. Verified against a real server as well - a database built from the migrations
and the one built by the old installer have the same 60 tables, with the same columns,
keys and options; five tables list their keys in a different order, which MySQL does
not care about.

The port grew two questions the baseline needs to be able to skip its own work:
hasForeignKey(), because a database may satisfy a foreign key with an index it already
has and then no index carries the name of the constraint, and fullText(), the word
index the forum and the library search with - left out on a database that has no such
thing.

AuthSchema and MailSchema also held the names of their tables, which models and queries
spell with them; those names live on in AuthTables and MailTables.

Tests build their schema by running migrations (Tests\Support\RunsMigrations) instead
of calling a second description of the same tables. A test may ask for single
migrations of a source, and for the core it has to: its index names come from 9.x,
where several tables name an index user_id, which MySQL keeps per table and SQLite per
database.
.
feat(database): add the migration engine
Migrations live in system/migrations and modules/<module>/migrations, one file per
step returning an anonymous class extending Migration. A migration is given a schema
and a connection and nothing else: it has to keep running unchanged on a site
upgrading years from now, and a model or a service it reached for will have moved on.

The journal is a table of the database it describes, so it travels with a restored
dump instead of being left behind in a config file. A row is identified by its source
and version rather than by a file name: two modules may well reach the same minute,
and the words after the timestamp are a description an author may correct.

Sources come from MigrationSourceProviderInterface, tagged by the container, so a
package that ships tables of its own joins the run without a line changed here. The
core is asked first and the modules after it - a module may point at the tables of the
core and the core points at nobody's.

A run takes a lock, applies one migration at a time and journals each before calling
it done. A failure stops the run and is not journalled, and the message says the step
may have carried out part of its work: MySQL does not roll back DDL.

Commands: migrate, migrate:status, migrate:rollback and make:migration. Rolling back
is a tool of development - it undoes a step the data has already been through, so a
migration that says nothing refuses instead, and a site not in debug mode has to pass
--force.