Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
util_test.clj 7.03 KiB
(ns ^:mb/driver-tests metabase.driver.sql.util-test
  (:require
   [clojure.string :as str]
   [clojure.test :refer :all]
   [metabase.driver :as driver]
   [metabase.driver.sql.util :as sql.u]
   [metabase.util.honey-sql-2 :as h2x]))

;;; Ok to hardcode driver names here because it's for a general util function and not something that needs to be run
;;; against all supported drivers
#_{:clj-kondo/ignore [:metabase/disallow-hardcoded-driver-names-in-tests]}
(deftest ^:parallel quote-name-test
  (are [driver expected] (= expected
                            (sql.u/quote-name driver :field "wow"))
    :mysql    "`wow`"
    :h2       "\"wow\""
    :postgres "\"wow\""))

(deftest ^:parallel select-clause-alias-everything-test
  (testing "first column is just <identifer>, wrap it like [<identifier> <alias>]"
    (is (= [[(h2x/identifier :field "A" "B" "C" "D") (h2x/identifier :field-alias "D")]
            [(h2x/identifier :field "F")             (h2x/identifier :field-alias "G")]]
           (sql.u/select-clause-alias-everything
            [(h2x/identifier :field "A" "B" "C" "D")
             [(h2x/identifier :field "F")            (h2x/identifier :field-alias "G")]])))))

(deftest ^:parallel select-clause-deduplicate-aliases
  (testing 'select-clause-deduplicate-aliases
    (testing "should use the last component of an identifier as the alias if it does not already have one"
      (is (= [[(h2x/identifier :field "A" "B" "C" "D") (h2x/identifier :field-alias "D")]
              [(h2x/identifier :field "F")             (h2x/identifier :field-alias "G")]]
             (sql.u/select-clause-deduplicate-aliases
              [(h2x/identifier :field "A" "B" "C" "D")
               [(h2x/identifier :field "F")            (h2x/identifier :field-alias "G")]]))))))

(deftest ^:parallel select-clause-deduplicate-aliases-2
  (testing 'select-clause-deduplicate-aliases
    (testing "should append numeric suffixes to duplicate aliases"
      (is (= [[(h2x/identifier :field "A" "B" "C" "D") (h2x/identifier :field-alias "D")]
              [(h2x/identifier :field "E" "D")         (h2x/identifier :field-alias "D_2")]
              [(h2x/identifier :field "F")             (h2x/identifier :field-alias "G")]]
             (sql.u/select-clause-deduplicate-aliases
              [(h2x/identifier :field "A" "B" "C" "D")
               (h2x/identifier :field "E" "D")
               [(h2x/identifier :field "F")            (h2x/identifier :field-alias "G")]]))))))

(deftest ^:parallel select-clause-deduplicate-aliases-3
  (testing 'select-clause-deduplicate-aliases
    (testing "should handle aliases that are already suffixed gracefully"
      (is (= [[(h2x/identifier :field "A" "B" "C" "D") (h2x/identifier :field-alias "D")]
              [(h2x/identifier :field "E" "D")         (h2x/identifier :field-alias "D_2")]
              [(h2x/identifier :field "F")             (h2x/identifier :field-alias "D_3")]]
             (sql.u/select-clause-deduplicate-aliases
              [(h2x/identifier :field "A" "B" "C" "D")
               (h2x/identifier :field "E" "D")
               [(h2x/identifier :field "F")            (h2x/identifier :field-alias "D_2")]]))))))

(deftest ^:parallel escape-sql-test
  (doseq [[escape-strategy s->expected]
          {:ansi
           {"Tito's Tacos"          "Tito''s Tacos"
            "\\\\\\\\' OR 1 = 1 --" "\\\\\\\\'' OR 1 = 1 --"
            "\\\\' OR 1 = 1 --"     "\\\\'' OR 1 = 1 --"
            "\\' OR 1 = 1 --"       "\\'' OR 1 = 1 --"
            "' OR 1 = 1 --"         "'' OR 1 = 1 --"}

           :backslashes
           {"Tito's Tacos"          "Tito\\'s Tacos"
            "\\\\\\\\' OR 1 = 1 --" "\\\\\\\\\\\\\\\\\\' OR 1 = 1 --"
            "\\\\' OR 1 = 1 --"     "\\\\\\\\\\' OR 1 = 1 --"
            "\\' OR 1 = 1 --"       "\\\\\\' OR 1 = 1 --"
            "' OR 1 = 1 --"         "\\' OR 1 = 1 --"}}

          [s expected] s->expected]
    (testing escape-strategy
      (testing (pr-str s)
        (is (= expected
               (sql.u/escape-sql s escape-strategy)))))))

