@@ -61,6 +61,7 @@ class BaseXCom(Base, LoggingMixin):
6161
6262 dag_run_id = Column (Integer (), nullable = False , primary_key = True )
6363 task_id = Column (String (ID_LEN , ** COLLATION_ARGS ), nullable = False , primary_key = True )
64+ map_index = Column (Integer , primary_key = True , nullable = False , server_default = "-1" )
6465 key = Column (String (512 , ** COLLATION_ARGS ), nullable = False , primary_key = True )
6566
6667 # Denormalized for easier lookup.
@@ -87,7 +88,7 @@ class BaseXCom(Base, LoggingMixin):
8788 # but it goes over MySQL's index length limit. So we instead create indexes
8889 # separately, and enforce uniqueness with DagRun.id instead.
8990 Index ("idx_xcom_key" , key ),
90- Index ("idx_xcom_ti_id" , dag_id , task_id , run_id ),
91+ Index ("idx_xcom_ti_id" , dag_id , task_id , run_id , map_index ),
9192 )
9293
9394 @reconstructor
@@ -111,6 +112,7 @@ def set(
111112 dag_id : str ,
112113 task_id : str ,
113114 run_id : str ,
115+ map_index : int = - 1 ,
114116 session : Session = NEW_SESSION ,
115117 ) -> None :
116118 """Store an XCom value.
@@ -123,6 +125,8 @@ def set(
123125 :param dag_id: DAG ID.
124126 :param task_id: Task ID.
125127 :param run_id: DAG run ID for the task.
128+ :param map_index: Optional map index to assign XCom for a mapped task.
129+ The default is ``-1`` (set for a non-mapped task).
126130 :param session: Database session. If not given, a new session will be
127131 created for this function.
128132 """
@@ -152,6 +156,7 @@ def set(
152156 session : Session = NEW_SESSION ,
153157 * ,
154158 run_id : Optional [str ] = None ,
159+ map_index : int = - 1 ,
155160 ) -> None :
156161 """:sphinx-autoapi-skip:"""
157162 from airflow .models .dagrun import DagRun
@@ -183,6 +188,7 @@ def set(
183188 task_id = task_id ,
184189 dag_id = dag_id ,
185190 run_id = run_id ,
191+ map_index = map_index ,
186192 )
187193
188194 # Remove duplicate XComs and insert a new one.
@@ -203,13 +209,49 @@ def set(
203209 session .add (new )
204210 session .flush ()
205211
212+ @classmethod
213+ @provide_session
214+ def get_value (
215+ cls ,
216+ * ,
217+ ti_key : "TaskInstanceKey" ,
218+ key : Optional [str ] = None ,
219+ session : Session = NEW_SESSION ,
220+ ) -> Any :
221+ """Retrieve an XCom value for a task instance.
222+
223+ This method returns "full" XCom values (i.e. uses ``deserialize_value``
224+ from the XCom backend). Use :meth:`get_many` if you want the "shortened"
225+ value via ``orm_deserialize_value``.
226+
227+ If there are no results, *None* is returned. If multiple XCom entries
228+ match the criteria, an arbitrary one is returned.
229+
230+ :param ti_key: The TaskInstanceKey to look up the XCom for.
231+ :param key: A key for the XCom. If provided, only XCom with matching
232+ keys will be returned. Pass *None* (default) to remove the filter.
233+ :param session: Database session. If not given, a new session will be
234+ created for this function.
235+ """
236+ return cls .get_one (
237+ key = key ,
238+ task_id = ti_key .task_id ,
239+ dag_id = ti_key .dag_id ,
240+ run_id = ti_key .run_id ,
241+ map_index = ti_key .map_index ,
242+ session = session ,
243+ )
244+
206245 @overload
207246 @classmethod
208247 def get_one (
209248 cls ,
210249 * ,
211250 key : Optional [str ] = None ,
212- ti_key : "TaskInstanceKey" ,
251+ dag_id : Optional [str ] = None ,
252+ task_id : Optional [str ] = None ,
253+ run_id : Optional [str ] = None ,
254+ map_index : Optional [int ] = None ,
213255 session : Session = NEW_SESSION ,
214256 ) -> Optional [Any ]:
215257 """Retrieve an XCom value, optionally meeting certain criteria.
@@ -218,12 +260,22 @@ def get_one(
218260 from the XCom backend). Use :meth:`get_many` if you want the "shortened"
219261 value via ``orm_deserialize_value``.
220262
221- If there are no results, *None* is returned.
263+ If there are no results, *None* is returned. If multiple XCom entries
264+ match the criteria, an arbitrary one is returned.
222265
223266 A deprecated form of this function accepts ``execution_date`` instead of
224267 ``run_id``. The two arguments are mutually exclusive.
225268
226- :param ti_key: The TaskInstanceKey to look up the XCom for
269+ .. seealso:: ``get_value()`` is a convenience function if you already
270+ have a structured TaskInstance or TaskInstanceKey object available.
271+
272+ :param run_id: DAG run ID for the task.
273+ :param dag_id: Only pull XCom from this DAG. Pass *None* (default) to
274+ remove the filter.
275+ :param task_id: Only XCom from task with matching ID will be pulled.
276+ Pass *None* (default) to remove the filter.
277+ :param map_index: Only XCom from task with matching ID will be pulled.
278+ Pass *None* (default) to remove the filter.
227279 :param key: A key for the XCom. If provided, only XCom with matching
228280 keys will be returned. Pass *None* (default) to remove the filter.
229281 :param include_prior_dates: If *False* (default), only XCom from the
@@ -233,19 +285,6 @@ def get_one(
233285 created for this function.
234286 """
235287
236- @overload
237- @classmethod
238- def get_one (
239- cls ,
240- * ,
241- key : Optional [str ] = None ,
242- task_id : str ,
243- dag_id : str ,
244- run_id : str ,
245- session : Session = NEW_SESSION ,
246- ) -> Optional [Any ]:
247- ...
248-
249288 @overload
250289 @classmethod
251290 def get_one (
@@ -271,27 +310,19 @@ def get_one(
271310 session : Session = NEW_SESSION ,
272311 * ,
273312 run_id : Optional [str ] = None ,
274- ti_key : Optional ["TaskInstanceKey" ] = None ,
313+ map_index : Optional [int ] = None ,
275314 ) -> Optional [Any ]:
276315 """:sphinx-autoapi-skip:"""
277- if not exactly_one (execution_date is not None , ti_key is not None , run_id is not None ):
316+ if not exactly_one (execution_date is not None , run_id is not None ):
278317 raise ValueError ("Exactly one of ti_key, run_id, or execution_date must be passed" )
279318
280- if ti_key is not None :
281- query = session .query (cls ).filter_by (
282- dag_id = ti_key .dag_id ,
283- run_id = ti_key .run_id ,
284- task_id = ti_key .task_id ,
285- )
286- if key :
287- query = query .filter_by (key = key )
288- query = query .limit (1 )
289- elif run_id :
319+ if run_id :
290320 query = cls .get_many (
291321 run_id = run_id ,
292322 key = key ,
293323 task_ids = task_id ,
294324 dag_ids = dag_id ,
325+ map_indexes = map_index ,
295326 include_prior_dates = include_prior_dates ,
296327 limit = 1 ,
297328 session = session ,
@@ -307,6 +338,7 @@ def get_one(
307338 key = key ,
308339 task_ids = task_id ,
309340 dag_ids = dag_id ,
341+ map_indexes = map_index ,
310342 include_prior_dates = include_prior_dates ,
311343 limit = 1 ,
312344 session = session ,
@@ -328,6 +360,7 @@ def get_many(
328360 key : Optional [str ] = None ,
329361 task_ids : Union [str , Iterable [str ], None ] = None ,
330362 dag_ids : Union [str , Iterable [str ], None ] = None ,
363+ map_indexes : Union [int , Iterable [int ], None ] = None ,
331364 include_prior_dates : bool = False ,
332365 limit : Optional [int ] = None ,
333366 session : Session = NEW_SESSION ,
@@ -347,6 +380,8 @@ def get_many(
347380 Pass *None* (default) to remove the filter.
348381 :param dag_id: Only pulls XComs from this DAG. If *None* (default), the
349382 DAG of the calling task is used.
383+ :param map_index: Only XComs from matching map indexes will be pulled.
384+ Pass *None* (default) to remove the filter.
350385 :param include_prior_dates: If *False* (default), only XComs from the
351386 specified DAG run are returned. If *True*, all matching XComs are
352387 returned regardless of the run it belongs to.
@@ -362,6 +397,7 @@ def get_many(
362397 key : Optional [str ] = None ,
363398 task_ids : Union [str , Iterable [str ], None ] = None ,
364399 dag_ids : Union [str , Iterable [str ], None ] = None ,
400+ map_indexes : Union [int , Iterable [int ], None ] = None ,
365401 include_prior_dates : bool = False ,
366402 limit : Optional [int ] = None ,
367403 session : Session = NEW_SESSION ,
@@ -376,6 +412,7 @@ def get_many(
376412 key : Optional [str ] = None ,
377413 task_ids : Optional [Union [str , Iterable [str ]]] = None ,
378414 dag_ids : Optional [Union [str , Iterable [str ]]] = None ,
415+ map_indexes : Union [int , Iterable [int ], None ] = None ,
379416 include_prior_dates : bool = False ,
380417 limit : Optional [int ] = None ,
381418 session : Session = NEW_SESSION ,
@@ -406,6 +443,11 @@ def get_many(
406443 elif dag_ids is not None :
407444 query = query .filter (cls .dag_id == dag_ids )
408445
446+ if is_container (map_indexes ):
447+ query = query .filter (cls .map_index .in_ (map_indexes ))
448+ elif map_indexes is not None :
449+ query = query .filter (cls .map_index == map_indexes )
450+
409451 if include_prior_dates :
410452 if execution_date is not None :
411453 query = query .filter (DagRun .execution_date <= execution_date )
@@ -438,7 +480,15 @@ def delete(cls, xcoms: Union["XCom", Iterable["XCom"]], session: Session) -> Non
438480
439481 @overload
440482 @classmethod
441- def clear (cls , * , dag_id : str , task_id : str , run_id : str , session : Optional [Session ] = None ) -> None :
483+ def clear (
484+ cls ,
485+ * ,
486+ dag_id : str ,
487+ task_id : str ,
488+ run_id : str ,
489+ map_index : Optional [int ] = None ,
490+ session : Session = NEW_SESSION ,
491+ ) -> None :
442492 """Clear all XCom data from the database for the given task instance.
443493
444494 A deprecated form of this function accepts ``execution_date`` instead of
@@ -447,6 +497,8 @@ def clear(cls, *, dag_id: str, task_id: str, run_id: str, session: Optional[Sess
447497 :param dag_id: ID of DAG to clear the XCom for.
448498 :param task_id: ID of task to clear the XCom for.
449499 :param run_id: ID of DAG run to clear the XCom for.
500+ :param map_index: If given, only clear XCom from this particular mapped
501+ task. The default ``None`` clears *all* XComs from the task.
450502 :param session: Database session. If not given, a new session will be
451503 created for this function.
452504 """
@@ -472,6 +524,7 @@ def clear(
472524 session : Session = NEW_SESSION ,
473525 * ,
474526 run_id : Optional [str ] = None ,
527+ map_index : Optional [int ] = None ,
475528 ) -> None :
476529 """:sphinx-autoapi-skip:"""
477530 from airflow .models import DagRun
@@ -495,17 +548,20 @@ def clear(
495548 .scalar ()
496549 )
497550
498- return session .query (cls ).filter_by (dag_id = dag_id , task_id = task_id , run_id = run_id ).delete ()
551+ query = session .query (cls ).filter_by (dag_id = dag_id , task_id = task_id , run_id = run_id )
552+ if map_index is not None :
553+ query = query .filter_by (map_index = map_index )
554+ query .delete ()
499555
500556 @staticmethod
501557 def serialize_value (
502558 value : Any ,
503559 * ,
504- key = None ,
505- task_id = None ,
506- dag_id = None ,
507- run_id = None ,
508- mapping_index : int = - 1 ,
560+ key : Optional [ str ] = None ,
561+ task_id : Optional [ str ] = None ,
562+ dag_id : Optional [ str ] = None ,
563+ run_id : Optional [ str ] = None ,
564+ map_index : Optional [ int ] = None ,
509565 ):
510566 """Serialize XCom value to str or pickled object"""
511567 if conf .getboolean ('core' , 'enable_xcom_pickling' ):
@@ -549,13 +605,14 @@ def orm_deserialize_value(self) -> Any:
549605 return BaseXCom .deserialize_value (self )
550606
551607
552- def _patch_outdated_serializer (clazz , params ):
553- """
554- Previously XCom.serialize_value only accepted one argument ``value``. In order to give
555- custom XCom backends more flexibility with how they store values we now forward to
556- ``XCom.serialize_value`` all params passed to ``XCom.set``. In order to maintain
557- compatibility with XCom backends written with the old signature we check the signature
558- and if necessary we patch with a method that ignores kwargs the backend does not accept.
608+ def _patch_outdated_serializer (clazz : Type [BaseXCom ], params : Iterable [str ]) -> None :
609+ """Patch a custom ``serialize_value`` to accept the modern signature.
610+
611+ To give custom XCom backends more flexibility with how they store values, we
612+ now forward all params passed to ``XCom.set`` to ``XCom.serialize_value``.
613+ In order to maintain compatibility with custom XCom backends written with
614+ the old signature, we check the signature and, if necessary, patch with a
615+ method that ignores kwargs the backend does not accept.
559616 """
560617 old_serializer = clazz .serialize_value
561618
@@ -570,7 +627,7 @@ def _shim(**kwargs):
570627 )
571628 return old_serializer (** kwargs )
572629
573- clazz .serialize_value = _shim
630+ clazz .serialize_value = _shim # type: ignore[assignment]
574631
575632
576633def _get_function_params (function ) -> List [str ]:
0 commit comments