Skip to content

Commit 43c428f

Browse files
authored
fix: raise processing error where filter is materialising null values (#162)
1 parent 3eca2f1 commit 43c428f

7 files changed

Lines changed: 70 additions & 9 deletions

File tree

‎src/dve/core_engine/backends/base/rules.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -448,8 +448,7 @@ def process_node(node: HierarchyNode):
448448
record=record, # type: ignore
449449
error_location=location,
450450
error_message=template_object(
451-
node.missing_parent_id_error_message,
452-
record
451+
node.missing_parent_id_error_message, record
453452
),
454453
failure_type="record",
455454
error_type="record",
@@ -522,10 +521,7 @@ def process_node(node: HierarchyNode) -> bool:
522521
entity=node.parent_entity,
523522
record=record, # type: ignore
524523
error_location=location,
525-
error_message=template_object(
526-
node.no_valid_records_error_message,
527-
record
528-
),
524+
error_message=template_object(node.no_valid_records_error_message, record),
529525
failure_type="record",
530526
error_type="record",
531527
error_code=node.no_valid_records_error_code,
@@ -624,6 +620,7 @@ def apply_sync_filters(
624620
excluded_columns=filter_column_names,
625621
reporting=rule.reporting,
626622
parent=rule.parent,
623+
error_on_null=True,
627624
),
628625
)
629626
if not success:
@@ -655,6 +652,7 @@ def apply_sync_filters(
655652
expression=f"NOT ({rule.expression})",
656653
reporting=rule.reporting,
657654
parent=rule.parent,
655+
error_on_null=True,
658656
),
659657
)
660658
if not success:
@@ -887,3 +885,8 @@ def filter_data_contract_record_rejections(
887885
):
888886
"""Method to filter out record rejection errors from the data contract for a given entity"""
889887
raise NotImplementedError()
888+
889+
@staticmethod
890+
def get_entity_count(entity: EntityType) -> int:
891+
"""Method to get count of records in entity"""
892+
raise NotImplementedError()

‎src/dve/core_engine/backends/implementations/duckdb/rules.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Business rule definitions for duckdb backend"""
2-
2+
# pylint: disable=R0801
33
from collections.abc import Callable, Iterator
44
from typing import get_type_hints
55
from uuid import uuid4
@@ -23,6 +23,7 @@
2323
from dve.core_engine.backends.implementations.duckdb.duckdb_helpers import (
2424
DDBStruct,
2525
ddb_filter_contract_errors,
26+
duckdb_get_entity_count,
2627
duckdb_read_parquet,
2728
duckdb_record_index,
2829
duckdb_rel_to_dictionaries,
@@ -62,6 +63,7 @@
6263
from dve.core_engine.type_hints import Messages
6364

6465

66+
@duckdb_get_entity_count
6567
@duckdb_record_index
6668
@duckdb_write_parquet
6769
@duckdb_read_parquet
@@ -568,7 +570,12 @@ def notify(self, entities: DuckDBEntities, *, config: Notification) -> Messages:
568570
"""
569571
messages: Messages = []
570572
entity = entities[config.entity_name]
571-
573+
if config.error_if_expression_null:
574+
if self.get_entity_count(entity.filter(f"({config.expression}) IS NULL")) > 0:
575+
raise ValueError(
576+
f"The filter evaluated for error code {config.reporting.code}"
577+
+ f" in entity {config.entity_name} produced some NULL results. Please investigate." # pylint: disable=C0301
578+
)
572579
matched = entity.filter(config.expression)
573580
if config.excluded_columns:
574581
matched = matched.select(StarExpression(exclude=config.excluded_columns))

