From 5a80e561019b256674959d629afb009517f835c4 Mon Sep 17 00:00:00 2001
From: Ngoc Khuat <qn.khuat@gmail.com>
Date: Thu, 29 Sep 2022 10:07:05 +0700
Subject: [PATCH] Advanced datetime extraction (#25277)

* Implement advanced date/time/zone manipulation, part 1

Incorporate new functions into MBQL and add tests:
 - get-year
 - get-quarter
 - get-month
 - get-day
 - get-day-of-week
 - get-hour
 - get-minute
 - get-second

* Fix BigQuery implementations to call extract

Mark as not supported in legacy driver

* Add date extraction fns for Postgres

* Disable in MongoDB (for now at least)

Disable in BigQuery (legacy driver)

Add implementations for presto-jdbc

* Misc cleanup from Jeff's PR

* Update Jeff's implementation of bigquery-cloud-sqk

* Reorganized tests

* Mongo

* Oracle

* Sqlserver

* Sqlite

* Add casting supports for presto

* Remove Jeff's implementation of presto-jdbc because  its parent is
sql-jdbc

* Update presto-jdbc tests to use the same catalog for all datasets

* Add date extraction functions to the expression editor (#25382)

* make sure the semantic type of aggregated columns are integer

* no recursive call in annotate for date-extract func

* get-unit -> temporal-extract(column, unit)

* desguar nested datetime extraction too
---
 .../run-presto-kerberos-integration-test.sh   |   1 +
 .../src/metabase/lib/expressions/config.js    |  49 ++++++
 .../lib/expressions/helper-text-strings.ts    |  96 ++++++++++++
 .../custom-column/cc-data-type.cy.spec.js     |  33 ++++
 .../bigquery_cloud_sdk/query_processor.clj    |  26 ++--
 .../mongo/src/metabase/driver/mongo.clj       |  16 +-
 .../metabase/driver/mongo/query_processor.clj |  52 ++++---
 .../oracle/src/metabase/driver/oracle.clj     |  27 ++--
 .../test/metabase/driver/presto_jdbc_test.clj |  22 +--
 .../test/metabase/test/data/presto_jdbc.clj   |  16 +-
 .../presto/src/metabase/driver/presto.clj     |  10 ++
 .../sqlite/src/metabase/driver/sqlite.clj     |   8 +
 .../src/metabase/driver/sqlserver.clj         |   8 +
 shared/src/metabase/mbql/normalize.cljc       |   4 +
 shared/src/metabase/mbql/predicates.cljc      |   4 +
 shared/src/metabase/mbql/schema.cljc          | 100 ++++++++++--
 shared/src/metabase/mbql/util.cljc            |  24 ++-
 shared/test/metabase/mbql/util_test.cljc      |  17 ++-
 src/metabase/driver.clj                       |   5 +
 src/metabase/driver/postgres.clj              |  28 ++--
 src/metabase/driver/sql/query_processor.clj   |  26 +++-
 .../query_processor/middleware/desugar.clj    |   9 +-
 src/metabase/util/date_2.clj                  |   4 +-
 src/metabase/util/honeysql_extensions.clj     |   5 +-
 .../automagic_dashboards/core_test.clj        |   2 +-
 .../middleware/desugar_test.clj               |   8 +-
 .../date_time_zone_functions_test.clj         | 144 ++++++++++++++++++
 27 files changed, 639 insertions(+), 105 deletions(-)
 create mode 100644 test/metabase/query_processor_test/date_time_zone_functions_test.clj

diff --git a/.github/scripts/run-presto-kerberos-integration-test.sh b/.github/scripts/run-presto-kerberos-integration-test.sh
index 093841be8c5..e1f385a7a74 100755
--- a/.github/scripts/run-presto-kerberos-integration-test.sh
+++ b/.github/scripts/run-presto-kerberos-integration-test.sh
@@ -3,6 +3,7 @@
 set -eo pipefail
 
 # Need Java commands on $PATH, which apparently is not yet the case
+export JAVA_HOME="/usr/lib/jvm/msopenjdk-current"
 export PATH="$PATH:$JAVA_HOME/bin"
 
 # ensure java commmand is available
diff --git a/frontend/src/metabase/lib/expressions/config.js b/frontend/src/metabase/lib/expressions/config.js
index 2acf81fd527..a952ecac3af 100644
--- a/frontend/src/metabase/lib/expressions/config.js
+++ b/frontend/src/metabase/lib/expressions/config.js
@@ -321,6 +321,46 @@ export const MBQL_CLAUSES = {
     type: "boolean",
     args: ["expression", "expression"],
   },
+  "get-year": {
+    displayName: `year`,
+    type: "number",
+    args: ["expression"],
+  },
+  "get-quarter": {
+    displayName: `quarter`,
+    type: "number",
+    args: ["expression"],
+  },
+  "get-month": {
+    displayName: `month`,
+    type: "number",
+    args: ["expression"],
+  },
+  "get-day": {
+    displayName: `day`,
+    type: "number",
+    args: ["expression"],
+  },
+  "get-day-of-week": {
+    displayName: `weekday`,
+    type: "number",
+    args: ["expression"],
+  },
+  "get-hour": {
+    displayName: `hour`,
+    type: "number",
+    args: ["expression"],
+  },
+  "get-minute": {
+    displayName: `minute`,
+    type: "number",
+    args: ["expression"],
+  },
+  "get-second": {
+    displayName: `second`,
+    type: "number",
+    args: ["expression"],
+  },
 };
 
 for (const [name, clause] of Object.entries(MBQL_CLAUSES)) {
@@ -392,6 +432,15 @@ export const EXPRESSION_FUNCTIONS = new Set([
   "power",
   "log",
   "exp",
+  // date/time
+  "get-year",
+  "get-quarter",
+  "get-month",
+  "get-day",
+  "get-day-of-week",
+  "get-hour",
+  "get-minute",
+  "get-second",
   // boolean
   "contains",
   "ends-with",
diff --git a/frontend/src/metabase/lib/expressions/helper-text-strings.ts b/frontend/src/metabase/lib/expressions/helper-text-strings.ts
index a702af8a7e9..3580e154594 100644
--- a/frontend/src/metabase/lib/expressions/helper-text-strings.ts
+++ b/frontend/src/metabase/lib/expressions/helper-text-strings.ts
@@ -571,6 +571,102 @@ const helperTextStrings: HelpText[] = [
     ],
     hasDocsPage: true,
   },
+  {
+    name: "get-year",
+    structure: "year(" + t`column` + ")",
+    description: t`Takes a datetime and returns an integer with the number of the year.`,
+    example: "year([" + t`Created At` + "])",
+    args: [
+      {
+        name: t`column`,
+        description: t`The datetime column.`,
+      },
+    ],
+  },
+  {
+    name: "get-quarter",
+    structure: "quarter(" + t`column` + ")",
+    description: t`Takes a datetime and returns an integer (1-4) with the number of the quarter in the year.`,
+    example: "quarter([" + t`Created At` + "])",
+    args: [
+      {
+        name: t`column`,
+        description: t`The datetime column.`,
+      },
+    ],
+  },
+  {
+    name: "get-month",
+    structure: "month(" + t`column` + ")",
+    description: t`Takes a datetime and returns an integer (1-12) with the number of the month in the year.`,
+    example: "month([" + t`Created At` + "])",
+    args: [
+      {
+        name: t`column`,
+        description: t`The datetime column.`,
+      },
+    ],
+  },
+  {
+    name: "get-day",
+    structure: "day(" + t`column` + ")",
+    description: t`Takes a datetime and returns an integer (1-31) with the number of the day of the month.`,
+    example: "day([" + t`Created At` + "])",
+    args: [
+      {
+        name: t`column`,
+        description: t`The datetime column.`,
+      },
+    ],
+  },
+  {
+    name: "get-day-of-week",
+    structure: "weekday(" + t`column` + ")",
+    description: t`Takes a datetime and returns an integer (1-7) with the number of the day of the week.`,
+    example: "weekday([" + t`Created At` + "])",
+    args: [
+      {
+        name: t`column`,
+        description: t`The datetime column.`,
+      },
+    ],
+  },
+  {
+    name: "get-hour",
+    structure: "hour(" + t`column` + ")",
+    description: t`Takes a datetime and returns an integer (0-23) with the number of the hour. No AM/PM.`,
+    example: "hour([" + t`Created At` + "])",
+    args: [
+      {
+        name: t`column`,
+        description: t`The datetime column.`,
+      },
+    ],
+  },
+  {
+    name: "get-minute",
+    structure: "minute(" + t`column` + ")",
+    description: t`Takes a datetime and returns an integer (0-59) with the number of the minute in the hour.`,
+    example: "minute([" + t`Created At` + "])",
+    args: [
+      {
+        name: t`column`,
+        description: t`The datetime column.`,
+      },
+    ],
+  },
+  {
+    name: "get-second",
+    structure: "second(" + t`column` + ")",
+    description: t`Takes a datetime and returns an integer (0-59) with the number of the seconds in the minute.`,
+    example: "second([" + t`Created At` + "])",
+    args: [
+      {
+        name: t`column`,
+        description: t`The datetime column.`,
+      },
+    ],
+  },
 ];
 
 export const getHelpText = (name: string): HelpText | undefined => {
diff --git a/frontend/test/metabase/scenarios/custom-column/cc-data-type.cy.spec.js b/frontend/test/metabase/scenarios/custom-column/cc-data-type.cy.spec.js
index 0b5f94226a5..f3e70629074 100644
--- a/frontend/test/metabase/scenarios/custom-column/cc-data-type.cy.spec.js
+++ b/frontend/test/metabase/scenarios/custom-column/cc-data-type.cy.spec.js
@@ -4,6 +4,9 @@ import {
   popover,
   enterCustomColumnDetails,
   filter,
+  openOrdersTable,
+  visualize,
+  getNotebookStep,
 } from "__support__/e2e/helpers";
 
 import { SAMPLE_DATABASE } from "__support__/e2e/cypress_sample_database";
@@ -34,6 +37,23 @@ describe("scenarios > question > custom column > data type", () => {
     cy.findByPlaceholderText("Enter some text");
   });
 
+  it("should understand date functions", () => {
+    openOrdersTable({ mode: "notebook" });
+
+    addCustomColumns([
+      { name: "Year", formula: "year([Created At])" },
+      { name: "Quarter", formula: "quarter([Created At])" },
+      { name: "Month", formula: "month([Created At])" },
+      { name: "Day", formula: "day([Created At])" },
+      { name: "Weekday", formula: "weekday([Created At])" },
+      { name: "Hour", formula: "hour([Created At])" },
+      { name: "Minute", formula: "minute([Created At])" },
+      { name: "Second", formula: "second([Created At])" },
+    ]);
+
+    visualize();
+  });
+
   it("should relay the type of a date field", () => {
     openCustomColumnInTable(PEOPLE_ID);
 
@@ -89,6 +109,19 @@ describe("scenarios > question > custom column > data type", () => {
   });
 });
 
+function addCustomColumns(columns) {
+  cy.wrap(columns).each((column, index) => {
+    if (index) {
+      getNotebookStep("expression").within(() => cy.icon("add").click());
+    } else {
+      cy.findByText("Custom column").click();
+    }
+
+    enterCustomColumnDetails(column);
+    cy.button("Done").click();
+  });
+}
+
 function openCustomColumnInTable(table) {
   openTable({ table, mode: "notebook" });
   cy.findByText("Custom column").click();
diff --git a/modules/drivers/bigquery-cloud-sdk/src/metabase/driver/bigquery_cloud_sdk/query_processor.clj b/modules/drivers/bigquery-cloud-sdk/src/metabase/driver/bigquery_cloud_sdk/query_processor.clj
index eff072fb28d..3461503c7a4 100644
--- a/modules/drivers/bigquery-cloud-sdk/src/metabase/driver/bigquery_cloud_sdk/query_processor.clj
+++ b/modules/drivers/bigquery-cloud-sdk/src/metabase/driver/bigquery_cloud_sdk/query_processor.clj
@@ -371,18 +371,20 @@
     ;; for datetimes or anything without a known temporal type, cast to timestamp and go from there
     (recur unit (->temporal-type :timestamp expr))))
 
-(defmethod sql.qp/date [:bigquery-cloud-sdk :minute]          [_ _ expr] (trunc   :minute    expr))
-(defmethod sql.qp/date [:bigquery-cloud-sdk :minute-of-hour]  [_ _ expr] (extract :minute    expr))
-(defmethod sql.qp/date [:bigquery-cloud-sdk :hour]            [_ _ expr] (trunc   :hour      expr))
-(defmethod sql.qp/date [:bigquery-cloud-sdk :hour-of-day]     [_ _ expr] (extract :hour      expr))
-(defmethod sql.qp/date [:bigquery-cloud-sdk :day]             [_ _ expr] (trunc   :day       expr))
-(defmethod sql.qp/date [:bigquery-cloud-sdk :day-of-month]    [_ _ expr] (extract :day       expr))
-(defmethod sql.qp/date [:bigquery-cloud-sdk :day-of-year]     [_ _ expr] (extract :dayofyear expr))
-(defmethod sql.qp/date [:bigquery-cloud-sdk :month]           [_ _ expr] (trunc   :month     expr))
-(defmethod sql.qp/date [:bigquery-cloud-sdk :month-of-year]   [_ _ expr] (extract :month     expr))
-(defmethod sql.qp/date [:bigquery-cloud-sdk :quarter]         [_ _ expr] (trunc   :quarter   expr))
-(defmethod sql.qp/date [:bigquery-cloud-sdk :quarter-of-year] [_ _ expr] (extract :quarter   expr))
-(defmethod sql.qp/date [:bigquery-cloud-sdk :year]            [_ _ expr] (trunc   :year      expr))
+(defmethod sql.qp/date [:bigquery-cloud-sdk :second-of-minute] [_ _ expr] (extract :second    expr))
+(defmethod sql.qp/date [:bigquery-cloud-sdk :minute]           [_ _ expr] (trunc   :minute    expr))
+(defmethod sql.qp/date [:bigquery-cloud-sdk :minute-of-hour]   [_ _ expr] (extract :minute    expr))
+(defmethod sql.qp/date [:bigquery-cloud-sdk :hour]             [_ _ expr] (trunc   :hour      expr))
+(defmethod sql.qp/date [:bigquery-cloud-sdk :hour-of-day]      [_ _ expr] (extract :hour      expr))
+(defmethod sql.qp/date [:bigquery-cloud-sdk :day]              [_ _ expr] (trunc   :day       expr))
+(defmethod sql.qp/date [:bigquery-cloud-sdk :day-of-month]     [_ _ expr] (extract :day       expr))
+(defmethod sql.qp/date [:bigquery-cloud-sdk :day-of-year]      [_ _ expr] (extract :dayofyear expr))
+(defmethod sql.qp/date [:bigquery-cloud-sdk :month]            [_ _ expr] (trunc   :month     expr))
+(defmethod sql.qp/date [:bigquery-cloud-sdk :month-of-year]    [_ _ expr] (extract :month     expr))
+(defmethod sql.qp/date [:bigquery-cloud-sdk :quarter]          [_ _ expr] (trunc   :quarter   expr))
+(defmethod sql.qp/date [:bigquery-cloud-sdk :quarter-of-year]  [_ _ expr] (extract :quarter   expr))
+(defmethod sql.qp/date [:bigquery-cloud-sdk :year]             [_ _ expr] (trunc   :year      expr))
+(defmethod sql.qp/date [:bigquery-cloud-sdk :year-of-era]      [_ _ expr] (extract :year      expr))
 
 ;; BigQuery mod is a function like mod(x, y) rather than an operator like x mod y
 (defmethod hformat/fn-handler (u/qualified-name ::mod)
diff --git a/modules/drivers/mongo/src/metabase/driver/mongo.clj b/modules/drivers/mongo/src/metabase/driver/mongo.clj
index e4fd88c9730..84c511e066e 100644
--- a/modules/drivers/mongo/src/metabase/driver/mongo.clj
+++ b/modules/drivers/mongo/src/metabase/driver/mongo.clj
@@ -156,14 +156,14 @@
 (defn- describe-table-field [field-kw field-info idx]
   (let [most-common-object-type  (most-common-object-type (vec (:types field-info)))
         [nested-fields idx-next]
-          (reduce
-           (fn [[nested-fields idx] nested-field]
-             (let [[nested-field idx-next] (describe-table-field nested-field
-                                                                 (nested-field (:nested-fields field-info))
-                                                                 idx)]
-               [(conj nested-fields nested-field) idx-next]))
-           [#{} (inc idx)]
-           (keys (:nested-fields field-info)))]
+        (reduce
+         (fn [[nested-fields idx] nested-field]
+           (let [[nested-field idx-next] (describe-table-field nested-field
+                                                               (nested-field (:nested-fields field-info))
+                                                               idx)]
+             [(conj nested-fields nested-field) idx-next]))
+         [#{} (inc idx)]
+         (keys (:nested-fields field-info)))]
     [(cond-> {:name              (name field-kw)
               :database-type     (some-> most-common-object-type .getName)
               :base-type         (class->base-type most-common-object-type)
diff --git a/modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj b/modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj
index f44c4f80bca..ef8a4aba035 100644
--- a/modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj
+++ b/modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj
@@ -21,8 +21,8 @@
             [metabase.util.schema :as su]
             [monger.operators :refer [$add $addToSet $and $avg $cond $dayOfMonth $dayOfWeek $dayOfYear $divide $eq
                                       $group $gt $gte $hour $limit $lt $lte $match $max $min $minute $mod $month
-                                      $multiply $ne $not $or $project $regex $size $skip $sort $strcasecmp $subtract
-                                      $sum $toLower]]
+                                      $multiply $ne $not $or $project $regex $second $size $skip $sort $strcasecmp $subtract
+                                      $sum $toLower $year]]
             [schema.core :as s])
   (:import org.bson.types.ObjectId))
 
@@ -200,20 +200,21 @@
       (letfn [(truncate [unit]
                 (truncate-to-resolution column unit))]
         (case unit
-          :default        column
-          :minute         (truncate :minute)
-          :minute-of-hour {$minute column}
-          :hour           (truncate :hour)
-          :hour-of-day    {$hour column}
-          :day            (truncate :day)
-          :day-of-week    (day-of-week column)
-          :day-of-month   {$dayOfMonth column}
-          :day-of-year    {$dayOfYear column}
-          :week           (truncate-to-resolution (week column) :day)
-          :week-of-year   {:$ceil {$divide [{$dayOfYear (week column)}
-                                            7.0]}}
-          :month          (truncate :month)
-          :month-of-year  {$month column}
+          :default          column
+          :second-of-minute {$second column}
+          :minute           (truncate :minute)
+          :minute-of-hour   {$minute column}
+          :hour             (truncate :hour)
+          :hour-of-day      {$hour column}
+          :day              (truncate :day)
+          :day-of-week      (day-of-week column)
+          :day-of-month     {$dayOfMonth column}
+          :day-of-year      {$dayOfYear column}
+          :week             (truncate-to-resolution (week column) :day)
+          :week-of-year     {:$ceil {$divide [{$dayOfYear (week column)}
+                                              7.0]}}
+          :month            (truncate :month)
+          :month-of-year    {$month column}
           ;; For quarter we'll just subtract enough days from the current date to put it in the correct month and
           ;; stringify it as yyyy-MM Subtracting (($dayOfYear(column) % 91) - 3) days will put you in correct month.
           ;; Trust me.
@@ -233,7 +234,10 @@
                       3]})
 
           :year
-          (truncate :year))))))
+          (truncate :year)
+
+          :year-of-era
+          {$year column})))))
 
 (defmethod ->rvalue :field
   [[_ id-or-name {:keys [temporal-unit]}]]
@@ -370,6 +374,20 @@
 (defmethod ->rvalue :concat    [[_ & args]] {"$concat" (mapv ->rvalue args)})
 (defmethod ->rvalue :substring [[_ & args]] {"$substrCP" (mapv ->rvalue args)})
 
+(def ^:private temporal-extract-unit->date-unit
+  {:second      :second-of-minute
+   :minute      :minute-of-hour
+   :hour        :hour-of-day
+   :day-of-week :day-of-week
+   :day         :day-of-month
+   :week        :week-of-year
+   :month       :month-of-year
+   :quarter     :quarter-of-year
+   :year        :year-of-era})
+
+(defmethod ->rvalue :temporal-extract [[_ inp unit]]
+  (with-rvalue-temporal-bucketing (->rvalue inp) (temporal-extract-unit->date-unit unit)))
+
 ;;; Intervals are not first class Mongo citizens, so they cannot be translated on their own.
 ;;; The only thing we can do with them is adding to or subtracting from a date valued expression.
 ;;; Also, date arithmetic with intervals was first implemented in version 5. (Before that only
diff --git a/modules/drivers/oracle/src/metabase/driver/oracle.clj b/modules/drivers/oracle/src/metabase/driver/oracle.clj
index 0de45ed0b2a..dd92a6e76bd 100644
--- a/modules/drivers/oracle/src/metabase/driver/oracle.clj
+++ b/modules/drivers/oracle/src/metabase/driver/oracle.clj
@@ -157,18 +157,25 @@
       ;; trunc() returns a date -- see https://docs.oracle.com/cd/E11882_01/server.112/e10729/ch4datetime.htm#NLSPG253
       (hx/with-database-type-info "date")))
 
