Shared code for drivers for SQL databases using their respective JDBC drivers under the hood.

(ns metabase.driver.sql-jdbc
  (:require
   [clojure.java.jdbc :as jdbc]
   [honey.sql :as sql]
   [metabase.driver :as driver]
   [metabase.driver.sql :as driver.sql]
   [metabase.driver.sql-jdbc.actions :as sql-jdbc.actions]
   [metabase.driver.sql-jdbc.connection :as sql-jdbc.conn]
   [metabase.driver.sql-jdbc.execute :as sql-jdbc.execute]
   [metabase.driver.sql-jdbc.metadata :as sql-jdbc.metadata]
   [metabase.driver.sql-jdbc.quoting :refer [with-quoting quote-columns quote-identifier quote-table]]
   [metabase.driver.sql-jdbc.sync :as sql-jdbc.sync]
   [metabase.driver.sql.query-processor :as sql.qp]
   [metabase.driver.sync :as driver.s]
   [metabase.query-processor.writeback :as qp.writeback]
   [metabase.util.honey-sql-2 :as h2x]
   [metabase.util.malli :as mu])
  (:import
   (java.sql Connection)))
(set! *warn-on-reflection* true)
(comment sql-jdbc.actions/keep-me)
(driver/register! :sql-jdbc, :parent :sql, :abstract? true)

+----------------------------------------------------------------------------------------------------------------+ | Run a Query | +----------------------------------------------------------------------------------------------------------------+

Execute a honeysql-form query against database, driver, and optionally table.

TODO - Seems like this is only used in a handful of places, consider moving to util namespace

(defn query
  ([driver database honeysql-form]
   (jdbc/query (sql-jdbc.conn/db->pooled-connection-spec database)
               (sql.qp/format-honeysql driver honeysql-form)))
  ([driver database table honeysql-form]
   (let [table-identifier (sql.qp/->honeysql driver (h2x/identifier :table (:schema table) (:name table)))]
     (query driver database (merge {:from [[table-identifier]]}
                                   honeysql-form)))))

+----------------------------------------------------------------------------------------------------------------+ | Default SQL JDBC metabase.driver impls | +----------------------------------------------------------------------------------------------------------------+

(defmethod driver/can-connect? :sql-jdbc
  [driver details]
  (sql-jdbc.conn/can-connect? driver details))
(defmethod driver/table-rows-seq :sql-jdbc
  [driver database table]
  (query driver database table {:select [:*]}))
(defn- has-method? [driver multifn]
  {:pre [(keyword? driver)]}
  (when-let [driver-method (get-method multifn driver)]
    (and driver-method
         (not (identical? driver-method (get-method multifn :sql-jdbc)))
         (not (identical? driver-method (get-method multifn :default))))))

TODO - this implementation should itself be deprecated! And have drivers implement it directly instead.

(defmethod driver/database-supports? [:sql-jdbc :set-timezone]
  [driver _feature _db]
  (boolean (seq (sql-jdbc.execute/set-timezone-sql driver))))
(defmethod driver/db-default-timezone :sql-jdbc
  [driver database]
  ;; if the driver has a non-default implementation of [[sql-jdbc.sync/db-default-timezone]], use that.
  #_{:clj-kondo/ignore [:deprecated-var]}
  (if (has-method? driver sql-jdbc.sync/db-default-timezone)
    (sql-jdbc.sync/db-default-timezone driver (sql-jdbc.conn/db->pooled-connection-spec database))
    ;; otherwise fall back to the default implementation.
    ((get-method driver/db-default-timezone :metabase.driver/driver) driver database)))
(defmethod driver/execute-reducible-query :sql-jdbc
  [driver query context respond]
  (sql-jdbc.execute/execute-reducible-query driver query context respond))
(defmethod driver/notify-database-updated :sql-jdbc
  [_ database]
  (sql-jdbc.conn/invalidate-pool-for-db! database))
(defmethod driver/dbms-version :sql-jdbc
  [driver database]
  (sql-jdbc.sync/dbms-version driver (sql-jdbc.conn/db->pooled-connection-spec database)))
(defmethod driver/describe-database :sql-jdbc
  [driver database]
  (sql-jdbc.sync/describe-database driver database))
(defmethod driver/describe-table :sql-jdbc
  [driver database table]
  (sql-jdbc.sync/describe-table driver database table))
(defmethod driver/describe-fields :sql-jdbc
  [driver database & {:as args}]
  (sql-jdbc.sync/describe-fields driver database args))
(defmethod driver/describe-indexes :sql-jdbc
  [driver database & {:as args}]
  (sql-jdbc.sync/describe-indexes driver database args))
#_{:clj-kondo/ignore [:deprecated-var]}
(defmethod driver/describe-table-fks :sql-jdbc
  [driver database table]
  (sql-jdbc.sync/describe-table-fks driver database table))
(defmethod driver/describe-fks :sql-jdbc
  [driver database & {:as args}]
  (sql-jdbc.sync/describe-fks driver database args))
(defmethod driver/describe-table-indexes :sql-jdbc
  [driver database table]
  (sql-jdbc.sync/describe-table-indexes driver database table))
(defmethod sql.qp/cast-temporal-string [:sql-jdbc :Coercion/ISO8601->DateTime]
  [_driver _semantic_type expr]
  (h2x/->timestamp expr))
(defmethod sql.qp/cast-temporal-string [:sql-jdbc :Coercion/ISO8601->Date]
  [_driver _semantic_type expr]
  (h2x/->date expr))
