Skip to content

Commit 879ae0a

Browse files
authored
Merge branch 'main' into bewithgaurav/update_pr_template
2 parents 799d99d + 3b7a613 commit 879ae0a

74 files changed

Lines changed: 2650 additions & 1009 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/agents/release-manager.agent.md

Lines changed: 436 additions & 0 deletions
Large diffs are not rendered by default.

.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ build/
4848

4949
# wheel files
5050
*.whl
51+
52+
# Coverage reports and artifacts
53+
.coverage
54+
coverage.json
55+
coverage*.xml
56+
htmlcov/
57+
unified-coverage/
58+
*.profraw
59+
*.profdata
60+
*.info
5161
*.tar.gz
5262
*.zip
5363

@@ -69,3 +79,11 @@ mssql_py_core/
6979

7080
# learning files
7181
learnings/
82+
83+
# Local development and experimental scripts (not part of the PR)
84+
add_platform_exclusions.py
85+
add_lcov_exclusions.py
86+
fix_multiline_log_exclusions.py
87+
test_pyodbc_decimal.py
88+
run_coverage_docker.ps1
89+
TRIAGE_REPORT_*.md

PyPI_Description.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,20 @@ PyBind11 provides:
3535
- Memory-safe bindings
3636
- Clean and Pythonic API, while performance-critical logic remains in robust, maintainable C++.
3737

38-
## What's new in v1.6.0
38+
## What's new in v1.8.0
3939

4040
### Enhancements
4141

