From 6842bb53eddfd3b9b07d2cf630be693a4b0cdaea Mon Sep 17 00:00:00 2001
From: "Donald A. Lobo" <lobo@civicrm.org>
Date: Mon, 14 Oct 2013 10:07:45 -0700
Subject: [PATCH] CRM-11794 - shorten trigger names and add random string to
 ensure uniqness

----------------------------------------
* CRM-11794: Drop Trigger IF Exists fatals if custom table too long
  http://issues.civicrm.org/jira/browse/CRM-11794
---
 CRM/Core/BAO/CustomGroup.php       |  2 +-
 CRM/Core/DAO.php                   | 34 +++++++++++++++++++++-
 CRM/Logging/Schema.php             | 46 ++++++++++++++++--------------
 tests/phpunit/CRM/Core/DAOTest.php | 20 +++++++++++++
 4 files changed, 78 insertions(+), 24 deletions(-)

diff --git a/CRM/Core/BAO/CustomGroup.php b/CRM/Core/BAO/CustomGroup.php
index 83636eb6be..1922e6656d 100644
--- a/CRM/Core/BAO/CustomGroup.php
+++ b/CRM/Core/BAO/CustomGroup.php
@@ -166,7 +166,7 @@ class CRM_Core_BAO_CustomGroup extends CRM_Core_DAO_CustomGroup {
     $group->save();
     if (!isset($params['id'])) {
       if (!isset($params['table_name'])) {
-        $munged_title = strtolower(CRM_Utils_String::munge($group->title, '_', 32));
+        $munged_title = strtolower(CRM_Utils_String::munge($group->title, '_', 42));
         $tableName = "civicrm_value_{$munged_title}_{$group->id}";
       }
       $group->table_name = $tableName;
diff --git a/CRM/Core/DAO.php b/CRM/Core/DAO.php
index 3420ca100a..395552221f 100644
--- a/CRM/Core/DAO.php
+++ b/CRM/Core/DAO.php
@@ -1650,7 +1650,8 @@ SELECT contact_id
         foreach ($events as $whenName => $parts) {
           $varString   = implode("\n", $parts['variables']);
           $sqlString   = implode("\n", $parts['sql']);
-          $triggerName = "{$tableName}_{$whenName}_{$eventName}";
+          $validName   = CRM_Core_DAO::shortenSQLName($tableName, 48, TRUE);
+          $triggerName = "{$validName}_{$whenName}_{$eventName}";
           $triggerSQL  = "CREATE TRIGGER $triggerName $whenName $eventName ON $tableName FOR EACH ROW BEGIN $varString $sqlString END";
 
           CRM_Core_DAO::executeQuery("DROP TRIGGER IF EXISTS $triggerName");
@@ -1895,4 +1896,35 @@ EOS;
       }
     }
   }
+
+  /**
+   * SQL has a limit of 64 characters on various names:
+   * table name, trigger name, column name ...
+   *
+   * For custom groups and fields we generated names from user entered input
+   * which can be longer than this length, this function helps with creating
+   * strings that meet various criteria.
+   *
+   * @param string $string - the string to be shortened
+   * @param int    $length - the max length of the string
+   */
+  public static function shortenSQLName($string, $length = 60, $makeRandom = FALSE) {
+    // early return for strings that meet the requirements
+    if (strlen($string) <= $length) {
+      return $string;
+    }
+
+    // easy return for calls that dont need a randomized uniq string
+    if (! $makeRandom) {
+      return substr($string, 0, $length);
+    }
+
+    // the string is longer than the length and we need a uniq string
+    // for the same tablename we need the same uniq string everytime
+    // hence we use md5 on the string, which is not random
+    // we'll append 16 characters to the end of the tableName
+    $md5string = substr(md5($string), 0, 16);
+    return substr($string, 0, $length - 17) . "_{$md5string}";
+  }
+
 }
diff --git a/CRM/Logging/Schema.php b/CRM/Logging/Schema.php
index a43cc30340..bb7efcd098 100644
--- a/CRM/Logging/Schema.php
+++ b/CRM/Logging/Schema.php
@@ -46,11 +46,11 @@ class CRM_Logging_Schema {
     'logging/contribute/summary',
   );
 
