統計
| ブランチ: | リビジョン:

pictcode / lib / Cake / Model / Datasource / DboSource.php @ 0b1b8047

履歴 | 表示 | アノテート | ダウンロード (102.335 KB)

1 635eef61 spyder1211
<?php
2
/**
3
 * Dbo Source
4
 *
5
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
6
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
7
 *
8
 * Licensed under The MIT License
9
 * For full copyright and license information, please see the LICENSE.txt
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
13
 * @link          http://cakephp.org CakePHP(tm) Project
14
 * @package       Cake.Model.Datasource
15
 * @since         CakePHP(tm) v 0.10.0.1076
16
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
17
 */
18
19
App::uses('DataSource', 'Model/Datasource');
20
App::uses('CakeText', 'Utility');
21
App::uses('View', 'View');
22
23
/**
24
 * DboSource
25
 *
26
 * Creates DBO-descendant objects from a given db connection configuration
27
 *
28
 * @package       Cake.Model.Datasource
29
 */
30
class DboSource extends DataSource {
31
32
/**
33
 * Description string for this Database Data Source.
34
 *
35
 * @var string
36
 */
37
        public $description = "Database Data Source";
38
39
/**
40
 * index definition, standard cake, primary, index, unique
41
 *
42
 * @var array
43
 */
44
        public $index = array('PRI' => 'primary', 'MUL' => 'index', 'UNI' => 'unique');
45
46
/**
47
 * Database keyword used to assign aliases to identifiers.
48
 *
49
 * @var string
50
 */
51
        public $alias = 'AS ';
52
53
/**
54
 * Caches result from query parsing operations. Cached results for both DboSource::name() and
55
 * DboSource::conditions() will be stored here. Method caching uses `md5()`. If you have
56
 * problems with collisions, set DboSource::$cacheMethods to false.
57
 *
58
 * @var array
59
 */
60
        public static $methodCache = array();
61
62
/**
63
 * Whether or not to cache the results of DboSource::name() and DboSource::conditions()
64
 * into the memory cache. Set to false to disable the use of the memory cache.
65
 *
66
 * @var bool
67
 */
68
        public $cacheMethods = true;
69
70
/**
71
 * Flag to support nested transactions. If it is set to false, you will be able to use
72
 * the transaction methods (begin/commit/rollback), but just the global transaction will
73
 * be executed.
74
 *
75
 * @var bool
76
 */
77
        public $useNestedTransactions = false;
78
79
/**
80
 * Print full query debug info?
81
 *
82
 * @var bool
83
 */
84
        public $fullDebug = false;
85
86
/**
87
 * String to hold how many rows were affected by the last SQL operation.
88
 *
89
 * @var string
90
 */
91
        public $affected = null;
92
93
/**
94
 * Number of rows in current resultset
95
 *
96
 * @var int
97
 */
98
        public $numRows = null;
99
100
/**
101
 * Time the last query took
102
 *
103
 * @var int
104
 */
105
        public $took = null;
106
107
/**
108
 * Result
109
 *
110
 * @var array
111
 */
112
        protected $_result = null;
113
114
/**
115
 * Queries count.
116
 *
117
 * @var int
118
 */
119
        protected $_queriesCnt = 0;
120
121
/**
122
 * Total duration of all queries.
123
 *
124
 * @var int
125
 */
126
        protected $_queriesTime = null;
127
128
/**
129
 * Log of queries executed by this DataSource
130
 *
131
 * @var array
132
 */
133
        protected $_queriesLog = array();
134
135
/**
136
 * Maximum number of items in query log
137
 *
138
 * This is to prevent query log taking over too much memory.
139
 *
140
 * @var int
141
 */
142
        protected $_queriesLogMax = 200;
143
144
/**
145
 * Caches serialized results of executed queries
146
 *
147
 * @var array
148
 */
149
        protected $_queryCache = array();
150
151
/**
152
 * A reference to the physical connection of this DataSource
153
 *
154
 * @var array
155
 */
156
        protected $_connection = null;
157
158
/**
159
 * The DataSource configuration key name
160
 *
161
 * @var string
162
 */
163
        public $configKeyName = null;
164
165
/**
166
 * The starting character that this DataSource uses for quoted identifiers.
167
 *
168
 * @var string
169
 */
170
        public $startQuote = null;
171
172
/**
173
 * The ending character that this DataSource uses for quoted identifiers.
174
 *
175
 * @var string
176
 */
177
        public $endQuote = null;
178
179
/**
180
 * The set of valid SQL operations usable in a WHERE statement
181
 *
182
 * @var array
183
 */
184
        protected $_sqlOps = array('like', 'ilike', 'rlike', 'or', 'not', 'in', 'between', 'regexp', 'similar to');
185
186
/**
187
 * Indicates the level of nested transactions
188
 *
189
 * @var int
190
 */
191
        protected $_transactionNesting = 0;
192
193
/**
194
 * Default fields that are used by the DBO
195
 *
196
 * @var array
197
 */
198
        protected $_queryDefaults = array(
199
                'conditions' => array(),
200
                'fields' => null,
201
                'table' => null,
202
                'alias' => null,
203
                'order' => null,
204
                'limit' => null,
205
                'joins' => array(),
206
                'group' => null,
207
                'offset' => null
208
        );
209
210
/**
211
 * Separator string for virtualField composition
212
 *
213
 * @var string
214
 */
215
        public $virtualFieldSeparator = '__';
216
217
/**
218
 * List of table engine specific parameters used on table creating
219
 *
220
 * @var array
221
 */
222
        public $tableParameters = array();
223
224
/**
225
 * List of engine specific additional field parameters used on table creating
226
 *
227
 * @var array
228
 */
229
        public $fieldParameters = array();
230
231
/**
232
 * Indicates whether there was a change on the cached results on the methods of this class
233
 * This will be used for storing in a more persistent cache
234
 *
235
 * @var bool
236
 */
237
        protected $_methodCacheChange = false;
238
239
/**
240
 * Constructor
241
 *
242
 * @param array $config Array of configuration information for the Datasource.
243
 * @param bool $autoConnect Whether or not the datasource should automatically connect.
244
 * @throws MissingConnectionException when a connection cannot be made.
245
 */
246
        public function __construct($config = null, $autoConnect = true) {
247
                if (!isset($config['prefix'])) {
248
                        $config['prefix'] = '';
249
                }
250
                parent::__construct($config);
251
                $this->fullDebug = Configure::read('debug') > 1;
252
                if (!$this->enabled()) {
253
                        throw new MissingConnectionException(array(
254
                                'class' => get_class($this),
255
                                'message' => __d('cake_dev', 'Selected driver is not enabled'),
256
                                'enabled' => false
257
                        ));
258
                }
259
                if ($autoConnect) {
260
                        $this->connect();
261
                }
262
        }
263
264
/**
265
 * Reconnects to database server with optional new settings
266
 *
267
 * @param array $config An array defining the new configuration settings
268
 * @return bool True on success, false on failure
269
 */
270
        public function reconnect($config = array()) {
271
                $this->disconnect();
272
                $this->setConfig($config);
273
                $this->_sources = null;
274
275
                return $this->connect();
276
        }
277
278
/**
279
 * Disconnects from database.
280
 *
281
 * @return bool Always true
282
 */
283
        public function disconnect() {
284
                if ($this->_result instanceof PDOStatement) {
285
                        $this->_result->closeCursor();
286
                }
287
                $this->_connection = null;
288
                $this->connected = false;
289
                return true;
290
        }
291
292
/**
293
 * Get the underlying connection object.
294
 *
295
 * @return PDO
296
 */
297
        public function getConnection() {
298
                return $this->_connection;
299
        }
300
301
/**
302
 * Gets the version string of the database server
303
 *
304
 * @return string The database version
305
 */
306
        public function getVersion() {
307
                return $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION);
308
        }
309
310
/**
311
 * Returns a quoted and escaped string of $data for use in an SQL statement.
312
 *
313
 * @param string $data String to be prepared for use in an SQL statement
314
 * @param string $column The column datatype into which this data will be inserted.
315
 * @param bool $null Column allows NULL values
316
 * @return string Quoted and escaped data
317
 */
318
        public function value($data, $column = null, $null = true) {
319
                if (is_array($data) && !empty($data)) {
320
                        return array_map(
321
                                array(&$this, 'value'),
322
                                $data, array_fill(0, count($data), $column)
323
                        );
324
                } elseif (is_object($data) && isset($data->type, $data->value)) {
325
                        if ($data->type === 'identifier') {
326
                                return $this->name($data->value);
327
                        } elseif ($data->type === 'expression') {
328
                                return $data->value;
329
                        }
330
                } elseif (in_array($data, array('{$__cakeID__$}', '{$__cakeForeignKey__$}'), true)) {
331
                        return $data;
332
                }
333
334
                if ($data === null || (is_array($data) && empty($data))) {
335
                        return 'NULL';
336
                }
337
338
                if (empty($column)) {
339
                        $column = $this->introspectType($data);
340
                }
341
342
                switch ($column) {
343
                        case 'binary':
344
                                return $this->_connection->quote($data, PDO::PARAM_LOB);
345
                        case 'boolean':
346
                                return $this->_connection->quote($this->boolean($data, true), PDO::PARAM_BOOL);
347
                        case 'string':
348
                        case 'text':
349
                                return $this->_connection->quote($data, PDO::PARAM_STR);
350
                        default:
351
                                if ($data === '') {
352
                                        return $null ? 'NULL' : '""';
353
                                }
354
                                if (is_float($data)) {
355
                                        return str_replace(',', '.', strval($data));
356
                                }
357
                                if ((is_int($data) || $data === '0') || (
358
                                        is_numeric($data) &&
359
                                        strpos($data, ',') === false &&
360
                                        $data[0] != '0' &&
361
                                        strpos($data, 'e') === false)
362
                                ) {
363
                                        return $data;
364
                                }
365
                                return $this->_connection->quote($data);
366
                }
367
        }
368
369
/**
370
 * Returns an object to represent a database identifier in a query. Expression objects
371
 * are not sanitized or escaped.
372
 *
373
 * @param string $identifier A SQL expression to be used as an identifier
374
 * @return stdClass An object representing a database identifier to be used in a query
375
 */
376
        public function identifier($identifier) {
377
                $obj = new stdClass();
378
                $obj->type = 'identifier';
379
                $obj->value = $identifier;
380
                return $obj;
381
        }
382
383
/**
384
 * Returns an object to represent a database expression in a query. Expression objects
385
 * are not sanitized or escaped.
386
 *
387
 * @param string $expression An arbitrary SQL expression to be inserted into a query.
388
 * @return stdClass An object representing a database expression to be used in a query
389
 */
390
        public function expression($expression) {
391
                $obj = new stdClass();
392
                $obj->type = 'expression';
393
                $obj->value = $expression;
394
                return $obj;
395
        }
396
397
/**
398
 * Executes given SQL statement.
399
 *
400
 * @param string $sql SQL statement
401
 * @param array $params Additional options for the query.
402
 * @return mixed Resource or object representing the result set, or false on failure
403
 */
404
        public function rawQuery($sql, $params = array()) {
405
                $this->took = $this->numRows = false;
406
                return $this->execute($sql, array(), $params);
407
        }
408
409
/**
410
 * Queries the database with given SQL statement, and obtains some metadata about the result
411
 * (rows affected, timing, any errors, number of rows in resultset). The query is also logged.
412
 * If Configure::read('debug') is set, the log is shown all the time, else it is only shown on errors.
413
 *
414
 * ### Options
415
 *
416
 * - log - Whether or not the query should be logged to the memory log.
417
 *
418
 * @param string $sql SQL statement
419
 * @param array $options The options for executing the query.
420
 * @param array $params values to be bound to the query.
421
 * @return mixed Resource or object representing the result set, or false on failure
422
 */
423
        public function execute($sql, $options = array(), $params = array()) {
424
                $options += array('log' => $this->fullDebug);
425
426
                $t = microtime(true);
427
                $this->_result = $this->_execute($sql, $params);
428
429
                if ($options['log']) {
430
                        $this->took = round((microtime(true) - $t) * 1000, 0);
431
                        $this->numRows = $this->affected = $this->lastAffected();
432
                        $this->logQuery($sql, $params);
433
                }
434
435
                return $this->_result;
436
        }
437
438
/**
439
 * Executes given SQL statement.
440
 *
441
 * @param string $sql SQL statement
442
 * @param array $params list of params to be bound to query
443
 * @param array $prepareOptions Options to be used in the prepare statement
444
 * @return mixed PDOStatement if query executes with no problem, true as the result of a successful, false on error
445
 * query returning no rows, such as a CREATE statement, false otherwise
446
 * @throws PDOException
447
 */
448
        protected function _execute($sql, $params = array(), $prepareOptions = array()) {
449
                $sql = trim($sql);
450
                if (preg_match('/^(?:CREATE|ALTER|DROP)\s+(?:TABLE|INDEX)/i', $sql)) {
451
                        $statements = array_filter(explode(';', $sql));
452
                        if (count($statements) > 1) {
453
                                $result = array_map(array($this, '_execute'), $statements);
454
                                return array_search(false, $result) === false;
455
                        }
456
                }
457
458
                try {
459
                        $query = $this->_connection->prepare($sql, $prepareOptions);
460
                        $query->setFetchMode(PDO::FETCH_LAZY);
461
                        if (!$query->execute($params)) {
462
                                $this->_results = $query;
463
                                $query->closeCursor();
464
                                return false;
465
                        }
466
                        if (!$query->columnCount()) {
467
                                $query->closeCursor();
468
                                if (!$query->rowCount()) {
469
                                        return true;
470
                                }
471
                        }
472
                        return $query;
473
                } catch (PDOException $e) {
474
                        if (isset($query->queryString)) {
475
                                $e->queryString = $query->queryString;
476
                        } else {
477
                                $e->queryString = $sql;
478
                        }
479
                        throw $e;
480
                }
481
        }
482
483
/**
484
 * Returns a formatted error message from previous database operation.
485
 *
486
 * @param PDOStatement $query the query to extract the error from if any
487
 * @return string Error message with error number
488
 */
489
        public function lastError(PDOStatement $query = null) {
490
                if ($query) {
491
                        $error = $query->errorInfo();
492
                } else {
493
                        $error = $this->_connection->errorInfo();
494
                }
495
                if (empty($error[2])) {
496
                        return null;
497
                }
498
                return $error[1] . ': ' . $error[2];
499
        }
500
501
/**
502
 * Returns number of affected rows in previous database operation. If no previous operation exists,
503
 * this returns false.
504
 *
505
 * @param mixed $source The source to check.
506
 * @return int Number of affected rows
507
 */
508
        public function lastAffected($source = null) {
509
                if ($this->hasResult()) {
510
                        return $this->_result->rowCount();
511
                }
512
                return 0;
513
        }
514
515
/**
516
 * Returns number of rows in previous resultset. If no previous resultset exists,
517
 * this returns false.
518
 *
519
 * @param mixed $source Not used
520
 * @return int Number of rows in resultset
521
 */
522
        public function lastNumRows($source = null) {
523
                return $this->lastAffected();
524
        }
525
526
/**
527
 * DataSource Query abstraction
528
 *
529
 * @return resource Result resource identifier.
530
 */
531
        public function query() {
532
                $args = func_get_args();
533
                $fields = null;
534
                $order = null;
535
                $limit = null;
536
                $page = null;
537
                $recursive = null;
538
539
                if (count($args) === 1) {
540
                        return $this->fetchAll($args[0]);
541
                } elseif (count($args) > 1 && (strpos($args[0], 'findBy') === 0 || strpos($args[0], 'findAllBy') === 0)) {
542
                        $params = $args[1];
543
544
                        if (substr($args[0], 0, 6) === 'findBy') {
545
                                $all = false;
546
                                $field = Inflector::underscore(substr($args[0], 6));
547
                        } else {
548
                                $all = true;
549
                                $field = Inflector::underscore(substr($args[0], 9));
550
                        }
551
552
                        $or = (strpos($field, '_or_') !== false);
553
                        if ($or) {
554
                                $field = explode('_or_', $field);
555
                        } else {
556
                                $field = explode('_and_', $field);
557
                        }
558
                        $off = count($field) - 1;
559
560
                        if (isset($params[1 + $off])) {
561
                                $fields = $params[1 + $off];
562
                        }
563
564
                        if (isset($params[2 + $off])) {
565
                                $order = $params[2 + $off];
566
                        }
567
568
                        if (!array_key_exists(0, $params)) {
569
                                return false;
570
                        }
571
572
                        $c = 0;
573
                        $conditions = array();
574
575
                        foreach ($field as $f) {
576
                                $conditions[$args[2]->alias . '.' . $f] = $params[$c++];
577
                        }
578
579
                        if ($or) {
580
                                $conditions = array('OR' => $conditions);
581
                        }
582
583
                        if ($all) {
584
                                if (isset($params[3 + $off])) {
585
                                        $limit = $params[3 + $off];
586
                                }
587
588
                                if (isset($params[4 + $off])) {
589
                                        $page = $params[4 + $off];
590
                                }
591
592
                                if (isset($params[5 + $off])) {
593
                                        $recursive = $params[5 + $off];
594
                                }
595
                                return $args[2]->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive'));
596
                        }
597
                        if (isset($params[3 + $off])) {
598
                                $recursive = $params[3 + $off];
599
                        }
600
                        return $args[2]->find('first', compact('conditions', 'fields', 'order', 'recursive'));
601
                }
602
                if (isset($args[1]) && $args[1] === true) {
603
                        return $this->fetchAll($args[0], true);
604
                } elseif (isset($args[1]) && !is_array($args[1])) {
605
                        return $this->fetchAll($args[0], false);
606
                } elseif (isset($args[1]) && is_array($args[1])) {
607
                        if (isset($args[2])) {
608
                                $cache = $args[2];
609
                        } else {
610
                                $cache = true;
611
                        }
612
                        return $this->fetchAll($args[0], $args[1], array('cache' => $cache));
613
                }
614
        }
