提交 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.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 将检查默认
test-data数据库是否已为 MySQL 创建、加载了数据并已同步。如果没有,它将调用测试扩展方法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 还有其他几个有用的实用函数。请仔细查看该命名空间以及 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 的文档。