Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backwards compatibility with older style structured properties. #126

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion src/google/cloud/ndb/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ def _entity_from_ds_entity(ds_entity, model_class=None):
Args:
ds_entity (google.cloud.datastore_v1.types.Entity): An entity to be
deserialized.
deserialized.
Returns:
.Model: The deserialized entity.
Expand All @@ -523,8 +523,47 @@ def _entity_from_ds_entity(ds_entity, model_class=None):
entity = model_class()
if ds_entity.key:
entity._key = key_module.Key._from_ds_key(ds_entity.key)

for name, value in ds_entity.items():
prop = getattr(model_class, name, None)

# Backwards compatibility shim. NDB previously stored structured
# properties as sets of dotted name properties. Datastore now has
# native support for embedded entities and NDB now uses that, by
# default. This handles the case of reading structured properties from
# older NDB datastore instances.
if prop is None and "." in name:
supername, subname = name.split(".", 1)
structprop = getattr(model_class, supername, None)
if isinstance(structprop, StructuredProperty):
subvalue = value
value = structprop._get_base_value(entity)
if value in (None, []): # empty list for repeated props
kind = structprop._model_class._get_kind()
key = key_module.Key(kind, None)
if structprop._repeated:
value = [
_BaseValue(entity_module.Entity(key._key))
for _ in subvalue
]
else:
value = entity_module.Entity(key._key)
value = _BaseValue(value)

structprop._store_value(entity, value)

if structprop._repeated:
# Branch coverage bug,
# See: https://github.com/nedbat/coveragepy/issues/817
for subentity, subsubvalue in zip( # pragma no branch
value, subvalue
):
subentity.b_val.update({subname: subsubvalue})
else:
value.b_val.update({subname: subvalue})

continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had trouble with coverage in continue statements before. Seems like the python optimizer just skips that and goes to the start of the loop directly, so the line is never reached. Solutions are: 1) Use "# pragma: no branch". 2) Turn off the optimizer. I have gone with 1) when I encounter this problem.