(defmethod sql.qp/cast-temporal-string [:sql-jdbc :Coercion/ISO8601->Time]
  [_driver _semantic_type expr]
  (h2x/->time expr))
(defmethod sql.qp/cast-temporal-string [:sql-jdbc :Coercion/YYYYMMDDHHMMSSString->Temporal]
  [_driver _semantic_type expr]
  (h2x/->timestamp expr))
(defn- create-table!-sql
  [driver table-name column-definitions & {:keys [primary-key]}]
  (with-quoting driver
    (first (sql/format {:create-table (quote-table table-name)
                        :with-columns (cond-> (mapv (fn [[col-name type-spec]]
                                                      (vec (cons (quote-identifier col-name)
                                                                 (if (string? type-spec)
                                                                   [[:raw type-spec]]
                                                                   type-spec))))
                                                    column-definitions)
                                        primary-key (conj [(into [:primary-key] primary-key)]))}
                       :quoted true
                       :dialect (sql.qp/quote-style driver)))))
(defmethod driver/create-table! :sql-jdbc
  [driver database-id table-name column-definitions & {:keys [primary-key]}]
  (let [sql (create-table!-sql driver table-name column-definitions :primary-key primary-key)]
    (qp.writeback/execute-write-sql! database-id sql)))
(defmethod driver/drop-table! :sql-jdbc
  [driver db-id table-name]
  (let [sql (first (sql/format {:drop-table [:if-exists (keyword table-name)]}
                               :quoted true
                               :dialect (sql.qp/quote-style driver)))]
    (qp.writeback/execute-write-sql! db-id sql)))
(defmethod driver/truncate! :sql-jdbc
  [driver db-id table-name]
  (let [table-name (keyword table-name)
        sql        (sql/format {:truncate table-name}
                               :quoted true
                               :dialect (sql.qp/quote-style driver))]
    (jdbc/with-db-transaction [conn (sql-jdbc.conn/db->pooled-connection-spec db-id)]
      (jdbc/execute! conn sql))))
(defmethod driver/insert-into! :sql-jdbc
  [driver db-id table-name column-names values]
  (let [;; We need to partition the insert into multiple statements for both performance and correctness.
        ;;
        ;; On Postgres with a large file, 100 (3.76m) was significantly faster than 50 (4.03m) and 25 (4.27m). 1,000 was a
        ;; little faster but not by much (3.63m), and 10,000 threw an error:
        ;;     PreparedStatement can have at most 65,535 parameters
        ;; One imagines that `(long (/ 65535 (count columns)))` might be best, but I don't trust the 65K limit to apply
        ;; across all drivers. With that in mind, 100 seems like a safe compromise.
        ;; There's nothing magic about 100, but it felt good in testing. There could well be a better number.
        chunks     (partition-all (or driver/*insert-chunk-rows* 100) values)
        dialect    (sql.qp/quote-style driver)
        sqls       (map #(sql/format {:insert-into (keyword table-name)
                                      :columns     (quote-columns driver column-names)
                                      :values      %}
                                     :quoted true
                                     :dialect dialect)
                        chunks)]
    (jdbc/with-db-transaction [conn (sql-jdbc.conn/db->pooled-connection-spec db-id)]
      (doseq [sql sqls]
        (jdbc/execute! conn sql)))))
(defmethod driver/add-columns! :sql-jdbc
  [driver db-id table-name column-definitions & {:keys [primary-key]}]
  (mu/validate-throw [:maybe [:cat :keyword]] primary-key) ; we only support adding a single primary key column for now
  (with-quoting driver
    (let [primary-key-column (first primary-key)
          sql                (first (sql/format {:alter-table (keyword table-name)
                                                 :add-column  (map (fn [[column-name type-and-constraints]]
                                                                     (cond-> (vec (cons (quote-identifier column-name)
                                                                                        (if (string? type-and-constraints)
                                                                                          [[:raw type-and-constraints]]
                                                                                          type-and-constraints)))
                                                                       (= primary-key-column column-name)
                                                                       (conj :primary-key)))
                                                                   column-definitions)}
                                                :quoted true
                                                :dialect (sql.qp/quote-style driver)))]
      (qp.writeback/execute-write-sql! db-id sql))))
(defmethod driver/alter-columns! :sql-jdbc
  [driver db-id table-name column-definitions]
  (qp.writeback/execute-write-sql! db-id (sql-jdbc.sync/alter-columns-sql driver table-name column-definitions)))
(defmethod driver/syncable-schemas :sql-jdbc
  [driver database]
  (sql-jdbc.execute/do-with-connection-with-options
   driver
   database
   nil
   (fn [^java.sql.Connection conn]
     (let [[inclusion-patterns
            exclusion-patterns] (driver.s/db-details->schema-filter-patterns database)]
       (into #{} (sql-jdbc.sync/filtered-syncable-schemas driver conn (.getMetaData conn) inclusion-patterns exclusion-patterns))))))
(defmethod driver/set-role! :sql-jdbc
  [driver conn role]
  (let [sql (driver.sql/set-role-statement driver role)]
    (with-open [stmt (.createStatement ^Connection conn)]
      (.execute stmt sql))))
(defmethod driver/current-user-table-privileges :sql-jdbc
  [driver database & {:as args}]
  (sql-jdbc.sync/current-user-table-privileges
   driver
   (sql-jdbc.conn/db->pooled-connection-spec database)
   args))
(defmethod driver/query-result-metadata :sql-jdbc
  [driver query]
  (sql-jdbc.metadata/query-result-metadata driver query))