Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 53
FEAT: BCP implementation in mssql-python driver using rust#402
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
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
bcad33c
base code
subrata-ms 284dd2f
additional test
subrata-ms 387d1e9
linting
subrata-ms 72f6953
test configuration
subrata-ms 1d16f97
Refactor the code for simplicity
subrata-ms f8f6737
romoving the test
subrata-ms f7a6606
Merge branch 'main' into subrata-ms/BCPIntegration
subrata-ms 4ca4969
review comments
subrata-ms 2d9faf7
Copilot review comment
subrata-ms 62f4795
review comment
subrata-ms e60cf2d
linting fix
subrata-ms 1501ff8
review comments
subrata-ms ebd4386
Merge branch 'main' into subrata-ms/BCPIntegration
subrata-ms cf42d22
linting fix in main.py
subrata-ms ffad1d6
review comment
subrata-ms b0c536f
linting fix
subrata-ms ce1e64d
removing duplicates to fix linting issue
subrata-ms f698b97
linting issues
subrata-ms 099f09f
Merge branch 'main' into subrata-ms/BCPIntegration
subrata-ms 854d983
review comment
subrata-ms 6fad210
review comment
subrata-ms File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -15,4 +15,4 @@ | ||
| print(f"Database ID: {row[0]}, Name: {row[1]}") | ||
| cursor.close() | ||
| conn.close() | ||
| conn.close() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -15,7 +15,7 @@ | ||
| import uuid | ||
| import datetime | ||
| import warnings | ||
| from typing import List, Union, Any, Optional, Tuple, Sequence, TYPE_CHECKING | ||
| from typing import List, Union, Any, Optional, Tuple, Sequence, TYPE_CHECKING, Iterable | ||
| from mssql_python.constants import ConstantsDDBC as ddbc_sql_const, SQLTypes | ||
| from mssql_python.helpers import check_error | ||
| from mssql_python.logging import logger | ||
| @@ -2451,6 +2451,190 @@ def nextset(self) -> Union[bool, None]: | ||
| ) | ||
| return True | ||
| def _bulkcopy( | ||
| self, table_name: str, data: Iterable[Union[Tuple, List]], **kwargs | ||
| ): # pragma: no cover | ||
| """ | ||
| Perform bulk copy operation for high-performance data loading. | ||
| Args: | ||
| table_name: Target table name (can include schema, e.g., 'dbo.MyTable'). | ||
| The table must exist and the user must have INSERT permissions. | ||
| data: Iterable of tuples or lists containing row data to be inserted. | ||
| Data Format Requirements: | ||
| - Each element in the iterable represents one row | ||
| - Each row should be a tuple or list of column values | ||
| - Column order must match the target table's column order (by ordinal | ||
| position), unless column_mappings is specified | ||
| - The number of values in each row must match the number of columns | ||
| in the target table | ||
| **kwargs: Additional bulk copy options. | ||
| column_mappings (List[Tuple[int, str]], optional): | ||
| Maps source data column indices to target table column names. | ||
| Each tuple is (source_index, target_column_name) where: | ||
| - source_index: 0-based index of the column in the source data | ||
| - target_column_name: Name of the target column in the database table | ||
| When omitted: Columns are mapped by ordinal position (first data | ||
| column → first table column, second → second, etc.) | ||
| When specified: Only the mapped columns are inserted; unmapped | ||
| source columns are ignored, and unmapped target columns must | ||
| have default values or allow NULL. | ||
| Returns: | ||
| Dictionary with bulk copy results including: | ||
| - rows_copied: Number of rows successfully copied | ||
| - batch_count: Number of batches processed | ||
| - elapsed_time: Time taken for the operation | ||
| Raises: | ||
| ImportError: If mssql_py_core library is not installed | ||
| TypeError: If data is None, not iterable, or is a string/bytes | ||
| ValueError: If table_name is empty or parameters are invalid | ||
| RuntimeError: If connection string is not available | ||
| """ | ||
| try: | ||
| import mssql_py_core | ||
| except ImportError as exc: | ||
| raise ImportError( | ||
| "Bulk copy requires the mssql_py_core library which is not installed. " | ||
| "To install, run: pip install mssql_py_core " | ||
| ) from exc | ||
| # Validate inputs | ||
| if not table_name or not isinstance(table_name, str): | ||
| raise ValueError("table_name must be a non-empty string") | ||
| # Validate that data is iterable (but not a string or bytes, which are technically iterable) | ||
| if data is None: | ||
| raise TypeError("data must be an iterable of tuples or lists, got None") | ||
| if isinstance(data, (str, bytes)): | ||
| raise TypeError( | ||
| f"data must be an iterable of tuples or lists, got {type(data).__name__}. " | ||
| "Strings and bytes are not valid row collections." | ||
| ) | ||
| if not hasattr(data, "__iter__"): | ||
| raise TypeError( | ||
| f"data must be an iterable of tuples or lists, got non-iterable {type(data).__name__}" | ||
| ) | ||
| # Extract and validate kwargs with defaults | ||
| batch_size = kwargs.get("batch_size", None) | ||
| timeout = kwargs.get("timeout", 30) | ||
| # Validate batch_size type and value (only if explicitly provided) | ||
| if batch_size is not None: | ||
| if not isinstance(batch_size, (int, float)): | ||
| raise TypeError( | ||
| f"batch_size must be a positive integer, got {type(batch_size).__name__}" | ||
| ) | ||
| if batch_size <= 0: | ||
| raise ValueError(f"batch_size must be positive, got {batch_size}") | ||
| # Validate timeout type and value | ||
| if not isinstance(timeout, (int, float)): | ||
| raise TypeError(f"timeout must be a positive number, got {type(timeout).__name__}") | ||
| if timeout <= 0: | ||
| raise ValueError(f"timeout must be positive, got {timeout}") | ||
| # Get and parse connection string | ||
| if not hasattr(self.connection, "connection_str"): | ||
| raise RuntimeError("Connection string not available for bulk copy") | ||
| # Use the proper connection string parser that handles braced values | ||
| from mssql_python.connection_string_parser import _ConnectionStringParser | ||
| parser = _ConnectionStringParser(validate_keywords=False) | ||
| params = parser._parse(self.connection.connection_str) | ||
| if not params.get("server"): | ||
| raise ValueError("SERVER parameter is required in connection string") | ||
| if not params.get("database"): | ||
| raise ValueError( | ||
| "DATABASE parameter is required in connection string for bulk copy. " | ||
| "Specify the target database explicitly to avoid accidentally writing to system databases." | ||
| ) | ||
| # Build connection context for bulk copy library | ||
| # Note: Password is extracted separately to avoid storing it in the main context | ||
| # dict that could be accidentally logged or exposed in error messages. | ||
| trust_cert = params.get("trustservercertificate", "yes").lower() in ("yes", "true") | ||
| # Parse encryption setting from connection string | ||
| encrypt_param = params.get("encrypt") | ||
| if encrypt_param is not None: | ||
| encrypt_value = encrypt_param.strip().lower() | ||
| if encrypt_value in ("yes", "true", "mandatory", "required"): | ||
| encryption = "Required" | ||
| elif encrypt_value in ("no", "false", "optional"): | ||
| encryption = "Optional" | ||
| else: | ||
| # Pass through unrecognized values (e.g., "Strict") to the underlying driver | ||
| encryption = encrypt_param | ||
| else: | ||
| encryption = "Optional" | ||
| context = { | ||
| "server": params.get("server"), | ||
| "database": params.get("database"), | ||
| "user_name": params.get("uid", ""), | ||
| "trust_server_certificate": trust_cert, | ||
| "encryption": encryption, | ||
| } | ||
| # Extract password separately to avoid storing it in generic context that may be logged | ||
| password = params.get("pwd", "") | ||
| pycore_context = dict(context) | ||
| pycore_context["password"] = password | ||
| pycore_connection = None | ||
| pycore_cursor = None | ||
| try: | ||
| pycore_connection = mssql_py_core.PyCoreConnection(pycore_context) | ||
| pycore_cursor = pycore_connection.cursor() | ||
| result = pycore_cursor.bulkcopy(table_name, iter(data), **kwargs) | ||
| return result | ||
| except Exception as e: | ||
| # Log the error for debugging (without exposing credentials) | ||
| logger.debug( | ||
| "Bulk copy operation failed for table '%s': %s: %s", | ||
| table_name, | ||
| type(e).__name__, | ||
| str(e), | ||
| ) | ||
| # Re-raise without exposing connection context in the error chain | ||
| # to prevent credential leakage in stack traces | ||
| raise type(e)(str(e)) from None | ||
| finally: | ||
| # Clear sensitive data to minimize memory exposure | ||
| password = "" | ||
| if pycore_context: | ||
| pycore_context["password"] = "" | ||
| pycore_context["user_name"] = "" | ||
| # Clean up bulk copy resources | ||
| for resource in (pycore_cursor, pycore_connection): | ||
| if resource and hasattr(resource, "close"): | ||
| try: | ||
| resource.close() | ||
| except Exception as cleanup_error: | ||
| # Log cleanup errors at debug level to aid troubleshooting | ||
| # without masking the original exception | ||
| logger.debug( | ||
subrata-ms marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| "Failed to close bulk copy resource %s: %s", | ||
| type(resource).__name__, | ||
| cleanup_error, | ||
| ) | ||
| def __enter__(self): | ||
| """ | ||
| Enter the runtime context for the cursor. | ||
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.