Skip to content

Commit dfb9c99

Browse files
authored
fix(warehouses/accelerator): Electricity ingest: Skip files that cannot be processed. (#169)
### Summary Catch errors loading the electricity consumption data. Users would rather some occasional gaps (that can be backfilled) rather than data loading that just stops. Refs #166 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **Bug Fixes** * Enhanced error handling for merge operations with improved logging of failed data loads. * Improved exception handling during data ingestion to prevent silent failures. * **New Features** * Extended merge configuration capabilities for incremental data synchronisation. * **Tests** * Expanded test coverage for merge key configurations and error scenarios. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 6ce2f1b commit dfb9c99

6 files changed

Lines changed: 65 additions & 15 deletions

File tree

elt-common/src/elt_common/dlt_destinations/pyiceberg/pyiceberg.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,11 +411,20 @@ def write_to_table(
411411
# skip is internally used for dlt tables
412412
table.append(table_data)
413413
elif write_disposition == "merge":
414-
strategy = self.prepare_load_table(table_name)["x-merge-strategy"] # type:ignore
414+
dlt_schema = self.prepare_load_table(table_name)
415+
join_columns = None
416+
if not table.schema().identifier_field_ids:
417+
join_columns = [
418+
name
419+
for name, column in dlt_schema["columns"].items()
420+
if column.get("merge_key", False)
421+
]
422+
# provide merge key columns if no identifier-field-ids exist to mark the primary key columns
423+
strategy = dlt_schema["x-merge-strategy"] # type:ignore
415424
if strategy == "upsert":
416-
# requires the indentifier fields to have been defined
417425
table.upsert(
418426
df=table_data,
427+
join_cols=join_columns,
419428
when_matched_update_all=True,
420429
when_not_matched_insert_all=True,
421430
case_sensitive=True,

elt-common/tests/e2e_tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class Settings(BaseSettings):
9292
# trino
9393
trino_http_scheme: str = "http"
9494
trino_host: str = "localhost"
95-
trino_port: str = "58088"
95+
trino_port: str = "59088"
9696
trino_user: str = "trino"
9797
trino_password: str = ""
9898

elt-common/tests/e2e_tests/elt_common/dlt_destinations/pyiceberg/test_pyiceberg_pipeline.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,19 @@ def pipeline_name(frame: FrameType | None):
3333
return frame.f_code.co_name if frame is not None else "pipeline_name_frame_none"
3434

3535

36-
def resource_factory(data: List[Dict[str, Any]] | None = None, primary_key: str | None = "id"):
36+
def resource_factory(
37+
data: List[Dict[str, Any]] | None = None,
38+
primary_key: str | None = "id",
39+
merge_key: str | None = None,
40+
):
41+
kwargs = {}
3742
if primary_key is not None:
38-
decorator = dlt.resource(primary_key=primary_key)
43+
kwargs["primary_key"] = primary_key
44+
if merge_key is not None:
45+
kwargs["merge_key"] = merge_key
46+
47+
if kwargs:
48+
decorator = dlt.resource(**kwargs)
3949
else:
4050
decorator = dlt.resource
4151

@@ -168,10 +178,18 @@ def test_explicit_replace(
168178
)
169179

170180

181+
@pytest.mark.parametrize(
182+
"identifier_keys",
183+
[
184+
{"primary_key": "id", "merge_key": None},
185+
{"primary_key": None, "merge_key": "id"},
186+
],
187+
)
171188
def test_explicit_merge_updates_expected_values(
172189
warehouse: Warehouse,
173190
pipelines_dir,
174191
destination_config: PyIcebergDestinationTestConfiguration,
192+
identifier_keys: dict,
175193
) -> None:
176194
num_records_first_run = 1000
177195
data = [
@@ -187,7 +205,7 @@ def test_explicit_merge_updates_expected_values(
187205
pipelines_dir=pipelines_dir,
188206
)
189207
# first run
190-
pipeline.run(resource_factory(data, primary_key="id"))
208+
pipeline.run(resource_factory(data, **identifier_keys))
191209
assert_table_has_data(
192210
pipeline,
193211
f"{pipeline.dataset_name}.data_items",
@@ -204,7 +222,7 @@ def test_explicit_merge_updates_expected_values(
204222
}
205223
for i in range(num_records_upserted)
206224
]
207-
pipeline.run(resource_factory(data_updated, primary_key="id"), write_disposition="merge")
225+
pipeline.run(resource_factory(data_updated, **identifier_keys), write_disposition="merge")
208226
expected_data = [
209227
{"id": i + 1, "category": "A" if i + 1 < id_updated_start else "B"}
210228
for i in range(num_records_first_run + num_records_upserted - id_updated_start + 1)
@@ -217,6 +235,28 @@ def test_explicit_merge_updates_expected_values(
217235
)
218236

219237

238+
def test_merge_without_primary_or_merge_key_raises_error(
239+
warehouse: Warehouse,
240+
pipelines_dir,
241+
destination_config: PyIcebergDestinationTestConfiguration,
242+
) -> None:
243+
num_records_first_run = 1000
244+
data = [
245+
{
246+
"id": i + 1,
247+
"category": "A",
248+
}
249+
for i in range(num_records_first_run)
250+
]
251+
pipeline = destination_config.setup_pipeline(
252+
warehouse,
253+
pipeline_name(inspect.currentframe()),
254+
pipelines_dir=pipelines_dir,
255+
)
256+
with pytest.raises(Exception):
257+
pipeline.run(resource_factory(data, primary_key=None), write_disposition="merge")
258+
259+
220260
@pytest.mark.parametrize("merge_strategy", ["delete-insert", "scd2"])
221261
def test_explicit_merge_not_supported_for_strategies_other_than_upsert(
222262
warehouse: Warehouse,

infra/local/docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#
66
# - Keycloak identity provider: http://localhost:58080/auth
77
# - Lakekeeper iceberg catalog UI: http://localhost:58080/iceberg/ui. Log in with the credentials listed in keycloak/README
8-
# - Trino query engine: http://localhost:58088
8+
# - Trino query engine: http://localhost:59088
99
# - Superset BI tool: http://localhost:58080/workspace/playground. Log in with the credentials defined in env-for-testing
1010
#
1111
# Login credentials
@@ -97,7 +97,7 @@ services:
9797
start_period: 5s
9898
ports:
9999
- "58080:80"
100-
- "58088:8088" # Trino won't allow itself to be deployed on a prefix path
100+
- "59088:8088" # Trino won't allow itself to be deployed on a prefix path
101101
volumes:
102102
- "./traefik/traefik.yml:/etc/traefik/traefik.yml"
103103
- "./traefik/traefik-dynamic.yml:/etc/traefik/traefik-dynamic.yml"

warehouses/accelerator/extract_load/electricity_sharepoint/extract_and_load.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,11 @@ def read_as_dataframe(file_obj: M365DriveItem) -> pd.DataFrame | None:
117117
df = read_power_consumption_excel(io.BytesIO(file_bytes), skip_rows)
118118
case _:
119119
raise RuntimeError(f"Unsupported file extension in '{file_name}'")
120-
except pd.errors.EmptyDataError as exc:
121-
raise RuntimeError(
122-
f"'{file_name} ({len(file_bytes)} bytes)': {str(exc)}"
123-
) from exc
120+
except Exception as exc:
121+
LOGGER.error(
122+
f"'Error loading {file_name} ({len(file_bytes)} bytes)'. File skipped.\nDetails: {str(exc)}"
123+
)
124+
return None
124125

125126
df["file_name"] = file_obj["file_name"]
126127
return df
@@ -140,7 +141,7 @@ def read_as_dataframe(file_obj: M365DriveItem) -> pd.DataFrame | None:
140141
yield df_batch
141142

142143

143-
@dlt.resource(merge_key="file_name")
144+
@dlt.resource(merge_key="DateTime", columns={"DateTime": {"nullable": False}})
144145
def rdm_data(
145146
datetime_cur=dlt.sources.incremental(
146147
"DateTime",

warehouses/accelerator/transform/accelerator/profiles.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ trino_catalog:
99
password: "{{ env_var('DBT_TRINO_PASSWORD', '') }}"
1010
http_scheme: "{{ env_var('DBT_TRINO_HTTP_SCHEME', 'http') }}"
1111
host: "{{ env_var('DBT_TRINO_HOST', 'analytics.localhost') }}"
12-
port: "{{ env_var('DBT_TRINO_PORT', '58088') | int }}"
12+
port: "{{ env_var('DBT_TRINO_PORT', '59088') | int }}"
1313
database: "{{ env_var('DBT_TRINO_CATALOG', 'accelerator') }}"
1414
schema: "{{ env_var('DBT_TRINO_CATALOG_SCHEMA_PREFIX', 'dev_') }}analytics"
1515
threads: "{{ env_var('DBT_TRINO_THREADS', '8') | int }}"

0 commit comments

Comments
 (0)