Skip to content

Commit 36b08ae

Browse files
authored
Improve keyword query parameters (#835)
* API docs: clarify precedence between keyword and dict query parameters * Fix: parameters of internal functions could clash with keyword parameters. E.g., `db` and `imp_user` could not be used as keyword query parameters causing a not necessarily easy to understand error message. This got fixed by merging keyword and dict parameters at the top-most level. * Add tests for query parameter precedence
1 parent 76a034c commit 36b08ae

File tree

8 files changed

+122
-34
lines changed

8 files changed

+122
-34
lines changed

neo4j/_async/work/result.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,24 +106,19 @@ def _qid(self):
106106
else:
107107
return self._raw_qid
108108

109-
async def _tx_ready_run(self, query, parameters, **kwargs):
109+
async def _tx_ready_run(self, query, parameters):
110110
# BEGIN+RUN does not carry any extra on the RUN message.
111111
# BEGIN {extra}
112112
# RUN "query" {parameters} {extra}
113-
await self._run(
114-
query, parameters, None, None, None, None, **kwargs
115-
)
113+
await self._run(query, parameters, None, None, None, None)
116114

117115
async def _run(
118-
self, query, parameters, db, imp_user, access_mode, bookmarks,
119-
**kwargs
116+
self, query, parameters, db, imp_user, access_mode, bookmarks
120117
):
121118
query_text = str(query) # Query or string object
122119
query_metadata = getattr(query, "metadata", None)
123120
query_timeout = getattr(query, "timeout", None)
124121

125-
parameters = dict(parameters or {}, **kwargs)
126-
127122
self._metadata = {
128123
"query": query_text,
129124
"parameters": parameters,

neo4j/_async/work/session.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,8 @@ async def run(
258258
259259
:param query: cypher query
260260
:param parameters: dictionary of parameters
261-
:param kwargs: additional keyword parameters
261+
:param kwargs: additional keyword parameters.
262+
These take precedence over parameters passed as ``parameters``.
262263
263264
:raises SessionError: if the session has been closed.
264265
@@ -286,10 +287,11 @@ async def run(
286287
self._result_error
287288
)
288289
bookmarks = await self._get_all_bookmarks()
290+
parameters = dict(parameters or {}, **kwargs)
289291
await self._auto_result._run(
290292
query, parameters, self._config.database,
291293
self._config.impersonated_user, self._config.default_access_mode,
292-
bookmarks, **kwargs
294+
bookmarks
293295
)
294296

295297
return self._auto_result

neo4j/_async/work/transaction.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ async def run(
119119
120120
:param query: cypher query
121121
:param parameters: dictionary of parameters
122-
:param kwparameters: additional keyword parameters
122+
:param kwparameters: additional keyword parameters.
123+
These take precedence over parameters passed as ``parameters``.
123124
124125
:raise TransactionError: if the transaction is already closed
125126
@@ -147,7 +148,8 @@ async def run(
147148
)
148149
self._results.append(result)
149150

150-
await result._tx_ready_run(query, parameters, **kwparameters)
151+
parameters = dict(parameters or {}, **kwparameters)
152+
await result._tx_ready_run(query, parameters)
151153

152154
return result
153155

neo4j/_sync/work/result.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,24 +106,19 @@ def _qid(self):
106106
else:
107107
return self._raw_qid
108108

109-
def _tx_ready_run(self, query, parameters, **kwargs):
109+
def _tx_ready_run(self, query, parameters):
110110
# BEGIN+RUN does not carry any extra on the RUN message.
111111
# BEGIN {extra}
112112
# RUN "query" {parameters} {extra}
113-
self._run(
114-
query, parameters, None, None, None, None, **kwargs
115-
)
113+
self._run(query, parameters, None, None, None, None)
116114

117115
def _run(
118-
self, query, parameters, db, imp_user, access_mode, bookmarks,
119-
**kwargs
116+
self, query, parameters, db, imp_user, access_mode, bookmarks
120117
):
121118
query_text = str(query) # Query or string object
122119
query_metadata = getattr(query, "metadata", None)
123120
query_timeout = getattr(query, "timeout", None)
124121

125-
parameters = dict(parameters or {}, **kwargs)
126-
127122
self._metadata = {
128123
"query": query_text,
129124
"parameters": parameters,

neo4j/_sync/work/session.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,8 @@ def run(
258258
259259
:param query: cypher query
260260
:param parameters: dictionary of parameters
261-
:param kwargs: additional keyword parameters
261+
:param kwargs: additional keyword parameters.
262+
These take precedence over parameters passed as ``parameters``.
262263
263264
:raises SessionError: if the session has been closed.
264265
@@ -286,10 +287,11 @@ def run(
286287
self._result_error
287288
)
288289
bookmarks = self._get_all_bookmarks()
290+
parameters = dict(parameters or {}, **kwargs)
289291
self._auto_result._run(
290292
query, parameters, self._config.database,
291293
self._config.impersonated_user, self._config.default_access_mode,
292-
bookmarks, **kwargs
294+
bookmarks
293295
)
294296

295297
return self._auto_result

neo4j/_sync/work/transaction.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ def run(
119119
120120
:param query: cypher query
121121
:param parameters: dictionary of parameters
122-
:param kwparameters: additional keyword parameters
122+
:param kwparameters: additional keyword parameters.
123+
These take precedence over parameters passed as ``parameters``.
123124
124125
:raise TransactionError: if the transaction is already closed
125126
@@ -147,7 +148,8 @@ def run(
147148
)
148149
self._results.append(result)
149150

150-
result._tx_ready_run(query, parameters, **kwparameters)
151+
parameters = dict(parameters or {}, **kwparameters)
152+
result._tx_ready_run(query, parameters)
151153

152154
return result
153155

tests/unit/async_/work/test_session.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -319,9 +319,7 @@ async def test_session_tx_type(fake_pool):
319319
))
320320
@pytest.mark.parametrize("run_type", ("auto", "unmanaged", "managed"))
321321
@mark_async_test
322-
async def test_session_run_with_parameters(
323-
fake_pool, parameters, run_type, mocker
324-
):
322+
async def test_session_run_with_parameters(fake_pool, parameters, run_type):
325323
async with AsyncSession(fake_pool, SessionConfig()) as session:
326324
if run_type == "auto":
327325
await session.run("RETURN $x", **parameters)
@@ -337,12 +335,59 @@ async def work(tx):
337335

338336
assert len(fake_pool.acquired_connection_mocks) == 1
339337
connection_mock = fake_pool.acquired_connection_mocks[0]
340-
assert connection_mock.run.called_once()
338+
connection_mock.run.assert_called_once()
341339
call = connection_mock.run.call_args
342340
assert call.args[0] == "RETURN $x"
343341
assert call.kwargs["parameters"] == parameters
344342

345343

344+
@pytest.mark.parametrize(
345+
("params", "kw_params", "expected_params"),
346+
(
347+
({"x": 1}, {}, {"x": 1}),
348+
({}, {"x": 1}, {"x": 1}),
349+
({"x": 1}, {"y": 2}, {"x": 1, "y": 2}),
350+
({"x": 1}, {"x": 2}, {"x": 2}),
351+
({"x": 1}, {"x": 2}, {"x": 2}),
352+
({"x": 1, "y": 3}, {"x": 2}, {"x": 2, "y": 3}),
353+
({"x": 1}, {"x": 2, "y": 3}, {"x": 2, "y": 3}),
354+
# potentially internally used keyword arguments
355+
({}, {"timeout": 2}, {"timeout": 2}),
356+
({"timeout": 2}, {}, {"timeout": 2}),
357+
({}, {"imp_user": "hans"}, {"imp_user": "hans"}),
358+
({"imp_user": "hans"}, {}, {"imp_user": "hans"}),
359+
({}, {"db": "neo4j"}, {"db": "neo4j"}),
360+
({"db": "neo4j"}, {}, {"db": "neo4j"}),
361+
({}, {"database": "neo4j"}, {"database": "neo4j"}),
362+
({"database": "neo4j"}, {}, {"database": "neo4j"}),
363+
)
364+
)
365+
@pytest.mark.parametrize("run_type", ("auto", "unmanaged", "managed"))
366+
@mark_async_test
367+
async def test_session_run_parameter_precedence(
368+
fake_pool, params, kw_params, expected_params, run_type
369+
):
370+
async with AsyncSession(fake_pool, SessionConfig()) as session:
371+
if run_type == "auto":
372+
await session.run("RETURN $x", params, **kw_params)
373+
elif run_type == "unmanaged":
374+
tx = await session.begin_transaction()
375+
await tx.run("RETURN $x", params, **kw_params)
376+
elif run_type == "managed":
377+
async def work(tx):
378+
await tx.run("RETURN $x", params, **kw_params)
379+
await session.execute_write(work)
380+
else:
381+
raise ValueError(run_type)
382+
383+
assert len(fake_pool.acquired_connection_mocks) == 1
384+
connection_mock = fake_pool.acquired_connection_mocks[0]
385+
connection_mock.run.assert_called_once()
386+
call = connection_mock.run.call_args
387+
assert call.args[0] == "RETURN $x"
388+
assert call.kwargs["parameters"] == expected_params
389+
390+
346391
@pytest.mark.parametrize("db", (None, "adb"))
347392
@pytest.mark.parametrize("routing", (True, False))
348393
# no home db resolution when connected to Neo4j 4.3 or earlier
@@ -446,7 +491,7 @@ async def bmm_gat_all_bookmarks():
446491

447492
assert len(fake_pool.acquired_connection_mocks) == 1
448493
connection_mock = fake_pool.acquired_connection_mocks[0]
449-
assert connection_mock.run.called_once()
494+
connection_mock.run.assert_called_once()
450495
connection_run_call_kwargs = connection_mock.run.call_args.kwargs
451496
assert (set(connection_run_call_kwargs["bookmarks"])
452497
== {"all", "bookmarks", *(additional_session_bookmarks or [])})

tests/unit/sync/work/test_session.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -319,9 +319,7 @@ def test_session_tx_type(fake_pool):
319319
))
320320
@pytest.mark.parametrize("run_type", ("auto", "unmanaged", "managed"))
321321
@mark_sync_test
322-
def test_session_run_with_parameters(
323-
fake_pool, parameters, run_type, mocker
324-
):
322+
def test_session_run_with_parameters(fake_pool, parameters, run_type):
325323
with Session(fake_pool, SessionConfig()) as session:
326324
if run_type == "auto":
327325
session.run("RETURN $x", **parameters)
@@ -337,12 +335,59 @@ def work(tx):
337335

338336
assert len(fake_pool.acquired_connection_mocks) == 1
339337
connection_mock = fake_pool.acquired_connection_mocks[0]
340-
assert connection_mock.run.called_once()
338+
connection_mock.run.assert_called_once()
341339
call = connection_mock.run.call_args
342340
assert call.args[0] == "RETURN $x"
343341
assert call.kwargs["parameters"] == parameters
344342

345343

344+
@pytest.mark.parametrize(
345+
("params", "kw_params", "expected_params"),
346+
(
347+
({"x": 1}, {}, {"x": 1}),
348+
({}, {"x": 1}, {"x": 1}),
349+
({"x": 1}, {"y": 2}, {"x": 1, "y": 2}),
350+
({"x": 1}, {"x": 2}, {"x": 2}),
351+
({"x": 1}, {"x": 2}, {"x": 2}),
352+
({"x": 1, "y": 3}, {"x": 2}, {"x": 2, "y": 3}),
353+
({"x": 1}, {"x": 2, "y": 3}, {"x": 2, "y": 3}),
354+
# potentially internally used keyword arguments
355+
({}, {"timeout": 2}, {"timeout": 2}),
356+
({"timeout": 2}, {}, {"timeout": 2}),
357+
({}, {"imp_user": "hans"}, {"imp_user": "hans"}),
358+
({"imp_user": "hans"}, {}, {"imp_user": "hans"}),
359+
({}, {"db": "neo4j"}, {"db": "neo4j"}),
360+
({"db": "neo4j"}, {}, {"db": "neo4j"}),
361+
({}, {"database": "neo4j"}, {"database": "neo4j"}),
362+
({"database": "neo4j"}, {}, {"database": "neo4j"}),
363+
)
364+
)
365+
@pytest.mark.parametrize("run_type", ("auto", "unmanaged", "managed"))
366+
@mark_sync_test
367+
def test_session_run_parameter_precedence(
368+
fake_pool, params, kw_params, expected_params, run_type
369+
):
370+
with Session(fake_pool, SessionConfig()) as session:
371+
if run_type == "auto":
372+
session.run("RETURN $x", params, **kw_params)
373+
elif run_type == "unmanaged":
374+
tx = session.begin_transaction()
375+
tx.run("RETURN $x", params, **kw_params)
376+
elif run_type == "managed":
377+
def work(tx):
378+
tx.run("RETURN $x", params, **kw_params)
379+
session.execute_write(work)
380+
else:
381+
raise ValueError(run_type)
382+
383+
assert len(fake_pool.acquired_connection_mocks) == 1
384+
connection_mock = fake_pool.acquired_connection_mocks[0]
385+
connection_mock.run.assert_called_once()
386+
call = connection_mock.run.call_args
387+
assert call.args[0] == "RETURN $x"
388+
assert call.kwargs["parameters"] == expected_params
389+
390+
346391
@pytest.mark.parametrize("db", (None, "adb"))
347392
@pytest.mark.parametrize("routing", (True, False))
348393
# no home db resolution when connected to Neo4j 4.3 or earlier
@@ -446,7 +491,7 @@ def bmm_gat_all_bookmarks():
446491

447492
assert len(fake_pool.acquired_connection_mocks) == 1
448493
connection_mock = fake_pool.acquired_connection_mocks[0]
449-
assert connection_mock.run.called_once()
494+
connection_mock.run.assert_called_once()
450495
connection_run_call_kwargs = connection_mock.run.call_args.kwargs
451496
assert (set(connection_run_call_kwargs["bookmarks"])
452497
== {"all", "bookmarks", *(additional_session_bookmarks or [])})

0 commit comments

Comments
 (0)