Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
081f3e2
OPTIMIZATION #1: Direct PyUnicode_DecodeUTF16 for NVARCHAR conversion…
bewithgaurav Nov 10, 2025
c7d1aa3
OPTIMIZATION #1: Direct PyUnicode_DecodeUTF16 for NVARCHAR conversion…
bewithgaurav Nov 10, 2025
94b8a69
OPTIMIZATION #2: Direct Python C API for numeric types
bewithgaurav Nov 10, 2025
7159d81
docs: Update OPTIMIZATION_PR_SUMMARY with OPT #2 details
bewithgaurav Nov 10, 2025
ef095fd
OPTIMIZATION #3: Metadata prefetch caching
bewithgaurav Nov 10, 2025
7ad0947
OPTIMIZATION #3 (FIX): Remove unused columnSize variables (Windows bu…
bewithgaurav Nov 10, 2025
55fb898
OPTIMIZATION #4: Batch row allocation with direct Python C API
bewithgaurav Nov 10, 2025
e1e827a
docs: Update OPTIMIZATION_PR_SUMMARY with OPT #4 details
bewithgaurav Nov 10, 2025
18e5350
OPTIMIZATION #4 (FIX): Remove unused columnSize variables (Windows bu…
bewithgaurav Nov 10, 2025
3c195f6
OPTIMIZATION #5: Function pointer dispatch for column processors
bewithgaurav Nov 10, 2025
c30974c
docs: Complete OPTIMIZATION_PR_SUMMARY with OPT #3 and OPT #5 details
bewithgaurav Nov 10, 2025
201025f
Fix script
bewithgaurav Nov 10, 2025
5e9a427
PERFORMANCE FIX: Use single-pass batch row allocation
bewithgaurav Nov 10, 2025
797a617
test: Add comprehensive NULL handling test for all numeric types
bewithgaurav Nov 10, 2025
81551d4
test: Add LOB and NULL tests for GUID/DATETIMEOFFSET to improve coverage
bewithgaurav Nov 10, 2025
3e9ab3a
perf: Remove wasteful OPT #3 metadata duplication
bewithgaurav Nov 10, 2025
9b0ff30
docs: Simplify PR summary focusing on implemented optimizations
bewithgaurav Nov 10, 2025
1d712e5
Suppress s360 for WChars to make it faster
bewithgaurav Nov 10, 2025
cc7282e
Suppress s360 for WChars to make it faster
bewithgaurav Nov 10, 2025
02fc960
Restore s360 fix
bewithgaurav Nov 10, 2025
757ef84
more tests
bewithgaurav Nov 10, 2025
8e84080
Update PR Summary
bewithgaurav Nov 10, 2025
b6ea039
more tests for coverage
bewithgaurav Nov 10, 2025
ceaa5ba
PR Summary reformat
bewithgaurav Nov 10, 2025
0730e1d
PR Summary
bewithgaurav Nov 10, 2025
414151f
PR Summary
bewithgaurav Nov 10, 2025
1276aa6
10 averages and pyodbc conn string fix
bewithgaurav Nov 10, 2025
e94365f
refactor: Move inline processor functions to header file
bewithgaurav Nov 12, 2025
c9364e8
refactor: Move inline processor functions and required structs to hea…
bewithgaurav Nov 12, 2025
fd6c8e8
Merge remote-tracking branch 'origin/bewithgaurav/perf-improvements'
bewithgaurav Nov 12, 2025
15ce44e
fix: Remove static from FetchLobColumnData to fix linker error
bewithgaurav Nov 12, 2025
ea19bd0
feat: Add NULL checks for all Python C API calls
bewithgaurav Nov 12, 2025
54a3f99
OPTIMIZATION #6: Consistent NULL checking before all processor calls
bewithgaurav Nov 12, 2025
2045263
Fix two-phase allocation pattern and PyList_Append reallocation issue
bewithgaurav Nov 12, 2025
e3258cd
Improvements and comments fixed
bewithgaurav Nov 12, 2025
7450314
Add comprehensive stress tests and fix NULL check in UTF-16 decode
bewithgaurav Nov 12, 2025
c443a82
Improve test coverage and Windows compatibility
bewithgaurav Nov 12, 2025
26f8157
Remove unused unix_buffers.h
bewithgaurav Nov 12, 2025
6542674
PR Summary changed
bewithgaurav Nov 12, 2025
b5a2d82
Add SQL_DOUBLE and NULL GUID coverage tests
bewithgaurav Nov 12, 2025
2d34574
gitignore restored
bewithgaurav Nov 12, 2025
5d319d1
removed PR summary
bewithgaurav Nov 12, 2025
55e20c6
remove the buggy GUID test
bewithgaurav Nov 12, 2025
c26902c
kicking off the pipelines again
bewithgaurav Nov 12, 2025
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
Prev Previous commit
Next Next commit
Improvements and comments fixed
  • Loading branch information
