Skip to content

Commit 0175169

Browse files
authored
Merge branch 'main' into saumya/api-bug
2 parents 389ab49 + 25131df commit 0175169

File tree

11 files changed

+1225
-204
lines changed

11 files changed

+1225
-204
lines changed

.github/workflows/pr-code-coverage.yml

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

.github/workflows/pr-format-check.yml

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -94,24 +94,35 @@ jobs:
9494
labelToAdd = 'pr-size: large';
9595
}
9696
97-
// Remove existing size labels if any
97+
// Get existing labels
9898
const existingLabels = pr.labels.map(l => l.name);
9999
const sizeLabels = ['pr-size: small', 'pr-size: medium', 'pr-size: large'];
100-
for (const label of existingLabels) {
101-
if (sizeLabels.includes(label)) {
100+
101+
// Find current size label (if any)
102+
const currentSizeLabel = existingLabels.find(label => sizeLabels.includes(label));
103+
104+
// Only make changes if the label needs to be updated
105+
if (currentSizeLabel !== labelToAdd) {
106+
console.log(`Current size label: ${currentSizeLabel || 'none'}`);
107+
console.log(`Required size label: ${labelToAdd} (Total changes: ${totalChanges})`);
108+
109+
// Remove existing size label if different from required
110+
if (currentSizeLabel) {
111+
console.log(`Removing outdated label: ${currentSizeLabel}`);
102112
await github.rest.issues.removeLabel({
103113
...context.repo,
104114
issue_number: pr.number,
105-
name: label,
115+
name: currentSizeLabel,
106116
});
107117
}
108-
}
109118
110-
// Add new size label
111-
await github.rest.issues.addLabels({
112-
...context.repo,
113-
issue_number: pr.number,
114-
labels: [labelToAdd],
115-
});
116-
117-
console.log(`Added label: ${labelToAdd} (Total changes: ${totalChanges})`);
119+
// Add new size label
120+
console.log(`Adding new label: ${labelToAdd}`);
121+
await github.rest.issues.addLabels({
122+
...context.repo,
123+
issue_number: pr.number,
124+
labels: [labelToAdd],
125+
});
126+
} else {
127+
console.log(`Label already correct: ${labelToAdd} (Total changes: ${totalChanges}) - no changes needed`);
128+
}

eng/pipelines/pr-validation-pipeline.yml

Lines changed: 119 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,44 @@ trigger:
77
- main
88

99
jobs:
10+
- job: CodeQLAnalysis
11+
displayName: 'CodeQL Security Analysis'
12+
pool:
13+
vmImage: 'ubuntu-latest'
14+
15+
steps:
16+
- script: |
17+
sudo apt-get update
18+
sudo apt-get install -y build-essential cmake curl git python3 python3-pip python3-dev python3-venv unixodbc-dev
19+
displayName: 'Install build dependencies for CodeQL'
20+
21+
- task: UsePythonVersion@0
22+
inputs:
23+
versionSpec: '3.13'
24+
addToPath: true
25+
displayName: 'Use Python 3.13 for CodeQL'
26+
27+
- script: |
28+
python -m pip install --upgrade pip
29+
pip install -r requirements.txt
30+
displayName: 'Install Python dependencies for CodeQL'
31+
32+
- task: CodeQL3000Init@0
33+
inputs:
34+
Enabled: true
35+
displayName: 'Initialize CodeQL'
36+
37+
# Build the C++ extension for CodeQL analysis
38+
- script: |
39+
cd mssql_python/pybind
40+
chmod +x build.sh
41+
./build.sh
42+
displayName: 'Build C++ extension for CodeQL analysis'
43+
44+
- task: CodeQL3000Finalize@0
45+
condition: always()
46+
displayName: 'Finalize CodeQL'
47+
1048
- job: PytestOnWindows
1149
displayName: 'Windows x64'
1250
pool:
@@ -72,11 +110,11 @@ jobs:
72110
testResultsFiles: '**/test-results.xml'
73111
testRunTitle: 'Publish test results'
74112

75-
- task: PublishCodeCoverageResults@1
76-
inputs:
77-
codeCoverageTool: 'Cobertura'
78-
summaryFileLocation: 'coverage.xml'
79-
displayName: 'Publish code coverage results'
113+
# - task: PublishCodeCoverageResults@1
114+
# inputs:
115+
# codeCoverageTool: 'Cobertura'
116+
# summaryFileLocation: 'coverage.xml'
117+
# displayName: 'Publish code coverage results'
80118

81119
- job: PytestOnMacOS
82120
displayName: 'macOS x86_64'
@@ -1477,3 +1515,79 @@ jobs:
14771515
inputs:
14781516
testResultsFiles: '**/test-results-alpine-arm64.xml'
14791517
testRunTitle: 'Publish pytest results on Alpine ARM64'
1518+
1519+
- job: CodeCoverageReport
1520+
displayName: 'Full Code Coverage Report in Ubuntu x86_64'
1521+
pool:
1522+
vmImage: 'ubuntu-latest'
1523+
1524+
steps:
1525+
- script: |
1526+
# Install build dependencies
1527+
sudo apt-get update
1528+
sudo apt-get install -y cmake gcc g++ lcov unixodbc-dev llvm clang
1529+
displayName: 'Install build dependencies'
1530+
1531+
- script: |
1532+
# Start SQL Server container
1533+
docker pull mcr.microsoft.com/mssql/server:2022-latest
1534+
docker run \
1535+
--name sqlserver \
1536+
-e ACCEPT_EULA=Y \
1537+
-e MSSQL_SA_PASSWORD="$(DB_PASSWORD)" \
1538+
-p 1433:1433 \
1539+
-d mcr.microsoft.com/mssql/server:2022-latest
1540+
1541+
# Wait until SQL Server is ready
1542+
for i in {1..30}; do
1543+
docker exec sqlserver \
1544+
/opt/mssql-tools18/bin/sqlcmd \
1545+
-S localhost \
1546+
-U SA \
1547+
-P "$(DB_PASSWORD)" \
1548+
-C -Q "SELECT 1" && break
1549+
sleep 2
1550+
done
1551+
displayName: 'Start SQL Server container'
1552+
env:
1553+
DB_PASSWORD: $(DB_PASSWORD)
1554+
1555+
- script: |
1556+
# Install Python dependencies
1557+
python -m pip install --upgrade pip
1558+
pip install -r requirements.txt
1559+
pip install coverage-lcov lcov-cobertura
1560+
displayName: 'Install Python dependencies'
1561+
1562+
- script: |
1563+
# Build pybind bindings with coverage instrumentation
1564+
cd mssql_python/pybind
1565+
./build.sh codecov
1566+
displayName: 'Build pybind bindings with coverage'
1567+
1568+
- script: |
1569+
# Generate unified coverage (Python + C++)
1570+
chmod +x ./generate_codecov.sh
1571+
./generate_codecov.sh
1572+
1573+
# Convert unified LCOV to Cobertura XML for ADO reporting
1574+
lcov_cobertura total.info --output unified-coverage/coverage.xml
1575+
displayName: 'Generate unified coverage (Python + C++)'
1576+
env:
1577+
DB_CONNECTION_STRING: 'Driver=ODBC Driver 18 for SQL Server;Server=tcp:127.0.0.1,1433;Database=master;Uid=SA;Pwd=$(DB_PASSWORD);TrustServerCertificate=yes'
1578+
DB_PASSWORD: $(DB_PASSWORD)
1579+
1580+
- task: PublishTestResults@2
1581+
condition: succeededOrFailed()
1582+
inputs:
1583+
testResultsFiles: '**/test-results.xml'
1584+
testRunTitle: 'Publish pytest results with unified coverage'
1585+
1586+
- task: PublishCodeCoverageResults@2
1587+
condition: succeededOrFailed()
1588+
inputs:
1589+
codeCoverageTool: Cobertura
1590+
summaryFileLocation: 'unified-coverage/coverage.xml'
1591+
reportDirectory: 'unified-coverage'
1592+
failIfCoverageEmpty: true
1593+
displayName: 'Publish unified code coverage results'

generate_codecov.sh

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
echo "==================================="
5+
echo "[STEP 1] Installing dependencies"
6+
echo "==================================="
7+
8+
# Update package list
9+
sudo apt-get update
10+
11+
# Install LLVM (for llvm-profdata, llvm-cov)
12+
if ! command -v llvm-profdata &>/dev/null; then
13+
echo "[ACTION] Installing LLVM via apt"
14+
sudo apt-get install -y llvm
15+
fi
16+
17+
# Install lcov (provides lcov + genhtml)
18+
if ! command -v genhtml &>/dev/null; then
19+
echo "[ACTION] Installing lcov via apt"
20+
sudo apt-get install -y lcov
21+
fi
22+
23+
# Install Python plugin for LCOV export
24+
if ! python -m pip show coverage-lcov &>/dev/null; then
25+
echo "[ACTION] Installing coverage-lcov via pip"
26+
python -m pip install coverage-lcov
27+
fi
28+
29+
# Install LCOV → Cobertura converter (for ADO)
30+
if ! python -m pip show lcov-cobertura &>/dev/null; then
31+
echo "[ACTION] Installing lcov-cobertura via pip"
32+
python -m pip install lcov-cobertura
33+
fi
34+
35+
echo "==================================="
36+
echo "[STEP 2] Running pytest with Python coverage"
37+
echo "==================================="
38+
39+
# Cleanup old coverage
40+
rm -f .coverage coverage.xml python-coverage.info cpp-coverage.info total.info
41+
rm -rf htmlcov unified-coverage
42+
43+
# Run pytest with Python coverage (XML + HTML output)
44+
python -m pytest -v \
45+
--junitxml=test-results.xml \
46+
--cov=mssql_python \
47+
--cov-report=xml:coverage.xml \
48+
--cov-report=html \
49+
--capture=tee-sys \
50+
--cache-clear
51+
52+
# Convert Python coverage to LCOV format (restrict to repo only)
53+
echo "[ACTION] Converting Python coverage to LCOV"
54+
coverage lcov -o python-coverage.info --include="mssql_python/*"
55+
56+
echo "==================================="
57+
echo "[STEP 3] Processing C++ coverage (Clang/LLVM)"
58+
echo "==================================="
59+
60+
# Merge raw profile data from pybind runs
61+
if [ ! -f default.profraw ]; then
62+
echo "[ERROR] default.profraw not found. Did you build with -fprofile-instr-generate?"
63+
exit 1
64+
fi
65+
66+
llvm-profdata merge -sparse default.profraw -o default.profdata
67+
68+
# Find the pybind .so file (Linux build)
69+
PYBIND_SO=$(find mssql_python -name "*.so" | head -n 1)
70+
if [ -z "$PYBIND_SO" ]; then
71+
echo "[ERROR] Could not find pybind .so"
72+
exit 1
73+
fi
74+
75+
echo "[INFO] Using pybind module: $PYBIND_SO"
76+
77+
# Export C++ coverage, excluding Python headers, pybind11, and system includes
78+
llvm-cov export "$PYBIND_SO" \
79+
-instr-profile=default.profdata \
80+
-ignore-filename-regex='(python3\.[0-9]+|cpython|pybind11|/usr/include/|/usr/lib/)' \
81+
--skip-functions \
82+
-format=lcov > cpp-coverage.info
83+
84+
echo "==================================="
85+
echo "[STEP 4] Merging Python + C++ coverage"
86+
echo "==================================="
87+
88+
# Merge LCOV reports (ignore inconsistencies in Python LCOV export)
89+
lcov -a python-coverage.info -a cpp-coverage.info -o total.info \
90+
--ignore-errors inconsistent,corrupt
91+
92+
# Normalize paths so everything starts from mssql_python/
93+
echo "[ACTION] Normalizing paths in LCOV report"
94+
sed -i "s|$(pwd)/||g" total.info
95+
96+
# Generate full HTML report
97+
genhtml total.info \
98+
--output-directory unified-coverage \
99+
--quiet \
100+
--title "Unified Coverage Report"
101+
102+
# Generate Cobertura XML (for Azure DevOps Code Coverage tab)
103+
lcov_cobertura total.info --output coverage.xml

mssql_python/__init__.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def getDecimalSeparator():
140140
from .logging_config import setup_logging, get_logger
141141

142142
# Constants
143-
from .constants import ConstantsDDBC
143+
from .constants import ConstantsDDBC, GetInfoConstants
144144

145145
# Export specific constants for setencoding()
146146
SQL_CHAR = ConstantsDDBC.SQL_CHAR.value
@@ -205,3 +205,55 @@ def _custom_setattr(name, value):
205205
SQL_DATE = ConstantsDDBC.SQL_DATE.value
206206
SQL_TIME = ConstantsDDBC.SQL_TIME.value
207207
SQL_TIMESTAMP = ConstantsDDBC.SQL_TIMESTAMP.value
208+
209+
# Export GetInfo constants at module level
210+
# Driver and database information
211+
SQL_DRIVER_NAME = GetInfoConstants.SQL_DRIVER_NAME.value
212+
SQL_DRIVER_VER = GetInfoConstants.SQL_DRIVER_VER.value
213+
SQL_DRIVER_ODBC_VER = GetInfoConstants.SQL_DRIVER_ODBC_VER.value
214+
SQL_DATA_SOURCE_NAME = GetInfoConstants.SQL_DATA_SOURCE_NAME.value
215+
SQL_DATABASE_NAME = GetInfoConstants.SQL_DATABASE_NAME.value
216+
SQL_SERVER_NAME = GetInfoConstants.SQL_SERVER_NAME.value
217+
SQL_USER_NAME = GetInfoConstants.SQL_USER_NAME.value
218+
219+
# SQL conformance and support
220+
SQL_SQL_CONFORMANCE = GetInfoConstants.SQL_SQL_CONFORMANCE.value
221+
SQL_KEYWORDS = GetInfoConstants.SQL_KEYWORDS.value
222+
SQL_IDENTIFIER_QUOTE_CHAR = GetInfoConstants.SQL_IDENTIFIER_QUOTE_CHAR.value
223+
SQL_SEARCH_PATTERN_ESCAPE = GetInfoConstants.SQL_SEARCH_PATTERN_ESCAPE.value
224+
225+
# Catalog and schema support
226+
SQL_CATALOG_TERM = GetInfoConstants.SQL_CATALOG_TERM.value
227+
SQL_SCHEMA_TERM = GetInfoConstants.SQL_SCHEMA_TERM.value
228+
SQL_TABLE_TERM = GetInfoConstants.SQL_TABLE_TERM.value
229+
SQL_PROCEDURE_TERM = GetInfoConstants.SQL_PROCEDURE_TERM.value
230+
231+
# Transaction support
232+
SQL_TXN_CAPABLE = GetInfoConstants.SQL_TXN_CAPABLE.value
233+
SQL_DEFAULT_TXN_ISOLATION = GetInfoConstants.SQL_DEFAULT_TXN_ISOLATION.value
234+
235+
# Data type support
236+
SQL_NUMERIC_FUNCTIONS = GetInfoConstants.SQL_NUMERIC_FUNCTIONS.value
237+
SQL_STRING_FUNCTIONS = GetInfoConstants.SQL_STRING_FUNCTIONS.value
238+
SQL_DATETIME_FUNCTIONS = GetInfoConstants.SQL_DATETIME_FUNCTIONS.value
239+
240+
# Limits
241+
SQL_MAX_COLUMN_NAME_LEN = GetInfoConstants.SQL_MAX_COLUMN_NAME_LEN.value
242+
SQL_MAX_TABLE_NAME_LEN = GetInfoConstants.SQL_MAX_TABLE_NAME_LEN.value
243+
SQL_MAX_SCHEMA_NAME_LEN = GetInfoConstants.SQL_MAX_SCHEMA_NAME_LEN.value
244+
SQL_MAX_CATALOG_NAME_LEN = GetInfoConstants.SQL_MAX_CATALOG_NAME_LEN.value
245+
SQL_MAX_IDENTIFIER_LEN = GetInfoConstants.SQL_MAX_IDENTIFIER_LEN.value
246+
247+
# Also provide a function to get all constants
248+
def get_info_constants():
249+
"""
250+
Returns a dictionary of all available GetInfo constants.
251+
252+
This provides all SQLGetInfo constants that can be used with the Connection.getinfo() method
253+
to retrieve metadata about the database server and driver.
254+
255+
Returns:
256+
dict: Dictionary mapping constant names to their integer values
257+
"""
258+
return {name: member.value for name, member in GetInfoConstants.__members__.items()}
259+

0 commit comments

Comments
 (0)