Skip to content
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

Moving transaction initialization before query analysis #1013

Merged
merged 8 commits into from
May 16, 2022
54 changes: 49 additions & 5 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"os"
"sync"

"github.com/dolthub/go-mysql-server/sql/transform"
"github.com/pkg/errors"

"github.com/dolthub/go-mysql-server/memory"
"github.com/dolthub/go-mysql-server/sql"
Expand All @@ -29,6 +29,7 @@ import (
"github.com/dolthub/go-mysql-server/sql/grant_tables"
"github.com/dolthub/go-mysql-server/sql/parse"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/transform"
)

// Config for the Engine.
Expand Down Expand Up @@ -174,17 +175,29 @@ func (e *Engine) QueryNodeWithBindings(
err error
)

if parsed == nil {
parsed, err = parse.Parse(ctx, query)
if err != nil {
return nil, nil, err
}
}

_, err = e.beginTransaction(ctx, parsed)
if err != nil {
return nil, nil, err
}

if p, ok := e.preparedDataForSession(ctx.Session); ok && p.Query == query {
analyzed, err = e.analyzePreparedQuery(ctx, query, bindings)
} else {
analyzed, err = e.analyzeQuery(ctx, query, parsed, bindings)
}
if err != nil {
return nil, nil, err
}
err2 := clearAutocommitTransaction(ctx)
if err2 != nil {
err = errors.Wrap(err, "unable to clear autocommit transaction: "+err2.Error())
}

_, err = e.beginTransaction(ctx, analyzed)
if err != nil {
return nil, nil, err
}

Expand All @@ -200,6 +213,11 @@ func (e *Engine) QueryNodeWithBindings(
iter, err = analyzed.RowIter(ctx, nil)
}
if err != nil {
err2 := clearAutocommitTransaction(ctx)
if err2 != nil {
err = errors.Wrap(err, "unable to clear autocommit transaction: "+err2.Error())
}

return nil, nil, err
}

Expand All @@ -214,6 +232,32 @@ func (e *Engine) QueryNodeWithBindings(
return analyzed.Schema(), iter, nil
}

// clearAutocommitTransaction unsets the transaction from the current session if it is an implicitly
// created autocommit transaction. This enables the next request to have an autocommit transaction
// correctly started.
func clearAutocommitTransaction(ctx *sql.Context) error {
// The GetignoreAutoCommit property essentially says the current transaction is an explicit,
// user-created transaction and we should not process autocommit. So, if it's set, then we
// don't need to do anything here to clear implicit transaction state.
//
// TODO: This logic would probably read more clearly if we could just ask the session/ctx if the
// current transaction is automatically created or explicitly created by the caller.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered adding something to Session or Transaction's interface to show whether the current transaction is automatically created by the system (i.e. autocommit) or is explicitly requested by the client, but wanted to keep this fix as small as possible. I think the code that needs to use GetIgnoreAutoCommit would read a little clearer with that refactor, so happy to follow up with that if you think it's worth it.

if ctx.GetIgnoreAutoCommit() {
return nil
}

autocommit, err := plan.IsSessionAutocommit(ctx)
if err != nil {
return err
}

if autocommit {
ctx.SetTransaction(nil)
}

return nil
}

