Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e04fe3c
Keeps what a table looked like before the migrations
albertlast Sep 6, 2026
cbba1eb
Copies rows into a backup without copying its source's sequence
albertlast Sep 6, 2026
9e65c10
Keeps the run's identity where a killed process cannot take it
albertlast Sep 6, 2026
c65554b
Gives each upgrade a row of its own
albertlast Sep 6, 2026
2f94acb
Leaves a backup this run already took alone
albertlast Sep 6, 2026
8d29787
Records the functions a table's definition leans on
albertlast Sep 6, 2026
3ddd6ac
Notes what a setting held before the upgrade changed it
albertlast Sep 6, 2026
feb383e
Puts a database back the way an upgrade found it
albertlast Sep 6, 2026
fa71362
Lets the recorded SQL past the checks meant for assembled queries
albertlast Sep 6, 2026
e8d9070
Finds the tables when the prefix names the database too
albertlast Sep 6, 2026
ca0ba28
Reports what a rollback could not run, and finds the tables to run it on
albertlast Sep 6, 2026
f241e1b
Puts a sequence where recreating it would have started it
albertlast Sep 6, 2026
90da295
Keeps the record of a run whole from the first write to the last
albertlast Sep 6, 2026
30e0375
Covers the splitting a rollback depends on
albertlast Sep 6, 2026
dc97399
Takes back the routines an upgrade added
albertlast Sep 6, 2026
d47145e
Puts the docblock for id_run above id_run
albertlast Sep 7, 2026
8e0e139
Offers to put the database back when an upgrade did not finish
albertlast Sep 6, 2026
8a84d27
Adds a way to exercise the offer to put the database back
albertlast Sep 6, 2026
3e33b26
Starts a new run when upgrading again after a rollback
albertlast Sep 6, 2026
ff68507
Leaves nothing behind that would misdirect the next upgrade
albertlast Sep 6, 2026
0674f6e
Takes the version to go back to from the run, not from memory
albertlast Sep 6, 2026
043862d
Says what to do next once the database is back
albertlast Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .docker/try-rollback-offer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# Upgrades a 2.1 database, marks the run as one that did not finish, and then
# asks the upgrader to put the database back with --rollback.
#
# .docker/try-rollback-offer.sh --engine postgresql --baseline ../SMF-2.1/.docker/baseline/artifacts/2.1.7-1/small/postgres.sql
#
# This exists to exercise the offer the upgrader makes when it finds a run that
# stopped part way. Killing a real upgrade at the right moment is a race, and
# what is being tested here is the offer rather than the interruption, which is
# covered by interrupt-upgrade.sh.
set -euo pipefail

. "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
. "$(dirname -- "${BASH_SOURCE[0]}")/upgrade-readings.sh"

ENGINE=''
BASELINE=''
OUT="$DOCKER_DIR/offer"

while [ $# -gt 0 ]; do
case "$1" in
--engine) ENGINE="$2"; shift 2 ;;
--baseline) BASELINE="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
*) die "unknown argument: $1" ;;
esac
done

[ -n "$ENGINE" ] || die 'need --engine'
[ -n "$BASELINE" ] || die 'need --baseline'

cd "$BOARD_DIR"
mkdir -p "$OUT"
OUT=$(cd -- "$OUT" && pwd)

log "${ENGINE}: emptying the database"
"$DOCKER_DIR/reset.sh" --engine "$ENGINE" >/dev/null

log "${ENGINE}: loading $(basename -- "$BASELINE")"
load_baseline "$ENGINE" "$BASELINE"

log "${ENGINE}: upgrading"
UPGRADE_ARGS='--backup' run_upgrade "$ENGINE" "$OUT/upgrade-${ENGINE}.log" \
|| die "${ENGINE}: the upgrade failed -- $OUT/upgrade-${ENGINE}.log"

log "${ENGINE}: upgraded to SMF $(installed_version "$ENGINE")"
snapshot "$ENGINE" upgraded "$OUT"

