HiveRunner を試す

# モチベーション

Hive のクエリのテストしたい。

mysql だったら、docker-compose とか比較的簡単にテストできるけど、hiveのクエリのテストも同じようにしたいけど、どうしたらいいですかね。(docker で ゴニョゴニョするのはちょっと辛そう..)

ってわけで良さそうなプラグインを適当に選んで試してみました。

選んだのは、[公式でいくつか紹介されているツール](https://cwiki.apache.org/confluence/display/Hive/Unit+Testing+Hive+SQL)の中で git のスターが一番多かった [HiveRunner](https://github.com/klarna/HiveRunner) です。(バージョンは `4.1.0`)


# 準備

`HiveRunner` は基本的に `Junit` のテストとして、クエリのテストをします。

外部の依存関係は必要なく、`JVM`上に`HiveServer`を立てて、`Junit` がその`HiveServer`に`hive sql` を実行するイメージみたいです。

今回は `maven` でプロジェクトを作っていきます。
以下、今回実行する際の `pom` です。 特に理由はありませんが、`BEELINE` のエミュレータを指定しています。

```xml
<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.21.0</version>
            <configuration>
                <forkMode>always</forkMode>
                <systemProperties>
                    <!-- Defaults to HIVE_CLI, other options include BEELINE and HIVE_CLI_PRE_V200 -->
                    <commandShellEmulator>BEELINE</commandShellEmulator>
                </systemProperties>
            </configuration>
        </plugin>
    </plugins>
</build>

<dependencies>
    <dependency>
        <groupId>com.klarna</groupId>
        <artifactId>hiverunner</artifactId>
        <version>4.1.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>
```

# とりあえず実行してみる


```java

@RunWith(StandaloneHiveRunner.class)
public class TestHiveRunner {
    @HiveSQL(files = {})
    private HiveShell shell;

    /*
     * DB と テーブルを作成
     */
    @Before
    public void setupSourceDatabase() {
        shell.execute(
                "CREATE DATABASE source_db; " +
                "CREATE TABLE source_db.test_table (year STRING, value INT);");

        shell.execute(
                "CREATE DATABASE my_schema; " +
                "CREATE EXTERNAL TABLE my_schema.result " +
                "(year STRING, value INT) " +
                "STORED AS PARQUET " +
                "TBLPROPERTIES (\"parquet.compress\"=\"snappy\")");
    }

    @Test
    public void testMaxValueByYear() {
        /*
         * 集計対象のテーブルにテストデータを格納
         */
        shell.insertInto("source_db", "test_table")
                .withColumns("year", "value")
                .addRow("2014", 3)
                .addRow("2014", 4)
                .addRow("2015", 2)
                .addRow("2015", 5)
                .commit();

        /*
         * 集計クエリを実行 (INSERT クエリ)
         */
        shell.executeStatement("INSERT INTO my_schema.result " +
                "SELECT " +
                "   year, " +
                "   MAX(value) " +
                "FROM " +
                "   source_db.test_table " +
                "GROUP BY year");

        /*
         * 集計結果が INSERT されているテーブルから結果を取得
         */
        List<Object[]> result = shell.executeStatement("SELECT * FROM my_schema.result");

        assertEquals(2, result.size());
        assertArrayEquals(new Object[]{"2014",4}, result.get(0));
        assertArrayEquals(new Object[]{"2015",5}, result.get(1));
    }
}
```

上記ほとんど Example に載っているコードをちょっと書き換えたものです。

とても簡単にテストすることができるように見えます。

切り出して置いたクエリの読み込みや、テストデータのインサートが多くなってきて別途`tsv`等に切り出した場合など、それらを読み込むこともできます。

# それぞれちょっとだけ見ていく

### @HiveSQL

```java

// デフォルトで src/test/resources ディレクトリを参照している。
@HiveSQL(files = {"create_test_table.sql", "create_max.sql"})
private HiveShell shell;
```

`files = {}` にSQLファイルを指定することで、インスタンスが作られた後に自動で実行してくれる。

`@HiveSQL(files = {...}, autoStart = false)` とすることで、**任意のセットアップ**を行なった後、起動させることができる。(start()メソッドをコールする)

ちなみに、**任意のセットアップ**で設定出来る項目は以下のようなものがあります。(オーバーロードされているメソッド多々あり.)

```java

// HiveConf を設定
void setProperty(String key, String value);
void setHiveConfValue(String key, String value);

// テストデータを HDFS にコピー
void addResource(String targetFile, File sourceFile);

// HiveShell起動時に実行されるスクリプトの登録
// @HiveSetupScript でも同様だが、以下はスクリプトの実行順序が保証される。
void addSetupScript(String script);
void addSetupScripts(Charset charset, File... scripts);

// stream を開いて、テストデータを HDFS に書き込んでくれるらしい
OutputStream getResourceOutputStream(String targetFile);
```


### execute

```java
// 直書き
shell.execute("CREATE DATABASE source_db; " +
        "CREATE TABLE source_db.test_table (year STRING, value INT);");


// 切り出されたSQLの読み込みも可
shell.execute(Paths.get("src/test/resources/calculate_max.sql"));
```

返り値なしの、スクリプト(クエリ)実行。

`;` で区切ることで、複数のクエリを実行できる。

複数のクエリを実行できるから、返り値がないってことですかね。

### executeQuery & executeStatement

```java

// 下記、executeQuery でも同様に実行できる。
shell.executeStatement("INSERT INTO my_schema.result " +
        "SELECT " +
        "   year, " +
        "   MAX(value) " +
        "FROM " +
        "   source_db.test_table " +
        "GROUP BY year");

// executeQuery に限り、切り出されたSQLの読み込みも可
shell.execute(Paths.get("src/test/resources/calculate_max.sql"));
```

`execute` と異なり、複数のクエリを一回で実行することはできないし、文末に　`;` が含まれるとエラーになる。

その代わり、`List<String>` でクエリの実行結果が返ってくる。

### insertInto

```java

shell.insertInto("source_db", "test_table")
        .withColumns("year", "value")
        .addRow("2014", 3)
        .addRow("2014", 4)
        .addRow("2015", 2)
        .addRow("2015", 5)
        .commit();

// tsv 等からデータをインサートすることもできる。
shell.insertInto("source_db", "test_table")
        .withColumns("year", "value")
        .addRowsFromTsv(new File("src/test/resources/insert_data_of_test_table.tsv"))
        .commit();
```

このメソッドは、`commit()` しないと実行されない。

テストデータの準備はめんどくさいですが、`tsv`でインサートしてくれるのはとても便利ですね。


# まとめ

クエリのテストやちょっとクエリ試したいときなど、(ちょっと重い感じがしますが)使えそうですね。

一方で、カラム数が多いテーブルだったり、テストデータの量が多い場合のテストだったり、めっちゃ複雑なクエリのテスト等では、メモリや実行速度的に辛そうですが、どうなるのでしょうか ([READMEに対処法など書いてありますが..](https://github.com/klarna/HiveRunner#cook-book))

とはいえ、`pom` に `hiverunner` を入れるだけで、`hive` の `sql` のテストができるのは魅力的ですね。