@@ -640,12 +640,18 @@ def _add_refetch_shape(
640640 ref_shape .fields [field_name ] = ptr_info
641641
642642 if obj .__gel_new__ :
643- # New objects should additionally refetch any computed properties
644643 for ptr_name , ptr_info in tp_pointers .items ():
645- if not ptr_info .computed or ptr_info .kind != PointerKind .Property :
646- continue
644+ # New objects should refetch computed properties
645+ if ptr_info .kind == PointerKind .Property and ptr_info .computed :
646+ ref_shape .fields [ptr_name ] = ptr_info
647647
648- ref_shape .fields [ptr_name ] = ptr_info
648+ # New objects should refetch single links
649+ if (
650+ ptr_info .kind == PointerKind .Link
651+ and not ptr_info .cardinality .is_multi ()
652+ and ptr_name != "__type__"
653+ ):
654+ ref_shape .fields [ptr_name ] = ptr_info
649655
650656
651657def push_refetch_new (
@@ -1347,13 +1353,17 @@ def _compile_refetch(
13471353 link_arg_order .append (ptr .name )
13481354 link_num = len (link_arg_order ) - 1
13491355
1356+ link_shape_elems : list [str ] = []
1357+ if not ptr .cardinality .is_multi ():
1358+ link_shape_elems += ["id" ]
13501359 if ptr .properties :
1351- props = "," . join (
1360+ link_shape_elems += [
13521361 f"@{ quote_ident (p )} " for p in ptr .properties
1353- )
1354- props = f"{{ { props } }}"
1355- else :
1356- props = ""
1362+ ]
1363+
1364+ link_shape = ""
1365+ if link_shape_elems :
1366+ link_shape = f"{{ { ',' .join (link_shape_elems )} }}"
13571367
13581368 maybe_assert = maybe_assert_end = ""
13591369
@@ -1365,25 +1375,34 @@ def _compile_refetch(
13651375 if ptr .computed :
13661376 computed_filter = "or .id in __existing"
13671377
1368- # The logic of the `filter` clause is:
1369- # - first test if the link was fetched for this object
1370- # at all - if not - we don't need to update it
1371- # - if the link was fetched for this particular GelModel --
1372- # filter it by all existing objects prior to sync() in
1373- # this link PLUS all new objects that were inserted
1374- # during sync().
1375- select_shape .append (f"""
1376- { quote_ident (ptr .name )} := { maybe_assert } (
1377- with
1378- __link := __obj_data.1[{ link_num } ]
1379- select .{ quote_ident (ptr .name )} { props }
1380- filter __link.0 and (
1381- .id in array_unpack(__link.1)
1382- or .id in __new
1383- { computed_filter }
1384- )
1385- ){ maybe_assert_end }
1386- """ )
1378+ if ptr .cardinality .is_multi ():
1379+ # The logic of the `filter` clause is:
1380+ # - first test if the link was fetched for this object
1381+ # at all - if not - we don't need to update it
1382+ # - if the link was fetched for this particular
1383+ # GelModel -- filter it by all existing objects prior
1384+ # to sync() in this link PLUS all new objects that
1385+ # were inserted during sync().
1386+ select_shape .append (f"""
1387+ { quote_ident (ptr .name )} := { maybe_assert } (
1388+ with
1389+ __link := __obj_data.1[{ link_num } ]
1390+ select .{ quote_ident (ptr .name )} { link_shape }
1391+ filter __link.0 and (
1392+ .id in array_unpack(__link.1)
1393+ or .id in __new
1394+ { computed_filter }
1395+ )
1396+ ){ maybe_assert_end }
1397+ """ )
1398+
1399+ else :
1400+ # Single links should always refetch the link
1401+ select_shape .append (f"""
1402+ { quote_ident (ptr .name )} := { maybe_assert } (
1403+ .{ quote_ident (ptr .name )} { link_shape }
1404+ ){ maybe_assert_end }
1405+ """ )
13871406
13881407 else :
13891408 select_shape .append (quote_ident (ptr .name ))
@@ -1568,6 +1587,8 @@ def _apply_refetched_data_shape(
15681587
15691588 model_or_models : GelModel | IDTracker [GelModel , None ]
15701589 if gel_id in self .new_objects :
1590+ # For new objects, update the data we got back from the batch
1591+ # queries.
15711592 model_or_models = self .new_objects [gel_id ]
15721593 else :
15731594 model_or_models = self .existing_objects [gel_id ]
@@ -1593,66 +1614,86 @@ def _apply_refetched_data_shape(
15931614 continue
15941615
15951616 shape_field = shape .fields [field ]
1596- is_multi = shape_field .cardinality .is_multi ()
1597- is_link = shape_field .kind .is_link ()
15981617
15991618 for model in models :
16001619 assert model .id == gel_id
1601- model_dict = model .__dict__
1602-
1603- model .__pydantic_fields_set__ .add (field )
1604-
1605- if is_link and is_multi :
1606- link = model_dict .get (field )
1607- if link is None :
1608- # This instance never had this link, but
1609- # now it will.
1610- # TODO: this needs to be optimized.
1611- link = copy .copy (new_value )
1612- model_dict [field ] = link
1613- link .__gel_replace_with_empty__ ()
1614-
1615- assert is_link_abstract_dlist (link )
1616- assert type (new_value ) is type (link )
1617- model_dict [field ] = link .__gel_reconcile__ (
1618- # no need to copy `new_value`,
1619- # it will only be iterated over
1620- new_value ,
1621- self .existing_objects , # type: ignore [arg-type]
1622- self .new_objects , # type: ignore [arg-type]
1623- )
1624-
1625- elif is_link :
1626- if new_value is None :
1627- model_dict [field ] = new_value
1628- elif shape_field .properties :
1629- model_dict [field ] = reconcile_proxy_link (
1630- existing = model_dict .get (field ),
1631- refetched = new_value , # pyright: ignore [reportArgumentType]
1632- existing_objects = self .existing_objects , # type: ignore [arg-type]
1633- new_objects = self .new_objects ,
1634- )
1635- else :
1636- model_dict [field ] = reconcile_link (
1637- existing = model_dict .get (field ),
1638- refetched = new_value ,
1639- existing_objects = self .existing_objects , # type: ignore [arg-type]
1640- new_objects = self .new_objects ,
1641- )
16421620
1643- elif not is_link :
1644- # could be a multi prop, could be a single prop
1645- # with an array value
1646- if shape_field .mutable :
1647- model_dict [field ] = deepcopy (new_value )
1648- else :
1649- model_dict [field ] = new_value
1621+ self ._apply_refetched_field_to_model (
1622+ shape_field , new_value , model , deepcopy
1623+ )
16501624
16511625 # Let's be extra cautious and help GC a bit here. `obj` can
16521626 # have recursive references to other objects via links and
16531627 # computed backlinks.
16541628 obj .__dict__ .clear ()
16551629
1630+ def _apply_refetched_field_to_model (
1631+ self ,
1632+ shape_field : GelPointerReflection ,
1633+ new_value : Any ,
1634+ model : GelModel ,
1635+ deepcopy : Callable [[T ], T ],
1636+ ) -> None :
1637+ field = shape_field .name
1638+ is_multi = shape_field .cardinality .is_multi ()
1639+ is_link = shape_field .kind .is_link ()
1640+
1641+ model_dict = model .__dict__
1642+
1643+ model .__pydantic_fields_set__ .add (field )
1644+
1645+ if is_link and is_multi :
1646+ link = model_dict .get (field )
1647+ assert isinstance (new_value , AbstractCollection )
1648+ if link is None :
1649+ # This instance never had this link, but
1650+ # now it will.
1651+ # TODO: this needs to be optimized.
1652+ link = copy .copy (new_value )
1653+ model_dict [field ] = link
1654+ link .__gel_replace_with_empty__ ()
1655+
1656+ assert is_link_abstract_dlist (link )
1657+ assert type (new_value ) is type (link )
1658+ model_dict [field ] = link .__gel_reconcile__ (
1659+ # no need to copy `new_value`,
1660+ # it will only be iterated over
1661+ new_value , # type: ignore [arg-type]
1662+ self .existing_objects , # type: ignore [arg-type]
1663+ self .new_objects , # type: ignore [arg-type]
1664+ )
1665+
1666+ elif is_link :
1667+ existing = model_dict .get (field )
1668+ if (
1669+ new_value is None
1670+ or existing is None
1671+ or existing is DEFAULT_VALUE
1672+ ):
1673+ model_dict [field ] = new_value
1674+ elif shape_field .properties :
1675+ model_dict [field ] = reconcile_proxy_link (
1676+ existing = existing ,
1677+ refetched = new_value , # pyright: ignore [reportArgumentType]
1678+ existing_objects = self .existing_objects , # type: ignore [arg-type]
1679+ new_objects = self .new_objects ,
1680+ )
1681+ else :
1682+ model_dict [field ] = reconcile_link (
1683+ existing = existing ,
1684+ refetched = new_value ,
1685+ existing_objects = self .existing_objects , # type: ignore [arg-type]
1686+ new_objects = self .new_objects ,
1687+ )
1688+
1689+ elif not is_link :
1690+ # could be a multi prop, could be a single prop
1691+ # with an array value
1692+ if shape_field .mutable :
1693+ model_dict [field ] = deepcopy (new_value )
1694+ else :
1695+ model_dict [field ] = new_value
1696+
16561697 def _commit (self ) -> None :
16571698 for obj , new_id in self .new_object_ids .items ():
16581699 assert new_id is not None
@@ -1662,25 +1703,36 @@ def _commit(self) -> None:
16621703 if self .refetch :
16631704 self ._apply_refetched_data ()
16641705
1706+ # Apply refetched data to new objects.
1707+ #
1708+ # There are 3 instances of a newly synced object:
1709+ # - The user instance
1710+ # - The batch instance
1711+ # - The refetch instance
1712+ #
1713+ # In _apply_refetched_data, the batch instance will be updated
1714+ # using data from the refetech instance. Here, we finally update
1715+ # the user's copy.
16651716 for obj , new_id in self .new_object_ids .items ():
16661717 assert new_id is not None
16671718
16681719 updated = self .new_objects [new_id ]
1669- pydantic_set_fields = obj .__pydantic_fields_set__
16701720
16711721 for prop in get_pointers (type (obj )):
16721722 if (
16731723 # id is already set and should never change.
16741724 prop .name == "id"
16751725 # prop not refetched
16761726 or prop .name not in updated .__dict__
1677- # TODO: Refetching links for new objects
1678- or prop .kind == PointerKind .Link
16791727 ):
16801728 continue
16811729
1682- obj .__dict__ [prop .name ] = updated .__dict__ [prop .name ]
1683- pydantic_set_fields .add (prop .name )
1730+ self ._apply_refetched_field_to_model (
1731+ prop ,
1732+ updated .__dict__ [prop .name ],
1733+ obj ,
1734+ _identity_func ,
1735+ )
16841736
16851737 self ._commit_recursive ()
16861738
0 commit comments