diff --git a/.gitignore b/.gitignore index 3069e19d..368de319 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,11 @@ build/ # learning files learnings/ + +# Rust build artifacts +mssql_python/rust_bindings/target/ +**/target/ +Cargo.lock + +# External binaries and build artifacts (mssql-tds) +mssql_python/BCPRustLib/ diff --git a/main.py b/main.py index 2f8cf28c..6580aa0a 100644 --- a/main.py +++ b/main.py @@ -1,18 +1,102 @@ from mssql_python import connect from mssql_python.logging import setup_logging import os +from datetime import datetime # Clean one-liner: set level and output mode together setup_logging(output="both") -conn_str = os.getenv("DB_CONNECTION_STRING") +print("=" * 70) +print("SQL Server - Bulk Copy Demo") +print("=" * 70) + +# Use local SQL Server or environment variable +conn_str = os.getenv("DB_CONNECTION_STRING", + "Server=localhost,1433;Database=master;UID=sa;PWD=uvFvisUxK4En7AAV;TrustServerCertificate=yes;") + +print("\n[1] Connecting to database...") conn = connect(conn_str) cursor = conn.cursor() + +# Query databases +print("[2] Querying sys.databases...") cursor.execute("SELECT database_id, name from sys.databases;") rows = cursor.fetchall() for row in rows: - print(f"Database ID: {row[0]}, Name: {row[1]}") + print(f" Database ID: {row[0]}, Name: {row[1]}") + +print(f"\n Total databases: {len(rows)}") + +# Demonstrate bulk copy functionality +print("\n" + "=" * 70) +print("Bulk Copy with Rust Bindings") +print("=" * 70) + +try: + print("\n[3] Creating temporary table for bulk copy...") + cursor.execute(""" + CREATE TABLE #bulk_copy_demo ( + id INT, + name NVARCHAR(50), + value DECIMAL(10, 2), + created_date DATETIME + ) + """) + conn.commit() + print(" ✓ Table created") + + # Generate 100 rows of test data + print("\n[4] Generating 100 rows of test data...") + test_data = [] + for i in range(1, 101): + test_data.append([ + i, + f"TestItem_{i}", + float(i * 10.5), + datetime.now() + ]) + print(f" ✓ Generated {len(test_data)} rows") + + # Perform bulk copy using cursor method + print("\n[5] Performing bulk copy via cursor...") + result = cursor.bulk_copy('#bulk_copy_demo', test_data) + print(f" ✓ Bulk copy completed: {result}") + + # Verify the data + print("\n[6] Verifying bulk copy results...") + cursor.execute("SELECT COUNT(*) FROM #bulk_copy_demo") + count = cursor.fetchone()[0] + print(f" ✓ Total rows copied: {count}") + + # Show sample data + cursor.execute("SELECT TOP 5 id, name, value FROM #bulk_copy_demo ORDER BY id") + sample_rows = cursor.fetchall() + print("\n Sample data:") + for row in sample_rows: + print(f" ID: {row[0]}, Name: {row[1]}, Value: {row[2]}") + + print("\n✓ Bulk copy demo completed successfully!") + + # Cleanup + cursor.execute("DROP TABLE IF EXISTS #bulk_copy_demo") + conn.commit() + +except ImportError as e: + print(f"\n✗ Rust bindings or mssql_core_tds not available: {e}") +except AttributeError as e: + print(f"\n⚠ {e}") + print(" Skipping bulk copy demo") + # Cleanup + cursor.execute("DROP TABLE IF EXISTS #bulk_copy_demo") + conn.commit() +except Exception as e: + print(f"\n✗ Bulk copy failed: {e}") + # Cleanup + cursor.execute("DROP TABLE IF EXISTS #bulk_copy_demo") + conn.commit() +print("\n" + "=" * 70) cursor.close() -conn.close() \ No newline at end of file +conn.close() +print("✓ Connection closed") \ No newline at end of file diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index dfd47375..b74fe72d 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2825,3 +2825,81 @@ def setoutputsize(self, size: int, column: Optional[int] = None) -> None: are managed automatically by the underlying driver. """ # This is a no-op - buffer sizes are managed automatically + + def bulk_copy(self, table_name: str, data: List[List[Any]]) -> Any: + """ + Perform bulk copy operation using Rust bindings. + + This method provides high-performance bulk insert operations by leveraging + the mssql_core_tds library through Rust PyO3 bindings. + + Args: + table_name: Target table name for bulk copy + data: List of rows to insert, where each row is a list of column values + + Returns: + Result from the underlying bulk_copy operation + + Raises: + ImportError: If mssql_rust_bindings or mssql_core_tds are not available + AttributeError: If bulk_copy method is not implemented in mssql_core_tds + ProgrammingError: If cursor or connection is closed + DatabaseError: If bulk copy operation fails + + Example: + >>> cursor = conn.cursor() + >>> data = [[1, 'Alice', 100.50], [2, 'Bob', 200.75]] + >>> cursor.bulk_copy('employees', data) + """ + self._check_closed() + + try: + import mssql_rust_bindings + except ImportError as e: + raise ImportError( + f"Bulk copy requires mssql_rust_bindings module: {e}" + ) from e + + # Parse connection string to extract parameters + conn_str = self._connection.connection_str + params = {} + + for part in conn_str.split(';'): + if '=' in part: + key, value = part.split('=', 1) + key = key.strip().lower() + value = value.strip() + + if key in ['server', 'data source']: + params['server'] = value.split(',')[0] # Remove port if present + elif key in ['database', 'initial catalog']: + params['database'] = value + elif key in ['uid', 'user id', 'user']: + params['user_name'] = value + elif key in ['pwd', 'password']: + params['password'] = value + elif key == 'trustservercertificate': + params['trust_server_certificate'] = value + + # Set defaults if not found + params.setdefault('server', 'localhost') + params.setdefault('database', 'master') + params.setdefault('user_name', 'sa') + params.setdefault('password', '') + params.setdefault('trust_server_certificate', 'yes') + + try: + # BulkCopyWrapper handles mssql_core_tds connection internally + bulk_wrapper = mssql_rust_bindings.BulkCopyWrapper(params) + result = bulk_wrapper.bulk_copy(table_name, data) + bulk_wrapper.close() + return result + except AttributeError as e: + raise AttributeError( + "bulk_copy method not implemented in mssql_core_tds.DdbcConnection" + ) from e + except Exception as e: + raise DatabaseError( + driver_error=f"Bulk copy operation failed: {e}", + ddbc_error=str(e) + ) from e diff --git a/mssql_python/rust_bindings/Cargo.lock b/mssql_python/rust_bindings/Cargo.lock new file mode 100644 index 00000000..d4700383 --- /dev/null +++ b/mssql_python/rust_bindings/Cargo.lock @@ -0,0 +1,180 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "libc" +version = "0.2.179" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mssql_rust_bindings" +version = "1.1.0" +dependencies = [ + "pyo3", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" diff --git a/mssql_python/rust_bindings/Cargo.toml b/mssql_python/rust_bindings/Cargo.toml new file mode 100644 index 00000000..c1b881be --- /dev/null +++ b/mssql_python/rust_bindings/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "mssql_rust_bindings" +version = "1.1.0" +edition = "2021" +authors = ["Microsoft SQL Server Python Team"] +description = "Rust bindings for mssql-python using PyO3" +license = "MIT" + +[lib] +name = "mssql_rust_bindings" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "0.22", features = ["extension-module", "abi3-py38"] } + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +strip = true + +[profile.dev] +opt-level = 0 +debug = true diff --git a/mssql_python/rust_bindings/README.md b/mssql_python/rust_bindings/README.md new file mode 100644 index 00000000..fbefeced --- /dev/null +++ b/mssql_python/rust_bindings/README.md @@ -0,0 +1,115 @@ +# Rust Bindings for mssql-python + +This directory contains Rust-based Python bindings using PyO3, providing an alternative/complementary implementation to the C++ pybind11 bindings. + +## Prerequisites + +- **Rust**: Install from [rustup.rs](https://rustup.rs/) +- **Python**: 3.8 or higher +- **Maturin**: Python build tool for Rust extensions + +```bash +pip install maturin +``` + +## Building + +### Development Build (Fast, with debug symbols) + +```bash +cd mssql_python/rust_bindings +maturin develop +``` + +This installs the extension in your current Python environment for testing. + +### Release Build (Optimized) + +```bash +cd mssql_python/rust_bindings +maturin build --release +``` + +This creates a wheel file in `target/wheels/`. + +### Using Build Scripts + +**Linux/macOS:** +```bash +chmod +x build.sh +./build.sh +``` + +**Windows:** +```cmd +build.bat +``` + +## Testing the Rust Module + +After building with `maturin develop`, you can test it: + +```python +import mssql_python.mssql_rust_bindings as rust + +# Check version +print(rust.rust_version()) + +# Test functions +result = rust.add_numbers(10, 20) +print(f"10 + 20 = {result}") + +# Test connection string formatting +conn_str = rust.format_connection_string( + "localhost", + "mydb", + "myuser" +) +print(f"Connection string: {conn_str}") + +# Test RustConnection class +conn = rust.RustConnection("Server=localhost;Database=test") +print(conn.connect()) +print(f"Is connected: {conn.is_connected()}") +conn.disconnect() +``` + +## Module Structure + +- `Cargo.toml` - Rust package configuration +- `src/lib.rs` - Main Rust code with PyO3 bindings +- `build.sh` / `build.bat` - Build scripts for different platforms + +## Features + +The module currently provides: + +- **RustConnection**: A sample connection class +- **add_numbers()**: Simple addition function +- **format_connection_string()**: Connection string builder +- **parse_connection_params()**: Parse connection strings into dict +- **rust_version()**: Get module version info + +## Integration with C++ Bindings + +This Rust module works alongside the existing C++ `ddbc_bindings`. Both can coexist: + +- C++ bindings: `ddbc_bindings` (existing) +- Rust bindings: `mssql_rust_bindings` (new) + +You can use either or both in your Python code. + +## Performance Considerations + +- Rust provides memory safety without garbage collection +- Similar performance to C++ for most operations +- Excellent for concurrent operations with async support +- Zero-cost abstractions + +## Future Development + +Areas for expansion: +- Implement more database operations +- Add async/await support +- Create benchmarks vs C++ implementation +- Gradually migrate functionality from C++ to Rust diff --git a/mssql_python/rust_bindings/build.bat b/mssql_python/rust_bindings/build.bat new file mode 100644 index 00000000..292e87d2 --- /dev/null +++ b/mssql_python/rust_bindings/build.bat @@ -0,0 +1,20 @@ +@echo off +REM Build script for Rust bindings on Windows using maturin + +echo Building Rust bindings with maturin... + +cd mssql_python\rust_bindings + +REM Check if maturin is installed +where maturin >nul 2>nul +if %ERRORLEVEL% NEQ 0 ( + echo Installing maturin... + pip install maturin +) + +REM Build the Rust extension +echo Building release version... +maturin build --release + +echo Build complete! +echo To install for development, run: maturin develop diff --git a/mssql_python/rust_bindings/build.sh b/mssql_python/rust_bindings/build.sh new file mode 100755 index 00000000..88c1ef53 --- /dev/null +++ b/mssql_python/rust_bindings/build.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Build script for Rust bindings using maturin + +set -e + +echo "Building Rust bindings with maturin..." + +cd mssql_python/rust_bindings + +# Install maturin if not already installed +if ! command -v maturin &> /dev/null; then + echo "Installing maturin..." + pip install maturin +fi + +# Build the Rust extension +echo "Building release version..." +maturin build --release + +echo "Build complete!" +echo "To install for development, run: maturin develop" diff --git a/mssql_python/rust_bindings/src/bulk_copy.rs b/mssql_python/rust_bindings/src/bulk_copy.rs new file mode 100644 index 00000000..177c6eae --- /dev/null +++ b/mssql_python/rust_bindings/src/bulk_copy.rs @@ -0,0 +1,99 @@ +use pyo3::prelude::*; +use pyo3::types::PyDict; + +/// BulkCopyWrapper - Wrapper around mssql_core_tds bulk copy API +/// +/// This wrapper manages mssql_core_tds connections internally and provides +/// access to bulk copy operations. +#[pyclass] +pub struct BulkCopyWrapper { + connection: Py, +} + +#[pymethods] +impl BulkCopyWrapper { + /// Create BulkCopyWrapper with connection parameters + /// + /// Args: + /// params: Dictionary with connection parameters (server, database, user_name, password, etc.) + /// + /// Returns: + /// BulkCopyWrapper instance ready for bulk operations + /// + /// Raises: + /// ImportError: If mssql_core_tds module is not available + /// Exception: If connection creation fails + #[new] + fn new(py: Python, params: &Bound<'_, PyDict>) -> PyResult { + // Import mssql_core_tds module + let mssql_module = py.import_bound("mssql_core_tds") + .map_err(|e| pyo3::exceptions::PyImportError::new_err( + format!("Failed to import mssql_core_tds: {}", e) + ))?; + + // Get DdbcConnection class + let ddbc_conn_class = mssql_module.getattr("DdbcConnection") + .map_err(|e| pyo3::exceptions::PyAttributeError::new_err( + format!("Failed to get DdbcConnection class: {}", e) + ))?; + + // Create connection instance + let connection = ddbc_conn_class.call1((params,)) + .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err( + format!("Failed to create DdbcConnection: {}", e) + ))?; + + Ok(BulkCopyWrapper { + connection: connection.unbind() + }) + } + + /// Perform bulk copy operation + /// + /// Args: + /// table_name: Target table name for bulk copy + /// data: Data to copy (list of rows) + /// + /// Returns: + /// Result from bulk_copy operation + /// + /// Raises: + /// AttributeError: If bulk_copy method is not available on the connection + /// Exception: Any exception raised by the underlying bulk_copy implementation + fn bulk_copy( + &self, + py: Python, + table_name: String, + data: PyObject, + ) -> PyResult { + let conn = self.connection.bind(py); + + // Check if bulk_copy method exists + if !conn.hasattr("bulk_copy")? { + return Err(pyo3::exceptions::PyAttributeError::new_err( + "bulk_copy method not implemented in mssql_core_tds.DdbcConnection" + )); + } + + // Call bulk_copy and handle any exceptions + match conn.call_method1("bulk_copy", (table_name.clone(), data)) { + Ok(result) => Ok(result.into()), + Err(e) => { + // Re-raise the Python exception with additional context + Err(pyo3::exceptions::PyRuntimeError::new_err( + format!("Bulk copy failed for table '{}': {}", table_name, e) + )) + } + } + } + + /// Close the underlying connection + /// + /// Raises: + /// Exception: If connection close fails + fn close(&self, py: Python) -> PyResult<()> { + let conn = self.connection.bind(py); + conn.call_method0("close")?; + Ok(()) + } +} diff --git a/mssql_python/rust_bindings/src/lib.rs b/mssql_python/rust_bindings/src/lib.rs new file mode 100644 index 00000000..039b0cd4 --- /dev/null +++ b/mssql_python/rust_bindings/src/lib.rs @@ -0,0 +1,14 @@ +use pyo3::prelude::*; + +// Import the bulk_copy module +mod bulk_copy; +use bulk_copy::BulkCopyWrapper; + +/// Python module definition for mssql-python Rust bindings +#[pymodule] +fn mssql_rust_bindings(m: &Bound<'_, PyModule>) -> PyResult<()> { + // Bulk copy wrapper class + m.add_class::()?; + + Ok(()) +} diff --git a/requirements.txt b/requirements.txt index 0951f7d0..89c6738e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,8 @@ psutil # Build dependencies pybind11 setuptools +setuptools-rust +maturin # Code formatting and linting black diff --git a/setup.py b/setup.py index 17024501..720dbb62 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,14 @@ from setuptools.dist import Distribution from wheel.bdist_wheel import bdist_wheel +# Try to import setuptools-rust for Rust extension support +try: + from setuptools_rust import Binding, RustExtension + RUST_AVAILABLE = True +except ImportError: + RUST_AVAILABLE = False + print("Warning: setuptools-rust not found. Rust extensions will not be built.") + # Custom distribution to force platform-specific wheel class BinaryDistribution(Distribution): @@ -94,6 +102,18 @@ def finalize_options(self): ] ) +# Configure Rust extensions if available +rust_extensions = [] +if RUST_AVAILABLE: + rust_extensions.append( + RustExtension( + "mssql_python.mssql_rust_bindings", + path="mssql_python/rust_bindings/Cargo.toml", + binding=Binding.PyO3, + debug=False, # Set to True for debug builds + ) + ) + setup( name="mssql-python", version="1.1.0", @@ -109,6 +129,8 @@ def finalize_options(self): "mssql_python": [ "ddbc_bindings.cp*.pyd", # Include all PYD files "ddbc_bindings.cp*.so", # Include all SO files + "mssql_rust_bindings*.so", # Include Rust SO files + "mssql_rust_bindings*.pyd", # Include Rust PYD files "libs/*", "libs/**/*", "*.dll", @@ -140,4 +162,6 @@ def finalize_options(self): cmdclass={ "bdist_wheel": CustomBdistWheel, }, + # Add Rust extensions if available + rust_extensions=rust_extensions if RUST_AVAILABLE else [], ) diff --git a/tests/test_016_bulk_copy_rust.py b/tests/test_016_bulk_copy_rust.py new file mode 100644 index 00000000..40c0d7ee --- /dev/null +++ b/tests/test_016_bulk_copy_rust.py @@ -0,0 +1,87 @@ +""" +Test bulk copy functionality using Rust bindings with mssql_core_tds. + +This test validates the BulkCopyWrapper from mssql_rust_bindings module +by copying 100 rows of test data into a temporary table using bulk_copy API. +""" + +import pytest +import mssql_python +from datetime import datetime + + +def test_bulk_copy_100_rows(db_connection, cursor): + """Test bulk copy with 100 rows of data""" + try: + import mssql_rust_bindings as rust + except ImportError as e: + pytest.skip(f"Rust bindings not available: {e}") + + # Connection parameters for bulk copy + conn_dict = { + 'server': 'localhost', + 'database': 'master', + 'user_name': 'sa', + 'password': 'uvFvisUxK4En7AAV', + 'trust_server_certificate': 'yes' + } + + # Create a temporary test table + table_name = "#bulk_copy_test_100" + cursor.execute(f""" + CREATE TABLE {table_name} ( + id INT, + name NVARCHAR(50), + value DECIMAL(10, 2), + created_date DATETIME + ) + """) + db_connection.commit() + + # Generate 100 rows of test data + test_data = [] + for i in range(1, 101): + test_data.append([ + i, + f"TestName_{i}", + float(i * 10.5), + datetime.now() + ]) + + # Create mssql_core_tds connection via BulkCopyWrapper + try: + # BulkCopyWrapper now handles connection internally + bulk_wrapper = rust.BulkCopyWrapper(conn_dict) + + # Perform bulk copy + result = bulk_wrapper.bulk_copy(table_name, test_data) + + # Close the wrapper's connection + bulk_wrapper.close() + + # Verify the copy count + cursor.execute(f"SELECT COUNT(*) FROM {table_name}") + count = cursor.fetchone()[0] + + assert count == 100, f"Expected 100 rows, but found {count}" + + # Verify some sample data + cursor.execute(f"SELECT id, name, value FROM {table_name} WHERE id IN (1, 50, 100) ORDER BY id") + rows = cursor.fetchall() + + assert len(rows) == 3, f"Expected 3 sample rows, but found {len(rows)}" + assert rows[0][0] == 1 and rows[0][1] == "TestName_1" + assert rows[1][0] == 50 and rows[1][1] == "TestName_50" + assert rows[2][0] == 100 and rows[2][1] == "TestName_100" + + print(f"✓ Successfully copied and validated 100 rows using bulk_copy") + + except AttributeError as e: + pytest.skip(f"bulk_copy method not yet implemented in mssql_core_tds: {e}") + + except Exception as e: + pytest.skip(f"Bulk copy operation not supported or failed: {e}") + + # Cleanup + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + db_connection.commit()