Merge pull request #11197 from agileware/CRM-21104
[civicrm-core.git] / CRM / Utils / SQL / Select.php
CommitLineData
e2b5e5b1 1<?php
d1d3c04a
CW
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
d1d3c04a 5 +--------------------------------------------------------------------+
8c9251b3 6 | Copyright CiviCRM LLC (c) 2004-2018 |
d1d3c04a
CW
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
e2b5e5b1
TO
27
28/**
29 * Dear God Why Do I Have To Write This (Dumb SQL Builder)
30 *
31 * Usage:
c5458931
TO
32 * @code
33 * $select = CRM_Utils_SQL_Select::from('civicrm_activity act')
e2b5e5b1 34 * ->join('absence', 'inner join civicrm_activity absence on absence.id = act.source_record_id')
c5458931
TO
35 * ->where('activity_type_id = #type', array('type' => 234))
36 * ->where('status_id IN (#statuses)', array('statuses' => array(1,2,3))
37 * ->where('subject like @subj', array('subj' => '%hello%'))
38 * ->where('!dynamicColumn = 1', array('dynamicColumn' => 'coalesce(is_active,0)'))
e2b5e5b1 39 * ->where('!column = @value', array(
c5458931
TO
40 * 'column' => $customField->column_name,
41 * 'value' => $form['foo']
e2b5e5b1
TO
42 * ))
43 * echo $select->toSQL();
c5458931 44 * @endcode
e2b5e5b1
TO
45 *
46 * Design principles:
47 * - Portable
48 * - No knowledge of the underlying SQL API (except for escaping -- CRM_Core_DAO::escapeString)
49 * - No knowledge of the underlying data model
e2b5e5b1
TO
50 * - SQL clauses correspond to PHP functions ($select->where("foo_id=123"))
51 * - Variable escaping is concise and controllable based on prefixes, eg
52 * - similar to Drupal's t()
53 * - use "@varname" to insert the escaped value
54 * - use "!varname" to insert raw (unescaped) values
55 * - use "#varname" to insert a numerical value (these are validated but not escaped)
56 * - to disable any preprocessing, simply omit the variable list
c5458931 57 * - control characters (@!#) are mandatory in expressions but optional in arg-keys
e2b5e5b1
TO
58 * - Variables may be individual values or arrays; arrays are imploded with commas
59 * - Conditionals are AND'd; if you need OR's, do it yourself
c3a8e3e5 60 * - Use classes/functions with documentation (rather than undocumented array-trees)
c5458931
TO
61 * - For any given string, interpolation is only performed once. After an interpolation,
62 * a string may never again be subjected to interpolation.
63 *
64 * The "interpolate-once" principle can be enforced by either interpolating on input
65 * xor output. The notations for input and output interpolation are a bit different,
66 * and they may not be mixed.
67 *
68 * @code
69 * // Interpolate on input. Set params when using them.
70 * $select->where('activity_type_id = #type', array(
71 * 'type' => 234,
72 * ));
73 *
74 * // Interpolate on output. Set params independently.
75 * $select
76 * ->where('activity_type_id = #type')
77 * ->param('type', 234),
78 * @endcode
d1d3c04a
CW
79 *
80 * @package CRM
8c9251b3 81 * @copyright CiviCRM LLC (c) 2004-2018
e2b5e5b1 82 */
bfd417c7 83class CRM_Utils_SQL_Select extends CRM_Utils_SQL_BaseParamQuery {
c5458931 84
c5458931 85 private $insertInto = NULL;
6dd717a6 86 private $insertVerb = 'INSERT INTO ';
c5458931 87 private $insertIntoFields = array();
e2b5e5b1
TO
88 private $selects = array();
89 private $from;
90 private $joins = array();
91 private $wheres = array();
92 private $groupBys = array();
93 private $havings = array();
94 private $orderBys = array();
c3a8e3e5
TO
95 private $limit = NULL;
96 private $offset = NULL;
f8da3b93 97 private $distinct = NULL;
c5458931 98
e2b5e5b1 99 /**
fe482240 100 * Create a new SELECT query.
e2b5e5b1 101 *
77855840
TO
102 * @param string $from
103 * Table-name and optional alias.
c5458931
TO
104 * @param array $options
105 * @return CRM_Utils_SQL_Select
106 */
107 public static function from($from, $options = array()) {
108 return new self($from, $options);
109 }
110
111 /**
112 * Create a partial SELECT query.
113 *
114 * @param array $options
e2b5e5b1
TO
115 * @return CRM_Utils_SQL_Select
116 */
c5458931
TO
117 public static function fragment($options = array()) {
118 return new self(NULL, $options);
e2b5e5b1
TO
119 }
120
c3a8e3e5 121 /**
fe482240 122 * Create a new SELECT query.
c3a8e3e5 123 *
77855840
TO
124 * @param string $from
125 * Table-name and optional alias.
c5458931 126 * @param array $options
c3a8e3e5 127 */
c5458931 128 public function __construct($from, $options = array()) {
e2b5e5b1 129 $this->from = $from;
c5458931
TO
130 $this->mode = isset($options['mode']) ? $options['mode'] : self::INTERPOLATE_AUTO;
131 }
132
133 /**
134 * Make a new copy of this query.
135 *
136 * @return CRM_Utils_SQL_Select
137 */
138 public function copy() {
139 return clone $this;
140 }
141
142 /**
3d469574 143 * Merge something or other.
144 *
ec2eca5a 145 * @param array|CRM_Utils_SQL_Select $other
c5458931
TO
146 * @param array|NULL $parts
147 * ex: 'joins', 'wheres'
14069c56 148 * @return CRM_Utils_SQL_Select
c5458931
TO
149 */
150 public function merge($other, $parts = NULL) {
151 if ($other === NULL) {
152 return $this;
153 }
154
ec2eca5a
TO
155 if (is_array($other)) {
156 foreach ($other as $fragment) {
157 $this->merge($fragment, $parts);
158 }
159 return $this;
160 }
161
c5458931
TO
162 if ($this->mode === self::INTERPOLATE_AUTO) {
163 $this->mode = $other->mode;
164 }
165 elseif ($other->mode === self::INTERPOLATE_AUTO) {
166 // Noop.
167 }
168 elseif ($this->mode !== $other->mode) {
169 // Mixing modes will lead to someone getting an expected substitution.
170 throw new RuntimeException("Cannot merge queries that use different interpolation modes ({$this->mode} vs {$other->mode}).");
171 }
172
173 $arrayFields = array('insertIntoFields', 'selects', 'joins', 'wheres', 'groupBys', 'havings', 'orderBys', 'params');
174 foreach ($arrayFields as $f) {
175 if ($parts === NULL || in_array($f, $parts)) {
176 $this->{$f} = array_merge($this->{$f}, $other->{$f});
177 }
178 }
179
180 $flatFields = array('insertInto', 'from', 'limit', 'offset');
181 foreach ($flatFields as $f) {
182 if ($parts === NULL || in_array($f, $parts)) {
183 if ($other->{$f} !== NULL) {
184 $this->{$f} = $other->{$f};
185 }
186 }
187 }
188
189 return $this;
e2b5e5b1
TO
190 }
191
c3a8e3e5 192 /**
fe482240 193 * Add a new JOIN clause.
c3a8e3e5 194 *
c5458931
TO
195 * Note: To add multiple JOINs at once, use $name===NULL and
196 * pass an array of $exprs.
197 *
198 * @param string|NULL $name
77855840 199 * The effective alias of the joined table.
c5458931 200 * @param string|array $exprs
77855840 201 * The complete join expression (eg "INNER JOIN mytable myalias ON mytable.id = maintable.foo_id").
c3a8e3e5
TO
202 * @param array|null $args
203 * @return CRM_Utils_SQL_Select
204 */
c5458931
TO
205 public function join($name, $exprs, $args = NULL) {
206 if ($name !== NULL) {
207 $this->joins[$name] = $this->interpolate($exprs, $args);
208 }
209 else {
210 foreach ($exprs as $name => $expr) {
211 $this->joins[$name] = $this->interpolate($expr, $args);
212 }
213 return $this;
214 }
e2b5e5b1
TO
215 return $this;
216 }
217
218 /**
c3a8e3e5
TO
219 * Specify the column(s)/value(s) to return by adding to the SELECT clause
220 *
e2b5e5b1
TO
221 * @param string|array $exprs list of SQL expressions
222 * @param null|array $args use NULL to disable interpolation; use an array of variables to enable
223 * @return CRM_Utils_SQL_Select
224 */
225 public function select($exprs, $args = NULL) {
226 $exprs = (array) $exprs;
227 foreach ($exprs as $expr) {
6e6685dc 228 $this->selects[] = $this->interpolate($expr, $args);
e2b5e5b1
TO
229 }
230 return $this;
231 }
232
f8da3b93 233 /**
234 * Return only distinct values
235 *
236 * @param bool $isDistinct allow DISTINCT select or not
237 * @return CRM_Utils_SQL_Select
238 */
239 public function distinct($isDistinct = TRUE) {
240 if ($isDistinct) {
241 $this->distinct = 'DISTINCT ';
242 }
243 return $this;
244 }
245
e2b5e5b1 246 /**
c3a8e3e5
TO
247 * Limit results by adding extra condition(s) to the WHERE clause
248 *
e2b5e5b1
TO
249 * @param string|array $exprs list of SQL expressions
250 * @param null|array $args use NULL to disable interpolation; use an array of variables to enable
251 * @return CRM_Utils_SQL_Select
252 */
253 public function where($exprs, $args = NULL) {
254 $exprs = (array) $exprs;
255 foreach ($exprs as $expr) {
fa7c2979
TO
256 $evaluatedExpr = $this->interpolate($expr, $args);
257 $this->wheres[$evaluatedExpr] = $evaluatedExpr;
e2b5e5b1
TO
258 }
259 return $this;
260 }
261
262 /**
fe482240 263 * Group results by adding extra items to the GROUP BY clause.
c3a8e3e5 264 *
e2b5e5b1
TO
265 * @param string|array $exprs list of SQL expressions
266 * @param null|array $args use NULL to disable interpolation; use an array of variables to enable
267 * @return CRM_Utils_SQL_Select
268 */
269 public function groupBy($exprs, $args = NULL) {
270 $exprs = (array) $exprs;
271 foreach ($exprs as $expr) {
6e6685dc
C
272 $evaluatedExpr = $this->interpolate($expr, $args);
273 $this->groupBys[$evaluatedExpr] = $evaluatedExpr;
e2b5e5b1
TO
274 }
275 return $this;
276 }
277
278 /**
c3a8e3e5
TO
279 * Limit results by adding extra condition(s) to the HAVING clause
280 *
e2b5e5b1
TO
281 * @param string|array $exprs list of SQL expressions
282 * @param null|array $args use NULL to disable interpolation; use an array of variables to enable
283 * @return CRM_Utils_SQL_Select
284 */
285 public function having($exprs, $args = NULL) {
286 $exprs = (array) $exprs;
287 foreach ($exprs as $expr) {
6e6685dc
C
288 $evaluatedExpr = $this->interpolate($expr, $args);
289 $this->havings[$evaluatedExpr] = $evaluatedExpr;
e2b5e5b1
TO
290 }
291 return $this;
292 }
293
294 /**
fe482240 295 * Sort results by adding extra items to the ORDER BY clause.
c3a8e3e5 296 *
e2b5e5b1
TO
297 * @param string|array $exprs list of SQL expressions
298 * @param null|array $args use NULL to disable interpolation; use an array of variables to enable
4c6cc364
CW
299 * @param int $weight
300 * @return \CRM_Utils_SQL_Select
e2b5e5b1 301 */
6db70618
TO
302 public function orderBy($exprs, $args = NULL, $weight = 0) {
303 static $guid = 0;
e2b5e5b1
TO
304 $exprs = (array) $exprs;
305 foreach ($exprs as $expr) {
6e6685dc 306 $evaluatedExpr = $this->interpolate($expr, $args);
6db70618 307 $this->orderBys[$evaluatedExpr] = array('value' => $evaluatedExpr, 'weight' => $weight, 'guid' => $guid++);
e2b5e5b1
TO
308 }
309 return $this;
310 }
311
c5458931
TO
312 /**
313 * Set one (or multiple) parameters to interpolate into the query.
314 *
315 * @param array|string $keys
316 * Key name, or an array of key-value pairs.
317 * @param null|mixed $value
1f7d270d
TO
318 * The new value of the parameter.
319 * Values may be strings, ints, or arrays thereof -- provided that the
320 * SQL query uses appropriate prefix (e.g. "@", "!", "#").
14069c56 321 * @return \CRM_Utils_SQL_Select
c5458931
TO
322 */
323 public function param($keys, $value = NULL) {
bfd417c7
TO
324 // Why bother with an override? To provide bett er type-hinting in `@return`.
325 return parent::param($keys, $value);
c5458931
TO
326 }
327
c3a8e3e5 328 /**
fe482240 329 * Set a limit on the number of records to return.
c3a8e3e5
TO
330 *
331 * @param int $limit
332 * @param int $offset
333 * @return CRM_Utils_SQL_Select
334 * @throws CRM_Core_Exception
335 */
336 public function limit($limit, $offset = 0) {
337 if ($limit !== NULL && !is_numeric($limit)) {
338 throw new CRM_Core_Exception("Illegal limit");
339 }
340 if ($offset !== NULL && !is_numeric($offset)) {
341 throw new CRM_Core_Exception("Illegal offset");
342 }
343 $this->limit = $limit;
344 $this->offset = $offset;
345 return $this;
346 }
347
c5458931
TO
348 /**
349 * Insert the results of the SELECT query into another
350 * table.
351 *
352 * @param string $table
353 * The name of the other table (which receives new data).
354 * @param array $fields
355 * The fields to fill in the other table (in order).
14069c56 356 * @return CRM_Utils_SQL_Select
c5458931
TO
357 * @see insertIntoField
358 */
359 public function insertInto($table, $fields = array()) {
360 $this->insertInto = $table;
361 $this->insertIntoField($fields);
362 return $this;
363 }
364
6dd717a6 365 /**
366 * Wrapper function of insertInto fn but sets insertVerb = "INSERT IGNORE INTO "
367 *
368 * @param string $table
369 * The name of the other table (which receives new data).
370 * @param array $fields
371 * The fields to fill in the other table (in order).
372 * @return CRM_Utils_SQL_Select
373 */
374 public function insertIgnoreInto($table, $fields = array()) {
375 $this->insertVerb = "INSERT IGNORE INTO ";
376 return $this->insertInto($table, $fields);
377 }
378
379 /**
380 * Wrapper function of insertInto fn but sets insertVerb = "REPLACE INTO "
381 *
382 * @param string $table
383 * The name of the other table (which receives new data).
384 * @param array $fields
385 * The fields to fill in the other table (in order).
386 */
387 public function replaceInto($table, $fields = array()) {
388 $this->insertVerb = "REPLACE INTO ";
389 return $this->insertInto($table, $fields);
390 }
391
392
c5458931
TO
393 /**
394 * @param array $fields
395 * The fields to fill in the other table (in order).
14069c56 396 * @return CRM_Utils_SQL_Select
c5458931
TO
397 */
398 public function insertIntoField($fields) {
399 $fields = (array) $fields;
400 foreach ($fields as $field) {
401 $this->insertIntoFields[] = $field;
402 }
403 return $this;
404 }
405
406 /**
407 * @param array|NULL $parts
408 * List of fields to check (e.g. 'selects', 'joins').
409 * Defaults to all.
410 * @return bool
411 */
412 public function isEmpty($parts = NULL) {
413 $empty = TRUE;
414 $fields = array(
415 'insertInto',
416 'insertIntoFields',
417 'selects',
418 'from',
419 'joins',
420 'wheres',
421 'groupBys',
422 'havings',
423 'orderBys',
424 'limit',
425 'offset',
426 );
427 if ($parts !== NULL) {
428 $fields = array_intersect($fields, $parts);
429 }
430 foreach ($fields as $field) {
431 if (!empty($this->{$field})) {
432 $empty = FALSE;
433 }
434 }
435 return $empty;
436 }
437
e2b5e5b1 438 /**
a6c01b45
CW
439 * @return string
440 * SQL statement
e2b5e5b1
TO
441 */
442 public function toSQL() {
c5458931
TO
443 $sql = '';
444 if ($this->insertInto) {
6dd717a6 445 $sql .= $this->insertVerb . $this->insertInto . ' (';
c5458931
TO
446 $sql .= implode(', ', $this->insertIntoFields);
447 $sql .= ")\n";
448 }
6dd717a6 449
e2b5e5b1 450 if ($this->selects) {
f8da3b93 451 $sql .= 'SELECT ' . $this->distinct . implode(', ', $this->selects) . "\n";
e2b5e5b1
TO
452 }
453 else {
c5458931
TO
454 $sql .= 'SELECT *' . "\n";
455 }
456 if ($this->from !== NULL) {
457 $sql .= 'FROM ' . $this->from . "\n";
e2b5e5b1 458 }
e2b5e5b1
TO
459 foreach ($this->joins as $join) {
460 $sql .= $join . "\n";
461 }
462 if ($this->wheres) {
463 $sql .= 'WHERE (' . implode(') AND (', $this->wheres) . ")\n";
464 }
465 if ($this->groupBys) {
466 $sql .= 'GROUP BY ' . implode(', ', $this->groupBys) . "\n";
467 }
468 if ($this->havings) {
469 $sql .= 'HAVING (' . implode(') AND (', $this->havings) . ")\n";
470 }
471 if ($this->orderBys) {
6db70618
TO
472 $orderBys = CRM_Utils_Array::crmArraySortByField($this->orderBys,
473 array('weight', 'guid'));
4c6cc364
CW
474 $orderBys = CRM_Utils_Array::collect('value', $orderBys);
475 $sql .= 'ORDER BY ' . implode(', ', $orderBys) . "\n";
e2b5e5b1 476 }
c3a8e3e5
TO
477 if ($this->limit !== NULL) {
478 $sql .= 'LIMIT ' . $this->limit . "\n";
479 if ($this->offset !== NULL) {
480 $sql .= 'OFFSET ' . $this->offset . "\n";
481 }
482 }
c5458931
TO
483 if ($this->mode === self::INTERPOLATE_OUTPUT) {
484 $sql = $this->interpolate($sql, $this->params, self::INTERPOLATE_OUTPUT);
485 }
e2b5e5b1
TO
486 return $sql;
487 }
96025800 488
77e74ae1 489 /**
c4dcc9cf
TO
490 * Execute the query.
491 *
492 * To examine the results, use a function like `fetch()`, `fetchAll()`,
493 * `fetchValue()`, or `fetchMap()`.
494 *
495 * @param string|NULL $daoName
496 * The return object should be an instance of this class.
497 * Ex: 'CRM_Contact_BAO_Contact'.
498 * @param bool $i18nRewrite
499 * If the system has multilingual features, should the field/table
500 * names be rewritten?
77e74ae1 501 * @return CRM_Core_DAO
c4dcc9cf
TO
502 * @see CRM_Core_DAO::executeQuery
503 * @see CRM_Core_I18n_Schema::rewriteQuery
77e74ae1
TO
504 */
505 public function execute($daoName = NULL, $i18nRewrite = TRUE) {
506 // Don't pass through $params. toSQL() handles interpolation.
507 $params = array();
508
509 // Don't pass through $abort, $trapException. Just use straight-up exceptions.
510 $abort = TRUE;
511 $trapException = FALSE;
512 $errorScope = CRM_Core_TemporaryErrorScope::useException();
513
514 // Don't pass through freeDAO. You can do it yourself.
515 $freeDAO = FALSE;
516
517 return CRM_Core_DAO::executeQuery($this->toSQL(), $params, $abort, $daoName,
518 $freeDAO, $i18nRewrite, $trapException);
519 }
520
e2b5e5b1 521}