Skip to content

Commit 6c2531e

Browse files
committed
feat(athena): add use_iceberg_write_to config for Iceberg Python models
Signed-off-by: Daisuke Taniwaki <daisuketaniwaki@gmail.com>
1 parent 77a0fe3 commit 6c2531e

3 files changed

Lines changed: 69 additions & 4 deletions

File tree

dbt-athena/src/dbt/include/athena/macros/adapters/python_submissions.sql

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
{% set merge_schema = optional_args.get("merge_schema", true) %}
1010
{% set bucket_count = optional_args.get("bucket_count") %}
1111
{% set field_delimiter = optional_args.get("field_delimiter") %}
12+
{% set table_type = optional_args.get("table_type", "") %}
13+
{% set extra_table_properties = optional_args.get("extra_table_properties") %}
14+
{% set use_iceberg_write_to = optional_args.get("use_iceberg_write_to", false) %}
1215
{% set spark_ctas = optional_args.get("spark_ctas", "") %}
1316

1417
import pyspark
@@ -25,7 +28,48 @@ def materialize(spark_session, df, target_relation):
2528
msg = f"{type(df)} is not a supported type for dbt Python materialization"
2629
raise Exception(msg)
2730

28-
{% if spark_ctas|length > 0 %}
31+
{% if use_iceberg_write_to %}
32+
import re
33+
from pyspark.sql import functions as F
34+
35+
def _parse_iceberg_partition(expr_str):
36+
expr_str = expr_str.strip()
37+
m = re.match(r"(\w+)\((.+)\)", expr_str)
38+
if not m:
39+
return F.col(expr_str)
40+
func = m.group(1).lower()
41+
args = [a.strip() for a in m.group(2).split(",")]
42+
if func in ("day", "days"):
43+
return F.days(F.col(args[0]))
44+
if func in ("month", "months"):
45+
return F.months(F.col(args[0]))
46+
if func in ("year", "years"):
47+
return F.years(F.col(args[0]))
48+
if func in ("hour", "hours"):
49+
return F.hours(F.col(args[0]))
50+
if func == "bucket":
51+
return F.bucket(int(args[1]), F.col(args[0]))
52+
if func == "truncate":
53+
return F.truncate(int(args[1]), F.col(args[0]))
54+
raise ValueError(f"Unknown Iceberg partition transform: {func}")
55+
56+
_writer = df.writeTo("{{ target_relation.schema | replace('\"', '`') }}.{{ target_relation.identifier | replace('\"', '`') }}")
57+
_writer = _writer.using("iceberg")
58+
_writer = _writer.tableProperty("location", "{{ location }}/")
59+
{% if extra_table_properties is not none %}
60+
{% for prop_name, prop_value in extra_table_properties.items() %}
61+
_writer = _writer.tableProperty("{{ prop_name }}", "{{ prop_value }}")
62+
{% endfor %}
63+
{% endif %}
64+
{% if partitioned_by is not none %}
65+
_writer = _writer.partitionedBy(
66+
{%- for part_expr in partitioned_by %}
67+
_parse_iceberg_partition("{{ part_expr }}"){{ "," if not loop.last }}
68+
{%- endfor %}
69+
)
70+
{% endif %}
71+
_writer.createOrReplace()
72+
{% elif spark_ctas|length > 0 %}
2973
df.createOrReplaceTempView("{{ target_relation.schema}}_{{ target_relation.identifier }}_tmpvw")
3074
spark_session.sql("""
3175
{{ spark_ctas }}

dbt-athena/src/dbt/include/athena/macros/materializations/models/table/create_table_as.sql

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@
4343
{%- endif -%}
4444

4545
{%- if language == 'python' -%}
46+
{%- set use_iceberg_write_to = config.get('use_iceberg_write_to', false) -%}
4647
{%- set spark_ctas = '' -%}
47-
{%- if table_type == 'iceberg' -%}
48+
{%- if table_type == 'iceberg' and not use_iceberg_write_to -%}
4849
{%- set spark_ctas -%}
4950
create table {{ relation.schema | replace('\"', '`') }}.{{ relation.identifier | replace('\"', '`') }}
5051
using iceberg
@@ -85,7 +86,10 @@
8586
'write_compression': write_compression,
8687
'bucket_count': bucket_count,
8788
'field_delimiter': field_delimiter,
88-
'spark_ctas': spark_ctas
89+
'table_type': table_type,
90+
'extra_table_properties': extra_table_properties,
91+
'use_iceberg_write_to': use_iceberg_write_to,
92+
'spark_ctas': spark_ctas,
8993
}
9094
)
9195
}}

dbt-athena/src/dbt/include/athena/macros/materializations/models/table/table.sql

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,24 @@
9191
{%- endif -%}
9292
{%- else -%}
9393

94-
{%- if old_relation is none -%}
94+
{%- set use_iceberg_write_to = language == 'python' and config.get('use_iceberg_write_to', false) -%}
95+
96+
{%- if use_iceberg_write_to -%}
97+
-- Python + use_iceberg_write_to: writeTo().createOrReplace() handles atomic replacement,
98+
-- so we skip the __ha intermediate table and write directly to target.
99+
-- Clean up leftover __ha / __bkp tables from previous HA-flow failures.
100+
{%- if old_tmp_relation is not none -%}
101+
{%- do drop_relation(old_tmp_relation) -%}
102+
{%- endif -%}
103+
{%- if old_bkp_relation is not none -%}
104+
{%- do drop_relation(old_bkp_relation) -%}
105+
{%- endif -%}
106+
{%- set query_result = safe_create_table_as(False, target_relation, compiled_code, language, force_batch) -%}
107+
{% call statement('create_table', language=language) %}
108+
{{ query_result }}
109+
{% endcall %}
110+
111+
{%- elif old_relation is none -%}
95112
{%- set query_result = safe_create_table_as(False, target_relation, compiled_code, language, force_batch) -%}
96113
-- Execute python code that is available in query result object
97114
{%- if language == 'python' -%}

0 commit comments

Comments
 (0)