615
616
/**
617
 * Returns a row from current resultset as an array
618
 *
619
 * @param string $sql Some SQL to be executed.
620
 * @return array The fetched row as an array
621
 */
622
        public function fetchRow($sql = null) {
623
                if (is_string($sql) && strlen($sql) > 5 && !$this->execute($sql)) {
624
                        return null;
625
                }
626
627
                if ($this->hasResult()) {
628
                        $this->resultSet($this->_result);
629
                        $resultRow = $this->fetchResult();
630
                        if (isset($resultRow[0])) {
631
                                $this->fetchVirtualField($resultRow);
632
                        }
633
                        return $resultRow;
634
                }
635
                return null;
636
        }
637
638
/**
639
 * Returns an array of all result rows for a given SQL query.
640
 *
641
 * Returns false if no rows matched.
642
 *
643
 * ### Options
644
 *
645
 * - `cache` - Returns the cached version of the query, if exists and stores the result in cache.
646
 *   This is a non-persistent cache, and only lasts for a single request. This option
647
 *   defaults to true. If you are directly calling this method, you can disable caching
648
 *   by setting $options to `false`
649
 *
650
 * @param string $sql SQL statement
651
 * @param array|bool $params Either parameters to be bound as values for the SQL statement,
652
 *  or a boolean to control query caching.
653
 * @param array $options additional options for the query.
654
 * @return bool|array Array of resultset rows, or false if no rows matched
655
 */
656
        public function fetchAll($sql, $params = array(), $options = array()) {
657
                if (is_string($options)) {
658
                        $options = array('modelName' => $options);
659
                }
660
                if (is_bool($params)) {
661
                        $options['cache'] = $params;
662
                        $params = array();
663
                }
664
                $options += array('cache' => true);
665
                $cache = $options['cache'];
666
                if ($cache && ($cached = $this->getQueryCache($sql, $params)) !== false) {
667
                        return $cached;
668
                }
669
                $result = $this->execute($sql, array(), $params);
670
                if ($result) {
671
                        $out = array();
672
673
                        if ($this->hasResult()) {
674
                                $first = $this->fetchRow();
675
                                if ($first) {
676
                                        $out[] = $first;
677
                                }
678
                                while ($item = $this->fetchResult()) {
679
                                        if (isset($item[0])) {
680
                                                $this->fetchVirtualField($item);
681
                                        }
682
                                        $out[] = $item;
683
                                }
684
                        }
685
686
                        if (!is_bool($result) && $cache) {
687
                                $this->_writeQueryCache($sql, $out, $params);
688
                        }
689
690
                        if (empty($out) && is_bool($this->_result)) {
691
                                return $this->_result;
692
                        }
693
                        return $out;
694
                }
695
                return false;
696
        }
697
698
/**
699
 * Fetches the next row from the current result set
700
 *
701
 * @return bool
702
 */
703
        public function fetchResult() {
704
                return false;
705
        }
706
707
/**
708
 * Modifies $result array to place virtual fields in model entry where they belongs to
709
 *
710
 * @param array &$result Reference to the fetched row
711
 * @return void
712
 */
713
        public function fetchVirtualField(&$result) {
714
                if (isset($result[0]) && is_array($result[0])) {
715
                        foreach ($result[0] as $field => $value) {
716
                                if (strpos($field, $this->virtualFieldSeparator) === false) {
717
                                        continue;
718
                                }
719
720
                                list($alias, $virtual) = explode($this->virtualFieldSeparator, $field);
721
722
                                if (!ClassRegistry::isKeySet($alias)) {
723
                                        return;
724
                                }
725
726
                                $Model = ClassRegistry::getObject($alias);
727
728
                                if ($Model->isVirtualField($virtual)) {
729
                                        $result[$alias][$virtual] = $value;
730
                                        unset($result[0][$field]);
731
                                }
732
                        }
733
                        if (empty($result[0])) {
734
                                unset($result[0]);
735
                        }
736
                }
737
        }
738
739
/**
740
 * Returns a single field of the first of query results for a given SQL query, or false if empty.
741
 *
742
 * @param string $name The name of the field to get.
743
 * @param string $sql The SQL query.
744
 * @return mixed Value of field read, or false if not found.
745
 */
746
        public function field($name, $sql) {
747
                $data = $this->fetchRow($sql);
748
                if (empty($data[$name])) {
749
                        return false;
750
                }
751
                return $data[$name];
752
        }
753
754
/**
755
 * Empties the method caches.
756
 * These caches are used by DboSource::name() and DboSource::conditions()
757
 *
758
 * @return void
759
 */
760
        public function flushMethodCache() {
761
                $this->_methodCacheChange = true;
762
                static::$methodCache = array();
763
        }
764
765
/**
766
 * Cache a value into the methodCaches. Will respect the value of DboSource::$cacheMethods.
767
 * Will retrieve a value from the cache if $value is null.
768
 *
769
 * If caching is disabled and a write is attempted, the $value will be returned.
770
 * A read will either return the value or null.
771
 *
772
 * @param string $method Name of the method being cached.
773
 * @param string $key The key name for the cache operation.
774
 * @param mixed $value The value to cache into memory.
775
 * @return mixed Either null on failure, or the value if its set.
776
 */
777
        public function cacheMethod($method, $key, $value = null) {
778
                if ($this->cacheMethods === false) {
779
                        return $value;
780
                }
781
                if (!$this->_methodCacheChange && empty(static::$methodCache)) {
782
                        static::$methodCache = (array)Cache::read('method_cache', '_cake_core_');
783
                }
784
                if ($value === null) {
785
                        return (isset(static::$methodCache[$method][$key])) ? static::$methodCache[$method][$key] : null;
786
                }
787
                $this->_methodCacheChange = true;
788
                return static::$methodCache[$method][$key] = $value;
789
        }
790
791
/**
792
 * Returns a quoted name of $data for use in an SQL statement.
793
 * Strips fields out of SQL functions before quoting.
794
 *
795
 * Results of this method are stored in a memory cache. This improves performance, but
796
 * because the method uses a hashing algorithm it can have collisions.
797
 * Setting DboSource::$cacheMethods to false will disable the memory cache.
798
 *
799
 * @param mixed $data Either a string with a column to quote. An array of columns to quote or an
800
 *   object from DboSource::expression() or DboSource::identifier()
801
 * @return string SQL field
802
 */
803
        public function name($data) {
804
                if (is_object($data) && isset($data->type)) {
805
                        return $data->value;
806
                }
807
                if ($data === '*') {
808
                        return '*';
809
                }
810
                if (is_array($data)) {
811
                        foreach ($data as $i => $dataItem) {
812
                                $data[$i] = $this->name($dataItem);
813
                        }
814
                        return $data;
815
                }
816
                $cacheKey = md5($this->startQuote . $data . $this->endQuote);
817
                if ($return = $this->cacheMethod(__FUNCTION__, $cacheKey)) {
818
                        return $return;
819
                }
820
                $data = trim($data);
821
                if (preg_match('/^[\w-]+(?:\.[^ \*]*)*$/', $data)) { // string, string.string
822
                        if (strpos($data, '.') === false) { // string
823
                                return $this->cacheMethod(__FUNCTION__, $cacheKey, $this->startQuote . $data . $this->endQuote);
824
                        }
825
                        $items = explode('.', $data);
826
                        return $this->cacheMethod(__FUNCTION__, $cacheKey,
827
                                $this->startQuote . implode($this->endQuote . '.' . $this->startQuote, $items) . $this->endQuote
828
                        );
829
                }
830
                if (preg_match('/^[\w-]+\.\*$/', $data)) { // string.*
831
                        return $this->cacheMethod(__FUNCTION__, $cacheKey,
832
                                $this->startQuote . str_replace('.*', $this->endQuote . '.*', $data)
833
                        );
834
                }
835
                if (preg_match('/^([\w-]+)\((.*)\)$/', $data, $matches)) { // Functions
836
                        return $this->cacheMethod(__FUNCTION__, $cacheKey,
837
                                $matches[1] . '(' . $this->name($matches[2]) . ')'
838
                        );
839
                }
840
                if (preg_match('/^([\w-]+(\.[\w-]+|\(.*\))*)\s+' . preg_quote($this->alias) . '\s*([\w-]+)$/i', $data, $matches)) {
841
                        return $this->cacheMethod(
842
                                __FUNCTION__, $cacheKey,
843
                                preg_replace(
844
                                        '/\s{2,}/', ' ', $this->name($matches[1]) . ' ' . $this->alias . ' ' . $this->name($matches[3])
845
                                )
846
                        );
847
                }
848
                if (preg_match('/^[\w-_\s]*[\w-_]+/', $data)) {
849
                        return $this->cacheMethod(__FUNCTION__, $cacheKey, $this->startQuote . $data . $this->endQuote);
850
                }
851
                return $this->cacheMethod(__FUNCTION__, $cacheKey, $data);
852
        }
853
854
/**
855
 * Checks if the source is connected to the database.
856
 *
857
 * @return bool True if the database is connected, else false
858
 */
859
        public function isConnected() {
860
                if ($this->_connection === null) {
861
                        $connected = false;
862
                } else {
863
                        try {
864
                                $connected = $this->_connection->query('SELECT 1');
865
                        } catch (Exception $e) {
866
                                $connected = false;
867
                        }
868
                }
869
                $this->connected = !empty($connected);
870
                return $this->connected;
871
        }
872
873
/**
874
 * Checks if the result is valid
875
 *
876
 * @return bool True if the result is valid else false
877
 */
878
        public function hasResult() {
879
                return $this->_result instanceof PDOStatement;
880
        }
881
882
/**
883
 * Get the query log as an array.
884
 *
885
 * @param bool $sorted Get the queries sorted by time taken, defaults to false.
886
 * @param bool $clear If True the existing log will cleared.
887
 * @return array Array of queries run as an array
888
 */
889
        public function getLog($sorted = false, $clear = true) {
890
                if ($sorted) {
891
                        $log = sortByKey($this->_queriesLog, 'took', 'desc', SORT_NUMERIC);
892
                } else {
893
                        $log = $this->_queriesLog;
894
                }
895
                if ($clear) {
896
                        $this->_queriesLog = array();
897
                }
898
                return array('log' => $log, 'count' => $this->_queriesCnt, 'time' => $this->_queriesTime);
899
        }
900
901
/**
902
 * Outputs the contents of the queries log. If in a non-CLI environment the sql_log element
903
 * will be rendered and output. If in a CLI environment, a plain text log is generated.
904
 *
905
 * @param bool $sorted Get the queries sorted by time taken, defaults to false.
906
 * @return void
907
 */
908
        public function showLog($sorted = false) {
909
                $log = $this->getLog($sorted, false);
910
                if (empty($log['log'])) {
911
                        return;
912
                }
913
                if (PHP_SAPI !== 'cli') {
914
                        $controller = null;
915
                        $View = new View($controller, false);
916
                        $View->set('sqlLogs', array($this->configKeyName => $log));
917
                        echo $View->element('sql_dump', array('_forced_from_dbo_' => true));
918
                } else {
919
                        foreach ($log['log'] as $k => $i) {
920
                                print (($k + 1) . ". {$i['query']}\n");
921
                        }
922
                }
923
        }
924
925
/**
926
 * Log given SQL query.
927
 *
928
 * @param string $sql SQL statement
929
 * @param array $params Values binded to the query (prepared statements)
930
 * @return void
931
 */
932
        public function logQuery($sql, $params = array()) {
933
                $this->_queriesCnt++;
934
                $this->_queriesTime += $this->took;
935
                $this->_queriesLog[] = array(
936
                        'query' => $sql,
937
                        'params' => $params,
938
                        'affected' => $this->affected,
939
                        'numRows' => $this->numRows,
940
                        'took' => $this->took
941
                );
942
                if (count($this->_queriesLog) > $this->_queriesLogMax) {
943
                        array_shift($this->_queriesLog);
944
                }
945
        }
946
947
/**
948
 * Gets full table name including prefix
949
 *
950
 * @param Model|string $model Either a Model object or a string table name.
951
 * @param bool $quote Whether you want the table name quoted.
952
 * @param bool $schema Whether you want the schema name included.
953
 * @return string Full quoted table name
954
 */
955
        public function fullTableName($model, $quote = true, $schema = true) {
956
                if (is_object($model)) {
957
                        $schemaName = $model->schemaName;
958
                        $table = $model->tablePrefix . $model->table;
959
                } elseif (!empty($this->config['prefix']) && strpos($model, $this->config['prefix']) !== 0) {
960
                        $table = $this->config['prefix'] . strval($model);
961
                } else {
962
                        $table = strval($model);
963
                }
964
965
                if ($schema && !isset($schemaName)) {
966
                        $schemaName = $this->getSchemaName();
967
                }
968
969
                if ($quote) {
970
                        if ($schema && !empty($schemaName)) {
971
                                if (strstr($table, '.') === false) {
972
                                        return $this->name($schemaName) . '.' . $this->name($table);
973
                                }
974
                        }
975
                        return $this->name($table);
976
                }
977
978
                if ($schema && !empty($schemaName)) {
979
                        if (strstr($table, '.') === false) {
980
                                return $schemaName . '.' . $table;
981
                        }
982
                }
983
984
                return $table;
985
        }
986
987
/**
988
 * The "C" in CRUD
989
 *
990
 * Creates new records in the database.
991
 *
992
 * @param Model $Model Model object that the record is for.
993
 * @param array $fields An array of field names to insert. If null, $Model->data will be
994
 *   used to generate field names.
995
 * @param array $values An array of values with keys matching the fields. If null, $Model->data will
996
 *   be used to generate values.
997
 * @return bool Success
998
 */
999
        public function create(Model $Model, $fields = null, $values = null) {
1000
                $id = null;
1001
1002
                if (!$fields) {
1003
                        unset($fields, $values);
1004
                        $fields = array_keys($Model->data);
1005
                        $values = array_values($Model->data);
1006
                }
1007
                $count = count($fields);
1008
1009
                for ($i = 0; $i < $count; $i++) {
1010
                        $schema = $Model->schema();
1011
                        $valueInsert[] = $this->value($values[$i], $Model->getColumnType($fields[$i]), isset($schema[$fields[$i]]) ? $schema[$fields[$i]]['null'] : true);
1012
                        $fieldInsert[] = $this->name($fields[$i]);
1013
                        if ($fields[$i] === $Model->primaryKey) {
1014
                                $id = $values[$i];
1015
                        }
1016
                }
1017
1018
                $query = array(
1019
                        'table' => $this->fullTableName($Model),
1020
                        'fields' => implode(', ', $fieldInsert),
1021
                        'values' => implode(', ', $valueInsert)
1022
                );
1023
1024
                if ($this->execute($this->renderStatement('create', $query))) {
1025
                        if (empty($id)) {
1026
                                $id = $this->lastInsertId($this->fullTableName($Model, false, false), $Model->primaryKey);
1027
                        }
1028
                        $Model->setInsertID($id);
1029
                        $Model->id = $id;
1030
                        return true;
1031
                }
1032
1033
                $Model->onError();
1034
                return false;
1035
        }
1036
1037
/**
1038
 * The "R" in CRUD
1039
 *
1040
 * Reads record(s) from the database.
1041
 *
1042
 * @param Model $Model A Model object that the query is for.
1043
 * @param array $queryData An array of queryData information containing keys similar to Model::find().
1044
 * @param int $recursive Number of levels of association
1045
 * @return mixed boolean false on error/failure. An array of results on success.
1046
 */