42-
- **Connection String Sanitization** - Connection string sanitization has been migrated from regex-based to parser-based logic, making it more robust and consistent with connection string parsing rules.
42+
- **ActiveDirectoryMSI Support for Bulk Copy** - Adds `Authentication=ActiveDirectoryMSI` support to bulk copy, enabling both system-assigned and user-assigned managed identity authentication for Azure-hosted services (#573).
43+
- **Row String-Key Indexing** - Row objects now support accessing values by column name as a string key (e.g., `row["col"]`), in addition to integer index and attribute access. Case-insensitive lookup is supported when the cursor's `lowercase` attribute is enabled (#589).
44+
- **Bundled ODBC Driver Upgrade** - Updated the bundled Microsoft ODBC Driver for SQL Server from 18.5.1.1 to 18.6.2.1 (#569).
4345

4446
### Bug Fixes
4547

46-
- **GIL Release During ODBC Connect/Disconnect** - The driver now releases the GIL during blocking ODBC connect and disconnect calls, improving concurrency for multi-threaded applications.
47-
- **setinputsizes() SQL_DECIMAL Crash Fix** - Fixed a crash in `cursor.setinputsizes()` when specifying `SQL_DECIMAL` type hints.
48-
- **ODBC Catalog fetchone() Fix** - Fixed an issue where `fetchone()` on ODBC catalog method results returned incorrect data.
49-
- **cursor.execute() Invalid Cursor State Fix** - Fixed `cursor.execute()` raising an Invalid cursor state error when called with `reset_cursor=False`.
50-
- **executemany Type Annotation Fix** - Corrected the type annotation for `executemany` `seq_of_parameters` parameter to accept `Mapping` types.
51-
- **setup_logging Path Traversal Guard** - Added path canonicalization and traversal guard to `setup_logging`'s `log_file_path` parameter to prevent path traversal issues.
52-
48+
- **Deferred Connect-Attribute Use-After-Free** - Fixed a use-after-free in `Connection.setAttribute` for deferred ODBC attributes (e.g., `SQL_COPT_SS_ACCESS_TOKEN`) that caused SIGBUS on macOS arm64 and authentication failures on Windows and Azure SQL (#596).
49+
- **Connection String Parsed Multiple Times in Auth Path** - Refactored authentication handling to use dictionary-based parameter processing instead of repeated string parsing, improving reliability and performance (#590).
50+
- **executemany Type Annotation Regression** - Fixed a typing regression where `Cursor.executemany` rejected valid `list[tuple[...]]` arguments under mypy due to invariant `List` type. The parameter type now uses covariant `Sequence` matching PEP 249 (#586).
51+
5352
For more information, please visit the project link on Github: https://github.com/microsoft/mssql-python
5453

5554
If you have any feedback, questions or need support please mail us at mssql-python@microsoft.com.

eng/pipelines/pr-validation-pipeline.yml

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -340,10 +340,10 @@ jobs:
340340
} else {
341341
Write-Host "ODBC Driver 18 for SQL Server not found, installing..."
342342
343-
# Download ODBC Driver 18.5.2.1 (x64) from official Microsoft link
343+
# Download ODBC Driver 18.6.2.1 (x64) from official Microsoft link
344344
$ProgressPreference = 'SilentlyContinue'
345-
$installerUrl = "https://go.microsoft.com/fwlink/?linkid=2335671"
346-
$installerPath = "$env:TEMP\msodbcsql_18.5.2.1_x64.msi"
345+
$installerUrl = "https://go.microsoft.com/fwlink/?linkid=2358430"
346+
$installerPath = "$env:TEMP\msodbcsql_18.6.2.1_x64.msi"
347347
348348
Write-Host "Downloading ODBC Driver 18 (x64) from Microsoft..."
349349
Write-Host " URL: $installerUrl"
@@ -808,7 +808,7 @@ jobs:
808808
odbcinst -u -d -n 'ODBC Driver 18 for SQL Server' || true
809809
echo 'Uninstalled ODBC Driver and cleaned up libraries'
810810
echo 'Verifying x86_64 debian_ubuntu driver library signatures:'
811-
ldd mssql_python/libs/linux/debian_ubuntu/x86_64/lib/libmsodbcsql-18.5.so.1.1
811+
ldd mssql_python/libs/linux/debian_ubuntu/x86_64/lib/libmsodbcsql-18.6.so.2.1
812812
"
813813
displayName: 'Uninstall ODBC Driver before running tests in $(distroName) container'
814814
@@ -1133,7 +1133,7 @@ jobs:
11331133
odbcinst -u -d -n 'ODBC Driver 11 for SQL Server' || true
11341134
echo 'Uninstalled ODBC Driver and cleaned up libraries'
11351135
echo 'Verifying arm64 debian_ubuntu driver library signatures:'
1136-
ldd mssql_python/libs/linux/debian_ubuntu/arm64/lib/libmsodbcsql-18.5.so.1.1
1136+
ldd mssql_python/libs/linux/debian_ubuntu/arm64/lib/libmsodbcsql-18.6.so.2.1
11371137
"
11381138
displayName: 'Uninstall ODBC Driver before running tests in $(distroName) ARM64 container'
11391139
@@ -1346,7 +1346,7 @@ jobs:
13461346
odbcinst -u -d -n 'ODBC Driver 11 for SQL Server' || true
13471347
echo 'Uninstalled ODBC Driver and cleaned up libraries'
13481348
echo 'Verifying x86_64 rhel driver library signatures:'
1349-
ldd mssql_python/libs/linux/rhel/x86_64/lib/libmsodbcsql-18.5.so.1.1
1349+
ldd mssql_python/libs/linux/rhel/x86_64/lib/libmsodbcsql-18.6.so.2.1
13501350
"
13511351
displayName: 'Uninstall ODBC Driver before running tests in RHEL 9 container'
13521352
@@ -1571,7 +1571,7 @@ jobs:
15711571
odbcinst -u -d -n 'ODBC Driver 18 for SQL Server' || true
15721572
echo 'Uninstalled ODBC Driver and cleaned up libraries'
15731573
echo 'Verifying arm64 rhel driver library signatures:'
1574-
ldd mssql_python/libs/linux/rhel/arm64/lib/libmsodbcsql-18.5.so.1.1
1574+
ldd mssql_python/libs/linux/rhel/arm64/lib/libmsodbcsql-18.6.so.2.1
15751575
"
15761576
displayName: 'Uninstall ODBC Driver before running tests in RHEL 9 ARM64 container'
15771577
@@ -1731,24 +1731,24 @@ jobs:
17311731
echo 'Detected architecture: '\$architecture
17321732
17331733
# Download the packages
1734-
curl -O https://download.microsoft.com/download/fae28b9a-d880-42fd-9b98-d779f0fdd77f/msodbcsql18_18.5.1.1-1_\$architecture.apk
1734+
curl -O https://download.microsoft.com/download/0b3d5518-b4a7-4a2b-afc7-7ee9e967f93c/msodbcsql18_18.6.2.1-1_\$architecture.apk
17351735
curl -O https://download.microsoft.com/download/7/6d/76de322a-d860-4894-9945-f0cc5d6a45f8/mssql-tools18_18.4.1.1-1_\$architecture.apk
17361736
17371737
# Download signatures for verification
1738-
curl -O https://download.microsoft.com/download/fae28b9a-d880-42fd-9b98-d779f0fdd77f/msodbcsql18_18.5.1.1-1_\$architecture.sig
1738+
curl -O https://download.microsoft.com/download/0b3d5518-b4a7-4a2b-afc7-7ee9e967f93c/msodbcsql18_18.6.2.1-1_\$architecture.sig
17391739
curl -O https://download.microsoft.com/download/7/6d/76de322a-d860-4894-9945-f0cc5d6a45f8/mssql-tools18_18.4.1.1-1_\$architecture.sig
17401740
17411741
# Import Microsoft GPG key and verify packages
17421742
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --import -
1743-
gpg --verify msodbcsql18_18.5.1.1-1_\$architecture.sig msodbcsql18_18.5.1.1-1_\$architecture.apk
1743+
gpg --verify msodbcsql18_18.6.2.1-1_\$architecture.sig msodbcsql18_18.6.2.1-1_\$architecture.apk
17441744
gpg --verify mssql-tools18_18.4.1.1-1_\$architecture.sig mssql-tools18_18.4.1.1-1_\$architecture.apk
17451745
17461746
# Install the packages
1747-
apk add --allow-untrusted msodbcsql18_18.5.1.1-1_\$architecture.apk
1747+
apk add --allow-untrusted msodbcsql18_18.6.2.1-1_\$architecture.apk
17481748
apk add --allow-untrusted mssql-tools18_18.4.1.1-1_\$architecture.apk
17491749
17501750
# Cleanup
1751-
rm -f msodbcsql18_18.5.1.1-1_\$architecture.* mssql-tools18_18.4.1.1-1_\$architecture.*
1751+
rm -f msodbcsql18_18.6.2.1-1_\$architecture.* mssql-tools18_18.4.1.1-1_\$architecture.*
17521752
17531753
# Add mssql-tools to PATH
17541754
export PATH=\"\$PATH:/opt/mssql-tools18/bin\"
@@ -1805,7 +1805,7 @@ jobs:
18051805
odbcinst -u -d -n 'ODBC Driver 18 for SQL Server' || true
18061806
echo 'Uninstalled system ODBC Driver and cleaned up libraries'
18071807
echo 'Verifying x86_64 alpine driver library signatures:'
1808-
ldd mssql_python/libs/linux/alpine/x86_64/lib/libmsodbcsql-18.5.so.1.1 || echo 'Driver library not found'
1808+
ldd mssql_python/libs/linux/alpine/x86_64/lib/libmsodbcsql-18.6.so.2.1 || echo 'Driver library not found'
18091809
"
18101810
displayName: 'Uninstall system ODBC Driver before running tests in Alpine x86_64 container'
18111811
@@ -1981,24 +1981,24 @@ jobs:
19811981
echo 'Detected architecture: '\$architecture
19821982
19831983
# Download the packages
1984-
curl -O https://download.microsoft.com/download/fae28b9a-d880-42fd-9b98-d779f0fdd77f/msodbcsql18_18.5.1.1-1_\$architecture.apk
1984+
curl -O https://download.microsoft.com/download/0b3d5518-b4a7-4a2b-afc7-7ee9e967f93c/msodbcsql18_18.6.2.1-1_\$architecture.apk
19851985
curl -O https://download.microsoft.com/download/7/6d/76de322a-d860-4894-9945-f0cc5d6a45f8/mssql-tools18_18.4.1.1-1_\$architecture.apk
19861986
19871987
# Download signatures for verification
1988-
curl -O https://download.microsoft.com/download/fae28b9a-d880-42fd-9b98-d779f0fdd77f/msodbcsql18_18.5.1.1-1_\$architecture.sig
1988+
curl -O https://download.microsoft.com/download/0b3d5518-b4a7-4a2b-afc7-7ee9e967f93c/msodbcsql18_18.6.2.1-1_\$architecture.sig
19891989
curl -O https://download.microsoft.com/download/7/6d/76de322a-d860-4894-9945-f0cc5d6a45f8/mssql-tools18_18.4.1.1-1_\$architecture.sig
19901990
19911991
# Import Microsoft GPG key and verify packages
19921992
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --import -
1993-
gpg --verify msodbcsql18_18.5.1.1-1_\$architecture.sig msodbcsql18_18.5.1.1-1_\$architecture.apk
1993+
gpg --verify msodbcsql18_18.6.2.1-1_\$architecture.sig msodbcsql18_18.6.2.1-1_\$architecture.apk
19941994
gpg --verify mssql-tools18_18.4.1.1-1_\$architecture.sig mssql-tools18_18.4.1.1-1_\$architecture.apk
19951995
19961996
# Install the packages
1997-
apk add --allow-untrusted msodbcsql18_18.5.1.1-1_\$architecture.apk
1997+
apk add --allow-untrusted msodbcsql18_18.6.2.1-1_\$architecture.apk
19981998
apk add --allow-untrusted mssql-tools18_18.4.1.1-1_\$architecture.apk
19991999
20002000
# Cleanup
2001-
rm -f msodbcsql18_18.5.1.1-1_\$architecture.* mssql-tools18_18.4.1.1-1_\$architecture.*
2001+
rm -f msodbcsql18_18.6.2.1-1_\$architecture.* mssql-tools18_18.4.1.1-1_\$architecture.*
20022002
20032003
# Add mssql-tools to PATH
20042004
export PATH=\"\$PATH:/opt/mssql-tools18/bin\"
@@ -2056,7 +2056,7 @@ jobs:
20562056
odbcinst -u -d -n 'ODBC Driver 18 for SQL Server' || true
20572057
echo 'Uninstalled system ODBC Driver and cleaned up libraries'
20582058
echo 'Verifying arm64 alpine driver library signatures:'
2059-
ldd mssql_python/libs/linux/alpine/arm64/lib/libmsodbcsql-18.5.so.1.1 || echo 'Driver library not found'
2059+
ldd mssql_python/libs/linux/alpine/arm64/lib/libmsodbcsql-18.6.so.2.1 || echo 'Driver library not found'
20602060
"
20612061
displayName: 'Uninstall system ODBC Driver before running tests in Alpine ARM64 container'
20622062
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Join multi-line LOG() calls onto single lines for LCOV coverage filtering.
4+
5+
This script is used only during coverage builds to simplify LOG statement exclusion.
6+
It doesn't modify the original source files - it works on copies during the build.
7+
Adjacent string literals are concatenated at compile time, so runtime behavior is identical.
8+
9+
Uses a proper C++ tokenizer to handle string literals, character literals, and comments
10+
correctly, avoiding issues with unbalanced parentheses or semicolons in strings.
11+
"""
12+
13+
import re
14+
import sys
15+
from pathlib import Path
16+
17+
18+
_LOG_MACRO_PATTERN = re.compile(r'\bLOG[A-Z_]*\s*\(')
19+
20+
21+
def _find_log_macro_open(line: str):
22+
"""Return the index of the opening parenthesis for a LOG-like macro, if present."""
23+
match = _LOG_MACRO_PATTERN.search(line)
24+
if not match:
25+
return None
26+
return match.end() - 1
27+
28+
29+
def _find_log_statement_end(lines, start_line, open_paren_index):
30+
"""Find the line index where the LOG macro call closes, ignoring literals/comments.
31+
32+
This properly handles:
33+
- String literals: LOG("unbalanced (", x);
34+
- Character literals: LOG(')', code);
35+
- Line comments: LOG("msg", x); // comment with )
36+
- Block comments: LOG("msg" /* comment ) */, x);
37+
"""
38+
depth = 0
39+
in_string = False
40+
in_char = False
41+
in_block_comment = False
42+
escape = False
43+
44+
for line_index in range(start_line, len(lines)):
45+
line = lines[line_index]
46+
i = open_paren_index if line_index == start_line else 0
47+
in_line_comment = False
48+
49+
while i < len(line):
50+
ch = line[i]
51+
nxt = line[i + 1] if i + 1 < len(line) else ''
52+
53+
# Line comments consume rest of line
54+
if in_line_comment:
55+
break
56+
57+
# Inside block comment - only look for */
58+
if in_block_comment:
59+
if ch == '*' and nxt == '/':
60+
in_block_comment = False
61+
i += 2
62+
continue
63+
i += 1
64+
continue
65+
66+
# Inside string literal - handle escapes
67+
if in_string:
68+
if escape:
69+
escape = False
70+
elif ch == '\\':
71+
escape = True
72+
elif ch == '"':
73+
in_string = False
74+
i += 1
75+
continue
76+
77+
# Inside character literal - handle escapes
78+
if in_char:
79+
if escape:
80+
escape = False
81+
elif ch == '\\':
82+
escape = True
83+
elif ch == "'":
84+
in_char = False
85+
i += 1
86+
continue
87+
88+
# Check for comment starts
89+
if ch == '/' and nxt == '/':
90+
in_line_comment = True
91+
break
92+
if ch == '/' and nxt == '*':
93+
in_block_comment = True
94+
i += 2
95+
continue
96+
97+
# Check for literal starts
98+
if ch == '"':
99+
in_string = True
100+
escape = False
101+
i += 1
102+
continue
103+
if ch == "'":
104+
in_char = True
105+
escape = False
106+
i += 1
107+
continue
108+
109+
# Count parentheses depth outside of literals/comments
110+
if ch == '(':
111+
depth += 1
112+
elif ch == ')':
113+
depth -= 1
114+
if depth == 0:
115+
return line_index
116+
117+
i += 1
118+
119+
return None
120+
121+
122+
def join_log_statements(content: str) -> str:
123+
"""Join multi-line LOG macro calls onto a single line using proper C++ tokenization."""
124+
lines = content.split('\n')
125+
result = []
126+
i = 0
127+
128+
while i < len(lines):
129+
line = lines[i]
130+
131+
# Check if this line contains a LOG macro start
132+
open_paren_index = _find_log_macro_open(line)
133+
if open_paren_index is not None:
134+
# Find where the LOG statement ends, respecting C++ syntax
135+
end_index = _find_log_statement_end(lines, i, open_paren_index)
136+
if end_index is not None and end_index > i:
137+
# Multi-line LOG statement found - join it
138+
full_statement = lines[i]
139+
for join_index in range(i + 1, end_index + 1):
140+
full_statement += ' ' + lines[join_index].strip()
141+
result.append(full_statement)
142+
i = end_index + 1
143+
continue
144+
145+
# Not a LOG statement or single-line LOG - keep as is
146+
result.append(line)
147+
i += 1
148+
149+
return '\n'.join(result)
150+
151+
152+
def process_file(filepath: Path) -> None:
153+
"""Process a single C++ source file."""
154+
try:
155+
with open(filepath, 'r', encoding='utf-8') as f:
156+
content = f.read()
157+
158+
modified = join_log_statements(content)
159+
160+
with open(filepath, 'w', encoding='utf-8') as f:
161+
f.write(modified)
162+
163+
print(f"[INFO] Processed: {filepath}")
164+
except Exception as e:
165+
print(f"[ERROR] Failed to process {filepath}: {e}", file=sys.stderr)
166+
sys.exit(1)
167+
168+
169+
def main():
170+
"""Process all .cpp and .hpp files in the pybind directory."""
171+
if len(sys.argv) > 1:
172+
# Process specific directory passed as argument
173+
base_dir = Path(sys.argv[1])
174+
else:
175+
# Default to current directory
176+
base_dir = Path.cwd()
177+
178+
if not base_dir.exists():
179+
print(f"[ERROR] Directory not found: {base_dir}", file=sys.stderr)
180+
sys.exit(1)
181+
182+
# Find all C++ source and header files (*.cpp, *.h, *.hpp)
183+
# Note: using '*.h*' pattern to match both .h and .hpp extensions
184+
cpp_files = list(base_dir.rglob('*.cpp')) + list(base_dir.rglob('*.h*'))
185+
186+
if not cpp_files:
187+
print(f"[WARNING] No .cpp or .hpp files found in {base_dir}")
188+
return
189+
190+
print(f"[INFO] Processing {len(cpp_files)} C++ files in {base_dir}")
191+
for filepath in cpp_files:
192+
# Skip cmake-generated files in build directory
193+
if 'build' in filepath.parts:
194+
continue
195+
process_file(filepath)
196+
197+
print(f"[SUCCESS] Joined LOG statements in {len(cpp_files)} files")
198+
199+
200+
if __name__ == '__main__':
201+
main()

0 commit comments

Comments
 (0)