Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f8d2bee

Browse files
author
Chris Rossi
authored
Backwards compatibility with older style structured properties. (#126)
When implementing structured properties the first time, I just used Datastore's native embedded entity functionality, not realizing that NDB had originally used dotted property names instead. (Probably GAE Datastore didn't have embedded entities when NDB was originally written.) The problem is that users migrating from GAE NDB can't load entities with structured properties from their existing datastore. This PR makes NDB backwards compatible with older, dotted name style structured properties so that existing repositories still work with the new NDB. Fixes #122.
1 parent 79d9b5d commit f8d2bee

File tree

6 files changed

+357
-7
lines changed

6 files changed

+357
-7
lines changed

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@ def _entity_from_ds_entity(ds_entity, model_class=None):
514514
515515
Args:
516516
ds_entity (google.cloud.datastore_v1.types.Entity): An entity to be
517-
deserialized.
517+
deserialized.
518518
519519
Returns:
520520
.Model: The deserialized entity.
@@ -523,8 +523,47 @@ def _entity_from_ds_entity(ds_entity, model_class=None):
523523
entity = model_class()
524524
if ds_entity.key:
525525
entity._key = key_module.Key._from_ds_key(ds_entity.key)
526+
526527
for name, value in ds_entity.items():
527528
prop = getattr(model_class, name, None)
529+
530+
# Backwards compatibility shim. NDB previously stored structured
531+
# properties as sets of dotted name properties. Datastore now has
532+
# native support for embedded entities and NDB now uses that, by
533+
# default. This handles the case of reading structured properties from
534+
# older NDB datastore instances.
535+
if prop is None and "." in name:
536+
supername, subname = name.split(".", 1)
537+
structprop = getattr(model_class, supername, None)
538+
if isinstance(structprop, StructuredProperty):
539+
subvalue = value
540+
value = structprop._get_base_value(entity)
541+
if value in (None, []): # empty list for repeated props
542+
kind = structprop._model_class._get_kind()
543+
key = key_module.Key(kind, None)
544+
if structprop._repeated:
545+
value = [
546+
_BaseValue(entity_module.Entity(key._key))
547+
for _ in subvalue
548+
]
549+
else:
550+
value = entity_module.Entity(key._key)
551+
value = _BaseValue(value)
552+
553+
structprop._store_value(entity, value)
554+
555+
if structprop._repeated:
556+
# Branch coverage bug,
557+
# See: https://github.com/nedbat/coveragepy/issues/817
558+
for subentity, subsubvalue in zip( # pragma no branch
559+
value, subvalue
560+
):
561+
subentity.b_val.update({subname: subsubvalue})
562+
else:
563+
value.b_val.update({subname: subvalue})
564+
565+
continue
566+
528567
if not (prop is not None and isinstance(prop, Property)):
529568
if value is not None and isinstance( # pragma: no branch
530569
entity, Expando
@@ -538,6 +577,7 @@ def _entity_from_ds_entity(ds_entity, model_class=None):
538577
value = _BaseValue(value)
539578
setattr(entity, name, value)
540579
continue
580+
541581
if value is not None:
542582
if prop._repeated:
543583
value = [
@@ -546,6 +586,7 @@ def _entity_from_ds_entity(ds_entity, model_class=None):
546586
]
547587
else:
548588
value = _BaseValue(value)
589+
549590
prop._store_value(entity, value)
550591

551592
return entity

packages/google-cloud-ndb/src/google/cloud/ndb/query.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -242,12 +242,41 @@ def __init__(self, name, match_keys, entity_pb):
242242
self.match_values = [entity_pb.properties[key] for key in match_keys]
243243

244244
def __call__(self, entity_pb):
245-
subentities = entity_pb.properties.get(self.name).array_value.values
246-
for subentity in subentities:
247-
properties = subentity.entity_value.properties
248-
values = [properties.get(key) for key in self.match_keys]
249-
if values == self.match_values:
250-
return True
245+
prop_pb = entity_pb.properties.get(self.name)
246+
if prop_pb:
247+
subentities = prop_pb.array_value.values
248+
for subentity in subentities:
249+
properties = subentity.entity_value.properties
250+
values = [properties.get(key) for key in self.match_keys]
251+
if values == self.match_values:
252+
return True
253+
254+
else:
255+
# Backwards compatibility. Legacy NDB, rather than using
256+
# Datastore's ability to embed subentities natively, used dotted
257+
# property names.
258+
prefix = self.name + "."
259+
subentities = ()
260+
for prop_name, prop_pb in entity_pb.properties.items():
261+
if not prop_name.startswith(prefix):
262+
continue
263+
264+
subprop_name = prop_name.split(".", 1)[1]
265+
if not subentities:
266+
subentities = [
267+
{subprop_name: value}
268+
for value in prop_pb.array_value.values
269+
]
270+
else:
271+
for subentity, value in zip(
272+
subentities, prop_pb.array_value.values
273+
):
274+
subentity[subprop_name] = value
275+
276+
for subentity in subentities:
277+
values = [subentity.get(key) for key in self.match_keys]
278+
if values == self.match_values:
279+
return True
251280

252281
return False
253282

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,59 @@ class SomeKind(ndb.Model):
417417
dispose_of(key._key)
418418

419419

420+
@pytest.mark.usefixtures("client_context")
421+
def test_retrieve_entity_with_legacy_structured_property(ds_entity):
422+
class OtherKind(ndb.Model):
423+
one = ndb.StringProperty()
424+
two = ndb.StringProperty()
425+
426+
class SomeKind(ndb.Model):
427+
foo = ndb.IntegerProperty()
428+
bar = ndb.StructuredProperty(OtherKind)
429+
430+
entity_id = test_utils.system.unique_resource_id()
431+
ds_entity(
432+
KIND, entity_id, **{"foo": 42, "bar.one": "hi", "bar.two": "mom"}
433+
)
434+
435+
key = ndb.Key(KIND, entity_id)
436+
retrieved = key.get()
437+
assert retrieved.foo == 42
438+
assert retrieved.bar.one == "hi"
439+
assert retrieved.bar.two == "mom"
440+
441+
assert isinstance(retrieved.bar, OtherKind)
442+
443+
444+
@pytest.mark.usefixtures("client_context")
445+
def test_retrieve_entity_with_legacy_repeated_structured_property(ds_entity):
446+
class OtherKind(ndb.Model):
447+
one = ndb.StringProperty()
448+
two = ndb.StringProperty()
449+
450+
class SomeKind(ndb.Model):
451+
foo = ndb.IntegerProperty()
452+
bar = ndb.StructuredProperty(OtherKind, repeated=True)
453+
454+
entity_id = test_utils.system.unique_resource_id()
455+
ds_entity(
456+
KIND,
457+
entity_id,
458+
**{"foo": 42, "bar.one": ["hi", "hello"], "bar.two": ["mom", "dad"]}
459+
)
460+
461+
key = ndb.Key(KIND, entity_id)
462+
retrieved = key.get()
463+
assert retrieved.foo == 42
464+
assert retrieved.bar[0].one == "hi"
465+
assert retrieved.bar[0].two == "mom"
466+
assert retrieved.bar[1].one == "hello"
467+
assert retrieved.bar[1].two == "dad"
468+
469+
assert isinstance(retrieved.bar[0], OtherKind)
470+
assert isinstance(retrieved.bar[1], OtherKind)
471+
472+
420473
@pytest.mark.usefixtures("client_context")
421474
def test_insert_expando(dispose_of):
422475
class SomeKind(ndb.Expando):

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

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,58 @@ def make_entities():
603603
assert results[1].foo == 2
604604

605605

606+
@pytest.mark.skip("Requires an index")
607+
@pytest.mark.usefixtures("client_context")
608+
def test_query_legacy_structured_property(ds_entity):
609+
class OtherKind(ndb.Model):
610+
one = ndb.StringProperty()
611+
two = ndb.StringProperty()
612+
three = ndb.StringProperty()
613+
614+
class SomeKind(ndb.Model):
615+
foo = ndb.IntegerProperty()
616+
bar = ndb.StructuredProperty(OtherKind)
617+
618+
entity_id = test_utils.system.unique_resource_id()
619+
ds_entity(
620+
KIND,
621+
entity_id,
622+
**{"foo": 1, "bar.one": "pish", "bar.two": "posh", "bar.three": "pash"}
623+
)
624+
625+
entity_id = test_utils.system.unique_resource_id()
626+
ds_entity(
627+
KIND,
628+
entity_id,
629+
**{"foo": 2, "bar.one": "pish", "bar.two": "posh", "bar.three": "push"}
630+
)
631+
632+
entity_id = test_utils.system.unique_resource_id()
633+
ds_entity(
634+
KIND,
635+
entity_id,
636+
**{
637+
"foo": 3,
638+
"bar.one": "pish",
639+
"bar.two": "moppish",
640+
"bar.three": "pass the peas",
641+
}
642+
)
643+
644+
eventually(SomeKind.query().fetch, _length_equals(3))
645+
646+
query = (
647+
SomeKind.query()
648+
.filter(SomeKind.bar.one == "pish", SomeKind.bar.two == "posh")
649+
.order(SomeKind.foo)
650+
)
651+
652+
results = query.fetch()
653+
assert len(results) == 2
654+
assert results[0].foo == 1
655+
assert results[1].foo == 2
656+
657+
606658
@pytest.mark.skip("Requires an index")
607659
@pytest.mark.usefixtures("client_context")
608660
def test_query_repeated_structured_property_with_properties(dispose_of):
@@ -723,3 +775,67 @@ def make_entities():
723775
results = query.fetch()
724776
assert len(results) == 1
725777
assert results[0].foo == 1
778+
779+
780+
@pytest.mark.skip("Requires an index")
781+
@pytest.mark.usefixtures("client_context")
782+
def test_query_legacy_repeated_structured_property(ds_entity):
783+
class OtherKind(ndb.Model):
784+
one = ndb.StringProperty()
785+
two = ndb.StringProperty()
786+
three = ndb.StringProperty()
787+
788+
class SomeKind(ndb.Model):
789+
foo = ndb.IntegerProperty()
790+
bar = ndb.StructuredProperty(OtherKind, repeated=True)
791+
792+
entity_id = test_utils.system.unique_resource_id()
793+
ds_entity(
794+
KIND,
795+
entity_id,
796+
**{
797+
"foo": 1,
798+
"bar.one": ["pish", "bish"],
799+
"bar.two": ["posh", "bosh"],
800+
"bar.three": ["pash", "bash"],
801+
}
802+
)
803+
804+
entity_id = test_utils.system.unique_resource_id()
805+
ds_entity(
806+
KIND,
807+
entity_id,
808+
**{
809+
"foo": 2,
810+
"bar.one": ["bish", "pish"],
811+
"bar.two": ["bosh", "posh"],
812+
"bar.three": ["bass", "pass"],
813+
}
814+
)
815+
816+
entity_id = test_utils.system.unique_resource_id()
817+
ds_entity(
818+
KIND,
819+
entity_id,
820+
**{
821+
"foo": 3,
822+
"bar.one": ["pish", "bish"],
823+
"bar.two": ["fosh", "posh"],
824+
"bar.three": ["fash", "bash"],
825+
}
826+
)
827+
828+
eventually(SomeKind.query().fetch, _length_equals(3))
829+
830+
query = (
831+
SomeKind.query()
832+
.filter(
833+
SomeKind.bar == OtherKind(one="pish", two="posh"),
834+
SomeKind.bar == OtherKind(two="posh", three="pash"),
835+
)
836+
.order(SomeKind.foo)
837+
)
838+
839+
results = query.fetch()
840+
assert len(results) == 1
841+
assert results[0].foo == 1

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4351,6 +4351,66 @@ class ThisKind(model.Model):
43514351
assert entity._key.kind() == "ThisKind"
43524352
assert entity._key.id() == 123
43534353

4354+
@staticmethod
4355+
@pytest.mark.usefixtures("in_context")
4356+
def test_legacy_structured_property():
4357+
class OtherKind(model.Model):
4358+
foo = model.IntegerProperty()
4359+
bar = model.StringProperty()
4360+
4361+
class ThisKind(model.Model):
4362+
baz = model.StructuredProperty(OtherKind)
4363+
copacetic = model.BooleanProperty()
4364+
4365+
key = datastore.Key("ThisKind", 123, project="testing")
4366+
datastore_entity = datastore.Entity(key=key)
4367+
datastore_entity.update(
4368+
{
4369+
"baz.foo": 42,
4370+
"baz.bar": "himom",
4371+
"copacetic": True,
4372+
"super.fluous": "whocares?",
4373+
}
4374+
)
4375+
protobuf = helpers.entity_to_protobuf(datastore_entity)
4376+
entity = model._entity_from_protobuf(protobuf)
4377+
assert isinstance(entity, ThisKind)
4378+
assert entity.baz.foo == 42
4379+
assert entity.baz.bar == "himom"
4380+
assert entity.copacetic is True
4381+
4382+
assert not hasattr(entity, "super")
4383+
assert not hasattr(entity, "super.fluous")
4384+
4385+
@staticmethod
4386+
@pytest.mark.usefixtures("in_context")
4387+
def test_legacy_repeated_structured_property():
4388+
class OtherKind(model.Model):
4389+
foo = model.IntegerProperty()
4390+
bar = model.StringProperty()
4391+
4392+
class ThisKind(model.Model):
4393+
baz = model.StructuredProperty(OtherKind, repeated=True)
4394+
copacetic = model.BooleanProperty()
4395+
4396+
key = datastore.Key("ThisKind", 123, project="testing")
4397+
datastore_entity = datastore.Entity(key=key)
4398+
datastore_entity.update(
4399+
{
4400+
"baz.foo": [42, 144],
4401+
"baz.bar": ["himom", "hellodad"],
4402+
"copacetic": True,
4403+
}
4404+
)
4405+
protobuf = helpers.entity_to_protobuf(datastore_entity)
4406+
entity = model._entity_from_protobuf(protobuf)
4407+
assert isinstance(entity, ThisKind)
4408+
assert entity.baz[0].foo == 42
4409+
assert entity.baz[0].bar == "himom"
4410+
assert entity.baz[1].foo == 144
4411+
assert entity.baz[1].bar == "hellodad"
4412+
assert entity.copacetic is True
4413+
43544414

43554415
class Test_entity_to_protobuf:
43564416
@staticmethod

0 commit comments

Comments
 (0)