diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index cf0226213cb3aa8726924547d4217033f77a3e1b..7d9b52f36196a3550e63b16116836450041ce119 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -594,7 +594,6 @@ ;; [:or ;; [:and "sql" [:? "-jdbc"]] ;; #"(?:sql(?:-jdbc)?)" - ;; "athena" ;; "bigquery-cloud-sdk" ;; [:and "presto" [:? [:or "-common" "-jdbc"]]] ;; "snowflake" @@ -605,7 +604,7 @@ ;; ".*")) ;; ;; Please keep this form updated when you change the generated regex! <3 - {:pattern "^metabase\\.(?!util\\.(?:(?:honeysql-extensions)|(?:honey-sql-1)))(?!query-processor-test)(?!(?:(?:driver)|(?:test\\.data))\\.(?:(?:sql(?:-jdbc)?)|(?:(?:sql(?:-jdbc)?))|(?:athena)|(?:bigquery-cloud-sdk)|(?:presto(?:(?:(?:-common)|(?:-jdbc)))?)|(?:snowflake)|(?:sqlite)|(?:sqlserver))(?:-test)?(?:(?:\\.)|(?:$))).*" + {:pattern "^metabase\\.(?!util\\.(?:(?:honeysql-extensions)|(?:honey-sql-1)))(?!query-processor-test)(?!(?:(?:driver)|(?:test\\.data))\\.(?:(?:sql(?:-jdbc)?)|(?:(?:sql(?:-jdbc)?))|(?:bigquery-cloud-sdk)|(?:presto(?:(?:(?:-common)|(?:-jdbc)))?)|(?:snowflake)|(?:sqlite)|(?:sqlserver))(?:-test)?(?:(?:\\.)|(?:$))).*" :name honey-sql-2-namespaces}] :config-in-ns diff --git a/modules/drivers/athena/src/metabase/driver/athena.clj b/modules/drivers/athena/src/metabase/driver/athena.clj index 7dafe32ac7e3ce3c4b4fbada7037a8f354fa9dd2..26e37e0340fca84f5ba6ce1f1a689c27f183b01d 100644 --- a/modules/drivers/athena/src/metabase/driver/athena.clj +++ b/modules/drivers/athena/src/metabase/driver/athena.clj @@ -4,7 +4,7 @@ [clojure.java.jdbc :as jdbc] [clojure.set :as set] [clojure.string :as str] - [honeysql.format :as hformat] + [honey.sql :as sql] [java-time :as t] [medley.core :as m] [metabase.driver :as driver] @@ -18,7 +18,7 @@ [metabase.public-settings.premium-features :as premium-features] [metabase.util :as u] [metabase.util.date-2 :as u.date] - [metabase.util.honeysql-extensions :as hx] + [metabase.util.honey-sql-2 :as h2x] [metabase.util.i18n :refer [trs]] [metabase.util.log :as log]) (:import @@ -29,6 +29,10 @@ (driver/register! :athena, :parent #{:sql-jdbc}) +(defmethod sql.qp/honey-sql-version :athena + [_driver] + 2) + ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | metabase.driver method impls | ;;; +----------------------------------------------------------------------------------------------------------------+ @@ -188,12 +192,7 @@ ;;; we'll have to have a custom implementation of `:page` here and do our own version of `:offset` that comes before ;;; `LIMIT`. -(hformat/register-clause! ::offset (dec (get hformat/default-clause-priorities :limit))) - -(defmethod hformat/format-clause ::offset - [[_clause n] honeysql-map] - ;; this has to be a map entry, otherwise HoneySQL has a fit - (hformat/format-clause (java.util.Map/entry :offset n) honeysql-map)) +(sql/register-clause! ::offset :offset :limit) (defmethod sql.qp/apply-top-level-clause [:athena :page] [_driver _top-level-clause honeysql-form {{:keys [items page]} :page}] @@ -202,7 +201,7 @@ :limit items ::offset (* items (dec page)))) -(defn- date-trunc [unit expr] (hx/call :date_trunc (hx/literal unit) expr)) +(defn- date-trunc [unit expr] [:date_trunc (h2x/literal unit) expr]) ;;; Example of handling report timezone ;;; (defn- date-trunc @@ -221,52 +220,52 @@ ;;; If `expr` is a date, we need to cast it to a timestamp before we can truncate to a finer granularity Ideally, we ;;; should make this conditional. There's a generic approach above, but different use cases should b tested. -(defmethod sql.qp/date [:athena :minute] [_driver _unit expr] (hx/call :date_trunc (hx/literal :minute) expr)) -(defmethod sql.qp/date [:athena :hour] [_driver _unit expr] (hx/call :date_trunc (hx/literal :hour) expr)) -(defmethod sql.qp/date [:athena :day] [_driver _unit expr] (hx/call :date_trunc (hx/literal :day) expr)) -(defmethod sql.qp/date [:athena :month] [_driver _unit expr] (hx/call :date_trunc (hx/literal :month) expr)) -(defmethod sql.qp/date [:athena :quarter] [_driver _unit expr] (hx/call :date_trunc (hx/literal :quarter) expr)) -(defmethod sql.qp/date [:athena :year] [_driver _unit expr] (hx/call :date_trunc (hx/literal :year) expr)) +(defmethod sql.qp/date [:athena :minute] [_driver _unit expr] [:date_trunc (h2x/literal :minute) expr]) +(defmethod sql.qp/date [:athena :hour] [_driver _unit expr] [:date_trunc (h2x/literal :hour) expr]) +(defmethod sql.qp/date [:athena :day] [_driver _unit expr] [:date_trunc (h2x/literal :day) expr]) +(defmethod sql.qp/date [:athena :month] [_driver _unit expr] [:date_trunc (h2x/literal :month) expr]) +(defmethod sql.qp/date [:athena :quarter] [_driver _unit expr] [:date_trunc (h2x/literal :quarter) expr]) +(defmethod sql.qp/date [:athena :year] [_driver _unit expr] [:date_trunc (h2x/literal :year) expr]) (defmethod sql.qp/date [:athena :week] [driver _ expr] - (sql.qp/adjust-start-of-week driver (partial hx/call :date_trunc (hx/literal :week)) expr)) + (sql.qp/adjust-start-of-week driver (partial conj [:date_trunc] (h2x/literal :week)) expr)) ;;;; Datetime extraction functions -(defmethod sql.qp/date [:athena :minute-of-hour] [_driver _unit expr] (hx/call :minute expr)) -(defmethod sql.qp/date [:athena :hour-of-day] [_driver _unit expr] (hx/call :hour expr)) -(defmethod sql.qp/date [:athena :day-of-month] [_driver _unit expr] (hx/call :day_of_month expr)) -(defmethod sql.qp/date [:athena :day-of-year] [_driver _unit expr] (hx/call :day_of_year expr)) -(defmethod sql.qp/date [:athena :month-of-year] [_driver _unit expr] (hx/call :month expr)) -(defmethod sql.qp/date [:athena :quarter-of-year] [_driver _unit expr] (hx/call :quarter expr)) +(defmethod sql.qp/date [:athena :minute-of-hour] [_driver _unit expr] [:minute expr]) +(defmethod sql.qp/date [:athena :hour-of-day] [_driver _unit expr] [:hour expr]) +(defmethod sql.qp/date [:athena :day-of-month] [_driver _unit expr] [:day_of_month expr]) +(defmethod sql.qp/date [:athena :day-of-year] [_driver _unit expr] [:day_of_year expr]) +(defmethod sql.qp/date [:athena :month-of-year] [_driver _unit expr] [:month expr]) +(defmethod sql.qp/date [:athena :quarter-of-year] [_driver _unit expr] [:quarter expr]) (defmethod sql.qp/date [:athena :day-of-week] [driver _ expr] - (sql.qp/adjust-day-of-week driver (hx/call :day_of_week expr))) + (sql.qp/adjust-day-of-week driver [:day_of_week expr])) (defmethod sql.qp/unix-timestamp->honeysql [:athena :seconds] [_driver _seconds-or-milliseconds expr] - (hx/call :from_unixtime expr)) + [:from_unixtime expr]) (defmethod sql.qp/add-interval-honeysql-form :athena [_driver hsql-form amount unit] - (hx/call :date_add - (hx/literal (name unit)) - (hx/raw (int amount)) - hsql-form)) + [:date_add + (h2x/literal (name unit)) + [:raw (int amount)] + hsql-form]) (defmethod sql.qp/cast-temporal-string [:athena :Coercion/ISO8601->DateTime] [_driver _semantic-type expr] - (hx/->timestamp expr)) + (h2x/->timestamp expr)) (defmethod sql.qp/cast-temporal-string [:athena :Coercion/ISO8601->Date] [_driver _semantic-type expr] - (hx/->date expr)) + (h2x/->date expr)) (defmethod sql.qp/cast-temporal-string [:athena :Coercion/ISO8601->Time] [_driver _semantic-type expr] - (hx/->time expr)) + (h2x/->time expr)) (defmethod sql.qp/->honeysql [:athena :datetime-diff] [driver [_ x y unit]] @@ -274,27 +273,27 @@ y (sql.qp/->honeysql driver y)] (case unit (:year :month :quarter :week :day) - (hx/call :date_diff (hx/literal unit) (date-trunc :day x) (date-trunc :day y)) + [:date_diff (h2x/literal unit) (date-trunc :day x) (date-trunc :day y)] (:hour :minute :second) - (hx/call :date_diff (hx/literal unit) (hx/->timestamp x) (hx/->timestamp y))))) + [:date_diff (h2x/literal unit) (h2x/->timestamp x) (h2x/->timestamp y)]))) ;; fix to allow integer division to be cast as double (float is not supported by athena) (defmethod sql.qp/->float :athena [_ value] - (hx/cast :double value)) + (h2x/cast :double value)) ;; Support for median/percentile functions (defmethod sql.qp/->honeysql [:athena :median] [driver [_ arg]] - (hx/call :approx_percentile (sql.qp/->honeysql driver arg) 0.5)) + [:approx_percentile (sql.qp/->honeysql driver arg) 0.5]) (defmethod sql.qp/->honeysql [:athena :percentile] [driver [_ arg p]] - (hx/call :approx_percentile (sql.qp/->honeysql driver arg) (sql.qp/->honeysql driver p))) + [:approx_percentile (sql.qp/->honeysql driver arg) (sql.qp/->honeysql driver p)]) (defmethod sql.qp/->honeysql [:athena :regex-match-first] [driver [_ arg pattern]] - (hx/call :regexp_extract (sql.qp/->honeysql driver arg) pattern)) + [:regexp_extract (sql.qp/->honeysql driver arg) pattern]) ;; keyword function converts database-type variable to a symbol, so we use symbols above to map the types (defn- database-type->base-type-or-warn diff --git a/modules/drivers/athena/test/metabase/driver/athena_test.clj b/modules/drivers/athena/test/metabase/driver/athena_test.clj index af1c97922385c76bf6b17aa27483b7a55cc8ff5e..3edf91077452e1e2432a4ac6af6f04619471c1be 100644 --- a/modules/drivers/athena/test/metabase/driver/athena_test.clj +++ b/modules/drivers/athena/test/metabase/driver/athena_test.clj @@ -103,10 +103,10 @@ ;; apparently you can't cast a TIMESTAMP WITH TIME ZONE to a regular TIMESTAMP. So make sure we're not trying to ;; do that cast. This only applies to Athena v3! I think we're currently testing against v2. When we upgrade this ;; should ensure things continue to work. - (let [literal (hx/raw "timestamp '2022-11-16 04:21:00 US/Pacific'") - [sql & args] (hformat/format {:select [[(sql.qp/add-interval-honeysql-form :athena literal 1 :day) - :t] - ]}) + (let [literal [:raw "timestamp '2022-11-16 04:21:00 US/Pacific'"] + [sql & args] (sql.qp/format-honeysql :athena + {:select [[(sql.qp/add-interval-honeysql-form :athena literal 1 :day) + :t]]}) query (mt/native-query {:query sql, :params args})] (mt/with-native-query-testing-context query (is (= ["2022-11-17T12:21:00Z"] @@ -133,3 +133,17 @@ (is (not (contains? (sql-jdbc.conn/connection-details->spec :athena {:region "us-west-2"}) :AwsCredentialsProviderClass))))))) + +(deftest page-test + (testing ":page clause places OFFSET *before* LIMIT" + (is (= [(str "SELECT \"default\".\"categories\".\"id\" AS \"id\"" + " FROM \"default\".\"categories\"" + " ORDER BY \"default\".\"categories\".\"id\" ASC" + " OFFSET ? LIMIT ?") 10 5] + (sql.qp/format-honeysql :athena + (sql.qp/apply-top-level-clause :athena :page + {:select [[:default.categories.id "id"]] + :from [:default.categories] + :order-by [[:default.categories.id :asc]]} + {:page {:page 3 + :items 5}}))))))