Skip to content

Commit 7224945

Browse files
committed
perf: optimize was_applied fast path for known LWT statements
Add a fast path in ResultSet.was_applied that skips batch detection (isinstance checks + regex match) when the query has a known LWT status from the server PREPARE response. For BoundStatement queries where is_lwt() returns True, the batch_regex match on the query string is entirely avoided. This benefits the most common LWT use case: prepared INSERT/UPDATE IF statements executed via BoundStatement, where the driver already knows from the PREPARE response whether the statement is an LWT. The slow path (isinstance + regex) is preserved for: - BatchStatement queries (detected via isinstance) - SimpleStatement batch queries (detected via regex) - Any query where is_lwt() returns False Also adds explicit tests for the fast path, non-LWT fallback, and BatchStatement handling in was_applied. Part of: scylladb#751
1 parent 8e6c4d4 commit 7224945

2 files changed

Lines changed: 81 additions & 11 deletions

File tree

cassandra/cluster.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5430,13 +5430,22 @@ def was_applied(self):
54305430
if self.response_future.row_factory not in (named_tuple_factory, dict_factory, tuple_factory):
54315431
raise RuntimeError("Cannot determine LWT result with row factory %s" % (self.response_future.row_factory,))
54325432

5433-
is_batch_statement = isinstance(self.response_future.query, BatchStatement) \
5434-
or (isinstance(self.response_future.query, SimpleStatement) and self.batch_regex.match(self.response_future.query.query_string))
5435-
if is_batch_statement and (not self.column_names or self.column_names[0] != "[applied]"):
5436-
raise RuntimeError("No LWT were present in the BatchStatement")
5433+
query = self.response_future.query
5434+
5435+
# Fast path: BoundStatement/PreparedStatement with known LWT status
5436+
# from the server PREPARE response avoids batch detection entirely.
5437+
if query.is_lwt() and not isinstance(query, BatchStatement):
5438+
# Known single LWT statement - skip batch detection
5439+
if len(self.current_rows) != 1:
5440+
raise RuntimeError("LWT result should have exactly one row. This has %d." % (len(self.current_rows)))
5441+
else:
5442+
is_batch_statement = isinstance(query, BatchStatement) \
5443+
or (isinstance(query, SimpleStatement) and self.batch_regex.match(query.query_string))
5444+
if is_batch_statement and (not self.column_names or self.column_names[0] != "[applied]"):
5445+
raise RuntimeError("No LWT were present in the BatchStatement")
54375446

5438-
if not is_batch_statement and len(self.current_rows) != 1:
5439-
raise RuntimeError("LWT result should have exactly one row. This has %d." % (len(self.current_rows)))
5447+
if not is_batch_statement and len(self.current_rows) != 1:
5448+
raise RuntimeError("LWT result should have exactly one row. This has %d." % (len(self.current_rows)))
54405449

54415450
row = self.current_rows[0]
54425451
if isinstance(row, tuple):

tests/unit/test_resultset.py

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from unittest.mock import Mock, PropertyMock, patch
1717

1818
from cassandra.cluster import ResultSet
19-
from cassandra.query import named_tuple_factory, dict_factory, tuple_factory
19+
from cassandra.query import named_tuple_factory, dict_factory, tuple_factory, SimpleStatement, BatchStatement
2020

2121
from tests.util import assertListEqual
2222
import pytest
@@ -175,11 +175,18 @@ def test_bool(self):
175175
assert ResultSet(Mock(has_more_pages=False), [1])
176176

177177
def test_was_applied(self):
178+
# Create a non-LWT query so these assertions exercise the slow (regex) path.
179+
# Without this, Mock().query.is_lwt() returns a truthy Mock, accidentally
180+
# routing all checks through the fast path.
181+
non_lwt_query = Mock(spec=SimpleStatement)
182+
non_lwt_query.is_lwt.return_value = False
183+
non_lwt_query.query_string = "INSERT INTO t (k) VALUES (1)"
184+
178185
# unknown row factory raises
179186
with pytest.raises(RuntimeError):
180-
ResultSet(Mock(), []).was_applied
187+
ResultSet(Mock(query=non_lwt_query), []).was_applied
181188

