Skip to content

Commit df83179

Browse files
DemetrisChrthejcfactor
authored andcommitted
PYCBC-1538: Get should not fail with InvalidArgumentException when projecting on more than 16 fields
Motivation ========== The RFC states that once the 16 field limit is reached the SDK should fall back to a full document fetch. Currently the Python SDK is raising an InvalidArgumentException. The C++ core already has the correct logic, so only the check for the number of fields in the projection needs to be removed Modifications ============= Remove the check for the number of fields in the projection Update test_project_too_many_projections for all APIs Results ======= All tests pass Change-Id: Ie9ece154d7269908f0b47520fc4d0fda34b76693 Reviewed-on: https://review.couchbase.org/c/couchbase-python-client/+/200237 Tested-by: Build Bot <[email protected]> Reviewed-by: Jared Casey <[email protected]>
1 parent 08bca91 commit df83179

File tree

4 files changed

+71
-25
lines changed

4 files changed

+71
-25
lines changed

acouchbase/tests/collection_t.py

+24-5
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
GetReplicaResult,
4040
GetResult,
4141
MutationResult)
42+
from tests.mock_server import MockServerType
4243

4344
from ._test_utils import (CollectionType,
4445
KVPair,
@@ -133,6 +134,11 @@ def check_multi_node(self, num_nodes):
133134
if num_nodes == 1:
134135
pytest.skip("Test only for clusters with more than a single node.")
135136

137+
@pytest.fixture(scope="class")
138+
def skip_if_go_caves(self, cb_env):
139+
if cb_env.is_mock_server and cb_env.mock_server_type == MockServerType.GoCAVES:
140+
pytest.skip("GoCAVES does not like this operation.")
141+
136142
@pytest.mark.asyncio
137143
async def test_exists(self, cb_env, default_kvp):
138144
cb = cb_env.collection
@@ -240,16 +246,29 @@ async def test_project_project_not_list(self, cb_env, default_kvp):
240246
with pytest.raises(InvalidArgumentException):
241247
await cb.get(key, GetOptions(project="thiswontwork"))
242248

249+
@pytest.mark.usefixtures('skip_if_go_caves')
243250
@pytest.mark.asyncio
244251
async def test_project_too_many_projections(self, cb_env, default_kvp):
245252
cb = cb_env.collection
246253
key = default_kvp.key
247-
project = []
248-
for _ in range(17):
249-
project.append("something")
254+
value = {f'f{i}': i for i in range(1, 21)}
255+
result = await cb.upsert(key, value, UpsertOptions(expiry=timedelta(seconds=2)))
250256

251-
with pytest.raises(InvalidArgumentException):
252-
await cb.get(key, GetOptions(project=project))
257+
async def cas_matches(cb, new_cas):
258+
r = await cb.get(key)
259+
if new_cas != r.cas:
260+
raise Exception(f"{new_cas} != {r.cas}")
261+
262+
await cb_env.try_n_times(10, 3, cas_matches, cb, result.cas)
263+
264+
project = [f'f{i}' for i in range(1, 19)]
265+
result = await cb.get(key, GetOptions(project=project))
266+
assert result.cas is not None
267+
res_dict = result.content_as[dict]
268+
assert res_dict != {}
269+
assert res_dict.get('f20') is None
270+
for field in project:
271+
assert res_dict.get(field) is not None
253272

254273
@pytest.mark.asyncio
255274
async def test_upsert(self, cb_env, default_kvp):

couchbase/logic/collection.py

-7
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,8 @@ class :class:`~couchbase.logic.CollectionLogic`.
135135
overrride provided :class:`~.options.GetOptions`
136136
137137
Raises:
138-
:class:`~.exceptions.InvalidArgumentException`: When attempting a get projection
139-
operation with more than 16 projections.
140138
:class:`~.exceptions.DocumentNotFoundException`: If the provided document key does not exist.
141139
"""
142-
projections = kwargs.get("project")
143-
if isinstance(projections, list) and len(projections) > 16:
144-
raise InvalidArgumentException(
145-
f"Maximum of 16 projects allowed. Provided {len(projections)}"
146-
)
147140
op_type = operations.GET.value
148141
return kv_operation(**self._get_connection_args(),
149142
key=key,

couchbase/tests/collection_t.py

+18-6
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ def check_multi_node(self, num_nodes):
124124
if num_nodes == 1:
125125
pytest.skip("Test only for clusters with more than a single node.")
126126

127+
@pytest.fixture(scope="class")
128+
def skip_if_go_caves(self, cb_env):
129+
if cb_env.is_mock_server and cb_env.mock_server_type == MockServerType.GoCAVES:
130+
pytest.skip("GoCAVES does not like this operation.")
131+
127132
@pytest.mark.usefixtures('check_xattr_supported')
128133
@pytest.mark.parametrize("expiry", [FIFTY_YEARS + 1,
129134
FIFTY_YEARS,
@@ -389,14 +394,21 @@ def test_project_project_not_list(self, cb_env):
389394
with pytest.raises(InvalidArgumentException):
390395
cb_env.collection.get(key, GetOptions(project='thiswontwork'))
391396

397+
@pytest.mark.usefixtures('skip_if_go_caves')
392398
def test_project_too_many_projections(self, cb_env):
393-
key = cb_env.get_existing_doc(key_only=True)
394-
project = []
395-
for _ in range(17):
396-
project.append('something')
399+
key = cb_env.get_new_doc(key_only=True)
400+
value = {f'f{i}': i for i in range(1, 21)}
401+
cb_env.collection.upsert(key, value)
397402

398-
with pytest.raises(InvalidArgumentException):
399-
cb_env.collection.get(key, GetOptions(project=project))
403+
project = [f'f{i}' for i in range(1, 19)]
404+
result = cb_env.collection.get(key, GetOptions(project=project))
405+
406+
assert result.cas is not None
407+
res_dict = result.content_as[dict]
408+
assert res_dict != {}
409+
assert res_dict.get('f20') is None
410+
for field in project:
411+
assert res_dict.get(field) is not None
400412

401413
def test_remove(self, cb_env):
402414
key = cb_env.get_existing_doc(key_only=True)

txcouchbase/tests/collection_t.py

+29-7
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
GetReplicaResult,
4242
GetResult,
4343
MutationResult)
44+
from tests.mock_server import MockServerType
4445

4546
from ._test_utils import (CollectionType,
4647
KVPair,
@@ -113,6 +114,11 @@ def check_replicas(self, cb_env):
113114
if kv_endpoints is None or len(kv_endpoints) < (num_replicas + 1):
114115
pytest.skip("Not all replicas are online")
115116

117+
@pytest.fixture(scope="class")
118+
def skip_if_go_caves(self, cb_env):
119+
if cb_env.is_mock_server and cb_env.mock_server_type == MockServerType.GoCAVES:
120+
pytest.skip("GoCAVES does not like this operation.")
121+
116122
@pytest.fixture(scope="class")
117123
def num_replicas(self, cb_env):
118124
bucket_settings = cb_env.try_n_times(10, 1, cb_env.bm.get_bucket, cb_env.bucket.name)
@@ -231,17 +237,33 @@ def test_project_project_not_list(self, cb_env, default_kvp):
231237
key,
232238
GetOptions(project="thiswontwork"))
233239

240+
@pytest.mark.usefixtures('skip_if_go_caves')
234241
def test_project_too_many_projections(self, cb_env, default_kvp):
235242
cb = cb_env.collection
236243
key = default_kvp.key
237-
project = []
238-
for _ in range(17):
239-
project.append("something")
244+
value = {f'f{i}': i for i in range(1, 21)}
245+
result = run_in_reactor_thread(cb.upsert,
246+
key,
247+
value,
248+
UpsertOptions(expiry=timedelta(seconds=2)))
240249

241-
with pytest.raises(InvalidArgumentException):
242-
run_in_reactor_thread(cb.get,
243-
key,
244-
GetOptions(project=project))
250+
def cas_matches(cb, new_cas):
251+
r = run_in_reactor_thread(cb.get, key)
252+
if new_cas != r.cas:
253+
raise Exception(f"{new_cas} != {r.cas}")
254+
255+
print('waiting for cas matches')
256+
cb_env.try_n_times(10, 3, cas_matches, cb, result.cas, is_deferred=False)
257+
258+
project = [f'f{i}' for i in range(1, 19)]
259+
result = run_in_reactor_thread(cb.get, key, GetOptions(project=project))
260+
261+
assert result.cas is not None
262+
res_dict = result.content_as[dict]
263+
assert res_dict != {}
264+
assert res_dict.get('f20') is None
265+
for field in project:
266+
assert res_dict.get(field) is not None
245267

246268
def test_upsert(self, cb_env, default_kvp):
247269
cb = cb_env.collection

0 commit comments

Comments
 (0)