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