[3.0] Offers to put the database back when an upgrade did not finish (part 2 of 2) - #9657
Open
albertlast wants to merge 22 commits into
Open
[3.0] Offers to put the database back when an upgrade did not finish (part 2 of 2)#9657albertlast wants to merge 22 commits into
albertlast wants to merge 22 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>
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>
An upgrade that stopped part way leaves an admin in front of a forum that is neither one version nor the other, with two ways out: carry on, or put things back. Only the second was missing. The offer is made on the options page, and only for a run that stopped part way and took a backup, since nothing else has the rows to put back. An upgrade that finished is not offered: undoing a working forum is something to go looking for, not something to be suggested. On the command line the same thing is asked for with --rollback. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Upgrades a baseline, marks the run as one that did not finish, and asks the upgrader for --rollback. Timing a kill so that it lands after the backup and before the end is a race, and what is being checked here is the offer rather than the interruption, which interrupt-upgrade.sh already covers. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
A run that has been undone was still being treated as one under way, so upgrading again picked it up where it left off: the tables it had already backed up were passed over, and the definitions describing the database it had already put back were kept. The second attempt ran with no backup of its own at all. Being undone finishes a run whatever its finishing time says. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
The rollback runs inside the upgrader, which started while the forum was on the version it has just been taken off. Stopping at step two, that process wrote its progress data out saying so, and the next upgrade read it, believed the work was done and skipped every v2_1 migration on a database that had just been put back to 2.1. It still said the upgrade was complete: the indexes kept their old names and twelve tables were left as 2.1 had them. Asking for the rollback also opened a run of its own, since writing any setting asks which run is under way. It copied nothing, and left open the next upgrade would take it up as unfinished work and pass over the backup it never made. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Config::$modSettings was read before the rollback and still names the version that has just gone, so the progress data was written out saying the forum was on 3.0 when it had been put back to 2.1, and the next upgrade skipped every v2_1 migration. The run itself knows what the forum was on before it touched anything, which is what it is on again. A run that never reached the backup step describes no table, so noting a setting or two on the way does not make it one worth keeping. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
The database is 2.1 again and the files on disk are still 3.0, so the forum will not run until one of them is changed. The notice that the rollback had happened said none of this and offered nowhere to go. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
albertlast
force-pushed
the
3.0/migration-rollback-offer
branch
from
September 7, 2026 04:16
fa6b0a0 to
043862d
Compare
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
Part 2 of 2. #9656 writes down what an upgrade found and can put it back; this
part is how an admin reaches that.
This PR contains #9656's commits as well, because its base has to be
release-3.0. Please merge #9656 first — the six commits on top of it are:The offer
An upgrade that stopped part way leaves a run with no finishing time on it. When
the upgrader finds one, the Upgrade Options step says so and offers a second way
out beside carrying on:
Only a run that actually took a backup is offered, since nothing else has the rows
to put back. A finished upgrade is never offered: putting a working forum back is
something an admin should have to go looking for, not something the upgrader
suggests.
Afterwards
The done notice says what state the forum is now in, rather than leaving the admin
on a page with nowhere to go:
Following that link starts a fresh run, which takes its own backup — the rolled
back run is not adopted and not overwritten.
On the command line
php upgrade.php --rollbackdoes the same thing without the browser.Also here
Three fixes that only a rollback-then-upgrade cycle could expose:
its own.
currentRun()now passes over a run that has been undone.Config::$modSettingsin memory, whichby then named the version that had just been removed. It comes from the run's
own
version_frominstead.maintenance_tool_progressbehind, pointing the next upgrade ata step that no longer applied to the database in front of it. In one run that
skipped the entire
v2_1migration batch while printing "Upgrade complete!".Verified
The offer, the button, the rollback and the upgrade after it, driven through a
browser on MySQL and on the command line on both engines.
.docker/try-rollback-offer.shsets up an unfinished run and asks the upgrader to undo it, so the offer can be
exercised without having to time a kill.
Issues References (Fixes|Related|Closes)