1047
        public function read(Model $Model, $queryData = array(), $recursive = null) {
1048
                $queryData = $this->_scrubQueryData($queryData);
1049
1050
                $array = array('callbacks' => $queryData['callbacks']);
1051
1052
                if ($recursive === null && isset($queryData['recursive'])) {
1053
                        $recursive = $queryData['recursive'];
1054
                }
1055
1056
                if ($recursive !== null) {
1057
                        $modelRecursive = $Model->recursive;
1058
                        $Model->recursive = $recursive;
1059
                }
1060
1061
                if (!empty($queryData['fields'])) {
1062
                        $noAssocFields = true;
1063
                        $queryData['fields'] = $this->fields($Model, null, $queryData['fields']);
1064
                } else {
1065
                        $noAssocFields = false;
1066
                        $queryData['fields'] = $this->fields($Model);
1067
                }
1068
1069
                if ($Model->recursive === -1) {
1070
                        // Primary model data only, no joins.
1071
                        $associations = array();
1072
1073
                } else {
1074
                        $associations = $Model->associations();
1075
1076
                        if ($Model->recursive === 0) {
1077
                                // Primary model data and its domain.
1078
                                unset($associations[2], $associations[3]);
1079
                        }
1080
                }
1081
1082
                $originalJoins = $queryData['joins'];
1083
                $queryData['joins'] = array();
1084
1085
                // Generate hasOne and belongsTo associations inside $queryData
1086
                $linkedModels = array();
1087
                foreach ($associations as $type) {
1088
                        if ($type !== 'hasOne' && $type !== 'belongsTo') {
1089
                                continue;
1090
                        }
1091
1092
                        foreach ($Model->{$type} as $assoc => $assocData) {
1093
                                $LinkModel = $Model->{$assoc};
1094
1095
                                if ($Model->useDbConfig !== $LinkModel->useDbConfig) {
1096
                                        continue;
1097
                                }
1098
1099
                                if ($noAssocFields) {
1100
                                        $assocData['fields'] = false;
1101
                                }
1102
1103
                                $external = isset($assocData['external']);
1104
1105
                                if ($this->generateAssociationQuery($Model, $LinkModel, $type, $assoc, $assocData, $queryData, $external) === true) {
1106
                                        $linkedModels[$type . '/' . $assoc] = true;
1107
                                }
1108
                        }
1109
                }
1110
1111
                if (!empty($originalJoins)) {
1112
                        $queryData['joins'] = array_merge($queryData['joins'], $originalJoins);
1113
                }
1114
1115
                // Build SQL statement with the primary model, plus hasOne and belongsTo associations
1116
                $query = $this->buildAssociationQuery($Model, $queryData);
1117
1118
                $resultSet = $this->fetchAll($query, $Model->cacheQueries);
1119
                unset($query);
1120
1121
                if ($resultSet === false) {
1122
                        $Model->onError();
1123
                        return false;
1124
                }
1125
1126
                $filtered = array();
1127
1128
                // Deep associations
1129
                if ($Model->recursive > -1) {
1130
                        $joined = array();
1131
                        if (isset($queryData['joins'][0]['alias'])) {
1132
                                $joined[$Model->alias] = (array)Hash::extract($queryData['joins'], '{n}.alias');
1133
                        }
1134
1135
                        foreach ($associations as $type) {
1136
                                foreach ($Model->{$type} as $assoc => $assocData) {
1137
                                        $LinkModel = $Model->{$assoc};
1138
1139
                                        if (!isset($linkedModels[$type . '/' . $assoc])) {
1140
                                                $db = $Model->useDbConfig === $LinkModel->useDbConfig ? $this : $LinkModel->getDataSource();
1141
                                        } elseif ($Model->recursive > 1) {
1142
                                                $db = $this;
1143
                                        }
1144
1145
                                        if (isset($db) && method_exists($db, 'queryAssociation')) {
1146
                                                $stack = array($assoc);
1147
                                                $stack['_joined'] = $joined;
1148
1149
                                                $db->queryAssociation($Model, $LinkModel, $type, $assoc, $assocData, $array, true, $resultSet, $Model->recursive - 1, $stack);
1150
                                                unset($db);
1151
1152
                                                if ($type === 'hasMany' || $type === 'hasAndBelongsToMany') {
1153
                                                        $filtered[] = $assoc;
1154
                                                }
1155
                                        }
1156
                                }
1157
                        }
1158
                }
1159
1160
                if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') {
1161
                        $this->_filterResults($resultSet, $Model, $filtered);
1162
                }
1163
1164
                if ($recursive !== null) {
1165
                        $Model->recursive = $modelRecursive;
1166
                }
1167
1168
                return $resultSet;
1169
        }
1170
1171
/**
1172
 * Passes association results through afterFind filters of the corresponding model.
1173
 *
1174
 * The primary model is always excluded, because the filtering is later done by Model::_filterResults().
1175
 *
1176
 * @param array &$resultSet Reference of resultset to be filtered.
1177
 * @param Model $Model Instance of model to operate against.
1178
 * @param array $filtered List of classes already filtered, to be skipped.
1179
 * @return array Array of results that have been filtered through $Model->afterFind.
1180
 */
1181
        protected function _filterResults(&$resultSet, Model $Model, $filtered = array()) {
1182
                if (!is_array($resultSet)) {
1183
                        return array();
1184
                }
1185
1186
                $current = reset($resultSet);
1187
                if (!is_array($current)) {
1188
                        return array();
1189
                }
1190
1191
                $keys = array_diff(array_keys($current), $filtered, array($Model->alias));
1192
                $filtering = array();
1193
1194
                foreach ($keys as $className) {
1195
                        if (!isset($Model->{$className}) || !is_object($Model->{$className})) {
1196
                                continue;
1197
                        }
1198
1199
                        $LinkedModel = $Model->{$className};
1200
                        $filtering[] = $className;
1201
1202
                        foreach ($resultSet as $key => &$result) {
1203
                                $data = $LinkedModel->afterFind(array(array($className => $result[$className])), false);
1204
                                if (isset($data[0][$className])) {
1205
                                        $result[$className] = $data[0][$className];
1206
                                } else {
1207
                                        unset($resultSet[$key]);
1208
                                }
1209
                        }
1210
                }
1211
1212
                return $filtering;
1213
        }
1214
1215
/**
1216
 * Passes association results through afterFind filters of the corresponding model.
1217
 *
1218
 * Similar to DboSource::_filterResults(), but this filters only specified models.
1219
 * The primary model can not be specified, because this call DboSource::_filterResults() internally.
1220
 *
1221
 * @param array &$resultSet Reference of resultset to be filtered.
1222
 * @param Model $Model Instance of model to operate against.
1223
 * @param array $toBeFiltered List of classes to be filtered.
1224
 * @return array Array of results that have been filtered through $Model->afterFind.
1225
 */
1226
        protected function _filterResultsInclusive(&$resultSet, Model $Model, $toBeFiltered = array()) {
1227
                $exclude = array();
1228
1229
                if (is_array($resultSet)) {
1230
                        $current = reset($resultSet);
1231
                        if (is_array($current)) {
1232
                                $exclude = array_diff(array_keys($current), $toBeFiltered);
1233
                        }
1234
                }
1235
1236
                return $this->_filterResults($resultSet, $Model, $exclude);
1237
        }
1238
1239
/**
1240
 * Queries associations.
1241
 *
1242
 * Used to fetch results on recursive models.
1243
 *
1244
 * - 'hasMany' associations with no limit set:
1245
 *    Fetch, filter and merge is done recursively for every level.
1246
 *
1247
 * - 'hasAndBelongsToMany' associations:
1248
 *    Fetch and filter is done unaffected by the (recursive) level set.
1249
 *
1250
 * @param Model $Model Primary Model object.
1251
 * @param Model $LinkModel Linked model object.
1252
 * @param string $type Association type, one of the model association types ie. hasMany.
1253
 * @param string $association Association name.
1254
 * @param array $assocData Association data.
1255
 * @param array &$queryData An array of queryData information containing keys similar to Model::find().
1256
 * @param bool $external Whether or not the association query is on an external datasource.
1257
 * @param array &$resultSet Existing results.
1258
 * @param int $recursive Number of levels of association.
1259
 * @param array $stack A list with joined models.
1260
 * @return mixed
1261
 * @throws CakeException when results cannot be created.
1262
 */
1263
        public function queryAssociation(Model $Model, Model $LinkModel, $type, $association, $assocData, &$queryData, $external, &$resultSet, $recursive, $stack) {
1264
                if (isset($stack['_joined'])) {
1265
                        $joined = $stack['_joined'];
1266
                        unset($stack['_joined']);
1267
                }
1268
1269
                $queryTemplate = $this->generateAssociationQuery($Model, $LinkModel, $type, $association, $assocData, $queryData, $external);
1270
                if (empty($queryTemplate)) {
1271
                        return null;
1272
                }
1273
1274
                if (!is_array($resultSet)) {
1275
                        throw new CakeException(__d('cake_dev', 'Error in Model %s', get_class($Model)));
1276
                }
1277
1278
                if ($type === 'hasMany' && empty($assocData['limit']) && !empty($assocData['foreignKey'])) {
1279
                        // 'hasMany' associations with no limit set.
1280
1281
                        $assocIds = array();
1282
                        foreach ($resultSet as $result) {
1283
                                $assocIds[] = $this->insertQueryData('{$__cakeID__$}', $result, $association, $Model, $stack);
1284
                        }
1285
                        $assocIds = array_filter($assocIds);
1286
1287
                        // Fetch
1288
                        $assocResultSet = array();
1289
                        if (!empty($assocIds)) {
1290
                                $assocResultSet = $this->_fetchHasMany($Model, $queryTemplate, $assocIds);
1291
                        }
1292
1293
                        // Recursively query associations
1294
                        if ($recursive > 0 && !empty($assocResultSet) && is_array($assocResultSet)) {
1295
                                foreach ($LinkModel->associations() as $type1) {
1296
                                        foreach ($LinkModel->{$type1} as $assoc1 => $assocData1) {
1297
                                                $DeepModel = $LinkModel->{$assoc1};
1298
                                                $tmpStack = $stack;
1299
                                                $tmpStack[] = $assoc1;
1300
1301
                                                $db = $LinkModel->useDbConfig === $DeepModel->useDbConfig ? $this : $DeepModel->getDataSource();
1302
1303
                                                $db->queryAssociation($LinkModel, $DeepModel, $type1, $assoc1, $assocData1, $queryData, true, $assocResultSet, $recursive - 1, $tmpStack);
1304
                                        }
1305
                                }
1306
                        }
1307
1308
                        // Filter
1309
                        if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') {
1310
                                $this->_filterResultsInclusive($assocResultSet, $Model, array($association));
1311
                        }
1312
1313
                        // Merge
1314
                        return $this->_mergeHasMany($resultSet, $assocResultSet, $association, $Model);
1315
1316
                } elseif ($type === 'hasAndBelongsToMany') {
1317
                        // 'hasAndBelongsToMany' associations.
1318
1319
                        $assocIds = array();
1320
                        foreach ($resultSet as $result) {
1321
                                $assocIds[] = $this->insertQueryData('{$__cakeID__$}', $result, $association, $Model, $stack);
1322
                        }
1323
                        $assocIds = array_filter($assocIds);
1324
1325
                        // Fetch
1326
                        $assocResultSet = array();
1327
                        if (!empty($assocIds)) {
1328
                                $assocResultSet = $this->_fetchHasAndBelongsToMany($Model, $queryTemplate, $assocIds, $association);
1329
                        }
1330
1331
                        $habtmAssocData = $Model->hasAndBelongsToMany[$association];
1332
                        $foreignKey = $habtmAssocData['foreignKey'];
1333
                        $joinKeys = array($foreignKey, $habtmAssocData['associationForeignKey']);
1334
                        list($with, $habtmFields) = $Model->joinModel($habtmAssocData['with'], $joinKeys);
1335
                        $habtmFieldsCount = count($habtmFields);
1336
1337
                        // Filter
1338
                        if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') {
1339
                                $this->_filterResultsInclusive($assocResultSet, $Model, array($association, $with));
1340
                        }
1341
                }
1342
1343
                $modelAlias = $Model->alias;
1344
                $primaryKey = $Model->primaryKey;
1345
                $selfJoin = ($Model->name === $LinkModel->name);
1346
1347
                foreach ($resultSet as &$row) {
1348
                        if ($type === 'hasOne' || $type === 'belongsTo' || $type === 'hasMany') {
1349
                                $assocResultSet = array();
1350
                                $prefetched = false;
1351
1352
                                if (($type === 'hasOne' || $type === 'belongsTo') &&
1353
                                        isset($row[$LinkModel->alias], $joined[$Model->alias]) &&
1354
                                        in_array($LinkModel->alias, $joined[$Model->alias])
1355
                                ) {
1356
                                        $joinedData = Hash::filter($row[$LinkModel->alias]);
1357
                                        if (!empty($joinedData)) {
1358
                                                $assocResultSet[0] = array($LinkModel->alias => $row[$LinkModel->alias]);
1359
                                        }
1360
                                        $prefetched = true;
1361
                                } else {
1362
                                        $query = $this->insertQueryData($queryTemplate, $row, $association, $Model, $stack);
1363
                                        if ($query !== false) {
1364
                                                $assocResultSet = $this->fetchAll($query, $Model->cacheQueries);
1365
                                        }
1366
                                }
1367
                        }
1368
1369
                        if (!empty($assocResultSet) && is_array($assocResultSet)) {
1370
                                if ($recursive > 0) {
1371
                                        foreach ($LinkModel->associations() as $type1) {
1372
                                                foreach ($LinkModel->{$type1} as $assoc1 => $assocData1) {
1373
                                                        $DeepModel = $LinkModel->{$assoc1};
1374
1375
                                                        if ($type1 === 'belongsTo' ||
1376
                                                                ($type === 'belongsTo' && $DeepModel->alias === $modelAlias) ||
1377
                                                                ($DeepModel->alias !== $modelAlias)
1378
                                                        ) {
1379
                                                                $tmpStack = $stack;
1380
                                                                $tmpStack[] = $assoc1;
1381
1382
                                                                $db = $LinkModel->useDbConfig === $DeepModel->useDbConfig ? $this : $DeepModel->getDataSource();
1383
1384
                                                                $db->queryAssociation($LinkModel, $DeepModel, $type1, $assoc1, $assocData1, $queryData, true, $assocResultSet, $recursive - 1, $tmpStack);
1385
                                                        }
1386
                                                }
1387
                                        }
1388
                                }
1389
1390
                                if ($type === 'hasAndBelongsToMany') {
1391
                                        $merge = array();
1392
                                        foreach ($assocResultSet as $data) {
1393
                                                if (isset($data[$with]) && $data[$with][$foreignKey] === $row[$modelAlias][$primaryKey]) {
1394
                                                        if ($habtmFieldsCount <= 2) {
1395
                                                                unset($data[$with]);
1396
                                                        }
1397
                                                        $merge[] = $data;
1398
                                                }
1399
                                        }
1400
1401
                                        if (empty($merge) && !isset($row[$association])) {
1402
                                                $row[$association] = $merge;
1403
                                        } else {
1404
                                                $this->_mergeAssociation($row, $merge, $association, $type);
1405
                                        }
1406
                                } else {
1407
                                        if (!$prefetched && $LinkModel->useConsistentAfterFind) {
1408
                                                if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') {
1409
                                                        $this->_filterResultsInclusive($assocResultSet, $Model, array($association));
1410
                                                }
1411
                                        }
1412
                                        $this->_mergeAssociation($row, $assocResultSet, $association, $type, $selfJoin);
1413
                                }
1414
1415
                                if ($type !== 'hasAndBelongsToMany' && isset($row[$association]) && !$prefetched && !$LinkModel->useConsistentAfterFind) {
1416
                                        $row[$association] = $LinkModel->afterFind($row[$association], false);
1417
                                }
1418
1419
                        } else {
1420
                                $tempArray[0][$association] = false;
1421
                                $this->_mergeAssociation($row, $tempArray, $association, $type, $selfJoin);
1422
                        }
1423
                }
1424
        }
1425
1426
/**
1427
 * Fetch 'hasMany' associations.
1428
 *
1429
 * This is just a proxy to maintain BC.
1430
 *
1431
 * @param Model $Model Primary model object.
1432
 * @param string $query Association query template.
1433
 * @param array $ids Array of IDs of associated records.
1434
 * @return array Association results.
1435
 * @see DboSource::_fetchHasMany()
1436
 */
1437
        public function fetchAssociated(Model $Model, $query, $ids) {
1438
                return $this->_fetchHasMany($Model, $query, $ids);
1439
        }
1440
1441
/**
1442
 * Fetch 'hasMany' associations.
1443
 *
1444
 * @param Model $Model Primary model object.
1445
 * @param string $query Association query template.
1446
 * @param array $ids Array of IDs of associated records.
1447
 * @return array Association results.
1448
 */
