5c33c9eaae869b499f1f3aa2666508c05f9df713
[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 = $this->_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      * convert recordset, array of ids or records to array of ids
1230      * 
1231      * @param  mixed  $_mixed
1232      * @return array
1233      */
1234     protected function _getIdsFromMixed($_mixed)
1235     {
1236         if ($_mixed instanceof Tinebase_Record_RecordSet) { // Record set
1237             $ids = $_mixed->getArrayOfIds();
1238             
1239         } elseif (is_array($_mixed)) { // array
1240             foreach ($_mixed as $mixed) {
1241                 if ($mixed instanceof Tinebase_Record_Abstract) {
1242                     $ids[] = $mixed->getId();
1243                 } else {
1244                     $ids[] = $mixed;
1245                 }
1246             }
1247             
1248         } else { // string
1249             $ids[] = $_mixed instanceof Tinebase_Record_Abstract ? $_mixed->getId() : $_mixed;
1250         }
1251         
1252         return $ids;
1253     }
1254     
1255     /**
1256      * do something after creation of record
1257      * 
1258      * @param Tinebase_Record_Abstract $_newRecord
1259      * @param Tinebase_Record_Abstract $_recordToCreate
1260      * @return void
1261      */
1262     protected function _inspectAfterCreate(Tinebase_Record_Abstract $_newRecord, Tinebase_Record_Abstract $_recordToCreate)
1263     {
1264     }
1265     
1266     /**
1267      * Updates existing entry
1268      *
1269      * @param Tinebase_Record_Interface $_record
1270      * @throws Tinebase_Exception_Record_Validation|Tinebase_Exception_InvalidArgument
1271      * @return Tinebase_Record_Interface Record|NULL
1272      */
1273     public function update(Tinebase_Record_Interface $_record) 
1274     {
1275         $identifier = $_record->getIdProperty();
1276         
1277         if (!$_record instanceof $this->_modelName) {
1278             throw new Tinebase_Exception_InvalidArgument('invalid model type: $_record is instance of "' . get_class($_record) . '". but should be instance of ' . $this->_modelName);
1279         }
1280         
1281         $_record->isValid(TRUE);
1282         
1283         $id = $_record->getId();
1284
1285         $recordArray = $this->_recordToRawData($_record);
1286         $recordArray = array_intersect_key($recordArray, $this->getSchema());
1287         
1288         $this->_prepareData($recordArray);
1289         
1290         $where  = array(
1291             $this->_db->quoteInto($this->_db->quoteIdentifier($identifier) . ' = ?', $id),
1292         );
1293         
1294         $this->_db->update($this->_tablePrefix . $this->_tableName, $recordArray, $where);
1295
1296         // update custom fields
1297         if ($_record->has('customfields')) {
1298             Tinebase_CustomField::getInstance()->saveRecordCustomFields($_record);
1299         }
1300         
1301         $this->_updateForeignKeys('update', $_record);
1302         
1303         $result = $this->get($id, true);
1304         
1305         return $result;
1306     }
1307     
1308     /**
1309      * Updates multiple entries
1310      *
1311      * @param array $_ids to update
1312      * @param array $_data
1313      * @return integer number of affected rows
1314      * @throws Tinebase_Exception_Record_Validation|Tinebase_Exception_InvalidArgument
1315      */
1316     public function updateMultiple($_ids, $_data) 
1317     {
1318         if (empty($_ids)) {
1319             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1320                 . ' No records updated.');
1321             return 0;
1322         }
1323         
1324         // separate CustomFields
1325         
1326         $myFields = array();
1327         $customFields = array();
1328         
1329         foreach($_data as $key => $value) {
1330             if(stristr($key, '#')) $customFields[substr($key,1)] = $value;
1331             else $myFields[$key] = $value;
1332         }
1333         
1334         // handle CustomFields
1335         
1336         if(count($customFields)) {
1337             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1338                 . ' CustomFields found.');
1339             Tinebase_CustomField::getInstance()->saveMultipleCustomFields($this->_modelName, $_ids, $customFields);
1340         }
1341         
1342         // handle StdFields
1343         
1344         if(!count($myFields)) { return 0; } 
1345
1346         $identifier = $this->_getRecordIdentifier();
1347         
1348         $recordArray = $myFields;
1349         $recordArray = array_intersect_key($recordArray, $this->getSchema());
1350         
1351         $this->_prepareData($recordArray);
1352                 
1353         $where  = array(
1354             $this->_db->quoteInto($this->_db->quoteIdentifier($identifier) . ' IN (?)', $_ids),
1355         );
1356         
1357         //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($where, TRUE));
1358         
1359         return $this->_db->update($this->_tablePrefix . $this->_tableName, $recordArray, $where);
1360     }
1361     
1362     /**
1363       * Deletes entries
1364       * 
1365       * @param string|integer|Tinebase_Record_Interface|array $_id
1366       * @return void
1367       * @return int The number of affected rows.
1368       */
1369     public function delete($_id)
1370     {
1371         if (empty($_id)) {
1372             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' No records deleted.');
1373             return 0;
1374         }
1375         
1376         $idArray = (! is_array($_id)) ? array(Tinebase_Record_Abstract::convertId($_id, $this->_modelName)) : $_id;
1377         $identifier = $this->_getRecordIdentifier();
1378         
1379         $where = array(
1380             $this->_db->quoteInto($this->_db->quoteIdentifier($identifier) . ' IN (?)', $idArray)
1381         );
1382         
1383         return $this->_db->delete($this->_tablePrefix . $this->_tableName, $where);
1384     }
1385     
1386     /**
1387      * delete rows by property
1388      * 
1389      * @param string|array $_value
1390      * @param string $_property
1391      * @param string $_operator (equals|in)
1392      * @return integer The number of affected rows.
1393      * @throws Tinebase_Exception_InvalidArgument
1394      */
1395     public function deleteByProperty($_value, $_property, $_operator = 'equals')
1396     {
1397         $schema = $this->getSchema();
1398         
1399         if (! (isset($schema[$_property]) || array_key_exists($_property, $schema))) {
1400             throw new Tinebase_Exception_InvalidArgument('Property ' . $_property . ' does not exist in table ' . $this->_tableName);
1401         }
1402         
1403         switch ($_operator) {
1404             case 'equals':
1405                 $op = ' = ?';
1406                 break;
1407             case 'in':
1408                 $op = ' IN (?)';
1409                 $_value = (array) $_value;
1410                 break;
1411             default:
1412                 throw new Tinebase_Exception_InvalidArgument('Invalid operator: ' . $_operator);
1413         }
1414         $where = array(
1415             $this->_db->quoteInto($this->_db->quoteIdentifier($_property) . $op, $_value)
1416         );
1417         
1418         return $this->_db->delete($this->_tablePrefix . $this->_tableName, $where);
1419     }
1420     
1421     /*************************** foreign record fetchers *******************************/
1422     
1423     /**
1424      * appends foreign record (1:1 relation) to given record
1425      *
1426      * @param Tinebase_Record_Abstract      $_record            Record to append the foreign record to
1427      * @param string                        $_appendTo          Property in the record where to append the foreign record to
1428      * @param string                        $_recordKey         Property in the record where the foreign key value is in
1429      * @param string                        $_foreignKey        Key property in foreign table of the record to append
1430      * @param Tinebase_Backend_Sql_Abstract $_foreignBackend    Foreign table backend 
1431      */
1432     public function appendForeignRecordToRecord($_record, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
1433     {
1434         try {
1435             $_record->$_appendTo = $_foreignBackend->getByProperty($_record->$_recordKey, $_foreignKey);
1436         } catch (Tinebase_Exception_NotFound $e) {
1437             $_record->$_appendTo = NULL;
1438         }
1439     }
1440     
1441     /**
1442      * appends foreign recordSet (1:n relation) to given record
1443      *
1444      * @param Tinebase_Record_Abstract      $_record            Record to append the foreign records to
1445      * @param string                        $_appendTo          Property in the record where to append the foreign records to
1446      * @param string                        $_recordKey         Property in the record where the foreign key value is in
1447      * @param string                        $_foreignKey        Key property in foreign table of the records to append
1448      * @param Tinebase_Backend_Sql_Abstract $_foreignBackend    Foreign table backend 
1449      */
1450     public function appendForeignRecordSetToRecord($_record, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
1451     {
1452         $_record->$_appendTo = $_foreignBackend->getMultipleByProperty($_record->$_recordKey, $_foreignKey);
1453     }
1454     
1455     /**
1456      * appends foreign record (1:1/n:1 relation) to given recordSet
1457      *
1458      * @param Tinebase_Record_RecordSet     $_recordSet         Records to append the foreign record to
1459      * @param string                        $_appendTo          Property in the records where to append the foreign record to
1460      * @param string                        $_recordKey         Property in the records where the foreign key value is in
1461      * @param string                        $_foreignKey        Key property in foreign table of the record to append
1462      * @param Tinebase_Backend_Sql_Abstract $_foreignBackend    Foreign table backend 
1463      */
1464     public function appendForeignRecordToRecordSet($_recordSet, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
1465     {
1466         $allForeignRecords = $_foreignBackend->getMultipleByProperty($_recordSet->$_recordKey, $_foreignKey);
1467         foreach ($_recordSet as $record) {
1468             $record->$_appendTo = $allForeignRecords->filter($_foreignKey, $record->$_recordKey)->getFirstRecord();
1469         }
1470     }
1471     
1472     /**
1473      * appends foreign recordSet (1:n relation) to given recordSet
1474      *
1475      * @param Tinebase_Record_RecordSet     $_recordSet         Records to append the foreign records to
1476      * @param string                        $_appendTo          Property in the records where to append the foreign records to
1477      * @param string                        $_recordKey         Property in the records where the foreign key value is in
1478      * @param string                        $_foreignKey        Key property in foreign table of the records to append
1479      * @param Tinebase_Backend_Sql_Abstract $_foreignBackend    Foreign table backend 
1480      */
1481     public function appendForeignRecordSetToRecordSet($_recordSet, $_appendTo, $_recordKey, $_foreignKey, $_foreignBackend)
1482     {
1483         $idxRecordKeyMap = $_recordSet->$_recordKey;
1484         $recordKeyIdxMap = array_flip($idxRecordKeyMap);
1485         $allForeignRecords = $_foreignBackend->getMultipleByProperty($idxRecordKeyMap, $_foreignKey);
1486         $foreignRecordsClassName = $allForeignRecords->getRecordClassName();
1487
1488         foreach ($_recordSet as $record) {
1489             $record->$_appendTo = new Tinebase_Record_RecordSet($foreignRecordsClassName);
1490         }
1491
1492         foreach($allForeignRecords as $foreignRecord) {
1493             $record = $_recordSet->getByIndex($recordKeyIdxMap[$foreignRecord->$_foreignKey]);
1494             $foreignRecordSet = $record->$_appendTo;
1495
1496             $foreignRecordSet->addRecord($foreignRecord);
1497         }
1498     }
1499     
1500     /*************************** other ************************************/
1501     
1502     /**
1503      * get table name
1504      *
1505      * @return string
1506      */
1507     public function getTableName()
1508     {
1509         return $this->_tableName;
1510     }
1511     
1512     /**
1513      * get foreign table information
1514      *
1515      * @return array
1516      */
1517     public function getForeignTables()
1518     {
1519         return $this->_foreignTables;
1520     }
1521     
1522     /**
1523      * get table prefix
1524      *
1525      * @return string
1526      */
1527     public function getTablePrefix()
1528     {
1529         return $this->_tablePrefix;
1530     }
1531     
1532     /**
1533      * get table identifier
1534      * 
1535      * @return string
1536      */
1537     public function getIdentifier()
1538     {
1539         return $this->_identifier;
1540     }
1541     
1542     /**
1543      * get db adapter
1544      *
1545      * @return Zend_Db_Adapter_Abstract
1546      * @throws Tinebase_Exception_Backend_Database
1547      */
1548     public function getAdapter()
1549     {
1550         if (! $this->_db instanceof Zend_Db_Adapter_Abstract) {
1551             throw new Tinebase_Exception_Backend_Database('Could not fetch database adapter');
1552         }
1553         
1554         return $this->_db;
1555     }
1556     
1557     /**
1558      * get dbCommand class
1559      *
1560      * @return Tinebase_Backend_Sql_Command_Interface
1561      * @throws Tinebase_Exception_Backend_Database
1562      */
1563     public function getDbCommand()
1564     {
1565         if (! $this->_dbCommand instanceof Tinebase_Backend_Sql_Command_Interface) {
1566             throw new Tinebase_Exception_Backend_Database('Could not fetch database command class');
1567         }
1568         
1569         return $this->_dbCommand;
1570     }
1571     
1572     /**
1573      * Public service for grouping treatment
1574      * 
1575      * @param string $tablePrefix
1576      * @param Zend_Db_Select $select
1577      */
1578     public static function traitGroup(Zend_Db_Select $select)
1579     {
1580         // not needed for MySQL backends
1581         if ($select->getAdapter() instanceof Zend_Db_Adapter_Pdo_Mysql) {
1582             return;
1583         }
1584         
1585         $group = $select->getPart(Zend_Db_Select::GROUP);
1586         
1587         if (empty($group)) {
1588             return;
1589         }
1590         
1591         $columns        = $select->getPart(Zend_Db_Select::COLUMNS);
1592         $updatedColumns = array();
1593         
1594         //$column is an array where 0 is table, 1 is field and 2 is alias
1595         foreach ($columns as $key => $column) {
1596             if ($column[1] instanceof Zend_Db_Expr) {
1597                 if (preg_match('/^\(.*\)/', $column[1])) {
1598                     $updatedColumns[] = array($column[0], new Zend_Db_Expr("MIN(" . $column[1] . ")"), $column[2]);
1599                 } else {
1600                     $updatedColumns[] = $column;
1601                 }
1602                 
1603                 continue;
1604             }
1605             
1606             if (preg_match('/^\(.*\)/', $column[1])) {
1607                 $updatedColumns[] = array($column[0], new Zend_Db_Expr("MIN(" . $column[1] . ")"), $column[2]);
1608                 
1609                 continue;
1610             }
1611             
1612             // resolve * to single columns
1613             if ($column[1] == '*') {
1614
1615                 $tableFields = Tinebase_Db_Table::getTableDescriptionFromCache(SQL_TABLE_PREFIX . $column[0], $select->getAdapter());
1616                 foreach ($tableFields as $columnName => $schema) {
1617                     
1618                     // adds columns into group by clause (table.field)
1619                     // checks if field has a function (that must be an aggregation)
1620                     $fieldName = "{$column[0]}.$columnName";
1621                     
1622                     if (in_array($fieldName, $group)) {
1623                         $updatedColumns[] = array($column[0], $fieldName, $columnName);
1624                     } else {
1625                         // any selected field which is not in the group by clause must have an aggregate function
1626                         // we choose MIN() as default. In practice the affected columns will have only one value anyways.
1627                         $updatedColumns[] = array($column[0], new Zend_Db_Expr("MIN(" . $select->getAdapter()->quoteIdentifier($fieldName) . ")"), $columnName);
1628                     }
1629                 }
1630                 
1631                 continue;
1632             }
1633             
1634             $fieldName = $column[0] . '.' . $column[1];
1635             
1636             if (in_array($fieldName, $group)) {
1637                 $updatedColumns[] = $column;
1638             } else {
1639                 // any selected field which is not in the group by clause must have an aggregate function
1640                 // we choose MIN() as default. In practice the affected columns will have only one value anyways.
1641                 $updatedColumns[] = array($column[0], new Zend_Db_Expr("MIN(" . $select->getAdapter()->quoteIdentifier($fieldName) . ")"), $column[2] ? $column[2] : $column[1]);
1642             }
1643         }
1644         
1645         $select->reset(Zend_Db_Select::COLUMNS);
1646         
1647         foreach ($updatedColumns as $column) {
1648             $select->columns(!empty($column[2]) ? array($column[2] => $column[1]) : $column[1], $column[0]);
1649         }
1650
1651         // add order by columns to group by
1652         $order = $select->getPart(Zend_Db_Select::ORDER);
1653         
1654         foreach($order as $column) {
1655             $field = $column[0];
1656             
1657             if (preg_match('/.*\..*/',$field) && !in_array($field,$group)) {
1658                 // adds column into group by clause (table.field)
1659                 $group[] = $field;
1660             }
1661         }
1662         
1663         $select->reset(Zend_Db_Select::GROUP);
1664         
1665         $select->group($group);
1666     }
1667
1668     /**
1669      * sets etags, expects ids as keys and etags as value
1670      *
1671      * @param array $etags
1672      * 
1673      * @todo maybe we should find a better place for the etag functions as this is currently only used in Calendar + Tasks
1674      */
1675     public function setETags(array $etags)
1676     {
1677         foreach ($etags as $id => $etag) {
1678             $where  = array(
1679                 $this->_db->quoteInto($this->_db->quoteIdentifier($this->_identifier) . ' = ?', $id),
1680             );
1681             $this->_db->update($this->_tablePrefix . $this->_tableName, array('etag' => $etag), $where);
1682         }
1683     }
1684     
1685     /**
1686      * checks if there is an event with this id and etag, or an event with the same id
1687      *
1688      * @param string $id
1689      * @param string $etag
1690      * @return boolean
1691      * @throws Tinebase_Exception_NotFound
1692      */
1693     public function checkETag($id, $etag)
1694     {
1695         $select = $this->_db->select();
1696         $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName), $this->_identifier);
1697         $select->where($this->_db->quoteIdentifier($this->_identifier) . ' = ?', $id);
1698         $select->orWhere($this->_db->quoteIdentifier('uid') . ' = ?', $id);
1699     
1700         $stmt = $select->query();
1701         $queryResult = $stmt->fetch();
1702         $stmt->closeCursor();
1703     
1704         if ($queryResult === false) {
1705             throw new Tinebase_Exception_NotFound('no record with id ' . $id .' found');
1706         }
1707     
1708         $select->where($this->_db->quoteIdentifier('etag') . ' = ?', $etag);
1709         $stmt = $select->query();
1710         $queryResult = $stmt->fetch();
1711         $stmt->closeCursor();
1712     
1713         return ($queryResult !== false);
1714     }
1715     
1716     /**
1717      * return etag set for given container
1718      * 
1719      * @param string $containerId
1720      * @return multitype:Ambigous <mixed, Ambigous <string, boolean, mixed>>
1721      */
1722     public function getEtagsForContainerId($containerId)
1723     {
1724         $select = $this->_db->select();
1725         $select->from(array($this->_tableName => $this->_tablePrefix . $this->_tableName), array($this->_identifier, 'etag', 'uid'));
1726         $select->where($this->_db->quoteIdentifier('container_id') . ' = ?', $containerId);
1727         $select->where($this->_db->quoteIdentifier('is_deleted') . ' = ?', 0);
1728     
1729         $stmt = $select->query();
1730         $queryResult = $stmt->fetchAll();
1731     
1732         $result = array();
1733         foreach ($queryResult as $row) {
1734             $result[$row['id']] = $row;
1735         }
1736         return $result;
1737     }
1738     
1739     /**
1740      * save value in in-class cache
1741      * 
1742      * @param  string   $method
1743      * @param  string   $cacheId
1744      * @param  string   $value
1745      * @param  string   $usePersistentCache
1746      * @param  integer  $persistantCacheTTL
1747      * @return Tinebase_Cache_PerRequest
1748      */
1749     public function saveInClassCache($method, $cacheId, $value, $usePersistentCache = false, $persistantCacheTTL = false)
1750     {
1751         return Tinebase_Cache_PerRequest::getInstance()->save($this->_getInClassCacheIdentifier(), $method, $cacheId, $value, $usePersistentCache, $persistantCacheTTL);
1752     }
1753     
1754     /**
1755      * load value from in-class cache
1756      * 
1757      * @param string $method
1758      * @param string $cacheId
1759      * @return mixed
1760      */
1761     public function loadFromClassCache($method, $cacheId, $usePersistentCache = false)
1762     {
1763         return Tinebase_Cache_PerRequest::getInstance()->load($this->_getInClassCacheIdentifier(), $method, $cacheId, $usePersistentCache);
1764     }
1765     
1766     /**
1767      * reset class cache
1768      * 
1769      * @param string $key
1770      * @return Tinebase_Model_Filter_Abstract
1771      */
1772     public function resetClassCache($method = null)
1773     {
1774         Tinebase_Cache_PerRequest::getInstance()->reset($this->_getInClassCacheIdentifier(), $method);
1775         
1776         return $this;
1777     }
1778     
1779     /**
1780      * return class cache identifier
1781      * 
1782      * if class extend parent class, you can define the name of the parent class here
1783      * 
1784      * @return string
1785      */
1786     public function _getInClassCacheIdentifier()
1787     {
1788         if (isset($this->_classCacheIdentifier)) {
1789             return $this->_classCacheIdentifier;
1790         }
1791         
1792         return get_class($this);
1793     }
1794 }