dbt-athena で Athena Iceberg テーブルに対して incremental + merge を使い、スキーマ変更を伴う運用をしていると、複合型(struct / array<struct>)のカラムで mismatched input '<' や TYPE_MISMATCH のエラーが発生することがあります。
この記事では、このエラーの根本原因を adapter のソースコードレベルで特定し、最小構成で再現する方法を紹介します。2つの異なるバグパターンを整理し、どの条件で発生するかを明確にします。
実際に DynamoDB からの取り込みパイプラインで本番障害としてこのエラーに遭遇し、dbt-athena-community の 1.9.4 と 1.10.0 の両方で再現検証を行いました。
dbt-athena で Athena Iceberg テーブルに対して incremental + merge を使い、スキーマ変更を伴う運用をしていると、複合型(struct / array<struct>)のカラムで mismatched input '<' や TYPE_MISMATCH のエラーが発生することがあります。
この記事では、このエラーの根本原因を adapter のソースコードレベルで特定し、最小構成で再現する方法を紹介します。2つの異なるバグパターンを整理し、どの条件で発生するかを明確にします。
実際に DynamoDB からの取り込みパイプラインで本番障害としてこのエラーに遭遇し、dbt-athena-community の 1.9.4 と 1.10.0 の両方で再現検証を行いました。
TL;DR
- dbt-athena の schema evolution(
on_schema_change='sync_all_columns'や'append_new_columns')で、struct / array<struct> を含むカラムに2つのバグがあります -
バグA: struct 内に
timestamp型フィールドがあると、ALTER TABLE で struct 全体がtimestampに誤変換される → MERGE 時にTYPE_MISMATCH -
バグB: 既存カラムの型が変わると(フィールド追加など)、UPDATE の CAST に Hive 形式
struct<...>が混入する →mismatched input '<' - dbt-athena-community 1.9.4 / 1.10.0 の両方で再現し、執筆時点(2026年4月)では未修正です
1. 何が起きたか
DynamoDB のデータを Glue Crawler 経由で Athena Iceberg テーブルに取り込み、dbt で incremental + merge の運用をしていました。DynamoDB の JSON は marshalized 形式で、各カラムが struct<S:string> や struct<M:struct<...>> のようなネストした複合型になります。
ある日、DynamoDB 側のデータにフィールドが追加され、Glue Crawler がスキーマを更新しました。dbt の on_schema_change='sync_all_columns' が schema evolution を実行しようとしたところ、以下のエラーが発生しました。
An error occurred (InvalidRequestException) when calling the StartQueryExecution operation:
line 2:112: mismatched input '<'. Expecting: '(', ')', 'ARRAY'
dbt --debug で生成された SQL を確認すると、UPDATE 文に以下のような CAST が含まれていました。
update ... set user_profile__dbt_alter = cast(user_profile as struct<m:struct<address:struct<...>>>);
Athena の DML(UPDATE / MERGE)は Trino エンジンで実行されるため、型名には row(...) 形式を使う必要があります。しかし、ここでは Hive 形式の struct<...> が使われてしまっていました。
2. なぜ起きるか — Hive 形式と Trino 形式の混在
Athena には DDL と DML で異なる型構文が使われるという仕様があります。
| 文脈 | エンジン | 複合型の書き方 |
|---|---|---|
| DDL(CREATE TABLE, ALTER TABLE) | Hive 互換 | struct<name:string, value:int> |
| DML(SELECT, UPDATE, MERGE) | Trino | row(name varchar, value integer) |
dbt-athena は、テーブルのスキーマ情報を boto3 の glue_client.get_table() で取得します。この API は Hive 形式で型を返します。
{
"Name": "complex_col",
"Type": "struct<event_time:timestamp,detail:string>"
}
一方、information_schema.columns から取得すると Trino 形式になります。
row(event_time timestamp(6), detail varchar)
根本原因: adapter のソースコード
バグの起点は impl.py の get_columns_in_relation です。Glue API から取得した Hive 形式の型文字列を、そのまま AthenaColumn.dtype に格納しています。
# impl.py L1383-1388
return [
AthenaColumn(column=c["Name"], dtype=c["Type"], table_type=table_type)
for c in columns + partition_keys
]
この dtype(Hive 形式)は DDL と DML の両方で使われますが、DML 文脈では Trino 形式への変換が必要です。adapter には ddl_data_type と dml_data_type という2つの変換マクロが存在するものの、schema evolution のコードパスではこの変換が正しく行われていません。
3. 最小再現プロジェクトの設計
本番のデータは複雑な DynamoDB marshalized JSON で、カラム数も多いため、最小構成での再現プロジェクトを作成しました。
再現に必要な条件は以下の通りです。
-
materialized='incremental'+incremental_strategy='merge' table_type='iceberg'-
on_schema_change='sync_all_columns'(または'append_new_columns') - Phase 1 でシンプルなテーブルを作成し、Phase 2 でスキーマを変更して再実行
比較軸として、以下のバリエーションを用意しました。
| TC | 列の変更内容 | 意図 |
|---|---|---|
| TC-01 | string 列追加 | ベースライン(成功するはず) |
| TC-02 | struct<timestamp, string> 追加 | バグA 再現 |
| TC-02b | struct<string, int> 追加 | timestamp 無しで比較 |
| TC-02e | array<struct> のフィールド増加 | バグB 再現(DynamoDB パターン) |
| TC-02f | struct 内の型変更 int→struct | バグB 再現(型昇格パターン) |
| TC-05 | struct 追加、ignore モード | schema evolution を走らせない比較 |
4. 再現結果 — 2つのバグパターン
dbt-athena-community 1.9.4 と 1.10.0 の両方で、全く同じ結果が得られました。
バグA: ALTER TABLE での struct → timestamp 誤変換
発生条件: struct 内に timestamp 型のフィールドが含まれる新規カラムの追加
原因は ddl_data_type マクロの 19-22行目 です。
-- ddl_data_type マクロ(抜粋)
{%- if table_type == 'iceberg' -%}
{%- if 'timestamp' in data_type -%}
{% set data_type = 'timestamp' -%}
{%- endif -%}
{%- endif -%}
Iceberg テーブルの場合、'timestamp' in data_type という部分一致で型を timestamp に置き換えています。struct<event_time:timestamp,detail:string> のように文字列中に timestamp が含まれていると、struct 全体が timestamp に化けてしまいます。
debug ログで確認すると、ALTER TABLE が以下のように生成されていました。
-- Glue から取得した型: struct<event_time:timestamp,detail:string>
-- 生成された ALTER TABLE:
alter table `repro_dbt_athena_struct`.`tc02_top_level_struct_add`
add columns (complex_col timestamp)
その結果、MERGE 文で src 側の row(event_time timestamp(6), detail varchar) と target 側の timestamp(6) が不一致となり TYPE_MISMATCH が発生します。
TYPE_MISMATCH: line 7:7: MERGE table column types don't match for MERGE case 0,
SET expressions: Table: [varchar, timestamp(6)],
Expressions: [varchar, row(event_time timestamp(6), detail varchar)]
興味深いことに、struct<name:string,value:int> のように timestamp を含まない struct ではこの問題は起きません(TC-02b で確認済み)。
バグB: DML の CAST に Hive 形式 struct<...> が混入
発生条件: 既存カラムの複合型が変更される場合(フィールド追加、型変更)
原因は athena__alter_column_type マクロの 76-78行目 です。
-- on_schema_change.sql(抜粋)
{%- set update_query -%}
update {{ relation.render_pure() }}
set {{ tmp_column }} = cast({{ column_name }} as {{ new_column_type }});
{%- endset -%}
DDL 用の ALTER TABLE(72行目)では ddl_data_type() で変換した new_ddl_data_type が使われますが、DML 用の UPDATE(77行目)では new_column_type(Glue 由来の Hive 形式)がそのまま CAST に埋め込まれています。dml_data_type() を経由していません。
-- ALTER TABLE (DDL) — Hive 形式で正しい
alter table ... add columns(items__dbt_alter array<struct<k:string,v:string,priority:int>>);
-- UPDATE (DML) — Hive 形式が混入してエラー
update ... set items__dbt_alter = cast(items as array(struct<k:string,v:string,priority:int>));
DDL の ALTER TABLE は Hive 形式の struct<...> で正しく動作しますが、DML の UPDATE CAST にもそのまま struct<...> が使われてしまい、Athena が mismatched input '<' を返します。
結果一覧
| TC | v1.9.4 | v1.10.0 | バグパターン |
|---|---|---|---|
| TC-01 (string追加) | SUCCESS | SUCCESS | — |
| TC-02 (struct<ts,string>) | FAIL | FAIL | A: TYPE_MISMATCH |
| TC-02b (struct<string,int>) | SUCCESS | SUCCESS | — |
| TC-02e (array<struct>フィールド増) | FAIL | FAIL | B: mismatched input '<' |
| TC-02f (int→struct型変更) | FAIL | FAIL | B: mismatched input '<' |
| TC-05 (ignore) | SUCCESS | SUCCESS | — |
5. 現時点での回避策と今後
執筆時点(2026年4月)では、dbt-athena-community の 1.9.4 / 1.10.0 の両方でこのバグが存在します。
dbt-adapters リポジトリの Issue #1433 で報告されており、triage:product ラベルが付いて Product チームのキューにありますが、修正 PR やコメントはまだありません。
回避策として考えられるもの:
-
on_schema_change='ignore'を使い、schema evolution を dbt に任せない - スキーマ変更が必要な場合は、手動で ALTER TABLE を実行してから dbt run する
-
athena__alter_column_typeマクロをプロジェクト内でオーバーライドし、UPDATE の CAST でdml_data_type()を経由させる
修正に必要な変更:
adapter 側で必要な修正は大きく2点です。
-
バグA:
ddl_data_typeマクロ の'timestamp' in data_typeを、struct 内の timestamp にマッチしないよう修正する(例: 正規表現でトップレベルの型のみを対象にする) -
バグB:
athena__alter_column_typeマクロ の UPDATE CAST でnew_column_typeを直接使わず、dml_data_type()で Trino 形式に変換する。ただし、現在のdml_data_type()はstruct<...>→row(...)の変換を行わないため、この変換ロジック自体の追加も必要です
環境情報
| 項目 | 値 |
|---|---|
| dbt-athena-community | 1.9.4 / 1.10.0 |
| dbt-core | 1.11.7 |
| Python | 3.11.6 |
| AWS リージョン | ap-northeast-1 |
| Athena Engine Version | 3 |
| テーブルフォーマット | Iceberg |
関連リンク
- dbt-adapters Issue #1433 — Bug report for this issue
-
dbt-athena
impl.py—get_columns_in_relation— Glue API から Hive 形式の型を取得する箇所 -
dbt-athena
ddl_dml_data_type.sql— DDL/DML 型変換マクロ(バグA の原因箇所を含む) -
dbt-athena
on_schema_change.sql— schema evolution 処理(バグB の原因箇所を含む) - Athena データ型ドキュメント — DDL/DML の型構文差異
Appendix: 再現用 dbt モデル SQL
以下は最小再現に使用した dbt モデルの SQL です。すべて materialized='incremental' + incremental_strategy='merge' + table_type='iceberg' + on_schema_change='sync_all_columns' で設定しています。
phase 変数で Phase 1(初期テーブル作成、--full-refresh)と Phase 2(スキーマ変更、incremental 実行)を切り替えます。
TC-02: struct<timestamp, string> 追加(バグA 再現)
-- Phase 1: シンプルなテーブル
select
cast(id as integer) as id,
cast(payload as varchar) as payload
from {{ ref('base_items_seed') }}
-- Phase 2: struct 列を追加
select
cast(id as integer) as id,
cast(payload as varchar) as payload,
cast(
row(cast(current_timestamp as timestamp(6)), 'detail_value')
as row(event_time timestamp(6), detail varchar)
) as complex_col
from {{ ref('base_items_seed') }}
結果: Phase 2 で ALTER TABLE が add columns (complex_col timestamp) を生成し、struct 全体が timestamp に誤変換される。MERGE 時に TYPE_MISMATCH。
TC-02b: struct<string, int> 追加(対照群 — 成功)
-- Phase 2: timestamp を含まない struct 列を追加
select
cast(id as integer) as id,
cast(payload as varchar) as payload,
cast(
row('some_name', 42)
as row(name varchar, value integer)
) as simple_struct_col
from {{ ref('base_items_seed') }}
結果: SUCCESS。struct<name:string,value:int> に timestamp が含まれないため、バグA は発生しない。
TC-02e: array<struct> のフィールド増加(バグB 再現)
-- Phase 1: 2フィールドの struct を持つ array
select
cast(id as integer) as id,
cast(payload as varchar) as payload,
cast(
array[row('key1', 'val1'), row('key2', 'val2')]
as array(row(k varchar, v varchar))
) as items
from {{ ref('base_items_seed') }}
-- Phase 2: struct に新フィールド priority が追加
select
cast(id as integer) as id,
cast(payload as varchar) as payload,
cast(
array[row('key1', 'val1', 1), row('key2', 'val2', 2)]
as array(row(k varchar, v varchar, priority integer))
) as items
from {{ ref('base_items_seed') }}
結果: Phase 2 で UPDATE の CAST に cast(items as array(struct<k:string,v:string,priority:int>)) が生成される。Hive 形式の struct<...> が DML に混入し、mismatched input '<'。
TC-02f: struct 内の型変更 int→struct(バグB 再現)
-- Phase 1: score は integer
select
cast(id as integer) as id,
cast(payload as varchar) as payload,
cast(
row(100, 'good')
as row(score integer, label varchar)
) as detail
from {{ ref('base_items_seed') }}
-- Phase 2: score が integer → struct に変わる
select
cast(id as integer) as id,
cast(payload as varchar) as payload,
cast(
row(row(100, 0.95e0), 'good')
as row(score row(raw integer, normalized double), label varchar)
) as detail
from {{ ref('base_items_seed') }}
結果: Phase 2 で UPDATE の CAST に cast(detail as struct<score:struct<raw:int,normalized:double>,label:string>) が生成される。mismatched input '<'。
再現手順(まとめ)
# 前提: dbt-athena-community 1.9.4 or 1.10.0, Athena Iceberg 対応の S3 + Glue DB
# Phase 1: 初期テーブル作成
dbt seed --full-refresh
dbt run --full-refresh --select tc02_top_level_struct_add tc02b_struct_no_timestamp \
tc02e_array_struct_field_evolve tc02f_field_type_change --vars "{phase: 'initial'}"
# Phase 2: スキーマ変更を伴う incremental 実行
dbt --debug run --select tc02_top_level_struct_add --vars "{phase: 'evolved'}"
# → TYPE_MISMATCH (バグA)
dbt --debug run --select tc02b_struct_no_timestamp --vars "{phase: 'evolved'}"
# → SUCCESS (対照群)
dbt --debug run --select tc02e_array_struct_field_evolve --vars "{phase: 'evolved'}"
# → mismatched input '<' (バグB)
dbt --debug run --select tc02f_field_type_change --vars "{phase: 'evolved'}"
# → mismatched input '<' (バグB)