improves Tinebase ID detection for install_dump
[tine20] / tine20 / Setup / Backend / Mysql.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Setup
6  * @subpackage  Backend
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Lars Kneschke <l.kneschke@metaways.de>
9  * @copyright   Copyright (c) 2008-2017 Metaways Infosystems GmbH (http://www.metaways.de)
10  *
11  */
12
13 /**
14  * setup backend class for MySQL 5.0 +
15  *
16  * @package     Setup
17  * @subpackage  Backend
18  */
19 class Setup_Backend_Mysql extends Setup_Backend_Abstract
20 {
21     /**
22      * Define how database agnostic data types get mapped to mysql data types
23      * 
24      * @var array
25      */
26     protected $_typeMappings = array(
27         'integer' => array(
28             'lengthTypes' => array(
29                 4 => 'tinyint',
30                 19 => 'int',
31                 64 => 'bigint'),
32             'defaultType' => 'int',
33             'defaultLength' => self::INTEGER_DEFAULT_LENGTH),
34         'boolean' => array(
35             'defaultType' => 'tinyint',
36             'defaultLength' => 1),
37         'text' => array(
38             'lengthTypes' => array(
39                 255 => 'varchar',
40                 65535 => 'text',
41                 16777215 => 'mediumtext',
42                 2147483647 => 'longtext'),
43             'defaultType' => 'text',
44             'defaultLength' => null,
45             'lengthLessTypes' => array(
46                 'mediumtext',
47                 'longtext'
48             )
49         ),
50         'float' => array(
51             'defaultType' => 'double'),
52         'decimal' => array(
53             'lengthTypes' => array(
54                 65 => 'decimal'),
55             'defaultType' => 'decimal',
56             'defaultScale' => '0'),
57         'datetime' => array(
58             'defaultType' => 'datetime'),
59         'time' => array(
60             'defaultType' => 'time'),
61         'date' => array(
62             'defaultType' => 'date'),
63         'blob' => array(
64             'defaultType' => 'longblob'),
65         'clob' => array(
66             'defaultType' => 'longtext'),
67         'enum' => array(
68             'defaultType' => 'enum')
69     );
70  
71     /**
72      * get create table statement
73      * 
74      * @param Setup_Backend_Schema_Table_Abstract $_table
75      * @return string
76      */
77     public function getCreateStatement(Setup_Backend_Schema_Table_Abstract  $_table)
78     {
79         $statement = "CREATE TABLE IF NOT EXISTS `" . SQL_TABLE_PREFIX . $_table->name . "` (\n";
80         $statementSnippets = array();
81      
82         foreach ($_table->fields as $field) {
83             if (isset($field->name)) {
84                $statementSnippets[] = $this->getFieldDeclarations($field);
85             }
86         }
87
88         foreach ($_table->indices as $index) {
89             if ($index->foreign) {
90                $statementSnippets[] = $this->getForeignKeyDeclarations($index);
91             } else {
92                $statementSnippets[] = $this->getIndexDeclarations($index);
93             }
94         }
95
96         $statement .= implode(",\n", array_filter($statementSnippets)) . "\n)";
97
98         if (isset($_table->engine)) {
99             $statement .= " ENGINE=" . $_table->engine . " DEFAULT CHARSET=" . $_table->charset;
100         } else {
101             $statement .= " ENGINE=InnoDB DEFAULT CHARSET=utf8 ";
102         }
103
104         if (isset($_table->comment)) {
105             $statement .= " COMMENT='" . $_table->comment . "'";
106         }
107
108         if (Setup_Core::isLogLevel(Zend_Log::TRACE)) Setup_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $statement);
109         
110         return $statement;
111     }
112
113     /**
114      * return list of all foreign key names for given table
115      *
116      * @param string $tableName
117      * @return array list of foreignkey names
118      */
119     public function getExistingForeignKeys($tableName)
120     {
121         $select = $this->_db->select()
122             ->from(array('table_constraints' => 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS'), array('TABLE_NAME', 'CONSTRAINT_NAME'))
123             ->join(
124                 array('key_column_usage' => 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE'), 
125                 $this->_db->quoteIdentifier('table_constraints.CONSTRAINT_NAME') . '=' . $this->_db->quoteIdentifier('key_column_usage.CONSTRAINT_NAME'),
126                 array()
127             )
128             ->where($this->_db->quoteIdentifier('table_constraints.CONSTRAINT_SCHEMA')    . ' = ?', $this->_config->database->dbname)
129             ->where($this->_db->quoteIdentifier('table_constraints.TABLE_SCHEMA')         . ' = ?', $this->_config->database->dbname)
130             ->where($this->_db->quoteIdentifier('key_column_usage.TABLE_SCHEMA')          . ' = ?', $this->_config->database->dbname)
131             ->where($this->_db->quoteIdentifier('table_constraints.CONSTRAINT_TYPE')      . ' = ?', 'FOREIGN KEY')
132             ->where($this->_db->quoteIdentifier('key_column_usage.REFERENCED_TABLE_NAME') . ' = ?', SQL_TABLE_PREFIX . $tableName);
133
134         $foreignKeyNames = array();
135
136         $stmt = $select->query();
137         while ($row = $stmt->fetch()) {
138             $foreignKeyNames[$row['CONSTRAINT_NAME']] = array(
139                 'table_name'      => preg_replace('/' . SQL_TABLE_PREFIX . '/', '', $row['TABLE_NAME']),
140                 'constraint_name' => preg_replace('/' . SQL_TABLE_PREFIX. '/', '', $row['CONSTRAINT_NAME']));
141         }
142         
143         return $foreignKeyNames;
144     }
145     
146     /**
147      * Get schema of existing table
148      * 
149      * @param String $_tableName
150      * 
151      * @return Setup_Backend_Schema_Table_Mysql
152      */
153     public function getExistingSchema($_tableName)
154     {
155         // Get common table information
156         $select = $this->_db->select()
157             ->from('information_schema.tables')
158             ->where($this->_db->quoteIdentifier('TABLE_SCHEMA') . ' = ?', $this->_config->database->dbname)
159             ->where($this->_db->quoteIdentifier('TABLE_NAME') . ' = ?',  SQL_TABLE_PREFIX . $_tableName);
160           
161           
162         $stmt = $select->query();
163         $tableInfo = $stmt->fetchObject();
164         
165         //$existingTable = new Setup_Backend_Schema_Table($tableInfo);
166         $existingTable = Setup_Backend_Schema_Table_Factory::factory('Mysql', $tableInfo);
167        // get field informations
168         $select = $this->_db->select()
169             ->from('information_schema.COLUMNS')
170             ->where($this->_db->quoteIdentifier('TABLE_NAME') . ' = ?', SQL_TABLE_PREFIX .  $_tableName);
171
172         $stmt = $select->query();
173         $tableColumns = $stmt->fetchAll();
174
175         foreach ($tableColumns as $tableColumn) {
176             $field = Setup_Backend_Schema_Field_Factory::factory('Mysql', $tableColumn);
177             $existingTable->addField($field);
178             
179             if ($field->primary === 'true' || $field->unique === 'true' || $field->mul === 'true') {
180                 $index = Setup_Backend_Schema_Index_Factory::factory('Mysql', $tableColumn);
181                         
182                 // get foreign keys
183                 $select = $this->_db->select()
184                     ->from('information_schema.KEY_COLUMN_USAGE')
185                     ->where($this->_db->quoteIdentifier('TABLE_NAME') . ' = ?', SQL_TABLE_PREFIX .  $_tableName)
186                     ->where($this->_db->quoteIdentifier('COLUMN_NAME') . ' = ?', $tableColumn['COLUMN_NAME']);
187
188                 $stmt = $select->query();
189                 $keyUsage = $stmt->fetchAll();
190
191                 foreach ($keyUsage as $keyUse) {
192                     if ($keyUse['REFERENCED_TABLE_NAME'] != NULL) {
193                         $index->setForeignKey($keyUse);
194                     }
195                 }
196                 $existingTable->addIndex($index);
197             }
198         }
199         
200         return $existingTable;
201     }
202
203     /**
204      * add column/field to database table
205      * 
206      * @param string $_tableName
207      * @param Setup_Backend_Schema_Field_Abstract $_declaration
208      * @param int $_position of future column
209      */    
210     public function addCol($_tableName, Setup_Backend_Schema_Field_Abstract $_declaration, $_position = NULL)
211     {
212         $this->execQueryVoid($this->addAddCol(null, $_tableName, $_declaration, $_position));
213     }
214
215     /**
216      * add column/field to database table
217      *
218      * @param string $_query
219      * @param string $_tableName
220      * @param Setup_Backend_Schema_Field_Abstract $_declaration
221      * @param int $_position of future column
222      * @return string
223      */
224     public function addAddCol($_query, $_tableName, Setup_Backend_Schema_Field_Abstract $_declaration, $_position = NULL)
225     {
226         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
227             . ' Add new column to table ' . $_tableName);
228
229         if (empty($_query)) {
230             $_query = "ALTER TABLE `" . SQL_TABLE_PREFIX . $_tableName . "`";
231         } else {
232             $_query .= ',';
233         }
234
235         $_query .= " ADD COLUMN " . $this->getFieldDeclarations($_declaration);
236
237         if ($_position !== NULL) {
238             if ($_position == 0) {
239                 $_query .= ' FIRST ';
240             } else {
241                 $before = $this->execQuery('DESCRIBE `' . SQL_TABLE_PREFIX . $_tableName . '` ');
242                 $_query .= ' AFTER `' . $before[$_position]['Field'] . '`';
243             }
244         }
245
246         return $_query;
247     }
248     
249     /**
250      * rename or redefines column/field in database table
251      * 
252      * @param string $_tableName
253      * @param Setup_Backend_Schema_Field_Abstract $_declaration
254      * @param string $_oldName column/field name
255      */    
256     public function alterCol($_tableName, Setup_Backend_Schema_Field_Abstract $_declaration, $_oldName = NULL)
257     {
258         $this->execQueryVoid($this->addAlterCol(null, $_tableName, $_declaration, $_oldName));
259     }
260
261     /**
262      * rename or redefines column/field in database table
263      *
264      * @param string $_query
265      * @param string $_tableName
266      * @param Setup_Backend_Schema_Field_Abstract $_declaration
267      * @param string $_oldName column/field name
268      * @return string
269      */
270     public function addAlterCol($_query, $_tableName, Setup_Backend_Schema_Field_Abstract $_declaration, $_oldName = NULL)
271     {
272         if (empty($_query)) {
273             $_query = "ALTER TABLE `" . SQL_TABLE_PREFIX . $_tableName . "`";
274         } else {
275             $_query .= ',';
276         }
277
278         $_query .= " CHANGE COLUMN " ;
279
280         if ($_oldName === NULL) {
281             $oldName = $_declaration->name;
282         } else {
283             $oldName = $_oldName;
284         }
285
286         $_query .= " `" . $oldName .  "` " . $this->getFieldDeclarations($_declaration);
287
288         return $_query;
289     }
290  
291     /**
292      * add a key to database table
293      * 
294      * @param string $_tableName
295      * @param Setup_Backend_Schema_Index_Abstract $_declaration
296      */     
297     public function addIndex($_tableName ,  Setup_Backend_Schema_Index_Abstract $_declaration)
298     {
299         $this->execQueryVoid($this->addAddIndex(null, $_tableName, $_declaration));
300     }
301
302     /**
303      * add a key to database table
304      *
305      * @param string $_query
306      * @param string $_tableName
307      * @param Setup_Backend_Schema_Index_Abstract $_declaration
308      * @return string
309      */
310     public function addAddIndex($_query, $_tableName ,  Setup_Backend_Schema_Index_Abstract $_declaration)
311     {
312         if (empty($indexDeclaration = $this->getIndexDeclarations($_declaration))) {
313             return $_query;
314         }
315
316         if (empty($_query)) {
317             $_query = "ALTER TABLE `" . SQL_TABLE_PREFIX . $_tableName . "`";
318         } else {
319             $_query .= ',';
320         }
321
322         $_query .= " ADD " . $indexDeclaration;
323
324         return $_query;
325     }
326
327     /**
328      * create the right mysql-statement-snippet for keys
329      *
330      * @param   Setup_Backend_Schema_Index_Abstract $_key
331      * @param String $_tableName [is not used in this Backend (MySQL)]
332      * @return  string
333      * @throws  Setup_Exception_NotFound
334      */
335     public function getIndexDeclarations(Setup_Backend_Schema_Index_Abstract $_key, $_tableName = '')
336     {
337         $keys = array();
338
339         $snippet = "  KEY `" . $_key->name . "`";
340         if (!empty($_key->primary)) {
341             $snippet = '  PRIMARY KEY ';
342         } elseif (!empty($_key->unique)) {
343             $snippet = "  UNIQUE KEY `" . $_key->name . "`" ;
344         } elseif (!empty($_key->fulltext)) {
345             if (!$this->supports('mysql >= 5.6.4')) {
346                 if (Setup_Core::isLogLevel(Zend_Log::WARN)) Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
347                     ' full text search is only supported on mysql/mariadb 5.6.4+ ... do yourself a favor and migrate. You need to add the missing full text indicies yourself manually now after migrating. Skipping creation of full text index!');
348                 return '';
349             }
350             $snippet = " FULLTEXT KEY `" . $_key->name . "`" ;
351         }
352         
353         foreach ((array)$_key->field as $keyfield) {
354             $key = '`' . (string)$keyfield . '`';
355             if ($_key->length !== NULL) {
356                 $key .= ' (' . $_key->length . ')';
357             }
358             else if ((isset($_key->fieldLength[(string)$keyfield]) || array_key_exists((string)$keyfield, $_key->fieldLength))) {
359                 $key .= ' (' . $_key->fieldLength[(string)$keyfield] . ')';
360             }
361             $keys[] = $key;
362         }
363
364         if (empty($keys)) {
365             throw new Setup_Exception_NotFound('no keys for index found');
366         }
367
368         $snippet .= ' (' . implode(",", $keys) . ')';
369         
370         return $snippet;
371     }
372
373     /**
374      *  create the right mysql-statement-snippet for foreign keys
375      *
376      * @param Setup_Backend_Schema_Index_Abstract $_key the xml index definition
377      * @return string
378      */
379     public function getForeignKeyDeclarations(Setup_Backend_Schema_Index_Abstract $_key)
380     {
381         $snippet = '  CONSTRAINT `' . SQL_TABLE_PREFIX . $_key->name . '` FOREIGN KEY ';
382         $snippet .= '(`' . $_key->field . "`) REFERENCES `" . SQL_TABLE_PREFIX
383                     . $_key->referenceTable . 
384                     "` (`" . $_key->referenceField . "`)";
385
386         if (!empty($_key->referenceOnDelete)) {
387             $snippet .= " ON DELETE " . strtoupper($_key->referenceOnDelete);
388         }
389         if (!empty($_key->referenceOnUpdate)) {
390             $snippet .= " ON UPDATE " . strtoupper($_key->referenceOnUpdate);
391         }
392
393         return $snippet;
394     }
395     
396     /**
397      * enable/disabled foreign key checks
398      *
399      * @param integer|string|boolean $_value
400      */
401     public function setForeignKeyChecks($_value)
402     {
403         if ($_value == 0 || $_value == 1) {
404             $this->_db->query("SET FOREIGN_KEY_CHECKS=" . $_value);
405         }
406     }
407
408     /**
409      * Backup Database
410      *
411      * @param $option
412      */
413     public function backup($option)
414     {
415         $backupDir = $option['backupDir'];
416
417         // hide password from shell via my.cnf
418         $mycnf = $backupDir . '/my.cnf';
419         $this->_createMyConf($mycnf, $this->_config->database);
420
421         $ignoreTables = '';
422         if (count($option['structTables']) > 0) {
423             $structDump = 'mysqldump --defaults-extra-file=' . $mycnf . ' --no-data ' .
424                 escapeshellarg($this->_config->database->dbname);
425             foreach($option['structTables'] as $table) {
426                 $structDump .= ' ' . escapeshellarg($table);
427                 $ignoreTables .= '--ignore-table=' . escapeshellarg($this->_config->database->dbname . '.' . $table) . ' ';
428             }
429         } else {
430             $structDump = false;
431         }
432
433         $cmd = ($structDump!==false?'{ ':'')
434               ."mysqldump --defaults-extra-file=$mycnf "
435               .$ignoreTables
436               ."--single-transaction "
437               ."--opt "
438               . escapeshellarg($this->_config->database->dbname)
439               . ($structDump!==false?'; ' . $structDump . '; }':'')
440               ." | bzip2 > $backupDir/tine20_mysql.sql.bz2";
441
442         exec($cmd);
443         unlink($mycnf);
444     }
445
446     /**
447      * Restore Database
448      *
449      * @param $backupDir
450      * @throws Exception
451      */
452     public function restore($backupDir)
453     {
454         $mysqlBackupFile = $backupDir . '/tine20_mysql.sql.bz2';
455         if (! file_exists($mysqlBackupFile)) {
456             throw new Exception("$mysqlBackupFile not found");
457         }
458
459         // hide password from shell via my.cnf
460         $mycnf = $backupDir . '/my.cnf';
461         $this->_createMyConf($mycnf, $this->_config->database);
462
463         $cmd = "bzcat $mysqlBackupFile"
464              . " | mysql --defaults-extra-file=$mycnf "
465              . escapeshellarg($this->_config->database->dbname);
466
467         exec($cmd);
468         unlink($mycnf);
469     }
470
471     /**
472      * create my.cnf
473      *
474      * @param $path
475      * @param $config
476      */
477     protected function _createMyConf($path, $config)
478     {
479         $port = $config->port ? $config->port : 3306;
480
481         $mycnfData = <<<EOT
482 [client]
483 host = {$config->host}
484 port = {$port}
485 user = {$config->username}
486 password = {$config->password}
487 EOT;
488         file_put_contents($path, $mycnfData);
489     }
490
491     /**
492      * checks whether this backend supports a specific requirement or not
493      *
494      * @param $requirement
495      * @return bool
496      */
497     public function supports($requirement)
498     {
499         if (preg_match('/^mysql ([<>=]+) ([\d\.]+)$/', $requirement, $m))
500         {
501             $version = $this->_db->getServerVersion();
502             if (version_compare($version, $m[2], $m[1]) === true) {
503                 return true;
504             }
505         }
506         return false;
507     }
508 }