Skip to content

Commit 0020d87

Browse files
committed
Tx config for all tx types
1 parent 3033bcb commit 0020d87

File tree

8 files changed

+113
-50
lines changed

8 files changed

+113
-50
lines changed

neo4j/__init__.py

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"RoutingDriver",
3333
"Session",
3434
"Transaction",
35+
"Statement",
3536
"StatementResult",
3637
"BoltStatementResult",
3738
"BoltStatementResultSummary",
@@ -72,7 +73,7 @@
7273
from warnings import warn
7374

7475

75-
from .compat import perf_counter, urlparse
76+
from .compat import perf_counter, urlparse, xstr
7677
from .config import *
7778
from .meta import version as __version__
7879

@@ -436,30 +437,49 @@ def run(self, statement, parameters=None, **kwparameters):
436437
protocol_version = cx.protocol_version
437438
server = cx.server
438439

439-
statement = ustr(statement)
440+
has_transaction = self.has_transaction()
441+
442+
statement_text = ustr(statement)
443+
statement_metadata = getattr(statement, "metadata", None)
444+
statement_timeout = getattr(statement, "timeout", None)
440445
parameters = fix_parameters(dict(parameters or {}, **kwparameters), protocol_version,
441446
supports_bytes=server.supports("bytes"))
442447

443448
def fail(_):
444449
self._close_transaction()
445450

