From b46a6592e700c9f69ad5f633913fcdd87fa674a8 Mon Sep 17 00:00:00 2001
From: Case Nelson <case@metabase.com>
Date: Wed, 4 Sep 2024 16:31:26 -0600
Subject: [PATCH] fix: sqlserver handle uniqueidentifier uuids (#47544)

* fix: sqlserver handle uniqueidentifier uuids

Fixes #46148

Include sqlserver in `uuid-type` handling as its `uniqueidentifier` type
stores uuids.

* Don't be so precise with varchar size

* Add seam for drivers to cast to text type

* Fix arg order
---
 .../athena/test/metabase/test/data/athena.clj |  1 +
 .../oracle/src/metabase/driver/oracle.clj     |  4 +++
 .../src/metabase/driver/sqlserver.clj         | 29 +++++++++++++++++--
 .../test/metabase/test/data/sqlserver.clj     |  1 +
 src/metabase/driver/sql/query_processor.clj   | 11 +++++--
 test/metabase/driver/sql_jdbc_test.clj        |  5 +++-
 test/metabase/test/data/h2.clj                |  1 +
 7 files changed, 47 insertions(+), 5 deletions(-)

diff --git a/modules/drivers/athena/test/metabase/test/data/athena.clj b/modules/drivers/athena/test/metabase/test/data/athena.clj
index 20531d04335..db4b4ea7d74 100644
--- a/modules/drivers/athena/test/metabase/test/data/athena.clj
+++ b/modules/drivers/athena/test/metabase/test/data/athena.clj
@@ -182,6 +182,7 @@
                               :type/Float          "DOUBLE"
                               :type/Integer        "INT"
                               :type/Text           "STRING"
+                              :type/UUID           "UUID"
                               :type/Time           "TIMESTAMP"}]
   (defmethod sql.tx/field-base-type->sql-type [:athena base-type] [_ _] sql-type))
 
diff --git a/modules/drivers/oracle/src/metabase/driver/oracle.clj b/modules/drivers/oracle/src/metabase/driver/oracle.clj
index cc9db027322..ac662866c46 100644
--- a/modules/drivers/oracle/src/metabase/driver/oracle.clj
+++ b/modules/drivers/oracle/src/metabase/driver/oracle.clj
@@ -476,6 +476,10 @@
   [_ bool]
   [:inline (if bool 1 0)])
 
+(defmethod sql.qp/->honeysql [:sql ::sql.qp/cast-to-text]
+  [driver [_ expr]]
+  (sql.qp/->honeysql driver [::sql.qp/cast expr "varchar"]))
+
 (defmethod driver/humanize-connection-error-message :oracle
   [_ message]
   ;; if the connection error message is caused by the assertion above checking whether sid or service-name is set,
diff --git a/modules/drivers/sqlserver/src/metabase/driver/sqlserver.clj b/modules/drivers/sqlserver/src/metabase/driver/sqlserver.clj
index bf9e0eb60a3..cbf55a5cff4 100644
--- a/modules/drivers/sqlserver/src/metabase/driver/sqlserver.clj
+++ b/modules/drivers/sqlserver/src/metabase/driver/sqlserver.clj
@@ -20,18 +20,27 @@
    [metabase.lib.util.match :as lib.util.match]
    [metabase.query-processor.interface :as qp.i]
    [metabase.query-processor.timezone :as qp.timezone]
+   [metabase.util :as u]
    [metabase.util.honey-sql-2 :as h2x]
    [metabase.util.log :as log])
   (:import
    (java.sql Connection ResultSet Time)
-   (java.time LocalDate LocalDateTime LocalTime OffsetDateTime OffsetTime ZonedDateTime)
-   (java.time.format DateTimeFormatter)))
+   (java.time
+    LocalDate
+    LocalDateTime
+    LocalTime
+    OffsetDateTime
+    OffsetTime
+    ZonedDateTime)
+   (java.time.format DateTimeFormatter)
+   [java.util UUID]))
 
 (set! *warn-on-reflection* true)
 
 (driver/register! :sqlserver, :parent :sql-jdbc)
 
 (doseq [[feature supported?] {:case-sensitivity-string-filter-options false
+                              :uuid-type                              true
                               :convert-timezone                       true
                               :datetime-diff                          true
                               :index-info                             true
@@ -620,6 +629,10 @@
                          args)]
         ((get-method sql.qp/->honeysql [:sql-jdbc op]) driver clause)))))
 