# What a killed process leaves behind is a run with no finishing time on it.
# Setting that here is the difference between testing the offer and testing
# how well a kill can be timed.
log "${ENGINE}: marking the run as one that did not finish"

if [ "$ENGINE" = 'mysql' ]; then
docker compose exec -T -e MYSQL_PWD="$DB_PASSWORD" mysql \
mysql -u"$DB_USER" -D "$DB_NAME" -e "UPDATE ${DB_PREFIX}migration_runs SET time_finished = 0;"
else
docker compose exec -T postgres \
psql -q -U "$DB_USER" -d "$DB_NAME" -c "UPDATE ${DB_PREFIX}migration_runs SET time_finished = 0;"
fi

log "${ENGINE}: asking the upgrader to put it back"
rm -f "$BOARD_DIR/install.php"
cp "$BOARD_DIR/other/upgrade.php" "$BOARD_DIR/upgrade.php"

status=0
docker compose exec -T web php upgrade.php --rollback > "$OUT/rollback-${ENGINE}.log" 2>&1 || status=$?

rm -f "$BOARD_DIR/upgrade.php"

printf '\n'
cat "$OUT/rollback-${ENGINE}.log"
printf '\n'

log "${ENGINE}: the forum is now SMF $(installed_version "$ENGINE")"

exit "$status"
8 changes: 8 additions & 0 deletions Languages/en_US/Maintenance.php
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,14 @@
// Upgrade options
$txt['upgrade_areyouready'] = 'Before the upgrade gets underway, please review the options below and press "Continue" when you are ready to begin.';
$txt['upgrade_backup_table'] = 'Backup SMF tables in your database using the prefix <span class="bbc_tt">{0}</span>';
$txt['upgrade_rollback_title'] = 'An unfinished upgrade';
$txt['upgrade_rollback_offer'] = 'An upgrade from {version} was started {date} and did not finish. You can carry on with it, or put the database back the way it was before it started. Only the database is put back: the files on disk are left alone.';
$txt['upgrade_rollback_button'] = 'Put the database back';
$txt['upgrade_rollback_done'] = 'The database has been put back the way it was before the upgrade started. The files on disk are still {version}, so the forum will not run until you either <a href="{url}">start the upgrade again</a> or put your old files back from a backup.';
$txt['log_rollback_starting'] = 'Putting the database back to {version}';
$txt['log_rollback_done'] = 'Put back {count} things.';
$txt['log_rollback_failed'] = 'Nothing was put back: {error}';
$txt['log_rollback_refused'] = 'The database refused: {statement}';
$txt['upgrade_maintenance'] = 'Put the forum into maintenance mode during upgrade.';
$txt['upgrade_maintenance_title'] = 'Maintenance Title:';
$txt['upgrade_maintenance_message'] = 'Maintenance Message:';
Expand Down
12 changes: 8 additions & 4 deletions Sources/Db/APIs/PostgreSQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -975,14 +975,18 @@ public function backup_table(string $table, string $backup_table): object|bool
);
}

