为新驱动程序提交 PR

如果您想提交 PR 以将驱动程序插件添加到 Metabase 仓库(而不是将其保留在单独的仓库中),您需要

  • 能够使用 Docker 在本地运行数据库。
  • 确保您的驱动程序通过 Metabase 的核心测试套件。

测试您的驱动程序

要测试您的驱动程序,您需要

  • 将您的插件移动到 Metabase 仓库中的 modules/drivers 目录。
  • 为您的驱动程序添加测试扩展
  • 编辑 .github/workflows/drivers.yml,以告知 GitHub Actions 如何为您的数据库设置 Docker 镜像并针对其运行测试。

为您的驱动程序添加测试扩展

测试扩展的作用是为给定的数据库定义创建新数据库并加载数据。 Metabase 定义了一个庞大的测试套件,该套件会自动针对所有驱动程序(包括您的新驱动程序)运行。

要使用您的驱动程序运行测试套件,您需要为特殊的测试扩展多方法编写一系列方法实现。测试扩展的作用是为数据库定义创建新数据库并加载数据。

这些测试扩展将告诉 Metabase 如何创建新数据库并使用测试数据加载它们,并提供有关 Metabae 可以从创建的数据库中期望获得的信息。测试扩展只是仅由测试使用的附加多方法。与核心驱动程序多方法一样,它们以驱动程序名称作为关键字进行调度,例如 :mysql

文件组织

驱动程序的测试扩展通常位于名为 metabase.test.data.<driver> 的命名空间中。如果您的驱动程序用于 SQLite,则您的文件应如下所示

metabase/modules/drivers/sqlite/deps.edn                           ; <- deps go in here
metabase/modules/drivers/sqlite/resources/metabase-plugin.yaml     ; <- plugin manifest
metabase/modules/drivers/sqilte/src/metabase/driver/sqlite.clj     ; <- main driver namespace

因此,您将创建一个新目录和文件来存放您的文本扩展方法实现。

metabase/modules/drivers/sqlite/test/metabase/test/data/sqlite.clj   ; <- test extensions

测试扩展方法在哪里定义?

Metabase 测试扩展位于 metabase.test.data.interface 命名空间中。与核心驱动程序方法一样,:sql:jdbc-sql 实现了某些测试扩展本身,但定义了您必须实现的其他方法才能使用它们;请参阅 metabase.test.data.sqlmetabase.test.data.sql-jdbc 命名空间。

您需要按如下方式要求以下命名空间,别名:

(require '[metabase.test.data.interface :as tx]) ; tx = test extensions
(require '[metabase.test.data.sql :as sql.tx])   ; sql test extensions
(require '[metabase.test.data.sql-jdbc :as sql-jdbc.tx])

注册测试扩展

与驱动程序本身一样,您需要注册您的驱动程序具有测试扩展的事实,以便 Metabase 知道它不需要尝试第二次加载它们。(如果尚未加载,Metabase 将在需要时通过查找名为 metabase.test.data.<driver> 的命名空间来加载它们,这就是为什么您需要遵循该命名模式的原因。):sql:sql-jdbc 驱动程序有自己的一组测试扩展,因此根据您用于驱动程序的父级,使用以下方式注册测试扩展

# Non-SQL drivers
(tx/add-test-extensions! :mongo)

# non-JDBC SQL
(sql/add-test-extensions! :bigquery)

# JDBC SQL
(sql-jdbc.tx/add-test-extensions! :mysql)

您只需要一个调用 - 对于 :sql-jdbc 驱动程序,无需执行所有三个调用。此调用应放在测试扩展命名空间的开头,如下所示

(ns metabase.test.data.mysql
  (:require [metabase.test.data.sql-jdbc :as sql-jdbc.tx]))

(sql-jdbc.tx/register-test-extensions! :mysql)

Metabase 测试的剖析

让我们看一个真实的 Metabase 测试,以便我们可以了解它的工作原理以及我们需要做些什么来为其提供支持

;; expect-with-non-timeseries-dbs = run against all drivers listed in `DRIVERS` env var except timeseries ones like Druid
(expect-with-non-timeseries-dbs
  ;; expected results
  [[ 5 "Brite Spot Family Restaurant" 20 34.0778 -118.261 2]
   [ 7 "Don Day Korean Restaurant"    44 34.0689 -118.305 2]
   [17 "Ruen Pair Thai Restaurant"    71 34.1021 -118.306 2]
   [45 "Tu Lan Restaurant"             4 37.7821 -122.41  1]
   [55 "Dal Rae Restaurant"           67 33.983  -118.096 4]]
  ;; actual results
  (-> (data/run-mbql-query venues
        {:filter   [:ends-with $name "Restaurant"]
         :order-by [[:asc $id]]})
      rows formatted-venues-rows))

假设我们使用以下命令启动测试:

DRIVERS=mysql clojure -X:dev:drivers:drivers-dev:test`.
  1. Metabase 将检查并查看是否已加载 :mysql 的测试扩展。如果未加载,它将 (require 'metabase.test.data.mysql)
  2. Metabase 将检查以查看是否已为 MySQL 创建默认的 test-data 数据库,加载了数据并进行了同步。如果未同步,它将调用测试扩展方法 tx/load-data! 以创建 test-data 数据库并将数据加载到其中。加载数据后,Metabase 会同步测试数据库。(这将在下面更详细地讨论。)
  3. Metabase 针对 MySQL test-data 数据库的 venues 表运行 MBQL 查询。run-mbql-query 宏是一个用于编写测试的助手,它根据名称查找字段 ID,用于在 from 前面带有 $ 的符号。现在不用太担心;只需知道实际运行的查询看起来像
    {:database 100 ; ID of MySQL test-data database
     :type :query
     :query {:source-table 20 ; Table 20 = MySQL test-data.venues
             :filter [:ends-with [:field-id 555] "Restaurant"] ; Field 555 = MySQL test-data.venues.name
             :order-by [[:asc [:field-id 556]]]}} ; Field 556 = MySQL test-data.venues.id
    
  4. 结果通过辅助函数 rowsformatted-venues-rows 运行,这些函数仅返回我们关心的查询结果部分
  5. 将这些结果与预期结果进行比较。

这就是您需要了解的关于 Metabase 测试内部工作原理的全部内容;现在我们已经介绍了这些内容,让我们看一下如何授权 Metabase 执行其需要执行的操作。

加载数据

为了确保不同驱动程序之间行为一致,Metabase 测试套件会创建新数据库并从一组共享的数据库定义中加载数据。这意味着,无论我们是针对 MySQL、Postgres、SQL Server 还是 MongoDB 运行测试,单个测试都可以检查我们是否为每个驱动程序获得完全相同的结果!

这些数据库定义大多位于 EDN 文件中;大多数测试针对名为“test data”的测试数据库运行,其定义可以在此处找到。看一下该文件 - 它只是一组简单的表名、列名和类型,然后是数千行要加载到这些表中的数据。

与测试扩展方法定义类似,DatabaseDefinition 的模式位于 metabase.test.data.interface 中 - 您可以查看并确切了解数据库定义应该是什么样的。

作为测试定义编写者的最大工作是编写所需的方法,以获取数据库定义,创建具有适当表和列的新数据库,并将数据加载到其中。 对于非 SQL 驱动程序,您需要实现 tx/load-data!:sql:sql-jdbc 具有子驱动程序使用的共享实现,但定义了自己的一组测试扩展方法。例如,:sql(和 :sql-jdbc)将处理用于创建表的 DDL 语句,但需要知道主键应使用的类型,因此您需要实现 sql.tx/pk-sql-type

(defmethod sql.tx/pk-sql-type :mysql [_] "INTEGER NOT NULL AUTO_INCREMENT")

我想在这里详细记录每个测试扩展方法,但在我找到时间这样做之前,这些方法都记录在代码库本身中;查看相应的测试扩展命名空间,查看您需要实现哪些方法。您还可以参考为其他类似驱动程序编写的测试扩展,以了解您需要做什么。

连接详情

当然,Metabase 还需要知道如何连接到您新创建的数据库。具体来说,它需要知道在将新创建的数据库保存为 Database 对象时,应将其保存为连接 :details 映射的一部分。具有测试扩展的所有驱动程序都需要实现 tx/dbdef->connection-details,以便为给定的数据库定义返回一组适当的 :details。例如

(defmethod tx/dbdef->connection-details :mysql [_ context {:keys [database-name]}]
  (merge
   {:host     (tx/db-test-env-var :mysql :host "localhost")
    :port     (tx/db-test-env-var :mysql :port 3306)
    :user     (tx/db-test-env-var :mysql :user "root")
    ;; :timezone :America/Los_Angeles
    :serverTimezone "UTC"}
   (when-let [password (tx/db-test-env-var :mysql :password)]
     {:password password})
   (when (= context :db)
     {:db database-name})))

让我们看一下这里发生了什么。

连接上下文

tx/dbdef->connection-details 在两种不同的上下文中调用

  • 创建数据库时,
  • 以及将数据加载到数据库并同步时。

大多数数据库不允许您连接到尚未创建的数据库,这意味着必须在指定 test-data 作为连接一部分的情况下运行类似 CREATE DATABASE "test-data"; 的语句。因此,需要 context 参数。context 可以是 :server,意思是“给我连接到 DBMS 服务器的详细信息,但不是连接到特定数据库”,也可以是 :db,意思是“给我连接到特定数据库的详细信息”。在 MySQL 的情况下,当 context 为 :db 时,它会添加 :db 连接属性。

从环境变量获取连接属性

您几乎肯定会在本地 Docker 容器中运行数据库。我们希望具有灵活性,而不是硬编码 Docker 容器的连接详细信息(用户名、主机、端口...),而是允许人们在环境变量中指定这些信息,以防他们针对不同的容器运行,或者只是在容器外部运行数据库,或者完全在另一台计算机上运行数据库。您可以使用 tx/db-test-env-var 从环境变量中获取详细信息。例如,

(tx/db-test-env-var :mysql :user "root")

告诉 Metabase 查找环境变量 MB_MYSQL_TEST_USER;如果未找到,则默认为 "root"。环境变量的名称遵循 MB_<driver>_TEST_<property> 模式,分别作为第一个和第二个参数传递给函数。您不需要为 tx/db-test-env-var 指定默认值;也许 user 是一个可选参数;如果未指定 MB_MYSQL_TEST_USER,则您无需在连接详细信息中指定它。

但是,如果您想要要求某些属性,但这些属性没有合理的默认值怎么办?在这些情况下,您可以使用 tx/db-test-env-var-or-throw。如果未设置相应的环境变量,这些将抛出异常,最终导致测试失败。

;; If MB_SQLSERVER_TEST_USER is unset, the test suite will quit with a message saying something like
;; "MB_SQLSERVER_TEST_USER is required to run tests against :sqlserver"
(tx/db-test-env-var-or-throw :sqlserver :user)

请注意,对于您未对其运行测试的驱动程序(即,未在 DRIVERS 环境变量中列出的驱动程序),根本不会调用 tx/dbdef->connection-details,因此例如,当针对 Mongo 运行测试时,您不会看到 SQL Server 错误消息。

除了 tx/db-test-env-var 之外,metabase.test.data.interface 还有几个其他有用的实用函数。仔细查看该命名空间以及 metabase.test.data.sql (如果您的数据库使用 SQL)和 metabase.test.data.sql-jdbc (如果您的数据库使用 JDBC 驱动程序)。

其他测试扩展

在比较测试结果时,Metabase 还需要了解其他一些事项。例如,不同的数据库以不同的方式命名表和列;存在一些方法可以让 Metabase 知道,对于将所有内容都大写的数据库,它应该期望 test-data 数据库定义中的类似 venues 表的结果为 VENUES。(我们认为命名上的这种细微差异仍然意味着相同的事物。)查看 tx/format-name 和其他类似的方法,看看您需要实现哪些方法。

对于不允许您以编程方式创建新数据库的 DBMS 怎么办?

这实际上是一个常见问题,幸运的是我们已经找到了解决方法。解决方案通常是使用不同的模式代替不同的数据库,或者在表名称前加上数据库名称,并在同一个数据库中创建所有内容。对于基于 SQL 的数据库,您可以实现 sql.tx/qualified-name-components,让测试使用不同的标识符,而不是它们通常会使用的标识符,例如 "shared_db"."test-data_venues".id 而不是 "test-data".venues.id。SQL Server 和 Oracle 测试扩展是这种黑魔法在实际应用中的很好的例子。

设置 CI

一旦所有测试都通过,您将需要设置 GitHub Actions 以针对您的驱动程序运行这些测试。您需要在 .github/workflows/drivers.yml 中添加一个新作业,以针对您的数据库运行测试。

这是一个 PostgreSQL 的示例配置。

be-tests-postgres-latest-ee:
  needs: files-changed
  if: github.event.pull_request.draft == false && needs.files-changed.outputs.backend_all == 'true'
  runs-on: ubuntu-22.04
  timeout-minutes: 60
  env:
    CI: "true"
    DRIVERS: postgres
    MB_DB_TYPE: postgres
    MB_DB_PORT: 5432
    MB_DB_HOST: localhost
    MB_DB_DBNAME: circle_test
    MB_DB_USER: circle_test
    MB_POSTGRESQL_TEST_USER: circle_test
    MB_POSTGRES_SSL_TEST_SSL: true
    MB_POSTGRES_SSL_TEST_SSL_MODE: verify-full
    MB_POSTGRES_SSL_TEST_SSL_ROOT_CERT_PATH: "test-resources/certificates/us-east-2-bundle.pem"
  services:
    postgres:
      image: circleci/postgres:latest
      ports:
        - "5432:5432"
      env:
        POSTGRES_USER: circle_test
        POSTGRES_DB: circle_test
        POSTGRES_HOST_AUTH_METHOD: trust
  steps:
    - uses: actions/checkout@v4
    - name: Test Postgres driver (latest)
      uses: ./.github/actions/test-driver
      with:
        junit-name: "be-tests-postgres-latest-ee"

有关您在此处所做的事情以及所有这些工作原理的更多信息,请参阅 GitHub Actions 的工作流程语法

阅读其他版本的 Metabase 的文档。