-(defmethod sql.qp/date [:oracle :minute]         [_ _ v] (trunc :mi v))
+(defmethod sql.qp/date [:oracle :second-of-minute] [_ _ v] (->> v
+                                                                hx/->timestamp
+                                                                (hsql/call :extract :second)
+                                                                (hsql/call :floor)
+                                                                hx/->integer))
+
+(defmethod sql.qp/date [:oracle :minute]           [_ _ v] (trunc :mi v))
 ;; you can only extract minute + hour from TIMESTAMPs, even though DATEs still have them (WTF), so cast first
-(defmethod sql.qp/date [:oracle :minute-of-hour] [_ _ v] (hsql/call :extract :minute (hx/->timestamp v)))
-(defmethod sql.qp/date [:oracle :hour]           [_ _ v] (trunc :hh v))
-(defmethod sql.qp/date [:oracle :hour-of-day]    [_ _ v] (hsql/call :extract :hour (hx/->timestamp v)))
-(defmethod sql.qp/date [:oracle :day]            [_ _ v] (trunc :dd v))
-(defmethod sql.qp/date [:oracle :day-of-month]   [_ _ v] (hsql/call :extract :day v))
+(defmethod sql.qp/date [:oracle :minute-of-hour]   [_ _ v] (hsql/call :extract :minute (hx/->timestamp v)))
+(defmethod sql.qp/date [:oracle :hour]             [_ _ v] (trunc :hh v))
+(defmethod sql.qp/date [:oracle :hour-of-day]      [_ _ v] (hsql/call :extract :hour (hx/->timestamp v)))
+(defmethod sql.qp/date [:oracle :day]              [_ _ v] (trunc :dd v))
+(defmethod sql.qp/date [:oracle :day-of-month]     [_ _ v] (hsql/call :extract :day v))
 ;; [SIC] The format template for truncating to start of week is 'day' in Oracle #WTF