/**
* @todo Should we create backups of sequences as well?
*/
// The copy takes the columns and their types. It does not take their
// defaults, because a default is copied as the expression it is written
// with: a column fed by a sequence would arrive pointing at the live
// table's sequence, leaving the backup as an object that sequence
// cannot be dropped without and drawing ids from it if anything were
// inserted here. What each column defaults to is kept by the upgrader
// alongside the rest of the table's definition, which is a fuller
// record than a copy of the defaults would have been.
$result = $this->query(
'CREATE TABLE {raw:backup_table}
(
LIKE {raw:table}
INCLUDING DEFAULTS
)',
[
'backup_table' => $backup_table,
Expand Down
110 changes: 110 additions & 0 deletions Sources/Db/Schema/v3_0/MigrationData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2026 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 3.0 Alpha 4
*/

declare(strict_types=1);

namespace SMF\Db\Schema\v3_0;

use SMF\Db\Schema\Column;
use SMF\Db\Schema\DbIndex;
use SMF\Db\Schema\Table;

/**
* Defines all the properties for a database table.
*/
class MigrationData extends Table
{
/****************
* Public methods
****************/

/**
* Constructor.
*/
public function __construct()
{
$this->name = 'migration_data';

$this->columns = [
'id_entry' => new Column(
name: 'id_entry',
type: 'int',
unsigned: true,
not_null: true,
auto: true,
),
'id_run' => new Column(
name: 'id_run',
type: 'varchar',
size: 36,
not_null: true,
default: '',
),
'migration' => new Column(
name: 'migration',
type: 'varchar',
size: 255,
not_null: true,
default: '',
),
'data_type' => new Column(
name: 'data_type',
type: 'varchar',
size: 30,
not_null: true,
default: '',
),
'data_key' => new Column(
name: 'data_key',
type: 'varchar',
size: 255,
not_null: true,
default: '',
),
'data' => new Column(
name: 'data',
type: 'mediumtext',
not_null: true,
),
'time_added' => new Column(
name: 'time_added',
type: 'bigint',
unsigned: true,
not_null: true,
default: 0,
),
];

$this->indexes = [
'primary' => new DbIndex(
type: 'primary',
columns: [
[
'name' => 'id_entry',
],
],
),
'idx_run' => new DbIndex(
name: 'idx_run',
columns: [
[
'name' => 'id_run',
],
[
'name' => 'data_type',
],
],
),
];
}
}
137 changes: 137 additions & 0 deletions Sources/Db/Schema/v3_0/MigrationRuns.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2026 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 3.0 Alpha 4
*/

declare(strict_types=1);

namespace SMF\Db\Schema\v3_0;

use SMF\Db\Schema\Column;
use SMF\Db\Schema\DbIndex;
use SMF\Db\Schema\Table;

/**
* Defines all the properties for a database table.
*/
class MigrationRuns extends Table
{
/****************
* Public methods
****************/

/**
* Constructor.
*/
public function __construct()
{
$this->name = 'migration_runs';

$this->columns = [
'id_run' => new Column(
name: 'id_run',
type: 'varchar',
size: 36,
not_null: true,
default: '',
),
'version_from' => new Column(
name: 'version_from',
type: 'varchar',
size: 20,
not_null: true,
default: '',
),
'version_to' => new Column(
name: 'version_to',
type: 'varchar',
size: 20,
not_null: true,
default: '',
),
'step' => new Column(
name: 'step',
type: 'smallint',
unsigned: true,
not_null: true,
default: 0,
),
'substep' => new Column(
name: 'substep',
type: 'int',
unsigned: true,
not_null: true,
default: 0,
),
'substep_start' => new Column(
name: 'substep_start',
type: 'int',
unsigned: true,
not_null: true,
default: 0,
),
'id_member' => new Column(
name: 'id_member',
type: 'int',
unsigned: true,
not_null: true,
default: 0,
),
'time_started' => new Column(
name: 'time_started',
type: 'bigint',
unsigned: true,
not_null: true,
default: 0,
),
'time_updated' => new Column(
name: 'time_updated',
type: 'bigint',
unsigned: true,
not_null: true,
default: 0,
),
'time_finished' => new Column(
name: 'time_finished',
type: 'bigint',
unsigned: true,
not_null: true,
default: 0,
),
'time_rolled_back' => new Column(
name: 'time_rolled_back',
type: 'bigint',
unsigned: true,
not_null: true,
default: 0,
),
];

$this->indexes = [
'primary' => new DbIndex(
type: 'primary',
columns: [
[
'name' => 'id_run',
],
],
),
'idx_time_finished' => new DbIndex(
name: 'idx_time_finished',
columns: [
[
'name' => 'time_finished',
],
],
),
];
}
}
Loading