Add rename-user SQL template; make batch transaction handling robust against non-Postgres backends
- New 'rename-user' template (templates / template rename-user <old> <new>): best-effort uid rename across core tables (oc_users, oc_preferences, oc_group_user, oc_group_admin, oc_ldap_user_mapping, oc_share, oc_mounts, oc_storages). Explicitly NOT a supported Nextcloud operation - the generated script carries an in-line warning (as leading SQL comments) that app-specific tables (Talk, Calendar, Contacts, Mail, 2FA/WebAuthn...) are not covered and that the data directory must be renamed on disk manually, followed by occ files:scan --all. Documented the same caveat in both READMEs. - Hardened the round-2 transaction wrapping: beginTransaction()/commit()/ rollBack() are now wrapped in try/catch. PostgreSQL (our backend) has fully transactional DDL so this wasn't actually broken here, but Nextcloud also supports MySQL/MariaDB via the same IDBConnection, where DDL implicitly commits - on that backend a DDL statement in the batch would make a later commit()/rollBack() throw 'no active transaction' and previously that exception was unhandled (HTTP 500 instead of a clean JSON response). Now: if rollBack() itself fails after an error, we report rollbackFailed+warning instead of falsely claiming rolledBack, since earlier statements in that batch may already be permanently applied. If commit() fails with nothing to commit (already auto-committed), that's logged only, since the effects are already durably persisted.
This commit is contained in:
@@ -259,10 +259,30 @@ class DbController extends Controller
|
||||
// выполненные в этой же пачке изменения откатываются, а не
|
||||
// остаются частично применёнными. SET (без LOCAL) не транзакционен
|
||||
// в PostgreSQL, поэтому откат не затрагивает current_setting().
|
||||
//
|
||||
// Про DDL: в PostgreSQL (наша БД) DDL — CREATE/ALTER/DROP TABLE и
|
||||
// т.п. — полностью транзакционен и откатывается вместе с остальными
|
||||
// изменениями пачки, поэтому здесь для Postgres проблемы нет. Но
|
||||
// Nextcloud работает и с MySQL/MariaDB через тот же IDBConnection, а
|
||||
// там DDL делает неявный COMMIT — если эта пачка выполнится на
|
||||
// MySQL-инстансе, commit()/rollBack() после такого DDL получат
|
||||
// исключение "no active transaction". Оборачиваем begin/commit/
|
||||
// rollBack в try/catch, чтобы это не превращалось в необработанное
|
||||
// исключение и HTTP 500 вместо аккуратного JSON-ответа.
|
||||
$results = [];
|
||||
$rolledBack = false;
|
||||
$rollbackFailed = false;
|
||||
$transactionStarted = false;
|
||||
|
||||
$this->db->beginTransaction();
|
||||
try {
|
||||
$this->db->beginTransaction();
|
||||
$transactionStarted = true;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('[occweb] beginTransaction() failed, proceeding without an explicit transaction: {error}', [
|
||||
'app' => 'occweb',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($queries as $query) {
|
||||
$normalized = $this->stripLeadingComments($query);
|
||||
@@ -321,20 +341,54 @@ class DbController extends Controller
|
||||
// Откатываем всю пачку и останавливаемся: не продолжаем
|
||||
// выполнять оставшиеся запросы (например, серию DELETE),
|
||||
// если один из предыдущих шагов не выполнился.
|
||||
$this->db->rollBack();
|
||||
$rolledBack = true;
|
||||
if ($transactionStarted) {
|
||||
try {
|
||||
$this->db->rollBack();
|
||||
$rolledBack = true;
|
||||
} catch (\Exception $rollbackError) {
|
||||
// Транзакция уже закрыта не нами — например, неявным
|
||||
// COMMIT-ом от DDL-запроса на MySQL/MariaDB. Всё, что
|
||||
// выполнилось ДО этой точки в пачке, могло остаться
|
||||
// применённым навсегда — намеренно НЕ ставим
|
||||
// rolledBack в true, это было бы неправдой.
|
||||
$rollbackFailed = true;
|
||||
$this->logger->warning('[occweb] rollBack() failed after an error — earlier statements in this batch may already be permanently applied: {error}', [
|
||||
'app' => 'occweb',
|
||||
'error' => $rollbackError->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$rolledBack) {
|
||||
$this->db->commit();
|
||||
if (!$rolledBack && !$rollbackFailed && $transactionStarted) {
|
||||
try {
|
||||
$this->db->commit();
|
||||
} catch (\Exception $e) {
|
||||
// Транзакция уже закоммичена неявно (например, DDL-запросом
|
||||
// на MySQL/MariaDB) — эффекты уже сохранены, это не ошибка
|
||||
// выполнения самой пачки.
|
||||
$this->logger->info('[occweb] commit() had nothing to commit (likely auto-committed by a DDL statement): {error}', [
|
||||
'app' => 'occweb',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return new JSONResponse([
|
||||
$response = [
|
||||
'success' => true,
|
||||
'rolledBack' => $rolledBack,
|
||||
'results' => $results
|
||||
]);
|
||||
];
|
||||
|
||||
if ($rollbackFailed) {
|
||||
$response['rollbackFailed'] = true;
|
||||
$response['warning'] = 'The batch failed partway through and the rollback itself failed (this can happen if an earlier '
|
||||
. 'statement, e.g. a DDL statement, implicitly committed on this database backend). Statements executed before the '
|
||||
. 'failure may have been permanently applied — check the results above and verify manually.';
|
||||
}
|
||||
|
||||
return new JSONResponse($response);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user