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:
Egor Bugaev
2026-07-06 19:52:05 +03:00
parent ca0605706f
commit 9e15c3b96c
4 changed files with 118 additions and 7 deletions
+61 -7
View File
@@ -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);
}
}