Skip to content

Commit 8ed0269

Browse files
committed
(improvement) metadata: replace dict_factory with lightweight _RowView
Introduce _RowView, a __slots__-based read-only row wrapper that stores data as tuples with a shared column-name-to-index map, and _row_factory that creates these views. Replace dict_factory in _SchemaParser._handle_results and get_column_from_system_local (both reachable from the V4 code path). This eliminates per-row dict allocation during schema parsing. All rows from the same result set share a single index map object. Also refactor SchemaParserV4._build_keyspace_metadata_internal to read from the row without mutating it, since _RowView is read-only. Note: V22-only dict_factory call sites are left unchanged as they do not affect the V3/V4 code path (V3 and V4 fully override _query_all).
1 parent aefde67 commit 8ed0269

2 files changed

Lines changed: 115 additions & 8 deletions

File tree

cassandra/metadata.py

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,46 @@
4646
from cassandra.tablets import Tablets
4747
from cassandra.util import maybe_add_timeout_to_query
4848

49+
50+
class _RowView(object):
51+
"""
52+
Lightweight read-only view over a row tuple, supporting dict-like access.
53+
Shares a single index map across all rows from the same result set,
54+
avoiding per-row dict allocation overhead.
55+
"""
56+
57+
__slots__ = ("_row", "_index_map")
58+
59+
def __init__(self, row, index_map):
60+
self._row = row
61+
self._index_map = index_map
62+
63+
def __getitem__(self, key):
64+
return self._row[self._index_map[key]]
65+
66+
def get(self, key, default=None):
67+
idx = self._index_map.get(key)
68+
if idx is not None:
69+
return self._row[idx]
70+
return default
71+
72+
def __contains__(self, key):
73+
return key in self._index_map
74+
75+
def __repr__(self):
76+
return repr({k: self._row[i] for k, i in self._index_map.items()})
77+
78+
79+
def _row_factory(colnames, rows):
80+
"""
81+
Lightweight replacement for dict_factory used internally by schema parsers.
82+
Returns a list of _RowView objects that support row["key"] and row.get("key")
83+
but store data as tuples with a shared column-name-to-index map.
84+
"""
85+
index_map = {name: i for i, name in enumerate(colnames)}
86+
return [_RowView(row, index_map) for row in rows]
87+
88+
4989
log = logging.getLogger(__name__)
5090

