Skip to content

Commit

Permalink
Adding tests for Query and Exec behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
fulghum committed Jul 9, 2024
1 parent cbeff3c commit 7fda060
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 16 deletions.
102 changes: 102 additions & 0 deletions smoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,108 @@ func TestMultiStatements(t *testing.T) {
require.NoError(t, conn.Close())
}

// TestMultiStatementsExecContext tests that using ExecContext to run a multi-statement query works as expected and
// matches the behavior of the MySQL driver.
func TestMultiStatementsExecContext(t *testing.T) {
conn, cleanupFunc := initializeTestDatabaseConnection(t, false)
defer cleanupFunc()

ctx := context.Background()
_, err := conn.ExecContext(ctx, "CREATE TABLE example_table (id int, name varchar(256));")
require.NoError(t, err)

// ExecContext returns the results from the LAST statement executed. This differs from the behavior for QueryContext.
result, err := conn.ExecContext(ctx, "INSERT into example_table VALUES (999, 'boo'); "+
"INSERT into example_table VALUES (998, 'foo'); INSERT into example_table VALUES (997, 'goo'), (996, 'loo');")
require.NoError(t, err)
rowsAffected, err := result.RowsAffected()
require.NoError(t, err)
require.EqualValues(t, 2, rowsAffected)

// ExecContext returns an error if ANY of the statements can't be executed. This also differs from the behavior of QueryContext.
_, err = conn.ExecContext(ctx, "INSERT into example_table VALUES (100, 'woo'); "+
"INSERT into example_table VALUES (1, 2, 'too many'); SET @allStatementsExecuted=1;")
require.NotNil(t, err)
if !runTestsAgainstMySQL {
require.Equal(t, "Error 1105: number of values does not match number of columns provided", err.Error())
} else {
require.Equal(t, "Error 1136 (21S01): Column count doesn't match value count at row 1", err.Error())
}

// Once an error occurs, additional statements are NOT executed. This code tests that the last SET statement
// above was NOT executed.
rows, err := conn.QueryContext(ctx, "SELECT @allStatementsExecuted;")
var v any
require.NoError(t, err)
require.True(t, rows.Next())
require.NoError(t, rows.Scan(&v))
require.Nil(t, v)
require.NoError(t, rows.Close())
}

// TestMultiStatementsQueryContext tests that using QueryContext to run a multi-statement query works as expected and
// matches the behavior of the MySQL driver.
func TestMultiStatementsQueryContext(t *testing.T) {
conn, cleanupFunc := initializeTestDatabaseConnection(t, false)
defer cleanupFunc()

// QueryContext returns the results from the FIRST statement executed. This differs from the behavior for ExecContext.
ctx := context.Background()
rows, err := conn.QueryContext(ctx, "SELECT 1 FROM dual; SELECT 2 FROM dual; ")
require.NoError(t, err)
require.NoError(t, rows.Err())

var v any
require.True(t, rows.Next())
require.NoError(t, rows.Scan(&v))
require.EqualValues(t, 1, v)
require.False(t, rows.Next())

require.True(t, rows.NextResultSet())
require.True(t, rows.Next())
require.NoError(t, rows.Scan(&v))
require.EqualValues(t, 2, v)
require.False(t, rows.Next())

require.False(t, rows.NextResultSet())
require.NoError(t, rows.Close())

// QueryContext returns an error only if the FIRST statement can't be executed.
rows, err = conn.QueryContext(ctx, "SELECT * FROM no_table; SELECT 42 FROM dual;")
require.Nil(t, rows)
require.NotNil(t, err)
if !runTestsAgainstMySQL {
require.Equal(t, "Error 1146: table not found: no_table", err.Error())
} else {
require.Equal(t, "Error 1146 (42S02): Table 'testdb.no_table' doesn't exist", err.Error())
}

// To access the error for statements after the first statement, you must use rows.Err()
rows, err = conn.QueryContext(ctx, "SELECT 42 FROM dual; SELECT * FROM no_table; SET @allStatementsExecuted=1;")
require.NoError(t, err)
require.True(t, rows.Next())
require.NoError(t, rows.Scan(&v))
require.EqualValues(t, 42, v)
require.False(t, rows.Next())
require.False(t, rows.NextResultSet())
require.NotNil(t, rows.Err())
if !runTestsAgainstMySQL {
require.Equal(t, "Error 1146: table not found: no_table", rows.Err().Error())
} else {
require.Equal(t, "Error 1146 (42S02): Table 'testdb.no_table' doesn't exist", rows.Err().Error())
}
require.NoError(t, rows.Close())

// Once an error occurs, additional statements are NOT executed. This code tests that the last SET statement
// above was NOT executed.
rows, err = conn.QueryContext(ctx, "SELECT @allStatementsExecuted;")
require.NoError(t, err)
require.True(t, rows.Next())
require.NoError(t, rows.Scan(&v))
require.Nil(t, v)
require.NoError(t, rows.Close())
}

// TestMultiStatementsWithNoSpaces tests that multistatements are parsed correctly, even when
// there is no space between the statement delimiter and the next statement.
func TestMultiStatementsWithNoSpaces(t *testing.T) {
Expand Down
28 changes: 12 additions & 16 deletions statement.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,15 @@ func (d doltMultiStmt) NumInput() int {
return -1
}

func (d doltMultiStmt) Exec(args []driver.Value) (result driver.Result, retErr error) {
func (d doltMultiStmt) Exec(args []driver.Value) (result driver.Result, err error) {
for _, stmt := range d.stmts {
var err error
result, err = stmt.Exec(args)
if err != nil && retErr == nil {
// If any error occurs, record the first error, but continue executing all statements
retErr = err
if err != nil {
// If any error occurs, return the error and don't execute any more statements
return nil, err
}
}

// Return the first error encountered, if there was one
if retErr != nil {
return nil, retErr
}

// Otherwise, return the last result, to match the MySQL driver's behavior
return result, nil
}
Expand All @@ -57,15 +51,17 @@ func (d doltMultiStmt) Query(args []driver.Value) (driver.Rows, error) {
for _, stmt := range d.stmts {
rows, err := stmt.Query(args)
if err != nil {
// To match the MySQL driver's behavior, we attempt to execute all statements in a multi-statement
// query, even if some statements fail. If the first statement errors out, then we return that error,
// otherwise we save the error from any statements, so that they can be returned from NextResultSet()
// with the caller requests that result set.
rows = &doltRows{err: err}
// If an error occurs, we don't execute any more statements in the multistatement query. Instead, we
// capture the error in a doltRows instance, so that rows.NextResultSet() will return the error when
// the caller requests that result set. This is to match the MySQL driver's behavior.
multiResultSet.rowSets = append(multiResultSet.rowSets, &doltRows{err: err})
break
} else {
multiResultSet.rowSets = append(multiResultSet.rowSets, rows.(*doltRows))
}
multiResultSet.rowSets = append(multiResultSet.rowSets, rows.(*doltRows))
}

// If an error occurred on the first statement, go ahead and return the error, without any result set.
if multiResultSet.rowSets[0].err != nil {
return nil, multiResultSet.rowSets[0].err
} else {
Expand Down

0 comments on commit 7fda060

Please sign in to comment.