PERF: Optimize checked temporal fetch construction - #795
Conversation
Use direct CPython date/time/datetime construction for exact cached standard types, preserving substituted constructors and exception behavior. Cover row-wise and batch fetch contracts in isolated subprocesses. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Native extension changes span multiple fetch paths, with limited platform and validation coverage.
Pull request overview
Optimizes native SQL temporal fetch construction while preserving custom constructors and conversion behavior.
Changes:
- Adds checked datetime construction helpers.
- Integrates them into six temporal fetch paths.
- Adds regression tests and changelog documentation.
File summaries
| File | Reviewed changes |
|---|---|
tests/test_038_fetch_temporal.py |
Temporal parity, constructor, exception, and fetch API coverage |
mssql_python/pybind/fetch_temporal.hpp |
Translation-unit-local checked construction helpers |
mssql_python/pybind/ddbc_bindings.cpp |
Integration into row-wise and batch temporal fetch paths |
CHANGELOG.md |
Documents the optimization and preserved behavior |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
📊 Code Coverage Report
Diff CoverageDiff: main...HEAD, staged and unstaged changes
Summary
mssql_python/pybind/ddbc_bindings.cppLines 4467-4475 4467 case SQL_SS_TIME2: {
4468 const SQL_SS_TIME2_STRUCT& t2 = buffers.timeBuffers[col - 1][i];
4469 py::object timeObj =
4470 FetchTemporal::time(t2.hour, t2.minute, t2.second, t2.fraction / 1000);
! 4471 PyList_SET_ITEM(row, col - 1, timeObj.release().ptr());
4472 break;
4473 }
4474 case SQL_SS_TIMESTAMPOFFSET: {
4475 SQLULEN rowIdx = i;mssql_python/pybind/fetch_temporal.hppLines 13-23 13
14 // datetime.h keeps PyDateTimeAPI per translation unit, so these helpers must too.
15 static inline void ensure_datetime_api() {
16 if (PyDateTimeAPI == nullptr) {
! 17 PyDateTime_IMPORT;
! 18 if (PyDateTimeAPI == nullptr) throw py::error_already_set();
! 19 }
20 }
21
22 static inline py::object date(int year, int month, int day) {
23 ensure_datetime_api();📋 Files Needing Attention📉 Files with overall lowest coverage (click to expand)mssql_python.pybind.performance_counter.hpp: 0.7%
mssql_python.pybind.logger_bridge.cpp: 57.9%
mssql_python.pybind.ddbc_bindings.h: 64.1%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 78.4%
mssql_python.pybind.connection.connection.cpp: 82.5%
mssql_python.pybind.connection.connection_pool.cpp: 82.9%
mssql_python.logging.py: 86.2%
mssql_python.pooling.py: 90.1%
mssql_python.pybind.fetch_temporal.hpp: 92.1%🔗 Quick Links
|
PR Performance Report✅ Performance improved1 database task consistently improved across 1 measured environment. No consistent slowdowns were detected. 1 IMPROVEMENT 0 SLOWDOWNS 2/2 ENVIRONMENTS Signal fingerprint
Coverage: 2 of 2 environments completed. Advisory result; does not block merging. Measured timings
Performance diagnosticsPhase times are inclusive diagnostics and must not be added together. They identify where measured time changed, not why it changed. Unix / SQL Server 2025Row fetching in batches of 10,000: py::fetchmany::cpp_call -31.017 ms; ddbc::FetchMany_wrap -30.945 ms; ddbc::FetchBatchData -18.479 ms. All database tasks and timingsUnix / SQL Server 2022
Unix / SQL Server 2025
Build and measurement detailsPR head:
A consistent change requires more than 20% median paired movement, at least 1 ms between the median runtimes, and at least 80% of pairs exceeding the relative threshold in the same direction. A slowdown without enough pair agreement is reported as inconsistent. The displayed change is the median of paired before-and-after ratios. It is not recalculated from the two displayed median runtimes. Both revisions use profiling-enabled builds on the same agent and database, with alternating order and discarded warmups. Results are diagnostic and do not represent production-wheel latency. Raw samples and logs are attached to the ADO run as |
There was a problem hiding this comment.
🟡 Changes recommended
The isolated Linux wheel-validation job fails on a source-tree-relative module path assertion.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 1
- Review effort level: Lite
Preserve strict source-snapshot checks while allowing isolated wheel tests to validate the native module against its imported package. Add focused source and installed layout regression coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sumit Sarabhai (sumitmsft)
left a comment
There was a problem hiding this comment.
PR #795: No actionable findings in the reviewed changes. The previous installed-wheel provenance concern is resolved through conditional source validation. Checked temporal construction preserves ranges, precision, substitutions, and exception propagation.
### Work Item / Issue Reference > [AB#44819](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/44819) ------------------------------------------------------------------- ### Summary Improve the deterministic PR Performance Report presentation while preserving its existing measurements, thresholds, and advisory behavior. - Add explicit success, regression, review, unavailable, and clean verdict headings. - Summarize signal counts with compact badges. - Add a task-by-environment signal fingerprint without directional arrows or a spread column. - Keep exact before/after values in the expandable measured-timings table. - Rename and emphasize the diagnostic, complete-results, and build-detail sections. - Run same-repository report workflows from PR code so formatter changes can be validated end to end. Fork pull requests continue to use trusted base-branch code. This PR is stacked on #795. When merged into that branch, #795 will rerun its performance publisher using this formatter. After #795 reaches `main`, other pull requests receive the format when they synchronize with `main`. **Validation** - 118 profiler CI contract tests passed. - Workflow YAML parsing, Black, Flake8, and diff checks passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Gaurav Sharma (bewithgaurav)
left a comment
There was a problem hiding this comment.
no regressions observed, lgtm
Preserve main's row factory and temporal NULL indicators alongside the six checked temporal construction sites. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Work Item / Issue Reference
Summary
Reduce Python object-construction overhead when fetching DATE, TIME, and TIMESTAMP values. Previously, native temporal fields were converted into Python arguments and passed through a generic call to an already-cached constructor. Repeated imports were not the bottleneck.
The new helper uses checked CPython construction APIs when the cached constructor is the exact standard type; substituted constructors retain the original call. Six row-wise/batch conversion sites change. Field validation and final object allocation remain. NULLs, precision, timezone/fold, ownership, and exception behavior are preserved; DATETIMEOFFSET, UUID, Decimal, and text are untouched by this PR.
flowchart LR A["Native temporal fields after NULL checks"] --> B["Before: Python arguments and generic cached-constructor call"] A --> C{"After: exact standard type?"} C -->|"Yes"| D["Direct checked CPython construction"] C -->|"No: original fallback"| B B --> E["Validated Python object in result row"] D --> EFresh measurements
Temporal cases contain NULLs every seventh row. Pure cases have eight temporal columns; the row-wise case adds one harmless MAX column. Mixed has DATE/TIME/DATETIME2/DATETIMEOFFSET; narrow is an unchanged int/text/float control.
fetchmany(1000)fetchmany(1000)fetchmany(1000)fetchall()/ all remainingfetchone()/ 1fetchmany(1000)Method/build: September 21 Docker Linux x64; Python 3.13.15, pybind11 3.0.1, GCC 12.2 Release
-O3 -DNDEBUG, profiling OFF, SQL Server 16.0.4225.2, ODBC 18.6.2.1. Mainc963ee1eversus PR5aaa6aae: 10 counterbalanced pairs × 5 samples × 14 cases, totaling 1,400 validated drains. Reductions are ratios of medians, excluding execute/validation; no outlier removal or retries. Fallback checks observed three callbacks per temporal type with 3/4/7 positional arguments—not optimized-path or allocation counts.Limits: identical-build A/A calibration was noisy (speed-ratio interval 0.882×–1.345×). Mixed
fetchoneand bounded DATEfetchallremain inconclusive; unchanged narrow many/all had negative point estimates with intervals spanning zero. These are scoped bulk-temporal gains, not universal speedups or a no-regression guarantee.Both builds passed six fresh-process compatibility modes covering boundaries, NULLs, types, substitutions, exceptions, recovery, and both fetch paths. No full-suite or all-OS success is claimed. Complete samples, intervals, provenance, and historical limitations remain in retained local evidence.