Skip to content

Commit 2e98496

Browse files
author
Samson Gebre
committed
Add distinct alias generation for SQL joins to prevent collisions
1 parent e916082 commit 2e98496

3 files changed

Lines changed: 89 additions & 3 deletions

File tree

examples/advanced/sql_examples.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,29 @@ def _run_examples(client):
935935
for j in joins[:5]:
936936
print(f" {j['column']:25s} -> {j['target']}.{j['target_pk']}")
937937

938+
# sql_joins -- alias uniqueness: multiple lookups to the same target
939+
# table (e.g. ownerid + createdby + modifiedby all point to systemuser)
940+
# must each get a distinct alias so the combined SQL is valid.
941+
# Expected output:
942+
# ownerid -> systemuser alias=s
943+
# createdby -> systemuser alias=s2
944+
# modifiedby -> systemuser alias=s3
945+
log_call("client.query.sql_joins('contact') -- distinct aliases for same target table")
946+
try:
947+
contact_joins = client.query.sql_joins("contact")
948+
systemuser_joins = [j for j in contact_joins if j["target"] == "systemuser"]
949+
print(f"[OK] {len(systemuser_joins)} lookup(s) from contact -> systemuser:")
950+
for j in systemuser_joins:
951+
alias = j["join_clause"].split()[2]
952+
print(f" {j['column']:30s} -> {j['target']} alias={alias}")
953+
aliases = [j["join_clause"].split()[2] for j in contact_joins]
954+
if len(aliases) != len(set(aliases)):
955+
print("[WARN] Duplicate aliases detected")
956+
else:
957+
print(f"[OK] All {len(contact_joins)} aliases unique")
958+
except Exception as e:
959+
print(f"[INFO] Alias check skipped: {e}")
960+
938961
# sql_join (auto-generate JOIN clause)
939962
log_call(f"client.query.sql_join('{child_table}', '{parent_table}', ...)")
940963
try:

src/PowerPlatform/Dataverse/operations/query.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ def sql_joins(
309309
table_lower = table.lower()
310310
rels = self._client.tables.list_table_relationships(table)
311311

312+
used_aliases: set = set()
312313
result: List[Dict[str, Any]] = []
313314
for r in rels:
314315
ref_entity = (r.get("ReferencingEntity") or "").lower()
@@ -321,9 +322,19 @@ def sql_joins(
321322
if not all([col, target, target_pk]):
322323
continue
323324

324-
# Generate a short alias for the target table
325-
alias = target[0] if target else "j"
326-
join_clause = f"JOIN {target} {alias} " f"ON {table_lower}.{col} = {alias}.{target_pk}"
325+
# Generate a unique alias — add a numeric suffix on collision so
326+
# two lookups to tables starting with the same letter (e.g.
327+
# "account" and "annotation") or two lookups to the same table
328+
# (e.g. "ownerid" and "createdby" both to "systemuser") produce
329+
# distinct aliases and valid SQL.
330+
base = target[0] if target else "j"
331+
alias = base
332+
counter = 2
333+
while alias in used_aliases:
334+
alias = f"{base}{counter}"
335+
counter += 1
336+
used_aliases.add(alias)
337+
join_clause = f"JOIN {target} {alias} ON {table_lower}.{col} = {alias}.{target_pk}"
327338

328339
result.append(
329340
{

tests/unit/test_query_operations.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,58 @@ def test_empty_relationships(self):
644644
joins = self.client.query.sql_joins("account")
645645
self.assertEqual(joins, [])
646646

647+
def test_alias_collision_same_first_letter(self):
648+
"""Two targets starting with the same letter get distinct aliases."""
649+
self._mock_rels(
650+
[
651+
{
652+
"ReferencingEntity": "contact",
653+
"ReferencingAttribute": "parentcustomerid",
654+
"ReferencedEntity": "account",
655+
"ReferencedAttribute": "accountid",
656+
"SchemaName": "contact_customer_accounts",
657+
},
658+
{
659+
"ReferencingEntity": "contact",
660+
"ReferencingAttribute": "regardingobjectid",
661+
"ReferencedEntity": "annotation",
662+
"ReferencedAttribute": "annotationid",
663+
"SchemaName": "contact_annotation",
664+
},
665+
]
666+
)
667+
joins = self.client.query.sql_joins("contact")
668+
self.assertEqual(len(joins), 2)
669+
aliases = [j["join_clause"].split()[2] for j in joins]
670+
self.assertEqual(len(set(aliases)), 2, "aliases must be unique")
671+
self.assertNotEqual(aliases[0], aliases[1])
672+
673+
def test_alias_collision_same_target_table(self):
674+
"""Two lookups to the same table (e.g. ownerid + createdby -> systemuser) get distinct aliases."""
675+
self._mock_rels(
676+
[
677+
{
678+
"ReferencingEntity": "contact",
679+
"ReferencingAttribute": "ownerid",
680+
"ReferencedEntity": "systemuser",
681+
"ReferencedAttribute": "systemuserid",
682+
"SchemaName": "contact_ownerid_systemuser",
683+
},
684+
{
685+
"ReferencingEntity": "contact",
686+
"ReferencingAttribute": "createdby",
687+
"ReferencedEntity": "systemuser",
688+
"ReferencedAttribute": "systemuserid",
689+
"SchemaName": "contact_createdby_systemuser",
690+
},
691+
]
692+
)
693+
joins = self.client.query.sql_joins("contact")
694+
self.assertEqual(len(joins), 2)
695+
aliases = [j["join_clause"].split()[2] for j in joins]
696+
self.assertEqual(len(set(aliases)), 2, "aliases must be unique")
697+
self.assertNotEqual(aliases[0], aliases[1])
698+
647699

648700
class TestSqlJoin(unittest.TestCase):
649701
"""Tests for client.query.sql_join()."""

0 commit comments

Comments
 (0)