From e1bf4a24d2d2ab7586cfd73d59a1e72b22b39bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 13 Oct 2025 10:47:47 +0000 Subject: [PATCH] Enforce filename uniqueness in `StoredObjectVersion` with partial unique index... --- .../unreleased/Fixed-20251013-123932.yaml | 6 ++ .../Entity/StoredObjectVersion.php | 4 ++ .../migrations/Version20251013094414.php | 63 +++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 .changes/unreleased/Fixed-20251013-123932.yaml create mode 100644 src/Bundle/ChillDocStoreBundle/migrations/Version20251013094414.php diff --git a/.changes/unreleased/Fixed-20251013-123932.yaml b/.changes/unreleased/Fixed-20251013-123932.yaml new file mode 100644 index 000000000..3c4b6ae74 --- /dev/null +++ b/.changes/unreleased/Fixed-20251013-123932.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: Add unique condition on stored object filename, with cleaning step on existing duplicate filenames +time: 2025-10-13T12:39:32.85885314+02:00 +custom: + Issue: "446" + SchemaChange: Drop or rename table or columns, or enforce new constraint that must be manually fixed diff --git a/src/Bundle/ChillDocStoreBundle/Entity/StoredObjectVersion.php b/src/Bundle/ChillDocStoreBundle/Entity/StoredObjectVersion.php index 18c78563a..472e77e55 100644 --- a/src/Bundle/ChillDocStoreBundle/Entity/StoredObjectVersion.php +++ b/src/Bundle/ChillDocStoreBundle/Entity/StoredObjectVersion.php @@ -23,10 +23,14 @@ use Random\RandomException; * Store each version of StoredObject's. * * A version should not be created manually: use the method @see{StoredObject::registerVersion} instead. + * + * Each filename must be unique within the same StoredObject. We add a condition on id to apply this condition only for + * newly created versions when this new index is applied. */ #[ORM\Entity] #[ORM\Table('chill_doc.stored_object_version')] #[ORM\UniqueConstraint(name: 'chill_doc_stored_object_version_unique_by_object', columns: ['stored_object_id', 'version'])] +#[ORM\UniqueConstraint(name: 'chill_doc_stored_object_version_unique_by_filename', columns: ['filename'], options: ['where' => '(id > 0)'])] class StoredObjectVersion implements TrackCreationInterface { use TrackCreationTrait; diff --git a/src/Bundle/ChillDocStoreBundle/migrations/Version20251013094414.php b/src/Bundle/ChillDocStoreBundle/migrations/Version20251013094414.php new file mode 100644 index 000000000..9195e2567 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/migrations/Version20251013094414.php @@ -0,0 +1,63 @@ +addSql(<<<'SQL' + WITH ranked AS ( + SELECT id, + rank() OVER ( + PARTITION BY stored_object_id, filename, "key"::jsonb, iv::jsonb + ORDER BY id DESC + ) AS rn + FROM chill_doc.stored_object_version + ) + DELETE FROM chill_doc.stored_object_version sov + USING ranked r + WHERE sov.id = r.id + AND r.rn > 1 + SQL); + + // 2) Create a partial unique index on filename that applies only to subsequently inserted rows. + // Per user's instruction, compute the cutoff using the stored_object_id sequence value. + $nextVal = (int) $this->connection->fetchOne("SELECT nextval('chill_doc.stored_object_version_id_seq')"); + + // Safety: if somehow sequence is not available, fallback to current max id from the table + if ($nextVal <= 0) { + $nextVal = (int) $this->connection->fetchOne('SELECT COALESCE(MAX(id), 0) FROM chill_doc.stored_object_version'); + } + + $this->addSql(sprintf( + 'CREATE UNIQUE INDEX chill_doc_stored_object_version_unique_by_filename ON chill_doc.stored_object_version (filename) WHERE id > %d', + $nextVal + )); + } + + public function down(Schema $schema): void + { + // Drop the partial unique index; data cleanup is irreversible. + $this->addSql('DROP INDEX IF EXISTS chill_doc_stored_object_version_unique_by_filename'); + } +}