Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9aa5ddc
Add optional query_params parameter to QueryMessage
sylwiaszunejko Mar 12, 2026
8bba6eb
Introduce skip_scylla_version_lt for integration tests
sylwiaszunejko Mar 13, 2026
bc864c1
Add client routes data types and route store
sylwiaszunejko Mar 17, 2026
ac91295
Add client routes handler for Private Link support
sylwiaszunejko Mar 17, 2026
1e0e6ca
Add ClientRoutesEndPoint and ClientRoutesEndPointFactory
sylwiaszunejko Mar 10, 2026
3202302
Integrate client routes handler into Cluster and ControlConnection
sylwiaszunejko Mar 10, 2026
b205f83
tests: add unit tests for client routes
sylwiaszunejko Mar 10, 2026
6743edd
tests: add integration tests for client routes
sylwiaszunejko Mar 11, 2026
efdc08a
Release 3.29.9: changelog, version and documentation
dkropachev Mar 18, 2026
fec90ae
Specify auth superuser name for tests (#759)
sylwiaszunejko Mar 23, 2026
153c913
(improvement) cqltypes: fast-path lookup_casstype() for simple type n…
mykaul Mar 6, 2026
70995bd
tests: remove redundant 10s sleep from setup_keyspace()
mykaul Mar 27, 2026
7931113
tests: replace high-priority time.sleep() calls with polling
mykaul Mar 27, 2026
4a23f72
tests: replace medium-priority time.sleep() calls with polling
mykaul Mar 27, 2026
9fe9931
tests: fix flaky tablet tests by increasing trace timeout and polling…
mykaul Mar 29, 2026
d31ea37
tests: replace fixed time.sleep() calls with polling (~17s saving)
mykaul Mar 29, 2026
e2a9511
Replace SCYLLA_EXT_OPTS env var with ccm updateconf options for auth …
sylwiaszunejko Mar 31, 2026
44cf752
chore(deps): update dependency pygments to v2.20.0 [security]
renovate[bot] Mar 30, 2026
d5f9d37
(fix) cluster: handle None control_connection_timeout in wait_for_sch…
mykaul Mar 26, 2026
94438c6
tests: fix flaky TestTwistedConnection.test_connection_initialization
mykaul Mar 26, 2026
4bff340
fix: correct 'clustering_key' to 'clustering' in column kind filter
mykaul Mar 24, 2026
ad12bed
chore(deps): update dependency tornado to v6.5.5 [security]
renovate[bot] Mar 30, 2026
c898583
metadata: conditionally skip triggers query for ScyllaDB
mykaul Dec 23, 2025
c3e2378
Fix code quality issues in test_cluster.py
Copilot Dec 23, 2025
8e6c4d4
Fix additional '== None' comparison for consistency
Copilot Dec 23, 2025
caa98b6
pool: drop per-query connection log
avikivity Apr 4, 2026
a662281
(improvement) cache namedtuple class in named_tuple_factory to avoid …
mykaul Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
3.29.9
======
March 18, 2026

Features
--------
* Add Private Link support via client routes handler
* Add optional query_params parameter to QueryMessage

Bug Fixes
---------
* Fix segmentation fault in libev prepare_callback during shutdown
* Add null checks to io_callback and timer_callback in libev wrapper
* Fix RecursionError in execute_concurrent on synchronous errbacks
* Fix floating-point precision loss for timestamps far from epoch

Others
------
* Cache parsed tablet routing type in ResponseFuture
* Remove deprecated setup_requires in favor of PEP 517 build-system.requires
* Update dependency hatchling to v1.29.0

3.29.8
======
February 09, 2026
Expand Down
206 changes: 206 additions & 0 deletions benchmarks/test_named_tuple_factory_benchmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# Copyright ScyllaDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Benchmarks for named_tuple_factory with and without namedtuple class caching.

Run with: pytest benchmarks/test_named_tuple_factory_benchmark.py -v
"""

import re
import warnings
from collections import namedtuple

import pytest

from cassandra.query import named_tuple_factory, _named_tuple_cache
from cassandra.util import _sanitize_identifiers


# ---------------------------------------------------------------------------
# Reference: original uncached implementation (copied from master)
# ---------------------------------------------------------------------------

NON_ALPHA_REGEX = re.compile("[^a-zA-Z0-9]")
START_BADCHAR_REGEX = re.compile("^[^a-zA-Z0-9]*")
END_BADCHAR_REGEX = re.compile("[^a-zA-Z0-9_]*$")

_clean_name_cache_old = {}


def _clean_column_name_old(name):
try:
return _clean_name_cache_old[name]
except KeyError:
clean = NON_ALPHA_REGEX.sub(
"_", START_BADCHAR_REGEX.sub("", END_BADCHAR_REGEX.sub("", name))
)
_clean_name_cache_old[name] = clean
return clean


def named_tuple_factory_uncached(colnames, rows):
"""Original implementation without caching (for benchmark comparison)."""
clean_column_names = map(_clean_column_name_old, colnames)
try:
Row = namedtuple("Row", clean_column_names)
except SyntaxError:
raise
except Exception:
clean_column_names = list(map(_clean_column_name_old, colnames))
Row = namedtuple("Row", _sanitize_identifiers(clean_column_names))
return [Row(*row) for row in rows]


# ---------------------------------------------------------------------------
# Test data generators
# ---------------------------------------------------------------------------


def make_colnames(n):
return tuple(f"col_{i}" for i in range(n))


def make_rows(ncols, nrows):
return [tuple(range(ncols)) for _ in range(nrows)]


# ---------------------------------------------------------------------------
# Correctness tests
# ---------------------------------------------------------------------------


class TestNamedTupleFactoryCorrectness:
"""Verify the cached implementation matches the uncached one."""

@pytest.mark.parametrize("ncols", [1, 5, 10, 20])
@pytest.mark.parametrize("nrows", [1, 10, 100])
def test_results_match(self, ncols, nrows):
colnames = make_colnames(ncols)
rows = make_rows(ncols, nrows)
_named_tuple_cache.clear()
cached_result = named_tuple_factory(colnames, rows)
uncached_result = named_tuple_factory_uncached(colnames, rows)
assert len(cached_result) == len(uncached_result)
for cr, ur in zip(cached_result, uncached_result):
assert tuple(cr) == tuple(ur)
assert cr._fields == ur._fields

def test_cache_hit_returns_same_class(self):
colnames = ("name", "age", "email")
rows1 = [("Alice", 30, "a@b.com")]
rows2 = [("Bob", 25, "b@c.com")]
_named_tuple_cache.clear()
result1 = named_tuple_factory(colnames, rows1)
result2 = named_tuple_factory(colnames, rows2)
# Same Row class should be reused
assert type(result1[0]) is type(result2[0])

def test_different_schemas_get_different_classes(self):
_named_tuple_cache.clear()
result1 = named_tuple_factory(("a", "b"), [(1, 2)])
result2 = named_tuple_factory(("x", "y"), [(3, 4)])
assert type(result1[0]) is not type(result2[0])
assert result1[0]._fields == ("a", "b")
assert result2[0]._fields == ("x", "y")


# ---------------------------------------------------------------------------
# Benchmarks
# ---------------------------------------------------------------------------


class TestNamedTupleFactoryBenchmark:
"""Benchmark cached vs uncached named_tuple_factory."""

# --- 5 columns, 100 rows ---

@pytest.mark.benchmark(group="ntf_5cols_100rows")
def test_uncached_5cols_100rows(self, benchmark):
colnames = make_colnames(5)
rows = make_rows(5, 100)
benchmark(named_tuple_factory_uncached, colnames, rows)

@pytest.mark.benchmark(group="ntf_5cols_100rows")
def test_cached_5cols_100rows(self, benchmark):
colnames = make_colnames(5)
rows = make_rows(5, 100)
_named_tuple_cache.clear()
# Warm the cache with one call
named_tuple_factory(colnames, rows)
benchmark(named_tuple_factory, colnames, rows)

# --- 10 columns, 100 rows ---

@pytest.mark.benchmark(group="ntf_10cols_100rows")
def test_uncached_10cols_100rows(self, benchmark):
colnames = make_colnames(10)
rows = make_rows(10, 100)
benchmark(named_tuple_factory_uncached, colnames, rows)

@pytest.mark.benchmark(group="ntf_10cols_100rows")
def test_cached_10cols_100rows(self, benchmark):
colnames = make_colnames(10)
rows = make_rows(10, 100)
_named_tuple_cache.clear()
named_tuple_factory(colnames, rows)
benchmark(named_tuple_factory, colnames, rows)

# --- 20 columns, 100 rows ---

@pytest.mark.benchmark(group="ntf_20cols_100rows")
def test_uncached_20cols_100rows(self, benchmark):
colnames = make_colnames(20)
rows = make_rows(20, 100)
benchmark(named_tuple_factory_uncached, colnames, rows)

@pytest.mark.benchmark(group="ntf_20cols_100rows")
def test_cached_20cols_100rows(self, benchmark):
colnames = make_colnames(20)
rows = make_rows(20, 100)
_named_tuple_cache.clear()
named_tuple_factory(colnames, rows)
benchmark(named_tuple_factory, colnames, rows)

# --- 5 columns, 1000 rows ---

@pytest.mark.benchmark(group="ntf_5cols_1000rows")
def test_uncached_5cols_1000rows(self, benchmark):
colnames = make_colnames(5)
rows = make_rows(5, 1000)
benchmark(named_tuple_factory_uncached, colnames, rows)

@pytest.mark.benchmark(group="ntf_5cols_1000rows")
def test_cached_5cols_1000rows(self, benchmark):
colnames = make_colnames(5)
rows = make_rows(5, 1000)
_named_tuple_cache.clear()
named_tuple_factory(colnames, rows)
benchmark(named_tuple_factory, colnames, rows)

# --- 10 columns, 1 row (measures class creation overhead most clearly) ---

@pytest.mark.benchmark(group="ntf_10cols_1row")
def test_uncached_10cols_1row(self, benchmark):
colnames = make_colnames(10)
rows = make_rows(10, 1)
benchmark(named_tuple_factory_uncached, colnames, rows)

@pytest.mark.benchmark(group="ntf_10cols_1row")
def test_cached_10cols_1row(self, benchmark):
colnames = make_colnames(10)
rows = make_rows(10, 1)
_named_tuple_cache.clear()
named_tuple_factory(colnames, rows)
benchmark(named_tuple_factory, colnames, rows)
2 changes: 1 addition & 1 deletion cassandra/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def emit(self, record):

logging.getLogger('cassandra').addHandler(NullHandler())

__version_info__ = (3, 29, 8)
__version_info__ = (3, 29, 9)
__version__ = '.'.join(map(str, __version_info__))


Expand Down
Loading
Loading