Skip to main content
The driver interface defines how Metabase communicates with different databases. Understanding this interface is essential for building new drivers or customizing existing ones.

What is a driver?

In Metabase, a driver is simply a keyword (like :postgres, :mysql, :mongodb) plus implementations of driver multimethods. There are no classes or objects—just a keyword and functions that dispatch on that keyword.
;; A driver is just this:
(driver/register! :mydriver)

;; Plus method implementations:
(defmethod driver/display-name :mydriver [_]
  "My Database")

Driver architecture

A Metabase driver provides four main capabilities:
1

Database information

Provides metadata like display name, connection properties, and capabilities.
2

Schema introspection

Discovers and reports database schema (tables, columns, foreign keys).
3

Query compilation

Converts MBQL (Metabase Query Language) queries into native database queries.
4

Query execution

Executes native queries and returns formatted results.

Driver multimethods

Multimethods are Clojure’s polymorphism mechanism. They dispatch on the driver keyword:
(ns metabase.driver.mydriver
  (:require [metabase.driver :as driver]))

;; Register the driver
(driver/register! :mydriver, :parent :sql-jdbc)

;; Implement multimethods
(defmethod driver/display-name :mydriver [_]
  "My Database")

(defmethod driver/supports? [:mydriver :foreign-keys] [_ _]
  true)

Listing available methods

To see all available driver methods:
# List method names
clojure -M:run driver-methods

# List with documentation
clojure -M:run driver-methods docs

Essential driver methods

Display and metadata

(defmethod driver/display-name :mydriver [_]
  "My Database")

Connection management

(defmethod sql-jdbc.conn/connection-details->spec :mydriver
  [_ {:keys [host port dbname user password]}]
  {:classname "com.mydb.Driver"
   :subprotocol "mydb"
   :subname (str "//" host ":" port "/" dbname)
   :user user
   :password password})

Database capabilities

;; Declare what features the database supports
(defmethod driver/database-supports? [:mydriver :foreign-keys]
  [_ _ _]
  true)

(defmethod driver/database-supports? [:mydriver :nested-queries]
  [_ _ _]
  true)

(defmethod driver/database-supports? [:mydriver :set-timezone]
  [_ _ _]
  false)

(defmethod driver/database-supports? [:mydriver :left-join]
  [_ _ _]
  true)

(defmethod driver/database-supports? [:mydriver :expressions]
  [_ _ _]
  true)

SQL generation

(defmethod sql.qp/quote-style :mydriver [_]
  :ansi)  ; or :mysql, :sqlserver, etc.

Type mapping