1449
        protected function _fetchHasMany(Model $Model, $query, $ids) {
1450
                $ids = array_unique($ids);
1451
1452
                if (count($ids) > 1) {
1453
                        $query = str_replace('= ({$__cakeID__$}', 'IN ({$__cakeID__$}', $query);
1454
                }
1455
                $query = str_replace('{$__cakeID__$}', implode(', ', $ids), $query);
1456
                return $this->fetchAll($query, $Model->cacheQueries);
1457
        }
1458
1459
/**
1460
 * Fetch 'hasAndBelongsToMany' associations.
1461
 *
1462
 * @param Model $Model Primary model object.
1463
 * @param string $query Association query.
1464
 * @param array $ids Array of IDs of associated records.
1465
 * @param string $association Association name.
1466
 * @return array Association results.
1467
 */
1468
        protected function _fetchHasAndBelongsToMany(Model $Model, $query, $ids, $association) {
1469
                $ids = array_unique($ids);
1470
1471
                if (count($ids) > 1) {
1472
                        $query = str_replace('{$__cakeID__$}', '(' . implode(', ', $ids) . ')', $query);
1473
                        $query = str_replace('= (', 'IN (', $query);
1474
                } else {
1475
                        $query = str_replace('{$__cakeID__$}', $ids[0], $query);
1476
                }
1477
                $query = str_replace(' WHERE 1 = 1', '', $query);
1478
1479
                return $this->fetchAll($query, $Model->cacheQueries);
1480
        }
1481
1482
/**
1483
 * Merge the results of 'hasMany' associations.
1484
 *
1485
 * Note: this function also deals with the formatting of the data.
1486
 *
1487
 * @param array &$resultSet Data to merge into.
1488
 * @param array $assocResultSet Data to merge.
1489
 * @param string $association Name of Model being merged.
1490
 * @param Model $Model Model being merged onto.
1491
 * @return void
1492
 */
1493
        protected function _mergeHasMany(&$resultSet, $assocResultSet, $association, Model $Model) {
1494
                $modelAlias = $Model->alias;
1495
                $primaryKey = $Model->primaryKey;
1496
                $foreignKey = $Model->hasMany[$association]['foreignKey'];
1497
1498
                foreach ($resultSet as &$result) {
1499
                        if (!isset($result[$modelAlias])) {
1500
                                continue;
1501
                        }
1502
1503
                        $resultPrimaryKey = $result[$modelAlias][$primaryKey];
1504
1505
                        $merged = array();
1506
                        foreach ($assocResultSet as $data) {
1507
                                if ($resultPrimaryKey !== $data[$association][$foreignKey]) {
1508
                                        continue;
1509
                                }
1510
1511
                                if (count($data) > 1) {
1512
                                        $data = array_merge($data[$association], $data);
1513
                                        unset($data[$association]);
1514
                                        foreach ($data as $key => $name) {
1515
                                                if (is_numeric($key)) {
1516
                                                        $data[$association][] = $name;
1517
                                                        unset($data[$key]);
1518
                                                }
1519
                                        }
1520
                                        $merged[] = $data;
1521
                                } else {
1522
                                        $merged[] = $data[$association];
1523
                                }
1524
                        }
1525
1526
                        $result = Hash::mergeDiff($result, array($association => $merged));
1527
                }
1528
        }
1529
1530
/**
1531
 * Merge association of merge into data
1532
 *
1533
 * @param array &$data The data to merge.
1534
 * @param array &$merge The data to merge.
1535
 * @param string $association The association name to merge.
1536
 * @param string $type The type of association
1537
 * @param bool $selfJoin Whether or not this is a self join.
1538
 * @return void
1539
 */
1540
        protected function _mergeAssociation(&$data, &$merge, $association, $type, $selfJoin = false) {
1541
                if (isset($merge[0]) && !isset($merge[0][$association])) {
1542
                        $association = Inflector::pluralize($association);
1543
                }
1544
1545
                $dataAssociation =& $data[$association];
1546
1547
                if ($type === 'belongsTo' || $type === 'hasOne') {
1548
                        if (isset($merge[$association])) {
1549
                                $dataAssociation = $merge[$association][0];
1550
                        } else {
1551
                                if (!empty($merge[0][$association])) {
1552
                                        foreach ($merge[0] as $assoc => $data2) {
1553
                                                if ($assoc !== $association) {
1554
                                                        $merge[0][$association][$assoc] = $data2;
1555
                                                }
1556
                                        }
1557
                                }
1558
                                if (!isset($dataAssociation)) {
1559
                                        $dataAssociation = array();
1560
                                        if ($merge[0][$association]) {
1561
                                                $dataAssociation = $merge[0][$association];
1562
                                        }
1563
                                } else {
1564
                                        if (is_array($merge[0][$association])) {
1565
                                                $mergeAssocTmp = array();
1566
                                                foreach ($dataAssociation as $k => $v) {
1567
                                                        if (!is_array($v)) {
1568
                                                                $dataAssocTmp[$k] = $v;
1569
                                                        }
1570
                                                }
1571
1572
                                                foreach ($merge[0][$association] as $k => $v) {
1573
                                                        if (!is_array($v)) {
1574
                                                                $mergeAssocTmp[$k] = $v;
1575
                                                        }
1576
                                                }
1577
                                                $dataKeys = array_keys($data);
1578
                                                $mergeKeys = array_keys($merge[0]);
1579
1580
                                                if ($mergeKeys[0] === $dataKeys[0] || $mergeKeys === $dataKeys) {
1581
                                                        $dataAssociation[$association] = $merge[0][$association];
1582
                                                } else {
1583
                                                        $diff = Hash::diff($dataAssocTmp, $mergeAssocTmp);
1584
                                                        $dataAssociation = array_merge($merge[0][$association], $diff);
1585
                                                }
1586
                                        } elseif ($selfJoin && array_key_exists($association, $merge[0])) {
1587
                                                $dataAssociation = array_merge($dataAssociation, array($association => array()));
1588
                                        }
1589
                                }
1590
                        }
1591
                } else {
1592
                        if (isset($merge[0][$association]) && $merge[0][$association] === false) {
1593
                                if (!isset($dataAssociation)) {
1594
                                        $dataAssociation = array();
1595
                                }
1596
                        } else {
1597
                                foreach ($merge as $row) {
1598
                                        $insert = array();
1599
                                        if (count($row) === 1) {
1600
                                                $insert = $row[$association];
1601
                                        } elseif (isset($row[$association])) {
1602
                                                $insert = array_merge($row[$association], $row);
1603
                                                unset($insert[$association]);
1604
                                        }
1605
1606
                                        if (empty($dataAssociation) || (isset($dataAssociation) && !in_array($insert, $dataAssociation, true))) {
1607
                                                $dataAssociation[] = $insert;
1608
                                        }
1609
                                }
1610
                        }
1611
                }
1612
        }
1613
1614
/**
1615
 * Prepares fields required by an SQL statement.
1616
 *
1617
 * When no fields are set, all the $Model fields are returned.
1618
 *
1619
 * @param Model $Model The model to prepare.
1620
 * @param array $queryData An array of queryData information containing keys similar to Model::find().
1621
 * @return array Array containing SQL fields.
1622
 */
1623
        public function prepareFields(Model $Model, $queryData) {
1624
                if (empty($queryData['fields'])) {
1625
                        $queryData['fields'] = $this->fields($Model);
1626
1627
                } elseif (!empty($Model->hasMany) && $Model->recursive > -1) {
1628
                        // hasMany relationships need the $Model primary key.
1629
                        $assocFields = $this->fields($Model, null, "{$Model->alias}.{$Model->primaryKey}");
1630
                        $passedFields = $queryData['fields'];
1631
1632
                        if (count($passedFields) > 1 ||
1633
                                (strpos($passedFields[0], $assocFields[0]) === false && !preg_match('/^[a-z]+\(/i', $passedFields[0]))
1634
                        ) {
1635
                                $queryData['fields'] = array_merge($passedFields, $assocFields);
1636
                        }
1637
                }
1638
1639
                return array_unique($queryData['fields']);
1640
        }
1641
1642
/**
1643
 * Builds an SQL statement.
1644
 *
1645
 * This is merely a convenient wrapper to DboSource::buildStatement().
1646
 *
1647
 * @param Model $Model The model to build an association query for.
1648
 * @param array $queryData An array of queryData information containing keys similar to Model::find().
1649
 * @return string String containing an SQL statement.
1650
 * @see DboSource::buildStatement()
1651
 */
1652
        public function buildAssociationQuery(Model $Model, $queryData) {
1653
                $queryData = $this->_scrubQueryData($queryData);
1654
1655
                return $this->buildStatement(
1656
                        array(
1657
                                'fields' => $this->prepareFields($Model, $queryData),
1658
                                'table' => $this->fullTableName($Model),
1659
                                'alias' => $Model->alias,
1660
                                'limit' => $queryData['limit'],
1661
                                'offset' => $queryData['offset'],
1662
                                'joins' => $queryData['joins'],
1663
                                'conditions' => $queryData['conditions'],
1664
                                'order' => $queryData['order'],
1665
                                'group' => $queryData['group']
1666
                        ),
1667
                        $Model
1668
                );
1669
        }
1670
1671
/**
1672
 * Generates a query or part of a query from a single model or two associated models.
1673
 *
1674
 * Builds a string containing an SQL statement template.
1675
 *
1676
 * @param Model $Model Primary Model object.
1677
 * @param Model|null $LinkModel Linked model object.
1678
 * @param string $type Association type, one of the model association types ie. hasMany.
1679
 * @param string $association Association name.
1680
 * @param array $assocData Association data.
1681
 * @param array &$queryData An array of queryData information containing keys similar to Model::find().
1682
 * @param bool $external Whether or not the association query is on an external datasource.
1683
 * @return mixed
1684
 *   String representing a query.
1685
 *   True, when $external is false and association $type is 'hasOne' or 'belongsTo'.
1686
 */
1687
        public function generateAssociationQuery(Model $Model, $LinkModel, $type, $association, $assocData, &$queryData, $external) {
1688
                $assocData = $this->_scrubQueryData($assocData);
1689
                $queryData = $this->_scrubQueryData($queryData);
1690
1691
                if ($LinkModel === null) {
1692
                        return $this->buildStatement(
1693
                                array(
1694
                                        'fields' => array_unique($queryData['fields']),
1695
                                        'table' => $this->fullTableName($Model),
1696
                                        'alias' => $Model->alias,
1697
                                        'limit' => $queryData['limit'],
1698
                                        'offset' => $queryData['offset'],
1699
                                        'joins' => $queryData['joins'],
1700
                                        'conditions' => $queryData['conditions'],
1701
                                        'order' => $queryData['order'],
1702
                                        'group' => $queryData['group']
1703
                                ),
1704
                                $Model
1705
                        );
1706
                }
1707
1708
                if ($external && !empty($assocData['finderQuery'])) {
1709
                        return $assocData['finderQuery'];
1710
                }
1711
1712
                if ($type === 'hasMany' || $type === 'hasAndBelongsToMany') {
1713
                        if (empty($assocData['offset']) && !empty($assocData['page'])) {
1714
                                $assocData['offset'] = ($assocData['page'] - 1) * $assocData['limit'];
1715
                        }
1716
                }
1717
1718
                switch ($type) {
1719
                        case 'hasOne':
1720
                        case 'belongsTo':
1721
                                $conditions = $this->_mergeConditions(
1722
                                        $assocData['conditions'],
1723
                                        $this->getConstraint($type, $Model, $LinkModel, $association, array_merge($assocData, compact('external')))
1724
                                );
1725
1726
                                if ($external) {
1727
                                        // Not self join
1728
                                        if ($Model->name !== $LinkModel->name) {
1729
                                                $modelAlias = $Model->alias;
1730
                                                foreach ($conditions as $key => $condition) {
1731
                                                        if (is_numeric($key) && strpos($condition, $modelAlias . '.') !== false) {
1732
                                                                unset($conditions[$key]);
1733
                                                        }
1734
                                                }
1735
                                        }
1736
1737
                                        $query = array_merge($assocData, array(
1738
                                                'conditions' => $conditions,
1739
                                                'table' => $this->fullTableName($LinkModel),
1740
                                                'fields' => $this->fields($LinkModel, $association, $assocData['fields']),
1741
                                                'alias' => $association,
1742
                                                'group' => null
1743
                                        ));
1744
                                } else {
1745
                                        $join = array(
1746
                                                'table' => $LinkModel,
1747
                                                'alias' => $association,
1748
                                                'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT',
1749
                                                'conditions' => trim($this->conditions($conditions, true, false, $Model))
1750
                                        );
1751
1752
                                        $fields = array();
1753
                                        if ($assocData['fields'] !== false) {
1754
                                                $fields = $this->fields($LinkModel, $association, $assocData['fields']);
1755
                                        }
1756
1757
                                        $queryData['fields'] = array_merge($this->prepareFields($Model, $queryData), $fields);
1758
1759
                                        if (!empty($assocData['order'])) {
1760
                                                $queryData['order'][] = $assocData['order'];
1761
                                        }
1762
                                        if (!in_array($join, $queryData['joins'], true)) {
1763
                                                $queryData['joins'][] = $join;
1764
                                        }
1765
1766
                                        return true;
1767
                                }
1768
                                break;
1769
                        case 'hasMany':
1770
                                $assocData['fields'] = $this->fields($LinkModel, $association, $assocData['fields']);
1771
                                if (!empty($assocData['foreignKey'])) {
1772
                                        $assocData['fields'] = array_merge($assocData['fields'], $this->fields($LinkModel, $association, array("{$association}.{$assocData['foreignKey']}")));
1773
                                }
1774
1775
                                $query = array(
1776
                                        'conditions' => $this->_mergeConditions($this->getConstraint('hasMany', $Model, $LinkModel, $association, $assocData), $assocData['conditions']),
1777
                                        'fields' => array_unique($assocData['fields']),
1778
                                        'table' => $this->fullTableName($LinkModel),
1779
                                        'alias' => $association,
1780
                                        'order' => $assocData['order'],
1781
                                        'limit' => $assocData['limit'],
1782
                                        'offset' => $assocData['offset'],
1783
                                        'group' => null
1784
                                );
1785
                                break;
1786
                        case 'hasAndBelongsToMany':
1787
                                $joinFields = array();
1788
                                $joinAssoc = null;
1789
1790
                                if (isset($assocData['with']) && !empty($assocData['with'])) {
1791
                                        $joinKeys = array($assocData['foreignKey'], $assocData['associationForeignKey']);
1792
                                        list($with, $joinFields) = $Model->joinModel($assocData['with'], $joinKeys);
1793
1794
                                        $joinTbl = $Model->{$with};
1795
                                        $joinAlias = $joinTbl;
1796
1797
                                        if (is_array($joinFields) && !empty($joinFields)) {
1798
                                                $joinAssoc = $joinAlias = $joinTbl->alias;
1799
                                                $joinFields = $this->fields($joinTbl, $joinAlias, $joinFields);
1800
                                        } else {
1801
                                                $joinFields = array();
1802
                                        }
1803
                                } else {
1804
                                        $joinTbl = $assocData['joinTable'];
1805
                                        $joinAlias = $this->fullTableName($assocData['joinTable']);
1806
                                }
1807
1808
                                $query = array(
1809
                                        'conditions' => $assocData['conditions'],
1810
                                        'limit' => $assocData['limit'],
1811
                                        'offset' => $assocData['offset'],
1812
                                        'table' => $this->fullTableName($LinkModel),
1813
                                        'alias' => $association,
1814
                                        'fields' => array_merge($this->fields($LinkModel, $association, $assocData['fields']), $joinFields),
1815
                                        'order' => $assocData['order'],
1816
                                        'group' => null,
1817
                                        'joins' => array(array(
1818
                                                'table' => $joinTbl,
1819
                                                'alias' => $joinAssoc,
1820
                                                'conditions' => $this->getConstraint('hasAndBelongsToMany', $Model, $LinkModel, $joinAlias, $assocData, $association)
1821
                                        ))
1822
                                );
1823
                                break;
1824
                }
1825
1826
                if (isset($query)) {
1827
                        return $this->buildStatement($query, $Model);
1828
                }
1829
1830
                return null;
1831
        }
1832
1833
/**
1834
 * Returns a conditions array for the constraint between two models.
1835
 *
1836
 * @param string $type Association type.
1837
 * @param Model $Model Primary Model object.
1838
 * @param Model $LinkModel Linked model object.
1839
 * @param string $association Association name.
1840
 * @param array $assocData Association data.
1841
 * @param string $association2 HABTM association name.
1842
 * @return array Conditions array defining the constraint between $Model and $LinkModel.
1843
 */
