Repository: metabase/metabase
Pull request #55691: `integer()` for MySQL
Author: login: ericnormand
Labels: name: no-backport
description: Do not backport this PR to any branch
name: .Team/Querying
Created: 2025-03-25T13:50:33Z | Merged: 2025-03-27T16:38:49Z

Closes QUE-785


Diff:
diff --git a/src/metabase/driver/mysql.clj b/src/metabase/driver/mysql.clj
index 3713f96b7388..c6186a0a3b16 100644
--- a/src/metabase/driver/mysql.clj
+++ b/src/metabase/driver/mysql.clj
@@ -70,6 +70,7 @@
                               :schemas                                false
                               :uploads                                true
                               :identifiers-with-spaces                true
+                              :cast                                   true
                               ;; MySQL doesn't let you have lag/lead in the same part of a query as a `GROUP BY`; to
                               ;; fully support `offset` we need to do some kooky query transformations just for MySQL
                               ;; and make this work.
@@ -374,6 +375,10 @@
                         %)
                      honeysql-expr))))
 
+(defmethod sql.qp/->honeysql [:mysql :integer]
+  [driver [_ value]]
+  (h2x/maybe-cast "SIGNED" (sql.qp/->honeysql driver value)))
+
 ;; Since MySQL doesn't have date_trunc() we fake it by formatting a date to an appropriate string and then converting
 ;; back to a date. See http://dev.mysql.com/doc/refman/5.6/en/date-and-time-functions.html#function_date-format for an
 ;; explanation of format specifiers
diff --git a/src/metabase/lib/core.cljc b/src/metabase/lib/core.cljc
index 75c44ed709fb..4a77ca7ce28a 100644
--- a/src/metabase/lib/core.cljc
+++ b/src/metabase/lib/core.cljc
@@ -162,6 +162,7 @@
   floor
   round
   power
+  date
   interval
   relative-datetime
   time
@@ -176,6 +177,7 @@
   get-minute
   get-second
   get-quarter
+  get-day-of-week
   datetime-add
   datetime-subtract
   concat
@@ -190,6 +192,7 @@
   lower
   offset
   text
+  split-part
   integer]
  [lib.extraction
   column-extractions
diff --git a/src/metabase/lib/schema/expression/arithmetic.cljc b/src/metabase/lib/schema/expression/arithmetic.cljc
index 261e7c6cea79..3b61d4be85a2 100644
--- a/src/metabase/lib/schema/expression/arithmetic.cljc
+++ b/src/metabase/lib/schema/expression/arithmetic.cljc
@@ -222,5 +222,4 @@
     :type/Float))
 
 (mbql-clause/define-tuple-mbql-clause :integer :- :type/BigInteger
-  [:schema [:or [:ref ::expression/number]
-            [:ref ::expression/string]]])
+  [:schema [:ref ::expression/string]])
diff --git a/test/metabase/driver/mysql_test.clj b/test/metabase/driver/mysql_test.clj
index aaf960628cfa..b5cfbf736211 100644
--- a/test/metabase/driver/mysql_test.clj
+++ b/test/metabase/driver/mysql_test.clj
@@ -815,3 +815,184 @@
             (is (= (->> unbinned-query qp/process-query mt/cols (map :database_type)
                         (map #(get {"TIMESTAMP" "DATETIME"} % %)))
                    (->> binned-query   qp/process-query mt/cols (map :database_type))))))))))
+
+;; integer()
+
+(defn- check-integer-query
+  ([query db-type uncasted-field]
+   (check-integer-query query db-type uncasted-field "`subquery`.`INTCAST`"))
+  ([query db-type uncasted-field casted-field]
+   (mt/native-query {:query (str "SELECT " casted-field ", "
+                                 ;; need to do regex because some strings have 0 in front
+                                 ;; MariaDB doesn't like the || operator :(
+                                 (name uncasted-field) " REGEXP CONCAT('^0*', " "CAST(" casted-field " AS " db-type ") COLLATE utf8mb4_unicode_ci, '$')"
+                                 ", "
+                                 (name uncasted-field) " "
+                                 "FROM ( "
+                                 (-> query qp.compile/compile :query)
+                                 " ) AS subquery "
+                                 "LIMIT 100")})))
+
+(deftest ^:parallel integer-cast-table-fields
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "CHAR"}]]]
+                {:keys [field db-type]} fields]
+          (testing (str "casting " table "." field "(" db-type ") to integer")
+            (let [field-md (lib.metadata/field mp (mt/id table field))
+                  query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [field-md])
+                            (lib/expression "INTCAST" (lib/integer field-md))
+                            (lib/limit 100))
+                  result (-> query (check-integer-query db-type field) qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is (= 1 equals?) (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression (lib/concat
+                                                             (lib.metadata/field mp (mt/id :people :id))
+                                                             (lib.metadata/field mp (mt/id :people :zip)))
+                                                :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer")
+            (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [])
+                            (lib/expression "UNCASTED" expression)
+                            (as-> q
+                                  (lib/expression q "INTCAST" (lib/integer (lib/expression-ref q "UNCASTED"))))
+                            (lib/limit 10))
+                  result (-> query (check-integer-query db-type "`subquery`.`UNCASTED`") qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is (= 1 equals?) (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-nested-native-query
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[_table expressions] [[:people [{:expression "'1238493839384848'" :db-type "CHAR"}
+                                                {:expression "'-12372636363626263'" :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer from native query")
+            (let [native-query (mt/native-query {:query (str "SELECT " expression " AS UNCASTED")})]
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 (mt/card-with-source-metadata-for-query native-query)]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/integer (->> q lib/visible-columns (filter #(= "UNCASTED" (:name %))) first)))))
+                      result (-> query (check-integer-query db-type "`UNCASTED`") qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is (= 1 equals?) (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-query
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "CHAR"}]]]
+                {:keys [field db-type]} fields]
+          (let [nested-query (lib/query mp (lib.metadata/table mp (mt/id table)))]
+            (testing (str "Casting " db-type " to integer")
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 (mt/card-with-source-metadata-for-query nested-query)]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (lib/with-fields [])
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/integer (lib.metadata/field mp (mt/id table field)))))
+                                (lib/limit 10))
+                      result (-> query (check-integer-query db-type field) qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is (= 1 equals?) (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-query-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression (lib/concat
+                                                             (lib.metadata/field mp (mt/id :people :id))
+                                                             (lib.metadata/field mp (mt/id :people :zip)))
+                                                :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (let [nested-query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                                 (lib/with-fields [])
+                                 (lib/expression "UNCASTED" expression)
+                                 (lib/limit 10))]
+            (testing (str "Casting " db-type " to integer")
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 (mt/card-with-source-metadata-for-query nested-query)]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/integer (->> q lib/visible-columns (filter #(= "UNCASTED" (:name %))) first))))
+                                (lib/limit 10))
+                      result (-> query (check-integer-query db-type "`subquery`.`UNCASTED`") qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is (= 1 equals?) (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression [(lib/concat
+                                                              (lib.metadata/field mp (mt/id :people :id))
+                                                              (lib.metadata/field mp (mt/id :people :zip)))
+                                                             (lib/concat
+                                                              (lib.metadata/field mp (mt/id :people :id))
+                                                              (lib.metadata/field mp (mt/id :people :zip)))]
+                                                :db-type "CHAR"}]]]
+                {db-type :db-type [e1 e2] :expression} expressions]
+          (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                          (lib/expression "UNCASTED" e1)
+                          (lib/expression "INTCAST" (lib/integer e2))
+                          (lib/limit 10))
+                result (-> query (check-integer-query db-type "`subquery`.`UNCASTED`") qp/process-query)
+                cols (mt/cols result)
+                rows (mt/rows result)]
+            (is (= :type/BigInteger (-> cols first :base_type)))
+            (doseq [[casted-value equals? uncasted-value] rows]
+              (is (= 1 equals?) (str "Not equal for: " casted-value " " uncasted-value)))))))))
+
+(deftest ^:parallel integer-cast-aggregations
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "CHAR"}]]]
+                {:keys [field db-type]} fields]
+          (testing (str "aggregating " table "." field "(" db-type ") and casting to integer")
+            (let [field-md (lib.metadata/field mp (mt/id table field))
+                  query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/aggregate (lib/max field-md))
+                            (lib/aggregate (lib/max (lib/integer field-md))))
+                  result (-> query (check-integer-query db-type "`subquery`.`max`" "`subquery`.`max_2`") qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value _equals? _uncasted-value] rows]
+                (is (int? casted-value))))))))))
diff --git a/test/metabase/driver/postgres_test.clj b/test/metabase/driver/postgres_test.clj
index a41373e55a18..7adf85eff7bc 100644
--- a/test/metabase/driver/postgres_test.clj
+++ b/test/metabase/driver/postgres_test.clj
@@ -1713,9 +1713,9 @@
       (doseq [main-string main-strings
               delimiter delimiters
               index indexes]
-        (testing (str "split part")
+        (testing "split part"
           (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id :people)))
-                          (lib/expression "SPLITPART" (lib/expression-clause :split-part [main-string delimiter index] nil))
+                          (lib/expression "SPLITPART" (lib/split-part main-string delimiter index))
                           (lib/limit 100))
                 result (-> query qp/process-query)
                 cols (mt/cols result)
