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