diff --git a/core/modules/datetime/datetime.views.inc b/core/modules/datetime/datetime.views.inc
index d3b0d18..93d6cd4 100644
--- a/core/modules/datetime/datetime.views.inc
+++ b/core/modules/datetime/datetime.views.inc
@@ -39,6 +39,8 @@ function datetime_field_views_data(FieldStorageConfigInterface $field_storage) {
         'argument' => [
           'field' => $field_storage->getName() . '_value',
           'id' => 'datetime_' . $argument_type,
+          'entity_type' => $field_storage->getTargetEntityTypeId(),
+          'field_name' => $field_storage->getName(),
         ],
         'group' => $group,
       ];
diff --git a/core/modules/datetime/src/Plugin/views/argument/Date.php b/core/modules/datetime/src/Plugin/views/argument/Date.php
index fb2bc4a..26e4303 100644
--- a/core/modules/datetime/src/Plugin/views/argument/Date.php
+++ b/core/modules/datetime/src/Plugin/views/argument/Date.php
@@ -2,6 +2,9 @@
 
 namespace Drupal\datetime\Plugin\views\Argument;
 
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
+use Drupal\views\FieldAPIHandlerTrait;
 use Drupal\views\Plugin\views\argument\Date as NumericDate;
 
 /**
@@ -22,12 +25,36 @@
  */
 class Date extends NumericDate {
 
+  use FieldAPIHandlerTrait;
+
+  /**
+   * Determines if the timezone offset is calculated.
+   *
+   * @var bool
+   */
+  protected $calculateOffset = TRUE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteMatchInterface $route_match) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $route_match);
+
+    $definition = $this->getFieldStorageDefinition();
+    if ($definition->getSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) {
+      // Timezone offset calculation is not applicable to dates that are stored
+      // as date-only.
+      $this->calculateOffset = FALSE;
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
   public function getDateField() {
-    // Return the real field, since it is already in string format.
-    return "$this->tableAlias.$this->realField";
+    // Use string date storage/formatting since datetime fields are stored as
+    // strings rather than UNIX timestamps.
+    return $this->query->getDateField("$this->tableAlias.$this->realField", TRUE, $this->calculateOffset);
   }
 
   /**
diff --git a/core/modules/datetime/src/Plugin/views/filter/Date.php b/core/modules/datetime/src/Plugin/views/filter/Date.php
index 9fd47fd..9b7ebc5 100644
--- a/core/modules/datetime/src/Plugin/views/filter/Date.php
+++ b/core/modules/datetime/src/Plugin/views/filter/Date.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\datetime\Plugin\views\filter;
 
+use Drupal\Component\Datetime\DateTimePlus;
 use Drupal\Core\Datetime\DateFormatterInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
@@ -41,6 +42,13 @@ class Date extends NumericDate implements ContainerFactoryPluginInterface {
   protected $dateFormat = DATETIME_DATETIME_STORAGE_FORMAT;
 
   /**
+   * Determines if the timezone offset is calculated.
+   *
+   * @var bool
+   */
+  protected $calculateOffset = TRUE;
+
+  /**
    * The request stack used to determin current time.
    *
    * @var \Symfony\Component\HttpFoundation\RequestStack
@@ -58,7 +66,7 @@ class Date extends NumericDate implements ContainerFactoryPluginInterface {
    *   The plugin implementation definition.
    * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
    *   The date formatter service.
-   * @param \Symfony\Component\HttpFoundation\RequestStack
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
    *   The request stack used to determine the current time.
    */
   public function __construct(array $configuration, $plugin_id, $plugin_definition, DateFormatterInterface $date_formatter, RequestStack $request_stack) {
@@ -66,10 +74,13 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
     $this->dateFormatter = $date_formatter;
     $this->requestStack = $request_stack;
 
-    // Date format depends on field storage format.
     $definition = $this->getFieldStorageDefinition();
     if ($definition->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATE) {
+      // Date format depends on field storage format.
       $this->dateFormat = DATETIME_DATE_STORAGE_FORMAT;
+      // Timezone offset calculation is not applicable to dates that are stored
+      // as date-only.
+      $this->calculateOffset = FALSE;
     }
   }
 
@@ -90,21 +101,25 @@ public static function create(ContainerInterface $container, array $configuratio
    * Override parent method, which deals with dates as integers.
    */
   protected function opBetween($field) {
-    $origin = ($this->value['type'] == 'offset') ? $this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME') : 0;
-    $a = intval(strtotime($this->value['min'], $origin));
-    $b = intval(strtotime($this->value['max'], $origin));
+    $timezone = ($this->dateFormat === DATETIME_DATE_STORAGE_FORMAT)
+      ? DATETIME_STORAGE_TIMEZONE
+      : drupal_get_user_timezone();
 
-    // Formatting will vary on date storage.
+    // Although both 'min' and 'max' values are required,
+    // default empty 'min' value as UNIX timestamp 0.
+    $min = (!empty($this->value['min'])) ? $this->value['min'] : '@0';
 
+    $a = new DateTimePlus($min, new \DateTimeZone($timezone));
+    $b = new DateTimePlus($this->value['max'], new \DateTimeZone($timezone));
 
     // Convert to ISO format and format for query. UTC timezone is used since
     // dates are stored in UTC.
-    $a = $this->query->getDateFormat("'" . $this->dateFormatter->format($a, 'custom', DATETIME_DATETIME_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE) . "'", $this->dateFormat, TRUE);
-    $b = $this->query->getDateFormat("'" . $this->dateFormatter->format($b, 'custom', DATETIME_DATETIME_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE) . "'", $this->dateFormat, TRUE);
+    $a = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format(intval($a->format('U')), 'custom', DATETIME_DATETIME_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE) . "'", TRUE, $this->calculateOffset), $this->dateFormat, TRUE);
+    $b = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format(intval($b->format('U')), 'custom', DATETIME_DATETIME_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE) . "'", TRUE, $this->calculateOffset), $this->dateFormat, TRUE);
 
     // This is safe because we are manually scrubbing the values.
     $operator = strtoupper($this->operator);
