Skip to content

Commit ae1ae31

Browse files
committed
PERF: Optimize fetch API performance with cached encodings, skip diag on SUCCESS, __slots__ Row, and C++ Row construction - Cache decoding encoding strings in cursor __init__ to avoid 2 method calls + 2 dict.get() per fetch - Skip DDBCSQLGetAllDiagRecords on SQL_SUCCESS (ODBC spec: zero records on SUCCESS) - Replace param.encode('ascii') try/except with str.isascii() (C-level check) - Class-level _SQL_TO_C_TYPE lookup table (built once, shared across cursors) - Add __slots__ to Row class (eliminates per-instance __dict__, ~232 bytes/row savings) - Add Row._fast_create static method (bypasses __init__ for common case) - Add C++ construct_rows function (builds Row objects in tight C loop, avoiding Python loop overhead) - Zero-copy Row fast path when no converters/UUID processing needed Benchmark results (5-run average, richbench repeat=5 number=5): - Fetch one: -1.7x -> -1.4x (18% improvement) - Fetch many: -1.7x -> -1.3x (24% improvement) - 100 inserts: 4.9x -> 5.6x (14% faster) - SELECT: -1.1x -> -1.0x (on par with pyodbc) Profiler wall clock (50K rows): - fetchall: 176.7ms -> 158.1ms (11% faster) - fetchmany: 166.6ms -> 138.6ms (17% faster) No overlap with PR #549 (execute fast path) or PR #526 (simdutf).
1 parent 00ea71c commit ae1ae31

3 files changed

Lines changed: 179 additions & 74 deletions

File tree

mssql_python/cursor.py

Lines changed: 84 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@ def __init__(self, connection: "Connection", timeout: int = 0) -> None:
158158
self._conn_native_uuid = getattr(self.connection, "_native_uuid", None)
159159
self._next_row_index = 0 # internal: index of the next row the driver will return (0-based)
160160
self._has_result_set = False # Track if we have an active result set
161+
# Cache decoding encoding strings — these don't change between fetches,
162+
# so we avoid 2 method calls + 2 dict.get() per fetch call.
163+
self._cached_char_encoding = self._get_decoding_settings(
164+
ddbc_sql_const.SQL_CHAR.value
165+
).get("encoding", "utf-8")
166+
self._cached_wchar_encoding = self._get_decoding_settings(
167+
ddbc_sql_const.SQL_WCHAR.value
168+
).get("encoding", "utf-16le")
161169
self._skip_increment_for_next_fetch = (
162170
False # Track if we need to skip incrementing the row index
163171
)
@@ -173,11 +181,7 @@ def _is_unicode_string(self, param: str) -> bool:
173181
Returns:
174182
True if the string contains non-ASCII characters, False otherwise.
175183
"""
176-
try:
177-
param.encode("ascii")
178-
return False # Can be encoded to ASCII, so not Unicode
179-
except UnicodeEncodeError:
180-
return True # Contains non-ASCII characters, so treat as Unicode
184+
return not param.isascii()
181185

182186
def _parse_date(self, param: str) -> Optional[datetime.date]:
183187
"""
@@ -895,45 +899,51 @@ def _reset_inputsizes(self) -> None:
895899
"""Reset input sizes after execution"""
896900
self._inputsizes = None
897901