1844
        public function getConstraint($type, Model $Model, Model $LinkModel, $association, $assocData, $association2 = null) {
1845
                $assocData += array('external' => false);
1846
1847
                if (empty($assocData['foreignKey'])) {
1848
                        return array();
1849
                }
1850
1851
                switch ($type) {
1852
                        case 'hasOne':
1853
                                if ($assocData['external']) {
1854
                                        return array(
1855
                                                "{$association}.{$assocData['foreignKey']}" => '{$__cakeID__$}'
1856
                                        );
1857
                                } else {
1858
                                        return array(
1859
                                                "{$association}.{$assocData['foreignKey']}" => $this->identifier("{$Model->alias}.{$Model->primaryKey}")
1860
                                        );
1861
                                }
1862
                        case 'belongsTo':
1863
                                if ($assocData['external']) {
1864
                                        return array(
1865
                                                "{$association}.{$LinkModel->primaryKey}" => '{$__cakeForeignKey__$}'
1866
                                        );
1867
                                } else {
1868
                                        return array(
1869
                                                "{$Model->alias}.{$assocData['foreignKey']}" => $this->identifier("{$association}.{$LinkModel->primaryKey}")
1870
                                        );
1871
                                }
1872
                        case 'hasMany':
1873
                                return array("{$association}.{$assocData['foreignKey']}" => array('{$__cakeID__$}'));
1874
                        case 'hasAndBelongsToMany':
1875
                                return array(
1876
                                        array(
1877
                                                "{$association}.{$assocData['foreignKey']}" => '{$__cakeID__$}'
1878
                                        ),
1879
                                        array(
1880
                                                "{$association}.{$assocData['associationForeignKey']}" => $this->identifier("{$association2}.{$LinkModel->primaryKey}")
1881
                                        )
1882
                                );
1883
                }
1884
1885
                return array();
1886
        }
1887
1888
/**
1889
 * Builds and generates a JOIN condition from an array. Handles final clean-up before conversion.
1890
 *
1891
 * @param array $join An array defining a JOIN condition in a query.
1892
 * @return string An SQL JOIN condition to be used in a query.
1893
 * @see DboSource::renderJoinStatement()
1894
 * @see DboSource::buildStatement()
1895
 */
1896
        public function buildJoinStatement($join) {
1897
                $data = array_merge(array(
1898
                        'type' => null,
1899
                        'alias' => null,
1900
                        'table' => 'join_table',
1901
                        'conditions' => '',
1902
                ), $join);
1903
1904
                if (!empty($data['alias'])) {
1905
                        $data['alias'] = $this->alias . $this->name($data['alias']);
1906
                }
1907
                if (!empty($data['conditions'])) {
1908
                        $data['conditions'] = trim($this->conditions($data['conditions'], true, false));
1909
                }
1910
                if (!empty($data['table']) && (!is_string($data['table']) || strpos($data['table'], '(') !== 0)) {
1911
                        $data['table'] = $this->fullTableName($data['table']);
1912
                }
1913
                return $this->renderJoinStatement($data);
1914
        }
1915
1916
/**
1917
 * Builds and generates an SQL statement from an array. Handles final clean-up before conversion.
1918
 *
1919
 * @param array $query An array defining an SQL query.
1920
 * @param Model $Model The model object which initiated the query.
1921
 * @return string An executable SQL statement.
1922
 * @see DboSource::renderStatement()
1923
 */
1924
        public function buildStatement($query, Model $Model) {
1925
                $query = array_merge($this->_queryDefaults, $query);
1926
1927
                if (!empty($query['joins'])) {
1928
                        $count = count($query['joins']);
1929
                        for ($i = 0; $i < $count; $i++) {
1930
                                if (is_array($query['joins'][$i])) {
1931
                                        $query['joins'][$i] = $this->buildJoinStatement($query['joins'][$i]);
1932
                                }
1933
                        }
1934
                }
1935
1936
                return $this->renderStatement('select', array(
1937
                        'conditions' => $this->conditions($query['conditions'], true, true, $Model),
1938
                        'fields' => implode(', ', $query['fields']),
1939
                        'table' => $query['table'],
1940
                        'alias' => $this->alias . $this->name($query['alias']),
1941
                        'order' => $this->order($query['order'], 'ASC', $Model),
1942
                        'limit' => $this->limit($query['limit'], $query['offset']),
1943
                        'joins' => implode(' ', $query['joins']),
1944
                        'group' => $this->group($query['group'], $Model)
1945
                ));
1946
        }
1947
1948
/**
1949
 * Renders a final SQL JOIN statement
1950
 *
1951
 * @param array $data The data to generate a join statement for.
1952
 * @return string
1953
 */
1954
        public function renderJoinStatement($data) {
1955
                if (strtoupper($data['type']) === 'CROSS' || empty($data['conditions'])) {
1956
                        return "{$data['type']} JOIN {$data['table']} {$data['alias']}";
1957
                }
1958
                return trim("{$data['type']} JOIN {$data['table']} {$data['alias']} ON ({$data['conditions']})");
1959
        }
1960
1961
/**
1962
 * Renders a final SQL statement by putting together the component parts in the correct order
1963
 *
1964
 * @param string $type type of query being run. e.g select, create, update, delete, schema, alter.
1965
 * @param array $data Array of data to insert into the query.
1966
 * @return string|null Rendered SQL expression to be run, otherwise null.
1967
 */
1968
        public function renderStatement($type, $data) {
1969
                extract($data);
1970
                $aliases = null;
1971
1972
                switch (strtolower($type)) {
1973
                        case 'select':
1974
                                return trim("SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order} {$limit}");
1975
                        case 'create':
1976
                                return "INSERT INTO {$table} ({$fields}) VALUES ({$values})";
1977
                        case 'update':
1978
                                if (!empty($alias)) {
1979
                                        $aliases = "{$this->alias}{$alias} {$joins} ";
1980
                                }
1981
                                return trim("UPDATE {$table} {$aliases}SET {$fields} {$conditions}");
1982
                        case 'delete':
1983
                                if (!empty($alias)) {
1984
                                        $aliases = "{$this->alias}{$alias} {$joins} ";
1985
                                }
1986
                                return trim("DELETE {$alias} FROM {$table} {$aliases}{$conditions}");
1987
                        case 'schema':
1988
                                foreach (array('columns', 'indexes', 'tableParameters') as $var) {
1989
                                        if (is_array(${$var})) {
1990
                                                ${$var} = "\t" . implode(",\n\t", array_filter(${$var}));
1991
                                        } else {
1992
                                                ${$var} = '';
1993
                                        }
1994
                                }
1995
                                if (trim($indexes) !== '') {
1996
                                        $columns .= ',';
1997
                                }
1998
                                return "CREATE TABLE {$table} (\n{$columns}{$indexes}) {$tableParameters};";
1999
                        case 'alter':
2000
                                return null;
2001
                }
2002
        }
2003
2004
/**
2005
 * Merges a mixed set of string/array conditions.
2006
 *
2007
 * @param mixed $query The query to merge conditions for.
2008
 * @param mixed $assoc The association names.
2009
 * @return array
2010
 */
2011
        protected function _mergeConditions($query, $assoc) {
2012
                if (empty($assoc)) {
2013
                        return $query;
2014
                }
2015
2016
                if (is_array($query)) {
2017
                        return array_merge((array)$assoc, $query);
2018
                }
2019
2020
                if (!empty($query)) {
2021
                        $query = array($query);
2022
                        if (is_array($assoc)) {
2023
                                $query = array_merge($query, $assoc);
2024
                        } else {
2025
                                $query[] = $assoc;
2026
                        }
2027
                        return $query;
2028
                }
2029
2030
                return $assoc;
2031
        }
2032
2033
/**
2034
 * Generates and executes an SQL UPDATE statement for given model, fields, and values.
2035
 * For databases that do not support aliases in UPDATE queries.
2036
 *
2037
 * @param Model $Model The model to update.
2038
 * @param array $fields The fields to update
2039
 * @param array $values The values fo the fields.
2040
 * @param mixed $conditions The conditions for the update. When non-empty $values will not be quoted.
2041
 * @return bool Success
2042
 */
2043
        public function update(Model $Model, $fields = array(), $values = null, $conditions = null) {
2044
                if (!$values) {
2045
                        $combined = $fields;
2046
                } else {
2047
                        $combined = array_combine($fields, $values);
2048
                }
2049
2050
                $fields = implode(', ', $this->_prepareUpdateFields($Model, $combined, empty($conditions)));
2051
2052
                $alias = $joins = null;
2053
                $table = $this->fullTableName($Model);
2054
                $conditions = $this->_matchRecords($Model, $conditions);
2055
2056
                if ($conditions === false) {
2057
                        return false;
2058
                }
2059
                $query = compact('table', 'alias', 'joins', 'fields', 'conditions');
2060
2061
                if (!$this->execute($this->renderStatement('update', $query))) {
2062
                        $Model->onError();
2063
                        return false;
2064
                }
2065
                return true;
2066
        }
2067
2068
/**
2069
 * Quotes and prepares fields and values for an SQL UPDATE statement
2070
 *
2071
 * @param Model $Model The model to prepare fields for.
2072
 * @param array $fields The fields to update.
2073
 * @param bool $quoteValues If values should be quoted, or treated as SQL snippets
2074
 * @param bool $alias Include the model alias in the field name
2075
 * @return array Fields and values, quoted and prepared
2076
 */
2077
        protected function _prepareUpdateFields(Model $Model, $fields, $quoteValues = true, $alias = false) {
2078
                $quotedAlias = $this->startQuote . $Model->alias . $this->endQuote;
2079
                $schema = $Model->schema();
2080
2081
                $updates = array();
2082
                foreach ($fields as $field => $value) {
2083
                        if ($alias && strpos($field, '.') === false) {
2084
                                $quoted = $Model->escapeField($field);
2085
                        } elseif (!$alias && strpos($field, '.') !== false) {
2086
                                $quoted = $this->name(str_replace($quotedAlias . '.', '', str_replace(
2087
                                        $Model->alias . '.', '', $field
2088
                                )));
2089
                        } else {
2090
                                $quoted = $this->name($field);
2091
                        }
2092
2093
                        if ($value === null) {
2094
                                $updates[] = $quoted . ' = NULL';
2095
                                continue;
2096
                        }
2097
                        $update = $quoted . ' = ';
2098
2099
                        if ($quoteValues) {
2100
                                $update .= $this->value($value, $Model->getColumnType($field), isset($schema[$field]) ? $schema[$field]['null'] : true);
2101
                        } elseif ($Model->getColumnType($field) === 'boolean' && (is_int($value) || is_bool($value))) {
2102
                                $update .= $this->boolean($value, true);
2103
                        } elseif (!$alias) {
2104
                                $update .= str_replace($quotedAlias . '.', '', str_replace(
2105
                                        $Model->alias . '.', '', $value
2106
                                ));
2107
                        } else {
2108
                                $update .= $value;
2109
                        }
2110
                        $updates[] = $update;
2111
                }
2112
                return $updates;
2113
        }
2114
2115
/**
2116
 * Generates and executes an SQL DELETE statement.
2117
 * For databases that do not support aliases in UPDATE queries.
2118
 *
2119
 * @param Model $Model The model to delete from
2120
 * @param mixed $conditions The conditions to use. If empty the model's primary key will be used.
2121
 * @return bool Success
2122
 */
2123
        public function delete(Model $Model, $conditions = null) {
2124
                $alias = $joins = null;
2125
                $table = $this->fullTableName($Model);
2126
                $conditions = $this->_matchRecords($Model, $conditions);
2127
2128
                if ($conditions === false) {
2129
                        return false;
2130
                }
2131
2132
                if ($this->execute($this->renderStatement('delete', compact('alias', 'table', 'joins', 'conditions'))) === false) {
2133
                        $Model->onError();
2134
                        return false;
2135
                }
2136
                return true;
2137
        }
2138
2139
/**
2140
 * Gets a list of record IDs for the given conditions. Used for multi-record updates and deletes
2141
 * in databases that do not support aliases in UPDATE/DELETE queries.
2142
 *
2143
 * @param Model $Model The model to find matching records for.
2144
 * @param mixed $conditions The conditions to match against.
2145
 * @return array List of record IDs
2146
 */
2147
        protected function _matchRecords(Model $Model, $conditions = null) {
2148
                if ($conditions === true) {
2149
                        $conditions = $this->conditions(true);
2150
                } elseif ($conditions === null) {
2151
                        $conditions = $this->conditions($this->defaultConditions($Model, $conditions, false), true, true, $Model);
2152
                } else {
2153
                        $noJoin = true;
2154
                        foreach ($conditions as $field => $value) {
2155
                                $originalField = $field;
2156
                                if (strpos($field, '.') !== false) {
2157
                                        list(, $field) = explode('.', $field);
2158
                                        $field = ltrim($field, $this->startQuote);
2159
                                        $field = rtrim($field, $this->endQuote);
2160
                                }
2161
                                if (!$Model->hasField($field)) {
2162
                                        $noJoin = false;
2163
                                        break;
2164
                                }
2165
                                if ($field !== $originalField) {
2166
                                        $conditions[$field] = $value;
2167
                                        unset($conditions[$originalField]);
2168
                                }
2169
                        }
2170
                        if ($noJoin === true) {
2171
                                return $this->conditions($conditions);
2172
                        }
2173
                        $idList = $Model->find('all', array(
2174
                                'fields' => "{$Model->alias}.{$Model->primaryKey}",
2175
                                'conditions' => $conditions
2176
                        ));
2177
2178
                        if (empty($idList)) {
2179
                                return false;
2180
                        }
2181
2182
                        $conditions = $this->conditions(array(
2183
                                $Model->primaryKey => Hash::extract($idList, "{n}.{$Model->alias}.{$Model->primaryKey}")
2184
                        ));
2185
                }
2186
2187
                return $conditions;
2188
        }
2189
2190
/**
2191
 * Returns an array of SQL JOIN conditions from a model's associations.
2192
 *
2193
 * @param Model $Model The model to get joins for.2
2194
 * @return array
2195
 */
2196
        protected function _getJoins(Model $Model) {
2197
                $join = array();
2198
                $joins = array_merge($Model->getAssociated('hasOne'), $Model->getAssociated('belongsTo'));
2199
2200
                foreach ($joins as $assoc) {
2201
                        if (!isset($Model->{$assoc})) {
2202
                                continue;
2203
                        }
2204
2205
                        $LinkModel = $Model->{$assoc};
2206
2207
                        if ($Model->useDbConfig !== $LinkModel->useDbConfig) {
2208
                                continue;
2209
                        }
2210
2211
                        $assocData = $Model->getAssociated($assoc);
2212
2213
                        $join[] = $this->buildJoinStatement(array(
2214
                                'table' => $LinkModel,
2215
                                'alias' => $assoc,
2216
                                'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT',
2217
                                'conditions' => trim($this->conditions(
2218
                                        $this->_mergeConditions($assocData['conditions'], $this->getConstraint($assocData['association'], $Model, $LinkModel, $assoc, $assocData)),
2219
                                        true,
2220
                                        false,
2221
                                        $Model
2222
                                ))
2223
                        ));
2224
                }
2225
2226
                return $join;
2227
        }
2228
2229
/**
2230
 * Returns an SQL calculation, i.e. COUNT() or MAX()
2231
 *
2232
 * @param Model $Model The model to get a calculated field for.
2233
 * @param string $func Lowercase name of SQL function, i.e. 'count' or 'max'
2234
 * @param array $params Function parameters (any values must be quoted manually)
2235
 * @return string An SQL calculation function
2236
 */
2237
        public function calculate(Model $Model, $func, $params = array()) {
2238
                $params = (array)$params;
2239
2240
                switch (strtolower($func)) {
2241
                        case 'count':
2242
                                if (!isset($params[0])) {
2243
                                        $params[0] = '*';
2244
                                }
2245
                                if (!isset($params[1])) {
2246
                                        $params[1] = 'count';
2247
                                }
2248
                                if ($Model->isVirtualField($params[0])) {
2249
                                        $arg = $this->_quoteFields($Model->getVirtualField($params[0]));
2250
                                } else {
2251
                                        $arg = $this->name($params[0]);
2252
                                }
2253
                                return 'COUNT(' . $arg . ') AS ' . $this->name($params[1]);
2254
                        case 'max':
2255
                        case 'min':
2256
                                if (!isset($params[1])) {
2257
                                        $params[1] = $params[0];
2258
                                }
2259
                                if ($Model->isVirtualField($params[0])) {
2260
                                        $arg = $this->_quoteFields($Model->getVirtualField($params[0]));
2261
                                } else {
2262
                                        $arg = $this->name($params[0]);
2263
                                }
2264
                                return strtoupper($func) . '(' . $arg . ') AS ' . $this->name($params[1]);
2265
                }
2266
        }
2267
2268
/**
2269
 * Deletes all the records in a table and resets the count of the auto-incrementing
2270
 * primary key, where applicable.
2271
 *
2272
 * @param Model|string $table A string or model class representing the table to be truncated
2273
 * @return bool SQL TRUNCATE TABLE statement, false if not applicable.
2274
 */
2275
        public function truncate($table) {
2276
                return $this->execute('TRUNCATE TABLE ' . $this->fullTableName($table));
2277
        }