-    $field = $this->query->getDateFormat($field, $this->dateFormat, TRUE);
+    $field = $this->query->getDateFormat($this->query->getDateField($field, TRUE, $this->calculateOffset), $this->dateFormat, TRUE);
     $this->query->addWhereExpression($this->options['group'], "$field $operator $a AND $b");
   }
 
@@ -112,14 +127,18 @@ protected function opBetween($field) {
    * Override parent method, which deals with dates as integers.
    */
   protected function opSimple($field) {
-    $origin = (!empty($this->value['type']) && $this->value['type'] == 'offset') ? $this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME') : 0;
-    $value = intval(strtotime($this->value['value'], $origin));
+    $timezone = ($this->dateFormat === DATETIME_DATE_STORAGE_FORMAT)
+      ? DATETIME_STORAGE_TIMEZONE
+      : drupal_get_user_timezone();
+
+    $value = new DateTimePlus($this->value['value'], new \DateTimeZone($timezone));
 
-    // Convert to ISO. UTC is used since dates are stored in UTC.
-    $value = $this->query->getDateFormat("'" . $this->dateFormatter->format($value, 'custom', DATETIME_DATETIME_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE) . "'", $this->dateFormat, TRUE);
+    // Convert to ISO. UTC timezone is used since
+    // dates are stored in UTC.
+    $value = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format(intval($value->format('U')), 'custom', DATETIME_DATETIME_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE) . "'", TRUE, $this->calculateOffset), $this->dateFormat, TRUE);
 
     // This is safe because we are manually scrubbing the value.
-    $field = $this->query->getDateFormat($field, $this->dateFormat, TRUE);
+    $field = $this->query->getDateFormat($this->query->getDateField($field, TRUE, $this->calculateOffset), $this->dateFormat, TRUE);
     $this->query->addWhereExpression($this->options['group'], "$field $this->operator $value");
   }
 