func (e *Engine) cachePreparedStmt(ctx *sql.Context, analyzed sql.Node, query string) {
e.mu.Lock()
defer e.mu.Unlock()
Expand Down
91 changes: 91 additions & 0 deletions enginetest/transaction_queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,86 @@ type TransactionTest struct {
}

var TransactionTests = []TransactionTest{
{
Name: "Changes from transactions are available before analyzing statements in other sessions (autocommit on)",
Assertions: []ScriptTestAssertion{
{
Query: "/* client a */ select @@autocommit;",
Expected: []sql.Row{{1}},
},
{
Query: "/* client b */ select @@autocommit;",
Expected: []sql.Row{{1}},
},
{
Query: "/* client a */ select * from t;",
ExpectedErr: sql.ErrTableNotFound,
},
{
Query: "/* client b */ select * from t;",
ExpectedErr: sql.ErrTableNotFound,
},
{
Query: "/* client a */ create table t(pk int primary key);",
Expected: []sql.Row{{sql.OkResult{}}},
},
{
Query: "/* client b */ select count(*) from t;",
Expected: []sql.Row{{0}},
},
},
},
{
fulghum marked this conversation as resolved.
Show resolved Hide resolved
Name: "Changes from transactions are available before analyzing statements in other sessions (autocommit off)",
Assertions: []ScriptTestAssertion{
{
Query: "/* client a */ set @@autocommit = 0;",
Expected: []sql.Row{{}},
},
{
Query: "/* client b */ set @@autocommit = 0;",
Expected: []sql.Row{{}},
},
{
Query: "/* client a */ select @@autocommit;",
Expected: []sql.Row{{0}},
},
{
Query: "/* client b */ select @@autocommit;",
Expected: []sql.Row{{0}},
},
{
Query: "/* client a */ start transaction;",
Expected: []sql.Row{},
},
{
Query: "/* client a */ select * from t;",
ExpectedErr: sql.ErrTableNotFound,
},
{
Query: "/* client a */ create table t(pk int primary key);",
Expected: []sql.Row{{sql.OkResult{}}},
},
{
// Trigger a query error to make sure explicit transaction is still
// correctly configured in the session despite the error
Query: "/* client a */ select * from t2;",
ExpectedErr: sql.ErrTableNotFound,
},
{
Query: "/* client a */ commit;",
Expected: []sql.Row{},
},
{
Query: "/* client b */ start transaction;",
Expected: []sql.Row{},
},
{
Query: "/* client b */ select count(*) from t;",
Expected: []sql.Row{{0}},
},
},
},
{
Name: "autocommit on",
SetUpScript: []string{
Expand Down Expand Up @@ -184,6 +264,11 @@ var TransactionTests = []TransactionTest{
Query: "/* client a */ start transaction",
Expected: []sql.Row{},
},
{
// Trigger an analyzer error to make sure transaction state is managed correctly
Query: "/* client a */ select * from doesnotexist;",
ExpectedErr: sql.ErrTableNotFound,
},
{
Query: "/* client a */ insert into t values (2, 2)",
Expected: []sql.Row{{sql.NewOkResult(1)}},
Expand All @@ -192,6 +277,12 @@ var TransactionTests = []TransactionTest{
Query: "/* client b */ select * from t order by x",
Expected: []sql.Row{{1, 1}},
},
{
// Trigger an analyzer error to make sure state for the explicitly started
// transaction is managed correctly and not cleared
Query: "/* client a */ select * from doesnotexist;",
ExpectedErr: sql.ErrTableNotFound,
},
{
Query: "/* client a */ commit",
Expected: []sql.Row{},
Expand Down
8 changes: 5 additions & 3 deletions sql/plan/transaction_committing_iter.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,10 @@ func (t transactionCommittingIter) Close(ctx *sql.Context) error {
}

tx := ctx.GetTransaction()
// TODO: In the future we should ensure that analyzer supports impicit commits instead of directly
// TODO: In the future we should ensure that analyzer supports implicit commits instead of directly
// accessing autocommit here.
// cc. https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html
autocommit, err := isSessionAutocommit(ctx)
autocommit, err := IsSessionAutocommit(ctx)
if err != nil {
return err
}
Expand All @@ -144,7 +144,9 @@ func (t transactionCommittingIter) Close(ctx *sql.Context) error {
return nil
}

func isSessionAutocommit(ctx *sql.Context) (bool, error) {
// IsSessionAutocommit returns true if the current session is using implicit transaction management
// through autocommit.
func IsSessionAutocommit(ctx *sql.Context) (bool, error) {
if ReadCommitted(ctx) {
return true, nil
}
Expand Down