👌

dataformのassertions周りのアップデート

2024/06/21に公開

dataform-coreのアップデート

まだβ版だが、dataformでは2.Xから3.0へのメジャーバージョンアップが進んでいる。

https://github.com/dataform-co/dataform/releases

設定ファイルがjsonからyamlに変更されるなど色々な変更があるようだが、その中でもassertion関連のリリースが目を引いたので、周辺パッケージのアップデートも含めてこの記事で紹介したい。

3.0では、dependOnDependencyAssertionsincludeDependentAssertionsという2つのオプションが追加されている。

このオプションが追加された背景を説明すると、dataformではdbtのbuildコマンドとは異なり、デフォルトではテストが失敗しても、後続処理のモデルはそのまま実行されていた。即ち、初期設定ではモデルの作成処理のみが依存関係になっている。

テストが失敗した場合に後続モデルが作成されないようにするには、都度テストごとにdependenciesに依存関係を追加する必要があった。例えば、first_viewがtest1~5のassertionを持つ場合は以下のようになる。

config {
    type: "view",
    dependencies: ["test1", "test2", "test3", "test4", "test5"]
}

SELECT
  test
FROM
  ${ref("first_view")}

重要なデータマートでは、テストが5件以上あることも珍しくない。このような場合、dependenciesにテストを都度追加するのはかなり面倒である。さらに、テストを追加したときにdependencies側を修正し忘れるリスクも高まる。

そこで登場したのがdependOnDependencyAssertionsオプションである。configブロック内で有効にすると、refで参照するモデルとdependenciesで定義するモデルすべてのassertionに対して依存関係が作られ、すべてのassertionが成功した場合にのみ実行されるようになった。

https://cloud.google.com/dataform/docs/assertions#all-dependency-assertions

config {
    type: "view",
    dependOnDependencyAssertions: true,
}

SELECT
  test
FROM
  ${ref("first_view")}

コードがシンプルになったのが一目でわかる。もう1つのincludeDependentAssertionsについては、以下のようにrefとdependencies両方に設定できるが、nameで指定したモデルのassertionsを対象に、依存関係として定義できる。こちらは特定のモデルに対してだけassertionの依存関係を設定したいときに使うようだ。

config { 
    type: "view",
    dependencies: [{name: "some_table", includeDependentAssertions: true}]
}

select test from ${ref({name: "some_other_table", includeDependentAssertions: true})}

この場合は、some_tablesome_other_tableのassertionがすべて成功した場合にのみこのモデルは実行されるようになる。

不満があるとすれば、dependency/dependentといった似た単語がdataformの用語としてテスト周りだけでなく全体に多用されているところだろうか。未だに混乱することがあるので、できればupstream/ancestorsなど直感的なワードを採用して欲しいところである[1]

dataform-assertionsの機能追加

dataformでリッチなテストを簡単に実装できるdataform-assertionsパッケージについては、前回の記事で触れたが、こちらのパッケージにもいくつか機能が追加された、というか一部は自分で実装してPRを送ってみたところマージしてもらえた。

source freshnessの範囲が拡張

  • 比較対象のカラムにTIEMSTAMP型が対応し、SECOND, MINUTE, HOURのようなより細かい単位での比較が可能に
  • date型の場合はtime zoneを指定できるように変更

元々date型がベースになっていることもあって、dbtのsource freshnessよりもリッチな仕様になっている。

dataFreshnessConditions: {
    "first_table": {
      "dateColumn": "updated_date",
      "timeUnit": "DAY",
      "delayCondition": 1,
      "timeZone": "America/Los_Angeles"
    },
    "second_table": {
      "dateColumn": "TIMESTAMP(updated_date)",
      "timeUnit": "HOUR",
      "delayCondition": 3,
    }
  },

configブロックでテストする範囲を指定できるように

この機能は、テスト対象がTB級のサイズで一部のデータしか更新されないテーブルであったとき、テスト対象を絞り込みスキャン量を減らしたい場合に便利である。変化していないデータで過去にテストが成功しているなら、再度テストを行う意味はないからだ。

dbtではcoreに実装されており、configブロックを使ってテスト範囲をテストごとに指定できる。


version: 2

models:
  - name: large_table
    columns:
      - name: my_column
        tests:
          - accepted_values:
              values: ["a", "b", "c"]
              config:
                where: "created_at > CURRENT_DATE() - 7"

https://docs.getdbt.com/reference/resource-configs/where

一方、dataformにはこれまで同等の機能はなく、カスタムテストを書くか、テスト用のモデルを個別に作るしかない状態だった。そのため、dbtのコンセプトをそのままこのパッケージにも実装した。

const commonAssertions = require("../index");

const commonAssertionsResult = commonAssertions({
  globalAssertionsParams: {
    "database": {project},
    "schema": "assertions_" + dataform.projectConfig.vars.env,
    "location": "EU",
    "tags": ["assertions"],
  },
  // この階層にテーブル単位で指定する
  config: {
    "first_table": {
      "where": "updated_date >= CURRENT_DATE() - 7"
    },
  },
  rowConditions: {
    "first_table": {
      "id_not_null": "id IS NOT NULL",
      "id_strict_positive": "id > 0"
    },
    "second_table": {
      "id_in_accepted_values": "id IN (1, 2, 3)"
    }
  },
  ...

id_not_nullのテストの場合、以下のようなクエリになる。事前にテーブルをフィルタリングしてから本来の条件式が実行される。

WITH
  filtering AS (
      SELECT
          *
      FROM
          `{project}.dataform.first_table`
      WHERE
          updated_date >= CURRENT_DATE() - 7
  )
  SELECT "Condition not met: id IS NOT NULL, Table: `{project}.dataform.first_table`" AS assertion_description
      FROM filtering
      WHERE NOT (id IS NOT NULL)

dataform-assertionsもまだ小さいパッケージなので、こういった機能追加もかなり短時間で実装できた。機能追加できる余地はまだまだあるので、dataformを利用されている方、OSSに関わってみたい方は是非contributeしてみて欲しい。

脚注
  1. 主体からの矢印の向きで考えると覚えやすい。A→B→Cというデータパイプラインがあったとき、Bから見ると、dependencyは「主体が依存している」という意味なので、A←Bの方向。dependentは「主体が依存されている」という意味なので、B←Cの方向である。ちなみに日本語版のドキュメントでは「依存」と「依存関係」という直訳で翻訳が放棄されている。 ↩︎

Discussion