bewithgaurav committed Nov 12, 2025
commit e3258cdbfc23aef9acd119a50fecd36ca183fc12
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ build/
*.pyd
*.pdb

# ODBC driver binaries (modified by build scripts with install_name_tool/patchelf/codesigning)
# macOS
mssql_python/libs/macos/*/lib/*.dylib
# Linux
mssql_python/libs/linux/*/*/lib/*.so*
# Windows
mssql_python/libs/windows/*/*.dll

# IDE files
.vscode/
.idea/
Expand Down
15 changes: 15 additions & 0 deletions mssql_python/pybind/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,21 @@ else
else
echo "[WARNING] macOS dylib configuration encountered issues"
fi

# Codesign the Python extension module (.so file) to prevent SIP crashes
echo "[ACTION] Codesigning Python extension module..."
SO_FILE="$PARENT_DIR/"*.so
for so in $SO_FILE; do
if [ -f "$so" ]; then
echo " Signing: $so"
codesign -s - -f "$so" 2>/dev/null
if [ $? -eq 0 ]; then
echo "[SUCCESS] Python extension codesigned: $so"
else
echo "[WARNING] Failed to codesign: $so"
fi
fi
done
fi
else
echo "[ERROR] Failed to copy .so file"
Expand Down
15 changes: 7 additions & 8 deletions mssql_python/pybind/ddbc_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3175,7 +3175,7 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum

std::string decimalSeparator = GetDecimalSeparator(); // Cache decimal separator

// OPTIMIZATION #5: Build function pointer dispatch table (once per batch)
// Performance: Build function pointer dispatch table (once per batch)
// This eliminates the switch statement from the hot loop - 10,000 rows × 10 cols
// reduces from 100,000 switch evaluations to just 10 switch evaluations
std::vector<ColumnProcessor> columnProcessors(numCols);
Expand Down Expand Up @@ -3237,10 +3237,9 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
}
}

size_t initialSize = rows.size();

// OPTIMIZATION #4: Pre-allocate outer list to avoid PyList_Append reallocations
// Directly create and fill rows in one pass to prevent data corruption window
// Performance: Single-phase row creation pattern
// Create each row, fill it completely, then append to results list
// This prevents data corruption (no partially-filled rows) and simplifies error handling
PyObject* rowsList = rows.ptr();

for (SQLULEN i = 0; i < numRowsFetched; i++) {
Expand All @@ -3252,8 +3251,8 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
}

for (SQLUSMALLINT col = 1; col <= numCols; col++) {
// OPTIMIZATION #6: Consistent NULL checking - check BEFORE calling processor functions
// This eliminates redundant NULL checks inside each processor and improves branch prediction
// Performance: Centralized NULL checking before calling processor functions
// This eliminates redundant NULL checks inside each processor and improves CPU branch prediction
SQLLEN dataLen = buffers.indicators[col - 1][i];

// Handle NULL and special indicator values first (applies to ALL types)
Expand All @@ -3269,7 +3268,7 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
continue;
}

