@@ -293,18 +293,23 @@ def _convert_double_quotes(text: str) -> str:
293293 i += 1
294294 elif c == '"' :
295295 # Double-quoted string: convert to single quotes
296+ # Escape any apostrophes inside, and preserve escaped "" as literal "
296297 result .append ("'" )
297298 i += 1
298299 while i < len (text ):
299300 if text [i ] == '"' :
300301 if i + 1 < len (text ) and text [i + 1 ] == '"' :
301- # Escaped double quote -> escaped single quote
302- result .append ("''" )
302+ # Escaped double quote "" -> literal " in single-quoted string
303+ result .append ('"' )
303304 i += 2
304305 else :
305306 result .append ("'" )
306307 i += 1
307308 break
309+ elif text [i ] == "'" :
310+ # Apostrophe inside string: escape for SQL single-quoted literal
311+ result .append ("''" )
312+ i += 1
308313 else :
309314 result .append (text [i ])
310315 i += 1
@@ -723,8 +728,7 @@ class _ObjectGraphJoin:
723728
724729 first_table : str
725730 second_table : str
726- first_field : str
727- second_field : str
731+ column_pairs : list [tuple [str , str ]] # [(first_field, second_field), ...]
728732
729733
730734@dataclass
@@ -1250,20 +1254,22 @@ def _parse_object_graph(self, ds_elem: ET.Element) -> _ObjectGraphInfo:
12501254 second_table = obj_map .get (second_ep .get ("object-id" , "" ), "" ) if second_ep is not None else ""
12511255
12521256 if first_table and second_table and pairs :
1253- left_col , right_col = pairs [0 ]
1254- left_field = left_col .rsplit ("." , 1 )[- 1 ] if "." in left_col else left_col
1255- right_field = right_col .rsplit ("." , 1 )[- 1 ] if "." in right_col else right_col
1257+ field_pairs = [
1258+ (
1259+ lc .rsplit ("." , 1 )[- 1 ] if "." in lc else lc ,
1260+ rc .rsplit ("." , 1 )[- 1 ] if "." in rc else rc ,
1261+ )
1262+ for lc , rc in pairs
1263+ ]
12561264 joins .append (
12571265 _ObjectGraphJoin (
12581266 first_table = first_table ,
12591267 second_table = second_table ,
1260- first_field = left_field ,
1261- second_field = right_field ,
1268+ column_pairs = field_pairs ,
12621269 )
12631270 )
1264- # Extract just the column names (strip table qualifiers)
1265- fk = left_field
1266- pk = right_field
1271+ # Use first pair for Relationship fk/pk
1272+ fk , pk = field_pairs [0 ]
12671273 relationships .append (
12681274 Relationship (
12691275 name = second_table ,
@@ -1316,9 +1322,8 @@ def _build_collection_sql(
13161322 join_clauses .append (
13171323 self ._build_collection_join_clause (
13181324 join .first_table ,
1319- join .first_field ,
13201325 join .second_table ,
1321- join .second_field ,
1326+ join .column_pairs ,
13221327 table_map ,
13231328 alias_by_table ,
13241329 field_sources ,
@@ -1328,12 +1333,13 @@ def _build_collection_sql(
13281333 remaining .remove (join )
13291334 progressed = True
13301335 elif join .second_table in connected and join .first_table not in connected :
1336+ # Reverse the column pairs for the swapped direction
1337+ reversed_pairs = [(rc , lc ) for lc , rc in join .column_pairs ]
13311338 join_clauses .append (
13321339 self ._build_collection_join_clause (
13331340 join .second_table ,
1334- join .second_field ,
13351341 join .first_table ,
1336- join . first_field ,
1342+ reversed_pairs ,
13371343 table_map ,
13381344 alias_by_table ,
13391345 field_sources ,
@@ -1373,30 +1379,23 @@ def _build_collection_sql(
13731379 def _build_collection_join_clause (
13741380 self ,
13751381 connected_table : str ,
1376- connected_field : str ,
13771382 joining_table : str ,
1378- joining_field : str ,
1383+ column_pairs : list [ tuple [ str , str ]] ,
13791384 table_map : dict [str , str ],
13801385 alias_by_table : dict [str , str ],
13811386 field_sources : dict [str , tuple [str , str ]],
13821387 ) -> str :
13831388 """Build one LEFT JOIN clause for a logical-layer collection."""
13841389 joining_table_qualified = table_map [joining_table ]
1385- left_expr = self ._collection_field_sql (
1386- connected_table ,
1387- connected_field ,
1388- alias_by_table ,
1389- field_sources ,
1390- )
1391- right_expr = self ._collection_field_sql (
1392- joining_table ,
1393- joining_field ,
1394- alias_by_table ,
1395- field_sources ,
1396- )
1390+ on_parts = []
1391+ for left_field , right_field in column_pairs :
1392+ left_expr = self ._collection_field_sql (connected_table , left_field , alias_by_table , field_sources )
1393+ right_expr = self ._collection_field_sql (joining_table , right_field , alias_by_table , field_sources )
1394+ on_parts .append (f"{ left_expr } = { right_expr } " )
1395+ on_clause = " AND " .join (on_parts )
13971396 return (
13981397 f"LEFT JOIN { _quote_table_reference (joining_table_qualified )} AS { alias_by_table [joining_table ]} "
1399- f"ON { left_expr } = { right_expr } "
1398+ f"ON { on_clause } "
14001399 )
14011400
14021401 def _collection_field_sql (
0 commit comments