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