From bcad33cd08bd4cd23425c3cec08ad053e110951e Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Wed, 14 Jan 2026 17:55:26 +0000 Subject: [PATCH 01/16] base code --- TestsBCP/test_bigint_BCP.py | 114 +++++++++++++++ TestsBCP/test_binary_BCP.py | 142 ++++++++++++++++++ TestsBCP/test_bit_BCP.py | 156 ++++++++++++++++++++ TestsBCP/test_cursor_bulkcopy.py | 96 +++++++++++++ mssql_python/BCPRustWrapper.py | 240 +++++++++++++++++++++++++++++++ mssql_python/cursor.py | 67 +++++++++ 6 files changed, 815 insertions(+) create mode 100644 TestsBCP/test_bigint_BCP.py create mode 100644 TestsBCP/test_binary_BCP.py create mode 100644 TestsBCP/test_bit_BCP.py create mode 100644 TestsBCP/test_cursor_bulkcopy.py create mode 100644 mssql_python/BCPRustWrapper.py diff --git a/TestsBCP/test_bigint_BCP.py b/TestsBCP/test_bigint_BCP.py new file mode 100644 index 00000000..9d51b0e2 --- /dev/null +++ b/TestsBCP/test_bigint_BCP.py @@ -0,0 +1,114 @@ +import sys +import os +import pytest + +# Add parent directory to path to import mssql_python +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from mssql_python import connect + + +def test_bigint_bulkcopy(): + """Test bulk copy functionality with BIGINT data type""" + # Get connection string from environment + conn_str = os.getenv("DB_CONNECTION_STRING") + assert conn_str is not None, "DB_CONNECTION_STRING environment variable must be set" + + print(f"Connection string length: {len(conn_str)}") + + # Connect using the regular mssql_python connection + conn = connect(conn_str) + print(f"Connection created: {type(conn)}") + + # Create cursor + cursor = conn.cursor() + print(f"Cursor created: {type(cursor)}") + + # Create a test table with BIGINT columns + table_name = "BulkCopyBigIntTest" + + print(f"\nCreating test table: {table_name}") + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") + cursor.execute(f"CREATE TABLE {table_name} (id INT, bigint_value BIGINT, description VARCHAR(100))") + conn.commit() + print("Test table created successfully") + + # Prepare test data with various BIGINT values + # BIGINT range: -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 + test_data = [ + (1, 0, "Zero"), + (2, 1, "Positive one"), + (3, -1, "Negative one"), + (4, 9223372036854775807, "Max BIGINT value"), + (5, -9223372036854775808, "Min BIGINT value"), + (6, 1000000000000, "One trillion"), + (7, -1000000000000, "Negative one trillion"), + (8, 9223372036854775806, "Near max value"), + (9, -9223372036854775807, "Near min value"), + (10, 123456789012345, "Random large value"), + ] + + print(f"\nPerforming bulk copy with {len(test_data)} rows using cursor.bulkcopy()...") + print("Testing BIGINT data type with edge cases...") + + # Perform bulk copy via cursor + result = cursor.bulkcopy( + table_name=table_name, + data=test_data, + batch_size=5, + timeout=30, + column_mappings=[ + (0, "id"), + (1, "bigint_value"), + (2, "description"), + ] + ) + + print(f"\nBulk copy completed successfully!") + print(f" Rows copied: {result['rows_copied']}") + print(f" Batch count: {result['batch_count']}") + print(f" Elapsed time: {result['elapsed_time']}") + + # Assertions + assert result['rows_copied'] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" + assert result['batch_count'] == 2, f"Expected 2 batches, got {result['batch_count']}" + + # Verify the data + print(f"\nVerifying inserted data...") + cursor.execute(f"SELECT id, bigint_value, description FROM {table_name} ORDER BY id") + rows = cursor.fetchall() + + print(f"Retrieved {len(rows)} rows:") + assert len(rows) == 10, f"Expected 10 rows retrieved, got {len(rows)}" + + for i, row in enumerate(rows): + print(f" ID: {row[0]}, BIGINT Value: {row[1]}, Description: {row[2]}") + assert row[0] == test_data[i][0], f"ID mismatch at row {i}" + assert row[1] == test_data[i][1], f"BIGINT value mismatch at row {i}: expected {test_data[i][1]}, got {row[1]}" + assert row[2] == test_data[i][2], f"Description mismatch at row {i}" + + # Additional verification for edge cases + print("\nVerifying edge case values...") + cursor.execute(f"SELECT bigint_value FROM {table_name} WHERE id = 4") + max_value = cursor.fetchone()[0] + assert max_value == 9223372036854775807, f"Max BIGINT verification failed" + print(f" ✓ Max BIGINT value verified: {max_value}") + + cursor.execute(f"SELECT bigint_value FROM {table_name} WHERE id = 5") + min_value = cursor.fetchone()[0] + assert min_value == -9223372036854775808, f"Min BIGINT verification failed" + print(f" ✓ Min BIGINT value verified: {min_value}") + + # Cleanup + print(f"\nCleaning up test table...") + cursor.execute(f"DROP TABLE {table_name}") + conn.commit() + + # Close cursor and connection + cursor.close() + conn.close() + print("\nTest completed successfully!") + + +if __name__ == "__main__": + test_bigint_bulkcopy() diff --git a/TestsBCP/test_binary_BCP.py b/TestsBCP/test_binary_BCP.py new file mode 100644 index 00000000..1cf6c1a4 --- /dev/null +++ b/TestsBCP/test_binary_BCP.py @@ -0,0 +1,142 @@ +import sys +import os +import pytest + +# Add parent directory to path to import mssql_python +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from mssql_python import connect + + +def test_binary_varbinary_bulkcopy(): + """Test bulk copy functionality with BINARY and VARBINARY data types""" + # Get connection string from environment + conn_str = os.getenv("DB_CONNECTION_STRING") + assert conn_str is not None, "DB_CONNECTION_STRING environment variable must be set" + + print(f"Connection string length: {len(conn_str)}") + + # Connect using the regular mssql_python connection + conn = connect(conn_str) + print(f"Connection created: {type(conn)}") + + # Create cursor + cursor = conn.cursor() + print(f"Cursor created: {type(cursor)}") + + # Create a test table with BINARY and VARBINARY columns + table_name = "BulkCopyBinaryTest" + + print(f"\nCreating test table: {table_name}") + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") + cursor.execute(f""" + CREATE TABLE {table_name} ( + id INT, + binary_data BINARY(16), + varbinary_data VARBINARY(100), + description VARCHAR(100) + ) + """) + conn.commit() + print("Test table created successfully") + + # Prepare test data with various BINARY/VARBINARY values + test_data = [ + (1, b'\x00' * 16, b'', "Empty varbinary"), + (2, b'\x01\x02\x03\x04' + b'\x00' * 12, b'\x01\x02\x03\x04', "Small binary data"), + (3, b'\xFF' * 16, b'\xFF' * 16, "All 0xFF bytes"), + (4, b'\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xAA\xBB\xCC\xDD\xEE\xFF', + b'\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xAA\xBB\xCC\xDD\xEE\xFF', "Hex sequence"), + (5, b'Hello World!!!!!' [:16], b'Hello World!', "ASCII text as binary"), + (6, bytes(range(16)), bytes(range(50)), "Sequential bytes"), + (7, b'\x00' * 16, b'\x00' * 100, "Max varbinary length"), + (8, b'\xDE\xAD\xBE\xEF' * 4, b'\xDE\xAD\xBE\xEF' * 5, "Repeated pattern"), + (9, b'\x01' * 16, b'\x01', "Single byte varbinary"), + (10, b'\x80' * 16, b'\x80\x90\xA0\xB0\xC0\xD0\xE0\xF0', "High-bit bytes"), + ] + + print(f"\nPerforming bulk copy with {len(test_data)} rows using cursor.bulkcopy()...") + print("Testing BINARY and VARBINARY data types with edge cases...") + + # Perform bulk copy via cursor + result = cursor.bulkcopy( + table_name=table_name, + data=test_data, + batch_size=5, + timeout=30, + column_mappings=[ + (0, "id"), + (1, "binary_data"), + (2, "varbinary_data"), + (3, "description"), + ] + ) + + print(f"\nBulk copy completed successfully!") + print(f" Rows copied: {result['rows_copied']}") + print(f" Batch count: {result['batch_count']}") + print(f" Elapsed time: {result['elapsed_time']}") + + # Assertions + assert result['rows_copied'] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" + assert result['batch_count'] == 2, f"Expected 2 batches, got {result['batch_count']}" + + # Verify the data + print(f"\nVerifying inserted data...") + cursor.execute(f"SELECT id, binary_data, varbinary_data, description FROM {table_name} ORDER BY id") + rows = cursor.fetchall() + + print(f"Retrieved {len(rows)} rows:") + assert len(rows) == 10, f"Expected 10 rows retrieved, got {len(rows)}" + + for i, row in enumerate(rows): + print(f" ID: {row[0]}, BINARY: {row[1].hex() if row[1] else 'NULL'}, " + + f"VARBINARY: {row[2].hex() if row[2] else 'NULL'}, Description: {row[3]}") + + assert row[0] == test_data[i][0], f"ID mismatch at row {i}" + + # BINARY comparison - SQL Server pads with zeros to fixed length + expected_binary = test_data[i][1] if len(test_data[i][1]) == 16 else test_data[i][1] + b'\x00' * (16 - len(test_data[i][1])) + assert row[1] == expected_binary, f"BINARY mismatch at row {i}: expected {expected_binary.hex()}, got {row[1].hex()}" + + # VARBINARY comparison - exact match expected + assert row[2] == test_data[i][2], f"VARBINARY mismatch at row {i}: expected {test_data[i][2].hex()}, got {row[2].hex()}" + + assert row[3] == test_data[i][3], f"Description mismatch at row {i}" + + # Additional verification for specific cases + print("\nVerifying specific edge cases...") + + # Empty varbinary + cursor.execute(f"SELECT varbinary_data FROM {table_name} WHERE id = 1") + empty_varbinary = cursor.fetchone()[0] + assert empty_varbinary == b'', f"Empty varbinary verification failed" + print(f" ✓ Empty varbinary verified: length = {len(empty_varbinary)}") + + # Max varbinary length + cursor.execute(f"SELECT varbinary_data FROM {table_name} WHERE id = 7") + max_varbinary = cursor.fetchone()[0] + assert len(max_varbinary) == 100, f"Max varbinary length verification failed" + assert max_varbinary == b'\x00' * 100, f"Max varbinary content verification failed" + print(f" ✓ Max varbinary length verified: {len(max_varbinary)} bytes") + + # All 0xFF bytes + cursor.execute(f"SELECT binary_data, varbinary_data FROM {table_name} WHERE id = 3") + all_ff_row = cursor.fetchone() + assert all_ff_row[0] == b'\xFF' * 16, f"All 0xFF BINARY verification failed" + assert all_ff_row[1] == b'\xFF' * 16, f"All 0xFF VARBINARY verification failed" + print(f" ✓ All 0xFF bytes verified for both BINARY and VARBINARY") + + # Cleanup + print(f"\nCleaning up test table...") + cursor.execute(f"DROP TABLE {table_name}") + conn.commit() + + # Close cursor and connection + cursor.close() + conn.close() + print("\nTest completed successfully!") + + +if __name__ == "__main__": + test_binary_varbinary_bulkcopy() diff --git a/TestsBCP/test_bit_BCP.py b/TestsBCP/test_bit_BCP.py new file mode 100644 index 00000000..f3f3755f --- /dev/null +++ b/TestsBCP/test_bit_BCP.py @@ -0,0 +1,156 @@ +import sys +import os +import pytest + +# Add parent directory to path to import mssql_python +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from mssql_python import connect + + +def test_bit_bulkcopy(): + """Test bulk copy functionality with BIT data type""" + # Get connection string from environment + conn_str = os.getenv("DB_CONNECTION_STRING") + assert conn_str is not None, "DB_CONNECTION_STRING environment variable must be set" + + print(f"Connection string length: {len(conn_str)}") + + # Connect using the regular mssql_python connection + conn = connect(conn_str) + print(f"Connection created: {type(conn)}") + + # Create cursor + cursor = conn.cursor() + print(f"Cursor created: {type(cursor)}") + + # Create a test table with BIT columns + table_name = "BulkCopyBitTest" + + print(f"\nCreating test table: {table_name}") + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") + cursor.execute(f""" + CREATE TABLE {table_name} ( + id INT, + bit_value BIT, + is_active BIT, + is_deleted BIT, + description VARCHAR(100) + ) + """) + conn.commit() + print("Test table created successfully") + + # Prepare test data with various BIT values + # BIT can be 0, 1, True, False, or NULL + test_data = [ + (1, 0, 0, 0, "All zeros (False)"), + (2, 1, 1, 1, "All ones (True)"), + (3, True, True, True, "All True"), + (4, False, False, False, "All False"), + (5, 1, 0, 1, "Mixed 1-0-1"), + (6, 0, 1, 0, "Mixed 0-1-0"), + (7, True, False, True, "Mixed True-False-True"), + (8, False, True, False, "Mixed False-True-False"), + (9, 1, True, 0, "Mixed 1-True-0"), + (10, False, 1, True, "Mixed False-1-True"), + ] + + print(f"\nPerforming bulk copy with {len(test_data)} rows using cursor.bulkcopy()...") + print("Testing BIT data type with True/False and 0/1 values...") + + # Perform bulk copy via cursor + result = cursor.bulkcopy( + table_name=table_name, + data=test_data, + batch_size=5, + timeout=30, + column_mappings=[ + (0, "id"), + (1, "bit_value"), + (2, "is_active"), + (3, "is_deleted"), + (4, "description"), + ] + ) + + print(f"\nBulk copy completed successfully!") + print(f" Rows copied: {result['rows_copied']}") + print(f" Batch count: {result['batch_count']}") + print(f" Elapsed time: {result['elapsed_time']}") + + # Assertions + assert result['rows_copied'] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" + assert result['batch_count'] == 2, f"Expected 2 batches, got {result['batch_count']}" + + # Verify the data + print(f"\nVerifying inserted data...") + cursor.execute(f"SELECT id, bit_value, is_active, is_deleted, description FROM {table_name} ORDER BY id") + rows = cursor.fetchall() + + print(f"Retrieved {len(rows)} rows:") + assert len(rows) == 10, f"Expected 10 rows retrieved, got {len(rows)}" + + # Expected values after conversion (SQL Server stores BIT as 0 or 1) + expected_values = [ + (1, False, False, False, "All zeros (False)"), + (2, True, True, True, "All ones (True)"), + (3, True, True, True, "All True"), + (4, False, False, False, "All False"), + (5, True, False, True, "Mixed 1-0-1"), + (6, False, True, False, "Mixed 0-1-0"), + (7, True, False, True, "Mixed True-False-True"), + (8, False, True, False, "Mixed False-True-False"), + (9, True, True, False, "Mixed 1-True-0"), + (10, False, True, True, "Mixed False-1-True"), + ] + + for i, row in enumerate(rows): + print(f" ID: {row[0]}, BIT: {row[1]}, IS_ACTIVE: {row[2]}, IS_DELETED: {row[3]}, Description: {row[4]}") + + assert row[0] == expected_values[i][0], f"ID mismatch at row {i}" + assert row[1] == expected_values[i][1], f"BIT value mismatch at row {i}: expected {expected_values[i][1]}, got {row[1]}" + assert row[2] == expected_values[i][2], f"IS_ACTIVE mismatch at row {i}: expected {expected_values[i][2]}, got {row[2]}" + assert row[3] == expected_values[i][3], f"IS_DELETED mismatch at row {i}: expected {expected_values[i][3]}, got {row[3]}" + assert row[4] == expected_values[i][4], f"Description mismatch at row {i}" + + # Additional verification for specific cases + print("\nVerifying specific edge cases...") + + # All False values + cursor.execute(f"SELECT bit_value, is_active, is_deleted FROM {table_name} WHERE id = 1") + all_false = cursor.fetchone() + assert all_false[0] == False and all_false[1] == False and all_false[2] == False, f"All False verification failed" + print(f" ✓ All False values verified: {all_false}") + + # All True values + cursor.execute(f"SELECT bit_value, is_active, is_deleted FROM {table_name} WHERE id = 2") + all_true = cursor.fetchone() + assert all_true[0] == True and all_true[1] == True and all_true[2] == True, f"All True verification failed" + print(f" ✓ All True values verified: {all_true}") + + # Count True values + cursor.execute(f"SELECT COUNT(*) FROM {table_name} WHERE bit_value = 1") + true_count = cursor.fetchone()[0] + print(f" ✓ Count of True bit_value: {true_count}") + + # Count False values + cursor.execute(f"SELECT COUNT(*) FROM {table_name} WHERE bit_value = 0") + false_count = cursor.fetchone()[0] + print(f" ✓ Count of False bit_value: {false_count}") + + assert true_count + false_count == 10, f"Total count mismatch" + + # Cleanup + print(f"\nCleaning up test table...") + cursor.execute(f"DROP TABLE {table_name}") + conn.commit() + + # Close cursor and connection + cursor.close() + conn.close() + print("\nTest completed successfully!") + + +if __name__ == "__main__": + test_bit_bulkcopy() diff --git a/TestsBCP/test_cursor_bulkcopy.py b/TestsBCP/test_cursor_bulkcopy.py new file mode 100644 index 00000000..5ef74a26 --- /dev/null +++ b/TestsBCP/test_cursor_bulkcopy.py @@ -0,0 +1,96 @@ +import sys +import os +import pytest + +# Add parent directory to path to import mssql_python +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from mssql_python import connect + + +def test_cursor_bulkcopy(): + """Test bulk copy functionality through cursor.bulkcopy() method""" + # Get connection string from environment + conn_str = os.getenv("DB_CONNECTION_STRING") + assert conn_str is not None, "DB_CONNECTION_STRING environment variable must be set" + + print(f"Connection string length: {len(conn_str)}") + + # Connect using the regular mssql_python connection + conn = connect(conn_str) + print(f"Connection created: {type(conn)}") + + # Create cursor + cursor = conn.cursor() + print(f"Cursor created: {type(cursor)}") + + # Create a test table + table_name = "BulkCopyCursorTest" + + print(f"\nCreating test table: {table_name}") + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") + cursor.execute(f"CREATE TABLE {table_name} (id INT, name VARCHAR(50), amount DECIMAL(10,2))") + conn.commit() + print("Test table created successfully") + + # Prepare test data + test_data = [ + (1, "Product A", 99.99), + (2, "Product B", 149.50), + (3, "Product C", 199.99), + (4, "Product D", 249.00), + (5, "Product E", 299.99), + (6, "Product F", 349.50), + (7, "Product G", 399.99), + (8, "Product H", 449.00), + (9, "Product I", 499.99), + (10, "Product J", 549.50), + ] + + print(f"\nPerforming bulk copy with {len(test_data)} rows using cursor.bulkcopy()...") + + # Perform bulk copy via cursor + result = cursor.bulkcopy( + table_name=table_name, + data=test_data, + batch_size=5, + timeout=30, + column_mappings=[ + (0, "id"), + (1, "name"), + (2, "amount"), + ] + ) + + print(f"\nBulk copy completed successfully!") + print(f" Rows copied: {result['rows_copied']}") + print(f" Batch count: {result['batch_count']}") + print(f" Elapsed time: {result['elapsed_time']}") + + # Assertions + assert result['rows_copied'] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" + assert result['batch_count'] == 2, f"Expected 2 batches, got {result['batch_count']}" + + # Verify the data + print(f"\nVerifying inserted data...") + cursor.execute(f"SELECT id, name, amount FROM {table_name} ORDER BY id") + rows = cursor.fetchall() + + print(f"Retrieved {len(rows)} rows:") + assert len(rows) == 10, f"Expected 10 rows retrieved, got {len(rows)}" + + for i, row in enumerate(rows): + print(f" ID: {row[0]}, Name: {row[1]}, Amount: {row[2]}") + assert row[0] == test_data[i][0], f"ID mismatch at row {i}" + assert row[1] == test_data[i][1], f"Name mismatch at row {i}" + assert float(row[2]) == test_data[i][2], f"Amount mismatch at row {i}" + + # Cleanup + print(f"\nCleaning up test table...") + cursor.execute(f"DROP TABLE {table_name}") + conn.commit() + + # Close cursor and connection + cursor.close() + conn.close() + print("\nTest completed successfully!") diff --git a/mssql_python/BCPRustWrapper.py b/mssql_python/BCPRustWrapper.py new file mode 100644 index 00000000..65ad85fe --- /dev/null +++ b/mssql_python/BCPRustWrapper.py @@ -0,0 +1,240 @@ +""" +BCP Rust Wrapper Module +Provides Python interface to the Rust-based mssql_py_core library +""" + +from typing import Optional, List, Tuple, Dict, Any, Iterable +from mssql_python.logging import logger + +try: + import mssql_py_core + RUST_CORE_AVAILABLE = True +except ImportError: + RUST_CORE_AVAILABLE = False + mssql_py_core = None + + +class BCPRustWrapper: + """ + Wrapper class for Rust-based BCP operations using mssql_py_core. + Supports context manager for automatic resource cleanup. + + Example: + with BCPRustWrapper(connection_string) as wrapper: + wrapper.connect() + result = wrapper.bulkcopy('TableName', data) + """ + + def __init__(self, connection_string: Optional[str] = None): + if not RUST_CORE_AVAILABLE: + raise ImportError( + "mssql_py_core is not installed. " + "Please install it from the BCPRustWheel directory." + ) + self._core = mssql_py_core + self._rust_connection = None + self._connection_string = connection_string + + def __enter__(self): + """Context manager entry""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit - ensures connection is closed""" + self.close() + return False + + def __del__(self): + """Destructor - cleanup resources if not already closed""" + try: + if self._rust_connection is not None: + logger.warning("BCPRustWrapper connection was not explicitly closed, cleaning up in destructor") + self.close() + except Exception: + # Ignore errors during cleanup in destructor + pass + + @property + def is_connected(self) -> bool: + """Check if connection is active""" + return self._rust_connection is not None + + def close(self): + """Close the connection and cleanup resources""" + if self._rust_connection: + try: + logger.info("Closing Rust connection") + # If the connection has a close method, call it + if hasattr(self._rust_connection, 'close'): + self._rust_connection.close() + except Exception as e: + logger.warning("Error closing connection: %s", str(e)) + finally: + # Always set to None to prevent reuse + self._rust_connection = None + + def connect(self, connection_string: Optional[str] = None): + """ + Create a connection using the Rust-based PyCoreConnection + + Args: + connection_string: SQL Server connection string + + Returns: + PyCoreConnection instance + + Raises: + ValueError: If connection string is missing or invalid + RuntimeError: If connection fails + """ + conn_str = connection_string or self._connection_string + if not conn_str: + raise ValueError("Connection string is required") + + # Close existing connection if any + if self._rust_connection: + logger.warning("Closing existing connection before creating new one") + self.close() + + try: + # Parse connection string into dictionary + params = {} + for pair in conn_str.split(';'): + if '=' in pair: + key, value = pair.split('=', 1) + params[key.strip().lower()] = value.strip() + + # Validate required parameters + if not params.get("server"): + raise ValueError("SERVER parameter is required in connection string") + + # PyCoreConnection expects a dictionary with specific keys + python_client_context = { + "server": params.get("server", "localhost"), + "database": params.get("database", "master"), + "user_name": params.get("uid", ""), + "password": params.get("pwd", ""), + "trust_server_certificate": params.get("trustservercertificate", "yes").lower() in ["yes", "true"], + "encryption": "Optional", + } + + logger.info("Attempting to connect to server: %s, database: %s", + python_client_context["server"], python_client_context["database"]) + + self._rust_connection = self._core.PyCoreConnection(python_client_context) + + logger.info("Connection established successfully") + return self._rust_connection + + except ValueError as ve: + logger.error("Connection string validation error: %s", str(ve)) + raise + except Exception as e: + logger.error("Failed to create connection: %s - %s", type(e).__name__, str(e)) + raise RuntimeError(f"Connection failed: {str(e)}") from e + + def bulkcopy(self, table_name: str, data: Iterable, batch_size: int = 1000, + timeout: int = 30, column_mappings: Optional[List[Tuple[int, str]]] = None) -> Dict[str, Any]: + """ + Perform bulk copy operation to insert data into SQL Server table. + + Args: + table_name: Target table name + data: Iterable of tuples/lists containing row data + batch_size: Number of rows per batch (default: 1000) + timeout: Timeout in seconds (default: 30) + column_mappings: List of tuples mapping source column index to target column name + e.g., [(0, "id"), (1, "name")] + + Returns: + Dictionary with bulk copy results containing: + - rows_copied: Number of rows successfully copied + - batch_count: Number of batches processed + - elapsed_time: Time taken for the operation + + Raises: + RuntimeError: If no active connection or cursor creation fails + ValueError: If parameters are invalid + """ + # Validate inputs + if not table_name or not isinstance(table_name, str): + raise ValueError("table_name must be a non-empty string") + + if batch_size <= 0: + raise ValueError(f"batch_size must be positive, got {batch_size}") + + if timeout <= 0: + raise ValueError(f"timeout must be positive, got {timeout}") + + if not self._rust_connection: + raise RuntimeError("No active connection. Call connect() first.") + + rust_cursor = None + try: + # Create cursor + rust_cursor = self._rust_connection.cursor() + except Exception as e: + logger.error("Failed to create cursor: %s - %s", type(e).__name__, str(e)) + raise RuntimeError(f"Cursor creation failed: {str(e)}") from e + + try: + # Build kwargs for bulkcopy + kwargs = { + "batch_size": batch_size, + "timeout": timeout, + } + + if column_mappings: + kwargs["column_mappings"] = column_mappings + + # Execute bulk copy with error handling + logger.info( + "Starting bulk copy to table '%s' - batch_size=%d, timeout=%d", + table_name, batch_size, timeout + ) + result = rust_cursor.bulkcopy(table_name, iter(data), kwargs=kwargs) + + logger.info( + "Bulk copy completed successfully - rows_copied=%d, batch_count=%d, elapsed_time=%s", + result.get('rows_copied', 0), + result.get('batch_count', 0), + result.get('elapsed_time', 'unknown') + ) + return result + except AttributeError as ae: + logger.error( + "Invalid cursor or method call for table '%s': %s", + table_name, str(ae) + ) + raise RuntimeError(f"Bulk copy method error: {str(ae)}") from ae + except TypeError as te: + logger.error( + "Invalid data type or parameters for table '%s': %s", + table_name, str(te) + ) + raise ValueError(f"Invalid bulk copy parameters: {str(te)}") from te + except Exception as e: + logger.error( + "Bulk copy failed for table '%s': %s - %s", + table_name, type(e).__name__, str(e) + ) + raise + finally: + # Always close cursor to prevent resource leak + if rust_cursor is not None: + try: + if hasattr(rust_cursor, 'close'): + rust_cursor.close() + logger.debug("Cursor closed successfully") + except Exception as e: + logger.warning("Error closing cursor: %s", str(e)) + + +def is_rust_core_available() -> bool: + """ + Check if the Rust core library is available + + Returns: + bool: True if mssql_py_core is installed, False otherwise + """ + return RUST_CORE_AVAILABLE diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index dfd47375..e0848ea7 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2451,6 +2451,73 @@ def nextset(self) -> Union[bool, None]: ) return True + def bulkcopy(self, table_name: str, data, batch_size: int = 1000, + timeout: int = 30, column_mappings: list = None): + """ + Perform bulk copy operation using Rust-based implementation. + + This method leverages the mssql_py_core Rust library for high-performance + bulk insert operations. + + Args: + table_name: Target table name + data: Iterable of tuples/lists containing row data + batch_size: Number of rows per batch (default: 1000) + timeout: Timeout in seconds (default: 30) + column_mappings: List of tuples mapping source column index to target column name + e.g., [(0, "id"), (1, "name")] + + Returns: + Dictionary with bulk copy results containing: + - 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 is not installed + RuntimeError: If no active connection exists + + Example: + >>> cursor = conn.cursor() + >>> data = [(1, "Alice"), (2, "Bob")] + >>> result = cursor.bulkcopy("users", data, + ... column_mappings=[(0, "id"), (1, "name")]) + >>> print(f"Copied {result['rows_copied']} rows") + """ + from mssql_python.BCPRustWrapper import BCPRustWrapper, is_rust_core_available + + if not is_rust_core_available(): + raise ImportError( + "Bulk copy requires mssql_py_core Rust library. " + "Please install it from the BCPRustWheel directory." + ) + + # Get connection string from the connection + if not hasattr(self.connection, 'connection_str'): + raise RuntimeError( + "Connection string not available. " + "Bulk copy requires connection string access." + ) + + # Create wrapper and use the existing connection's connection string + wrapper = BCPRustWrapper(self.connection.connection_str) + wrapper.connect() + + try: + # Perform bulk copy + result = wrapper.bulkcopy( + table_name=table_name, + data=data, + batch_size=batch_size, + timeout=timeout, + column_mappings=column_mappings + ) + return result + finally: + # Close the wrapper's connection + if wrapper._rust_connection and hasattr(wrapper._rust_connection, 'close'): + wrapper._rust_connection.close() + def __enter__(self): """ Enter the runtime context for the cursor. From 284dd2fee43dae24889008d5ef4329d87076741b Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Wed, 14 Jan 2026 18:45:40 +0000 Subject: [PATCH 02/16] additional test --- TestsBCP/test_column_mismatch_BCP.py | 300 +++++++++++++++++++++++++ TestsBCP/test_date_BCP.py | 258 +++++++++++++++++++++ TestsBCP/test_datetime_BCP.py | 324 +++++++++++++++++++++++++++ 3 files changed, 882 insertions(+) create mode 100644 TestsBCP/test_column_mismatch_BCP.py create mode 100644 TestsBCP/test_date_BCP.py create mode 100644 TestsBCP/test_datetime_BCP.py diff --git a/TestsBCP/test_column_mismatch_BCP.py b/TestsBCP/test_column_mismatch_BCP.py new file mode 100644 index 00000000..b8dd8cd5 --- /dev/null +++ b/TestsBCP/test_column_mismatch_BCP.py @@ -0,0 +1,300 @@ +"""Bulk copy tests for column count mismatch scenarios. + +Tests the behavior when bulk copying data where: +1. Source data has more columns than the target table +2. Source data has fewer columns than the target table + +According to expected behavior: +- Extra columns in the source should be dropped/ignored +- Missing columns should result in NULL values (if nullable) or defaults +""" + +import pytest + + +@pytest.mark.integration +def test_bulkcopy_more_columns_than_table(cursor): + """Test bulk copy where source has more columns than the target table. + + The extra columns should be dropped and the bulk copy should succeed. + Only the columns specified in column_mappings should be inserted. + """ + + # Create a test table with 3 INT columns + table_name = "BulkCopyMoreColumnsTest" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute( + f"CREATE TABLE {table_name} (id INT PRIMARY KEY, value1 INT, value2 INT)" + ) + cursor.connection.commit() + + # Source data has 5 columns, but table only has 3 + # Extra columns (indices 3 and 4) should be ignored via column_mappings + data = [ + (1, 100, 30, 999, 888), + (2, 200, 25, 999, 888), + (3, 300, 35, 999, 888), + (4, 400, 28, 999, 888), + ] + + # Execute bulk copy with explicit column mappings for first 3 columns only + result = cursor.bulkcopy( + table_name=table_name, + data=data, + batch_size=1000, + timeout=30, + column_mappings=[ + (0, "id"), # Map source column 0 to 'id' + (1, "value1"), # Map source column 1 to 'value1' + (2, "value2"), # Map source column 2 to 'value2' + # Columns 3 and 4 are NOT mapped, so they're dropped + ] + ) + + # Verify results + assert result is not None + assert result["rows_copied"] == 4, "Expected 4 rows to be copied" + assert result["batch_count"] >= 1 + + # Verify data was inserted correctly (only first 3 columns) + cursor.execute(f"SELECT id, value1, value2 FROM {table_name} ORDER BY id") + rows = cursor.fetchall() + + assert len(rows) == 4, "Expected 4 rows in table" + assert rows[0][0] == 1 and rows[0][1] == 100 and rows[0][2] == 30 + assert rows[1][0] == 2 and rows[1][1] == 200 and rows[1][2] == 25 + assert rows[2][0] == 3 and rows[2][1] == 300 and rows[2][2] == 35 + assert rows[3][0] == 4 and rows[3][1] == 400 and rows[3][2] == 28 + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() + + +@pytest.mark.integration +def test_bulkcopy_fewer_columns_than_table(cursor): + """Test bulk copy where source has fewer columns than the target table. + + Missing columns should be filled with NULL (if nullable). + The bulk copy should succeed. + """ + + # Create a test table with 3 INT columns (value2 is nullable) + table_name = "BulkCopyFewerColumnsTest" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute( + f"CREATE TABLE {table_name} (id INT PRIMARY KEY, value1 INT, value2 INT NULL)" + ) + cursor.connection.commit() + + # Source data has only 2 columns (id, value1) - missing 'value2' + data = [ + (1, 100), + (2, 200), + (3, 300), + (4, 400), + ] + + # Execute bulk copy with mappings for only 2 columns + # 'value2' column is not mapped, so it should get NULL values + result = cursor.bulkcopy( + table_name=table_name, + data=data, + batch_size=1000, + timeout=30, + column_mappings=[ + (0, "id"), # Map source column 0 to 'id' + (1, "value1"), # Map source column 1 to 'value1' + # 'value2' is not mapped, should be NULL + ] + ) + + # Verify results + assert result is not None + assert result["rows_copied"] == 4, "Expected 4 rows to be copied" + assert result["batch_count"] >= 1 + + # Verify data was inserted with NULL for missing 'value2' column + cursor.execute(f"SELECT id, value1, value2 FROM {table_name} ORDER BY id") + rows = cursor.fetchall() + + assert len(rows) == 4, "Expected 4 rows in table" + assert rows[0][0] == 1 and rows[0][1] == 100 and rows[0][2] is None, "value2 should be NULL" + assert rows[1][0] == 2 and rows[1][1] == 200 and rows[1][2] is None, "value2 should be NULL" + assert rows[2][0] == 3 and rows[2][1] == 300 and rows[2][2] is None, "value2 should be NULL" + assert rows[3][0] == 4 and rows[3][1] == 400 and rows[3][2] is None, "value2 should be NULL" + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() + + +@pytest.mark.integration +def test_bulkcopy_auto_mapping_with_extra_columns(cursor): + """Test bulk copy with auto-mapping when source has more columns than table. + + Without explicit column_mappings, auto-mapping should use the first N columns + where N is the number of columns in the target table. Extra source columns are ignored. + """ + + # Create a test table with 3 INT columns + table_name = "BulkCopyAutoMapExtraTest" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute( + f"CREATE TABLE {table_name} (id INT, value1 INT, value2 INT)" + ) + cursor.connection.commit() + + # Source data has 5 columns, table has 3 + # Auto-mapping should use first 3 columns + data = [ + (1, 100, 30, 777, 666), + (2, 200, 25, 777, 666), + (3, 300, 35, 777, 666), + ] + + # Execute bulk copy WITHOUT explicit column mappings + # Auto-mapping should map first 3 columns to table's 3 columns + result = cursor.bulkcopy( + table_name=table_name, + data=data, + batch_size=1000, + timeout=30 + ) + + # Verify results + assert result is not None + assert result["rows_copied"] == 3, "Expected 3 rows to be copied" + + # Verify data was inserted correctly (first 3 columns only) + cursor.execute(f"SELECT id, value1, value2 FROM {table_name} ORDER BY id") + rows = cursor.fetchall() + + assert len(rows) == 3, "Expected 3 rows in table" + assert rows[0][0] == 1 and rows[0][1] == 100 and rows[0][2] == 30 + assert rows[1][0] == 2 and rows[1][1] == 200 and rows[1][2] == 25 + assert rows[2][0] == 3 and rows[2][1] == 300 and rows[2][2] == 35 + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() + + +@pytest.mark.integration +def test_bulkcopy_fewer_columns_with_defaults(cursor): + """Test bulk copy where missing columns have default values. + + Missing columns should use their default values instead of NULL. + """ + + # Create a test table with default values + table_name = "BulkCopyFewerColumnsDefaultTest" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute( + f"""CREATE TABLE {table_name} ( + id INT PRIMARY KEY, + value1 INT, + value2 INT DEFAULT 999, + status VARCHAR(10) DEFAULT 'active' + )""" + ) + cursor.connection.commit() + + # Source data has only 2 columns - missing value2 and status + data = [ + (1, 100), + (2, 200), + (3, 300), + ] + + # Execute bulk copy mapping only 2 columns + result = cursor.bulkcopy( + table_name=table_name, + data=data, + batch_size=1000, + timeout=30, + column_mappings=[ + (0, "id"), + (1, "value1"), + # value2 and status not mapped - should use defaults + ] + ) + + # Verify results + assert result["rows_copied"] == 3 + + # Verify data was inserted with default values + cursor.execute(f"SELECT id, value1, value2, status FROM {table_name} ORDER BY id") + rows = cursor.fetchall() + + assert len(rows) == 3 + assert rows[0][0] == 1 and rows[0][1] == 100 and rows[0][2] == 999 and rows[0][3] == 'active' + assert rows[1][0] == 2 and rows[1][1] == 200 and rows[1][2] == 999 and rows[1][3] == 'active' + assert rows[2][0] == 3 and rows[2][1] == 300 and rows[2][2] == 999 and rows[2][3] == 'active' + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() + + +@pytest.mark.integration +def test_bulkcopy_column_reordering(cursor): + """Test bulk copy with column reordering. + + Source columns can be mapped to target columns in different order. + """ + + # Create a test table + table_name = "BulkCopyColumnReorderTest" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute( + f"CREATE TABLE {table_name} (id INT, name VARCHAR(50), age INT, city VARCHAR(50))" + ) + cursor.connection.commit() + + # Source data: (name, age, city, id) - different order than table + data = [ + ("Alice", 30, "NYC", 1), + ("Bob", 25, "LA", 2), + ("Carol", 35, "Chicago", 3), + ] + + # Map source columns to target in different order + result = cursor.bulkcopy( + table_name=table_name, + data=data, + batch_size=1000, + timeout=30, + column_mappings=[ + (3, "id"), # Source column 3 (id) → Target id + (0, "name"), # Source column 0 (name) → Target name + (1, "age"), # Source column 1 (age) → Target age + (2, "city"), # Source column 2 (city) → Target city + ] + ) + + # Verify results + assert result["rows_copied"] == 3 + + # Verify data was inserted correctly with proper column mapping + cursor.execute(f"SELECT id, name, age, city FROM {table_name} ORDER BY id") + rows = cursor.fetchall() + + assert len(rows) == 3 + assert rows[0][0] == 1 and rows[0][1] == "Alice" and rows[0][2] == 30 and rows[0][3] == "NYC" + assert rows[1][0] == 2 and rows[1][1] == "Bob" and rows[1][2] == 25 and rows[1][3] == "LA" + assert rows[2][0] == 3 and rows[2][1] == "Carol" and rows[2][2] == 35 and rows[2][3] == "Chicago" + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() diff --git a/TestsBCP/test_date_BCP.py b/TestsBCP/test_date_BCP.py new file mode 100644 index 00000000..55c9ef80 --- /dev/null +++ b/TestsBCP/test_date_BCP.py @@ -0,0 +1,258 @@ +"""Bulk copy tests for DATE data type.""" +import pytest +import datetime + + +@pytest.mark.integration +def test_cursor_bulkcopy_date_basic(cursor): + """Test cursor bulkcopy method with two date columns and explicit mappings.""" + + # Create a test table with two date columns + table_name = "BulkCopyTestTableDate" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute(f"CREATE TABLE {table_name} (event_date DATE, birth_date DATE)") + cursor.connection.commit() + + # Prepare test data - two columns, both date + data = [ + (datetime.date(2020, 1, 15), datetime.date(1990, 5, 20)), + (datetime.date(2021, 6, 10), datetime.date(1985, 3, 25)), + (datetime.date(2022, 12, 25), datetime.date(2000, 7, 4)), + ] + + # Execute bulk copy with explicit column mappings + result = cursor.bulkcopy( + table_name=table_name, + data=data, + batch_size=1000, + timeout=30, + column_mappings=[ + (0, "event_date"), + (1, "birth_date"), + ] + ) + + # Verify results + assert result is not None + assert result["rows_copied"] == 3 + assert result["batch_count"] == 1 + assert "elapsed_time" in result + + # Verify data was inserted correctly + cursor.execute(f"SELECT event_date, birth_date FROM {table_name} ORDER BY event_date") + rows = cursor.fetchall() + assert len(rows) == 3 + assert rows[0][0] == datetime.date(2020, 1, 15) and rows[0][1] == datetime.date(1990, 5, 20) + assert rows[1][0] == datetime.date(2021, 6, 10) and rows[1][1] == datetime.date(1985, 3, 25) + assert rows[2][0] == datetime.date(2022, 12, 25) and rows[2][1] == datetime.date(2000, 7, 4) + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() + + +@pytest.mark.integration +def test_cursor_bulkcopy_date_auto_mapping(cursor): + """Test cursor bulkcopy with automatic column mapping. + + Tests bulkcopy when no mappings are specified, including NULL value handling. + """ + + # Create a test table with two nullable date columns + table_name = "BulkCopyAutoMapTableDate" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute(f"CREATE TABLE {table_name} (event_date DATE, birth_date DATE)") + cursor.connection.commit() + + # Prepare test data - two columns, both date, with NULL values + data = [ + (datetime.date(2020, 1, 15), datetime.date(1990, 5, 20)), + (datetime.date(2021, 6, 10), None), # NULL value in second column + (None, datetime.date(1985, 3, 25)), # NULL value in first column + (datetime.date(2022, 12, 25), datetime.date(2000, 7, 4)), + ] + + # Execute bulk copy WITHOUT column mappings - should auto-generate + result = cursor.bulkcopy( + table_name=table_name, + data=data, + batch_size=1000, + timeout=30 + ) + + # Verify results + assert result is not None + assert result["rows_copied"] == 4 + assert result["batch_count"] == 1 + assert "elapsed_time" in result + + # Verify data including NULLs + cursor.execute(f"SELECT event_date, birth_date FROM {table_name} ORDER BY COALESCE(event_date, '9999-12-31')") + rows = cursor.fetchall() + assert len(rows) == 4 + assert rows[0][0] == datetime.date(2020, 1, 15) and rows[0][1] == datetime.date(1990, 5, 20) + assert rows[1][0] == datetime.date(2021, 6, 10) and rows[1][1] is None + assert rows[2][0] == datetime.date(2022, 12, 25) and rows[2][1] == datetime.date(2000, 7, 4) + assert rows[3][0] is None and rows[3][1] == datetime.date(1985, 3, 25) + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() + + +@pytest.mark.integration +def test_cursor_bulkcopy_date_string_to_date_conversion(cursor): + """Test cursor bulkcopy with string values that should convert to date columns. + + Tests type coercion when source data contains date strings but + destination columns are DATE type. + """ + + # Create a test table with two date columns + table_name = "BulkCopyStringToDateTable" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute(f"CREATE TABLE {table_name} (event_date DATE, birth_date DATE)") + cursor.connection.commit() + + # Prepare test data - strings containing valid dates in ISO format + data = [ + ("2020-01-15", "1990-05-20"), + ("2021-06-10", "1985-03-25"), + ("2022-12-25", "2000-07-04"), + ] + + # Execute bulk copy without explicit mappings + result = cursor.bulkcopy( + table_name=table_name, + data=data, + batch_size=1000, + timeout=30 + ) + + # Verify results + assert result is not None + assert result["rows_copied"] == 3 + assert result["batch_count"] == 1 + assert "elapsed_time" in result + + # Verify data was converted correctly + cursor.execute(f"SELECT event_date, birth_date FROM {table_name} ORDER BY event_date") + rows = cursor.fetchall() + assert len(rows) == 3 + assert rows[0][0] == datetime.date(2020, 1, 15) and rows[0][1] == datetime.date(1990, 5, 20) + assert rows[1][0] == datetime.date(2021, 6, 10) and rows[1][1] == datetime.date(1985, 3, 25) + assert rows[2][0] == datetime.date(2022, 12, 25) and rows[2][1] == datetime.date(2000, 7, 4) + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() + + +@pytest.mark.integration +def test_cursor_bulkcopy_date_boundary_values(cursor): + """Test cursor bulkcopy with DATE boundary values.""" + + # Create a test table + table_name = "BulkCopyDateBoundaryTest" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute(f"CREATE TABLE {table_name} (id INT, test_date DATE)") + cursor.connection.commit() + + # Test data with boundary values + # DATE range: 0001-01-01 to 9999-12-31 + data = [ + (1, datetime.date(1, 1, 1)), # Min DATE + (2, datetime.date(9999, 12, 31)), # Max DATE + (3, datetime.date(2000, 1, 1)), # Y2K + (4, datetime.date(1900, 1, 1)), # Century boundary + (5, datetime.date(2024, 2, 29)), # Leap year + ] + + # Execute bulk copy + result = cursor.bulkcopy( + table_name=table_name, + data=data, + batch_size=1000, + timeout=30, + column_mappings=[ + (0, "id"), + (1, "test_date"), + ] + ) + + # Verify results + assert result["rows_copied"] == 5 + assert result["batch_count"] == 1 + + # Verify data + cursor.execute(f"SELECT id, test_date FROM {table_name} ORDER BY id") + rows = cursor.fetchall() + assert len(rows) == 5 + assert rows[0][1] == datetime.date(1, 1, 1) # Min DATE + assert rows[1][1] == datetime.date(9999, 12, 31) # Max DATE + assert rows[2][1] == datetime.date(2000, 1, 1) # Y2K + assert rows[3][1] == datetime.date(1900, 1, 1) # Century boundary + assert rows[4][1] == datetime.date(2024, 2, 29) # Leap year + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() + + +@pytest.mark.integration +def test_cursor_bulkcopy_date_large_batch(cursor): + """Test cursor bulkcopy with a large number of DATE rows.""" + + # Create a test table + table_name = "BulkCopyDateLargeBatchTest" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute(f"CREATE TABLE {table_name} (id INT, test_date DATE)") + cursor.connection.commit() + + # Generate 365 rows (one for each day of 2024) + base_date = datetime.date(2024, 1, 1) + data = [(i + 1, base_date + datetime.timedelta(days=i)) for i in range(365)] + + # Execute bulk copy with smaller batch size + result = cursor.bulkcopy( + table_name=table_name, + data=data, + batch_size=50, # ~8 batches + timeout=30, + column_mappings=[ + (0, "id"), + (1, "test_date"), + ] + ) + + # Verify results + assert result["rows_copied"] == 365 + assert result["batch_count"] >= 7 # 365 / 50 = 7.3 + + # Verify row count + cursor.execute(f"SELECT COUNT(*) FROM {table_name}") + count = cursor.fetchone()[0] + assert count == 365 + + # Verify sample data + cursor.execute(f"SELECT id, test_date FROM {table_name} WHERE id IN (1, 100, 200, 365)") + rows = cursor.fetchall() + assert len(rows) == 4 + assert rows[0][0] == 1 and rows[0][1] == datetime.date(2024, 1, 1) + assert rows[1][0] == 100 and rows[1][1] == datetime.date(2024, 4, 9) + assert rows[2][0] == 200 and rows[2][1] == datetime.date(2024, 7, 18) + assert rows[3][0] == 365 and rows[3][1] == datetime.date(2024, 12, 30) + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() diff --git a/TestsBCP/test_datetime_BCP.py b/TestsBCP/test_datetime_BCP.py new file mode 100644 index 00000000..4c2bcb63 --- /dev/null +++ b/TestsBCP/test_datetime_BCP.py @@ -0,0 +1,324 @@ +"""Bulk copy tests for DATETIME data type.""" +import pytest +import datetime + + +@pytest.mark.integration +def test_cursor_bulkcopy_datetime_basic(cursor): + """Test cursor bulkcopy method with two datetime columns and explicit mappings.""" + # Create a test table with two datetime columns + table_name = "BulkCopyTestTableDateTime" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute(f"CREATE TABLE {table_name} (start_datetime DATETIME, end_datetime DATETIME)") + cursor.connection.commit() + + # Prepare test data - two columns, both datetime + data = [ + (datetime.datetime(2024, 1, 15, 9, 30, 0), datetime.datetime(2024, 1, 15, 17, 45, 30)), + (datetime.datetime(2024, 2, 20, 8, 15, 45), datetime.datetime(2024, 2, 20, 16, 30, 15)), + (datetime.datetime(2024, 3, 10, 10, 0, 0), datetime.datetime(2024, 3, 10, 18, 0, 0)), + ] + + # Execute bulk copy with explicit column mappings + result = cursor.bulkcopy( + table_name, + data, + batch_size=1000, + timeout=30, + column_mappings=[ + (0, "start_datetime"), + (1, "end_datetime"), + ], + ) + + # Verify results + assert result is not None + assert result["rows_copied"] == 3 + assert result["batch_count"] == 1 + assert "elapsed_time" in result + + # Verify data was inserted by checking the count + cursor.execute(f"SELECT COUNT(*) FROM {table_name}") + rows = cursor.fetchall() + count = rows[0][0] + assert count == 3 + + # Verify actual datetime values + cursor.execute(f"SELECT start_datetime, end_datetime FROM {table_name} ORDER BY start_datetime") + rows = cursor.fetchall() + assert len(rows) == 3 + + # Verify first row + assert rows[0][0] == datetime.datetime(2024, 1, 15, 9, 30, 0) + assert rows[0][1] == datetime.datetime(2024, 1, 15, 17, 45, 30) + + # Verify second row + assert rows[1][0] == datetime.datetime(2024, 2, 20, 8, 15, 45) + assert rows[1][1] == datetime.datetime(2024, 2, 20, 16, 30, 15) + + # Verify third row + assert rows[2][0] == datetime.datetime(2024, 3, 10, 10, 0, 0) + assert rows[2][1] == datetime.datetime(2024, 3, 10, 18, 0, 0) + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() + + +@pytest.mark.integration +def test_cursor_bulkcopy_datetime_auto_mapping(cursor): + """Test cursor bulkcopy with automatic column mapping. + + Tests bulkcopy when no mappings are specified, including NULL value handling. + """ + # Create a test table with two nullable datetime columns + table_name = "BulkCopyAutoMapTableDateTime" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute(f"CREATE TABLE {table_name} (start_datetime DATETIME, end_datetime DATETIME)") + cursor.connection.commit() + + # Prepare test data - two columns, both datetime, with NULL values + data = [ + (datetime.datetime(2024, 1, 15, 9, 30, 0), datetime.datetime(2024, 1, 15, 17, 45, 30)), + (datetime.datetime(2024, 2, 20, 8, 15, 45), None), # NULL value in second column + (None, datetime.datetime(2024, 2, 20, 16, 30, 15)), # NULL value in first column + (datetime.datetime(2024, 3, 10, 10, 0, 0), datetime.datetime(2024, 3, 10, 18, 0, 0)), + ] + + # Execute bulk copy WITHOUT column mappings - should auto-generate + result = cursor.bulkcopy( + table_name, + data, + batch_size=1000, + timeout=30 + ) + + # Verify results + assert result is not None + assert result["rows_copied"] == 4 + assert result["batch_count"] == 1 + assert "elapsed_time" in result + + # Verify data was inserted by checking the count + cursor.execute(f"SELECT COUNT(*) FROM {table_name}") + rows = cursor.fetchall() + count = rows[0][0] + assert count == 4 + + # Verify NULL handling + cursor.execute(f"SELECT start_datetime, end_datetime FROM {table_name} ORDER BY ISNULL(start_datetime, '1900-01-01')") + rows = cursor.fetchall() + assert len(rows) == 4 + + # Verify NULL value in first column (third row after sorting) + assert rows[0][0] is None + assert rows[0][1] == datetime.datetime(2024, 2, 20, 16, 30, 15) + + # Verify NULL value in second column + assert rows[2][0] == datetime.datetime(2024, 2, 20, 8, 15, 45) + assert rows[2][1] is None + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() + + +@pytest.mark.integration +def test_cursor_bulkcopy_datetime_string_to_datetime_conversion(cursor): + """Test cursor bulkcopy with string values that should convert to datetime columns. + + Tests type coercion when source data contains datetime strings but + destination columns are DATETIME type. + """ + # Create a test table with two datetime columns + table_name = "BulkCopyStringToDateTimeTable" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute(f"CREATE TABLE {table_name} (start_datetime DATETIME, end_datetime DATETIME)") + cursor.connection.commit() + + # Prepare test data - strings containing valid datetimes in ISO format + data = [ + ("2024-01-15 09:30:00", "2024-01-15 17:45:30"), + ("2024-02-20 08:15:45", "2024-02-20 16:30:15"), + ("2024-03-10 10:00:00", "2024-03-10 18:00:00"), + ] + + # Execute bulk copy without explicit mappings + result = cursor.bulkcopy( + table_name, + data, + batch_size=1000, + timeout=30 + ) + + # Verify results + assert result is not None + assert result["rows_copied"] == 3 + assert result["batch_count"] == 1 + assert "elapsed_time" in result + + # Verify data was inserted by checking the count + cursor.execute(f"SELECT COUNT(*) FROM {table_name}") + rows = cursor.fetchall() + count = rows[0][0] + assert count == 3 + + # Verify the datetime values were properly converted from strings + cursor.execute(f"SELECT start_datetime, end_datetime FROM {table_name} ORDER BY start_datetime") + rows = cursor.fetchall() + assert len(rows) == 3 + + # Verify first row + assert rows[0][0] == datetime.datetime(2024, 1, 15, 9, 30, 0) + assert rows[0][1] == datetime.datetime(2024, 1, 15, 17, 45, 30) + + # Verify second row + assert rows[1][0] == datetime.datetime(2024, 2, 20, 8, 15, 45) + assert rows[1][1] == datetime.datetime(2024, 2, 20, 16, 30, 15) + + # Verify third row + assert rows[2][0] == datetime.datetime(2024, 3, 10, 10, 0, 0) + assert rows[2][1] == datetime.datetime(2024, 3, 10, 18, 0, 0) + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() + + +@pytest.mark.integration +def test_cursor_bulkcopy_datetime_boundary_values(cursor): + """Test cursor bulkcopy with DATETIME boundary values. + + DATETIME range: 1753-01-01 00:00:00 to 9999-12-31 23:59:59.997 + Precision: Rounded to increments of .000, .003, or .007 seconds + """ + table_name = "BulkCopyDateTimeBoundaryTable" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute(f"CREATE TABLE {table_name} (dt_value DATETIME)") + cursor.connection.commit() + + # Test data with boundary values and edge cases + data = [ + (datetime.datetime(1753, 1, 1, 0, 0, 0),), # Minimum datetime + (datetime.datetime(9999, 12, 31, 23, 59, 59),), # Maximum datetime + (datetime.datetime(2000, 1, 1, 0, 0, 0),), # Y2K + (datetime.datetime(1999, 12, 31, 23, 59, 59),), # Pre-Y2K + (datetime.datetime(2024, 2, 29, 12, 0, 0),), # Leap year + (datetime.datetime(2024, 12, 31, 23, 59, 59),), # End of year + ] + + # Execute bulk copy + result = cursor.bulkcopy( + table_name, + data, + batch_size=1000, + timeout=30 + ) + + # Verify results + assert result is not None + assert result["rows_copied"] == 6 + assert result["batch_count"] == 1 + + # Verify data + cursor.execute(f"SELECT dt_value FROM {table_name} ORDER BY dt_value") + rows = cursor.fetchall() + assert len(rows) == 6 + + # Verify minimum datetime + assert rows[0][0] == datetime.datetime(1753, 1, 1, 0, 0, 0) + + # Verify pre-Y2K + assert rows[1][0] == datetime.datetime(1999, 12, 31, 23, 59, 59) + + # Verify Y2K + assert rows[2][0] == datetime.datetime(2000, 1, 1, 0, 0, 0) + + # Verify leap year + assert rows[3][0] == datetime.datetime(2024, 2, 29, 12, 0, 0) + + # Verify end of year + assert rows[4][0] == datetime.datetime(2024, 12, 31, 23, 59, 59) + + # Verify maximum datetime + assert rows[5][0] == datetime.datetime(9999, 12, 31, 23, 59, 59) + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() + + +@pytest.mark.integration +def test_cursor_bulkcopy_datetime_mixed_types(cursor): + """Test cursor bulkcopy with DATETIME in a table with mixed column types. + + Verifies that DATETIME columns work correctly alongside other data types. + """ + table_name = "BulkCopyDateTimeMixedTable" + cursor.execute( + f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" + ) + cursor.execute(f""" + CREATE TABLE {table_name} ( + id INT, + created_at DATETIME, + is_active BIT, + modified_at DATETIME + ) + """) + cursor.connection.commit() + + # Test data with mixed types (INT, DATETIME, BIT, DATETIME) + data = [ + (1, datetime.datetime(2024, 1, 15, 9, 30, 0), True, datetime.datetime(2024, 1, 15, 10, 0, 0)), + (2, datetime.datetime(2024, 2, 20, 8, 15, 45), False, datetime.datetime(2024, 2, 20, 14, 30, 0)), + (3, datetime.datetime(2024, 3, 10, 10, 0, 0), True, None), # NULL datetime + ] + + # Execute bulk copy + result = cursor.bulkcopy( + table_name, + data, + batch_size=1000, + timeout=30 + ) + + # Verify bulk copy succeeded + assert result is not None + assert result["rows_copied"] == 3 + + # Verify the data was inserted correctly + cursor.execute(f"SELECT id, created_at, is_active, modified_at FROM {table_name} ORDER BY id") + rows = cursor.fetchall() + + assert len(rows) == 3 + + # Verify first row + assert rows[0][0] == 1 + assert rows[0][1] == datetime.datetime(2024, 1, 15, 9, 30, 0) + assert rows[0][2] == True + assert rows[0][3] == datetime.datetime(2024, 1, 15, 10, 0, 0) + + # Verify second row + assert rows[1][0] == 2 + assert rows[1][1] == datetime.datetime(2024, 2, 20, 8, 15, 45) + assert rows[1][2] == False + assert rows[1][3] == datetime.datetime(2024, 2, 20, 14, 30, 0) + + # Verify third row (with NULL datetime) + assert rows[2][0] == 3 + assert rows[2][1] == datetime.datetime(2024, 3, 10, 10, 0, 0) + assert rows[2][2] == True + assert rows[2][3] is None # NULL modified_at + + # Cleanup + cursor.execute(f"DROP TABLE {table_name}") + cursor.connection.commit() From 387d1e9fd542600949047bb9eb0f307281c7d3dc Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Wed, 14 Jan 2026 18:49:19 +0000 Subject: [PATCH 03/16] linting --- TestsBCP/test_bigint_BCP.py | 48 ++++++----- TestsBCP/test_binary_BCP.py | 114 ++++++++++++++----------- TestsBCP/test_bit_BCP.py | 86 +++++++++++-------- TestsBCP/test_column_mismatch_BCP.py | 95 +++++++++------------ TestsBCP/test_cursor_bulkcopy.py | 36 ++++---- TestsBCP/test_date_BCP.py | 75 +++++++--------- TestsBCP/test_datetime_BCP.py | 111 +++++++++++------------- mssql_python/BCPRustWrapper.py | 122 ++++++++++++++------------- mssql_python/cursor.py | 39 +++++---- 9 files changed, 363 insertions(+), 363 deletions(-) diff --git a/TestsBCP/test_bigint_BCP.py b/TestsBCP/test_bigint_BCP.py index 9d51b0e2..f2806ea1 100644 --- a/TestsBCP/test_bigint_BCP.py +++ b/TestsBCP/test_bigint_BCP.py @@ -13,26 +13,28 @@ def test_bigint_bulkcopy(): # Get connection string from environment conn_str = os.getenv("DB_CONNECTION_STRING") assert conn_str is not None, "DB_CONNECTION_STRING environment variable must be set" - + print(f"Connection string length: {len(conn_str)}") - + # Connect using the regular mssql_python connection conn = connect(conn_str) print(f"Connection created: {type(conn)}") - + # Create cursor cursor = conn.cursor() print(f"Cursor created: {type(cursor)}") - + # Create a test table with BIGINT columns table_name = "BulkCopyBigIntTest" - + print(f"\nCreating test table: {table_name}") cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f"CREATE TABLE {table_name} (id INT, bigint_value BIGINT, description VARCHAR(100))") + cursor.execute( + f"CREATE TABLE {table_name} (id INT, bigint_value BIGINT, description VARCHAR(100))" + ) conn.commit() print("Test table created successfully") - + # Prepare test data with various BIGINT values # BIGINT range: -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 test_data = [ @@ -47,10 +49,10 @@ def test_bigint_bulkcopy(): (9, -9223372036854775807, "Near min value"), (10, 123456789012345, "Random large value"), ] - + print(f"\nPerforming bulk copy with {len(test_data)} rows using cursor.bulkcopy()...") print("Testing BIGINT data type with edge cases...") - + # Perform bulk copy via cursor result = cursor.bulkcopy( table_name=table_name, @@ -61,49 +63,51 @@ def test_bigint_bulkcopy(): (0, "id"), (1, "bigint_value"), (2, "description"), - ] + ], ) - + print(f"\nBulk copy completed successfully!") print(f" Rows copied: {result['rows_copied']}") print(f" Batch count: {result['batch_count']}") print(f" Elapsed time: {result['elapsed_time']}") - + # Assertions - assert result['rows_copied'] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" - assert result['batch_count'] == 2, f"Expected 2 batches, got {result['batch_count']}" - + assert result["rows_copied"] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" + assert result["batch_count"] == 2, f"Expected 2 batches, got {result['batch_count']}" + # Verify the data print(f"\nVerifying inserted data...") cursor.execute(f"SELECT id, bigint_value, description FROM {table_name} ORDER BY id") rows = cursor.fetchall() - + print(f"Retrieved {len(rows)} rows:") assert len(rows) == 10, f"Expected 10 rows retrieved, got {len(rows)}" - + for i, row in enumerate(rows): print(f" ID: {row[0]}, BIGINT Value: {row[1]}, Description: {row[2]}") assert row[0] == test_data[i][0], f"ID mismatch at row {i}" - assert row[1] == test_data[i][1], f"BIGINT value mismatch at row {i}: expected {test_data[i][1]}, got {row[1]}" + assert ( + row[1] == test_data[i][1] + ), f"BIGINT value mismatch at row {i}: expected {test_data[i][1]}, got {row[1]}" assert row[2] == test_data[i][2], f"Description mismatch at row {i}" - + # Additional verification for edge cases print("\nVerifying edge case values...") cursor.execute(f"SELECT bigint_value FROM {table_name} WHERE id = 4") max_value = cursor.fetchone()[0] assert max_value == 9223372036854775807, f"Max BIGINT verification failed" print(f" ✓ Max BIGINT value verified: {max_value}") - + cursor.execute(f"SELECT bigint_value FROM {table_name} WHERE id = 5") min_value = cursor.fetchone()[0] assert min_value == -9223372036854775808, f"Min BIGINT verification failed" print(f" ✓ Min BIGINT value verified: {min_value}") - + # Cleanup print(f"\nCleaning up test table...") cursor.execute(f"DROP TABLE {table_name}") conn.commit() - + # Close cursor and connection cursor.close() conn.close() diff --git a/TestsBCP/test_binary_BCP.py b/TestsBCP/test_binary_BCP.py index 1cf6c1a4..66455018 100644 --- a/TestsBCP/test_binary_BCP.py +++ b/TestsBCP/test_binary_BCP.py @@ -13,51 +13,57 @@ def test_binary_varbinary_bulkcopy(): # Get connection string from environment conn_str = os.getenv("DB_CONNECTION_STRING") assert conn_str is not None, "DB_CONNECTION_STRING environment variable must be set" - + print(f"Connection string length: {len(conn_str)}") - + # Connect using the regular mssql_python connection conn = connect(conn_str) print(f"Connection created: {type(conn)}") - + # Create cursor cursor = conn.cursor() print(f"Cursor created: {type(cursor)}") - + # Create a test table with BINARY and VARBINARY columns table_name = "BulkCopyBinaryTest" - + print(f"\nCreating test table: {table_name}") cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f""" + cursor.execute( + f""" CREATE TABLE {table_name} ( id INT, binary_data BINARY(16), varbinary_data VARBINARY(100), description VARCHAR(100) ) - """) + """ + ) conn.commit() print("Test table created successfully") - + # Prepare test data with various BINARY/VARBINARY values test_data = [ - (1, b'\x00' * 16, b'', "Empty varbinary"), - (2, b'\x01\x02\x03\x04' + b'\x00' * 12, b'\x01\x02\x03\x04', "Small binary data"), - (3, b'\xFF' * 16, b'\xFF' * 16, "All 0xFF bytes"), - (4, b'\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xAA\xBB\xCC\xDD\xEE\xFF', - b'\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xAA\xBB\xCC\xDD\xEE\xFF', "Hex sequence"), - (5, b'Hello World!!!!!' [:16], b'Hello World!', "ASCII text as binary"), + (1, b"\x00" * 16, b"", "Empty varbinary"), + (2, b"\x01\x02\x03\x04" + b"\x00" * 12, b"\x01\x02\x03\x04", "Small binary data"), + (3, b"\xff" * 16, b"\xff" * 16, "All 0xFF bytes"), + ( + 4, + b"\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff", + b"\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff", + "Hex sequence", + ), + (5, b"Hello World!!!!!"[:16], b"Hello World!", "ASCII text as binary"), (6, bytes(range(16)), bytes(range(50)), "Sequential bytes"), - (7, b'\x00' * 16, b'\x00' * 100, "Max varbinary length"), - (8, b'\xDE\xAD\xBE\xEF' * 4, b'\xDE\xAD\xBE\xEF' * 5, "Repeated pattern"), - (9, b'\x01' * 16, b'\x01', "Single byte varbinary"), - (10, b'\x80' * 16, b'\x80\x90\xA0\xB0\xC0\xD0\xE0\xF0', "High-bit bytes"), + (7, b"\x00" * 16, b"\x00" * 100, "Max varbinary length"), + (8, b"\xde\xad\xbe\xef" * 4, b"\xde\xad\xbe\xef" * 5, "Repeated pattern"), + (9, b"\x01" * 16, b"\x01", "Single byte varbinary"), + (10, b"\x80" * 16, b"\x80\x90\xa0\xb0\xc0\xd0\xe0\xf0", "High-bit bytes"), ] - + print(f"\nPerforming bulk copy with {len(test_data)} rows using cursor.bulkcopy()...") print("Testing BINARY and VARBINARY data types with edge cases...") - + # Perform bulk copy via cursor result = cursor.bulkcopy( table_name=table_name, @@ -69,69 +75,81 @@ def test_binary_varbinary_bulkcopy(): (1, "binary_data"), (2, "varbinary_data"), (3, "description"), - ] + ], ) - + print(f"\nBulk copy completed successfully!") print(f" Rows copied: {result['rows_copied']}") print(f" Batch count: {result['batch_count']}") print(f" Elapsed time: {result['elapsed_time']}") - + # Assertions - assert result['rows_copied'] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" - assert result['batch_count'] == 2, f"Expected 2 batches, got {result['batch_count']}" - + assert result["rows_copied"] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" + assert result["batch_count"] == 2, f"Expected 2 batches, got {result['batch_count']}" + # Verify the data print(f"\nVerifying inserted data...") - cursor.execute(f"SELECT id, binary_data, varbinary_data, description FROM {table_name} ORDER BY id") + cursor.execute( + f"SELECT id, binary_data, varbinary_data, description FROM {table_name} ORDER BY id" + ) rows = cursor.fetchall() - + print(f"Retrieved {len(rows)} rows:") assert len(rows) == 10, f"Expected 10 rows retrieved, got {len(rows)}" - + for i, row in enumerate(rows): - print(f" ID: {row[0]}, BINARY: {row[1].hex() if row[1] else 'NULL'}, " + - f"VARBINARY: {row[2].hex() if row[2] else 'NULL'}, Description: {row[3]}") - + print( + f" ID: {row[0]}, BINARY: {row[1].hex() if row[1] else 'NULL'}, " + + f"VARBINARY: {row[2].hex() if row[2] else 'NULL'}, Description: {row[3]}" + ) + assert row[0] == test_data[i][0], f"ID mismatch at row {i}" - + # BINARY comparison - SQL Server pads with zeros to fixed length - expected_binary = test_data[i][1] if len(test_data[i][1]) == 16 else test_data[i][1] + b'\x00' * (16 - len(test_data[i][1])) - assert row[1] == expected_binary, f"BINARY mismatch at row {i}: expected {expected_binary.hex()}, got {row[1].hex()}" - + expected_binary = ( + test_data[i][1] + if len(test_data[i][1]) == 16 + else test_data[i][1] + b"\x00" * (16 - len(test_data[i][1])) + ) + assert ( + row[1] == expected_binary + ), f"BINARY mismatch at row {i}: expected {expected_binary.hex()}, got {row[1].hex()}" + # VARBINARY comparison - exact match expected - assert row[2] == test_data[i][2], f"VARBINARY mismatch at row {i}: expected {test_data[i][2].hex()}, got {row[2].hex()}" - + assert ( + row[2] == test_data[i][2] + ), f"VARBINARY mismatch at row {i}: expected {test_data[i][2].hex()}, got {row[2].hex()}" + assert row[3] == test_data[i][3], f"Description mismatch at row {i}" - + # Additional verification for specific cases print("\nVerifying specific edge cases...") - + # Empty varbinary cursor.execute(f"SELECT varbinary_data FROM {table_name} WHERE id = 1") empty_varbinary = cursor.fetchone()[0] - assert empty_varbinary == b'', f"Empty varbinary verification failed" + assert empty_varbinary == b"", f"Empty varbinary verification failed" print(f" ✓ Empty varbinary verified: length = {len(empty_varbinary)}") - + # Max varbinary length cursor.execute(f"SELECT varbinary_data FROM {table_name} WHERE id = 7") max_varbinary = cursor.fetchone()[0] assert len(max_varbinary) == 100, f"Max varbinary length verification failed" - assert max_varbinary == b'\x00' * 100, f"Max varbinary content verification failed" + assert max_varbinary == b"\x00" * 100, f"Max varbinary content verification failed" print(f" ✓ Max varbinary length verified: {len(max_varbinary)} bytes") - + # All 0xFF bytes cursor.execute(f"SELECT binary_data, varbinary_data FROM {table_name} WHERE id = 3") all_ff_row = cursor.fetchone() - assert all_ff_row[0] == b'\xFF' * 16, f"All 0xFF BINARY verification failed" - assert all_ff_row[1] == b'\xFF' * 16, f"All 0xFF VARBINARY verification failed" + assert all_ff_row[0] == b"\xff" * 16, f"All 0xFF BINARY verification failed" + assert all_ff_row[1] == b"\xff" * 16, f"All 0xFF VARBINARY verification failed" print(f" ✓ All 0xFF bytes verified for both BINARY and VARBINARY") - + # Cleanup print(f"\nCleaning up test table...") cursor.execute(f"DROP TABLE {table_name}") conn.commit() - + # Close cursor and connection cursor.close() conn.close() diff --git a/TestsBCP/test_bit_BCP.py b/TestsBCP/test_bit_BCP.py index f3f3755f..42813e2c 100644 --- a/TestsBCP/test_bit_BCP.py +++ b/TestsBCP/test_bit_BCP.py @@ -13,23 +13,24 @@ def test_bit_bulkcopy(): # Get connection string from environment conn_str = os.getenv("DB_CONNECTION_STRING") assert conn_str is not None, "DB_CONNECTION_STRING environment variable must be set" - + print(f"Connection string length: {len(conn_str)}") - + # Connect using the regular mssql_python connection conn = connect(conn_str) print(f"Connection created: {type(conn)}") - + # Create cursor cursor = conn.cursor() print(f"Cursor created: {type(cursor)}") - + # Create a test table with BIT columns table_name = "BulkCopyBitTest" - + print(f"\nCreating test table: {table_name}") cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f""" + cursor.execute( + f""" CREATE TABLE {table_name} ( id INT, bit_value BIT, @@ -37,10 +38,11 @@ def test_bit_bulkcopy(): is_deleted BIT, description VARCHAR(100) ) - """) + """ + ) conn.commit() print("Test table created successfully") - + # Prepare test data with various BIT values # BIT can be 0, 1, True, False, or NULL test_data = [ @@ -55,10 +57,10 @@ def test_bit_bulkcopy(): (9, 1, True, 0, "Mixed 1-True-0"), (10, False, 1, True, "Mixed False-1-True"), ] - + print(f"\nPerforming bulk copy with {len(test_data)} rows using cursor.bulkcopy()...") print("Testing BIT data type with True/False and 0/1 values...") - + # Perform bulk copy via cursor result = cursor.bulkcopy( table_name=table_name, @@ -71,26 +73,28 @@ def test_bit_bulkcopy(): (2, "is_active"), (3, "is_deleted"), (4, "description"), - ] + ], ) - + print(f"\nBulk copy completed successfully!") print(f" Rows copied: {result['rows_copied']}") print(f" Batch count: {result['batch_count']}") print(f" Elapsed time: {result['elapsed_time']}") - + # Assertions - assert result['rows_copied'] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" - assert result['batch_count'] == 2, f"Expected 2 batches, got {result['batch_count']}" - + assert result["rows_copied"] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" + assert result["batch_count"] == 2, f"Expected 2 batches, got {result['batch_count']}" + # Verify the data print(f"\nVerifying inserted data...") - cursor.execute(f"SELECT id, bit_value, is_active, is_deleted, description FROM {table_name} ORDER BY id") + cursor.execute( + f"SELECT id, bit_value, is_active, is_deleted, description FROM {table_name} ORDER BY id" + ) rows = cursor.fetchall() - + print(f"Retrieved {len(rows)} rows:") assert len(rows) == 10, f"Expected 10 rows retrieved, got {len(rows)}" - + # Expected values after conversion (SQL Server stores BIT as 0 or 1) expected_values = [ (1, False, False, False, "All zeros (False)"), @@ -104,48 +108,60 @@ def test_bit_bulkcopy(): (9, True, True, False, "Mixed 1-True-0"), (10, False, True, True, "Mixed False-1-True"), ] - + for i, row in enumerate(rows): - print(f" ID: {row[0]}, BIT: {row[1]}, IS_ACTIVE: {row[2]}, IS_DELETED: {row[3]}, Description: {row[4]}") - + print( + f" ID: {row[0]}, BIT: {row[1]}, IS_ACTIVE: {row[2]}, IS_DELETED: {row[3]}, Description: {row[4]}" + ) + assert row[0] == expected_values[i][0], f"ID mismatch at row {i}" - assert row[1] == expected_values[i][1], f"BIT value mismatch at row {i}: expected {expected_values[i][1]}, got {row[1]}" - assert row[2] == expected_values[i][2], f"IS_ACTIVE mismatch at row {i}: expected {expected_values[i][2]}, got {row[2]}" - assert row[3] == expected_values[i][3], f"IS_DELETED mismatch at row {i}: expected {expected_values[i][3]}, got {row[3]}" + assert ( + row[1] == expected_values[i][1] + ), f"BIT value mismatch at row {i}: expected {expected_values[i][1]}, got {row[1]}" + assert ( + row[2] == expected_values[i][2] + ), f"IS_ACTIVE mismatch at row {i}: expected {expected_values[i][2]}, got {row[2]}" + assert ( + row[3] == expected_values[i][3] + ), f"IS_DELETED mismatch at row {i}: expected {expected_values[i][3]}, got {row[3]}" assert row[4] == expected_values[i][4], f"Description mismatch at row {i}" - + # Additional verification for specific cases print("\nVerifying specific edge cases...") - + # All False values cursor.execute(f"SELECT bit_value, is_active, is_deleted FROM {table_name} WHERE id = 1") all_false = cursor.fetchone() - assert all_false[0] == False and all_false[1] == False and all_false[2] == False, f"All False verification failed" + assert ( + all_false[0] == False and all_false[1] == False and all_false[2] == False + ), f"All False verification failed" print(f" ✓ All False values verified: {all_false}") - + # All True values cursor.execute(f"SELECT bit_value, is_active, is_deleted FROM {table_name} WHERE id = 2") all_true = cursor.fetchone() - assert all_true[0] == True and all_true[1] == True and all_true[2] == True, f"All True verification failed" + assert ( + all_true[0] == True and all_true[1] == True and all_true[2] == True + ), f"All True verification failed" print(f" ✓ All True values verified: {all_true}") - + # Count True values cursor.execute(f"SELECT COUNT(*) FROM {table_name} WHERE bit_value = 1") true_count = cursor.fetchone()[0] print(f" ✓ Count of True bit_value: {true_count}") - + # Count False values cursor.execute(f"SELECT COUNT(*) FROM {table_name} WHERE bit_value = 0") false_count = cursor.fetchone()[0] print(f" ✓ Count of False bit_value: {false_count}") - + assert true_count + false_count == 10, f"Total count mismatch" - + # Cleanup print(f"\nCleaning up test table...") cursor.execute(f"DROP TABLE {table_name}") conn.commit() - + # Close cursor and connection cursor.close() conn.close() diff --git a/TestsBCP/test_column_mismatch_BCP.py b/TestsBCP/test_column_mismatch_BCP.py index b8dd8cd5..7884135e 100644 --- a/TestsBCP/test_column_mismatch_BCP.py +++ b/TestsBCP/test_column_mismatch_BCP.py @@ -15,19 +15,15 @@ @pytest.mark.integration def test_bulkcopy_more_columns_than_table(cursor): """Test bulk copy where source has more columns than the target table. - + The extra columns should be dropped and the bulk copy should succeed. Only the columns specified in column_mappings should be inserted. """ - + # Create a test table with 3 INT columns table_name = "BulkCopyMoreColumnsTest" - cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) - cursor.execute( - f"CREATE TABLE {table_name} (id INT PRIMARY KEY, value1 INT, value2 INT)" - ) + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") + cursor.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, value1 INT, value2 INT)") cursor.connection.commit() # Source data has 5 columns, but table only has 3 @@ -46,11 +42,11 @@ def test_bulkcopy_more_columns_than_table(cursor): batch_size=1000, timeout=30, column_mappings=[ - (0, "id"), # Map source column 0 to 'id' - (1, "value1"), # Map source column 1 to 'value1' - (2, "value2"), # Map source column 2 to 'value2' + (0, "id"), # Map source column 0 to 'id' + (1, "value1"), # Map source column 1 to 'value1' + (2, "value2"), # Map source column 2 to 'value2' # Columns 3 and 4 are NOT mapped, so they're dropped - ] + ], ) # Verify results @@ -76,19 +72,15 @@ def test_bulkcopy_more_columns_than_table(cursor): @pytest.mark.integration def test_bulkcopy_fewer_columns_than_table(cursor): """Test bulk copy where source has fewer columns than the target table. - + Missing columns should be filled with NULL (if nullable). The bulk copy should succeed. """ - + # Create a test table with 3 INT columns (value2 is nullable) table_name = "BulkCopyFewerColumnsTest" - cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) - cursor.execute( - f"CREATE TABLE {table_name} (id INT PRIMARY KEY, value1 INT, value2 INT NULL)" - ) + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") + cursor.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, value1 INT, value2 INT NULL)") cursor.connection.commit() # Source data has only 2 columns (id, value1) - missing 'value2' @@ -107,10 +99,10 @@ def test_bulkcopy_fewer_columns_than_table(cursor): batch_size=1000, timeout=30, column_mappings=[ - (0, "id"), # Map source column 0 to 'id' - (1, "value1"), # Map source column 1 to 'value1' + (0, "id"), # Map source column 0 to 'id' + (1, "value1"), # Map source column 1 to 'value1' # 'value2' is not mapped, should be NULL - ] + ], ) # Verify results @@ -136,19 +128,15 @@ def test_bulkcopy_fewer_columns_than_table(cursor): @pytest.mark.integration def test_bulkcopy_auto_mapping_with_extra_columns(cursor): """Test bulk copy with auto-mapping when source has more columns than table. - + Without explicit column_mappings, auto-mapping should use the first N columns where N is the number of columns in the target table. Extra source columns are ignored. """ - + # Create a test table with 3 INT columns table_name = "BulkCopyAutoMapExtraTest" - cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) - cursor.execute( - f"CREATE TABLE {table_name} (id INT, value1 INT, value2 INT)" - ) + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") + cursor.execute(f"CREATE TABLE {table_name} (id INT, value1 INT, value2 INT)") cursor.connection.commit() # Source data has 5 columns, table has 3 @@ -161,12 +149,7 @@ def test_bulkcopy_auto_mapping_with_extra_columns(cursor): # Execute bulk copy WITHOUT explicit column mappings # Auto-mapping should map first 3 columns to table's 3 columns - result = cursor.bulkcopy( - table_name=table_name, - data=data, - batch_size=1000, - timeout=30 - ) + result = cursor.bulkcopy(table_name=table_name, data=data, batch_size=1000, timeout=30) # Verify results assert result is not None @@ -189,15 +172,13 @@ def test_bulkcopy_auto_mapping_with_extra_columns(cursor): @pytest.mark.integration def test_bulkcopy_fewer_columns_with_defaults(cursor): """Test bulk copy where missing columns have default values. - + Missing columns should use their default values instead of NULL. """ - + # Create a test table with default values table_name = "BulkCopyFewerColumnsDefaultTest" - cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") cursor.execute( f"""CREATE TABLE {table_name} ( id INT PRIMARY KEY, @@ -225,7 +206,7 @@ def test_bulkcopy_fewer_columns_with_defaults(cursor): (0, "id"), (1, "value1"), # value2 and status not mapped - should use defaults - ] + ], ) # Verify results @@ -236,9 +217,9 @@ def test_bulkcopy_fewer_columns_with_defaults(cursor): rows = cursor.fetchall() assert len(rows) == 3 - assert rows[0][0] == 1 and rows[0][1] == 100 and rows[0][2] == 999 and rows[0][3] == 'active' - assert rows[1][0] == 2 and rows[1][1] == 200 and rows[1][2] == 999 and rows[1][3] == 'active' - assert rows[2][0] == 3 and rows[2][1] == 300 and rows[2][2] == 999 and rows[2][3] == 'active' + assert rows[0][0] == 1 and rows[0][1] == 100 and rows[0][2] == 999 and rows[0][3] == "active" + assert rows[1][0] == 2 and rows[1][1] == 200 and rows[1][2] == 999 and rows[1][3] == "active" + assert rows[2][0] == 3 and rows[2][1] == 300 and rows[2][2] == 999 and rows[2][3] == "active" # Cleanup cursor.execute(f"DROP TABLE {table_name}") @@ -248,15 +229,13 @@ def test_bulkcopy_fewer_columns_with_defaults(cursor): @pytest.mark.integration def test_bulkcopy_column_reordering(cursor): """Test bulk copy with column reordering. - + Source columns can be mapped to target columns in different order. """ - + # Create a test table table_name = "BulkCopyColumnReorderTest" - cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") cursor.execute( f"CREATE TABLE {table_name} (id INT, name VARCHAR(50), age INT, city VARCHAR(50))" ) @@ -276,11 +255,11 @@ def test_bulkcopy_column_reordering(cursor): batch_size=1000, timeout=30, column_mappings=[ - (3, "id"), # Source column 3 (id) → Target id - (0, "name"), # Source column 0 (name) → Target name - (1, "age"), # Source column 1 (age) → Target age - (2, "city"), # Source column 2 (city) → Target city - ] + (3, "id"), # Source column 3 (id) → Target id + (0, "name"), # Source column 0 (name) → Target name + (1, "age"), # Source column 1 (age) → Target age + (2, "city"), # Source column 2 (city) → Target city + ], ) # Verify results @@ -293,7 +272,9 @@ def test_bulkcopy_column_reordering(cursor): assert len(rows) == 3 assert rows[0][0] == 1 and rows[0][1] == "Alice" and rows[0][2] == 30 and rows[0][3] == "NYC" assert rows[1][0] == 2 and rows[1][1] == "Bob" and rows[1][2] == 25 and rows[1][3] == "LA" - assert rows[2][0] == 3 and rows[2][1] == "Carol" and rows[2][2] == 35 and rows[2][3] == "Chicago" + assert ( + rows[2][0] == 3 and rows[2][1] == "Carol" and rows[2][2] == 35 and rows[2][3] == "Chicago" + ) # Cleanup cursor.execute(f"DROP TABLE {table_name}") diff --git a/TestsBCP/test_cursor_bulkcopy.py b/TestsBCP/test_cursor_bulkcopy.py index 5ef74a26..a27cc14f 100644 --- a/TestsBCP/test_cursor_bulkcopy.py +++ b/TestsBCP/test_cursor_bulkcopy.py @@ -13,26 +13,26 @@ def test_cursor_bulkcopy(): # Get connection string from environment conn_str = os.getenv("DB_CONNECTION_STRING") assert conn_str is not None, "DB_CONNECTION_STRING environment variable must be set" - + print(f"Connection string length: {len(conn_str)}") - + # Connect using the regular mssql_python connection conn = connect(conn_str) print(f"Connection created: {type(conn)}") - + # Create cursor cursor = conn.cursor() print(f"Cursor created: {type(cursor)}") - + # Create a test table table_name = "BulkCopyCursorTest" - + print(f"\nCreating test table: {table_name}") cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") cursor.execute(f"CREATE TABLE {table_name} (id INT, name VARCHAR(50), amount DECIMAL(10,2))") conn.commit() print("Test table created successfully") - + # Prepare test data test_data = [ (1, "Product A", 99.99), @@ -46,9 +46,9 @@ def test_cursor_bulkcopy(): (9, "Product I", 499.99), (10, "Product J", 549.50), ] - + print(f"\nPerforming bulk copy with {len(test_data)} rows using cursor.bulkcopy()...") - + # Perform bulk copy via cursor result = cursor.bulkcopy( table_name=table_name, @@ -59,37 +59,37 @@ def test_cursor_bulkcopy(): (0, "id"), (1, "name"), (2, "amount"), - ] + ], ) - + print(f"\nBulk copy completed successfully!") print(f" Rows copied: {result['rows_copied']}") print(f" Batch count: {result['batch_count']}") print(f" Elapsed time: {result['elapsed_time']}") - + # Assertions - assert result['rows_copied'] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" - assert result['batch_count'] == 2, f"Expected 2 batches, got {result['batch_count']}" - + assert result["rows_copied"] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" + assert result["batch_count"] == 2, f"Expected 2 batches, got {result['batch_count']}" + # Verify the data print(f"\nVerifying inserted data...") cursor.execute(f"SELECT id, name, amount FROM {table_name} ORDER BY id") rows = cursor.fetchall() - + print(f"Retrieved {len(rows)} rows:") assert len(rows) == 10, f"Expected 10 rows retrieved, got {len(rows)}" - + for i, row in enumerate(rows): print(f" ID: {row[0]}, Name: {row[1]}, Amount: {row[2]}") assert row[0] == test_data[i][0], f"ID mismatch at row {i}" assert row[1] == test_data[i][1], f"Name mismatch at row {i}" assert float(row[2]) == test_data[i][2], f"Amount mismatch at row {i}" - + # Cleanup print(f"\nCleaning up test table...") cursor.execute(f"DROP TABLE {table_name}") conn.commit() - + # Close cursor and connection cursor.close() conn.close() diff --git a/TestsBCP/test_date_BCP.py b/TestsBCP/test_date_BCP.py index 55c9ef80..545b0dca 100644 --- a/TestsBCP/test_date_BCP.py +++ b/TestsBCP/test_date_BCP.py @@ -1,4 +1,5 @@ """Bulk copy tests for DATE data type.""" + import pytest import datetime @@ -6,12 +7,10 @@ @pytest.mark.integration def test_cursor_bulkcopy_date_basic(cursor): """Test cursor bulkcopy method with two date columns and explicit mappings.""" - + # Create a test table with two date columns table_name = "BulkCopyTestTableDate" - cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") cursor.execute(f"CREATE TABLE {table_name} (event_date DATE, birth_date DATE)") cursor.connection.commit() @@ -31,7 +30,7 @@ def test_cursor_bulkcopy_date_basic(cursor): column_mappings=[ (0, "event_date"), (1, "birth_date"), - ] + ], ) # Verify results @@ -59,12 +58,10 @@ def test_cursor_bulkcopy_date_auto_mapping(cursor): Tests bulkcopy when no mappings are specified, including NULL value handling. """ - + # Create a test table with two nullable date columns table_name = "BulkCopyAutoMapTableDate" - cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") cursor.execute(f"CREATE TABLE {table_name} (event_date DATE, birth_date DATE)") cursor.connection.commit() @@ -77,12 +74,7 @@ def test_cursor_bulkcopy_date_auto_mapping(cursor): ] # Execute bulk copy WITHOUT column mappings - should auto-generate - result = cursor.bulkcopy( - table_name=table_name, - data=data, - batch_size=1000, - timeout=30 - ) + result = cursor.bulkcopy(table_name=table_name, data=data, batch_size=1000, timeout=30) # Verify results assert result is not None @@ -91,7 +83,9 @@ def test_cursor_bulkcopy_date_auto_mapping(cursor): assert "elapsed_time" in result # Verify data including NULLs - cursor.execute(f"SELECT event_date, birth_date FROM {table_name} ORDER BY COALESCE(event_date, '9999-12-31')") + cursor.execute( + f"SELECT event_date, birth_date FROM {table_name} ORDER BY COALESCE(event_date, '9999-12-31')" + ) rows = cursor.fetchall() assert len(rows) == 4 assert rows[0][0] == datetime.date(2020, 1, 15) and rows[0][1] == datetime.date(1990, 5, 20) @@ -111,12 +105,10 @@ def test_cursor_bulkcopy_date_string_to_date_conversion(cursor): Tests type coercion when source data contains date strings but destination columns are DATE type. """ - + # Create a test table with two date columns table_name = "BulkCopyStringToDateTable" - cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") cursor.execute(f"CREATE TABLE {table_name} (event_date DATE, birth_date DATE)") cursor.connection.commit() @@ -128,12 +120,7 @@ def test_cursor_bulkcopy_date_string_to_date_conversion(cursor): ] # Execute bulk copy without explicit mappings - result = cursor.bulkcopy( - table_name=table_name, - data=data, - batch_size=1000, - timeout=30 - ) + result = cursor.bulkcopy(table_name=table_name, data=data, batch_size=1000, timeout=30) # Verify results assert result is not None @@ -157,23 +144,21 @@ def test_cursor_bulkcopy_date_string_to_date_conversion(cursor): @pytest.mark.integration def test_cursor_bulkcopy_date_boundary_values(cursor): """Test cursor bulkcopy with DATE boundary values.""" - + # Create a test table table_name = "BulkCopyDateBoundaryTest" - cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") cursor.execute(f"CREATE TABLE {table_name} (id INT, test_date DATE)") cursor.connection.commit() # Test data with boundary values # DATE range: 0001-01-01 to 9999-12-31 data = [ - (1, datetime.date(1, 1, 1)), # Min DATE - (2, datetime.date(9999, 12, 31)), # Max DATE - (3, datetime.date(2000, 1, 1)), # Y2K - (4, datetime.date(1900, 1, 1)), # Century boundary - (5, datetime.date(2024, 2, 29)), # Leap year + (1, datetime.date(1, 1, 1)), # Min DATE + (2, datetime.date(9999, 12, 31)), # Max DATE + (3, datetime.date(2000, 1, 1)), # Y2K + (4, datetime.date(1900, 1, 1)), # Century boundary + (5, datetime.date(2024, 2, 29)), # Leap year ] # Execute bulk copy @@ -185,7 +170,7 @@ def test_cursor_bulkcopy_date_boundary_values(cursor): column_mappings=[ (0, "id"), (1, "test_date"), - ] + ], ) # Verify results @@ -196,11 +181,11 @@ def test_cursor_bulkcopy_date_boundary_values(cursor): cursor.execute(f"SELECT id, test_date FROM {table_name} ORDER BY id") rows = cursor.fetchall() assert len(rows) == 5 - assert rows[0][1] == datetime.date(1, 1, 1) # Min DATE - assert rows[1][1] == datetime.date(9999, 12, 31) # Max DATE - assert rows[2][1] == datetime.date(2000, 1, 1) # Y2K - assert rows[3][1] == datetime.date(1900, 1, 1) # Century boundary - assert rows[4][1] == datetime.date(2024, 2, 29) # Leap year + assert rows[0][1] == datetime.date(1, 1, 1) # Min DATE + assert rows[1][1] == datetime.date(9999, 12, 31) # Max DATE + assert rows[2][1] == datetime.date(2000, 1, 1) # Y2K + assert rows[3][1] == datetime.date(1900, 1, 1) # Century boundary + assert rows[4][1] == datetime.date(2024, 2, 29) # Leap year # Cleanup cursor.execute(f"DROP TABLE {table_name}") @@ -210,12 +195,10 @@ def test_cursor_bulkcopy_date_boundary_values(cursor): @pytest.mark.integration def test_cursor_bulkcopy_date_large_batch(cursor): """Test cursor bulkcopy with a large number of DATE rows.""" - + # Create a test table table_name = "BulkCopyDateLargeBatchTest" - cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") cursor.execute(f"CREATE TABLE {table_name} (id INT, test_date DATE)") cursor.connection.commit() @@ -232,7 +215,7 @@ def test_cursor_bulkcopy_date_large_batch(cursor): column_mappings=[ (0, "id"), (1, "test_date"), - ] + ], ) # Verify results diff --git a/TestsBCP/test_datetime_BCP.py b/TestsBCP/test_datetime_BCP.py index 4c2bcb63..af857a5f 100644 --- a/TestsBCP/test_datetime_BCP.py +++ b/TestsBCP/test_datetime_BCP.py @@ -1,4 +1,5 @@ """Bulk copy tests for DATETIME data type.""" + import pytest import datetime @@ -8,9 +9,7 @@ def test_cursor_bulkcopy_datetime_basic(cursor): """Test cursor bulkcopy method with two datetime columns and explicit mappings.""" # Create a test table with two datetime columns table_name = "BulkCopyTestTableDateTime" - cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") cursor.execute(f"CREATE TABLE {table_name} (start_datetime DATETIME, end_datetime DATETIME)") cursor.connection.commit() @@ -49,15 +48,15 @@ def test_cursor_bulkcopy_datetime_basic(cursor): cursor.execute(f"SELECT start_datetime, end_datetime FROM {table_name} ORDER BY start_datetime") rows = cursor.fetchall() assert len(rows) == 3 - + # Verify first row assert rows[0][0] == datetime.datetime(2024, 1, 15, 9, 30, 0) assert rows[0][1] == datetime.datetime(2024, 1, 15, 17, 45, 30) - + # Verify second row assert rows[1][0] == datetime.datetime(2024, 2, 20, 8, 15, 45) assert rows[1][1] == datetime.datetime(2024, 2, 20, 16, 30, 15) - + # Verify third row assert rows[2][0] == datetime.datetime(2024, 3, 10, 10, 0, 0) assert rows[2][1] == datetime.datetime(2024, 3, 10, 18, 0, 0) @@ -75,9 +74,7 @@ def test_cursor_bulkcopy_datetime_auto_mapping(cursor): """ # Create a test table with two nullable datetime columns table_name = "BulkCopyAutoMapTableDateTime" - cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") cursor.execute(f"CREATE TABLE {table_name} (start_datetime DATETIME, end_datetime DATETIME)") cursor.connection.commit() @@ -90,12 +87,7 @@ def test_cursor_bulkcopy_datetime_auto_mapping(cursor): ] # Execute bulk copy WITHOUT column mappings - should auto-generate - result = cursor.bulkcopy( - table_name, - data, - batch_size=1000, - timeout=30 - ) + result = cursor.bulkcopy(table_name, data, batch_size=1000, timeout=30) # Verify results assert result is not None @@ -110,14 +102,16 @@ def test_cursor_bulkcopy_datetime_auto_mapping(cursor): assert count == 4 # Verify NULL handling - cursor.execute(f"SELECT start_datetime, end_datetime FROM {table_name} ORDER BY ISNULL(start_datetime, '1900-01-01')") + cursor.execute( + f"SELECT start_datetime, end_datetime FROM {table_name} ORDER BY ISNULL(start_datetime, '1900-01-01')" + ) rows = cursor.fetchall() assert len(rows) == 4 - + # Verify NULL value in first column (third row after sorting) assert rows[0][0] is None assert rows[0][1] == datetime.datetime(2024, 2, 20, 16, 30, 15) - + # Verify NULL value in second column assert rows[2][0] == datetime.datetime(2024, 2, 20, 8, 15, 45) assert rows[2][1] is None @@ -136,9 +130,7 @@ def test_cursor_bulkcopy_datetime_string_to_datetime_conversion(cursor): """ # Create a test table with two datetime columns table_name = "BulkCopyStringToDateTimeTable" - cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") cursor.execute(f"CREATE TABLE {table_name} (start_datetime DATETIME, end_datetime DATETIME)") cursor.connection.commit() @@ -150,12 +142,7 @@ def test_cursor_bulkcopy_datetime_string_to_datetime_conversion(cursor): ] # Execute bulk copy without explicit mappings - result = cursor.bulkcopy( - table_name, - data, - batch_size=1000, - timeout=30 - ) + result = cursor.bulkcopy(table_name, data, batch_size=1000, timeout=30) # Verify results assert result is not None @@ -173,15 +160,15 @@ def test_cursor_bulkcopy_datetime_string_to_datetime_conversion(cursor): cursor.execute(f"SELECT start_datetime, end_datetime FROM {table_name} ORDER BY start_datetime") rows = cursor.fetchall() assert len(rows) == 3 - + # Verify first row assert rows[0][0] == datetime.datetime(2024, 1, 15, 9, 30, 0) assert rows[0][1] == datetime.datetime(2024, 1, 15, 17, 45, 30) - + # Verify second row assert rows[1][0] == datetime.datetime(2024, 2, 20, 8, 15, 45) assert rows[1][1] == datetime.datetime(2024, 2, 20, 16, 30, 15) - + # Verify third row assert rows[2][0] == datetime.datetime(2024, 3, 10, 10, 0, 0) assert rows[2][1] == datetime.datetime(2024, 3, 10, 18, 0, 0) @@ -194,14 +181,12 @@ def test_cursor_bulkcopy_datetime_string_to_datetime_conversion(cursor): @pytest.mark.integration def test_cursor_bulkcopy_datetime_boundary_values(cursor): """Test cursor bulkcopy with DATETIME boundary values. - + DATETIME range: 1753-01-01 00:00:00 to 9999-12-31 23:59:59.997 Precision: Rounded to increments of .000, .003, or .007 seconds """ table_name = "BulkCopyDateTimeBoundaryTable" - cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") cursor.execute(f"CREATE TABLE {table_name} (dt_value DATETIME)") cursor.connection.commit() @@ -216,12 +201,7 @@ def test_cursor_bulkcopy_datetime_boundary_values(cursor): ] # Execute bulk copy - result = cursor.bulkcopy( - table_name, - data, - batch_size=1000, - timeout=30 - ) + result = cursor.bulkcopy(table_name, data, batch_size=1000, timeout=30) # Verify results assert result is not None @@ -232,22 +212,22 @@ def test_cursor_bulkcopy_datetime_boundary_values(cursor): cursor.execute(f"SELECT dt_value FROM {table_name} ORDER BY dt_value") rows = cursor.fetchall() assert len(rows) == 6 - + # Verify minimum datetime assert rows[0][0] == datetime.datetime(1753, 1, 1, 0, 0, 0) - + # Verify pre-Y2K assert rows[1][0] == datetime.datetime(1999, 12, 31, 23, 59, 59) - + # Verify Y2K assert rows[2][0] == datetime.datetime(2000, 1, 1, 0, 0, 0) - + # Verify leap year assert rows[3][0] == datetime.datetime(2024, 2, 29, 12, 0, 0) - + # Verify end of year assert rows[4][0] == datetime.datetime(2024, 12, 31, 23, 59, 59) - + # Verify maximum datetime assert rows[5][0] == datetime.datetime(9999, 12, 31, 23, 59, 59) @@ -259,37 +239,42 @@ def test_cursor_bulkcopy_datetime_boundary_values(cursor): @pytest.mark.integration def test_cursor_bulkcopy_datetime_mixed_types(cursor): """Test cursor bulkcopy with DATETIME in a table with mixed column types. - + Verifies that DATETIME columns work correctly alongside other data types. """ table_name = "BulkCopyDateTimeMixedTable" + cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") cursor.execute( - f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}" - ) - cursor.execute(f""" + f""" CREATE TABLE {table_name} ( id INT, created_at DATETIME, is_active BIT, modified_at DATETIME ) - """) + """ + ) cursor.connection.commit() # Test data with mixed types (INT, DATETIME, BIT, DATETIME) data = [ - (1, datetime.datetime(2024, 1, 15, 9, 30, 0), True, datetime.datetime(2024, 1, 15, 10, 0, 0)), - (2, datetime.datetime(2024, 2, 20, 8, 15, 45), False, datetime.datetime(2024, 2, 20, 14, 30, 0)), + ( + 1, + datetime.datetime(2024, 1, 15, 9, 30, 0), + True, + datetime.datetime(2024, 1, 15, 10, 0, 0), + ), + ( + 2, + datetime.datetime(2024, 2, 20, 8, 15, 45), + False, + datetime.datetime(2024, 2, 20, 14, 30, 0), + ), (3, datetime.datetime(2024, 3, 10, 10, 0, 0), True, None), # NULL datetime ] # Execute bulk copy - result = cursor.bulkcopy( - table_name, - data, - batch_size=1000, - timeout=30 - ) + result = cursor.bulkcopy(table_name, data, batch_size=1000, timeout=30) # Verify bulk copy succeeded assert result is not None @@ -298,21 +283,21 @@ def test_cursor_bulkcopy_datetime_mixed_types(cursor): # Verify the data was inserted correctly cursor.execute(f"SELECT id, created_at, is_active, modified_at FROM {table_name} ORDER BY id") rows = cursor.fetchall() - + assert len(rows) == 3 - + # Verify first row assert rows[0][0] == 1 assert rows[0][1] == datetime.datetime(2024, 1, 15, 9, 30, 0) assert rows[0][2] == True assert rows[0][3] == datetime.datetime(2024, 1, 15, 10, 0, 0) - + # Verify second row assert rows[1][0] == 2 assert rows[1][1] == datetime.datetime(2024, 2, 20, 8, 15, 45) assert rows[1][2] == False assert rows[1][3] == datetime.datetime(2024, 2, 20, 14, 30, 0) - + # Verify third row (with NULL datetime) assert rows[2][0] == 3 assert rows[2][1] == datetime.datetime(2024, 3, 10, 10, 0, 0) diff --git a/mssql_python/BCPRustWrapper.py b/mssql_python/BCPRustWrapper.py index 65ad85fe..4518648b 100644 --- a/mssql_python/BCPRustWrapper.py +++ b/mssql_python/BCPRustWrapper.py @@ -8,6 +8,7 @@ try: import mssql_py_core + RUST_CORE_AVAILABLE = True except ImportError: RUST_CORE_AVAILABLE = False @@ -18,13 +19,13 @@ class BCPRustWrapper: """ Wrapper class for Rust-based BCP operations using mssql_py_core. Supports context manager for automatic resource cleanup. - + Example: with BCPRustWrapper(connection_string) as wrapper: wrapper.connect() result = wrapper.bulkcopy('TableName', data) """ - + def __init__(self, connection_string: Optional[str] = None): if not RUST_CORE_AVAILABLE: raise ImportError( @@ -34,55 +35,57 @@ def __init__(self, connection_string: Optional[str] = None): self._core = mssql_py_core self._rust_connection = None self._connection_string = connection_string - + def __enter__(self): """Context manager entry""" return self - + def __exit__(self, exc_type, exc_val, exc_tb): """Context manager exit - ensures connection is closed""" self.close() return False - + def __del__(self): """Destructor - cleanup resources if not already closed""" try: if self._rust_connection is not None: - logger.warning("BCPRustWrapper connection was not explicitly closed, cleaning up in destructor") + logger.warning( + "BCPRustWrapper connection was not explicitly closed, cleaning up in destructor" + ) self.close() except Exception: # Ignore errors during cleanup in destructor pass - + @property def is_connected(self) -> bool: """Check if connection is active""" return self._rust_connection is not None - + def close(self): """Close the connection and cleanup resources""" if self._rust_connection: try: logger.info("Closing Rust connection") # If the connection has a close method, call it - if hasattr(self._rust_connection, 'close'): + if hasattr(self._rust_connection, "close"): self._rust_connection.close() except Exception as e: logger.warning("Error closing connection: %s", str(e)) finally: # Always set to None to prevent reuse self._rust_connection = None - + def connect(self, connection_string: Optional[str] = None): """ Create a connection using the Rust-based PyCoreConnection - + Args: connection_string: SQL Server connection string - + Returns: PyCoreConnection instance - + Raises: ValueError: If connection string is missing or invalid RuntimeError: If connection fails @@ -90,54 +93,64 @@ def connect(self, connection_string: Optional[str] = None): conn_str = connection_string or self._connection_string if not conn_str: raise ValueError("Connection string is required") - + # Close existing connection if any if self._rust_connection: logger.warning("Closing existing connection before creating new one") self.close() - + try: # Parse connection string into dictionary params = {} - for pair in conn_str.split(';'): - if '=' in pair: - key, value = pair.split('=', 1) + for pair in conn_str.split(";"): + if "=" in pair: + key, value = pair.split("=", 1) params[key.strip().lower()] = value.strip() - + # Validate required parameters if not params.get("server"): raise ValueError("SERVER parameter is required in connection string") - + # PyCoreConnection expects a dictionary with specific keys python_client_context = { "server": params.get("server", "localhost"), "database": params.get("database", "master"), "user_name": params.get("uid", ""), "password": params.get("pwd", ""), - "trust_server_certificate": params.get("trustservercertificate", "yes").lower() in ["yes", "true"], + "trust_server_certificate": params.get("trustservercertificate", "yes").lower() + in ["yes", "true"], "encryption": "Optional", } - - logger.info("Attempting to connect to server: %s, database: %s", - python_client_context["server"], python_client_context["database"]) - + + logger.info( + "Attempting to connect to server: %s, database: %s", + python_client_context["server"], + python_client_context["database"], + ) + self._rust_connection = self._core.PyCoreConnection(python_client_context) - + logger.info("Connection established successfully") return self._rust_connection - + except ValueError as ve: logger.error("Connection string validation error: %s", str(ve)) raise except Exception as e: logger.error("Failed to create connection: %s - %s", type(e).__name__, str(e)) raise RuntimeError(f"Connection failed: {str(e)}") from e - - def bulkcopy(self, table_name: str, data: Iterable, batch_size: int = 1000, - timeout: int = 30, column_mappings: Optional[List[Tuple[int, str]]] = None) -> Dict[str, Any]: + + def bulkcopy( + self, + table_name: str, + data: Iterable, + batch_size: int = 1000, + timeout: int = 30, + column_mappings: Optional[List[Tuple[int, str]]] = None, + ) -> Dict[str, Any]: """ Perform bulk copy operation to insert data into SQL Server table. - + Args: table_name: Target table name data: Iterable of tuples/lists containing row data @@ -145,13 +158,13 @@ def bulkcopy(self, table_name: str, data: Iterable, batch_size: int = 1000, timeout: Timeout in seconds (default: 30) column_mappings: List of tuples mapping source column index to target column name e.g., [(0, "id"), (1, "name")] - + Returns: Dictionary with bulk copy results containing: - rows_copied: Number of rows successfully copied - batch_count: Number of batches processed - elapsed_time: Time taken for the operation - + Raises: RuntimeError: If no active connection or cursor creation fails ValueError: If parameters are invalid @@ -159,16 +172,16 @@ def bulkcopy(self, table_name: str, data: Iterable, batch_size: int = 1000, # Validate inputs if not table_name or not isinstance(table_name, str): raise ValueError("table_name must be a non-empty string") - + if batch_size <= 0: raise ValueError(f"batch_size must be positive, got {batch_size}") - + if timeout <= 0: raise ValueError(f"timeout must be positive, got {timeout}") - + if not self._rust_connection: raise RuntimeError("No active connection. Call connect() first.") - + rust_cursor = None try: # Create cursor @@ -176,54 +189,49 @@ def bulkcopy(self, table_name: str, data: Iterable, batch_size: int = 1000, except Exception as e: logger.error("Failed to create cursor: %s - %s", type(e).__name__, str(e)) raise RuntimeError(f"Cursor creation failed: {str(e)}") from e - + try: # Build kwargs for bulkcopy kwargs = { "batch_size": batch_size, "timeout": timeout, } - + if column_mappings: kwargs["column_mappings"] = column_mappings - + # Execute bulk copy with error handling logger.info( "Starting bulk copy to table '%s' - batch_size=%d, timeout=%d", - table_name, batch_size, timeout + table_name, + batch_size, + timeout, ) result = rust_cursor.bulkcopy(table_name, iter(data), kwargs=kwargs) - + logger.info( "Bulk copy completed successfully - rows_copied=%d, batch_count=%d, elapsed_time=%s", - result.get('rows_copied', 0), - result.get('batch_count', 0), - result.get('elapsed_time', 'unknown') + result.get("rows_copied", 0), + result.get("batch_count", 0), + result.get("elapsed_time", "unknown"), ) return result except AttributeError as ae: - logger.error( - "Invalid cursor or method call for table '%s': %s", - table_name, str(ae) - ) + logger.error("Invalid cursor or method call for table '%s': %s", table_name, str(ae)) raise RuntimeError(f"Bulk copy method error: {str(ae)}") from ae except TypeError as te: - logger.error( - "Invalid data type or parameters for table '%s': %s", - table_name, str(te) - ) + logger.error("Invalid data type or parameters for table '%s': %s", table_name, str(te)) raise ValueError(f"Invalid bulk copy parameters: {str(te)}") from te except Exception as e: logger.error( - "Bulk copy failed for table '%s': %s - %s", - table_name, type(e).__name__, str(e) + "Bulk copy failed for table '%s': %s - %s", table_name, type(e).__name__, str(e) ) raise finally: # Always close cursor to prevent resource leak if rust_cursor is not None: try: - if hasattr(rust_cursor, 'close'): + if hasattr(rust_cursor, "close"): rust_cursor.close() logger.debug("Cursor closed successfully") except Exception as e: @@ -233,7 +241,7 @@ def bulkcopy(self, table_name: str, data: Iterable, batch_size: int = 1000, def is_rust_core_available() -> bool: """ Check if the Rust core library is available - + Returns: bool: True if mssql_py_core is installed, False otherwise """ diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index e0848ea7..a4eed416 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2451,14 +2451,20 @@ def nextset(self) -> Union[bool, None]: ) return True - def bulkcopy(self, table_name: str, data, batch_size: int = 1000, - timeout: int = 30, column_mappings: list = None): + def bulkcopy( + self, + table_name: str, + data, + batch_size: int = 1000, + timeout: int = 30, + column_mappings: list = None, + ): """ Perform bulk copy operation using Rust-based implementation. - + This method leverages the mssql_py_core Rust library for high-performance bulk insert operations. - + Args: table_name: Target table name data: Iterable of tuples/lists containing row data @@ -2466,43 +2472,42 @@ def bulkcopy(self, table_name: str, data, batch_size: int = 1000, timeout: Timeout in seconds (default: 30) column_mappings: List of tuples mapping source column index to target column name e.g., [(0, "id"), (1, "name")] - + Returns: Dictionary with bulk copy results containing: - 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 is not installed RuntimeError: If no active connection exists - + Example: >>> cursor = conn.cursor() >>> data = [(1, "Alice"), (2, "Bob")] - >>> result = cursor.bulkcopy("users", data, + >>> result = cursor.bulkcopy("users", data, ... column_mappings=[(0, "id"), (1, "name")]) >>> print(f"Copied {result['rows_copied']} rows") """ from mssql_python.BCPRustWrapper import BCPRustWrapper, is_rust_core_available - + if not is_rust_core_available(): raise ImportError( "Bulk copy requires mssql_py_core Rust library. " "Please install it from the BCPRustWheel directory." ) - + # Get connection string from the connection - if not hasattr(self.connection, 'connection_str'): + if not hasattr(self.connection, "connection_str"): raise RuntimeError( - "Connection string not available. " - "Bulk copy requires connection string access." + "Connection string not available. " "Bulk copy requires connection string access." ) - + # Create wrapper and use the existing connection's connection string wrapper = BCPRustWrapper(self.connection.connection_str) wrapper.connect() - + try: # Perform bulk copy result = wrapper.bulkcopy( @@ -2510,12 +2515,12 @@ def bulkcopy(self, table_name: str, data, batch_size: int = 1000, data=data, batch_size=batch_size, timeout=timeout, - column_mappings=column_mappings + column_mappings=column_mappings, ) return result finally: # Close the wrapper's connection - if wrapper._rust_connection and hasattr(wrapper._rust_connection, 'close'): + if wrapper._rust_connection and hasattr(wrapper._rust_connection, "close"): wrapper._rust_connection.close() def __enter__(self): From 72f6953b0b3f0dccd3bdda68336f740934ce82db Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Wed, 14 Jan 2026 18:54:25 +0000 Subject: [PATCH 04/16] test configuration --- TestsBCP/conftest.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 TestsBCP/conftest.py diff --git a/TestsBCP/conftest.py b/TestsBCP/conftest.py new file mode 100644 index 00000000..14898bbc --- /dev/null +++ b/TestsBCP/conftest.py @@ -0,0 +1,34 @@ +"""Pytest configuration and fixtures for BCP tests.""" +import pytest +import os +import sys + +# Add parent directory to path to import mssql_python +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import mssql_python + + +def get_connection_string(): + """Get connection string from environment variable.""" + conn_str = os.environ.get("DB_CONNECTION_STRING") + if not conn_str: + pytest.skip("DB_CONNECTION_STRING environment variable not set") + return conn_str + + +@pytest.fixture +def connection(): + """Provide a connected database connection.""" + conn_str = get_connection_string() + conn = mssql_python.connect(conn_str) + yield conn + conn.close() + + +@pytest.fixture +def cursor(connection): + """Provide a database cursor.""" + cur = connection.cursor() + yield cur + cur.close() From 1d16f97f6574156e6404e1f81906075687a4ec71 Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Fri, 16 Jan 2026 07:50:45 +0000 Subject: [PATCH 05/16] Refactor the code for simplicity --- mssql_python/BCPRustWrapper.py | 248 --------------------------------- mssql_python/cursor.py | 108 ++++++++------ 2 files changed, 67 insertions(+), 289 deletions(-) delete mode 100644 mssql_python/BCPRustWrapper.py diff --git a/mssql_python/BCPRustWrapper.py b/mssql_python/BCPRustWrapper.py deleted file mode 100644 index 4518648b..00000000 --- a/mssql_python/BCPRustWrapper.py +++ /dev/null @@ -1,248 +0,0 @@ -""" -BCP Rust Wrapper Module -Provides Python interface to the Rust-based mssql_py_core library -""" - -from typing import Optional, List, Tuple, Dict, Any, Iterable -from mssql_python.logging import logger - -try: - import mssql_py_core - - RUST_CORE_AVAILABLE = True -except ImportError: - RUST_CORE_AVAILABLE = False - mssql_py_core = None - - -class BCPRustWrapper: - """ - Wrapper class for Rust-based BCP operations using mssql_py_core. - Supports context manager for automatic resource cleanup. - - Example: - with BCPRustWrapper(connection_string) as wrapper: - wrapper.connect() - result = wrapper.bulkcopy('TableName', data) - """ - - def __init__(self, connection_string: Optional[str] = None): - if not RUST_CORE_AVAILABLE: - raise ImportError( - "mssql_py_core is not installed. " - "Please install it from the BCPRustWheel directory." - ) - self._core = mssql_py_core - self._rust_connection = None - self._connection_string = connection_string - - def __enter__(self): - """Context manager entry""" - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """Context manager exit - ensures connection is closed""" - self.close() - return False - - def __del__(self): - """Destructor - cleanup resources if not already closed""" - try: - if self._rust_connection is not None: - logger.warning( - "BCPRustWrapper connection was not explicitly closed, cleaning up in destructor" - ) - self.close() - except Exception: - # Ignore errors during cleanup in destructor - pass - - @property - def is_connected(self) -> bool: - """Check if connection is active""" - return self._rust_connection is not None - - def close(self): - """Close the connection and cleanup resources""" - if self._rust_connection: - try: - logger.info("Closing Rust connection") - # If the connection has a close method, call it - if hasattr(self._rust_connection, "close"): - self._rust_connection.close() - except Exception as e: - logger.warning("Error closing connection: %s", str(e)) - finally: - # Always set to None to prevent reuse - self._rust_connection = None - - def connect(self, connection_string: Optional[str] = None): - """ - Create a connection using the Rust-based PyCoreConnection - - Args: - connection_string: SQL Server connection string - - Returns: - PyCoreConnection instance - - Raises: - ValueError: If connection string is missing or invalid - RuntimeError: If connection fails - """ - conn_str = connection_string or self._connection_string - if not conn_str: - raise ValueError("Connection string is required") - - # Close existing connection if any - if self._rust_connection: - logger.warning("Closing existing connection before creating new one") - self.close() - - try: - # Parse connection string into dictionary - params = {} - for pair in conn_str.split(";"): - if "=" in pair: - key, value = pair.split("=", 1) - params[key.strip().lower()] = value.strip() - - # Validate required parameters - if not params.get("server"): - raise ValueError("SERVER parameter is required in connection string") - - # PyCoreConnection expects a dictionary with specific keys - python_client_context = { - "server": params.get("server", "localhost"), - "database": params.get("database", "master"), - "user_name": params.get("uid", ""), - "password": params.get("pwd", ""), - "trust_server_certificate": params.get("trustservercertificate", "yes").lower() - in ["yes", "true"], - "encryption": "Optional", - } - - logger.info( - "Attempting to connect to server: %s, database: %s", - python_client_context["server"], - python_client_context["database"], - ) - - self._rust_connection = self._core.PyCoreConnection(python_client_context) - - logger.info("Connection established successfully") - return self._rust_connection - - except ValueError as ve: - logger.error("Connection string validation error: %s", str(ve)) - raise - except Exception as e: - logger.error("Failed to create connection: %s - %s", type(e).__name__, str(e)) - raise RuntimeError(f"Connection failed: {str(e)}") from e - - def bulkcopy( - self, - table_name: str, - data: Iterable, - batch_size: int = 1000, - timeout: int = 30, - column_mappings: Optional[List[Tuple[int, str]]] = None, - ) -> Dict[str, Any]: - """ - Perform bulk copy operation to insert data into SQL Server table. - - Args: - table_name: Target table name - data: Iterable of tuples/lists containing row data - batch_size: Number of rows per batch (default: 1000) - timeout: Timeout in seconds (default: 30) - column_mappings: List of tuples mapping source column index to target column name - e.g., [(0, "id"), (1, "name")] - - Returns: - Dictionary with bulk copy results containing: - - rows_copied: Number of rows successfully copied - - batch_count: Number of batches processed - - elapsed_time: Time taken for the operation - - Raises: - RuntimeError: If no active connection or cursor creation fails - ValueError: If parameters are invalid - """ - # Validate inputs - if not table_name or not isinstance(table_name, str): - raise ValueError("table_name must be a non-empty string") - - if batch_size <= 0: - raise ValueError(f"batch_size must be positive, got {batch_size}") - - if timeout <= 0: - raise ValueError(f"timeout must be positive, got {timeout}") - - if not self._rust_connection: - raise RuntimeError("No active connection. Call connect() first.") - - rust_cursor = None - try: - # Create cursor - rust_cursor = self._rust_connection.cursor() - except Exception as e: - logger.error("Failed to create cursor: %s - %s", type(e).__name__, str(e)) - raise RuntimeError(f"Cursor creation failed: {str(e)}") from e - - try: - # Build kwargs for bulkcopy - kwargs = { - "batch_size": batch_size, - "timeout": timeout, - } - - if column_mappings: - kwargs["column_mappings"] = column_mappings - - # Execute bulk copy with error handling - logger.info( - "Starting bulk copy to table '%s' - batch_size=%d, timeout=%d", - table_name, - batch_size, - timeout, - ) - result = rust_cursor.bulkcopy(table_name, iter(data), kwargs=kwargs) - - logger.info( - "Bulk copy completed successfully - rows_copied=%d, batch_count=%d, elapsed_time=%s", - result.get("rows_copied", 0), - result.get("batch_count", 0), - result.get("elapsed_time", "unknown"), - ) - return result - except AttributeError as ae: - logger.error("Invalid cursor or method call for table '%s': %s", table_name, str(ae)) - raise RuntimeError(f"Bulk copy method error: {str(ae)}") from ae - except TypeError as te: - logger.error("Invalid data type or parameters for table '%s': %s", table_name, str(te)) - raise ValueError(f"Invalid bulk copy parameters: {str(te)}") from te - except Exception as e: - logger.error( - "Bulk copy failed for table '%s': %s - %s", table_name, type(e).__name__, str(e) - ) - raise - finally: - # Always close cursor to prevent resource leak - if rust_cursor is not None: - try: - if hasattr(rust_cursor, "close"): - rust_cursor.close() - logger.debug("Cursor closed successfully") - except Exception as e: - logger.warning("Error closing cursor: %s", str(e)) - - -def is_rust_core_available() -> bool: - """ - Check if the Rust core library is available - - Returns: - bool: True if mssql_py_core is installed, False otherwise - """ - return RUST_CORE_AVAILABLE diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index a4eed416..1d738969 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2451,7 +2451,7 @@ def nextset(self) -> Union[bool, None]: ) return True - def bulkcopy( + def _bulkcopy( self, table_name: str, data, @@ -2462,66 +2462,92 @@ def bulkcopy( """ Perform bulk copy operation using Rust-based implementation. - This method leverages the mssql_py_core Rust library for high-performance - bulk insert operations. - Args: table_name: Target table name data: Iterable of tuples/lists containing row data batch_size: Number of rows per batch (default: 1000) timeout: Timeout in seconds (default: 30) column_mappings: List of tuples mapping source column index to target column name - e.g., [(0, "id"), (1, "name")] Returns: - Dictionary with bulk copy results containing: - - rows_copied: Number of rows successfully copied - - batch_count: Number of batches processed - - elapsed_time: Time taken for the operation + Dictionary with rows_copied, batch_count, and elapsed_time Raises: ImportError: If mssql_py_core is not installed - RuntimeError: If no active connection exists - - Example: - >>> cursor = conn.cursor() - >>> data = [(1, "Alice"), (2, "Bob")] - >>> result = cursor.bulkcopy("users", data, - ... column_mappings=[(0, "id"), (1, "name")]) - >>> print(f"Copied {result['rows_copied']} rows") + ValueError: If parameters are invalid + RuntimeError: If connection string is not available """ - from mssql_python.BCPRustWrapper import BCPRustWrapper, is_rust_core_available - - if not is_rust_core_available(): + try: + import mssql_py_core + except ImportError as exc: raise ImportError( "Bulk copy requires mssql_py_core Rust library. " - "Please install it from the BCPRustWheel directory." - ) - - # Get connection string from the connection + "Install from BCPRustWheel directory." + ) from exc + + # Validate inputs + if not table_name or not isinstance(table_name, str): + raise ValueError("table_name must be a non-empty string") + if batch_size <= 0: + raise ValueError(f"batch_size must be positive, got {batch_size}") + 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. " "Bulk copy requires connection string access." - ) + raise RuntimeError("Connection string not available for bulk copy") - # Create wrapper and use the existing connection's connection string - wrapper = BCPRustWrapper(self.connection.connection_str) - wrapper.connect() + params = { + k.strip().lower(): v.strip() + for pair in self.connection.connection_str.split(";") + if "=" in pair + for k, v in [pair.split("=", 1)] + } + + if not params.get("server"): + raise ValueError("SERVER parameter is required in connection string") + + # Build connection context for Rust library + trust_cert = params.get("trustservercertificate", "yes").lower() in ("yes", "true") + context = { + "server": params.get("server", "localhost"), + "database": params.get("database", "master"), + "user_name": params.get("uid", ""), + "password": params.get("pwd", ""), + "trust_server_certificate": trust_cert, + "encryption": "Optional", + } + logger.debug("Bulk copy connecting to %s/%s", context["server"], context["database"]) + + rust_connection = None + rust_cursor = None try: - # Perform bulk copy - result = wrapper.bulkcopy( - table_name=table_name, - data=data, - batch_size=batch_size, - timeout=timeout, - column_mappings=column_mappings, - ) + rust_connection = mssql_py_core.PyCoreConnection(context) + rust_cursor = rust_connection.cursor() + + kwargs = {"batch_size": batch_size, "timeout": timeout} + if column_mappings: + kwargs["column_mappings"] = column_mappings + + logger.debug("Bulk copy to '%s' - batch_size=%d", table_name, batch_size) + result = rust_cursor.bulkcopy(table_name, iter(data), kwargs=kwargs) + + logger.debug("Bulk copy completed - rows=%d", result.get("rows_copied", 0)) return result + + except Exception as e: + logger.error("Bulk copy failed: %s - %s", type(e).__name__, str(e)) + raise + finally: - # Close the wrapper's connection - if wrapper._rust_connection and hasattr(wrapper._rust_connection, "close"): - wrapper._rust_connection.close() + # Clean up Rust resources + for resource in (rust_cursor, rust_connection): + if resource and hasattr(resource, "close"): + try: + resource.close() + except Exception: + pass def __enter__(self): """ From f8f67372bda0ef234b430fa0dcb148183cbfc697 Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Fri, 16 Jan 2026 07:57:47 +0000 Subject: [PATCH 06/16] romoving the test --- TestsBCP/conftest.py | 34 --- TestsBCP/test_bigint_BCP.py | 118 ---------- TestsBCP/test_binary_BCP.py | 160 -------------- TestsBCP/test_bit_BCP.py | 172 --------------- TestsBCP/test_column_mismatch_BCP.py | 281 ------------------------ TestsBCP/test_cursor_bulkcopy.py | 96 --------- TestsBCP/test_date_BCP.py | 241 --------------------- TestsBCP/test_datetime_BCP.py | 309 --------------------------- 8 files changed, 1411 deletions(-) delete mode 100644 TestsBCP/conftest.py delete mode 100644 TestsBCP/test_bigint_BCP.py delete mode 100644 TestsBCP/test_binary_BCP.py delete mode 100644 TestsBCP/test_bit_BCP.py delete mode 100644 TestsBCP/test_column_mismatch_BCP.py delete mode 100644 TestsBCP/test_cursor_bulkcopy.py delete mode 100644 TestsBCP/test_date_BCP.py delete mode 100644 TestsBCP/test_datetime_BCP.py diff --git a/TestsBCP/conftest.py b/TestsBCP/conftest.py deleted file mode 100644 index 14898bbc..00000000 --- a/TestsBCP/conftest.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Pytest configuration and fixtures for BCP tests.""" -import pytest -import os -import sys - -# Add parent directory to path to import mssql_python -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -import mssql_python - - -def get_connection_string(): - """Get connection string from environment variable.""" - conn_str = os.environ.get("DB_CONNECTION_STRING") - if not conn_str: - pytest.skip("DB_CONNECTION_STRING environment variable not set") - return conn_str - - -@pytest.fixture -def connection(): - """Provide a connected database connection.""" - conn_str = get_connection_string() - conn = mssql_python.connect(conn_str) - yield conn - conn.close() - - -@pytest.fixture -def cursor(connection): - """Provide a database cursor.""" - cur = connection.cursor() - yield cur - cur.close() diff --git a/TestsBCP/test_bigint_BCP.py b/TestsBCP/test_bigint_BCP.py deleted file mode 100644 index f2806ea1..00000000 --- a/TestsBCP/test_bigint_BCP.py +++ /dev/null @@ -1,118 +0,0 @@ -import sys -import os -import pytest - -# Add parent directory to path to import mssql_python -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from mssql_python import connect - - -def test_bigint_bulkcopy(): - """Test bulk copy functionality with BIGINT data type""" - # Get connection string from environment - conn_str = os.getenv("DB_CONNECTION_STRING") - assert conn_str is not None, "DB_CONNECTION_STRING environment variable must be set" - - print(f"Connection string length: {len(conn_str)}") - - # Connect using the regular mssql_python connection - conn = connect(conn_str) - print(f"Connection created: {type(conn)}") - - # Create cursor - cursor = conn.cursor() - print(f"Cursor created: {type(cursor)}") - - # Create a test table with BIGINT columns - table_name = "BulkCopyBigIntTest" - - print(f"\nCreating test table: {table_name}") - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute( - f"CREATE TABLE {table_name} (id INT, bigint_value BIGINT, description VARCHAR(100))" - ) - conn.commit() - print("Test table created successfully") - - # Prepare test data with various BIGINT values - # BIGINT range: -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 - test_data = [ - (1, 0, "Zero"), - (2, 1, "Positive one"), - (3, -1, "Negative one"), - (4, 9223372036854775807, "Max BIGINT value"), - (5, -9223372036854775808, "Min BIGINT value"), - (6, 1000000000000, "One trillion"), - (7, -1000000000000, "Negative one trillion"), - (8, 9223372036854775806, "Near max value"), - (9, -9223372036854775807, "Near min value"), - (10, 123456789012345, "Random large value"), - ] - - print(f"\nPerforming bulk copy with {len(test_data)} rows using cursor.bulkcopy()...") - print("Testing BIGINT data type with edge cases...") - - # Perform bulk copy via cursor - result = cursor.bulkcopy( - table_name=table_name, - data=test_data, - batch_size=5, - timeout=30, - column_mappings=[ - (0, "id"), - (1, "bigint_value"), - (2, "description"), - ], - ) - - print(f"\nBulk copy completed successfully!") - print(f" Rows copied: {result['rows_copied']}") - print(f" Batch count: {result['batch_count']}") - print(f" Elapsed time: {result['elapsed_time']}") - - # Assertions - assert result["rows_copied"] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" - assert result["batch_count"] == 2, f"Expected 2 batches, got {result['batch_count']}" - - # Verify the data - print(f"\nVerifying inserted data...") - cursor.execute(f"SELECT id, bigint_value, description FROM {table_name} ORDER BY id") - rows = cursor.fetchall() - - print(f"Retrieved {len(rows)} rows:") - assert len(rows) == 10, f"Expected 10 rows retrieved, got {len(rows)}" - - for i, row in enumerate(rows): - print(f" ID: {row[0]}, BIGINT Value: {row[1]}, Description: {row[2]}") - assert row[0] == test_data[i][0], f"ID mismatch at row {i}" - assert ( - row[1] == test_data[i][1] - ), f"BIGINT value mismatch at row {i}: expected {test_data[i][1]}, got {row[1]}" - assert row[2] == test_data[i][2], f"Description mismatch at row {i}" - - # Additional verification for edge cases - print("\nVerifying edge case values...") - cursor.execute(f"SELECT bigint_value FROM {table_name} WHERE id = 4") - max_value = cursor.fetchone()[0] - assert max_value == 9223372036854775807, f"Max BIGINT verification failed" - print(f" ✓ Max BIGINT value verified: {max_value}") - - cursor.execute(f"SELECT bigint_value FROM {table_name} WHERE id = 5") - min_value = cursor.fetchone()[0] - assert min_value == -9223372036854775808, f"Min BIGINT verification failed" - print(f" ✓ Min BIGINT value verified: {min_value}") - - # Cleanup - print(f"\nCleaning up test table...") - cursor.execute(f"DROP TABLE {table_name}") - conn.commit() - - # Close cursor and connection - cursor.close() - conn.close() - print("\nTest completed successfully!") - - -if __name__ == "__main__": - test_bigint_bulkcopy() diff --git a/TestsBCP/test_binary_BCP.py b/TestsBCP/test_binary_BCP.py deleted file mode 100644 index 66455018..00000000 --- a/TestsBCP/test_binary_BCP.py +++ /dev/null @@ -1,160 +0,0 @@ -import sys -import os -import pytest - -# Add parent directory to path to import mssql_python -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from mssql_python import connect - - -def test_binary_varbinary_bulkcopy(): - """Test bulk copy functionality with BINARY and VARBINARY data types""" - # Get connection string from environment - conn_str = os.getenv("DB_CONNECTION_STRING") - assert conn_str is not None, "DB_CONNECTION_STRING environment variable must be set" - - print(f"Connection string length: {len(conn_str)}") - - # Connect using the regular mssql_python connection - conn = connect(conn_str) - print(f"Connection created: {type(conn)}") - - # Create cursor - cursor = conn.cursor() - print(f"Cursor created: {type(cursor)}") - - # Create a test table with BINARY and VARBINARY columns - table_name = "BulkCopyBinaryTest" - - print(f"\nCreating test table: {table_name}") - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute( - f""" - CREATE TABLE {table_name} ( - id INT, - binary_data BINARY(16), - varbinary_data VARBINARY(100), - description VARCHAR(100) - ) - """ - ) - conn.commit() - print("Test table created successfully") - - # Prepare test data with various BINARY/VARBINARY values - test_data = [ - (1, b"\x00" * 16, b"", "Empty varbinary"), - (2, b"\x01\x02\x03\x04" + b"\x00" * 12, b"\x01\x02\x03\x04", "Small binary data"), - (3, b"\xff" * 16, b"\xff" * 16, "All 0xFF bytes"), - ( - 4, - b"\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff", - b"\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff", - "Hex sequence", - ), - (5, b"Hello World!!!!!"[:16], b"Hello World!", "ASCII text as binary"), - (6, bytes(range(16)), bytes(range(50)), "Sequential bytes"), - (7, b"\x00" * 16, b"\x00" * 100, "Max varbinary length"), - (8, b"\xde\xad\xbe\xef" * 4, b"\xde\xad\xbe\xef" * 5, "Repeated pattern"), - (9, b"\x01" * 16, b"\x01", "Single byte varbinary"), - (10, b"\x80" * 16, b"\x80\x90\xa0\xb0\xc0\xd0\xe0\xf0", "High-bit bytes"), - ] - - print(f"\nPerforming bulk copy with {len(test_data)} rows using cursor.bulkcopy()...") - print("Testing BINARY and VARBINARY data types with edge cases...") - - # Perform bulk copy via cursor - result = cursor.bulkcopy( - table_name=table_name, - data=test_data, - batch_size=5, - timeout=30, - column_mappings=[ - (0, "id"), - (1, "binary_data"), - (2, "varbinary_data"), - (3, "description"), - ], - ) - - print(f"\nBulk copy completed successfully!") - print(f" Rows copied: {result['rows_copied']}") - print(f" Batch count: {result['batch_count']}") - print(f" Elapsed time: {result['elapsed_time']}") - - # Assertions - assert result["rows_copied"] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" - assert result["batch_count"] == 2, f"Expected 2 batches, got {result['batch_count']}" - - # Verify the data - print(f"\nVerifying inserted data...") - cursor.execute( - f"SELECT id, binary_data, varbinary_data, description FROM {table_name} ORDER BY id" - ) - rows = cursor.fetchall() - - print(f"Retrieved {len(rows)} rows:") - assert len(rows) == 10, f"Expected 10 rows retrieved, got {len(rows)}" - - for i, row in enumerate(rows): - print( - f" ID: {row[0]}, BINARY: {row[1].hex() if row[1] else 'NULL'}, " - + f"VARBINARY: {row[2].hex() if row[2] else 'NULL'}, Description: {row[3]}" - ) - - assert row[0] == test_data[i][0], f"ID mismatch at row {i}" - - # BINARY comparison - SQL Server pads with zeros to fixed length - expected_binary = ( - test_data[i][1] - if len(test_data[i][1]) == 16 - else test_data[i][1] + b"\x00" * (16 - len(test_data[i][1])) - ) - assert ( - row[1] == expected_binary - ), f"BINARY mismatch at row {i}: expected {expected_binary.hex()}, got {row[1].hex()}" - - # VARBINARY comparison - exact match expected - assert ( - row[2] == test_data[i][2] - ), f"VARBINARY mismatch at row {i}: expected {test_data[i][2].hex()}, got {row[2].hex()}" - - assert row[3] == test_data[i][3], f"Description mismatch at row {i}" - - # Additional verification for specific cases - print("\nVerifying specific edge cases...") - - # Empty varbinary - cursor.execute(f"SELECT varbinary_data FROM {table_name} WHERE id = 1") - empty_varbinary = cursor.fetchone()[0] - assert empty_varbinary == b"", f"Empty varbinary verification failed" - print(f" ✓ Empty varbinary verified: length = {len(empty_varbinary)}") - - # Max varbinary length - cursor.execute(f"SELECT varbinary_data FROM {table_name} WHERE id = 7") - max_varbinary = cursor.fetchone()[0] - assert len(max_varbinary) == 100, f"Max varbinary length verification failed" - assert max_varbinary == b"\x00" * 100, f"Max varbinary content verification failed" - print(f" ✓ Max varbinary length verified: {len(max_varbinary)} bytes") - - # All 0xFF bytes - cursor.execute(f"SELECT binary_data, varbinary_data FROM {table_name} WHERE id = 3") - all_ff_row = cursor.fetchone() - assert all_ff_row[0] == b"\xff" * 16, f"All 0xFF BINARY verification failed" - assert all_ff_row[1] == b"\xff" * 16, f"All 0xFF VARBINARY verification failed" - print(f" ✓ All 0xFF bytes verified for both BINARY and VARBINARY") - - # Cleanup - print(f"\nCleaning up test table...") - cursor.execute(f"DROP TABLE {table_name}") - conn.commit() - - # Close cursor and connection - cursor.close() - conn.close() - print("\nTest completed successfully!") - - -if __name__ == "__main__": - test_binary_varbinary_bulkcopy() diff --git a/TestsBCP/test_bit_BCP.py b/TestsBCP/test_bit_BCP.py deleted file mode 100644 index 42813e2c..00000000 --- a/TestsBCP/test_bit_BCP.py +++ /dev/null @@ -1,172 +0,0 @@ -import sys -import os -import pytest - -# Add parent directory to path to import mssql_python -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from mssql_python import connect - - -def test_bit_bulkcopy(): - """Test bulk copy functionality with BIT data type""" - # Get connection string from environment - conn_str = os.getenv("DB_CONNECTION_STRING") - assert conn_str is not None, "DB_CONNECTION_STRING environment variable must be set" - - print(f"Connection string length: {len(conn_str)}") - - # Connect using the regular mssql_python connection - conn = connect(conn_str) - print(f"Connection created: {type(conn)}") - - # Create cursor - cursor = conn.cursor() - print(f"Cursor created: {type(cursor)}") - - # Create a test table with BIT columns - table_name = "BulkCopyBitTest" - - print(f"\nCreating test table: {table_name}") - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute( - f""" - CREATE TABLE {table_name} ( - id INT, - bit_value BIT, - is_active BIT, - is_deleted BIT, - description VARCHAR(100) - ) - """ - ) - conn.commit() - print("Test table created successfully") - - # Prepare test data with various BIT values - # BIT can be 0, 1, True, False, or NULL - test_data = [ - (1, 0, 0, 0, "All zeros (False)"), - (2, 1, 1, 1, "All ones (True)"), - (3, True, True, True, "All True"), - (4, False, False, False, "All False"), - (5, 1, 0, 1, "Mixed 1-0-1"), - (6, 0, 1, 0, "Mixed 0-1-0"), - (7, True, False, True, "Mixed True-False-True"), - (8, False, True, False, "Mixed False-True-False"), - (9, 1, True, 0, "Mixed 1-True-0"), - (10, False, 1, True, "Mixed False-1-True"), - ] - - print(f"\nPerforming bulk copy with {len(test_data)} rows using cursor.bulkcopy()...") - print("Testing BIT data type with True/False and 0/1 values...") - - # Perform bulk copy via cursor - result = cursor.bulkcopy( - table_name=table_name, - data=test_data, - batch_size=5, - timeout=30, - column_mappings=[ - (0, "id"), - (1, "bit_value"), - (2, "is_active"), - (3, "is_deleted"), - (4, "description"), - ], - ) - - print(f"\nBulk copy completed successfully!") - print(f" Rows copied: {result['rows_copied']}") - print(f" Batch count: {result['batch_count']}") - print(f" Elapsed time: {result['elapsed_time']}") - - # Assertions - assert result["rows_copied"] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" - assert result["batch_count"] == 2, f"Expected 2 batches, got {result['batch_count']}" - - # Verify the data - print(f"\nVerifying inserted data...") - cursor.execute( - f"SELECT id, bit_value, is_active, is_deleted, description FROM {table_name} ORDER BY id" - ) - rows = cursor.fetchall() - - print(f"Retrieved {len(rows)} rows:") - assert len(rows) == 10, f"Expected 10 rows retrieved, got {len(rows)}" - - # Expected values after conversion (SQL Server stores BIT as 0 or 1) - expected_values = [ - (1, False, False, False, "All zeros (False)"), - (2, True, True, True, "All ones (True)"), - (3, True, True, True, "All True"), - (4, False, False, False, "All False"), - (5, True, False, True, "Mixed 1-0-1"), - (6, False, True, False, "Mixed 0-1-0"), - (7, True, False, True, "Mixed True-False-True"), - (8, False, True, False, "Mixed False-True-False"), - (9, True, True, False, "Mixed 1-True-0"), - (10, False, True, True, "Mixed False-1-True"), - ] - - for i, row in enumerate(rows): - print( - f" ID: {row[0]}, BIT: {row[1]}, IS_ACTIVE: {row[2]}, IS_DELETED: {row[3]}, Description: {row[4]}" - ) - - assert row[0] == expected_values[i][0], f"ID mismatch at row {i}" - assert ( - row[1] == expected_values[i][1] - ), f"BIT value mismatch at row {i}: expected {expected_values[i][1]}, got {row[1]}" - assert ( - row[2] == expected_values[i][2] - ), f"IS_ACTIVE mismatch at row {i}: expected {expected_values[i][2]}, got {row[2]}" - assert ( - row[3] == expected_values[i][3] - ), f"IS_DELETED mismatch at row {i}: expected {expected_values[i][3]}, got {row[3]}" - assert row[4] == expected_values[i][4], f"Description mismatch at row {i}" - - # Additional verification for specific cases - print("\nVerifying specific edge cases...") - - # All False values - cursor.execute(f"SELECT bit_value, is_active, is_deleted FROM {table_name} WHERE id = 1") - all_false = cursor.fetchone() - assert ( - all_false[0] == False and all_false[1] == False and all_false[2] == False - ), f"All False verification failed" - print(f" ✓ All False values verified: {all_false}") - - # All True values - cursor.execute(f"SELECT bit_value, is_active, is_deleted FROM {table_name} WHERE id = 2") - all_true = cursor.fetchone() - assert ( - all_true[0] == True and all_true[1] == True and all_true[2] == True - ), f"All True verification failed" - print(f" ✓ All True values verified: {all_true}") - - # Count True values - cursor.execute(f"SELECT COUNT(*) FROM {table_name} WHERE bit_value = 1") - true_count = cursor.fetchone()[0] - print(f" ✓ Count of True bit_value: {true_count}") - - # Count False values - cursor.execute(f"SELECT COUNT(*) FROM {table_name} WHERE bit_value = 0") - false_count = cursor.fetchone()[0] - print(f" ✓ Count of False bit_value: {false_count}") - - assert true_count + false_count == 10, f"Total count mismatch" - - # Cleanup - print(f"\nCleaning up test table...") - cursor.execute(f"DROP TABLE {table_name}") - conn.commit() - - # Close cursor and connection - cursor.close() - conn.close() - print("\nTest completed successfully!") - - -if __name__ == "__main__": - test_bit_bulkcopy() diff --git a/TestsBCP/test_column_mismatch_BCP.py b/TestsBCP/test_column_mismatch_BCP.py deleted file mode 100644 index 7884135e..00000000 --- a/TestsBCP/test_column_mismatch_BCP.py +++ /dev/null @@ -1,281 +0,0 @@ -"""Bulk copy tests for column count mismatch scenarios. - -Tests the behavior when bulk copying data where: -1. Source data has more columns than the target table -2. Source data has fewer columns than the target table - -According to expected behavior: -- Extra columns in the source should be dropped/ignored -- Missing columns should result in NULL values (if nullable) or defaults -""" - -import pytest - - -@pytest.mark.integration -def test_bulkcopy_more_columns_than_table(cursor): - """Test bulk copy where source has more columns than the target table. - - The extra columns should be dropped and the bulk copy should succeed. - Only the columns specified in column_mappings should be inserted. - """ - - # Create a test table with 3 INT columns - table_name = "BulkCopyMoreColumnsTest" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, value1 INT, value2 INT)") - cursor.connection.commit() - - # Source data has 5 columns, but table only has 3 - # Extra columns (indices 3 and 4) should be ignored via column_mappings - data = [ - (1, 100, 30, 999, 888), - (2, 200, 25, 999, 888), - (3, 300, 35, 999, 888), - (4, 400, 28, 999, 888), - ] - - # Execute bulk copy with explicit column mappings for first 3 columns only - result = cursor.bulkcopy( - table_name=table_name, - data=data, - batch_size=1000, - timeout=30, - column_mappings=[ - (0, "id"), # Map source column 0 to 'id' - (1, "value1"), # Map source column 1 to 'value1' - (2, "value2"), # Map source column 2 to 'value2' - # Columns 3 and 4 are NOT mapped, so they're dropped - ], - ) - - # Verify results - assert result is not None - assert result["rows_copied"] == 4, "Expected 4 rows to be copied" - assert result["batch_count"] >= 1 - - # Verify data was inserted correctly (only first 3 columns) - cursor.execute(f"SELECT id, value1, value2 FROM {table_name} ORDER BY id") - rows = cursor.fetchall() - - assert len(rows) == 4, "Expected 4 rows in table" - assert rows[0][0] == 1 and rows[0][1] == 100 and rows[0][2] == 30 - assert rows[1][0] == 2 and rows[1][1] == 200 and rows[1][2] == 25 - assert rows[2][0] == 3 and rows[2][1] == 300 and rows[2][2] == 35 - assert rows[3][0] == 4 and rows[3][1] == 400 and rows[3][2] == 28 - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() - - -@pytest.mark.integration -def test_bulkcopy_fewer_columns_than_table(cursor): - """Test bulk copy where source has fewer columns than the target table. - - Missing columns should be filled with NULL (if nullable). - The bulk copy should succeed. - """ - - # Create a test table with 3 INT columns (value2 is nullable) - table_name = "BulkCopyFewerColumnsTest" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, value1 INT, value2 INT NULL)") - cursor.connection.commit() - - # Source data has only 2 columns (id, value1) - missing 'value2' - data = [ - (1, 100), - (2, 200), - (3, 300), - (4, 400), - ] - - # Execute bulk copy with mappings for only 2 columns - # 'value2' column is not mapped, so it should get NULL values - result = cursor.bulkcopy( - table_name=table_name, - data=data, - batch_size=1000, - timeout=30, - column_mappings=[ - (0, "id"), # Map source column 0 to 'id' - (1, "value1"), # Map source column 1 to 'value1' - # 'value2' is not mapped, should be NULL - ], - ) - - # Verify results - assert result is not None - assert result["rows_copied"] == 4, "Expected 4 rows to be copied" - assert result["batch_count"] >= 1 - - # Verify data was inserted with NULL for missing 'value2' column - cursor.execute(f"SELECT id, value1, value2 FROM {table_name} ORDER BY id") - rows = cursor.fetchall() - - assert len(rows) == 4, "Expected 4 rows in table" - assert rows[0][0] == 1 and rows[0][1] == 100 and rows[0][2] is None, "value2 should be NULL" - assert rows[1][0] == 2 and rows[1][1] == 200 and rows[1][2] is None, "value2 should be NULL" - assert rows[2][0] == 3 and rows[2][1] == 300 and rows[2][2] is None, "value2 should be NULL" - assert rows[3][0] == 4 and rows[3][1] == 400 and rows[3][2] is None, "value2 should be NULL" - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() - - -@pytest.mark.integration -def test_bulkcopy_auto_mapping_with_extra_columns(cursor): - """Test bulk copy with auto-mapping when source has more columns than table. - - Without explicit column_mappings, auto-mapping should use the first N columns - where N is the number of columns in the target table. Extra source columns are ignored. - """ - - # Create a test table with 3 INT columns - table_name = "BulkCopyAutoMapExtraTest" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f"CREATE TABLE {table_name} (id INT, value1 INT, value2 INT)") - cursor.connection.commit() - - # Source data has 5 columns, table has 3 - # Auto-mapping should use first 3 columns - data = [ - (1, 100, 30, 777, 666), - (2, 200, 25, 777, 666), - (3, 300, 35, 777, 666), - ] - - # Execute bulk copy WITHOUT explicit column mappings - # Auto-mapping should map first 3 columns to table's 3 columns - result = cursor.bulkcopy(table_name=table_name, data=data, batch_size=1000, timeout=30) - - # Verify results - assert result is not None - assert result["rows_copied"] == 3, "Expected 3 rows to be copied" - - # Verify data was inserted correctly (first 3 columns only) - cursor.execute(f"SELECT id, value1, value2 FROM {table_name} ORDER BY id") - rows = cursor.fetchall() - - assert len(rows) == 3, "Expected 3 rows in table" - assert rows[0][0] == 1 and rows[0][1] == 100 and rows[0][2] == 30 - assert rows[1][0] == 2 and rows[1][1] == 200 and rows[1][2] == 25 - assert rows[2][0] == 3 and rows[2][1] == 300 and rows[2][2] == 35 - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() - - -@pytest.mark.integration -def test_bulkcopy_fewer_columns_with_defaults(cursor): - """Test bulk copy where missing columns have default values. - - Missing columns should use their default values instead of NULL. - """ - - # Create a test table with default values - table_name = "BulkCopyFewerColumnsDefaultTest" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute( - f"""CREATE TABLE {table_name} ( - id INT PRIMARY KEY, - value1 INT, - value2 INT DEFAULT 999, - status VARCHAR(10) DEFAULT 'active' - )""" - ) - cursor.connection.commit() - - # Source data has only 2 columns - missing value2 and status - data = [ - (1, 100), - (2, 200), - (3, 300), - ] - - # Execute bulk copy mapping only 2 columns - result = cursor.bulkcopy( - table_name=table_name, - data=data, - batch_size=1000, - timeout=30, - column_mappings=[ - (0, "id"), - (1, "value1"), - # value2 and status not mapped - should use defaults - ], - ) - - # Verify results - assert result["rows_copied"] == 3 - - # Verify data was inserted with default values - cursor.execute(f"SELECT id, value1, value2, status FROM {table_name} ORDER BY id") - rows = cursor.fetchall() - - assert len(rows) == 3 - assert rows[0][0] == 1 and rows[0][1] == 100 and rows[0][2] == 999 and rows[0][3] == "active" - assert rows[1][0] == 2 and rows[1][1] == 200 and rows[1][2] == 999 and rows[1][3] == "active" - assert rows[2][0] == 3 and rows[2][1] == 300 and rows[2][2] == 999 and rows[2][3] == "active" - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() - - -@pytest.mark.integration -def test_bulkcopy_column_reordering(cursor): - """Test bulk copy with column reordering. - - Source columns can be mapped to target columns in different order. - """ - - # Create a test table - table_name = "BulkCopyColumnReorderTest" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute( - f"CREATE TABLE {table_name} (id INT, name VARCHAR(50), age INT, city VARCHAR(50))" - ) - cursor.connection.commit() - - # Source data: (name, age, city, id) - different order than table - data = [ - ("Alice", 30, "NYC", 1), - ("Bob", 25, "LA", 2), - ("Carol", 35, "Chicago", 3), - ] - - # Map source columns to target in different order - result = cursor.bulkcopy( - table_name=table_name, - data=data, - batch_size=1000, - timeout=30, - column_mappings=[ - (3, "id"), # Source column 3 (id) → Target id - (0, "name"), # Source column 0 (name) → Target name - (1, "age"), # Source column 1 (age) → Target age - (2, "city"), # Source column 2 (city) → Target city - ], - ) - - # Verify results - assert result["rows_copied"] == 3 - - # Verify data was inserted correctly with proper column mapping - cursor.execute(f"SELECT id, name, age, city FROM {table_name} ORDER BY id") - rows = cursor.fetchall() - - assert len(rows) == 3 - assert rows[0][0] == 1 and rows[0][1] == "Alice" and rows[0][2] == 30 and rows[0][3] == "NYC" - assert rows[1][0] == 2 and rows[1][1] == "Bob" and rows[1][2] == 25 and rows[1][3] == "LA" - assert ( - rows[2][0] == 3 and rows[2][1] == "Carol" and rows[2][2] == 35 and rows[2][3] == "Chicago" - ) - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() diff --git a/TestsBCP/test_cursor_bulkcopy.py b/TestsBCP/test_cursor_bulkcopy.py deleted file mode 100644 index a27cc14f..00000000 --- a/TestsBCP/test_cursor_bulkcopy.py +++ /dev/null @@ -1,96 +0,0 @@ -import sys -import os -import pytest - -# Add parent directory to path to import mssql_python -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from mssql_python import connect - - -def test_cursor_bulkcopy(): - """Test bulk copy functionality through cursor.bulkcopy() method""" - # Get connection string from environment - conn_str = os.getenv("DB_CONNECTION_STRING") - assert conn_str is not None, "DB_CONNECTION_STRING environment variable must be set" - - print(f"Connection string length: {len(conn_str)}") - - # Connect using the regular mssql_python connection - conn = connect(conn_str) - print(f"Connection created: {type(conn)}") - - # Create cursor - cursor = conn.cursor() - print(f"Cursor created: {type(cursor)}") - - # Create a test table - table_name = "BulkCopyCursorTest" - - print(f"\nCreating test table: {table_name}") - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f"CREATE TABLE {table_name} (id INT, name VARCHAR(50), amount DECIMAL(10,2))") - conn.commit() - print("Test table created successfully") - - # Prepare test data - test_data = [ - (1, "Product A", 99.99), - (2, "Product B", 149.50), - (3, "Product C", 199.99), - (4, "Product D", 249.00), - (5, "Product E", 299.99), - (6, "Product F", 349.50), - (7, "Product G", 399.99), - (8, "Product H", 449.00), - (9, "Product I", 499.99), - (10, "Product J", 549.50), - ] - - print(f"\nPerforming bulk copy with {len(test_data)} rows using cursor.bulkcopy()...") - - # Perform bulk copy via cursor - result = cursor.bulkcopy( - table_name=table_name, - data=test_data, - batch_size=5, - timeout=30, - column_mappings=[ - (0, "id"), - (1, "name"), - (2, "amount"), - ], - ) - - print(f"\nBulk copy completed successfully!") - print(f" Rows copied: {result['rows_copied']}") - print(f" Batch count: {result['batch_count']}") - print(f" Elapsed time: {result['elapsed_time']}") - - # Assertions - assert result["rows_copied"] == 10, f"Expected 10 rows copied, got {result['rows_copied']}" - assert result["batch_count"] == 2, f"Expected 2 batches, got {result['batch_count']}" - - # Verify the data - print(f"\nVerifying inserted data...") - cursor.execute(f"SELECT id, name, amount FROM {table_name} ORDER BY id") - rows = cursor.fetchall() - - print(f"Retrieved {len(rows)} rows:") - assert len(rows) == 10, f"Expected 10 rows retrieved, got {len(rows)}" - - for i, row in enumerate(rows): - print(f" ID: {row[0]}, Name: {row[1]}, Amount: {row[2]}") - assert row[0] == test_data[i][0], f"ID mismatch at row {i}" - assert row[1] == test_data[i][1], f"Name mismatch at row {i}" - assert float(row[2]) == test_data[i][2], f"Amount mismatch at row {i}" - - # Cleanup - print(f"\nCleaning up test table...") - cursor.execute(f"DROP TABLE {table_name}") - conn.commit() - - # Close cursor and connection - cursor.close() - conn.close() - print("\nTest completed successfully!") diff --git a/TestsBCP/test_date_BCP.py b/TestsBCP/test_date_BCP.py deleted file mode 100644 index 545b0dca..00000000 --- a/TestsBCP/test_date_BCP.py +++ /dev/null @@ -1,241 +0,0 @@ -"""Bulk copy tests for DATE data type.""" - -import pytest -import datetime - - -@pytest.mark.integration -def test_cursor_bulkcopy_date_basic(cursor): - """Test cursor bulkcopy method with two date columns and explicit mappings.""" - - # Create a test table with two date columns - table_name = "BulkCopyTestTableDate" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f"CREATE TABLE {table_name} (event_date DATE, birth_date DATE)") - cursor.connection.commit() - - # Prepare test data - two columns, both date - data = [ - (datetime.date(2020, 1, 15), datetime.date(1990, 5, 20)), - (datetime.date(2021, 6, 10), datetime.date(1985, 3, 25)), - (datetime.date(2022, 12, 25), datetime.date(2000, 7, 4)), - ] - - # Execute bulk copy with explicit column mappings - result = cursor.bulkcopy( - table_name=table_name, - data=data, - batch_size=1000, - timeout=30, - column_mappings=[ - (0, "event_date"), - (1, "birth_date"), - ], - ) - - # Verify results - assert result is not None - assert result["rows_copied"] == 3 - assert result["batch_count"] == 1 - assert "elapsed_time" in result - - # Verify data was inserted correctly - cursor.execute(f"SELECT event_date, birth_date FROM {table_name} ORDER BY event_date") - rows = cursor.fetchall() - assert len(rows) == 3 - assert rows[0][0] == datetime.date(2020, 1, 15) and rows[0][1] == datetime.date(1990, 5, 20) - assert rows[1][0] == datetime.date(2021, 6, 10) and rows[1][1] == datetime.date(1985, 3, 25) - assert rows[2][0] == datetime.date(2022, 12, 25) and rows[2][1] == datetime.date(2000, 7, 4) - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() - - -@pytest.mark.integration -def test_cursor_bulkcopy_date_auto_mapping(cursor): - """Test cursor bulkcopy with automatic column mapping. - - Tests bulkcopy when no mappings are specified, including NULL value handling. - """ - - # Create a test table with two nullable date columns - table_name = "BulkCopyAutoMapTableDate" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f"CREATE TABLE {table_name} (event_date DATE, birth_date DATE)") - cursor.connection.commit() - - # Prepare test data - two columns, both date, with NULL values - data = [ - (datetime.date(2020, 1, 15), datetime.date(1990, 5, 20)), - (datetime.date(2021, 6, 10), None), # NULL value in second column - (None, datetime.date(1985, 3, 25)), # NULL value in first column - (datetime.date(2022, 12, 25), datetime.date(2000, 7, 4)), - ] - - # Execute bulk copy WITHOUT column mappings - should auto-generate - result = cursor.bulkcopy(table_name=table_name, data=data, batch_size=1000, timeout=30) - - # Verify results - assert result is not None - assert result["rows_copied"] == 4 - assert result["batch_count"] == 1 - assert "elapsed_time" in result - - # Verify data including NULLs - cursor.execute( - f"SELECT event_date, birth_date FROM {table_name} ORDER BY COALESCE(event_date, '9999-12-31')" - ) - rows = cursor.fetchall() - assert len(rows) == 4 - assert rows[0][0] == datetime.date(2020, 1, 15) and rows[0][1] == datetime.date(1990, 5, 20) - assert rows[1][0] == datetime.date(2021, 6, 10) and rows[1][1] is None - assert rows[2][0] == datetime.date(2022, 12, 25) and rows[2][1] == datetime.date(2000, 7, 4) - assert rows[3][0] is None and rows[3][1] == datetime.date(1985, 3, 25) - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() - - -@pytest.mark.integration -def test_cursor_bulkcopy_date_string_to_date_conversion(cursor): - """Test cursor bulkcopy with string values that should convert to date columns. - - Tests type coercion when source data contains date strings but - destination columns are DATE type. - """ - - # Create a test table with two date columns - table_name = "BulkCopyStringToDateTable" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f"CREATE TABLE {table_name} (event_date DATE, birth_date DATE)") - cursor.connection.commit() - - # Prepare test data - strings containing valid dates in ISO format - data = [ - ("2020-01-15", "1990-05-20"), - ("2021-06-10", "1985-03-25"), - ("2022-12-25", "2000-07-04"), - ] - - # Execute bulk copy without explicit mappings - result = cursor.bulkcopy(table_name=table_name, data=data, batch_size=1000, timeout=30) - - # Verify results - assert result is not None - assert result["rows_copied"] == 3 - assert result["batch_count"] == 1 - assert "elapsed_time" in result - - # Verify data was converted correctly - cursor.execute(f"SELECT event_date, birth_date FROM {table_name} ORDER BY event_date") - rows = cursor.fetchall() - assert len(rows) == 3 - assert rows[0][0] == datetime.date(2020, 1, 15) and rows[0][1] == datetime.date(1990, 5, 20) - assert rows[1][0] == datetime.date(2021, 6, 10) and rows[1][1] == datetime.date(1985, 3, 25) - assert rows[2][0] == datetime.date(2022, 12, 25) and rows[2][1] == datetime.date(2000, 7, 4) - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() - - -@pytest.mark.integration -def test_cursor_bulkcopy_date_boundary_values(cursor): - """Test cursor bulkcopy with DATE boundary values.""" - - # Create a test table - table_name = "BulkCopyDateBoundaryTest" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f"CREATE TABLE {table_name} (id INT, test_date DATE)") - cursor.connection.commit() - - # Test data with boundary values - # DATE range: 0001-01-01 to 9999-12-31 - data = [ - (1, datetime.date(1, 1, 1)), # Min DATE - (2, datetime.date(9999, 12, 31)), # Max DATE - (3, datetime.date(2000, 1, 1)), # Y2K - (4, datetime.date(1900, 1, 1)), # Century boundary - (5, datetime.date(2024, 2, 29)), # Leap year - ] - - # Execute bulk copy - result = cursor.bulkcopy( - table_name=table_name, - data=data, - batch_size=1000, - timeout=30, - column_mappings=[ - (0, "id"), - (1, "test_date"), - ], - ) - - # Verify results - assert result["rows_copied"] == 5 - assert result["batch_count"] == 1 - - # Verify data - cursor.execute(f"SELECT id, test_date FROM {table_name} ORDER BY id") - rows = cursor.fetchall() - assert len(rows) == 5 - assert rows[0][1] == datetime.date(1, 1, 1) # Min DATE - assert rows[1][1] == datetime.date(9999, 12, 31) # Max DATE - assert rows[2][1] == datetime.date(2000, 1, 1) # Y2K - assert rows[3][1] == datetime.date(1900, 1, 1) # Century boundary - assert rows[4][1] == datetime.date(2024, 2, 29) # Leap year - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() - - -@pytest.mark.integration -def test_cursor_bulkcopy_date_large_batch(cursor): - """Test cursor bulkcopy with a large number of DATE rows.""" - - # Create a test table - table_name = "BulkCopyDateLargeBatchTest" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f"CREATE TABLE {table_name} (id INT, test_date DATE)") - cursor.connection.commit() - - # Generate 365 rows (one for each day of 2024) - base_date = datetime.date(2024, 1, 1) - data = [(i + 1, base_date + datetime.timedelta(days=i)) for i in range(365)] - - # Execute bulk copy with smaller batch size - result = cursor.bulkcopy( - table_name=table_name, - data=data, - batch_size=50, # ~8 batches - timeout=30, - column_mappings=[ - (0, "id"), - (1, "test_date"), - ], - ) - - # Verify results - assert result["rows_copied"] == 365 - assert result["batch_count"] >= 7 # 365 / 50 = 7.3 - - # Verify row count - cursor.execute(f"SELECT COUNT(*) FROM {table_name}") - count = cursor.fetchone()[0] - assert count == 365 - - # Verify sample data - cursor.execute(f"SELECT id, test_date FROM {table_name} WHERE id IN (1, 100, 200, 365)") - rows = cursor.fetchall() - assert len(rows) == 4 - assert rows[0][0] == 1 and rows[0][1] == datetime.date(2024, 1, 1) - assert rows[1][0] == 100 and rows[1][1] == datetime.date(2024, 4, 9) - assert rows[2][0] == 200 and rows[2][1] == datetime.date(2024, 7, 18) - assert rows[3][0] == 365 and rows[3][1] == datetime.date(2024, 12, 30) - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() diff --git a/TestsBCP/test_datetime_BCP.py b/TestsBCP/test_datetime_BCP.py deleted file mode 100644 index af857a5f..00000000 --- a/TestsBCP/test_datetime_BCP.py +++ /dev/null @@ -1,309 +0,0 @@ -"""Bulk copy tests for DATETIME data type.""" - -import pytest -import datetime - - -@pytest.mark.integration -def test_cursor_bulkcopy_datetime_basic(cursor): - """Test cursor bulkcopy method with two datetime columns and explicit mappings.""" - # Create a test table with two datetime columns - table_name = "BulkCopyTestTableDateTime" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f"CREATE TABLE {table_name} (start_datetime DATETIME, end_datetime DATETIME)") - cursor.connection.commit() - - # Prepare test data - two columns, both datetime - data = [ - (datetime.datetime(2024, 1, 15, 9, 30, 0), datetime.datetime(2024, 1, 15, 17, 45, 30)), - (datetime.datetime(2024, 2, 20, 8, 15, 45), datetime.datetime(2024, 2, 20, 16, 30, 15)), - (datetime.datetime(2024, 3, 10, 10, 0, 0), datetime.datetime(2024, 3, 10, 18, 0, 0)), - ] - - # Execute bulk copy with explicit column mappings - result = cursor.bulkcopy( - table_name, - data, - batch_size=1000, - timeout=30, - column_mappings=[ - (0, "start_datetime"), - (1, "end_datetime"), - ], - ) - - # Verify results - assert result is not None - assert result["rows_copied"] == 3 - assert result["batch_count"] == 1 - assert "elapsed_time" in result - - # Verify data was inserted by checking the count - cursor.execute(f"SELECT COUNT(*) FROM {table_name}") - rows = cursor.fetchall() - count = rows[0][0] - assert count == 3 - - # Verify actual datetime values - cursor.execute(f"SELECT start_datetime, end_datetime FROM {table_name} ORDER BY start_datetime") - rows = cursor.fetchall() - assert len(rows) == 3 - - # Verify first row - assert rows[0][0] == datetime.datetime(2024, 1, 15, 9, 30, 0) - assert rows[0][1] == datetime.datetime(2024, 1, 15, 17, 45, 30) - - # Verify second row - assert rows[1][0] == datetime.datetime(2024, 2, 20, 8, 15, 45) - assert rows[1][1] == datetime.datetime(2024, 2, 20, 16, 30, 15) - - # Verify third row - assert rows[2][0] == datetime.datetime(2024, 3, 10, 10, 0, 0) - assert rows[2][1] == datetime.datetime(2024, 3, 10, 18, 0, 0) - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() - - -@pytest.mark.integration -def test_cursor_bulkcopy_datetime_auto_mapping(cursor): - """Test cursor bulkcopy with automatic column mapping. - - Tests bulkcopy when no mappings are specified, including NULL value handling. - """ - # Create a test table with two nullable datetime columns - table_name = "BulkCopyAutoMapTableDateTime" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f"CREATE TABLE {table_name} (start_datetime DATETIME, end_datetime DATETIME)") - cursor.connection.commit() - - # Prepare test data - two columns, both datetime, with NULL values - data = [ - (datetime.datetime(2024, 1, 15, 9, 30, 0), datetime.datetime(2024, 1, 15, 17, 45, 30)), - (datetime.datetime(2024, 2, 20, 8, 15, 45), None), # NULL value in second column - (None, datetime.datetime(2024, 2, 20, 16, 30, 15)), # NULL value in first column - (datetime.datetime(2024, 3, 10, 10, 0, 0), datetime.datetime(2024, 3, 10, 18, 0, 0)), - ] - - # Execute bulk copy WITHOUT column mappings - should auto-generate - result = cursor.bulkcopy(table_name, data, batch_size=1000, timeout=30) - - # Verify results - assert result is not None - assert result["rows_copied"] == 4 - assert result["batch_count"] == 1 - assert "elapsed_time" in result - - # Verify data was inserted by checking the count - cursor.execute(f"SELECT COUNT(*) FROM {table_name}") - rows = cursor.fetchall() - count = rows[0][0] - assert count == 4 - - # Verify NULL handling - cursor.execute( - f"SELECT start_datetime, end_datetime FROM {table_name} ORDER BY ISNULL(start_datetime, '1900-01-01')" - ) - rows = cursor.fetchall() - assert len(rows) == 4 - - # Verify NULL value in first column (third row after sorting) - assert rows[0][0] is None - assert rows[0][1] == datetime.datetime(2024, 2, 20, 16, 30, 15) - - # Verify NULL value in second column - assert rows[2][0] == datetime.datetime(2024, 2, 20, 8, 15, 45) - assert rows[2][1] is None - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() - - -@pytest.mark.integration -def test_cursor_bulkcopy_datetime_string_to_datetime_conversion(cursor): - """Test cursor bulkcopy with string values that should convert to datetime columns. - - Tests type coercion when source data contains datetime strings but - destination columns are DATETIME type. - """ - # Create a test table with two datetime columns - table_name = "BulkCopyStringToDateTimeTable" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f"CREATE TABLE {table_name} (start_datetime DATETIME, end_datetime DATETIME)") - cursor.connection.commit() - - # Prepare test data - strings containing valid datetimes in ISO format - data = [ - ("2024-01-15 09:30:00", "2024-01-15 17:45:30"), - ("2024-02-20 08:15:45", "2024-02-20 16:30:15"), - ("2024-03-10 10:00:00", "2024-03-10 18:00:00"), - ] - - # Execute bulk copy without explicit mappings - result = cursor.bulkcopy(table_name, data, batch_size=1000, timeout=30) - - # Verify results - assert result is not None - assert result["rows_copied"] == 3 - assert result["batch_count"] == 1 - assert "elapsed_time" in result - - # Verify data was inserted by checking the count - cursor.execute(f"SELECT COUNT(*) FROM {table_name}") - rows = cursor.fetchall() - count = rows[0][0] - assert count == 3 - - # Verify the datetime values were properly converted from strings - cursor.execute(f"SELECT start_datetime, end_datetime FROM {table_name} ORDER BY start_datetime") - rows = cursor.fetchall() - assert len(rows) == 3 - - # Verify first row - assert rows[0][0] == datetime.datetime(2024, 1, 15, 9, 30, 0) - assert rows[0][1] == datetime.datetime(2024, 1, 15, 17, 45, 30) - - # Verify second row - assert rows[1][0] == datetime.datetime(2024, 2, 20, 8, 15, 45) - assert rows[1][1] == datetime.datetime(2024, 2, 20, 16, 30, 15) - - # Verify third row - assert rows[2][0] == datetime.datetime(2024, 3, 10, 10, 0, 0) - assert rows[2][1] == datetime.datetime(2024, 3, 10, 18, 0, 0) - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() - - -@pytest.mark.integration -def test_cursor_bulkcopy_datetime_boundary_values(cursor): - """Test cursor bulkcopy with DATETIME boundary values. - - DATETIME range: 1753-01-01 00:00:00 to 9999-12-31 23:59:59.997 - Precision: Rounded to increments of .000, .003, or .007 seconds - """ - table_name = "BulkCopyDateTimeBoundaryTable" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute(f"CREATE TABLE {table_name} (dt_value DATETIME)") - cursor.connection.commit() - - # Test data with boundary values and edge cases - data = [ - (datetime.datetime(1753, 1, 1, 0, 0, 0),), # Minimum datetime - (datetime.datetime(9999, 12, 31, 23, 59, 59),), # Maximum datetime - (datetime.datetime(2000, 1, 1, 0, 0, 0),), # Y2K - (datetime.datetime(1999, 12, 31, 23, 59, 59),), # Pre-Y2K - (datetime.datetime(2024, 2, 29, 12, 0, 0),), # Leap year - (datetime.datetime(2024, 12, 31, 23, 59, 59),), # End of year - ] - - # Execute bulk copy - result = cursor.bulkcopy(table_name, data, batch_size=1000, timeout=30) - - # Verify results - assert result is not None - assert result["rows_copied"] == 6 - assert result["batch_count"] == 1 - - # Verify data - cursor.execute(f"SELECT dt_value FROM {table_name} ORDER BY dt_value") - rows = cursor.fetchall() - assert len(rows) == 6 - - # Verify minimum datetime - assert rows[0][0] == datetime.datetime(1753, 1, 1, 0, 0, 0) - - # Verify pre-Y2K - assert rows[1][0] == datetime.datetime(1999, 12, 31, 23, 59, 59) - - # Verify Y2K - assert rows[2][0] == datetime.datetime(2000, 1, 1, 0, 0, 0) - - # Verify leap year - assert rows[3][0] == datetime.datetime(2024, 2, 29, 12, 0, 0) - - # Verify end of year - assert rows[4][0] == datetime.datetime(2024, 12, 31, 23, 59, 59) - - # Verify maximum datetime - assert rows[5][0] == datetime.datetime(9999, 12, 31, 23, 59, 59) - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() - - -@pytest.mark.integration -def test_cursor_bulkcopy_datetime_mixed_types(cursor): - """Test cursor bulkcopy with DATETIME in a table with mixed column types. - - Verifies that DATETIME columns work correctly alongside other data types. - """ - table_name = "BulkCopyDateTimeMixedTable" - cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}") - cursor.execute( - f""" - CREATE TABLE {table_name} ( - id INT, - created_at DATETIME, - is_active BIT, - modified_at DATETIME - ) - """ - ) - cursor.connection.commit() - - # Test data with mixed types (INT, DATETIME, BIT, DATETIME) - data = [ - ( - 1, - datetime.datetime(2024, 1, 15, 9, 30, 0), - True, - datetime.datetime(2024, 1, 15, 10, 0, 0), - ), - ( - 2, - datetime.datetime(2024, 2, 20, 8, 15, 45), - False, - datetime.datetime(2024, 2, 20, 14, 30, 0), - ), - (3, datetime.datetime(2024, 3, 10, 10, 0, 0), True, None), # NULL datetime - ] - - # Execute bulk copy - result = cursor.bulkcopy(table_name, data, batch_size=1000, timeout=30) - - # Verify bulk copy succeeded - assert result is not None - assert result["rows_copied"] == 3 - - # Verify the data was inserted correctly - cursor.execute(f"SELECT id, created_at, is_active, modified_at FROM {table_name} ORDER BY id") - rows = cursor.fetchall() - - assert len(rows) == 3 - - # Verify first row - assert rows[0][0] == 1 - assert rows[0][1] == datetime.datetime(2024, 1, 15, 9, 30, 0) - assert rows[0][2] == True - assert rows[0][3] == datetime.datetime(2024, 1, 15, 10, 0, 0) - - # Verify second row - assert rows[1][0] == 2 - assert rows[1][1] == datetime.datetime(2024, 2, 20, 8, 15, 45) - assert rows[1][2] == False - assert rows[1][3] == datetime.datetime(2024, 2, 20, 14, 30, 0) - - # Verify third row (with NULL datetime) - assert rows[2][0] == 3 - assert rows[2][1] == datetime.datetime(2024, 3, 10, 10, 0, 0) - assert rows[2][2] == True - assert rows[2][3] is None # NULL modified_at - - # Cleanup - cursor.execute(f"DROP TABLE {table_name}") - cursor.connection.commit() From 4ca496919c0bca284c12fed233e8480799b266fc Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Fri, 16 Jan 2026 11:50:43 +0000 Subject: [PATCH 07/16] review comments --- mssql_python/cursor.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 1d738969..3c2983b3 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2451,23 +2451,17 @@ def nextset(self) -> Union[bool, None]: ) return True - def _bulkcopy( - self, - table_name: str, - data, - batch_size: int = 1000, - timeout: int = 30, - column_mappings: list = None, - ): + def _bulkcopy(self, table_name: str, data, **kwargs): """ Perform bulk copy operation using Rust-based implementation. Args: table_name: Target table name data: Iterable of tuples/lists containing row data - batch_size: Number of rows per batch (default: 1000) - timeout: Timeout in seconds (default: 30) - column_mappings: List of tuples mapping source column index to target column name + **kwargs: Additional options passed to the Rust bulkcopy method + - batch_size: Number of rows per batch (default: 1000) + - timeout: Timeout in seconds (default: 30) + - column_mappings: List of tuples mapping source column index to target column name Returns: Dictionary with rows_copied, batch_count, and elapsed_time @@ -2488,6 +2482,11 @@ def _bulkcopy( # Validate inputs if not table_name or not isinstance(table_name, str): raise ValueError("table_name must be a non-empty string") + + # Extract and validate kwargs with defaults + batch_size = kwargs.get("batch_size", 1000) + timeout = kwargs.get("timeout", 30) + if batch_size <= 0: raise ValueError(f"batch_size must be positive, got {batch_size}") if timeout <= 0: @@ -2526,10 +2525,6 @@ def _bulkcopy( rust_connection = mssql_py_core.PyCoreConnection(context) rust_cursor = rust_connection.cursor() - kwargs = {"batch_size": batch_size, "timeout": timeout} - if column_mappings: - kwargs["column_mappings"] = column_mappings - logger.debug("Bulk copy to '%s' - batch_size=%d", table_name, batch_size) result = rust_cursor.bulkcopy(table_name, iter(data), kwargs=kwargs) From 2d9faf78cfc561b4916a31531a6b3953cc7888ed Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Mon, 19 Jan 2026 06:39:16 +0000 Subject: [PATCH 08/16] Copilot review comment --- mssql_python/cursor.py | 150 +++++++++++++++++++++++++++++++++-------- 1 file changed, 123 insertions(+), 27 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 3c2983b3..16594ba1 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2455,28 +2455,97 @@ def _bulkcopy(self, table_name: str, data, **kwargs): """ Perform bulk copy operation using Rust-based implementation. + This method uses a separate connection to the database via the Rust library + (mssql_py_core) for optimized bulk data transfer. The connection parameters + are extracted from the current Python connection's connection string. + + Important: Transaction Isolation + The bulk copy operation creates its own connection and does NOT participate + in the current Python connection's transaction context. This means: + - Bulk copied data is committed independently of any open transaction + - Rolling back the Python connection will NOT rollback bulk copied data + - The bulk copy operation is essentially auto-committed + + If you need transactional bulk operations, consider using executemany() + or batched INSERT statements within your transaction instead. + Args: - table_name: Target table name - data: Iterable of tuples/lists containing row data + 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 + + Supported Python Types and SQL Server Mappings: + - None → NULL (for any nullable column) + - int → INT, BIGINT, SMALLINT, TINYINT + - float → FLOAT, REAL + - str → VARCHAR, NVARCHAR, CHAR, NCHAR, TEXT, NTEXT + - bool → BIT + - datetime.date → DATE + - datetime.datetime → DATETIME, DATETIME2, SMALLDATETIME + - datetime.time → TIME + - decimal.Decimal → DECIMAL, NUMERIC, MONEY, SMALLMONEY + - bytes → BINARY, VARBINARY, IMAGE + - uuid.UUID → UNIQUEIDENTIFIER + + NULL Values: + - Use Python's None to represent SQL NULL values + - Empty strings ('') are NOT treated as NULL + + Example data formats: + # Simple list of tuples + data = [(1, 'Alice', None), (2, 'Bob', 25)] + + # Generator for memory-efficient large datasets + def generate_data(): + for i in range(1000000): + yield (i, f'Name_{i}', datetime.date.today()) + **kwargs: Additional options passed to the Rust bulkcopy method - - batch_size: Number of rows per batch (default: 1000) - - timeout: Timeout in seconds (default: 30) - - column_mappings: List of tuples mapping source column index to target column name + - column_mappings: List of tuples mapping source column index to + target column name, e.g., [(0, 'id'), (1, 'name')] Returns: - Dictionary with rows_copied, batch_count, and elapsed_time + 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 is not installed - ValueError: If parameters are invalid + ImportError: If mssql_py_core Rust library is not installed + ValueError: If table_name is empty or parameters are invalid RuntimeError: If connection string is not available + + Example: + >>> # Basic usage with list of tuples + >>> data = [(1, 'Alice', None), (2, 'Bob', 25), (3, 'Charlie', 30)] + >>> result = cursor._bulkcopy('users', data) + >>> print(f"Copied {result['rows_copied']} rows") + + >>> # Using a generator for large datasets + >>> from decimal import Decimal + >>> from datetime import date + >>> def generate_orders(): + ... for i in range(10000): + ... yield (i, Decimal('99.99'), date.today(), None) + >>> result = cursor._bulkcopy('orders', generate_orders()) """ try: import mssql_py_core except ImportError as exc: raise ImportError( - "Bulk copy requires mssql_py_core Rust library. " - "Install from BCPRustWheel directory." + "Bulk copy requires the mssql_py_core Rust library which is not installed. " + "To install, run: pip install mssql_py_core " + "or install from the wheel file in the BCPRustWheel directory of the mssql-python repository: " + "pip install BCPRustWheel/mssql_py_core--.whl" ) from exc # Validate inputs @@ -2496,53 +2565,80 @@ def _bulkcopy(self, table_name: str, data, **kwargs): if not hasattr(self.connection, "connection_str"): raise RuntimeError("Connection string not available for bulk copy") - params = { - k.strip().lower(): v.strip() - for pair in self.connection.connection_str.split(";") - if "=" in pair - for k, v in [pair.split("=", 1)] - } + # 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") # Build connection context for Rust 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", "localhost"), "database": params.get("database", "master"), "user_name": params.get("uid", ""), - "password": params.get("pwd", ""), "trust_server_certificate": trust_cert, - "encryption": "Optional", + "encryption": encryption, } - logger.debug("Bulk copy connecting to %s/%s", context["server"], context["database"]) + # Extract password separately to avoid storing it in generic context that may be logged + password = params.get("pwd", "") + rust_context = dict(context) + rust_context["password"] = password rust_connection = None rust_cursor = None try: - rust_connection = mssql_py_core.PyCoreConnection(context) + rust_connection = mssql_py_core.PyCoreConnection(rust_context) rust_cursor = rust_connection.cursor() - logger.debug("Bulk copy to '%s' - batch_size=%d", table_name, batch_size) - result = rust_cursor.bulkcopy(table_name, iter(data), kwargs=kwargs) + result = rust_cursor.bulkcopy(table_name, iter(data), **kwargs) - logger.debug("Bulk copy completed - rows=%d", result.get("rows_copied", 0)) return result except Exception as e: - logger.error("Bulk copy failed: %s - %s", type(e).__name__, str(e)) - raise + # 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 rust_context: + rust_context["password"] = "" + rust_context["user_name"] = "" # Clean up Rust resources for resource in (rust_cursor, rust_connection): if resource and hasattr(resource, "close"): try: resource.close() - except Exception: - pass + except Exception as cleanup_error: + # Log cleanup errors at debug level to aid troubleshooting + # without masking the original exception + logger.debug( + "Failed to close bulk copy resource %s: %s", + type(resource).__name__, + cleanup_error + ) def __enter__(self): """ From 62f47953026a82938f16b35637905e0fcd7c7a43 Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Mon, 19 Jan 2026 06:46:01 +0000 Subject: [PATCH 09/16] review comment --- mssql_python/cursor.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 16594ba1..ed758238 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2573,6 +2573,12 @@ def generate_data(): 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 Rust 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. @@ -2593,8 +2599,8 @@ def generate_data(): encryption = "Optional" context = { - "server": params.get("server", "localhost"), - "database": params.get("database", "master"), + "server": params.get("server"), + "database": params.get("database"), "user_name": params.get("uid", ""), "trust_server_certificate": trust_cert, "encryption": encryption, From e60cf2d311fe3ce647ff99d573bc11482da60feb Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Mon, 19 Jan 2026 06:49:11 +0000 Subject: [PATCH 10/16] linting fix --- mssql_python/cursor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index ed758238..025190ab 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2567,6 +2567,7 @@ def generate_data(): # 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) @@ -2643,7 +2644,7 @@ def generate_data(): logger.debug( "Failed to close bulk copy resource %s: %s", type(resource).__name__, - cleanup_error + cleanup_error, ) def __enter__(self): From 1501ff882f794302810a7550dee266dfd506a6a1 Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Mon, 19 Jan 2026 07:10:38 +0000 Subject: [PATCH 11/16] review comments --- mssql_python/cursor.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 025190ab..5050ac84 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2451,13 +2451,9 @@ def nextset(self) -> Union[bool, None]: ) return True - def _bulkcopy(self, table_name: str, data, **kwargs): + def _bulkcopy(self, table_name: str, data, **kwargs): # pragma: no cover """ - Perform bulk copy operation using Rust-based implementation. - - This method uses a separate connection to the database via the Rust library - (mssql_py_core) for optimized bulk data transfer. The connection parameters - are extracted from the current Python connection's connection string. + Perform bulk copy operation for high-performance data loading. Important: Transaction Isolation The bulk copy operation creates its own connection and does NOT participate @@ -2509,9 +2505,27 @@ def generate_data(): for i in range(1000000): yield (i, f'Name_{i}', datetime.date.today()) - **kwargs: Additional options passed to the Rust bulkcopy method - - column_mappings: List of tuples mapping source column index to - target column name, e.g., [(0, 'id'), (1, 'name')] + **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. + + Example: + # Source data has columns: [id, first_name, last_name, age] + # Target table has columns: [user_id, name, age] + # Map source index 0 to 'user_id', index 1 to 'name', index 3 to 'age' + column_mappings = [(0, 'user_id'), (1, 'name'), (3, 'age')] + result = cursor._bulkcopy('users', data, column_mappings=column_mappings) Returns: Dictionary with bulk copy results including: @@ -2520,7 +2534,7 @@ def generate_data(): - elapsed_time: Time taken for the operation Raises: - ImportError: If mssql_py_core Rust library is not installed + ImportError: If mssql_py_core library is not installed ValueError: If table_name is empty or parameters are invalid RuntimeError: If connection string is not available From cf42d224b855f4a0daeb60af25317efcdbb8dc3d Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Mon, 19 Jan 2026 07:54:00 +0000 Subject: [PATCH 12/16] linting fix in main.py --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 2f8cf28c..7e56b2fe 100644 --- a/main.py +++ b/main.py @@ -15,4 +15,4 @@ print(f"Database ID: {row[0]}, Name: {row[1]}") cursor.close() -conn.close() \ No newline at end of file +conn.close() From ffad1d685f0ee83d61661cc6e1cb5f4450f5766c Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Mon, 19 Jan 2026 10:16:10 +0000 Subject: [PATCH 13/16] review comment --- mssql_python/cursor.py | 123 ++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 74 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 5050ac84..222b265a 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -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,20 +2451,12 @@ def nextset(self) -> Union[bool, None]: ) return True - def _bulkcopy(self, table_name: str, data, **kwargs): # pragma: no cover + def _bulkcopy( + self, table_name: str, data: Iterable[Union[Tuple, List]], **kwargs + ): # pragma: no cover """ Perform bulk copy operation for high-performance data loading. - Important: Transaction Isolation - The bulk copy operation creates its own connection and does NOT participate - in the current Python connection's transaction context. This means: - - Bulk copied data is committed independently of any open transaction - - Rolling back the Python connection will NOT rollback bulk copied data - - The bulk copy operation is essentially auto-committed - - If you need transactional bulk operations, consider using executemany() - or batched INSERT statements within your transaction instead. - Args: table_name: Target table name (can include schema, e.g., 'dbo.MyTable'). The table must exist and the user must have INSERT permissions. @@ -2479,32 +2471,6 @@ def _bulkcopy(self, table_name: str, data, **kwargs): # pragma: no cover - The number of values in each row must match the number of columns in the target table - Supported Python Types and SQL Server Mappings: - - None → NULL (for any nullable column) - - int → INT, BIGINT, SMALLINT, TINYINT - - float → FLOAT, REAL - - str → VARCHAR, NVARCHAR, CHAR, NCHAR, TEXT, NTEXT - - bool → BIT - - datetime.date → DATE - - datetime.datetime → DATETIME, DATETIME2, SMALLDATETIME - - datetime.time → TIME - - decimal.Decimal → DECIMAL, NUMERIC, MONEY, SMALLMONEY - - bytes → BINARY, VARBINARY, IMAGE - - uuid.UUID → UNIQUEIDENTIFIER - - NULL Values: - - Use Python's None to represent SQL NULL values - - Empty strings ('') are NOT treated as NULL - - Example data formats: - # Simple list of tuples - data = [(1, 'Alice', None), (2, 'Bob', 25)] - - # Generator for memory-efficient large datasets - def generate_data(): - for i in range(1000000): - yield (i, f'Name_{i}', datetime.date.today()) - **kwargs: Additional bulk copy options. column_mappings (List[Tuple[int, str]], optional): @@ -2520,13 +2486,6 @@ def generate_data(): source columns are ignored, and unmapped target columns must have default values or allow NULL. - Example: - # Source data has columns: [id, first_name, last_name, age] - # Target table has columns: [user_id, name, age] - # Map source index 0 to 'user_id', index 1 to 'name', index 3 to 'age' - column_mappings = [(0, 'user_id'), (1, 'name'), (3, 'age')] - result = cursor._bulkcopy('users', data, column_mappings=column_mappings) - Returns: Dictionary with bulk copy results including: - rows_copied: Number of rows successfully copied @@ -2535,28 +2494,15 @@ def generate_data(): 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 - - Example: - >>> # Basic usage with list of tuples - >>> data = [(1, 'Alice', None), (2, 'Bob', 25), (3, 'Charlie', 30)] - >>> result = cursor._bulkcopy('users', data) - >>> print(f"Copied {result['rows_copied']} rows") - - >>> # Using a generator for large datasets - >>> from decimal import Decimal - >>> from datetime import date - >>> def generate_orders(): - ... for i in range(10000): - ... yield (i, Decimal('99.99'), date.today(), None) - >>> result = cursor._bulkcopy('orders', generate_orders()) """ try: import mssql_py_core except ImportError as exc: raise ImportError( - "Bulk copy requires the mssql_py_core Rust library which is not installed. " + "Bulk copy requires the mssql_py_core library which is not installed. " "To install, run: pip install mssql_py_core " "or install from the wheel file in the BCPRustWheel directory of the mssql-python repository: " "pip install BCPRustWheel/mssql_py_core--.whl" @@ -2566,12 +2512,34 @@ def generate_data(): 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", 1000) + batch_size = kwargs.get("batch_size", 0) timeout = kwargs.get("timeout", 30) + # Validate batch_size type and value + 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}") @@ -2594,7 +2562,7 @@ def generate_data(): "Specify the target database explicitly to avoid accidentally writing to system databases." ) - # Build connection context for Rust library + # 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") @@ -2623,20 +2591,27 @@ def generate_data(): # Extract password separately to avoid storing it in generic context that may be logged password = params.get("pwd", "") - rust_context = dict(context) - rust_context["password"] = password + pycore_context = dict(context) + pycore_context["password"] = password - rust_connection = None - rust_cursor = None + pycore_connection = None + pycore_cursor = None try: - rust_connection = mssql_py_core.PyCoreConnection(rust_context) - rust_cursor = rust_connection.cursor() + pycore_connection = mssql_py_core.PyCoreConnection(pycore_context) + pycore_cursor = pycore_connection.cursor() - result = rust_cursor.bulkcopy(table_name, iter(data), **kwargs) + 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 @@ -2644,11 +2619,11 @@ def generate_data(): finally: # Clear sensitive data to minimize memory exposure password = "" - if rust_context: - rust_context["password"] = "" - rust_context["user_name"] = "" - # Clean up Rust resources - for resource in (rust_cursor, rust_connection): + 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() From b0c536f2f6f2437791866c3962bfd83f3966303a Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Mon, 19 Jan 2026 10:27:28 +0000 Subject: [PATCH 14/16] linting fix --- tests/test_004_cursor.py | 123 --------------------------------------- 1 file changed, 123 deletions(-) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index f63972f1..9d0c72cf 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -14985,129 +14985,6 @@ def test_zero_length_complex_types(cursor, db_connection): db_connection.commit() -def test_guid_with_nulls(cursor, db_connection): - """Test GUID type with NULL values""" - try: - drop_table_if_exists(cursor, "#pytest_guid_nulls") - cursor.execute( - """ - CREATE TABLE #pytest_guid_nulls ( - id INT, - guid_col UNIQUEIDENTIFIER - ) - """ - ) - db_connection.commit() - - # Insert NULL GUID - cursor.execute("INSERT INTO #pytest_guid_nulls VALUES (1, NULL)") - # Insert actual GUID - cursor.execute("INSERT INTO #pytest_guid_nulls VALUES (2, NEWID())") - db_connection.commit() - - cursor.execute("SELECT id, guid_col FROM #pytest_guid_nulls ORDER BY id") - rows = cursor.fetchall() - - assert len(rows) == 2, "Should have exactly 2 rows" - assert rows[0][1] is None, "First GUID should be NULL" - assert rows[1][1] is not None, "Second GUID should not be NULL" - - except Exception as e: - pytest.fail(f"GUID with NULLs test failed: {e}") - finally: - drop_table_if_exists(cursor, "#pytest_guid_nulls") - db_connection.commit() - - -def test_datetimeoffset_with_nulls(cursor, db_connection): - """Test DATETIMEOFFSET type with NULL values""" - try: - drop_table_if_exists(cursor, "#pytest_dto_nulls") - cursor.execute( - """ - CREATE TABLE #pytest_dto_nulls ( - id INT, - dto_col DATETIMEOFFSET - ) - """ - ) - db_connection.commit() - - # Insert NULL DATETIMEOFFSET - cursor.execute("INSERT INTO #pytest_dto_nulls VALUES (1, NULL)") - # Insert actual DATETIMEOFFSET - cursor.execute("INSERT INTO #pytest_dto_nulls VALUES (2, SYSDATETIMEOFFSET())") - db_connection.commit() - - cursor.execute("SELECT id, dto_col FROM #pytest_dto_nulls ORDER BY id") - rows = cursor.fetchall() - - assert len(rows) == 2, "Should have exactly 2 rows" - assert rows[0][1] is None, "First DATETIMEOFFSET should be NULL" - assert rows[1][1] is not None, "Second DATETIMEOFFSET should not be NULL" - - except Exception as e: - pytest.fail(f"DATETIMEOFFSET with NULLs test failed: {e}") - finally: - drop_table_if_exists(cursor, "#pytest_dto_nulls") - db_connection.commit() - - -def test_decimal_conversion_edge_cases(cursor, db_connection): - """Test DECIMAL/NUMERIC type conversion including edge cases""" - try: - drop_table_if_exists(cursor, "#pytest_decimal_edge") - cursor.execute( - """ - CREATE TABLE #pytest_decimal_edge ( - id INT, - dec_col DECIMAL(18, 4) - ) - """ - ) - db_connection.commit() - - # Insert various decimal values including edge cases - test_values = [ - (1, "123.4567"), - (2, "0.0001"), - (3, "-999999999999.9999"), - (4, "999999999999.9999"), - (5, "0.0000"), - ] - - for id_val, dec_val in test_values: - cursor.execute( - "INSERT INTO #pytest_decimal_edge VALUES (?, ?)", (id_val, decimal.Decimal(dec_val)) - ) - - # Also insert NULL - cursor.execute("INSERT INTO #pytest_decimal_edge VALUES (6, NULL)") - db_connection.commit() - - cursor.execute("SELECT id, dec_col FROM #pytest_decimal_edge ORDER BY id") - rows = cursor.fetchall() - - assert len(rows) == 6, "Should have exactly 6 rows" - - # Verify the values - for i, (id_val, expected_str) in enumerate(test_values): - assert rows[i][0] == id_val, f"Row {i} ID should be {id_val}" - assert rows[i][1] == decimal.Decimal( - expected_str - ), f"Row {i} decimal should match {expected_str}" - - # Verify NULL - assert rows[5][0] == 6, "Last row ID should be 6" - assert rows[5][1] is None, "Last decimal should be NULL" - - except Exception as e: - pytest.fail(f"Decimal conversion edge cases test failed: {e}") - finally: - drop_table_if_exists(cursor, "#pytest_decimal_edge") - db_connection.commit() - - def test_fixed_length_char_type(cursor, db_connection): """Test SQL_CHAR (fixed-length CHAR) column processor path (Lines 3464-3467)""" try: From ce1e64d01e21456cf50ed3c1e8192d2b80ad6eec Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Mon, 19 Jan 2026 10:37:01 +0000 Subject: [PATCH 15/16] removing duplicates to fix linting issue --- tests/test_004_cursor.py | 100 --------------------------------------- 1 file changed, 100 deletions(-) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 9d0c72cf..9037820b 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -14948,106 +14948,6 @@ def test_lob_binary_column_types(cursor, db_connection): db_connection.commit() -def test_zero_length_complex_types(cursor, db_connection): - """Test zero-length data for complex types (covers lines 3531-3533)""" - try: - drop_table_if_exists(cursor, "#pytest_zero_length") - cursor.execute( - """ - CREATE TABLE #pytest_zero_length ( - id INT, - empty_varchar VARCHAR(100), - empty_nvarchar NVARCHAR(100), - empty_binary VARBINARY(100) - ) - """ - ) - db_connection.commit() - - # Insert empty (non-NULL) values - cursor.execute("INSERT INTO #pytest_zero_length VALUES (?, ?, ?, ?)", (1, "", "", b"")) - db_connection.commit() - - cursor.execute( - "SELECT id, empty_varchar, empty_nvarchar, empty_binary FROM #pytest_zero_length" - ) - row = cursor.fetchone() - - assert row[0] == 1, "ID should be 1" - assert row[1] == "", "Empty VARCHAR should be empty string" - assert row[2] == "", "Empty NVARCHAR should be empty string" - assert row[3] == b"", "Empty VARBINARY should be empty bytes" - - except Exception as e: - pytest.fail(f"Zero-length complex types test failed: {e}") - finally: - drop_table_if_exists(cursor, "#pytest_zero_length") - db_connection.commit() - - -def test_fixed_length_char_type(cursor, db_connection): - """Test SQL_CHAR (fixed-length CHAR) column processor path (Lines 3464-3467)""" - try: - cursor.execute("CREATE TABLE #pytest_char_test (id INT, char_col CHAR(10))") - cursor.execute("INSERT INTO #pytest_char_test VALUES (1, 'hello')") - cursor.execute("INSERT INTO #pytest_char_test VALUES (2, 'world')") - - cursor.execute("SELECT char_col FROM #pytest_char_test ORDER BY id") - rows = cursor.fetchall() - - # CHAR pads with spaces to fixed length - assert len(rows) == 2, "Should fetch 2 rows" - assert rows[0][0].rstrip() == "hello", "First CHAR value should be 'hello'" - assert rows[1][0].rstrip() == "world", "Second CHAR value should be 'world'" - - cursor.execute("DROP TABLE #pytest_char_test") - except Exception as e: - pytest.fail(f"Fixed-length CHAR test failed: {e}") - - -def test_fixed_length_nchar_type(cursor, db_connection): - """Test SQL_WCHAR (fixed-length NCHAR) column processor path (Lines 3469-3472)""" - try: - cursor.execute("CREATE TABLE #pytest_nchar_test (id INT, nchar_col NCHAR(10))") - cursor.execute("INSERT INTO #pytest_nchar_test VALUES (1, N'hello')") - cursor.execute("INSERT INTO #pytest_nchar_test VALUES (2, N'世界')") # Unicode test - - cursor.execute("SELECT nchar_col FROM #pytest_nchar_test ORDER BY id") - rows = cursor.fetchall() - - # NCHAR pads with spaces to fixed length - assert len(rows) == 2, "Should fetch 2 rows" - assert rows[0][0].rstrip() == "hello", "First NCHAR value should be 'hello'" - assert rows[1][0].rstrip() == "世界", "Second NCHAR value should be '世界'" - - cursor.execute("DROP TABLE #pytest_nchar_test") - except Exception as e: - pytest.fail(f"Fixed-length NCHAR test failed: {e}") - - -def test_fixed_length_binary_type(cursor, db_connection): - """Test SQL_BINARY (fixed-length BINARY) column processor path (Lines 3474-3477)""" - try: - cursor.execute("CREATE TABLE #pytest_binary_test (id INT, binary_col BINARY(8))") - cursor.execute("INSERT INTO #pytest_binary_test VALUES (1, 0x0102030405)") - cursor.execute("INSERT INTO #pytest_binary_test VALUES (2, 0xAABBCCDD)") - - cursor.execute("SELECT binary_col FROM #pytest_binary_test ORDER BY id") - rows = cursor.fetchall() - - # BINARY pads with zeros to fixed length (8 bytes) - assert len(rows) == 2, "Should fetch 2 rows" - assert len(rows[0][0]) == 8, "BINARY(8) should be 8 bytes" - assert len(rows[1][0]) == 8, "BINARY(8) should be 8 bytes" - # First 5 bytes should match, rest padded with zeros - assert ( - rows[0][0][:5] == b"\x01\x02\x03\x04\x05" - ), "First BINARY value should start with inserted bytes" - assert rows[0][0][5:] == b"\x00\x00\x00", "BINARY should be zero-padded" - - cursor.execute("DROP TABLE #pytest_binary_test") - except Exception as e: - pytest.fail(f"Fixed-length BINARY test failed: {e}") def test_fetchall_with_integrity_constraint(cursor, db_connection): From f698b978f0a092dd1aa8496bd0c63a5ece70235f Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Mon, 19 Jan 2026 10:43:11 +0000 Subject: [PATCH 16/16] linting issues --- tests/test_004_cursor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 9037820b..1b5dc15f 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -14948,8 +14948,6 @@ def test_lob_binary_column_types(cursor, db_connection): db_connection.commit() - - def test_fetchall_with_integrity_constraint(cursor, db_connection): """ Test that UNIQUE constraint errors are appropriately triggered for multi-row INSERT