diff --git a/core/modules/datetime/datetime.views.inc b/core/modules/datetime/datetime.views.inc
index d3b0d18617d04b7343a9c668517e8ae41263bd6f..93d6cd4d30382d8386bf031180d470cb3a1784f1 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 3e7d461adff55a4b02b9548eede162ee66f1589e..4b8caa57c6aa1747f3c175506b7b09c519abd464 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 378f33fe84c80366cfe18f3faf4f5d8dcfb6dbb7..14215206c58ea2739a77ede2ee3869ae643cec87 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 = DateTimeItemInterface::DATETIME_STORAGE_FORMAT;
 
+  /**
+   * Determines if the timezone offset is calculated.
+   *
+   * @var bool
+   */
+  protected $calculateOffset = TRUE;
+
   /**
    * The request stack used to determin current time.
    *
@@ -67,10 +75,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 = DateTimeItemInterface::DATE_STORAGE_FORMAT;
+      // Timezone offset calculation is not applicable to dates that are stored
+      // as date-only.
+      $this->calculateOffset = FALSE;
     }
   }
 
@@ -91,20 +102,23 @@ 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->getTimezone();
+    $origin_offset = $this->getOffset($this->value['min'], $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';
 
     // 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', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", $this->dateFormat, TRUE);
-    $b = $this->query->getDateFormat("'" . $this->dateFormatter->format($b, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", $this->dateFormat, TRUE);
+    $a = new DateTimePlus($min, new \DateTimeZone($timezone));
+    $a = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format($a->getTimestamp() + $origin_offset, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", TRUE, $this->calculateOffset), $this->dateFormat, TRUE);
+    $b = new DateTimePlus($this->value['max'], new \DateTimeZone($timezone));
+    $b = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format($b->getTimestamp() + $origin_offset, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::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,15 +126,57 @@ 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->getTimezone();
+    $origin_offset = $this->getOffset($this->value['value'], $timezone);
 
-    // Convert to ISO. UTC is used since dates are stored in UTC.
-    $value = $this->query->getDateFormat("'" . $this->dateFormatter->format($value, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", $this->dateFormat, TRUE);
+    // Convert to ISO. UTC timezone is used since dates are stored in UTC.
+    $value = new DateTimePlus($this->value['value'], new \DateTimeZone($timezone));
+    $value = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format($value->getTimestamp() + $origin_offset, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::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");
   }
 
+  /**
+   * Get the proper time zone to use in computations.
+   *
+   * Date-only fields do not have a time zone associated with them, so the
+   * filter input needs to use UTC for reference. Otherwise, use the time zone
+   * for the current user.
+   *
+   * @return string
+   *   The time zone name.
+   */
+  protected function getTimezone() {
+    return $this->dateFormat === DateTimeItemInterface::DATE_STORAGE_FORMAT
+      ? DateTimeItemInterface::STORAGE_TIMEZONE
+      : drupal_get_user_timezone();
+  }
+
+  /**
+   * Get the proper offset from UTC to use in computations.
+   *
+   * @param string $time
+   *   A date/time string compatible with \DateTime. It is used as the
+   *   reference for computing the offset, which can vary based on the time
+   *   zone rules.
+   * @param string $timezone
+   *   The time zone that $time is in.
+   *
+   * @return int
+   *   The computed offset in seconds.
+   */
+  protected function getOffset($time, $timezone) {
+    // Date-only fields do not have a time zone or offset from UTC associated
+    // with them. For relative (i.e. 'offset') comparisons, we need to compute
+    // the user's offset from UTC for use in the query.
+    $origin_offset = 0;
+    if ($this->dateFormat === DateTimeItemInterface::DATE_STORAGE_FORMAT && $this->value['type'] === 'offset') {
+      $origin_offset = $origin_offset + timezone_offset_get(new \DateTimeZone(drupal_get_user_timezone()), new \DateTime($time, new \DateTimeZone($timezone)));
+    }
+
+    return $origin_offset;
+  }
+
 }
