Skip to content

Commit 88f14fa

Browse files
fix: support sub-meanings for datastore v2.20.3 (#1014)
1 parent b1ee94c commit 88f14fa

File tree

4 files changed

+228
-14
lines changed

4 files changed

+228
-14
lines changed

google/cloud/ndb/key.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,13 +716,13 @@ def reference(self):
716716
>>> key = ndb.Key("Trampoline", 88, project="xy", database="wv", namespace="zt")
717717
>>> key.reference()
718718
app: "xy"
719-
name_space: "zt"
720719
path {
721720
element {
722721
type: "Trampoline"
723722
id: 88
724723
}
725724
}
725+
name_space: "zt"
726726
database_id: "wv"
727727
<BLANKLINE>
728728
"""

google/cloud/ndb/model.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2698,14 +2698,26 @@ def _from_datastore(self, ds_entity, value):
26982698
Need to check the ds_entity for a compressed meaning that would
26992699
indicate we are getting a compressed value.
27002700
"""
2701-
if self._name in ds_entity._meanings:
2702-
meaning = ds_entity._meanings[self._name][0]
2703-
if meaning == _MEANING_COMPRESSED and not self._compressed:
2704-
if self._repeated:
2705-
for sub_value in value:
2706-
sub_value.b_val = zlib.decompress(sub_value.b_val)
2707-
else:
2708-
value.b_val = zlib.decompress(value.b_val)
2701+
if self._name in ds_entity._meanings and not self._compressed:
2702+
root_meaning = ds_entity._meanings[self._name][0]
2703+
sub_meanings = None
2704+
# meaning may be a tuple. Attempt unwrap
2705+
if isinstance(root_meaning, tuple):
2706+
root_meaning, sub_meanings = root_meaning
2707+
# decompress values if needed
2708+
if root_meaning == _MEANING_COMPRESSED and not self._repeated:
2709+
value.b_val = zlib.decompress(value.b_val)
2710+
elif root_meaning == _MEANING_COMPRESSED and self._repeated:
2711+
for sub_value in value:
2712+
sub_value.b_val = zlib.decompress(sub_value.b_val)
2713+
elif isinstance(sub_meanings, list) and self._repeated:
2714+
for idx, sub_value in enumerate(value):
2715+
try:
2716+
if sub_meanings[idx] == _MEANING_COMPRESSED:
2717+
sub_value.b_val = zlib.decompress(sub_value.b_val)
2718+
except IndexError:
2719+
# value list size exceeds sub_meanings list
2720+
break
27092721
return value
27102722

27112723
def _db_set_compressed_meaning(self, p):

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def main():
4141
readme = readme_file.read()
4242
dependencies = [
4343
"google-api-core[grpc] >= 1.34.0, <3.0.0dev,!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*",
44-
"google-cloud-datastore >= 2.16.0, < 3.0.0dev",
44+
"google-cloud-datastore >= 2.16.0, != 2.20.2, < 3.0.0dev",
4545
"protobuf >= 3.20.2, <6.0.0dev,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5",
4646
"pymemcache >= 2.1.0, < 5.0.0dev",
4747
"pytz >= 2018.3",

tests/unit/test_model.py

Lines changed: 206 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1929,9 +1929,8 @@ class ThisKind(model.Model):
19291929
ds_entity = model._entity_to_ds_entity(entity)
19301930
assert ds_entity["foo"] == compressed_value
19311931

1932-
@staticmethod
19331932
@pytest.mark.usefixtures("in_context")
1934-
def test__from_datastore_compressed_repeated_to_compressed():
1933+
def test__from_datastore_compressed_repeated_to_compressed(self):
19351934
class ThisKind(model.Model):
19361935
foo = model.BlobProperty(compressed=True, repeated=True)
19371936

@@ -1955,9 +1954,48 @@ class ThisKind(model.Model):
19551954
ds_entity = model._entity_to_ds_entity(entity)
19561955
assert ds_entity["foo"] == [compressed_value_one, compressed_value_two]
19571956

1958-
@staticmethod
1957+
@pytest.mark.skipif(
1958+
[int(v) for v in datastore.__version__.split(".")] < [2, 20, 2],
1959+
reason="uses meanings semantics from datastore v2.20.2 and later",
1960+
)
1961+
@pytest.mark.parametrize(
1962+
"meaning",
1963+
[
1964+
(model._MEANING_COMPRESSED, None), # set root meaning
1965+
(model._MEANING_COMPRESSED, []),
1966+
(model._MEANING_COMPRESSED, [1, 1]),
1967+
(None, [model._MEANING_COMPRESSED] * 2), # set sub-meanings
1968+
],
1969+
)
19591970
@pytest.mark.usefixtures("in_context")
1960-
def test__from_datastore_compressed_repeated_to_uncompressed():
1971+
def test__from_datastore_compressed_repeated_to_compressed_tuple_meaning(
1972+
self, meaning
1973+
):
1974+
class ThisKind(model.Model):
1975+
foo = model.BlobProperty(compressed=True, repeated=True)
1976+
1977+
key = datastore.Key("ThisKind", 123, project="testing")
1978+
datastore_entity = datastore.Entity(key=key)
1979+
uncompressed_value_one = b"abc" * 1000
1980+
compressed_value_one = zlib.compress(uncompressed_value_one)
1981+
uncompressed_value_two = b"xyz" * 1000
1982+
compressed_value_two = zlib.compress(uncompressed_value_two)
1983+
compressed_value = [compressed_value_one, compressed_value_two]
1984+
datastore_entity.update({"foo": compressed_value})
1985+
meanings = {
1986+
"foo": (
1987+
meaning,
1988+
compressed_value,
1989+
)
1990+
}
1991+
datastore_entity._meanings = meanings
1992+
protobuf = helpers.entity_to_protobuf(datastore_entity)
1993+
entity = model._entity_from_protobuf(protobuf)
1994+
ds_entity = model._entity_to_ds_entity(entity)
1995+
assert ds_entity["foo"] == [compressed_value_one, compressed_value_two]
1996+
1997+
@pytest.mark.usefixtures("in_context")
1998+
def test__from_datastore_compressed_repeated_to_uncompressed(self):
19611999
class ThisKind(model.Model):
19622000
foo = model.BlobProperty(compressed=False, repeated=True)
19632001

@@ -1981,6 +2019,170 @@ class ThisKind(model.Model):
19812019
ds_entity = model._entity_to_ds_entity(entity)
19822020
assert ds_entity["foo"] == [uncompressed_value_one, uncompressed_value_two]
19832021

2022+
@pytest.mark.skipif(
2023+
[int(v) for v in datastore.__version__.split(".")] < [2, 20, 2],
2024+
reason="uses meanings semantics from datastore v2.20.2 and later",
2025+
)
2026+
@pytest.mark.parametrize(
2027+
"meaning",
2028+
[
2029+
(model._MEANING_COMPRESSED, None), # set root meaning
2030+
(model._MEANING_COMPRESSED, []),
2031+
(model._MEANING_COMPRESSED, [1, 1]),
2032+
(None, [model._MEANING_COMPRESSED] * 2), # set sub-meanings
2033+
],
2034+
)
2035+
@pytest.mark.usefixtures("in_context")
2036+
def test__from_datastore_compressed_repeated_to_uncompressed_tuple_meaning(
2037+
self, meaning
2038+
):
2039+
class ThisKind(model.Model):
2040+
foo = model.BlobProperty(compressed=False, repeated=True)
2041+
2042+
key = datastore.Key("ThisKind", 123, project="testing")
2043+
datastore_entity = datastore.Entity(key=key)
2044+
uncompressed_value_one = b"abc" * 1000
2045+
compressed_value_one = zlib.compress(uncompressed_value_one)
2046+
uncompressed_value_two = b"xyz" * 1000
2047+
compressed_value_two = zlib.compress(uncompressed_value_two)
2048+
compressed_value = [compressed_value_one, compressed_value_two]
2049+
datastore_entity.update({"foo": compressed_value})
2050+
meanings = {
2051+
"foo": (
2052+
meaning,
2053+
compressed_value,
2054+
)
2055+
}
2056+
datastore_entity._meanings = meanings
2057+
protobuf = helpers.entity_to_protobuf(datastore_entity)
2058+
entity = model._entity_from_protobuf(protobuf)
2059+
ds_entity = model._entity_to_ds_entity(entity)
2060+
assert ds_entity["foo"] == [uncompressed_value_one, uncompressed_value_two]
2061+
2062+
@pytest.mark.skipif(
2063+
[int(v) for v in datastore.__version__.split(".")] < [2, 20, 2],
2064+
reason="uses meanings semantics from datastore v2.20.2 and later",
2065+
)
2066+
@pytest.mark.parametrize(
2067+
"meaning",
2068+
[
2069+
(None, [model._MEANING_COMPRESSED, None]),
2070+
(None, [model._MEANING_COMPRESSED, None, None]),
2071+
(1, [model._MEANING_COMPRESSED, 1]),
2072+
(None, [model._MEANING_COMPRESSED]),
2073+
],
2074+
)
2075+
@pytest.mark.usefixtures("in_context")
2076+
def test__from_datastore_compressed_repeated_to_uncompressed_mixed_meaning(
2077+
self, meaning
2078+
):
2079+
"""
2080+
One item is compressed, one uncompressed
2081+
"""
2082+
2083+
class ThisKind(model.Model):
2084+
foo = model.BlobProperty(compressed=False, repeated=True)
2085+
2086+
key = datastore.Key("ThisKind", 123, project="testing")
2087+
datastore_entity = datastore.Entity(key=key)
2088+
uncompressed_value_one = b"abc" * 1000
2089+
compressed_value_one = zlib.compress(uncompressed_value_one)
2090+
uncompressed_value_two = b"xyz" * 1000
2091+
compressed_value_two = zlib.compress(uncompressed_value_two)
2092+
compressed_value = [compressed_value_one, compressed_value_two]
2093+
datastore_entity.update({"foo": compressed_value})
2094+
meanings = {
2095+
"foo": (
2096+
meaning,
2097+
compressed_value,
2098+
)
2099+
}
2100+
datastore_entity._meanings = meanings
2101+
protobuf = helpers.entity_to_protobuf(datastore_entity)
2102+
entity = model._entity_from_protobuf(protobuf)
2103+
ds_entity = model._entity_to_ds_entity(entity)
2104+
assert ds_entity["foo"] == [uncompressed_value_one, compressed_value_two]
2105+
2106+
@pytest.mark.skipif(
2107+
[int(v) for v in datastore.__version__.split(".")] < [2, 20, 2],
2108+
reason="uses meanings semantics from datastore v2.20.2 and later",
2109+
)
2110+
@pytest.mark.parametrize(
2111+
"meaning",
2112+
[
2113+
(None, None),
2114+
(None, []),
2115+
(None, [None]),
2116+
(None, [None, None]),
2117+
(1, []),
2118+
(1, [1]),
2119+
(1, [1, 1]),
2120+
],
2121+
)
2122+
@pytest.mark.usefixtures("in_context")
2123+
def test__from_datastore_compressed_repeated_no_meaning(self, meaning):
2124+
"""
2125+
could be uncompressed, but meaning not set
2126+
"""
2127+
2128+
class ThisKind(model.Model):
2129+
foo = model.BlobProperty(compressed=False, repeated=True)
2130+
2131+
key = datastore.Key("ThisKind", 123, project="testing")
2132+
datastore_entity = datastore.Entity(key=key)
2133+
uncompressed_value_one = b"abc" * 1000
2134+
compressed_value_one = zlib.compress(uncompressed_value_one)
2135+
uncompressed_value_two = b"xyz" * 1000
2136+
compressed_value_two = zlib.compress(uncompressed_value_two)
2137+
compressed_value = [compressed_value_one, compressed_value_two]
2138+
datastore_entity.update({"foo": compressed_value})
2139+
meanings = {
2140+
"foo": (
2141+
meaning,
2142+
compressed_value,
2143+
)
2144+
}
2145+
datastore_entity._meanings = meanings
2146+
protobuf = helpers.entity_to_protobuf(datastore_entity)
2147+
entity = model._entity_from_protobuf(protobuf)
2148+
ds_entity = model._entity_to_ds_entity(entity)
2149+
assert ds_entity["foo"] == [compressed_value_one, compressed_value_two]
2150+
2151+
@staticmethod
2152+
@pytest.mark.usefixtures("in_context")
2153+
def test__from_datastore_large_value_list():
2154+
"""
2155+
try calling _from_datastore with a meaning list smaller than the value list
2156+
"""
2157+
2158+
prop = model.BlobProperty(compressed=False, repeated=True, name="foo")
2159+
2160+
key = datastore.Key("ThisKind", 123, project="testing")
2161+
datastore_entity = datastore.Entity(key=key)
2162+
uncompressed_value_one = b"abc" * 1000
2163+
compressed_value_one = zlib.compress(uncompressed_value_one)
2164+
uncompressed_value_two = b"xyz" * 1000
2165+
compressed_value_two = zlib.compress(uncompressed_value_two)
2166+
compressed_value = [
2167+
model._BaseValue(compressed_value_one),
2168+
model._BaseValue(compressed_value_two),
2169+
]
2170+
datastore_entity.update({"foo": compressed_value})
2171+
meanings = {
2172+
"foo": (
2173+
(None, [model._MEANING_COMPRESSED]),
2174+
compressed_value,
2175+
)
2176+
}
2177+
2178+
datastore_entity._meanings = meanings
2179+
2180+
updated_value = prop._from_datastore(datastore_entity, compressed_value)
2181+
assert len(updated_value) == 2
2182+
assert updated_value[0].b_val == uncompressed_value_one
2183+
# second value should remain compressed
2184+
assert updated_value[1].b_val == compressed_value_two
2185+
19842186
@staticmethod
19852187
@pytest.mark.usefixtures("in_context")
19862188
def test__from_datastore_uncompressed_to_uncompressed():

0 commit comments

Comments
 (0)