2278
2279
/**
2280
 * Check if the server support nested transactions
2281
 *
2282
 * @return bool
2283
 */
2284
        public function nestedTransactionSupported() {
2285
                return false;
2286
        }
2287
2288
/**
2289
 * Begin a transaction
2290
 *
2291
 * @return bool True on success, false on fail
2292
 * (i.e. if the database/model does not support transactions,
2293
 * or a transaction has not started).
2294
 */
2295
        public function begin() {
2296
                if ($this->_transactionStarted) {
2297
                        if ($this->nestedTransactionSupported()) {
2298
                                return $this->_beginNested();
2299
                        }
2300
                        $this->_transactionNesting++;
2301
                        return $this->_transactionStarted;
2302
                }
2303
2304
                $this->_transactionNesting = 0;
2305
                if ($this->fullDebug) {
2306
                        $this->logQuery('BEGIN');
2307
                }
2308
                return $this->_transactionStarted = $this->_connection->beginTransaction();
2309
        }
2310
2311
/**
2312
 * Begin a nested transaction
2313
 *
2314
 * @return bool
2315
 */
2316
        protected function _beginNested() {
2317
                $query = 'SAVEPOINT LEVEL' . ++$this->_transactionNesting;
2318
                if ($this->fullDebug) {
2319
                        $this->logQuery($query);
2320
                }
2321
                $this->_connection->exec($query);
2322
                return true;
2323
        }
2324
2325
/**
2326
 * Commit a transaction
2327
 *
2328
 * @return bool True on success, false on fail
2329
 * (i.e. if the database/model does not support transactions,
2330
 * or a transaction has not started).
2331
 */
2332
        public function commit() {
2333
                if (!$this->_transactionStarted) {
2334
                        return false;
2335
                }
2336
2337
                if ($this->_transactionNesting === 0) {
2338
                        if ($this->fullDebug) {
2339
                                $this->logQuery('COMMIT');
2340
                        }
2341
                        $this->_transactionStarted = false;
2342
                        return $this->_connection->commit();
2343
                }
2344
2345
                if ($this->nestedTransactionSupported()) {
2346
                        return $this->_commitNested();
2347
                }
2348
2349
                $this->_transactionNesting--;
2350
                return true;
2351
        }
2352
2353
/**
2354
 * Commit a nested transaction
2355
 *
2356
 * @return bool
2357
 */
2358
        protected function _commitNested() {
2359
                $query = 'RELEASE SAVEPOINT LEVEL' . $this->_transactionNesting--;
2360
                if ($this->fullDebug) {
2361
                        $this->logQuery($query);
2362
                }
2363
                $this->_connection->exec($query);
2364
                return true;
2365
        }
2366
2367
/**
2368
 * Rollback a transaction
2369
 *
2370
 * @return bool True on success, false on fail
2371
 * (i.e. if the database/model does not support transactions,
2372
 * or a transaction has not started).
2373
 */
2374
        public function rollback() {
2375
                if (!$this->_transactionStarted) {
2376
                        return false;
2377
                }
2378
2379
                if ($this->_transactionNesting === 0) {
2380
                        if ($this->fullDebug) {
2381
                                $this->logQuery('ROLLBACK');
2382
                        }
2383
                        $this->_transactionStarted = false;
2384
                        return $this->_connection->rollBack();
2385
                }
2386
2387
                if ($this->nestedTransactionSupported()) {
2388
                        return $this->_rollbackNested();
2389
                }
2390
2391
                $this->_transactionNesting--;
2392
                return true;
2393
        }
2394
2395
/**
2396
 * Rollback a nested transaction
2397
 *
2398
 * @return bool
2399
 */
2400
        protected function _rollbackNested() {
2401
                $query = 'ROLLBACK TO SAVEPOINT LEVEL' . $this->_transactionNesting--;
2402
                if ($this->fullDebug) {
2403
                        $this->logQuery($query);
2404
                }
2405
                $this->_connection->exec($query);
2406
                return true;
2407
        }
2408
2409
/**
2410
 * Returns the ID generated from the previous INSERT operation.
2411
 *
2412
 * @param mixed $source The source to get an id for.
2413
 * @return mixed
2414
 */
2415
        public function lastInsertId($source = null) {
2416
                return $this->_connection->lastInsertId();
2417
        }
2418
2419
/**
2420
 * Creates a default set of conditions from the model if $conditions is null/empty.
2421
 * If conditions are supplied then they will be returned. If a model doesn't exist and no conditions
2422
 * were provided either null or false will be returned based on what was input.
2423
 *
2424
 * @param Model $Model The model to get conditions for.
2425
 * @param string|array|bool $conditions Array of conditions, conditions string, null or false. If an array of conditions,
2426
 *   or string conditions those conditions will be returned. With other values the model's existence will be checked.
2427
 *   If the model doesn't exist a null or false will be returned depending on the input value.
2428
 * @param bool $useAlias Use model aliases rather than table names when generating conditions
2429
 * @return mixed Either null, false, $conditions or an array of default conditions to use.
2430
 * @see DboSource::update()
2431
 * @see DboSource::conditions()
2432
 */
2433
        public function defaultConditions(Model $Model, $conditions, $useAlias = true) {
2434
                if (!empty($conditions)) {
2435
                        return $conditions;
2436
                }
2437
                $exists = $Model->exists();
2438
                if (!$exists && ($conditions !== null || !empty($Model->__safeUpdateMode))) {
2439
                        return false;
2440
                } elseif (!$exists) {
2441
                        return null;
2442
                }
2443
                $alias = $Model->alias;
2444
2445
                if (!$useAlias) {
2446
                        $alias = $this->fullTableName($Model, false);
2447
                }
2448
                return array("{$alias}.{$Model->primaryKey}" => $Model->getID());
2449
        }
2450
2451
/**
2452
 * Returns a key formatted like a string Model.fieldname(i.e. Post.title, or Country.name)
2453
 *
2454
 * @param Model $Model The model to get a key for.
2455
 * @param string $key The key field.
2456
 * @param string $assoc The association name.
2457
 * @return string
2458
 */
2459
        public function resolveKey(Model $Model, $key, $assoc = null) {
2460
                if (strpos('.', $key) !== false) {
2461
                        return $this->name($Model->alias) . '.' . $this->name($key);
2462
                }
2463
                return $key;
2464
        }
2465
2466
/**
2467
 * Private helper method to remove query metadata in given data array.
2468
 *
2469
 * @param array $data The data to scrub.
2470
 * @return array
2471
 */
2472
        protected function _scrubQueryData($data) {
2473
                static $base = null;
2474
                if ($base === null) {
2475
                        $base = array_fill_keys(array('conditions', 'fields', 'joins', 'order', 'limit', 'offset', 'group'), array());
2476
                        $base['callbacks'] = null;
2477
                }
2478
                return (array)$data + $base;
2479
        }
2480
2481
/**
2482
 * Converts model virtual fields into sql expressions to be fetched later
2483
 *
2484
 * @param Model $Model The model to get virtual fields for.
2485
 * @param string $alias Alias table name
2486
 * @param array $fields virtual fields to be used on query
2487
 * @return array
2488
 */
2489
        protected function _constructVirtualFields(Model $Model, $alias, $fields) {
2490
                $virtual = array();
2491
                foreach ($fields as $field) {
2492
                        $virtualField = $this->name($alias . $this->virtualFieldSeparator . $field);
2493
                        $expression = $this->_quoteFields($Model->getVirtualField($field));
2494
                        $virtual[] = '(' . $expression . ") {$this->alias} {$virtualField}";
2495
                }
2496
                return $virtual;
2497
        }
2498
2499
/**
2500
 * Generates the fields list of an SQL query.
2501
 *
2502
 * @param Model $Model The model to get fields for.
2503
 * @param string $alias Alias table name
2504
 * @param mixed $fields The provided list of fields.
2505
 * @param bool $quote If false, returns fields array unquoted
2506
 * @return array
2507
 */
2508
        public function fields(Model $Model, $alias = null, $fields = array(), $quote = true) {
2509
                if (empty($alias)) {
2510
                        $alias = $Model->alias;
2511
                }
2512
                $virtualFields = $Model->getVirtualField();
2513
                $cacheKey = array(
2514
                        $alias,
2515
                        get_class($Model),
2516
                        $Model->alias,
2517
                        $virtualFields,
2518
                        $fields,
2519
                        $quote,
2520
                        ConnectionManager::getSourceName($this),
2521
                        $Model->schemaName,
2522
                        $Model->table
2523
                );
2524
                $cacheKey = md5(serialize($cacheKey));
2525
                if ($return = $this->cacheMethod(__FUNCTION__, $cacheKey)) {
2526
                        return $return;
2527
                }
2528
                $allFields = empty($fields);
2529
                if ($allFields) {
2530
                        $fields = array_keys($Model->schema());
2531
                } elseif (!is_array($fields)) {
2532
                        $fields = CakeText::tokenize($fields);
2533
                }
2534
                $fields = array_values(array_filter($fields));
2535
                $allFields = $allFields || in_array('*', $fields) || in_array($Model->alias . '.*', $fields);
2536
2537
                $virtual = array();
2538
                if (!empty($virtualFields)) {
2539
                        $virtualKeys = array_keys($virtualFields);
2540
                        foreach ($virtualKeys as $field) {
2541
                                $virtualKeys[] = $Model->alias . '.' . $field;
2542
                        }
2543
                        $virtual = ($allFields) ? $virtualKeys : array_intersect($virtualKeys, $fields);
2544
                        foreach ($virtual as $i => $field) {
2545
                                if (strpos($field, '.') !== false) {
2546
                                        $virtual[$i] = str_replace($Model->alias . '.', '', $field);
2547
                                }
2548
                                $fields = array_diff($fields, array($field));
2549
                        }
2550
                        $fields = array_values($fields);
2551
                }
2552
                if (!$quote) {
2553
                        if (!empty($virtual)) {
2554
                                $fields = array_merge($fields, $this->_constructVirtualFields($Model, $alias, $virtual));
2555
                        }
2556
                        return $fields;
2557
                }
2558
                $count = count($fields);
2559
2560
                if ($count >= 1 && !in_array($fields[0], array('*', 'COUNT(*)'))) {
2561
                        for ($i = 0; $i < $count; $i++) {
2562
                                if (is_string($fields[$i]) && in_array($fields[$i], $virtual)) {
2563
                                        unset($fields[$i]);
2564
                                        continue;
2565
                                }
2566
                                if (is_object($fields[$i]) && isset($fields[$i]->type) && $fields[$i]->type === 'expression') {
2567
                                        $fields[$i] = $fields[$i]->value;
2568
                                } elseif (preg_match('/^\(.*\)\s' . $this->alias . '.*/i', $fields[$i])) {
2569
                                        continue;
2570
                                } elseif (!preg_match('/^.+\\(.*\\)/', $fields[$i])) {
2571
                                        $prepend = '';
2572
2573
                                        if (strpos($fields[$i], 'DISTINCT') !== false) {
2574
                                                $prepend = 'DISTINCT ';
2575
                                                $fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i]));
2576
                                        }
2577
                                        $dot = strpos($fields[$i], '.');
2578
2579
                                        if ($dot === false) {
2580
                                                $prefix = !(
2581
                                                        strpos($fields[$i], ' ') !== false ||
2582
                                                        strpos($fields[$i], '(') !== false
2583
                                                );
2584
                                                $fields[$i] = $this->name(($prefix ? $alias . '.' : '') . $fields[$i]);
2585
                                        } else {
2586
                                                if (strpos($fields[$i], ',') === false) {
2587
                                                        $build = explode('.', $fields[$i]);
2588
                                                        if (!Hash::numeric($build)) {
2589
                                                                $fields[$i] = $this->name(implode('.', $build));
2590
                                                        }
2591
                                                }
2592
                                        }
2593
                                        $fields[$i] = $prepend . $fields[$i];
2594
                                } elseif (preg_match('/\(([\.\w]+)\)/', $fields[$i], $field)) {
2595
                                        if (isset($field[1])) {
2596
                                                if (strpos($field[1], '.') === false) {
2597
                                                        $field[1] = $this->name($alias . '.' . $field[1]);
2598
                                                } else {
2599
                                                        $field[0] = explode('.', $field[1]);
2600
                                                        if (!Hash::numeric($field[0])) {
2601
                                                                $field[0] = implode('.', array_map(array(&$this, 'name'), $field[0]));
2602
                                                                $fields[$i] = preg_replace('/\(' . $field[1] . '\)/', '(' . $field[0] . ')', $fields[$i], 1);
2603
                                                        }
2604
                                                }
2605
                                        }
2606
                                }
2607
                        }
2608
                }
2609
                if (!empty($virtual)) {
2610
                        $fields = array_merge($fields, $this->_constructVirtualFields($Model, $alias, $virtual));
2611
                }
2612
                return $this->cacheMethod(__FUNCTION__, $cacheKey, array_unique($fields));
2613
        }
2614
2615
/**
2616
 * Creates a WHERE clause by parsing given conditions data. If an array or string
2617
 * conditions are provided those conditions will be parsed and quoted. If a boolean
2618
 * is given it will be integer cast as condition. Null will return 1 = 1.
2619
 *
2620
 * Results of this method are stored in a memory cache. This improves performance, but
2621
 * because the method uses a hashing algorithm it can have collisions.
2622
 * Setting DboSource::$cacheMethods to false will disable the memory cache.
2623
 *
2624
 * @param mixed $conditions Array or string of conditions, or any value.
2625
 * @param bool $quoteValues If true, values should be quoted
2626
 * @param bool $where If true, "WHERE " will be prepended to the return value
2627
 * @param Model $Model A reference to the Model instance making the query
2628
 * @return string SQL fragment
2629
 */
2630
        public function conditions($conditions, $quoteValues = true, $where = true, Model $Model = null) {
2631
                $clause = $out = '';
2632
2633
                if ($where) {
2634
                        $clause = ' WHERE ';
2635
                }
2636
2637
                if (is_array($conditions) && !empty($conditions)) {
2638
                        $out = $this->conditionKeysToString($conditions, $quoteValues, $Model);
2639
2640
                        if (empty($out)) {
2641
                                return $clause . ' 1 = 1';
2642
                        }
2643
                        return $clause . implode(' AND ', $out);
2644
                }
2645
2646
                if (is_bool($conditions)) {
2647
                        return $clause . (int)$conditions . ' = 1';
2648
                }
2649
2650
                if (empty($conditions) || trim($conditions) === '') {
2651
                        return $clause . '1 = 1';
2652
                }
2653
2654
                $clauses = '/^WHERE\\x20|^GROUP\\x20BY\\x20|^HAVING\\x20|^ORDER\\x20BY\\x20/i';
2655
2656
                if (preg_match($clauses, $conditions)) {
2657
                        $clause = '';
2658
                }
2659
2660
                $conditions = $this->_quoteFields($conditions);
2661
2662
                return $clause . $conditions;
2663
        }
2664
2665
/**
2666
 * Creates a WHERE clause by parsing given conditions array. Used by DboSource::conditions().
2667
 *
2668
 * @param array $conditions Array or string of conditions
2669
 * @param bool $quoteValues If true, values should be quoted
2670
 * @param Model $Model A reference to the Model instance making the query
2671
 * @return string SQL fragment
2672
 */