-  //CRM-13028 / NYSS-6933 - table => array (cols) - to be excluded from the update statement 
+  //CRM-13028 / NYSS-6933 - table => array (cols) - to be excluded from the update statement
   private $exceptions = array(
     'civicrm_job'   => array('last_run'),
     'civicrm_group' => array('cache_date'),
-  ); 
+  );
 
   /**
    * Populate $this->tables and $this->logs with current db state.
@@ -153,15 +153,17 @@ AND    TABLE_NAME LIKE 'log_civicrm_%'
     }
 
     foreach ($tableNames as $table) {
+      $validName = CRM_Core_DAO::shortenSQLName($table, 48, TRUE);
+
       // before triggers
-      $dao->executeQuery("DROP TRIGGER IF EXISTS {$table}_before_insert");
-      $dao->executeQuery("DROP TRIGGER IF EXISTS {$table}_before_update");
-      $dao->executeQuery("DROP TRIGGER IF EXISTS {$table}_before_delete");
+      $dao->executeQuery("DROP TRIGGER IF EXISTS {$validName}_before_insert");
+      $dao->executeQuery("DROP TRIGGER IF EXISTS {$validName}_before_update");
+      $dao->executeQuery("DROP TRIGGER IF EXISTS {$validName}_before_delete");
 
      // after triggers
-      $dao->executeQuery("DROP TRIGGER IF EXISTS {$table}_after_insert");
-      $dao->executeQuery("DROP TRIGGER IF EXISTS {$table}_after_update");
-      $dao->executeQuery("DROP TRIGGER IF EXISTS {$table}_after_delete");
+      $dao->executeQuery("DROP TRIGGER IF EXISTS {$validName}_after_insert");
+      $dao->executeQuery("DROP TRIGGER IF EXISTS {$validName}_after_update");
+      $dao->executeQuery("DROP TRIGGER IF EXISTS {$validName}_after_delete");
     }
   }
 
@@ -356,7 +358,7 @@ AND    TABLE_NAME LIKE 'log_civicrm_%'
         $civiDB = $dao->_database;
       }
       CRM_Core_Error::ignoreException();
-      // NOTE: W.r.t Performance using one query to find all details and storing in static array is much faster 
+      // NOTE: W.r.t Performance using one query to find all details and storing in static array is much faster
       // than firing query for every given table.
       $query = "
 SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT
@@ -371,11 +373,11 @@ WHERE  table_schema IN ('{$this->db}', '{$civiDB}')";
         if (!array_key_exists($dao->TABLE_NAME, $columnSpecs)) {
           $columnSpecs[$dao->TABLE_NAME] = array();
         }
-        $columnSpecs[$dao->TABLE_NAME][$dao->COLUMN_NAME] = 
+        $columnSpecs[$dao->TABLE_NAME][$dao->COLUMN_NAME] =
           array(
-              'COLUMN_NAME' => $dao->COLUMN_NAME, 
-              'DATA_TYPE'   => $dao->DATA_TYPE, 
-              'IS_NULLABLE' => $dao->IS_NULLABLE, 
+              'COLUMN_NAME' => $dao->COLUMN_NAME,
+              'DATA_TYPE'   => $dao->DATA_TYPE,
+              'IS_NULLABLE' => $dao->IS_NULLABLE,
               'COLUMN_DEFAULT' => $dao->COLUMN_DEFAULT
             );
       }
@@ -386,38 +388,38 @@ WHERE  table_schema IN ('{$this->db}', '{$civiDB}')";
   function columnsWithDiffSpecs($civiTable, $logTable) {
     $civiTableSpecs = $this->columnSpecsOf($civiTable);
     $logTableSpecs  = $this->columnSpecsOf($logTable);
-    
+
     $diff = array('ADD' => array(), 'MODIFY' => array(), 'OBSOLETE' => array());
-    
+
     // columns to be added
     $diff['ADD'] = array_diff(array_keys($civiTableSpecs), array_keys($logTableSpecs));
-    
+
     // columns to be modified
-    // NOTE: we consider only those columns for modifications where there is a spec change, and that the column definition 
+    // NOTE: we consider only those columns for modifications where there is a spec change, and that the column definition
     // wasn't deliberately modified by fixTimeStampAndNotNullSQL() method.
     foreach ($civiTableSpecs as $col => $colSpecs) {
       $specDiff = array_diff($civiTableSpecs[$col], $logTableSpecs[$col]);
       if (!empty($specDiff) && $col != 'id' && !array_key_exists($col, $diff['ADD'])) {
         // ignore 'id' column for any spec changes, to avoid any auto-increment mysql errors
         if ($civiTableSpecs[$col]['DATA_TYPE'] != $logTableSpecs[$col]['DATA_TYPE']) {
-          // if data-type is different, surely consider the column 
+          // if data-type is different, surely consider the column
           $diff['MODIFY'][] = $col;
-        } else if ($civiTableSpecs[$col]['IS_NULLABLE'] != $logTableSpecs[$col]['IS_NULLABLE'] && 
+        } else if ($civiTableSpecs[$col]['IS_NULLABLE'] != $logTableSpecs[$col]['IS_NULLABLE'] &&
           $logTableSpecs[$col]['IS_NULLABLE'] == 'NO') {
           // if is-null property is different, and log table's column is NOT-NULL, surely consider the column
           $diff['MODIFY'][] = $col;
-        } else if ($civiTableSpecs[$col]['COLUMN_DEFAULT'] != $logTableSpecs[$col]['COLUMN_DEFAULT'] && 
+        } else if ($civiTableSpecs[$col]['COLUMN_DEFAULT'] != $logTableSpecs[$col]['COLUMN_DEFAULT'] &&
           !strstr($civiTableSpecs[$col]['COLUMN_DEFAULT'], 'TIMESTAMP')) {
           // if default property is different, and its not about a timestamp column, consider it
           $diff['MODIFY'][] = $col;
         }
-      } 
+      }
     }
 
     // columns to made obsolete by turning into not-null
     $oldCols = array_diff(array_keys($logTableSpecs), array_keys($civiTableSpecs));
     foreach ($oldCols as $col) {
-      if (!in_array($col, array('log_date', 'log_conn_id', 'log_user_id', 'log_action')) && 
+      if (!in_array($col, array('log_date', 'log_conn_id', 'log_user_id', 'log_action')) &&
         $logTableSpecs[$col]['IS_NULLABLE'] == 'NO') {
         // if its a column present only in log table, not among those used by log tables for special purpose, and not-null
         $diff['OBSOLETE'][] = $col;
diff --git a/tests/phpunit/CRM/Core/DAOTest.php b/tests/phpunit/CRM/Core/DAOTest.php
index 946de3836c..d6eedd98fd 100644
--- a/tests/phpunit/CRM/Core/DAOTest.php
+++ b/tests/phpunit/CRM/Core/DAOTest.php
@@ -159,4 +159,24 @@ class CRM_Core_DAOTest extends CiviUnitTestCase {
     $actualSql = CRM_Core_DAO::composeQuery($inputSql, $inputParams);
     $this->assertFalse(($expectSql == $actualSql));
   }
+
+  function sqlNameDataProvider() {
+    return array(
+      array('this is a long string', 30, FALSE, 'this is a long string'),
+
+      array('this is an even longer string which is exactly 60 character', 60, FALSE, 'this is an even longer string which is exactly 60 character'),
+      array('this is an even longer string which is exactly 60 character', 60, TRUE , 'this is an even longer string which is exactly 60 character'),
+
+      array('this is an even longer string which is a bit more than 60 character', 60, FALSE, 'this is an even longer string which is a bit more than 60 ch'),
+      array('this is an even longer string which is a bit more than 60 character', 60, TRUE , 'this is an even longer string which is a bi_c1cbd5198187eb96'),
+    );
+  }
+
+  /**
+   * @dataProvider sqlNameDataProvider
+   */
+  function testShortenSQLName($inputData, $length, $makeRandom, $expectedResult) {
+    $this->assertEquals($expectedResult, CRM_Core_DAO::shortenSQLName($inputData, $length, $makeRandom));
+  }
+
 }
-- 
GitLab