;;; Ok to hardcode driver names in the tests below because they're for general util functions and not something that
;;; needs to be run against all supported drivers

#_{:clj-kondo/ignore [:metabase/disallow-hardcoded-driver-names-in-tests]}
(deftest ^:parallel format-sql-with-params-test
  (testing "Baseline: format-sql expands metabase params, which is not desired."
    (is (= "SELECT\n  *\nFROM\n  { { # 1234 } }"
           (sql.u/format-sql :postgres "SELECT * FROM {{#1234}}")))
    (is (= "SELECT\n  *\nFROM\n  { { #1234}}"
           (sql.u/format-sql :mysql "SELECT * FROM {{#1234}}")))))

#_{:clj-kondo/ignore [:metabase/disallow-hardcoded-driver-names-in-tests]}
(deftest ^:parallel format-sql-with-params-test-2
  (testing "A compact representation should remain compact (and inner spaces removed, if any)."
    (is (= "SELECT\n  *\nFROM\n  {{#1234}}"
           (sql.u/format-sql-and-fix-params :postgres "SELECT * FROM {{ #1234 }}")))
    (is (= "SELECT\n  *\nFROM\n  {{#1234}}"
           (sql.u/format-sql-and-fix-params :postgres "SELECT * FROM {{#1234}}")))))

#_{:clj-kondo/ignore [:metabase/disallow-hardcoded-driver-names-in-tests]}
(deftest ^:parallel format-sql-with-params-test-3
  (testing "Symbolic params should also have spaces removed."
    (is (= "SELECT\n  *\nFROM\n  {{FOO_BAR}}"
           (sql.u/format-sql-and-fix-params :postgres "SELECT * FROM {{FOO_BAR}}")))
    (is (= "SELECT\n  *\nFROM\n  {{FOO_BAR}}"
           (sql.u/format-sql-and-fix-params :postgres "SELECT * FROM {{ FOO_BAR }}")))))

#_{:clj-kondo/ignore [:metabase/disallow-hardcoded-driver-names-in-tests]}
(deftest ^:parallel format-sql-with-params-test-4
  (testing "Dialect-specific versions should work"
    (is (= "SELECT\n  A\nFROM\n  {{#1234}} WHERE {{STATE}}"
           (sql.u/format-sql-and-fix-params :mysql "SELECT A FROM { { #1234}} WHERE {{ STATE}  }")))
    (is (= "SELECT\n  A\nFROM\n  {{#1234}}\nWHERE\n  {{STATE}}"
           (sql.u/format-sql-and-fix-params :postgres "SELECT A FROM { { #1234}} WHERE {{ STATE}  }")))))

(defmulti pound-sign-is-special-word-char?
  {:arglists '([driver-or-dialect])}
  keyword)

(defmethod pound-sign-is-special-word-char? :default
  [_driver-or-dialect]
  false)

;;; the formatter includes # in specialWordChars for :db2, :plsql, :redshift, and :tsql
(doseq [driver-or-dialect [:db2 :plsql :redshift :tsql]]
  (defmethod pound-sign-is-special-word-char? driver-or-dialect
    [_driver-or-dialect]
    true))

(deftest ^:parallel format-sql-with-additional-operators
  (testing "Should not split additional operators (#36175)"
    (doseq [op @#'sql.u/additional-operators
            :let [q (str "SELECT a FROM t WHERE x " op " 12")]]
      (testing (str "operator " op)
        (doseq [driver-or-dialect (into #{} cat [(descendants driver/hierarchy :sql) (keys sql.u/dialects)])
                :when (not (and (str/includes? op "#")
                                (pound-sign-is-special-word-char? driver-or-dialect)))]
          (testing (str "driver or dialect " driver-or-dialect)
            (is (str/includes? (sql.u/format-sql-and-fix-params driver-or-dialect q) op))))))))