Skip to content

Commit 7d714be

Browse files
author
Saurabh Badenkal
committed
Revert CreateMultiple ID auto-extraction from created_ids
Design improvement: created_ids now ONLY returns entity_id values from OData-EntityId response headers (the standard OData response mechanism). It no longer auto-extracts IDs from CreateMultiple/UpsertMultiple response body data['Ids']. Rationale: A batch can contain heterogeneous operations (creates, updates, deletes, queries, table ops). Mixing two different response formats (OData-EntityId header vs action body) into one property was non-standard and could mislead callers. The OData Web API pattern is for callers to iterate result.responses and handle each response by type. For CreateMultiple/UpsertMultiple bulk IDs, callers access them via: for resp in result.succeeded: if resp.data and 'Ids' in resp.data: bulk_ids = resp.data['Ids'] This aligns with the .NET SDK batch response model (BatchResponse returns raw HttpResponseMessages for caller iteration) and follows OData spec conventions. Updated tests to verify the correct access pattern.
1 parent 40956b7 commit 7d714be

2 files changed

Lines changed: 50 additions & 40 deletions

File tree

src/PowerPlatform/Dataverse/models/batch.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -87,20 +87,19 @@ def has_errors(self) -> bool:
8787

8888
@property
8989
def created_ids(self) -> List[str]:
90-
"""GUIDs from all successful create operations.
90+
"""GUIDs extracted from ``OData-EntityId`` headers of successful responses.
9191
92-
Collects IDs from two sources:
92+
Returns entity IDs from any successful (2xx) response that includes an
93+
``OData-EntityId`` header. Individual ``POST`` creates return this
94+
header with the new record's GUID.
9395
94-
- ``entity_id`` extracted from the ``OData-EntityId`` response header
95-
(individual POST creates return ``204 No Content`` with this header).
96-
- ``data["Ids"]`` from ``CreateMultiple`` / ``UpsertMultiple`` action
97-
responses (these return ``200 OK`` with ``{"Ids": [...]}`` in the
98-
body instead of per-record headers).
96+
.. note::
97+
``CreateMultiple`` and ``UpsertMultiple`` action responses do **not**
98+
return per-record ``OData-EntityId`` headers. Their IDs are in the
99+
JSON response body (``data["Ids"]``). Access them via::
100+
101+
for resp in result.succeeded:
102+
if resp.data and "Ids" in resp.data:
103+
bulk_ids = resp.data["Ids"]
99104
"""
100-
ids: List[str] = []
101-
for r in self.succeeded:
102-
if r.entity_id is not None:
103-
ids.append(r.entity_id)
104-
elif r.data is not None and isinstance(r.data.get("Ids"), list):
105-
ids.extend(i for i in r.data["Ids"] if isinstance(i, str))
106-
return ids
105+
return [r.entity_id for r in self.succeeded if r.entity_id is not None]

tests/unit/data/test_batch_edge_cases.py

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -643,8 +643,12 @@ def test_single_failure_makes_has_errors_true(self):
643643
self.assertTrue(result.has_errors)
644644
self.assertEqual(len(result.failed), 1)
645645

646-
def test_created_ids_from_create_multiple_response_body(self):
647-
"""CreateMultiple returns IDs in data['Ids'], not in entity_id header."""
646+
def test_created_ids_from_create_multiple_not_in_created_ids(self):
647+
"""CreateMultiple IDs are in data['Ids'], NOT in created_ids property.
648+
649+
created_ids only returns entity_id from OData-EntityId headers.
650+
Callers access CreateMultiple IDs via response.data['Ids'] directly.
651+
"""
648652
responses = [
649653
# CreateMultiple response: 200 OK with {"Ids": [...]} body
650654
BatchItemResponse(
@@ -654,44 +658,46 @@ def test_created_ids_from_create_multiple_response_body(self):
654658
),
655659
]
656660
result = BatchResult(responses=responses)
657-
self.assertEqual(result.created_ids, ["guid-1", "guid-2", "guid-3"])
661+
# created_ids does NOT include bulk IDs (no OData-EntityId header)
662+
self.assertEqual(result.created_ids, [])
663+
# Callers access them from the response data
664+
self.assertEqual(result.succeeded[0].data["Ids"], ["guid-1", "guid-2", "guid-3"])
658665

659-
def test_created_ids_combines_header_and_body_ids(self):
660-
"""created_ids includes both OData-EntityId (single create) and Ids array (bulk)."""
666+
def test_created_ids_only_from_odata_entity_id_header(self):
667+
"""created_ids only collects entity_id from OData-EntityId headers."""
661668
responses = [
662669
# Single create: entity_id from header
663670
BatchItemResponse(status_code=204, entity_id="single-id"),
664-
# CreateMultiple: Ids from body
671+
# CreateMultiple: Ids from body (not in created_ids)
665672
BatchItemResponse(
666673
status_code=200,
667674
entity_id=None,
668675
data={"Ids": ["bulk-id-1", "bulk-id-2"]},
669676
),
670677
]
671678
result = BatchResult(responses=responses)
672-
self.assertEqual(result.created_ids, ["single-id", "bulk-id-1", "bulk-id-2"])
679+
# Only the header-based entity_id
680+
self.assertEqual(result.created_ids, ["single-id"])
681+
# Bulk IDs accessed via response.data
682+
self.assertEqual(result.responses[1].data["Ids"], ["bulk-id-1", "bulk-id-2"])
673683

674-
def test_created_ids_ignores_non_string_ids_in_body(self):
675-
"""Non-string values in data['Ids'] are filtered out."""
684+
def test_bulk_ids_accessible_via_response_data(self):
685+
"""Callers iterate responses to access CreateMultiple/UpsertMultiple IDs."""
676686
responses = [
687+
BatchItemResponse(status_code=204, entity_id="id-1"),
677688
BatchItemResponse(
678689
status_code=200,
679-
data={"Ids": ["good-id", 12345, None, "another-id"]},
680-
),
681-
]
682-
result = BatchResult(responses=responses)
683-
self.assertEqual(result.created_ids, ["good-id", "another-id"])
684-
685-
def test_created_ids_skips_failed_create_multiple(self):
686-
"""Failed CreateMultiple responses should not contribute IDs."""
687-
responses = [
688-
BatchItemResponse(
689-
status_code=400,
690-
data={"error": {"code": "0x123", "message": "fail"}},
690+
data={"Ids": ["id-2", "id-3"]},
691691
),
692+
BatchItemResponse(status_code=204), # delete, no entity_id
692693
]
693694
result = BatchResult(responses=responses)
694-
self.assertEqual(result.created_ids, [])
695+
# Collect all IDs from both sources (what a caller would do)
696+
all_ids = list(result.created_ids)
697+
for resp in result.succeeded:
698+
if resp.data and isinstance(resp.data.get("Ids"), list):
699+
all_ids.extend(resp.data["Ids"])
700+
self.assertEqual(all_ids, ["id-1", "id-2", "id-3"])
695701

696702

697703
# ---------------------------------------------------------------------------
@@ -733,8 +739,11 @@ def test_create_multiple_response_parsed(self):
733739
self.assertIsNone(result.succeeded[0].entity_id)
734740
# But data should contain the Ids array
735741
self.assertEqual(result.succeeded[0].data["Ids"], ["aaa-111", "bbb-222", "ccc-333"])
736-
# And created_ids should extract them
737-
self.assertEqual(result.created_ids, ["aaa-111", "bbb-222", "ccc-333"])
742+
# created_ids won't have these (no OData-EntityId header)
743+
self.assertEqual(result.created_ids, [])
744+
# Callers access bulk IDs via response.data["Ids"]
745+
bulk_ids = result.succeeded[0].data["Ids"]
746+
self.assertEqual(len(bulk_ids), 3)
738747

739748
def test_mixed_single_and_bulk_creates(self):
740749
"""Batch with both individual POST create and CreateMultiple."""
@@ -775,8 +784,10 @@ def test_mixed_single_and_bulk_creates(self):
775784

776785
self.assertFalse(result.has_errors)
777786
self.assertEqual(len(result.succeeded), 2)
778-
# All 3 IDs should appear in created_ids
779-
self.assertEqual(result.created_ids, [single_guid, "bulk-1", "bulk-2"])
787+
# created_ids only has the individual create's entity_id
788+
self.assertEqual(result.created_ids, [single_guid])
789+
# CreateMultiple IDs are in the second response's data
790+
self.assertEqual(result.responses[1].data["Ids"], ["bulk-1", "bulk-2"])
780791

781792

782793
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)