2673
        public function conditionKeysToString($conditions, $quoteValues = true, Model $Model = null) {
2674
                $out = array();
2675
                $data = $columnType = null;
2676
                $bool = array('and', 'or', 'not', 'and not', 'or not', 'xor', '||', '&&');
2677
2678
                foreach ($conditions as $key => $value) {
2679
                        $join = ' AND ';
2680
                        $not = null;
2681
2682
                        if (is_array($value)) {
2683
                                $valueInsert = (
2684
                                        !empty($value) &&
2685
                                        (substr_count($key, '?') === count($value) || substr_count($key, ':') === count($value))
2686
                                );
2687
                        }
2688
2689
                        if (is_numeric($key) && empty($value)) {
2690
                                continue;
2691
                        } elseif (is_numeric($key) && is_string($value)) {
2692
                                $out[] = $this->_quoteFields($value);
2693
                        } elseif ((is_numeric($key) && is_array($value)) || in_array(strtolower(trim($key)), $bool)) {
2694
                                if (in_array(strtolower(trim($key)), $bool)) {
2695
                                        $join = ' ' . strtoupper($key) . ' ';
2696
                                } else {
2697
                                        $key = $join;
2698
                                }
2699
                                $value = $this->conditionKeysToString($value, $quoteValues, $Model);
2700
2701
                                if (strpos($join, 'NOT') !== false) {
2702
                                        if (strtoupper(trim($key)) === 'NOT') {
2703
                                                $key = 'AND ' . trim($key);
2704
                                        }
2705
                                        $not = 'NOT ';
2706
                                }
2707
2708
                                if (empty($value)) {
2709
                                        continue;
2710
                                }
2711
2712
                                if (empty($value[1])) {
2713
                                        if ($not) {
2714
                                                $out[] = $not . '(' . $value[0] . ')';
2715
                                        } else {
2716
                                                $out[] = $value[0];
2717
                                        }
2718
                                } else {
2719
                                        $out[] = '(' . $not . '(' . implode(') ' . strtoupper($key) . ' (', $value) . '))';
2720
                                }
2721
                        } else {
2722
                                if (is_object($value) && isset($value->type)) {
2723
                                        if ($value->type === 'identifier') {
2724
                                                $data .= $this->name($key) . ' = ' . $this->name($value->value);
2725
                                        } elseif ($value->type === 'expression') {
2726
                                                if (is_numeric($key)) {
2727
                                                        $data .= $value->value;
2728
                                                } else {
2729
                                                        $data .= $this->name($key) . ' = ' . $value->value;
2730
                                                }
2731
                                        }
2732
                                } elseif (is_array($value) && !empty($value) && !$valueInsert) {
2733
                                        $keys = array_keys($value);
2734
                                        if ($keys === array_values($keys)) {
2735
                                                $count = count($value);
2736
                                                if ($count === 1 && !preg_match('/\s+(?:NOT|\!=)$/', $key)) {
2737
                                                        $data = $this->_quoteFields($key) . ' = (';
2738
                                                        if ($quoteValues) {
2739
                                                                if ($Model !== null) {
2740
                                                                        $columnType = $Model->getColumnType($key);
2741
                                                                }
2742
                                                                $data .= implode(', ', $this->value($value, $columnType));
2743
                                                        }
2744
                                                        $data .= ')';
2745
                                                } else {
2746
                                                        $data = $this->_parseKey($key, $value, $Model);
2747
                                                }
2748
                                        } else {
2749
                                                $ret = $this->conditionKeysToString($value, $quoteValues, $Model);
2750
                                                if (count($ret) > 1) {
2751
                                                        $data = '(' . implode(') AND (', $ret) . ')';
2752
                                                } elseif (isset($ret[0])) {
2753
                                                        $data = $ret[0];
2754
                                                }
2755
                                        }
2756
                                } elseif (is_numeric($key) && !empty($value)) {
2757
                                        $data = $this->_quoteFields($value);
2758
                                } else {
2759
                                        $data = $this->_parseKey(trim($key), $value, $Model);
2760
                                }
2761
2762
                                if ($data) {
2763
                                        $out[] = $data;
2764
                                        $data = null;
2765
                                }
2766
                        }
2767
                }
2768
                return $out;
2769
        }
2770
2771
/**
2772
 * Extracts a Model.field identifier and an SQL condition operator from a string, formats
2773
 * and inserts values, and composes them into an SQL snippet.
2774
 *
2775
 * @param string $key An SQL key snippet containing a field and optional SQL operator
2776
 * @param mixed $value The value(s) to be inserted in the string
2777
 * @param Model $Model Model object initiating the query
2778
 * @return string
2779
 */
2780
        protected function _parseKey($key, $value, Model $Model = null) {
2781
                $operatorMatch = '/^(((' . implode(')|(', $this->_sqlOps);
2782
                $operatorMatch .= ')\\x20?)|<[>=]?(?![^>]+>)\\x20?|[>=!]{1,3}(?!<)\\x20?)/is';
2783
                $bound = (strpos($key, '?') !== false || (is_array($value) && strpos($key, ':') !== false));
2784
2785
                $key = trim($key);
2786
                if (strpos($key, ' ') === false) {
2787
                        $operator = '=';
2788
                } else {
2789
                        list($key, $operator) = explode(' ', $key, 2);
2790
2791
                        if (!preg_match($operatorMatch, trim($operator)) && strpos($operator, ' ') !== false) {
2792
                                $key = $key . ' ' . $operator;
2793
                                $split = strrpos($key, ' ');
2794
                                $operator = substr($key, $split);
2795
                                $key = substr($key, 0, $split);
2796
                        }
2797
                }
2798
2799
                $virtual = false;
2800
                $type = null;
2801
2802
                if ($Model !== null) {
2803
                        if ($Model->isVirtualField($key)) {
2804
                                $key = $this->_quoteFields($Model->getVirtualField($key));
2805
                                $virtual = true;
2806
                        }
2807
2808
                        $type = $Model->getColumnType($key);
2809
                }
2810
2811
                $null = $value === null || (is_array($value) && empty($value));
2812
2813
                if (strtolower($operator) === 'not') {
2814
                        $data = $this->conditionKeysToString(
2815
                                array($operator => array($key => $value)), true, $Model
2816
                        );
2817
                        return $data[0];
2818
                }
2819
2820
                $value = $this->value($value, $type);
2821
2822
                if (!$virtual && $key !== '?') {
2823
                        $isKey = (
2824
                                strpos($key, '(') !== false ||
2825
                                strpos($key, ')') !== false ||
2826
                                strpos($key, '|') !== false
2827
                        );
2828
                        $key = $isKey ? $this->_quoteFields($key) : $this->name($key);
2829
                }
2830
2831
                if ($bound) {
2832
                        return CakeText::insert($key . ' ' . trim($operator), $value);
2833
                }
2834
2835
                if (!preg_match($operatorMatch, trim($operator))) {
2836
                        $operator .= is_array($value) ? ' IN' : ' =';
2837
                }
2838
                $operator = trim($operator);
2839
2840
                if (is_array($value)) {
2841
                        $value = implode(', ', $value);
2842
2843
                        switch ($operator) {
2844
                                case '=':
2845
                                        $operator = 'IN';
2846
                                        break;
2847
                                case '!=':
2848
                                case '<>':
2849
                                        $operator = 'NOT IN';
2850
                                        break;
2851
                        }
2852
                        $value = "({$value})";
2853
                } elseif ($null || $value === 'NULL') {
2854
                        switch ($operator) {
2855
                                case '=':
2856
                                        $operator = 'IS';
2857
                                        break;
2858
                                case '!=':
2859
                                case '<>':
2860
                                        $operator = 'IS NOT';
2861
                                        break;
2862
                        }
2863
                }
2864
                if ($virtual) {
2865
                        return "({$key}) {$operator} {$value}";
2866
                }
2867
                return "{$key} {$operator} {$value}";
2868
        }
2869
2870
/**
2871
 * Quotes Model.fields
2872
 *
2873
 * @param string $conditions The conditions to quote.
2874
 * @return string or false if no match
2875
 */
2876
        protected function _quoteFields($conditions) {
2877
                $start = $end = null;
2878
                $original = $conditions;
2879
2880
                if (!empty($this->startQuote)) {
2881
                        $start = preg_quote($this->startQuote);
2882
                }
2883
                if (!empty($this->endQuote)) {
2884
                        $end = preg_quote($this->endQuote);
2885
                }
2886
                // Remove quotes and requote all the Model.field names.
2887
                $conditions = str_replace(array($start, $end), '', $conditions);
2888
                $conditions = preg_replace_callback(
2889
                        '/(?:[\'\"][^\'\"\\\]*(?:\\\.[^\'\"\\\]*)*[\'\"])|([a-z0-9_][a-z0-9\\-_]*\\.[a-z0-9_][a-z0-9_\\-]*)/i',
2890
                        array(&$this, '_quoteMatchedField'),
2891
                        $conditions
2892
                );
2893
                // Quote `table_name AS Alias`
2894
                $conditions = preg_replace(
2895
                        '/(\s[a-z0-9\\-_.' . $start . $end . ']*' . $end . ')\s+AS\s+([a-z0-9\\-_]+)/i',
2896
                        '\1 AS ' . $this->startQuote . '\2' . $this->endQuote,
2897
                        $conditions
2898
                );
2899
                if ($conditions !== null) {
2900
                        return $conditions;
2901
                }
2902
                return $original;
2903
        }
2904
2905
/**
2906
 * Auxiliary function to quote matches `Model.fields` from a preg_replace_callback call
2907
 *
2908
 * @param string $match matched string
2909
 * @return string quoted string
2910
 */
2911
        protected function _quoteMatchedField($match) {
2912
                if (is_numeric($match[0])) {
2913
                        return $match[0];
2914
                }
2915
                return $this->name($match[0]);
2916
        }
2917
2918
/**
2919
 * Returns a limit statement in the correct format for the particular database.
2920
 *
2921
 * @param int $limit Limit of results returned
2922
 * @param int $offset Offset from which to start results
2923
 * @return string SQL limit/offset statement
2924
 */
2925
        public function limit($limit, $offset = null) {
2926
                if ($limit) {
2927
                        $rt = ' LIMIT';
2928
2929
                        if ($offset) {
2930
                                $rt .= sprintf(' %u,', $offset);
2931
                        }
2932
2933
                        $rt .= sprintf(' %u', $limit);
2934
                        return $rt;
2935
                }
2936
                return null;
2937
        }
2938
2939
/**
2940
 * Returns an ORDER BY clause as a string.
2941
 *
2942
 * @param array|string $keys Field reference, as a key (i.e. Post.title)
2943
 * @param string $direction Direction (ASC or DESC)
2944
 * @param Model $Model Model reference (used to look for virtual field)
2945
 * @return string ORDER BY clause
2946
 */
2947
        public function order($keys, $direction = 'ASC', Model $Model = null) {
2948
                if (!is_array($keys)) {
2949
                        $keys = array($keys);
2950
                }
2951
2952
                $keys = array_filter($keys);
2953
2954
                $result = array();
2955
                while (!empty($keys)) {
2956
                        list($key, $dir) = each($keys);
2957
                        array_shift($keys);
2958
2959
                        if (is_numeric($key)) {
2960
                                $key = $dir;
2961
                                $dir = $direction;
2962
                        }
2963
2964
                        if (is_string($key) && strpos($key, ',') !== false && !preg_match('/\(.+\,.+\)/', $key)) {
2965
                                $key = array_map('trim', explode(',', $key));
2966
                        }
2967
2968
                        if (is_array($key)) {
2969
                                //Flatten the array
2970
                                $key = array_reverse($key, true);
2971
                                foreach ($key as $k => $v) {
2972
                                        if (is_numeric($k)) {
2973
                                                array_unshift($keys, $v);
2974
                                        } else {
2975
                                                $keys = array($k => $v) + $keys;
2976
                                        }
2977
                                }
2978
                                continue;
2979
                        } elseif (is_object($key) && isset($key->type) && $key->type === 'expression') {
2980
                                $result[] = $key->value;
2981
                                continue;
2982
                        }
2983
2984
                        if (preg_match('/\\x20(ASC|DESC).*/i', $key, $_dir)) {
2985
                                $dir = $_dir[0];
2986
                                $key = preg_replace('/\\x20(ASC|DESC).*/i', '', $key);
2987
                        }
2988
2989
                        $key = trim($key);
2990
2991
                        if ($Model !== null) {
2992
                                if ($Model->isVirtualField($key)) {
2993
                                        $key = '(' . $this->_quoteFields($Model->getVirtualField($key)) . ')';
2994
                                }
2995
2996
                                list($alias) = pluginSplit($key);
2997
2998
                                if ($alias !== $Model->alias && is_object($Model->{$alias}) && $Model->{$alias}->isVirtualField($key)) {
2999
                                        $key = '(' . $this->_quoteFields($Model->{$alias}->getVirtualField($key)) . ')';
3000
                                }
3001
                        }
3002
3003
                        if (strpos($key, '.')) {
3004
                                $key = preg_replace_callback('/([a-zA-Z0-9_-]{1,})\\.([a-zA-Z0-9_-]{1,})/', array(&$this, '_quoteMatchedField'), $key);
3005
                        }
3006
3007
                        if (!preg_match('/\s/', $key) && strpos($key, '.') === false) {
3008
                                $key = $this->name($key);
3009
                        }
3010
3011
                        $key .= ' ' . trim($dir);
3012
3013
                        $result[] = $key;
3014
                }
3015
3016
                if (!empty($result)) {
3017
                        return ' ORDER BY ' . implode(', ', $result);
3018
                }
3019
3020
                return '';
3021
        }
3022
3023
/**
3024
 * Create a GROUP BY SQL clause.
3025
 *
3026
 * @param string|array $fields Group By fields
3027
 * @param Model $Model The model to get group by fields for.
3028
 * @return string Group By clause or null.
3029
 */
3030
        public function group($fields, Model $Model = null) {
3031
                if (empty($fields)) {
3032
                        return null;
3033
                }
3034
3035
                if (!is_array($fields)) {
3036
                        $fields = array($fields);
3037
                }
3038
3039
                if ($Model !== null) {
3040
                        foreach ($fields as $index => $key) {
3041
                                if ($Model->isVirtualField($key)) {
3042
                                        $fields[$index] = '(' . $Model->getVirtualField($key) . ')';
3043
                                }
3044
                        }
3045
                }
3046
3047
                $fields = implode(', ', $fields);
3048
3049
                return ' GROUP BY ' . $this->_quoteFields($fields);
3050
        }
3051
3052
/**
3053
 * Disconnects database, kills the connection and says the connection is closed.
3054
 *
3055
 * @return void
3056
 */
3057
        public function close() {
3058
                $this->disconnect();
3059
        }
3060
3061
/**
3062
 * Checks if the specified table contains any record matching specified SQL
3063
 *
3064
 * @param Model $Model Model to search
3065
 * @param string $sql SQL WHERE clause (condition only, not the "WHERE" part)
3066
 * @return bool True if the table has a matching record, else false
3067
 */
3068
        public function hasAny(Model $Model, $sql) {
3069
                $sql = $this->conditions($sql);
3070
                $table = $this->fullTableName($Model);
3071
                $alias = $this->alias . $this->name($Model->alias);
3072
                $where = $sql ? "{$sql}" : ' WHERE 1 = 1';
3073
                $id = $Model->escapeField();
3074
3075
                $out = $this->fetchRow("SELECT COUNT({$id}) {$this->alias}count FROM {$table} {$alias}{$where}");
3076
3077
                if (is_array($out)) {
3078
                        return $out[0]['count'];
3079
                }
3080
                return false;
3081
        }
3082
3083
/**
3084
 * Gets the length of a database-native column description, or null if no length
3085
 *
3086
 * @param string $real Real database-layer column type (i.e. "varchar(255)")
3087
 * @return mixed An integer or string representing the length of the column, or null for unknown length.
3088
 */
3089
        public function length($real) {
3090
                if (!preg_match_all('/([\w\s]+)(?:\((\d+)(?:,(\d+))?\))?(\sunsigned)?(\szerofill)?/', $real, $result)) {
3091
                        $col = str_replace(array(')', 'unsigned'), '', $real);
3092
                        $limit = null;
3093
3094
                        if (strpos($col, '(') !== false) {
3095
                                list($col, $limit) = explode('(', $col);
3096
                        }
3097
                        if ($limit !== null) {
3098
                                return (int)$limit;
3099
                        }
3100
                        return null;
3101
                }
3102
3103
                $types = array(
3104
                        'int' => 1, 'tinyint' => 1, 'smallint' => 1, 'mediumint' => 1, 'integer' => 1, 'bigint' => 1
3105
                );
3106
3107
                list($real, $type, $length, $offset, $sign) = $result;
3108
                $typeArr = $type;
3109
                $type = $type[0];
3110
                $length = $length[0];
3111
                $offset = $offset[0];
3112
3113
                $isFloat = in_array($type, array('dec', 'decimal', 'float', 'numeric', 'double'));
3114
                if ($isFloat && $offset) {
3115
                        return $length . ',' . $offset;
3116
                }
3117
3118
                if (($real[0] == $type) && (count($real) === 1)) {
3119
                        return null;
3120
                }
3121
3122
                if (isset($types[$type])) {
3123
                        $length += $types[$type];
3124
                        if (!empty($sign)) {
3125
                                $length--;
3126
                        }
3127
                } elseif (in_array($type, array('enum', 'set'))) {
3128
                        $length = 0;
3129
                        foreach ($typeArr as $key => $enumValue) {
3130
                                if ($key === 0) {
3131
                                        continue;
3132
                                }
3133
                                $tmpLength = strlen($enumValue);
3134
                                if ($tmpLength > $length) {
3135
                                        $length = $tmpLength;
3136
                                }
3137
                        }
3138
                }
3139
                return (int)$length;
3140
        }
3141
3142
/**
3143
 * Translates between PHP boolean values and Database (faked) boolean values
3144
 *
3145
 * @param mixed $data Value to be translated
3146
 * @param bool $quote Whether or not the field should be cast to a string.
3147
 * @return string|bool Converted boolean value
3148
 */
3149
        public function boolean($data, $quote = false) {
3150
                if ($quote) {
3151
                        return !empty($data) ? '1' : '0';
3152
                }
3153
                return !empty($data);
3154
        }