902+
# Pre-built constant lookup table — avoids rebuilding ~30 entries on every call.
903+
# Used by setinputsizes fallback path (PR #549 fast path doesn't need this).
904+
_SQL_TO_C_TYPE = None
905+
906+
@classmethod
907+
def _get_sql_to_c_type_map(cls):
908+
if cls._SQL_TO_C_TYPE is None:
909+
cls._SQL_TO_C_TYPE = {
910+
ddbc_sql_const.SQL_CHAR.value: ddbc_sql_const.SQL_C_CHAR.value,
911+
ddbc_sql_const.SQL_VARCHAR.value: ddbc_sql_const.SQL_C_CHAR.value,
912+
ddbc_sql_const.SQL_LONGVARCHAR.value: ddbc_sql_const.SQL_C_CHAR.value,
913+
ddbc_sql_const.SQL_WCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value,
914+
ddbc_sql_const.SQL_WVARCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value,
915+
ddbc_sql_const.SQL_WLONGVARCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value,
916+
ddbc_sql_const.SQL_DECIMAL.value: ddbc_sql_const.SQL_C_NUMERIC.value,
917+
ddbc_sql_const.SQL_NUMERIC.value: ddbc_sql_const.SQL_C_NUMERIC.value,
918+
ddbc_sql_const.SQL_BIT.value: ddbc_sql_const.SQL_C_BIT.value,
919+
ddbc_sql_const.SQL_TINYINT.value: ddbc_sql_const.SQL_C_TINYINT.value,
920+
ddbc_sql_const.SQL_SMALLINT.value: ddbc_sql_const.SQL_C_SHORT.value,
921+
ddbc_sql_const.SQL_INTEGER.value: ddbc_sql_const.SQL_C_LONG.value,
922+
ddbc_sql_const.SQL_BIGINT.value: ddbc_sql_const.SQL_C_SBIGINT.value,
923+
ddbc_sql_const.SQL_REAL.value: ddbc_sql_const.SQL_C_FLOAT.value,
924+
ddbc_sql_const.SQL_FLOAT.value: ddbc_sql_const.SQL_C_DOUBLE.value,
925+
ddbc_sql_const.SQL_DOUBLE.value: ddbc_sql_const.SQL_C_DOUBLE.value,
926+
ddbc_sql_const.SQL_BINARY.value: ddbc_sql_const.SQL_C_BINARY.value,
927+
ddbc_sql_const.SQL_VARBINARY.value: ddbc_sql_const.SQL_C_BINARY.value,
928+
ddbc_sql_const.SQL_LONGVARBINARY.value: ddbc_sql_const.SQL_C_BINARY.value,
929+
ddbc_sql_const.SQL_SS_UDT.value: ddbc_sql_const.SQL_C_BINARY.value,
930+
ddbc_sql_const.SQL_TYPE_DATE.value: ddbc_sql_const.SQL_C_TYPE_DATE.value,
931+
ddbc_sql_const.SQL_TYPE_TIME.value: ddbc_sql_const.SQL_C_TYPE_TIME.value,
932+
ddbc_sql_const.SQL_TYPE_TIMESTAMP.value: ddbc_sql_const.SQL_C_TYPE_TIMESTAMP.value,
933+
ddbc_sql_const.SQL_SS_TIME2.value: ddbc_sql_const.SQL_C_TYPE_TIME.value,
934+
ddbc_sql_const.SQL_DATETIMEOFFSET.value: ddbc_sql_const.SQL_C_SS_TIMESTAMPOFFSET.value,
935+
ddbc_sql_const.SQL_DATE.value: ddbc_sql_const.SQL_C_TYPE_DATE.value,
936+
ddbc_sql_const.SQL_TIME.value: ddbc_sql_const.SQL_C_TYPE_TIME.value,
937+
ddbc_sql_const.SQL_TIMESTAMP.value: ddbc_sql_const.SQL_C_TYPE_TIMESTAMP.value,
938+
ddbc_sql_const.SQL_GUID.value: ddbc_sql_const.SQL_C_GUID.value,
939+
ddbc_sql_const.SQL_SS_XML.value: ddbc_sql_const.SQL_C_WCHAR.value,
940+
ddbc_sql_const.SQL_SS_VARIANT.value: ddbc_sql_const.SQL_C_BINARY.value,
941+
}
942+
return cls._SQL_TO_C_TYPE
943+
898944
def _get_c_type_for_sql_type(self, sql_type: int) -> int:
899945
"""Map SQL type to appropriate C type for parameter binding."""
900-
sql_to_c_type = {
901-
ddbc_sql_const.SQL_CHAR.value: ddbc_sql_const.SQL_C_CHAR.value,
902-
ddbc_sql_const.SQL_VARCHAR.value: ddbc_sql_const.SQL_C_CHAR.value,
903-
ddbc_sql_const.SQL_LONGVARCHAR.value: ddbc_sql_const.SQL_C_CHAR.value,
904-
ddbc_sql_const.SQL_WCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value,
905-
ddbc_sql_const.SQL_WVARCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value,
906-
ddbc_sql_const.SQL_WLONGVARCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value,
907-
ddbc_sql_const.SQL_DECIMAL.value: ddbc_sql_const.SQL_C_NUMERIC.value,
908-
ddbc_sql_const.SQL_NUMERIC.value: ddbc_sql_const.SQL_C_NUMERIC.value,
909-
ddbc_sql_const.SQL_BIT.value: ddbc_sql_const.SQL_C_BIT.value,
910-
ddbc_sql_const.SQL_TINYINT.value: ddbc_sql_const.SQL_C_TINYINT.value,
911-
ddbc_sql_const.SQL_SMALLINT.value: ddbc_sql_const.SQL_C_SHORT.value,
912-
ddbc_sql_const.SQL_INTEGER.value: ddbc_sql_const.SQL_C_LONG.value,
913-
ddbc_sql_const.SQL_BIGINT.value: ddbc_sql_const.SQL_C_SBIGINT.value,
914-
ddbc_sql_const.SQL_REAL.value: ddbc_sql_const.SQL_C_FLOAT.value,
915-
ddbc_sql_const.SQL_FLOAT.value: ddbc_sql_const.SQL_C_DOUBLE.value,
916-
ddbc_sql_const.SQL_DOUBLE.value: ddbc_sql_const.SQL_C_DOUBLE.value,
917-
ddbc_sql_const.SQL_BINARY.value: ddbc_sql_const.SQL_C_BINARY.value,
918-
ddbc_sql_const.SQL_VARBINARY.value: ddbc_sql_const.SQL_C_BINARY.value,
919-
ddbc_sql_const.SQL_LONGVARBINARY.value: ddbc_sql_const.SQL_C_BINARY.value,
920-
ddbc_sql_const.SQL_SS_UDT.value: ddbc_sql_const.SQL_C_BINARY.value,
921-
# ODBC 3.x date/time types (reported by ODBC 18 driver)
922-
ddbc_sql_const.SQL_TYPE_DATE.value: ddbc_sql_const.SQL_C_TYPE_DATE.value,
923-
ddbc_sql_const.SQL_TYPE_TIME.value: ddbc_sql_const.SQL_C_TYPE_TIME.value,
924-
ddbc_sql_const.SQL_TYPE_TIMESTAMP.value: ddbc_sql_const.SQL_C_TYPE_TIMESTAMP.value,
925-
ddbc_sql_const.SQL_SS_TIME2.value: ddbc_sql_const.SQL_C_TYPE_TIME.value,
926-
ddbc_sql_const.SQL_DATETIMEOFFSET.value: ddbc_sql_const.SQL_C_SS_TIMESTAMPOFFSET.value,
927-
# ODBC 2.x aliases (accepted by setinputsizes via SQLTypes)
928-
ddbc_sql_const.SQL_DATE.value: ddbc_sql_const.SQL_C_TYPE_DATE.value,
929-
ddbc_sql_const.SQL_TIME.value: ddbc_sql_const.SQL_C_TYPE_TIME.value,
930-
ddbc_sql_const.SQL_TIMESTAMP.value: ddbc_sql_const.SQL_C_TYPE_TIMESTAMP.value,
931-
# Other types
932-
ddbc_sql_const.SQL_GUID.value: ddbc_sql_const.SQL_C_GUID.value,
933-
ddbc_sql_const.SQL_SS_XML.value: ddbc_sql_const.SQL_C_WCHAR.value,
934-
ddbc_sql_const.SQL_SS_VARIANT.value: ddbc_sql_const.SQL_C_BINARY.value,
935-
}
936-
return sql_to_c_type.get(sql_type, ddbc_sql_const.SQL_C_DEFAULT.value)
946+
return self._get_sql_to_c_type_map().get(sql_type, ddbc_sql_const.SQL_C_DEFAULT.value)
937947

