Skip to content

Commit e450739

Browse files
authored
fix: allow legacy ndb to read LocalStructuredProperty entities. (#344)
fix: allow legacy ndb to read LocalStructuredProperty entities Legacy ndb is not able to read back serialized entities when using repeated LocalStructuredProperty.
1 parent f1a1189 commit e450739

File tree

3 files changed

+131
-3
lines changed

3 files changed

+131
-3
lines changed

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

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4174,7 +4174,7 @@ def _to_datastore(self, entity, data, prefix="", repeated=False):
41744174
behavior to store everything in a single Datastore entity that uses
41754175
dotted attribute names, rather than nesting entities.
41764176
"""
4177-
# Avoid Python 2.7 circularf import
4177+
# Avoid Python 2.7 circular import
41784178
from google.cloud.ndb import context as context_module
41794179

41804180
context = context_module.get_context()
@@ -4323,6 +4323,40 @@ def _prepare_for_put(self, entity):
43234323
if value is not None:
43244324
value._prepare_for_put()
43254325

4326+
def _to_datastore(self, entity, data, prefix="", repeated=False):
4327+
"""Override of :method:`Property._to_datastore`.
4328+
4329+
Although this property's entities should be stored as serialized
4330+
strings, when stored using old NDB they appear as unserialized
4331+
entities in the datastore. When serialized as strings in this class,
4332+
they can't be read by old NDB either. To avoid these incompatibilities,
4333+
we store them as entities when legacy_data is set to True, which is the
4334+
default behavior.
4335+
"""
4336+
# Avoid Python 2.7 circular import
4337+
from google.cloud.ndb import context as context_module
4338+
4339+
context = context_module.get_context()
4340+
4341+
keys = super(LocalStructuredProperty, self)._to_datastore(
4342+
entity, data, prefix=prefix, repeated=repeated
4343+
)
4344+
4345+
if context.legacy_data:
4346+
values = self._get_user_value(entity)
4347+
if not self._repeated:
4348+
values = [values]
4349+
legacy_values = []
4350+
for value in values:
4351+
legacy_values.append(
4352+
_entity_to_ds_entity(value, set_key=False)
4353+
)
4354+
if not self._repeated:
4355+
legacy_values = legacy_values[0]
4356+
data[self._name] = legacy_values
4357+
4358+
return keys
4359+
43264360

43274361
class GenericProperty(Property):
43284362
"""A Property whose value can be (almost) any basic type.
@@ -5161,7 +5195,7 @@ def _put_async(self, **kwargs):
51615195
tasklets.Future: The eventual result will be the key for the
51625196
entity. This is always a complete key.
51635197
"""
5164-
# Avoid Python 2.7 circularf import
5198+
# Avoid Python 2.7 circular import
51655199
from google.cloud.ndb import context as context_module
51665200
from google.cloud.ndb import _datastore_api
51675201

@@ -5378,7 +5412,7 @@ def _allocate_ids_async(
53785412
tasklets.Future: Eventual result is ``tuple(key.Key)``: Keys for
53795413
the newly allocated IDs.
53805414
"""
5381-
# Avoid Python 2.7 circularf import
5415+
# Avoid Python 2.7 circular import
53825416
from google.cloud.ndb import _datastore_api
53835417

53845418
if max:

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,3 +1216,41 @@ class SomeKind(ndb.Model):
12161216

12171217
ds_entity = ds_client.get(key._key)
12181218
assert zlib.decompress(ds_entity["foo"]) == b"Compress this!"
1219+
1220+
1221+
def test_insert_entity_with_repeated_local_structured_property_legacy_data(
1222+
client_context, dispose_of, ds_client
1223+
):
1224+
"""Regression test for #326
1225+
1226+
https://github.com/googleapis/python-ndb/issues/326
1227+
"""
1228+
1229+
class OtherKind(ndb.Model):
1230+
one = ndb.StringProperty()
1231+
two = ndb.StringProperty()
1232+
1233+
class SomeKind(ndb.Model):
1234+
foo = ndb.IntegerProperty()
1235+
bar = ndb.LocalStructuredProperty(OtherKind, repeated=True)
1236+
1237+
with client_context.new(legacy_data=True).use():
1238+
entity = SomeKind(
1239+
foo=42,
1240+
bar=[
1241+
OtherKind(one="hi", two="mom"),
1242+
OtherKind(one="and", two="dad"),
1243+
],
1244+
)
1245+
key = entity.put()
1246+
dispose_of(key._key)
1247+
1248+
retrieved = key.get()
1249+
assert retrieved.foo == 42
1250+
assert retrieved.bar[0].one == "hi"
1251+
assert retrieved.bar[0].two == "mom"
1252+
assert retrieved.bar[1].one == "and"
1253+
assert retrieved.bar[1].two == "dad"
1254+
1255+
assert isinstance(retrieved.bar[0], OtherKind)
1256+
assert isinstance(retrieved.bar[1], OtherKind)

packages/google-cloud-ndb/tests/unit/test_model.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3670,6 +3670,62 @@ class SomeKind(model.Model):
36703670
entity = SomeKind()
36713671
SomeKind.foo._prepare_for_put(entity) # noop
36723672

3673+
@staticmethod
3674+
@pytest.mark.usefixtures("in_context")
3675+
def test_repeated_local_structured_property():
3676+
class SubKind(model.Model):
3677+
bar = model.Property()
3678+
3679+
class SomeKind(model.Model):
3680+
foo = model.LocalStructuredProperty(
3681+
SubKind, repeated=True, indexed=False
3682+
)
3683+
3684+
entity = SomeKind(foo=[SubKind(bar="baz")])
3685+
data = {}
3686+
protobuf = model._entity_to_protobuf(entity.foo[0], set_key=False)
3687+
protobuf = protobuf.SerializePartialToString()
3688+
assert SomeKind.foo._to_datastore(entity, data, repeated=True) == (
3689+
"foo",
3690+
)
3691+
assert data == {"foo": [[protobuf]]}
3692+
3693+
@staticmethod
3694+
def test_legacy_repeated_local_structured_property(in_context):
3695+
class SubKind(model.Model):
3696+
bar = model.Property()
3697+
3698+
class SomeKind(model.Model):
3699+
foo = model.LocalStructuredProperty(
3700+
SubKind, repeated=True, indexed=False
3701+
)
3702+
3703+
with in_context.new(legacy_data=True).use():
3704+
entity = SomeKind(foo=[SubKind(bar="baz")])
3705+
data = {}
3706+
ds_entity = model._entity_to_ds_entity(
3707+
entity.foo[0], set_key=False
3708+
)
3709+
assert SomeKind.foo._to_datastore(entity, data, repeated=True) == (
3710+
"foo",
3711+
)
3712+
assert data == {"foo": [ds_entity]}
3713+
3714+
@staticmethod
3715+
def test_legacy_non_repeated_local_structured_property(in_context):
3716+
class SubKind(model.Model):
3717+
bar = model.Property()
3718+
3719+
class SomeKind(model.Model):
3720+
foo = model.LocalStructuredProperty(SubKind)
3721+
3722+
with in_context.new(legacy_data=True).use():
3723+
entity = SomeKind(foo=SubKind(bar="baz"))
3724+
data = {}
3725+
assert SomeKind.foo._to_datastore(entity, data) == ("foo",)
3726+
ds_entity = model._entity_to_ds_entity(entity.foo, set_key=False)
3727+
assert data == {"foo": ds_entity}
3728+
36733729

36743730
class TestGenericProperty:
36753731
@staticmethod

0 commit comments

Comments
 (0)