diff --git a/Languages/en_US/Maintenance.php b/Languages/en_US/Maintenance.php
index 38b817e242..3f3f4ee447 100644
--- a/Languages/en_US/Maintenance.php
+++ b/Languages/en_US/Maintenance.php
@@ -65,13 +65,7 @@
 $txt['maintenance_step'] = 'Step';
 $txt['maintenance_overall_progress'] = 'Overall Progress';
 $txt['maintenance_substep_progress'] = 'Step Progress';
-$txt['maintenance_time_elasped_ms'] = 'Time Elapsed {m, plural,
-	one {# minute}
-	other {# minutes}
-} and {s, plural,
-	one {# second}
-	other {# seconds}
-}';
+$txt['maintenance_time_elapsed'] = 'Time Elapsed: ';
 
 // File Permissions.
 $txt['chmod_linux_info'] = 'If you have a shell account, the following command can automatically correct permissions on these files';
diff --git a/Sources/Config.php b/Sources/Config.php
index 05fa3155df..bac937dcaa 100644
--- a/Sources/Config.php
+++ b/Sources/Config.php
@@ -998,23 +998,23 @@ public static function set(array $settings): void
 		}
 
 		// Make sure the paths are correct... at least try to fix them.
-		if (empty(self::$boarddir) || !is_dir(realpath(self::$boarddir))) {
+		if (empty(self::$boarddir) || !is_dir((string) realpath(self::$boarddir))) {
 			self::$boarddir = !empty($_SERVER['SCRIPT_FILENAME']) ? \dirname(realpath($_SERVER['SCRIPT_FILENAME'])) : \dirname(__DIR__);
 		}
 
-		if ((empty(self::$sourcedir) || !is_dir(realpath(self::$sourcedir))) && is_dir(self::$boarddir . '/Sources')) {
+		if ((empty(self::$sourcedir) || !is_dir((string) realpath(self::$sourcedir))) && is_dir(self::$boarddir . '/Sources')) {
 			self::$sourcedir = self::$boarddir . '/Sources';
 		}
 
-		if ((empty(self::$vendordir) || !is_dir(realpath(self::$vendordir))) && is_dir(self::$boarddir . '/vendor')) {
+		if ((empty(self::$vendordir) || !is_dir((string) realpath(self::$vendordir))) && is_dir(self::$boarddir . '/vendor')) {
 			self::$vendordir = self::$boarddir . '/vendor';
 		}
 
-		if ((empty(self::$packagesdir) || !is_dir(realpath(self::$packagesdir))) && is_dir(self::$boarddir . '/Packages')) {
+		if ((empty(self::$packagesdir) || !is_dir((string) realpath(self::$packagesdir))) && is_dir(self::$boarddir . '/Packages')) {
 			self::$packagesdir = self::$boarddir . '/Packages';
 		}
 
-		if ((empty(self::$languagesdir) || !is_dir(realpath(self::$languagesdir))) && is_dir(self::$boarddir . '/Languages')) {
+		if ((empty(self::$languagesdir) || !is_dir((string) realpath(self::$languagesdir))) && is_dir(self::$boarddir . '/Languages')) {
 			self::$languagesdir = self::$boarddir . '/Languages';
 		}
 
diff --git a/Sources/Db/APIs/MySQL.php b/Sources/Db/APIs/MySQL.php
index 27106cd73f..e4b712a4f9 100644
--- a/Sources/Db/APIs/MySQL.php
+++ b/Sources/Db/APIs/MySQL.php
@@ -938,19 +938,29 @@ public function backup_table(string $table, string $backup_table): object|bool
 			],
 		);
 
-		// If this failed, we go old school.
 		if ($result) {
+			$columns = [];
+
+			// Do we have any generated columns to deal with?
+			foreach ($this->list_columns($table, true) as $column) {
+				// Skip generated columns in the insert statement.
+				if (empty($column['generation_expression'])) {
+					$columns[] = $column['name'];
+				}
+			}
+
 			$request = $this->query(
 				'INSERT INTO {raw:backup_table}
-				SELECT *
+				({raw:columns})
+				SELECT {raw:columns}
 				FROM {raw:table}',
 				[
 					'backup_table' => $backup_table,
 					'table' => $table,
+					'columns' => implode(', ', $columns),
 				],
 			);
 
-			// Old school or no school?
 			if ($request) {
 				return $request;
 			}
@@ -1047,6 +1057,13 @@ public function backup_table(string $table, string $backup_table): object|bool
 			);
 		}
 
+		// Restore the generation expressions on any generated columns.
+		foreach ($this->list_columns($table, true) as $column) {
+			if (!empty($column['generation_expression'])) {
+				$this->change_column($backup_table, $column['name'], $column);
+			}
+		}
+
 		return $request;
 	}
 
@@ -1373,7 +1390,7 @@ public function add_column(string $table_name, array $column_info, array $parame
 		$column_info['size'] = isset($column_info['size']) && is_numeric($column_info['size']) ? $column_info['size'] : null;
 
 		// Now add the thing!
-		$this->query(
+		$result = $this->query(
 			'ALTER TABLE ' . $short_table_name . '
 			ADD ' . $this->create_query_column($column_info) . (empty($column_info['auto']) ? '' : ' primary key'),
 			[
@@ -1381,7 +1398,7 @@ public function add_column(string $table_name, array $column_info, array $parame
 			],
 		);
 
-		return true;
+		return $result !== false;
 	}
 
 	/**
@@ -1913,7 +1930,7 @@ public function create_table(string $table_name, array $columns, array $indexes
 		}
 
 		// Create the table!
-		$this->query(
+		$result = $this->query(
 			$table_query,
 			[
 				'security_override' => true,
@@ -1954,7 +1971,7 @@ public function create_table(string $table_name, array $columns, array $indexes
 			$this->drop_table($short_table_name . '_old');
 		}
 
-		return true;
+		return $result !== false;
 	}
 
 	/**
@@ -1980,15 +1997,14 @@ public function drop_table(string $table_name, array $parameters = [], string $e
 		$tables = $this->list_tables($database);
 
 		if (\in_array($full_table_name, $tables)) {
-			$query = 'DROP TABLE ' . $short_table_name;
-			$this->query(
-				$query,
+			$result = $this->query(
+				'DROP TABLE ' . $short_table_name,
 				[
 					'security_override' => true,
 				],
 			);
 
-			return true;
+			return $result !== false;
 		}
 
 		// Otherwise do 'nout.
@@ -2032,14 +2048,14 @@ public function rename_table(string $old_name, string $new_name, bool $allowed_r
 			return false;
 		}
 
-		$this->query(
+		$result = $this->query(
 			'ALTER TABLE ' . $short_old_name . ' RENAME ' . $short_new_name,
 			[
 				'security_override' => true,
 			],
 		);
 
-		return true;
+		return $result !== false;
 	}
 
 	/**
@@ -2088,14 +2104,12 @@ public function list_columns(string $table_name, bool $detail = false, array $pa
 		$database = !empty($match[2]) ? $match[2] : $this->name;
 
 		$result = $this->query(
-			'SELECT column_name "Field", COLUMN_TYPE "Type", is_nullable "Null", COLUMN_KEY "Key" , column_default "Default", extra "Extra", generation_expression "generation_expression"
-			FROM information_schema.columns
-			WHERE table_name = {string:table_name}
-				AND table_schema = {string:db_name}
-			ORDER BY ordinal_position',
+			'SHOW COLUMNS
+			FROM {identifier:table_name}
+			IN {identifier:db}',
 			[
+				'db' => strtr($database, ['`' => '']),
 				'table_name' => $real_table_name,
-				'db_name' => $this->name,
 			],
 		);
 		$columns = [];
@@ -2109,8 +2123,7 @@ public function list_columns(string $table_name, bool $detail = false, array $pa
 
 				// Can we split out the size?
 				if (preg_match('~^(.+?)\s*\((\d+)\)$~', $row['Type'], $matches)) {
-					$type = $matches[1];
-					$size = $matches[2];
+					[$type, $size] = $this->calculate_type($matches[1], (int) $matches[2], true);
 				} elseif (preg_match('~^(.+?)\s+unsigned$~', $row['Type'], $matches)) {
 					$type = $matches[1];
 					$size = null;
@@ -2135,12 +2148,32 @@ public function list_columns(string $table_name, bool $detail = false, array $pa
 					unset($unsigned);
 				}
 
+				// If this is a generated column, look up its generation expression.
 				if (str_contains($row['Extra'], 'GENERATED')) {
-					$columns[$row['Field']]['generation_expression'] = $row['generation_expression'];
+					$result2 = $this->query(
+						'SELECT generation_expression
+						FROM information_schema.columns
+						WHERE column_name = {string:field}
+							AND table_name = {string:table_name}
+							AND table_schema = {string:db}',
+						[
+							'db' => strtr($database, ['`' => '']),
+							'table_name' => $real_table_name,
+							'field' => $row['Field'],
+						],
+					);
+
+					[$generation_expression] = $this->fetch_row($result2);
+
+					$this->free_result($result2);
+
+					$columns[$row['Field']]['generation_expression'] = $this->unescape_string($generation_expression);
+
 					$columns[$row['Field']]['stored'] = str_contains($row['Extra'], 'STORED');
 				}
 			}
 		}
+
 		$this->free_result($result);
 
 		return $columns;
@@ -2215,7 +2248,7 @@ public function remove_column(string $table_name, string $column_name, array $pa
 
 		foreach ($columns as $column) {
 			if ($column['name'] == $column_name) {
-				$this->query(
+				$result = $this->query(
 					'ALTER TABLE ' . $short_table_name . '
 					DROP COLUMN ' . $column_name,
 					[
@@ -2223,7 +2256,7 @@ public function remove_column(string $table_name, string $column_name, array $pa
 					],
 				);
 
-				return true;
+				return $result !== false;
 			}
 		}
 
@@ -2245,7 +2278,7 @@ public function remove_index(string $table_name, string $index_name, array $para
 			// If the name is primary we want the primary key!
 			if ($index['type'] == 'primary' && $index_name == 'primary') {
 				// Dropping primary key?
-				$this->query(
+				$result = $this->query(
 					'ALTER TABLE ' . $short_table_name . '
 					DROP PRIMARY KEY',
 					[
@@ -2253,12 +2286,12 @@ public function remove_index(string $table_name, string $index_name, array $para
 					],
 				);
 
-				return true;
+				return $result !== false;
 			}
 
 			if ($index['name'] == $index_name) {
 				// Drop the bugger...
-				$this->query(
+				$result = $this->query(
 					'ALTER TABLE ' . $short_table_name . '
 					DROP INDEX ' . $index_name,
 					[
@@ -2266,7 +2299,7 @@ public function remove_index(string $table_name, string $index_name, array $para
 					],
 				);
 
-				return true;
+				return $result !== false;
 			}
 		}
 
@@ -2399,11 +2432,11 @@ public function setSqlMode(string $mode = 'default'): bool
 			$sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION,PIPES_AS_CONCAT';
 		}
 
-		$this->query('SET SESSION sql_mode = {string:sql_mode}', [
+		$result = $this->query('SET SESSION sql_mode = {string:sql_mode}', [
 			'sql_mode' => $sql_mode,
 		]);
 
-		return true;
+		return $result !== false;
 	}
 
 	/**
diff --git a/Sources/Db/APIs/PostgreSQL.php b/Sources/Db/APIs/PostgreSQL.php
index b15525ee35..bcfd20294b 100644
--- a/Sources/Db/APIs/PostgreSQL.php
+++ b/Sources/Db/APIs/PostgreSQL.php
@@ -1314,7 +1314,7 @@ public function add_column(string $table_name, array $column_info, array $parame
 		}
 
 		// Now add the thing!
-		$this->query(
+		$result = $this->query(
 			'ALTER TABLE ' . $short_table_name . '
 			ADD COLUMN ' . $column_info['name'] . ' ' . $type . $generated,
 			[
@@ -1334,7 +1334,7 @@ public function add_column(string $table_name, array $column_info, array $parame
 			return $this->change_column($table_name, $column_info['name'], $column_info);
 		}
 
-		return true;
+		return $result !== false;
 	}
 
 	/**
@@ -1876,7 +1876,7 @@ public function create_table(string $table_name, array $columns, array $indexes
 		$table_query .= ')';
 
 		// Create the table!
-		$this->query(
+		$result = $this->query(
 			$table_query,
 			[
 				'security_override' => true,
@@ -1931,7 +1931,7 @@ public function create_table(string $table_name, array $columns, array $indexes
 			$this->drop_table($table_name . '_old');
 		}
 
-		return true;
+		return $result !== false;
 	}
 
 	/**
@@ -1966,7 +1966,7 @@ public function drop_table(string $table_name, array $parameters = [], string $e
 			$sequence_query = 'DROP SEQUENCE IF EXISTS ' . $short_table_name . '_seq';
 
 			// drop them
-			$this->query(
+			$result = $this->query(
 				$table_query,
 				[
 					'security_override' => true,
@@ -1981,7 +1981,7 @@ public function drop_table(string $table_name, array $parameters = [], string $e
 
 			$this->transaction('commit');
 
-			return true;
+			return $result !== false;
 		}
 
 		// Otherwise do 'nout.
@@ -2025,14 +2025,14 @@ public function rename_table(string $old_name, string $new_name, bool $allowed_r
 			return false;
 		}
 
-		$this->query(
+		$result = $this->query(
 			'ALTER TABLE ' . $short_old_name . ' RENAME TO ' . $short_new_name,
 			[
 				'security_override' => true,
 			],
 		);
 
-		return true;
+		return $result !== false;
 	}
 
 	/**
@@ -2198,7 +2198,7 @@ public function remove_column(string $table_name, string $column_name, array $pa
 					);
 				}
 
-				$this->query(
+				$result = $this->query(
 					'ALTER TABLE ' . $short_table_name . '
 					DROP COLUMN ' . $column_name,
 					[
@@ -2206,7 +2206,7 @@ public function remove_column(string $table_name, string $column_name, array $pa
 					],
 				);
 
-				return true;
+				return $result !== false;
 			}
 		}
 
@@ -2234,7 +2234,7 @@ public function remove_index(string $table_name, string $index_name, array $para
 			// If the name is primary we want the primary key!
 			if ($index['type'] == 'primary' && $index_name == 'primary') {
 				// Dropping primary key?
-				$this->query(
+				$result = $this->query(
 					'ALTER TABLE ' . $real_table_name . '
 					DROP CONSTRAINT ' . $index['name'],
 					[
@@ -2242,19 +2242,19 @@ public function remove_index(string $table_name, string $index_name, array $para
 					],
 				);
 
-				return true;
+				return $result !== false;
 			}
 
 			if ($index['name'] == $index_name) {
 				// Drop the bugger...
-				$this->query(
+				$result = $this->query(
 					'DROP INDEX ' . $real_table_name . '_' . $index_name,
 					[
 						'security_override' => true,
 					],
 				);
 
-				return true;
+				return $result !== false;
 			}
 		}
 
diff --git a/Sources/Db/Schema/Column.php b/Sources/Db/Schema/Column.php
index 63539d6f2c..f5210c1cc9 100644
--- a/Sources/Db/Schema/Column.php
+++ b/Sources/Db/Schema/Column.php
@@ -91,8 +91,6 @@ class Column
 	 * @var bool
 	 *
 	 * Set this to true to drop the default during an ALTER TABLE operation.
-	 *
-	 * This is not set by __construct(). It can be set afterward.
 	 */
 	public bool $drop_default = false;
 
@@ -116,6 +114,8 @@ class Column
 	 *    Only applicable to integer columns.
 	 * @param ?string $charset The character set for string data.
 	 *    Only applicable to string types. If null, will be set automatically.
+	 * @param bool $drop_default Whether to drop the column's default during
+	 *    ALTER TABLE operations. Default: false.
 	 */
 	public function __construct(
 		string $name,
@@ -126,6 +126,7 @@ public function __construct(
 		string|float|int|bool|null $default = null,
 		?bool $auto = null,
 		?string $charset = null,
+		bool $drop_default = false,
 	) {
 		$this->name = strtolower($name);
 		$this->type = strtolower($type);
@@ -134,7 +135,7 @@ public function __construct(
 			$this->default = $default === 'NULL' ? null : $default;
 		}
 
-		foreach (['auto', 'size', 'unsigned', 'not_null'] as $var) {
+		foreach (['auto', 'size', 'unsigned', 'not_null', 'drop_default'] as $var) {
 			if (isset($var)) {
 				$this->{$var} = ${$var};
 			}
diff --git a/Sources/Db/Schema/Table.php b/Sources/Db/Schema/Table.php
index 7bd0239212..56a4f26477 100644
--- a/Sources/Db/Schema/Table.php
+++ b/Sources/Db/Schema/Table.php
@@ -76,6 +76,17 @@ class Table
 	 */
 	public ?int $auto_start;
 
+	/****************************
+	 * Internal static properties
+	 ****************************/
+
+	/**
+	 * @var array
+	 *
+	 * Cached output of Db::$db->list_tables
+	 */
+	protected static array $existing_tables = [];
+
 	/****************
 	 * Public methods
 	 ****************/
@@ -94,6 +105,21 @@ public function __construct()
 		}
 	}
 
+	/**
+	 * Checks whether a table with this name exists in the database.
+	 *
+	 * @param bool $force_refresh If true, force a refresh of the tables list.
+	 * @return bool Whether this table exists.
+	 */
+	public function exists(bool $force_refresh = false)
+	{
+		if ($force_refresh || empty(self::$existing_tables)) {
+			self::$existing_tables = Db::$db->list_tables();
+		}
+
+		return \in_array(Db::$db->prefix . $this->name, self::$existing_tables);
+	}
+
 	/**
 	 * Checks all the columns and indexes in this table to make sure they
 	 * are defined the way they should be, and fixes any that aren't.
@@ -106,7 +132,7 @@ public function normalize(): bool
 			return false;
 		}
 
-		if (empty(Db::$db->list_tables(false, Db::$db->prefix . $this->name))) {
+		if (!$this->exists()) {
 			return $this->create();
 		}
 
@@ -287,13 +313,20 @@ public function create(array $parameters = [], string $if_exists = 'ignore'): bo
 			return false;
 		}
 
-		return Db::$db->create_table(
+		$success = Db::$db->create_table(
 			'{db_prefix}' . $this->name,
 			array_map('get_object_vars', array_values($this->columns)),
 			array_map('get_object_vars', array_values($this->indexes)),
 			$parameters,
 			$if_exists,
 		);
+
+		if ($success) {
+			// Force a refresh of the list of tables.
+			self::$existing_tables = [];
+		}
+
+		return $success;
 	}
 
 	/**
@@ -305,7 +338,14 @@ public function create(array $parameters = [], string $if_exists = 'ignore'): bo
 	 */
 	public function drop(): bool
 	{
-		return Db::$db->drop_table('{db_prefix}' . $this->name);
+		$success = Db::$db->drop_table('{db_prefix}' . $this->name);
+
+		if ($success) {
+			// Force a refresh of the list of tables.
+			self::$existing_tables = [];
+		}
+
+		return $success;
 	}
 
 	/**
@@ -636,23 +676,21 @@ final public static function find(string $table_name, string $schema_version): ?
 	}
 
 	/**
-	 * Gets all known table schemas.
+	 * Gets database initializer queries for the indicated SMF version.
 	 *
+	 * @param string $schema_version E.g. 'v3_0'.
 	 * @return array All known table schemas.
 	 */
-	final public static function getInitializers(string $schema_version, string $title): array
+	final public static function getInitializers(string $schema_version): array
 	{
-		if (file_exists(__DIR__ . '/' . $schema_version . '/Initialize/' . $title . '.php')) {
+		if (file_exists(__DIR__ . '/' . $schema_version . '/Initialize/' . Db::$db->title . '.php')) {
 
-			$fully_qualified_class_name = __NAMESPACE__ . '\\' . $schema_version . '\\Initialize\\' . $title;
+			$fully_qualified_class_name = __NAMESPACE__ . '\\' . $schema_version . '\\Initialize\\' . Db::$db->title;
 
 			if (!class_exists($fully_qualified_class_name)) {
 				return [];
 			}
 
-			/**
-			 * @var \SMF\Db\Schema\v3_0\Initialize\Base
-			 */
 			$intializer = new $fully_qualified_class_name(Db::$db->get_version());
 
 			return $intializer->getAll();
diff --git a/Sources/Db/Schema/v2_1/BanGroups.php b/Sources/Db/Schema/v2_1/BanGroups.php
index c9aec8cb9c..165cbac388 100644
--- a/Sources/Db/Schema/v2_1/BanGroups.php
+++ b/Sources/Db/Schema/v2_1/BanGroups.php
@@ -61,6 +61,9 @@ public function __construct()
 				name: 'expire_time',
 				type: 'int',
 				unsigned: true,
+				not_null: false,
+				default: null,
+				drop_default: true,
 			),
 			'cannot_access' => new Column(
 				name: 'cannot_access',
diff --git a/Sources/Db/Schema/v3_0/BanGroups.php b/Sources/Db/Schema/v3_0/BanGroups.php
index 221135416a..0b61dbdc14 100644
--- a/Sources/Db/Schema/v3_0/BanGroups.php
+++ b/Sources/Db/Schema/v3_0/BanGroups.php
@@ -63,6 +63,7 @@ public function __construct()
 				unsigned: true,
 				not_null: false,
 				default: null,
+				drop_default: true,
 			),
 			'cannot_access' => new Column(
 				name: 'cannot_access',
diff --git a/Sources/Db/Schema/v3_0/Initialize/Base.php b/Sources/Db/Schema/v3_0/Initialize/Base.php
index 498b91235d..94cc1aed0a 100644
--- a/Sources/Db/Schema/v3_0/Initialize/Base.php
+++ b/Sources/Db/Schema/v3_0/Initialize/Base.php
@@ -31,21 +31,41 @@ class Base
 	 * Public methods
 	 ****************/
 
-	public function __construct(?string $version)
+	/**
+	 * Constructor.
+	 *
+	 * @param string $version Version of the database engine.
+	 */
+	public function __construct(string $version)
 	{
 		$this->version = $version;
 	}
 
+	/**
+	 * Gets queries that create custom SQL functions and operators.
+	 *
+	 * @return array SQL queries.
+	 */
 	public function getAll(): array
 	{
 		return $this->functions() + $this->operators();
 	}
 
+	/**
+	 * Gets queries that create custom SQL functions.
+	 *
+	 * @return array SQL queries.
+	 */
 	public function functions(): array
 	{
 		return [];
 	}
 
+	/**
+	 * Gets queries that create custom SQL operators.
+	 *
+	 * @return array SQL queries.
+	 */
 	public function operators(): array
 	{
 		return [];
diff --git a/Sources/Maintenance/Cleanup/CleanupBase.php b/Sources/Maintenance/Cleanup/CleanupBase.php
index 4f37911097..7bdce49640 100644
--- a/Sources/Maintenance/Cleanup/CleanupBase.php
+++ b/Sources/Maintenance/Cleanup/CleanupBase.php
@@ -38,9 +38,7 @@ abstract class CleanupBase implements SubStepInterface
 	 ****************/
 
 	/**
-	 * Check if the task should be performed or not.
 	 *
-	 * @return bool True if this task needs to be run, false otherwise.
 	 */
 	public function isCandidate(): bool
 	{
diff --git a/Sources/Maintenance/Cleanup/v3_0/OldFiles.php b/Sources/Maintenance/Cleanup/OldFilesBase.php
similarity index 91%
rename from Sources/Maintenance/Cleanup/v3_0/OldFiles.php
rename to Sources/Maintenance/Cleanup/OldFilesBase.php
index 1f04f87e28..7d376fea78 100644
--- a/Sources/Maintenance/Cleanup/v3_0/OldFiles.php
+++ b/Sources/Maintenance/Cleanup/OldFilesBase.php
@@ -13,13 +13,16 @@
 
 declare(strict_types=1);
 
-namespace SMF\Maintenance\Cleanup\v3_0;
+namespace SMF\Maintenance\Cleanup;
 
 use SMF\Config;
-use SMF\Maintenance\Cleanup\CleanupBase;
 use SMF\Utils;
 
-class OldFiles extends CleanupBase
+/**
+ * Base class for cleanup tasks that delete files that have been removed in a
+ * new version of SMF.
+ */
+abstract class OldFilesBase extends CleanupBase
 {
 	/*******************
 	 * Public properties
@@ -37,7 +40,7 @@ class OldFiles extends CleanupBase
 	/**
 	 * @var array
 	 *
-	 * List of files removed in SMF 3.0.
+	 * List of files removed in the relevant version of SMF.
 	 */
 	protected array $removed = [
 		// Files in the Themes directory.
@@ -57,9 +60,7 @@ class OldFiles extends CleanupBase
 	 ****************/
 
 	/**
-	 * Check if the task should be performed or not.
 	 *
-	 * @return bool True if this task needs to be run, false otherwise.
 	 */
 	public function isCandidate(): bool
 	{
diff --git a/Sources/Maintenance/Cleanup/v2_1/OldFiles.php b/Sources/Maintenance/Cleanup/v2_1/OldFiles.php
index e69755f663..7b0717e342 100644
--- a/Sources/Maintenance/Cleanup/v2_1/OldFiles.php
+++ b/Sources/Maintenance/Cleanup/v2_1/OldFiles.php
@@ -15,10 +15,10 @@
 
 namespace SMF\Maintenance\Cleanup\v2_1;
 
-use SMF\Maintenance\Cleanup\v3_0\OldFiles as OldFilesBase;
+use SMF\Maintenance\Cleanup\OldFilesBase;
 
 /**
- * Just like the v3_0 version of OldFiles, but with a different list of files.
+ * Deletes files that were present in SMF 2.0 but not in SMF 2.1.
  */
 class OldFiles extends OldFilesBase
 {
diff --git a/Sources/Maintenance/Cleanup/v3_0/TasksDirCase.php b/Sources/Maintenance/Cleanup/v3_0/TasksDirCase.php
index 6bf62068a1..140713af43 100644
--- a/Sources/Maintenance/Cleanup/v3_0/TasksDirCase.php
+++ b/Sources/Maintenance/Cleanup/v3_0/TasksDirCase.php
@@ -35,9 +35,7 @@ class TasksDirCase extends CleanupBase
 	 ****************/
 
 	/**
-	 * Check if the task should be performed or not.
 	 *
-	 * @return bool True if this task needs to be run, false otherwise.
 	 */
 	public function isCandidate(): bool
 	{
diff --git a/Sources/Maintenance/Maintenance.php b/Sources/Maintenance/Maintenance.php
index 2043f29264..5c9997a639 100644
--- a/Sources/Maintenance/Maintenance.php
+++ b/Sources/Maintenance/Maintenance.php
@@ -228,7 +228,7 @@ class Maintenance
 	public function __construct()
 	{
 		Security::frameOptionsHeader('SAMEORIGIN');
-		self::$theme_dir = \dirname(SMF_SETTINGS_FILE) . '/Themes/default';
+		self::$theme_dir = self::getBaseDir() . '/Themes/default';
 
 		// This might be overwritten by the tool, but we need a default value.
 		self::$context['started'] = (int) TIME_START;
@@ -744,12 +744,17 @@ public static function loginWithDatabasePassword(
 	 */
 	public static function getTimeElapsed(): string
 	{
-		// How long have we been running this?
-		$elapsed = time() - (int) self::$context['started'];
-		$mins = (int) ($elapsed / 60);
-		$seconds = $elapsed - $mins * 60;
+		$duration = (new \DateTime('@' . self::$context['started']))->diff(new \DateTime());
 
-		return Lang::getTxt('maintenance_time_elasped_ms', ['m' => $mins, 's' => $seconds]);
+		if ((int) $duration->format('%a') > 0) {
+			return \strval((int) $duration->format('%h') + ((int) $duration->format('%a') * 24)) . $duration->format(':%I:%S');
+		}
+
+		if ((int) $duration->format('%h') > 0) {
+			return $duration->format('%h:%I:%S');
+		}
+
+		return $duration->format('%i:%S');
 	}
 
 	/**
diff --git a/Sources/Maintenance/Migration/v2_1/AgreementUpdate.php b/Sources/Maintenance/Migration/v2_1/AgreementUpdate.php
index adf662aaac..eaae86caec 100644
--- a/Sources/Maintenance/Migration/v2_1/AgreementUpdate.php
+++ b/Sources/Maintenance/Migration/v2_1/AgreementUpdate.php
@@ -110,7 +110,7 @@ public function execute(): bool
 			$this->handleTimeout($start);
 
 			$extras = [];
-			$request = Db::$db->query(
+			$request = $this->query(
 				'SELECT id_action, extra
 					FROM {db_prefix}log_actions
 					WHERE id_member = {int:blank_id}
@@ -145,7 +145,7 @@ public function execute(): bool
 				}
 
 				if (!empty($extra['applicator'])) {
-					$request = Db::$db->query(
+					$request = $this->query(
 						'UPDATE {db_prefix}log_actions
 							SET id_member = {int:id_member}
 							WHERE id_action = {int:id_action}',
diff --git a/Sources/Maintenance/Migration/v2_1/BoardPermissionsView.php b/Sources/Maintenance/Migration/v2_1/BoardPermissionsView.php
index b77b509c14..04bec7f32b 100644
--- a/Sources/Maintenance/Migration/v2_1/BoardPermissionsView.php
+++ b/Sources/Maintenance/Migration/v2_1/BoardPermissionsView.php
@@ -15,7 +15,6 @@
 
 namespace SMF\Maintenance\Migration\v2_1;
 
-use SMF\Config;
 use SMF\Db\DatabaseApi as Db;
 use SMF\Db\Schema;
 use SMF\Maintenance\Maintenance;
@@ -46,11 +45,7 @@ public function execute(): bool
 		// Create table board_permissions_view
 		if ($start <= 0) {
 			$table = new Schema\v2_1\BoardPermissionsView();
-			$existing_tables = Db::$db->list_tables();
-
-			if (!\in_array(Config::$db_prefix . $table->name, $existing_tables)) {
-				$table->create();
-			}
+			$table->create();
 
 			$this->handleTimeout(++$start);
 		}
diff --git a/Sources/Maintenance/Migration/v2_1/CreateAlerts.php b/Sources/Maintenance/Migration/v2_1/CreateAlerts.php
index cfe950fc98..5fab23bd87 100644
--- a/Sources/Maintenance/Migration/v2_1/CreateAlerts.php
+++ b/Sources/Maintenance/Migration/v2_1/CreateAlerts.php
@@ -60,15 +60,8 @@ public function execute(): bool
 		$user_alert_table = new Schema\v2_1\UserAlerts();
 		$user_alert_prefs_table = new Schema\v2_1\UserAlertsPrefs();
 
-		$tables = Db::$db->list_tables();
-
-		if (!\in_array($user_alert_table->name, $tables)) {
-			$user_alert_table->create();
-		}
-
-		if (!\in_array($user_alert_prefs_table->name, $tables)) {
-			$user_alert_prefs_table->create();
-		}
+		$user_alert_table->create();
+		$user_alert_prefs_table->create();
 
 		$existing_structure = $members_table->getCurrentStructure();
 
@@ -150,25 +143,10 @@ public function execute(): bool
 			} while (Maintenance::getCurrentStart() < Maintenance::$total_items);
 		}
 
-		if (\in_array('notify_send_body', $member_columns)) {
-			Db::$db->remove_column('{db_prefix}members', 'notify_send_body');
-			$this->handleTimeout();
-		}
-
-		if (\in_array('notify_types', $member_columns)) {
-			Db::$db->remove_column('{db_prefix}members', 'notify_types');
-			$this->handleTimeout();
-		}
-
-		if (\in_array('notify_regularity', $member_columns)) {
-			Db::$db->remove_column('{db_prefix}members', 'notify_regularity');
-			$this->handleTimeout();
-		}
-
-		if (\in_array('notify_announcements', $member_columns)) {
-			Db::$db->remove_column('{db_prefix}members', 'notify_announcements');
-			$this->handleTimeout();
-		}
+		$members_table->dropColumn('notify_send_body');
+		$members_table->dropColumn('notify_types');
+		$members_table->dropColumn('notify_regularity');
+		$members_table->dropColumn('notify_announcements');
 
 		return true;
 	}
diff --git a/Sources/Maintenance/Migration/v2_1/CreateBackgroundTasks.php b/Sources/Maintenance/Migration/v2_1/CreateBackgroundTasks.php
index 6f4d5c0dde..f0bdc1b205 100644
--- a/Sources/Maintenance/Migration/v2_1/CreateBackgroundTasks.php
+++ b/Sources/Maintenance/Migration/v2_1/CreateBackgroundTasks.php
@@ -15,8 +15,6 @@
 
 namespace SMF\Maintenance\Migration\v2_1;
 
-use SMF\Config;
-use SMF\Db\DatabaseApi as Db;
 use SMF\Db\Schema;
 use SMF\Maintenance\Migration\MigrationBase;
 
@@ -41,12 +39,7 @@ class CreateBackgroundTasks extends MigrationBase
 	public function execute(): bool
 	{
 		$background_tasks_table = new Schema\v2_1\BackgroundTasks();
-
-		$tables = Db::$db->list_tables();
-
-		if (!\in_array(Config::$db_prefix . $background_tasks_table->name, $tables)) {
-			$background_tasks_table->create();
-		}
+		$background_tasks_table->create();
 
 		return true;
 	}
diff --git a/Sources/Maintenance/Migration/v2_1/CreateMemberLogins.php b/Sources/Maintenance/Migration/v2_1/CreateMemberLogins.php
index 10a6ccae77..26a91fb77d 100644
--- a/Sources/Maintenance/Migration/v2_1/CreateMemberLogins.php
+++ b/Sources/Maintenance/Migration/v2_1/CreateMemberLogins.php
@@ -15,8 +15,6 @@
 
 namespace SMF\Maintenance\Migration\v2_1;
 
-use SMF\Config;
-use SMF\Db\DatabaseApi as Db;
 use SMF\Db\Schema;
 use SMF\Maintenance\Migration\MigrationBase;
 
@@ -40,12 +38,8 @@ class CreateMemberLogins extends MigrationBase
 	 */
 	public function execute(): bool
 	{
-		$tables = Db::$db->list_tables();
-
-		if (!\in_array(Config::$db_prefix . 'member_logins', $tables)) {
-			$member_logins = new Schema\v2_1\MemberLogins();
-			$member_logins->create();
-		}
+		$member_logins = new Schema\v2_1\MemberLogins();
+		$member_logins->create();
 
 		return true;
 	}
diff --git a/Sources/Maintenance/Migration/v2_1/FixDates.php b/Sources/Maintenance/Migration/v2_1/FixDates.php
index 81e46379c6..3f9b22e7f7 100644
--- a/Sources/Maintenance/Migration/v2_1/FixDates.php
+++ b/Sources/Maintenance/Migration/v2_1/FixDates.php
@@ -19,6 +19,12 @@
 use SMF\Maintenance\Maintenance;
 use SMF\Maintenance\Migration\MigrationBase;
 
+/**
+ * @todo Find a SQL standard way of handling this.
+ *    Maybe DATEADD with a calc on YEAR() to find what it takes to make it 1004?
+ *    PostgreSQL does not have DATEFROMPARTS, but does have make_date (9.4>),
+ *    which would be similar the more standard DATEFROMPARTS.
+ */
 class FixDates extends MigrationBase
 {
 	/*******************
@@ -39,121 +45,61 @@ class FixDates extends MigrationBase
 	 */
 	public function execute(): bool
 	{
-		// @@ TODO: Find a SQL standard way of handling this.  Maybe DATEADD with a calc on YEAR() to find what it takes to make it 1004?
-		// PostgreSQL does not have DATEFROMPARTS, but does have make_date (9.4>), which would be similar the more standard DATEFROMPARTS.
-
-		// PostgreSQL does the query a bit different.
-		$is_pgsql = Db::$db->title === POSTGRE_TITLE;
-
-		if (Maintenance::getCurrentStart() < 1 && $is_pgsql) {
-			$this->query(
-				'UPDATE {db_prefix}calendar
-				SET start_date = concat_ws({literal:-}, CASE WHEN EXTRACT(YEAR FROM start_date) < 1004 THEN 1004 END, EXTRACT(MONTH FROM start_date), EXTRACT(DAY FROM start_date))::date
-				WHERE EXTRACT(YEAR FROM start_date) < 1004',
-				[],
-			);
-		} elseif (Maintenance::getCurrentStart() < 1) {
-			$this->query(
-				'UPDATE {db_prefix}calendar
-				SET start_date = DATE(CONCAT(1004, {literal:-}, MONTH(start_date), {literal:-}, DAY(start_date)))
-				WHERE YEAR(start_date) < 1004',
-				[],
-			);
+		if (Db::$db->title === POSTGRE_TITLE) {
+			$boilerplate = 'UPDATE {db_prefix}%1$s
+				SET %2$s = concat_ws({literal:-}, CASE WHEN EXTRACT(YEAR FROM %2$s) < 1004 THEN 1004 END, EXTRACT(MONTH FROM %2$s), EXTRACT(DAY FROM %2$s))::date
+				WHERE EXTRACT(YEAR FROM %2$s) < 1004';
+
+			$bday_query = 'UPDATE {db_prefix}members
+				SET birthdate = concat_ws({literal:-}, CASE WHEN EXTRACT(YEAR FROM birthdate) < 1004 THEN 1004 END, CASE WHEN EXTRACT(MONTH FROM birthdate) < 1 THEN 1 ELSE EXTRACT(MONTH FROM birthdate) END, CASE WHEN EXTRACT(DAY FROM birthdate) < 1 THEN 1 ELSE EXTRACT(DAY FROM birthdate) END)::date
+				WHERE EXTRACT(YEAR FROM birthdate) < 1004 OR EXTRACT(MONTH FROM birthdate) < 1 OR EXTRACT(DAY FROM birthdate) < 1';
+
+		} else {
+			$boilerplate = 'UPDATE {db_prefix}%1$s
+				SET %2$s = DATE(CONCAT(1004, {literal:-}, MONTH(%2$s), {literal:-}, DAY(%2$s)))
+				WHERE YEAR(%2$s) < 1004';
+
+			$bday_query = 'UPDATE {db_prefix}members
+				SET birthdate = DATE(CONCAT(IF(YEAR(birthdate) < 1004, 1004, YEAR(birthdate)), {literal:-}, IF(MONTH(birthdate) < 1, 1, MONTH(birthdate)), {literal:-}, IF(DAY(birthdate) < 1, 1, DAY(birthdate))))
+				WHERE YEAR(birthdate) < 1004 OR MONTH(birthdate) < 1 OR DAY(birthdate) < 1';
 		}
-		Maintenance::setCurrentStart();
-		$this->handleTimeout();
-
-		if (Maintenance::getCurrentStart() < 2 && $is_pgsql) {
-			$this->query(
-				'UPDATE {db_prefix}calendar
-				SET end_date = concat_ws({literal:-}, CASE WHEN EXTRACT(YEAR FROM end_date) < 1004 THEN 1004 END, EXTRACT(MONTH FROM end_date), EXTRACT(DAY FROM end_date))::date
-				WHERE EXTRACT(YEAR FROM end_date) < 1004',
-				[],
-			);
-		} elseif (Maintenance::getCurrentStart() < 2) {
-			$this->query(
-				'UPDATE {db_prefix}calendar
-				SET end_date = DATE(CONCAT(1004, {literal:-}, MONTH(end_date), {literal:-}, DAY(end_date)))
-				WHERE YEAR(end_date) < 1004',
-				[],
-			);
+
+		if (Maintenance::getCurrentStart() < 1) {
+			$this->query(\sprintf($boilerplate, 'calendar', 'start_date'));
+
+			Maintenance::setCurrentStart();
+			$this->handleTimeout();
 		}
-		Maintenance::setCurrentStart();
-		$this->handleTimeout();
-
-		if (Maintenance::getCurrentStart() < 3 && $is_pgsql) {
-			$this->query(
-				'UPDATE {db_prefix}calendar_holidays
-				SET event_date = concat_ws({literal:-}, CASE WHEN EXTRACT(YEAR FROM event_date) < 1004 THEN 1004 END, EXTRACT(MONTH FROM event_date), EXTRACT(DAY FROM event_date))::date
-				WHERE EXTRACT(YEAR FROM event_date) < 1004',
-				[],
-			);
-		} elseif (Maintenance::getCurrentStart() < 3) {
-			$this->query(
-				'UPDATE {db_prefix}calendar_holidays
-				SET event_date = DATE(CONCAT(1004, {literal:-}, MONTH(event_date), {literal:-}, DAY(event_date)))
-				WHERE YEAR(event_date) < 1004',
-				[],
-			);
+
+		if (Maintenance::getCurrentStart() < 2) {
+			$this->query(\sprintf($boilerplate, 'calendar', 'end_date'));
+
+			Maintenance::setCurrentStart();
+			$this->handleTimeout();
 		}
-		Maintenance::setCurrentStart();
-		$this->handleTimeout();
-
-		if (Maintenance::getCurrentStart() < 4 && $is_pgsql) {
-			$this->query(
-				'UPDATE {db_prefix}log_spider_stats
-				SET stat_date = concat_ws({literal:-}, CASE WHEN EXTRACT(YEAR FROM stat_date) < 1004 THEN 1004 END, EXTRACT(MONTH FROM stat_date), EXTRACT(DAY FROM stat_date))::date
-				WHERE EXTRACT(YEAR FROM stat_date) < 1004',
-				[],
-			);
-		} elseif (Maintenance::getCurrentStart() < 4) {
-			$this->query(
-				'UPDATE {db_prefix}log_spider_stats
-				SET stat_date = DATE(CONCAT(1004, {literal:-}, MONTH(stat_date), {literal:-}, DAY(stat_date)))
-				WHERE YEAR(stat_date) < 1004',
-				[],
-			);
+
+		if (Maintenance::getCurrentStart() < 3) {
+			$this->query(\sprintf($boilerplate, 'calendar_holidays', 'event_date'));
+
+			Maintenance::setCurrentStart();
+			$this->handleTimeout();
 		}
-		Maintenance::setCurrentStart();
-		$this->handleTimeout();
 
-		if (Maintenance::getCurrentStart() < 5 && $is_pgsql) {
-			$this->query(
-				'UPDATE {db_prefix}log_spider_stats
-				SET birthdate = concat_ws({literal:-}, CASE WHEN EXTRACT(YEAR FROM birthdate) < 1004 THEN 1004 END, CASE WHEN EXTRACT(MONTH FROM birthdate) < 1 THEN 1 ELSE EXTRACT(MONTH FROM birthdate) END, CASE WHEN EXTRACT(DAY FROM birthdate) < 1 THEN 1 ELSE EXTRACT(DAY FROM birthdate) END)::date
-				WHERE EXTRACT(YEAR FROM birthdate) < 1004 OR EXTRACT(MONTH FROM birthdate) < 1 OR EXTRACT(DAY FROM birthdate) < 1',
-				[],
-			);
-		} elseif (Maintenance::getCurrentStart() < 5) {
-			$this->query(
-				'UPDATE {db_prefix}members
-				SET birthdate = DATE(CONCAT(IF(YEAR(birthdate) < 1004, 1004, YEAR(birthdate)), {literal:-}, IF(MONTH(birthdate) < 1, 1, MONTH(birthdate)), {literal:-}, IF(DAY(birthdate) < 1, 1, DAY(birthdate))))
-				WHERE YEAR(birthdate) < 1004 OR MONTH(birthdate) < 1 OR DAY(birthdate) < 1',
-				[],
-			);
+		if (Maintenance::getCurrentStart() < 4) {
+			$this->query(\sprintf($boilerplate, 'log_spider_stats', 'stat_date'));
+
+			Maintenance::setCurrentStart();
+			$this->handleTimeout();
 		}
-		Maintenance::setCurrentStart();
-		$this->handleTimeout();
 
-		if (Maintenance::getCurrentStart() < 6 && $is_pgsql) {
-			$this->query(
-				'UPDATE {db_prefix}members
-				SET birthdate = concat_ws({literal:-}, CASE WHEN EXTRACT(YEAR FROM birthdate) < 1004 THEN 1004 END, CASE WHEN EXTRACT(MONTH FROM birthdate) < 1 THEN 1 ELSE EXTRACT(MONTH FROM birthdate) END, CASE WHEN EXTRACT(DAY FROM birthdate) < 1 THEN 1 ELSE EXTRACT(DAY FROM birthdate) END)::date
-				WHERE EXTRACT(YEAR FROM birthdate) < 1004 OR EXTRACT(MONTH FROM birthdate) < 1 OR EXTRACT(DAY FROM birthdate) < 1',
-				[],
-			);
-		} elseif (Maintenance::getCurrentStart() < 6) {
-			$this->query(
-				'UPDATE {db_prefix}members
-				SET birthdate = DATE(CONCAT(IF(YEAR(birthdate) < 1004, 1004, YEAR(birthdate)), {literal:-}, IF(MONTH(birthdate) < 1, 1, MONTH(birthdate)), {literal:-}, IF(DAY(birthdate) < 1, 1, DAY(birthdate))))
-				WHERE YEAR(birthdate) < 1004 OR MONTH(birthdate) < 1 OR DAY(birthdate) < 1',
-				[],
-			);
+		if (Maintenance::getCurrentStart() < 5) {
+			$this->query($bday_query);
+
+			Maintenance::setCurrentStart();
+			$this->handleTimeout();
 		}
-		Maintenance::setCurrentStart();
-		$this->handleTimeout();
 
-		if (Maintenance::getCurrentStart() < 7) {
+		if (Maintenance::getCurrentStart() < 6) {
 			Db::$db->change_column(
 				'{db_prefix}log_activity',
 				'DATE',
@@ -162,22 +108,63 @@ public function execute(): bool
 					'default' => null,
 				],
 			);
+
+			Maintenance::setCurrentStart();
+			$this->handleTimeout();
+		}
+
+		if (Maintenance::getCurrentStart() < 7) {
+			Db::$db->change_column(
+				'{db_prefix}calendar',
+				'start_date',
+				['default' => '1004-01-01'],
+			);
+
+			Maintenance::setCurrentStart();
+			$this->handleTimeout();
+		}
+
+		if (Maintenance::getCurrentStart() < 8) {
+			Db::$db->change_column(
+				'{db_prefix}calendar',
+				'end_date',
+				['default' => '1004-01-01'],
+			);
+
+			Maintenance::setCurrentStart();
+			$this->handleTimeout();
+		}
+
+		if (Maintenance::getCurrentStart() < 9) {
+			Db::$db->change_column(
+				'{db_prefix}calendar_holidays',
+				'event_date',
+				['default' => '1004-01-01'],
+			);
+
+			Maintenance::setCurrentStart();
+			$this->handleTimeout();
 		}
-		Maintenance::setCurrentStart();
-		$this->handleTimeout();
 
-		$fixes = [
-			['tbl' => '{db_prefix}calendar', 'col' => 'start_date'],
-			['tbl' => '{db_prefix}calendar', 'col' => 'end_date'],
-			['tbl' => '{db_prefix}calendar_holidays', 'col' => 'event_date'],
-			['tbl' => '{db_prefix}log_spider_stats', 'col' => 'stat_date'],
-			['tbl' => '{db_prefix}members', 'col' => 'birthdate'],
-		];
+		if (Maintenance::getCurrentStart() < 10) {
+			Db::$db->change_column(
+				'{db_prefix}log_spider_stats',
+				'stat_date',
+				['default' => '1004-01-01'],
+			);
+
+			Maintenance::setCurrentStart();
+			$this->handleTimeout();
+		}
 
-		for ($key = Maintenance::getCurrentStart(); $key < \count($fixes); Maintenance::setCurrentStart()) {
-			$fix = $fixes[$key - 7];
+		if (Maintenance::getCurrentStart() < 11) {
+			Db::$db->change_column(
+				'{db_prefix}members',
+				'stat_date',
+				['birthdate' => '1004-01-01'],
+			);
 
-			Db::$db->change_column($fix['tbl'], $fix['col'], ['default' => '1004-01-01']);
+			Maintenance::setCurrentStart();
 			$this->handleTimeout();
 		}
 
diff --git a/Sources/Maintenance/Migration/v2_1/Ipv6BanItem.php b/Sources/Maintenance/Migration/v2_1/Ipv6BanItem.php
index 20edf01d35..59418c9141 100644
--- a/Sources/Maintenance/Migration/v2_1/Ipv6BanItem.php
+++ b/Sources/Maintenance/Migration/v2_1/Ipv6BanItem.php
@@ -88,23 +88,6 @@ public function execute(): bool
 				// Note, We now support MySQL 8+, which means we could use INET6_ATON.
 				// The upgrade logic was built this way and should remain the same.
 				// If changed, a full upgrade from 2.0 to 3.0 would need to be tested.
-				$this->quote(
-					'UPDATE IGNORE {db_prefix}ban_items
-					SET ip_low =
-						UNHEX(
-							hex(
-								INET_ATON(concat(ip_low1,{literal:.},ip_low2,{literal:.},ip_low3,{literal:.},ip_low4))
-							)
-						),
-					ip_high =
-						UNHEX(
-							hex(
-								INET_ATON(concat(ip_high1,{literal:.},ip_high2,{literal:.},ip_high3,{literal:.},ip_high4))
-							)
-						)
-					WHERE ip_low1 > 0',
-				);
-
 				$this->query(
 					'UPDATE IGNORE {db_prefix}ban_items
 					SET ip_low =
@@ -121,7 +104,6 @@ public function execute(): bool
 						)
 					WHERE ip_low1 > 0',
 				);
-
 			}
 
 			$this->handleTimeout(++$start);
diff --git a/Sources/Maintenance/Migration/v2_1/Ipv6Base.php b/Sources/Maintenance/Migration/v2_1/Ipv6Base.php
index 68ff0e508d..5109187944 100644
--- a/Sources/Maintenance/Migration/v2_1/Ipv6Base.php
+++ b/Sources/Maintenance/Migration/v2_1/Ipv6Base.php
@@ -111,7 +111,7 @@ public function convertData(string $targetTable, string $oldCol, string $newCol,
 			// Execute updates every $setSize & also when done with contents of $arIp
 			if ((($i + 1) == $count) || (($i + 1) % $setSize === 0)) {
 				$updates['whereSet'] = array_values($updates);
-				Db::$db->query(
+				$this->query(
 					'UPDATE {db_prefix}' . $targetTable . '
 					SET ' . $newCol . ' = CASE ' .
 					implode('
diff --git a/Sources/Maintenance/Migration/v2_1/Likes.php b/Sources/Maintenance/Migration/v2_1/Likes.php
index 61a5866871..7ff4bf2e40 100644
--- a/Sources/Maintenance/Migration/v2_1/Likes.php
+++ b/Sources/Maintenance/Migration/v2_1/Likes.php
@@ -15,8 +15,7 @@
 
 namespace SMF\Maintenance\Migration\v2_1;
 
-use SMF\Config;
-use SMF\Db\DatabaseApi as Db;
+use SMF\Db\Schema;
 use SMF\Maintenance\Maintenance;
 use SMF\Maintenance\Migration\MigrationBase;
 
@@ -40,9 +39,9 @@ class Likes extends MigrationBase
 	 */
 	public function isCandidate(): bool
 	{
-		$tables = Db::$db->list_tables();
+		$table = new Schema\v2_1\UserLikes();
 
-		return !\in_array(Config::$db_prefix . 'user_likes', $tables);
+		return !$table->exists();
 	}
 
 	/**
@@ -52,27 +51,19 @@ public function execute(): bool
 	{
 		$start = Maintenance::getCurrentStart();
 
-		$LikesTable = new \SMF\Db\Schema\v2_1\UserLikes();
-
-		$tables = Db::$db->list_tables();
-
-		// Creating draft table.
-		if ($start <= 0 && !\in_array(Config::$db_prefix . 'user_likes', $tables)) {
-			$LikesTable->create();
-
+		if ($start <= 0) {
+			$likes_table = new Schema\v2_1\UserLikes();
+			$likes_table->create();
 			$this->handleTimeout(++$start);
 		}
 
 		// Adding likes column to the messages table. (May take a while)
 		if ($start <= 1) {
-			$MessagesTable = new \SMF\Db\Schema\v2_1\Messages();
-			$existing_structure = $MessagesTable->getCurrentStructure();
+			$messages_table = new Schema\v2_1\Messages();
+			$existing_structure = $messages_table->getCurrentStructure();
 
-			foreach ($MessagesTable->columns as $column) {
-				// Add the columns.
-				if ($column->name === 'likes' && !isset($existing_structure['columns'][$column->name])) {
-					$MessagesTable->addColumn($column);
-				}
+			if (!isset($existing_structure['columns']['likes'])) {
+				$messages_table->addColumn($messages_table->columns['likes']);
 			}
 
 			$this->handleTimeout(++$start);
diff --git a/Sources/Maintenance/Migration/v2_1/Mentions.php b/Sources/Maintenance/Migration/v2_1/Mentions.php
index de32bc69fa..61222eb5c5 100644
--- a/Sources/Maintenance/Migration/v2_1/Mentions.php
+++ b/Sources/Maintenance/Migration/v2_1/Mentions.php
@@ -15,8 +15,6 @@
 
 namespace SMF\Maintenance\Migration\v2_1;
 
-use SMF\Config;
-use SMF\Db\DatabaseApi as Db;
 use SMF\Db\Schema;
 use SMF\Maintenance\Migration\MigrationBase;
 
@@ -40,9 +38,9 @@ class Mentions extends MigrationBase
 	 */
 	public function isCandidate(): bool
 	{
-		$tables = Db::$db->list_tables();
+		$table = new Schema\v2_1\Mentions();
 
-		return !\in_array(Config::$db_prefix . 'mentions', $tables);
+		return !$table->exists();
 	}
 
 	/**
diff --git a/Sources/Maintenance/Migration/v2_1/ModeratorGroups.php b/Sources/Maintenance/Migration/v2_1/ModeratorGroups.php
index 70be7572da..7c160bcdf0 100644
--- a/Sources/Maintenance/Migration/v2_1/ModeratorGroups.php
+++ b/Sources/Maintenance/Migration/v2_1/ModeratorGroups.php
@@ -15,8 +15,6 @@
 
 namespace SMF\Maintenance\Migration\v2_1;
 
-use SMF\Config;
-use SMF\Db\DatabaseApi as Db;
 use SMF\Db\Schema;
 use SMF\Maintenance\Migration\MigrationBase;
 
@@ -40,9 +38,9 @@ class ModeratorGroups extends MigrationBase
 	 */
 	public function isCandidate(): bool
 	{
-		$tables = Db::$db->list_tables();
+		$table = new Schema\v2_1\ModeratorGroups();
 
-		return !\in_array(Config::$db_prefix . 'moderator_groups', $tables);
+		return !$table->exists();
 	}
 
 	/**
diff --git a/Sources/Maintenance/Migration/v2_1/Permissions.php b/Sources/Maintenance/Migration/v2_1/Permissions.php
index 0c8cbcfa90..f7f543ae7c 100644
--- a/Sources/Maintenance/Migration/v2_1/Permissions.php
+++ b/Sources/Maintenance/Migration/v2_1/Permissions.php
@@ -248,7 +248,7 @@ public function execute(): bool
 				);
 			}
 
-			Db::$db->query(
+			$this->query(
 				'DELETE FROM {db_prefix}settings
 				WHERE variable = {string:warning_show}',
 				[
@@ -306,7 +306,7 @@ public function execute(): bool
 
 		$this->handleTimeout(++$start);
 
-		Db::$db->query(
+		$this->query(
 			'DELETE FROM {db_prefix}permissions
 			WHERE id_group = {int:guests}
 				AND permission IN ({array_string:illegal_perms})',
diff --git a/Sources/Maintenance/Migration/v2_1/PersonalMessageLabels.php b/Sources/Maintenance/Migration/v2_1/PersonalMessageLabels.php
index d307db5414..6eacbc7b07 100644
--- a/Sources/Maintenance/Migration/v2_1/PersonalMessageLabels.php
+++ b/Sources/Maintenance/Migration/v2_1/PersonalMessageLabels.php
@@ -15,7 +15,6 @@
 
 namespace SMF\Maintenance\Migration\v2_1;
 
-use SMF\Config;
 use SMF\Db\DatabaseApi as Db;
 use SMF\Db\Schema;
 use SMF\Db\Schema\Column;
@@ -75,15 +74,13 @@ public function execute(): bool
 		$pm_labels_table = new Schema\v2_1\PmLabels();
 		$pm_labeled_messages_table = new Schema\v2_1\PmLabeledMessages();
 
-		$tables = Db::$db->list_tables();
-
 		if ($start <= 0) {
-			if (!\in_array(Config::$db_prefix . 'pm_labels', $tables)) {
+			if (!$pm_labels_table->exists()) {
 				$pm_labels_table->create();
 				$this->handleTimeout(0);
 			}
 
-			if (!\in_array(Config::$db_prefix . 'pm_labeled_messages', $tables)) {
+			if (!$pm_labeled_messages_table->exists()) {
 				$pm_labeled_messages_table->create();
 				$this->handleTimeout(0);
 			}
@@ -116,10 +113,12 @@ public function execute(): bool
 			while (!$is_done) {
 				$this->handleTimeout($start);
 
+				$label_info = [];
+				$member_list = [];
 				$inserts = [];
 
 				// Pull the label info
-				$get_labels = Db::$db->query(
+				$get_labels = $this->query(
 					'SELECT id_member, message_labels
 					FROM {db_prefix}members
 					WHERE message_labels != {string:blank}
@@ -131,9 +130,6 @@ public function execute(): bool
 					],
 				);
 
-				$label_info = [];
-				$member_list = [];
-
 				while ($row = Db::$db->fetch_assoc($get_labels)) {
 					$member_list[] = $row['id_member'];
 
@@ -149,6 +145,11 @@ public function execute(): bool
 
 				Db::$db->free_result($get_labels);
 
+				if (empty($member_list)) {
+					$is_done = true;
+					break;
+				}
+
 				foreach ($label_info as $id_member => $labels) {
 					foreach ($labels as $label => $index) {
 						$inserts[] = [$id_member, $label];
@@ -172,7 +173,7 @@ public function execute(): bool
 				}
 
 				// This is the easy part - update the inbox stuff
-				Db::$db->query(
+				$this->query(
 					'UPDATE {db_prefix}pm_recipients
 					SET in_inbox = {int:in_inbox}
 					WHERE FIND_IN_SET({int:minusone}, labels)
@@ -185,7 +186,7 @@ public function execute(): bool
 				);
 
 				// Now we go pull the new IDs for each label
-				$get_new_label_ids = Db::$db->query(
+				$get_new_label_ids = $this->query(
 					'SELECT *
 					FROM {db_prefix}pm_labels
 					WHERE id_member IN ({array_int:member_list})',
@@ -206,7 +207,7 @@ public function execute(): bool
 
 				// Pull label info from pm_recipients
 				// Ignore any that are only in the inbox
-				$get_pm_labels = Db::$db->query(
+				$get_pm_labels = $this->query(
 					'SELECT id_pm, id_member, labels
 					FROM {db_prefix}pm_recipients
 					WHERE deleted = {int:not_deleted}
@@ -249,7 +250,7 @@ public function execute(): bool
 				}
 
 				// Final step of this ridiculously massive process
-				$get_pm_rules = Db::$db->query(
+				$get_pm_rules = $this->query(
 					'SELECT id_member, id_rule, actions
 					FROM {db_prefix}pm_rules
 					WHERE id_member IN ({array_int:member_list})',
@@ -278,7 +279,7 @@ public function execute(): bool
 						// Put this back into a string
 						$actions = serialize($actions);
 
-						Db::$db->query(
+						$this->query(
 							'UPDATE {db_prefix}pm_rules
 							SET actions = {string:actions}
 							WHERE id_rule = {int:id_rule}',
@@ -291,7 +292,7 @@ public function execute(): bool
 				}
 
 				// Remove processed pm labels, to avoid duplicated data if upgrader is restarted.
-				Db::$db->query(
+				$this->query(
 					'UPDATE {db_prefix}members
 					SET message_labels = {string:blank}
 					WHERE id_member IN ({array_int:member_list})',
@@ -311,32 +312,10 @@ public function execute(): bool
 		}
 
 		$pm_recipients_table = new Schema\v2_1\PmRecipients();
-		$existing_structure = $pm_recipients_table->getCurrentStructure();
-
-		foreach ($existing_structure['columns'] as $column) {
-			if ($column['name'] == 'labels') {
-				$col = new Column(
-					name: $column['name'],
-					type: 'varchar',
-				);
-
-				$pm_recipients_table->dropColumn($col);
-			}
-		}
+		$pm_recipients_table->dropColumn('labels');
 
 		$members_table = new Schema\v2_1\Members();
-		$existing_structure = $members_table->getCurrentStructure();
-
-		foreach ($existing_structure['columns'] as $column) {
-			if ($column['name'] == 'message_labels') {
-				$col = new Column(
-					name: $column['name'],
-					type: 'varchar',
-				);
-
-				$members_table->dropColumn($col);
-			}
-		}
+		$members_table->dropColumn('message_labels');
 
 		return true;
 	}
diff --git a/Sources/Maintenance/Migration/v2_1/PostgreSqlSequences.php b/Sources/Maintenance/Migration/v2_1/PostgreSqlSequences.php
index 357608b120..72a6a29210 100644
--- a/Sources/Maintenance/Migration/v2_1/PostgreSqlSequences.php
+++ b/Sources/Maintenance/Migration/v2_1/PostgreSqlSequences.php
@@ -238,7 +238,7 @@ public function isCandidate(): bool
 	 */
 	public function execute(): bool
 	{
-		for ($i = Maintenance::getCurrentStart(); $i < \count($this->sequences); Maintenance::setCurrentStart()) {
+		for ($i = Maintenance::getCurrentStart(); $i < \count($this->sequences); $i++) {
 			$this->handleTimeout();
 
 			$value = $this->sequences[$i];
@@ -248,9 +248,11 @@ public function execute(): bool
 				[
 					'key' => Config::$db_prefix . $value['key'],
 					'field' => $value['field'],
-					'table' => $value['table'],
+					'table' => Config::$db_prefix . $value['table'],
 				],
 			);
+
+			Maintenance::setCurrentStart();
 		}
 
 		return true;
diff --git a/Sources/Maintenance/Migration/v2_1/Smileys.php b/Sources/Maintenance/Migration/v2_1/Smileys.php
index 007e8c11c4..c4536b8e37 100644
--- a/Sources/Maintenance/Migration/v2_1/Smileys.php
+++ b/Sources/Maintenance/Migration/v2_1/Smileys.php
@@ -48,12 +48,7 @@ public function execute(): bool
 		// Adding the new `smiley_files` table
 		if ($start <= 0) {
 			$table = new Schema\v2_1\SmileyFiles();
-			$existing_tables = Db::$db->list_tables();
-
-			if (!\in_array(Config::$db_prefix . $table->name, $existing_tables)) {
-				$table->create();
-			}
-
+			$table->create();
 			$this->handleTimeout(++$start);
 		}
 
diff --git a/Sources/Maintenance/Migration/v2_1/UserDrafts.php b/Sources/Maintenance/Migration/v2_1/UserDrafts.php
index 7e32267a52..6abe26cccc 100644
--- a/Sources/Maintenance/Migration/v2_1/UserDrafts.php
+++ b/Sources/Maintenance/Migration/v2_1/UserDrafts.php
@@ -41,9 +41,9 @@ class UserDrafts extends MigrationBase
 	 */
 	public function isCandidate(): bool
 	{
-		$tables = Db::$db->list_tables();
+		$table = new Schema\v2_1\UserDrafts();
 
-		return !\in_array(Config::$db_prefix . 'user_drafts', $tables) || Maintenance::getCurrentStart() > 0;
+		return !$table->exists();
 	}
 
 	/**
@@ -53,14 +53,10 @@ public function execute(): bool
 	{
 		$start = Maintenance::getCurrentStart();
 
-		$drafts_table = new Schema\v2_1\UserDrafts();
-
-		$tables = Db::$db->list_tables();
-
 		// Creating draft table.
-		if ($start <= 0 && !\in_array(Config::$db_prefix . 'user_drafts', $tables)) {
-			$drafts_table->create();
-
+		if ($start <= 0) {
+			$table = new Schema\v2_1\UserDrafts();
+			$table->create();
 			$this->handleTimeout(++$start);
 		}
 
@@ -74,8 +70,8 @@ public function execute(): bool
 			)
 		) {
 			// Anyone who can currently post unapproved topics we assume can create drafts as well ...
-			$request = Db::$db->query(
-				'SELECT id_group, id_board, add_deny, permission
+			$request = $this->query(
+				'SELECT id_group, id_profile, add_deny, permission
 				FROM {db_prefix}board_permissions
 				WHERE permission = {literal:post_unapproved_topics}',
 				[],
@@ -86,7 +82,7 @@ public function execute(): bool
 			while ($row = Db::$db->fetch_assoc($request)) {
 				$inserts[] = [
 					(int) $row['id_group'],
-					(int) $row['id_board'],
+					(int) $row['id_profile'],
 					'post_draft',
 					(int) $row['add_deny'],
 				];
@@ -99,7 +95,7 @@ public function execute(): bool
 					'{db_prefix}board_permissions',
 					[
 						'id_group' => 'int',
-						'id_board' => 'int',
+						'id_profile' => 'int',
 						'permission' => 'string',
 						'add_deny' => 'int',
 					],
diff --git a/Sources/Maintenance/Migration/v2_1/VerificationQuestions.php b/Sources/Maintenance/Migration/v2_1/VerificationQuestions.php
index f0d44b5ee5..e9a500a81a 100644
--- a/Sources/Maintenance/Migration/v2_1/VerificationQuestions.php
+++ b/Sources/Maintenance/Migration/v2_1/VerificationQuestions.php
@@ -15,7 +15,6 @@
 
 namespace SMF\Maintenance\Migration\v2_1;
 
-use SMF\Config;
 use SMF\Db\DatabaseApi as Db;
 use SMF\Db\Schema;
 use SMF\Maintenance\Maintenance;
@@ -41,9 +40,9 @@ class VerificationQuestions extends MigrationBase
 	 */
 	public function isCandidate(): bool
 	{
-		$tables = Db::$db->list_tables();
+		$table = new Schema\v2_1\Qanda();
 
-		return !\in_array(Config::$db_prefix . 'qanda', $tables);
+		return !$table->exists();
 	}
 
 	/**
@@ -53,14 +52,10 @@ public function execute(): bool
 	{
 		$start = Maintenance::getCurrentStart();
 
-		$table = new Schema\v2_1\Qanda();
-
-		$tables = Db::$db->list_tables();
-
-		// Creating draft table.
-		if ($start <= 0 && !\in_array(Config::$db_prefix . 'qanda', $tables)) {
+		// Creating table.
+		if ($start <= 0) {
+			$table = new Schema\v2_1\Qanda();
 			$table->create();
-
 			$this->handleTimeout(++$start);
 		}
 
diff --git a/Sources/Maintenance/Migration/v3_0/ConvertToInnoDb.php b/Sources/Maintenance/Migration/v3_0/ConvertToInnoDb.php
index e3d0ffa935..a0a82b4b07 100644
--- a/Sources/Maintenance/Migration/v3_0/ConvertToInnoDb.php
+++ b/Sources/Maintenance/Migration/v3_0/ConvertToInnoDb.php
@@ -54,7 +54,7 @@ public function execute(): bool
 			$result = true;
 
 			if ($structure['engine'] !== 'InnoDB') {
-				$result = Db::$db->query(
+				$result = $this->query(
 					'ALTER TABLE {identifier:table}
 					ENGINE {literal:InnoDB}
 					ROW_FORMAT=DYNAMIC',
@@ -63,7 +63,7 @@ public function execute(): bool
 					],
 				);
 			} elseif ($structure['row_format'] !== 'Dynamic') {
-				$result = Db::$db->query(
+				$result = $this->query(
 					'ALTER TABLE {identifier:table}
 					ROW_FORMAT=DYNAMIC',
 					[
@@ -80,7 +80,7 @@ public function execute(): bool
 		// Try to ensure all future tables use dynamic row format.
 		$can_set_global_default = false;
 
-		$request = Db::$db->query('SHOW GRANTS');
+		$request = $this->query('SHOW GRANTS');
 
 		while ($row = Db::$db->fetch_row($request)) {
 			if (
@@ -95,7 +95,7 @@ public function execute(): bool
 		Db::$db->free_result($request);
 
 		if ($can_set_global_default) {
-			$result = Db::$db->query(
+			$result = $this->query(
 				'SET GLOBAL innodb_default_row_format=DYNAMIC',
 				[
 					'db_error_skip' => true,
diff --git a/Sources/Maintenance/Migration/v3_0/EditHistory.php b/Sources/Maintenance/Migration/v3_0/EditHistory.php
index 42e559964b..28507c2792 100644
--- a/Sources/Maintenance/Migration/v3_0/EditHistory.php
+++ b/Sources/Maintenance/Migration/v3_0/EditHistory.php
@@ -50,7 +50,7 @@ public function execute(): bool
 		}
 
 		// Populate edit_history.
-		$request = Db::$db->query(
+		$request = $this->query(
 			'SELECT id_msg, body, modified_time, modified_name, modified_reason, edit_history
 			FROM {db_prefix}messages
 			WHERE id_msg > {int:start}
@@ -77,7 +77,7 @@ public function execute(): bool
 				$row['modified_reason'],
 			]]);
 
-			Db::$db->query(
+			$this->query(
 				'UPDATE {db_prefix}messages
 				SET edit_history = {string:edit_history}
 				WHERE id_msg = {int:id_msg}',
diff --git a/Sources/Maintenance/Migration/v3_0/EventUids.php b/Sources/Maintenance/Migration/v3_0/EventUids.php
index 446978eae0..bf1389e5f2 100644
--- a/Sources/Maintenance/Migration/v3_0/EventUids.php
+++ b/Sources/Maintenance/Migration/v3_0/EventUids.php
@@ -41,7 +41,7 @@ public function execute(): bool
 	{
 		$calendar_updates = [];
 
-		$request = Db::$db->query(
+		$request = $this->query(
 			'SELECT id_event, uid
 			FROM {db_prefix}calendar',
 			[],
@@ -56,7 +56,7 @@ public function execute(): bool
 		Db::$db->free_result($request);
 
 		foreach ($calendar_updates as $calendar_update) {
-			Db::$db->query(
+			$this->query(
 				'UPDATE {db_prefix}calendar
 				SET uid = {string:uid}
 				WHERE id_event = {int:id_event}',
diff --git a/Sources/Maintenance/Migration/v3_0/HolidaysToEvents.php b/Sources/Maintenance/Migration/v3_0/HolidaysToEvents.php
index 199524a028..e5697819e9 100644
--- a/Sources/Maintenance/Migration/v3_0/HolidaysToEvents.php
+++ b/Sources/Maintenance/Migration/v3_0/HolidaysToEvents.php
@@ -634,7 +634,7 @@ public function execute(): bool
 			User::load();
 		}
 
-		$request = Db::$db->query(
+		$request = $this->query(
 			'SELECT title, GROUP_CONCAT(event_date) as rdates
 			FROM {db_prefix}calendar_holidays
 			GROUP BY title',
diff --git a/Sources/Maintenance/Migration/v3_0/LanguageDirectory.php b/Sources/Maintenance/Migration/v3_0/LanguageDirectory.php
index 57c527cd77..4a85c9fa1c 100644
--- a/Sources/Maintenance/Migration/v3_0/LanguageDirectory.php
+++ b/Sources/Maintenance/Migration/v3_0/LanguageDirectory.php
@@ -72,7 +72,7 @@ public function execute(): bool
 			$this->handleTimeout();
 
 			// Skip errors here so we don't croak if the columns don't exist...
-			$request = Db::$db->query(
+			$request = $this->query(
 				'SELECT id_member
                 FROM {db_prefix}members
                 WHERE lngfile IN ({array_string:possible_languages})
@@ -105,7 +105,7 @@ public function execute(): bool
 			$args['search_members'] = $members;
 
 
-			Db::$db->query(
+			$this->query(
 				'UPDATE {db_prefix}members
                 SET lngfile = CASE
                     ' . implode(' ', $statements) . '
diff --git a/Sources/Maintenance/Migration/v3_0/MailType.php b/Sources/Maintenance/Migration/v3_0/MailType.php
index c8e1a005d4..d60f295789 100644
--- a/Sources/Maintenance/Migration/v3_0/MailType.php
+++ b/Sources/Maintenance/Migration/v3_0/MailType.php
@@ -15,7 +15,6 @@
 
 namespace SMF\Maintenance\Migration\v3_0;
 
-use SMF\Db\DatabaseApi as Db;
 use SMF\Maintenance\Migration\MigrationBase;
 
 class MailType extends MigrationBase
@@ -38,7 +37,7 @@ class MailType extends MigrationBase
 	 */
 	public function execute(): bool
 	{
-		Db::$db->query(
+		$this->query(
 			'UPDATE {db_prefix}settings
 			SET value =
 				CASE
diff --git a/Sources/Maintenance/Migration/v3_0/PermissionChanges.php b/Sources/Maintenance/Migration/v3_0/PermissionChanges.php
index 17061c1b74..75cd3dabb1 100644
--- a/Sources/Maintenance/Migration/v3_0/PermissionChanges.php
+++ b/Sources/Maintenance/Migration/v3_0/PermissionChanges.php
@@ -15,7 +15,6 @@
 
 namespace SMF\Maintenance\Migration\v3_0;
 
-use SMF\Db\DatabaseApi as Db;
 use SMF\Maintenance\Migration\MigrationBase;
 
 class PermissionChanges extends MigrationBase
@@ -38,7 +37,7 @@ class PermissionChanges extends MigrationBase
 	 */
 	public function execute(): bool
 	{
-		Db::$db->query(
+		$this->query(
 			'DELETE FROM {db_prefix}permissions
 			WHERE permission IN ({array_string:perms})',
 			[
diff --git a/Sources/Maintenance/Migration/v3_0/PostgreSqlFunctions.php b/Sources/Maintenance/Migration/v3_0/PostgreSqlFunctions.php
index 493a23bf26..bf3ace41f8 100644
--- a/Sources/Maintenance/Migration/v3_0/PostgreSqlFunctions.php
+++ b/Sources/Maintenance/Migration/v3_0/PostgreSqlFunctions.php
@@ -49,7 +49,7 @@ public function execute(): bool
 	{
 		$schema_version = substr(__NAMESPACE__, strrpos(__NAMESPACE__, '\\', -1) + 1);
 
-		$queries = Table::getInitializers($schema_version, POSTGRE_TITLE);
+		$queries = Table::getInitializers($schema_version);
 
 		foreach ($queries as $query) {
 			// Use the upgrade query handler.
diff --git a/Sources/Maintenance/Migration/v3_0/RecurringEvents.php b/Sources/Maintenance/Migration/v3_0/RecurringEvents.php
index ce10d2a8e5..c3cc255fb6 100644
--- a/Sources/Maintenance/Migration/v3_0/RecurringEvents.php
+++ b/Sources/Maintenance/Migration/v3_0/RecurringEvents.php
@@ -51,13 +51,13 @@ public function execute(): bool
 		}
 
 		if (Db::$db->title === MYSQL_TITLE) {
-			Db::$db->query(
+			$this->query(
 				'ALTER TABLE {db_prefix}calendar
 				MODIFY COLUMN start_date DATE AFTER id_member',
 				[],
 			);
 
-			Db::$db->query(
+			$this->query(
 				'ALTER TABLE {db_prefix}calendar
 				MODIFY COLUMN end_date DATE AFTER start_date',
 				[],
@@ -67,7 +67,7 @@ public function execute(): bool
 
 		$updates = [];
 
-		$request = Db::$db->query(
+		$request = $this->query(
 			'SELECT id_event, start_date, end_date, start_time, end_time, timezone
 			FROM {db_prefix}calendar',
 			[],
@@ -120,7 +120,7 @@ public function execute(): bool
 		Db::$db->free_result($request);
 
 		foreach ($updates as $id_event => $changes) {
-			Db::$db->query(
+			$this->query(
 				'UPDATE {db_prefix}calendar
 				SET duration = {string:duration}, end_date = {date:end_date}, rrule = {string:rrule}
 				WHERE id_event = {int:id_event}',
diff --git a/Sources/Maintenance/Tools/Install.php b/Sources/Maintenance/Tools/Install.php
index 29a50f32ad..72b77c54b6 100644
--- a/Sources/Maintenance/Tools/Install.php
+++ b/Sources/Maintenance/Tools/Install.php
@@ -742,7 +742,7 @@ public function databasePopulation(): bool
 		// Some initialization may exist.
 		Db::$db->disableQueryCheck = true;
 
-		foreach (Table::getInitializers($this->schema_version, Db::$db->title) as $query) {
+		foreach (Table::getInitializers($this->schema_version) as $query) {
 			Db::$db->query($query, [
 				'security_override' => true,
 			]);
diff --git a/Sources/Maintenance/Tools/ToolsBase.php b/Sources/Maintenance/Tools/ToolsBase.php
index abe6ee09fa..8442c464b0 100644
--- a/Sources/Maintenance/Tools/ToolsBase.php
+++ b/Sources/Maintenance/Tools/ToolsBase.php
@@ -559,7 +559,7 @@ public function checkAndHandleTimeout(): void
 			Maintenance::setQueryString();
 		}
 
-		Maintenance::exit();
+		Maintenance::exit(Maintenance::isJson());
 
 		throw new \Exception('Zombies!');
 	}
@@ -585,6 +585,15 @@ public function updateSettingsFile(array $config_vars, ?bool $keep_quotes = null
 			$this->logProgress(Lang::getTxt('log_settings_file_save', ['setting_names' => Lang::sentenceList(array_keys($config_vars))], file: 'Maintenance'), true);
 		}
 
+		if ($rebuild) {
+			// Remove all the existing comments to make the rebuild nice and clean.
+			Config::safeFileWrite(
+				file: SMF_SETTINGS_FILE,
+				data: Config::stripPhpComments(file_get_contents(SMF_SETTINGS_FILE)),
+				mtime: time(),
+			);
+		}
+
 		if (!Config::updateSettingsFile($config_vars, $keep_quotes, $rebuild)) {
 			$this->logProgress(Lang::getTxt('log_failed_with_error', ['error' => Lang::getTxt('settings_error', file: 'Maintenance')], file: 'Maintenance'));
 
diff --git a/Sources/Maintenance/Tools/Upgrade.php b/Sources/Maintenance/Tools/Upgrade.php
index 5af6b9bb82..ebb5f5d08c 100644
--- a/Sources/Maintenance/Tools/Upgrade.php
+++ b/Sources/Maintenance/Tools/Upgrade.php
@@ -86,7 +86,6 @@ class Upgrade extends ToolsBase implements ToolsInterface
 			Migration\v2_1\FixDates::class,
 			Migration\v2_1\CreateMemberLogins::class,
 			Migration\v2_1\CollapsedCategories::class,
-			Migration\v2_1\BoardDescriptions::class,
 			Migration\v2_1\LegacyAttachments::class,
 			Migration\v2_1\AttachmentSizes::class,
 			Migration\v2_1\AttachmentDirectory::class,
@@ -154,6 +153,7 @@ class Upgrade extends ToolsBase implements ToolsInterface
 			Migration\v2_1\IdxLogComments::class,
 			Migration\v2_1\MysqlLegacyData::class,
 			Migration\v2_1\Smileys::class,
+			Migration\v2_1\BoardDescriptions::class,
 			Migration\v2_1\LogErrorsBacktrace::class,
 			Migration\v2_1\BoardPermissionsView::class,
 			Migration\v2_1\PostgreSqlSchemaDiff::class,
@@ -193,7 +193,6 @@ class Upgrade extends ToolsBase implements ToolsInterface
 		// Cleanup steps for 2.1 -> 3.0
 		'v3_0' => [
 			Cleanup\v3_0\TasksDirCase::class,
-			Cleanup\v3_0\OldFiles::class,
 		],
 	];
 
@@ -979,18 +978,15 @@ public function upgradeOptions(): bool
 		// If they have a "host:port" setup for the host, split that into separate values
 		// You should never have a : in the hostname if you're not on MySQL, but better safe than sorry
 		if (strpos(Config::$db_server, ':') !== false) {
-			list(Config::$db_server, Config::$db_port) = explode(':', Config::$db_server);
+			list(Config::$db_server, $db_port) = explode(':', Config::$db_server);
+			Config::$db_port = (int) $db_port;
 
 			$file_settings['db_server'] = Config::$db_server;
-
-			// Only set this if we're not using the default port
-			if (Config::$db_port != Db::$db->getDefaultPort()) {
-				$file_settings['db_port'] = (int) Config::$db_port;
-			}
+			$file_settings['db_port'] = Config::$db_port;
 		}
 
 		// If db_port is set and is the same as the default, set it to 0.
-		if (!empty(Config::$db_port) && Config::$db_port != Db::$db->getDefaultPort()) {
+		if (!empty(Config::$db_port) && Config::$db_port == Db::$db->getDefaultPort()) {
 			$file_settings['db_port'] = 0;
 		}
 
@@ -1223,10 +1219,11 @@ public function finalize(): bool
 
 		// Log what we've done.
 		if (!isset(User::$me)) {
-			User::load();
+			User::load(dataset: 'minimal');
 		}
 
 		if (empty(User::$me->id) && !empty($this->user['id'])) {
+			User::load($this->user['id'], dataset: 'minimal');
 			User::setMe($this->user['id']);
 		}
 
@@ -1611,6 +1608,7 @@ private function performSubsteps(array $substeps): bool
 
 		// Load up the current user safely.
 		if (!isset(User::$me)) {
+			User::load($this->user['id'], dataset: 'minimal');
 			User::setMe($this->user['id']);
 
 			if ($this->user['id'] === 0 && $this->user['name'] === 'Database Admin') {
diff --git a/Sources/QueryString.php b/Sources/QueryString.php
index c92a0a3ed5..554130c65c 100644
--- a/Sources/QueryString.php
+++ b/Sources/QueryString.php
@@ -639,6 +639,7 @@ protected static function sslRedirect(): void
 			&& !Sapi::httpsOn()
 			&& str_starts_with($_SERVER['REQUEST_URL'] ?? '', 'http://')
 			&& SMF != 'SSI'
+			&& !\defined('SMF_INSTALLING')
 		) {
 			if (isset($_GET['sslRedirect'])) {
 				ErrorHandler::fatalLang('login_ssl_required', false);
@@ -654,7 +655,7 @@ protected static function sslRedirect(): void
 	 */
 	protected static function wwwRedirect(): void
 	{
-		if (SMF == 'SSI') {
+		if (SMF == 'SSI' || \defined('SMF_INSTALLING')) {
 			return;
 		}
 
@@ -678,7 +679,7 @@ protected static function wwwRedirect(): void
 	 */
 	protected static function fixUrl(): void
 	{
-		if (SMF == 'SSI') {
+		if (SMF == 'SSI' || \defined('SMF_INSTALLING')) {
 			return;
 		}
 
diff --git a/Themes/default/Admin.template.php b/Themes/default/Admin.template.php
index 2be9b2150e..bfc0c72d49 100644
--- a/Themes/default/Admin.template.php
+++ b/Themes/default/Admin.template.php
@@ -1557,7 +1557,7 @@ function template_repair_boards()
 	if (!empty(Utils::$context['redirect_to_recount'])) {
 		echo '
 					<script>
-						doAutoSubmit(5, "', Utils::escapeJavaScript(Lang::getTxt('errors_recount_now', file: 'Admin')), '", "recount_form", "recount_now");
+						doAutoSubmit(5, "', Utils::escapeJavaScript(Lang::getTxt('errors_recount_now', file: 'Admin')), '", "recount_form", "recount");
 					</script>';
 	}
 }
diff --git a/Themes/default/MaintenanceTemplate.php b/Themes/default/MaintenanceTemplate.php
index e95ae46dbf..90f98f97f0 100644
--- a/Themes/default/MaintenanceTemplate.php
+++ b/Themes/default/MaintenanceTemplate.php
@@ -36,16 +36,17 @@ public static function header(): void
 		<meta charset="', Lang::getTxt('lang_character_set', file: 'General') ?? 'UTF-8', '">
 		<meta name="robots" content="noindex">
 		<title>', Maintenance::$tool->getPageTitle(), '</title>
-		<link rel="stylesheet" href="', Maintenance::$theme_url, '/css/index.css">
-		<link rel="stylesheet" href="', Maintenance::$theme_url, '/css/maintenance.css">', Lang::getTxt('lang_rtl', file: 'General') == '1' ? '
-		<link rel="stylesheet" href="' . Maintenance::$theme_url . '/css/rtl.css">' : '', '
+		<link rel="stylesheet" href="', Maintenance::$theme_url, '/css/index.css?' . Maintenance::$context['started'] . '">
+		<link rel="stylesheet" href="', Maintenance::$theme_url, '/css/maintenance.css?' . Maintenance::$context['started'] . '">', Lang::getTxt('lang_rtl', file: 'General') == '1' ? '
+		<link rel="stylesheet" href="' . Maintenance::$theme_url . '/css/rtl.css?' . Maintenance::$context['started'] . '">' : '', '
 		<script src="', Maintenance::$theme_url, '/scripts/jquery-' . JQUERY_VERSION . '.min.js"></script>
-		<script src="', Maintenance::$theme_url, '/scripts/script.js"></script>
+		<script src="', Maintenance::$theme_url, '/scripts/script.js?' . Maintenance::$context['started'] . '"></script>
 		<script>
 			const smf_scripturl = \'', Maintenance::getSelf(), '\';
 			const smf_charset = \'UTF-8\';
 			const allow_xhjr_credentials = true;
 			const isDebug = ', Maintenance::$tool->isDebug() ? 'true' : 'false', ';
+			const timeStarted = ', Maintenance::$context['started'], ';
 			let startPercent = ', Maintenance::$overall_percent, ';
 		</script>
 	</head>
@@ -124,8 +125,9 @@ public static function header(): void
 							<span id="substep_text">', Maintenance::getItemsProgress(), '%</span>
 						</div>
 
-						<div id="time_elapsed" class="smalltext time_elapsed">
-							', Maintenance::getTimeElapsed(), '
+						<div class="smalltext time_elapsed">
+							', Lang::getTxt('maintenance_time_elapsed', file: 'Maintenance'), '
+							<time id="time_elapsed">', Maintenance::getTimeElapsed(), '</time>
 						</div>
 					</div>
 					<div id="main_screen" class="clear">
@@ -183,6 +185,20 @@ function updateItemsProgress(current, max)
 					setInnerHTML(document.getElementById("item_text"), width + "%");
 				}
 			}
+
+			// This function dynamically updates the "time elapsed" counter.
+			function updateTimeElapsed()
+			{
+				const elapsed = (Date.now() / 1000) - timeStarted;
+
+				const s = elapsed % 60;
+				const m = Math.floor(elapsed / 60) % 60;
+				const h = Math.floor(elapsed / 3600);
+
+				if (document.getElementById("time_elapsed")) {
+					document.getElementById("time_elapsed").textContent = ((h > 0) ? ((h < 10) ? "0" + h.toString() : h.toString()) + ":" : "") + ((m < 10) ? "0" + m.toString() : m.toString()) + ":" + ((s < 10) ? "0" + s.toString() : s.toString());
+				}
+			}
 		</script>
 	</body>
 	</html>';
@@ -311,7 +327,7 @@ protected static function showStepWithSubSteps(string $type, string $done_param)
 			<h3>', Lang::getTxt('upgrade_performing_substeps', ['type' => $type], file: 'Maintenance'), '</h3>
 			<h4><em>', Lang::getTxt('upgrade_please_be_patient', file: 'Maintenance'), '</em></h4>
 			<input type="hidden" name="', $done_param, '" id="', $done_param, '" value="0">
-			<div id="debug_section" class="bbc_code"><span id="debuginfo"></span></div>';
+			<div id="debug_section" class="bbc_code', !Maintenance::$tool->isDebug() ? ' hidden' : '', '"><span id="debuginfo"></span></div>';
 
 		echo '
 			<h3 id="current_tab">',
@@ -360,7 +376,7 @@ protected static function showStepWithSubSteps(string $type, string $done_param)
 
 				function getNextSubstep()
 				{
-					const url = "' . Maintenance::getSelf() . '?' . Maintenance::setQueryString() . '&json"
+					const url = "' . Maintenance::getSelf() . '?' . Maintenance::setQueryString() . ';json"
 						.replace(/substep=\d+/, "substep=" + iCurrentSubStep)
 						.replace(/start=\d+/, "start=" + iCurrentStart);
 
diff --git a/Themes/default/ManageNews.template.php b/Themes/default/ManageNews.template.php
index aa39082a84..2b629a8e44 100644
--- a/Themes/default/ManageNews.template.php
+++ b/Themes/default/ManageNews.template.php
@@ -414,7 +414,7 @@ function template_email_members_send()
 		</form>
 
 	<script>
-			doAutoSubmit(2, ', Utils::escapeJavaScript(Lang::getTxt('email_continue', file: 'Admin')), ');
+			doAutoSubmit(2, ', Utils::escapeJavaScript(Lang::getTxt('email_continue', file: 'Admin')), ', "autoSubmit", "b");
 	</script>';
 }
 
diff --git a/Themes/default/ManageSearch.template.php b/Themes/default/ManageSearch.template.php
index 2925b00102..7997c5e0f9 100644
--- a/Themes/default/ManageSearch.template.php
+++ b/Themes/default/ManageSearch.template.php
@@ -331,7 +331,7 @@ function template_create_index_progress()
 		<input type="hidden" name="', Utils::$context['session_var'], '" value="', Utils::$context['session_id'], '">
 	</form>
 	<script>
-		doAutoSubmit(10, ', Utils::escapeJavaScript(Lang::getTxt('search_create_index_continue', file: 'Search')), ');
+		doAutoSubmit(10, ', Utils::escapeJavaScript(Lang::getTxt('search_create_index_continue', file: 'Search')), ', "autoSubmit", "b");
 	</script>';
 
 }
diff --git a/Themes/default/Packages.template.php b/Themes/default/Packages.template.php
index 6beda4e4d7..4f0b3ec558 100644
--- a/Themes/default/Packages.template.php
+++ b/Themes/default/Packages.template.php
@@ -1776,7 +1776,7 @@ function template_action_permissions()
 	$countDown = 3;
 
 	echo '
-		<form action="', Config::$scripturl, '?action=admin;area=packages;sa=perms;', Utils::$context['session_var'], '=', Utils::$context['session_id'], '" id="perm_submit" method="post" accept-charset="UTF-8">
+		<form action="', Config::$scripturl, '?action=admin;area=packages;sa=perms;', Utils::$context['session_var'], '=', Utils::$context['session_id'], '" name="perm_submit" id="perm_submit" method="post" accept-charset="UTF-8">
 			<div class="cat_bar">
 				<h3 class="catbg">', Lang::getTxt('package_file_perms_applying', file: 'Packages'), '</h3>
 			</div>';
@@ -1878,6 +1878,6 @@ function template_action_permissions()
 	// Just the countdown stuff
 	echo '
 	<script>
-		doAutoSubmit(', $countDown, ', ', Utils::escapeJavaScript(Lang::getTxt('not_done_continue', file: 'Admin')), '"perm_submit", "go");
+		doAutoSubmit(', $countDown, ', ', Utils::escapeJavaScript(Lang::getTxt('not_done_continue', file: 'Admin')), ', "perm_submit", "go");
 	</script>';
 }
diff --git a/Themes/default/Post.template.php b/Themes/default/Post.template.php
index d168725fc1..5ff3312b06 100644
--- a/Themes/default/Post.template.php
+++ b/Themes/default/Post.template.php
@@ -872,8 +872,7 @@ function template_announcement_send()
 	</div><!-- #announcement -->
 	<br>
 	<script>
-		var countdown = 2;
-		doAutoSubmit(2, ', Utils::escapeJavaScript(Lang::getTxt('announce_continue', file: 'Post')), ');
+		doAutoSubmit(2, ', Utils::escapeJavaScript(Lang::getTxt('announce_continue', file: 'Post')), ', "autoSubmit", "b");
 	</script>';
 }
 
diff --git a/Themes/default/UpgradeTemplate.php b/Themes/default/UpgradeTemplate.php
index 12fd0b60a1..96cec3ffb1 100644
--- a/Themes/default/UpgradeTemplate.php
+++ b/Themes/default/UpgradeTemplate.php
@@ -91,17 +91,11 @@ public static function lower(): void
 		echo '
 			</form>';
 
-		echo '
-			<script>
-				setTimeout(\'doAutoSubmit(' . (Maintenance::$tool->isDebug() ? '-1' : '3') . ', "', Lang::getTxt('action_continue', file: 'Maintenance'), '", "upform", "contbutt");\', 1000);
-			</script>';
-
 		// Are we on a pause?
-		// !!! TODO: Why?
-		if (!empty(Maintenance::$context['pause'])) {
+		if (!empty(Maintenance::$context['pause']) && !Maintenance::$tool->isDebug()) {
 			echo '
-			<script defer>
-				window.onload = doAutoSubmit;
+			<script>
+				doAutoSubmit(3, "', Lang::getTxt('action_continue', file: 'Maintenance'), '", "upform", "contbutt");
 			</script>';
 		}
 	}