@@ -1750,7 +1750,7 @@
           (let [field-md (lib.metadata/field mp (mt/id table field))
                 query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
                           (lib/with-fields [field-md])
-                          (lib/expression "TEXTCAST" (lib/expression-clause :text [field-md] nil))
+                          (lib/expression "TEXTCAST" (lib/text field-md))
                           (lib/limit 100))
                 result (-> query (check-query db-type field) qp/process-query)
                 cols (mt/cols result)
@@ -1763,12 +1763,12 @@
 (deftest ^:parallel text-cast-custom-expressions
   (mt/test-driver :postgres
     (let [mp (mt/metadata-provider)]
-      (doseq [[table expressions] [[:people [{:expression (lib/expression-clause :concat
-                                                                                 [(lib.metadata/field mp (mt/id :people :name))
-                                                                                  (lib.metadata/field mp (mt/id :people :name))] nil)
+      (doseq [[table expressions] [[:people [{:expression (lib/concat
+                                                           (lib.metadata/field mp (mt/id :people :name))
+                                                           (lib.metadata/field mp (mt/id :people :name)))
                                               :db-type "TEXT"}
-                                             {:expression (lib/expression-clause :get-day-of-week
-                                                                                 [(lib.metadata/field mp (mt/id :people :birth_date))] nil)
+                                             {:expression (lib/get-day-of-week
+                                                           (lib.metadata/field mp (mt/id :people :birth_date)))
                                               :db-type "INTEGER"}]]]
               {:keys [expression db-type]} expressions]
         (testing (str "Casting " db-type " to text")
@@ -1776,7 +1776,7 @@
                           (lib/with-fields [])
                           (lib/expression "UNCASTED" expression)
                           (as-> q
-                                (lib/expression q "TEXTCAST" (lib/expression-clause :text [(lib/expression-ref q "UNCASTED")] nil)))
+                                (lib/expression q "TEXTCAST" (lib/text (lib/expression-ref q "UNCASTED"))))
                           (lib/limit 10))
                 result (-> query (check-query db-type "\"subquery\".\"UNCASTED\"") qp/process-query)
                 cols (mt/cols result)
@@ -1800,12 +1800,10 @@
             (mt/with-temp
               [:model/Card
                {card-id :id}
-               {:dataset_query native-query
-                :result_metadata (-> (qp/process-query native-query) :data :results_metadata :columns)
-                :type :question}]
+               (mt/card-with-source-metadata-for-query native-query)]
               (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
                               (as-> q
-                                    (lib/expression q "TEXTCAST" (lib/expression-clause :text [(->> q lib/visible-columns (filter #(= "uncasted" (:name %))) first)] nil))))
+                                    (lib/expression q "TEXTCAST" (lib/text (->> q lib/visible-columns (filter #(= "uncasted" (:name %))) first)))))
                     result (-> query (check-query db-type "\"uncasted\"") qp/process-query)
                     cols (mt/cols result)
                     rows (mt/rows result)]
@@ -1828,13 +1826,11 @@
             (mt/with-temp
               [:model/Card
                {card-id :id}
-               {:dataset_query nested-query
-                :result_metadata (-> (qp/process-query nested-query) :data :results_metadata :columns)
-                :type :question}]
+               (mt/card-with-source-metadata-for-query nested-query)]
               (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
                               (lib/with-fields [])
                               (as-> q
-                                    (lib/expression q "TEXTCAST" (lib/expression-clause :text [(lib.metadata/field mp (mt/id table field))] nil)))
+                                    (lib/expression q "TEXTCAST" (lib/text (lib.metadata/field mp (mt/id table field)))))
                               (lib/limit 10))
                     result (-> query (check-query db-type field) qp/process-query)
                     cols (mt/cols result)
@@ -1847,12 +1843,12 @@
 (deftest ^:parallel text-cast-nested-query-custom-expressions
   (mt/test-driver :postgres
     (let [mp (mt/metadata-provider)]
-      (doseq [[table expressions] [[:people [{:expression (lib/expression-clause :concat
-                                                                                 [(lib.metadata/field mp (mt/id :people :name))
-                                                                                  (lib.metadata/field mp (mt/id :people :name))] nil)
+      (doseq [[table expressions] [[:people [{:expression (lib/concat
+                                                           (lib.metadata/field mp (mt/id :people :name))
+                                                           (lib.metadata/field mp (mt/id :people :name)))
                                               :db-type "TEXT"}
-                                             {:expression (lib/expression-clause :get-day-of-week
-                                                                                 [(lib.metadata/field mp (mt/id :people :birth_date))] nil)
+                                             {:expression (lib/get-day-of-week
+                                                           (lib.metadata/field mp (mt/id :people :birth_date)))
                                               :db-type "INTEGER"}]]]
               {:keys [expression db-type]} expressions]
         (let [nested-query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
@@ -1863,12 +1859,10 @@
             (mt/with-temp
               [:model/Card
                {card-id :id}
-               {:dataset_query nested-query
-                :result_metadata (-> (qp/process-query nested-query) :data :results_metadata :columns)
-                :type :question}]
+               (mt/card-with-source-metadata-for-query nested-query)]
               (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
                               (as-> q
-                                    (lib/expression q "TEXTCAST" (lib/expression-clause :text [(->> q lib/visible-columns (filter #(= "UNCASTED" (:name %))) first)] nil)))
+                                    (lib/expression q "TEXTCAST" (lib/text (->> q lib/visible-columns (filter #(= "UNCASTED" (:name %))) first))))
                               (lib/limit 10))
                     result (-> query (check-query db-type "\"subquery\".\"UNCASTED\"") qp/process-query)
                     cols (mt/cols result)
@@ -1881,22 +1875,22 @@
 (deftest ^:parallel text-cast-nested-custom-expressions
   (mt/test-driver :postgres
     (let [mp (mt/metadata-provider)]
-      (doseq [[table expressions] [[:people [{:expression [(lib/expression-clause :concat
-                                                                                  [(lib.metadata/field mp (mt/id :people :name))
-                                                                                   (lib.metadata/field mp (mt/id :people :name))] nil)
-                                                           (lib/expression-clause :concat
-                                                                                  [(lib.metadata/field mp (mt/id :people :name))
-                                                                                   (lib.metadata/field mp (mt/id :people :name))] nil)]
+      (doseq [[table expressions] [[:people [{:expression [(lib/concat
+                                                            (lib.metadata/field mp (mt/id :people :name))
+                                                            (lib.metadata/field mp (mt/id :people :name)))
+                                                           (lib/concat
+                                                            (lib.metadata/field mp (mt/id :people :name))
+                                                            (lib.metadata/field mp (mt/id :people :name)))]
                                               :db-type "TEXT"}
-                                             {:expression [(lib/expression-clause :get-day-of-week
-                                                                                  [(lib.metadata/field mp (mt/id :people :birth_date))] nil)
-                                                           (lib/expression-clause :get-day-of-week
-                                                                                  [(lib.metadata/field mp (mt/id :people :birth_date))] nil)]
+                                             {:expression [(lib/get-day-of-week
+                                                            (lib.metadata/field mp (mt/id :people :birth_date)))
+                                                           (lib/get-day-of-week
+                                                            (lib.metadata/field mp (mt/id :people :birth_date)))]
                                               :db-type "INTEGER"}]]]
               {db-type :db-type [e1 e2] :expression} expressions]
         (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
                         (lib/expression "UNCASTED" e1)
-                        (lib/expression "TEXTCAST" (lib/expression-clause :text [e2] nil))
+                        (lib/expression "TEXTCAST" (lib/text e2))
                         (lib/limit 10))
               result (-> query (check-query db-type "\"subquery\".\"UNCASTED\"") qp/process-query)
               cols (mt/cols result)
@@ -1927,225 +1921,6 @@
             (doseq [[casted-value _equals? _uncasted-value] rows]
               (is (string? casted-value)))))))))
 
-;; integer()
-
-(defn- check-integer-query
-  ([query db-type uncasted-field]
-   (check-integer-query query db-type uncasted-field "\"subquery\".\"INTCAST\""))
-  ([query db-type uncasted-field casted-field]
-   (mt/native-query {:query (str "SELECT " casted-field ", "
-                                 (case db-type
-                                   ;; need to do regex because some strings have 0 in front
-                                   "TEXT"    (str (name uncasted-field) " ~ '^0*' || " "CAST(" casted-field " AS " db-type ") || '$'")
-                                   "INTEGER" (str "CAST(" casted-field " AS " db-type ") = " (name uncasted-field))
-                                   "FLOAT"   (str "ABS(CAST(" casted-field " AS " db-type ") - " (name uncasted-field) ") < 1"))
-                                 ", "
-                                 (name uncasted-field) " "
-                                 "FROM ( "
-                                 (-> query qp.compile/compile :query)
-                                 " ) AS subquery "
-                                 "LIMIT 100")})))
-
-(deftest ^:parallel integer-cast-table-fields
-  (mt/test-driver :postgres
-    (let [mp (mt/metadata-provider)]
-      (doseq [[table fields] [[:people [{:field :longitude :db-type "FLOAT"}
-                                        {:field :id :db-type "INTEGER"}
-                                        {:field :zip :db-type "TEXT"}]]
-                              [:orders [{:field :user_id :db-type "INTEGER"}
-                                        {:field :subtotal :db-type "FLOAT"}]]]
-              {:keys [field db-type]} fields]
-        (testing (str "casting " table "." field "(" db-type ") to integer")
-          (let [field-md (lib.metadata/field mp (mt/id table field))
-                query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
-                          (lib/with-fields [field-md])
-                          (lib/expression "INTCAST" (lib/expression-clause :integer [field-md] nil))
-                          (lib/limit 100))
-                result (-> query (check-integer-query db-type field) qp/process-query)
-                cols (mt/cols result)
-                rows (mt/rows result)]
-            (is (= :type/BigInteger (-> cols first :base_type)))
-            (doseq [[casted-value equals? uncasted-value] rows]
-              (is (int? casted-value))
-              (is equals? (str "Not equal for: " casted-value " " uncasted-value)))))))))
-
-(deftest ^:parallel integer-cast-custom-expressions
-  (mt/test-driver :postgres
-    (let [mp (mt/metadata-provider)]
-      (doseq [[table expressions] [[:people [{:expression (lib/expression-clause :concat
-                                                                                 [(lib.metadata/field mp (mt/id :people :id))
-                                                                                  (lib.metadata/field mp (mt/id :people :zip))] nil)
-                                              :db-type "TEXT"}
-                                             {:expression (lib/expression-clause :get-day-of-week
-                                                                                 [(lib.metadata/field mp (mt/id :people :birth_date))] nil)
-                                              :db-type "INTEGER"}]]
-                                   [:orders [{:expression (lib/+ (lib.metadata/field mp (mt/id :orders :total))
-                                                                 (lib.metadata/field mp (mt/id :orders :quantity)))
-                                              :db-type "FLOAT"}]]]
-              {:keys [expression db-type]} expressions]
-        (testing (str "Casting " db-type " to integer")
-          (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
-                          (lib/with-fields [])
-                          (lib/expression "UNCASTED" expression)
-                          (as-> q
-                                (lib/expression q "INTCAST" (lib/expression-clause :integer [(lib/expression-ref q "UNCASTED")] nil)))
-                          (lib/limit 10))
-                result (-> query (check-integer-query db-type "\"subquery\".\"UNCASTED\"") qp/process-query)
-                cols (mt/cols result)
-                rows (mt/rows result)]
-            (is (= :type/BigInteger (-> cols first :base_type)))
-            (doseq [[casted-value equals? uncasted-value] rows]
-              (is (int? casted-value))
-              (is equals? (str "Not equal for: " casted-value " " uncasted-value)))))))))
-
-(deftest ^:parallel integer-cast-nested-native-query
-  (mt/test-driver :postgres
-    (let [mp (mt/metadata-provider)]
-      (doseq [[_table expressions] [[:people [{:expression 1 :db-type "INTEGER"}
-                                              {:expression "'123'" :db-type "TEXT"}
-                                              {:expression "'-123'" :db-type "TEXT"}
-                                              {:expression 4.5 :db-type "FLOAT"}]]]
-              {:keys [expression db-type]} expressions]
-        (testing (str "Casting " db-type " to integer from native query")
-          (let [native-query (mt/native-query {:query (str "SELECT " expression " AS UNCASTED")})]
-            (mt/with-temp
-              [:model/Card
-               {card-id :id}
-               {:dataset_query native-query
-                :result_metadata (-> (qp/process-query native-query) :data :results_metadata :columns)
-                :type :question}]
-              (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
-                              (as-> q
-                                    (lib/expression q "INTCAST" (lib/expression-clause :integer [(->> q lib/visible-columns (filter #(= "uncasted" (:name %))) first)] nil))))
-                    result (-> query (check-integer-query db-type "\"uncasted\"") qp/process-query)
-                    cols (mt/cols result)
-                    rows (mt/rows result)]
-                (is (= :type/BigInteger (-> cols first :base_type)))
-                (doseq [[casted-value equals? uncasted-value] rows]
-                  (is (int? casted-value))
-                  (is equals? (str "Not equal for: " casted-value " " uncasted-value)))))))))))
-
-(deftest ^:parallel integer-cast-nested-query
-  (mt/test-driver :postgres
-    (let [mp (mt/metadata-provider)]
-      (doseq [[table fields] [[:people [{:field :longitude :db-type "FLOAT"}
-                                        {:field :id :db-type "INTEGER"}
-                                        {:field :zip :db-type "TEXT"}]]
-                              [:orders [{:field :user_id :db-type "INTEGER"}
-                                        {:field :subtotal :db-type "FLOAT"}]]]
-              {:keys [field db-type]} fields]
-        (let [nested-query (lib/query mp (lib.metadata/table mp (mt/id table)))]
-          (testing (str "Casting " db-type " to integer")
-            (mt/with-temp
-              [:model/Card
-               {card-id :id}
-               {:dataset_query nested-query
-                :result_metadata (-> (qp/process-query nested-query) :data :results_metadata :columns)
-                :type :question}]
-              (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
-                              (lib/with-fields [])
-                              (as-> q
-                                    (lib/expression q "INTCAST" (lib/expression-clause :integer [(lib.metadata/field mp (mt/id table field))] nil)))
-                              (lib/limit 10))
-                    result (-> query (check-integer-query db-type field) qp/process-query)
-                    cols (mt/cols result)
-                    rows (mt/rows result)]
-                (is (= :type/BigInteger (-> cols first :base_type)))
-                (doseq [[casted-value equals? uncasted-value] rows]
-                  (is (int? casted-value))
-                  (is equals? (str "Not equal for: " casted-value " " uncasted-value)))))))))))
-
-(deftest ^:parallel integer-cast-nested-query-custom-expressions
-  (mt/test-driver :postgres
-    (let [mp (mt/metadata-provider)]
-      (doseq [[table expressions] [[:people [{:expression (lib/expression-clause :concat
-                                                                                 [(lib.metadata/field mp (mt/id :people :id))
-                                                                                  (lib.metadata/field mp (mt/id :people :zip))] nil)
-                                              :db-type "TEXT"}
-                                             {:expression (lib/expression-clause :get-day-of-week
-                                                                                 [(lib.metadata/field mp (mt/id :people :birth_date))] nil)
-                                              :db-type "INTEGER"}]]
-                                   [:orders [{:expression (lib/+ (lib.metadata/field mp (mt/id :orders :total))
-                                                                 (lib.metadata/field mp (mt/id :orders :quantity)))
-                                              :db-type "FLOAT"}]]]
-              {:keys [expression db-type]} expressions]
-        (let [nested-query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
-                               (lib/with-fields [])
-                               (lib/expression "UNCASTED" expression)
-                               (lib/limit 10))]
-          (testing (str "Casting " db-type " to integer")
-            (mt/with-temp
-              [:model/Card
-               {card-id :id}
-               {:dataset_query nested-query
-                :result_metadata (-> (qp/process-query nested-query) :data :results_metadata :columns)
-                :type :question}]
-              (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
-                              (as-> q
-                                    (lib/expression q "INTCAST" (lib/expression-clause :integer [(->> q lib/visible-columns (filter #(= "UNCASTED" (:name %))) first)] nil)))
-                              (lib/limit 10))
-                    result (-> query (check-integer-query db-type "\"subquery\".\"UNCASTED\"") qp/process-query)
-                    cols (mt/cols result)
-                    rows (mt/rows result)]
-                (is (= :type/BigInteger (-> cols first :base_type)))
-                (doseq [[casted-value equals? uncasted-value] rows]
-                  (is (int? casted-value))
-                  (is equals? (str "Not equal for: " casted-value " " uncasted-value)))))))))))
-
-(deftest ^:parallel integer-cast-nested-custom-expressions
-  (mt/test-driver :postgres
-    (let [mp (mt/metadata-provider)]
-      (doseq [[table expressions] [[:people [{:expression [(lib/expression-clause :concat
-                                                                                  [(lib.metadata/field mp (mt/id :people :id))
-                                                                                   (lib.metadata/field mp (mt/id :people :zip))] nil)
-                                                           (lib/expression-clause :concat
-                                                                                  [(lib.metadata/field mp (mt/id :people :id))
-                                                                                   (lib.metadata/field mp (mt/id :people :zip))] nil)]
-                                              :db-type "TEXT"}
-                                             {:expression [(lib/expression-clause :get-day-of-week
-                                                                                  [(lib.metadata/field mp (mt/id :people :birth_date))] nil)
-                                                           (lib/expression-clause :get-day-of-week
-                                                                                  [(lib.metadata/field mp (mt/id :people :birth_date))] nil)]
-                                              :db-type "INTEGER"}]]
-                                   [:orders [{:expression [(lib/+ (lib.metadata/field mp (mt/id :orders :total))
-                                                                  (lib.metadata/field mp (mt/id :orders :quantity)))
-                                                           (lib/+ (lib.metadata/field mp (mt/id :orders :total))
-                                                                  (lib.metadata/field mp (mt/id :orders :quantity)))]
-                                              :db-type "FLOAT"}]]]
-              {db-type :db-type [e1 e2] :expression} expressions]
-        (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
-                        (lib/expression "UNCASTED" e1)
-                        (lib/expression "INTCAST" (lib/expression-clause :integer [e2] nil))
-                        (lib/limit 10))
-              result (-> query (check-integer-query db-type "\"subquery\".\"UNCASTED\"") qp/process-query)
-              cols (mt/cols result)
-              rows (mt/rows result)]
-          (is (= :type/BigInteger (-> cols first :base_type)))
-          (doseq [[casted-value equals? uncasted-value] rows]
-            (is (int? casted-value))
-            (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))
-
-(deftest ^:parallel integer-cast-aggregations
-  (mt/test-driver :postgres
-    (let [mp (mt/metadata-provider)]
-      (doseq [[table fields] [[:people [{:field :longitude :db-type "FLOAT"}
-                                        {:field :id :db-type "INTEGER"}
-                                        {:field :zip :db-type "TEXT"}]]
-                              [:orders [{:field :user_id :db-type "INTEGER"}
-                                        {:field :subtotal :db-type "FLOAT"}]]]
-              {:keys [field db-type]} fields]
-        (testing (str "aggregating " table "." field "(" db-type ") and casting to integer")
-          (let [field-md (lib.metadata/field mp (mt/id table field))
-                query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
-                          (lib/aggregate (lib/max field-md))
-                          (lib/aggregate (lib/max (lib/integer field-md))))
-                result (-> query (check-integer-query db-type "\"subquery\".\"max\"" "\"subquery\".\"max_2\"") qp/process-query)
-                cols (mt/cols result)
-                rows (mt/rows result)]
-            (is (= :type/BigInteger (-> cols first :base_type)))
-            (doseq [[casted-value _equals? _uncasted-value] rows]
-              (is (int? casted-value)))))))))
-
 (defn- check-date-query
   ([query db-type uncasted-field casted-field]
    (let [compiled-query (qp.compile/compile query)]
@@ -2168,7 +1943,7 @@
           (let [field-md (lib.metadata/field mp (mt/id table field))
                 query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
                           (lib/with-fields [field-md])
-                          (lib/expression "DATECAST" (lib/expression-clause :date [(lib/expression-clause :text [field-md] nil)] nil))
+                          (lib/expression "DATECAST" (lib/date (lib/text field-md)))
                           (lib/limit 100))
                 result (-> query (check-date-query "DATE" field "\"subquery\".\"DATECAST\"") qp/process-query)
                 cols (mt/cols result)
@@ -2182,11 +1957,11 @@
     (let [mp (mt/metadata-provider)]
       (doseq [[table exprs] [[:people [(fn [] (lib/concat "2010" "-" "10" "-" "02"))]]]
               ef exprs]
-        (testing (str "casting custom expression to date")
+        (testing "casting custom expression to date"
           (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
                           (lib/with-fields [])
                           (lib/expression "CUSTOMEXPR" (ef))
-                          (lib/expression "DATECAST" (lib/expression-clause :date [(ef)] nil))
+                          (lib/expression "DATECAST" (lib/date (ef)))
                           (lib/limit 100))
                 result (-> query (check-date-query "TEXT" "\"subquery\".\"CUSTOMEXPR\"" "\"subquery\".\"DATECAST\"") qp/process-query)
                 cols (mt/cols result)
@@ -2205,7 +1980,7 @@
                 query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
                           (lib/with-fields [field-md])
                           (lib/aggregate (lib/max field-md))
-                          (lib/aggregate (lib/max (lib/expression-clause :date [(lib/expression-clause :text [field-md] nil)] nil)))
+                          (lib/aggregate (lib/max (lib/date (lib/text field-md))))
                           (lib/limit 100))
                 result (-> query (check-date-query "DATE" "\"subquery\".\"max\"" "\"subquery\".\"max_2\"") qp/process-query)
                 cols (mt/cols result)
@@ -2213,3 +1988,184 @@
             (is (= :type/Date (-> cols first :base_type)))
             (doseq [[casted-value uncasted-value equals?] rows]
               (is equals? (str "Not equal for: " casted-value " " uncasted-value)))))))))
+
+;; integer()
+
+(defn- check-integer-query
+  ([query db-type uncasted-field]
+   (check-integer-query query db-type uncasted-field "\"subquery\".\"INTCAST\""))
+  ([query db-type uncasted-field casted-field]
+   (mt/native-query {:query (str "SELECT " casted-field ", "
+                                 ;; need to do regex because some strings have 0 in front
+                                 (name uncasted-field) " ~ '^0*' || " "CAST(" casted-field " AS " db-type ") || '$'"
+                                 ", "
+                                 (name uncasted-field) " "
+                                 "FROM ( "
+                                 (-> query qp.compile/compile :query)
+                                 " ) AS subquery "
+                                 "LIMIT 100")})))
+
+(deftest ^:parallel integer-cast-table-fields
+  (mt/test-driver :postgres
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "TEXT"}]]]
+                {:keys [field db-type]} fields]
+          (testing (str "casting " table "." field "(" db-type ") to integer")
+            (let [field-md (lib.metadata/field mp (mt/id table field))
+                  query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [field-md])
+                            (lib/expression "INTCAST" (lib/integer field-md))
+                            (lib/limit 100))
+                  result (-> query (check-integer-query db-type field) qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-custom-expressions
+  (mt/test-driver :postgres
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression (lib/concat
+                                                             (lib.metadata/field mp (mt/id :people :id))
+                                                             (lib.metadata/field mp (mt/id :people :zip)))
+                                                :db-type "TEXT"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer")
+            (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [])
+                            (lib/expression "UNCASTED" expression)
+                            (as-> q
+                                  (lib/expression q "INTCAST" (lib/integer (lib/expression-ref q "UNCASTED"))))
+                            (lib/limit 10))
+                  result (-> query (check-integer-query db-type "\"subquery\".\"UNCASTED\"") qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-nested-native-query
+  (mt/test-driver :postgres
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[_table expressions] [[:people [{:expression "'123'" :db-type "TEXT"}
+                                                {:expression "'-123'" :db-type "TEXT"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer from native query")
+            (let [native-query (mt/native-query {:query (str "SELECT " expression " AS UNCASTED")})]
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 (mt/card-with-source-metadata-for-query native-query)]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/integer (->> q lib/visible-columns (filter #(= "uncasted" (:name %))) first)))))
+                      result (-> query (check-integer-query db-type "\"uncasted\"") qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-query
+  (mt/test-driver :postgres
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "TEXT"}]]]
+                {:keys [field db-type]} fields]
+          (let [nested-query (lib/query mp (lib.metadata/table mp (mt/id table)))]
+            (testing (str "Casting " db-type " to integer")
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 (mt/card-with-source-metadata-for-query nested-query)]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (lib/with-fields [])
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/integer (lib.metadata/field mp (mt/id table field)))))
+                                (lib/limit 10))
+                      result (-> query (check-integer-query db-type field) qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-query-custom-expressions
+  (mt/test-driver :postgres
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression (lib/concat
+                                                             (lib.metadata/field mp (mt/id :people :id))
+                                                             (lib.metadata/field mp (mt/id :people :zip)))
+                                                :db-type "TEXT"}]]]
+                {:keys [expression db-type]} expressions]
+          (let [nested-query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                                 (lib/with-fields [])
+                                 (lib/expression "UNCASTED" expression)
+                                 (lib/limit 10))]
+            (testing (str "Casting " db-type " to integer")
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 (mt/card-with-source-metadata-for-query nested-query)]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/integer (->> q lib/visible-columns (filter #(= "UNCASTED" (:name %))) first))))
+                                (lib/limit 10))
+                      result (-> query (check-integer-query db-type "\"subquery\".\"UNCASTED\"") qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-custom-expressions
+  (mt/test-driver :postgres
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression [(lib/concat
+                                                              (lib.metadata/field mp (mt/id :people :id))
+                                                              (lib.metadata/field mp (mt/id :people :zip)))
+                                                             (lib/concat
+                                                              (lib.metadata/field mp (mt/id :people :id))
+                                                              (lib.metadata/field mp (mt/id :people :zip)))]
+                                                :db-type "TEXT"}]]]
+                {db-type :db-type [e1 e2] :expression} expressions]
+          (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                          (lib/expression "UNCASTED" e1)
+                          (lib/expression "INTCAST" (lib/integer e2))
+                          (lib/limit 10))
+                result (-> query (check-integer-query db-type "\"subquery\".\"UNCASTED\"") qp/process-query)
+                cols (mt/cols result)
+                rows (mt/rows result)]
+            (is (= :type/BigInteger (-> cols first :base_type)))
+            (doseq [[casted-value equals? uncasted-value] rows]
+              (is (int? casted-value))
+              (is equals? (str "Not equal for: " casted-value " " uncasted-value)))))))))
+
+(deftest ^:parallel integer-cast-aggregations
+  (mt/test-driver :postgres
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "TEXT"}]]]
+                {:keys [field db-type]} fields]
+          (testing (str "aggregating " table "." field "(" db-type ") and casting to integer")
+            (let [field-md (lib.metadata/field mp (mt/id table field))
+                  query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/aggregate (lib/max field-md))
+                            (lib/aggregate (lib/max (lib/integer field-md))))
+                  result (-> query (check-integer-query db-type "\"subquery\".\"max\"" "\"subquery\".\"max_2\"") qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value _equals? _uncasted-value] rows]
+                (is (int? casted-value))))))))))


Reviews:
user: login: bshepherdson
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T14:37:31Z
user: login: ericnormand
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T15:10:39Z
user: login: ericnormand
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T15:10:56Z
user: login: ericnormand
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T15:11:10Z

Review comments:
diff_hunk: @@ -815,3 +815,191 @@
             (is (= (->> unbinned-query qp/process-query mt/cols (map :database_type)
                         (map #(get {"TIMESTAMP" "DATETIME"} % %)))
                    (->> binned-query   qp/process-query mt/cols (map :database_type))))))))))
+
+;; integer()
+
+(defn- check-integer-query
+  ([query db-type uncasted-field]
+   (check-integer-query query db-type uncasted-field "`subquery`.`INTCAST`"))
+  ([query db-type uncasted-field casted-field]
+   (mt/native-query {:query (str "SELECT " casted-field ", "
+                                 ;; need to do regex because some strings have 0 in front
+                                 ;; MariaDB doesn't like the || operator :(
+                                 (name uncasted-field) " REGEXP CONCAT('^0*', " "CAST(" casted-field " AS " db-type ") COLLATE utf8mb4_unicode_ci, '$')"
+                                 ", "
+                                 (name uncasted-field) " "
+                                 "FROM ( "
+                                 (-> query qp.compile/compile :query)
+                                 " ) AS subquery "
+                                 "LIMIT 100")})))
+
+(deftest ^:parallel integer-cast-table-fields
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "CHAR"}]]]
+                {:keys [field db-type]} fields]
+          (testing (str "casting " table "." field "(" db-type ") to integer")
+            (let [field-md (lib.metadata/field mp (mt/id table field))
+                  query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [field-md])
+                            (lib/expression "INTCAST" (lib/expression-clause :integer [field-md] nil))
+                            (lib/limit 100))
+                  result (-> query (check-integer-query db-type field) qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression (lib/expression-clause :concat
+                                                                                   [(lib.metadata/field mp (mt/id :people :id))
+                                                                                    (lib.metadata/field mp (mt/id :people :zip))] nil)
+                                                :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer")
+            (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [])
+                            (lib/expression "UNCASTED" expression)
+                            (as-> q
+                                  (lib/expression q "INTCAST" (lib/expression-clause :integer [(lib/expression-ref q "UNCASTED")] nil)))
+                            (lib/limit 10))
+                  result (-> query (check-integer-query db-type "`subquery`.`UNCASTED`") qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-nested-native-query
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[_table expressions] [[:people [{:expression "'1238493839384848'" :db-type "CHAR"}
+                                                {:expression "'-12372636363626263'" :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer from native query")
+            (let [native-query (mt/native-query {:query (str "SELECT " expression " AS UNCASTED")})]
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 {:dataset_query native-query
+                  :result_metadata (-> (qp/process-query native-query) :data :results_metadata :columns)
+                  :type :question}]
path: test/metabase/driver/mysql_test.clj
user: login: bshepherdson
body: ```suggestion
                 (mt/card-with-source-metadata-for-query native-query)]
```
created_at: 2025-03-26T20:25:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -815,3 +815,191 @@
             (is (= (->> unbinned-query qp/process-query mt/cols (map :database_type)
                         (map #(get {"TIMESTAMP" "DATETIME"} % %)))
                    (->> binned-query   qp/process-query mt/cols (map :database_type))))))))))
+
+;; integer()
+
+(defn- check-integer-query
+  ([query db-type uncasted-field]
+   (check-integer-query query db-type uncasted-field "`subquery`.`INTCAST`"))
+  ([query db-type uncasted-field casted-field]
+   (mt/native-query {:query (str "SELECT " casted-field ", "
+                                 ;; need to do regex because some strings have 0 in front
+                                 ;; MariaDB doesn't like the || operator :(
+                                 (name uncasted-field) " REGEXP CONCAT('^0*', " "CAST(" casted-field " AS " db-type ") COLLATE utf8mb4_unicode_ci, '$')"
+                                 ", "
+                                 (name uncasted-field) " "
+                                 "FROM ( "
+                                 (-> query qp.compile/compile :query)
+                                 " ) AS subquery "
+                                 "LIMIT 100")})))
+
+(deftest ^:parallel integer-cast-table-fields
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "CHAR"}]]]
+                {:keys [field db-type]} fields]
+          (testing (str "casting " table "." field "(" db-type ") to integer")
+            (let [field-md (lib.metadata/field mp (mt/id table field))
+                  query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [field-md])
+                            (lib/expression "INTCAST" (lib/expression-clause :integer [field-md] nil))
+                            (lib/limit 100))
+                  result (-> query (check-integer-query db-type field) qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression (lib/expression-clause :concat
+                                                                                   [(lib.metadata/field mp (mt/id :people :id))
+                                                                                    (lib.metadata/field mp (mt/id :people :zip))] nil)
+                                                :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer")
+            (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [])
+                            (lib/expression "UNCASTED" expression)
+                            (as-> q
+                                  (lib/expression q "INTCAST" (lib/expression-clause :integer [(lib/expression-ref q "UNCASTED")] nil)))
+                            (lib/limit 10))
+                  result (-> query (check-integer-query db-type "`subquery`.`UNCASTED`") qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-nested-native-query
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[_table expressions] [[:people [{:expression "'1238493839384848'" :db-type "CHAR"}
+                                                {:expression "'-12372636363626263'" :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer from native query")
+            (let [native-query (mt/native-query {:query (str "SELECT " expression " AS UNCASTED")})]
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 {:dataset_query native-query
+                  :result_metadata (-> (qp/process-query native-query) :data :results_metadata :columns)
+                  :type :question}]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/expression-clause :integer [(->> q lib/visible-columns (filter #(= "UNCASTED" (:name %))) first)] nil))))
+                      result (-> query (check-integer-query db-type "`UNCASTED`") qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-query
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "CHAR"}]]]
+                {:keys [field db-type]} fields]
+          (let [nested-query (lib/query mp (lib.metadata/table mp (mt/id table)))]
+            (testing (str "Casting " db-type " to integer")
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 {:dataset_query nested-query
+                  :result_metadata (-> (qp/process-query nested-query) :data :results_metadata :columns)
+                  :type :question}]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (lib/with-fields [])
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/expression-clause :integer [(lib.metadata/field mp (mt/id table field))] nil)))
+                                (lib/limit 10))
+                      result (-> query (check-integer-query db-type field) qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-query-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression (lib/expression-clause :concat
+                                                                                   [(lib.metadata/field mp (mt/id :people :id))
+                                                                                    (lib.metadata/field mp (mt/id :people :zip))] nil)
+                                                :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (let [nested-query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                                 (lib/with-fields [])
+                                 (lib/expression "UNCASTED" expression)
+                                 (lib/limit 10))]
+            (testing (str "Casting " db-type " to integer")
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 {:dataset_query nested-query
+                  :result_metadata (-> (qp/process-query nested-query) :data :results_metadata :columns)
+                  :type :question}]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/expression-clause :integer [(->> q lib/visible-columns (filter #(= "UNCASTED" (:name %))) first)] nil)))
+                                (lib/limit 10))
+                      result (-> query (check-integer-query db-type "`subquery`.`UNCASTED`") qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression [(lib/expression-clause :concat
+                                                                                    [(lib.metadata/field mp (mt/id :people :id))
+                                                                                     (lib.metadata/field mp (mt/id :people :zip))] nil)
+                                                             (lib/expression-clause :concat
+                                                                                    [(lib.metadata/field mp (mt/id :people :id))
+                                                                                     (lib.metadata/field mp (mt/id :people :zip))] nil)]
path: test/metabase/driver/mysql_test.clj
user: login: bshepherdson
body: Prefer `lib/concat` et al, calling the specific functions in `metabase.lib.expression`, over calling `lib/expression-clause`. That's a `lib.fe-util` function that should only have been exported from `lib.js`, not `lib.core`.
created_at: 2025-03-27T14:35:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -815,3 +815,191 @@
             (is (= (->> unbinned-query qp/process-query mt/cols (map :database_type)
                         (map #(get {"TIMESTAMP" "DATETIME"} % %)))
                    (->> binned-query   qp/process-query mt/cols (map :database_type))))))))))
+
+;; integer()
+
+(defn- check-integer-query
+  ([query db-type uncasted-field]
+   (check-integer-query query db-type uncasted-field "`subquery`.`INTCAST`"))
+  ([query db-type uncasted-field casted-field]
+   (mt/native-query {:query (str "SELECT " casted-field ", "
+                                 ;; need to do regex because some strings have 0 in front
+                                 ;; MariaDB doesn't like the || operator :(
+                                 (name uncasted-field) " REGEXP CONCAT('^0*', " "CAST(" casted-field " AS " db-type ") COLLATE utf8mb4_unicode_ci, '$')"
+                                 ", "
+                                 (name uncasted-field) " "
+                                 "FROM ( "
+                                 (-> query qp.compile/compile :query)
+                                 " ) AS subquery "
+                                 "LIMIT 100")})))
+
+(deftest ^:parallel integer-cast-table-fields
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "CHAR"}]]]
+                {:keys [field db-type]} fields]
+          (testing (str "casting " table "." field "(" db-type ") to integer")
+            (let [field-md (lib.metadata/field mp (mt/id table field))
+                  query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [field-md])
+                            (lib/expression "INTCAST" (lib/expression-clause :integer [field-md] nil))
+                            (lib/limit 100))
+                  result (-> query (check-integer-query db-type field) qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression (lib/expression-clause :concat
+                                                                                   [(lib.metadata/field mp (mt/id :people :id))
+                                                                                    (lib.metadata/field mp (mt/id :people :zip))] nil)
+                                                :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer")
+            (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [])
+                            (lib/expression "UNCASTED" expression)
+                            (as-> q
+                                  (lib/expression q "INTCAST" (lib/expression-clause :integer [(lib/expression-ref q "UNCASTED")] nil)))
+                            (lib/limit 10))
+                  result (-> query (check-integer-query db-type "`subquery`.`UNCASTED`") qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-nested-native-query
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[_table expressions] [[:people [{:expression "'1238493839384848'" :db-type "CHAR"}
+                                                {:expression "'-12372636363626263'" :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer from native query")
+            (let [native-query (mt/native-query {:query (str "SELECT " expression " AS UNCASTED")})]
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 {:dataset_query native-query
+                  :result_metadata (-> (qp/process-query native-query) :data :results_metadata :columns)
+                  :type :question}]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/expression-clause :integer [(->> q lib/visible-columns (filter #(= "UNCASTED" (:name %))) first)] nil))))
+                      result (-> query (check-integer-query db-type "`UNCASTED`") qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-query
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "CHAR"}]]]
+                {:keys [field db-type]} fields]
+          (let [nested-query (lib/query mp (lib.metadata/table mp (mt/id table)))]
+            (testing (str "Casting " db-type " to integer")
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 {:dataset_query nested-query
+                  :result_metadata (-> (qp/process-query nested-query) :data :results_metadata :columns)
+                  :type :question}]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (lib/with-fields [])
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/expression-clause :integer [(lib.metadata/field mp (mt/id table field))] nil)))
+                                (lib/limit 10))
+                      result (-> query (check-integer-query db-type field) qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-query-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression (lib/expression-clause :concat
+                                                                                   [(lib.metadata/field mp (mt/id :people :id))
+                                                                                    (lib.metadata/field mp (mt/id :people :zip))] nil)
+                                                :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (let [nested-query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                                 (lib/with-fields [])
+                                 (lib/expression "UNCASTED" expression)
+                                 (lib/limit 10))]
+            (testing (str "Casting " db-type " to integer")
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 {:dataset_query nested-query
+                  :result_metadata (-> (qp/process-query nested-query) :data :results_metadata :columns)
+                  :type :question}]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/expression-clause :integer [(->> q lib/visible-columns (filter #(= "UNCASTED" (:name %))) first)] nil)))
+                                (lib/limit 10))
+                      result (-> query (check-integer-query db-type "`subquery`.`UNCASTED`") qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression [(lib/expression-clause :concat
+                                                                                    [(lib.metadata/field mp (mt/id :people :id))
+                                                                                     (lib.metadata/field mp (mt/id :people :zip))] nil)
+                                                             (lib/expression-clause :concat
+                                                                                    [(lib.metadata/field mp (mt/id :people :id))
+                                                                                     (lib.metadata/field mp (mt/id :people :zip))] nil)]
+                                                :db-type "CHAR"}]]]
+                {db-type :db-type [e1 e2] :expression} expressions]
+          (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                          (lib/expression "UNCASTED" e1)
+                          (lib/expression "INTCAST" (lib/expression-clause :integer [e2] nil))
path: test/metabase/driver/mysql_test.clj
user: login: bshepherdson
body: Likewise, this should be `(lib/integer e2)`, calling `lib.expression/integer`.
created_at: 2025-03-27T14:35:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -815,3 +815,191 @@
             (is (= (->> unbinned-query qp/process-query mt/cols (map :database_type)
                         (map #(get {"TIMESTAMP" "DATETIME"} % %)))
                    (->> binned-query   qp/process-query mt/cols (map :database_type))))))))))
+
+;; integer()
+
+(defn- check-integer-query
+  ([query db-type uncasted-field]
+   (check-integer-query query db-type uncasted-field "`subquery`.`INTCAST`"))
+  ([query db-type uncasted-field casted-field]
+   (mt/native-query {:query (str "SELECT " casted-field ", "
+                                 ;; need to do regex because some strings have 0 in front
+                                 ;; MariaDB doesn't like the || operator :(
+                                 (name uncasted-field) " REGEXP CONCAT('^0*', " "CAST(" casted-field " AS " db-type ") COLLATE utf8mb4_unicode_ci, '$')"
+                                 ", "
+                                 (name uncasted-field) " "
+                                 "FROM ( "
+                                 (-> query qp.compile/compile :query)
+                                 " ) AS subquery "
+                                 "LIMIT 100")})))
+
+(deftest ^:parallel integer-cast-table-fields
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "CHAR"}]]]
+                {:keys [field db-type]} fields]
+          (testing (str "casting " table "." field "(" db-type ") to integer")
+            (let [field-md (lib.metadata/field mp (mt/id table field))
+                  query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [field-md])
+                            (lib/expression "INTCAST" (lib/expression-clause :integer [field-md] nil))
+                            (lib/limit 100))
+                  result (-> query (check-integer-query db-type field) qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression (lib/expression-clause :concat
+                                                                                   [(lib.metadata/field mp (mt/id :people :id))
+                                                                                    (lib.metadata/field mp (mt/id :people :zip))] nil)
+                                                :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer")
+            (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [])
+                            (lib/expression "UNCASTED" expression)
+                            (as-> q
+                                  (lib/expression q "INTCAST" (lib/expression-clause :integer [(lib/expression-ref q "UNCASTED")] nil)))
+                            (lib/limit 10))
+                  result (-> query (check-integer-query db-type "`subquery`.`UNCASTED`") qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-nested-native-query
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[_table expressions] [[:people [{:expression "'1238493839384848'" :db-type "CHAR"}
+                                                {:expression "'-12372636363626263'" :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer from native query")
+            (let [native-query (mt/native-query {:query (str "SELECT " expression " AS UNCASTED")})]
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 {:dataset_query native-query
+                  :result_metadata (-> (qp/process-query native-query) :data :results_metadata :columns)
+                  :type :question}]
path: test/metabase/driver/mysql_test.clj
user: login: ericnormand
body: 👍 
created_at: 2025-03-27T15:10:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -815,3 +815,191 @@
             (is (= (->> unbinned-query qp/process-query mt/cols (map :database_type)
                         (map #(get {"TIMESTAMP" "DATETIME"} % %)))
                    (->> binned-query   qp/process-query mt/cols (map :database_type))))))))))
+
+;; integer()
+
+(defn- check-integer-query
+  ([query db-type uncasted-field]
+   (check-integer-query query db-type uncasted-field "`subquery`.`INTCAST`"))
+  ([query db-type uncasted-field casted-field]
+   (mt/native-query {:query (str "SELECT " casted-field ", "
+                                 ;; need to do regex because some strings have 0 in front
+                                 ;; MariaDB doesn't like the || operator :(
+                                 (name uncasted-field) " REGEXP CONCAT('^0*', " "CAST(" casted-field " AS " db-type ") COLLATE utf8mb4_unicode_ci, '$')"
+                                 ", "
+                                 (name uncasted-field) " "
+                                 "FROM ( "
+                                 (-> query qp.compile/compile :query)
+                                 " ) AS subquery "
+                                 "LIMIT 100")})))
+
+(deftest ^:parallel integer-cast-table-fields
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "CHAR"}]]]
+                {:keys [field db-type]} fields]
+          (testing (str "casting " table "." field "(" db-type ") to integer")
+            (let [field-md (lib.metadata/field mp (mt/id table field))
+                  query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [field-md])
+                            (lib/expression "INTCAST" (lib/expression-clause :integer [field-md] nil))
+                            (lib/limit 100))
+                  result (-> query (check-integer-query db-type field) qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression (lib/expression-clause :concat
+                                                                                   [(lib.metadata/field mp (mt/id :people :id))
+                                                                                    (lib.metadata/field mp (mt/id :people :zip))] nil)
+                                                :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer")
+            (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [])
+                            (lib/expression "UNCASTED" expression)
+                            (as-> q
+                                  (lib/expression q "INTCAST" (lib/expression-clause :integer [(lib/expression-ref q "UNCASTED")] nil)))
+                            (lib/limit 10))
+                  result (-> query (check-integer-query db-type "`subquery`.`UNCASTED`") qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-nested-native-query
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[_table expressions] [[:people [{:expression "'1238493839384848'" :db-type "CHAR"}
+                                                {:expression "'-12372636363626263'" :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer from native query")
+            (let [native-query (mt/native-query {:query (str "SELECT " expression " AS UNCASTED")})]
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 {:dataset_query native-query
+                  :result_metadata (-> (qp/process-query native-query) :data :results_metadata :columns)
+                  :type :question}]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/expression-clause :integer [(->> q lib/visible-columns (filter #(= "UNCASTED" (:name %))) first)] nil))))
+                      result (-> query (check-integer-query db-type "`UNCASTED`") qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-query
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "CHAR"}]]]
+                {:keys [field db-type]} fields]
+          (let [nested-query (lib/query mp (lib.metadata/table mp (mt/id table)))]
+            (testing (str "Casting " db-type " to integer")
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 {:dataset_query nested-query
+                  :result_metadata (-> (qp/process-query nested-query) :data :results_metadata :columns)
+                  :type :question}]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (lib/with-fields [])
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/expression-clause :integer [(lib.metadata/field mp (mt/id table field))] nil)))
+                                (lib/limit 10))
+                      result (-> query (check-integer-query db-type field) qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-query-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression (lib/expression-clause :concat
+                                                                                   [(lib.metadata/field mp (mt/id :people :id))
+                                                                                    (lib.metadata/field mp (mt/id :people :zip))] nil)
+                                                :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (let [nested-query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                                 (lib/with-fields [])
+                                 (lib/expression "UNCASTED" expression)
+                                 (lib/limit 10))]
+            (testing (str "Casting " db-type " to integer")
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 {:dataset_query nested-query
+                  :result_metadata (-> (qp/process-query nested-query) :data :results_metadata :columns)
+                  :type :question}]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/expression-clause :integer [(->> q lib/visible-columns (filter #(= "UNCASTED" (:name %))) first)] nil)))
+                                (lib/limit 10))
+                      result (-> query (check-integer-query db-type "`subquery`.`UNCASTED`") qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression [(lib/expression-clause :concat
+                                                                                    [(lib.metadata/field mp (mt/id :people :id))
+                                                                                     (lib.metadata/field mp (mt/id :people :zip))] nil)
+                                                             (lib/expression-clause :concat
+                                                                                    [(lib.metadata/field mp (mt/id :people :id))
+                                                                                     (lib.metadata/field mp (mt/id :people :zip))] nil)]
+                                                :db-type "CHAR"}]]]
+                {db-type :db-type [e1 e2] :expression} expressions]
+          (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                          (lib/expression "UNCASTED" e1)
+                          (lib/expression "INTCAST" (lib/expression-clause :integer [e2] nil))
path: test/metabase/driver/mysql_test.clj
user: login: ericnormand
body: 👍 
created_at: 2025-03-27T15:10:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -815,3 +815,191 @@
             (is (= (->> unbinned-query qp/process-query mt/cols (map :database_type)
                         (map #(get {"TIMESTAMP" "DATETIME"} % %)))
                    (->> binned-query   qp/process-query mt/cols (map :database_type))))))))))
+
+;; integer()
+
+(defn- check-integer-query
+  ([query db-type uncasted-field]
+   (check-integer-query query db-type uncasted-field "`subquery`.`INTCAST`"))
+  ([query db-type uncasted-field casted-field]
+   (mt/native-query {:query (str "SELECT " casted-field ", "
+                                 ;; need to do regex because some strings have 0 in front
+                                 ;; MariaDB doesn't like the || operator :(
+                                 (name uncasted-field) " REGEXP CONCAT('^0*', " "CAST(" casted-field " AS " db-type ") COLLATE utf8mb4_unicode_ci, '$')"
+                                 ", "
+                                 (name uncasted-field) " "
+                                 "FROM ( "
+                                 (-> query qp.compile/compile :query)
+                                 " ) AS subquery "
+                                 "LIMIT 100")})))
+
+(deftest ^:parallel integer-cast-table-fields
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "CHAR"}]]]
+                {:keys [field db-type]} fields]
+          (testing (str "casting " table "." field "(" db-type ") to integer")
+            (let [field-md (lib.metadata/field mp (mt/id table field))
+                  query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [field-md])
+                            (lib/expression "INTCAST" (lib/expression-clause :integer [field-md] nil))
+                            (lib/limit 100))
+                  result (-> query (check-integer-query db-type field) qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression (lib/expression-clause :concat
+                                                                                   [(lib.metadata/field mp (mt/id :people :id))
+                                                                                    (lib.metadata/field mp (mt/id :people :zip))] nil)
+                                                :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer")
+            (let [query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                            (lib/with-fields [])
+                            (lib/expression "UNCASTED" expression)
+                            (as-> q
+                                  (lib/expression q "INTCAST" (lib/expression-clause :integer [(lib/expression-ref q "UNCASTED")] nil)))
+                            (lib/limit 10))
+                  result (-> query (check-integer-query db-type "`subquery`.`UNCASTED`") qp/process-query)
+                  cols (mt/cols result)
+                  rows (mt/rows result)]
+              (is (= :type/BigInteger (-> cols first :base_type)))
+              (doseq [[casted-value equals? uncasted-value] rows]
+                (is (int? casted-value))
+                (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))
+
+(deftest ^:parallel integer-cast-nested-native-query
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[_table expressions] [[:people [{:expression "'1238493839384848'" :db-type "CHAR"}
+                                                {:expression "'-12372636363626263'" :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (testing (str "Casting " db-type " to integer from native query")
+            (let [native-query (mt/native-query {:query (str "SELECT " expression " AS UNCASTED")})]
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 {:dataset_query native-query
+                  :result_metadata (-> (qp/process-query native-query) :data :results_metadata :columns)
+                  :type :question}]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/expression-clause :integer [(->> q lib/visible-columns (filter #(= "UNCASTED" (:name %))) first)] nil))))
+                      result (-> query (check-integer-query db-type "`UNCASTED`") qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-query
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table fields] [[:people [{:field :zip :db-type "CHAR"}]]]
+                {:keys [field db-type]} fields]
+          (let [nested-query (lib/query mp (lib.metadata/table mp (mt/id table)))]
+            (testing (str "Casting " db-type " to integer")
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 {:dataset_query nested-query
+                  :result_metadata (-> (qp/process-query nested-query) :data :results_metadata :columns)
+                  :type :question}]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (lib/with-fields [])
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/expression-clause :integer [(lib.metadata/field mp (mt/id table field))] nil)))
+                                (lib/limit 10))
+                      result (-> query (check-integer-query db-type field) qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-query-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression (lib/expression-clause :concat
+                                                                                   [(lib.metadata/field mp (mt/id :people :id))
+                                                                                    (lib.metadata/field mp (mt/id :people :zip))] nil)
+                                                :db-type "CHAR"}]]]
+                {:keys [expression db-type]} expressions]
+          (let [nested-query (-> (lib/query mp (lib.metadata/table mp (mt/id table)))
+                                 (lib/with-fields [])
+                                 (lib/expression "UNCASTED" expression)
+                                 (lib/limit 10))]
+            (testing (str "Casting " db-type " to integer")
+              (mt/with-temp
+                [:model/Card
+                 {card-id :id}
+                 {:dataset_query nested-query
+                  :result_metadata (-> (qp/process-query nested-query) :data :results_metadata :columns)
+                  :type :question}]
+                (let [query (-> (lib/query mp (lib.metadata/card mp card-id))
+                                (as-> q
+                                      (lib/expression q "INTCAST" (lib/expression-clause :integer [(->> q lib/visible-columns (filter #(= "UNCASTED" (:name %))) first)] nil)))
+                                (lib/limit 10))
+                      result (-> query (check-integer-query db-type "`subquery`.`UNCASTED`") qp/process-query)
+                      cols (mt/cols result)
+                      rows (mt/rows result)]
+                  (is (= :type/BigInteger (-> cols first :base_type)))
+                  (doseq [[casted-value equals? uncasted-value] rows]
+                    (is (int? casted-value))
+                    (is equals? (str "Not equal for: " casted-value " " uncasted-value))))))))))))
+
+(deftest ^:parallel integer-cast-nested-custom-expressions
+  (mt/test-driver :mysql
+    (mt/dataset test-data
+      (let [mp (mt/metadata-provider)]
+        (doseq [[table expressions] [[:people [{:expression [(lib/expression-clause :concat
+                                                                                    [(lib.metadata/field mp (mt/id :people :id))
+                                                                                     (lib.metadata/field mp (mt/id :people :zip))] nil)
+                                                             (lib/expression-clause :concat
+                                                                                    [(lib.metadata/field mp (mt/id :people :id))
+                                                                                     (lib.metadata/field mp (mt/id :people :zip))] nil)]
path: test/metabase/driver/mysql_test.clj
user: login: ericnormand
body: 👍 
created_at: 2025-03-27T15:11:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: trunk-io[bot]
created_at: 2025-03-25T14:40:58Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://img.shields.io/badge/92344_tests-ebd501c2-purple)](https://app.trunk.io/metabase/flaky-tests/pr/55691?repo=metabase/metabase&commitHash=ebd501c268e412891ddba9ae7f5151484afd3203&utm_source=gh-comment) &ensp; [![Static Badge](https://img.shields.io/badge/4-failed-crimson)](https://app.trunk.io/metabase/flaky-tests/pr/55691?repo=metabase/metabase&commitHash=ebd501c268e412891ddba9ae7f5151484afd3203&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/286-flaky-red)](https://app.trunk.io/metabase/flaky-tests/pr/55691?repo=metabase/metabase&commitHash=ebd501c268e412891ddba9ae7f5151484afd3203&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/metabase/flaky-tests/pr/55691?repo=metabase/metabase&commitHash=ebd501c268e412891ddba9ae7f5151484afd3203&utm_source=gh-comment&runConclusion=QUARANTINED)


| Failed Test | Failure Summary | Logs |
| --- | --- | --- |
| <code>join-order-test</code> | Joins should be emitted in the same order as they were specified in MBQL. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/55691?repo=metabase/metabase&commitHash=ebd501c268e412891ddba9ae7f5151484afd3203&utm_source=gh-comment&runConclusion=FAILURE&runId=be876b67-9934-54ed-b1e9-def679ec9172) |
| <code>include-no-fields-test</code> | Failed to close statement due to Socket is closed by peer. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/55691?repo=metabase/metabase&commitHash=ebd501c268e412891ddba9ae7f5151484afd3203&utm_source=gh-comment&runConclusion=FAILURE&runId=cd8a8c51-a607-522a-a741-a2506c04cc40) |
| <code>left-outer-join-test</code> | Socket connection was closed by peer, preventing query execution. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/55691?repo=metabase/metabase&commitHash=ebd501c268e412891ddba9ae7f5151484afd3203&utm_source=gh-comment&runConclusion=FAILURE&runId=12b76d23-4ad3-51df-b9a8-e12bc82b2fe4) |
| <code>inner-join-test</code> | HiveStatement failed to close statement due to Socket being closed by peer. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/55691?repo=metabase/metabase&commitHash=ebd501c268e412891ddba9ae7f5151484afd3203&utm_source=gh-comment&runConclusion=FAILURE&runId=d729131c-020c-5664-aec7-dc06f3b66fa0) |


| Flaky Test | Failure Summary | Logs |
| --- | --- | --- |
| <code>identity-hash-test</code> | The hash of a collection did not match the expected value. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/55691?repo=metabase/metabase&commitHash=ebd501c268e412891ddba9ae7f5151484afd3203&utm_source=gh-comment&runConclusion=FAILURE&runId=86f92ccc-0c93-5961-b0e5-43500aa40f16) |
| <code>unsubscribe-receive-email-test</code> | The unsubscribe confirmation email was sent with incorrect body format. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/55691?repo=metabase/metabase&commitHash=ebd501c268e412891ddba9ae7f5151484afd3203&utm_source=gh-comment&runConclusion=FAILURE&runId=76af7322-a58f-5043-b3e4-452d3c713fcf) |
| <code>fetch-token-does-not-call-db-when-cached</code> | failure\_text: token\_check\_test.clj:163No DB calls are made for the user count when checking token status if the status is cachedexpected: (= 4 (... | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/55691?repo=metabase/metabase&commitHash=ebd501c268e412891ddba9ae7f5151484afd3203&utm_source=gh-comment&runConclusion=FAILURE&runId=34c08a76-8689-59ba-a6ff-d5c46ebe720d) |
| <code>test-concat-non-string-args</code> | Connection to Hive database failed with "Connection refused" error. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/55691?repo=metabase/metabase&commitHash=ebd501c268e412891ddba9ae7f5151484afd3203&utm_source=gh-comment&runConclusion=FAILURE&runId=c5cf355e-bac6-50e2-9267-bc17f90a8cb2) |

[... and 282 more](https://app.trunk.io/metabase/flaky-tests/pr/55691?repo=metabase/metabase&commitHash=ebd501c268e412891ddba9ae7f5151484afd3203&utm_source=gh-comment&runConclusion=FAILURE)
<sub>

[View Full Report ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/55691?repo=metabase/metabase&commitHash=ebd501c268e412891ddba9ae7f5151484afd3203&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE