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