forked from denisenkom/go-mssqldb
-
Notifications
You must be signed in to change notification settings - Fork 91
fix: prevent RowsAffected double-counting when triggers fire #363
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dlevy-msft-sql
wants to merge
4
commits into
microsoft:main
Choose a base branch
from
dlevy-msft-sql:fix/rows-affected-trigger-double-count
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+237
−2
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
78bbc9d
fix: prevent RowsAffected double-counting when triggers fire
dlevy-msft-sql f876715
test: add unit tests for doneStruct row count assignment semantics
dlevy-msft-sql dc1b893
fix: use assignment for doneInProcStruct to prevent RPC double-counting
dlevy-msft-sql c240049
test: add bulk update scenario while no-NOCOUNT trigger is active
dlevy-msft-sql File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| package mssql | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "math/rand" | ||
| "testing" | ||
| ) | ||
|
|
||
| // TestRowsAffectedWithTrigger verifies that RowsAffected returns the correct | ||
| // count for the outermost DML statement even when AFTER triggers fire and | ||
| // produce their own intermediate DONEINPROC row counts. See #204. | ||
| func TestRowsAffectedWithTrigger(t *testing.T) { | ||
| conn, logger := open(t) | ||
| defer conn.Close() | ||
| defer logger.StopLogging() | ||
|
|
||
| ctx := context.Background() | ||
|
|
||
| suffix := fmt.Sprintf("%d", rand.Intn(999999)) | ||
| tbl := "test_ra204_" + suffix | ||
| audit := "test_ra204_audit_" + suffix | ||
| trg := "tr_ra204_" + suffix | ||
|
|
||
| cleanup := func() { | ||
| conn.ExecContext(ctx, "drop trigger if exists "+trg) | ||
| conn.ExecContext(ctx, "drop table if exists "+audit) | ||
| conn.ExecContext(ctx, "drop table if exists "+tbl) | ||
| } | ||
| cleanup() | ||
| defer cleanup() | ||
|
|
||
| _, err := conn.ExecContext(ctx, "create table "+tbl+" (id int primary key, value nvarchar(100))") | ||
| if err != nil { | ||
| t.Fatal("create table failed:", err) | ||
| } | ||
|
|
||
| _, err = conn.ExecContext(ctx, "insert into "+tbl+" values (1, 'old'), (2, 'old'), (3, 'old')") | ||
| if err != nil { | ||
| t.Fatal("insert failed:", err) | ||
| } | ||
|
|
||
| // Scenario 1: Basic update without trigger | ||
| result, err := conn.ExecContext(ctx, "update "+tbl+" set value = 'test' where id = 1") | ||
| if err != nil { | ||
| t.Fatal("update failed:", err) | ||
| } | ||
| rowsAffected, _ := result.RowsAffected() | ||
| if rowsAffected != 1 { | ||
| t.Errorf("basic update: expected RowsAffected=1, got %d", rowsAffected) | ||
| } | ||
|
|
||
| // Create audit table and trigger without NOCOUNT | ||
| _, err = conn.ExecContext(ctx, "create table "+audit+" (id int identity, action nvarchar(50))") | ||
| if err != nil { | ||
| t.Fatal("create audit table failed:", err) | ||
| } | ||
| _, err = conn.ExecContext(ctx, "create trigger "+trg+" on "+tbl+" after update as begin insert into "+audit+" (action) select 'updated' from inserted end") | ||
| if err != nil { | ||
| t.Fatal("create trigger failed:", err) | ||
| } | ||
|
|
||
| // Scenario 2: Update with trigger (no NOCOUNT) - trigger produces extra DONEINPROC | ||
| result, err = conn.ExecContext(ctx, "update "+tbl+" set value = 'triggered' where id = 1") | ||
| if err != nil { | ||
| t.Fatal("triggered update failed:", err) | ||
| } | ||
| rowsAffected, _ = result.RowsAffected() | ||
| if rowsAffected != 1 { | ||
| t.Errorf("trigger without NOCOUNT: expected RowsAffected=1, got %d", rowsAffected) | ||
| } | ||
|
dlevy-msft-sql marked this conversation as resolved.
|
||
|
|
||
| // Scenario 2b: Bulk update all rows with no-NOCOUNT trigger active. | ||
| // With the old += bug, the trigger's DONEINPROC (3 rows) plus the | ||
| // outer DONE (3 rows) would yield 6 instead of 3. | ||
| result, err = conn.ExecContext(ctx, "update "+tbl+" set value = 'bulk'") | ||
| if err != nil { | ||
| t.Fatal("bulk update without NOCOUNT failed:", err) | ||
| } | ||
| rowsAffected, _ = result.RowsAffected() | ||
| if rowsAffected != 3 { | ||
| t.Errorf("bulk update without NOCOUNT: expected RowsAffected=3, got %d", rowsAffected) | ||
| } | ||
|
|
||
| // Scenario 3: Recreate trigger with NOCOUNT | ||
| conn.ExecContext(ctx, "drop trigger "+trg) | ||
|
dlevy-msft-sql marked this conversation as resolved.
|
||
| _, err = conn.ExecContext(ctx, "create trigger "+trg+" on "+tbl+" after update as begin set nocount on; insert into "+audit+" (action) select 'updated' from inserted end") | ||
| if err != nil { | ||
| t.Fatal("create nocount trigger failed:", err) | ||
| } | ||
|
|
||
| result, err = conn.ExecContext(ctx, "update "+tbl+" set value = 'nocount' where id = 1") | ||
| if err != nil { | ||
| t.Fatal("nocount triggered update failed:", err) | ||
| } | ||
| rowsAffected, _ = result.RowsAffected() | ||
| if rowsAffected != 1 { | ||
| t.Errorf("trigger with NOCOUNT: expected RowsAffected=1, got %d", rowsAffected) | ||
| } | ||
|
|
||
| // Scenario 4: Update all rows with trigger | ||
| result, err = conn.ExecContext(ctx, "update "+tbl+" set value = 'all'") | ||
| if err != nil { | ||
| t.Fatal("bulk update failed:", err) | ||
| } | ||
|
dlevy-msft-sql marked this conversation as resolved.
|
||
| rowsAffected, _ = result.RowsAffected() | ||
| if rowsAffected != 3 { | ||
| t.Errorf("bulk update with trigger: expected RowsAffected=3, got %d", rowsAffected) | ||
| } | ||
| } | ||
|
|
||
| // TestMultiStatementBatchRowsAffected verifies that RowsAffected returns the | ||
| // last statement's count for a multi-statement batch. The assignment (=) | ||
| // semantics in doneStruct processing mean each DONE token replaces (not | ||
| // accumulates) the row count, so the final DONE is authoritative. | ||
| func TestMultiStatementBatchRowsAffected(t *testing.T) { | ||
| conn, logger := open(t) | ||
| defer conn.Close() | ||
| defer logger.StopLogging() | ||
|
|
||
| ctx := context.Background() | ||
|
|
||
| suffix := fmt.Sprintf("%d", rand.Intn(999999)) | ||
| tbl := "test_msbatch_" + suffix | ||
|
|
||
| cleanup := func() { | ||
| conn.ExecContext(ctx, "drop table if exists "+tbl) | ||
| } | ||
| cleanup() | ||
| defer cleanup() | ||
|
|
||
| _, err := conn.ExecContext(ctx, "create table "+tbl+" (id int primary key, value nvarchar(100))") | ||
| if err != nil { | ||
| t.Fatal("create table failed:", err) | ||
| } | ||
|
|
||
| // Seed 5 rows. | ||
| _, err = conn.ExecContext(ctx, "insert into "+tbl+" values (1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')") | ||
| if err != nil { | ||
| t.Fatal("insert failed:", err) | ||
| } | ||
|
|
||
| // Multi-statement batch: first UPDATE touches 3 rows, second touches 2. | ||
| // RowsAffected should be 2 (the last statement), not 5 (the sum). | ||
| batch := "update " + tbl + " set value='x' where id <= 3; " + | ||
| "update " + tbl + " set value='y' where id > 3" | ||
| result, err := conn.ExecContext(ctx, batch) | ||
| if err != nil { | ||
| t.Fatal("multi-statement batch failed:", err) | ||
| } | ||
| rowsAffected, _ := result.RowsAffected() | ||
| if rowsAffected != 2 { | ||
| t.Errorf("multi-statement batch: expected RowsAffected=2 (last stmt), got %d", rowsAffected) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.