diff --git a/core/modules/datetime/src/Plugin/views/sort/Date.php b/core/modules/datetime/src/Plugin/views/sort/Date.php
index 2c8338a..220b063 100644
--- a/core/modules/datetime/src/Plugin/views/sort/Date.php
+++ b/core/modules/datetime/src/Plugin/views/sort/Date.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\datetime\Plugin\views\sort;
 
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
+use Drupal\views\FieldAPIHandlerTrait;
 use Drupal\views\Plugin\views\sort\Date as NumericDate;
 
 /**
@@ -14,12 +16,38 @@
  */
 class Date extends NumericDate {
 
+  use FieldAPIHandlerTrait;
+
   /**
+   * Determines if the timezone offset is calculated.
+   *
+   * @var bool
+   */
+  protected $calculateOffset = TRUE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $definition = $this->getFieldStorageDefinition();
+    if ($definition->getSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) {
+      // Timezone offset calculation is not applicable to dates that are stored
+      // as date-only.
+      $this->calculateOffset = FALSE;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   *
    * Override to account for dates stored as strings.
    */
   public function getDateField() {
-    // Return the real field, since it is already in string format.
-    return "$this->tableAlias.$this->realField";
+    // Use string date storage/formatting since datetime fields are stored as
+    // strings rather than UNIX timestamps.
+    return $this->query->getDateField("$this->tableAlias.$this->realField", TRUE, $this->calculateOffset);
   }
 
   /**
diff --git a/core/modules/datetime/src/Tests/Views/ArgumentDateTimeTest.php b/core/modules/datetime/src/Tests/Views/ArgumentDateTimeTest.php
index e3a17bb..010d63b 100644
--- a/core/modules/datetime/src/Tests/Views/ArgumentDateTimeTest.php
+++ b/core/modules/datetime/src/Tests/Views/ArgumentDateTimeTest.php
@@ -27,6 +27,8 @@ protected function setUp() {
       '2000-10-10',
       '2001-10-10',
       '2002-01-01',
+      // Add a date that in in 2002 in UTC, but in 2003 site (Sydney).
+      '2002-12-31T23:00:00',
     ];
     foreach ($dates as $date) {
       $this->nodes[] = $this->drupalCreateNode([
@@ -59,6 +61,31 @@ public function testDatetimeArgumentYear() {
     $expected[] = ['nid' => $this->nodes[2]->id()];
     $this->assertIdenticalResultset($view, $expected, $this->map);
     $view->destroy();
+
+    $view->setDisplay('default');
+    $this->executeView($view, ['2003']);
+    $expected = [];
+    $expected[] = ['nid' => $this->nodes[3]->id()];
+    $this->assertIdenticalResultset($view, $expected, $this->map);
+    $view->destroy();
+
+    // Test as a user with a different timezone.
+    $this->config('system.date')
+      ->set('timezone.user.configurable', TRUE)
+      ->save();
+    $user = $this->drupalCreateUser();
+    $user->set('timezone', 'America/Vancouver');
+    $user->save();
+    $this->drupalLogin($user);
+
+    $view->setDisplay('default');
+    $this->executeView($view, ['2002']);
+    $expected = [];
+    // Only the 3rd node is returned here since UTC 2002-01-01T00:00:00 is still
+    // in 2001 for this user timezone.
+    $expected[] = ['nid' => $this->nodes[3]->id()];
+    $this->assertIdenticalResultset($view, $expected, $this->map);
+    $view->destroy();
   }
 
   /**
@@ -82,6 +109,7 @@ public function testDatetimeArgumentMonth() {
     $this->executeView($view, ['01']);
     $expected = [];
     $expected[] = ['nid' => $this->nodes[2]->id()];
+    $expected[] = ['nid' => $this->nodes[3]->id()];
     $this->assertIdenticalResultset($view, $expected, $this->map);
     $view->destroy();
   }
@@ -107,6 +135,7 @@ public function testDatetimeArgumentDay() {
     $this->executeView($view, ['01']);
     $expected = [];
     $expected[] = ['nid' => $this->nodes[2]->id()];
+    $expected[] = ['nid' => $this->nodes[3]->id()];
     $this->assertIdenticalResultset($view, $expected, $this->map);
     $view->destroy();
   }
@@ -152,6 +181,7 @@ public function testDatetimeArgumentWeek() {
     $this->executeView($view, ['01']);
     $expected = [];
     $expected[] = ['nid' => $this->nodes[2]->id()];
+    $expected[] = ['nid' => $this->nodes[3]->id()];
     $this->assertIdenticalResultset($view, $expected, $this->map);
     $view->destroy();
   }
diff --git a/core/modules/datetime/src/Tests/Views/FilterDateTest.php b/core/modules/datetime/src/Tests/Views/FilterDateTest.php
index 2a8d7cd..4c6f1c8 100644
--- a/core/modules/datetime/src/Tests/Views/FilterDateTest.php
+++ b/core/modules/datetime/src/Tests/Views/FilterDateTest.php
@@ -18,9 +18,26 @@ class FilterDateTest extends DateTimeHandlerTestBase {
   public static $testViews = ['test_filter_datetime'];
 
   /**
-   * For offset tests, set to the current time.
+   * An array of timezone extremes to test.
+   *
+   * @var string[]
    */
-  protected static $date;
+  protected static $timezones = [
+    // UTC-12, no DST.
+    'Pacific/Kwajalein',
+    // UTC-11, no DST.
+    'Pacific/Midway',
+    // UTC-7, no DST.
+    'America/Phoenix',
+    // UTC.
+    'UTC',
+    // UTC+5:30, no DST.
+    'Asia/Kolkata',
+    // UTC+12, no DST.
+    'Pacific/Funafuti',
+    // UTC+13, no DST.
+    'Pacific/Tongatapu',
+  ];
 
   /**
    * {@inheritdoc}
@@ -30,28 +47,23 @@ class FilterDateTest extends DateTimeHandlerTestBase {
   protected function setUp() {
     parent::setUp();
 
-    // Set to 'today'.
-    static::$date = REQUEST_TIME;
-
     // Change field storage to date-only.
     $storage = FieldStorageConfig::load('node.' . static::$field_name);
     $storage->setSetting('datetime_type', DateTimeItem::DATETIME_TYPE_DATE);
     $storage->save();
 
-    $dates = [
-      // Tomorrow.
-      \Drupal::service('date.formatter')->format(static::$date + 86400, 'custom', DATETIME_DATE_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE),
-      // Today.
-      \Drupal::service('date.formatter')->format(static::$date, 'custom', DATETIME_DATE_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE),
-      // Yesterday.
-      \Drupal::service('date.formatter')->format(static::$date - 86400, 'custom', DATETIME_DATE_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE),
-    ];
+    // Retrieve tomorrow, today and yesterday dates
+    // just to create the nodes.
+    $timestamp = $this->getUTCEquivalentOfUserNowAsTimestamp();
+    $dates = $this->getRelativeDateValuesFromTimestamp($timestamp);
 
+    // Clean the nodes on setUp.
+    $this->nodes = [];
     foreach ($dates as $date) {
       $this->nodes[] = $this->drupalCreateNode([
         'field_date' => [
           'value' => $date,
-        ]
+        ],
       ]);
     }
   }
@@ -63,48 +75,168 @@ public function testDateOffsets() {
     $view = Views::getView('test_filter_datetime');
     $field = static::$field_name . '_value';
 
-    // Test simple operations.
-    $view->initHandlers();
-
-    // A greater than or equal to 'now', should return the 'today' and
-    // the 'tomorrow' node.
-    $view->filter[$field]->operator = '>=';
-    $view->filter[$field]->value['type'] = 'offset';
-    $view->filter[$field]->value['value'] = 'now';
-    $view->setDisplay('default');
-    $this->executeView($view);
-    $expected_result = [
-      ['nid' => $this->nodes[0]->id()],
-      ['nid' => $this->nodes[1]->id()],
-    ];
-    $this->assertIdenticalResultset($view, $expected_result, $this->map);
-    $view->destroy();
-
-    // Only dates in the past.
-    $view->initHandlers();
-    $view->filter[$field]->operator = '<';
-    $view->filter[$field]->value['type'] = 'offset';
-    $view->filter[$field]->value['value'] = 'now';
-    $view->setDisplay('default');
-    $this->executeView($view);
-    $expected_result = [
-      ['nid' => $this->nodes[2]->id()],
-    ];
-    $this->assertIdenticalResultset($view, $expected_result, $this->map);
-    $view->destroy();
-
-    // Test offset for between operator. Only the 'tomorrow' node should appear.
-    $view->initHandlers();
-    $view->filter[$field]->operator = 'between';
-    $view->filter[$field]->value['type'] = 'offset';
-    $view->filter[$field]->value['max'] = '+2 days';
-    $view->filter[$field]->value['min'] = '+1 day';
-    $view->setDisplay('default');
-    $this->executeView($view);
-    $expected_result = [
-      ['nid' => $this->nodes[0]->id()],
+    foreach (static::$timezones as $timezone) {
+
+      $this->setSiteTimezone($timezone);
+      $timestamp = $this->getUTCEquivalentOfUserNowAsTimestamp();
+      $dates = $this->getRelativeDateValuesFromTimestamp($timestamp);
+      $this->updateNodesDateFieldsValues($dates);
+
+      // Test simple operations.
+      $view->initHandlers();
+
+      // A greater than or equal to 'now', should return the 'today' and
+      // the 'tomorrow' node.
+      $view->filter[$field]->operator = '>=';
+      $view->filter[$field]->value['type'] = 'offset';
+      $view->filter[$field]->value['value'] = 'now';
+      $view->setDisplay('default');
+      $this->executeView($view);
+      $expected_result = [
+        ['nid' => $this->nodes[0]->id()],
+        ['nid' => $this->nodes[1]->id()],
+      ];
+      $this->assertIdenticalResultset($view, $expected_result, $this->map);
+      $view->destroy();
+
+      // Only dates in the past.
+      $view->initHandlers();
+      $view->filter[$field]->operator = '<';
+      $view->filter[$field]->value['type'] = 'offset';
+      $view->filter[$field]->value['value'] = 'now';
+      $view->setDisplay('default');
+      $this->executeView($view);
+      $expected_result = [
+        ['nid' => $this->nodes[2]->id()],
+      ];
+      $this->assertIdenticalResultset($view, $expected_result, $this->map);
+      $view->destroy();
+
+      // Test offset for between operator. Only 'tomorrow' node should appear.
+      $view->initHandlers();
+      $view->filter[$field]->operator = 'between';
+      $view->filter[$field]->value['type'] = 'offset';
+      $view->filter[$field]->value['max'] = '+2 days';
+      $view->filter[$field]->value['min'] = '+1 day';
+      $view->setDisplay('default');
+      $this->executeView($view);
+      $expected_result = [
+        ['nid' => $this->nodes[0]->id()],
+      ];
+      $this->assertIdenticalResultset($view, $expected_result, $this->map);
+      $view->destroy();
+    }
+  }
+
+  /**
+   * Test date filter with date-only fields.
+   */
+  public function testDateIs() {
+    $view = Views::getView('test_filter_datetime');
+    $field = static::$field_name . '_value';
+
+    foreach (static::$timezones as $timezone) {
+
+      $this->setSiteTimezone($timezone);
+      $timestamp = $this->getUTCEquivalentOfUserNowAsTimestamp();
+      $dates = $this->getRelativeDateValuesFromTimestamp($timestamp);
+      $this->updateNodesDateFieldsValues($dates);
+
+      // Test simple operations.
+      $view->initHandlers();
+
+      // Filtering with nodes date-only values (format: Y-m-d) to test
+      // UTC conversion does NOT change the day.
+      $view->filter[$field]->operator = '=';
+      $view->filter[$field]->value['type'] = 'date';
+      $view->filter[$field]->value['value'] = $this->nodes[2]->field_date->first()->getValue()['value'];
+      $view->setDisplay('default');
+      $this->executeView($view);
+      $expected_result = [
+        ['nid' => $this->nodes[2]->id()],
+      ];
+      $this->assertIdenticalResultset($view, $expected_result, $this->map);
+      $view->destroy();
+
+      // Test offset for between operator.
+      // Only 'today' and 'tomorrow' nodes should appear.
+      $view->initHandlers();
+      $view->filter[$field]->operator = 'between';
+      $view->filter[$field]->value['type'] = 'date';
+      $view->filter[$field]->value['max'] = $this->nodes[0]->field_date->first()->getValue()['value'];
+      $view->filter[$field]->value['min'] = $this->nodes[1]->field_date->first()->getValue()['value'];
+      $view->setDisplay('default');
+      $this->executeView($view);
+      $expected_result = [
+        ['nid' => $this->nodes[0]->id()],
+        ['nid' => $this->nodes[1]->id()],
+      ];
+      $this->assertIdenticalResultset($view, $expected_result, $this->map);
+      $view->destroy();
+    }
+  }
+
+  /**
+   * Returns UTC timestamp of user's TZ 'now'.
+   *
+   * The date field stores date_only values without conversion,
+   * considering them already as UTC.
+   * This method returns the UTC equivalent of user's "now"
+   * as a unix timestamp, so they match using Y-m-d format.
+   *
+   * @return int
+   *   Unix timestamp.
+   */
+  protected function getUTCEquivalentOfUserNowAsTimestamp() {
+    return REQUEST_TIME - abs(timezone_offset_get(new \DateTimeZone(drupal_get_user_timezone()), new \DateTime('now', new \DateTimeZone(DATETIME_STORAGE_TIMEZONE))));
+  }
+
+  /**
+   * Returns an array formatted date_only values.
+   *
+   * @param int $timestamp
+   *   Unix Timestamp equivalent to user's "now".
+   *
+   * @return array
+   *   An array of DATETIME_DATE_STORAGE_FORMAT date values.
+   *   In order tomorrow, today and yesterday.
+   */
+  protected function getRelativeDateValuesFromTimestamp($timestamp) {
+    return [
+      // Tomorrow.
+      \Drupal::service('date.formatter')->format($timestamp + 86400, 'custom', DATETIME_DATE_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE),
+      // Today.
+      \Drupal::service('date.formatter')->format($timestamp, 'custom', DATETIME_DATE_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE),
+      // Yesterday.
+      \Drupal::service('date.formatter')->format($timestamp - 86400, 'custom', DATETIME_DATE_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE),
     ];
-    $this->assertIdenticalResultset($view, $expected_result, $this->map);
+  }
+
+  /**
+   * Updates tests nodes date fields values.
+   *
+   * @param array $dates
+   *   An array of DATETIME_DATE_STORAGE_FORMAT date values.
+   */
+  protected function updateNodesDateFieldsValues(array $dates) {
+    foreach ($dates as $index => $date) {
+      $this->nodes[$index]->{static::$field_name}->value = $date;
+      $this->nodes[$index]->save();
+    }
+  }
+
+  /**
+   * Sets the site timezone to a given timezone.
+   *
+   * @param string $timezone
+   *   The timezone identifier to set.
+   */
+  protected function setSiteTimezone($timezone) {
+    // Set an explicit site timezone, and disallow per-user timezones.
+    $this->config('system.date')
+      ->set('timezone.user.configurable', 0)
+      ->set('timezone.default', $timezone)
+      ->save();
   }
 
 }
diff --git a/core/modules/datetime/src/Tests/Views/FilterDateTimeTest.php b/core/modules/datetime/src/Tests/Views/FilterDateTimeTest.php
index a15263d..1c774c3 100644
--- a/core/modules/datetime/src/Tests/Views/FilterDateTimeTest.php
+++ b/core/modules/datetime/src/Tests/Views/FilterDateTimeTest.php
@@ -36,6 +36,9 @@ protected function setUp() {
 
     // Set the timezone.
     date_default_timezone_set(static::$timezone);
+    $this->config('system.date')
+      ->set('timezone.default', static::$timezone)
+      ->save();
 
     // Add some basic test nodes.
     $dates = [
diff --git a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
index d4e1b23..6e1e8a0 100644
--- a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
+++ b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
@@ -206,11 +206,16 @@ function loadEntities(&$results) {}
    *
    * @param string $field
    *   The query field that will be used in the expression.
+   * @param bool $string_date
+   *   For certain databases, date format functions vary depending on string or
+   *   numeric storage.
+   * @param bool $calculate_offset
+   *   If set to TRUE, the timezone offset will be included in the returned field.
    *
    * @return string
    *   An expression representing a timestamp with time zone.
    */
-  public function getDateField($field) {
+  public function getDateField($field, $string_date = FALSE, $calculate_offset = TRUE) {
     return $field;
   }
 
@@ -346,6 +351,36 @@ public function getCacheTags() {
     return [];
   }
 
+  /**
+   * Applies a timezone offset to the given field.
+   *
+   * @param string &$field
+   *   The date field, in string format.
+   * @param int $offset
+   *   The timezone offset to apply to the field.
+   */
+  public function setFieldTimezoneOffset(&$field, $offset) {
+    // No-op. Timezone offsets are implementation-specific and should implement
+    // this method as needed.
+  }
+
+  /**
+   * Get the timezone offset in seconds.
+   *
+   * @return int
+   *   The offset, in seconds, for the timezone being used.
+   */
+  public function getTimezoneOffset() {
+    $timezone = $this->setupTimezone();
+    $offset = 0;
+    if ($timezone) {
+      $dtz = new \DateTimeZone($timezone);
+      $dt = new \DateTime('now', $dtz);
+      $offset = $dtz->getOffset($dt);
+    }
+    return $offset;
+  }
+
 }
 
 /**
diff --git a/core/modules/views/src/Plugin/views/query/Sql.php b/core/modules/views/src/Plugin/views/query/Sql.php
index cfe593c..753fbb4 100644
--- a/core/modules/views/src/Plugin/views/query/Sql.php
+++ b/core/modules/views/src/Plugin/views/query/Sql.php
@@ -116,6 +116,13 @@ class Sql extends QueryPluginBase {
   protected $entityTypeManager;
 
   /**
+   * The database-specific date handler.
+   *
+   * @var \Drupal\views\Plugin\views\query\DateSqlInterface
+   */
+  protected $dateSql;
+
+  /**
    * Constructs a Sql object.
    *
    * @param array $configuration
@@ -126,11 +133,14 @@ class Sql extends QueryPluginBase {
    *   The plugin implementation definition.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    *   The entity type manager.
+   * @param \Drupal\views\Plugin\views\query\DateSqlInterface $date_sql
+   *   The database-specific date handler.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, DateSqlInterface $date_sql) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
 
     $this->entityTypeManager = $entity_type_manager;
+    $this->dateSql = $date_sql;
   }
 
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
@@ -138,7 +148,8 @@ public static function create(ContainerInterface $container, array $configuratio
       $configuration,
       $plugin_id,
       $plugin_definition,
-      $container->get('entity_type.manager')
+      $container->get('entity_type.manager'),
+      $container->get('views.date_sql')
     );
   }
 
@@ -1754,175 +1765,40 @@ public function aggregationMethodDistinct($group_type, $field) {
   /**
    * {@inheritdoc}
    */
-  public function getDateField($field) {
-    $db_type = Database::getConnection()->databaseType();
-    $offset = $this->setupTimezone();
-    if (isset($offset) && !is_numeric($offset)) {
-      $dtz = new \DateTimeZone($offset);
-      $dt = new \DateTime('now', $dtz);
-      $offset_seconds = $dtz->getOffset($dt);
-    }
-
-    switch ($db_type) {
-      case 'mysql':
-        $field = "DATE_ADD('19700101', INTERVAL $field SECOND)";
-        if (!empty($offset)) {
-          $field = "($field + INTERVAL $offset_seconds SECOND)";
-        }
-        break;
-      case 'pgsql':
-        $field = "TO_TIMESTAMP($field)";
-        if (!empty($offset)) {
-          $field = "($field + INTERVAL '$offset_seconds SECONDS')";
-        }
-        break;
-      case 'sqlite':
-        if (!empty($offset)) {
-          $field = "($field + $offset_seconds)";
-        }
-        break;
+  public function getDateField($field, $string_date = FALSE, $calculate_offset = TRUE) {
+    $field = $this->dateSql->getDateField($field, $string_date);
+    if ($calculate_offset && $offset = $this->getTimezoneOffset()) {
+      $this->setFieldTimezoneOffset($field, $offset);
     }
-
     return $field;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function setupTimezone() {
-    $timezone = drupal_get_user_timezone();
-
-    // set up the database timezone
-    $db_type = Database::getConnection()->databaseType();
-    if (in_array($db_type, array('mysql', 'pgsql'))) {
-      $offset = '+00:00';
-      static $already_set = FALSE;
-      if (!$already_set) {
-        if ($db_type == 'pgsql') {
-          Database::getConnection()->query("SET TIME ZONE INTERVAL '$offset' HOUR TO MINUTE");
-        }
-        elseif ($db_type == 'mysql') {
-          Database::getConnection()->query("SET @@session.time_zone = '$offset'");
-        }
+  public function setFieldTimezoneOffset(&$field, $offset) {
+    $this->dateSql->setFieldTimezoneOffset($field, $offset);
+  }
 
-        $already_set = TRUE;
-      }
+  /**
+   * {@inheritdoc}
+   */
+  public function setupTimezone() {
+    // Set the database timezone offset.
+    static $already_set = FALSE;
+    if (!$already_set) {
+      $this->dateSql->setTimezoneOffset('+00:00');
+      $already_set = TRUE;
     }
 
-    return $timezone;
+    return parent::setupTimezone();
   }
 
   /**
    * {@inheritdoc}
    */
   public function getDateFormat($field, $format, $string_date = FALSE) {
-    $db_type = Database::getConnection()->databaseType();
-    switch ($db_type) {
-      case 'mysql':
-        $replace = array(
-          'Y' => '%Y',
-          'y' => '%y',
-          'M' => '%b',
-          'm' => '%m',
-          'n' => '%c',
-          'F' => '%M',
-          'D' => '%a',
-          'd' => '%d',
-          'l' => '%W',
-          'j' => '%e',
-          'W' => '%v',
-          'H' => '%H',
-          'h' => '%h',
-          'i' => '%i',
-          's' => '%s',
-          'A' => '%p',
-        );
-        $format = strtr($format, $replace);
-        return "DATE_FORMAT($field, '$format')";
-      case 'pgsql':
-        $replace = array(
-          'Y' => 'YYYY',
-          'y' => 'YY',
-          'M' => 'Mon',
-          'm' => 'MM',
-          // No format for Numeric representation of a month, without leading
-          // zeros.
-          'n' => 'MM',
-          'F' => 'Month',
-          'D' => 'Dy',
-          'd' => 'DD',
-          'l' => 'Day',
-          // No format for Day of the month without leading zeros.
-          'j' => 'DD',
-          'W' => 'IW',
-          'H' => 'HH24',
-          'h' => 'HH12',
-          'i' => 'MI',
-          's' => 'SS',
-          'A' => 'AM',
-        );
-        $format = strtr($format, $replace);
-        if (!$string_date) {
-          return "TO_CHAR($field, '$format')";
-        }
-        // In order to allow for partials (eg, only the year), transform to a
-        // date, back to a string again.
-        return "TO_CHAR(TO_TIMESTAMP($field, 'YYYY-MM-DD HH24:MI:SS'), '$format')";
-      case 'sqlite':
-        $replace = array(
-          'Y' => '%Y',
-          // No format for 2 digit year number.
-          'y' => '%Y',
-          // No format for 3 letter month name.
-          'M' => '%m',
-          'm' => '%m',
-          // No format for month number without leading zeros.
-          'n' => '%m',
-          // No format for full month name.
-          'F' => '%m',
-          // No format for 3 letter day name.
-          'D' => '%d',
-          'd' => '%d',
-          // No format for full day name.
-          'l' => '%d',
-          // no format for day of month number without leading zeros.
-          'j' => '%d',
-          'W' => '%W',
-          'H' => '%H',
-          // No format for 12 hour hour with leading zeros.
-          'h' => '%H',
-          'i' => '%M',
-          's' => '%S',
-          // No format for AM/PM.
-          'A' => '',
-        );
-        $format = strtr($format, $replace);
-
-        // Don't use the 'unixepoch' flag for string date comparisons.
-        $unixepoch = $string_date ? '' : ", 'unixepoch'";
-
-        // SQLite does not have a ISO week substitution string, so it needs
-        // special handling.
-        // @see http://wikipedia.org/wiki/ISO_week_date#Calculation
-        // @see http://stackoverflow.com/a/15511864/1499564
-        if ($format === '%W') {
-          $expression = "((strftime('%j', date(strftime('%Y-%m-%d', $field" . $unixepoch . "), '-3 days', 'weekday 4')) - 1) / 7 + 1)";
-        }
-        else {
-          $expression = "strftime('$format', $field" . $unixepoch . ")";
-        }
-        // The expression yields a string, but the comparison value is an
-        // integer in case the comparison value is a float, integer, or numeric.
-        // All of the above SQLite format tokens only produce integers. However,
-        // the given $format may contain 'Y-m-d', which results in a string.
-        // @see \Drupal\Core\Database\Driver\sqlite\Connection::expandArguments()
-        // @see http://www.sqlite.org/lang_datefunc.html
-        // @see http://www.sqlite.org/lang_expr.html#castexpr
-        if (preg_match('/^(?:%\w)+$/', $format)) {
-          $expression = "CAST($expression AS NUMERIC)";
-        }
-        return $expression;
-    }
+    return $this->dateSql->getDateFormat($field, $format);
   }
 
 }
diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php
index 449474a..fbecf9f 100644
--- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php
+++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php
@@ -151,4 +151,9 @@ public function calculateDependencies() {
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function setFieldTimezoneOffset(&$field, $offset) {}
+
 }
diff --git a/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php b/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php
index 02a5ce6..d15a520 100644
--- a/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php
+++ b/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php
@@ -7,6 +7,7 @@
 use Drupal\Core\Entity\EntityType;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Tests\UnitTestCase;
+use Drupal\views\Plugin\views\query\DateSqlInterface;
 use Drupal\views\Plugin\views\query\Sql;
 use Drupal\views\Plugin\views\relationship\RelationshipPluginBase;
 use Drupal\views\ResultRow;
@@ -29,8 +30,9 @@ class SqlTest extends UnitTestCase {
   public function testGetCacheTags() {
     $view = $this->prophesize('Drupal\views\ViewExecutable')->reveal();
     $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
+    $date_sql = $this->prophesize(DateSqlInterface::class);
 
-    $query = new Sql([], 'sql', [], $entity_type_manager->reveal());
+    $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal());
     $query->view = $view;
 
     $result = [];
@@ -75,8 +77,9 @@ public function testGetCacheTags() {
   public function testGetCacheMaxAge() {
     $view = $this->prophesize('Drupal\views\ViewExecutable')->reveal();
     $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
+    $date_sql = $this->prophesize(DateSqlInterface::class);
 
-    $query = new Sql([], 'sql', [], $entity_type_manager->reveal());
+    $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal());
     $query->view = $view;
 
     $view->result = [];
@@ -249,8 +252,9 @@ public function testLoadEntitiesWithEmptyResult() {
     $view->storage = $view_entity->reveal();
 
     $entity_type_manager = $this->setupEntityTypes();
+    $date_sql = $this->prophesize(DateSqlInterface::class);
 
-    $query = new Sql([], 'sql', [], $entity_type_manager->reveal());
+    $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal());
     $query->view = $view;
 
     $result = [];
@@ -277,8 +281,9 @@ public function testLoadEntitiesWithNoRelationshipAndNoRevision() {
       ],
     ];
     $entity_type_manager = $this->setupEntityTypes($entities);
+    $date_sql = $this->prophesize(DateSqlInterface::class);
 
-    $query = new Sql([], 'sql', [], $entity_type_manager->reveal());
+    $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal());
     $query->view = $view;
 
     $result = [];
@@ -340,8 +345,9 @@ public function testLoadEntitiesWithRelationship() {
       ],
     ];
     $entity_type_manager = $this->setupEntityTypes($entities);
+    $date_sql = $this->prophesize(DateSqlInterface::class);
 
-    $query = new Sql([], 'sql', [], $entity_type_manager->reveal());
+    $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal());
     $query->view = $view;
 
     $result = [];
@@ -394,8 +400,9 @@ public function testLoadEntitiesWithNonEntityRelationship() {
       ],
     ];
     $entity_type_manager = $this->setupEntityTypes($entities);
+    $date_sql = $this->prophesize(DateSqlInterface::class);
 
-    $query = new Sql([], 'sql', [], $entity_type_manager->reveal());
+    $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal());
     $query->view = $view;
 
     $result = [];
@@ -444,8 +451,9 @@ public function testLoadEntitiesWithRevision() {
       ],
     ];
     $entity_type_manager = $this->setupEntityTypes([], $entity_revisions);
+    $date_sql = $this->prophesize(DateSqlInterface::class);
 
-    $query = new Sql([], 'sql', [], $entity_type_manager->reveal());
+    $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal());
     $query->view = $view;
 
     $result = [];
@@ -497,8 +505,9 @@ public function testLoadEntitiesWithRevisionOfSameEntityType() {
       ],
     ];
     $entity_type_manager = $this->setupEntityTypes($entity, $entity_revisions);
