-
Notifications
You must be signed in to change notification settings - Fork 27
FIX: Encoding Decoding Github issue #301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR strengthens encoding validation for SQL Server connections by ensuring UTF-16 encoding compliance when using wide character types. It adds validation checks with automatic fallback to safe defaults and removes duplicate test functions.
Key Changes
- Added UTF-16 encoding enforcement for
SQL_WCHARinsetencodingmethod - Enhanced
setdecodingwith UTF-16 validation for bothSQL_WCHARandSQL_WMETADATAtypes - Removed duplicate test functions (
test_decimal_separator_*,test_lowercase_attribute,test_rowcount)
Reviewed Changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| mssql_python/connection.py | Added encoding validation logic to enforce UTF-16 for wide character types with warning logs and safe defaults |
| tests/test_004_cursor.py | Removed duplicate test functions for decimal separator, lowercase attribute, and rowcount functionality |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
📊 Code Coverage Report
Diff CoverageDiff: main...HEAD, staged and unstaged changes
Summary
mssql_python/cursor.pyLines 297-308 297 """
298 if hasattr(self._connection, 'getencoding'):
299 try:
300 return self._connection.getencoding()
! 301 except (OperationalError, DatabaseError) as db_error:
302 # Only catch database-related errors, not programming errors
! 303 log('warning', f"Failed to get encoding settings from connection due to database error: {db_error}")
! 304 return {
305 'encoding': 'utf-16le',
306 'ctype': ddbc_sql_const.SQL_WCHAR.value
307 }
308 Lines 306-314 306 'ctype': ddbc_sql_const.SQL_WCHAR.value
307 }
308
309 # Return default encoding settings if getencoding is not available
! 310 return {
311 'encoding': 'utf-16le',
312 'ctype': ddbc_sql_const.SQL_WCHAR.value
313 }Lines 324-338 324 """
325 try:
326 # Get decoding settings from connection for this SQL type
327 return self._connection.getdecoding(sql_type)
! 328 except (OperationalError, DatabaseError) as db_error:
329 # Only handle expected database-related errors
! 330 log('warning', f"Failed to get decoding settings for SQL type {sql_type} due to database error: {db_error}")
! 331 if sql_type == ddbc_sql_const.SQL_WCHAR.value:
! 332 return {'encoding': 'utf-16le', 'ctype': ddbc_sql_const.SQL_WCHAR.value}
333 else:
! 334 return {'encoding': 'utf-8', 'ctype': ddbc_sql_const.SQL_CHAR.value}
335
336 def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-return-statements,too-many-branches
337 self,
338 param: Any,mssql_python/pybind/ddbc_bindings.cppLines 200-220 200
201 // Encoding function with fallback strategy
202 static py::bytes EncodingString(const std::string& text,
203 const std::string& encoding,
! 204 const std::string& errors = "strict") {
! 205 try {
! 206 py::gil_scoped_acquire gil;
! 207 py::str unicode_str = py::str(text);
208
209 // Direct encoding - let Python handle errors strictly
! 210 py::bytes encoded = unicode_str.attr("encode")(encoding, errors);
! 211 return encoded;
! 212 } catch (const py::error_already_set& e) {
213 // Re-raise Python exceptions (UnicodeEncodeError, etc.)
! 214 throw std::runtime_error("Encoding failed: " + std::string(e.what()));
! 215 }
! 216 }
217
218 static py::str DecodingString(const char* data, size_t length,
219 const std::string& encoding,
220 const std::string& errors = "strict") {Lines 226-344 226 py::str decoded = byte_data.attr("decode")(encoding, errors);
227 return decoded;
228 } catch (const py::error_already_set& e) {
229 // Re-raise Python exceptions (UnicodeDecodeError, etc.)
! 230 throw std::runtime_error("Decoding failed: " + std::string(e.what()));
! 231 }
232 }
233
234 // Helper function to validate that an encoding string is a legitimate Python
235 // codec This prevents injection attacks while allowing all valid encodings
! 236 static bool is_valid_encoding(const std::string& enc) {
! 237 if (enc.empty() || enc.length() > 100) { // Reasonable length limit
! 238 return false;
! 239 }
240
241 // Check for potentially dangerous characters that shouldn't be in codec
242 // names
! 243 for (char c : enc) {
! 244 if (!std::isalnum(c) && c != '-' && c != '_' && c != '.') {
! 245 return false; // Reject suspicious characters
! 246 }
! 247 }
248
249 // Verify it's a valid Python codec by attempting a test lookup
! 250 try {
! 251 py::gil_scoped_acquire gil;
! 252 py::module_ codecs = py::module_::import("codecs");
253
254 // This will raise LookupError if the codec doesn't exist
! 255 codecs.attr("lookup")(enc);
256
! 257 return true; // Codec exists and is valid
! 258 } catch (const py::error_already_set& e) {
259 // Expected: LookupError for invalid codec names
! 260 LOG("Codec validation failed for '{}': {}", enc, e.what());
! 261 return false; // Invalid codec name
! 262 } catch (const std::exception& e) {
263 // Unexpected C++ exception during validation
! 264 LOG("Unexpected exception validating encoding '{}': {}", enc, e.what());
! 265 return false;
! 266 } catch (...) {
267 // Last resort: unknown exception type
! 268 LOG("Unknown exception validating encoding '{}'", enc);
! 269 return false;
! 270 }
! 271 }
272
273 // Helper function to validate error handling mode against an allowlist
! 274 static bool is_valid_error_mode(const std::string& mode) {
! 275 static const std::unordered_set<std::string> allowed = {
! 276 "strict", "ignore", "replace", "xmlcharrefreplace", "backslashreplace"};
! 277 return allowed.find(mode) != allowed.end();
! 278 }
279
280 // Helper function to safely extract encoding settings from Python dict
281 static std::pair<std::string, std::string> extract_encoding_settings(
! 282 const py::dict& settings) {
! 283 try {
! 284 std::string encoding = "utf-8"; // Default
! 285 std::string errors = "strict"; // Default
286
! 287 if (settings.contains("encoding") && !settings["encoding"].is_none()) {
! 288 std::string proposed_encoding =
! 289 settings["encoding"].cast<std::string>();
290
291 // SECURITY: Validate encoding to prevent injection attacks
292 // Allows any valid Python codec (including SQL Server-supported
293 // encodings)
! 294 if (is_valid_encoding(proposed_encoding)) {
! 295 encoding = proposed_encoding;
! 296 } else {
! 297 LOG("Invalid or unsafe encoding '{}' rejected, using default "
! 298 "'utf-8'",
! 299 proposed_encoding);
300 // Fall back to safe default
! 301 encoding = "utf-8";
! 302 }
! 303 }
304
! 305 if (settings.contains("errors") && !settings["errors"].is_none()) {
! 306 std::string proposed_errors =
! 307 settings["errors"].cast<std::string>();
308
309 // SECURITY: Validate error mode against allowlist
! 310 if (is_valid_error_mode(proposed_errors)) {
! 311 errors = proposed_errors;
! 312 } else {
! 313 LOG("Invalid error mode '{}' rejected, using default 'strict'",
! 314 proposed_errors);
315 // Fall back to safe default
! 316 errors = "strict";
! 317 }
! 318 }
319
! 320 return std::make_pair(encoding, errors);
! 321 } catch (const py::error_already_set& e) {
322 // Log Python exceptions (KeyError, TypeError, etc.)
! 323 LOG("Python exception while extracting encoding settings: {}. Using "
! 324 "defaults (utf-8, "
! 325 "strict)",
! 326 e.what());
! 327 return std::make_pair("utf-8", "strict");
! 328 } catch (const std::exception& e) {
329 // Log C++ standard exceptions
! 330 LOG("Exception while extracting encoding settings: {}. Using defaults "
! 331 "(utf-8, strict)",
! 332 e.what());
! 333 return std::make_pair("utf-8", "strict");
! 334 } catch (...) {
335 // Last resort: unknown exception type
! 336 LOG("Unknown exception while extracting encoding settings. Using "
! 337 "defaults (utf-8, strict)");
! 338 return std::make_pair("utf-8", "strict");
! 339 }
! 340 }
341
342 namespace {
343
344 const char* GetSqlCTypeAsString(const SQLSMALLINT cType) {Lines 371-382 371 }
372 }
373
374 std::string MakeParamMismatchErrorStr(const SQLSMALLINT cType,
! 375 const int paramIndex) {
376 std::string errorString =
! 377 "Parameter's object type does not match parameter's C type. paramIndex "
! 378 "- " +
379 std::to_string(paramIndex) + ", C type - " + GetSqlCTypeAsString(cType);
380 return errorString;
381 }Lines 431-519 431
432 // TODO: Add more data types like money, guid, interval, TVPs etc.
433 switch (paramInfo.paramCType) {
434 case SQL_C_CHAR: {
! 435 if (!py::isinstance<py::str>(param)) {
! 436 ThrowStdException(MakeParamMismatchErrorStr(
! 437 paramInfo.paramCType, paramIndex));
438 }
439
! 440 std::string strValue;
441
442 // Check if we have encoding settings and this is SQL_C_CHAR
443 // (not SQL_C_WCHAR)
! 444 if (encoding_settings && !encoding_settings.is_none()) {
! 445 try {
446 // SECURITY: Use extract_encoding_settings for full
447 // validation This validates encoding against allowlist
448 // and error mode
! 449 py::dict settings_dict =
! 450 encoding_settings.cast<py::dict>();
! 451 auto [encoding, errors] =
! 452 extract_encoding_settings(settings_dict);
453
454 // Validate ctype against allowlist
! 455 if (settings_dict.contains("ctype")) {
! 456 SQLSMALLINT ctype =
! 457 settings_dict["ctype"].cast<SQLSMALLINT>();
458
459 // Only SQL_C_CHAR and SQL_C_WCHAR are allowed
! 460 if (ctype != SQL_C_CHAR && ctype != SQL_C_WCHAR) {
! 461 LOG("Invalid ctype {} for parameter {}, using "
! 462 "default",
! 463 ctype, paramIndex);
464 // Fall through to default behavior
! 465 strValue = param.cast<std::string>();
! 466 } else if (ctype == SQL_C_CHAR) {
467 // Only use dynamic encoding for SQL_C_CHAR
! 468 py::bytes encoded_bytes =
! 469 EncodingString(param.cast<std::string>(),
! 470 encoding, errors);
! 471 strValue = encoded_bytes.cast<std::string>();
! 472 } else {
473 // SQL_C_WCHAR - use default behavior
! 474 strValue = param.cast<std::string>();
! 475 }
! 476 } else {
477 // No ctype specified, use default behavior
! 478 strValue = param.cast<std::string>();
! 479 }
! 480 } catch (const std::exception& e) {
! 481 LOG("Encoding settings processing failed for parameter "
! 482 "{}: {}. Using "
! 483 "default.",
! 484 paramIndex, e.what());
485 // Fall back to safe default behavior
! 486 strValue = param.cast<std::string>();
! 487 }
488 } else {
489 // No encoding settings, use default behavior
! 490 strValue = param.cast<std::string>();
491 }
492
493 // Allocate buffer and copy string data
! 494 size_t bufferSize =
! 495 strValue.length() + 1; // +1 for null terminator
! 496 char* buffer =
! 497 AllocateParamBufferArray<char>(paramBuffers, bufferSize);
498
! 499 if (!buffer) {
! 500 ThrowStdException(
! 501 "Failed to allocate buffer for SQL_C_CHAR parameter at "
! 502 "index " +
! 503 std::to_string(paramIndex));
! 504 }
505
506 // SECURITY: Validate size before copying to prevent buffer
507 // overflow
! 508 size_t copyLength = strValue.length();
! 509 if (copyLength >= bufferSize) {
! 510 ThrowStdException(
! 511 "Buffer overflow prevented: string length exceeds "
! 512 "allocated buffer at "
! 513 "index " +
! 514 std::to_string(paramIndex));
! 515 }
516
517 // Use secure copy with bounds checking
518 #ifdef _WIN32
519 // Windows: Use memcpy_s for secure copyLines 526-545 526 std::to_string(paramIndex));
527 }
528 #else
529 // POSIX: Use std::copy_n with explicit bounds checking
! 530 if (copyLength > 0) {
! 531 std::copy_n(strValue.data(), copyLength, buffer);
! 532 }
! 533 #endif
534
! 535 buffer[copyLength] = '\0'; // Ensure null termination
536
! 537 paramInfo.strLenOrInd = copyLength;
538
! 539 LOG("Binding SQL_C_CHAR parameter at index {} with encoded "
! 540 "length {}",
! 541 paramIndex, strValue.length());
542 break;
543 }
544 case SQL_C_BINARY: {
545 if (!py::isinstance<py::str>(param) &&Lines 544-553 544 case SQL_C_BINARY: {
545 if (!py::isinstance<py::str>(param) &&
546 !py::isinstance<py::bytearray>(param) &&
547 !py::isinstance<py::bytes>(param)) {
! 548 ThrowStdException(MakeParamMismatchErrorStr(
! 549 paramInfo.paramCType, paramIndex));
550 }
551 if (paramInfo.isDAE) {
552 // Deferred execution for VARBINARY(MAX)
553 LOG("Parameter[{}] is marked for DAE streaming "Lines 583-592 583 case SQL_C_WCHAR: {
584 if (!py::isinstance<py::str>(param) &&
585 !py::isinstance<py::bytearray>(param) &&
586 !py::isinstance<py::bytes>(param)) {
! 587 ThrowStdException(MakeParamMismatchErrorStr(
! 588 paramInfo.paramCType, paramIndex));
589 }
590 if (paramInfo.isDAE) {
591 // deferred execution
592 LOG("Parameter[{}] is marked for DAE streaming",Lines 613-622 613 break;
614 }
615 case SQL_C_BIT: {
616 if (!py::isinstance<py::bool_>(param)) {
! 617 ThrowStdException(MakeParamMismatchErrorStr(
! 618 paramInfo.paramCType, paramIndex));
619 }
620 dataPtr = static_cast<void*>(AllocateParamBuffer<bool>(
621 paramBuffers, param.cast<bool>()));
622 break;Lines 622-631 622 break;
623 }
624 case SQL_C_DEFAULT: {
625 if (!py::isinstance<py::none>(param)) {
! 626 ThrowStdException(MakeParamMismatchErrorStr(
! 627 paramInfo.paramCType, paramIndex));
628 }
629 SQLSMALLINT sqlType = paramInfo.paramSQLType;
630 SQLULEN columnSize = paramInfo.columnSize;
631 SQLSMALLINT decimalDigits = paramInfo.decimalDigits;Lines 630-652 630 SQLULEN columnSize = paramInfo.columnSize;
631 SQLSMALLINT decimalDigits = paramInfo.decimalDigits;
632 if (sqlType == SQL_UNKNOWN_TYPE) {
633 SQLSMALLINT describedType;
! 634 SQLULEN describedSize;
635 SQLSMALLINT describedDigits;
636 SQLSMALLINT nullable;
637 RETCODE rc = SQLDescribeParam_ptr(
! 638 hStmt, static_cast<SQLUSMALLINT>(paramIndex + 1),
! 639 &describedType, &describedSize, &describedDigits,
! 640 &nullable);
641 if (!SQL_SUCCEEDED(rc)) {
! 642 LOG("SQLDescribeParam failed for parameter {} with "
! 643 "error code {}",
! 644 paramIndex, rc);
645 return rc;
646 }
! 647 sqlType = describedType;
! 648 columnSize = describedSize;
649 decimalDigits = describedDigits;
650 }
651 dataPtr = nullptr;
652 strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);Lines 661-670 661 case SQL_C_TINYINT:
662 case SQL_C_SSHORT:
663 case SQL_C_SHORT: {
664 if (!py::isinstance<py::int_>(param)) {
! 665 ThrowStdException(MakeParamMismatchErrorStr(
! 666 paramInfo.paramCType, paramIndex));
667 }
668 int value = param.cast<int>();
669 // Range validation for signed 16-bit integer
670 if (value < std::numeric_limits<int16_t>::min() ||Lines 668-679 668 int value = param.cast<int>();
669 // Range validation for signed 16-bit integer
670 if (value < std::numeric_limits<int16_t>::min() ||
671 value > std::numeric_limits<int16_t>::max()) {
! 672 ThrowStdException(
! 673 "Signed short integer parameter out of range at "
! 674 "paramIndex " +
! 675 std::to_string(paramIndex));
676 }
677 dataPtr = static_cast<void*>(
678 AllocateParamBuffer<int>(paramBuffers, param.cast<int>()));
679 break;Lines 680-699 680 }
681 case SQL_C_UTINYINT:
682 case SQL_C_USHORT: {
683 if (!py::isinstance<py::int_>(param)) {
! 684 ThrowStdException(MakeParamMismatchErrorStr(
! 685 paramInfo.paramCType, paramIndex));
686 }
687 unsigned int value = param.cast<unsigned int>();
! 688 if (value > std::numeric_limits<uint16_t>::max()) {
! 689 ThrowStdException(
! 690 "Unsigned short integer parameter out of range at "
! 691 "paramIndex " +
! 692 std::to_string(paramIndex));
693 }
! 694 dataPtr = static_cast<void*>(AllocateParamBuffer<unsigned int>(
! 695 paramBuffers, param.cast<unsigned int>()));
696 break;
697 }
698 case SQL_C_SBIGINT:
699 case SQL_C_SLONG:Lines 698-707 698 case SQL_C_SBIGINT:
699 case SQL_C_SLONG:
700 case SQL_C_LONG: {
701 if (!py::isinstance<py::int_>(param)) {
! 702 ThrowStdException(MakeParamMismatchErrorStr(
! 703 paramInfo.paramCType, paramIndex));
704 }
705 int64_t value = param.cast<int64_t>();
706 // Range validation for signed 64-bit integer
707 if (value < std::numeric_limits<int64_t>::min() ||Lines 705-716 705 int64_t value = param.cast<int64_t>();
706 // Range validation for signed 64-bit integer
707 if (value < std::numeric_limits<int64_t>::min() ||
708 value > std::numeric_limits<int64_t>::max()) {
! 709 ThrowStdException(
! 710 "Signed 64-bit integer parameter out of range at "
! 711 "paramIndex " +
! 712 std::to_string(paramIndex));
713 }
714 dataPtr = static_cast<void*>(AllocateParamBuffer<int64_t>(
715 paramBuffers, param.cast<int64_t>()));
716 break;Lines 717-752 717 }
718 case SQL_C_UBIGINT:
719 case SQL_C_ULONG: {
720 if (!py::isinstance<py::int_>(param)) {
! 721 ThrowStdException(MakeParamMismatchErrorStr(
! 722 paramInfo.paramCType, paramIndex));
723 }
724 uint64_t value = param.cast<uint64_t>();
725 // Range validation for unsigned 64-bit integer
726 if (value > std::numeric_limits<uint64_t>::max()) {
! 727 ThrowStdException(
! 728 "Unsigned 64-bit integer parameter out of range at "
! 729 "paramIndex " +
! 730 std::to_string(paramIndex));
731 }
! 732 dataPtr = static_cast<void*>(AllocateParamBuffer<uint64_t>(
! 733 paramBuffers, param.cast<uint64_t>()));
734 break;
735 }
736 case SQL_C_FLOAT: {
737 if (!py::isinstance<py::float_>(param)) {
! 738 ThrowStdException(MakeParamMismatchErrorStr(
! 739 paramInfo.paramCType, paramIndex));
740 }
! 741 dataPtr = static_cast<void*>(AllocateParamBuffer<float>(
! 742 paramBuffers, param.cast<float>()));
743 break;
744 }
745 case SQL_C_DOUBLE: {
746 if (!py::isinstance<py::float_>(param)) {
! 747 ThrowStdException(MakeParamMismatchErrorStr(
! 748 paramInfo.paramCType, paramIndex));
749 }
750 dataPtr = static_cast<void*>(AllocateParamBuffer<double>(
751 paramBuffers, param.cast<double>()));
752 break;Lines 754-770 754 case SQL_C_TYPE_DATE: {
755 py::object dateType =
756 py::module_::import("datetime").attr("date");
757 if (!py::isinstance(param, dateType)) {
! 758 ThrowStdException(MakeParamMismatchErrorStr(
! 759 paramInfo.paramCType, paramIndex));
760 }
761 int year = param.attr("year").cast<int>();
762 if (year < 1753 || year > 9999) {
! 763 ThrowStdException(
! 764 "Date out of range for SQL Server (1753-9999) at "
! 765 "paramIndex " +
! 766 std::to_string(paramIndex));
767 }
768 // TODO: can be moved to python by registering SQL_DATE_STRUCT
769 // in pybind
770 SQL_DATE_STRUCT* sqlDatePtr =Lines 781-790 781 case SQL_C_TYPE_TIME: {
782 py::object timeType =
783 py::module_::import("datetime").attr("time");
784 if (!py::isinstance(param, timeType)) {
! 785 ThrowStdException(MakeParamMismatchErrorStr(
! 786 paramInfo.paramCType, paramIndex));
787 }
788 // TODO: can be moved to python by registering SQL_TIME_STRUCT
789 // in pybind
790 SQL_TIME_STRUCT* sqlTimePtr =Lines 801-818 801 case SQL_C_SS_TIMESTAMPOFFSET: {
802 py::object datetimeType =
803 py::module_::import("datetime").attr("datetime");
804 if (!py::isinstance(param, datetimeType)) {
! 805 ThrowStdException(MakeParamMismatchErrorStr(
! 806 paramInfo.paramCType, paramIndex));
807 }
808 // Checking if the object has a timezone
809 py::object tzinfo = param.attr("tzinfo");
810 if (tzinfo.is_none()) {
! 811 ThrowStdException(
! 812 "Datetime object must have tzinfo for "
! 813 "SQL_C_SS_TIMESTAMPOFFSET at paramIndex " +
! 814 std::to_string(paramIndex));
815 }
816
817 DateTimeOffset* dtoPtr =
818 AllocateParamBuffer<DateTimeOffset>(paramBuffers);Lines 834-845 834 param.attr("microsecond").cast<int>() * 1000);
835
836 py::object utcoffset = tzinfo.attr("utcoffset")(param);
837 if (utcoffset.is_none()) {
! 838 ThrowStdException(
! 839 "Datetime object's tzinfo.utcoffset() returned None at "
! 840 "paramIndex " +
! 841 std::to_string(paramIndex));
842 }
843
844 int total_seconds = static_cast<int>(
845 utcoffset.attr("total_seconds")().cast<double>());Lines 867-876 867 case SQL_C_TYPE_TIMESTAMP: {
868 py::object datetimeType =
869 py::module_::import("datetime").attr("datetime");
870 if (!py::isinstance(param, datetimeType)) {
! 871 ThrowStdException(MakeParamMismatchErrorStr(
! 872 paramInfo.paramCType, paramIndex));
873 }
874 SQL_TIMESTAMP_STRUCT* sqlTimestampPtr =
875 AllocateParamBuffer<SQL_TIMESTAMP_STRUCT>(paramBuffers);
876 sqlTimestampPtr->year =Lines 893-902 893 break;
894 }
895 case SQL_C_NUMERIC: {
896 if (!py::isinstance<NumericData>(param)) {
! 897 ThrowStdException(MakeParamMismatchErrorStr(
! 898 paramInfo.paramCType, paramIndex));
899 }
900 NumericData decimalParam = param.cast<NumericData>();
901 LOG("Received numeric parameter: precision - {}, scale- {}, "
902 "sign - {}, value - {}",Lines 921-930 921 break;
922 }
923 case SQL_C_GUID: {
924 if (!py::isinstance<py::bytes>(param)) {
! 925 ThrowStdException(MakeParamMismatchErrorStr(
! 926 paramInfo.paramCType, paramIndex));
927 }
928 py::bytes uuid_bytes = param.cast<py::bytes>();
929 const unsigned char* uuid_data =
930 reinterpret_cast<const unsigned char*>(Lines 929-942 929 const unsigned char* uuid_data =
930 reinterpret_cast<const unsigned char*>(
931 PyBytes_AS_STRING(uuid_bytes.ptr()));
932 if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) {
! 933 LOG("Invalid UUID parameter at index {}: expected 16 "
! 934 "bytes, got {} bytes, type {}",
! 935 paramIndex, PyBytes_GET_SIZE(uuid_bytes.ptr()),
! 936 paramInfo.paramCType);
! 937 ThrowStdException(
! 938 "UUID binary data must be exactly 16 bytes long.");
939 }
940 SQLGUID* guid_data_ptr =
941 AllocateParamBuffer<SQLGUID>(paramBuffers);
942 guid_data_ptr->Data1 =Lines 959-969 959 break;
960 }
961 default: {
962 std::ostringstream errorString;
! 963 errorString << "Unsupported parameter type - "
! 964 << paramInfo.paramCType << " for parameter - "
! 965 << paramIndex;
966 ThrowStdException(errorString.str());
967 }
968 }
969 assert(SQLBindParameter_ptr && SQLGetStmtAttr_ptr &&Lines 992-1001 992 }
993 rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_TYPE,
994 (SQLPOINTER)SQL_C_NUMERIC, 0);
995 if (!SQL_SUCCEEDED(rc)) {
! 996 LOG("Error when setting descriptor field SQL_DESC_TYPE - {}",
! 997 paramIndex);
998 return rc;
999 }
1000 SQL_NUMERIC_STRUCT* numericPtr =
1001 reinterpret_cast<SQL_NUMERIC_STRUCT*>(dataPtr);Lines 1001-1011 1001 reinterpret_cast<SQL_NUMERIC_STRUCT*>(dataPtr);
1002 rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_PRECISION,
1003 (SQLPOINTER)numericPtr->precision, 0);
1004 if (!SQL_SUCCEEDED(rc)) {
! 1005 LOG("Error when setting descriptor field SQL_DESC_PRECISION - "
! 1006 "{}",
! 1007 paramIndex);
1008 return rc;
1009 }
1010
1011 rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_SCALE,Lines 1010-1019 1010
1011 rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_SCALE,
1012 (SQLPOINTER)numericPtr->scale, 0);
1013 if (!SQL_SUCCEEDED(rc)) {
! 1014 LOG("Error when setting descriptor field SQL_DESC_SCALE - {}",
! 1015 paramIndex);
1016 return rc;
1017 }
1018
1019 rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_DATA_PTR,Lines 1018-1028 1018
1019 rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_DATA_PTR,
1020 (SQLPOINTER)numericPtr, 0);
1021 if (!SQL_SUCCEEDED(rc)) {
! 1022 LOG("Error when setting descriptor field SQL_DESC_DATA_PTR - "
! 1023 "{}",
! 1024 paramIndex);
1025 return rc;
1026 }
1027 }
1028 }Lines 1059-1069 1059 // Check if the attribute exists before accessing it (for Python
1060 // version compatibility)
1061 if (py::hasattr(sys_module, "_is_finalizing")) {
1062 py::object finalizing_func = sys_module.attr("_is_finalizing");
! 1063 if (!finalizing_func.is_none() &&
! 1064 finalizing_func().cast<bool>()) {
! 1065 return true; // Python is finalizing
1066 }
1067 }
1068 }
1069 return false;Lines 1067-1076 1067 }
1068 }
1069 return false;
1070 } catch (...) {
! 1071 std::cerr << "Error occurred while checking Python finalization state."
! 1072 << std::endl;
1073 // Be conservative - don't assume shutdown on any exception
1074 // Only return true if we're absolutely certain Python is shutting down
1075 return false;
1076 }Lines 1091-1105 1091 .attr("get_logger")();
1092 if (py::isinstance<py::none>(logger)) return;
1093
1094 try {
! 1095 std::string ddbcFormatString =
! 1096 "[DDBC Bindings log] " + formatString;
1097 if constexpr (sizeof...(args) == 0) {
1098 logger.attr("debug")(py::str(ddbcFormatString));
1099 } else {
! 1100 py::str message = py::str(ddbcFormatString)
! 1101 .format(std::forward<Args>(args)...);
1102 logger.attr("debug")(message);
1103 }
1104 } catch (const std::exception& e) {
1105 std::cerr << "Logging error: " << e.what() << std::endl;Lines 1105-1117 1105 std::cerr << "Logging error: " << e.what() << std::endl;
1106 }
1107 } catch (const py::error_already_set& e) {
1108 // Python is shutting down or in an inconsistent state, silently ignore
! 1109 (void)e; // Suppress unused variable warning
1110 return;
1111 } catch (const std::exception& e) {
1112 // Any other error, ignore to prevent crash during cleanup
! 1113 (void)e; // Suppress unused variable warning
1114 return;
1115 }
1116 }Lines 1230-1244 1230
1231 // Detect platform and set path
1232 #ifdef __linux__
1233 if (fs::exists("/etc/alpine-release")) {
! 1234 platform = "alpine";
1235 } else if (fs::exists("/etc/redhat-release") ||
1236 fs::exists("/etc/centos-release")) {
! 1237 platform = "rhel";
1238 } else if (fs::exists("/etc/SuSE-release") ||
1239 fs::exists("/etc/SUSE-brand")) {
! 1240 platform = "suse";
1241 } else {
1242 platform =
1243 "debian_ubuntu"; // Default to debian_ubuntu for other distros
1244 }Lines 1320-1331 1320
1321 DriverHandle handle = LoadDriverLibrary(driverPath.string());
1322 if (!handle) {
1323 LOG("Failed to load driver: {}", GetLastErrorMessage());
! 1324 ThrowStdException(
! 1325 "Failed to load the driver. Please read the documentation "
! 1326 "(https://github.com/microsoft/mssql-python#installation) to "
! 1327 "install the required dependencies.");
1328 }
1329 LOG("Driver library successfully loaded.");
1330
1331 // Load function pointers using helperLines 1417-1426 1417 SQLForeignKeys_ptr && SQLPrimaryKeys_ptr && SQLSpecialColumns_ptr &&
1418 SQLStatistics_ptr && SQLColumns_ptr;
1419
1420 if (!success) {
! 1421 ThrowStdException(
! 1422 "Failed to load required function pointers from driver.");
1423 }
1424 LOG("All driver function pointers successfully loaded.");
1425 return handle;
1426 }Lines 1785-1793 1785 LOG("Checking errors for retcode - {}", retcode);
1786 ErrorInfo errorInfo;
1787 if (retcode == SQL_INVALID_HANDLE) {
1788 LOG("Invalid handle received");
! 1789 errorInfo.ddbcErrorMsg = std::wstring(L"Invalid handle!");
1790 return errorInfo;
1791 }
1792 assert(handle != 0);
1793 SQLHANDLE rawHandle = handle->get();Lines 1888-1896 1888 }
1889
1890 // Wrap SQLExecDirect
1891 SQLRETURN SQLExecDirect_wrap(SqlHandlePtr StatementHandle,
! 1892 const std::wstring& Query) {
1893 LOG("Execute SQL query directly - {}", Query.c_str());
1894 if (!SQLExecDirect_ptr) {
1895 LOG("Function pointer not initialized. Loading the driver.");
1896 DriverLoader::getInstance().loadDriver(); // Load the driverLines 1897-1908 1897 }
1898
1899 // Ensure statement is scrollable BEFORE executing
1900 if (SQLSetStmtAttr_ptr && StatementHandle && StatementHandle->get()) {
! 1901 SQLSetStmtAttr_ptr(StatementHandle->get(), SQL_ATTR_CURSOR_TYPE,
! 1902 (SQLPOINTER)SQL_CURSOR_STATIC, 0);
! 1903 SQLSetStmtAttr_ptr(StatementHandle->get(), SQL_ATTR_CONCURRENCY,
! 1904 (SQLPOINTER)SQL_CONCUR_READ_ONLY, 0);
1905 }
1906
1907 SQLWCHAR* queryPtr;
1908 #if defined(__APPLE__) || defined(__linux__)Lines 1910-1919 1910 queryPtr = queryBuffer.data();
1911 #else
1912 queryPtr = const_cast<SQLWCHAR*>(Query.c_str());
1913 #endif
! 1914 SQLRETURN ret =
! 1915 SQLExecDirect_ptr(StatementHandle->get(), queryPtr, SQL_NTS);
1916 if (!SQL_SUCCEEDED(ret)) {
1917 LOG("Failed to execute query directly");
1918 }
1919 return ret;Lines 2101-2110 2101 break;
2102 }
2103 }
2104 if (!matchedInfo) {
! 2105 ThrowStdException(
! 2106 "Unrecognized paramToken returned by SQLParamData");
2107 }
2108 const py::object& pyObj = matchedInfo->dataPtr;
2109 if (pyObj.is_none()) {
2110 SQLPutData_ptr(hStmt, nullptr, 0);Lines 2131-2141 2131 size_t lenBytes = len * sizeof(SQLWCHAR);
2132 if (lenBytes >
2133 static_cast<size_t>(
2134 std::numeric_limits<SQLLEN>::max())) {
! 2135 ThrowStdException(
! 2136 "Chunk size exceeds maximum allowed by "
! 2137 "SQLLEN");
2138 }
2139 rc = SQLPutData_ptr(hStmt,
2140 (SQLPOINTER)(dataPtr + offset),
2141 static_cast<SQLLEN>(lenBytes));Lines 2139-2148 2139 rc = SQLPutData_ptr(hStmt,
2140 (SQLPOINTER)(dataPtr + offset),
2141 static_cast<SQLLEN>(lenBytes));
2142 if (!SQL_SUCCEEDED(rc)) {
! 2143 LOG("SQLPutData failed at offset {} of {}",
! 2144 offset, totalChars);
2145 return rc;
2146 }
2147 offset += len;
2148 }Lines 2152-2168 2152 const char* dataPtr = s.data();
2153 size_t offset = 0;
2154 size_t chunkBytes = DAE_CHUNK_SIZE;
2155 while (offset < totalBytes) {
! 2156 size_t len =
! 2157 std::min(chunkBytes, totalBytes - offset);
2158
! 2159 rc = SQLPutData_ptr(hStmt,
! 2160 (SQLPOINTER)(dataPtr + offset),
! 2161 static_cast<SQLLEN>(len));
2162 if (!SQL_SUCCEEDED(rc)) {
! 2163 LOG("SQLPutData failed at offset {} of {}",
! 2164 offset, totalBytes);
2165 return rc;
2166 }
2167 offset += len;
2168 }Lines 2182-2191 2182 rc = SQLPutData_ptr(hStmt,
2183 (SQLPOINTER)(dataPtr + offset),
2184 static_cast<SQLLEN>(len));
2185 if (!SQL_SUCCEEDED(rc)) {
! 2186 LOG("SQLPutData failed at offset {} of {}", offset,
! 2187 totalBytes);
2188 return rc;
2189 }
2190 }
2191 } else {Lines 2228-2237 2228 const py::list& columnValues =
2229 columnwise_params[paramIndex].cast<py::list>();
2230 const ParamInfo& info = paramInfos[paramIndex];
2231 if (columnValues.size() != paramSetSize) {
! 2232 ThrowStdException("Column " + std::to_string(paramIndex) +
! 2233 " has mismatched size.");
2234 }
2235 void* dataPtr = nullptr;
2236 SQLLEN* strLenOrIndArray = nullptr;
2237 SQLLEN bufferLength = 0;Lines 2241-2251 2241 tempBuffers, paramSetSize);
2242 for (size_t i = 0; i < paramSetSize; ++i) {
2243 if (columnValues[i].is_none()) {
2244 if (!strLenOrIndArray)
! 2245 strLenOrIndArray =
! 2246 AllocateParamBufferArray<SQLLEN>(
! 2247 tempBuffers, paramSetSize);
2248 dataArray[i] = 0;
2249 strLenOrIndArray[i] = SQL_NULL_DATA;
2250 } else {
2251 dataArray[i] = columnValues[i].cast<int>();Lines 2255-2270 2255 dataPtr = dataArray;
2256 break;
2257 }
2258 case SQL_C_DOUBLE: {
! 2259 double* dataArray = AllocateParamBufferArray<double>(
! 2260 tempBuffers, paramSetSize);
2261 for (size_t i = 0; i < paramSetSize; ++i) {
2262 if (columnValues[i].is_none()) {
2263 if (!strLenOrIndArray)
! 2264 strLenOrIndArray =
! 2265 AllocateParamBufferArray<SQLLEN>(
! 2266 tempBuffers, paramSetSize);
2267 dataArray[i] = 0;
2268 strLenOrIndArray[i] = SQL_NULL_DATA;
2269 } else {
2270 dataArray[i] = columnValues[i].cast<double>();Lines 2296-2312 2296 // against column size
2297 if (utf16Buf.size() > 0 &&
2298 (utf16Buf.size() - 1) > info.columnSize) {
2299 std::string offending = WideToUTF8(wstr);
! 2300 ThrowStdException(
! 2301 "Input string UTF-16 length exceeds "
! 2302 "allowed column size at parameter index " +
! 2303 std::to_string(paramIndex) +
! 2304 ". UTF-16 length: " +
! 2305 std::to_string(utf16Buf.size() - 1) +
! 2306 ", Column size: " +
! 2307 std::to_string(info.columnSize));
! 2308 }
2309 // Secure copy: use validated bounds for
2310 // defense-in-depth
2311 size_t copyBytes =
2312 utf16Buf.size() * sizeof(SQLWCHAR);Lines 2315-2329 2315 SQLWCHAR* destPtr =
2316 wcharArray + i * (info.columnSize + 1);
2317
2318 if (copyBytes > bufferBytes) {
! 2319 ThrowStdException(
! 2320 "Buffer overflow prevented in WCHAR array "
! 2321 "binding at parameter "
! 2322 "index " +
! 2323 std::to_string(paramIndex) +
! 2324 ", array element " + std::to_string(i));
! 2325 }
2326 if (copyBytes > 0) {
2327 std::copy_n(reinterpret_cast<const char*>(
2328 utf16Buf.data()),
2329 copyBytes,Lines 2381-2391 2381 strLenOrIndArray[i] = SQL_NULL_DATA;
2382 } else {
2383 int intVal = columnValues[i].cast<int>();
2384 if (intVal < 0 || intVal > 255) {
! 2385 ThrowStdException(
! 2386 "UTINYINT value out of range at rowIndex " +
! 2387 std::to_string(i));
2388 }
2389 dataArray[i] = static_cast<unsigned char>(intVal);
2390 if (strLenOrIndArray) strLenOrIndArray[i] = 0;
2391 }Lines 2399-2409 2399 tempBuffers, paramSetSize);
2400 for (size_t i = 0; i < paramSetSize; ++i) {
2401 if (columnValues[i].is_none()) {
2402 if (!strLenOrIndArray)
! 2403 strLenOrIndArray =
! 2404 AllocateParamBufferArray<SQLLEN>(
! 2405 tempBuffers, paramSetSize);
2406 dataArray[i] = 0;
2407 strLenOrIndArray[i] = SQL_NULL_DATA;
2408 } else {
2409 int intVal = columnValues[i].cast<int>();Lines 2408-2418 2408 } else {
2409 int intVal = columnValues[i].cast<int>();
2410 if (intVal < std::numeric_limits<int16_t>::min() ||
2411 intVal > std::numeric_limits<int16_t>::max()) {
! 2412 ThrowStdException(
! 2413 "SHORT value out of range at rowIndex " +
! 2414 std::to_string(i));
2415 }
2416 dataArray[i] = static_cast<int16_t>(intVal);
2417 if (strLenOrIndArray) strLenOrIndArray[i] = 0;
2418 }Lines 2441-2475 2441 encoding_settings &&
2442 !encoding_settings.is_none() &&
2443 encoding_settings.contains("ctype") &&
2444 encoding_settings.contains("encoding")) {
! 2445 SQLSMALLINT ctype = encoding_settings["ctype"]
! 2446 .cast<SQLSMALLINT>();
! 2447 if (ctype == SQL_C_CHAR) {
! 2448 try {
! 2449 py::dict settings_dict =
! 2450 encoding_settings.cast<py::dict>();
! 2451 auto [encoding, errors] =
! 2452 extract_encoding_settings(
! 2453 settings_dict);
2454 // Use our safe encoding function
! 2455 py::bytes encoded_bytes =
! 2456 EncodingString(
! 2457 columnValues[i]
! 2458 .cast<std::string>(),
! 2459 encoding, errors);
! 2460 str = encoded_bytes.cast<std::string>();
! 2461 } catch (const std::exception& e) {
! 2462 ThrowStdException(
! 2463 "Failed to encode "
! 2464 "parameter array element " +
! 2465 std::to_string(i) + ": " +
! 2466 e.what());
! 2467 }
! 2468 } else {
2469 // Default behavior
! 2470 str = columnValues[i].cast<std::string>();
! 2471 }
2472 } else {
2473 // No encoding settings or SQL_C_BINARY - use
2474 // default behavior
2475 str = columnValues[i].cast<std::string>();Lines 2474-2485 2474 // default behavior
2475 str = columnValues[i].cast<std::string>();
2476 }
2477 if (str.size() > info.columnSize) {
! 2478 ThrowStdException(
! 2479 "Input exceeds column size at index " +
! 2480 std::to_string(i));
! 2481 }
2482
2483 // SECURITY: Use secure copy with bounds checking
2484 size_t destOffset = i * (info.columnSize + 1);
2485 size_t destBufferSize = info.columnSize + 1;Lines 2486-2498 2486 size_t copyLength = str.size();
2487
2488 // Validate bounds to prevent buffer overflow
2489 if (copyLength >= destBufferSize) {
! 2490 ThrowStdException(
! 2491 "Buffer overflow prevented at parameter "
! 2492 "array index " +
! 2493 std::to_string(i));
! 2494 }
2495
2496 #ifdef _WIN32
2497 // Windows: Use memcpy_s for secure copy
2498 errno_t err =Lines 2522-2533 2522 bufferLength = info.columnSize + 1;
2523 break;
2524 }
2525 case SQL_C_BIT: {
! 2526 char* boolArray = AllocateParamBufferArray<char>(
! 2527 tempBuffers, paramSetSize);
! 2528 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(
! 2529 tempBuffers, paramSetSize);
2530 for (size_t i = 0; i < paramSetSize; ++i) {
2531 if (columnValues[i].is_none()) {
2532 boolArray[i] = 0;
2533 strLenOrIndArray[i] = SQL_NULL_DATA;Lines 2541-2552 2541 break;
2542 }
2543 case SQL_C_STINYINT:
2544 case SQL_C_USHORT: {
! 2545 uint16_t* dataArray = AllocateParamBufferArray<uint16_t>(
! 2546 tempBuffers, paramSetSize);
! 2547 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(
! 2548 tempBuffers, paramSetSize);
2549 for (size_t i = 0; i < paramSetSize; ++i) {
2550 if (columnValues[i].is_none()) {
2551 strLenOrIndArray[i] = SQL_NULL_DATA;
2552 dataArray[i] = 0;Lines 2550-2563 2550 if (columnValues[i].is_none()) {
2551 strLenOrIndArray[i] = SQL_NULL_DATA;
2552 dataArray[i] = 0;
2553 } else {
! 2554 dataArray[i] = columnValues[i].cast<uint16_t>();
2555 strLenOrIndArray[i] = 0;
2556 }
2557 }
2558 dataPtr = dataArray;
! 2559 bufferLength = sizeof(uint16_t);
2560 break;
2561 }
2562 case SQL_C_SBIGINT:
2563 case SQL_C_SLONG:Lines 2598-2623 2598 bufferLength = sizeof(float);
2599 break;
2600 }
2601 case SQL_C_TYPE_DATE: {
! 2602 SQL_DATE_STRUCT* dateArray =
! 2603 AllocateParamBufferArray<SQL_DATE_STRUCT>(tempBuffers,
! 2604 paramSetSize);
! 2605 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(
! 2606 tempBuffers, paramSetSize);
2607 for (size_t i = 0; i < paramSetSize; ++i) {
2608 if (columnValues[i].is_none()) {
2609 strLenOrIndArray[i] = SQL_NULL_DATA;
! 2610 std::memset(&dateArray[i], 0,
! 2611 sizeof(SQL_DATE_STRUCT));
2612 } else {
2613 py::object dateObj = columnValues[i];
! 2614 dateArray[i].year =
! 2615 dateObj.attr("year").cast<SQLSMALLINT>();
! 2616 dateArray[i].month =
! 2617 dateObj.attr("month").cast<SQLUSMALLINT>();
! 2618 dateArray[i].day =
! 2619 dateObj.attr("day").cast<SQLUSMALLINT>();
2620 strLenOrIndArray[i] = 0;
2621 }
2622 }
2623 dataPtr = dateArray;Lines 2624-2649 2624 bufferLength = sizeof(SQL_DATE_STRUCT);
2625 break;
2626 }
2627 case SQL_C_TYPE_TIME: {
! 2628 SQL_TIME_STRUCT* timeArray =
! 2629 AllocateParamBufferArray<SQL_TIME_STRUCT>(tempBuffers,
! 2630 paramSetSize);
! 2631 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(
! 2632 tempBuffers, paramSetSize);
2633 for (size_t i = 0; i < paramSetSize; ++i) {
2634 if (columnValues[i].is_none()) {
2635 strLenOrIndArray[i] = SQL_NULL_DATA;
! 2636 std::memset(&timeArray[i], 0,
! 2637 sizeof(SQL_TIME_STRUCT));
2638 } else {
2639 py::object timeObj = columnValues[i];
! 2640 timeArray[i].hour =
! 2641 timeObj.attr("hour").cast<SQLUSMALLINT>();
! 2642 timeArray[i].minute =
! 2643 timeObj.attr("minute").cast<SQLUSMALLINT>();
! 2644 timeArray[i].second =
! 2645 timeObj.attr("second").cast<SQLUSMALLINT>();
2646 strLenOrIndArray[i] = 0;
2647 }
2648 }
2649 dataPtr = timeArray;Lines 2650-2684 2650 bufferLength = sizeof(SQL_TIME_STRUCT);
2651 break;
2652 }
2653 case SQL_C_TYPE_TIMESTAMP: {
! 2654 SQL_TIMESTAMP_STRUCT* tsArray =
! 2655 AllocateParamBufferArray<SQL_TIMESTAMP_STRUCT>(
! 2656 tempBuffers, paramSetSize);
! 2657 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(
! 2658 tempBuffers, paramSetSize);
2659 for (size_t i = 0; i < paramSetSize; ++i) {
2660 if (columnValues[i].is_none()) {
2661 strLenOrIndArray[i] = SQL_NULL_DATA;
! 2662 std::memset(&tsArray[i], 0,
! 2663 sizeof(SQL_TIMESTAMP_STRUCT));
2664 } else {
2665 py::object dtObj = columnValues[i];
! 2666 tsArray[i].year =
! 2667 dtObj.attr("year").cast<SQLSMALLINT>();
! 2668 tsArray[i].month =
! 2669 dtObj.attr("month").cast<SQLUSMALLINT>();
! 2670 tsArray[i].day =
! 2671 dtObj.attr("day").cast<SQLUSMALLINT>();
! 2672 tsArray[i].hour =
! 2673 dtObj.attr("hour").cast<SQLUSMALLINT>();
! 2674 tsArray[i].minute =
! 2675 dtObj.attr("minute").cast<SQLUSMALLINT>();
! 2676 tsArray[i].second =
! 2677 dtObj.attr("second").cast<SQLUSMALLINT>();
! 2678 tsArray[i].fraction = static_cast<SQLUINTEGER>(
! 2679 dtObj.attr("microsecond").cast<int>() *
! 2680 1000); // µs to ns
2681 strLenOrIndArray[i] = 0;
2682 }
2683 }
2684 dataPtr = tsArray;Lines 2698-2720 2698 for (size_t i = 0; i < paramSetSize; ++i) {
2699 const py::handle& param = columnValues[i];
2700
2701 if (param.is_none()) {
! 2702 std::memset(&dtoArray[i], 0,
! 2703 sizeof(DateTimeOffset));
2704 strLenOrIndArray[i] = SQL_NULL_DATA;
2705 } else {
2706 if (!py::isinstance(param, datetimeType)) {
! 2707 ThrowStdException(MakeParamMismatchErrorStr(
! 2708 info.paramCType, paramIndex));
2709 }
2710
2711 py::object tzinfo = param.attr("tzinfo");
2712 if (tzinfo.is_none()) {
! 2713 ThrowStdException(
! 2714 "Datetime object must have "
! 2715 "tzinfo for SQL_C_SS_TIMESTAMPOFFSET at "
! 2716 "paramIndex " +
2717 std::to_string(paramIndex));
2718 }
2719
2720 // Populate the C++ struct directly from the PythonLines 2757-2786 2757 bufferLength = sizeof(DateTimeOffset);
2758 break;
2759 }
2760 case SQL_C_NUMERIC: {
! 2761 SQL_NUMERIC_STRUCT* numericArray =
! 2762 AllocateParamBufferArray<SQL_NUMERIC_STRUCT>(
! 2763 tempBuffers, paramSetSize);
! 2764 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(
! 2765 tempBuffers, paramSetSize);
2766 for (size_t i = 0; i < paramSetSize; ++i) {
2767 const py::handle& element = columnValues[i];
2768 if (element.is_none()) {
2769 strLenOrIndArray[i] = SQL_NULL_DATA;
! 2770 std::memset(&numericArray[i], 0,
! 2771 sizeof(SQL_NUMERIC_STRUCT));
2772 continue;
2773 }
2774 if (!py::isinstance<NumericData>(element)) {
! 2775 throw std::runtime_error(MakeParamMismatchErrorStr(
! 2776 info.paramCType, paramIndex));
2777 }
2778 NumericData decimalParam = element.cast<NumericData>();
! 2779 LOG("Received numeric parameter at [%zu]: "
! 2780 "precision=%d, scale=%d, sign=%d, val=%s",
! 2781 i, decimalParam.precision, decimalParam.scale,
! 2782 decimalParam.sign, decimalParam.val.c_str());
2783 SQL_NUMERIC_STRUCT& target = numericArray[i];
2784 std::memset(&target, 0, sizeof(SQL_NUMERIC_STRUCT));
2785 target.precision = decimalParam.precision;
2786 target.scale = decimalParam.scale;Lines 2784-2797 2784 std::memset(&target, 0, sizeof(SQL_NUMERIC_STRUCT));
2785 target.precision = decimalParam.precision;
2786 target.scale = decimalParam.scale;
2787 target.sign = decimalParam.sign;
! 2788 size_t copyLen = std::min(decimalParam.val.size(),
! 2789 sizeof(target.val));
2790 // Secure copy: bounds already validated with std::min
2791 if (copyLen > 0) {
! 2792 std::copy_n(decimalParam.val.data(), copyLen,
! 2793 target.val);
2794 }
2795 strLenOrIndArray[i] = sizeof(SQL_NUMERIC_STRUCT);
2796 }
2797 dataPtr = numericArray;Lines 2819-2835 2819 continue;
2820 } else if (py::isinstance<py::bytes>(element)) {
2821 py::bytes b = element.cast<py::bytes>();
2822 if (PyBytes_GET_SIZE(b.ptr()) != 16) {
! 2823 ThrowStdException(
! 2824 "UUID binary data must be exactly "
! 2825 "16 bytes long.");
2826 }
2827 // Secure copy: Fixed 16-byte copy, size validated
2828 // above
! 2829 std::copy_n(reinterpret_cast<const unsigned char*>(
! 2830 PyBytes_AS_STRING(b.ptr())),
! 2831 16, uuid_bytes.data());
2832 } else if (py::isinstance(element, uuid_class)) {
2833 py::bytes b =
2834 element.attr("bytes_le").cast<py::bytes>();
2835 // Secure copy: Fixed 16-byte copy from UUIDLines 2837-2846 2837 std::copy_n(reinterpret_cast<const unsigned char*>(
2838 PyBytes_AS_STRING(b.ptr())),
2839 16, uuid_bytes.data());
2840 } else {
! 2841 ThrowStdException(MakeParamMismatchErrorStr(
! 2842 info.paramCType, paramIndex));
2843 }
2844 guidArray[i].Data1 =
2845 (static_cast<uint32_t>(uuid_bytes[3]) << 24) |
2846 (static_cast<uint32_t>(uuid_bytes[2]) << 16) |Lines 2861-2871 2861 bufferLength = sizeof(SQLGUID);
2862 break;
2863 }
2864 default: {
! 2865 ThrowStdException(
! 2866 "BindParameterArray: Unsupported C type: " +
! 2867 std::to_string(info.paramCType));
2868 }
2869 }
2870 RETCODE rc = SQLBindParameter_ptr(
2871 hStmt, static_cast<SQLUSMALLINT>(paramIndex + 1),Lines 2928-2938 2928 for (size_t rowIndex = 0; rowIndex < rowCount; ++rowIndex) {
2929 py::list rowParams = columnwise_params[rowIndex];
2930
2931 std::vector<std::shared_ptr<void>> paramBuffers;
! 2932 rc = BindParameters(hStmt, rowParams,
! 2933 const_cast<std::vector<ParamInfo>&>(paramInfos),
! 2934 paramBuffers, encoding_settings);
2935 if (!SQL_SUCCEEDED(rc)) return rc;
2936
2937 rc = SQLExecute_ptr(hStmt);
2938 while (rc == SQL_NEED_DATA) {Lines 2945-2960 2945
2946 if (py::isinstance<py::str>(*py_obj_ptr)) {
2947 std::string data = py_obj_ptr->cast<std::string>();
2948 SQLLEN data_len = static_cast<SQLLEN>(data.size());
! 2949 rc = SQLPutData_ptr(hStmt, (SQLPOINTER)data.c_str(),
! 2950 data_len);
! 2951 } else if (py::isinstance<py::bytes>(*py_obj_ptr) ||
! 2952 py::isinstance<py::bytearray>(*py_obj_ptr)) {
2953 std::string data = py_obj_ptr->cast<std::string>();
2954 SQLLEN data_len = static_cast<SQLLEN>(data.size());
! 2955 rc = SQLPutData_ptr(hStmt, (SQLPOINTER)data.c_str(),
! 2956 data_len);
2957 } else {
2958 LOG("Unsupported DAE parameter type in row {}", rowIndex);
2959 return SQL_ERROR;
2960 }Lines 3106-3114 3106 if (ret == SQL_ERROR ||
3107 (!SQL_SUCCEEDED(ret) && ret != SQL_SUCCESS_WITH_INFO)) {
3108 std::ostringstream oss;
3109 oss << "Error fetching LOB for column " << colIndex
! 3110 << ", cType=" << cType << ", loop=" << loopCount
3111 << ", SQLGetData return=" << ret;
3112 LOG(oss.str());
3113 ThrowStdException(oss.str());
3114 }Lines 3204-3222 3204 "using encoding '{}'",
3205 char_encoding);
3206 return decoded_str;
3207 } catch (const std::exception& e) {
! 3208 LOG("FetchLobColumnData: Dynamic decoding failed: {}. "
! 3209 "Using fallback.",
! 3210 e.what());
3211 // Fallback to original logic
! 3212 }
3213 }
3214
3215 // Fallback: original behavior for SQL_C_CHAR
3216 std::string str(buffer.data(), buffer.size());
! 3217 LOG("FetchLobColumnData: Returning narrow string of length {}",
! 3218 str.length());
3219 return py::str(str);
3220 }
3221
3222 // Helper function to retrieve column dataLines 3246-3256 3246 ret = SQLDescribeCol_ptr(
3247 hStmt, i, columnName, sizeof(columnName) / sizeof(SQLWCHAR),
3248 &columnNameLen, &dataType, &columnSize, &decimalDigits, &nullable);
3249 if (!SQL_SUCCEEDED(ret)) {
! 3250 LOG("Error retrieving data for column - {}, SQLDescribeCol "
! 3251 "return code - {}",
! 3252 i, ret);
3253 row.append(py::none());
3254 continue;
3255 }Lines 3287-3302 3287 LOG("Applied dynamic decoding for CHAR "
3288 "column {} using encoding '{}'",
3289 i, char_encoding);
3290 } catch (const std::exception& e) {
! 3291 LOG("Dynamic decoding failed for column "
! 3292 "{}: {}. Using fallback.",
! 3293 i, e.what());
3294 // Fallback to platform-specific handling
! 3295 #if defined(__APPLE__) || defined(__linux__)
! 3296 std::string fullStr(reinterpret_cast<char*>(
! 3297 dataBuffer.data()));
! 3298 row.append(fullStr);
3299 #else
3300 row.append(
3301 std::string(reinterpret_cast<char*>(
3302 dataBuffer.data())));Lines 3300-3316 3300 row.append(
3301 std::string(reinterpret_cast<char*>(
3302 dataBuffer.data())));
3303 #endif
! 3304 }
3305 } else {
3306 // Buffer too small, fallback to streaming
! 3307 LOG("CHAR column {} data truncated, "
! 3308 "using streaming LOB",
! 3309 i);
! 3310 row.append(FetchLobColumnData(
! 3311 hStmt, i, SQL_C_CHAR, false, false,
! 3312 char_encoding));
3313 }
3314 } else if (dataLen == SQL_NULL_DATA) {
3315 LOG("Column {} is NULL (CHAR)", i);
3316 row.append(py::none());Lines 3316-3345 3316 row.append(py::none());
3317 } else if (dataLen == 0) {
3318 row.append(py::str(""));
3319 } else if (dataLen == SQL_NO_TOTAL) {
! 3320 LOG("SQLGetData couldn't determine the length of "
! 3321 "the "
! 3322 "data. Returning NULL value instead. Column ID "
! 3323 "- {}, "
! 3324 "Data Type - {}",
! 3325 i, dataType);
3326 row.append(py::none());
3327 } else if (dataLen < 0) {
! 3328 LOG("SQLGetData returned an unexpected negative "
! 3329 "data "
! 3330 "length. Raising exception. Column ID - {}, "
! 3331 "Data Type - {}, Data Length - {}",
3332 i, dataType, dataLen);
! 3333 ThrowStdException(
! 3334 "SQLGetData returned an unexpected "
! 3335 "negative data length");
3336 }
3337 } else {
! 3338 LOG("Error retrieving data for column - {}, data type "
! 3339 "- "
! 3340 "{}, SQLGetData return code - {}. Returning NULL "
! 3341 "value instead",
3342 i, dataType, ret);
3343 row.append(py::none());
3344 }
3345 }Lines 3401-3427 3401 row.append(py::none());
3402 } else if (dataLen == 0) {
3403 row.append(py::str(""));
3404 } else if (dataLen == SQL_NO_TOTAL) {
! 3405 LOG("SQLGetData couldn't determine the length of "
! 3406 "the NVARCHAR data. Returning NULL. "
! 3407 "Column ID - {}",
! 3408 i);
3409 row.append(py::none());
3410 } else if (dataLen < 0) {
! 3411 LOG("SQLGetData returned an unexpected negative "
! 3412 "data "
! 3413 "length. Raising exception. Column ID - {}, "
! 3414 "Data Type - {}, Data Length - {}",
3415 i, dataType, dataLen);
! 3416 ThrowStdException(
! 3417 "SQLGetData returned an unexpected "
! 3418 "negative data length");
3419 }
3420 } else {
! 3421 LOG("Error retrieving data for column {} (NVARCHAR), "
! 3422 "SQLGetData return code {}",
! 3423 i, ret);
3424 row.append(py::none());
3425 }
3426 }
3427 break;Lines 3442-3452 3442 NULL);
3443 if (SQL_SUCCEEDED(ret)) {
3444 row.append(static_cast<int>(smallIntValue));
3445 } else {
! 3446 LOG("Error retrieving data for column - {}, "
! 3447 "data type - {}, SQLGetData return code - {}. "
! 3448 "Returning NULL value instead",
3449 i, dataType, ret);
3450 row.append(py::none());
3451 }
3452 break;Lines 3457-3467 3457 SQLGetData_ptr(hStmt, i, SQL_C_FLOAT, &realValue, 0, NULL);
3458 if (SQL_SUCCEEDED(ret)) {
3459 row.append(realValue);
3460 } else {
! 3461 LOG("Error retrieving data for column - {}, "
! 3462 "data type - {}, SQLGetData return code - {}. "
! 3463 "Returning NULL value instead",
3464 i, dataType, ret);
3465 row.append(py::none());
3466 }
3467 break;Lines 3500-3509 3500 }
3501 }
3502 // if no null found, use the full buffer size as a
3503 // conservative fallback
! 3504 if (safeLen == 0 && bufSize > 0 &&
! 3505 cnum[0] != '\0') {
3506 safeLen = bufSize;
3507 }
3508 }Lines 3523-3532 3523 LOG("Error converting to decimal: {}", e.what());
3524 row.append(py::none());
3525 }
3526 } else {
! 3527 LOG("Error retrieving data for column - {}, data type - "
! 3528 "{}, SQLGetData return "
3529 "code - {}. Returning NULL value instead",
3530 i, dataType, ret);
3531 row.append(py::none());
3532 }Lines 3540-3549 3540 NULL);
3541 if (SQL_SUCCEEDED(ret)) {
3542 row.append(doubleValue);
3543 } else {
! 3544 LOG("Error retrieving data for column - {}, data type - "
! 3545 "{}, SQLGetData return "
3546 "code - {}. Returning NULL value instead",
3547 i, dataType, ret);
3548 row.append(py::none());
3549 }Lines 3555-3564 3555 NULL);
3556 if (SQL_SUCCEEDED(ret)) {
3557 row.append(static_cast<int64_t>(bigintValue));
3558 } else {
! 3559 LOG("Error retrieving data for column - {}, data type - "
! 3560 "{}, SQLGetData return "
3561 "code - {}. Returning NULL value instead",
3562 i, dataType, ret);
3563 row.append(py::none());
3564 }Lines 3573-3582 3573 .attr("date")(dateValue.year,
3574 dateValue.month,
3575 dateValue.day));
3576 } else {
! 3577 LOG("Error retrieving data for column - {}, data type - "
! 3578 "{}, SQLGetData return "
3579 "code - {}. Returning NULL value instead",
3580 i, dataType, ret);
3581 row.append(py::none());
3582 }Lines 3593-3602 3593 .attr("time")(timeValue.hour,
3594 timeValue.minute,
3595 timeValue.second));
3596 } else {
! 3597 LOG("Error retrieving data for column - {}, data type - "
! 3598 "{}, SQLGetData return "
3599 "code - {}. Returning NULL value instead",
3600 i, dataType, ret);
3601 row.append(py::none());
3602 }Lines 3618-3627 3618 timestampValue.minute, timestampValue.second,
3619 timestampValue.fraction /
3620 1000)); // Convert back ns to µs
3621 } else {
! 3622 LOG("Error retrieving data for column - {}, data type - "
! 3623 "{}, SQLGetData return "
3624 "code - {}. Returning NULL value instead",
3625 i, dataType, ret);
3626 row.append(py::none());
3627 }Lines 3644-3653 3644 dtoValue.timezone_hour * 60 + dtoValue.timezone_minute;
3645 // Validating offset
3646 if (totalMinutes < -24 * 60 || totalMinutes > 24 * 60) {
3647 std::ostringstream oss;
! 3648 oss << "Invalid timezone offset from "
! 3649 "SQL_SS_TIMESTAMPOFFSET_STRUCT: "
3650 << totalMinutes << " minutes for column " << i;
3651 ThrowStdException(oss.str());
3652 }
3653 // Convert fraction from ns to µsLines 3660-3669 3660 dtoValue.hour, dtoValue.minute, dtoValue.second,
3661 microseconds, tzinfo);
3662 row.append(py_dt);
3663 } else {
! 3664 LOG("Error fetching DATETIMEOFFSET for column {}, ret={}",
! 3665 i, ret);
3666 row.append(py::none());
3667 }
3668 break;
3669 }Lines 3692-3704 3692 py::bytes(reinterpret_cast<const char*>(
3693 dataBuffer.data()),
3694 dataLen));
3695 } else {
! 3696 LOG("VARBINARY column {} data truncated, "
! 3697 "using streaming LOB",
! 3698 i);
! 3699 row.append(FetchLobColumnData(
! 3700 hStmt, i, SQL_C_BINARY, false, true));
3701 }
3702 } else if (dataLen == SQL_NULL_DATA) {
3703 row.append(py::none());
3704 } else if (dataLen == 0) {Lines 3704-3722 3704 } else if (dataLen == 0) {
3705 row.append(py::bytes(""));
3706 } else {
3707 std::ostringstream oss;
! 3708 oss << "Unexpected negative length (" << dataLen
! 3709 << ") returned by SQLGetData. ColumnID=" << i
! 3710 << ", dataType=" << dataType
! 3711 << ", bufferSize=" << columnSize;
3712 LOG("Error: {}", oss.str());
3713 ThrowStdException(oss.str());
3714 }
3715 } else {
! 3716 LOG("Error retrieving VARBINARY data for column {}. "
! 3717 "SQLGetData rc = {}",
! 3718 i, ret);
3719 row.append(py::none());
3720 }
3721 }
3722 break;Lines 3727-3737 3727 NULL);
3728 if (SQL_SUCCEEDED(ret)) {
3729 row.append(static_cast<int>(tinyIntValue));
3730 } else {
! 3731 LOG("Error retrieving data for column - {}, data type - "
! 3732 "{}, SQLGetData return code - {}. Returning NULL "
! 3733 "value instead",
3734 i, dataType, ret);
3735 row.append(py::none());
3736 }
3737 break;Lines 3741-3751 3741 ret = SQLGetData_ptr(hStmt, i, SQL_C_BIT, &bitValue, 0, NULL);
3742 if (SQL_SUCCEEDED(ret)) {
3743 row.append(static_cast<bool>(bitValue));
3744 } else {
! 3745 LOG("Error retrieving data for column - {}, data type - "
! 3746 "{}, SQLGetData return code - {}. Returning NULL "
! 3747 "value instead",
3748 i, dataType, ret);
3749 row.append(py::none());
3750 }
3751 break;Lines 3787-3797 3787 row.append(uuid_obj);
3788 } else if (indicator == SQL_NULL_DATA) {
3789 row.append(py::none());
3790 } else {
! 3791 LOG("Error retrieving data for column - {}, data type - "
! 3792 "{}, SQLGetData return code - {}. Returning NULL "
! 3793 "value instead",
3794 i, dataType, ret);
3795 row.append(py::none());
3796 }
3797 break;Lines 3798-3808 3798 }
3799 #endif
3800 default:
3801 std::ostringstream errorString;
! 3802 errorString << "Unsupported data type for column - "
! 3803 << columnName << ", Type - " << dataType
! 3804 << ", column ID - " << i;
3805 LOG(errorString.str());
3806 ThrowStdException(errorString.str());
3807 break;
3808 }Lines 4002-4015 4002 sizeof(DateTimeOffset) * fetchSize,
4003 buffers.indicators[col - 1].data());
4004 break;
4005 default:
! 4006 std::wstring columnName =
! 4007 columnMeta["ColumnName"].cast<std::wstring>();
4008 std::ostringstream errorString;
! 4009 errorString << "Unsupported data type for column - "
! 4010 << columnName.c_str() << ", Type - " << dataType
! 4011 << ", column ID - " << col;
4012 LOG(errorString.str());
4013 ThrowStdException(errorString.str());
4014 break;
4015 }Lines 4013-4025 4013 ThrowStdException(errorString.str());
4014 break;
4015 }
4016 if (!SQL_SUCCEEDED(ret)) {
! 4017 std::wstring columnName =
! 4018 columnMeta["ColumnName"].cast<std::wstring>();
4019 std::ostringstream errorString;
! 4020 errorString << "Failed to bind column - " << columnName.c_str()
! 4021 << ", Type - " << dataType << ", column ID - " << col;
4022 LOG(errorString.str());
4023 ThrowStdException(errorString.str());
4024 return ret;
4025 }Lines 4065-4081 4065 // wont suffice
4066 // This value indicates that the driver cannot determine the
4067 // length of the data
4068 if (dataLen == SQL_NO_TOTAL) {
! 4069 LOG("Cannot determine the length of the data. Returning "
! 4070 "NULL value instead. Column ID - {}",
! 4071 col);
4072 row.append(py::none());
4073 continue;
4074 } else if (dataLen == SQL_NULL_DATA) {
! 4075 LOG("Column data is NULL. Appending None to the result "
! 4076 "row. Column ID - {}",
! 4077 col);
4078 row.append(py::none());
4079 continue;
4080 } else if (dataLen == 0) {
4081 // Handle zero-length (non-NULL) dataLines 4087-4101 4087 py::str decoded_str =
4088 DecodingString("", 0, char_encoding, "strict");
4089 row.append(decoded_str);
4090 } catch (const std::exception& e) {
! 4091 LOG("Decoding failed for empty SQL_CHAR data: {}",
! 4092 e.what());
! 4093 row.append(std::string(""));
! 4094 }
4095 } else {
! 4096 row.append(std::string(""));
! 4097 }
4098 } else if (dataType == SQL_WCHAR || dataType == SQL_WVARCHAR ||
4099 dataType == SQL_WLONGVARCHAR) {
4100 row.append(std::wstring(L""));
4101 } else if (dataType == SQL_BINARY ||Lines 4104-4115 4104 row.append(py::bytes(""));
4105 } else {
4106 // For other datatypes, 0 length is unexpected. Log &
4107 // append None
! 4108 LOG("Column data length is 0 for non-string/binary "
! 4109 "datatype. Appending None to the result row. Column "
! 4110 "ID - {}",
! 4111 col);
4112 row.append(py::none());
4113 }
4114 continue;
4115 } else if (dataLen < 0) {Lines 4114-4127 4114 continue;
4115 } else if (dataLen < 0) {
4116 // Negative value is unexpected, log column index, SQL type &
4117 // raise exception
! 4118 LOG("Unexpected negative data length. Column ID - {}, "
! 4119 "SQL Type - {}, Data Length - {}",
! 4120 col, dataType, dataLen);
! 4121 ThrowStdException(
! 4122 "Unexpected negative data length, check "
! 4123 "logs for details");
4124 }
4125 assert(dataLen > 0 && "Data length must be > 0");
4126
4127 switch (dataType) {Lines 4150-4171 4150 LOG("Applied dynamic decoding for batch CHAR "
4151 "column {} using encoding '{}'",
4152 col, char_encoding);
4153 } catch (const std::exception& e) {
! 4154 LOG("Dynamic decoding failed for batch column "
! 4155 "{}: {}. Using fallback.",
! 4156 col, e.what());
4157 // Fallback to original logic
! 4158 row.append(std::string(
! 4159 reinterpret_cast<char*>(
! 4160 &buffers.charBuffers[col - 1]
! 4161 [i * fetchBufferSize]),
! 4162 numCharsInData));
! 4163 }
4164 } else {
! 4165 row.append(FetchLobColumnData(hStmt, col, SQL_C_CHAR,
! 4166 false, false,
! 4167 char_encoding));
4168 }
4169 break;
4170 }
4171 case SQL_WCHAR:Lines 4202-4211 4202 [i * fetchBufferSize]),
4203 numCharsInData));
4204 #endif
4205 } else {
! 4206 row.append(FetchLobColumnData(hStmt, col, SQL_C_WCHAR,
! 4207 true, false));
4208 }
4209 break;
4210 }
4211 case SQL_INTEGER: {Lines 4379-4398 4379 reinterpret_cast<const char*>(
4380 &buffers.charBuffers[col - 1][i * columnSize]),
4381 dataLen));
4382 } else {
! 4383 row.append(FetchLobColumnData(hStmt, col, SQL_C_BINARY,
! 4384 false, true));
4385 }
4386 break;
4387 }
4388 default: {
! 4389 std::wstring columnName =
! 4390 columnMeta["ColumnName"].cast<std::wstring>();
4391 std::ostringstream errorString;
! 4392 errorString << "Unsupported data type for column - "
! 4393 << columnName.c_str() << ", Type - " << dataType
! 4394 << ", column ID - " << col;
4395 LOG(errorString.str());
4396 ThrowStdException(errorString.str());
4397 break;
4398 }Lines 4475-4488 4475 case SQL_SS_TIMESTAMPOFFSET:
4476 rowSize += sizeof(DateTimeOffset);
4477 break;
4478 default:
! 4479 std::wstring columnName =
! 4480 columnMeta["ColumnName"].cast<std::wstring>();
4481 std::ostringstream errorString;
! 4482 errorString << "Unsupported data type for column - "
! 4483 << columnName.c_str() << ", Type - " << dataType
! 4484 << ", column ID - " << col;
4485 LOG(errorString.str());
4486 ThrowStdException(errorString.str());
4487 break;
4488 }Lines 4943-4952 4943 "Set the decimal separator character");
4944 m.def(
4945 "DDBCSQLSetStmtAttr",
4946 [](SqlHandlePtr stmt, SQLINTEGER attr, SQLPOINTER value) {
! 4947 return SQLSetStmtAttr_ptr(stmt->get(), attr, value, 0);
! 4948 },
4949 "Set statement attributes");
4950 m.def("DDBCSQLGetTypeInfo", &SQLGetTypeInfo_Wrapper,
4951 "Returns information about the data types that are supported by "
4952 "the data source",Lines 5019-5027 5019 DriverLoader::getInstance().loadDriver(); // Load the driver
5020 } catch (const std::exception& e) {
5021 // Log the error but don't throw -
5022 // let the error happen when functions are called
! 5023 LOG("Failed to load ODBC driver during module initialization: {}",
! 5024 e.what());
5025 }
5026 }📋 Files Needing Attention📉 Files with overall lowest coverage (click to expand)mssql_python.pybind.ddbc_bindings.cpp: 65.9%
mssql_python.row.py: 77.9%
mssql_python.ddbc_bindings.py: 79.6%
mssql_python.pybind.connection.connection.cpp: 81.2%
mssql_python.cursor.py: 83.1%
mssql_python.connection.py: 83.9%
mssql_python.pybind.connection.connection_pool.cpp: 84.8%
mssql_python.auth.py: 87.1%
mssql_python.pooling.py: 87.7%
mssql_python.exceptions.py: 92.1%🔗 Quick Links
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left few comments and also suggested some tests cases to Jahnvi
### Work Item / Issue Reference <!-- IMPORTANT: Please follow the PR template guidelines below. For mssql-python maintainers: Insert your ADO Work Item ID below (e.g. AB#37452) For external contributors: Insert Github Issue number below (e.g. #149) Only one reference is required - either GitHub issue OR ADO Work Item. --> <!-- mssql-python maintainers: ADO Work Item --> > [AB#38478](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/38478) ------------------------------------------------------------------- ### Summary This pull request primarily refactors the formatting and structure of the `mssql_python/pybind/ddbc_bindings.cpp` file, focusing on code readability and maintainability. No functional logic changes are introduced; instead, the changes consist of improved line wrapping, consistent indentation, and clearer inline comments, especially in function definitions and pybind11 module bindings. Formatting and readability improvements: * Reformatted function signatures and argument lists in several places (e.g., `FetchMany_wrap`, pybind11 bindings) for better readability and consistency. * Improved line wrapping and indentation in conditional logic and function calls, making code easier to follow. * Enhanced inline comments, especially around LOB streaming and module-level UUID caching, for clarity. * Updated error logging during ODBC driver loading to use multi-line comments and clearer formatting. Header and include adjustments: * Reordered and deduplicated header includes, grouping standard library headers and removing redundant imports.
Work Item / Issue Reference
Summary
This pull request introduces robust and secure handling of encoding settings throughout the
mssql_pythoncodebase, with a particular focus on validating encoding names, enforcing UTF-16 restrictions for certain SQL types, and ensuring safe propagation of encoding parameters between Python and C++ layers. The changes improve both security (by preventing injection attacks) and correctness (by enforcing SQL Server encoding requirements), while also adding better error handling and logging for encoding-related issues.Encoding validation and enforcement
_validate_encodingfunction inconnection.pyto strictly validate encoding names for security (length and allowed characters) and correctness (must be a valid Python codec), rejecting unsafe or invalid encodings.SQL_WCHARandSQL_WMETADATAtypes in bothsetencodingandsetdecoding, with warnings and automatic fallback to'utf-16le'if an invalid encoding is provided.Propagation of encoding settings
cursor.py(_get_encoding_settingsand_get_decoding_settings) to retrieve encoding and decoding settings from the connection, with error handling and sensible defaults. These settings are now passed to statement execution and data fetching functions.C++ layer encoding safety
ddbc_bindings.cppfor encoding/decoding strings (EncodingString,DecodingString), validating encoding names (is_valid_encoding), error modes, and extracting encoding settings safely from Python dictionaries. These changes ensure only safe and valid encodings are used in the native layer and provide detailed logging for failures.Interface changes for parameter binding
Error handling improvements
cursor.pyto includeOperationalErrorandDatabaseErrorwhen retrieving encoding settings, ensuring that only database-related errors are caught and logged, and that safe defaults are used if retrieval fails.Let me know if you want to walk through how encoding settings flow from Python to C++ or discuss the impact of these changes on query execution and data retrieval.