From 177361acaeaeeea49a79aa764cba0ebaebe5d80a Mon Sep 17 00:00:00 2001 From: Dillard Blom Date: Wed, 21 Oct 2026 09:08:50 +0200 Subject: [PATCH] Index load keeps the index when it refuses an empty listing; counters of unchanged tables are repaired too, never moved backwards --- lib/Db/DbDumper.php | 12 +++++++++--- lib/Repository/BlobIndex.php | 3 ++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/Db/DbDumper.php b/lib/Db/DbDumper.php index f99da8f..b0918f7 100644 --- a/lib/Db/DbDumper.php +++ b/lib/Db/DbDumper.php @@ -216,7 +216,8 @@ final class DbDumper { } $report['restored'][] = $table; } - $this->restoreSequences($manifest['sequences'] ?? [], $report['missing'], $report['restored']); + // unchanged tables too: a restore that stopped earlier may have left their counters at 1 + $this->restoreSequences($manifest['sequences'] ?? [], $report['missing'], array_merge($report['restored'], $report['skipped'])); $this->resumeForeignKeys($foreignKeys); $this->db->commit(); // Foreign keys of the tables created above, now their rows are in. Not inside the @@ -575,7 +576,7 @@ final class DbDumper { /** * @param list $leftOut tables not restored (--skip-missing-tables): they do not exist here - * @param list $restored tables restored: their other counters go after their data + * @param list $restored tables restored or unchanged: their other counters go after their data */ private function restoreSequences(array $sequences, array $leftOut = [], array $restored = []): void { if (!$this->db->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform) { @@ -616,7 +617,12 @@ final class DbDumper { } $max = (int)$this->db->fetchOne(sprintf('SELECT COALESCE(MAX(%s), 0) FROM %s', $this->db->quoteIdentifier($r['column_name']), $this->db->quoteIdentifier($r['table_name']))); - $this->db->executeQuery('SELECT setval(CAST(? AS regclass), ?, false)', [$r['seq'], $max + 1]); + // never backwards: a counter already past the data stays where it is + $state = $this->db->fetchAssociative('SELECT last_value, is_called FROM ' . self::sequenceIdentifier($r['seq'])); + $next = $state === false ? 1 : (int)$state['last_value'] + ($state['is_called'] ? 1 : 0); + if ($next <= $max) { + $this->db->executeQuery('SELECT setval(CAST(? AS regclass), ?, false)', [$r['seq'], $max + 1]); + } } } } diff --git a/lib/Repository/BlobIndex.php b/lib/Repository/BlobIndex.php index 32ca142..fc8e844 100644 --- a/lib/Repository/BlobIndex.php +++ b/lib/Repository/BlobIndex.php @@ -94,7 +94,6 @@ final class BlobIndex { */ public function load(bool $verify = false, ?callable $progress = null): void { $hadIndex = $this->blobs !== []; - $this->blobs = []; $cached = $verify ? [] : ($this->cache?->all($this->repositoryId) ?? []); $present = []; $this->downloaded = 0; @@ -114,6 +113,8 @@ final class BlobIndex { if ($present === [] && ($hadIndex || $cached !== [] || ($verify && ($this->cache?->all($this->repositoryId) ?? []) !== []))) { throw new BackendException('The location listed no index files, while some are known: not trusting that listing'); } + // only now: an empty listing that was refused above leaves the index as it was + $this->blobs = []; $this->loaded = $present; // The rest is downloaded several at a time where the location allows it: on a large // repository these are tens of thousands of small files. getMany() keeps their order, so