11"""Business rule definitions for duckdb backend"""
22
33from collections .abc import Callable , Iterator
4- from typing import get_type_hints
4+ from typing import get_type_hints , Optional
55from uuid import uuid4
66
77from duckdb import (
5656 SemiJoin ,
5757 TableUnion ,
5858)
59+ from dve .core_engine .configuration .v1 .hierarchy import EntityHierarchy
5960from dve .core_engine .constants import ORPHANED_RECORD_ENTITY_NAME , RECORD_INDEX_COLUMN_NAME
6061from dve .core_engine .functions import implementations as functions
6162from dve .core_engine .message import FeedbackMessage
6263from dve .core_engine .templating import template_object
63- from dve .core_engine .type_hints import Messages
64+ from dve .core_engine .type_hints import URI , EntityName , Messages
6465
6566
6667@duckdb_record_index
@@ -73,6 +74,7 @@ class DuckDBStepImplementations(BaseStepImplementations[DuckDBPyRelation]):
7374 def __init__ (self , connection : DuckDBPyConnection , ** kwargs ):
7475 self ._connection = connection
7576 self .registered_functions = get_all_registered_udfs (self ._connection )
77+ self ._temp_tables : dict [EntityName , str ] = {}
7678 super ().__init__ (** kwargs )
7779
7880 @property
@@ -113,6 +115,29 @@ def register_udfs( # type: ignore
113115 connection .sql (_sql )
114116 return cls (connection = connection , ** kwargs )
115117
118+ def _materialise_temp_tables_from_entity (
119+ self ,
120+ entities : DuckDBEntities ,
121+ entity_names : list [EntityName ],
122+ refresh : bool = False
123+ ):
124+ """Materialise an entity into a temporary duckdb table."""
125+ for entity_name in entity_names :
126+ temp_name = f"temp_{ entity_name } "
127+ if entity_name in self ._temp_tables :
128+ if not refresh :
129+ continue
130+ self .connection .unregister (temp_name )
131+ del self ._temp_tables [entity_name ]
132+
133+ self .connection .register (temp_name , entities [entity_name ])
134+ self ._temp_tables [entity_name ] = temp_name
135+
136+ def _drop_temp_tables (self ) -> None :
137+ for temp_name in self ._temp_tables .values ():
138+ self .connection .unregister (temp_name )
139+ self ._temp_tables .clear ()
140+
116141 def add (self , entities : DuckDBEntities , * , config : ColumnAddition ) -> Messages :
117142 """A transformation step which adds a column to an entity."""
118143 entity : DuckDBPyRelation = entities [config .entity_name ]
@@ -392,9 +417,14 @@ def identify_orphans(
392417 logical OR of its current value and the value it would have been set to otherwise.
393418
394419 """
395- source_rel : DuckDBPyRelation = entities [config .entity_name ]
420+ self ._materialise_temp_tables_from_entity (
421+ entities ,
422+ [config .entity_name , config .target_name ]
423+ )
424+
425+ source_rel : DuckDBPyRelation = self .connection .table (self ._temp_tables [config .entity_name ])
396426 source_rel = source_rel .set_alias (config .entity_name )
397- target_rel : DuckDBPyRelation = entities [config .target_name ]
427+ target_rel : DuckDBPyRelation = self . connection . table ( self . _temp_tables [config .target_name ])
398428 target_rel = target_rel .set_alias (config .target_name )
399429
400430 if relation_is_empty (source_rel ):
@@ -465,8 +495,31 @@ def remove_orphans(self, entities: DuckDBEntities, *, config: OrphanRemoval) ->
465495
466496 entities [config .entity_name ] = filtered_rel
467497
498+ self ._materialise_temp_tables_from_entity (entities , [config .entity_name ], refresh = True )
499+
468500 return duckdb_rel_to_dictionaries (message_rel )
469501
502+ def identify_and_remove_orphans (
503+ self ,
504+ working_directory : URI ,
505+ entities : DuckDBEntities ,
506+ entity_hierarchy : EntityHierarchy ,
507+ key_fields : Optional [dict [str , list [str ]]] = None ,
508+ ) -> tuple [Messages , dict [EntityName , bool ]]:
509+ """
510+ Identifies and removes orphan records by traversing the EntityHierarchy object.
511+ An orphan is a child record whose parent FK does not exist in the parent entity.
512+ Processes recursively: removes orphans at each level, then processes children.
513+ """
514+ _msgs , entity_issues_found = super ().identify_and_remove_orphans (
515+ working_directory ,
516+ entities ,
517+ entity_hierarchy ,
518+ key_fields ,
519+ )
520+ self ._drop_temp_tables ()
521+ return _msgs , entity_issues_found
522+
470523 def check_mandatory_group (
471524 self , entities : DuckDBEntities , * , config : GroupIdentification
472525 ) -> Iterator :
0 commit comments