Skip to content

Transaction does not return error on next statement after breaking. #244

Open
@mgregur

Description

@mgregur

A transaction inserts two values into a table, and does a select on another table and returns no errors other than on commit.
After the commit finishes data is partially committed, only one row in Orders table. The transaction being broken by the select statement without a returned error in between the two insert statements.

package main

import (
	"database/sql"
	"fmt"
	"log"

	mssql "github.com/microsoft/go-mssqldb"
)

// The transaction inserts two values into Orders table, and returns no errors other than on commit.
// After the commit finisher data is partially committed, only one row in Orders table.
// The user has no way to tell there was an error in the transaction and half of it was rolled back other than committing,
// at which point it's too late.

// The error occurs because I sent the wrong data type to the query an integer rather than a string. You can fix the error by:
//  - change 12345 to "12345" on line 102
//  - remove second insert into Users, for user 'some_other_user' on line 76

// docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=password1!password1' -p 1433:1433  mcr.microsoft.com/mssql/server:2019-latest

const (
	dbConnectionString = "sqlserver://sa:password1!password1@localhost:1433?database=master&encrypt=disable"
)

func main() {
	connector, err := mssql.NewConnector(dbConnectionString)
	if err != nil {
		log.Println(err)
		return
	}

	connector.SessionInitSQL = "SET XACT_ABORT ON"
	db := sql.OpenDB(connector)
	if err != nil {
		log.Fatalf("failed to connect to db: %v", err)
	}
	defer db.Close()

	// Create the necessary tables. They have no relation to each other.
	_, err = db.Exec(`
		IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='Users' AND xtype='U')
		CREATE TABLE Users (
			id INT IDENTITY(1,1) PRIMARY KEY,
			username VARCHAR(50),
			code VARCHAR(50)
		)`)
	if err != nil {
		log.Fatalf("failed to create Users table: %v", err)
	}

	_, err = db.Exec(`
		IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='Orders' AND xtype='U')
		CREATE TABLE Orders (
			id INT IDENTITY(1,1) PRIMARY KEY,
			order_desc VARCHAR(100)
		)`)
	if err != nil {
		log.Fatalf("failed to create Orders table: %v", err)
	}

	// Clean any previous data
	_, err = db.Exec("DELETE FROM Users")
	if err != nil {
		log.Fatalf("failed to delete from Users table: %v", err)
	}

	_, err = db.Exec("DELETE FROM Orders")
	if err != nil {
		log.Fatalf("failed to delete from Orders table: %v", err)
	}

	_, err = db.Exec(
		"INSERT INTO Users (username, code) VALUES (@username, @code)",
		sql.Named("username", "some_user"),
		sql.Named("code", "12345"),
	)
	if err != nil {
		log.Fatalf("failed to insert user: %v", err)
	}

	// This user is newer explicitly searched for but it's existence with the non int castable code column break the transaction.
	_, err = db.Exec(
		"INSERT INTO Users (username, code) VALUES (@username, @code)",
		sql.Named("username", "some_other_user"),
		sql.Named("code", "not_castable_to_int"),
	)
	if err != nil {
		log.Fatalf("failed to insert user: %v", err)
	}

	tx, err := db.Begin()
	if err != nil {
		log.Fatalf("failed to start transaction: %v", err)
	}

	_, err = tx.Exec("INSERT INTO Orders (order_desc) VALUES (@order_desc)", sql.Named("order_desc", "First order"))
	if err != nil {
		tx.Rollback()
		log.Fatalf("failed to insert first order: %v", err)
	}

	var userResult string
	// This query will break the transaction and rollback the first insert. It however does not indicate any error has occurred.
	err = tx.QueryRow("SELECT username FROM Users WHERE code = @code", sql.Named("code", 12345)).Scan(&userResult)
	log.Printf("user query, err: %v, result %v", err, userResult)
	if err != nil {
		tx.Rollback()
		log.Fatalf("failed to query user: %v", err)
	}

	_, err = tx.Exec("INSERT INTO Orders (order_desc) VALUES (@order_desc)", sql.Named("order_desc", "Second order"))
	if err != nil {
		tx.Rollback()
		log.Fatalf("failed to insert second order: %v", err)
	}

	err = tx.Commit()
	if err != nil {
		log.Printf("failed to commit transaction: %v", err)
	} else {
		fmt.Println("Transaction committed successfully")
	}

	ordersInDb, err := db.Query("SELECT * FROM Orders")
	if err != nil {
		log.Fatalf("failed to query orders: %v", err)
	}

	log.Printf("Orders in the db:")
	for ordersInDb.Next() {
		var id int
		var orderDesc string
		err = ordersInDb.Scan(&id, &orderDesc)
		if err != nil {
			log.Fatalf("failed to scan order: %v", err)
		}
		fmt.Printf("Order id: %d, Order desc: %s\n", id, orderDesc)
	}
	log.Printf("======END================")
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions