3 namespace Drupal\views\Plugin\views\query;
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;
21 * Views query plugin for an SQL query.
23 * @ingroup views_query_plugins
27 * title = @Translation("SQL Query"),
28 * help = @Translation("Query will be generated and run using the Drupal database API.")
31 class Sql extends QueryPluginBase {
34 * A list of tables in the order they should be added, keyed by alias.
36 protected $tableQueue = [];
39 * Holds an array of tables and counts added so that we can create aliases
44 * Holds an array of relationships, which are aliases of the primary
45 * table that represent different ways to join the same table in.
47 public $relationships = [];
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
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
62 * The default operator to use when connecting the WHERE groups. May be
67 protected $groupOperator = 'AND';
70 * A simple array of order by clauses.
75 * A simple array of group by clauses.
86 * A flag as to whether or not to make the primary field distinct.
90 public $distinct = FALSE;
95 protected $hasAggregate = FALSE;
98 * Should this query be optimized for counts, for example no sorts.
100 protected $getCountOptimized = NULL;
103 * An array mapping table aliases and field names to field aliases.
105 protected $fieldAliases = [];
108 * Query tags which will be passed over to the dbtng query object.
113 * Is the view marked as not distinct.
117 protected $noDistinct;
120 * The entity type manager.
122 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
124 protected $entityTypeManager;
127 * The database-specific date handler.
129 * @var \Drupal\views\Plugin\views\query\DateSqlInterface
134 * Constructs a Sql object.
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.
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);
150 $this->entityTypeManager = $entity_type_manager;
151 $this->dateSql = $date_sql;
154 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
159 $container->get('entity_type.manager'),
160 $container->get('views.date_sql')
167 public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
168 parent::init($view, $display, $options);
170 $base_table = $this->view->storage->get('base_table');
171 $base_field = $this->view->storage->get('base_field');
172 $this->relationships[$base_table] = [
174 'table' => $base_table,
175 'alias' => $base_table,
176 'base' => $base_table
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,
187 // init the tables with our primary table
188 $this->tables[$base_table][$base_table] = [
190 'alias' => $base_table,
193 $this->count_field = [
194 'table' => $base_table,
195 'field' => $base_field,
196 'alias' => $base_field,
202 * Returns a reference to the table queue array for this query.
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.
208 * Note that if you want to manipulate the table queue array, this method must
209 * be called by reference as well:
212 * $tables =& $query->getTableQueue();
216 * A reference to the table queue array structure.
218 public function &getTableQueue() {
219 return $this->tableQueue;
223 * Set the view to be distinct (per base field).
226 * Should the view be distincted.
228 protected function setDistinct($value = TRUE) {
229 if (!(isset($this->noDistinct) && $value)) {
230 $this->distinct = $value;
235 * Set what field the query will count() on for paging.
237 public function setCountField($table, $field, $alias = NULL) {
239 $alias = $table . '_' . $field;
241 $this->count_field = [
249 protected function defineOptions() {
250 $options = parent::defineOptions();
251 $options['disable_sql_rewrite'] = [
254 $options['distinct'] = [
257 $options['replica'] = [
260 $options['query_comment'] = [
263 $options['query_tags'] = [
271 * Add settings for the ui.
273 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
274 parent::buildOptionsForm($form, $form_state);
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>',
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']),
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']),
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'],
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'],
311 * Special submit handling.
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);
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
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.
334 * What this relationship will be called, and is also the alias
336 * @param \Drupal\views\Plugin\views\join\JoinPluginBase $join
337 * A Join object (or derived object) to join the alias in.
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.
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.
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');
352 elseif (!array_key_exists($link_point, $this->relationships)) {
356 // Make sure $alias isn't already used; if it, start adding stuff.
357 $alias_base = $alias;
359 while (!empty($this->relationships[$alias])) {
360 $alias = $alias_base . '_' . $count++;
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);
368 // Add the table directly to the queue to avoid accidentally marking
370 $this->tableQueue[$alias] = [
371 'table' => $join->table,
375 'relationship' => $link_point,
378 $this->relationships[$alias] = [
379 'link' => $link_point,
380 'table' => $join->table,
384 $this->tables[$this->view->storage->get('base_table')][$alias] = [
393 * Add a table to the query, ensuring the path exists.
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.
400 * The name of the table to add. It needs to exist in the global table
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.
414 * A specific alias to use, rather than the default alias.
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
422 public function addTable($table, $relationship = NULL, JoinPluginBase $join = NULL, $alias = NULL) {
423 if (!$this->ensurePath($table, $relationship, $join)) {
427 if ($join && $relationship) {
428 $join = $this->adjustJoin($join, $relationship);
431 return $this->queueTable($table, $relationship, $join, $alias);
435 * Add a table to the query without ensuring the path.
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.
442 * The name of the table to add. It needs to exist in the global table
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.
453 * A specific alias to use, rather than the default alias.
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
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])) {
467 if (empty($relationship)) {
468 $relationship = $this->view->storage->get('base_table');
471 if (!array_key_exists($relationship, $this->relationships)) {
475 if (!$alias && $join && $relationship && !empty($join->adjusted) && $table != $join->table) {
476 if ($relationship == $this->view->storage->get('base_table')) {
480 $alias = $relationship . '_' . $table;
484 // Check this again to make sure we don't blow up existing aliases for already
486 if (isset($this->tableQueue[$alias])) {
490 $alias = $this->markTable($table, $relationship, $alias);
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'];
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);
503 // If no join is specified, pull it from the table data.
505 $join = $this->getJoinData($table, $this->relationships[$relationship]['base']);
510 $join = $this->adjustJoin($join, $relationship);
513 $this->tableQueue[$alias] = [
515 'num' => $this->tables[$relationship][$table]['count'],
518 'relationship' => $relationship,
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)) {
529 if ($relationship != $this->view->storage->get('base_table')) {
530 // double underscore will help prevent accidental name
532 $alias = $relationship . '__';
536 $this->tables[$relationship][$table] = [
542 $this->tables[$relationship][$table]['count']++;
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.
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.
563 * The alias used to refer to this specific table, or NULL if the table
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');
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'];
580 if (!array_key_exists($relationship, $this->relationships)) {
584 if ($table == $this->relationships[$relationship]['base']) {
585 return $relationship;
588 // If we do not have join info, fetch it.
590 $join = $this->getJoinData($table, $this->relationships[$relationship]['base']);
593 // If it can't be fetched, this won't work.
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);
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.
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.
617 // scan through the table queue to see if a matching join and
618 // relationship exists. If so, use it instead of this join.
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'];
634 return $this->queueTable($table, $relationship, $join);
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.
645 protected function ensurePath($table, $relationship = NULL, $join = NULL, $traced = [], $add = []) {
646 if (!isset($relationship)) {
647 $relationship = $this->view->storage->get('base_table');
650 if (!array_key_exists($relationship, $this->relationships)) {
654 // If we do not have join info, fetch it.
656 $join = $this->getJoinData($table, $this->relationships[$relationship]['base']);
659 // If it can't be fetched, this won't work.
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'])) {
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));
676 // Have we been this way?
677 if (isset($traced[$join->leftTable])) {
678 // We looped. Broken.
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;
689 $traced[$join->leftTable] = TRUE;
690 return $this->ensurePath($join->leftTable, $relationship, $left_join, $traced, $add);
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.
697 protected function adjustJoin($join, $relationship) {
698 if (!empty($join->adjusted)) {
702 if (empty($relationship) || empty($this->relationships[$relationship])) {
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.
711 // Safety! Don't modify an original here.
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);
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;
725 // then, try the base alias.
726 elseif (isset($this->tables[$relationship][$join->leftTable]['alias'])) {
727 $join->leftTable = $this->tables[$relationship][$join->leftTable]['alias'];
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'];
735 $join->adjusted = TRUE;
740 * Retrieve join data from the larger join data cache.
743 * The table to get the join information for.
745 * The path we're following to get this join.
747 * @return \Drupal\views\Plugin\views\join\JoinPluginBase
748 * A Join object or child object, if one exists.
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'];
756 return HandlerBase::getTableJoin($table, $base_table);
760 * Get the information associated with a table.
762 * If you need the alias of a table with a particular relationship, use
765 public function getTableInfo($table) {
766 if (!empty($this->tableQueue[$table])) {
767 return $this->tableQueue[$table];
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];
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.
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
789 * The name of the field to add. This may be a real field or a formula.
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
795 * An array of parameters additional to the field that will control items
796 * such as aggregation functions and DISTINCT. Some values that are
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.
803 * The name that this field can be referred to as. Usually this is the alias.
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');
811 if ($table && empty($this->tableQueue[$table])) {
812 $this->ensureTable($table);
815 if (!$alias && $table) {
816 $alias = $table . '_' . $field;
819 // Make sure an alias is assigned
820 $alias = $alias ? $alias : $field;
822 // PostgreSQL truncates aliases to 63 characters:
823 // https://www.drupal.org/node/571548.
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));
829 // Create a field info array.
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:
841 while (!empty($this->fields[$alias]) && $this->fields[$alias] != $field_info) {
842 $field_info['alias'] = $alias = $base . '_' . ++$counter;
845 if (empty($this->fields[$alias])) {
846 $this->fields[$alias] = $field_info;
849 // Keep track of all aliases used.
850 $this->fieldAliases[$table][$field] = $alias;
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.
859 public function clearFields() {
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.
868 * The $field, $value and $operator arguments can also be passed in with a
869 * single DatabaseCondition object, like this:
871 * $this->query->addWhere(
872 * $this->options['group'],
873 * (new Condition('OR'))
874 * ->condition($field, $value, 'NOT IN')
875 * ->condition($field, $value, 'IS NULL')
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.
884 * The name of the field to check.
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.
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.
894 * @see \Drupal\Core\Database\Query\ConditionInterface::condition()
895 * @see \Drupal\Core\Database\Query\Condition
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.
904 // Check for a group.
905 if (!isset($this->where[$group])) {
906 $this->setWhereGroup('AND', $group);
909 $this->where[$group]['conditions'][] = [
912 'operator' => $operator,
917 * Add a complex WHERE clause to the query.
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.
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.
928 * The snippet to check. This can be either a column or
929 * a complex expression like "UPPER(table.field) = 'value'"
931 * An associative array of arguments.
933 * @see QueryConditionInterface::where()
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.
942 // Check for a group.
943 if (!isset($this->where[$group])) {
944 $this->setWhereGroup('AND', $group);
947 $this->where[$group]['conditions'][] = [
950 'operator' => 'formula',
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.
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.
965 * The snippet to check. This can be either a column or
966 * a complex expression like "COUNT(table.field) > 3"
968 * An associative array of arguments.
970 * @see QueryConditionInterface::having()
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.
979 // Check for a group.
980 if (!isset($this->having[$group])) {
981 $this->setWhereGroup('AND', $group, 'having');
984 // Add the clause and the args.
985 $this->having[$group]['conditions'][] = [
988 'operator' => 'formula',
993 * Add an ORDER BY clause to the query.
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.
999 * The field or formula to sort on. If already a field, enter NULL
1000 * and put in the alias.
1002 * Either ASC or DESC.
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.
1009 * Any params that should be passed through to the addField.
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);
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;
1028 $as = $this->addField($table, $field, $as, $params);
1031 $this->orderby[] = [
1033 'direction' => strtoupper($order)
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
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;
1050 * Returns the alias for the given field added to $table.
1054 * @see \Drupal\views\Plugin\views\query\Sql::addField
1056 protected function getFieldAlias($table_alias, $field) {
1057 return isset($this->fieldAliases[$table_alias][$field]) ? $this->fieldAliases[$table_alias][$field] : FALSE;
1061 * Adds a query tag to the sql object.
1063 * @see SelectQuery::addTag()
1065 public function addTag($tag) {
1066 $this->tags[] = $tag;
1070 * Generates a unique placeholder used in the db query.
1072 public function placeholder($base = 'views') {
1073 static $placeholders = [];
1074 if (!isset($placeholders[$base])) {
1075 $placeholders[$base] = 0;
1079 return ':' . $base . ++$placeholders[$base];
1084 * Construct the "WHERE" or "HAVING" part of the query.
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
1092 * 'where' or 'having'.
1094 protected function buildCondition($where = 'where') {
1095 $has_condition = FALSE;
1096 $has_arguments = FALSE;
1097 $has_filter = FALSE;
1099 $main_group = new Condition('AND');
1100 $filter_group = $this->groupOperator == 'OR' ? new Condition('OR') : new Condition('AND');
1102 foreach ($this->$where as $group => $info) {
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']);
1112 $has_condition = TRUE;
1113 $sub_group->condition($clause['field'], $clause['value'], $clause['operator']);
1117 // Add the item to the filter group.
1120 $filter_group->condition($sub_group);
1123 $has_arguments = TRUE;
1124 $main_group->condition($sub_group);
1130 $main_group->condition($filter_group);
1133 if (!$has_arguments && $has_condition) {
1134 return $filter_group;
1136 if ($has_arguments && $has_condition) {
1142 * Returns a list of non-aggregates to be added to the "group by" clause.
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.
1150 * An array of the fieldnames which are non-aggregates.
1152 protected function getNonAggregates() {
1153 $non_aggregates = [];
1154 foreach ($this->fields as $field) {
1156 if (!empty($field['table'])) {
1157 $string .= $field['table'] . '.';
1159 $string .= $field['field'];
1160 $fieldname = (!empty($field['alias']) ? $field['alias'] : $string);
1162 if (!empty($field['count'])) {
1163 // Retained for compatibility.
1164 $field['function'] = 'count';
1167 if (!empty($field['function'])) {
1168 $this->hasAggregate = TRUE;
1170 // This is a formula, using no tables.
1171 elseif (empty($field['table'])) {
1172 $non_aggregates[] = $fieldname;
1174 elseif (empty($field['aggregate'])) {
1175 $non_aggregates[] = $fieldname;
1178 if ($this->getCountOptimized) {
1179 // We only want the first field in this case.
1184 return $non_aggregates;
1188 * Adds fields to the query.
1190 * @param \Drupal\Core\Database\Query\SelectInterface $query
1191 * The drupal query object.
1193 protected function compileFields($query) {
1194 foreach ($this->fields as $field) {
1196 if (!empty($field['table'])) {
1197 $string .= $field['table'] . '.';
1199 $string .= $field['field'];
1200 $fieldname = (!empty($field['alias']) ? $field['alias'] : $string);
1202 if (!empty($field['count'])) {
1203 // Retained for compatibility.
1204 $field['function'] = 'count';
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);
1215 $this->hasAggregate = TRUE;
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);
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);
1225 elseif (empty($field['aggregate'])) {
1226 $query->addField(!empty($field['table']) ? $field['table'] : $this->view->storage->get('base_table'), $field['field'], $fieldname);
1229 if ($this->getCountOptimized) {
1230 // We only want the first field in this case.
1237 * Generate a query and a countquery from all of the information supplied
1241 * Provide a countquery if this is true, otherwise provide a normal query.
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);
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.
1255 if ($get_count && !$this->groupby) {
1256 foreach ($this->fields as $field) {
1257 if (!empty($field['distinct']) || !empty($field['function'])) {
1258 $this->getCountOptimized = FALSE;
1264 $this->getCountOptimized = FALSE;
1266 if (!isset($this->getCountOptimized)) {
1267 $this->getCountOptimized = TRUE;
1271 $target = 'default';
1273 // Detect an external database and set the
1274 if (isset($this->view->base_database)) {
1275 $key = $this->view->base_database;
1278 // Set the replica target if the replica option is set
1279 if (!empty($this->options['replica'])) {
1280 $target = 'replica';
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)
1288 ->addTag('views_' . $this->view->storage->id());
1290 // Add the tags added to the view itself.
1291 foreach ($this->tags as $tag) {
1292 $query->addTag($tag);
1295 if (!empty($distinct)) {
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);
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;
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');
1317 if ($this->hasAggregate && (!empty($this->groupby) || !empty($non_aggregates))) {
1318 $groupby = array_unique(array_merge($this->groupby, $non_aggregates));
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) {
1327 // Handle grouping, by retrieving the minimum entity_id.
1329 'function' => 'min',
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);
1340 // Add all fields to the query.
1341 $this->compileFields($query);
1345 foreach ($groupby as $field) {
1346 // Handle group by of field without table alias to avoid ambiguous
1348 if ($field == $this->view->storage->get('base_field')) {
1349 $field = $this->view->storage->get('base_table') . '.' . $field;
1351 $query->groupBy($field);
1353 if (!empty($this->having) && $condition = $this->buildCondition('having')) {
1354 $query->havingCondition($condition);
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();
1366 $query->orderBy($order['field'], $order['direction']);
1372 if (!empty($this->where) && $condition = $this->buildCondition('where')) {
1373 $query->condition($condition);
1376 // Add a query comment.
1377 if (!empty($this->options['query_comment'])) {
1378 $query->comment($this->options['query_comment']);
1381 // Add the query tags.
1382 if (!empty($this->options['query_tags'])) {
1383 foreach ($this->options['query_tags'] as $tag) {
1384 $query->addTag($tag);
1388 // Add all query substitutions as metadata.
1389 $query->addMetaData('views_substitutions', \Drupal::moduleHandler()->invokeAll('views_query_substitutions', [$this->view]));
1395 * Get the arguments attached to the WHERE and HAVING clauses of this query.
1397 public function getWhereArgs() {
1399 foreach ($this->where as $where) {
1400 $args = array_merge($args, $where['args']);
1402 foreach ($this->having as $having) {
1403 $args = array_merge($args, $having['args']);
1409 * Let modules modify the query just prior to finalizing it.
1411 public function alter(ViewExecutable $view) {
1412 \Drupal::moduleHandler()->invokeAll('views_query_alter', [$view, $this]);
1416 * Builds the necessary info to execute the query.
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);
1424 // Store the view in the object to be able to use it later.
1425 $this->view = $view;
1429 // Let the pager modify the query to add limits.
1430 $view->pager->query();
1432 $view->build_info['query'] = $this->query();
1433 $view->build_info['count_query'] = $this->query(TRUE);
1437 * Executes the query and fills the associated view object with according
1440 * Values to set: $view->result, $view->total_rows, $view->execute_time,
1441 * $view->current_page.
1443 public function execute(ViewExecutable $view) {
1444 $query = $view->build_info['query'];
1445 $count_query = $view->build_info['count_query'];
1447 $query->addMetaData('view', $view);
1448 $count_query->addMetaData('view', $view);
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);
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);
1467 $additional_arguments = \Drupal::moduleHandler()->invokeAll('views_query_substitutions', [$view]);
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();
1478 // Build the count query.
1479 $count_query = $count_query->countQuery();
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);
1489 $start = microtime(TRUE);
1492 if ($view->pager->useCountQuery() || !empty($view->get_total_rows)) {
1493 $view->pager->executeCountQuery($count_query);
1496 // Let the pager modify the query to add limits.
1497 $view->pager->preExecute($query);
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);
1506 $result = $query->execute();
1507 $result->setFetchMode(\PDO::FETCH_CLASS, 'Drupal\views\ResultRow');
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;
1515 $view->pager->postExecute($view->result);
1516 $view->pager->updatePageInfo();
1517 $view->total_rows = $view->pager->getTotalItems();
1519 // Load all entities contained in the results.
1520 $this->loadEntities($view->result);
1522 catch (DatabaseExceptionWrapper $e) {
1524 if (!empty($view->live_preview)) {
1525 drupal_set_message($e->getMessage(), 'error');
1528 throw new DatabaseExceptionWrapper("Exception in {$view->storage->label()}[{$view->storage->id()}]: {$e->getMessage()}");
1534 $start = microtime(TRUE);
1536 $view->execute_time = microtime(TRUE) - $start;
1540 * Loads all entities contained in the passed-in $results.
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];
1546 * @param \Drupal\views\ResultRow[] $results
1547 * The result of the SQL query.
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)) {
1556 // Extract all entity types from entity_information.
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);
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);
1577 foreach ($results as $index => $result) {
1578 // Store the entity id if it was found.
1579 if (isset($result->{$id_alias}) && $result->{$id_alias} != '') {
1581 $revision_ids_by_type[$entity_type][$index][$relationship_id] = $result->$id_alias;
1584 $entity_ids_by_type[$entity_type][$index][$relationship_id] = $result->$id_alias;
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);
1595 $entities = $entity_storage->loadMultiple(array_unique($flat_ids));
1596 $results = $this->assignEntitiesToResult($ids, $entities, $results);
1599 // Now load all revisions.
1600 foreach ($revision_ids_by_type as $entity_type => $revision_ids) {
1601 $entity_storage = $this->entityTypeManager->getStorage($entity_type);
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;
1612 $results = $this->assignEntitiesToResult($revision_ids, $entities, $results);
1617 * Sets entities onto the view result row objects.
1619 * This method takes into account the relationship in which the entity was
1620 * needed in the first place.
1622 * @param mixed[][] $ids
1623 * A two dimensional array of identifiers (entity ID / revision ID) keyed by
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.
1630 * @return \Drupal\views\ResultRow[]
1631 * The changed views results.
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];
1643 if ($relationship_id == 'none') {
1644 $results[$index]->_entity = $entity;
1647 $results[$index]->_relationship_entities[$relationship_id] = $entity;
1657 public function getCacheTags() {
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);
1672 public function getCacheMaxAge() {
1673 $max_age = parent::getCacheMaxAge();
1674 foreach ($this->getAllEntities() as $entity) {
1675 $max_age = Cache::mergeMaxAges($max_age, $entity->getCacheMaxAge());
1682 * Gets all the involved entities of the view.
1684 * @return \Drupal\Core\Entity\EntityInterface[]
1686 protected function getAllEntities() {
1688 foreach ($this->view->result as $row) {
1689 if ($row->_entity) {
1690 $entities[] = $row->_entity;
1692 foreach ($row->_relationship_entities as $entity) {
1693 $entities[] = $entity;
1700 public function addSignature(ViewExecutable $view) {
1701 $view->query->addField(NULL, "'" . $view->storage->id() . ':' . $view->current_display . "'", 'view_name');
1704 public function getAggregationInfo() {
1705 // @todo -- need a way to get database specific and customized aggregation
1706 // functions into here.
1709 'title' => $this->t('Group results together'),
1710 'is aggregate' => FALSE,
1713 'title' => $this->t('Count'),
1714 'method' => 'aggregationMethodSimple',
1716 'argument' => 'groupby_numeric',
1717 'field' => 'numeric',
1718 'filter' => 'groupby_numeric',
1719 'sort' => 'groupby_numeric',
1722 'count_distinct' => [
1723 'title' => $this->t('Count DISTINCT'),
1724 'method' => 'aggregationMethodDistinct',
1726 'argument' => 'groupby_numeric',
1727 'field' => 'numeric',
1728 'filter' => 'groupby_numeric',
1729 'sort' => 'groupby_numeric',
1733 'title' => $this->t('Sum'),
1734 'method' => 'aggregationMethodSimple',
1736 'argument' => 'groupby_numeric',
1737 'field' => 'numeric',
1738 'filter' => 'groupby_numeric',
1739 'sort' => 'groupby_numeric',
1743 'title' => $this->t('Average'),
1744 'method' => 'aggregationMethodSimple',
1746 'argument' => 'groupby_numeric',
1747 'field' => 'numeric',
1748 'filter' => 'groupby_numeric',
1749 'sort' => 'groupby_numeric',
1753 'title' => $this->t('Minimum'),
1754 'method' => 'aggregationMethodSimple',
1756 'argument' => 'groupby_numeric',
1757 'field' => 'numeric',
1758 'filter' => 'groupby_numeric',
1759 'sort' => 'groupby_numeric',
1763 'title' => $this->t('Maximum'),
1764 'method' => 'aggregationMethodSimple',
1766 'argument' => 'groupby_numeric',
1767 'field' => 'numeric',
1768 'filter' => 'groupby_numeric',
1769 'sort' => 'groupby_numeric',
1773 'title' => $this->t('Standard deviation'),
1774 'method' => 'aggregationMethodSimple',
1776 'argument' => 'groupby_numeric',
1777 'field' => 'numeric',
1778 'filter' => 'groupby_numeric',
1779 'sort' => 'groupby_numeric',
1785 public function aggregationMethodSimple($group_type, $field) {
1786 return strtoupper($group_type) . '(' . $field . ')';
1789 public function aggregationMethodDistinct($group_type, $field) {
1790 $group_type = str_replace('_distinct', '', $group_type);
1791 return strtoupper($group_type) . '(DISTINCT ' . $field . ')';
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);
1808 public function setFieldTimezoneOffset(&$field, $offset) {
1809 $this->dateSql->setFieldTimezoneOffset($field, $offset);
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;
1823 return parent::setupTimezone();
1829 public function getDateFormat($field, $format, $string_date = FALSE) {
1830 return $this->dateSql->getDateFormat($field, $format);