diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index bc4b29336..77d679f74 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -73,6 +73,11 @@ define('DEL_SILENT', 2); */ define('DEL_MOVEUP', 3); +/** + * Do nothing at least automatically + */ +define('DEL_NONE', 4); + /** * For Link sets: tracking_level diff --git a/core/dbobject.class.php b/core/dbobject.class.php index e452f1815..18b67e770 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -5361,16 +5361,19 @@ abstract class DBObject implements iDisplay * @throws \MySQLException * @throws \MySQLHasGoneAwayException */ - protected function GetReferencingObjects($bAllowAllData = false) + protected function GetReferencingObjectsForDeletion($bAllowAllData = false) { $aDependentObjects = array(); $aRererencingMe = MetaModel::EnumReferencingClasses(get_class($this)); foreach($aRererencingMe as $sRemoteClass => $aExtKeys) { + /** @var \AttributeExternalKey $oExtKeyAttDef */ foreach($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef) { + // skip if external key doesn't require the deletion cascading + if($oExtKeyAttDef->GetDeletionPropagationOption() === DEL_NONE) continue; + // skip if this external key is behind an external field - /** @var \AttributeDefinition $oExtKeyAttDef */ if (!$oExtKeyAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) continue; $oSearch = new DBObjectSearch($sRemoteClass); @@ -5434,13 +5437,11 @@ abstract class DBObject implements iDisplay $this->CheckToWriteForTargetObjects(true); $oDeletionPlan->SetDeletionIssues($this, $this->m_aDeleteIssues, $this->m_bSecurityIssue); - $aDependentObjects = $this->GetReferencingObjects(true /* allow all data */); - // Getting and setting time limit are not symmetric: // www.php.net/manual/fr/function.set-time-limit.php#72305 $iPreviousTimeLimit = ini_get('max_execution_time'); - foreach ($aDependentObjects as $aPotentialDeletes) + foreach ($this->GetReferencingObjectsForDeletion(true /* allow all data */) as $aPotentialDeletes) { foreach ($aPotentialDeletes as $aData) { diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index 8d95d519a..c154153f3 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -2953,7 +2953,7 @@ CSS; if (($sAttType == 'AttributeExternalKey') || ($sAttType == 'AttributeHierarchicalKey')) { $sOnTargetDel = $oField->GetChildText('on_target_delete'); - if (($sOnTargetDel == 'DEL_AUTO') || ($sOnTargetDel == 'DEL_SILENT')) + if (($sOnTargetDel == 'DEL_AUTO') || ($sOnTargetDel == 'DEL_SILENT') || ($sOnTargetDel == 'DEL_NONE')) { $sTargetClass = $oField->GetChildText('target_class'); $aLinkToClasses[$oClass->getAttribute('id')][] = $sTargetClass; diff --git a/synchro/synchrodatasource.class.inc.php b/synchro/synchrodatasource.class.inc.php index d35e6c16c..f418b4eb9 100644 --- a/synchro/synchrodatasource.class.inc.php +++ b/synchro/synchrodatasource.class.inc.php @@ -1037,6 +1037,11 @@ EOF $sDropTable = "DROP TABLE IF EXISTS `$sTable`"; // Do not fail if the table is already deleted (corrupted database) CMDBSource::Query($sDropTable); // TO DO - check that triggers get dropped with the table + + $sSyncReplicaTable = MetaModel::DBGetTable(SynchroReplica::class); + $sSyncSourceIDColumn = MetaModel::GetAttributeDef(SynchroReplica::class, 'sync_source_id'); + $sSqlDeleteReplica = "DELETE FROM `$sSyncReplicaTable` WHERE {$sSyncSourceIDColumn->Get('sql')} = '{$this->GetKey()}'"; + CMDBSource::Query($sSqlDeleteReplica); } /** @@ -1991,7 +1996,7 @@ class SynchroReplica extends DBObject implements iDisplay 'allowed_values' => null, 'sql' => 'sync_source_id', 'is_null_allowed' => false, - 'on_target_delete' => DEL_SILENT, + 'on_target_delete' => DEL_NONE, 'depends_on' => array(), ))); MetaModel::Init_AddAttribute(new AttributeExternalField('base_class', @@ -2156,13 +2161,9 @@ class SynchroReplica extends DBObject implements iDisplay } // Overload the deletion -> the replica has been created by the mean of a trigger, - // it will be deleted by the mean of a trigger too + // it will be deleted by SynchroDataSource::AfterDelete protected function DBDeleteSingleObject() { - $oKPI = new ExecutionKPI(); - $this->OnDelete(); - $oKPI->ComputeStatsForExtension($this, 'OnDelete'); - if (!MetaModel::DBIsReadOnly()) { $oDataSource = MetaModel::GetObject('SynchroDataSource', $this->Get('sync_source_id'), false); @@ -2176,10 +2177,7 @@ class SynchroReplica extends DBObject implements iDisplay // else the whole datasource has probably been already deleted } - $this->AfterDelete(); - - $this->m_bIsInDB = false; - $this->m_iKey = null; + parent::DBDeleteSingleObject(); } public function SetLastError($sMessage, $oException = null) diff --git a/tests/php-unit-tests/unitary-tests/synchro/DataSynchroTest.php b/tests/php-unit-tests/unitary-tests/synchro/DataSynchroTest.php index d418e23c2..74c94f7fb 100644 --- a/tests/php-unit-tests/unitary-tests/synchro/DataSynchroTest.php +++ b/tests/php-unit-tests/unitary-tests/synchro/DataSynchroTest.php @@ -15,6 +15,7 @@ namespace Combodo\iTop\Test\UnitTest\Synchro; +use CMDBSource; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use DBObjectSearch; use DBObjectSet; @@ -363,6 +364,8 @@ class DataSynchroTest extends ItopDataTestCase } } } + + return $oDataSource; } private function GetNominalUsecaseData(){ @@ -423,6 +426,38 @@ class DataSynchroTest extends ItopDataTestCase $this->RunDataSynchroTest($aUserLoginUsecase); } + public function testDataSynchroDeletionCleanup(){ + + // run test data synchro + $oDataSource = $this->RunDataSynchroTest($this->GetNominalUsecaseData()); + + // QUERY to check data table for the new data source + $sDataSourceTable = $oDataSource->Get('database_table_name'); + $sShowTable = "SHOW TABLES LIKE '$sDataSourceTable'"; + + // AFTER CREATION TEST A: Verify that replicas have been created + $oObjects = new DBObjectSet(DBObjectSearch::FromOQL("SELECT SynchroReplica WHERE sync_source_id='{$oDataSource->GetKey()}'")); + $aCountReplicaBeforeDelete = count($oObjects->ToArray()); + $this->assertEquals(1, $aCountReplicaBeforeDelete); + // AFTER CREATION TEST B: Verify that data source table exist + $aResult = CMDBSource::Query($sShowTable); + $this->assertEquals(1, $aResult->num_rows, 'We must have a table in the database for the datasource'); + // AFTER CREATION TEST C: Verify that data source table have one row + $sCountRows = CMDBSource::QueryToScalar("SELECT count(*) FROM $sDataSourceTable"); + $this->assertEquals(1, $sCountRows, 'We must have a one row in datasource table'); + + // datasource deletion + $oDataSource->DBDelete(); + + // AFTER DELETION TEST A: Verify that replicas have been deleted + $oObjects = new DBObjectSet(DBObjectSearch::FromOQL("SELECT SynchroReplica WHERE sync_source_id='{$oDataSource->GetKey()}'")); + $aCountReplicaAfterDelete = count($oObjects->ToArray()); + $this->assertEquals(0, $aCountReplicaAfterDelete); + // AFTER DELETION TEST B: Verify that data source table have been dropped + $aResult = CMDBSource::Query($sShowTable); + $this->assertEquals(0, $aResult->num_rows, 'The database datasource table must have been deleted'); + } + /*public function testWithDeleteOption(){ $aUserLoginUsecase = array( 'desc' => 'Simple scenario with delete option (and extkey given as org/name)',