‎src/dve/core_engine/backends/implementations/spark/rules.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Step implementations in Spark."""
2-
2+
# pylint: disable=R0801
33
from collections.abc import Callable, Iterator
44
from typing import Optional
55
from uuid import uuid4
@@ -15,6 +15,7 @@
1515
get_all_registered_udfs,
1616
object_to_spark_literal,
1717
spark_filter_contract_errors,
18+
spark_get_entity_count,
1819
spark_read_parquet,
1920
spark_record_index,
2021
spark_write_parquet,
@@ -53,6 +54,7 @@
5354
from dve.core_engine.type_hints import Messages
5455

5556

57+
@spark_get_entity_count
5658
@spark_record_index
5759
@spark_write_parquet
5860
@spark_read_parquet
@@ -411,6 +413,13 @@ def notify(self, entities: SparkEntities, *, config: Notification) -> Messages:
411413
messages: Messages = []
412414
entity = entities[config.entity_name]
413415

416+
if config.error_if_expression_null:
417+
if self.get_entity_count(entity.filter(f"({config.expression}) IS NULL")) > 0:
418+
raise ValueError(
419+
f"The filter evaluated for error code {config.reporting.code}"
420+
+ f" in entity {config.entity_name} produced some NULL results. Please investigate." # pylint: disable=C0301
421+
)
422+
414423
matched = entity.filter(config.expression)
415424
if config.excluded_columns:
416425
matched = matched.drop(*config.excluded_columns)

‎src/dve/core_engine/backends/metadata/rules.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@ class Notification(AbstractStep):
282282
"""Columns to be excluded from the record in the report."""
283283
reporting: ReportingConfig
284284
"""The reporting information for the filter."""
285+
error_if_expression_null: bool = False
286+
"""Raise error if the results of evaluating the expression passed leads to some NULL results"""
285287

286288
def get_required_entities(self) -> set[EntityName]:
287289
return {self.entity_name}

‎src/dve/reporting/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
"""Error reports module."""
2+
# pylint: disable=R0801

‎tests/test_core_engine/test_backends/test_implementations/test_duckdb/test_rules.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,25 @@ def test_planets_notify(planets_rel: DuckDBPyRelation):
820820
assert len(messages[0]) == 4
821821

822822

823+
def test_notify_null_errors(planets_rel: DuckDBPyRelation):
824+
825+
config = Notification(
826+
entity_name="planets",
827+
expression="CASE WHEN planet=='Mercury' THEN NULL ELSE False END",
828+
excluded_columns=["mass", "diameter"],
829+
reporting=ReportingConfig(
830+
code="TESTNULLERROR", message="this is a test", location="planet, has_ring_system"
831+
),
832+
error_if_expression_null=True
833+
)
834+
entities = EntityManager({"planets": planets_rel})
835+
messages, success = DUCKDB_STEP_BACKEND.evaluate(entities, config=config)
836+
837+
assert not success
838+
assert len(messages) == 1
839+
assert messages[0].is_critical
840+
841+
823842
def test_read_and_write_simple_parquet(simple_typecast_parquet):
824843
parquet_uri, data = simple_typecast_parquet
825844
entity: DuckDBPyRelation = DUCKDB_STEP_BACKEND.read_parquet(path=parquet_uri)

‎tests/test_core_engine/test_backends/test_implementations/test_spark/test_rules.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from dve.core_engine.backends.base.core import EntityManager
2222
from dve.core_engine.backends.exceptions import MissingEntity
2323
from dve.core_engine.backends.implementations.spark.rules import SparkStepImplementations
24+
from dve.core_engine.backends.metadata.reporting import ReportingConfig
2425
from dve.core_engine.backends.metadata.rules import (
2526
Aggregation,
2627
AntiJoin,
@@ -32,6 +33,7 @@
3233
HeaderJoin,
3334
InnerJoin,
3435
LeftJoin,
36+
Notification,
3537
OneToOneJoin,
3638
OrphanIdentification,
3739
RenameEntity,
@@ -447,6 +449,24 @@ def test_join_can_take_all_cols(
447449
expected_rows = sorted(expected_df.collect(), key=lambda row: row.planet)
448450

449451
assert actual_rows == expected_rows
452+
453+
def test_notify_null_errors(planets_df: DataFrame):
454+
455+
config = Notification(
456+
entity_name="planets",
457+
expression="CASE WHEN planet=='Mercury' THEN NULL ELSE False END",
458+
excluded_columns=["mass", "diameter"],
459+
reporting=ReportingConfig(
460+
code="TESTNULLERROR", message="this is a test", location="planet, has_ring_system"
461+
),
462+
error_if_expression_null=True
463+
)
464+
entities = EntityManager({"planets": planets_df})
465+
messages, success = SPARK_STEP_BACKEND.evaluate(entities, config=config)
466+
467+
assert not success
468+
assert len(messages) == 1
469+
assert messages[0].is_critical
450470

451471

452472
def test_one_to_one_join_multi_matches_raises(planets_df: DataFrame, satellites_df: DataFrame):

0 commit comments

Comments
 (0)