Skip to content

Metabase Backend Dev Secrets

dpsutton edited this page Jun 21, 2022 · 25 revisions

Setting up the app DB.

If you see a message like this in the REPL:

DB is not set up. Make sure to call set-default-db-connection! or bind *db-connection*

You just need to set up the app DB.

(metabase.db/setup-db!)

This process is slightly different in a test. To set up the app database for use in your tests, do the following at the top of the test:

(use-fixtures :once (fixtures/initialize :db))

Starting a server from the REPL

This is a really good way to get the server up and running for development with all of the drivers. Start the REPL through your normal means (clojure-jack-in, lein repl, etc)

(require 'dev)
(dev/start!)

Starting a server with nREPL and code reloading

Add the :nrepl entry below to the Ring config in project.clj, and run lein ring server:

IMPORTANT NOTE: lein ring server is currently not working correctly -- see https://github.com/metabase/metabase/issues/12181 -- so this won't work until we fix it. (Not super high priority).
     :ring
     {:handler      metabase.handler/app
      :init         metabase.core/init!
      :async?       true
      :destroy      metabase.core/destroy
      :nrepl        {:start? true, :port 9998}
      :reload-paths ["src"]}

:port is optional and, if not specified, a random open port will be used. [1]

The test-data Database

The test-data Database is used for most of our tests and its definition lives in test-data.edn. When running tests, the driver "test extensions" will automatically create this database, load the appropriate data, and sync it, as needed. You can get this Database by calling (metabase.test/db) or get its ID, or the ID of one of its Tables or Fields, by calling (metabase.test/id), (metabase.test/id :table-name), or (metabase.test/id :table-name :field-name), respectively. You will see these calls throughout the tests.

Running queries against the test DB

The definition of MBQL lives in metabase.mbql.schema. To make it easier to run MBQL queries, you can use the metabase.test/mbql-query macro, which will replace symbols like $field-name with the appropriate calls to metabase.test/id to get the ID of the Field in question. You can macroexpand-1 the mbql-query macro to see the exact MBQL query it generates.

(require '[metabase.query-processor :as qp]
         '[metabase.test :as mt])

(qp/process-query
 (mt/mbql-query venues
   {:aggregation [[:count]]
    :breakout    [$price]}))

Running queries with a different driver

The current driver is a keyword bound to metabase.driver/*driver*; macros like metabase.test/mbql-query will look at this and generate queries with the appropriate database. Use driver/with-driver to rebind the dynamic var.

(require '[metabase.query-processor :as qp]
         '[metabase.test :as mt]
         '[metabase.driver :as driver])

(driver/with-driver :postgres
  (qp/process-query
   (mt/mbql-query venues
     {:aggregation [[:count]]
      :breakout    [$price]})))

Changing the current test drivers

By default, tests run only against H2, or whatever the value of the DRIVERS env var is, so they don't take the rest of your life to run. You can change the set of drivers to test against from the REPL:

(require '[metabase.test :as mt])

(mt/set-test-drivers! #{:postgres})

Running queries with a different dataset

The test-data dataset is sufficient for many purposes, but sometimes you need to test against different data. You can easily switch out the dataset used internally by metabase.test/id, metabase.test/db, metabase.test/mbql-query, and the like by using the metabase.test/dataset macro:

(require '[metabase.test :as mt])

(mt/dataset sad-toucan-incidents
  (mt/mbql-query incidents
    {:breakout [$timestamp]}))

metabase.test/dataset will rebind functions used internally by metabase.test/id and metabase.test/db, causing them to create a database and load the data from that dataset for the current driver on-demand the first time one of them is called (whether directly or via something like metabase.test/mbql-query). The second arg to the macro, the dataset to load, can be one of the following:

  • A symbol...

    • ...naming a dataset definition in either the current namespace, or in metabase.test.data.dataset-definitions.

      (mt/dataset sad-toucan-incidents ...)
    • ...qualified by a namespace, naming a dataset definition in that namespace.

      (mt/dataset my-namespace/my-dataset-def ...)
    • ...naming a local binding.

      (let [my-dataset-def (get-dataset-definition)]
        (mt/dataset my-dataset-def ...)
  • An inline dataset definition:

    (mt/dataset (get-dataset-definition) ...)

Most of the datasets you'll see in use in the tests are defined externally in EDN files in the dataset_definitions directory, or defined as a top-level form the same namespace as the test. It's worth going thru the existing definitions to see if there's already something that will work for the test you want -- with some databases, loading a new dataset can be very slow! See metabase.test.data.dataset-definitions for predefined dataset definitions shared throughout the test codebase, or the metabase.test.data.interface namespace for the schema and functions you can use to create a new dataset.

Debugging Stuff

(require '[metabase.test :as mt])

(mt/with-log-level :trace
  (do-something))

Will increase the log level and print out tons of useful messages for the Query Processor and other code. (In Emacs, this shows up in the *nrepl-connection* buffer, rather than the CIDER REPL buffer.)

Running Various DBs Locally

https://github.com/metabase/dev-scripts has scripts for running Docker images with the appropriate env vars to test against different DBs we support such as MariaDB or Spark SQL.

Some differences

Maria and Mysql can have some confusing behavior around boolean columns. They don't have a proper boolean type and just alias bool as tinyint(1) (values 0, 1). This leads to some confusing behavior in union clauses. The following is based around a new boolean field on cards called collection_preview

;; on Server version: 8.0.28 Homebrew
collection=> (db/query {:select [:collection_preview]
                        :from [:report_card]})
({:collection_preview true})
collection=> (db/query {:union [{:select [:collection_preview]
                                 :from [:report_card]}
                                ;; unioning the nils
                                {:select [[nil :collection_preview]]
                                 :from [:report_card]}]})
({:collection_preview true} {:collection_preview nil})

;; vs the maria from dev-scripts/run-mariadb-latest.sh
;; Server version: 5.5.5-10.8.3-MariaDB-1:10.8.3+maria~jammy mariadb.org binary distribution
collection=> (db/query {:select [:collection_preview]
                        :from [:report_card]})
({:collection_preview true})
collection=> (db/query {:union [{:select [:collection_preview]
                                 :from [:report_card]}
                                ;; unioning the nils
                                {:select [[nil :collection_preview]]
                                 :from [:report_card]}]})
({:collection_preview 1M} {:collection_preview nil})

On some versions of Maria, the bool facade is dropped when unioning against nil columns. This happens when we union columns in search results and collection items. Quite bizarre because you might not run into the issue locally.

Example of workaround in metabase.api.search

(letfn [(bit->boolean [v]
          (if (number? v)
            (not (zero? v))
            v))]

 ...

  (comp
   (filter check-permissions-for-model)
   ;; MySQL returns `:bookmark` and `:archived` as `1` or `0` so convert those to boolean as needed
   (map #(update % :bookmark bit->boolean))
   (map #(update % :archived bit->boolean))
   (map (partial scoring/score-and-result (:search-string search-ctx)))
   (filter some?)))
Clone this wiki locally