5191
cql_keywords = set((
@@ -1927,7 +1967,7 @@ def get_next_pages():
19271967
yield next_result.parsed_rows
19281968

19291969
result.parsed_rows += itertools.chain(*get_next_pages())
1930-
return dict_factory(result.column_names, result.parsed_rows) if result else []
1970+
return _row_factory(result.column_names, result.parsed_rows) if result else []
19311971
else:
19321972
raise result
19331973

@@ -2976,11 +3016,13 @@ def get_all_keyspaces(self):
29763016

29773017
@staticmethod
29783018
def _build_keyspace_metadata_internal(row):
2979-
# necessary fields that aren't int virtual ks
2980-
row["durable_writes"] = row.get("durable_writes", None)
2981-
row["replication"] = row.get("replication", {})
2982-
row["replication"]["class"] = row["replication"].get("class", None)
2983-
return super(SchemaParserV4, SchemaParserV4)._build_keyspace_metadata_internal(row)
3019+
# necessary fields that aren't in virtual ks — read without mutating the row
3020+
name = row["keyspace_name"]
3021+
durable_writes = row.get("durable_writes", None)
3022+
replication = dict(row.get("replication")) if "replication" in row else {}
3023+
replication_class = replication.pop("class") if "class" in replication else None
3024+
graph_engine = row.get("graph_engine", None)
3025+
return KeyspaceMetadata(name, durable_writes, replication_class, replication, graph_engine)
29843026

29853027

29863028
class SchemaParserDSE67(SchemaParserV4):
@@ -3375,7 +3417,7 @@ def get_column_from_system_local(connection, column_name: str, timeout, metadata
33753417
, timeout=timeout, fail_on_error=False)
33763418
if not success or not local_result.parsed_rows:
33773419
return ""
3378-
local_rows = dict_factory(local_result.column_names, local_result.parsed_rows)
3420+
local_rows = _row_factory(local_result.column_names, local_result.parsed_rows)
33793421
local_row = local_rows[0]
33803422
return local_row.get(column_name)
33813423

tests/unit/test_metadata.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
_UnknownStrategy, ColumnMetadata, TableMetadata,
3333
IndexMetadata, Function, Aggregate,
3434
Metadata, TokenMap, ReplicationFactor,
35-
SchemaParserDSE68)
35+
SchemaParserDSE68,
36+
_RowView, _row_factory)
3637
from cassandra.policies import SimpleConvictionPolicy
3738
from cassandra.pool import Host
3839
from cassandra.protocol import QueryMessage
@@ -846,3 +847,67 @@ def test_strip_frozen(self):
846847
for argument, expected_result in argument_to_expected_results:
847848
result = strip_frozen(argument)
848849
assert result == expected_result, "strip_frozen() arg: {}".format(argument)
850+
851+
852+
class RowViewTest(unittest.TestCase):
853+
"""Tests for the internal _RowView and _row_factory helpers."""
854+
855+
def test_getitem(self):
856+
rv = _RowView(("a_val", "b_val"), {"a": 0, "b": 1})
857+
self.assertEqual(rv["a"], "a_val")
858+
self.assertEqual(rv["b"], "b_val")
859+
860+
def test_getitem_missing_key(self):
861+
rv = _RowView(("a_val",), {"a": 0})
862+
with self.assertRaises(KeyError):
863+
rv["missing"]
864+
865+
def test_get_present(self):
866+
rv = _RowView(("a_val", "b_val"), {"a": 0, "b": 1})
867+
self.assertEqual(rv.get("a"), "a_val")
868+
self.assertEqual(rv.get("b"), "b_val")
869+
870+
def test_get_missing_returns_default(self):
871+
rv = _RowView(("a_val",), {"a": 0})
872+
self.assertIsNone(rv.get("missing"))
873+
self.assertEqual(rv.get("missing", 42), 42)
874+
875+
def test_contains(self):
876+
rv = _RowView(("a_val",), {"a": 0})
877+
self.assertIn("a", rv)
878+
self.assertNotIn("b", rv)
879+
880+
def test_repr(self):
881+
rv = _RowView(("a_val", "b_val"), {"a": 0, "b": 1})
882+
r = repr(rv)
883+
self.assertIn("'a'", r)
884+
self.assertIn("'a_val'", r)
885+
886+
def test_shared_index_map(self):
887+
"""All _RowView objects from the same _row_factory call share one index map."""
888+
rows = _row_factory(["x", "y"], [("x1", "y1"), ("x2", "y2")])
889+
self.assertIs(rows[0]._index_map, rows[1]._index_map)
890+
891+
def test_read_only(self):
892+
"""_RowView must not allow item assignment or deletion."""
893+
rv = _RowView(("val",), {"col": 0})
894+
with self.assertRaises(TypeError):
895+
rv["col"] = "new"
896+
with self.assertRaises(TypeError):
897+
del rv["col"]
898+
899+
def test_row_factory_empty(self):
900+
result = _row_factory(["a", "b"], [])
901+
self.assertEqual(result, [])
902+
903+
def test_row_factory_single_column(self):
904+
rows = _row_factory(["only"], [("v1",), ("v2",)])
905+
self.assertEqual(rows[0]["only"], "v1")
906+
self.assertEqual(rows[1]["only"], "v2")
907+
908+
def test_row_factory_values(self):
909+
rows = _row_factory(["id", "name"], [(1, "alice"), (2, "bob")])
910+
self.assertEqual(rows[0]["id"], 1)
911+
self.assertEqual(rows[0]["name"], "alice")
912+
self.assertEqual(rows[1]["id"], 2)
913+
self.assertEqual(rows[1]["name"], "bob")

0 commit comments

Comments
 (0)