-
Notifications
You must be signed in to change notification settings - Fork 27
FEAT: Improved Connection String handling in Python #307
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?
Changes from all commits
12b910e
392e64f
3bdc632
f47b214
1cb96d5
cba7ff5
feef9bb
264f877
35d55d5
2d5498b
fa126d9
e6fcea7
08d2adb
a4d4ae0
40140cb
9a20195
280e0e3
3904340
3e6fb27
c28eaf9
2593057
5043f8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
| 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 {} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| 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 noticeCode 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 | ||
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.
Instead of adding this new line can you please append in the above list on line 15
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.
Also please add the new files and classes that you have added in this folder