DB exception occur after detach and attach DID repeatedly
Motivation
----------
After detach, an entry will add into table contents_history. If one attach and detach it again, a same entry can not add into table because of primary key constrain. As a result, the DID can never be detached .

Modification
------------

diff --git a/lib/rucio/core/did.py b/lib/rucio/core/did.py
index 8ee148f60..38b00ddb8 100644
--- a/lib/rucio/core/did.py
+++ b/lib/rucio/core/did.py
@@ -707,23 +707,25 @@ def detach_dids(scope, name, dids, session=None):
         associ_did.delete(session=session)
 
         # Archive contents
-        models.DataIdentifierAssociationHistory(scope=associ_did.scope,
-                                                name=associ_did.name,
-                                                child_scope=associ_did.child_scope,
-                                                child_name=associ_did.child_name,
-                                                did_type=associ_did.did_type,
-                                                child_type=associ_did.child_type,
-                                                bytes=associ_did.bytes,
-                                                adler32=associ_did.adler32,
-                                                md5=associ_did.md5,
-                                                guid=associ_did.guid,
-                                                events=associ_did.events,
-                                                rule_evaluation=associ_did.rule_evaluation,
-                                                did_created_at=did.created_at,
-                                                created_at=associ_did.created_at,
-                                                updated_at=associ_did.updated_at,
-                                                deleted_at=datetime.utcnow()).\\
-            save(session=session, flush=False)
+        # If reattach happens, merge the latest due to primary key constraint
+        new_detach = models.DataIdentifierAssociationHistory(scope=associ_did.scope,
+                                                             name=associ_did.name,
+                                                             child_scope=associ_did.child_scope,
+                                                             child_name=associ_did.child_name,
+                                                             did_type=associ_did.did_type,
+                                                             child_type=associ_did.child_type,
+                                                             bytes=associ_did.bytes,
+                                                             adler32=associ_did.adler32,
+                                                             md5=associ_did.md5,
+                                                             guid=associ_did.guid,
+                                                             events=associ_did.events,
+                                                             rule_evaluation=associ_did.rule_evaluation,
+                                                             did_created_at=did.created_at,
+                                                             created_at=associ_did.created_at,
+                                                             updated_at=associ_did.updated_at,
+                                                             deleted_at=datetime.utcnow())
+        new_detach = session.merge(new_detach)
+        new_detach.save(session=session, flush=False)
 
         # Send message for AMI. To be removed in the future when they use the DETACH messages
         if did.did_type == DIDType.CONTAINER:
diff --git a/lib/rucio/tests/test_did.py b/lib/rucio/tests/test_did.py
index 49c594234..15065909b 100644
--- a/lib/rucio/tests/test_did.py
+++ b/lib/rucio/tests/test_did.py
@@ -15,7 +15,7 @@
 # Authors:
 # - Vincent Garonne <vgaronne@gmail.com>, 2013-2018
 # - Martin Barisits <martin.barisits@cern.ch>, 2013-2018
-# - Mario Lassnig <mario.lassnig@cern.ch>, 2013
+# - Mario Lassnig <mario.lassnig@cern.ch>, 2013-2018
 # - Cedric Serfon <cedric.serfon@cern.ch>, 2013-2015
 # - Ralph Vigne <ralph.vigne@cern.ch>, 2013
 # - Yun-Pin Sun <winter0128@gmail.com>, 2013
@@ -42,7 +42,7 @@ from rucio.common.exception import (DataIdentifierNotFound, DataIdentifierAlread
                                     UnsupportedStatus, ScopeNotFound)
 from rucio.common.utils import generate_uuid
 from rucio.core.account_limit import set_account_limit
-from rucio.core.did import (list_dids, add_did, delete_dids, get_did_atime, touch_dids, attach_dids,
+from rucio.core.did import (list_dids, add_did, delete_dids, get_did_atime, touch_dids, attach_dids, detach_dids,
                             get_metadata, set_metadata, get_did, get_did_access_cnt)
 from rucio.core.rse import get_rse_id
 from rucio.core.replica import add_replica
@@ -144,6 +144,23 @@ class TestDIDCore:
         assert_equal(get_did(scope=tmp_scope, name=tmp_dsn1, dynamic=True)['bytes'], 20)
         assert_equal(get_did(scope=tmp_scope, name=tmp_dsn4, dynamic=True)['bytes'], 20)
 
+    def test_reattach_dids(self):
+        """ DATA IDENTIFIERS (CORE): Repeatedly attach and detach DIDs """
+        tmp_scope = 'mock'
+        parent_name = 'parent_%s' % generate_uuid()
+        add_did(scope=tmp_scope, name=parent_name, type=DIDType.DATASET, account='root')
+
+        child_name = 'child_%s' % generate_uuid()
+        files = [{'scope': tmp_scope, 'name': child_name,
+                  'bytes': 12345, 'adler32': '0cc737eb'}]
+        attach_dids(scope=tmp_scope, name=parent_name, rse='MOCK', dids=files, account='root')
+
+        detach_dids(scope=tmp_scope, name=parent_name, dids=files)
+
+        attach_dids(scope=tmp_scope, name=parent_name, rse='MOCK', dids=files, account='root')
+
+        detach_dids(scope=tmp_scope, name=parent_name, dids=files)
+
 
 class TestDIDApi:
 
['lib/rucio/tests/test_did.py', 'lib/rucio/core/did.py']