-(defmethod sql.qp/date [:oracle :month]          [_ _ v] (trunc :month v))
-(defmethod sql.qp/date [:oracle :month-of-year]  [_ _ v] (hsql/call :extract :month v))
-(defmethod sql.qp/date [:oracle :quarter]        [_ _ v] (trunc :q v))
-(defmethod sql.qp/date [:oracle :year]           [_ _ v] (trunc :year v))
+(defmethod sql.qp/date [:oracle :month]            [_ _ v] (trunc :month v))
+(defmethod sql.qp/date [:oracle :month-of-year]    [_ _ v] (hsql/call :extract :month v))
+(defmethod sql.qp/date [:oracle :quarter]          [_ _ v] (trunc :q v))
+(defmethod sql.qp/date [:oracle :year]             [_ _ v] (trunc :year v))
+(defmethod sql.qp/date [:oracle :year-of-era]      [_ _ v] (hsql/call :extract :year v))
 
 (defmethod sql.qp/date [:oracle :week]
   [driver _ v]
diff --git a/modules/drivers/presto-jdbc/test/metabase/driver/presto_jdbc_test.clj b/modules/drivers/presto-jdbc/test/metabase/driver/presto_jdbc_test.clj
index ebd1baaacb0..61317822ec1 100644
--- a/modules/drivers/presto-jdbc/test/metabase/driver/presto_jdbc_test.clj
+++ b/modules/drivers/presto-jdbc/test/metabase/driver/presto_jdbc_test.clj
@@ -26,20 +26,20 @@
 
 (deftest describe-database-test
   (mt/test-driver :presto-jdbc
-    (is (= {:tables #{{:name "categories" :schema "default"}
-                      {:name "venues" :schema "default"}
-                      {:name "checkins" :schema "default"}
-                      {:name "users" :schema "default"}}}
+    (is (= {:tables #{{:name "test_data_categories" :schema "default"}
+                      {:name "test_data_venues" :schema "default"}
+                      {:name "test_data_checkins" :schema "default"}
+                      {:name "test_data_users" :schema "default"}}}
            (-> (driver/describe-database :presto-jdbc (mt/db))
-               (update :tables (comp set (partial filter (comp #{"categories"
-                                                                 "venues"
-                                                                 "checkins"
-                                                                 "users"}
+               (update :tables (comp set (partial filter (comp #{"test_data_categories"
+                                                                 "test_data_venues"
+                                                                 "test_data_checkins"
+                                                                 "test_data_users"}
                                                                :name)))))))))
 
 (deftest describe-table-test
   (mt/test-driver :presto-jdbc
-    (is (= {:name   "venues"
+    (is (= {:name   "test_data_venues"
             :schema "default"
             :fields #{{:name          "name",
                        ;; for HTTP based Presto driver, this is coming back as varchar(255)
@@ -135,8 +135,8 @@
                    :filter      [:= $name "wow"]})]
       (testing "The native query returned in query results should use user-friendly splicing"
         (is (= (str "SELECT count(*) AS \"count\" "
-                    "FROM \"default\".\"venues\" "
-                    "WHERE \"default\".\"venues\".\"name\" = 'wow'")
+                    "FROM \"default\".\"test_data_venues\" "
+                    "WHERE \"default\".\"test_data_venues\".\"name\" = 'wow'")
                (:query (qp/compile-and-splice-parameters query))
                (-> (qp/process-query query) :data :native_form :query)))))))
 
diff --git a/modules/drivers/presto-jdbc/test/metabase/test/data/presto_jdbc.clj b/modules/drivers/presto-jdbc/test/metabase/test/data/presto_jdbc.clj
index 84fe3abce21..ab99a02c6dd 100644
--- a/modules/drivers/presto-jdbc/test/metabase/test/data/presto_jdbc.clj
+++ b/modules/drivers/presto-jdbc/test/metabase/test/data/presto_jdbc.clj
@@ -24,6 +24,12 @@
 ;; during unit tests don't treat presto as having FK support
 (defmethod driver/supports? [:presto-jdbc :foreign-keys] [_ _] (not config/is-test?))
 
+;; in the past, we had to manually update our Docker image and add a new catalog for every new dataset definition we
+;; added. That's insane. Just use the `test-data` catalog and put everything in that, and use
+;; `db-qualified-table-name` like everyone else.
+(def ^:private test-catalog-name "test_data")
+
+
 (doseq [[base-type db-type] {:type/BigInteger             "BIGINT"
                              :type/Boolean                "BOOLEAN"
                              :type/Date                   "DATE"
@@ -39,7 +45,7 @@
                              :type/TimeWithTZ             "TIME WITH TIME ZONE"}]
   (defmethod sql.tx/field-base-type->sql-type [:presto-jdbc base-type] [_ _] db-type))
 
-(defn dbdef->connection-details [database-name]
+(defn dbdef->connection-details [_database-name]
   (let [base-details
         {:host                               (tx/db-test-env-var-or-throw :presto-jdbc :host "localhost")
          :port                               (tx/db-test-env-var :presto-jdbc :port "8080")
@@ -58,7 +64,7 @@
          :kerberos-keytab-path               (tx/db-test-env-var :presto-jdbc :kerberos-keytab-path nil)
          :kerberos-config-path               (tx/db-test-env-var :presto-jdbc :kerberos-config-path nil)
          :kerberos-service-principal-pattern (tx/db-test-env-var :presto-jdbc :kerberos-service-principal-pattern nil)
-         :catalog                            (u/snake-key database-name)
+         :catalog                            test-catalog-name
          :schema                             (tx/db-test-env-var :presto-jdbc :schema nil)}]
     (assoc base-details
            :ssl-use-keystore (every? some? (map base-details [:ssl-keystore-path :ssl-keystore-password-value]))
@@ -124,9 +130,9 @@
 
 (defmethod sql.tx/qualified-name-components :presto-jdbc
   ;; use the default schema from the in-memory connector
-  ([_ db-name]                       [(u/snake-key db-name) "default"])
-  ([_ db-name table-name]            [(u/snake-key db-name) "default" (u/snake-key table-name)])
-  ([_ db-name table-name field-name] [(u/snake-key db-name) "default" (u/snake-key table-name) field-name]))
+  ([_ _db-name]                       [test-catalog-name "default"])
+  ([_ db-name table-name]            [test-catalog-name "default" (tx/db-qualified-table-name db-name table-name)])
+  ([_ db-name table-name field-name] [test-catalog-name "default" (tx/db-qualified-table-name db-name table-name) field-name]))
 
 (defmethod sql.tx/pk-sql-type :presto-jdbc
   [_]
diff --git a/modules/drivers/presto/src/metabase/driver/presto.clj b/modules/drivers/presto/src/metabase/driver/presto.clj
index 8d9ff111618..7d462473bdb 100644
--- a/modules/drivers/presto/src/metabase/driver/presto.clj
+++ b/modules/drivers/presto/src/metabase/driver/presto.clj
@@ -9,6 +9,7 @@
             [metabase.driver :as driver]
             [metabase.driver.presto-common :as presto-common]
             [metabase.driver.sql-jdbc.sync.describe-database :as sql-jdbc.describe-database]
+            [metabase.driver.sql.query-processor :as sql.qp]
             [metabase.driver.sql.util :as sql.u]
             [metabase.driver.sql.util.unprepare :as unprepare]
             [metabase.query-processor.context :as qp.context]
@@ -17,6 +18,7 @@
             [metabase.query-processor.util :as qp.util]
             [metabase.util :as u]
             [metabase.util.date-2 :as u.date]
+            [metabase.util.honeysql-extensions :as hx]
             [metabase.util.i18n :refer [trs tru]]
             [metabase.util.schema :as su]
             [metabase.util.ssh :as ssh]
@@ -24,6 +26,14 @@
 
 (driver/register! :presto, :parent :presto-common)
 
+(defmethod sql.qp/cast-temporal-string [:presto :Coercion/ISO8601->DateTime]
+  [_ _coercion-strategy expr]
+  (hx/->timestamp expr))
+
+(defmethod sql.qp/cast-temporal-string [:presto :Coercion/ISO8601->Date]
+  [_ _coercion-strategy expr]
+  (hx/->date expr))
+
 ;;; Presto API helpers
 
 (def ^:private PrestoConnectionDetails
diff --git a/modules/drivers/sqlite/src/metabase/driver/sqlite.clj b/modules/drivers/sqlite/src/metabase/driver/sqlite.clj
index 97eda18cb28..1695b1fd422 100644
--- a/modules/drivers/sqlite/src/metabase/driver/sqlite.clj
+++ b/modules/drivers/sqlite/src/metabase/driver/sqlite.clj
@@ -128,6 +128,10 @@
   [driver _ expr]
   (->datetime (strftime "%Y-%m-%d %H:%M:%S" (sql.qp/->honeysql driver expr))))
 
+(defmethod sql.qp/date [:sqlite :second-of-minute]
+  [driver _ expr]
+  (hx/->integer (strftime "%S" (sql.qp/->honeysql driver expr))))
+
 (defmethod sql.qp/date [:sqlite :minute]
   [driver _ expr]
   (->datetime (strftime "%Y-%m-%d %H:%M" (sql.qp/->honeysql driver expr))))
@@ -206,6 +210,10 @@
   [driver _ expr]
   (->date (sql.qp/->honeysql driver expr) (hx/literal "start of year")))
 
+(defmethod sql.qp/date [:sqlite :year-of-era]
+  [driver _ expr]
+  (hx/->integer (strftime "%Y" (sql.qp/->honeysql driver expr))))
+
 (defmethod sql.qp/add-interval-honeysql-form :sqlite
   [_driver hsql-form amount unit]
   (let [[multiplier sqlite-unit] (case unit
diff --git a/modules/drivers/sqlserver/src/metabase/driver/sqlserver.clj b/modules/drivers/sqlserver/src/metabase/driver/sqlserver.clj
index a0cde770165..2d521f830d2 100644
--- a/modules/drivers/sqlserver/src/metabase/driver/sqlserver.clj
+++ b/modules/drivers/sqlserver/src/metabase/driver/sqlserver.clj
@@ -128,6 +128,10 @@
   [_ _ expr]
   expr)
 
+(defmethod sql.qp/date [:sqlserver :second-of-minute]
+  [_ _ expr]
+  (date-part :second expr))
+
 (defmethod sql.qp/date [:sqlserver :minute]
   [_ _ expr]
   (hx/maybe-cast :smalldatetime expr))
@@ -203,6 +207,10 @@
     (hx/year expr)
     (hsql/call :DateFromParts (hx/year expr) 1 1)))
 
+(defmethod sql.qp/date [:sqlserver :year-of-era]
+  [_ _ expr]
+  (date-part :year expr))
+
 (defmethod sql.qp/add-interval-honeysql-form :sqlserver
   [_ hsql-form amount unit]
   (date-add unit amount hsql-form))
diff --git a/shared/src/metabase/mbql/normalize.cljc b/shared/src/metabase/mbql/normalize.cljc
index b89ff8d52df..ebb400e2c24 100644
--- a/shared/src/metabase/mbql/normalize.cljc
+++ b/shared/src/metabase/mbql/normalize.cljc
@@ -149,6 +149,10 @@
   [[_ amount unit]]
   [:interval amount (maybe-normalize-token unit)])
 
+(defmethod normalize-mbql-clause-tokens :temporal-extract
+  [[_ field unit]]
+  [:temporal-extract (normalize-tokens field :ignore-path) (maybe-normalize-token unit)])
+
 (defmethod normalize-mbql-clause-tokens :value
   ;; The args of a `value` clause shouldn't be normalized.
   ;; See https://github.com/metabase/metabase/issues/23354 for details
diff --git a/shared/src/metabase/mbql/predicates.cljc b/shared/src/metabase/mbql/predicates.cljc
index 019982be640..1c6788bb161 100644
--- a/shared/src/metabase/mbql/predicates.cljc
+++ b/shared/src/metabase/mbql/predicates.cljc
@@ -29,3 +29,7 @@
 (def ^{:arglists '([filter-clause])} Filter?
   "Is this a valid `:filter` clause?"
   (complement (s/checker mbql.s/Filter)))
+
+(def ^{:arglists '([filter-clause])} DatetimeExpression?
+  "Is this a valid DatetimeExpression clause?"
+  (complement (s/checker mbql.s/DatetimeExpression)))
diff --git a/shared/src/metabase/mbql/schema.cljc b/shared/src/metabase/mbql/schema.cljc
index dfc6ced8279..2ab06654bfc 100644
--- a/shared/src/metabase/mbql/schema.cljc
+++ b/shared/src/metabase/mbql/schema.cljc
@@ -78,6 +78,12 @@
    (apply s/enum datetime-bucketing-units)
    "datetime-bucketing-unit"))
 
+(def TemporalExtractUnits
+  "Valid units to extract from a temporal."
+  (s/named
+    (apply s/enum #{:second :minute :hour :day :day-of-week :week :month :quarter :year})
+    "temporal-extract-units"))
+
 (def ^:private RelativeDatetimeUnit
   (s/named
    (apply s/enum #{:default :minute :hour :day :week :month :quarter :year})
@@ -462,8 +468,20 @@
 
 (def ^:private aggregations #{:sum :avg :stddev :var :median :percentile :min :max :cum-count :cum-sum :count-where :sum-where :share :distinct :metric :aggregation-options :count})
 
+(def temporal-extract-functions
+  "Functions to extract components of a date, datetime."
+  #{;; extraction functions (get some component of a given temporal value/column)
+    :temporal-extract
+    ;; SUGAR drivers do not need to implement
+    :get-year :get-quarter :get-month :get-day :get-day-of-week :get-hour :get-minute :get-second})
+
+(def date+time+timezone-functions
+  "Date, time, and timezone related functions."
+  (set/union temporal-extract-functions))
+
 (declare ArithmeticExpression)
 (declare BooleanExpression)
+(declare DatetimeExpression)
 (declare Aggregation)
 
 (def ^:private NumericExpressionArg
@@ -474,6 +492,9 @@
    (partial is-clause? arithmetic-expressions)
    (s/recursive #'ArithmeticExpression)
 
+   (partial is-clause? temporal-extract-functions)
+   (s/recursive #'DatetimeExpression)
+
    (partial is-clause? aggregations)
    (s/recursive #'Aggregation)
 
@@ -483,6 +504,17 @@
    :else
    Field))
 
+(def ^:private DateTimeExpressionArg
+  (s/conditional
+    (partial is-clause? aggregations)
+    (s/recursive #'Aggregation)
+
+    (partial is-clause? :value)
+    value
+
+    :else
+    Field))
+
 (def ^:private ExpressionArg
   (s/conditional
    number?
@@ -503,6 +535,9 @@
    (partial is-clause? string-expressions)
    (s/recursive #'StringExpression)
 
+   (partial is-clause? temporal-extract-functions)
+   (s/recursive #'DatetimeExpression)
+
    (partial is-clause? :value)
    value
 
@@ -587,13 +622,51 @@
   "Schema for the definition of an arithmetic expression."
   (s/recursive #'ArithmeticExpression*))
 
+(defclause ^{:requires-features #{:temporal-extract}} temporal-extract
+  datetime DateTimeExpressionArg
+  unit     TemporalExtractUnits)
+
+;; SUGAR CLAUSE: get-year, get-month... clauses are all sugars clause that will be rewritten as [:temporal-extract column :year]
+(defclause ^{:requires-features #{:temporal-extract}} ^:sugar get-year
+  date DateTimeExpressionArg)
+
+(defclause ^{:requires-features #{:temporal-extract}} ^:sugar get-quarter
+  date DateTimeExpressionArg)
+
+(defclause ^{:requires-features #{:temporal-extract}} ^:sugar get-month
+  date DateTimeExpressionArg)
+
+(defclause ^{:requires-features #{:temporal-extract}} ^:sugar get-day
+  date DateTimeExpressionArg)
+
+(defclause ^{:requires-features #{:temporal-extract}} ^:sugar get-day-of-week
+  date DateTimeExpressionArg)
+
+(defclause ^{:requires-features #{:temporal-extract}} ^:sugar get-hour
+  datetime DateTimeExpressionArg)
+
+(defclause ^{:requires-features #{:temporal-extract}} ^:sugar get-minute
+  datetime DateTimeExpressionArg)
+
+(defclause ^{:requires-features #{:temporal-extract}} ^:sugar get-second
+  datetime DateTimeExpressionArg)
+
+(def ^:private DatetimeExpression*
+  (one-of temporal-extract
+          ;; SUGAR drivers do not need to implement
+          get-year get-quarter get-month get-day get-day-of-week get-hour
+          get-minute get-second))
+
+(def DatetimeExpression
+  "Schema for the definition of a date function expression."
+  (s/recursive #'DatetimeExpression*))
+
 (declare StringExpression*)
 
 (def ^:private StringExpression
   "Schema for the definition of an string expression."
   (s/recursive #'StringExpression*))
 
-
 ;;; ----------------------------------------------------- Filter -----------------------------------------------------
 
 (declare Filter)
@@ -769,18 +842,18 @@
   "Schema for anything that is accepted as a top-level expression definition, either an arithmetic expression such as a
   `:+` clause or a `:field` clause."
   (s/conditional
-   (partial is-clause? arithmetic-expressions) ArithmeticExpression
-   (partial is-clause? string-expressions)     StringExpression
-   (partial is-clause? boolean-expressions)    BooleanExpression
-   (partial is-clause? :case)                  case
-   :else                                       Field))
-
+   (partial is-clause? arithmetic-expressions)       ArithmeticExpression
+   (partial is-clause? string-expressions)           StringExpression
+   (partial is-clause? boolean-expressions)          BooleanExpression
+   (partial is-clause? date+time+timezone-functions) DatetimeExpression
+   (partial is-clause? :case)                        case
+   :else                                             Field))
 
 ;;; -------------------------------------------------- Aggregations --------------------------------------------------
 
 ;; For all of the 'normal' Aggregations below (excluding Metrics) fields are implicit Field IDs
 
-;; cum-sum and cum-count are SUGAR because they're implemented in middleware. They clauses are swapped out with
+;; cum-sum and cum-count are SUGAR because they're implemented in middleware. The clauses are swapped out with
 ;; `count` and `sum` aggregations respectively and summation is done in Clojure-land
 (defclause ^{:requires-features #{:basic-aggregations}} ^:sugar count,     field (optional Field))
 (defclause ^{:requires-features #{:basic-aggregations}} ^:sugar cum-count, field (optional Field))
@@ -837,8 +910,11 @@
 (def ^:private UnnamedAggregation*
   (s/if (partial is-clause? arithmetic-expressions)
     ArithmeticExpression
-    (one-of count avg cum-count cum-sum distinct stddev sum min max metric share count-where
-            sum-where case median percentile ag:var)))
+    (one-of avg cum-sum distinct stddev sum min max metric share count-where
+            sum-where case median percentile ag:var
+            ;; SUGAR clauses
+            cum-count count)))
+
 
 (def ^:private UnnamedAggregation
   (s/recursive #'UnnamedAggregation*))
@@ -1348,8 +1424,8 @@
    :number/between          {:type :numeric, :operator :binary, :allowed-for #{:number/between}}
    :string/!=               {:type :string, :operator :variadic, :allowed-for #{:string/!=}}
    :string/=                {:type :string, :operator :variadic, :allowed-for #{:string/= :text :id :category
-                                                                                 :location/city :location/state
-                                                                                 :location/zip_code :location/country}}
+                                                                                :location/city :location/state
+                                                                                :location/zip_code :location/country}}
    :string/contains         {:type :string, :operator :unary, :allowed-for #{:string/contains}}
    :string/does-not-contain {:type :string, :operator :unary, :allowed-for #{:string/does-not-contain}}
    :string/ends-with        {:type :string, :operator :unary, :allowed-for #{:string/ends-with}}
diff --git a/shared/src/metabase/mbql/util.cljc b/shared/src/metabase/mbql/util.cljc
index 8ea82b2062a..e4b3a9901ba 100644
--- a/shared/src/metabase/mbql/util.cljc
+++ b/shared/src/metabase/mbql/util.cljc
@@ -250,6 +250,27 @@
                              [:relative-datetime :current]
                              [:relative-datetime 0 temporal-unit])))))
 
+(def temporal-extract-ops->unit
+  "Mapping from the sugar syntax to extract datetime to the unit."
+  {:get-year        :year
+   :get-quarter     :quarter
+   :get-month       :month
+   :get-day         :day
+   :get-day-of-week :day-of-week
+   :get-hour        :hour
+   :get-minute      :minute
+   :get-second      :second})
+
+(def ^:private temporal-extract-ops
+  (set (keys temporal-extract-ops->unit)))
+
+(defn desugar-temporal-extract
+  "Replace datetime extractions clauses like `[:get-year field]` with `[:temporal-extract field :year]`."
+  [m]
+  (mbql.match/replace m
+    [(op :guard temporal-extract-ops) field]
+    [:temporal-extract field (temporal-extract-ops->unit op)]))
+
 (s/defn desugar-filter-clause :- mbql.s/Filter
   "Rewrite various 'syntatic sugar' filter clauses like `:time-interval` and `:inside` as simpler, logically
   equivalent clauses. This can be used to simplify the number of filter clauses that need to be supported by anything
@@ -264,7 +285,8 @@
       desugar-is-null-and-not-null
       desugar-is-empty-and-not-empty
       desugar-inside
-      simplify-compound-filter))
+      simplify-compound-filter
+      desugar-temporal-extract))
 
 (defmulti ^:private negate* first)
 
diff --git a/shared/test/metabase/mbql/util_test.cljc b/shared/test/metabase/mbql/util_test.cljc
index 255c3ce7028..6ef0fc41b14 100644
--- a/shared/test/metabase/mbql/util_test.cljc
+++ b/shared/test/metabase/mbql/util_test.cljc
@@ -362,6 +362,15 @@
     (t/is (= [:not [:contains [:field 1 nil] "ABC" {:case-sensitive false}]]
              (mbql.u/desugar-filter-clause [:does-not-contain [:field 1 nil] "ABC" {:case-sensitive false}])))))
 
+(t/deftest ^:parallel desugar-temporal-extract-test
+  (t/testing "desugaring :get-year, :get-month, etc"
+    (doseq [[op unit] mbql.u/temporal-extract-ops->unit]
+      (t/is (= [:temporal-extract [:field 1 nil] unit]
+               (mbql.u/desugar-temporal-extract [op [:field 1 nil]])))
+
+      (t/is (= [:+ [:temporal-extract [:field 1 nil] unit] 1]
+               (mbql.u/desugar-temporal-extract [:+ [op [:field 1 nil]] 1]))))))
+
 (t/deftest ^:parallel negate-simple-filter-clause-test
   (t/testing :=
     (t/is (= [:!= [:field 1 nil] 10]
@@ -564,12 +573,12 @@
 
 
     (t/testing "ok, can we do the same thing as the tests above but make those names *unique* at the same time?"
-      (t/is (= [[:aggregation-options [:sum [:field 1 nil]]   {:name "sum"}  ]
+      (t/is (= [[:aggregation-options [:sum [:field 1 nil]]   {:name "sum"}]
                 [:aggregation-options [:count [:field 1 nil]] {:name "count"}]
                 [:aggregation-options [:sum [:field 1 nil]]   {:name "sum_2"}]
-                [:aggregation-options [:avg [:field 1 nil]]   {:name "avg"}  ]
+                [:aggregation-options [:avg [:field 1 nil]]   {:name "avg"}]
                 [:aggregation-options [:sum [:field 1 nil]]   {:name "sum_3"}]
-                [:aggregation-options [:min [:field 1 nil]]   {:name "min"}  ]]
+                [:aggregation-options [:min [:field 1 nil]]   {:name "min"}]]
                (mbql.u/pre-alias-and-uniquify-aggregations simple-ag->name
                  [[:sum [:field 1 nil]]
                   [:count [:field 1 nil]]
@@ -701,7 +710,7 @@
            "if nothing is set return `nil`"
            {{:database 1
              :type     :query
-             :query    {:source-table 1}} nil}} ]
+             :query    {:source-table 1}} nil}}]
     (t/testing group
       (doseq [[query expected] query->expected]
         (t/testing (pr-str (list 'query->max-rows-limit query))
diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj
index 6b53e1342ba..5f37b241735 100644
--- a/src/metabase/driver.clj
+++ b/src/metabase/driver.clj
@@ -436,6 +436,10 @@
     ;; Does the driver support percentile calculations (including median)
     :percentile-aggregations
 
+    ;; Does the driver support date, time, and timezone manipulation functions?
+    ;; DEFAULTS TO TRUE
+    :temporal-extract
+
     ;; Does the driver support experimental "writeback" actions like "delete this row" or "insert a new row" from 44+?
     :actions
 
@@ -462,6 +466,7 @@
 
 (defmethod supports? [::driver :basic-aggregations] [_ _] true)
 (defmethod supports? [::driver :case-sensitivity-string-filter-options] [_ _] true)
+(defmethod supports? [::driver :temporal-extract] [_ _] true)
 
 (defmulti database-supports?
   "Does this driver and specific instance of a database support a certain `feature`?
diff --git a/src/metabase/driver/postgres.clj b/src/metabase/driver/postgres.clj
index e730295e514..cf7bf21f281 100644
--- a/src/metabase/driver/postgres.clj
+++ b/src/metabase/driver/postgres.clj
@@ -245,19 +245,21 @@
 
 (def ^:private extract-integer (comp hx/->integer extract))
 
-(defmethod sql.qp/date [:postgres :default]         [_ _ expr] expr)
-(defmethod sql.qp/date [:postgres :minute]          [_ _ expr] (date-trunc :minute expr))
-(defmethod sql.qp/date [:postgres :minute-of-hour]  [_ _ expr] (extract-integer :minute expr))
-(defmethod sql.qp/date [:postgres :hour]            [_ _ expr] (date-trunc :hour expr))
-(defmethod sql.qp/date [:postgres :hour-of-day]     [_ _ expr] (extract-integer :hour expr))
-(defmethod sql.qp/date [:postgres :day]             [_ _ expr] (hx/->date expr))
-(defmethod sql.qp/date [:postgres :day-of-month]    [_ _ expr] (extract-integer :day expr))
-(defmethod sql.qp/date [:postgres :day-of-year]     [_ _ expr] (extract-integer :doy expr))
-(defmethod sql.qp/date [:postgres :month]           [_ _ expr] (date-trunc :month expr))
-(defmethod sql.qp/date [:postgres :month-of-year]   [_ _ expr] (extract-integer :month expr))
-(defmethod sql.qp/date [:postgres :quarter]         [_ _ expr] (date-trunc :quarter expr))
-(defmethod sql.qp/date [:postgres :quarter-of-year] [_ _ expr] (extract-integer :quarter expr))
-(defmethod sql.qp/date [:postgres :year]            [_ _ expr] (date-trunc :year expr))
+(defmethod sql.qp/date [:postgres :default]          [_ _ expr] expr)
+(defmethod sql.qp/date [:postgres :second-of-minute] [_ _ expr] (extract-integer :second expr))
+(defmethod sql.qp/date [:postgres :minute]           [_ _ expr] (date-trunc :minute expr))
+(defmethod sql.qp/date [:postgres :minute-of-hour]   [_ _ expr] (extract-integer :minute expr))
+(defmethod sql.qp/date [:postgres :hour]             [_ _ expr] (date-trunc :hour expr))
+(defmethod sql.qp/date [:postgres :hour-of-day]      [_ _ expr] (extract-integer :hour expr))
+(defmethod sql.qp/date [:postgres :day]              [_ _ expr] (hx/->date expr))
+(defmethod sql.qp/date [:postgres :day-of-month]     [_ _ expr] (extract-integer :day expr))
+(defmethod sql.qp/date [:postgres :day-of-year]      [_ _ expr] (extract-integer :doy expr))
+(defmethod sql.qp/date [:postgres :month]            [_ _ expr] (date-trunc :month expr))
+(defmethod sql.qp/date [:postgres :month-of-year]    [_ _ expr] (extract-integer :month expr))
+(defmethod sql.qp/date [:postgres :quarter]          [_ _ expr] (date-trunc :quarter expr))
+(defmethod sql.qp/date [:postgres :quarter-of-year]  [_ _ expr] (extract-integer :quarter expr))
+(defmethod sql.qp/date [:postgres :year]             [_ _ expr] (date-trunc :year expr))
+(defmethod sql.qp/date [:postgres :year-of-era]      [_ _ expr] (extract-integer :year expr))
 
 (defmethod sql.qp/date [:postgres :day-of-week]
   [_ driver expr]
diff --git a/src/metabase/driver/sql/query_processor.clj b/src/metabase/driver/sql/query_processor.clj
index f740fa51b38..de69ec71f9c 100644
--- a/src/metabase/driver/sql/query_processor.clj
+++ b/src/metabase/driver/sql/query_processor.clj
@@ -99,6 +99,18 @@
   [_driver]
   :%now)
 
+(def temporal-extract-unit->date-unit
+  "Mapping from the unit we used in `extract` function to the unit we used for `date` function."
+  {:second      :second-of-minute
+   :minute      :minute-of-hour
+   :hour        :hour-of-day
+   :day-of-week :day-of-week
+   :day         :day-of-month
+   :week        :week-of-year
+   :month       :month-of-year
+   :quarter     :quarter-of-year
+   :year        :year-of-era})
+
 ;; TODO - rename this to `temporal-bucket` or something that better describes what it actually does
 (defmulti date
   "Return a HoneySQL form for truncating a date or timestamp field or value to a given resolution, or extracting a date
@@ -109,12 +121,17 @@
 
 ;; default implementation for `:default` bucketing returns expression as-is
 (defmethod date [:sql :default] [_ _ expr] expr)
-
 ;; We have to roll our own to account for arbitrary start of week
-(defmethod date [:sql :week-of-year]
-  [driver _ expr]
+
+(defmethod date [:sql :second-of-minute] [_driver _ expr] (hx/second expr))
+(defmethod date [:sql :minute-of-hour]   [_driver _ expr] (hx/minute expr))
+(defmethod date [:sql :hour-of-day]      [_driver _ expr] (hx/hour expr))
+(defmethod date [:sql :week-of-year]     [driver _ expr]
   ;; Some DBs truncate when doing integer division, therefore force float arithmetics
   (->honeysql driver [:ceil (hx// (date driver :day-of-year (date driver :week expr)) 7.0)]))
+(defmethod date [:sql :month-of-year]    [_driver _ expr] (hx/month expr))
+(defmethod date [:sql :quarter-of-year]  [_driver _ expr] (hx/quarter expr))
+(defmethod date [:sql :year-of-era]      [_driver _ expr] (hx/year expr))
 
 (defmulti add-interval-honeysql-form
   "Return a HoneySQL form that performs represents addition of some temporal interval to the original `hsql-form`.
@@ -569,6 +586,9 @@
                       (current-datetime-honeysql-form driver)
                       (add-interval-honeysql-form driver (current-datetime-honeysql-form driver) amount unit))))
 
+(defmethod ->honeysql [:sql :temporal-extract]
+  [driver [_ arg unit]]
+  (date driver (temporal-extract-unit->date-unit unit) (->honeysql driver arg)))
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                            Field Aliases (AS Forms)                                            |
diff --git a/src/metabase/query_processor/middleware/desugar.clj b/src/metabase/query_processor/middleware/desugar.clj
index 34c3765c033..5c065dfb161 100644
--- a/src/metabase/query_processor/middleware/desugar.clj
+++ b/src/metabase/query_processor/middleware/desugar.clj
@@ -11,6 +11,9 @@
   drivers need to support. Clauses replaced by this middleware are marked `^:sugar` in the MBQL schema."
   [query]
   (m/update-existing query :query (fn [query]
-                                    (mbql.u/replace query
-                                      (filter-clause :guard mbql.preds/Filter?)
-                                      (mbql.u/desugar-filter-clause filter-clause)))))
+                                     (mbql.u/replace query
+                                       (filter-clause :guard mbql.preds/Filter?)
+                                       (mbql.u/desugar-filter-clause filter-clause)
+
+                                       (temporal-extract-clause :guard mbql.preds/DatetimeExpression?)
+                                       (mbql.u/desugar-temporal-extract temporal-extract-clause)))))
diff --git a/src/metabase/util/date_2.clj b/src/metabase/util/date_2.clj
index 3e27d2bc523..98a4c396122 100644
--- a/src/metabase/util/date_2.clj
+++ b/src/metabase/util/date_2.clj
@@ -190,7 +190,8 @@
 ;; well. Not sure where we'd use these, but we should have them for consistency
 (def extract-units
   "Units which return a (numerical, periodic) component of a date"
-  #{:minute-of-hour
+  #{:second-of-minute
+    :minute-of-hour
     :hour-of-day
     :day-of-week
     :day-of-month
@@ -234,6 +235,7 @@
 
   ([t :- Temporal, unit :- (apply s/enum extract-units)]
    (t/as t (case unit
+             :second-of-minute :second-of-minute
              :minute-of-hour   :minute-of-hour
              :hour-of-day      :hour-of-day
              :day-of-week      (.dayOfWeek (week-fields (start-of-week)))
diff --git a/src/metabase/util/honeysql_extensions.clj b/src/metabase/util/honeysql_extensions.clj
index a37129f7646..8bf0ba677bc 100644
--- a/src/metabase/util/honeysql_extensions.clj
+++ b/src/metabase/util/honeysql_extensions.clj
@@ -1,5 +1,5 @@
 (ns metabase.util.honeysql-extensions
-  (:refer-clojure :exclude [+ - / * mod inc dec cast concat format])
+  (:refer-clojure :exclude [+ - / * mod inc dec cast concat format second])
   (:require [clojure.pprint :as pprint]
             [clojure.string :as str]
             [honeysql.core :as hsql]
@@ -300,8 +300,9 @@
 
 ;;; Random SQL fns. Not all DBs support all these!
 (def ^{:arglists '([& exprs])} floor   "SQL `floor` function."   (partial hsql/call :floor))
-(def ^{:arglists '([& exprs])} hour    "SQL `hour` function."    (partial hsql/call :hour))
+(def ^{:arglists '([& exprs])} second  "SQL `second` function."  (partial hsql/call :second))
 (def ^{:arglists '([& exprs])} minute  "SQL `minute` function."  (partial hsql/call :minute))
+(def ^{:arglists '([& exprs])} hour    "SQL `hour` function."    (partial hsql/call :hour))
 (def ^{:arglists '([& exprs])} day     "SQL `day` function."     (partial hsql/call :day))
 (def ^{:arglists '([& exprs])} week    "SQL `week` function."    (partial hsql/call :week))
 (def ^{:arglists '([& exprs])} month   "SQL `month` function."   (partial hsql/call :month))
diff --git a/test/metabase/automagic_dashboards/core_test.clj b/test/metabase/automagic_dashboards/core_test.clj
index 8b21ff15cfb..ff2a513e521 100644
--- a/test/metabase/automagic_dashboards/core_test.clj
+++ b/test/metabase/automagic_dashboards/core_test.clj
@@ -514,7 +514,7 @@
 (deftest handlers-test
   (testing "Make sure we have handlers for all the units available"
     (doseq [unit (disj (set (concat u.date/extract-units u.date/truncate-units))
-                       :iso-day-of-year :millisecond)]
+                       :iso-day-of-year :second-of-minute :millisecond)]
       (testing unit
         (is (some? (#'magic/humanize-datetime "1990-09-09T12:30:00" unit)))))))
 
diff --git a/test/metabase/query_processor/middleware/desugar_test.clj b/test/metabase/query_processor/middleware/desugar_test.clj
index 4fc00f14ed4..658f84a2944 100644
--- a/test/metabase/query_processor/middleware/desugar_test.clj
+++ b/test/metabase/query_processor/middleware/desugar_test.clj
@@ -14,7 +14,9 @@
                                      [:relative-datetime -30 :day]
                                      [:relative-datetime -1 :day]]
                                     [:!= [:field 3 nil] "(not set)"]
-                                    [:!= [:field 3 nil] "url"]]
+                                    [:!= [:field 3 nil] "url"]
+                                    [:> [:temporal-extract [:field 4 nil] :year] 2004]]
+                     :expressions  {"year" [:temporal-extract [:field 4 nil] :year]}
                      :aggregation  [[:share [:and
                                              [:= [:field 1 nil] "Run Query"]
                                              [:between
@@ -30,7 +32,9 @@
                       :filter       [:and
                                      [:= [:field 1 nil] "Run Query"]
                                      [:time-interval [:field 2 nil] -30 :day]
-                                     [:!= [:field 3 nil] "(not set)" "url"]]
+                                     [:!= [:field 3 nil] "(not set)" "url"]
+                                     [:> [:get-year [:field 4 nil]] 2004]]
+                      :expressions  {"year" [:get-year [:field 4 nil]]}
                       :aggregation  [[:share [:and
                                               [:= [:field 1 nil] "Run Query"]
                                               [:time-interval [:field 2 nil] -30 :day]
diff --git a/test/metabase/query_processor_test/date_time_zone_functions_test.clj b/test/metabase/query_processor_test/date_time_zone_functions_test.clj
new file mode 100644
index 00000000000..943425e0727
--- /dev/null
+++ b/test/metabase/query_processor_test/date_time_zone_functions_test.clj
@@ -0,0 +1,144 @@
+(ns metabase.query-processor-test.date-time-zone-functions-test
+  (:require [clojure.test :refer :all]
+            [metabase.driver :as driver]
+            [metabase.test :as mt]
+            [metabase.util.date-2 :as u.date]))
+
+(defn test-temporal-extract
+  [{:keys [aggregation breakout expressions fields filter limit]}]
+  (if breakout
+    (->> (mt/run-mbql-query times {:expressions expressions
+                                   :aggregation aggregation
+                                   :limit       limit
+                                   :filter      filter
+                                   :breakout    breakout})
+         (mt/formatted-rows [int int]))
+    (->> (mt/run-mbql-query times {:expressions expressions
+                                   :aggregation aggregation
+                                   :limit       limit
+                                   :filter      filter
+                                   :fields      fields})
+         (mt/formatted-rows [int])
+         (map first))))
+
+(mt/defdataset times-mixed
+  [["times" [{:field-name "index"
+              :base-type :type/Integer}
+             {:field-name "dt"
+              :base-type :type/DateTime}
+             {:field-name "d"
+              :base-type :type/Date}
+             {:field-name "as_dt"
+              :base-type :type/Text
+              :effective-type :type/DateTime
+              :coercion-strategy :Coercion/ISO8601->DateTime}
+             {:field-name "as_d"
+              :base-type :type/Text
+              :effective-type :type/Date
+              :coercion-strategy :Coercion/ISO8601->Date}]
+    [[1 #t "2004-03-19 09:19:09" #t "2004-03-19" "2004-03-19 09:19:09" "2004-03-19"]
+     [2 #t "2008-06-20 10:20:10" #t "2008-06-20" "2008-06-20 10:20:10" "2008-06-20"]
+     [3 #t "2012-11-21 11:21:11" #t "2012-11-21" "2012-11-21 11:21:11" "2012-11-21"]
+     [4 #t "2012-11-21 11:21:11" #t "2012-11-21" "2012-11-21 11:21:11" "2012-11-21"]]]])
+
+(def ^:private temporal-extraction-op->unit
+  {:get-second      :second-of-minute
+   :get-minute      :minute-of-hour
+   :get-hour        :hour-of-day
+   :get-day-of-week :day-of-week
+   :get-day         :day-of-month
+   :get-week        :week-of-year
+   :get-month       :month-of-year
+   :get-quarter     :quarter-of-year
+   :get-year        :year})
+
+(defn- extract
+  [x op]
+  (u.date/extract x (temporal-extraction-op->unit op)))
+
+(def ^:private extraction-test-cases
+  [{:expected-fn (fn [op]          [(extract #t "2004-03-19 09:19:09" op) (extract #t "2008-06-20 10:20:10" op)
+                                    (extract #t "2012-11-21 11:21:11" op) (extract #t "2012-11-21 11:21:11" op)])
+    :query-fn    (fn [op field-id] {:expressions {"expr" [op [:field field-id nil]]}
+                                    :fields      [[:expression "expr"]]})}
+   {:expected-fn (fn [op]          (into [] (frequencies [(extract #t "2004-03-19 09:19:09" op)
+                                                          (extract #t "2008-06-20 10:20:10" op)
+                                                          (extract #t "2012-11-21 11:21:11" op)
+                                                          (extract #t "2012-11-21 11:21:11" op)])))
+    :query-fn    (fn [op field-id] {:expressions {"expr" [op [:field field-id nil]]}
+                                    :aggregation [[:count]]
+                                    :breakout    [[:expression "expr"]]})}])
+
+(deftest extraction-function-tests
+  (mt/dataset times-mixed
+    ;; need to have seperate tests for mongo because it doesn't have supports for casting yet
+    (mt/test-drivers (disj (mt/normal-drivers-with-feature :temporal-extract) :mongo)
+      (testing "with datetime columns"
+        (doseq [[col-type field-id] [[:datetime (mt/id :times :dt)] [:text-as-datetime (mt/id :times :as_dt)]]
+                op                  [:get-year :get-quarter :get-month :get-day
+                                     :get-day-of-week :get-hour :get-minute :get-second]
+                {:keys [expected-fn query-fn]}
+                extraction-test-cases]
+          (testing (format "extract %s function works as expected on %s column for driver %s" op col-type driver/*driver*)
+            (is (= (set (expected-fn op)) (set (test-temporal-extract (query-fn op field-id))))))))
+
+     (testing "with date columns"
+       (doseq [[col-type field-id] [[:date (mt/id :times :d)] [:text-as-date (mt/id :times :as_d)]]
+               op                  [:get-year :get-quarter :get-month :get-day :get-day-of-week]
+               {:keys [expected-fn query-fn]}
+               extraction-test-cases]
+        (testing (format "extract %s function works as expected on %s column for driver %s" op col-type driver/*driver*)
+          (is (= (set (expected-fn op)) (set (test-temporal-extract (query-fn op field-id)))))))))
+
+    ;; need to have seperate tests for mongo because it doesn't have supports for casting yet
+    (mt/test-driver :mongo
+      (testing "with datetimes columns"
+        (let [[col-type field-id] [:datetime (mt/id :times :dt)]]
+          (doseq [op              [:get-year :get-quarter :get-month :get-day
+                                   :get-day-of-week :get-hour :get-minute :get-second]
+                  {:keys [expected-fn query-fn]}
+                  extraction-test-cases]
+           (testing (format "extract %s function works as expected on %s column for driver %s" op col-type driver/*driver*)
+             (is (= (set (expected-fn op)) (set (test-temporal-extract (query-fn op field-id)))))))))
+
+      (testing "with date columns"
+        (let [[col-type field-id] [:date (mt/id :times :d)]]
+          (doseq [op               [:get-year :get-quarter :get-month :get-day :get-day-of-week]
+                  {:keys [expected-fn query-fn]}
+                  extraction-test-cases]
+           (testing (format "extract %s function works as expected on %s column for driver %s" op col-type driver/*driver*)
+             (is (= (set (expected-fn op)) (set (test-temporal-extract (query-fn op field-id))))))))))))
+
+
+(deftest temporal-extraction-with-filter-expresion-tests
+  (mt/test-drivers (mt/normal-drivers-with-feature :temporal-extract)
+    (mt/dataset times-mixed
+      (doseq [{:keys [title expected query]}
+              [{:title    "Nested expression"
+                :expected [2004]
+                :query    {:expressions {"expr" [:abs [:get-year [:field (mt/id :times :dt) nil]]]}
+                           :filter      [:= [:field (mt/id :times :index) nil] 1]
+                           :fields      [[:expression "expr"]]}}
+
+               {:title     "Nested with arithmetic"
+                :expected  [4008]
+                :query     {:expressions {"expr" [:* [:get-year [:field (mt/id :times :dt) nil]] 2]}
+                            :filter      [:= [:field (mt/id :times :index) nil] 1]
+                            :fields      [[:expression "expr"]]}}
+
+               {:title    "Filter using the extracted result - equality"
+                :expected [1]
+                :query    {:filter [:= [:get-year [:field (mt/id :times :dt) nil]] 2004]
+                           :fields [[:field (mt/id :times :index) nil]]}}
+
+               {:title    "Filter using the extracted result - comparable"
+                :expected [1]
+                :query    {:filter [:< [:get-year [:field (mt/id :times :dt) nil]] 2005]
+                           :fields [[:field (mt/id :times :index) nil]]}}
+
+               {:title    "Nested expression in fitler"
+                :expected [1]
+                :query    {:filter [:= [:* [:get-year [:field (mt/id :times :dt) nil]] 2] 4008]
+                           :fields [[:field (mt/id :times :index) nil]]}}]]
+        (testing title
+          (is (= expected (test-temporal-extract query))))))))
-- 
GitLab