FEAT: Arrow Support in Bulk Copy - #95
Conversation
Loosen resolve_kind so any integer Arrow type (Int8/16/32/64, UInt8/16/32) can target any integer SQL type (tinyint/smallint/int/bigint). narrow_int already range-checks each cell, so out-of-range values are rejected rather than wrapped. Fixes the common pandas/pyarrow case where the default int64 column could not be written to an INT column. Adds 4 unit tests.
Add UInt64 ColumnPlanKind mapping uint64 -> bigint. Values above i64::MAX are rejected with a clear error rather than wrapping to a negative bigint. Adds 2 unit tests (in-range, overflow).
…etime2 rejection C1: reject tz-aware timestamp -> DATETIME2 (points user at DATETIMEOFFSET); only tz-naive coerces. B1: resolve_arrow_reader accepts __arrow_c_array__ single-batch producers via _import_from_c_capsule. D2: release the GIL (py.detach) around the Tokio bulk-load block_on so other Python threads run during transfer; ArrowRowIter re-attaches per batch.
Add 21 test_bulkcopy_arrow_<type>.py files under mssql-py-core/tests/ mirroring the existing test_bulkcopy_<type>.py structure, driving cursor.bulkcopy_arrow with typed pyarrow sources. Covers bit, tinyint, smallint, int, bigint, float, decimal, numeric, nvarchar, nchar, ntext, varchar, char, text, binary, image, uuid, date, datetime2, time, datetimeoffset. 77 tests, all passing against SQL Server.
Fix rustfmt formatting to satisfy the 'Check Format' pipeline step (cargo fmt -- --check in mssql-py-core). Formatting-only; no logic changes.
📊 Code Coverage Report
Diff CoverageDiff: main...HEAD, staged and unstaged changes
Summary
mssql-py-core/src/arrow_bulkcopy.rsmssql-py-core/src/cursor.rs🔗 Quick Links |
…s, unsupported-type rejection) Mirror the bulkcopy cross-cutting suites for bulkcopy_arrow: options (keep_nulls/keep_identity/batch_size full; table_lock smoke; fire_triggers/check_constraints observable), column mismatch/mapping (incl by-name Arrow field mapping), multipart table names, use_internal_transaction + batch_size cadence, and rejection tests for out-of-matrix SQL types (datetime/smalldatetime/money/smallmoney/xml/sql_variant). Adds decimal precision-overflow case. 111 arrow tests pass against SQL Server.
…e docstring The removed test claimed column_mappings=['a','b'] performs Arrow-field-name matching, but List[str] mappings are by source ordinal (index -> dest name); true source-name mapping (List[(str,str)]) is rejected on the Arrow/iterator path. The remaining ordinal/tuple mapping cases already cover the supported behavior. Also corrected the batch_size test docstring: batch_count is derived arithmetically, so it does not assert on-wire commit cadence.
…ine job New test_bulkcopy_arrow_longhaul.py streams pyarrow.RecordBatch data into bulkcopy_arrow for a configurable duration over a 15-column Arrow-supported wide table. Parametrize test-longhaul-template.yml (testSelector/resultsFile/testRunTitle) and add pyarrow to its pip install; add a parallel LongHaul_BCP_Arrow_Test job so the Arrow long-haul runs alongside the tuple long-haul (-k arrow / -k 'not arrow') without doubling stage time. Validated locally: 342,500 rows in 20s.
The Python test/coverage step (dev/test-python.sh) did not install pyarrow, so every Arrow bulkcopy test hit pytest.importorskip('pyarrow') and skipped -- meaning the Arrow integration tests never ran in CI and contributed nothing to coverage. Add pyarrow to the dependency install.
There was a problem hiding this comment.
Pull request overview
Adds an Arrow-based bulk-copy ingestion path to mssql-py-core so Python callers can bulk insert from pyarrow sources via Arrow’s C data interface, alongside a large integration-test suite and CI wiring (including a dedicated Arrow long-haul job).
Changes:
- Introduces
PyCoreCursor.bulkcopy_arrow(...), Arrow source coercion, and an Arrow-to-TDS row adapter (arrow_bulkcopy). - Adds broad integration coverage for Arrow → SQL type mappings, options, and column mapping behavior (plus a new Arrow long-haul test).
- Updates Python test runners / pipelines to install
pyarrowand split Arrow long-haul execution into its own parallel job.
Reviewed changes
Copilot reviewed 34 out of 34 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| mssql-py-core/src/lib.rs | Registers the new arrow_bulkcopy module in the crate. |
| mssql-py-core/src/cursor.rs | Adds bulkcopy_arrow, Arrow source resolution, schema import, and Arrow batch iteration feeding the bulk-copy writer. |
| mssql-py-core/src/arrow_bulkcopy.rs | Implements Arrow type planning + per-row adaptation into BulkLoadRow for zero-copy-ish bulk-copy writes. |
| mssql-py-core/Cargo.toml | Adds the arrow dependency (FFI enabled) needed for Arrow C data interface interop. |
| dev/test-python.sh | Installs pyarrow in the Python test environment. |
| .pipeline/templates/validation-stages.yml | Adds a dedicated Arrow long-haul CI job and adjusts selectors to keep tuple/Arrow runs parallel. |
| .pipeline/templates/test-longhaul-template.yml | Parameterizes long-haul pytest selection/results output and installs pyarrow. |
| mssql-py-core/tests/test_bulkcopy_arrow_bigint.py | Integration coverage for BIGINT + uint64 overflow checks. |
| mssql-py-core/tests/test_bulkcopy_arrow_binary.py | Integration coverage for BINARY/VARBINARY Arrow ingestion and NULL constraints. |
| mssql-py-core/tests/test_bulkcopy_arrow_bit.py | Integration coverage for BIT Arrow ingestion and NULL constraints. |
| mssql-py-core/tests/test_bulkcopy_arrow_char.py | Integration coverage for CHAR Arrow ingestion and NULL constraints. |
| mssql-py-core/tests/test_bulkcopy_arrow_column_mismatch.py | Column mapping + count mismatch parity tests for the Arrow bulk copy path. |
| mssql-py-core/tests/test_bulkcopy_arrow_date.py | Integration coverage for DATE Arrow ingestion and NULL constraints. |
| mssql-py-core/tests/test_bulkcopy_arrow_datetime2.py | Integration coverage for DATETIME2 Arrow ingestion + tz-aware rejection behavior. |
| mssql-py-core/tests/test_bulkcopy_arrow_datetimeoffset.py | Integration coverage for DATETIMEOFFSET Arrow ingestion + UTC normalization behavior. |
| mssql-py-core/tests/test_bulkcopy_arrow_decimal.py | Integration coverage for DECIMAL Arrow ingestion, signs, and overflow scenarios. |
| mssql-py-core/tests/test_bulkcopy_arrow_float.py | Integration coverage for REAL/FLOAT Arrow ingestion and NULL constraints. |
| mssql-py-core/tests/test_bulkcopy_arrow_image.py | Integration coverage for IMAGE Arrow ingestion and NULL constraints. |
| mssql-py-core/tests/test_bulkcopy_arrow_int.py | Integration coverage for INT Arrow ingestion, int64 narrowing, and overflow checks. |
| mssql-py-core/tests/test_bulkcopy_arrow_longhaul.py | Adds an Arrow streaming long-haul stress test over a wide schema. |
| mssql-py-core/tests/test_bulkcopy_arrow_multipart_table_names.py | Arrow path parity tests for multipart/bracketed table-name handling. |
| mssql-py-core/tests/test_bulkcopy_arrow_nchar.py | Integration coverage for NCHAR Arrow ingestion and NULL constraints. |
| mssql-py-core/tests/test_bulkcopy_arrow_ntext.py | Integration coverage for NTEXT Arrow ingestion and NULL constraints. |
| mssql-py-core/tests/test_bulkcopy_arrow_numeric.py | Integration coverage for NUMERIC Arrow ingestion and NULL constraints. |
| mssql-py-core/tests/test_bulkcopy_arrow_nvarchar.py | Integration coverage for NVARCHAR Arrow ingestion incl. Unicode/MAX cases. |
| mssql-py-core/tests/test_bulkcopy_arrow_options.py | Integration coverage for bulk-copy options (triggers/constraints/nulls/identity/locks) via Arrow sources. |
| mssql-py-core/tests/test_bulkcopy_arrow_smallint.py | Integration coverage for SMALLINT Arrow ingestion and out-of-range handling. |
| mssql-py-core/tests/test_bulkcopy_arrow_text.py | Integration coverage for TEXT Arrow ingestion and NULL constraints. |
| mssql-py-core/tests/test_bulkcopy_arrow_time.py | Integration coverage for TIME Arrow ingestion and NULL constraints. |
| mssql-py-core/tests/test_bulkcopy_arrow_tinyint.py | Integration coverage for TINYINT Arrow ingestion and out-of-range handling. |
| mssql-py-core/tests/test_bulkcopy_arrow_transactions.py | Integration coverage for batch_size + use_internal_transaction semantics via Arrow sources. |
| mssql-py-core/tests/test_bulkcopy_arrow_unsupported_types.py | Integration coverage ensuring unsupported Arrow→SQL pairs fail fast with “not supported”. |
| mssql-py-core/tests/test_bulkcopy_arrow_uuid.py | Integration coverage for UNIQUEIDENTIFIER ingestion from 16-byte Arrow binary. |
| mssql-py-core/tests/test_bulkcopy_arrow_varchar.py | Integration coverage for VARCHAR ingestion incl. MAX case and NULL constraints. |
… json Extend the Arrow row-major writer to the remaining bulkcopy SQL types (except vector/variant): decimal128 -> MONEY/SMALLMONEY (integer rescale + lsb/msb split, no per-cell String), tz-naive timestamp -> DATETIME/SMALLDATETIME (reuses crate::types::datetime_to_ticks for 1/300s rounding parity; smalldatetime minute rounding), utf8/large_utf8 -> XML (UTF-16LE) and native JSON (UTF-8, no re-validation). tz-aware timestamp -> datetime/smalldatetime is rejected (C1). Adds 11 Rust unit tests and per-type Python integration tests; trims the unsupported-types test to SQL_VARIANT + tz-aware-datetime rejection. Validated vs SQL Server: 118 passed, 3 json skipped (needs SQL 2025).
…scale-0 temporal columns (2) ArrowRowIter no longer swallows batch read/import errors: it records them in a shared cell and bulkcopy_arrow surfaces the failure instead of reporting a partial (silently truncated) success. (3) pick_timestamp_scale now trusts the destination column's advertised scale, so explicit TIME(0)/DATETIME2(0)/DATETIMEOFFSET(0) are preserved instead of being overridden by the Arrow TimeUnit. Adds Rust unit tests (scale-0) and integration tests (DATETIME2(0) round-trip, mid-stream reader error). The RecordBatch import comment is a false positive - arrow re-exports RecordBatch from arrow::array and it compiles.
…haul coverage Review round 2: (A) the Arrow decimal path used the Arrow field's precision/scale and always emitted ColumnValues::Decimal. It now re-scales to the destination column's precision/scale (so decimal128(10,2) -> DECIMAL(10,4) stores 123.4500, not 1.2345) and emits ColumnValues::Numeric for NUMERIC columns, matching the tuple path. (B) the long-haul docstring claiming MONEY/SMALLMONEY/DATETIME are omitted was stale; the wide stress table now includes MONEY/SMALLMONEY/DATETIME/SMALLDATETIME/XML. Adds Rust unit tests (rescale, numeric variant) and integration tests (mismatched-scale round-trip). Validated: 121 passed, enriched long-haul green.
Addresses review (David-Engel): the planner only mapped FixedSizeBinary(16) to BINARY/VARBINARY, rejecting other widths (e.g. pa.binary(4) -> BINARY(4)) even though the extractor already handles FixedSizeBinaryArray of any width. Generalize the arm to FixedSizeBinary(_) for Binary/VarBinary/Image (mirroring the variable-width binary arm) while keeping FixedSizeBinary(16) -> UniqueIdentifier. Adds a Rust unit test and an integration test using genuine fixed-size binary (pa.binary(4)).
…1970 dates read_timestamp/read_time_ticks used truncate-toward-zero (raw / 100), which for pre-epoch (negative) nanosecond timestamps landed 100ns high vs floor. Use div_euclid(100) for sign-consistent flooring, matching the day/tick split elsewhere in the module. No effect on non-negative values (time is always >= 0). Adds a pre-epoch nanosecond regression test.
David-Engel
left a comment
There was a problem hiding this comment.
Can you improve the diff coverage to >= overall coverage? We rely heavily on test automation for validation so we want to maintain a high bar there.
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
David-Engel
left a comment
There was a problem hiding this comment.
Better! One more push for your agent to cover more should do it. I'd suggest they make sure to include negative tests, based on the current missing lines.
|
@gargsaumya merge from main. Then the unit test reporting in mssql-py-core will start showing up as well in the diff coverage. That would help with the test numbers a lot |
…UID->uniqueidentifier and by-name Arrow source mapping
Review Context:
mssql-py-core/src/arrow_bulkcopy.rs- the type contract: build_column_plans → resolve_kind (one dispatch per column per batch) → extract_value per cell. Every unsupported (Arrow, SQL) pair fails fast before any row is written.mssql-py-core/src/cursor.rs- the bulkcopy_arrow binding: GIL released for the transfer, re-acquired only to pump each batch; batch read/import errors are surfaced as a hard error.Pythonside is a thin wrapper mirroring bulkcopy.In scope / supported destinations: bit, all int/uint (range-checked narrowing; uint64→bigint overflow-checked), real/float, (n)char/(n)varchar/(n)text, binary/varbinary/image (incl. any-width fixed_size_binary), uniqueidentifier, date, datetime2, time, datetimeoffset, decimal/numeric, money/smallmoney, datetime/smalldatetime, xml, and json.
Testing: 37 Rust unit tests + 122 Arrow integration tests + a long-haul stress job wired into CI.
Description
This pull request introduces comprehensive support and test coverage for Arrow-based bulk copy operations in the
mssql-py-coreproject, focusing on the integration of the Arrow library for high-performance data loading. It adds new pyarrow-based tests for various SQL types, updates pipeline templates for more flexible test selection and reporting, and ensures the Arrow dependency is included in both the Rust and Python environments.Arrow Bulk Copy Feature and Test Coverage
cursor.bulkcopy_arrow) covering SQL types: BIGINT, BINARY/VARBINARY, BIT, and CHAR, including edge cases like null handling and type-specific constraints. [1] [2] [3] [4]arrowcrate as a dependency inmssql-py-core/Cargo.tomlto enable Arrow integration in the Rust backend.arrow_bulkcopyRust module to the project structure.Pipeline and Test Infrastructure Improvements
.pipeline/templates/test-longhaul-template.ymlto parameterize test selection, results file, and test run title, allowing for more flexible and parallelized long-haul test execution. [1] [2] [3].pipeline/templates/validation-stages.ymlto run Arrow-specific long-haul tests in parallel with tuple-based tests, optimizing CI wall-clock time.pyarrowis installed in both pipeline and local development scripts (dev/test-python.sh).These changes collectively improve the reliability, flexibility, and coverage of Arrow-based data loading in the project, and enhance the CI pipeline for future development.
Related Issues
Fixes #37 and AB#46077
Checklist
cargo bfmtpassescargo bclippypassescargo btestpasses