09a754bb433e6521fca77707497028f3ab7512c4
[yaffs-website] / web / core / modules / views / src / Plugin / views / query / Sql.php
1 <?php
2
3 namespace Drupal\views\Plugin\views\query;
4
5 use Drupal\Component\Utility\NestedArray;
6 use Drupal\Core\Cache\Cache;
7 use Drupal\Core\Database\Database;
8 use Drupal\Core\Database\Query\Condition;
9 use Drupal\Core\Entity\EntityTypeManagerInterface;
10 use Drupal\Core\Form\FormStateInterface;
11 use Drupal\views\Plugin\views\display\DisplayPluginBase;
12 use Drupal\Core\Database\DatabaseExceptionWrapper;
13 use Drupal\views\Plugin\views\join\JoinPluginBase;
14 use Drupal\views\Plugin\views\HandlerBase;
15 use Drupal\views\ResultRow;
16 use Drupal\views\ViewExecutable;
17 use Drupal\views\Views;
18 use Symfony\Component\DependencyInjection\ContainerInterface;
19
20 /**
21  * Views query plugin for an SQL query.
22  *
23  * @ingroup views_query_plugins
24  *
25  * @ViewsQuery(
26  *   id = "views_query",
27  *   title = @Translation("SQL Query"),
28  *   help = @Translation("Query will be generated and run using the Drupal database API.")
29  * )
30  */
31 class Sql extends QueryPluginBase {
32
33   /**
34    * A list of tables in the order they should be added, keyed by alias.
35    */
36   protected $tableQueue = [];
37
38   /**
39    * Holds an array of tables and counts added so that we can create aliases
40    */
41   public $tables = [];
42
43   /**
44    * Holds an array of relationships, which are aliases of the primary
45    * table that represent different ways to join the same table in.
46    */
47   public $relationships = [];
48
49   /**
50    * An array of sections of the WHERE query. Each section is in itself
51    * an array of pieces and a flag as to whether or not it should be AND
52    * or OR.
53    */
54   public $where = [];
55   /**
56    * An array of sections of the HAVING query. Each section is in itself
57    * an array of pieces and a flag as to whether or not it should be AND
58    * or OR.
59    */
60   public $having = [];
61   /**
62    * The default operator to use when connecting the WHERE groups. May be
63    * AND or OR.
64    *
65    * @var string
66    */
67   protected $groupOperator = 'AND';
68
69   /**
70    * A simple array of order by clauses.
71    */
72   public $orderby = [];
73
74   /**
75    * A simple array of group by clauses.
76    */
77   public $groupby = [];
78
79
80   /**
81    * An array of fields.
82    */
83   public $fields = [];
84
85   /**
86    * A flag as to whether or not to make the primary field distinct.
87    *
88    * @var bool
89    */
90   public $distinct = FALSE;
91
92   /**
93    * @var bool
94    */
95   protected $hasAggregate = FALSE;
96
97   /**
98    * Should this query be optimized for counts, for example no sorts.
99    */
100   protected $getCountOptimized = NULL;
101
102   /**
103    * An array mapping table aliases and field names to field aliases.
104    */
105   protected $fieldAliases = [];
106
107   /**
108    * Query tags which will be passed over to the dbtng query object.
109    */
110   public $tags = [];
111
112   /**
113    * Is the view marked as not distinct.
114    *
115    * @var bool
116    */
117   protected $noDistinct;
118
119   /**
120    * The entity type manager.
121    *
122    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
123    */
124   protected $entityTypeManager;
125
126   /**
127    * The database-specific date handler.
128    *
129    * @var \Drupal\views\Plugin\views\query\DateSqlInterface
130    */
131   protected $dateSql;
132
133   /**
134    * Constructs a Sql object.
135    *
136    * @param array $configuration
137    *   A configuration array containing information about the plugin instance.
138    * @param string $plugin_id
139    *   The plugin_id for the plugin instance.
140    * @param mixed $plugin_definition
141    *   The plugin implementation definition.
142    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
143    *   The entity type manager.
144    * @param \Drupal\views\Plugin\views\query\DateSqlInterface $date_sql
145    *   The database-specific date handler.
146    */
147   public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, DateSqlInterface $date_sql) {
148     parent::__construct($configuration, $plugin_id, $plugin_definition);
149
150     $this->entityTypeManager = $entity_type_manager;
151     $this->dateSql = $date_sql;
152   }
153
154   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
155     return new static(
156       $configuration,
157       $plugin_id,
158       $plugin_definition,
159       $container->get('entity_type.manager'),
160       $container->get('views.date_sql')
161     );
162   }
163
164   /**
165    * {@inheritdoc}
166    */
167   public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
168     parent::init($view, $display, $options);
169
170     $base_table = $this->view->storage->get('base_table');
171     $base_field = $this->view->storage->get('base_field');
172     $this->relationships[$base_table] = [
173       'link' => NULL,
174       'table' => $base_table,
175       'alias' => $base_table,
176       'base' => $base_table
177     ];
178
179     // init the table queue with our primary table.
180     $this->tableQueue[$base_table] = [
181       'alias' => $base_table,
182       'table' => $base_table,
183       'relationship' => $base_table,
184       'join' => NULL,
185     ];
186
187     // init the tables with our primary table
188     $this->tables[$base_table][$base_table] = [
189       'count' => 1,
190       'alias' => $base_table,
191     ];
192
193     $this->count_field = [
194       'table' => $base_table,
195       'field' => $base_field,
196       'alias' => $base_field,
197       'count' => TRUE,
198     ];
199   }
200
201   /**
202    * Returns a reference to the table queue array for this query.
203    *
204    * Because this method returns by reference, alter hooks may edit the tables
205    * array directly to make their changes. If just adding tables, however, the
206    * use of the addTable() method is preferred.
207    *
208    * Note that if you want to manipulate the table queue array, this method must
209    * be called by reference as well:
210    *
211    * @code
212    * $tables =& $query->getTableQueue();
213    * @endcode
214    *
215    * @return array
216    *   A reference to the table queue array structure.
217    */
218   public function &getTableQueue() {
219     return $this->tableQueue;
220   }
221
222   /**
223    * Set the view to be distinct (per base field).
224    *
225    * @param bool $value
226    *   Should the view be distincted.
227    */
228   protected function setDistinct($value = TRUE) {
229     if (!(isset($this->noDistinct) && $value)) {
230       $this->distinct = $value;
231     }
232   }
233
234   /**
235    * Set what field the query will count() on for paging.
236    */
237   public function setCountField($table, $field, $alias = NULL) {
238     if (empty($alias)) {
239       $alias = $table . '_' . $field;
240     }
241     $this->count_field = [
242       'table' => $table,
243       'field' => $field,
244       'alias' => $alias,
245       'count' => TRUE,
246     ];
247   }
248
249   protected function defineOptions() {
250     $options = parent::defineOptions();
251     $options['disable_sql_rewrite'] = [
252       'default' => FALSE,
253     ];
254     $options['distinct'] = [
255       'default' => FALSE,
256     ];
257     $options['replica'] = [
258       'default' => FALSE,
259     ];
260     $options['query_comment'] = [
261       'default' => '',
262     ];
263     $options['query_tags'] = [
264       'default' => [],
265     ];
266
267     return $options;
268   }
269
270   /**
271    * Add settings for the ui.
272    */
273   public function buildOptionsForm(&$form, FormStateInterface $form_state) {
274     parent::buildOptionsForm($form, $form_state);
275
276     $form['disable_sql_rewrite'] = [
277       '#title' => $this->t('Disable SQL rewriting'),
278       '#description' => $this->t('Disabling SQL rewriting will omit all query tags, i. e. disable node access checks as well as override hook_query_alter() implementations in other modules.'),
279       '#type' => 'checkbox',
280       '#default_value' => !empty($this->options['disable_sql_rewrite']),
281       '#suffix' => '<div class="messages messages--warning sql-rewrite-warning js-hide">' . $this->t('WARNING: Disabling SQL rewriting means that node access security is disabled. This may allow users to see data they should not be able to see if your view is misconfigured. Use this option only if you understand and accept this security risk.') . '</div>',
282     ];
283     $form['distinct'] = [
284       '#type' => 'checkbox',
285       '#title' => $this->t('Distinct'),
286       '#description' => $this->t('This will make the view display only distinct items. If there are multiple identical items, each will be displayed only once. You can use this to try and remove duplicates from a view, though it does not always work. Note that this can slow queries down, so use it with caution.'),
287       '#default_value' => !empty($this->options['distinct']),
288     ];
289     $form['replica'] = [
290       '#type' => 'checkbox',
291       '#title' => $this->t('Use Secondary Server'),
292       '#description' => $this->t('This will make the query attempt to connect to a replica server if available.  If no replica server is defined or available, it will fall back to the default server.'),
293       '#default_value' => !empty($this->options['replica']),
294     ];
295     $form['query_comment'] = [
296       '#type' => 'textfield',
297       '#title' => $this->t('Query Comment'),
298       '#description' => $this->t('If set, this comment will be embedded in the query and passed to the SQL server. This can be helpful for logging or debugging.'),
299       '#default_value' => $this->options['query_comment'],
300     ];
301     $form['query_tags'] = [
302       '#type' => 'textfield',
303       '#title' => $this->t('Query Tags'),
304       '#description' => $this->t('If set, these tags will be appended to the query and can be used to identify the query in a module. This can be helpful for altering queries.'),
305       '#default_value' => implode(', ', $this->options['query_tags']),
306       '#element_validate' => ['views_element_validate_tags'],
307     ];
308   }
309
310   /**
311    * Special submit handling.
312    */
313   public function submitOptionsForm(&$form, FormStateInterface $form_state) {
314     $element = ['#parents' => ['query', 'options', 'query_tags']];
315     $value = explode(',', NestedArray::getValue($form_state->getValues(), $element['#parents']));
316     $value = array_filter(array_map('trim', $value));
317     $form_state->setValueForElement($element, $value);
318   }
319
320   /**
321    * A relationship is an alternative endpoint to a series of table
322    * joins. Relationships must be aliases of the primary table and
323    * they must join either to the primary table or to a pre-existing
324    * relationship.
325    *
326    * An example of a relationship would be a nodereference table.
327    * If you have a nodereference named 'book_parent' which links to a
328    * parent node, you could set up a relationship 'node_book_parent'
329    * to 'node'. Then, anything that links to 'node' can link to
330    * 'node_book_parent' instead, thus allowing all properties of
331    * both nodes to be available in the query.
332    *
333    * @param $alias
334    *   What this relationship will be called, and is also the alias
335    *   for the table.
336    * @param \Drupal\views\Plugin\views\join\JoinPluginBase $join
337    *   A Join object (or derived object) to join the alias in.
338    * @param $base
339    *   The name of the 'base' table this relationship represents; this
340    *   tells the join search which path to attempt to use when finding
341    *   the path to this relationship.
342    * @param $link_point
343    *   If this relationship links to something other than the primary
344    *   table, specify that table here. For example, a 'track' node
345    *   might have a relationship to an 'album' node, which might
346    *   have a relationship to an 'artist' node.
347    */
348   public function addRelationship($alias, JoinPluginBase $join, $base, $link_point = NULL) {
349     if (empty($link_point)) {
350       $link_point = $this->view->storage->get('base_table');
351     }
352     elseif (!array_key_exists($link_point, $this->relationships)) {
353       return FALSE;
354     }
355
356     // Make sure $alias isn't already used; if it, start adding stuff.
357     $alias_base = $alias;
358     $count = 1;
359     while (!empty($this->relationships[$alias])) {
360       $alias = $alias_base . '_' . $count++;
361     }
362
363     // Make sure this join is adjusted for our relationship.
364     if ($link_point && isset($this->relationships[$link_point])) {
365       $join = $this->adjustJoin($join, $link_point);
366     }
367
368     // Add the table directly to the queue to avoid accidentally marking
369     // it.
370     $this->tableQueue[$alias] = [
371       'table' => $join->table,
372       'num' => 1,
373       'alias' => $alias,
374       'join' => $join,
375       'relationship' => $link_point,
376     ];
377
378     $this->relationships[$alias] = [
379       'link' => $link_point,
380       'table' => $join->table,
381       'base' => $base,
382     ];
383
384     $this->tables[$this->view->storage->get('base_table')][$alias] = [
385       'count' => 1,
386       'alias' => $alias,
387     ];
388
389     return $alias;
390   }
391
392   /**
393    * Add a table to the query, ensuring the path exists.
394    *
395    * This function will test to ensure that the path back to the primary
396    * table is valid and exists; if you do not wish for this testing to
397    * occur, use $query->queueTable() instead.
398    *
399    * @param $table
400    *   The name of the table to add. It needs to exist in the global table
401    *   array.
402    * @param $relationship
403    *   An alias of a table; if this is set, the path back to this table will
404    *   be tested prior to adding the table, making sure that all intermediary
405    *   tables exist and are properly aliased. If set to NULL the path to
406    *   the primary table will be ensured. If the path cannot be made, the
407    *   table will NOT be added.
408    * @param \Drupal\views\Plugin\views\join\JoinPluginBase $join
409    *   In some join configurations this table may actually join back through
410    *   a different method; this is most likely to be used when tracing
411    *   a hierarchy path. (node->parent->parent2->parent3). This parameter
412    *   will specify how this table joins if it is not the default.
413    * @param $alias
414    *   A specific alias to use, rather than the default alias.
415    *
416    * @return string
417    *   The alias of the table; this alias can be used to access information
418    *   about the table and should always be used to refer to the table when
419    *   adding parts to the query. Or FALSE if the table was not able to be
420    *   added.
421    */
422   public function addTable($table, $relationship = NULL, JoinPluginBase $join = NULL, $alias = NULL) {
423     if (!$this->ensurePath($table, $relationship, $join)) {
424       return FALSE;
425     }
426
427     if ($join && $relationship) {
428       $join = $this->adjustJoin($join, $relationship);
429     }
430
431     return $this->queueTable($table, $relationship, $join, $alias);
432   }
433
434   /**
435    * Add a table to the query without ensuring the path.
436    *
437    * This is a pretty internal function to Views and addTable() or
438    * ensureTable() should be used instead of this one, unless you are
439    * absolutely sure this is what you want.
440    *
441    * @param $table
442    *   The name of the table to add. It needs to exist in the global table
443    *   array.
444    * @param $relationship
445    *   The primary table alias this table is related to. If not set, the
446    *   primary table will be used.
447    * @param \Drupal\views\Plugin\views\join\JoinPluginBase $join
448    *   In some join configurations this table may actually join back through
449    *   a different method; this is most likely to be used when tracing
450    *   a hierarchy path. (node->parent->parent2->parent3). This parameter
451    *   will specify how this table joins if it is not the default.
452    * @param $alias
453    *   A specific alias to use, rather than the default alias.
454    *
455    * @return string
456    *   The alias of the table; this alias can be used to access information
457    *   about the table and should always be used to refer to the table when
458    *   adding parts to the query. Or FALSE if the table was not able to be
459    *   added.
460    */
461   public function queueTable($table, $relationship = NULL, JoinPluginBase $join = NULL, $alias = NULL) {
462     // If the alias is set, make sure it doesn't already exist.
463     if (isset($this->tableQueue[$alias])) {
464       return $alias;
465     }
466
467     if (empty($relationship)) {
468       $relationship = $this->view->storage->get('base_table');
469     }
470
471     if (!array_key_exists($relationship, $this->relationships)) {
472       return FALSE;
473     }
474
475     if (!$alias && $join && $relationship && !empty($join->adjusted) && $table != $join->table) {
476       if ($relationship == $this->view->storage->get('base_table')) {
477         $alias = $table;
478       }
479       else {
480         $alias = $relationship . '_' . $table;
481       }
482     }
483
484     // Check this again to make sure we don't blow up existing aliases for already
485     // adjusted joins.
486     if (isset($this->tableQueue[$alias])) {
487       return $alias;
488     }
489
490     $alias = $this->markTable($table, $relationship, $alias);
491
492     // If no alias is specified, give it the default.
493     if (!isset($alias)) {
494       $alias = $this->tables[$relationship][$table]['alias'] . $this->tables[$relationship][$table]['count'];
495     }
496
497     // If this is a relationship based table, add a marker with
498     // the relationship as a primary table for the alias.
499     if ($table != $alias) {
500       $this->markTable($alias, $this->view->storage->get('base_table'), $alias);
501     }
502
503     // If no join is specified, pull it from the table data.
504     if (!isset($join)) {
505       $join = $this->getJoinData($table, $this->relationships[$relationship]['base']);
506       if (empty($join)) {
507         return FALSE;
508       }
509
510       $join = $this->adjustJoin($join, $relationship);
511     }
512
513     $this->tableQueue[$alias] = [
514       'table' => $table,
515       'num' => $this->tables[$relationship][$table]['count'],
516       'alias' => $alias,
517       'join' => $join,
518       'relationship' => $relationship,
519     ];
520
521     return $alias;
522   }
523
524   protected function markTable($table, $relationship, $alias) {
525     // Mark that this table has been added.
526     if (empty($this->tables[$relationship][$table])) {
527       if (!isset($alias)) {
528         $alias = '';
529         if ($relationship != $this->view->storage->get('base_table')) {
530           // double underscore will help prevent accidental name
531           // space collisions.
532           $alias = $relationship . '__';
533         }
534         $alias .= $table;
535       }
536       $this->tables[$relationship][$table] = [
537         'count' => 1,
538         'alias' => $alias,
539       ];
540     }
541     else {
542       $this->tables[$relationship][$table]['count']++;
543     }
544
545     return $alias;
546   }
547
548   /**
549    * Ensure a table exists in the queue; if it already exists it won't
550    * do anything, but if it doesn't it will add the table queue. It will ensure
551    * a path leads back to the relationship table.
552    *
553    * @param $table
554    *   The unaliased name of the table to ensure.
555    * @param $relationship
556    *   The relationship to ensure the table links to. Each relationship will
557    *   get a unique instance of the table being added. If not specified,
558    *   will be the primary table.
559    * @param \Drupal\views\Plugin\views\join\JoinPluginBase $join
560    *   A Join object (or derived object) to join the alias in.
561    *
562    * @return
563    *   The alias used to refer to this specific table, or NULL if the table
564    *   cannot be ensured.
565    */
566   public function ensureTable($table, $relationship = NULL, JoinPluginBase $join = NULL) {
567     // ensure a relationship
568     if (empty($relationship)) {
569       $relationship = $this->view->storage->get('base_table');
570     }
571
572     // If the relationship is the primary table, this actually be a relationship
573     // link back from an alias. We store all aliases along with the primary table
574     // to detect this state, because eventually it'll hit a table we already
575     // have and that's when we want to stop.
576     if ($relationship == $this->view->storage->get('base_table') && !empty($this->tables[$relationship][$table])) {
577       return $this->tables[$relationship][$table]['alias'];
578     }
579
580     if (!array_key_exists($relationship, $this->relationships)) {
581       return FALSE;
582     }
583
584     if ($table == $this->relationships[$relationship]['base']) {
585       return $relationship;
586     }
587
588     // If we do not have join info, fetch it.
589     if (!isset($join)) {
590       $join = $this->getJoinData($table, $this->relationships[$relationship]['base']);
591     }
592
593     // If it can't be fetched, this won't work.
594     if (empty($join)) {
595       return;
596     }
597
598     // Adjust this join for the relationship, which will ensure that the 'base'
599     // table it links to is correct. Tables adjoined to a relationship
600     // join to a link point, not the base table.
601     $join = $this->adjustJoin($join, $relationship);
602
603     if ($this->ensurePath($table, $relationship, $join)) {
604       // Attempt to eliminate redundant joins.  If this table's
605       // relationship and join exactly matches an existing table's
606       // relationship and join, we do not have to join to it again;
607       // just return the existing table's alias.  See
608       // http://groups.drupal.org/node/11288 for details.
609       //
610       // This can be done safely here but not lower down in
611       // queueTable(), because queueTable() is also used by
612       // addTable() which requires the ability to intentionally add
613       // the same table with the same join multiple times.  For
614       // example, a view that filters on 3 taxonomy terms using AND
615       // needs to join taxonomy_term_data 3 times with the same join.
616
617       // scan through the table queue to see if a matching join and
618       // relationship exists.  If so, use it instead of this join.
619
620       // TODO: Scanning through $this->tableQueue results in an
621       // O(N^2) algorithm, and this code runs every time the view is
622       // instantiated (Views 2 does not currently cache queries).
623       // There are a couple possible "improvements" but we should do
624       // some performance testing before picking one.
625       foreach ($this->tableQueue as $queued_table) {
626         // In PHP 4 and 5, the == operation returns TRUE for two objects
627         // if they are instances of the same class and have the same
628         // attributes and values.
629         if ($queued_table['relationship'] == $relationship && $queued_table['join'] == $join) {
630           return $queued_table['alias'];
631         }
632       }
633
634       return $this->queueTable($table, $relationship, $join);
635     }
636   }
637
638   /**
639    * Make sure that the specified table can be properly linked to the primary
640    * table in the JOINs. This function uses recursion. If the tables
641    * needed to complete the path back to the primary table are not in the
642    * query they will be added, but additional copies will NOT be added
643    * if the table is already there.
644    */
645   protected function ensurePath($table, $relationship = NULL, $join = NULL, $traced = [], $add = []) {
646     if (!isset($relationship)) {
647       $relationship = $this->view->storage->get('base_table');
648     }
649
650     if (!array_key_exists($relationship, $this->relationships)) {
651       return FALSE;
652     }
653
654     // If we do not have join info, fetch it.
655     if (!isset($join)) {
656       $join = $this->getJoinData($table, $this->relationships[$relationship]['base']);
657     }
658
659     // If it can't be fetched, this won't work.
660     if (empty($join)) {
661       return FALSE;
662     }
663
664     // Does a table along this path exist?
665     if (isset($this->tables[$relationship][$table]) ||
666       ($join && $join->leftTable == $relationship) ||
667       ($join && $join->leftTable == $this->relationships[$relationship]['table'])) {
668
669       // Make sure that we're linking to the correct table for our relationship.
670       foreach (array_reverse($add) as $table => $path_join) {
671         $this->queueTable($table, $relationship, $this->adjustJoin($path_join, $relationship));
672       }
673       return TRUE;
674     }
675
676     // Have we been this way?
677     if (isset($traced[$join->leftTable])) {
678       // We looped. Broken.
679       return FALSE;
680     }
681
682     // Do we have to add this table?
683     $left_join = $this->getJoinData($join->leftTable, $this->relationships[$relationship]['base']);
684     if (!isset($this->tables[$relationship][$join->leftTable])) {
685       $add[$join->leftTable] = $left_join;
686     }
687
688     // Keep looking.
689     $traced[$join->leftTable] = TRUE;
690     return $this->ensurePath($join->leftTable, $relationship, $left_join, $traced, $add);
691   }
692
693   /**
694    * Fix a join to adhere to the proper relationship; the left table can vary
695    * based upon what relationship items are joined in on.
696    */
697   protected function adjustJoin($join, $relationship) {
698     if (!empty($join->adjusted)) {
699       return $join;
700     }
701
702     if (empty($relationship) || empty($this->relationships[$relationship])) {
703       return $join;
704     }
705
706     // Adjusts the left table for our relationship.
707     if ($relationship != $this->view->storage->get('base_table')) {
708       // If we're linking to the primary table, the relationship to use will
709       // be the prior relationship. Unless it's a direct link.
710
711       // Safety! Don't modify an original here.
712       $join = clone $join;
713
714       // Do we need to try to ensure a path?
715       if ($join->leftTable != $this->relationships[$relationship]['table'] &&
716         $join->leftTable != $this->relationships[$relationship]['base'] &&
717         !isset($this->tables[$relationship][$join->leftTable]['alias'])) {
718         $this->ensureTable($join->leftTable, $relationship);
719       }
720
721       // First, if this is our link point/anchor table, just use the relationship
722       if ($join->leftTable == $this->relationships[$relationship]['table']) {
723         $join->leftTable = $relationship;
724       }
725       // then, try the base alias.
726       elseif (isset($this->tables[$relationship][$join->leftTable]['alias'])) {
727         $join->leftTable = $this->tables[$relationship][$join->leftTable]['alias'];
728       }
729       // But if we're already looking at an alias, use that instead.
730       elseif (isset($this->tableQueue[$relationship]['alias'])) {
731         $join->leftTable = $this->tableQueue[$relationship]['alias'];
732       }
733     }
734
735     $join->adjusted = TRUE;
736     return $join;
737   }
738
739   /**
740    * Retrieve join data from the larger join data cache.
741    *
742    * @param $table
743    *   The table to get the join information for.
744    * @param $base_table
745    *   The path we're following to get this join.
746    *
747    * @return \Drupal\views\Plugin\views\join\JoinPluginBase
748    *   A Join object or child object, if one exists.
749    */
750   public function getJoinData($table, $base_table) {
751     // Check to see if we're linking to a known alias. If so, get the real
752     // table's data instead.
753     if (!empty($this->tableQueue[$table])) {
754       $table = $this->tableQueue[$table]['table'];
755     }
756     return HandlerBase::getTableJoin($table, $base_table);
757   }
758
759   /**
760    * Get the information associated with a table.
761    *
762    * If you need the alias of a table with a particular relationship, use
763    * ensureTable().
764    */
765   public function getTableInfo($table) {
766     if (!empty($this->tableQueue[$table])) {
767       return $this->tableQueue[$table];
768     }
769
770     // In rare cases we might *only* have aliased versions of the table.
771     if (!empty($this->tables[$this->view->storage->get('base_table')][$table])) {
772       $alias = $this->tables[$this->view->storage->get('base_table')][$table]['alias'];
773       if (!empty($this->tableQueue[$alias])) {
774         return $this->tableQueue[$alias];
775       }
776     }
777   }
778
779   /**
780    * Add a field to the query table, possibly with an alias. This will
781    * automatically call ensureTable to make sure the required table
782    * exists, *unless* $table is unset.
783    *
784    * @param $table
785    *   The table this field is attached to. If NULL, it is assumed this will
786    *   be a formula; otherwise, ensureTable is used to make sure the
787    *   table exists.
788    * @param $field
789    *   The name of the field to add. This may be a real field or a formula.
790    * @param $alias
791    *   The alias to create. If not specified, the alias will be $table_$field
792    *   unless $table is NULL. When adding formulae, it is recommended that an
793    *   alias be used.
794    * @param $params
795    *   An array of parameters additional to the field that will control items
796    *   such as aggregation functions and DISTINCT. Some values that are
797    *   recognized:
798    *   - function: An aggregation function to apply, such as SUM.
799    *   - aggregate: Set to TRUE to indicate that this value should be
800    *     aggregated in a GROUP BY.
801    *
802    * @return string
803    *   The name that this field can be referred to as. Usually this is the alias.
804    */
805   public function addField($table, $field, $alias = '', $params = []) {
806     // We check for this specifically because it gets a special alias.
807     if ($table == $this->view->storage->get('base_table') && $field == $this->view->storage->get('base_field') && empty($alias)) {
808       $alias = $this->view->storage->get('base_field');
809     }
810
811     if ($table && empty($this->tableQueue[$table])) {
812       $this->ensureTable($table);
813     }
814
815     if (!$alias && $table) {
816       $alias = $table . '_' . $field;
817     }
818
819     // Make sure an alias is assigned
820     $alias = $alias ? $alias : $field;
821
822     // PostgreSQL truncates aliases to 63 characters:
823     //   https://www.drupal.org/node/571548.
824
825     // We limit the length of the original alias up to 60 characters
826     // to get a unique alias later if its have duplicates
827     $alias = strtolower(substr($alias, 0, 60));
828
829     // Create a field info array.
830     $field_info = [
831       'field' => $field,
832       'table' => $table,
833       'alias' => $alias,
834     ] + $params;
835
836     // Test to see if the field is actually the same or not. Due to
837     // differing parameters changing the aggregation function, we need
838     // to do some automatic alias collision detection:
839     $base = $alias;
840     $counter = 0;
841     while (!empty($this->fields[$alias]) && $this->fields[$alias] != $field_info) {
842       $field_info['alias'] = $alias = $base . '_' . ++$counter;
843     }
844
845     if (empty($this->fields[$alias])) {
846       $this->fields[$alias] = $field_info;
847     }
848
849     // Keep track of all aliases used.
850     $this->fieldAliases[$table][$field] = $alias;
851
852     return $alias;
853   }
854
855   /**
856    * Remove all fields that may've been added; primarily used for summary
857    * mode where we're changing the query because we didn't get data we needed.
858    */
859   public function clearFields() {
860     $this->fields = [];
861   }
862
863   /**
864    * Add a simple WHERE clause to the query. The caller is responsible for
865    * ensuring that all fields are fully qualified (TABLE.FIELD) and that
866    * the table already exists in the query.
867    *
868    * The $field, $value and $operator arguments can also be passed in with a
869    * single DatabaseCondition object, like this:
870    * @code
871    * $this->query->addWhere(
872    *   $this->options['group'],
873    *   (new Condition('OR'))
874    *     ->condition($field, $value, 'NOT IN')
875    *     ->condition($field, $value, 'IS NULL')
876    * );
877    * @endcode
878    *
879    * @param $group
880    *   The WHERE group to add these to; groups are used to create AND/OR
881    *   sections. Groups cannot be nested. Use 0 as the default group.
882    *   If the group does not yet exist it will be created as an AND group.
883    * @param $field
884    *   The name of the field to check.
885    * @param $value
886    *   The value to test the field against. In most cases, this is a scalar. For more
887    *   complex options, it is an array. The meaning of each element in the array is
888    *   dependent on the $operator.
889    * @param $operator
890    *   The comparison operator, such as =, <, or >=. It also accepts more
891    *   complex options such as IN, LIKE, LIKE BINARY, or BETWEEN. Defaults to =.
892    *   If $field is a string you have to use 'formula' here.
893    *
894    * @see \Drupal\Core\Database\Query\ConditionInterface::condition()
895    * @see \Drupal\Core\Database\Query\Condition
896    */
897   public function addWhere($group, $field, $value = NULL, $operator = NULL) {
898     // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all
899     // the default group.
900     if (empty($group)) {
901       $group = 0;
902     }
903
904     // Check for a group.
905     if (!isset($this->where[$group])) {
906       $this->setWhereGroup('AND', $group);
907     }
908
909     $this->where[$group]['conditions'][] = [
910       'field' => $field,
911       'value' => $value,
912       'operator' => $operator,
913     ];
914   }
915
916   /**
917    * Add a complex WHERE clause to the query.
918    *
919    * The caller is responsible for ensuring that all fields are fully qualified
920    * (TABLE.FIELD) and that the table already exists in the query.
921    * Internally the dbtng method "where" is used.
922    *
923    * @param $group
924    *   The WHERE group to add these to; groups are used to create AND/OR
925    *   sections. Groups cannot be nested. Use 0 as the default group.
926    *   If the group does not yet exist it will be created as an AND group.
927    * @param $snippet
928    *   The snippet to check. This can be either a column or
929    *   a complex expression like "UPPER(table.field) = 'value'"
930    * @param $args
931    *   An associative array of arguments.
932    *
933    * @see QueryConditionInterface::where()
934    */
935   public function addWhereExpression($group, $snippet, $args = []) {
936     // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all
937     // the default group.
938     if (empty($group)) {
939       $group = 0;
940     }
941
942     // Check for a group.
943     if (!isset($this->where[$group])) {
944       $this->setWhereGroup('AND', $group);
945     }
946
947     $this->where[$group]['conditions'][] = [
948       'field' => $snippet,
949       'value' => $args,
950       'operator' => 'formula',
951     ];
952   }
953
954   /**
955    * Add a complex HAVING clause to the query.
956    * The caller is responsible for ensuring that all fields are fully qualified
957    * (TABLE.FIELD) and that the table and an appropriate GROUP BY already exist in the query.
958    * Internally the dbtng method "having" is used.
959    *
960    * @param $group
961    *   The HAVING group to add these to; groups are used to create AND/OR
962    *   sections. Groups cannot be nested. Use 0 as the default group.
963    *   If the group does not yet exist it will be created as an AND group.
964    * @param $snippet
965    *   The snippet to check. This can be either a column or
966    *   a complex expression like "COUNT(table.field) > 3"
967    * @param $args
968    *   An associative array of arguments.
969    *
970    * @see QueryConditionInterface::having()
971    */
972   public function addHavingExpression($group, $snippet, $args = []) {
973     // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all
974     // the default group.
975     if (empty($group)) {
976       $group = 0;
977     }
978
979     // Check for a group.
980     if (!isset($this->having[$group])) {
981       $this->setWhereGroup('AND', $group, 'having');
982     }
983
984     // Add the clause and the args.
985     $this->having[$group]['conditions'][] = [
986       'field' => $snippet,
987       'value' => $args,
988       'operator' => 'formula',
989     ];
990   }
991
992   /**
993    * Add an ORDER BY clause to the query.
994    *
995    * @param $table
996    *   The table this field is part of. If a formula, enter NULL.
997    *   If you want to orderby random use "rand" as table and nothing else.
998    * @param $field
999    *   The field or formula to sort on. If already a field, enter NULL
1000    *   and put in the alias.
1001    * @param $order
1002    *   Either ASC or DESC.
1003    * @param $alias
1004    *   The alias to add the field as. In SQL, all fields in the order by
1005    *   must also be in the SELECT portion. If an $alias isn't specified
1006    *   one will be generated for from the $field; however, if the
1007    *   $field is a formula, this alias will likely fail.
1008    * @param $params
1009    *   Any params that should be passed through to the addField.
1010    */
1011   public function addOrderBy($table, $field = NULL, $order = 'ASC', $alias = '', $params = []) {
1012     // Only ensure the table if it's not the special random key.
1013     // @todo: Maybe it would make sense to just add an addOrderByRand or something similar.
1014     if ($table && $table != 'rand') {
1015       $this->ensureTable($table);
1016     }
1017
1018     // Only fill out this aliasing if there is a table;
1019     // otherwise we assume it is a formula.
1020     if (!$alias && $table) {
1021       $as = $table . '_' . $field;
1022     }
1023     else {
1024       $as = $alias;
1025     }
1026
1027     if ($field) {
1028       $as = $this->addField($table, $field, $as, $params);
1029     }
1030
1031     $this->orderby[] = [
1032       'field' => $as,
1033       'direction' => strtoupper($order)
1034     ];
1035   }
1036
1037   /**
1038    * Add a simple GROUP BY clause to the query. The caller is responsible
1039    * for ensuring that the fields are fully qualified and the table is properly
1040    * added.
1041    */
1042   public function addGroupBy($clause) {
1043     // Only add it if it's not already in there.
1044     if (!in_array($clause, $this->groupby)) {
1045       $this->groupby[] = $clause;
1046     }
1047   }
1048
1049   /**
1050    * Returns the alias for the given field added to $table.
1051    *
1052    * @access protected
1053    *
1054    * @see \Drupal\views\Plugin\views\query\Sql::addField
1055    */
1056   protected function getFieldAlias($table_alias, $field) {
1057     return isset($this->fieldAliases[$table_alias][$field]) ? $this->fieldAliases[$table_alias][$field] : FALSE;
1058   }
1059
1060   /**
1061    * Adds a query tag to the sql object.
1062    *
1063    * @see SelectQuery::addTag()
1064    */
1065   public function addTag($tag) {
1066     $this->tags[] = $tag;
1067   }
1068
1069   /**
1070    * Generates a unique placeholder used in the db query.
1071    */
1072   public function placeholder($base = 'views') {
1073     static $placeholders = [];
1074     if (!isset($placeholders[$base])) {
1075       $placeholders[$base] = 0;
1076       return ':' . $base;
1077     }
1078     else {
1079       return ':' . $base . ++$placeholders[$base];
1080     }
1081   }
1082
1083   /**
1084    * Construct the "WHERE" or "HAVING" part of the query.
1085    *
1086    * As views has to wrap the conditions from arguments with AND, a special
1087    * group is wrapped around all conditions. This special group has the ID 0.
1088    * There is other code in filters which makes sure that the group IDs are
1089    * higher than zero.
1090    *
1091    * @param $where
1092    *   'where' or 'having'.
1093    */
1094   protected function buildCondition($where = 'where') {
1095     $has_condition = FALSE;
1096     $has_arguments = FALSE;
1097     $has_filter = FALSE;
1098
1099     $main_group = new Condition('AND');
1100     $filter_group = $this->groupOperator == 'OR' ? new Condition('OR') : new Condition('AND');
1101
1102     foreach ($this->$where as $group => $info) {
1103
1104       if (!empty($info['conditions'])) {
1105         $sub_group = $info['type'] == 'OR' ? new Condition('OR') : new Condition('AND');
1106         foreach ($info['conditions'] as $clause) {
1107           if ($clause['operator'] == 'formula') {
1108             $has_condition = TRUE;
1109             $sub_group->where($clause['field'], $clause['value']);
1110           }
1111           else {
1112             $has_condition = TRUE;
1113             $sub_group->condition($clause['field'], $clause['value'], $clause['operator']);
1114           }
1115         }
1116
1117         // Add the item to the filter group.
1118         if ($group != 0) {
1119           $has_filter = TRUE;
1120           $filter_group->condition($sub_group);
1121         }
1122         else {
1123           $has_arguments = TRUE;
1124           $main_group->condition($sub_group);
1125         }
1126       }
1127     }
1128
1129     if ($has_filter) {
1130       $main_group->condition($filter_group);
1131     }
1132
1133     if (!$has_arguments && $has_condition) {
1134       return $filter_group;
1135     }
1136     if ($has_arguments && $has_condition) {
1137       return $main_group;
1138     }
1139   }
1140
1141   /**
1142    * Returns a list of non-aggregates to be added to the "group by" clause.
1143    *
1144    * Non-aggregates are fields that have no aggregation function (count, sum,
1145    * etc) applied. Since the SQL standard requires all fields to either have
1146    * an aggregation function applied, or to be in the GROUP BY clause, Views
1147    * gathers those fields and adds them to the GROUP BY clause.
1148    *
1149    * @return array
1150    *   An array of the fieldnames which are non-aggregates.
1151    */
1152   protected function getNonAggregates() {
1153     $non_aggregates = [];
1154     foreach ($this->fields as $field) {
1155       $string = '';
1156       if (!empty($field['table'])) {
1157         $string .= $field['table'] . '.';
1158       }
1159       $string .= $field['field'];
1160       $fieldname = (!empty($field['alias']) ? $field['alias'] : $string);
1161
1162       if (!empty($field['count'])) {
1163         // Retained for compatibility.
1164         $field['function'] = 'count';
1165       }
1166
1167       if (!empty($field['function'])) {
1168         $this->hasAggregate = TRUE;
1169       }
1170       // This is a formula, using no tables.
1171       elseif (empty($field['table'])) {
1172         $non_aggregates[] = $fieldname;
1173       }
1174       elseif (empty($field['aggregate'])) {
1175         $non_aggregates[] = $fieldname;
1176       }
1177
1178       if ($this->getCountOptimized) {
1179         // We only want the first field in this case.
1180         break;
1181       }
1182     }
1183
1184     return $non_aggregates;
1185   }
1186
1187   /**
1188    * Adds fields to the query.
1189    *
1190    * @param \Drupal\Core\Database\Query\SelectInterface $query
1191    *   The drupal query object.
1192    */
1193   protected function compileFields($query) {
1194     foreach ($this->fields as $field) {
1195       $string = '';
1196       if (!empty($field['table'])) {
1197         $string .= $field['table'] . '.';
1198       }
1199       $string .= $field['field'];
1200       $fieldname = (!empty($field['alias']) ? $field['alias'] : $string);
1201
1202       if (!empty($field['count'])) {
1203         // Retained for compatibility.
1204         $field['function'] = 'count';
1205       }
1206
1207       if (!empty($field['function'])) {
1208         $info = $this->getAggregationInfo();
1209         if (!empty($info[$field['function']]['method']) && is_callable([$this, $info[$field['function']]['method']])) {
1210           $string = $this::{$info[$field['function']]['method']}($field['function'], $string);
1211           $placeholders = !empty($field['placeholders']) ? $field['placeholders'] : [];
1212           $query->addExpression($string, $fieldname, $placeholders);
1213         }
1214
1215         $this->hasAggregate = TRUE;
1216       }
1217       // This is a formula, using no tables.
1218       elseif (empty($field['table'])) {
1219         $placeholders = !empty($field['placeholders']) ? $field['placeholders'] : [];
1220         $query->addExpression($string, $fieldname, $placeholders);
1221       }
1222       elseif ($this->distinct && !in_array($fieldname, $this->groupby)) {
1223         $query->addField(!empty($field['table']) ? $field['table'] : $this->view->storage->get('base_table'), $field['field'], $fieldname);
1224       }
1225       elseif (empty($field['aggregate'])) {
1226         $query->addField(!empty($field['table']) ? $field['table'] : $this->view->storage->get('base_table'), $field['field'], $fieldname);
1227       }
1228
1229       if ($this->getCountOptimized) {
1230         // We only want the first field in this case.
1231         break;
1232       }
1233     }
1234   }
1235
1236   /**
1237    * Generate a query and a countquery from all of the information supplied
1238    * to the object.
1239    *
1240    * @param $get_count
1241    *   Provide a countquery if this is true, otherwise provide a normal query.
1242    */
1243   public function query($get_count = FALSE) {
1244     // Check query distinct value.
1245     if (empty($this->noDistinct) && $this->distinct && !empty($this->fields)) {
1246       $base_field_alias = $this->addField($this->view->storage->get('base_table'), $this->view->storage->get('base_field'));
1247       $this->addGroupBy($base_field_alias);
1248       $distinct = TRUE;
1249     }
1250
1251     /**
1252      * An optimized count query includes just the base field instead of all the fields.
1253      * Determine of this query qualifies by checking for a groupby or distinct.
1254      */
1255     if ($get_count && !$this->groupby) {
1256       foreach ($this->fields as $field) {
1257         if (!empty($field['distinct']) || !empty($field['function'])) {
1258           $this->getCountOptimized = FALSE;
1259           break;
1260         }
1261       }
1262     }
1263     else {
1264       $this->getCountOptimized = FALSE;
1265     }
1266     if (!isset($this->getCountOptimized)) {
1267       $this->getCountOptimized = TRUE;
1268     }
1269
1270     $options = [];
1271     $target = 'default';
1272     $key = 'default';
1273     // Detect an external database and set the
1274     if (isset($this->view->base_database)) {
1275       $key = $this->view->base_database;
1276     }
1277
1278     // Set the replica target if the replica option is set
1279     if (!empty($this->options['replica'])) {
1280       $target = 'replica';
1281     }
1282
1283     // Go ahead and build the query.
1284     // db_select doesn't support to specify the key, so use getConnection directly.
1285     $query = Database::getConnection($target, $key)
1286       ->select($this->view->storage->get('base_table'), $this->view->storage->get('base_table'), $options)
1287       ->addTag('views')
1288       ->addTag('views_' . $this->view->storage->id());
1289
1290     // Add the tags added to the view itself.
1291     foreach ($this->tags as $tag) {
1292       $query->addTag($tag);
1293     }
1294
1295     if (!empty($distinct)) {
1296       $query->distinct();
1297     }
1298
1299     // Add all the tables to the query via joins. We assume all LEFT joins.
1300     foreach ($this->tableQueue as $table) {
1301       if (is_object($table['join'])) {
1302         $table['join']->buildJoin($query, $table, $this);
1303       }
1304     }
1305
1306     // Assemble the groupby clause, if any.
1307     $this->hasAggregate = FALSE;
1308     $non_aggregates = $this->getNonAggregates();
1309     if (count($this->having)) {
1310       $this->hasAggregate = TRUE;
1311     }
1312     elseif (!$this->hasAggregate) {
1313       // Allow 'GROUP BY' even no aggregation function has been set.
1314       $this->hasAggregate = $this->view->display_handler->getOption('group_by');
1315     }
1316     $groupby = [];
1317     if ($this->hasAggregate && (!empty($this->groupby) || !empty($non_aggregates))) {
1318       $groupby = array_unique(array_merge($this->groupby, $non_aggregates));
1319     }
1320
1321     // Make sure each entity table has the base field added so that the
1322     // entities can be loaded.
1323     $entity_information = $this->getEntityTableInfo();
1324     if ($entity_information) {
1325       $params = [];
1326       if ($groupby) {
1327         // Handle grouping, by retrieving the minimum entity_id.
1328         $params = [
1329           'function' => 'min',
1330         ];
1331       }
1332
1333       foreach ($entity_information as $entity_type_id => $info) {
1334         $entity_type = \Drupal::entityManager()->getDefinition($info['entity_type']);
1335         $base_field = !$info['revision'] ? $entity_type->getKey('id') : $entity_type->getKey('revision');
1336         $this->addField($info['alias'], $base_field, '', $params);
1337       }
1338     }
1339
1340     // Add all fields to the query.
1341     $this->compileFields($query);
1342
1343     // Add groupby.
1344     if ($groupby) {
1345       foreach ($groupby as $field) {
1346         // Handle group by of field without table alias to avoid ambiguous
1347         // column error.
1348         if ($field == $this->view->storage->get('base_field')) {
1349           $field = $this->view->storage->get('base_table') . '.' . $field;
1350         }
1351         $query->groupBy($field);
1352       }
1353       if (!empty($this->having) && $condition = $this->buildCondition('having')) {
1354         $query->havingCondition($condition);
1355       }
1356     }
1357
1358     if (!$this->getCountOptimized) {
1359       // we only add the orderby if we're not counting.
1360       if ($this->orderby) {
1361         foreach ($this->orderby as $order) {
1362           if ($order['field'] == 'rand_') {
1363             $query->orderRandom();
1364           }
1365           else {
1366             $query->orderBy($order['field'], $order['direction']);
1367           }
1368         }
1369       }
1370     }
1371
1372     if (!empty($this->where) && $condition = $this->buildCondition('where')) {
1373       $query->condition($condition);
1374     }
1375
1376     // Add a query comment.
1377     if (!empty($this->options['query_comment'])) {
1378       $query->comment($this->options['query_comment']);
1379     }
1380
1381     // Add the query tags.
1382     if (!empty($this->options['query_tags'])) {
1383       foreach ($this->options['query_tags'] as $tag) {
1384         $query->addTag($tag);
1385       }
1386     }
1387
1388     // Add all query substitutions as metadata.
1389     $query->addMetaData('views_substitutions', \Drupal::moduleHandler()->invokeAll('views_query_substitutions', [$this->view]));
1390
1391     return $query;
1392   }
1393
1394   /**
1395    * Get the arguments attached to the WHERE and HAVING clauses of this query.
1396    */
1397   public function getWhereArgs() {
1398     $args = [];
1399     foreach ($this->where as $where) {
1400       $args = array_merge($args, $where['args']);
1401     }
1402     foreach ($this->having as $having) {
1403       $args = array_merge($args, $having['args']);
1404     }
1405     return $args;
1406   }
1407
1408   /**
1409    * Let modules modify the query just prior to finalizing it.
1410    */
1411   public function alter(ViewExecutable $view) {
1412     \Drupal::moduleHandler()->invokeAll('views_query_alter', [$view, $this]);
1413   }
1414
1415   /**
1416    * Builds the necessary info to execute the query.
1417    */
1418   public function build(ViewExecutable $view) {
1419     // Make the query distinct if the option was set.
1420     if (!empty($this->options['distinct'])) {
1421       $this->setDistinct(TRUE);
1422     }
1423
1424     // Store the view in the object to be able to use it later.
1425     $this->view = $view;
1426
1427     $view->initPager();
1428
1429     // Let the pager modify the query to add limits.
1430     $view->pager->query();
1431
1432     $view->build_info['query'] = $this->query();
1433     $view->build_info['count_query'] = $this->query(TRUE);
1434   }
1435
1436   /**
1437    * Executes the query and fills the associated view object with according
1438    * values.
1439    *
1440    * Values to set: $view->result, $view->total_rows, $view->execute_time,
1441    * $view->current_page.
1442    */
1443   public function execute(ViewExecutable $view) {
1444     $query = $view->build_info['query'];
1445     $count_query = $view->build_info['count_query'];
1446
1447     $query->addMetaData('view', $view);
1448     $count_query->addMetaData('view', $view);
1449
1450     if (empty($this->options['disable_sql_rewrite'])) {
1451       $base_table_data = Views::viewsData()->get($this->view->storage->get('base_table'));
1452       if (isset($base_table_data['table']['base']['access query tag'])) {
1453         $access_tag = $base_table_data['table']['base']['access query tag'];
1454         $query->addTag($access_tag);
1455         $count_query->addTag($access_tag);
1456       }
1457
1458       if (isset($base_table_data['table']['base']['query metadata'])) {
1459         foreach ($base_table_data['table']['base']['query metadata'] as $key => $value) {
1460           $query->addMetaData($key, $value);
1461           $count_query->addMetaData($key, $value);
1462         }
1463       }
1464     }
1465
1466     if ($query) {
1467       $additional_arguments = \Drupal::moduleHandler()->invokeAll('views_query_substitutions', [$view]);
1468
1469       // Count queries must be run through the preExecute() method.
1470       // If not, then hook_query_node_access_alter() may munge the count by
1471       // adding a distinct against an empty query string
1472       // (e.g. COUNT DISTINCT(1) ...) and no pager will return.
1473       // See pager.inc > PagerDefault::execute()
1474       // http://api.drupal.org/api/drupal/includes--pager.inc/function/PagerDefault::execute/7
1475       // See https://www.drupal.org/node/1046170.
1476       $count_query->preExecute();
1477
1478       // Build the count query.
1479       $count_query = $count_query->countQuery();
1480
1481       // Add additional arguments as a fake condition.
1482       // XXX: this doesn't work, because PDO mandates that all bound arguments
1483       // are used on the query. TODO: Find a better way to do this.
1484       if (!empty($additional_arguments)) {
1485         // $query->where('1 = 1', $additional_arguments);
1486         // $count_query->where('1 = 1', $additional_arguments);
1487       }
1488
1489       $start = microtime(TRUE);
1490
1491       try {
1492         if ($view->pager->useCountQuery() || !empty($view->get_total_rows)) {
1493           $view->pager->executeCountQuery($count_query);
1494         }
1495
1496         // Let the pager modify the query to add limits.
1497         $view->pager->preExecute($query);
1498
1499         if (!empty($this->limit) || !empty($this->offset)) {
1500           // We can't have an offset without a limit, so provide a very large limit instead.
1501           $limit  = intval(!empty($this->limit) ? $this->limit : 999999);
1502           $offset = intval(!empty($this->offset) ? $this->offset : 0);
1503           $query->range($offset, $limit);
1504         }
1505
1506         $result = $query->execute();
1507         $result->setFetchMode(\PDO::FETCH_CLASS, 'Drupal\views\ResultRow');
1508
1509         // Setup the result row objects.
1510         $view->result = iterator_to_array($result);
1511         array_walk($view->result, function (ResultRow $row, $index) {
1512           $row->index = $index;
1513         });
1514
1515         $view->pager->postExecute($view->result);
1516         $view->pager->updatePageInfo();
1517         $view->total_rows = $view->pager->getTotalItems();
1518
1519         // Load all entities contained in the results.
1520         $this->loadEntities($view->result);
1521       }
1522       catch (DatabaseExceptionWrapper $e) {
1523         $view->result = [];
1524         if (!empty($view->live_preview)) {
1525           drupal_set_message($e->getMessage(), 'error');
1526         }
1527         else {
1528           throw new DatabaseExceptionWrapper("Exception in {$view->storage->label()}[{$view->storage->id()}]: {$e->getMessage()}");
1529         }
1530       }
1531
1532     }
1533     else {
1534       $start = microtime(TRUE);
1535     }
1536     $view->execute_time = microtime(TRUE) - $start;
1537   }
1538
1539   /**
1540    * Loads all entities contained in the passed-in $results.
1541    *.
1542    * If the entity belongs to the base table, then it gets stored in
1543    * $result->_entity. Otherwise, it gets stored in
1544    * $result->_relationship_entities[$relationship_id];
1545    *
1546    * @param \Drupal\views\ResultRow[] $results
1547    *   The result of the SQL query.
1548    */
1549   public function loadEntities(&$results) {
1550     $entity_information = $this->getEntityTableInfo();
1551     // No entity tables found, nothing else to do here.
1552     if (empty($entity_information)) {
1553       return;
1554     }
1555
1556     // Extract all entity types from entity_information.
1557     $entity_types = [];
1558     foreach ($entity_information as $info) {
1559       $entity_type = $info['entity_type'];
1560       if (!isset($entity_types[$entity_type])) {
1561         $entity_types[$entity_type] = $this->entityTypeManager->getDefinition($entity_type);
1562       }
1563     }
1564
1565     // Assemble a list of entities to load.
1566     $entity_ids_by_type = [];
1567     $revision_ids_by_type = [];
1568     foreach ($entity_information as $info) {
1569       $relationship_id = $info['relationship_id'];
1570       $entity_type = $info['entity_type'];
1571       /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_info */
1572       $entity_info = $entity_types[$entity_type];
1573       $revision = $info['revision'];
1574       $id_key = !$revision ? $entity_info->getKey('id') : $entity_info->getKey('revision');
1575       $id_alias = $this->getFieldAlias($info['alias'], $id_key);
1576
1577       foreach ($results as $index => $result) {
1578         // Store the entity id if it was found.
1579         if (isset($result->{$id_alias}) && $result->{$id_alias} != '') {
1580           if ($revision) {
1581             $revision_ids_by_type[$entity_type][$index][$relationship_id] = $result->$id_alias;
1582           }
1583           else {
1584             $entity_ids_by_type[$entity_type][$index][$relationship_id] = $result->$id_alias;
1585           }
1586         }
1587       }
1588     }
1589
1590     // Load all entities and assign them to the correct result row.
1591     foreach ($entity_ids_by_type as $entity_type => $ids) {
1592       $entity_storage = $this->entityTypeManager->getStorage($entity_type);
1593       $flat_ids = iterator_to_array(new \RecursiveIteratorIterator(new \RecursiveArrayIterator($ids)), FALSE);
1594
1595       $entities = $entity_storage->loadMultiple(array_unique($flat_ids));
1596       $results = $this->assignEntitiesToResult($ids, $entities, $results);
1597     }
1598
1599     // Now load all revisions.
1600     foreach ($revision_ids_by_type as $entity_type => $revision_ids) {
1601       $entity_storage = $this->entityTypeManager->getStorage($entity_type);
1602       $entities = [];
1603
1604       foreach ($revision_ids as $index => $revision_id_by_relationship) {
1605         foreach ($revision_id_by_relationship as $revision => $revision_id) {
1606           // Drupal core currently has no way to load multiple revisions.
1607           $entity = $entity_storage->loadRevision($revision_id);
1608           $entities[$revision_id] = $entity;
1609         }
1610       }
1611
1612       $results = $this->assignEntitiesToResult($revision_ids, $entities, $results);
1613     }
1614   }
1615
1616   /**
1617    * Sets entities onto the view result row objects.
1618    *
1619    * This method takes into account the relationship in which the entity was
1620    * needed in the first place.
1621    *
1622    * @param mixed[][] $ids
1623    *   A two dimensional array of identifiers (entity ID / revision ID) keyed by
1624    *   relationship.
1625    * @param \Drupal\Core\Entity\EntityInterface[] $entities
1626    *   An array of entities keyed by their identified (entity ID / revision ID).
1627    * @param \Drupal\views\ResultRow[] $results
1628    *   The entire views result.
1629    *
1630    * @return \Drupal\views\ResultRow[]
1631    *   The changed views results.
1632    */
1633   protected function assignEntitiesToResult($ids, array $entities, array $results) {
1634     foreach ($ids as $index => $relationships) {
1635       foreach ($relationships as $relationship_id => $id) {
1636         if (isset($entities[$id])) {
1637           $entity = $entities[$id];
1638         }
1639         else {
1640           $entity = NULL;
1641         }
1642
1643         if ($relationship_id == 'none') {
1644           $results[$index]->_entity = $entity;
1645         }
1646         else {
1647           $results[$index]->_relationship_entities[$relationship_id] = $entity;
1648         }
1649       }
1650     }
1651     return $results;
1652   }
1653
1654   /**
1655    * {@inheritdoc}
1656    */
1657   public function getCacheTags() {
1658     $tags = [];
1659     // Add cache tags for each row, if there is an entity associated with it.
1660     if (!$this->hasAggregate) {
1661       foreach ($this->getAllEntities() as $entity) {
1662         $tags = Cache::mergeTags($entity->getCacheTags(), $tags);
1663       }
1664     }
1665
1666     return $tags;
1667   }
1668
1669   /**
1670    * {@inheritdoc}
1671    */
1672   public function getCacheMaxAge() {
1673     $max_age = parent::getCacheMaxAge();
1674     foreach ($this->getAllEntities() as $entity) {
1675       $max_age = Cache::mergeMaxAges($max_age, $entity->getCacheMaxAge());
1676     }
1677
1678     return $max_age;
1679   }
1680
1681   /**
1682    * Gets all the involved entities of the view.
1683    *
1684    * @return \Drupal\Core\Entity\EntityInterface[]
1685    */
1686   protected function getAllEntities() {
1687     $entities = [];
1688     foreach ($this->view->result as $row) {
1689       if ($row->_entity) {
1690         $entities[] = $row->_entity;
1691       }
1692       foreach ($row->_relationship_entities as $entity) {
1693         $entities[] = $entity;
1694       }
1695     }
1696
1697     return $entities;
1698   }
1699
1700   public function addSignature(ViewExecutable $view) {
1701     $view->query->addField(NULL, "'" . $view->storage->id() . ':' . $view->current_display . "'", 'view_name');
1702   }
1703
1704   public function getAggregationInfo() {
1705     // @todo -- need a way to get database specific and customized aggregation
1706     // functions into here.
1707     return [
1708       'group' => [
1709         'title' => $this->t('Group results together'),
1710         'is aggregate' => FALSE,
1711       ],
1712       'count' => [
1713         'title' => $this->t('Count'),
1714         'method' => 'aggregationMethodSimple',
1715         'handler' => [
1716           'argument' => 'groupby_numeric',
1717           'field' => 'numeric',
1718           'filter' => 'groupby_numeric',
1719           'sort' => 'groupby_numeric',
1720         ],
1721       ],
1722       'count_distinct' => [
1723         'title' => $this->t('Count DISTINCT'),
1724         'method' => 'aggregationMethodDistinct',
1725         'handler' => [
1726           'argument' => 'groupby_numeric',
1727           'field' => 'numeric',
1728           'filter' => 'groupby_numeric',
1729           'sort' => 'groupby_numeric',
1730         ],
1731       ],
1732       'sum' => [
1733         'title' => $this->t('Sum'),
1734         'method' => 'aggregationMethodSimple',
1735         'handler' => [
1736           'argument' => 'groupby_numeric',
1737           'field' => 'numeric',
1738           'filter' => 'groupby_numeric',
1739           'sort' => 'groupby_numeric',
1740         ],
1741       ],
1742       'avg' => [
1743         'title' => $this->t('Average'),
1744         'method' => 'aggregationMethodSimple',
1745         'handler' => [
1746           'argument' => 'groupby_numeric',
1747           'field' => 'numeric',
1748           'filter' => 'groupby_numeric',
1749           'sort' => 'groupby_numeric',
1750         ],
1751       ],
1752       'min' => [
1753         'title' => $this->t('Minimum'),
1754         'method' => 'aggregationMethodSimple',
1755         'handler' => [
1756           'argument' => 'groupby_numeric',
1757           'field' => 'numeric',
1758           'filter' => 'groupby_numeric',
1759           'sort' => 'groupby_numeric',
1760         ],
1761       ],
1762       'max' => [
1763         'title' => $this->t('Maximum'),
1764         'method' => 'aggregationMethodSimple',
1765         'handler' => [
1766           'argument' => 'groupby_numeric',
1767           'field' => 'numeric',
1768           'filter' => 'groupby_numeric',
1769           'sort' => 'groupby_numeric',
1770         ],
1771       ],
1772       'stddev_pop' => [
1773         'title' => $this->t('Standard deviation'),
1774         'method' => 'aggregationMethodSimple',
1775         'handler' => [
1776           'argument' => 'groupby_numeric',
1777           'field' => 'numeric',
1778           'filter' => 'groupby_numeric',
1779           'sort' => 'groupby_numeric',
1780         ],
1781       ]
1782     ];
1783   }
1784
1785   public function aggregationMethodSimple($group_type, $field) {
1786     return strtoupper($group_type) . '(' . $field . ')';
1787   }
1788
1789   public function aggregationMethodDistinct($group_type, $field) {
1790     $group_type = str_replace('_distinct', '', $group_type);
1791     return strtoupper($group_type) . '(DISTINCT ' . $field . ')';
1792   }
1793
1794   /**
1795    * {@inheritdoc}
1796    */
1797   public function getDateField($field, $string_date = FALSE, $calculate_offset = TRUE) {
1798     $field = $this->dateSql->getDateField($field, $string_date);
1799     if ($calculate_offset && $offset = $this->getTimezoneOffset()) {
1800       $this->setFieldTimezoneOffset($field, $offset);
1801     }
1802     return $field;
1803   }
1804
1805   /**
1806    * {@inheritdoc}
1807    */
1808   public function setFieldTimezoneOffset(&$field, $offset) {
1809     $this->dateSql->setFieldTimezoneOffset($field, $offset);
1810   }
1811
1812   /**
1813    * {@inheritdoc}
1814    */
1815   public function setupTimezone() {
1816     // Set the database timezone offset.
1817     static $already_set = FALSE;
1818     if (!$already_set) {
1819       $this->dateSql->setTimezoneOffset('+00:00');
1820       $already_set = TRUE;
1821     }
1822
1823     return parent::setupTimezone();
1824   }
1825
1826   /**
1827    * {@inheritdoc}
1828    */
1829   public function getDateFormat($field, $format, $string_date = FALSE) {
1830     return $this->dateSql->getDateFormat($field, $format);
1831   }
1832
1833 }