// OPTIMIZATION #5: Use function pointer if available (fast path for common types)
// Performance: Use function pointer dispatch for simple types (fast path)
// This eliminates the switch statement from hot loop - reduces 100,000 switch
// evaluations (1000 rows × 10 cols × 10 types) to just 10 (setup only)
Comment thread
bewithgaurav marked this conversation as resolved.
// Note: Processor functions no longer need to check for NULL since we do it above
Expand Down
44 changes: 22 additions & 22 deletions mssql_python/pybind/ddbc_bindings.h
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ struct ColumnBuffers {
indicators(numCols, std::vector<SQLLEN>(fetchSize)) {}
};

// OPTIMIZATION #4: Column processor function type - processes one cell
// Performance: Column processor function type for fast type conversion
// Using function pointers eliminates switch statement overhead in the hot loop
typedef void (*ColumnProcessor)(PyObject* row, ColumnBuffers& buffers, const void* colInfo,
SQLUSMALLINT col, SQLULEN rowIdx, SQLHSTMT hStmt);
Expand All @@ -639,10 +639,10 @@ namespace ColumnProcessors {
// Process SQL INTEGER (4-byte int) column into Python int
// SAFETY: PyList_SET_ITEM is safe here because row is freshly allocated with PyList_New()
// and each slot is filled exactly once (NULL -> value)
// NOTE: NULL check removed - handled centrally before processor is called (OPTIMIZATION #6)
// Performance: NULL check removed - handled centrally before processor is called
inline void ProcessInteger(PyObject* row, ColumnBuffers& buffers, const void*, SQLUSMALLINT col,
SQLULEN rowIdx, SQLHSTMT) {
// OPTIMIZATION #2: Direct Python C API call (bypasses pybind11)
// Performance: Direct Python C API call (bypasses pybind11 overhead)
PyObject* pyInt = PyLong_FromLong(buffers.intBuffers[col - 1][rowIdx]);
if (!pyInt) { // Handle memory allocation failure
Py_INCREF(Py_None);
Expand All @@ -653,10 +653,10 @@ inline void ProcessInteger(PyObject* row, ColumnBuffers& buffers, const void*, S
}

// Process SQL SMALLINT (2-byte int) column into Python int
// NOTE: NULL check removed - handled centrally before processor is called (OPTIMIZATION #6)
// Performance: NULL check removed - handled centrally before processor is called
inline void ProcessSmallInt(PyObject* row, ColumnBuffers& buffers, const void*, SQLUSMALLINT col,
SQLULEN rowIdx, SQLHSTMT) {
// OPTIMIZATION #2: Direct Python C API call
// Performance: Direct Python C API call
PyObject* pyInt = PyLong_FromLong(buffers.smallIntBuffers[col - 1][rowIdx]);
if (!pyInt) { // Handle memory allocation failure
Py_INCREF(Py_None);
Expand All @@ -667,10 +667,10 @@ inline void ProcessSmallInt(PyObject* row, ColumnBuffers& buffers, const void*,
}

// Process SQL BIGINT (8-byte int) column into Python int
// NOTE: NULL check removed - handled centrally before processor is called (OPTIMIZATION #6)
// Performance: NULL check removed - handled centrally before processor is called
inline void ProcessBigInt(PyObject* row, ColumnBuffers& buffers, const void*, SQLUSMALLINT col,
SQLULEN rowIdx, SQLHSTMT) {
// OPTIMIZATION #2: Direct Python C API call
// Performance: Direct Python C API call
PyObject* pyInt = PyLong_FromLongLong(buffers.bigIntBuffers[col - 1][rowIdx]);
if (!pyInt) { // Handle memory allocation failure
Py_INCREF(Py_None);
Expand All @@ -681,10 +681,10 @@ inline void ProcessBigInt(PyObject* row, ColumnBuffers& buffers, const void*, SQ
}

// Process SQL TINYINT (1-byte unsigned int) column into Python int
// NOTE: NULL check removed - handled centrally before processor is called (OPTIMIZATION #6)
// Performance: NULL check removed - handled centrally before processor is called
inline void ProcessTinyInt(PyObject* row, ColumnBuffers& buffers, const void*, SQLUSMALLINT col,
SQLULEN rowIdx, SQLHSTMT) {
// OPTIMIZATION #2: Direct Python C API call
// Performance: Direct Python C API call
PyObject* pyInt = PyLong_FromLong(buffers.charBuffers[col - 1][rowIdx]);
if (!pyInt) { // Handle memory allocation failure
Py_INCREF(Py_None);
Expand All @@ -695,10 +695,10 @@ inline void ProcessTinyInt(PyObject* row, ColumnBuffers& buffers, const void*, S
}

// Process SQL BIT column into Python bool
// NOTE: NULL check removed - handled centrally before processor is called (OPTIMIZATION #6)
// Performance: NULL check removed - handled centrally before processor is called
inline void ProcessBit(PyObject* row, ColumnBuffers& buffers, const void*, SQLUSMALLINT col,
SQLULEN rowIdx, SQLHSTMT) {
// OPTIMIZATION #2: Direct Python C API call (converts 0/1 to True/False)
// Performance: Direct Python C API call (converts 0/1 to True/False)
PyObject* pyBool = PyBool_FromLong(buffers.charBuffers[col - 1][rowIdx]);
if (!pyBool) { // Handle memory allocation failure
Py_INCREF(Py_None);
Expand All @@ -709,10 +709,10 @@ inline void ProcessBit(PyObject* row, ColumnBuffers& buffers, const void*, SQLUS
}

// Process SQL REAL (4-byte float) column into Python float
// NOTE: NULL check removed - handled centrally before processor is called (OPTIMIZATION #6)
// Performance: NULL check removed - handled centrally before processor is called
inline void ProcessReal(PyObject* row, ColumnBuffers& buffers, const void*, SQLUSMALLINT col,
SQLULEN rowIdx, SQLHSTMT) {
// OPTIMIZATION #2: Direct Python C API call
// Performance: Direct Python C API call
PyObject* pyFloat = PyFloat_FromDouble(buffers.realBuffers[col - 1][rowIdx]);
if (!pyFloat) { // Handle memory allocation failure
Py_INCREF(Py_None);
Expand All @@ -723,10 +723,10 @@ inline void ProcessReal(PyObject* row, ColumnBuffers& buffers, const void*, SQLU
}

// Process SQL DOUBLE/FLOAT (8-byte float) column into Python float
// NOTE: NULL check removed - handled centrally before processor is called (OPTIMIZATION #6)
// Performance: NULL check removed - handled centrally before processor is called
inline void ProcessDouble(PyObject* row, ColumnBuffers& buffers, const void*, SQLUSMALLINT col,
SQLULEN rowIdx, SQLHSTMT) {
// OPTIMIZATION #2: Direct Python C API call
// Performance: Direct Python C API call
PyObject* pyFloat = PyFloat_FromDouble(buffers.doubleBuffers[col - 1][rowIdx]);
if (!pyFloat) { // Handle memory allocation failure
Py_INCREF(Py_None);
Expand All @@ -737,7 +737,7 @@ inline void ProcessDouble(PyObject* row, ColumnBuffers& buffers, const void*, SQ
}

// Process SQL CHAR/VARCHAR (single-byte string) column into Python str
// NOTE: NULL/NO_TOTAL checks removed - handled centrally before processor is called (OPTIMIZATION #6)
// Performance: NULL/NO_TOTAL checks removed - handled centrally before processor is called
inline void ProcessChar(PyObject* row, ColumnBuffers& buffers, const void* colInfoPtr,
SQLUSMALLINT col, SQLULEN rowIdx, SQLHSTMT hStmt) {
const ColumnInfoExt* colInfo = static_cast<const ColumnInfoExt*>(colInfoPtr);
Expand All @@ -759,7 +759,7 @@ inline void ProcessChar(PyObject* row, ColumnBuffers& buffers, const void* colIn
// Fast path: Data fits in buffer (not LOB or truncated)
// fetchBufferSize includes null-terminator, numCharsInData doesn't. Hence '<'
if (!colInfo->isLob && numCharsInData < colInfo->fetchBufferSize) {
// OPTIMIZATION #2: Direct Python C API call - create string from buffer
// Performance: Direct Python C API call - create string from buffer
PyObject* pyStr = PyUnicode_FromStringAndSize(
reinterpret_cast<char*>(&buffers.charBuffers[col - 1][rowIdx * colInfo->fetchBufferSize]),
numCharsInData);
Expand All @@ -776,7 +776,7 @@ inline void ProcessChar(PyObject* row, ColumnBuffers& buffers, const void* colIn
}

// Process SQL NCHAR/NVARCHAR (wide/Unicode string) column into Python str
// NOTE: NULL/NO_TOTAL checks removed - handled centrally before processor is called (OPTIMIZATION #6)
// Performance: NULL/NO_TOTAL checks removed - handled centrally before processor is called
inline void ProcessWChar(PyObject* row, ColumnBuffers& buffers, const void* colInfoPtr,
SQLUSMALLINT col, SQLULEN rowIdx, SQLHSTMT hStmt) {
const ColumnInfoExt* colInfo = static_cast<const ColumnInfoExt*>(colInfoPtr);
Expand All @@ -799,7 +799,7 @@ inline void ProcessWChar(PyObject* row, ColumnBuffers& buffers, const void* colI
// fetchBufferSize includes null-terminator, numCharsInData doesn't. Hence '<'
if (!colInfo->isLob && numCharsInData < colInfo->fetchBufferSize) {
#if defined(__APPLE__) || defined(__linux__)
// OPTIMIZATION #1: Direct UTF-16 decode (SQLWCHAR is 2 bytes on Linux/macOS)
// Performance: Direct UTF-16 decode (SQLWCHAR is 2 bytes on Linux/macOS)
SQLWCHAR* wcharData = &buffers.wcharBuffers[col - 1][rowIdx * colInfo->fetchBufferSize];
PyObject* pyStr = PyUnicode_DecodeUTF16(
reinterpret_cast<const char*>(wcharData),
Expand All @@ -814,7 +814,7 @@ inline void ProcessWChar(PyObject* row, ColumnBuffers& buffers, const void* colI
PyList_SET_ITEM(row, col - 1, PyUnicode_FromStringAndSize("", 0));
}
#else
// OPTIMIZATION #2: Direct Python C API call (Windows where SQLWCHAR == wchar_t)
// Performance: Direct Python C API call (Windows where SQLWCHAR == wchar_t)
PyObject* pyStr = PyUnicode_FromWideChar(
reinterpret_cast<wchar_t*>(&buffers.wcharBuffers[col - 1][rowIdx * colInfo->fetchBufferSize]),
numCharsInData);
Expand All @@ -832,7 +832,7 @@ inline void ProcessWChar(PyObject* row, ColumnBuffers& buffers, const void* colI
}

// Process SQL BINARY/VARBINARY (binary data) column into Python bytes
// NOTE: NULL/NO_TOTAL checks removed - handled centrally before processor is called (OPTIMIZATION #6)
// Performance: NULL/NO_TOTAL checks removed - handled centrally before processor is called
inline void ProcessBinary(PyObject* row, ColumnBuffers& buffers, const void* colInfoPtr,
SQLUSMALLINT col, SQLULEN rowIdx, SQLHSTMT hStmt) {
const ColumnInfoExt* colInfo = static_cast<const ColumnInfoExt*>(colInfoPtr);
Expand All @@ -852,7 +852,7 @@ inline void ProcessBinary(PyObject* row, ColumnBuffers& buffers, const void* col

// Fast path: Data fits in buffer (not LOB or truncated)
if (!colInfo->isLob && static_cast<size_t>(dataLen) <= colInfo->processedColumnSize) {
// OPTIMIZATION #2: Direct Python C API call - create bytes from buffer
// Performance: Direct Python C API call - create bytes from buffer
PyObject* pyBytes = PyBytes_FromStringAndSize(
reinterpret_cast<const char*>(&buffers.charBuffers[col - 1][rowIdx * colInfo->processedColumnSize]),
dataLen);
Expand Down