{"record":{"id":"ff5e784363e6b87b","repo":"halo-dev/halo","slug":"duplicate-key-for-index","errorCode":null,"errorMessage":"Duplicate key '{}' for index '{}'","messagePattern":"Duplicate key '(.+?)' for index '(.+?)'","errorType":"validation","errorClass":"DuplicateKeyException","httpStatus":null,"severity":"error","filePath":"application/src/main/java/run/halo/app/extension/index/SingleValueIndex.java","lineNumber":281,"sourceCode":"        private boolean committed;\n\n        UpsertTransactionalOperation(String primaryKey, @Nullable K newKey) {\n            this.primaryKey = primaryKey;\n            this.newKey = newKey;\n        }\n\n        @Override\n        public void prepare() {\n            // preflight checks\n            if (!spec.isNullable() && newKey == null) {\n                throw new IllegalArgumentException(\"Index %s of %s is not nullable\".formatted(getName(), primaryKey));\n            }\n            previousKey = invertedIndex.get(primaryKey);\n            previousNull = nullKeyValues.contains(primaryKey);\n            if (isUnique() && newKey != null && !Objects.equals(previousKey, newKey)) {\n                var existingPrimaryKeys = index.get(newKey);\n                if (!CollectionUtils.isEmpty(existingPrimaryKeys)) {\n                    throw new DuplicateKeyException(\"Duplicate key '\" + newKey + \"' for index '\" + getName() + \"'\");\n                }\n            }\n        }\n\n        @Override\n        public void commit() {\n            if (committed) {\n                return;\n            }\n            committed = true;\n            removeKey(primaryKey, previousKey);\n            addKey(primaryKey, newKey);\n        }\n\n        @Override\n        public void rollback() {\n            if (!committed) {\n                return;","sourceCodeStart":263,"sourceCodeEnd":299,"githubUrl":"https://github.com/halo-dev/halo/blob/d2f5165f9c8f055ffcb3fa9c3f4032821a7b68c8/application/src/main/java/run/halo/app/extension/index/SingleValueIndex.java#L263-L299","documentation":"Thrown as org.springframework.dao.DuplicateKeyException in the prepare() preflight of SingleValueIndex.UpsertTransactionalOperation. Before any mutation, it checks whether the index is unique, the new key differs from the row's previous key, and the new key already maps to other primary keys. If so it aborts the transaction before commit, so no partial state is written.","triggerScenarios":"Updating a single-value unique index to a new value that another resource already owns: isUnique()==true, newKey != null, !Objects.equals(previousKey, newKey), and index.get(newKey) returns a non-empty primary-key set.","commonSituations":"Renaming a uniquely-indexed field (e.g. username, slug) to a value already in use; bulk re-import that assigns colliding keys; concurrent edits where two requests target the same free value.","solutions":["Resolve the collision: choose another value, or delete/update the owner of the existing key first.","Guard the update with a pre-check via the indexer/query API for the candidate key before attempting the write.","Verify the extension's index schema really should be unique; if not, relax it at the schema level.","Retry with exponential backoff only if the collision is from a concurrent transient state you intend to clear."],"exampleFix":"// before\nextensionClient.update(resource).block(); // newKey collides\n\n// after\nboolean taken = queryIndex.equal(\"fieldName\", candidateValue).stream().anyMatch(id -> !id.equals(resource.getMetadata().getName()));\nif (taken) {\n    throw new IllegalStateException(\"Value already in use: \" + candidateValue);\n}\nextensionClient.update(resource).block();","handlingStrategy":"validation","validationCode":"// Preflight: is the new single-value key free for someone other than self?\nSet<String> existing = valueIndexQuery.equal(\"<field>\", newKey);\nboolean collision = !existing.isEmpty() && !existing.equals(Set.of(selfPrimaryKey));\nif (collision) throw new IllegalStateException(\"Key taken: \" + newKey);","typeGuard":null,"tryCatchPattern":"try {\n    indexer.update(...); // goes through prepare()->commit()\n} catch (DuplicateKeyException e) {\n    throw new ConflictException(\"Rename target already in use\", e);\n}","preventionTips":["Treat prepare() DuplicateKeyException as recoverable: re-read and prompt for a new value.","Avoid renaming uniquely-indexed fields in bulk without per-row collision checks.","Keep the prepare/commit transaction boundary intact; don't bypass it."],"tags":["index","unique-constraint","extension-store","preflight"],"backgroundTag":null,"analyzedSha":"d2f5165f9c8f055ffcb3fa9c3f4032821a7b68c8","analyzedAt":"2026-08-14T00:18:38.915Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}