446451
hydrant = PackStreamHydrator(protocol_version)
447-
metadata = {
448-
"statement": statement,
452+
result_metadata = {
453+
"statement": statement_text,
449454
"parameters": parameters,
450455
"server": server,
451456
"protocol_version": protocol_version,
452457
}
458+
run_metadata = {
459+
"metadata": statement_metadata,
460+
"timeout": statement_timeout,
461+
"on_success": result_metadata.update,
462+
"on_failure": fail,
463+
}
453464

454465
def done(summary_metadata):
455-
metadata.update(summary_metadata)
456-
bookmark = metadata.get("bookmark")
466+
result_metadata.update(summary_metadata)
467+
bookmark = result_metadata.get("bookmark")
457468
if bookmark:
458469
self._bookmarks_in = tuple([bookmark])
459470
self._bookmark_out = bookmark
460471

461-
self._last_result = result = BoltStatementResult(self, hydrant, metadata)
462-
cx.run(statement, parameters, on_success=metadata.update, on_failure=fail)
472+
self._last_result = result = BoltStatementResult(self, hydrant, result_metadata)
473+
474+
if has_transaction:
475+
if statement_metadata:
476+
raise ValueError("Metadata can only be attached at transaction level")
477+
if statement_timeout:
478+
raise ValueError("Timeouts only apply at transaction level")
479+
else:
480+
run_metadata["bookmarks"] = self._bookmarks_in
481+
482+
cx.run(statement_text, parameters, **run_metadata)
463483
cx.pull_all(
464484
on_records=lambda records: result._records.extend(
465485
hydrant.hydrate_records(result.keys(), records)),
@@ -468,7 +488,7 @@ def done(summary_metadata):
468488
on_summary=lambda: result.detach(sync=False),
469489
)
470490

471-
if not self.has_transaction():
491+
if not has_transaction:
472492
try:
473493
self._connection.send()
474494
self._connection.fetch()
@@ -805,6 +825,36 @@ def closed(self):
805825
return self._closed
806826

807827

828+
class Statement(object):
829+
830+
def __init__(self, text, metadata=None, timeout=None):
831+
self.text = text
832+
try:
833+
self.metadata = metadata
834+
except TypeError:
835+
raise TypeError("Metadata must be coercible to a dict")
836+
try:
837+
self.timeout = timeout
838+
except TypeError:
839+
raise TypeError("Timeout must be specified as a number of seconds")
840+
841+
def __str__(self):
842+
return xstr(self.text)
843+
844+
845+
def fix_parameters(parameters, protocol_version, **kwargs):
846+
if not parameters:
847+
return {}
848+
dehydrator = PackStreamDehydrator(protocol_version, **kwargs)
849+
try:
850+
dehydrated, = dehydrator.dehydrate([parameters])
851+
except TypeError as error:
852+
value = error.args[0]
853+
raise TypeError("Parameters of type {} are not supported".format(type(value).__name__))
854+
else:
855+
return dehydrated
856+
857+
808858
class StatementResult(object):
809859
""" A handler for the result of Cypher statement execution. Instances
810860
of this class are typically constructed and returned by
@@ -1406,19 +1456,6 @@ def custom_auth(principal, credentials, realm, scheme, **parameters):
14061456
return AuthToken(scheme, principal, credentials, realm, **parameters)
14071457

14081458

1409-
def fix_parameters(parameters, protocol_version, **kwargs):
1410-
if not parameters:
1411-
return {}
1412-
dehydrator = PackStreamDehydrator(protocol_version, **kwargs)
1413-
try:
1414-
dehydrated, = dehydrator.dehydrate([parameters])
1415-
except TypeError as error:
1416-
value = error.args[0]
1417-
raise TypeError("Parameters of type {} are not supported".format(type(value).__name__))
1418-
else:
1419-
return dehydrated
1420-
1421-
14221459
def iter_items(iterable):
14231460
""" Iterate through all items (key-value pairs) within an iterable
14241461
dictionary-like object. If the object has a `keys` method, this is

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
neobolt==1.7.0rc3
1+
neobolt==1.7.0rc4
22
neotime

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from neo4j.meta import package, version
3131

3232
install_requires = [
33-
"neobolt==1.7.0rc3",
33+
"neobolt==1.7.0rc4",
3434
"neotime",
3535
]
3636
classifiers = [

test/integration/test_session.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from neo4j import \
2727
READ_ACCESS, WRITE_ACCESS, \
28-
CypherError, SessionError, TransactionError, unit_of_work
28+
CypherError, SessionError, TransactionError, unit_of_work, Statement
2929
from neo4j.types.graph import Node, Relationship, Path
3030
from neo4j.exceptions import CypherSyntaxError, TransientError
3131

@@ -175,6 +175,32 @@ def test_should_not_allow_empty_statements(self):
175175
with self.assertRaises(ValueError):
176176
_ = session.run("")
177177

178+
def test_statement_object(self):
179+
if self.protocol_version() < 3:
180+
raise SkipTest("Test requires Bolt v3")
181+
with self.driver.session() as session:
182+
value = session.run(Statement("RETURN $x"), x=1).single().value()
183+
self.assertEqual(value, 1)
184+
185+
def test_autocommit_transactions_should_support_metadata(self):
186+
if self.protocol_version() < 3:
187+
raise SkipTest("Test requires Bolt v3")
188+
metadata_in = {"foo": "bar"}
189+
with self.driver.session() as session:
190+
metadata_out = session.run(Statement("CALL dbms.getTXMetaData", metadata=metadata_in)).single().value()
191+
self.assertEqual(metadata_in, metadata_out)
192+
193+
def test_autocommit_transactions_should_support_timeout(self):
194+
if self.protocol_version() < 3:
195+
raise SkipTest("Test requires Bolt v3")
196+
with self.driver.session() as s1:
197+
s1.run("CREATE (a:Node)").consume()
198+
with self.driver.session() as s2:
199+
tx1 = s1.begin_transaction()
200+
tx1.run("MATCH (a:Node) SET a.property = 1").consume()
201+
with self.assertRaises(TransientError):
202+
s2.run(Statement("MATCH (a:Node) SET a.property = 2", timeout=0.25)).consume()
203+
178204

179205
class SummaryTestCase(DirectIntegrationTestCase):
180206

@@ -388,6 +414,12 @@ def test_last_run_statement_should_be_cleared_on_failure(self):
388414
assert connection_2._last_run_statement is None
389415
tx.close()
390416

417+
def test_statement_object_not_supported(self):
418+
with self.driver.session() as session:
419+
with session.begin_transaction() as tx:
420+
with self.assertRaises(ValueError):
421+
tx.run(Statement("RETURN 1", timeout=0.25))
422+
391423
def test_transaction_metadata(self):
392424
if self.protocol_version() < 3:
393425
raise SkipTest("Test requires Bolt v3")

test/integration/test_types.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
from pytz import FixedOffset, timezone, utc
2626

2727
from neo4j.exceptions import CypherTypeError
28-
from neo4j.v1.types.graph import Node, Relationship, Path
29-
from neo4j.v1.types.spatial import CartesianPoint, WGS84Point
30-
from neo4j.v1.types.temporal import Duration, Date, Time, DateTime
28+
from neo4j.types.graph import Node, Relationship, Path
29+
from neo4j.types.spatial import CartesianPoint, WGS84Point
30+
from neo4j.types.temporal import Duration, Date, Time, DateTime
3131

3232
from test.integration.tools import DirectIntegrationTestCase
3333

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
1-
!: AUTO INIT
1+
!: BOLT 3
2+
!: AUTO HELLO
3+
!: AUTO GOODBYE
24
!: AUTO RESET
35

4-
C: RUN "BEGIN" {"bookmark": "bookmark:1", "bookmarks": ["bookmark:1"]}
5-
DISCARD_ALL
6+
C: BEGIN {"bookmarks": ["bookmark:1"]}
67
S: SUCCESS {}
7-
SUCCESS {}
8-
C: RUN "COMMIT" {}
9-
DISCARD_ALL
10-
S: SUCCESS {"bookmark": "bookmark:2", "bookmarks": ["bookmark:2"]}
11-
SUCCESS {}
8+
C: COMMIT
9+
S: SUCCESS {"bookmark": "bookmark:2"}
1210

13-
C: RUN "RETURN 1" {}
11+
C: RUN "RETURN 1" {} {"bookmarks": ["bookmark:2"]}
1412
PULL_ALL
15-
S: SUCCESS {"bookmark": "bookmark:x", "bookmarks": ["bookmark:x"]}
13+
S: SUCCESS {"bookmark": "bookmark:3"}
1614
SUCCESS {}
1715

18-
C: RUN "BEGIN" {"bookmark": "bookmark:2", "bookmarks": ["bookmark:2"]}
19-
DISCARD_ALL
16+
C: BEGIN {"bookmarks": ["bookmark:3"]}
2017
S: SUCCESS {}
21-
SUCCESS {}
22-
C: RUN "COMMIT" {}
23-
DISCARD_ALL
24-
S: SUCCESS {"bookmark": "bookmark:3", "bookmarks": ["bookmark:3"]}
25-
SUCCESS {}
18+
C: COMMIT
19+
S: SUCCESS {"bookmark": "bookmark:4"}

test/stub/test_bookmarking.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def test_should_automatically_chain_bookmarks(self):
5959
pass
6060
assert session.last_bookmark() == "bookmark:3"
6161

62-
def test_autocommit_transaction_does_not_break_chain(self):
62+
def test_autocommit_transaction_included_in_chain(self):
6363
with StubCluster({9001: "router.script", 9004: "bookmark_chain_with_autocommit.script"}):
6464
uri = "bolt+routing://localhost:9001"
6565
with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
@@ -68,7 +68,7 @@ def test_autocommit_transaction_does_not_break_chain(self):
6868
pass
6969
assert session.last_bookmark() == "bookmark:2"
7070
session.run("RETURN 1").consume()
71-
assert session.last_bookmark() == "bookmark:2"
71+
assert session.last_bookmark() == "bookmark:3"
7272
with session.begin_transaction():
7373
pass
74-
assert session.last_bookmark() == "bookmark:3"
74+
assert session.last_bookmark() == "bookmark:4"

test/unit/test_types.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
from unittest import TestCase
2323

2424
from neobolt.packstream import Structure
25-
from neo4j.v1.types import PackStreamHydrator
26-
from neo4j.v1.types.graph import Node, Path, Graph
25+
from neo4j.types import PackStreamHydrator
26+
from neo4j.types.graph import Node, Path, Graph
2727

2828

2929
class NodeTestCase(TestCase):

0 commit comments

Comments
 (0)