提交新驱动的 PR
如果您想提交 PR 将驱动插件添加到 Metabase 仓库(而不是将其保留在单独的仓库中),您需要
- 能够使用 Docker 在本地运行您的数据库。
- 确保您的驱动通过 Metabase 的核心测试套件。
测试您的驱动
要测试您的驱动,您需要
- 将您的插件移动到 Metabase 仓库的
modules/drivers
目录中。 - 为您的驱动添加测试扩展。
- 编辑
.github/workflows/drivers.yml
,告诉 GitHub Actions 如何为您的数据库设置 Docker 镜像并对其运行测试。
为您的驱动添加测试扩展
测试扩展执行诸如创建新数据库和为给定数据库定义加载数据之类的操作。Metabase 定义了一个庞大的测试套件,会自动针对所有驱动(包括您的新驱动)运行。
要使用您的驱动运行测试套件,您需要为特殊的测试扩展多方法编写一系列方法实现。测试扩展执行诸如创建新数据库并加载数据以用于数据库定义之类的操作。
这些测试扩展将告诉 Metabase 如何创建新数据库并加载测试数据,并提供有关 Metabase 对所创建数据库的预期信息。测试扩展只是仅供测试使用的额外多方法。与核心驱动多方法一样,它们也以驱动名称作为关键字进行调度,例如 :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.sql
和 metabase.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`.
- Metabase 将检查
:mysql
的测试扩展是否已加载。如果未加载,它将(require 'metabase.test.data.mysql)
。 - Metabase 将检查是否已为 MySQL 创建、加载数据并同步了默认的
test-data
数据库。如果未创建,它将调用测试扩展方法tx/load-data!
来创建test-data
数据库并向其中加载数据。加载数据后,Metabase 会同步测试数据库。(这将在下面更详细地讨论。) - Metabase 对 MySQL
test-data
数据库的venues
表执行 MBQL 查询。run-mbql-query
宏是一个用于编写测试的辅助函数,它根据符号前带有$
的名称查找字段 ID。现在不用太担心,只需知道实际执行的查询将类似于{: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
- 结果通过辅助函数
rows
和formatted-venues-rows
处理,这些函数只返回我们关心的查询结果部分 - 这些结果将与预期结果进行比较。
关于 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
在两种不同的上下文中被调用
- 创建数据库时,
- 以及加载数据并同步时。
大多数数据库不允许您以编程方式创建新数据库,这意味着类似 CREATE DATABASE "test-data";
的语句必须在不指定 test-data
作为连接一部分的情况下运行。因此,出现了 context
参数。context
要么是 :server
,表示“给我连接到 DBMS 服务器的详细信息,但不连接到特定数据库”,要么是 :db
,表示“给我连接到特定数据库的详细信息”。在 MySQL 的情况下,当上下文为 :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
还提供了其他几个有用的实用函数。如果您的数据库使用 SQL,请仔细查看该命名空间以及 metabase.test.data.sql
;如果您的数据库使用 JDBC 驱动,请查看 metabase.test.data.sql-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 版本的文档。