0543c89735b3b15641a352904fd62498e9a45118
[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-2011 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      * use subselect in searchCount fn
74      *
75      * @var boolean
76      */
77     protected $_useSubselectForCount = TRUE;
78     
79     /**
80      * Identifier
81      *
82      * @var string
83      */
84     protected $_identifier = 'id';
85     
86     /**
87      * @var Zend_Db_Adapter_Abstract
88      */
89     protected $_db;
90     
91     /**
92      * @var Tinebase_Backend_Sql_Command_Interface
93      */
94     protected $_dbCommand;
95     
96     /**
97      * schema of the table
98      *
99      * @var array
100      */
101     protected $_schema = NULL;
102     
103     /**
104      * foreign tables 
105      * name => array(table, joinOn, field)
106      *
107      * @var array
108      */
109     protected $_foreignTables = array();
110     
111     /**
112      * additional search count columns
113      * 
114      * @var array
115      */
116     protected $_additionalSearchCountCols = array();
117     
118     /**
119      * default secondary sort criteria
120      * 
121      * @var string
122      */
123     protected $_defaultSecondarySort = NULL;
124     
125     /**
126      * default column(s) for count
127      * 
128      * @var string
129      */
130     protected $_defaultCountCol = '*';
131     
132     /**
133      * the constructor
134      * 
135      * allowed options:
136      *  - modelName
137      *  - tableName
138      *  - tablePrefix
139      *  - modlogActive
140      *  - useSubselectForCount
141      *  
142      * @param Zend_Db_Adapter_Abstract $_db (optional)
143      * @param array $_options (optional)
144      * @throws Tinebase_Exception_Backend_Database
145      */
146     public function __construct($_dbAdapter = NULL, $_options = array())
147     {
148         $this->_db        = ($_dbAdapter instanceof Zend_Db_Adapter_Abstract) ? $_dbAdapter : Tinebase_Core::getDb();
149         $this->_dbCommand = Tinebase_Backend_Sql_Command::factory($this->_db);
150         
151         $this->_modelName            = array_key_exists('modelName', $_options)            ? $_options['modelName']    : $this->_modelName;
152         $this->_tableName            = array_key_exists('tableName', $_options)            ? $_options['tableName']    : $this->_tableName;
153         $this->_tablePrefix          = array_key_exists('tablePrefix', $_options)          ? $_options['tablePrefix']  : $this->_db->table_prefix;
154         $this->_modlogActive         = array_key_exists('modlogActive', $_options)         ? $_options['modlogActive'] : $this->_modlogActive;
155         $this->_useSubselectForCount = array_key_exists('useSubselectForCount', $_options) ? $_options['useSubselectForCount'] : $this->_useSubselectForCount;
156         
157         if (! ($this->_tableName && $this->_modelName)) {
158             throw new Tinebase_Exception_Backend_Database('modelName and tableName must be configured or given.');
159         }
160         if (! $this->_db) {
161             throw new Tinebase_Exception_Backend_Database('Database adapter must be configured or given.');
162         }
163         
164         try {
165             $this->_schema = Tinebase_Db_Table::getTableDescriptionFromCache($this->_tablePrefix . $this->_tableName, $this->_db);
166         } catch (Zend_Db_Adapter_Exception $zdae) {
167             throw new Tinebase_Exception_Backend_Database('Connection failed: ' . $zdae->getMessage());
168         }
169     }
170     
171     /*************************** getters and setters *********************************/
172     
173     /**
174      * sets modlog active flag
175      * 
176      * @param $_bool
177      * @return Tinebase_Backend_Sql_Abstract
178      */
179     public function setModlogActive($_bool)
180     {
181         $this->_modlogActive = (bool) $_bool;
182         return $this;
183     }
184     
185     /**
186      * checks if modlog is active or not
187      * 
188      * @return bool
189      */
190     public function getModlogActive()
191     {
192         return $this->_modlogActive;
193     }
194     
195     /*************************** get/search funcs ************************************/
196
197     /**
198      * Gets one entry (by id)
199      *
200      * @param integer|Tinebase_Record_Interface $_id
201      * @param $_getDeleted get deleted records
202      * @return Tinebase_Record_Interface
203      * @throws Tinebase_Exception_NotFound
204      */
205     public function get($_id, $_getDeleted = FALSE) 
206     {
207         if (empty($_id)) {
208             throw new Tinebase_Exception_NotFound('$_id can not be empty');
209         }
210         
211         $id = Tinebase_Record_Abstract::convertId($_id, $this->_modelName);
212         
213         return $this->getByProperty($id, $this->_identifier, $_getDeleted);
214     }
215
216     /**
217      * splits identifier if table name is given (i.e. for joined tables)
218      *
219      * @return string identifier name
220      */
221     protected function _getRecordIdentifier()
222     {
223         if (preg_match("/\./", $this->_identifier)) {
224             list($table, $identifier) = explode('.', $this->_identifier);
225         } else {
226             $identifier = $this->_identifier;
227         }
228         
229         return $identifier;
230     }
231
232     /**
233      * Gets one entry (by property)
234      *
235      * @param  mixed  $value
236      * @param  string $property
237      * @param  bool   $getDeleted
238      * @return Tinebase_Record_Interface
239      * @throws Tinebase_Exception_NotFound
240      */
241     public function getByProperty($value, $property = 'name', $getDeleted = FALSE) 
242     {
243         $select = $this->_getSelect('*', $getDeleted)
244             ->limit(1);
245         
246         if ($value !== NULL) {
247             $select->where($this->_db->quoteIdentifier($this->_tableName . '.' . $property) . ' = ?', $value);
248         } else {
249             $select->where($this->_db->quoteIdentifier($this->_tableName . '.' . $property) . ' IS NULL');
250         }
251         
252         Tinebase_Backend_Sql_Abstract::traitGroup($select);
253         
254         $stmt = $this->_db->query($select);
255         $queryResult = $stmt->fetch();
256         $stmt->closeCursor();
257         
258         if (!$queryResult) {
259             $messageValue = ($value !== NULL) ? $value : 'NULL';
260             throw new Tinebase_Exception_NotFound($this->_modelName . " record with $property = $messageValue not found!");
261         }
262         
263         $result = $this->_rawDataToRecord($queryResult);
264         
265         return $result;
266     }
267     
268     /**
269      * fetch a single property for all records defined in array of $ids
270      * 
271      * @param array|string $ids
272      * @param string $property
273      * @return array (key = id, value = property value)
274      */
275     public function getPropertyByIds($ids, $property)
276     {
277         $select = $this->_getSelect(array($property, $this->_identifier));
278         $select->where($this->_db->quoteIdentifier($this->_tableName . '.' . $this->_identifier) . ' IN (?)', (array) $ids);
279         Tinebase_Backend_Sql_Abstract::traitGroup($select);
280         
281         $stmt = $this->_db->query($select);
282         $queryResult = $stmt->fetchAll();
283         $stmt->closeCursor();
284         
285         $result = array();
286         foreach($queryResult as $row) {
287             $result[$row[$this->_identifier]] = $row[$property];
288         }
289         return $result;
290     }
291     
292     /**
293      * converts raw data from adapter into a single record
294      *
295      * @param  array $_rawData
296      * @return Tinebase_Record_Abstract
297      */
298     protected function _rawDataToRecord(array $_rawData)
299     {
300         $result = new $this->_modelName($_rawData, true);
301         
302         $this->_explodeForeignValues($result);
303         
304         return $result;
305     }
306     
307     /**
308      * explode foreign values
309      * 
310      * @param Tinebase_Record_Interface $_record
311      */
312     protected function _explodeForeignValues(Tinebase_Record_Interface $_record)
313     {
314         foreach (array_keys($this->_foreignTables) as $field) {
315             $isSingleValue = (array_key_exists('singleValue', $this->_foreignTables[$field]) && $this->_foreignTables[$field]['singleValue']);
316             if (! $isSingleValue) {
317                 $_record->{$field} = (! empty($_record->{$field})) ? explode(',', $_record->{$field}) : array();
318             }
319         }
320     }
321     
322     /**
323      * gets multiple entries (by property)
324      *
325      * @param  mixed  $_value
326      * @param  string $_property
327      * @param  bool   $_getDeleted
328      * @param  string $_orderBy        defaults to $_property
329      * @param  string $_orderDirection defaults to 'ASC'
330      * @return Tinebase_Record_RecordSet
331      */
332     public function getMultipleByProperty($_value, $_property='name', $_getDeleted = FALSE, $_orderBy = NULL, $_orderDirection = 'ASC')
333     {
334         $columnName = $this->_db->quoteIdentifier($this->_tableName . '.' . $_property);
335         $value = empty($_value) ? array('') : (array)$_value;
336         $orderBy = $this->_tableName . '.' . ($_orderBy ? $_orderBy : $_property);
337         
338         $select = $this->_getSelect('*', $_getDeleted)
339                        ->where($columnName . 'IN (?)', $value)
340                        ->order($orderBy . ' ' . $_orderDirection);
341         
342         Tinebase_Backend_Sql_Abstract::traitGroup($select);
343         
344         $stmt = $this->_db->query($select);
345         
346         $resultSet = $this->_rawDataToRecordSet($stmt->fetchAll());
347         $resultSet->addIndices(array($_property));
348         
349         return $resultSet;
350     }
351     
352     /**
353      * converts raw data from adapter into a set of records
354      *
355      * @param  array $_rawDatas of arrays
356      * @return Tinebase_Record_RecordSet
357      */
358     protected function _rawDataToRecordSet(array $_rawDatas)
359     {
360         $result = new Tinebase_Record_RecordSet($this->_modelName, $_rawDatas, true);
361         
362         if (! empty($this->_foreignTables)) {
363             foreach ($result as $record) {
364                 $this->_explodeForeignValues($record);
365             }
366         }
367         
368         return $result;
369     }
370     
371     /**
372      * Get multiple entries
373      *
374      * @param string|array $_id Ids
375      * @param array $_containerIds all allowed container ids that are added to getMultiple query
376      * @return Tinebase_Record_RecordSet
377      * 
378      * @todo get custom fields here as well
379      */
380     public function getMultiple($_id, $_containerIds = NULL) 
381     {
382         // filter out any emtpy values
383         $ids = array_filter((array) $_id, function($value) {
384             return !empty($value);
385         });
386         
387         if (empty($ids)) {
388             return new Tinebase_Record_RecordSet($this->_modelName);
389         }
390
391         // replace objects with their id's
392         foreach ($ids as &$id) {
393             if ($id instanceof Tinebase_Record_Interface) {
394                 $id = $id->getId();
395             }
396         }
397         
398         $select = $this->_getSelect();
399         $select->where($this->_db->quoteIdentifier($this->_tableName . '.' . $this->_identifier) . ' IN (?)', $ids);
400         
401         if ($_containerIds !== NULL && isset($this->_schema['container_id'])) {
402             if (empty($_containerIds)) {
403                 $select->where('1=0 /* insufficient grants */');
404             } else {
405                 $select->where($this->_db->quoteIdentifier($this->_tableName . '.container_id') . ' IN (?) /* add acl in getMultiple */', (array) $_containerIds);
406             }
407         }
408         
409         Tinebase_Backend_Sql_Abstract::traitGroup($select);
410         
411         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $select->__toString());
412         
413         $stmt = $this->_db->query($select);
414         $queryResult = $stmt->fetchAll();
415         
416         $result = $this->_rawDataToRecordSet($queryResult);
417         
418         return $result;
419     }
420     
421     /**
422      * Gets all entries
423      *
424      * @param string $_orderBy Order result by
425      * @param string $_orderDirection Order direction - allowed are ASC and DESC
426      * @throws Tinebase_Exception_InvalidArgument
427      * @return Tinebase_Record_RecordSet
428      */
429     public function getAll($_orderBy = NULL, $_orderDirection = 'ASC') 
430     {
431         $orderBy = $_orderBy ? $_orderBy : $this->_tableName . '.' . $this->_identifier;
432         
433         if(!in_array($_orderDirection, array('ASC', 'DESC'))) {
434             throw new Tinebase_Exception_InvalidArgument('$_orderDirection is invalid');
435         }
436         
437         $select = $this->_getSelect();
438         $select->order($orderBy . ' ' . $_orderDirection);
439         
440         Tinebase_Backend_Sql_Abstract::traitGroup($select);
441         
442         //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $select->__toString());
443             
444         $stmt = $this->_db->query($select);
445         $queryResult = $stmt->fetchAll();
446         
447         //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($queryResult, true));
448         
449         $result = $this->_rawDataToRecordSet($queryResult);
450         
451         return $result;
452     }
453     
454     /**
455      * Search for records matching given filter
456      *
457      * @param  Tinebase_Model_Filter_FilterGroup    $_filter
458      * @param  Tinebase_Model_Pagination            $_pagination
459      * @param  array|string|boolean                 $_cols columns to get, * per default / use self::IDCOL or TRUE to get only ids
460      * @return Tinebase_Record_RecordSet|array
461      */
462     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL, $_cols = '*')
463     {
464         if ($_pagination === NULL) {
465             $_pagination = new Tinebase_Model_Pagination(NULL, TRUE);
466         }
467         
468         // legacy: $_cols param was $_onlyIds (boolean) ...
469         if ($_cols === TRUE) {
470             $_cols = self::IDCOL;
471         } else if ($_cols === FALSE) {
472             $_cols = '*';
473         }
474         
475         // (1) get ids or id/value pair
476         list($colsToFetch, $getIdValuePair) = $this->_getColumnsToFetch($_cols, $_filter, $_pagination);
477         $select = $this->_getSelect($colsToFetch);
478         if ($_filter !== NULL) {
479             $this->_addFilter($select, $_filter);
480         }
481         $this->_addSecondarySort($_pagination);
482         $_pagination->appendPaginationSql($select);
483
484         Tinebase_Backend_Sql_Abstract::traitGroup($select);
485
486         if ($getIdValuePair) {
487             return $this->_fetch($select, self::FETCH_MODE_PAIR);
488         } else {
489             $ids = $this->_fetch($select);
490         }
491         
492         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Fetched ' . count($ids) .' ids.');
493         
494         if ($_cols === self::IDCOL) {
495             return $ids;
496         } else if (empty($ids)) {
497             return new Tinebase_Record_RecordSet($this->_modelName);
498         } else {
499             // (2) get other columns and do joins
500             $select = $this->_getSelect($_cols);
501             $this->_addWhereIdIn($select, $ids);
502             $_pagination->appendSort($select);
503             
504             $rows = $this->_fetch($select, self::FETCH_ALL);
505             
506             return $this->_rawDataToRecordSet($rows);
507         }
508     }
509
510     /**
511      * add the fields to search for to the query
512      *
513      * @param  Zend_Db_Select                       $_select current where filter
514      * @param  Tinebase_Model_Filter_FilterGroup    $_filter the string to search for
515      * @return void
516      */
517     protected function _addFilter(Zend_Db_Select $_select, /*Tinebase_Model_Filter_FilterGroup */$_filter)
518     {
519         Tinebase_Backend_Sql_Filter_FilterGroup::appendFilters($_select, $_filter, $this);
520     }
521     
522     /**
523      * Gets total count of search with $_filter
524      * 
525      * @param Tinebase_Model_Filter_FilterGroup $_filter
526      * @return int
527      */
528     public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter)
529     {
530         $defaultCountCol = $this->_defaultCountCol == '*' ?  '*' : $this->_db->quoteIdentifier($this->_defaultCountCol);
531         $searchCountCols = array_merge(array('count' => 'COUNT(' . $defaultCountCol . ')'), $this->_additionalSearchCountCols);
532         
533         if ($this->_useSubselectForCount) {
534             // use normal search query as subselect to get count -> select count(*) from (select [...]) as count
535             $subselectCols = (count($searchCountCols) === 1) 
536                 ? array_merge(array_keys($this->_foreignTables), array($this->_defaultCountCol)) : '*';
537             
538             $select = $this->_getSelect($subselectCols);
539             $this->_addFilter($select, $_filter);
540             Tinebase_Backend_Sql_Abstract::traitGroup($select);
541             $countSelect = $this->_db->select()->from($select, $searchCountCols);
542             
543         } else {
544             $countSelect = $this->_getSelect($searchCountCols);
545             $this->_addFilter($countSelect, $_filter);
546         }
547         
548         Tinebase_Backend_Sql_Abstract::traitGroup($countSelect);
549         
550         #if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $countSelect);
551         
552         if (! empty($this->_additionalSearchCountCols)) {
553             $result = $this->_db->fetchRow($countSelect);
554         } else {
555             $result = $this->_db->fetchOne($countSelect);
556         }
557         
558         return $result;
559     }
560     
561     /**
562      * returns columns to fetch in first query and if an id/value pair is requested 
563      * 
564      * @param array|string $_cols
565      * @param Tinebase_Model_Filter_FilterGroup $_filter
566      * @param Tinebase_Model_Pagination $_pagination
567      * @return array
568      */
569     protected function _getColumnsToFetch($_cols, Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL)
570     {
571         $getIdValuePair = FALSE;
572
573         if ($_cols === '*') {
574             $colsToFetch = array('id' => self::IDCOL);
575         } else {
576             $colsToFetch = (array) $_cols;
577             
578             if (in_array(self::IDCOL, $colsToFetch) && count($colsToFetch) == 2) {
579                 // id/value pair requested
580                 $getIdValuePair = TRUE;
581             } else if (! in_array(self::IDCOL, $colsToFetch) && count($colsToFetch) == 1) {
582                 // only one non-id column was requested -> add id and treat it like id/value pair
583                 array_push($colsToFetch, self::IDCOL);
584                 $getIdValuePair = TRUE;
585             } else {
586                 $colsToFetch = array('id' => self::IDCOL);
587             }
588         }
589         
590         if ($_filter !== NULL) {
591             $colsToFetch = $this->_addFilterColumns($colsToFetch, $_filter);
592         }
593         
594         foreach((array) $_pagination->sort as $sort) {
595             if (! array_key_exists($sort, $colsToFetch)) {
596                 $colsToFetch[$sort] = (substr_count($sort, $this->_tableName) === 0) ? $this->_tableName . '.' . $sort : $sort;
597             }
598         }
599         
600         return array($colsToFetch, $getIdValuePair);
601     }
602     
603     /**
604      * add columns from filter
605      * 
606      * @param array $_colsToFetch
607      * @param Tinebase_Model_Filter_FilterGroup $_filter
608      * @return array
609      */
610     protected function _addFilterColumns($_colsToFetch, Tinebase_Model_Filter_FilterGroup $_filter)
611     {
612         // need to ask filter if it needs additional columns
613         $filterCols = $_filter->getRequiredColumnsForSelect();
614         foreach ($filterCols as $key => $filterCol) {
615             if (! array_key_exists($key, $_colsToFetch)) {
616                 $_colsToFetch[$key] = $filterCol;
617             }
618         }
619         
620         return $_colsToFetch;
621     }
622     
623     /**
624      * add default secondary sort criteria
625      * 
626      * @param Tinebase_Model_Pagination $_pagination
627      */
628     protected function _addSecondarySort(Tinebase_Model_Pagination $_pagination)
629     {
630         if (! empty($this->_defaultSecondarySort)) {
631             if (! is_array($_pagination->sort) || ! in_array($this->_defaultSecondarySort, $_pagination->sort)) {
632                 $_pagination->sort = array_merge((array)$_pagination->sort, array($this->_defaultSecondarySort));
633             }
634         }
635     }
636     
637     /**
638      * adds 'id in (...)' where stmt
639      * 
640      * @param Zend_Db_Select $_select
641      * @param string|array $_ids
642      * @return Zend_Db_Select
643      */
644     protected function _addWhereIdIn(Zend_Db_Select $_select, $_ids)
645     {
646         $_select->where($this->_db->quoteInto($this->_db->quoteIdentifier($this->_tableName . '.' . $this->_identifier) . ' in (?)', (array) $_ids));
647         
648         return $_select;
649     }
650     
651     /**
652      * fetch rows from db
653      * 
654      * @param Zend_Db_Select $_select
655      * @param string $_mode
656      * @return array
657      */
658     protected function _fetch(Zend_Db_Select $_select, $_mode = self::FETCH_MODE_SINGLE)
659     {
660         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $_select->__toString());
661         
662         Tinebase_Backend_Sql_Abstract::traitGroup($_select);
663         
664         $stmt = $this->_db->query($_select);
665         
666         if ($_mode === self::FETCH_ALL) {
667             $result = (array) $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
668         } else {
669             $result = array();
670             while ($row = $stmt->fetch(Zend_Db::FETCH_NUM)) {
671                 if ($_mode === self::FETCH_MODE_SINGLE) {
672                     $result[] = $row[0];
673                 } else if ($_mode === self::FETCH_MODE_PAIR) {
674                     $result[$row[0]] = $row[1];
675                 }
676             }
677         }
678         
679         return $result;
680     }
681     
682     /**
683      * get the basic select object to fetch records from the database
684      *  
685      * @param array|string $_cols columns to get, * per default
686      * @param boolean $_getDeleted get deleted records (if modlog is active)
687      * @return Zend_Db_Select
688      */
689     protected function _getSelect($_cols = '*', $_getDeleted = FALSE)
690     {
691         if ($_cols !== '*' ) {
692             $cols = array();
693             // make sure cols is an array, prepend tablename and fix keys
694             foreach ((array) $_cols as $id => $col) {
695                 $key = (is_numeric($id)) ? ($col === self::IDCOL) ? $this->_identifier : $col : $id;
696                 $cols[$key] = ($col === self::IDCOL) ? $this->_tableName . '.' . $this->_identifier : $col;
697             }
698         } else {
699             $cols = '*';
700         }
701         
702         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($cols, TRUE));
703         
704         $select = $this->getAdapter()->select();
705         $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName), $cols);
706         
707         if (!$_getDeleted && $this->_modlogActive) {
708             // don't fetch deleted objects
709             $select->where($this->_db->quoteIdentifier($this->_tableName . '.is_deleted') . ' = 0');
710         }
711         
712         $this->_addForeignTableJoins($select, $cols);
713         
714         return $select;
715     }
716     
717     /**
718      * add foreign table joins
719      * 
720      * @param Zend_Db_Select $_select
721      * @param array|string $_cols columns to get, * per default
722      * 
723      * @todo find a way to preserve columns if needed without the need for the preserve setting
724      * @todo get joins from Zend_Db_Select before trying to join the same tables twice (+ remove try/catch)
725      */
726     protected function _addForeignTableJoins(Zend_Db_Select $_select, $_cols, $_groupBy = NULL)
727     {
728         if (! empty($this->_foreignTables)) {
729             $groupBy = ($_groupBy !== NULL) ? $_groupBy : $this->_tableName . '.' . $this->_identifier;
730             $_select->group($groupBy);
731             
732             $cols = (array) $_cols;
733             foreach ($this->_foreignTables as $foreignColumn => $join) {
734                 // only join if field is in cols
735                 if (in_array('*', $cols) || array_key_exists($foreignColumn, $cols)) {
736                     if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' foreign column: ' . $foreignColumn);
737                     
738                     $selectArray = (array_key_exists('select', $join))
739                         ? $join['select'] 
740                         : ((array_key_exists('field', $join) && (! array_key_exists('singleValue', $join) || ! $join['singleValue']))
741                             ? array($foreignColumn => $this->_dbCommand->getAggregate($join['table'] . '.' . $join['field']))
742                             : array($foreignColumn => $join['table'] . '.id'));
743                     $joinId = (array_key_exists('joinId', $join)) ? $join['joinId'] : $this->_identifier;
744                     
745                     $this->_removeColFromSelect($_select, $cols, $foreignColumn);
746                     
747                     try {
748                         $_select->joinLeft(
749                             /* table  */ array($join['table'] => $this->_tablePrefix . $join['table']), 
750                             /* on     */ $this->_db->quoteIdentifier($this->_tableName . '.' . $joinId) . ' = ' . $this->_db->quoteIdentifier($join['table'] . '.' . $join['joinOn']),
751                             /* select */ $selectArray
752                         );
753                         // need to add it to cols to prevent _removeColFromSelect from removing it
754                         if (array_key_exists('preserve', $join) && $join['preserve'] && array_key_exists($foreignColumn, $selectArray)) {
755                             $cols[$foreignColumn] = $selectArray[$foreignColumn];
756                         }
757                     } catch (Zend_Db_Select_Exception $zdse) {
758                         $_select->columns($selectArray, $join['table']);
759                     }
760                 }
761             }
762         }
763     }
764     
765     /**
766      * remove column from select to avoid duplicates 
767      * 
768      * @param Zend_Db_Select $_select
769      * @param array|string $_cols
770      * @param string $_column
771      */
772     protected function _removeColFromSelect(Zend_Db_Select $_select, &$_cols, $_column)
773     {
774         if (! is_array($_cols)) {
775             return;
776         }
777         
778         foreach ($_cols as $name => $correlation) {
779             if ($name == $_column) {
780                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Removing ' . $_column . ' from columns.');
781                 unset($_cols[$_column]);
782                 $_select->reset(Zend_Db_Select::COLUMNS);
783                 $_select->columns($_cols);
784             }
785         }
786     }
787     
788     /*************************** create / update / delete ****************************/
789     
790     /**
791      * Creates new entry
792      *
793      * @param   Tinebase_Record_Interface $_record
794      * @return  Tinebase_Record_Interface
795      * @throws  Tinebase_Exception_InvalidArgument
796      * @throws  Tinebase_Exception_UnexpectedValue
797      * 
798      * @todo    remove autoincremental ids later
799      */
800     public function create(Tinebase_Record_Interface $_record) 
801     {
802         $identifier = $_record->getIdProperty();
803         
804         if (!$_record instanceof $this->_modelName) {
805             throw new Tinebase_Exception_InvalidArgument('invalid model type: $_record is instance of "' . get_class($_record) . '". but should be instance of ' . $this->_modelName);
806         }
807         
808         // set uid if record has hash id and id is empty
809         if ($this->_hasHashId() && empty($_record->$identifier)) {
810             $newId = $_record->generateUID();
811             $_record->setId($newId);
812         }
813         
814         $recordArray = $this->_recordToRawData($_record);
815         
816         // unset id if autoincrement & still empty
817         if (empty($_record->$identifier) || $_record->$identifier == 'NULL' ) {
818             unset($recordArray['id']);
819         }
820         
821         $recordArray = array_intersect_key($recordArray, $this->_schema);
822
823         $this->_prepareData($recordArray);
824         $this->_db->insert($this->_tablePrefix . $this->_tableName, $recordArray);
825         
826         if (!$this->_hasHashId()) {
827             $newId = $this->_db->lastInsertId($this->getTablePrefix() . $this->getTableName(), $identifier);
828             if(!$newId && isset($_record[$identifier])){
829                 $newId = $_record[$identifier];
830             }
831         }
832
833         // if we insert a record without an id, we need to get back one
834         if (empty($_record->$identifier) && $newId == 0) {
835             throw new Tinebase_Exception_UnexpectedValue("Returned record id is 0.");
836         }
837         
838         // if the record had no id set, set the id now
839         if ($_record->$identifier == NULL || $_record->$identifier == 'NULL') {
840             $_record->$identifier = $newId;
841         }
842         
843         // add custom fields
844         if ($_record->has('customfields') && !empty($_record->customfields)) {
845             Tinebase_CustomField::getInstance()->saveRecordCustomFields($_record);
846         }
847         
848         $this->_updateForeignKeys('create', $_record);
849         
850         $result = $this->get($_record->$identifier);
851         
852         $this->_inspectAfterCreate($result, $_record);
853         
854         return $result;
855     }
856     
857     /**
858      * returns true if id is a hash value and false if integer
859      *
860      * @return  boolean
861      * @todo    remove that when all tables use hash ids 
862      */
863     protected function _hasHashId()
864     {
865         $identifier = $this->_getRecordIdentifier();
866         $result = (in_array($this->_schema[$identifier]['DATA_TYPE'], array('varchar', 'VARCHAR2')) && $this->_schema[$identifier]['LENGTH'] == 40);
867         
868         return $result;
869     }
870     
871     /**
872      * converts record into raw data for adapter
873      *
874      * @param  Tinebase_Record_Abstract $_record
875      * @return array
876      */
877     protected function _recordToRawData($_record)
878     {
879         $readOnlyFields = $_record->getReadOnlyFields();
880         $raw = $_record->toArray(FALSE);
881         foreach ($raw as $key => $value) {
882             if ($value instanceof Tinebase_Record_Interface) {
883                 $raw[$key] = $value->getId();
884             }
885             if (in_array($key, $readOnlyFields)) {
886                 unset($raw[$key]);
887             }
888         }
889         
890         return $raw;
891     }
892     
893     /**
894      * prepare record data array
895      * - replace int and bool values by Zend_Db_Expr
896      *
897      * @param array &$_recordArray
898      * @return array with the prepared data
899      */
900     protected function _prepareData(&$_recordArray) 
901     {
902         
903         foreach ($_recordArray as $key => $value) {
904             if (is_bool($value)) {
905                 $_recordArray[$key] = ($value) ? new Zend_Db_Expr('1') : new Zend_Db_Expr('0');
906             } elseif (is_int($value)) {
907                 $_recordArray[$key] = new Zend_Db_Expr((string) $value);
908             }
909         }
910     }
911     
912     /**
913      * update foreign key values
914      * 
915      * @param string $_mode create|update
916      * @param Tinebase_Record_Abstract $_record
917      */
918     protected function _updateForeignKeys($_mode, Tinebase_Record_Abstract $_record)
919     {
920         if (! empty($this->_foreignTables)) {
921             
922             foreach ($this->_foreignTables as $modelName => $join) {
923                 
924                 if (! array_key_exists('field', $join)) {
925                     continue;
926                 }
927                 
928                 $idsToAdd    = array();
929                 $idsToRemove = array();
930                 
931                 if (!empty($_record->$modelName)) {
932                     $idsToAdd = $this->_getIdsFromMixed($_record->$modelName);
933                 }
934                 
935                 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
936                 
937                 if ($_mode == 'update') {
938                     $select = $this->_db->select();
939         
940                     $select->from(array($join['table'] => $this->_tablePrefix . $join['table']), array($join['field']))
941                         ->where($this->_db->quoteIdentifier($join['table'] . '.' . $join['joinOn']) . ' = ?', $_record->getId());
942                     
943                     Tinebase_Backend_Sql_Abstract::traitGroup($select);
944                     
945                     $stmt = $this->_db->query($select);
946                     $currentIds = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
947                     $stmt->closeCursor();
948                     
949                     $idsToRemove = array_diff($currentIds, $idsToAdd);
950                     $idsToAdd    = array_diff($idsToAdd, $currentIds);
951                 }
952                 
953                 if (!empty($idsToRemove)) {
954                     $where = '(' . 
955                         $this->_db->quoteInto($this->_db->quoteIdentifier($this->_tablePrefix . $join['table'] . '.' . $join['joinOn']) . ' = ?', $_record->getId()) .
956                         ' AND ' . 
957                         $this->_db->quoteInto($this->_db->quoteIdentifier($this->_tablePrefix . $join['table'] . '.' . $join['field']) . ' IN (?)', $idsToRemove) .
958                     ')';
959                         
960                     $this->_db->delete($this->_tablePrefix . $join['table'], $where);
961                 }
962                 
963                 foreach ($idsToAdd as $id) {
964                     $recordArray = array (
965                         $join['joinOn'] => $_record->getId(),
966                         $join['field']  => $id
967                     );
968                     $this->_db->insert($this->_tablePrefix . $join['table'], $recordArray);
969                 }
970                     
971                 
972                 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
973             }
974         }
975     }
976
977     /**
978      * convert recordset, array of ids or records to array of ids
979      * 
980      * @param  mixed  $_mixed
981      * @return array
982      */
983     protected function _getIdsFromMixed($_mixed)
984     {
985         if ($_mixed instanceof Tinebase_Record_RecordSet) { // Record set
986             $ids = $_mixed->getArrayOfIds();
987             
988         } elseif (is_array($_mixed)) { // array
989             foreach ($_mixed as $mixed) {
990                 if ($mixed instanceof Tinebase_Record_Abstract) {
991                     $ids[] = $mixed->getId();
992                 } else {
993                     $ids[] = $mixed;
994                 }
995             }
996             
997         } else { // string
998             $ids[] = $_mixed instanceof Tinebase_Record_Abstract ? $_mixed->getId() : $_mixed;
999         }
1000         
1001         return $ids;
1002     }
1003     
1004     /**
1005      * do something after creation of record
1006      * 
1007      * @param Tinebase_Record_Abstract $_newRecord
1008      * @param Tinebase_Record_Abstract $_recordToCreate
1009      * @return void
1010      */
1011     protected function _inspectAfterCreate(Tinebase_Record_Abstract $_newRecord, Tinebase_Record_Abstract $_recordToCreate)
1012     {
1013     }
1014     
1015     /**
1016      * Updates existing entry
1017      *
1018      * @param Tinebase_Record_Interface $_record
1019      * @throws Tinebase_Exception_Record_Validation|Tinebase_Exception_InvalidArgument
1020      * @return Tinebase_Record_Interface Record|NULL
1021      */
1022     public function update(Tinebase_Record_Interface $_record) 
1023     {
1024         $identifier = $_record->getIdProperty();
1025         
1026         if (!$_record instanceof $this->_modelName) {
1027             throw new Tinebase_Exception_InvalidArgument('invalid model type: $_record is instance of "' . get_class($_record) . '". but should be instance of ' . $this->_modelName);
1028         }
1029         
1030         $_record->isValid(TRUE);
1031         
1032         $id = $_record->getId();
1033
1034         $recordArray = $this->_recordToRawData($_record);
1035         $recordArray = array_intersect_key($recordArray, $this->_schema);
1036         
1037         $this->_prepareData($recordArray);
1038                 
1039         $where  = array(
1040             $this->_db->quoteInto($this->_db->quoteIdentifier($identifier) . ' = ?', $id),
1041         );
1042         
1043         $this->_db->update($this->_tablePrefix . $this->_tableName, $recordArray, $where);
1044         
1045         // update custom fields
1046         if ($_record->has('customfields')) {
1047             Tinebase_CustomField::getInstance()->saveRecordCustomFields($_record);
1048         }
1049                 
1050         $this->_updateForeignKeys('update', $_record);
1051         
1052         $result = $this->get($id, true);
1053         
1054         return $result;
1055     }
1056     
1057     /**
1058      * Updates multiple entries
1059      *
1060      * @param array $_ids to update
1061      * @param array $_data
1062      * @return integer number of affected rows
1063      * @throws Tinebase_Exception_Record_Validation|Tinebase_Exception_InvalidArgument
1064      */
1065     public function updateMultiple($_ids, $_data) 
1066     {
1067         if (empty($_ids)) {
1068             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' No records updated.');
1069             return 0;
1070         }        
1071         
1072         // separate CustomFields
1073         
1074         $myFields = array();
1075         $customFields = array();
1076         
1077         foreach($_data as $key => $value) {
1078             if(stristr($key, '#')) $customFields[substr($key,1)] = $value;
1079             else $myFields[$key] = $value;
1080         }
1081         
1082         // handle CustomFields
1083         
1084         if(count($customFields)) {
1085             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' CustomFields found.');
1086             Tinebase_CustomField::getInstance()->saveMultipleCustomFields($this->_modelName, $_ids, $customFields);
1087         }
1088         
1089         // handle StdFields
1090         
1091         if(!count($myFields)) { return 0; } 
1092
1093         $identifier = $this->_getRecordIdentifier();
1094   
1095         $recordArray = $myFields;
1096         $recordArray = array_intersect_key($recordArray, $this->_schema);
1097         
1098         $this->_prepareData($recordArray);
1099                 
1100         $where  = array(
1101             $this->_db->quoteInto($this->_db->quoteIdentifier($identifier) . ' IN (?)', $_ids),
1102         );
1103         
1104         //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($where, TRUE));
1105         
1106         return $this->_db->update($this->_tablePrefix . $this->_tableName, $recordArray, $where);
1107     }
1108     
1109     /**
1110       * Deletes entries
1111       * 
1112       * @param string|integer|Tinebase_Record_Interface|array $_id
1113       * @return void
1114       * @return int The number of affected rows.
1115       */
1116     public function delete($_id) 
1117     {
1118         if (empty($_id)) {
1119             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' No records deleted.');
1120             return 0;
1121         }
1122         
1123         $idArray = (! is_array($_id)) ? array(Tinebase_Record_Abstract::convertId($_id, $this->_modelName)) : $_id;
1124         $identifier = $this->_getRecordIdentifier();
1125         
1126         $where = array(
1127             $this->_db->quoteInto($this->_db->quoteIdentifier($identifier) . ' IN (?)', $idArray)
1128         );
1129         
1130         return $this->_db->delete($this->_tablePrefix . $this->_tableName, $where);
1131     }
1132     
1133     /**
1134      * delete rows by property
1135      * 
1136      * @param string|array $_value
1137      * @param string $_property
1138      * @param string $_operator (equals|in)
1139      * @return integer The number of affected rows.
1140      * @throws Tinebase_Exception_InvalidArgument
1141      */
1142     public function deleteByProperty($_value, $_property, $_operator = 'equals')
1143     {
1144         if (! array_key_exists($_property, $this->_schema)) {
1145             throw new Tinebase_Exception_InvalidArgument('Property ' . $_property . ' does not exist in table ' . $this->_tableName);
1146         }
1147         
1148         switch ($_operator) {
1149             case 'equals':
1150                 $op = ' = ?';
1151                 break;
1152             case 'in':
1153                 $op = ' IN (?)';
1154                 $_value = (array) $_value;
1155                 break;
1156             default:
1157                 throw new Tinebase_Exception_InvalidArgument('Invalid operator: ' . $_operator);
1158         }
1159         $where = array(
1160             $this->_db->quoteInto($this->_db->quoteIdentifier($_property) . $op, $_value)
1161         );
1162         
1163         return $this->_db->delete($this->_tablePrefix . $this->_tableName, $where);
1164     }
1165     
1166     /*************************** foreign record fetchers *******************************/
1167     
1168     /**
1169      * appends foreign record (1:1 relation) to given record
1170      *
1171      * @param Tinebase_Record_Abstract      $_record            Record to append the foreign record to
1172      * @param string                        $_appendTo          Property in the record where to append the foreign record to
1173      * @param string                        $_recordKey         Property in the record where the foreign key value is in
1174      * @param string                        $_foreignKey        Key property in foreign table of the record to append
1175      * @param Tinebase_Backend_Sql_Abstract $_foreignBackend    Foreign table backend 
1176      */
1177     public function appendForeignRecordToRecord($_record, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
1178     {
1179         try {
1180             $_record->$_appendTo = $_foreignBackend->getByProperty($_record->$_recordKey, $_foreignKey);
1181         } catch (Tinebase_Exception_NotFound $e) {
1182             $_record->$_appendTo = NULL;
1183         }
1184     }
1185     
1186     /**
1187      * appends foreign recordSet (1:n relation) to given record
1188      *
1189      * @param Tinebase_Record_Abstract      $_record            Record to append the foreign records to
1190      * @param string                        $_appendTo          Property in the record where to append the foreign records to
1191      * @param string                        $_recordKey         Property in the record where the foreign key value is in
1192      * @param string                        $_foreignKey        Key property in foreign table of the records to append
1193      * @param Tinebase_Backend_Sql_Abstract $_foreignBackend    Foreign table backend 
1194      */
1195     public function appendForeignRecordSetToRecord($_record, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
1196     {
1197         $_record->$_appendTo = $_foreignBackend->getMultipleByProperty($_record->$_recordKey, $_foreignKey);
1198     }
1199     
1200     /**
1201      * appends foreign record (1:1/n:1 relation) to given recordSet
1202      *
1203      * @param Tinebase_Record_RecordSet     $_recordSet         Records to append the foreign record to
1204      * @param string                        $_appendTo          Property in the records where to append the foreign record to
1205      * @param string                        $_recordKey         Property in the records where the foreign key value is in
1206      * @param string                        $_foreignKey        Key property in foreign table of the record to append
1207      * @param Tinebase_Backend_Sql_Abstract $_foreignBackend    Foreign table backend 
1208      */
1209     public function appendForeignRecordToRecordSet($_recordSet, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
1210     {
1211         $allForeignRecords = $_foreignBackend->getMultipleByProperty($_recordSet->$_recordKey, $_foreignKey);
1212         foreach ($_recordSet as $record) {
1213             $record->$_appendTo = $allForeignRecords->filter($_foreignKey, $record->$_recordKey)->getFirstRecord();
1214         }
1215     }
1216     
1217     /**
1218      * appends foreign recordSet (1:n/m:n relation) to given recordSet
1219      *
1220      * @param Tinebase_Record_RecordSet     $_recordSet         Records to append the foreign records to
1221      * @param string                        $_appendTo          Property in the records where to append the foreign records to
1222      * @param string                        $_recordKey         Property in the records where the foreign key value is in
1223      * @param string                        $_foreignKey        Key property in foreign table of the records to append
1224      * @param Tinebase_Backend_Sql_Abstract $_foreignBackend    Foreign table backend 
1225      */
1226     public function appendForeignRecordSetToRecordSet($_recordSet, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
1227     {
1228         $allForeignRecords = $_foreignBackend->getMultipleByProperty($_recordSet->$_recordKey, $_foreignKey);
1229         foreach ($_recordSet as $record) {
1230             $record->$_appendTo = $allForeignRecords->filter($_foreignKey, $record->$_recordKey);
1231         }
1232     }
1233     
1234     /*************************** other ************************************/
1235     
1236     /**
1237      * get table name
1238      *
1239      * @return string
1240      */
1241     public function getTableName()
1242     {
1243         return $this->_tableName;
1244     }
1245     
1246     /**
1247      * get foreign table information
1248      *
1249      * @return array
1250      */
1251     public function getForeignTables()
1252     {
1253         return $this->_foreignTables;
1254     }
1255     
1256     /**
1257      * get table prefix
1258      *
1259      * @return string
1260      */
1261     public function getTablePrefix()
1262     {
1263         return $this->_tablePrefix;
1264     }
1265     
1266     /**
1267      * get db adapter
1268      *
1269      * @return Zend_Db_Adapter_Abstract
1270      * @throws Tinebase_Exception_Backend_Database
1271      */
1272     public function getAdapter()
1273     {
1274         if (! $this->_db instanceof Zend_Db_Adapter_Abstract) {
1275             throw new Tinebase_Exception_Backend_Database('Could not fetch database adapter');
1276         }
1277         
1278         return $this->_db;
1279     }
1280     
1281     /**
1282      * get dbCommand class
1283      *
1284      * @return Tinebase_Backend_Sql_Command_Interface
1285      * @throws Tinebase_Exception_Backend_Database
1286      */
1287     public function getDbCommand()
1288     {
1289         if (! $this->_dbCommand instanceof Tinebase_Backend_Sql_Command_Interface) {
1290             throw new Tinebase_Exception_Backend_Database('Could not fetch database command class');
1291         }
1292         
1293         return $this->_dbCommand;
1294     }
1295     
1296     /**
1297      * Public service for grouping treatment
1298      * 
1299      * @param string $tablePrefix
1300      * @param Zend_Db_Select $select
1301      */
1302     public static function traitGroup(Zend_Db_Select $select)
1303     {
1304         // not needed for MySQL backends
1305         if ($select->getAdapter() instanceof Zend_Db_Adapter_Pdo_Mysql) {
1306             return;
1307         }
1308         
1309         $group = $select->getPart(Zend_Db_Select::GROUP);
1310         
1311         if (empty($group)) {
1312             return;
1313         }
1314         
1315         $columns        = $select->getPart(Zend_Db_Select::COLUMNS);
1316         $updatedColumns = array();
1317         
1318         //$column is an array where 0 is table, 1 is field and 2 is alias
1319         foreach ($columns as $key => $column) {
1320             if ($column[1] instanceof Zend_Db_Expr) {
1321                 if (preg_match('/^\(.*\)/', $column[1])) {
1322                     $updatedColumns[] = array($column[0], new Zend_Db_Expr("MIN(" . $column[1] . ")"), $column[2]);
1323                 } else {
1324                     $updatedColumns[] = $column;
1325                 }
1326                 
1327                 continue;
1328             }
1329             
1330             if (preg_match('/^\(.*\)/', $column[1])) {
1331                 $updatedColumns[] = array($column[0], new Zend_Db_Expr("MIN(" . $column[1] . ")"), $column[2]);
1332                 
1333                 continue;
1334             }
1335             
1336             // resolve * to single columns
1337             if ($column[1] == '*') {
1338
1339                 $tableFields = Tinebase_Db_Table::getTableDescriptionFromCache(SQL_TABLE_PREFIX . $column[0], $select->getAdapter());
1340                 foreach ($tableFields as $columnName => $schema) {
1341                     
1342                     // adds columns into group by clause (table.field)
1343                     // checks if field has a function (that must be an aggregation)
1344                     $fieldName = "{$column[0]}.$columnName";
1345                     
1346                     if (in_array($fieldName, $group)) {
1347                         $updatedColumns[] = array($column[0], $fieldName, $columnName);
1348                     } else {
1349                         // any selected field which is not in the group by clause must have an aggregate function
1350                         // we choose MIN() as default. In practice the affected columns will have only one value anyways.
1351                         $updatedColumns[] = array($column[0], new Zend_Db_Expr("MIN(" . $select->getAdapter()->quoteIdentifier($fieldName) . ")"), $columnName);
1352                     }
1353                 }
1354                 
1355                 continue;
1356             }
1357             
1358             $fieldName = $column[0] . '.' . $column[1];
1359             
1360             if (in_array($fieldName, $group)) {
1361                 $updatedColumns[] = $column;
1362             } else {
1363                 // any selected field which is not in the group by clause must have an aggregate function
1364                 // we choose MIN() as default. In practice the affected columns will have only one value anyways.
1365                 $updatedColumns[] = array($column[0], new Zend_Db_Expr("MIN(" . $select->getAdapter()->quoteIdentifier($fieldName) . ")"), $column[2] ? $column[2] : $column[1]);
1366             }
1367         }
1368         
1369         $select->reset(Zend_Db_Select::COLUMNS);
1370         
1371         foreach ($updatedColumns as $column) {
1372             $select->columns(!empty($column[2]) ? array($column[2] => $column[1]) : $column[1], $column[0]);
1373         }
1374
1375         // add order by columns to group by
1376         $order = $select->getPart(Zend_Db_Select::ORDER);
1377         
1378         foreach($order as $column) {
1379             $field = $column[0];
1380             
1381             if (preg_match('/.*\..*/',$field) && !in_array($field,$group)) {
1382                 // adds column into group by clause (table.field)
1383                 $group[] = $field;
1384             }
1385         }
1386         
1387         $select->reset(Zend_Db_Select::GROUP);
1388         
1389         $select->group($group);
1390     }
1391 }