(defmethod sql-jdbc.sync/database-type->base-type :mydriver
  [_ database-type]
  (case (keyword database-type)
    :varchar    :type/Text
    :text       :type/Text
    :integer    :type/Integer
    :bigint     :type/BigInteger
    :float      :type/Float
    :double     :type/Float
    :decimal    :type/Decimal
    :date       :type/Date
    :datetime   :type/DateTime
    :timestamp  :type/DateTimeWithLocalTZ
    :boolean    :type/Boolean
    :json       :type/JSON
    :type/*))   ; Default fallback

Parent drivers

Parent drivers provide shared functionality, similar to inheritance:

Common parent drivers

The most common parent for SQL databases with JDBC support:
(driver/register! :mydriver, :parent :sql-jdbc)
Provides:
  • Connection pooling
  • Query execution
  • Transaction management
  • Basic schema sync
  • Result set processing
For SQL databases without JDBC (like BigQuery):
(driver/register! :bigquery, :parent :sql)
Provides:
  • SQL query compilation
  • Query preprocessing
  • Some schema sync functionality
You implement:
  • Connection management
  • Query execution
Some drivers inherit from other concrete drivers:
(driver/register! :redshift, :parent :postgres)
Only override methods where behavior differs.

Multiple parents

Drivers can have multiple parents:
(driver/register! :bigquery, :parent #{:sql :google})
This combines functionality from both parents.

Calling parent implementations

Call parent methods using get-method:
(defmethod driver/mbql->native :mydriver
  [driver query]
  ;; Do some custom preprocessing
  (let [query (preprocess-query query)]
    ;; Then call parent implementation
    ((get-method driver/mbql->native :sql-jdbc) driver query)))
Always pass the driver argument as-is to parent implementations. Don’t replace it with a keyword.
;; ✅ CORRECT
((get-method driver/mbql->native :sql) driver query)

;; ❌ WRONG - breaks method dispatch
((get-method driver/mbql->native :sql) :sql query)
((get-method driver/mbql->native :sql) :mydriver query)

Driver namespaces

Driver code can be organized across multiple namespaces:
metabase/driver/mydriver/
├── core.clj             # Main driver registration
├── query_processor.clj  # MBQL compilation
├── sync.clj            # Schema introspection
└── execute.clj         # Query execution
Example structure:
;; metabase.driver.mydriver.core
(ns metabase.driver.mydriver.core
  (:require [metabase.driver :as driver]
            [metabase.driver.mydriver.query-processor]
            [metabase.driver.mydriver.sync]))

(driver/register! :mydriver, :parent :sql-jdbc)

(defmethod driver/display-name :mydriver [_]
  "My Database")

Driver initialization

Drivers can run initialization code once using driver/initialize!:
(driver/initialize! :mydriver
  (fn []
    ;; Run once when driver loads
    (setup-connection-pool)
    (register-type-handlers)
    (log/info "MyDriver initialized")))
Use initialize! only for one-time setup like resource allocation or system property configuration.

JDBC-specific methods

For JDBC-based drivers, implement these SQL-JDBC methods:

Connection

(defmethod sql-jdbc.conn/connection-details->spec :mydriver
  [_ {:keys [host port dbname user password ssl]}]
  (merge
    {:classname "com.mydb.Driver"
     :subprotocol "mydb"
     :subname (str "//" host ":" port "/" dbname)
     :user user
     :password password}
    (when ssl
      {:ssl true
       :sslfactory "org.postgresql.ssl.NonValidatingFactory"})))

Timezone handling

(defmethod sql-jdbc.execute/set-timezone-sql :mydriver [_]
  "SET TIME ZONE %s;")

Result processing

(defmethod sql-jdbc.execute/read-column-thunk [:mydriver Types/TIMESTAMP]
  [_ ^ResultSet rs ^Integer i]
  (fn []
    (.getTimestamp rs i)))

Non-JDBC drivers

For databases without JDBC, implement query execution:
(defmethod driver/execute-reducible-query :mydriver
  [driver query context respond]
  (let [native-query (qp/compile driver query)
        conn (get-connection (:database query))]
    (try
      (let [results (execute-query conn native-query)]
        (respond {:cols (result-columns results)
                  :rows (result-rows results)}))
      (finally
        (close-connection conn)))))

Schema introspection

Implement sync methods for database schema discovery:
(defmethod driver/describe-database :mydriver
  [_ database]
  {:tables (get-tables database)})

(defmethod driver/describe-table :mydriver
  [_ database table]
  {:name   (:name table)
   :schema (:schema table)
   :fields (get-fields database table)})

(defmethod driver/describe-table-fks :mydriver
  [_ database table]
  (get-foreign-keys database table))

Testing driver methods

Test your driver implementations:
(ns metabase.driver.mydriver-test
  (:require
   [clojure.test :refer :all]
   [metabase.driver :as driver]
   [metabase.test :as mt]))

(deftest display-name-test
  (is (= "My Database"
         (driver/display-name :mydriver))))

(deftest supports-foreign-keys-test
  (is (driver/database-supports? :mydriver :foreign-keys nil)))

(deftest connection-test
  (testing "Can connect with valid credentials"
    (is (driver/can-connect? :mydriver
                             {:host "localhost"
                              :port 5432
                              :dbname "test"}))))

Working from the REPL

Develop and test drivers interactively:
# Start REPL with driver dependencies
clojure -A:dev:drivers:drivers-dev
In the REPL:
;; Load your driver
(require 'metabase.driver.mydriver :reload)

;; Test methods
(driver/display-name :mydriver)
(driver/database-supports? :mydriver :foreign-keys nil)

;; Test connection
(driver/can-connect? :mydriver {:host "localhost" :port 5432})

;; Run a query
(require 'metabase.test :as mt)
(mt/dataset test-data
  (mt/run-mbql-query venues {:limit 10}))

Reference

Key namespaces

  • metabase.driver - Core driver interface
  • metabase.driver.sql - SQL driver abstractions
  • metabase.driver.sql-jdbc - JDBC driver abstractions
  • metabase.driver.sql.query-processor - SQL query compilation
  • metabase.driver.sql-jdbc.connection - JDBC connections
  • metabase.driver.sql-jdbc.sync - JDBC schema sync
  • metabase.driver.sql-jdbc.execute - JDBC query execution

Example drivers to study

  • PostgreSQL - Full-featured JDBC driver
  • SQLite - Simple JDBC driver
  • MongoDB - Non-SQL driver
  • BigQuery - SQL non-JDBC driver
  • Redshift - Driver with parent override

Next steps