Skip to content

Commit 75a6a93

Browse files
amc-corey-coxclaude
andcommitted
Add context manager protocol to LookupIndex and cleanup in transform_spec
LookupIndex now supports `with` statement usage via __enter__/__exit__. transform_spec() closes the LookupIndex it creates after iteration completes, preventing DuckDB connection leaks. Closes #143 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e9b12ab commit 75a6a93

2 files changed

Lines changed: 46 additions & 33 deletions

File tree

src/linkml_map/transformer/engine.py

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -41,41 +41,46 @@ def transform_spec(
4141
if spec is None:
4242
return
4343

44-
if transformer.lookup_index is None:
44+
owns_index = transformer.lookup_index is None
45+
if owns_index:
4546
transformer.lookup_index = LookupIndex()
4647

47-
for class_deriv in spec.class_derivations:
48-
table_name = class_deriv.populated_from or class_deriv.name
49-
if table_name not in data_loader:
50-
logger.debug("Skipping class_derivation %s: no data found", class_deriv.name)
51-
continue
48+
try:
49+
for class_deriv in spec.class_derivations:
50+
table_name = class_deriv.populated_from or class_deriv.name
51+
if table_name not in data_loader:
52+
logger.debug("Skipping class_derivation %s: no data found", class_deriv.name)
53+
continue
5254

53-
joined_tables: list[str] = []
54-
try:
55-
# Register secondary (joined) tables
56-
if class_deriv.joins:
57-
for join_name, join_spec in class_deriv.joins.items():
58-
lookup_key = join_spec.lookup_key or join_spec.join_on
59-
source_key = join_spec.source_key or join_spec.join_on
60-
if not lookup_key or not source_key:
61-
msg = (
62-
f"Join {join_name!r} must specify 'join_on' or both "
63-
f"'source_key' and 'lookup_key'"
55+
joined_tables: list[str] = []
56+
try:
57+
# Register secondary (joined) tables
58+
if class_deriv.joins:
59+
for join_name, join_spec in class_deriv.joins.items():
60+
lookup_key = join_spec.lookup_key or join_spec.join_on
61+
source_key = join_spec.source_key or join_spec.join_on
62+
if not lookup_key or not source_key:
63+
msg = (
64+
f"Join {join_name!r} must specify 'join_on' or both "
65+
f"'source_key' and 'lookup_key'"
66+
)
67+
raise ValueError(msg)
68+
join_path = data_loader.get_path(join_name)
69+
transformer.lookup_index.register_table(
70+
join_name, join_path, lookup_key
6471
)
65-
raise ValueError(msg)
66-
join_path = data_loader.get_path(join_name)
67-
transformer.lookup_index.register_table(
68-
join_name, join_path, lookup_key
69-
)
70-
joined_tables.append(join_name)
72+
joined_tables.append(join_name)
7173

72-
# Stream primary table rows
73-
for row in data_loader[table_name]:
74-
yield transformer.map_object(
75-
row,
76-
source_type=source_type or table_name,
77-
class_derivation=class_deriv,
78-
)
79-
finally:
80-
for jt in joined_tables:
81-
transformer.lookup_index.drop(jt)
74+
# Stream primary table rows
75+
for row in data_loader[table_name]:
76+
yield transformer.map_object(
77+
row,
78+
source_type=source_type or table_name,
79+
class_derivation=class_deriv,
80+
)
81+
finally:
82+
for jt in joined_tables:
83+
transformer.lookup_index.drop(jt)
84+
finally:
85+
if owns_index:
86+
transformer.lookup_index.close()

src/linkml_map/utils/lookup_index.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ def is_registered(self, table: str) -> bool:
8484
"""Check whether *table* has been registered."""
8585
return table in self._tables
8686

87+
def __enter__(self) -> LookupIndex:
88+
"""Enter the context manager."""
89+
return self
90+
91+
def __exit__(self, *exc_info: object) -> None:
92+
"""Exit the context manager, closing the DuckDB connection."""
93+
self.close()
94+
8795
def close(self) -> None:
8896
"""Close the DuckDB connection."""
8997
self._conn.close()

0 commit comments

Comments
 (0)