diff --git a/core/modules/datetime/src/Plugin/views/sort/Date.php b/core/modules/datetime/src/Plugin/views/sort/Date.php
index 2c8338ad259945bc72f94278c3b2669a9995ca26..0049e867feb7a1681e716e8dc319cd00f4132805 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/tests/src/Kernel/Views/ArgumentDateTimeTest.php b/core/modules/datetime/tests/src/Kernel/Views/ArgumentDateTimeTest.php
index 614548395876a8b1df5b6b275c7bcacaa46e740e..8261b27793f8555c3069110cf681c3bbf4426ca0 100644
--- a/core/modules/datetime/tests/src/Kernel/Views/ArgumentDateTimeTest.php
+++ b/core/modules/datetime/tests/src/Kernel/Views/ArgumentDateTimeTest.php
@@ -28,6 +28,9 @@ protected function setUp($import_test_views = TRUE) {
       '2000-10-10',
       '2001-10-10',
       '2002-01-01',
+      // Add a date that is the year 2002 in UTC, but 2003 in the site's time
+      // zone (Australia/Sydney).
+      '2002-12-31T23:00:00',
     ];
     foreach ($dates as $date) {
       $node = Node::create([
@@ -64,6 +67,25 @@ 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();
+
+    // Tests different system timezone with the same nodes.
+    $this->setSiteTimezone('America/Vancouver');
+
+    $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();
   }
 
   /**
@@ -87,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();
   }
@@ -112,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();
   }
@@ -157,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/tests/src/Kernel/Views/DateTimeHandlerTestBase.php b/core/modules/datetime/tests/src/Kernel/Views/DateTimeHandlerTestBase.php
index f0c9c786144adbd3ccf15f70df575aed2a78efeb..20a3542319f8919da8ffe173aa8f856d86bf0c9d 100644
--- a/core/modules/datetime/tests/src/Kernel/Views/DateTimeHandlerTestBase.php
+++ b/core/modules/datetime/tests/src/Kernel/Views/DateTimeHandlerTestBase.php
@@ -41,6 +41,7 @@ abstract class DateTimeHandlerTestBase extends ViewsKernelTestBase {
   protected function setUp($import_test_views = TRUE) {
     parent::setUp($import_test_views);
 
+    $this->installSchema('node', 'node_access');
     $this->installEntitySchema('node');
     $this->installEntitySchema('user');
 
@@ -76,4 +77,18 @@ protected function setUp($import_test_views = TRUE) {
     ViewTestData::createTestViews(get_class($this), ['datetime_test']);
   }
 
+  /**
+   * 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/tests/src/Kernel/Views/FilterDateTest.php b/core/modules/datetime/tests/src/Kernel/Views/FilterDateTest.php
index 23f40948e868c250b218fef2a1670458d13c9a1f..f4a6342956a7cf2fe0a45ca6363f5e5cdd3f8aaa 100644
--- a/core/modules/datetime/tests/src/Kernel/Views/FilterDateTest.php
+++ b/core/modules/datetime/tests/src/Kernel/Views/FilterDateTest.php
@@ -2,8 +2,8 @@
 
 namespace Drupal\Tests\datetime\Kernel\Views;
 
+use Drupal\Component\Datetime\DateTimePlus;
 use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
-use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\node\Entity\Node;
 use Drupal\views\Views;
@@ -21,9 +21,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}
@@ -33,30 +50,24 @@ class FilterDateTest extends DateTimeHandlerTestBase {
   protected function setUp($import_test_views = TRUE) {
     parent::setUp($import_test_views);
 
-    // 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', DateTimeItemInterface::DATE_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE),
-      // Today.
-      \Drupal::service('date.formatter')->format(static::$date, 'custom', DateTimeItemInterface::DATE_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE),
-      // Yesterday.
-      \Drupal::service('date.formatter')->format(static::$date - 86400, 'custom', DateTimeItemInterface::DATE_STORAGE_FORMAT, DateTimeItemInterface::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) {
       $node = Node::create([
         'title' => $this->randomMachineName(8),
         'type' => 'page',
         'field_date' => [
           'value' => $date,
-        ]
+        ],
       ]);
       $node->save();
       $this->nodes[] = $node;
@@ -70,48 +81,156 @@ 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() {
+    $user_now = new DateTimePlus('now', new \DateTimeZone(drupal_get_user_timezone()));
+    $utc_equivalent = new DateTimePlus($user_now->format('Y-m-d H:i:s'), new \DateTimeZone(DATETIME_STORAGE_TIMEZONE));
+
+    return $utc_equivalent->getTimestamp();
+  }
+
+  /**
+   * 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();
+    }
   }
 
 }
diff --git a/core/modules/datetime/tests/src/Kernel/Views/FilterDateTimeTest.php b/core/modules/datetime/tests/src/Kernel/Views/FilterDateTimeTest.php
index 5282da33ef7790ddf10dc6d0d5ba20009f98032f..7cb0fa040ab8c04651129e2c8e08a753febc84cf 100644
--- a/core/modules/datetime/tests/src/Kernel/Views/FilterDateTimeTest.php
+++ b/core/modules/datetime/tests/src/Kernel/Views/FilterDateTimeTest.php
@@ -40,6 +40,9 @@ protected function setUp($import_test_views = TRUE) {
 
     // 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/DateSqlInterface.php b/core/modules/views/src/Plugin/views/query/DateSqlInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..caa741939b300724e71af9a7778a818d931d426d
--- /dev/null
+++ b/core/modules/views/src/Plugin/views/query/DateSqlInterface.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\views\Plugin\views\query;
+
+/**
+ * Defines an interface for handling date queries with SQL.
+ *
+ * @internal
+ *   Classes implementing this interface should only be used by the Views SQL
+ *   query plugin.
+ *
+ * @see \Drupal\views\Plugin\views\query\Sql
+ */
+interface DateSqlInterface {
+
+  /**
+   * Returns a native database expression for a given field.
+   *
+   * @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.
+   *
+   * @return string
+   *   An expression representing a date field with timezone.
+   */
+  public function getDateField($field, $string_date);
+
+  /**
+   * Creates a native database date formatting.
+   *
+   * @param string $field
+   *   An appropriate query expression pointing to the date field.
+   * @param string $format
+   *   A format string for the result. For example: 'Y-m-d H:i:s'.
+   *
+   * @return string
+   *   A string representing the field formatted as a date as specified by
+   *   $format.
+   */
+  public function getDateFormat($field, $format);
+
+  /**
+   * Applies the given offset to the given field.
+   *
+   * @param string &$field
+   *   The date field in a string format.
+   * @param int $offset
+   *   The timezone offset in seconds.
+   */
+  public function setFieldTimezoneOffset(&$field, $offset);
+
+  /**
+   * Set the database to the given timezone.
+   *
+   * @param string $offset
+   *   The timezone.
+   */
+  public function setTimezoneOffset($offset);
+
+}
diff --git a/core/modules/views/src/Plugin/views/query/MysqlDateSql.php b/core/modules/views/src/Plugin/views/query/MysqlDateSql.php
new file mode 100644
index 0000000000000000000000000000000000000000..00ad873b83a8e076f56c1becc060c9cce61866ab
--- /dev/null
+++ b/core/modules/views/src/Plugin/views/query/MysqlDateSql.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Drupal\views\Plugin\views\query;
+
+use Drupal\Core\Database\Connection;
+
+/**
+ * MySQL-specific date handling.
+ *
+ * @internal
+ *   This class should only be used by the Views SQL query plugin.
+ *
+ * @see \Drupal\views\Plugin\views\query\Sql
+ */
+class MysqlDateSql implements DateSqlInterface {
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * An array of PHP-to-MySQL replacement patterns.
+   */
+  protected static $replace = [
+    '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',
+  ];
+
+  /**
+   * Constructs the MySQL-specific date sql class.
+   *
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database connection.
+   */
+  public function __construct(Connection $database) {
+    $this->database = $database;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDateField($field, $string_date) {
+    if ($string_date) {
+      return $field;
+    }
+
+    // Base date field storage is timestamp, so the date to be returned here is
+    // epoch + stored value (seconds from epoch).
+    return "DATE_ADD('19700101', INTERVAL $field SECOND)";
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDateFormat($field, $format) {
+    $format = strtr($format, static::$replace);
+    return "DATE_FORMAT($field, '$format')";
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTimezoneOffset($offset) {
+    $this->database->query("SET @@session.time_zone = '$offset'");
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setFieldTimezoneOffset(&$field, $offset) {
+    if (!empty($offset)) {
+      $field = "($field + INTERVAL $offset SECOND)";
+    }
+  }
+
+}
diff --git a/core/modules/views/src/Plugin/views/query/PostgresqlDateSql.php b/core/modules/views/src/Plugin/views/query/PostgresqlDateSql.php
new file mode 100644
index 0000000000000000000000000000000000000000..c03c416456a88a4d0eb8875c9d815ff105efef66
--- /dev/null
+++ b/core/modules/views/src/Plugin/views/query/PostgresqlDateSql.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Drupal\views\Plugin\views\query;
+
+use Drupal\Core\Database\Connection;
+
+/**
+ * PostgreSQL-specific date handling.
+ *
+ * @internal
+ *   This class should only be used by the Views SQL query plugin.
+ *
+ * @see \Drupal\views\Plugin\views\query\Sql
+ */
+class PostgresqlDateSql implements DateSqlInterface {
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * An array of PHP-to-PostgreSQL replacement patterns.
+   *
+   * @var array
+   */
+  protected static $replace = [
+    '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',
+  ];
+
+  /**
+   * Constructs the PostgreSQL-specific date sql class.
+   *
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database connection.
+   */
+  public function __construct(Connection $database) {
+    $this->database = $database;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDateField($field, $string_date) {
+    if ($string_date) {
+      // Ensures compatibility with field offset operation below.
+      return "TO_TIMESTAMP($field, 'YYYY-MM-DD HH24:MI:SS')";
+    }
+    return "TO_TIMESTAMP($field)";
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDateFormat($field, $format) {
+    $format = strtr($format, static::$replace);
+    return "TO_CHAR($field, '$format')";
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setFieldTimezoneOffset(&$field, $offset) {
+    $field = "($field + INTERVAL '$offset SECONDS')";
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTimezoneOffset($offset) {
+    $this->database->query("SET TIME ZONE INTERVAL '$offset' HOUR TO MINUTE");
+  }
+
+}
diff --git a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
index 35453c1172dd991b4ce5d15386e96ef668519ac2..83e0bcfa2403e04581f0937da4f9e462a0df7f7c 100644
--- a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
+++ b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
@@ -206,11 +206,17 @@ public 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 +352,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 7df6521b1cbb5529f419f4954197c5b871d79ff1..2771a3d7c035a53a6de91c488886b0fec8f7a24f 100644
--- a/core/modules/views/src/Plugin/views/query/Sql.php
+++ b/core/modules/views/src/Plugin/views/query/Sql.php
@@ -123,6 +123,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.
    *
@@ -134,11 +141,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) {
@@ -146,7 +156,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')
     );
   }
 
@@ -1762,175 +1773,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, ['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 = [
-          '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 = [
-          '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 = [
-          '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/src/Plugin/views/query/SqliteDateSql.php b/core/modules/views/src/Plugin/views/query/SqliteDateSql.php
new file mode 100644
index 0000000000000000000000000000000000000000..628e1c68faec5c2a3613bc710ce814cb37b009fb
--- /dev/null
+++ b/core/modules/views/src/Plugin/views/query/SqliteDateSql.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Drupal\views\Plugin\views\query;
+
+use Drupal\Core\Database\Connection;
+
+/**
+ * SQLite-specific date handling.
+ *
+ * @internal
+ *   This class should only be used by the Views SQL query plugin.
+ *
+ * @see \Drupal\views\Plugin\views\query\Sql
+ */
+class SqliteDateSql implements DateSqlInterface {
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * An array of PHP-to-SQLite date replacement patterns.
+   *
+   * @var array
+   */
+  protected static $replace = [
+    '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' => '',
+  ];
+
+  /**
+   * Constructs the SQLite-specific date sql class.
+   *
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database connection.
+   */
+  public function __construct(Connection $database) {
+    $this->database = $database;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDateField($field, $string_date) {
+    if ($string_date) {
+      $field = "strftime('%s', $field)";
+    }
+    return $field;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDateFormat($field, $format) {
+    $format = strtr($format, static::$replace);
+
+    // 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;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTimezoneOffset($offset) {
+    // Nothing to do here.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setFieldTimezoneOffset(&$field, $offset, $string_date = FALSE) {
+    if (!empty($offset)) {
+      $field = "($field + $offset)";
+    }
+  }
+
+}
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 cf430af25c094dee4b8857d18abd6720b269d8b8..8868f75de667a3d3941129c8819c814a10da798e 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 bae90240f0057d9cb59e186b9bbd21a642df0e5f..80e8a334ef6a6ba9fb120b245494c17efbe8b50e 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/tests/src/Unit/Plugin/views/query/MysqlDateSqlTest.php b/core/modules/views/tests/src/Unit/Plugin/views/query/MysqlDateSqlTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..575cc58cf690f4b1515310fd2b94b0bbf9286cb6
--- /dev/null
+++ b/core/modules/views/tests/src/Unit/Plugin/views/query/MysqlDateSqlTest.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Drupal\Tests\views\Unit\Plugin\views\query;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Tests\UnitTestCase;
+use Drupal\views\Plugin\views\query\MysqlDateSql;
+
+/**
+ * Tests the MySQL-specific date query handler.
+ *
+ * @coversDefaultClass \Drupal\views\Plugin\views\query\MysqlDateSql
+ *
+ * @group views
+ */
+class MysqlDateSqlTest extends UnitTestCase {
+
+  /**
+   * The mocked database service.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->database = $this->prophesize(Connection::class)->reveal();
+  }
+
+  /**
+   * Tests the getDateField method.
+   *
+   * @covers ::getDateField
+   */
+  public function testGetDateField() {
+    $date_sql = new MysqlDateSql($this->database);
+
+    $expected = 'foo.field';
+    $this->assertEquals($expected, $date_sql->getDateField('foo.field', TRUE));
+
+    $expected = "DATE_ADD('19700101', INTERVAL foo.field SECOND)";
+    $this->assertEquals($expected, $date_sql->getDateField('foo.field', FALSE));
+  }
+
+  /**
+   * Tests date formatting replacement.
+   *
+   * @covers ::getDateFormat
+   *
+   * @dataProvider providerTestGetDateFormat
+   */
+  public function testGetDateFormat($field, $format, $expected_format) {
+    $date_sql = new MysqlDateSql($this->database);
+
+    $this->assertEquals("DATE_FORMAT($field, '$expected_format')", $date_sql->getDateFormat($field, $format));
+  }
+
+  /**
+   * Provider for date formatting test.
+   */
+  public function providerTestGetDateFormat() {
+    return [
+      ['foo.field', 'Y-y-M-m', '%Y-%y-%b-%m'],
+      ['bar.field', 'n-F D d l', '%c-%M %a %d %W'],
+      ['baz.bar_field', 'j/W/H-h i s A', '%e/%v/%H-%h %i %s %p'],
+    ];
+  }
+
+  /**
+   * Tests timezone offset formatting.
+   *
+   * @covers ::setFieldTimezoneOffset
+   */
+  public function testSetFieldTimezoneOffset() {
+    $date_sql = new MysqlDateSql($this->database);
+
+    $field = 'foobar.field';
+    $date_sql->setFieldTimezoneOffset($field, 42);
+    $this->assertEquals("(foobar.field + INTERVAL 42 SECOND)", $field);
+  }
+
+  /**
+   * Tests setting the database offset.
+   *
+   * @covers ::setTimezoneOffset
+   */
+  public function testSetTimezoneOffset() {
+    $database = $this->prophesize(Connection::class);
+    $database->query("SET @@session.time_zone = '42'")->shouldBeCalledTimes(1);
+    $date_sql = new MysqlDateSql($database->reveal());
+    $date_sql->setTimezoneOffset(42);
+  }
+
+}
diff --git a/core/modules/views/tests/src/Unit/Plugin/views/query/PostgresqlDateSqlTest.php b/core/modules/views/tests/src/Unit/Plugin/views/query/PostgresqlDateSqlTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..14f367d261dce4e3d449898f13c4ce35cf4800ce
--- /dev/null
+++ b/core/modules/views/tests/src/Unit/Plugin/views/query/PostgresqlDateSqlTest.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Drupal\Tests\views\Unit\Plugin\views\query;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Tests\UnitTestCase;
+use Drupal\views\Plugin\views\query\PostgresqlDateSql;
+
+/**
+ * Tests the PostgreSQL-specific date query handler.
+ *
+ * @coversDefaultClass \Drupal\views\Plugin\views\query\PostgresqlDateSql
+ *
+ * @group views
+ */
+class PostgresqlDateSqlTest extends UnitTestCase {
+
+  /**
+   * The mocked database service.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->database = $this->prophesize(Connection::class)->reveal();
+  }
+
+  /**
+   * Tests the getDateField method.
+   *
+   * @covers ::getDateField
+   */
+  public function testGetDateField() {
+    $date_sql = new PostgresqlDateSql($this->database);
+
+    $expected = "TO_TIMESTAMP(foo.field, 'YYYY-MM-DD HH24:MI:SS')";
+    $this->assertEquals($expected, $date_sql->getDateField('foo.field', TRUE));
+
+    $expected = 'TO_TIMESTAMP(foo.field)';
+    $this->assertEquals($expected, $date_sql->getDateField('foo.field', FALSE));
+  }
+
+  /**
+   * Tests date formatting replacement.
+   *
+   * @covers ::getDateFormat
+   *
+   * @dataProvider providerTestGetDateFormat
+   */
+  public function testGetDateFormat($field, $format, $expected_format) {
+    $date_sql = new PostgresqlDateSql($this->database);
+
+    $this->assertEquals("TO_CHAR($field, '$expected_format')", $date_sql->getDateFormat($field, $format));
+  }
+
+  /**
+   * Provider for date formatting test.
+   */
+  public function providerTestGetDateFormat() {
+    return [
+      ['foo.field', 'Y-y-M-m', 'YYYY-YY-Mon-MM'],
+      ['bar.field', 'n-F D d l', 'MM-Month Dy DD Day'],
+      ['baz.bar_field', 'j/W/H-h i s A', 'DD/IW/HH24-HH12 MI SS AM'],
+    ];
+  }
+
+  /**
+   * Tests timezone offset formatting.
+   *
+   * @covers ::setFieldTimezoneOffset
+   */
+  public function testSetFieldTimezoneOffset() {
+    $date_sql = new PostgresqlDateSql($this->database);
+
+    $field = 'foobar.field';
+    $date_sql->setFieldTimezoneOffset($field, 42);
+    $this->assertEquals("(foobar.field + INTERVAL '42 SECONDS')", $field);
+  }
+
+  /**
+   * Tests setting the database offset.
+   *
+   * @covers ::setTimezoneOffset
+   */
+  public function testSetTimezoneOffset() {
+    $database = $this->prophesize(Connection::class);
+    $database->query("SET TIME ZONE INTERVAL '42' HOUR TO MINUTE")->shouldBeCalledTimes(1);
+    $date_sql = new PostgresqlDateSql($database->reveal());
+    $date_sql->setTimezoneOffset(42);
+  }
+
+}
diff --git a/core/modules/views/tests/src/Unit/Plugin/views/query/SqliteDateSqlTest.php b/core/modules/views/tests/src/Unit/Plugin/views/query/SqliteDateSqlTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..f29650796c5631bd14f443166ff75abfaa85c5cc
--- /dev/null
+++ b/core/modules/views/tests/src/Unit/Plugin/views/query/SqliteDateSqlTest.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Drupal\Tests\views\Unit\Plugin\views\query;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Tests\UnitTestCase;
+use Drupal\views\Plugin\views\query\SqliteDateSql;
+
+/**
+ * Tests the MySQL-specific date query handler.
+ *
+ * @coversDefaultClass \Drupal\views\Plugin\views\query\SqliteDateSql
+ *
+ * @group views
+ */
+class SqliteDateSqlTest extends UnitTestCase {
+
+  /**
+   * The mocked database service.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->database = $this->prophesize(Connection::class)->reveal();
+  }
+
+  /**
+   * Tests the getDateField method.
+   *
+   * @covers ::getDateField
+   */
+  public function testGetDateField() {
+    $date_sql = new SqliteDateSql($this->database);
+
+    $expected = "strftime('%s', foo.field)";
+    $this->assertEquals($expected, $date_sql->getDateField('foo.field', TRUE));
+
+    $expected = 'foo.field';
+    $this->assertEquals($expected, $date_sql->getDateField('foo.field', FALSE));
+  }
+
+  /**
+   * Tests date formatting replacement.
+   *
+   * @covers ::getDateFormat
+   *
+   * @dataProvider providerTestGetDateFormat
+   */
+  public function testGetDateFormat($field, $format, $expected) {
+    $date_sql = new SqliteDateSql($this->database);
+
+    $this->assertEquals($expected, $date_sql->getDateFormat($field, $format));
+  }
+
+  /**
+   * Provider for date formatting test.
+   */
+  public function providerTestGetDateFormat() {
+    return [
+      ['foo.field', 'Y-y-M-m', "strftime('%Y-%Y-%m-%m', foo.field, 'unixepoch')"],
+      ['bar.field', 'n-F D d l', "strftime('%m-%m %d %d %d', bar.field, 'unixepoch')"],
+      ['baz.bar_field', 'j/W/H-h i s A', "strftime('%d/%W/%H-%H %M %S ', baz.bar_field, 'unixepoch')"],
+      ['foo.field', 'W', "CAST(((strftime('%j', date(strftime('%Y-%m-%d', foo.field, 'unixepoch'), '-3 days', 'weekday 4')) - 1) / 7 + 1) AS NUMERIC)"]
+    ];
+  }
+
+  /**
+   * Tests timezone offset formatting.
+   *
+   * @covers ::setFieldTimezoneOffset
+   */
+  public function testSetFieldTimezoneOffset() {
+    $date_sql = new SqliteDateSql($this->database);
+
+    $field = 'foobar.field';
+    $date_sql->setFieldTimezoneOffset($field, 42);
+    $this->assertEquals("(foobar.field + 42)", $field);
+  }
+
+  /**
+   * Tests setting the database offset.
+   *
+   * @covers ::setTimezoneOffset
+   */
+  public function testSetTimezoneOffset() {
+    $database = $this->prophesize(Connection::class);
+    $database->query()->shouldNotBeCalled();
+    $date_sql = new SqliteDateSql($database->reveal());
+    $date_sql->setTimezoneOffset(42);
+  }
+
+}
diff --git a/core/modules/views/views.services.yml b/core/modules/views/views.services.yml
index 28f8d0d333d35882d1e6659203f87d4995da94ca..ffed39460f83d13e22cd38bd921b42e8af5a96ca 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\views\Plugin\views\query\MysqlDateSql
+    arguments: ['@database']
+    tags:
+       - { name: backend_overridable }
+  pgsql.views.date_sql:
+    class: Drupal\views\Plugin\views\query\PostgresqlDateSql
+    arguments: ['@database']
+    public: false
+  sqlite.views.date_sql:
+    class: Drupal\views\Plugin\views\query\SqliteDateSql
+    arguments: ['@database']
+    public: false