Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

Dataformで同名テーブルを作る方法と単体テストの書き方

データ戦略部門のウチウゾウです。今月、地方拠点の開発体制強化施策の一環で、中部支店に異動してきました。今回は、Dataformで異なるデータセット、同名のテーブルを作成する方法について、単体テストの書き方も含めて紹介します。

サマリ

同名のテーブルをDataformで利用するには3箇所対応する必要があります。

  • ref関数に、データセット名, テーブル名の順番で引数を渡します。
  • inputメソッドの引数は、データセット名, テーブル名という順番の配列にします。
  • datasetメソッドでは、schema, nameというプロパティを持ったオブジェクトを引数とします。

背景

Dataformは、BigQuery上で実行できるサーバーレスで、スケーラブルなSQLによるデータ変換パイプラインです。BigQueryのスキャン料金は発生しますが、追加料金なしで利用できます。Dataformの単体テストについてはこちらのブログに詳しく記載されています。

buildersbox.corp-sansan.com

私たちのチームでは、Dataformのクエリに対して、単体テストを書いています。同名テーブルがあったときの対応方法について、実装当時、記事があまりなかったため、今回紹介しようと思いました。

想定する問題

次のようなsqlxファイルが存在していたと仮定します。

dataset1/table1.sqlx

config {
  type: 'table',
  schema: 'dataset1'
}

SELECT
   *
FROM
  ${ref('raw_table1')} 

dataset2/table1_mart.sqlx

config {
  type: 'table',
  schema: 'dataset2',
}

SELECT
  *
FROM
  ${ref('table1')} 

ある日、dataset3にtable1と同じテーブル名でテーブルを作成するように要望を受けました。なお、dataset3は他チームと連携しているデータセットだったため、そのデータセット内の既存のテーブルと命名規則を揃えるために、テーブル名はtable1にする必要がありました。*1

dataset3/table1.sqlx

config {
  type: 'table',
  schema: 'dataset3',
}

SELECT
  *
FROM
  ${ref('raw_table2')}


しかし、上記のコードでは、table1がどちらのデータセットのテーブルを指しているかDataformが解釈できなくなり、エラーが発生します。同名のテーブルをDataformで利用するには3箇所対応する必要があります。

対応1:ref関数の引数にデータセットを指定する

前述のコードで、npx dataform compile を実行すると、次のようなエラーが発生します。

Compilation errors:
  dataset2/table1_mart.sqlx: 
    Error: Ambiguous Action name: table1. 
    Did you mean one of: dataset1.table1, dataset3.table1.
  dataset2/table1_mart.sqlx: 
    Error: Ambiguous Action name: {"name":"table1","includeDependentAssertions":false}.
    Did you mean one of: dataset1.table1, dataset3.table1.

そこで、こちらのDataformのドキュメントをもとに、sqlxファイルのref関数の引数に、データセット名を指定します。*2

cloud.google.com

dataset2/table1_mart.sqlx

config {
  type: 'table',
  schema: 'dataset2',
}

SELECT
  *
FROM
  ${ref('dataset1', 'table1')} 

対応2:inputメソッドの引数にデータセットを指定する

ref関数で、データセットを指定するだけでは、次のようなエラーが発生しました。

Compilation errors:
  dataset2/table1_mart.sqlx: Error: Input for dataset "{"schema":"dataset1","name":"table1"}" has not been provided. 
  Provided inputs: {"name":"table1"}

上記のエラーを見た時、dataset2/table1_mart.sqlxに問題があるように見えますが問題があったのは、テストの方でした。(このエラーの原因を特定するには時間がかかりました。)table1_mart.sqlxのテストは次のようになっていました。

dataset2/table1_mart.test.js

test(
  'dataset1_table1_mart_test_case_1'
)
  .dataset(
    'table1_mart',
  )
  .input(
    'table1', // ここでもデータセット名を指定する必要がある
    /*sql*/`SELECT 1 as col_1`
  )
  .expect(
    /*sql*/`SELECT 1 as col_1`
  );

inputメソッドの引数で指定しているテーブル名と、ref関数で指定しているデータセット名とテーブル名が一致していないことでエラーが発生していました。しかし、inputメソッドでデータセット名を指定する方法がすぐには見つかりませんでした。そのため、Dataformの内部実装を確認し、inputメソッドでは文字列のほかにも配列で受け取れることがわかりました。

core/actions/test.ts

public input(refName: string | string[], contextableQuery: Contextable<ICommonContext, string>) {
  this.contextableInputs.set(resolvableAsTarget(toResolvable(refName)), contextableQuery);
  return this;
}

そこで、次のようにinputメソッドの第一引数を配列に修正すると、期待通り動くことが確認できました。

dataset2/table1_mart.test.js

test(
  'dataset1_table1_mart_test_case_1'
)
  .dataset(
    'table1_mart',
  )
  .input(
    ['dataset1', 'table1']// 配列で指定するとコンパイルに成功する
    /*sql*/`SELECT 1 as col_1`
  )
  .expect(
    /*sql*/`SELECT 1 as col_1`
  );

対応3:datasetメソッドの引数にデータセットを指定する

最後に、同名テーブルがあるdataset3/table1.sqlxのテストの書き方について紹介します。Dataformの実装を見に行くと、datasetメソッドの引数にResolvable型が指定されていました。

core/actions/test.ts

public dataset(ref: Resolvable) {
  this.datasetToTest = ref;
  return this;
}

Resolvable型は、対象のテーブル名の文字列と、テーブル名やデータセット名を指定できるオブジェクトのUNION型でした。
core/common.ts

/**
 * A reference to a dataset within the warehouse.
 */
export interface ITarget {
  database?: string;

  schema?: string;

  name?: string;

  includeDependentAssertions?: boolean;
}

/**
 * A resolvable can be either the name of a dataset as string, or
 * an object that describes the full path to the relation.
 */
export type Resolvable = string | ITarget;

そこで、オブジェクトを使って、データセット名を指定することで、単体テストを実施できました。

dataset3/table1.test.js

test(
  'dataset3_table1_test_case_1'
)
  .dataset({
    schema: 'dataset3',
    name: 'table1',
  }) // オブジェクトで指定するとコンパイルに成功する
  .input(
    ['raw_table2']
    /*sql*/`SELECT 1 as col_1`
  )
  .expect(
    /*sql*/`SELECT 1 as col_1`
  );

最後に

ここまで、Dataformで異なるデータセットに、同名のテーブルを作成する方法について、単体テストの書き方も含めて紹介してきました。私たちのチームでは、Dataformを使ってクライアントが直接利用可能なビジネスデータを構築しています。このビジネスデータはSansanの強みの一つである名寄せの仕組みを支えています。また、Sansanでは東京に限らず、名古屋・大阪・福岡でも、ビジネスデータ基盤の構築を通じてビジネスの成長に貢献できるエンジニアを募集しています。ビジネスインフラの実現に挑戦してみませんか?

media.sansan-engineering.com

Sansan技術本部ではカジュアル面談を実施しています

Sansan技術本部では中途・新卒採用向けにカジュアル面談を実施しています。Sansan技術本部での働き方、仕事の魅力について、現役エンジニアの視点からお話します。「実際に働く人の話を直接聞きたい」「どんな人が働いているのかを事前に知っておきたい」とお考えの方は、ぜひエントリーをご検討ください。

*1:実際には、既存のdataset1のテーブル名を変更するということが可能だったため、既存のテーブル名を変更しています。

*2:2025/02/23時点で、VSCodeの拡張のDataform toolsによる参照ジャンプで、該当テーブルに移動できませんでした。

© Sansan, Inc.