+    $date_sql = $this->prophesize(DateSqlInterface::class);
 
-    $query = new Sql([], 'sql', [], $entity_type_manager->reveal());
+    $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal());
     $query->view = $view;
 
     $result = [];
@@ -554,8 +563,9 @@ public function testLoadEntitiesWithRelationshipAndRevision() {
       ],
     ];
     $entity_type_manager = $this->setupEntityTypes($entities, $entity_revisions);
+    $date_sql = $this->prophesize(DateSqlInterface::class);
 
-    $query = new Sql([], 'sql', [], $entity_type_manager->reveal());
+    $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal());
     $query->view = $view;
 
     $result = [];
diff --git a/core/modules/views/views.services.yml b/core/modules/views/views.services.yml
index 1a01543..ac8ce14 100644
--- a/core/modules/views/views.services.yml
+++ b/core/modules/views/views.services.yml
@@ -80,3 +80,16 @@ services:
     arguments: ['@entity.manager']
     tags:
       - { name: 'event_subscriber' }
+  views.date_sql:
+    class: Drupal\datetime\GenericDateField
+    tags:
+       - { name: backend_overridable }
+  mysql.views.date_sql:
+    class: Drupal\views\Plugin\views\query\MysqlDateSql
+    arguments: ['@database']
+  pgsql.views.date_sql:
+    class: Drupal\views\Plugin\views\query\PostgresqlDateSql
+    arguments: ['@database']
+  sqlite.views.date_sql:
+    class: Drupal\views\Plugin\views\query\SqliteDateSql
+    arguments: ['@database']
