Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
12b910e
Spec for the changes
Oct 23, 2025
392e64f
Initial changes
Oct 28, 2025
3bdc632
Merge remote-tracking branch 'origin/main' into connection_string_all…
Oct 28, 2025
f47b214
Remove special characters
Oct 28, 2025
1cb96d5
Reomve driver name from pipelines
Oct 28, 2025
cba7ff5
Remove markdowns
Oct 29, 2025
feef9bb
Refactor connection string handling by consolidating reserved paramet…
Oct 29, 2025
264f877
Enhance connection string allow-list by updating parameter synonyms a…
Oct 29, 2025
35d55d5
Refactor connection string handling by normalizing parameter names an…
Oct 29, 2025
2d5498b
Refactor connection string handling: rename ConnectionStringAllowList…
Oct 29, 2025
fa126d9
Refactor connection string handling: remove unused has_param method a…
Oct 29, 2025
e6fcea7
Enhance connection string allow-list: add snake_case synonyms and upd…
Oct 30, 2025
08d2adb
Enhance connection string allow-list: update packet size parameter to…
Oct 30, 2025
a4d4ae0
Docs take too long to render in CR
saurabh500 Nov 4, 2025
40140cb
Remove implementation and parser changes summaries for connection str…
saurabh500 Nov 4, 2025
9a20195
Update tests/test_012_connection_string_integration.py
saurabh500 Nov 5, 2025
280e0e3
CHORE: Remove unused imports from connection string modules
saurabh500 Nov 5, 2025
3904340
Merge remote-tracking branch 'origin/main' into saurabh/connection_st…
saurabh500 Nov 5, 2025
3e6fb27
Remove unused
saurabh500 Nov 5, 2025
c28eaf9
REFACTOR: Move connection string allowlist to constants module
saurabh500 Nov 5, 2025
2593057
Merge branch 'main' into saurabh/connection_string_allow_list
saurabh500 Nov 5, 2025
5043f8e
Merge branch 'main' into saurabh/connection_string_allow_list
saurabh500 Nov 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions eng/pipelines/build-whl-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@
python -m pytest -v
displayName: 'Run Pytest to validate bindings'
env:
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'
DB_CONNECTION_STRING: 'Server=tcp:127.0.0.1,1433;Database=master;Uid=SA;Pwd=$(DB_PASSWORD);TrustServerCertificate=yes'

Check notice

Code scanning / devskim

Accessing localhost could indicate debug code, or could hinder scaling. Note

Do not leave debug code in production

# Build wheel package for universal2
- script: |
Expand Down Expand Up @@ -801,7 +801,7 @@

displayName: 'Test wheel installation and basic functionality on $(BASE_IMAGE)'
env:
DB_CONNECTION_STRING: 'Driver=ODBC Driver 18 for SQL Server;Server=localhost;Database=TestDB;Uid=SA;Pwd=$(DB_PASSWORD);TrustServerCertificate=yes'
DB_CONNECTION_STRING: 'Server=localhost;Database=TestDB;Uid=SA;Pwd=$(DB_PASSWORD);TrustServerCertificate=yes'

Check notice

Code scanning / devskim

Accessing localhost could indicate debug code, or could hinder scaling. Note

Do not leave debug code in production

# Run pytest with source code while testing installed wheel
- script: |
Expand Down Expand Up @@ -856,7 +856,7 @@
"
displayName: 'Run pytest suite on $(BASE_IMAGE) $(ARCH)'
env:
DB_CONNECTION_STRING: 'Driver=ODBC Driver 18 for SQL Server;Server=localhost;Database=TestDB;Uid=SA;Pwd=$(DB_PASSWORD);TrustServerCertificate=yes'
DB_CONNECTION_STRING: 'Server=localhost;Database=TestDB;Uid=SA;Pwd=$(DB_PASSWORD);TrustServerCertificate=yes'

Check notice

Code scanning / devskim

Accessing localhost could indicate debug code, or could hinder scaling. Note

Do not leave debug code in production
continueOnError: true # Don't fail pipeline if tests fail

# Cleanup
Expand Down
3 changes: 3 additions & 0 deletions mssql_python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
NotSupportedError,
)

# Connection string parser exceptions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding this new line can you please append in the above list on line 15

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also please add the new files and classes that you have added in this folder

from .exceptions import ConnectionStringParseError