182-
response_future = Mock(row_factory=named_tuple_factory)
189+
response_future = Mock(row_factory=named_tuple_factory, query=non_lwt_query)
183190

184191
# no row
185192
with pytest.raises(RuntimeError):
@@ -192,14 +199,68 @@ def test_was_applied(self):
192199
# various internal row factories
193200
for row_factory in (named_tuple_factory, tuple_factory):
194201
for applied in (True, False):
195-
rs = ResultSet(Mock(row_factory=row_factory), [(applied,)])
202+
rs = ResultSet(Mock(row_factory=row_factory, query=non_lwt_query), [(applied,)])
196203
assert rs.was_applied == applied
197204

198205
row_factory = dict_factory
199206
for applied in (True, False):
200-
rs = ResultSet(Mock(row_factory=row_factory), [{'[applied]': applied}])
207+
rs = ResultSet(Mock(row_factory=row_factory, query=non_lwt_query), [{'[applied]': applied}])
201208
assert rs.was_applied == applied
202209

210+
211+
def test_was_applied_lwt_fast_path(self):
212+
"""Test that was_applied uses fast path for known LWT statements."""
213+
# BoundStatement-like query with is_lwt() = True (fast path)
214+
lwt_query = Mock()
215+
lwt_query.is_lwt.return_value = True
216+
for row_factory in (named_tuple_factory, tuple_factory):
217+
for applied in (True, False):
218+
rf = Mock(row_factory=row_factory, query=lwt_query)
219+
rs = ResultSet(rf, [(applied,)])
220+
assert rs.was_applied == applied
221+
222+
for applied in (True, False):
223+
rf = Mock(row_factory=dict_factory, query=lwt_query)
224+
rs = ResultSet(rf, [{'[applied]': applied}])
225+
assert rs.was_applied == applied
226+
227+
# Fast path with too many rows should raise
228+
rf = Mock(row_factory=named_tuple_factory, query=lwt_query)
229+
with pytest.raises(RuntimeError, match="exactly one row"):
230+
ResultSet(rf, [tuple(), tuple()]).was_applied
231+
232+
def test_was_applied_non_lwt_fallback(self):
233+
"""Test that was_applied falls back to slow path for non-LWT statements."""
234+
# SimpleStatement-like query with is_lwt() = False (slow path, non-batch)
235+
non_lwt_query = Mock(spec=SimpleStatement)
236+
non_lwt_query.is_lwt.return_value = False
237+
non_lwt_query.query_string = "INSERT INTO t (k) VALUES (1)"
238+
239+
for applied in (True, False):
240+
rf = Mock(row_factory=tuple_factory, query=non_lwt_query)
241+
rs = ResultSet(rf, [(applied,)])
242+
assert rs.was_applied == applied
243+
244+
def test_was_applied_batch_statement(self):
245+
"""Test that was_applied handles BatchStatement correctly (slow path)."""
246+
# BatchStatement with LWT should check column_names
247+
batch_query = Mock(spec=BatchStatement)
248+
batch_query.is_lwt.return_value = True
249+
250+
# Batch with [applied] column -- pass _col_names so ResultSet.__init__
251+
# sets column_names correctly (instead of post-construction override).
252+
rf = Mock(row_factory=tuple_factory, query=batch_query,
253+
_col_names=['[applied]'], _col_types=None)
254+
rs = ResultSet(rf, [(True,)])
255+
assert rs.was_applied == True
256+
257+
# Batch without [applied] column raises
258+
rf = Mock(row_factory=tuple_factory, query=batch_query,
259+
_col_names=['other'], _col_types=None)
260+
rs = ResultSet(rf, [(True,)])
261+
with pytest.raises(RuntimeError, match="No LWT were present"):
262+
rs.was_applied
263+
203264
def test_one(self):
204265
# no pages
205266
first, second = Mock(), Mock()

0 commit comments

Comments
 (0)