0011336: support backup and restore via cli
[tine20] / tine20 / Setup / Backend / Abstract.php
1 <?php
2 /**
3  * Tine 2.0 - http://www.tine20.org
4  * 
5  * @package     Setup
6  * @subpackage  Backend
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Matthias Greiling <m.greiling@metaways.de>
9  * @copyright   Copyright (c) 2007-2012 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * interface for backend class
14  * 
15  * @package     Setup
16  * @subpackage  Backend
17  */
18 abstract class Setup_Backend_Abstract implements Setup_Backend_Interface
19 {
20     /**
21      * Maximum length of table-, index-, contraint- and field names.
22      * 
23      * @var integer
24      */
25     const MAX_NAME_LENGTH = 30;
26     
27     /**
28      * default length of integer fields
29      * 
30      * @var integer
31      */
32     const INTEGER_DEFAULT_LENGTH = 11;
33
34     /**
35      * Define how database agnostic data types get mapped to database sepcific data types
36      * 
37      * @var array
38      */
39     protected $_typeMappings = array();
40  
41     /**
42      * @var Zend_Db_Adapter_Abstract
43      */
44     protected $_db = NULL;
45     
46     /**
47      * config object
48      *
49      * @var Zend_Config
50      */
51     protected $_config = NULL;
52     
53     /**
54      * Return the mapping from the given database-agnostic data {@param $_type} to the
55      * corresponding database specific data type
56      * 
57      * @param String $_type
58      * @return array | null
59      */
60     public function getTypeMapping($_type)
61     {
62         if ((isset($this->_typeMappings[$_type]) || array_key_exists($_type, $this->_typeMappings))) {
63             return $this->_typeMappings[$_type];
64         }
65         return null;
66     }
67     
68     /**
69      * constructor
70      *
71      */
72     public function __construct()
73     {
74         $this->_config = Tinebase_Core::getConfig();
75         $this->_db = Tinebase_Core::getDb();
76     }
77     
78     /**
79      * get db adapter
80      * 
81      * @return Zend_Db_Adapter_Abstract
82      */
83     public function getDb()
84     {
85         return $this->_db;
86     }
87     
88     /**
89      * checks if application is installed at all
90      *
91      * @param unknown_type $_application
92      * @return boolean
93      */
94     public function applicationExists($_application)
95     {
96         if ($this->tableExists('applications')) {
97             if ($this->applicationVersionQuery($_application) != false) {
98                 return true;
99             }
100         }
101         
102         return false;
103     }
104     
105     /**
106      * check's a given database table version 
107      *
108      * @param string $_tableName
109      * @return boolean|string "version" if the table exists, otherwise false
110      */
111     public function tableVersionQuery($_tableName)
112     {
113         $select = $this->_db->select()
114             ->from(SQL_TABLE_PREFIX . 'application_tables')
115             ->where($this->_db->quoteIdentifier('name') . ' = ?', SQL_TABLE_PREFIX . $_tableName)
116             ->orwhere($this->_db->quoteIdentifier('name') . ' = ?', $_tableName);
117
118         $stmt = $select->query();
119         $version = $stmt->fetchAll();
120         
121         return (! empty($version)) ? $version[0]['version'] : FALSE;
122     }
123     
124     /**
125      * truncate table in database
126      * 
127      * @param string tableName
128      */
129     public function truncateTable($_tableName)
130     {
131         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Truncate table ' . $_tableName);
132         $statement = "TRUNCATE TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName);
133         $this->execQueryVoid($statement);
134     }
135     
136     /**
137      * check's a given application version
138      *
139      * @param string $_application
140      * @return boolean return string "version" if the table exists, otherwise false
141      */
142     public function applicationVersionQuery($_application)
143     {
144         $select = $this->_db->select()
145             ->from( SQL_TABLE_PREFIX . 'applications')
146             ->where($this->_db->quoteIdentifier('name') . ' = ?', $_application);
147
148         $stmt = $select->query();
149         $version = $stmt->fetchAll();
150         
151         if (empty($version)) {
152             return false;
153         } else {
154             return $version[0]['version'];
155         }
156     }
157     
158     /**
159      * execute insert statement for default values (records)
160      * handles some special fields, which can't contain static values
161      * 
162      * @param   SimpleXMLElement $_record
163      * @throws  Setup_Exception
164      */
165     public function execInsertStatement(SimpleXMLElement $_record)
166     {
167         $data = array();
168         
169         foreach ($_record->field as $field) {
170             if (isset($field->value['special'])) {
171                 switch(strtolower($field->value['special'])) {
172                     case 'now':
173                         $value = Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG);
174                         break;
175                     
176                     case 'account_id':
177                         break;
178                     
179                     case 'application_id':
180                         $application = Tinebase_Application::getInstance()->getApplicationByName((string) $field->value);
181                         $value = $application->id;
182                         break;
183                     
184                     case 'uid':
185                         $value = Tinebase_Record_Abstract::generateUID();
186                         break;
187                         
188                     default:
189                         throw new Setup_Exception('Unsupported special type ' . strtolower($field->value['special']));
190                     }
191             } else {
192                 $value = $field->value;
193             }
194             // buffer for insert statement
195             $data[(string)$field->name] = (string)$value;
196         }
197         
198         #$table = new Tinebase_Db_Table(array(
199         #   'name' => SQL_TABLE_PREFIX . $_record->table->name
200         #));
201
202         #// final insert process
203         #$table->insert($data);
204         
205         #var_dump($data);
206         #var_dump(SQL_TABLE_PREFIX . $_record->table->name);
207         $this->_db->insert(SQL_TABLE_PREFIX . $_record->table->name, $data);
208     }
209
210     /**
211      * execute statement without return values
212      * 
213      * @param string statement
214      */    
215     public function execQueryVoid($_statement, $bind = array())
216     {
217         $stmt = $this->_db->query($_statement, $bind);
218     }
219     
220     /**
221      * execute statement  return values
222      * 
223      * @param string statement
224      * @return stdClass object
225      */
226     public function execQuery($_statement, $bind = array())
227     {
228         $stmt = $this->_db->query($_statement, $bind);
229         
230         return $stmt->fetchAll();
231     }
232     
233     /**
234      * checks if a given table exists
235      *
236      * @param string $_tableSchema
237      * @param string $_tableName
238      * @return boolean return true if the table exists, otherwise false
239      */
240     public function tableExists($_tableName)
241     {
242         $tableName = SQL_TABLE_PREFIX . $_tableName;
243         try {
244             $tableInfo = $this->_db->describeTable($tableName);
245         } catch (Zend_Db_Statement_Exception $e) {
246             $tableInfo = null;
247         }
248         return !empty($tableInfo);
249     }
250     
251     /**
252      * takes the xml stream and creates a table
253      *
254      * @param object $_table xml stream
255      * @param string $_appName if appname and tablename are given, we create an entry in the application table
256      * @param string $_tableName
257      */
258     public function createTable(Setup_Backend_Schema_Table_Abstract $_table, $_appName = NULL, $_tableName = NULL)
259     {
260         $statement = $this->getCreateStatement($_table);
261         $this->execQueryVoid($statement);
262         
263         if ($_appName !== NULL && $_tableName !== NULL) {
264             Tinebase_Application::getInstance()->addApplicationTable(
265                 Tinebase_Application::getInstance()->getApplicationByName($_appName), 
266                 $_tableName, 
267                 1
268             );
269         }
270     }
271     
272     /**
273      * removes table from database (and from application table if app id is given
274      * 
275      * @param string $_tableName
276      * @param string $_applicationId
277      */
278     public function dropTable($_tableName, $_applicationId = NULL)
279     {
280         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Dropping table ' . $_tableName);
281         $statement = "DROP TABLE IF EXISTS " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName);
282         $this->execQueryVoid($statement);
283         
284         if ($_applicationId !== NULL) {
285             Tinebase_Application::getInstance()->removeApplicationTable($_applicationId, $_tableName);
286         }
287     }
288     
289     /**
290      * renames table in database
291      * 
292      * @param string tableName
293      */
294     public function renameTable($_tableName, $_newName)
295     {
296         $statement = 'ALTER TABLE ' . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . ' RENAME TO ' . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_newName);
297         $this->execQueryVoid($statement);
298     }
299     
300     /**
301      * checks if a given column {@param $_columnName} exists in table {@param $_tableName}.
302      *
303      * @param string $_columnName
304      * @param string $_tableName
305      * @return boolean
306      */
307     public function columnExists($_columnName, $_tableName)
308     {
309         $columns = Tinebase_Db_Table::getTableDescriptionFromCache(SQL_TABLE_PREFIX . $_tableName, $this->_db); 
310         return (isset($columns[$_columnName]) || array_key_exists($_columnName, $columns));
311     }
312     
313     /**
314      * drop column/field in database table
315      * 
316      * @param string tableName
317      * @param string column/field name 
318      */    
319     public function dropCol($_tableName, $_colName)
320     {
321         $statement = 'ALTER TABLE ' . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . ' DROP COLUMN ' . $this->_db->quoteIdentifier($_colName);
322         $this->execQueryVoid($statement);
323     }
324     
325     /**
326      * add a primary key to database table
327      * 
328      * Delegates to {@see addPrimaryKey()}
329      * 
330      * @param string tableName 
331      * @param Setup_Backend_Schema_Index_Abstract declaration
332      */
333     public function addPrimaryKey($_tableName, Setup_Backend_Schema_Index_Abstract $_declaration)
334     {
335         $this->addIndex($_tableName, $_declaration);
336     }
337     
338     /**
339      * removes a primary key from database table
340      * 
341      * @param string tableName (there is just one primary key...)
342      */
343     public function dropPrimaryKey($_tableName)
344     {
345         $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . " DROP PRIMARY KEY " ;
346         $this->execQueryVoid($statement);
347     }
348
349     /**
350      * add a foreign key to database table
351      * 
352      * @param string tableName
353      * @param Setup_Backend_Schema_Index_Abstract declaration
354      */
355     public function addForeignKey($_tableName, Setup_Backend_Schema_Index_Abstract $_declaration)
356     {
357         $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . " ADD " 
358                     . $this->getForeignKeyDeclarations($_declaration, $_tableName);
359         $this->execQueryVoid($statement);
360     }
361     
362     /**
363      * removes a foreign key from database table
364      * 
365      * @param string tableName
366      * @param string foreign key name
367      */
368     public function dropForeignKey($_tableName, $_name)
369     {
370         try {
371             $this->_dropForeignKey($_tableName, SQL_TABLE_PREFIX . $_name);
372         } catch (Zend_Db_Statement_Exception $zdse) {
373             // try it again without table prefix
374             try {
375                 $this->_dropForeignKey($_tableName, $_name);
376             } catch (Zend_Db_Statement_Exception $zdse) {
377                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
378                     . ' ' . $zdse);
379                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
380                     . ' At first remove constraint, then remove key ...');
381                 
382                 $constraint = str_replace(array(
383                     '::',
384                     '--'
385                 ), '??', $_name);
386                 try {
387                     $this->_dropForeignKey($_tableName, SQL_TABLE_PREFIX . $constraint);
388                     $this->_dropForeignKey($_tableName, SQL_TABLE_PREFIX . $_name, FALSE);
389                 } catch (Zend_Db_Statement_Exception $zdse) {
390                     // do it again without prefix
391                     $this->_dropForeignKey($_tableName, $constraint);
392                     $this->_dropForeignKey($_tableName, $_name, FALSE);
393                 }
394             }
395         }
396     }
397     
398     /**
399      * helper function for removing (foreign) keys
400      * 
401      * @param string tableName
402      * @param string $keyName
403      * @param boolean $foreign
404      */
405     protected function _dropForeignKey($tableName, $keyName, $foreign = TRUE)
406     {
407         $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $tableName) 
408             . " DROP" . ($foreign ? ' FOREIGN' : '') . " KEY `" . $keyName . "`" ;
409         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
410             . ' ' . $statement);
411         $this->execQueryVoid($statement);
412     }
413     
414     /**
415      * removes a key from database table
416      * 
417      * @param string tableName 
418      * @param string key name
419      */
420     public function dropIndex($_tableName, $_indexName)
421     {
422         $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . " DROP INDEX " . $this->_db->quoteIdentifier($_indexName);
423         try {
424             $this->execQueryVoid($statement);
425         } catch (Zend_Db_Statement_Exception $zdse) {
426             if (Setup_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
427                 . ' ' . $zdse);
428             
429             // try it again with table prefix
430             $statement = "ALTER TABLE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_tableName) . " DROP INDEX " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . $_indexName);
431             $this->execQueryVoid($statement);
432         }
433     }
434
435     /**
436      * create the right mysql-statement-snippet for columns/fields
437      *
438      * @param Setup_Backend_Schema_Field_Abstract field / column
439      * @param String | optional $_tableName [Not used in this backend (MySQL)]
440      * @return string
441      */
442     public function getFieldDeclarations(Setup_Backend_Schema_Field_Abstract $_field, $_tableName = '')
443     {
444         $buffer = $this->_getFieldDeclarations($_field, $_tableName);
445
446         $definition = implode(' ', $buffer);
447
448         return $definition;
449     }
450     
451     /**
452      * Concrete implementation of 
453      * @see tine20/Setup/Backend/Setup_Backend_Interface#checkTable($_table)
454      */
455     public function checkTable(Setup_Backend_Schema_Table_Abstract $_table)
456     {
457         $dbTable = $this->getExistingSchema($_table->name);
458         return $dbTable->equals($_table);
459     }
460
461     /**
462      * Backup Database
463      *
464      * @param $options
465      * @throws Setup_Backend_Exception_NotImplemented
466      */
467     public function backup($options)
468     {
469         throw new Setup_Backend_Exception_NotImplemented('backup not yet implemented');
470     }
471
472     /**
473      * Restore Database
474      *
475      * @param $options
476      * @throws Setup_Backend_Exception_NotImplemented
477      */
478     public function restore($options)
479     {
480         throw new Setup_Backend_Exception_NotImplemented('restore not yet implemented');
481     }
482
483     /**
484      * create the right mysql-statement-snippet for columns/fields
485      *
486      * @param Setup_Backend_Schema_Field_Abstract field / column
487      * @param String | optional $_tableName [Not used in this backend (MySQL)]
488      * @return string
489      */
490     protected function _getFieldDeclarations(Setup_Backend_Schema_Field_Abstract $_field, $_tableName = '')
491     {
492         $buffer = array();
493         $buffer[] = '  ' . $this->_db->quoteIdentifier($_field->name);
494
495         $buffer = $this->_addDeclarationFieldType($buffer, $_field, $_tableName);
496         $buffer = $this->_addDeclarationUnsigned($buffer, $_field);
497         $buffer = $this->_addDeclarationDefaultValue($buffer, $_field);
498         $buffer = $this->_addDeclarationNotNull($buffer, $_field);
499         $buffer = $this->_addDeclarationAutoincrement($buffer, $_field);
500         $buffer = $this->_addDeclarationComment($buffer, $_field);
501         
502         return $buffer;
503     }
504     
505     protected function _addDeclarationFieldType(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field, $_tableName = '')
506     {
507         $typeMapping = $this->getTypeMapping($_field->type);
508         if (!$typeMapping) {
509             throw new Setup_Backend_Exception_InvalidSchema("Could not get field declaration for field {$_field->name}: The given field type {$_field->type} is not supported");
510         }
511         
512         $fieldType = $typeMapping['defaultType'];
513         if (isset($typeMapping['declarationMethod'])) {
514             $fieldBuffer = call_user_func(array($this, $typeMapping['declarationMethod']), $_field, $_tableName);
515             $_buffer = array_merge($_buffer, $fieldBuffer);
516         } else {
517             if ($_field->length !== NULL) {
518                 if ($this->_db instanceof Zend_Db_Adapter_Oracle) {
519                     if ($_field->type == 'integer' && $_field->length == '64') {
520                         $_field->length = '38';
521                     }
522                 }
523                 if (isset($typeMapping['lengthTypes']) && is_array($typeMapping['lengthTypes'])) {
524                     foreach ($typeMapping['lengthTypes'] as $maxLength => $type) {
525                         if ($_field->length <= $maxLength) {
526                             $fieldType = $type;
527                             $scale  = '';
528                             if (isset($_field->scale)) {
529                                 $scale = ',' . $_field->scale;
530                             } elseif(isset($typeMapping['defaultScale'])) {
531                                 $scale = ',' . $typeMapping['defaultScale'];
532                             }
533                              
534                             $options = "({$_field->length}{$scale})";
535                             break;
536                         }
537                     }
538                     if (!isset($options)) {
539                         throw new Setup_Backend_Exception_InvalidSchema("Could not get field declaration for field {$_field->name}: The given length of {$_field->length} is not supported by field type {$_field->type}");
540                     }
541                 } else {
542                     throw new Setup_Backend_Exception_InvalidSchema("Could not get field declaration for field {$_field->name}: Length option was specified but is not supported by field type {$_field->type}");
543                 }
544             } else {
545                 $options = '';
546                 if (isset($_field->value)) {
547                     foreach ($_field->value as $value) {
548                         $values[] = $value;
549                     }
550                     $options = "('" . implode("','", $values) . "')";
551                 } elseif(isset($typeMapping['defaultLength'])) {
552                     $scale = isset($typeMapping['defaultScale']) ? ',' . $typeMapping['defaultScale'] : '';
553                     $options = "({$typeMapping['defaultLength']}{$scale})";
554                 }
555             }
556
557             $_buffer[] = $fieldType . $options;
558         }
559         
560         return $_buffer;
561     }
562     
563     protected function _addDeclarationDefaultValue(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
564     {
565         if (isset($_field->default)) {
566             $_buffer[] = $this->_db->quoteInto("DEFAULT ?", $_field->default) ;
567         }
568         return $_buffer;
569     }
570     
571     protected function _addDeclarationNotNull(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
572     {
573         if ($_field->notnull === true) {
574             $_buffer[] = 'NOT NULL';
575         }
576         return $_buffer;
577     }
578     
579     protected function _addDeclarationUnsigned(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
580     {
581         if (isset($_field->unsigned) && $_field->unsigned === true) {
582             $_buffer[] = 'unsigned';
583         }
584         return $_buffer;
585     }
586
587     protected function _addDeclarationAutoincrement(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
588     {
589         if (isset($_field->autoincrement) && $_field->autoincrement === true) {
590             $_buffer[] = 'auto_increment';
591         }
592         return $_buffer;
593     }
594
595     protected function _addDeclarationComment(array $_buffer, Setup_Backend_Schema_Field_Abstract $_field)
596     {
597         if (isset($_field->comment)) {
598             $_buffer[] = "COMMENT '" .  $_field->comment . "'";
599         }
600         return $_buffer;
601     }
602     
603     protected function _sanititzeName($_name)
604     {
605         if (strlen($_name) > Setup_Backend_Abstract::MAX_NAME_LENGTH) {
606             $_name = substr(md5($_name), 0 , Setup_Backend_Abstract::MAX_NAME_LENGTH);
607         }
608         return $_name;
609     }
610 }