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