Skip to content

Commit e87229e

Browse files
Chris Rossicguardiacrwilcox
authored
fix: prevent mismatch error when using default namespace on ancestor queries (#614)
* fix: prevent mismatch error when using default namespace on ancestor queries fixes #577 * Let keys inherit the namespace of their parent. This reproduces the behavior of Datastore as well as legacy NDB. Co-authored-by: Carlos de la Guardia <[email protected]> Co-authored-by: Christopher Wilcox <[email protected]>
1 parent b950639 commit e87229e

File tree

5 files changed

+92
-10
lines changed

5 files changed

+92
-10
lines changed

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

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -279,17 +279,9 @@ class Key(object):
279279
_hash_value = None
280280

281281
def __new__(cls, *path_args, **kwargs):
282-
# Avoid circular import in Python 2.7
283-
from google.cloud.ndb import context as context_module
284-
285282
_constructor_handle_positional(path_args, kwargs)
286283
instance = super(Key, cls).__new__(cls)
287284

288-
# Make sure to pass in the namespace if it's not explicitly set.
289-
if kwargs.get("namespace", UNDEFINED) is UNDEFINED:
290-
context = context_module.get_context()
291-
kwargs["namespace"] = context.get_namespace()
292-
293285
if "reference" in kwargs or "serialized" in kwargs or "urlsafe" in kwargs:
294286
ds_key, reference = _parse_from_ref(cls, **kwargs)
295287
elif "pairs" in kwargs or "flat" in kwargs:
@@ -1319,7 +1311,7 @@ def _parse_from_ref(
13191311

13201312

13211313
def _parse_from_args(
1322-
pairs=None, flat=None, project=None, app=None, namespace=None, parent=None
1314+
pairs=None, flat=None, project=None, app=None, namespace=UNDEFINED, parent=None
13231315
):
13241316
"""Construct a key the path (and possibly a parent key).
13251317
@@ -1344,6 +1336,9 @@ def _parse_from_args(
13441336
Raises:
13451337
.BadValueError: If ``parent`` is passed but is not a ``Key``.
13461338
"""
1339+
# Avoid circular import in Python 2.7
1340+
from google.cloud.ndb import context as context_module
1341+
13471342
flat = _get_path(flat, pairs)
13481343
_clean_flat_path(flat)
13491344

@@ -1355,12 +1350,20 @@ def _parse_from_args(
13551350
parent_ds_key = None
13561351
if parent is None:
13571352
project = _project_from_app(app)
1353+
if namespace is UNDEFINED:
1354+
context = context_module.get_context()
1355+
namespace = context.get_namespace()
1356+
13581357
else:
13591358
project = _project_from_app(app, allow_empty=True)
13601359
if not isinstance(parent, Key):
13611360
raise exceptions.BadValueError(
13621361
"Expected Key instance, got {!r}".format(parent)
13631362
)
1363+
1364+
if namespace is UNDEFINED:
1365+
namespace = None
1366+
13641367
# Offload verification of parent to ``google.cloud.datastore.Key()``.
13651368
parent_ds_key = parent._key
13661369

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1392,7 +1392,13 @@ def __init__(
13921392
else:
13931393
project = ancestor.app()
13941394
if namespace is not None:
1395-
if namespace != ancestor.namespace():
1395+
# if namespace is the empty string, that means default
1396+
# namespace, but after a put, if the ancestor is using
1397+
# the default namespace, its namespace will be None,
1398+
# so skip the test to avoid a false mismatch error.
1399+
if namespace == "" and ancestor.namespace() is None:
1400+
pass
1401+
elif namespace != ancestor.namespace():
13961402
raise TypeError("ancestor/namespace mismatch")
13971403
else:
13981404
namespace = ancestor.namespace()

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,56 @@ class SomeKind(ndb.Model):
161161
assert [entity.foo for entity in results] == [-1, 0, 1, 2, 3, 4]
162162

163163

164+
def test_ancestor_query_with_namespace(client_context, dispose_of, other_namespace):
165+
class Dummy(ndb.Model):
166+
foo = ndb.StringProperty(default="")
167+
168+
entity1 = Dummy(foo="bar", namespace="xyz")
169+
parent_key = entity1.put()
170+
dispose_of(entity1.key._key)
171+
172+
entity2 = Dummy(foo="child", parent=parent_key, namespace=None)
173+
entity2.put()
174+
dispose_of(entity2.key._key)
175+
176+
entity3 = Dummy(foo="childless", namespace="xyz")
177+
entity3.put()
178+
dispose_of(entity3.key._key)
179+
180+
with client_context.new(namespace=other_namespace).use():
181+
query = Dummy.query(ancestor=parent_key, namespace="xyz")
182+
results = eventually(query.fetch, length_equals(2))
183+
184+
assert results[0].foo == "bar"
185+
assert results[1].foo == "child"
186+
187+
188+
def test_ancestor_query_with_default_namespace(
189+
client_context, dispose_of, other_namespace
190+
):
191+
class Dummy(ndb.Model):
192+
foo = ndb.StringProperty(default="")
193+
194+
entity1 = Dummy(foo="bar", namespace="")
195+
parent_key = entity1.put()
196+
dispose_of(entity1.key._key)
197+
198+
entity2 = Dummy(foo="child", parent=parent_key)
199+
entity2.put()
200+
dispose_of(entity2.key._key)
201+
202+
entity3 = Dummy(foo="childless", namespace="")
203+
entity3.put()
204+
dispose_of(entity3.key._key)
205+
206+
with client_context.new(namespace=other_namespace).use():
207+
query = Dummy.query(ancestor=parent_key, namespace="")
208+
results = eventually(query.fetch, length_equals(2))
209+
210+
assert results[0].foo == "bar"
211+
assert results[1].foo == "child"
212+
213+
164214
@pytest.mark.usefixtures("client_context")
165215
def test_projection(ds_entity):
166216
entity_id = test_utils.system.unique_resource_id()

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,22 @@ def test_constructor_with_parent(self):
222222
)
223223
assert key._reference is None
224224

225+
@pytest.mark.usefixtures("in_context")
226+
def test_constructor_with_parent_and_namespace(self):
227+
parent = key_module.Key(urlsafe=self.URLSAFE)
228+
key = key_module.Key("Zip", 10, parent=parent, namespace=None)
229+
230+
assert key._key == google.cloud.datastore.Key(
231+
"Kind", "Thing", "Zip", 10, project="fire"
232+
)
233+
assert key._reference is None
234+
235+
@pytest.mark.usefixtures("in_context")
236+
def test_constructor_with_parent_and_mismatched_namespace(self):
237+
parent = key_module.Key(urlsafe=self.URLSAFE)
238+
with pytest.raises(ValueError):
239+
key_module.Key("Zip", 10, parent=parent, namespace="foo")
240+
225241
@pytest.mark.usefixtures("in_context")
226242
def test_constructor_with_parent_bad_type(self):
227243
parent = mock.sentinel.parent

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,6 +1244,13 @@ def test_constructor_with_ancestor_and_namespace():
12441244
query = query_module.Query(ancestor=key, namespace="space")
12451245
assert query.namespace == "space"
12461246

1247+
@staticmethod
1248+
@pytest.mark.usefixtures("in_context")
1249+
def test_constructor_with_ancestor_and_default_namespace():
1250+
key = key_module.Key("a", "b", namespace=None)
1251+
query = query_module.Query(ancestor=key, namespace="")
1252+
assert query.namespace == ""
1253+
12471254
@staticmethod
12481255
@pytest.mark.usefixtures("in_context")
12491256
def test_constructor_with_ancestor_parameterized_thing():

0 commit comments

Comments
 (0)