if not (prop is not None and isinstance(prop, Property)):
if value is not None and isinstance( # pragma: no branch
entity, Expando
Expand All @@ -538,6 +577,7 @@ def _entity_from_ds_entity(ds_entity, model_class=None):
value = _BaseValue(value)
setattr(entity, name, value)
continue

if value is not None:
if prop._repeated:
value = [
Expand All @@ -546,6 +586,7 @@ def _entity_from_ds_entity(ds_entity, model_class=None):
]
else:
value = _BaseValue(value)

prop._store_value(entity, value)

return entity
Expand Down
41 changes: 35 additions & 6 deletions src/google/cloud/ndb/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,41 @@ def __init__(self, name, match_keys, entity_pb):
self.match_values = [entity_pb.properties[key] for key in match_keys]

def __call__(self, entity_pb):
subentities = entity_pb.properties.get(self.name).array_value.values
for subentity in subentities:
properties = subentity.entity_value.properties
values = [properties.get(key) for key in self.match_keys]
if values == self.match_values:
return True
prop_pb = entity_pb.properties.get(self.name)
if prop_pb:
subentities = prop_pb.array_value.values
for subentity in subentities:
properties = subentity.entity_value.properties
values = [properties.get(key) for key in self.match_keys]
if values == self.match_values:
return True

else:
# Backwards compatibility. Legacy NDB, rather than using
# Datastore's ability to embed subentities natively, used dotted
# property names.
prefix = self.name + "."
subentities = ()
for prop_name, prop_pb in entity_pb.properties.items():
if not prop_name.startswith(prefix):
continue

subprop_name = prop_name.split(".", 1)[1]
if not subentities:
subentities = [
{subprop_name: value}
for value in prop_pb.array_value.values
]
else:
for subentity, value in zip(
subentities, prop_pb.array_value.values
):
subentity[subprop_name] = value

for subentity in subentities:
values = [subentity.get(key) for key in self.match_keys]
if values == self.match_values:
return True

return False

Expand Down
53 changes: 53 additions & 0 deletions tests/system/test_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,59 @@ class SomeKind(ndb.Model):
dispose_of(key._key)


@pytest.mark.usefixtures("client_context")
def test_retrieve_entity_with_legacy_structured_property(ds_entity):
class OtherKind(ndb.Model):
one = ndb.StringProperty()
two = ndb.StringProperty()

class SomeKind(ndb.Model):
foo = ndb.IntegerProperty()
bar = ndb.StructuredProperty(OtherKind)

entity_id = test_utils.system.unique_resource_id()
ds_entity(
KIND, entity_id, **{"foo": 42, "bar.one": "hi", "bar.two": "mom"}
)

key = ndb.Key(KIND, entity_id)
retrieved = key.get()
assert retrieved.foo == 42
assert retrieved.bar.one == "hi"
assert retrieved.bar.two == "mom"

assert isinstance(retrieved.bar, OtherKind)


@pytest.mark.usefixtures("client_context")
def test_retrieve_entity_with_legacy_repeated_structured_property(ds_entity):
class OtherKind(ndb.Model):
one = ndb.StringProperty()
two = ndb.StringProperty()

class SomeKind(ndb.Model):
foo = ndb.IntegerProperty()
bar = ndb.StructuredProperty(OtherKind, repeated=True)

entity_id = test_utils.system.unique_resource_id()
ds_entity(
KIND,
entity_id,
**{"foo": 42, "bar.one": ["hi", "hello"], "bar.two": ["mom", "dad"]}
)

key = ndb.Key(KIND, entity_id)
retrieved = key.get()
assert retrieved.foo == 42
assert retrieved.bar[0].one == "hi"
assert retrieved.bar[0].two == "mom"
assert retrieved.bar[1].one == "hello"
assert retrieved.bar[1].two == "dad"

assert isinstance(retrieved.bar[0], OtherKind)
assert isinstance(retrieved.bar[1], OtherKind)


@pytest.mark.usefixtures("client_context")
def test_insert_expando(dispose_of):
class SomeKind(ndb.Expando):
Expand Down
116 changes: 116 additions & 0 deletions tests/system/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,58 @@ def make_entities():
assert results[1].foo == 2


@pytest.mark.skip("Requires an index")
@pytest.mark.usefixtures("client_context")
def test_query_legacy_structured_property(ds_entity):
class OtherKind(ndb.Model):
one = ndb.StringProperty()
two = ndb.StringProperty()
three = ndb.StringProperty()

class SomeKind(ndb.Model):
foo = ndb.IntegerProperty()
bar = ndb.StructuredProperty(OtherKind)

entity_id = test_utils.system.unique_resource_id()
ds_entity(
KIND,
entity_id,
**{"foo": 1, "bar.one": "pish", "bar.two": "posh", "bar.three": "pash"}
)

entity_id = test_utils.system.unique_resource_id()
ds_entity(
KIND,
entity_id,
**{"foo": 2, "bar.one": "pish", "bar.two": "posh", "bar.three": "push"}
)

entity_id = test_utils.system.unique_resource_id()
ds_entity(
KIND,
entity_id,
**{
"foo": 3,
"bar.one": "pish",
"bar.two": "moppish",
"bar.three": "pass the peas",
}
)

eventually(SomeKind.query().fetch, _length_equals(3))

query = (
SomeKind.query()
.filter(SomeKind.bar.one == "pish", SomeKind.bar.two == "posh")
.order(SomeKind.foo)
)

results = query.fetch()
assert len(results) == 2
assert results[0].foo == 1
assert results[1].foo == 2


@pytest.mark.skip("Requires an index")
@pytest.mark.usefixtures("client_context")
def test_query_repeated_structured_property_with_properties(dispose_of):
Expand Down Expand Up @@ -723,3 +775,67 @@ def make_entities():
results = query.fetch()
assert len(results) == 1
assert results[0].foo == 1


@pytest.mark.skip("Requires an index")
@pytest.mark.usefixtures("client_context")
def test_query_legacy_repeated_structured_property(ds_entity):
class OtherKind(ndb.Model):
one = ndb.StringProperty()
two = ndb.StringProperty()
three = ndb.StringProperty()

class SomeKind(ndb.Model):
foo = ndb.IntegerProperty()
bar = ndb.StructuredProperty(OtherKind, repeated=True)

entity_id = test_utils.system.unique_resource_id()
ds_entity(
KIND,
entity_id,
**{
"foo": 1,
"bar.one": ["pish", "bish"],
"bar.two": ["posh", "bosh"],
"bar.three": ["pash", "bash"],
}
)

entity_id = test_utils.system.unique_resource_id()
ds_entity(
KIND,
entity_id,
**{
"foo": 2,
"bar.one": ["bish", "pish"],
"bar.two": ["bosh", "posh"],
"bar.three": ["bass", "pass"],
}
)

entity_id = test_utils.system.unique_resource_id()
ds_entity(
KIND,
entity_id,
**{
"foo": 3,
"bar.one": ["pish", "bish"],
"bar.two": ["fosh", "posh"],
"bar.three": ["fash", "bash"],
}
)

eventually(SomeKind.query().fetch, _length_equals(3))

query = (
SomeKind.query()
.filter(
SomeKind.bar == OtherKind(one="pish", two="posh"),
SomeKind.bar == OtherKind(two="posh", three="pash"),
)
.order(SomeKind.foo)
)

results = query.fetch()
assert len(results) == 1
assert results[0].foo == 1
60 changes: 60 additions & 0 deletions tests/unit/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -4351,6 +4351,66 @@ class ThisKind(model.Model):
assert entity._key.kind() == "ThisKind"
assert entity._key.id() == 123

@staticmethod
@pytest.mark.usefixtures("in_context")
def test_legacy_structured_property():
class OtherKind(model.Model):
foo = model.IntegerProperty()
bar = model.StringProperty()

class ThisKind(model.Model):
baz = model.StructuredProperty(OtherKind)
copacetic = model.BooleanProperty()

key = datastore.Key("ThisKind", 123, project="testing")
datastore_entity = datastore.Entity(key=key)
datastore_entity.update(
{
"baz.foo": 42,
"baz.bar": "himom",
"copacetic": True,
"super.fluous": "whocares?",
}
)
protobuf = helpers.entity_to_protobuf(datastore_entity)
entity = model._entity_from_protobuf(protobuf)
assert isinstance(entity, ThisKind)
assert entity.baz.foo == 42
assert entity.baz.bar == "himom"
assert entity.copacetic is True

assert not hasattr(entity, "super")
assert not hasattr(entity, "super.fluous")

@staticmethod
@pytest.mark.usefixtures("in_context")
def test_legacy_repeated_structured_property():
class OtherKind(model.Model):
foo = model.IntegerProperty()
bar = model.StringProperty()

class ThisKind(model.Model):
baz = model.StructuredProperty(OtherKind, repeated=True)
copacetic = model.BooleanProperty()

key = datastore.Key("ThisKind", 123, project="testing")
datastore_entity = datastore.Entity(key=key)
datastore_entity.update(
{
"baz.foo": [42, 144],
"baz.bar": ["himom", "hellodad"],
"copacetic": True,
}
)
protobuf = helpers.entity_to_protobuf(datastore_entity)
entity = model._entity_from_protobuf(protobuf)
assert isinstance(entity, ThisKind)
assert entity.baz[0].foo == 42
assert entity.baz[0].bar == "himom"
assert entity.baz[1].foo == 144
assert entity.baz[1].bar == "hellodad"
assert entity.copacetic is True


class Test_entity_to_protobuf:
@staticmethod
Expand Down
Loading