[3.0] Records what an upgrade found, so it can be put back (part 1 of 2) - #9656
Open
albertlast wants to merge 16 commits into
Open
[3.0] Records what an upgrade found, so it can be put back (part 1 of 2)#9656albertlast wants to merge 16 commits into
albertlast wants to merge 16 commits into
Conversation
A backup table holds the rows and, on both engines, very little else: LIKE copies columns and their types but no indexes, no keys and no sequence, so what an admin has afterwards is not enough to rebuild the table it came from. The migrations table is somewhere a migration can leave what it knew before it acted, grouped by the run that wrote it. The backup step uses it to keep each table's definition. A run that is interrupted and started again is the same run, finds what it already wrote and leaves it alone, so the definition stays the one taken before anything changed. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
A default is copied as the expression it is written with, so a column fed by a sequence left backup_smf_calendar_holidays.id_holiday pointing at smf_calendar_holidays_seq. The backup became an object that sequence could not be dropped without, and HolidaysToEvents drops it. Nothing is lost by leaving the defaults out now: the table's definition is recorded before the migrations run, and it holds the defaults along with the indexes, the keys and the sequence that a copy never carried. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
The progress data in Settings.php is written by preExit(), which the command line reaches only once the upgrade has finished -- and a killed process never reaches at all -- so on that path nothing is ever written and every attempt would look like a new run. The settings table is what both ways of running this share and what survives being killed, so the id lives there, and the step that records the new version forgets it again. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
The detail table alone had nowhere to say what a run was, and the id had to be parked in a setting to survive being killed. A run now has a row: where it started from, who started it, when it was last heard from, and where it had got to. A run is open until something says it finished, so a killed process leaves its row behind and the next attempt finds it and carries on as the same run. The position is written as each substep begins, which is a record of where an upgrade stopped rather than a place to resume from: restarting still begins at the top. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
backup_table() drops the backup table before it writes it, so an upgrade that is started again after being interrupted replaces a copy of the database as the admin had it with a copy of it half migrated. The run now records each table it has copied, and passes over the ones it did. This is what the copies are for. The definitions taken alongside them describe the tables as they were, and neither is worth much if the rows beside them describe something else. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
An index can be built over an expression rather than a column, and members has one over indexable_month_day(birthdate). The SQL that rebuilds the table names the function without saying what it is, so a definition on its own cannot rebuild the table it describes. The upgrader's own tables are left out of the backup at the same time. A copy of the record of what was copied helps nobody putting a forum back, and restoring it would put back an older account of the run doing the restoring. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
A database put back to the shape it had is not a working forum if Settings.php still describes the one it was upgraded to. db_character_set and db_mb4 say what the database is, and after a rollback they would be saying it about a database that is no longer there. Only the settings the upgrade writes are noted, and only the first time each is touched, so this is a note of what to put back rather than a copy of the file. Settings.php holds the database password, and a copy of that inside the database would be in every dump taken from then on, so it and its like are left out. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Everything needed was written down while the upgrade ran: what each table looked like, the functions its indexes call, the rows in the backup_ tables, and what the settings held. This puts them back in the order they depend on each other, drops the tables the upgrade added, and notes on the run that it has been undone. The upgrader's own tables are left alone throughout. They are where the answer is read from, and a rollback that took them with it could not record that it had happened. Nothing outside the database is touched. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
The database layer refuses a query holding a quote, and refuses one holding a semicolon whatever else it is told, so a recorded CREATE TABLE and the body of a recorded function were both turned away. Neither is built around anything supplied from outside: they are the database's own account of itself, read back. The installer and the migrations turn the same checks off around their own DDL. This puts the setting back as it found it. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
On MySQL the prefix can be `smf`.smf_ while list_tables() answers with bare names, so anything comparing one against the other never finds a table that is sitting right there. The upgrader saw a plain prefix and recorded everything; a rollback asked for through SSI saw the qualified one and reported there was nothing to put back. Table::exists() compares them that way too, which is worth knowing about separately. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
A prefix that names the database means nothing ever selected one, since every query SMF makes says which database it means. The recorded SQL does not, having been written while the prefix was plain, so on MySQL every statement was refused with 'No database selected' and the rollback reported itself done having restored nothing. Errors are still skipped, so that one bad statement does not abandon a half restored forum, but what was refused is kept and told to the caller. Skipped is not the same as unnoticed. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Dropping a table does not drop the sequence feeding it, so a sequence being put back is already there and asking for it again is refused. All 41 of them were refused on every rollback, and nothing noticed, because the sequences were already at usable values and the comparison saw nothing wrong. What the statement was for is the number it would have started at, and setval still gives that, whether or not the sequence survived. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Four things kept a run from describing the upgrade that made it. The list of tables to back up is indexed by substep, and array_filter leaves a hole where each name it dropped had been. The command line walks the dense list of substeps instead, so only the browser reached the gap, where it stopped mid backup on an undefined index. Config::getCurrentSettings() refuses a Settings.php touched since the request began. An upgrade writes that file more than once, so from the second write onwards it was reading its own work and giving up, and db_character_set and db_mb4 -- the two the rollback most needs -- were never written down. The run closed before that last write rather than after it, and the write then opened a run of its own to record settings into: a row with no backup behind it, left behind by every upgrade that finished. The run now opens where the admin presses Continue, so the settings that step changes have somewhere to go, and closes after the last thing the upgrade writes. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
statements() is the only part of the rollback that needs no database, and it is the part where a mistake is silent: a function body split on the semicolons inside it becomes fragments that parse as nothing, and the index leaning on that function never comes back. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
A rollback put the tables back and left SMF 3.0's own functions behind, so a database calling itself 2.1.7 still held group_concat, its final function and migrate_inet. Every routine in the public schema is now named in the record, whether or not it could be written down, since the names are what says which ones the upgrade went on to add. Those are dropped once the tables are, aggregates before the plain functions they are built from. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
live627
reviewed
Sep 6, 2026
It was left behind above UNRECORDED_SETTINGS, which has a docblock of its own, so the constant carried two and the property carried none. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
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.
Description
An upgrade that stops part way leaves a database that is neither the version it
was nor the version it was going to be. The upgrader already copies every table
into
backup_smf_*before it starts, but a copy of the rows is not enough to puta forum back: nothing records what shape those rows were in, which functions the
indexes over them call, or what the settings said beforehand.
This is the record. It is part 1 of 2 — this part writes everything down and can
put it back; [part 2] offers that to the admin and adds
--rollback.Two tables
migration_runsis one row per upgrade: which version it started from, which itreached, where it got to, who ran it, and when it started, finished or was undone.
migration_datahangs off it, holding four kinds of thing:data_typedefinitiontable_sql()backuproutinesettingSeveral runs can sit side by side, so a forum upgraded, put back and upgraded again
keeps an honest account of each attempt rather than overwriting the first.
What is deliberately not recorded
Settings.phpholdsdb_passwd. A copy of it inside the database would be inevery dump taken from then on, so
db_passwd,db_user,image_proxy_secret,auth_secretandmaintenance_tool_progressare excluded.Putting it back
MigrationRollbackreplays the routines, then each table's definition, refills itfrom its backup, drops the tables the upgrade added, drops the routines it added
(aggregates before the plain functions they are built from), puts the settings back
and stamps the run as undone. The upgrader's own two tables are excluded everywhere,
since a rollback that took them with it could not record that it had happened.
Nothing outside the database is touched. A forum that has been put back still has
the newer code on disk.
Verified
Full
2.1.7 -> 3.0 -> rollback -> 3.0cycles on MySQL 8.4 and PostgreSQL 17,from the 2.1.7 baseline forum, with the database compared against a single clean
upgrade each time:
ROW_FORMAT=DYNAMICclauserestating the server default is set aside.
and the re-upgrade restores all 20.
routines on PostgreSQL. Both runs report
version_from = 2.1.7.request there and the command line does not.
Tests
statements()is the only part of the rollback that needs no database, and it isthe part where a mistake is silent: a PostgreSQL function body split on the
semicolons inside it becomes fragments that parse as nothing, and the index leaning
on that function never comes back.
tests/Unit/MigrationRollbackTest.phpcovers itthrough
ReflectionMethod— dollar quoting, escaped quotes, backticks andpositional parameters. The rest needs
Db::$dband is out of the suite's reach.Depends on
table_sql()is what records a table's shape, and it has four bugs that stop itround-tripping. All four are open separately and this does not work without them:
trim()UNLOGGEDis droppedKnown limits
Table::exists()compares a database-qualified prefix against bare table names,so it is always false on MySQL when the prefix is
`smf`.smf_.MigrationData::prefix()works around it here; the underlying bug is elsewhere and is not fixed by this.
Issues References (Fixes|Related|Closes)