938948
def _create_parameter_types_list( # pylint: disable=too-many-arguments,too-many-positional-arguments
939949
self,
@@ -2453,26 +2463,27 @@ def fetchone(self) -> Union[None, Row]:
24532463
"""
24542464
self._check_closed() # Check if the cursor is closed
24552465

2456-
char_decoding = self._get_decoding_settings(ddbc_sql_const.SQL_CHAR.value)
2457-
wchar_decoding = self._get_decoding_settings(ddbc_sql_const.SQL_WCHAR.value)
2466+
# Use cached encoding strings — eliminates 2 method calls + 2 dict.get() per fetch
2467+
char_enc = self._cached_char_encoding
2468+
wchar_enc = self._cached_wchar_encoding
24582469

24592470
# Fetch raw data
24602471
row_data = []
24612472
try:
24622473
ret = ddbc_bindings.DDBCSQLFetchOne(
24632474
self.hstmt,
24642475
row_data,
2465-
char_decoding.get("encoding", "utf-8"),
2466-
wchar_decoding.get("encoding", "utf-16le"),
2476+
char_enc,
2477+
wchar_enc,
24672478
)
24682479

2469-
if self.hstmt:
2480+
# Only retrieve diag records on SQL_SUCCESS_WITH_INFO.
2481+
if ret == ddbc_sql_const.SQL_SUCCESS_WITH_INFO.value and self.hstmt:
24702482
self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt))
24712483

24722484
if ret == ddbc_sql_const.SQL_NO_DATA.value:
24732485
# No more data available
24742486
if self._next_row_index == 0 and self.description is not None:
2475-
# This is an empty result set, set rowcount to 0
24762487
self.rowcount = 0
24772488
return None
24782489

@@ -2487,6 +2498,9 @@ def fetchone(self) -> Union[None, Row]:
24872498

24882499
# Get column and converter maps
24892500
column_map, converter_map = self._get_column_and_converter_maps()
2501+
# Fast path: skip __init__ overhead when no converters/UUID processing
2502+
if not converter_map and not self._uuid_str_indices:
2503+
return Row._fast_create(row_data, column_map, self)
24902504
return Row(
24912505
row_data,
24922506
column_map,
@@ -2518,8 +2532,9 @@ def fetchmany(self, size: Optional[int] = None) -> List[Row]:
25182532
if size <= 0:
25192533
return []
25202534

2521-
char_decoding = self._get_decoding_settings(ddbc_sql_const.SQL_CHAR.value)
2522-
wchar_decoding = self._get_decoding_settings(ddbc_sql_const.SQL_WCHAR.value)
2535+
# Use cached encoding strings
2536+
char_enc = self._cached_char_encoding
2537+
wchar_enc = self._cached_wchar_encoding
25232538

25242539
# Fetch raw data
25252540
rows_data = []
@@ -2528,11 +2543,11 @@ def fetchmany(self, size: Optional[int] = None) -> List[Row]:
25282543
self.hstmt,
25292544
rows_data,
25302545
size,
2531-
char_decoding.get("encoding", "utf-8"),
2532-
wchar_decoding.get("encoding", "utf-16le"),
2546+
char_enc,
2547+
wchar_enc,
25332548
)
25342549

2535-
if self.hstmt:
2550+
if ret == ddbc_sql_const.SQL_SUCCESS_WITH_INFO.value and self.hstmt:
25362551
self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt))
25372552

25382553
# Update rownumber for the number of rows actually fetched
@@ -2552,6 +2567,11 @@ def fetchmany(self, size: Optional[int] = None) -> List[Row]:
25522567

25532568
# Convert raw data to Row objects
25542569
uuid_idx = self._uuid_str_indices
2570+
# Fast path: build Row objects in C++ — avoids Python loop overhead
2571+
if not converter_map and not uuid_idx:
2572+
return ddbc_bindings.construct_rows(
2573+
rows_data, Row, column_map, self
2574+
)
25552575
return [
25562576
Row(
25572577
row_data,
@@ -2577,23 +2597,24 @@ def fetchall(self) -> List[Row]:
25772597
if not self._has_result_set and self.description:
25782598
self._reset_rownumber()
25792599

2580-
char_decoding = self._get_decoding_settings(ddbc_sql_const.SQL_CHAR.value)
2581-
wchar_decoding = self._get_decoding_settings(ddbc_sql_const.SQL_WCHAR.value)
2600+
# Use cached encoding strings
2601+
char_enc = self._cached_char_encoding
2602+
wchar_enc = self._cached_wchar_encoding
25822603

25832604
# Fetch raw data
25842605
rows_data = []
25852606
try:
25862607
ret = ddbc_bindings.DDBCSQLFetchAll(
25872608
self.hstmt,
25882609
rows_data,
2589-
char_decoding.get("encoding", "utf-8"),
2590-
wchar_decoding.get("encoding", "utf-16le"),
2610+
char_enc,
2611+
wchar_enc,
25912612
)
25922613

25932614
# Check for errors
25942615
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
25952616

2596-
if self.hstmt:
2617+
if ret == ddbc_sql_const.SQL_SUCCESS_WITH_INFO.value and self.hstmt:
25972618
self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt))
25982619

25992620
# Update rownumber for the number of rows actually fetched
@@ -2612,6 +2633,11 @@ def fetchall(self) -> List[Row]:
26122633

26132634
# Convert raw data to Row objects
26142635
uuid_idx = self._uuid_str_indices
2636+
# Fast path: build Row objects in C++ — avoids Python loop overhead
2637+
if not converter_map and not uuid_idx:
2638+
return ddbc_bindings.construct_rows(
2639+
rows_data, Row, column_map, self
2640+
)
26152641
return [
26162642
Row(
26172643
row_data,

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5850,6 +5850,56 @@ void DDBCSetDecimalSeparator(const std::string& separator) {
58505850
#endif
58515851

58525852
// Functions/data to be exposed to Python as a part of ddbc_bindings module
5853+
// ---------------------------------------------------------------------------
5854+
// construct_rows — Build Row objects entirely in C++.
5855+
//
5856+
// Replaces the Python list comprehension:
5857+
// [Row._fast_create(rd, column_map, cursor) for rd in rows_data]
5858+
//
5859+
// By doing tp_alloc + slot assignment in a tight C loop, this avoids:
5860+
// - Python bytecode dispatch (FOR_ITER, LOAD_FAST, CALL_FUNCTION)
5861+
// - Keyword argument processing overhead per Row
5862+
// - Python function call frame setup per iteration
5863+
//
5864+
// Requires Row to have __slots__ = ('_values', '_column_map', '_cursor').
5865+
// Semantically identical to _fast_create — no converter or UUID processing.
5866+
// ---------------------------------------------------------------------------
5867+
py::list construct_rows(const py::list& rows_data,
5868+
const py::object& row_class,
5869+
const py::object& column_map,
5870+
const py::object& cursor_obj) {
5871+
PyTypeObject* row_type = reinterpret_cast<PyTypeObject*>(row_class.ptr());
5872+
Py_ssize_t n = PyList_GET_SIZE(rows_data.ptr());
5873+
5874+
// Pre-intern slot name strings (cached by CPython after first call)
5875+
static PyObject* attr_values = PyUnicode_InternFromString("_values");
5876+
static PyObject* attr_column_map = PyUnicode_InternFromString("_column_map");
5877+
static PyObject* attr_cursor = PyUnicode_InternFromString("_cursor");
5878+
5879+
py::list result(n);
5880+
5881+
for (Py_ssize_t i = 0; i < n; ++i) {
5882+
// Allocate Row without calling __init__
5883+
PyObject* row = row_type->tp_alloc(row_type, 0);
5884+
if (!row) throw py::error_already_set();
5885+
5886+
PyObject* row_data = PyList_GET_ITEM(rows_data.ptr(), i);
5887+
5888+
// Set __slots__ via GenericSetAttr (uses descriptor offsets — fast path)
5889+
if (PyObject_GenericSetAttr(row, attr_values, row_data) < 0 ||
5890+
PyObject_GenericSetAttr(row, attr_column_map, column_map.ptr()) < 0 ||
5891+
PyObject_GenericSetAttr(row, attr_cursor, cursor_obj.ptr()) < 0) {
5892+
Py_DECREF(row);
5893+
throw py::error_already_set();
5894+
}
5895+
5896+
// PyList_SET_ITEM steals the reference — don't Py_DECREF row
5897+
PyList_SET_ITEM(result.ptr(), i, row);
5898+
}
5899+
5900+
return result;
5901+
}
5902+
58535903
PYBIND11_MODULE(ddbc_bindings, m) {
58545904
m.doc() = "msodbcsql driver api bindings for Python";
58555905

@@ -6007,6 +6057,12 @@ PYBIND11_MODULE(ddbc_bindings, m) {
60076057
// Add a version attribute
60086058
m.attr("__version__") = "1.0.0";
60096059

6060+
// Fast Row construction in C++ — replaces Python list comprehension
6061+
m.def("construct_rows", &construct_rows,
6062+
"Build Row objects in C++ for fetchall/fetchmany fast path",
6063+
py::arg("rows_data"), py::arg("row_class"),
6064+
py::arg("column_map"), py::arg("cursor"));
6065+
60106066
// Expose logger bridge function to Python
60116067
m.def("update_log_level", &mssql_python::logging::LoggerBridge::updateLevel,
60126068
"Update the cached log level in C++ bridge");

mssql_python/row.py

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,24 @@ class Row:
2727
print(row.column_name) # Access by column name (case sensitivity varies)
2828
"""
2929

30+
# __slots__ eliminates per-instance __dict__ (~232 bytes/row savings),
31+
# and makes attribute access ~30% faster (array index vs dict lookup).
32+
__slots__ = ('_values', '_column_map', '_cursor')
33+
34+
@staticmethod
35+
def _fast_create(values, column_map, cursor):
36+
"""Construct a Row bypassing __init__ — for the common fast path.
37+
38+
Used by fetchall/fetchmany when no output converters and no UUID
39+
stringification are needed (the vast majority of queries). Skips
40+
the entire if/elif/else chain and keyword argument overhead in __init__.
41+
"""
42+
r = Row.__new__(Row)
43+
r._values = values
44+
r._column_map = column_map
45+
r._cursor = cursor
46+
return r
47+
3048
def __init__(self, values, column_map, cursor=None, converter_map=None, uuid_str_indices=None):
3149
"""
3250
Initialize a Row object with values and pre-built column map.
@@ -39,24 +57,29 @@ def __init__(self, values, column_map, cursor=None, converter_map=None, uuid_str
3957
converted to str. Pre-computed once per result set when native_uuid=False.
4058
None means no conversion (native_uuid=True, the default).
4159
"""
42-
# Apply output converters if available using pre-computed converter map
43-
if converter_map:
44-
self._values = self._apply_output_converters_optimized(values, converter_map)
45-
elif (
46-
cursor
47-
and hasattr(cursor.connection, "_output_converters")
48-
and cursor.connection._output_converters
49-
):
50-
# Fallback to original method for backward compatibility
51-
self._values = self._apply_output_converters(values, cursor)
60+
# Fast path: no converters and no UUID stringification (common case).
61+
# Avoids the converter_map iteration and list copy entirely.
62+
if not converter_map and not uuid_str_indices:
63+
if (
64+
cursor
65+
and hasattr(cursor.connection, "_output_converters")
66+
and cursor.connection._output_converters
67+
):
68+
# Fallback to original method for backward compatibility
69+
self._values = self._apply_output_converters(values, cursor)
70+
else:
71+
# Zero-copy: just store the reference directly
72+
self._values = values
5273
else:
53-
self._values = values
74+
# Apply output converters if available using pre-computed converter map
75+
if converter_map:
76+
self._values = self._apply_output_converters_optimized(values, converter_map)
77+
else:
78+
self._values = values
5479

55-
# Convert UUID columns to str when native_uuid=False.
56-
# uuid_str_indices is pre-computed once at execute() time, so this is
57-
# O(num_uuid_columns) per row — zero cost when native_uuid=True (the default).
58-
if uuid_str_indices:
59-
self._stringify_uuids(uuid_str_indices)
80+
# Convert UUID columns to str when native_uuid=False.
81+
if uuid_str_indices:
82+
self._stringify_uuids(uuid_str_indices)
6083

6184
self._column_map = column_map
6285
self._cursor = cursor

0 commit comments

Comments
 (0)