3155
3156
/**
3157
 * Inserts multiple values into a table
3158
 *
3159
 * @param string $table The table being inserted into.
3160
 * @param array $fields The array of field/column names being inserted.
3161
 * @param array $values The array of values to insert. The values should
3162
 *   be an array of rows. Each row should have values keyed by the column name.
3163
 *   Each row must have the values in the same order as $fields.
3164
 * @return bool
3165
 */
3166
        public function insertMulti($table, $fields, $values) {
3167
                $table = $this->fullTableName($table);
3168
                $holder = implode(',', array_fill(0, count($fields), '?'));
3169
                $fields = implode(', ', array_map(array(&$this, 'name'), $fields));
3170
3171
                $pdoMap = array(
3172
                        'integer' => PDO::PARAM_INT,
3173
                        'float' => PDO::PARAM_STR,
3174
                        'boolean' => PDO::PARAM_BOOL,
3175
                        'string' => PDO::PARAM_STR,
3176
                        'text' => PDO::PARAM_STR
3177
                );
3178
                $columnMap = array();
3179
3180
                $sql = "INSERT INTO {$table} ({$fields}) VALUES ({$holder})";
3181
                $statement = $this->_connection->prepare($sql);
3182
                $this->begin();
3183
3184
                foreach ($values[key($values)] as $key => $val) {
3185
                        $type = $this->introspectType($val);
3186
                        $columnMap[$key] = $pdoMap[$type];
3187
                }
3188
3189
                foreach ($values as $value) {
3190
                        $i = 1;
3191
                        foreach ($value as $col => $val) {
3192
                                $statement->bindValue($i, $val, $columnMap[$col]);
3193
                                $i += 1;
3194
                        }
3195
                        $statement->execute();
3196
                        $statement->closeCursor();
3197
3198
                        if ($this->fullDebug) {
3199
                                $this->logQuery($sql, $value);
3200
                        }
3201
                }
3202
                return $this->commit();
3203
        }
3204
3205
/**
3206
 * Reset a sequence based on the MAX() value of $column. Useful
3207
 * for resetting sequences after using insertMulti().
3208
 *
3209
 * This method should be implemented by datasources that require sequences to be used.
3210
 *
3211
 * @param string $table The name of the table to update.
3212
 * @param string $column The column to use when resetting the sequence value.
3213
 * @return bool Success.
3214
 */
3215
        public function resetSequence($table, $column) {
3216
        }
3217
3218
/**
3219
 * Returns an array of the indexes in given datasource name.
3220
 *
3221
 * @param string $model Name of model to inspect
3222
 * @return array Fields in table. Keys are column and unique
3223
 */
3224
        public function index($model) {
3225
                return array();
3226
        }
3227
3228
/**
3229
 * Generate a database-native schema for the given Schema object
3230
 *
3231
 * @param CakeSchema $schema An instance of a subclass of CakeSchema
3232
 * @param string $tableName Optional. If specified only the table name given will be generated.
3233
 *   Otherwise, all tables defined in the schema are generated.
3234
 * @return string
3235
 */
3236
        public function createSchema($schema, $tableName = null) {
3237
                if (!$schema instanceof CakeSchema) {
3238
                        trigger_error(__d('cake_dev', 'Invalid schema object'), E_USER_WARNING);
3239
                        return null;
3240
                }
3241
                $out = '';
3242
3243
                foreach ($schema->tables as $curTable => $columns) {
3244
                        if (!$tableName || $tableName === $curTable) {
3245
                                $cols = $indexes = $tableParameters = array();
3246
                                $primary = null;
3247
                                $table = $this->fullTableName($curTable);
3248
3249
                                $primaryCount = 0;
3250
                                foreach ($columns as $col) {
3251
                                        if (isset($col['key']) && $col['key'] === 'primary') {
3252
                                                $primaryCount++;
3253
                                        }
3254
                                }
3255
3256
                                foreach ($columns as $name => $col) {
3257
                                        if (is_string($col)) {
3258
                                                $col = array('type' => $col);
3259
                                        }
3260
                                        $isPrimary = isset($col['key']) && $col['key'] === 'primary';
3261
                                        // Multi-column primary keys are not supported.
3262
                                        if ($isPrimary && $primaryCount > 1) {
3263
                                                unset($col['key']);
3264
                                                $isPrimary = false;
3265
                                        }
3266
                                        if ($isPrimary) {
3267
                                                $primary = $name;
3268
                                        }
3269
                                        if ($name !== 'indexes' && $name !== 'tableParameters') {
3270
                                                $col['name'] = $name;
3271
                                                if (!isset($col['type'])) {
3272
                                                        $col['type'] = 'string';
3273
                                                }
3274
                                                $cols[] = $this->buildColumn($col);
3275
                                        } elseif ($name === 'indexes') {
3276
                                                $indexes = array_merge($indexes, $this->buildIndex($col, $table));
3277
                                        } elseif ($name === 'tableParameters') {
3278
                                                $tableParameters = array_merge($tableParameters, $this->buildTableParameters($col, $table));
3279
                                        }
3280
                                }
3281
                                if (!isset($columns['indexes']['PRIMARY']) && !empty($primary)) {
3282
                                        $col = array('PRIMARY' => array('column' => $primary, 'unique' => 1));
3283
                                        $indexes = array_merge($indexes, $this->buildIndex($col, $table));
3284
                                }
3285
                                $columns = $cols;
3286
                                $out .= $this->renderStatement('schema', compact('table', 'columns', 'indexes', 'tableParameters')) . "\n\n";
3287
                        }
3288
                }
3289
                return $out;
3290
        }
3291
3292
/**
3293
 * Generate an alter syntax from CakeSchema::compare()
3294
 *
3295
 * @param mixed $compare The comparison data.
3296
 * @param string $table The table name.
3297
 * @return bool
3298
 */
3299
        public function alterSchema($compare, $table = null) {
3300
                return false;
3301
        }
3302
3303
/**
3304
 * Generate a "drop table" statement for the given Schema object
3305
 *
3306
 * @param CakeSchema $schema An instance of a subclass of CakeSchema
3307
 * @param string $table Optional. If specified only the table name given will be generated.
3308
 *   Otherwise, all tables defined in the schema are generated.
3309
 * @return string
3310
 */
3311
        public function dropSchema(CakeSchema $schema, $table = null) {
3312
                $out = '';
3313
3314
                if ($table && array_key_exists($table, $schema->tables)) {
3315
                        return $this->_dropTable($table) . "\n";
3316
                } elseif ($table) {
3317
                        return $out;
3318
                }
3319
3320
                foreach (array_keys($schema->tables) as $curTable) {
3321
                        $out .= $this->_dropTable($curTable) . "\n";
3322
                }
3323
                return $out;
3324
        }
3325
3326
/**
3327
 * Generate a "drop table" statement for a single table
3328
 *
3329
 * @param type $table Name of the table to drop
3330
 * @return string Drop table SQL statement
3331
 */
3332
        protected function _dropTable($table) {
3333
                return 'DROP TABLE ' . $this->fullTableName($table) . ";";
3334
        }
3335
3336
/**
3337
 * Generate a database-native column schema string
3338
 *
3339
 * @param array $column An array structured like the following: array('name' => 'value', 'type' => 'value'[, options]),
3340
 *   where options can be 'default', 'length', or 'key'.
3341
 * @return string
3342
 */
3343
        public function buildColumn($column) {
3344
                $name = $type = null;
3345
                extract(array_merge(array('null' => true), $column));
3346
3347
                if (empty($name) || empty($type)) {
3348
                        trigger_error(__d('cake_dev', 'Column name or type not defined in schema'), E_USER_WARNING);
3349
                        return null;
3350
                }
3351
3352
                if (!isset($this->columns[$type])) {
3353
                        trigger_error(__d('cake_dev', 'Column type %s does not exist', $type), E_USER_WARNING);
3354
                        return null;
3355
                }
3356
3357
                $real = $this->columns[$type];
3358
                $out = $this->name($name) . ' ' . $real['name'];
3359
3360
                if (isset($column['length'])) {
3361
                        $length = $column['length'];
3362
                } elseif (isset($column['limit'])) {
3363
                        $length = $column['limit'];
3364
                } elseif (isset($real['length'])) {
3365
                        $length = $real['length'];
3366
                } elseif (isset($real['limit'])) {
3367
                        $length = $real['limit'];
3368
                }
3369
                if (isset($length)) {
3370
                        $out .= '(' . $length . ')';
3371
                }
3372
3373
                if (($column['type'] === 'integer' || $column['type'] === 'float') && isset($column['default']) && $column['default'] === '') {
3374
                        $column['default'] = null;
3375
                }
3376
                $out = $this->_buildFieldParameters($out, $column, 'beforeDefault');
3377
3378
                if (isset($column['key']) && $column['key'] === 'primary' && ($type === 'integer' || $type === 'biginteger')) {
3379
                        $out .= ' ' . $this->columns['primary_key']['name'];
3380
                } elseif (isset($column['key']) && $column['key'] === 'primary') {
3381
                        $out .= ' NOT NULL';
3382
                } elseif (isset($column['default']) && isset($column['null']) && $column['null'] === false) {
3383
                        $out .= ' DEFAULT ' . $this->value($column['default'], $type) . ' NOT NULL';
3384
                } elseif (isset($column['default'])) {
3385
                        $out .= ' DEFAULT ' . $this->value($column['default'], $type);
3386
                } elseif ($type !== 'timestamp' && !empty($column['null'])) {
3387
                        $out .= ' DEFAULT NULL';
3388
                } elseif ($type === 'timestamp' && !empty($column['null'])) {
3389
                        $out .= ' NULL';
3390
                } elseif (isset($column['null']) && $column['null'] === false) {
3391
                        $out .= ' NOT NULL';
3392
                }
3393
                if ($type === 'timestamp' && isset($column['default']) && strtolower($column['default']) === 'current_timestamp') {
3394
                        $out = str_replace(array("'CURRENT_TIMESTAMP'", "'current_timestamp'"), 'CURRENT_TIMESTAMP', $out);
3395
                }
3396
                return $this->_buildFieldParameters($out, $column, 'afterDefault');
3397
        }
3398
3399
/**
3400
 * Build the field parameters, in a position
3401
 *
3402
 * @param string $columnString The partially built column string
3403
 * @param array $columnData The array of column data.
3404
 * @param string $position The position type to use. 'beforeDefault' or 'afterDefault' are common
3405
 * @return string a built column with the field parameters added.
3406
 */
3407
        protected function _buildFieldParameters($columnString, $columnData, $position) {
3408
                foreach ($this->fieldParameters as $paramName => $value) {
3409
                        if (isset($columnData[$paramName]) && $value['position'] == $position) {
3410
                                if (isset($value['options']) && !in_array($columnData[$paramName], $value['options'], true)) {
3411
                                        continue;
3412
                                }
3413
                                if (isset($value['types']) && !in_array($columnData['type'], $value['types'], true)) {
3414
                                        continue;
3415
                                }
3416
                                $val = $columnData[$paramName];
3417
                                if ($value['quote']) {
3418
                                        $val = $this->value($val);
3419
                                }
3420
                                $columnString .= ' ' . $value['value'] . (empty($value['noVal']) ? $value['join'] . $val : '');
3421
                        }
3422
                }
3423
                return $columnString;
3424
        }
3425
3426
/**
3427
 * Format indexes for create table.
3428
 *
3429
 * @param array $indexes The indexes to build
3430
 * @param string $table The table name.
3431
 * @return array
3432
 */
3433
        public function buildIndex($indexes, $table = null) {
3434
                $join = array();
3435
                foreach ($indexes as $name => $value) {
3436
                        $out = '';
3437
                        if ($name === 'PRIMARY') {
3438
                                $out .= 'PRIMARY ';
3439
                                $name = null;
3440
                        } else {
3441
                                if (!empty($value['unique'])) {
3442
                                        $out .= 'UNIQUE ';
3443
                                }
3444
                                $name = $this->startQuote . $name . $this->endQuote;
3445
                        }
3446
                        if (is_array($value['column'])) {
3447
                                $out .= 'KEY ' . $name . ' (' . implode(', ', array_map(array(&$this, 'name'), $value['column'])) . ')';
3448
                        } else {
3449
                                $out .= 'KEY ' . $name . ' (' . $this->name($value['column']) . ')';
3450
                        }
3451
                        $join[] = $out;
3452
                }
3453
                return $join;
3454
        }
3455
3456
/**
3457
 * Read additional table parameters
3458
 *
3459
 * @param string $name The table name to read.
3460
 * @return array
3461
 */
3462
        public function readTableParameters($name) {
3463
                $parameters = array();
3464
                if (method_exists($this, 'listDetailedSources')) {
3465
                        $currentTableDetails = $this->listDetailedSources($name);
3466
                        foreach ($this->tableParameters as $paramName => $parameter) {
3467
                                if (!empty($parameter['column']) && !empty($currentTableDetails[$parameter['column']])) {
3468
                                        $parameters[$paramName] = $currentTableDetails[$parameter['column']];
3469
                                }
3470
                        }
3471
                }
3472
                return $parameters;
3473
        }
3474
3475
/**
3476
 * Format parameters for create table
3477
 *
3478
 * @param array $parameters The parameters to create SQL for.
3479
 * @param string $table The table name.
3480
 * @return array
3481
 */
3482
        public function buildTableParameters($parameters, $table = null) {
3483
                $result = array();
3484
                foreach ($parameters as $name => $value) {
3485
                        if (isset($this->tableParameters[$name])) {
3486
                                if ($this->tableParameters[$name]['quote']) {
3487
                                        $value = $this->value($value);
3488
                                }
3489
                                $result[] = $this->tableParameters[$name]['value'] . $this->tableParameters[$name]['join'] . $value;
3490
                        }
3491
                }
3492
                return $result;
3493
        }
3494
3495
/**
3496
 * Guesses the data type of an array
3497
 *
3498
 * @param string $value The value to introspect for type data.
3499
 * @return string
3500
 */
3501
        public function introspectType($value) {
3502
                if (!is_array($value)) {
3503
                        if (is_bool($value)) {
3504
                                return 'boolean';
3505
                        }
3506
                        if (is_float($value) && (float)$value === $value) {
3507
                                return 'float';
3508
                        }
3509
                        if (is_int($value) && (int)$value === $value) {
3510
                                return 'integer';
3511
                        }
3512
                        if (is_string($value) && strlen($value) > 255) {
3513
                                return 'text';
3514
                        }
3515
                        return 'string';
3516
                }
3517
3518
                $isAllFloat = $isAllInt = true;
3519
                $containsInt = $containsString = false;
3520
                foreach ($value as $valElement) {
3521
                        $valElement = trim($valElement);
3522
                        if (!is_float($valElement) && !preg_match('/^[\d]+\.[\d]+$/', $valElement)) {
3523
                                $isAllFloat = false;
3524
                        } else {
3525
                                continue;
3526
                        }
3527
                        if (!is_int($valElement) && !preg_match('/^[\d]+$/', $valElement)) {
3528
                                $isAllInt = false;
3529
                        } else {
3530
                                $containsInt = true;
3531
                                continue;
3532
                        }
3533
                        $containsString = true;
3534
                }
3535
3536
                if ($isAllFloat) {
3537
                        return 'float';
3538
                }
3539
                if ($isAllInt) {
3540
                        return 'integer';
3541
                }
3542
3543
                if ($containsInt && !$containsString) {
3544
                        return 'integer';
3545
                }
3546
                return 'string';
3547
        }
3548
3549
/**
3550
 * Writes a new key for the in memory sql query cache
3551
 *
3552
 * @param string $sql SQL query
3553
 * @param mixed $data result of $sql query
3554
 * @param array $params query params bound as values
3555
 * @return void
3556
 */
3557
        protected function _writeQueryCache($sql, $data, $params = array()) {
3558
                if (preg_match('/^\s*select/i', $sql)) {
3559
                        $this->_queryCache[$sql][serialize($params)] = $data;
3560
                }
3561
        }
3562
3563
/**
3564
 * Returns the result for a sql query if it is already cached
3565
 *
3566
 * @param string $sql SQL query
3567
 * @param array $params query params bound as values
3568
 * @return mixed results for query if it is cached, false otherwise
3569
 */
3570
        public function getQueryCache($sql, $params = array()) {
3571
                if (isset($this->_queryCache[$sql]) && preg_match('/^\s*select/i', $sql)) {
3572
                        $serialized = serialize($params);
3573
                        if (isset($this->_queryCache[$sql][$serialized])) {
3574
                                return $this->_queryCache[$sql][$serialized];
3575
                        }
3576
                }
3577
                return false;
3578
        }
3579
3580
/**
3581
 * Used for storing in cache the results of the in-memory methodCache
3582
 */
3583
        public function __destruct() {
3584
                if ($this->_methodCacheChange) {
3585
                        Cache::write('method_cache', static::$methodCache, '_cake_core_');
3586
                }
3587
                parent::__destruct();
3588
        }
3589
3590
}