Implement SQLRowCount - #151
Conversation
Report rows affected by the last INSERT/UPDATE/DELETE via SQLRowCount, matching msodbcsql: -1 (SQL_NO_ROWCOUNT_TOTAL) for SELECT/DDL/SET NOCOUNT ON and no HY010 check in the driver. Plumb the DONE-token count through TdsClient::last_rows_affected into StmtState.row_count (set on execute and refreshed by SQLMoreResults). Adds unit tests and a live e2e suite.
# Conflicts: # mssql-odbc/src/api/exec_direct.rs # mssql-odbc/src/handles/stmt.rs # mssql-odbc/tests/e2e/CMakeLists.txt
📊 Code Coverage Report
Diff CoverageDiff: main...HEAD, staged and unstaged changes
Summary
mssql-odbc/src/api/close_cursor.rsmssql-odbc/src/api/exec_common.rsmssql-odbc/src/api/exports.rsmssql-odbc/src/api/more_results.rs🔗 Quick Links |
There was a problem hiding this comment.
Pull request overview
Implements ODBC SQLRowCount by plumbing TDS DONE-token counts into statement state.
Changes:
- Tracks affected rows in
TdsClient. - Implements the safe ODBC
SQLRowCountentry point. - Adds unit and live end-to-end coverage.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
mssql-tds/src/connection/tds_client.rs |
Captures DONE-token row counts. |
mssql-odbc/src/handles/stmt.rs |
Stores per-statement row count. |
mssql-odbc/src/api/row_count.rs |
Implements SQLRowCount. |
mssql-odbc/src/api/exec_common.rs |
Copies counts after execution. |
mssql-odbc/src/api/exec_direct.rs |
Resets counts before direct execution. |
mssql-odbc/src/api/more_results.rs |
Refreshes counts across results. |
mssql-odbc/src/api/exports.rs |
Replaces the exported stub. |
mssql-odbc/src/api/mod.rs |
Registers the API module. |
mssql-odbc/tests/e2e/tests/row_count_test.cpp |
Adds live behavior tests. |
mssql-odbc/tests/e2e/CMakeLists.txt |
Registers the new test suite. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
mssql-tds/src/connection/tds_client.rs:613
- This reset only covers plain SQL batches. Parameterized
SQLExecDirectWusesexecute_sp_executesql, whileSQLExecuteusesexecute_sp_prepexec/execute_sp_execute; none of those entry points reset this field. After a DML sets a count, a later parameterized or prepared SELECT/DDL can therefore copy the stale count infinish_execute. Move the reset into the shared command-start path (or reset it in every execution entry point) so every new execution starts at-1.
// Reset the affected-row count so a stale value from a prior execute on
// this connection cannot leak into `SQLRowCount` for the new statement.
self.last_rows_affected = -1;
mssql-tds/src/connection/tds_client.rs:1884
- The value is only overwritten by another
DONE_COUNT, so it is not scoped to the result currently being exposed. ForINSERT ...; SELECT ...,move_to_column_metadataconsumes the INSERT's counted DONE and then stops at the SELECT metadata;finish_executeconsequently reports the INSERT count while the SELECT is current. Reset the value when positioning a metadata-bearing result and preserve counts as per-result state rather than one connection-global last-count field.
// Capture the affected-row count for `SQLRowCount`, but only
// when the DONE_COUNT flag is set — otherwise `row_count` is
// not meaningful (DDL, SET NOCOUNT ON) and must stay -1.
if done.status.contains(DoneStatus::COUNT) {
self.last_rows_affected = i64::try_from(done.row_count).unwrap_or(i64::MAX);
}
mssql-odbc/src/api/more_results.rs:109
- This refresh runs only when
move_to_next()findsCOLMETADATA. A DML result has no metadata, so a batch such asSELECT 1; UPDATE ...causesmove_to_next()to consume the UPDATE's counted DONE and returnfalse;SQLMoreResultsreturnsSQL_NO_DATAand the DML count is never exposed. The result iterator must represent no-column DML results soSQLMoreResultscan return success andSQLRowCountcan read their count.
// Refresh the count for the newly-positioned result set (-1 for a SELECT).
stmt_state.row_count = client.last_rows_affected();
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
mssql-tds/src/connection/tds_client.rs:613
- This reset only covers plain SQL batches.
SQLExecDirectWwith markers callsexecute_sp_executesql, andSQLExecutecallsexecute_sp_prepexec/execute_sp_execute; those paths callbegin_commandbut never reset this field. After a counted DML, a parameterized SELECT, DDL, orSET NOCOUNT ONexecution can therefore retain the old value, whichfinish_executecopies into the statement andSQLRowCountreports. Reset the field in the common per-command initialization (or every execution entry point) so RPC executions cannot inherit a prior command's count.
// Reset the affected-row count so a stale value from a prior execute on
// this connection cannot leak into `SQLRowCount` for the new statement.
self.last_rows_affected = -1;
mssql-odbc/src/api/more_results.rs:109
- This refresh runs only when
move_to_next()returnstrue, but that method returnstrueonly for a result withCOLMETADATA;move_to_column_metadataconsumes INSERT/UPDATE/DELETE DONE-only results while searching for metadata and returnsfalseat batch end. Consequently, forSELECT ...; UPDATE ...,SQLMoreResultsreturnsSQL_NO_DATAand the UPDATE count is never observable. To make row counts follow the currently positioned result, DONE-only results must be surfaced as results (or otherwise distinguished from true batch exhaustion).
// Refresh the count for the newly-positioned result set (-1 for a SELECT).
stmt_state.row_count = client.last_rows_affected();
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
mssql-odbc/src/api/exec_common.rs:277
- The shared completion path also covers
SQLExecute, but every new end-to-end row-count assertion executes DML throughSQLExecDirect. Prepared execution usessp_prepexecon the first call andsp_executethereafter, producing a differentDONEINPROC/DONEPROCtoken sequence, so neither RPC path is currently verified to preserve the DML count. Add a prepared-DML test that callsSQLRowCountafter both the initialSQLExecuteand a re-execution.
stmt_state.row_count = client.last_rows_affected();
52f8390 to
d0984fb
Compare
# Conflicts: # mssql-odbc/tests/e2e/CMakeLists.txt
Description
Implement
SQLRowCountfor the mssql-odbc driver, replacing the previous no-op stub.SQLRowCountnow reports the number of rows affected by the lastINSERT/UPDATE/DELETE, matching the classic msodbcsql C++ driver:DONEtoken.SET NOCOUNT ON→-1(
SQL_NO_ROWCOUNT_TOTAL).HY010sequence check in the driver — the Driver Manager rejects the callon an unexecuted statement before the driver is invoked, exactly as it does
for msodbcsql. Verified live against unixODBC.
GetRowInforecompute branch is intentionallyomitted (those cursor types are not yet supported) and documented in code.
How the count is plumbed
mssql-tds: newTdsClient::last_rows_affected()accessor backed by a fieldreset at each
execute()and populated from theDONEtoken when theDONE_COUNTflag is set.mssql-odbc:StmtState.row_countis set on execute (SQLExecDirectW) andrefreshed by
SQLMoreResultsso the count tracks the currently-positionedresult set.
SQLRowCountreads it, following the crate'ssafe-core/unsafe-shell +
ffi_entry!conventions.Tests
mssql-odbc/src/api/row_count.rs(null handle, null out-ptr,fresh statement →
-1via direct driver call, stored DML count).tests/e2e/tests/row_count_test.cpp— 14/14 e2e suitespass against a live SQL Server (INSERT/UPDATE/DELETE counts, SELECT/DDL/
SET NOCOUNT ON→-1, multi-result-set refresh, and the DM-enforcedHY010on a fresh statement).Related Issues
Closes AB#46414
Checklist
cargo bfmtpassescargo bclippypassescargo btestpasses