# Type Objects
from .type import (
Date,
Expand Down
77 changes: 52 additions & 25 deletions mssql_python/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,39 +242,66 @@ def _construct_connection_string(
self, connection_str: str = "", **kwargs: Any
) -> str:
"""
Construct the connection string by concatenating the connection string
with key/value pairs from kwargs.

Construct the connection string by parsing, validating, and merging parameters.

This method performs a 6-step process:
1. Parse and validate the base connection_str (validates against allowlist)
2. Normalize parameter names (e.g., addr/address -> Server, uid -> UID)
3. Merge kwargs (which override connection_str params after normalization)
4. Build connection string from normalized, merged params
5. Add Driver and APP parameters (always controlled by the driver)
6. Return the final connection string

Args:
connection_str (str): The base connection string.
**kwargs: Additional key/value pairs for the connection string.

Returns:
str: The constructed connection string.
str: The constructed and validated connection string.
"""
# Add the driver attribute to the connection string
conn_str = add_driver_to_connection_str(connection_str)

# Add additional key-value pairs to the connection string
from mssql_python.connection_string_parser import _ConnectionStringParser, RESERVED_PARAMETERS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add all the import statements at the start of the file

from mssql_python.constants import _ConnectionStringAllowList
from mssql_python.connection_string_builder import _ConnectionStringBuilder

# Step 1: Parse base connection string with allowlist validation
# The parser validates everything: unknown params, reserved params, duplicates, syntax
allowlist = _ConnectionStringAllowList()
parser = _ConnectionStringParser(allowlist=allowlist)
parsed_params = parser._parse(connection_str)

# Step 2: Normalize parameter names (e.g., addr/address -> Server, uid -> UID)
# This handles synonym mapping and deduplication via normalized keys
normalized_params = _ConnectionStringAllowList._normalize_params(parsed_params, warn_rejected=False)

# Step 3: Process kwargs and merge with normalized_params
# kwargs override connection string values (processed after, so they take precedence)
for key, value in kwargs.items():
if key.lower() == "host" or key.lower() == "server":
key = "Server"
elif key.lower() == "user" or key.lower() == "uid":
key = "Uid"
elif key.lower() == "password" or key.lower() == "pwd":
key = "Pwd"
elif key.lower() == "database":
key = "Database"
elif key.lower() == "encrypt":
key = "Encrypt"
elif key.lower() == "trust_server_certificate":
key = "TrustServerCertificate"
normalized_key = _ConnectionStringAllowList.normalize_key(key)
if normalized_key:
# Driver and APP are reserved - raise error if user tries to set them
if normalized_key in RESERVED_PARAMETERS:
raise ValueError(
f"Connection parameter '{key}' is reserved and controlled by the driver. "
f"It cannot be set by the user."
)
# kwargs override any existing values from connection string
normalized_params[normalized_key] = str(value)
else:
continue
conn_str += f"{key}={value};"

log("info", "Final connection string: %s", sanitize_connection_string(conn_str))

log('warning', f"Ignoring unknown connection parameter from kwargs: {key}")

# Step 4: Build connection string with merged params
builder = _ConnectionStringBuilder(normalized_params)

# Step 5: Add Driver and APP parameters (always controlled by the driver)
# These maintain existing behavior: Driver is always hardcoded, APP is always MSSQL-Python
builder.add_param('Driver', 'ODBC Driver 18 for SQL Server')
builder.add_param('APP', 'MSSQL-Python')

# Step 6: Build final string
conn_str = builder.build()

log('info', "Final connection string: %s", sanitize_connection_string(conn_str))

return conn_str

@property
Expand Down
113 changes: 113 additions & 0 deletions mssql_python/connection_string_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
Copyright (c) Microsoft Corporation.
Licensed under the MIT license.
Connection string builder for mssql-python.
Reconstructs ODBC connection strings from parameter dictionaries
with proper escaping and formatting per MS-ODBCSTR specification.
"""

from typing import Dict, Optional


class _ConnectionStringBuilder:
"""
Internal builder for ODBC connection strings. Not part of public API.
Handles proper escaping of special characters and reconstructs
connection strings in ODBC format.
"""

def __init__(self, initial_params: Optional[Dict[str, str]] = None):
"""
Initialize the builder with optional initial parameters.
Args:
initial_params: Dictionary of initial connection parameters
"""
self._params: Dict[str, str] = initial_params.copy() if initial_params else {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we creating a copy instead of directly assigning the value of initial_params?


def add_param(self, key: str, value: str) -> '_ConnectionStringBuilder':
"""
Add or update a connection parameter.
Args:
key: Parameter name (should be normalized canonical name)
value: Parameter value
Returns:
Self for method chaining
"""
self._params[key] = str(value)
return self

def build(self) -> str:
"""
Build the final connection string.
Returns:
ODBC-formatted connection string with proper escaping
Note:
- Driver parameter is placed first
- Other parameters are sorted for consistency
- Values are escaped if they contain special characters
"""
parts = []

# Build in specific order: Driver first, then others
if 'Driver' in self._params:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we find 'driver' or 'DRIVER' or 'dRiver', this if statement will become false.
Can we make all keys to smallercase or uppercase before comparing?

parts.append(f"Driver={self._escape_value(self._params['Driver'])}")

# Add other parameters (sorted for consistency)
for key in sorted(self._params.keys()):
if key == 'Driver':
continue # Already added

value = self._params[key]
escaped_value = self._escape_value(value)
parts.append(f"{key}={escaped_value}")

# Join with semicolons
return ';'.join(parts)

def _escape_value(self, value: str) -> str:
"""
Escape a parameter value if it contains special characters.
Per MS-ODBCSTR specification:
- Values containing ';', '{', '}', '=', or spaces should be braced for safety
- '}' inside braced values is escaped as '}}'
- '{' inside braced values is escaped as '{{'
Args:
value: Parameter value to escape
Returns:
Escaped value (possibly wrapped in braces)
Examples:
>>> builder = _ConnectionStringBuilder()
>>> builder._escape_value("localhost")
'localhost'

Check notice

Code scanning / devskim

Accessing localhost could indicate debug code, or could hinder scaling. Note

Do not leave debug code in production
>>> builder._escape_value("local;host")
'{local;host}'
>>> builder._escape_value("p}w{d")
'{p}}w{{d}'
>>> builder._escape_value("ODBC Driver 18 for SQL Server")
'{ODBC Driver 18 for SQL Server}'
"""
if not value:
return value

# Check if value contains special characters that require bracing
# Include spaces and = for safety, even though technically not always required
needs_braces = any(ch in value for ch in ';{}= ')

if needs_braces:
# Escape existing braces by doubling them
escaped = value.replace('}', '}}').replace('{', '{{')
return f'{{{escaped}}}'
else:
return value
Loading
Loading