+(defmethod sql.qp/->honeysql [:sqlserver ::sql.qp/cast-to-text]
+  [driver [_ expr]]
+  (sql.qp/->honeysql driver [::sql.qp/cast expr "varchar(256)"]))
+
 (defmethod driver/db-default-timezone :sqlserver
   [driver database]
   (sql-jdbc.execute/do-with-connection-with-options
@@ -782,6 +795,18 @@
   [driver ps i t]
   (sql-jdbc.execute/set-parameter driver ps i (t/local-time (t/with-offset-same-instant t (t/zone-offset 0)))))
 
+(defmethod sql-jdbc.execute/read-column-thunk [:sqlserver java.sql.Types/CHAR]
+  [driver ^java.sql.ResultSet rs ^java.sql.ResultSetMetaData rsmeta ^Integer i]
+  (condp = (u/lower-case-en (.getColumnTypeName rsmeta i))
+    "uniqueidentifier"
+    (fn read-column-as-UUID []
+      (when-let [s (.getObject rs i)]
+        (try
+          (UUID/fromString s)
+          (catch IllegalArgumentException _
+            s))))
+    ((get-method sql-jdbc.execute/read-column-thunk [:sql-jdbc java.sql.Types/CHAR]) driver rs rsmeta i)))
+
 ;; instead of default `microsoft.sql.DateTimeOffset`
 (defmethod sql-jdbc.execute/read-column-thunk [:sqlserver microsoft.sql.Types/DATETIMEOFFSET]
   [_ ^ResultSet rs _ ^Integer i]
diff --git a/modules/drivers/sqlserver/test/metabase/test/data/sqlserver.clj b/modules/drivers/sqlserver/test/metabase/test/data/sqlserver.clj
index 6328577e065..2d52635f413 100644
--- a/modules/drivers/sqlserver/test/metabase/test/data/sqlserver.clj
+++ b/modules/drivers/sqlserver/test/metabase/test/data/sqlserver.clj
@@ -17,6 +17,7 @@
                                    :type/Decimal        "DECIMAL"
                                    :type/Float          "FLOAT"
                                    :type/Integer        "INTEGER"
+                                   :type/UUID           "UNIQUEIDENTIFIER"
                                    ;; TEXT is considered deprecated -- see
                                    ;; https://msdn.microsoft.com/en-us/library/ms187993.aspx
                                    :type/Text           "VARCHAR(1024)"
diff --git a/src/metabase/driver/sql/query_processor.clj b/src/metabase/driver/sql/query_processor.clj
index 594476a8103..2b5560f0db3 100644
--- a/src/metabase/driver/sql/query_processor.clj
+++ b/src/metabase/driver/sql/query_processor.clj
@@ -1332,7 +1332,7 @@
            (not (uuid? (->honeysql driver arg)))
              ;; Check for inlined values
            (not (= (:database-type (h2x/type-info (->honeysql driver arg))) "uuid")))
-    [::cast field "varchar"]
+    [::cast-to-text field]
     field))
 
 (mu/defn- maybe-cast-uuid-for-text-compare
@@ -1340,13 +1340,20 @@
    Comparing UUID fields against with these operations requires casting as the right side will have `%` for `LIKE` operations."
   [field]
   (if (uuid-field? field)
-    [::cast field "varchar"]
+    [::cast-to-text field]
     field))
 
 (defmethod ->honeysql [:sql ::cast]
   [driver [_ expr database-type]]
   (h2x/maybe-cast database-type (->honeysql driver expr)))
 
+(defmethod ->honeysql [:sql ::cast-to-text]
+  [driver [_ expr]]
+  ;; Oracle does not support text type,
+  ;; sqlserver limits varchar to 30 in casts,
+  ;; athena cannot cast uuid to bounded varchars
+  (->honeysql driver [::cast expr "text"]))
+
 (defmethod ->honeysql [:sql :starts-with]
   [driver [_ field arg options]]
   (like-clause (->honeysql driver (maybe-cast-uuid-for-text-compare field))
diff --git a/test/metabase/driver/sql_jdbc_test.clj b/test/metabase/driver/sql_jdbc_test.clj
index fd768ee9d69..c4bdf789bb6 100644
--- a/test/metabase/driver/sql_jdbc_test.clj
+++ b/test/metabase/driver/sql_jdbc_test.clj
@@ -14,6 +14,7 @@
    [metabase.query-processor.compile :as qp.compile]
    [metabase.test :as mt]
    [metabase.test.data.dataset-definition-test :as dataset-definition-test]
+   [metabase.test.data.sql :as sql.tx]
    [metabase.util :as u]
    [toucan2.core :as t2]
    [toucan2.tools.with-temp :as t2.with-temp]))
@@ -213,7 +214,9 @@
                     (mt/sql-jdbc-drivers)
                     (mt/normal-drivers-with-feature :uuid-type))
     (let [uuid (random-uuid)
-          uuid-query (mt/native-query {:query (format "select cast('%s' as uuid) as x" uuid)})
+          uuid-query (mt/native-query {:query (format "select cast('%s' as %s) as x"
+                                                      uuid
+                                                      (sql.tx/field-base-type->sql-type driver/*driver* :type/UUID))})
           results (qp/process-query uuid-query)
           result-metadata (get-in results [:data :results_metadata :columns])
           col-metadata (first result-metadata)]
diff --git a/test/metabase/test/data/h2.clj b/test/metabase/test/data/h2.clj
index a8c83118789..6e438401721 100644
--- a/test/metabase/test/data/h2.clj
+++ b/test/metabase/test/data/h2.clj
@@ -29,6 +29,7 @@
                                    :type/Float          "FLOAT"
                                    :type/Integer        "INTEGER"
                                    :type/Text           "VARCHAR"
+                                   :type/UUID           "UUID"
                                    :type/Time           "TIME"}]
   (defmethod sql.tx/field-base-type->sql-type [:h2 base-type] [_ _] database-type))
 
-- 
GitLab