11784 support ownclouds X-OC-MTime header
[tine20] / tine20 / Tinebase / Backend / Sql / Abstract.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @subpackage  Backend
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Philipp Schüle <p.schuele@metaways.de>
10  * 
11  * @todo        think about removing the appendForeignRecord* functions
12  * @todo        use const for type (set in constructor)
13  * @todo        move custom fields handling to controller?
14  */
15
16 /**
17  * Abstract class for a Tine 2.0 sql backend
18  * 
19  * @package     Tinebase
20  * @subpackage  Backend
21  */
22 abstract class Tinebase_Backend_Sql_Abstract extends Tinebase_Backend_Abstract implements Tinebase_Backend_Sql_Interface
23 {
24     /**
25      * placeholder for id column for search()/_getSelect()
26      */
27     const IDCOL             = '_id_';
28     
29     /**
30      * fetch single column with db query
31      */
32     const FETCH_MODE_SINGLE = 'fetch_single';
33
34     /**
35      * fetch two columns (id + X) with db query
36      */
37     const FETCH_MODE_PAIR   = 'fetch_pair';
38     
39     /**
40      * fetch all columns with db query
41      */
42     const FETCH_ALL         = 'fetch_all';
43
44     /**
45      * backend type
46      *
47      * @var string
48      */
49     protected $_type = 'Sql';
50     
51     /**
52      * Table name without prefix
53      *
54      * @var string
55      */
56     protected $_tableName = NULL;
57     
58     /**
59      * Table prefix
60      *
61      * @var string
62      */
63     protected $_tablePrefix = NULL;
64     
65     /**
66      * if modlog is active, we add 'is_deleted = 0' to select object in _getSelect()
67      *
68      * @var boolean
69      */
70     protected $_modlogActive = FALSE;
71     
72     /**
73      * Identifier
74      *
75      * @var string
76      */
77     protected $_identifier = 'id';
78     
79     /**
80      * @var Zend_Db_Adapter_Abstract
81      */
82     protected $_db;
83     
84     /**
85      * @var Tinebase_Backend_Sql_Command_Interface
86      */
87     protected $_dbCommand;
88     
89     /**
90      * schema of the table
91      *
92      * @var array
93      */
94     protected $_schema = NULL;
95     
96     /**
97      * foreign tables 
98      * name => array(table, joinOn, field)
99      *
100      * @var array
101      */
102     protected $_foreignTables = array();
103     
104     /**
105      * additional search count columns
106      * 
107      * @var array
108      */
109     protected $_additionalSearchCountCols = array();
110     
111     /**
112      * default secondary sort criteria
113      * 
114      * @var string
115      */
116     protected $_defaultSecondarySort = NULL;
117     
118     /**
119      * default column(s) for count
120      * 
121      * @var string
122      */
123     protected $_defaultCountCol = '*';
124     
125     /**
126      * the constructor
127      * 
128      * allowed options:
129      *  - modelName
130      *  - tableName
131      *  - tablePrefix
132      *  - modlogActive
133      *  
134      * @param Zend_Db_Adapter_Abstract $_db (optional)
135      * @param array $_options (optional)
136      * @throws Tinebase_Exception_Backend_Database
137      */
138     public function __construct($_dbAdapter = NULL, $_options = array())
139     {
140         $this->_db        = ($_dbAdapter instanceof Zend_Db_Adapter_Abstract) ? $_dbAdapter : Tinebase_Core::getDb();
141         $this->_dbCommand = Tinebase_Backend_Sql_Command::factory($this->_db);
142         
143         $this->_modelName            = (isset($_options['modelName']) || array_key_exists('modelName', $_options))            ? $_options['modelName']    : $this->_modelName;
144         $this->_tableName            = (isset($_options['tableName']) || array_key_exists('tableName', $_options))            ? $_options['tableName']    : $this->_tableName;
145         $this->_tablePrefix          = (isset($_options['tablePrefix']) || array_key_exists('tablePrefix', $_options))          ? $_options['tablePrefix']  : $this->_db->table_prefix;
146         $this->_modlogActive         = (isset($_options['modlogActive']) || array_key_exists('modlogActive', $_options))         ? $_options['modlogActive'] : $this->_modlogActive;
147         
148         if (! ($this->_tableName && $this->_modelName)) {
149             throw new Tinebase_Exception_Backend_Database('modelName and tableName must be configured or given.');
150         }
151         if (! $this->_db) {
152             throw new Tinebase_Exception_Backend_Database('Database adapter must be configured or given.');
153         }
154     }
155     
156     /*************************** getters and setters *********************************/
157     
158     /**
159      * sets modlog active flag
160      * 
161      * @param $_bool
162      * @return Tinebase_Backend_Sql_Abstract
163      */
164     public function setModlogActive($_bool)
165     {
166         $this->_modlogActive = (bool) $_bool;
167         return $this;
168     }
169     
170     /**
171      * checks if modlog is active or not
172      * 
173      * @return bool
174      */
175     public function getModlogActive()
176     {
177         return $this->_modlogActive;
178     }
179     
180     /**
181      * returns the db schema
182      * 
183      * @return array
184      */
185     public function getSchema()
186     {
187         if (!$this->_schema) {
188             try {
189                 $this->_schema = Tinebase_Db_Table::getTableDescriptionFromCache($this->_tablePrefix . $this->_tableName, $this->_db);
190             } catch (Zend_Db_Adapter_Exception $zdae) {
191                 throw new Tinebase_Exception_Backend_Database('Connection failed: ' . $zdae->getMessage());
192             }
193         }
194         
195         return $this->_schema;
196     }
197     
198     /*************************** get/search funcs ************************************/
199
200     /**
201      * Gets one entry (by id)
202      *
203      * @param integer|Tinebase_Record_Interface $_id
204      * @param $_getDeleted get deleted records
205      * @return Tinebase_Record_Interface
206      * @throws Tinebase_Exception_NotFound
207      */
208     public function get($_id, $_getDeleted = FALSE) 
209     {
210         if (empty($_id)) {
211             throw new Tinebase_Exception_NotFound('$_id can not be empty');
212         }
213         
214         $id = Tinebase_Record_Abstract::convertId($_id, $this->_modelName);
215         
216         return $this->getByProperty($id, $this->_identifier, $_getDeleted);
217     }
218
219     /**
220      * splits identifier if table name is given (i.e. for joined tables)
221      *
222      * @return string identifier name
223      */
224     protected function _getRecordIdentifier()
225     {
226         if (preg_match("/\./", $this->_identifier)) {
227             list($table, $identifier) = explode('.', $this->_identifier);
228         } else {
229             $identifier = $this->_identifier;
230         }
231         
232         return $identifier;
233     }
234
235     /**
236      * Gets one entry (by property)
237      *
238      * @param  mixed  $value
239      * @param  string $property
240      * @param  bool   $getDeleted
241      * @return Tinebase_Record_Interface
242      * @throws Tinebase_Exception_NotFound
243      */
244     public function getByProperty($value, $property = 'name', $getDeleted = FALSE) 
245     {
246         $select = $this->_getSelect('*', $getDeleted)
247             ->limit(1);
248         
249         if ($value !== NULL) {
250             $select->where($this->_db->quoteIdentifier($this->_tableName . '.' . $property) . ' = ?', $value);
251         } else {
252             $select->where($this->_db->quoteIdentifier($this->_tableName . '.' . $property) . ' IS NULL');
253         }
254         
255         Tinebase_Backend_Sql_Abstract::traitGroup($select);
256         
257         $stmt = $this->_db->query($select);
258         $queryResult = $stmt->fetch();
259         $stmt->closeCursor();
260         
261         if (!$queryResult) {
262             $messageValue = ($value !== NULL) ? $value : 'NULL';
263             throw new Tinebase_Exception_NotFound($this->_modelName . " record with $property = $messageValue not found!");
264         }
265         
266         $result = $this->_rawDataToRecord($queryResult);
267         
268         return $result;
269     }
270     
271     /**
272      * fetch a single property for all records defined in array of $ids
273      * 
274      * @param array|string $ids
275      * @param string $property
276      * @return array (key = id, value = property value)
277      */
278     public function getPropertyByIds($ids, $property)
279     {
280         $select = $this->_getSelect(array($property, $this->_identifier));
281         $select->where($this->_db->quoteIdentifier($this->_tableName . '.' . $this->_identifier) . ' IN (?)', (array) $ids);
282         Tinebase_Backend_Sql_Abstract::traitGroup($select);
283         
284         $this->_checkTracing($select);
285         
286         $stmt = $this->_db->query($select);
287         $queryResult = $stmt->fetchAll();
288         $stmt->closeCursor();
289         
290         $result = array();
291         foreach($queryResult as $row) {
292             $result[$row[$this->_identifier]] = $row[$property];
293         }
294         return $result;
295     }
296     
297     /**
298      * converts raw data from adapter into a single record
299      *
300      * @param  array $_rawData
301      * @return Tinebase_Record_Abstract
302      */
303     protected function _rawDataToRecord(array $_rawData)
304     {
305         $result = new $this->_modelName($_rawData, true);
306         
307         $this->_explodeForeignValues($result);
308         
309         return $result;
310     }
311     
312     /**
313      * explode foreign values
314      * 
315      * @param Tinebase_Record_Interface $_record
316      */
317     protected function _explodeForeignValues(Tinebase_Record_Interface $_record)
318     {
319         foreach (array_keys($this->_foreignTables) as $field) {
320             $isSingleValue = ((isset($this->_foreignTables[$field]['singleValue']) || array_key_exists('singleValue', $this->_foreignTables[$field])) && $this->_foreignTables[$field]['singleValue']);
321             if (! $isSingleValue) {
322                 $_record->{$field} = (! empty($_record->{$field})) ? explode(',', $_record->{$field}) : array();
323             }
324         }
325     }
326     
327     /**
328      * gets multiple entries (by property)
329      *
330      * @param  mixed  $_value
331      * @param  string $_property
332      * @param  bool   $_getDeleted
333      * @param  string $_orderBy        defaults to $_property
334      * @param  string $_orderDirection defaults to 'ASC'
335      * @return Tinebase_Record_RecordSet
336      */
337     public function getMultipleByProperty($_value, $_property='name', $_getDeleted = FALSE, $_orderBy = NULL, $_orderDirection = 'ASC')
338     {
339         $columnName = $this->_db->quoteIdentifier($this->_tableName . '.' . $_property);
340         if (! empty($_value)) {
341             $value = (array)$_value;
342             $orderBy = $this->_tableName . '.' . ($_orderBy ? $_orderBy : $_property);
343
344             $select = $this->_getSelect('*', $_getDeleted)
345                 ->where($columnName . 'IN (?)', $value)
346                 ->order($orderBy . ' ' . $_orderDirection);
347
348                 Tinebase_Backend_Sql_Abstract::traitGroup($select);
349         } else {
350                 $select = $this->_getSelect('*', $_getDeleted)->where('1=0');
351         }
352         
353         $this->_checkTracing($select);
354         
355         $stmt = $this->_db->query($select);
356         
357         $resultSet = $this->_rawDataToRecordSet($stmt->fetchAll());
358         $resultSet->addIndices(array($_property));
359         
360         return $resultSet;
361     }
362     
363     /**
364      * converts raw data from adapter into a set of records
365      *
366      * @param  array $_rawDatas of arrays
367      * @return Tinebase_Record_RecordSet
368      */
369     protected function _rawDataToRecordSet(array $_rawDatas)
370     {
371         $result = new Tinebase_Record_RecordSet($this->_modelName, $_rawDatas, true);
372         
373         if (! empty($this->_foreignTables)) {
374             foreach ($result as $record) {
375                 $this->_explodeForeignValues($record);
376             }
377         }
378         
379         return $result;
380     }
381     
382     /**
383      * Get multiple entries
384      *
385      * @param string|array $_id Ids
386      * @param array $_containerIds all allowed container ids that are added to getMultiple query
387      * @return Tinebase_Record_RecordSet
388      * 
389      * @todo get custom fields here as well
390      */
391     public function getMultiple($_id, $_containerIds = NULL) 
392     {
393         // filter out any emtpy values
394         $ids = array_filter((array) $_id, function($value) {
395             return !empty($value);
396         });
397         
398         if (empty($ids)) {
399             return new Tinebase_Record_RecordSet($this->_modelName);
400         }
401
402         // replace objects with their id's
403         foreach ($ids as &$id) {
404             if ($id instanceof Tinebase_Record_Interface) {
405                 $id = $id->getId();
406             }
407         }
408         
409         $select = $this->_getSelect();
410         $select->where($this->_db->quoteIdentifier($this->_tableName . '.' . $this->_identifier) . ' IN (?)', $ids);
411         
412         $schema = $this->getSchema();
413         
414         if ($_containerIds !== NULL && isset($schema['container_id'])) {
415             if (empty($_containerIds)) {
416                 $select->where('1=0 /* insufficient grants */');
417             } else {
418                 $select->where($this->_db->quoteIdentifier($this->_tableName . '.container_id') . ' IN (?) /* add acl in getMultiple */', (array) $_containerIds);
419             }
420         }
421         
422         Tinebase_Backend_Sql_Abstract::traitGroup($select);
423         
424         $this->_checkTracing($select);
425         
426         $stmt = $this->_db->query($select);
427         $queryResult = $stmt->fetchAll();
428         
429         $result = $this->_rawDataToRecordSet($queryResult);
430         
431         return $result;
432     }
433     
434     /**
435      * Gets all entries
436      *
437      * @param string $_orderBy Order result by
438      * @param string $_orderDirection Order direction - allowed are ASC and DESC
439      * @throws Tinebase_Exception_InvalidArgument
440      * @return Tinebase_Record_RecordSet
441      */
442     public function getAll($_orderBy = NULL, $_orderDirection = 'ASC') 
443     {
444         $orderBy = $_orderBy ? $_orderBy : $this->_tableName . '.' . $this->_identifier;
445         
446         if(!in_array($_orderDirection, array('ASC', 'DESC'))) {
447             throw new Tinebase_Exception_InvalidArgument('$_orderDirection is invalid');
448         }
449         
450         $select = $this->_getSelect();
451         $select->order($orderBy . ' ' . $_orderDirection);
452         
453         Tinebase_Backend_Sql_Abstract::traitGroup($select);
454         
455         //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $select->__toString());
456             
457         $stmt = $this->_db->query($select);
458         $queryResult = $stmt->fetchAll();
459         
460         //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($queryResult, true));
461         
462         $result = $this->_rawDataToRecordSet($queryResult);
463         
464         return $result;
465     }
466     
467     /**
468      * Search for records matching given filter
469      *
470      * @param  Tinebase_Model_Filter_FilterGroup    $_filter
471      * @param  Tinebase_Model_Pagination            $_pagination
472      * @param  array|string|boolean                 $_cols columns to get, * per default / use self::IDCOL or TRUE to get only ids
473      * @return Tinebase_Record_RecordSet|array
474      */
475     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL, $_cols = '*')
476     {
477         $getDeleted = !!$_filter && $_filter->getFilter('is_deleted');
478         
479         if ($_pagination === NULL) {
480             $_pagination = new Tinebase_Model_Pagination(NULL, TRUE);
481         }
482         
483         // legacy: $_cols param was $_onlyIds (boolean) ...
484         if ($_cols === TRUE) {
485             $_cols = self::IDCOL;
486         } else if ($_cols === FALSE) {
487             $_cols = '*';
488         }
489         
490         // (1) eventually get only ids or id/value pair
491         list($colsToFetch, $getIdValuePair) = $this->_getColumnsToFetch($_cols, $_filter, $_pagination);
492
493         // check if we should do one or two queries
494         $doSecondQuery = true;
495         if (!$getIdValuePair && $_cols !== self::IDCOL)
496         {
497             if ($this->_compareRequiredJoins($_cols, $colsToFetch)) {
498                 $doSecondQuery = false;
499             }
500         }
501         if ($doSecondQuery) {
502             $select = $this->_getSelect($colsToFetch, $getDeleted);
503         } else {
504             $select = $this->_getSelect($_cols, $getDeleted);
505         }
506         
507         if ($_filter !== NULL) {
508             $this->_addFilter($select, $_filter);
509         }
510         $this->_addSecondarySort($_pagination);
511         $this->_appendForeignSort($_pagination, $select);
512         $_pagination->appendPaginationSql($select);
513         
514         Tinebase_Backend_Sql_Abstract::traitGroup($select);
515         
516         if ($getIdValuePair) {
517             return $this->_fetch($select, self::FETCH_MODE_PAIR);
518         } elseif($_cols === self::IDCOL) {
519             return $this->_fetch($select);
520         }
521         
522         if (!$doSecondQuery) {
523             $rows = $this->_fetch($select, self::FETCH_ALL);
524             if (empty($rows)) {
525                 return new Tinebase_Record_RecordSet($this->_modelName);
526             } else {
527                 return $this->_rawDataToRecordSet($rows);
528             }
529         }
530         
531         // (2) get other columns and do joins
532         $ids = $this->_fetch($select);
533         if (empty($ids)) {
534             return new Tinebase_Record_RecordSet($this->_modelName);
535         }
536         
537         $select = $this->_getSelect($_cols, $getDeleted);
538         $this->_addWhereIdIn($select, $ids);
539         $_pagination->appendSort($select);
540         
541         $rows = $this->_fetch($select, self::FETCH_ALL);
542         
543         return $this->_rawDataToRecordSet($rows);
544     }
545
546     /**
547      * add the fields to search for to the query
548      *
549      * @param  Zend_Db_Select                       $_select current where filter
550      * @param  Tinebase_Model_Filter_FilterGroup    $_filter the string to search for
551      * @return void
552      */
553     protected function _addFilter(Zend_Db_Select $_select, /*Tinebase_Model_Filter_FilterGroup */$_filter)
554     {
555         Tinebase_Backend_Sql_Filter_FilterGroup::appendFilters($_select, $_filter, $this);
556     }
557     
558     /**
559      * Gets total count of search with $_filter
560      * 
561      * @param Tinebase_Model_Filter_FilterGroup $_filter
562      * @return int|array
563      */
564     public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter)
565     {
566         $getDeleted = !!$_filter && $_filter->getFilter('is_deleted');
567
568         $defaultCountCol = $this->_defaultCountCol == '*' ?  '*' : $this->_db->quoteIdentifier($this->_defaultCountCol);
569         
570         $searchCountCols = array('count' => 'COUNT(' . $defaultCountCol . ')');
571         foreach ($this->_additionalSearchCountCols as $column => $select) {
572             $searchCountCols['sum_' . $column] = new Zend_Db_Expr('SUM(' . $this->_db->quoteIdentifier($column) . ')');
573         }
574         
575         list($subSelectColumns, $getIdValuePair) = $this->_getColumnsToFetch(self::IDCOL, $_filter);
576         if (!empty($this->_additionalSearchCountCols)) {
577             $subSelectColumns = array_merge($subSelectColumns, $this->_additionalSearchCountCols);
578         }
579         
580         $subSelect = $this->_getSelect($subSelectColumns, $getDeleted);
581         $this->_addFilter($subSelect, $_filter);
582         
583         Tinebase_Backend_Sql_Abstract::traitGroup($subSelect);
584         
585         $countSelect = $this->_db->select()->from($subSelect, $searchCountCols);
586         
587         if (!empty($this->_additionalSearchCountCols)) {
588             $result = $this->_db->fetchRow($countSelect);
589         } else {
590             $result = $this->_db->fetchOne($countSelect);
591         }
592         
593         return $result;
594     }
595     
596     /**
597      * returns columns to fetch in first query and if an id/value pair is requested 
598      * 
599      * @param array|string $_cols
600      * @param Tinebase_Model_Filter_FilterGroup $_filter
601      * @param Tinebase_Model_Pagination $_pagination
602      * @return array
603      */
604     protected function _getColumnsToFetch($_cols, Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL)
605     {
606         $getIdValuePair = FALSE;
607
608         if ($_cols === '*') {
609             $colsToFetch = array('id' => self::IDCOL);
610         } else {
611             $colsToFetch = (array) $_cols;
612             
613             if (in_array(self::IDCOL, $colsToFetch) && count($colsToFetch) == 2) {
614                 // id/value pair requested
615                 $getIdValuePair = TRUE;
616             } else if (! in_array(self::IDCOL, $colsToFetch) && count($colsToFetch) == 1) {
617                 // only one non-id column was requested -> add id and treat it like id/value pair
618                 array_push($colsToFetch, self::IDCOL);
619                 $getIdValuePair = TRUE;
620             } else {
621                 $colsToFetch = array('id' => self::IDCOL);
622             }
623         }
624         
625         if ($_filter !== NULL) {
626             $colsToFetch = $this->_addFilterColumns($colsToFetch, $_filter);
627         }
628         
629         if ($_pagination instanceof Tinebase_Model_Pagination) {
630             foreach((array) $_pagination->sort as $sort) {
631                 if (! (isset($colsToFetch[$sort]) || array_key_exists($sort, $colsToFetch))) {
632                     $colsToFetch[$sort] = (substr_count($sort, $this->_tableName) === 0) ? $this->_tableName . '.' . $sort : $sort;
633                 }
634             }
635         }
636         
637         return array($colsToFetch, $getIdValuePair);
638     }
639     
640     /**
641      * add columns from filter
642      * 
643      * @param array $_colsToFetch
644      * @param Tinebase_Model_Filter_FilterGroup $_filter
645      * @return array
646      */
647     protected function _addFilterColumns($_colsToFetch, Tinebase_Model_Filter_FilterGroup $_filter)
648     {
649         // need to ask filter if it needs additional columns
650         $filterCols = $_filter->getRequiredColumnsForSelect();
651         foreach ($filterCols as $key => $filterCol) {
652             if (! (isset($_colsToFetch[$key]) || array_key_exists($key, $_colsToFetch))) {
653                 $_colsToFetch[$key] = $filterCol;
654             }
655         }
656         
657         return $_colsToFetch;
658     }
659     
660     /**
661      * add default secondary sort criteria
662      * 
663      * @param Tinebase_Model_Pagination $_pagination
664      */
665     protected function _addSecondarySort(Tinebase_Model_Pagination $_pagination)
666     {
667         if (! empty($this->_defaultSecondarySort)) {
668             if (! is_array($_pagination->sort) || ! in_array($this->_defaultSecondarySort, $_pagination->sort)) {
669                 $_pagination->sort = array_merge((array)$_pagination->sort, array($this->_defaultSecondarySort));
670             }
671         }
672     }
673     
674     /**
675      * append foreign sorting to select
676      * 
677      * @param Tinebase_Model_Pagination $pagination
678      * @param Zend_Db_Select $select
679      * 
680      * @todo allow generic foreign record/relation/keyfield sorting
681      */
682     protected function _appendForeignSort(Tinebase_Model_Pagination $pagination, Zend_Db_Select $select)
683     {
684     }
685     
686     /**
687      * adds 'id in (...)' where stmt
688      * 
689      * @param Zend_Db_Select $_select
690      * @param string|array $_ids
691      * @return Zend_Db_Select
692      */
693     protected function _addWhereIdIn(Zend_Db_Select $_select, $_ids)
694     {
695         $_select->where($this->_db->quoteInto($this->_db->quoteIdentifier($this->_tableName . '.' . $this->_identifier) . ' in (?)', (array) $_ids));
696         
697         return $_select;
698     }
699     
700     /**
701      * Checks if backtrace and query should be logged
702      * 
703      * For enabling this feature, you must add a key in config.inc.php:
704      * 
705      *     'logger' => 
706      *         array(
707      *             // logger stuff
708      *             'traceQueryOrigins' => true,
709      *             'priority' => 8
710      *         ),
711      *
712      * @param Zend_Db_Select $select
713      */
714     protected function _checkTracing(Zend_Db_Select $select)
715     {
716         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE) && $config = Tinebase_Core::getConfig()->logger) {
717             if ($config->traceQueryOrigins) {
718                 $e = new Exception();
719                 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . "\n" . 
720                     "BACKTRACE: \n" . $e->getTraceAsString() . "\n" . 
721                     "SQL QUERY: \n" . $select);
722             }
723         }
724     }
725     
726     /**
727      * fetch rows from db
728      * 
729      * @param Zend_Db_Select $_select
730      * @param string $_mode
731      * @return array
732      */
733     protected function _fetch(Zend_Db_Select $_select, $_mode = self::FETCH_MODE_SINGLE)
734     {
735         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $_select->__toString());
736         
737         Tinebase_Backend_Sql_Abstract::traitGroup($_select);
738         
739         $this->_checkTracing($_select);
740         
741         $stmt = $this->_db->query($_select);
742         
743         if ($_mode === self::FETCH_ALL) {
744             $result = (array) $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
745         } else {
746             $result = array();
747             while ($row = $stmt->fetch(Zend_Db::FETCH_NUM)) {
748                 if ($_mode === self::FETCH_MODE_SINGLE) {
749                     $result[] = $row[0];
750                 } else if ($_mode === self::FETCH_MODE_PAIR) {
751                     $result[$row[0]] = $row[1];
752                 }
753             }
754         }
755         
756         return $result;
757     }
758     
759     /**
760      * get the basic select object to fetch records from the database
761      *  
762      * @param array|string $_cols columns to get, * per default
763      * @param boolean $_getDeleted get deleted records (if modlog is active)
764      * @return Zend_Db_Select
765      */
766     protected function _getSelect($_cols = '*', $_getDeleted = FALSE)
767     {
768         if ($_cols !== '*' ) {
769             $cols = array();
770             // make sure cols is an array, prepend tablename and fix keys
771             foreach ((array) $_cols as $id => $col) {
772                 $key = (is_numeric($id)) ? ($col === self::IDCOL) ? $this->_identifier : $col : $id;
773                 $cols[$key] = ($col === self::IDCOL) ? $this->_tableName . '.' . $this->_identifier : $col;
774             }
775         } else {
776             $cols = '*';
777         }
778         
779         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($cols, TRUE));
780         
781         $select = $this->getAdapter()->select();
782         $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName), $cols);
783         
784         if (!$_getDeleted && $this->_modlogActive) {
785             // don't fetch deleted objects
786             $select->where($this->_db->quoteIdentifier($this->_tableName . '.is_deleted') . ' = 0');
787         }
788         
789         $this->_addForeignTableJoins($select, $cols);
790         
791         return $select;
792     }
793     
794     /**
795      * add foreign table joins
796      * 
797      * @param Zend_Db_Select $_select
798      * @param array|string $_cols columns to get, * per default
799      * 
800      * @todo joining the same table twice with same name but different "on"'s is not possible currently
801      */
802     protected function _addForeignTableJoins(Zend_Db_Select $_select, $_cols, $_groupBy = NULL)
803     {
804         if (! empty($this->_foreignTables)) {
805             $groupBy = ($_groupBy !== NULL) ? $_groupBy : $this->_tableName . '.' . $this->_identifier;
806             $_select->group($groupBy);
807             
808             $cols = (array) $_cols;
809             foreach ($this->_foreignTables as $foreignColumn => $join) {
810                 // only join if field is in cols
811                 if (in_array('*', $cols) || (isset($cols[$foreignColumn]) || array_key_exists($foreignColumn, $cols))) {
812                     if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' foreign column: ' . $foreignColumn);
813                     
814                     $selectArray = ((isset($join['select']) || array_key_exists('select', $join)))
815                         ? $join['select'] 
816                         : (((isset($join['field']) || array_key_exists('field', $join)) && (! (isset($join['singleValue']) || array_key_exists('singleValue', $join)) || ! $join['singleValue']))
817                             ? array($foreignColumn => $this->_dbCommand->getAggregate($join['table'] . '.' . $join['field']))
818                             : array($foreignColumn => $join['table'] . '.id'));
819                     $joinId = isset($join['joinId']) ? $join['joinId'] : $this->_identifier;
820                     
821                     // avoid duplicate columns => will be added again in the next few lines of code
822                     $this->_removeColFromSelect($_select, $foreignColumn);
823                     
824                     $from = $_select->getPart(Zend_Db_Select::FROM);
825                     
826                     if (!isset($from[$join['table']])) {
827                         $_select->joinLeft(
828                             /* table  */ array($join['table'] => $this->_tablePrefix . $join['table']), 
829                             /* on     */ $this->_db->quoteIdentifier($this->_tableName . '.' . $joinId) . ' = ' . $this->_db->quoteIdentifier($join['table'] . '.' . $join['joinOn']),
830                             /* select */ $selectArray
831                         );
832                     } else {
833                         // join is defined already => just add the column
834                         $_select->columns($selectArray, $join['table']);
835                     }
836                 }
837             }
838         }
839     }
840     
841     /**
842      * returns true if joins are equal, false if not
843      * 
844      * @param array $finalCols
845      * @param array $interimCols
846      */
847     protected function _compareRequiredJoins( $finalCols, $interimCols )
848     {
849         $ret = true;
850         if (! empty($this->_foreignTables)) {
851             $finalCols = (array) $finalCols;
852             $finalColsJoins = array();
853             $interimColsJoins = array();
854             foreach ($this->_foreignTables as $foreignColumn => $join) {
855                 // only join if field is in cols
856                 if (in_array('*', $finalCols) || (isset($finalCols[$foreignColumn]) || array_key_exists($foreignColumn, $finalCols))) {
857                     $finalColsJoins[$join['table']] = 1;
858                 }
859                 if (in_array('*', $interimCols) || (isset($interimCols[$foreignColumn]) || array_key_exists($foreignColumn, $interimCols))) {
860                     $interimColsJoins[$join['table']] = 1;
861                 }
862             }
863             if (count(array_diff_key($finalColsJoins,$interimColsJoins)) > 0) {
864                 $ret = false;
865             }
866         }
867         return $ret;
868     }
869     
870     /**
871      * remove column from select to avoid duplicates 
872      * 
873      * @param Zend_Db_Select $_select
874      * @param string $_column
875      * @todo remove $_cols parameter
876      */
877     protected function _removeColFromSelect(Zend_Db_Select $_select, $_column)
878     {
879         $columns = $_select->getPart(Zend_Db_Select::COLUMNS);
880         $from = $_select->getPart(Zend_Db_Select::FROM);
881         
882         foreach ($columns as $id => $column) {
883             if ($column[2] == $_column) {
884                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(
885                     __METHOD__ . '::' . __LINE__ . ' Removing ' . $_column . ' from columns.');
886                 
887                 unset($columns[$id]);
888                 
889                 // reset all all columns and add as again
890                 $_select->reset(Zend_Db_Select::COLUMNS);
891                 foreach ($columns as $newColumn) {
892                     
893                     if (isset($from[$newColumn[0]])) {
894                         $_select->columns(!empty($newColumn[2]) ? array($newColumn[2] => $newColumn[1]) : $newColumn[1], $newColumn[0]);
895                     }
896                 }
897                 
898                 break;
899             }
900         }
901     }
902     
903     /*************************** create / update / delete ****************************/
904     
905     /**
906      * Creates new entry
907      *
908      * @param   Tinebase_Record_Interface $_record
909      * @return  Tinebase_Record_Interface
910      * @throws  Tinebase_Exception_InvalidArgument
911      * @throws  Tinebase_Exception_UnexpectedValue
912      * 
913      * @todo    remove autoincremental ids later
914      */
915     public function create(Tinebase_Record_Interface $_record) 
916     {
917         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($this->_db);
918         try {
919             $identifier = $_record->getIdProperty();
920             
921             if (!$_record instanceof $this->_modelName) {
922                 throw new Tinebase_Exception_InvalidArgument('invalid model type: $_record is instance of "' . get_class($_record) . '". but should be instance of ' . $this->_modelName);
923
924             }
925             
926             // set uid if record has hash id and id is empty
927             if (empty($_record->$identifier) && $this->_hasHashId()) {
928                 $_record->setId($_record->generateUID());
929             }
930             
931             $recordArray = $this->_recordToRawData($_record);
932             
933             // unset id if autoincrement & still empty
934             if ($this->_hasAutoIncrementId() || $_record->$identifier == 'NULL' ) {
935                 unset($recordArray['id']);
936             }
937             
938             $recordArray = array_intersect_key($recordArray, $this->getSchema());
939             
940             $this->_prepareData($recordArray);
941             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
942                 . " Prepared data for INSERT: " . print_r($recordArray, true)
943             );
944             
945             $this->_db->insert($this->_tablePrefix . $this->_tableName, $recordArray);
946             
947             if ($this->_hasAutoIncrementId()) {
948                 $newId = $this->_db->lastInsertId($this->getTablePrefix() . $this->getTableName(), $identifier);
949                 if (!$newId) {
950                     throw new Tinebase_Exception_UnexpectedValue("New record auto increment id is empty");
951                 }
952                 $_record->setId($newId);
953             }
954             
955             // if we insert a record without an id, we need to get back one
956             if (empty($_record->$identifier)) {
957                 throw new Tinebase_Exception_UnexpectedValue("Returned record id is empty.");
958             }
959             
960             // add custom fields
961             if ($_record->has('customfields') && !empty($_record->customfields)) {
962                 Tinebase_CustomField::getInstance()->saveRecordCustomFields($_record);
963             }
964             
965             $this->_updateForeignKeys('create', $_record);
966             
967             $result = $this->get($_record->$identifier);
968             
969             $this->_inspectAfterCreate($result, $_record);
970             
971             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
972         } catch(Exception $e) {
973             Tinebase_TransactionManager::getInstance()->rollBack();
974             throw $e;
975         }
976         
977         return $result;
978     }
979     
980     /**
981      * returns true if id is a hash value and false if integer
982      *
983      * @return  boolean
984      * @todo    remove that when all tables use hash ids 
985      */
986     protected function _hasHashId()
987     {
988         $identifier = $this->_getRecordIdentifier();
989         $schema     = $this->getSchema();
990         
991         if (!isset($schema[$identifier])) {
992             // should never happen
993             return false;
994         }
995         
996         $column = $schema[$identifier];
997         
998         if (!in_array($column['DATA_TYPE'], array('varchar', 'VARCHAR2'))) {
999             return false;
1000         }
1001         
1002         return ($column['LENGTH'] == 40);
1003     }
1004     
1005     /**
1006      * returns true if id is an autoincrementing column
1007      * 
1008      * IDENTITY is 1 for auto increment columns
1009      * 
1010      * MySQL
1011      * 
1012      * Array (
1013         [SCHEMA_NAME] => 
1014         [TABLE_NAME] => tine20_container
1015         [COLUMN_NAME] => id
1016         [COLUMN_POSITION] => 1
1017         [DATA_TYPE] => int
1018         [DEFAULT] => 
1019         [NULLABLE] => 
1020         [LENGTH] => 
1021         [SCALE] => 
1022         [PRECISION] => 
1023         [UNSIGNED] => 1
1024         [PRIMARY] => 1
1025         [PRIMARY_POSITION] => 1
1026         [IDENTITY] => 1
1027      * )
1028      * 
1029      * PostgreSQL
1030      * 
1031      * Array (
1032         [SCHEMA_NAME] => public
1033         [TABLE_NAME] => tine20_container
1034         [COLUMN_NAME] => id
1035         [COLUMN_POSITION] => 1
1036         [DATA_TYPE] => int4
1037         [DEFAULT] => nextval('tine20_container_id_seq'::regclass)
1038         [NULLABLE] => 
1039         [LENGTH] => 4
1040         [SCALE] => 
1041         [PRECISION] => 
1042         [UNSIGNED] => 
1043         [PRIMARY] => 1
1044         [PRIMARY_POSITION] => 1
1045         [IDENTITY] => 1
1046      * )
1047      * 
1048      * @return boolean
1049      */
1050     protected function _hasAutoIncrementId()
1051     {
1052         $identifier = $this->_getRecordIdentifier();
1053         $schema     = $this->getSchema();
1054         
1055         if (!isset($schema[$identifier])) {
1056             // should never happen
1057             return false;
1058         }
1059         
1060         $column = $schema[$identifier];
1061         
1062         if (!in_array($column['DATA_TYPE'], array('int', 'int4'))) {
1063             return false;
1064         }
1065         
1066         return !!$column['IDENTITY'];
1067     }
1068     
1069     /**
1070      * converts record into raw data for adapter
1071      *
1072      * @param  Tinebase_Record_Abstract $_record
1073      * @return array
1074      */
1075     protected function _recordToRawData($_record)
1076     {
1077         $readOnlyFields = $_record->getReadOnlyFields();
1078         $raw = $_record->toArray(FALSE);
1079         foreach ($raw as $key => $value) {
1080             if ($value instanceof Tinebase_Record_Interface) {
1081                 $raw[$key] = $value->getId();
1082             }
1083             if (in_array($key, $readOnlyFields)) {
1084                 unset($raw[$key]);
1085             }
1086         }
1087         
1088         return $raw;
1089     }
1090     
1091     /**
1092      * prepare record data array
1093      * - replace int and bool values by Zend_Db_Expr
1094      *
1095      * @param array &$_recordArray
1096      * @return array with the prepared data
1097      */
1098     protected function _prepareData(&$_recordArray) 
1099     {
1100         
1101         foreach ($_recordArray as $key => $value) {
1102             if (is_bool($value)) {
1103                 $_recordArray[$key] = ($value) ? new Zend_Db_Expr('1') : new Zend_Db_Expr('0');
1104             } elseif (is_null($value)) {
1105                 $_recordArray[$key] = new Zend_Db_Expr('NULL');
1106             } elseif (is_int($value)) {
1107                 $_recordArray[$key] = new Zend_Db_Expr((string) $value);
1108             }
1109         }
1110     }
1111     
1112     /**
1113      * update foreign key values
1114      * 
1115      * @param string $_mode create|update
1116      * @param Tinebase_Record_Abstract $_record
1117      */
1118     protected function _updateForeignKeys($_mode, Tinebase_Record_Abstract $_record)
1119     {
1120         if (! empty($this->_foreignTables)) {
1121             
1122             foreach ($this->_foreignTables as $modelName => $join) {
1123                 
1124                 if (! (isset($join['field']) || array_key_exists('field', $join))) {
1125                     continue;
1126                 }
1127                 
1128                 $idsToAdd    = array();
1129                 $idsToRemove = array();
1130                 
1131                 if (!empty($_record->$modelName)) {
1132                     $idsToAdd = $this->_getIdsFromMixed($_record->$modelName);
1133                 }
1134                 
1135                 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1136                 
1137                 if ($_mode == 'update') {
1138                     $select = $this->_db->select();
1139         
1140                     $select->from(array($join['table'] => $this->_tablePrefix . $join['table']), array($join['field']))
1141                         ->where($this->_db->quoteIdentifier($join['table'] . '.' . $join['joinOn']) . ' = ?', $_record->getId());
1142                     
1143                     Tinebase_Backend_Sql_Abstract::traitGroup($select);
1144                     
1145                     $stmt = $this->_db->query($select);
1146                     $currentIds = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
1147                     $stmt->closeCursor();
1148                     
1149                     $idsToRemove = array_diff($currentIds, $idsToAdd);
1150                     $idsToAdd    = array_diff($idsToAdd, $currentIds);
1151                 }
1152                 
1153                 if (!empty($idsToRemove)) {
1154                     $where = '(' . 
1155                         $this->_db->quoteInto($this->_db->quoteIdentifier($this->_tablePrefix . $join['table'] . '.' . $join['joinOn']) . ' = ?', $_record->getId()) .
1156                         ' AND ' . 
1157                         $this->_db->quoteInto($this->_db->quoteIdentifier($this->_tablePrefix . $join['table'] . '.' . $join['field']) . ' IN (?)', $idsToRemove) .
1158                     ')';
1159                         
1160                     $this->_db->delete($this->_tablePrefix . $join['table'], $where);
1161                 }
1162                 
1163                 foreach ($idsToAdd as $id) {
1164                     $recordArray = array (
1165                         $join['joinOn'] => $_record->getId(),
1166                         $join['field']  => $id
1167                     );
1168                     $this->_db->insert($this->_tablePrefix . $join['table'], $recordArray);
1169                 }
1170                     
1171                 
1172                 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1173             }
1174         }
1175     }
1176
1177     /**
1178      * convert recordset, array of ids or records to array of ids
1179      * 
1180      * @param  mixed  $_mixed
1181      * @return array
1182      */
1183     protected function _getIdsFromMixed($_mixed)
1184     {
1185         if ($_mixed instanceof Tinebase_Record_RecordSet) { // Record set
1186             $ids = $_mixed->getArrayOfIds();
1187             
1188         } elseif (is_array($_mixed)) { // array
1189             foreach ($_mixed as $mixed) {
1190                 if ($mixed instanceof Tinebase_Record_Abstract) {
1191                     $ids[] = $mixed->getId();
1192                 } else {
1193                     $ids[] = $mixed;
1194                 }
1195             }
1196             
1197         } else { // string
1198             $ids[] = $_mixed instanceof Tinebase_Record_Abstract ? $_mixed->getId() : $_mixed;
1199         }
1200         
1201         return $ids;
1202     }
1203     
1204     /**
1205      * do something after creation of record
1206      * 
1207      * @param Tinebase_Record_Abstract $_newRecord
1208      * @param Tinebase_Record_Abstract $_recordToCreate
1209      * @return void
1210      */
1211     protected function _inspectAfterCreate(Tinebase_Record_Abstract $_newRecord, Tinebase_Record_Abstract $_recordToCreate)
1212     {
1213     }
1214     
1215     /**
1216      * Updates existing entry
1217      *
1218      * @param Tinebase_Record_Interface $_record
1219      * @throws Tinebase_Exception_Record_Validation|Tinebase_Exception_InvalidArgument
1220      * @return Tinebase_Record_Interface Record|NULL
1221      */
1222     public function update(Tinebase_Record_Interface $_record) 
1223     {
1224         $identifier = $_record->getIdProperty();
1225         
1226         if (!$_record instanceof $this->_modelName) {
1227             throw new Tinebase_Exception_InvalidArgument('invalid model type: $_record is instance of "' . get_class($_record) . '". but should be instance of ' . $this->_modelName);
1228         }
1229         
1230         $_record->isValid(TRUE);
1231         
1232         $id = $_record->getId();
1233
1234         $recordArray = $this->_recordToRawData($_record);
1235         $recordArray = array_intersect_key($recordArray, $this->getSchema());
1236         
1237         $this->_prepareData($recordArray);
1238         
1239         $where  = array(
1240             $this->_db->quoteInto($this->_db->quoteIdentifier($identifier) . ' = ?', $id),
1241         );
1242         
1243         $this->_db->update($this->_tablePrefix . $this->_tableName, $recordArray, $where);
1244
1245         // update custom fields
1246         if ($_record->has('customfields')) {
1247             Tinebase_CustomField::getInstance()->saveRecordCustomFields($_record);
1248         }
1249         
1250         $this->_updateForeignKeys('update', $_record);
1251         
1252         $result = $this->get($id, true);
1253         
1254         return $result;
1255     }
1256     
1257     /**
1258      * Updates multiple entries
1259      *
1260      * @param array $_ids to update
1261      * @param array $_data
1262      * @return integer number of affected rows
1263      * @throws Tinebase_Exception_Record_Validation|Tinebase_Exception_InvalidArgument
1264      */
1265     public function updateMultiple($_ids, $_data) 
1266     {
1267         if (empty($_ids)) {
1268             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1269                 . ' No records updated.');
1270             return 0;
1271         }
1272         
1273         // separate CustomFields
1274         
1275         $myFields = array();
1276         $customFields = array();
1277         
1278         foreach($_data as $key => $value) {
1279             if(stristr($key, '#')) $customFields[substr($key,1)] = $value;
1280             else $myFields[$key] = $value;
1281         }
1282         
1283         // handle CustomFields
1284         
1285         if(count($customFields)) {
1286             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1287                 . ' CustomFields found.');
1288             Tinebase_CustomField::getInstance()->saveMultipleCustomFields($this->_modelName, $_ids, $customFields);
1289         }
1290         
1291         // handle StdFields
1292         
1293         if(!count($myFields)) { return 0; } 
1294
1295         $identifier = $this->_getRecordIdentifier();
1296         
1297         $recordArray = $myFields;
1298         $recordArray = array_intersect_key($recordArray, $this->getSchema());
1299         
1300         $this->_prepareData($recordArray);
1301                 
1302         $where  = array(
1303             $this->_db->quoteInto($this->_db->quoteIdentifier($identifier) . ' IN (?)', $_ids),
1304         );
1305         
1306         //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($where, TRUE));
1307         
1308         return $this->_db->update($this->_tablePrefix . $this->_tableName, $recordArray, $where);
1309     }
1310     
1311     /**
1312       * Deletes entries
1313       * 
1314       * @param string|integer|Tinebase_Record_Interface|array $_id
1315       * @return void
1316       * @return int The number of affected rows.
1317       */
1318     public function delete($_id)
1319     {
1320         if (empty($_id)) {
1321             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' No records deleted.');
1322             return 0;
1323         }
1324         
1325         $idArray = (! is_array($_id)) ? array(Tinebase_Record_Abstract::convertId($_id, $this->_modelName)) : $_id;
1326         $identifier = $this->_getRecordIdentifier();
1327         
1328         $where = array(
1329             $this->_db->quoteInto($this->_db->quoteIdentifier($identifier) . ' IN (?)', $idArray)
1330         );
1331         
1332         return $this->_db->delete($this->_tablePrefix . $this->_tableName, $where);
1333     }
1334     
1335     /**
1336      * delete rows by property
1337      * 
1338      * @param string|array $_value
1339      * @param string $_property
1340      * @param string $_operator (equals|in)
1341      * @return integer The number of affected rows.
1342      * @throws Tinebase_Exception_InvalidArgument
1343      */
1344     public function deleteByProperty($_value, $_property, $_operator = 'equals')
1345     {
1346         $schema = $this->getSchema();
1347         
1348         if (! (isset($schema[$_property]) || array_key_exists($_property, $schema))) {
1349             throw new Tinebase_Exception_InvalidArgument('Property ' . $_property . ' does not exist in table ' . $this->_tableName);
1350         }
1351         
1352         switch ($_operator) {
1353             case 'equals':
1354                 $op = ' = ?';
1355                 break;
1356             case 'in':
1357                 $op = ' IN (?)';
1358                 $_value = (array) $_value;
1359                 break;
1360             default:
1361                 throw new Tinebase_Exception_InvalidArgument('Invalid operator: ' . $_operator);
1362         }
1363         $where = array(
1364             $this->_db->quoteInto($this->_db->quoteIdentifier($_property) . $op, $_value)
1365         );
1366         
1367         return $this->_db->delete($this->_tablePrefix . $this->_tableName, $where);
1368     }
1369     
1370     /*************************** foreign record fetchers *******************************/
1371     
1372     /**
1373      * appends foreign record (1:1 relation) to given record
1374      *
1375      * @param Tinebase_Record_Abstract      $_record            Record to append the foreign record to
1376      * @param string                        $_appendTo          Property in the record where to append the foreign record to
1377      * @param string                        $_recordKey         Property in the record where the foreign key value is in
1378      * @param string                        $_foreignKey        Key property in foreign table of the record to append
1379      * @param Tinebase_Backend_Sql_Abstract $_foreignBackend    Foreign table backend 
1380      */
1381     public function appendForeignRecordToRecord($_record, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
1382     {
1383         try {
1384             $_record->$_appendTo = $_foreignBackend->getByProperty($_record->$_recordKey, $_foreignKey);
1385         } catch (Tinebase_Exception_NotFound $e) {
1386             $_record->$_appendTo = NULL;
1387         }
1388     }
1389     
1390     /**
1391      * appends foreign recordSet (1:n relation) to given record
1392      *
1393      * @param Tinebase_Record_Abstract      $_record            Record to append the foreign records to
1394      * @param string                        $_appendTo          Property in the record where to append the foreign records to
1395      * @param string                        $_recordKey         Property in the record where the foreign key value is in
1396      * @param string                        $_foreignKey        Key property in foreign table of the records to append
1397      * @param Tinebase_Backend_Sql_Abstract $_foreignBackend    Foreign table backend 
1398      */
1399     public function appendForeignRecordSetToRecord($_record, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
1400     {
1401         $_record->$_appendTo = $_foreignBackend->getMultipleByProperty($_record->$_recordKey, $_foreignKey);
1402     }
1403     
1404     /**
1405      * appends foreign record (1:1/n:1 relation) to given recordSet
1406      *
1407      * @param Tinebase_Record_RecordSet     $_recordSet         Records to append the foreign record to
1408      * @param string                        $_appendTo          Property in the records where to append the foreign record to
1409      * @param string                        $_recordKey         Property in the records where the foreign key value is in
1410      * @param string                        $_foreignKey        Key property in foreign table of the record to append
1411      * @param Tinebase_Backend_Sql_Abstract $_foreignBackend    Foreign table backend 
1412      */
1413     public function appendForeignRecordToRecordSet($_recordSet, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
1414     {
1415         $allForeignRecords = $_foreignBackend->getMultipleByProperty($_recordSet->$_recordKey, $_foreignKey);
1416         foreach ($_recordSet as $record) {
1417             $record->$_appendTo = $allForeignRecords->filter($_foreignKey, $record->$_recordKey)->getFirstRecord();
1418         }
1419     }
1420     
1421     /**
1422      * appends foreign recordSet (1:n relation) to given recordSet
1423      *
1424      * @param Tinebase_Record_RecordSet     $_recordSet         Records to append the foreign records to
1425      * @param string                        $_appendTo          Property in the records where to append the foreign records to
1426      * @param string                        $_recordKey         Property in the records where the foreign key value is in
1427      * @param string                        $_foreignKey        Key property in foreign table of the records to append
1428      * @param Tinebase_Backend_Sql_Abstract $_foreignBackend    Foreign table backend 
1429      */
1430     public function appendForeignRecordSetToRecordSet($_recordSet, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
1431     {
1432         $idxRecordKeyMap = $_recordSet->$_recordKey;
1433         $recordKeyIdxMap = array_flip($idxRecordKeyMap);
1434         $allForeignRecords = $_foreignBackend->getMultipleByProperty($idxRecordKeyMap, $_foreignKey);
1435         $foreignRecordsClassName = $allForeignRecords->getRecordClassName();
1436
1437         foreach ($_recordSet as $record) {
1438             $record->$_appendTo = new Tinebase_Record_RecordSet($foreignRecordsClassName);
1439         }
1440
1441         foreach($allForeignRecords as $foreignRecord) {
1442             $record = $_recordSet->getByIndex($recordKeyIdxMap[$foreignRecord->$_foreignKey]);
1443             $foreignRecordSet = $record->$_appendTo;
1444
1445             $foreignRecordSet->addRecord($foreignRecord);
1446         }
1447     }
1448     
1449     /*************************** other ************************************/
1450     
1451     /**
1452      * get table name
1453      *
1454      * @return string
1455      */
1456     public function getTableName()
1457     {
1458         return $this->_tableName;
1459     }
1460     
1461     /**
1462      * get foreign table information
1463      *
1464      * @return array
1465      */
1466     public function getForeignTables()
1467     {
1468         return $this->_foreignTables;
1469     }
1470     
1471     /**
1472      * get table prefix
1473      *
1474      * @return string
1475      */
1476     public function getTablePrefix()
1477     {
1478         return $this->_tablePrefix;
1479     }
1480     
1481     /**
1482      * get table identifier
1483      * 
1484      * @return string
1485      */
1486     public function getIdentifier()
1487     {
1488         return $this->_identifier;
1489     }
1490     
1491     /**
1492      * get db adapter
1493      *
1494      * @return Zend_Db_Adapter_Abstract
1495      * @throws Tinebase_Exception_Backend_Database
1496      */
1497     public function getAdapter()
1498     {
1499         if (! $this->_db instanceof Zend_Db_Adapter_Abstract) {
1500             throw new Tinebase_Exception_Backend_Database('Could not fetch database adapter');
1501         }
1502         
1503         return $this->_db;
1504     }
1505     
1506     /**
1507      * get dbCommand class
1508      *
1509      * @return Tinebase_Backend_Sql_Command_Interface
1510      * @throws Tinebase_Exception_Backend_Database
1511      */
1512     public function getDbCommand()
1513     {
1514         if (! $this->_dbCommand instanceof Tinebase_Backend_Sql_Command_Interface) {
1515             throw new Tinebase_Exception_Backend_Database('Could not fetch database command class');
1516         }
1517         
1518         return $this->_dbCommand;
1519     }
1520     
1521     /**
1522      * Public service for grouping treatment
1523      * 
1524      * @param string $tablePrefix
1525      * @param Zend_Db_Select $select
1526      */
1527     public static function traitGroup(Zend_Db_Select $select)
1528     {
1529         // not needed for MySQL backends
1530         if ($select->getAdapter() instanceof Zend_Db_Adapter_Pdo_Mysql) {
1531             return;
1532         }
1533         
1534         $group = $select->getPart(Zend_Db_Select::GROUP);
1535         
1536         if (empty($group)) {
1537             return;
1538         }
1539         
1540         $columns        = $select->getPart(Zend_Db_Select::COLUMNS);
1541         $updatedColumns = array();
1542         
1543         //$column is an array where 0 is table, 1 is field and 2 is alias
1544         foreach ($columns as $key => $column) {
1545             if ($column[1] instanceof Zend_Db_Expr) {
1546                 if (preg_match('/^\(.*\)/', $column[1])) {
1547                     $updatedColumns[] = array($column[0], new Zend_Db_Expr("MIN(" . $column[1] . ")"), $column[2]);
1548                 } else {
1549                     $updatedColumns[] = $column;
1550                 }
1551                 
1552                 continue;
1553             }
1554             
1555             if (preg_match('/^\(.*\)/', $column[1])) {
1556                 $updatedColumns[] = array($column[0], new Zend_Db_Expr("MIN(" . $column[1] . ")"), $column[2]);
1557                 
1558                 continue;
1559             }
1560             
1561             // resolve * to single columns
1562             if ($column[1] == '*') {
1563
1564                 $tableFields = Tinebase_Db_Table::getTableDescriptionFromCache(SQL_TABLE_PREFIX . $column[0], $select->getAdapter());
1565                 foreach ($tableFields as $columnName => $schema) {
1566                     
1567                     // adds columns into group by clause (table.field)
1568                     // checks if field has a function (that must be an aggregation)
1569                     $fieldName = "{$column[0]}.$columnName";
1570                     
1571                     if (in_array($fieldName, $group)) {
1572                         $updatedColumns[] = array($column[0], $fieldName, $columnName);
1573                     } else {
1574                         // any selected field which is not in the group by clause must have an aggregate function
1575                         // we choose MIN() as default. In practice the affected columns will have only one value anyways.
1576                         $updatedColumns[] = array($column[0], new Zend_Db_Expr("MIN(" . $select->getAdapter()->quoteIdentifier($fieldName) . ")"), $columnName);
1577                     }
1578                 }
1579                 
1580                 continue;
1581             }
1582             
1583             $fieldName = $column[0] . '.' . $column[1];
1584             
1585             if (in_array($fieldName, $group)) {
1586                 $updatedColumns[] = $column;
1587             } else {
1588                 // any selected field which is not in the group by clause must have an aggregate function
1589                 // we choose MIN() as default. In practice the affected columns will have only one value anyways.
1590                 $updatedColumns[] = array($column[0], new Zend_Db_Expr("MIN(" . $select->getAdapter()->quoteIdentifier($fieldName) . ")"), $column[2] ? $column[2] : $column[1]);
1591             }
1592         }
1593         
1594         $select->reset(Zend_Db_Select::COLUMNS);
1595         
1596         foreach ($updatedColumns as $column) {
1597             $select->columns(!empty($column[2]) ? array($column[2] => $column[1]) : $column[1], $column[0]);
1598         }
1599
1600         // add order by columns to group by
1601         $order = $select->getPart(Zend_Db_Select::ORDER);
1602         
1603         foreach($order as $column) {
1604             $field = $column[0];
1605             
1606             if (preg_match('/.*\..*/',$field) && !in_array($field,$group)) {
1607                 // adds column into group by clause (table.field)
1608                 $group[] = $field;
1609             }
1610         }
1611         
1612         $select->reset(Zend_Db_Select::GROUP);
1613         
1614         $select->group($group);
1615     }
1616
1617     /**
1618      * sets etags, expects ids as keys and etags as value
1619      *
1620      * @param array $etags
1621      * 
1622      * @todo maybe we should find a better place for the etag functions as this is currently only used in Calendar + Tasks
1623      */
1624     public function setETags(array $etags)
1625     {
1626         foreach ($etags as $id => $etag) {
1627             $where  = array(
1628                 $this->_db->quoteInto($this->_db->quoteIdentifier($this->_identifier) . ' = ?', $id),
1629             );
1630             $this->_db->update($this->_tablePrefix . $this->_tableName, array('etag' => $etag), $where);
1631         }
1632     }
1633     
1634     /**
1635      * checks if there is an event with this id and etag, or an event with the same id
1636      *
1637      * @param string $id
1638      * @param string $etag
1639      * @return boolean
1640      * @throws Tinebase_Exception_NotFound
1641      */
1642     public function checkETag($id, $etag)
1643     {
1644         $select = $this->_db->select();
1645         $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName), $this->_identifier);
1646         $select->where($this->_db->quoteIdentifier($this->_identifier) . ' = ?', $id);
1647         $select->orWhere($this->_db->quoteIdentifier('uid') . ' = ?', $id);
1648     
1649         $stmt = $select->query();
1650         $queryResult = $stmt->fetch();
1651         $stmt->closeCursor();
1652     
1653         if ($queryResult === false) {
1654             throw new Tinebase_Exception_NotFound('no record with id ' . $id .' found');
1655         }
1656     
1657         $select->where($this->_db->quoteIdentifier('etag') . ' = ?', $etag);
1658         $stmt = $select->query();
1659         $queryResult = $stmt->fetch();
1660         $stmt->closeCursor();
1661     
1662         return ($queryResult !== false);
1663     }
1664     
1665     /**
1666      * return etag set for given container
1667      * 
1668      * @param string $containerId
1669      * @return multitype:Ambigous <mixed, Ambigous <string, boolean, mixed>>
1670      */
1671     public function getEtagsForContainerId($containerId)
1672     {
1673         $select = $this->_db->select();
1674         $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName), array($this->_identifier, 'etag', 'uid'));
1675         $select->where($this->_db->quoteIdentifier('container_id') . ' = ?', $containerId);
1676         $select->where($this->_db->quoteIdentifier('is_deleted') . ' = ?', 0);
1677     
1678         $stmt = $select->query();
1679         $queryResult = $stmt->fetchAll();
1680     
1681         $result = array();
1682         foreach ($queryResult as $row) {
1683             $result[$row['id']] = $row;
1684         }
1685         return $result;
1686     }
1687     
1688     /**
1689      * save value in in-class cache
1690      * 
1691      * @param string $method
1692      * @param string $cacheId
1693      * @param string $value
1694      * @return Tinebase_Cache_PerRequest
1695      */
1696     public function saveInClassCache($method, $cacheId, $value)
1697     {
1698         return Tinebase_Cache_PerRequest::getInstance()->save($this->_getInClassCacheIdentifier(), $method, $cacheId, $value);
1699     }
1700     
1701     /**
1702      * load value from in-class cache
1703      * 
1704      * @param string $method
1705      * @param string $cacheId
1706      * @return mixed
1707      */
1708     public function loadFromClassCache($method, $cacheId)
1709     {
1710         return Tinebase_Cache_PerRequest::getInstance()->load($this->_getInClassCacheIdentifier(), $method, $cacheId);
1711     }
1712     
1713     /**
1714      * reset class cache
1715      * 
1716      * @param string $key
1717      * @return Tinebase_Model_Filter_Abstract
1718      */
1719     public function resetClassCache($method = null)
1720     {
1721         Tinebase_Cache_PerRequest::getInstance()->resetCache($this->_getInClassCacheIdentifier(), $method);
1722         
1723         return $this;
1724     }
1725     
1726     /**
1727      * return class cache identifier
1728      * 
1729      * if class extend parent class, you can define the name of the parent class here
1730      * 
1731      * @return string
1732      */
1733     public function _getInClassCacheIdentifier()
1734     {
1735         if (isset($this->_classCacheIdentifier)) {
1736             return $this->_classCacheIdentifier;
1737         }
1738         
1739         return get_class($this);
1740     }
1741 }