Skip to content

Commit 94d2717

Browse files
author
Chris Rossi
authored
fix: fix bug with repeated structured properties with Expando values (#671)
In the legacy data format, the dotted properties stored in Datastore were not properly padded for missing values. Fixes #669
1 parent dbdb8d6 commit 94d2717

File tree

2 files changed

+62
-10
lines changed

2 files changed

+62
-10
lines changed

packages/google-cloud-ndb/google/cloud/ndb/model.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -696,26 +696,29 @@ def _entity_from_protobuf(protobuf):
696696
return _entity_from_ds_entity(ds_entity)
697697

698698

699-
def _properties_of(entity):
700-
"""Get the model properties for an entity.
699+
def _properties_of(*entities):
700+
"""Get the model properties for one or more entities.
701701
702-
After collecting any properties local to the given entity, will traverse the
703-
entity's MRO (class hierarchy) up from the entity's class through all of its
704-
ancestors, collecting an ``Property`` instances defined for those classes.
702+
After collecting any properties local to the given entities, will traverse the
703+
entities' MRO (class hierarchy) up from the entities' class through all of its
704+
ancestors, collecting any ``Property`` instances defined for those classes.
705705
706706
Args:
707-
entity (model.Model): The entity to get properties for.
707+
entities (Tuple[model.Model]): The entities to get properties for. All entities
708+
are expected to be of the same class.
708709
709710
Returns:
710-
Iterator[Property]: Iterator over the entity's properties.
711+
Iterator[Property]: Iterator over the entities' properties.
711712
"""
712713
seen = set()
713714

714-
for level in (entity,) + tuple(type(entity).mro()):
715+
entity_type = type(entities[0]) # assume all entities are same type
716+
for level in entities + tuple(entity_type.mro()):
715717
if not hasattr(level, "_properties"):
716718
continue
717719

718-
for prop in level._properties.values():
720+
level_properties = getattr(level, "_properties", {})
721+
for prop in level_properties.values():
719722
if (
720723
not isinstance(prop, Property)
721724
or isinstance(prop, ModelKey)
@@ -4299,6 +4302,8 @@ def _to_datastore(self, entity, data, prefix="", repeated=False):
42994302
if not self._repeated:
43004303
values = (values,)
43014304

4305+
props = tuple(_properties_of(*values))
4306+
43024307
for value in values:
43034308
if value is None:
43044309
keys.extend(
@@ -4308,7 +4313,7 @@ def _to_datastore(self, entity, data, prefix="", repeated=False):
43084313
)
43094314
continue
43104315

4311-
for prop in _properties_of(value):
4316+
for prop in props:
43124317
keys.extend(
43134318
prop._to_datastore(
43144319
value, data, prefix=next_prefix, repeated=next_repeated

packages/google-cloud-ndb/tests/system/test_crud.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,53 @@ class SomeKind(ndb.Model):
11171117
assert isinstance(retrieved.bar[1], OtherKind)
11181118

11191119

1120+
@pytest.mark.usefixtures("client_context")
1121+
def test_legacy_repeated_structured_property_w_expando(
1122+
ds_client, dispose_of, client_context
1123+
):
1124+
"""Regression test for #669
1125+
1126+
https://github.com/googleapis/python-ndb/issues/669
1127+
"""
1128+
1129+
class OtherKind(ndb.Expando):
1130+
one = ndb.StringProperty()
1131+
1132+
class SomeKind(ndb.Model):
1133+
foo = ndb.IntegerProperty()
1134+
bar = ndb.StructuredProperty(OtherKind, repeated=True)
1135+
1136+
entity = SomeKind(
1137+
foo=42,
1138+
bar=[
1139+
OtherKind(one="one-a"),
1140+
OtherKind(two="two-b"),
1141+
OtherKind(one="one-c", two="two-c"),
1142+
],
1143+
)
1144+
1145+
with client_context.new(legacy_data=True).use():
1146+
key = entity.put()
1147+
dispose_of(key._key)
1148+
1149+
ds_entity = ds_client.get(key._key)
1150+
assert ds_entity["bar.one"] == ["one-a", None, "one-c"]
1151+
assert ds_entity["bar.two"] == [None, "two-b", "two-c"]
1152+
1153+
retrieved = key.get()
1154+
assert retrieved.foo == 42
1155+
assert retrieved.bar[0].one == "one-a"
1156+
assert not hasattr(retrieved.bar[0], "two")
1157+
assert retrieved.bar[1].one is None
1158+
assert retrieved.bar[1].two == "two-b"
1159+
assert retrieved.bar[2].one == "one-c"
1160+
assert retrieved.bar[2].two == "two-c"
1161+
1162+
assert isinstance(retrieved.bar[0], OtherKind)
1163+
assert isinstance(retrieved.bar[1], OtherKind)
1164+
assert isinstance(retrieved.bar[2], OtherKind)
1165+
1166+
11201167
@pytest.mark.usefixtures("client_context")
11211168
def test_insert_expando(dispose_of):
11221169
class SomeKind(ndb.Expando):

0 commit comments

Comments
 (0)