Repository: src-d/go-mysql-server
Pull request #361 (2018-09-13 14:58:13)

*: implement session configuration

erizocosmico: Whoops, forgot to remove it
On Thu, 13 Sep 2018 at 17:32, Antonio Navarro Perez <notifications@github.com<mailto:notifications@github.com>> wrote:

@ajnavarro requested changes on this pull request.

________________________________

In sql/analyzer/resolve_columns.go<https://github.com/src-d/go-mysql-server/pull/361#discussion_r217427611>:

> @@ -261,3 +281,41 @@ func dedupStrings(in []string) []string {
        }
        return result
 }
+
+func isGlobalOrSessionColumn(col *expression.UnresolvedColumn) bool {
+       return strings.HasPrefix(col.Name(), "@@") || strings.HasPrefix(col.Table(), "@@")
+}
+
+func guessSQLType(v interface{}) (sql.Type, error) {


this is not used.

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub<https://github.com/src-d/go-mysql-server/pull/361#pullrequestreview-155136829>, or mute the thread<https://github.com/notifications/unsubscribe-auth/ABQFFyPPH_GJcBGngVeYVARvuCbxwoQaks5uanqcgaJpZM4WnbvH>.


Patch:
diff --git a/sql/analyzer/resolve_columns.go b/sql/analyzer/resolve_columns.go
index 3b67bfe..e04ac67 100644
--- a/sql/analyzer/resolve_columns.go
+++ b/sql/analyzer/resolve_columns.go
@@ -3,6 +3,7 @@ package analyzer
 import (
 	"strings"
 
+	errors "gopkg.in/src-d/go-errors.v1"
 	"gopkg.in/src-d/go-mysql-server.v0/sql"
 	"gopkg.in/src-d/go-mysql-server.v0/sql/expression"
 	"gopkg.in/src-d/go-mysql-server.v0/sql/plan"
@@ -63,6 +64,11 @@ func qualifyColumns(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error)
 			a.Log("transforming expression of type: %T", e)
 			switch col := e.(type) {
 			case *expression.UnresolvedColumn:
+				// Skip this step for global and session variables
+				if isGlobalOrSessionColumn(col) {
+					return col, nil
+				}
+
 				col = expression.NewUnresolvedQualifiedColumn(col.Table(), col.Name())
 
 				if col.Table() == "" {
@@ -124,6 +130,8 @@ func qualifyColumns(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error)
 	})
 }
 
+var errGlobalVariablesNotSupported = errors.NewKind("can't resolve global variable, %s was requested")
+
 func resolveColumns(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error) {
 	span, ctx := ctx.Span("resolve_columns")
 	defer span.Finish()
@@ -184,6 +192,15 @@ func resolveColumns(ctx *sql.Context, a *Analyzer, n sql.Node) (sql.Node, error)
 			if !ok {
 				switch uc := uc.(type) {
 				case *expression.UnresolvedColumn:
+					if isGlobalOrSessionColumn(uc) {
+						if uc.Table() != "" && strings.ToLower(uc.Table()) != "@@session" {
+							return nil, errGlobalVariablesNotSupported.New(uc)
+						}
+
+						typ, value := ctx.Get(strings.TrimLeft(uc.Name(), "@"))
+						return expression.NewGetSessionField(uc.Name(), typ, value), nil
+					}
+
 					a.Log("evaluation of column %q was deferred", uc.Name())
 					return &deferredColumn{uc}, nil
 				default:
@@ -261,3 +278,7 @@ func dedupStrings(in []string) []string {
 	}
 	return result
 }
+
+func isGlobalOrSessionColumn(col *expression.UnresolvedColumn) bool {
+	return strings.HasPrefix(col.Name(), "@@") || strings.HasPrefix(col.Table(), "@@")
+}
diff --git a/sql/expression/get_field.go b/sql/expression/get_field.go
index e26753f..75690a5 100644
--- a/sql/expression/get_field.go
+++ b/sql/expression/get_field.go
@@ -91,3 +91,40 @@ func (p *GetField) WithIndex(n int) sql.Expression {
 	p2.fieldIndex = n
 	return &p2
 }
+
+// GetSessionField is an expression that returns the value of a session configuration.
+type GetSessionField struct {
+	name  string
+	typ   sql.Type
+	value interface{}
+}
+
+// NewGetSessionField creates a new GetSessionField expression.
+func NewGetSessionField(name string, typ sql.Type, value interface{}) *GetSessionField {
+	return &GetSessionField{name, typ, value}
+}
+
+// Children implements the sql.Expression interface.
+func (f *GetSessionField) Children() []sql.Expression { return nil }
+
+// Eval implements the sql.Expression interface.
+func (f *GetSessionField) Eval(*sql.Context, sql.Row) (interface{}, error) {
+	return f.value, nil
+}
+
+// Type implements the sql.Expression interface.
+func (f *GetSessionField) Type() sql.Type { return f.typ }
+
+// IsNullable implements the sql.Expression interface.
+func (f *GetSessionField) IsNullable() bool { return f.value == nil }
+
+// Resolved implements the sql.Expression interface.
+func (f *GetSessionField) Resolved() bool { return true }
+
+// String implements the sql.Expression interface.
+func (f *GetSessionField) String() string { return "@@" + f.name }
+
+// TransformUp implements the sql.Expression interface.
+func (f *GetSessionField) TransformUp(fn sql.TransformExprFunc) (sql.Expression, error) {
+	return fn(f)
+}
diff --git a/sql/parse/parse.go b/sql/parse/parse.go
index 4e27174..2f6de72 100644
--- a/sql/parse/parse.go
+++ b/sql/parse/parse.go
@@ -92,9 +92,39 @@ func convert(ctx *sql.Context, stmt sqlparser.Statement, query string) (sql.Node
 		return convertInsert(ctx, n)
 	case *sqlparser.DDL:
 		return convertDDL(n)
+	case *sqlparser.Set:
+		return convertSet(ctx, n)
 	}
 }
 
+func convertSet(ctx *sql.Context, n *sqlparser.Set) (sql.Node, error) {
+	if n.Scope == sqlparser.GlobalStr {
+		return nil, ErrUnsupportedFeature.New("SET global variables")
+	}
+
+	var variables = make([]plan.SetVariable, len(n.Exprs))
+	for i, e := range n.Exprs {
+		expr, err := exprToExpression(e.Expr)
+		if err != nil {
+			return nil, err
+		}
+
+		fmt.Println(strings.TrimSpace(strings.ToLower(e.Name.Qualifier.Name.String())))
+		switch strings.TrimSpace(strings.ToLower(e.Name.Qualifier.Name.String())) {
+		case "@@session", "": // do nothing
+		default:
+			return nil, ErrUnsupportedFeature.New("qualifiers in set variable names other than @@session")
+		}
+
+		variables[i] = plan.SetVariable{
+			Name:  e.Name.Name.Lowered(),
+			Value: expr,
+		}
+	}
+
+	return plan.NewSet(variables...), nil
+}
+
 func convertShow(s *sqlparser.Show) (sql.Node, error) {
 	if s.Type != sqlparser.KeywordString(sqlparser.TABLES) {
 		unsupportedShow := fmt.Sprintf("SHOW %s", s.Type)
diff --git a/sql/plan/set.go b/sql/plan/set.go
new file mode 100644
index 0000000..0f56ec3
--- /dev/null
+++ b/sql/plan/set.go
@@ -0,0 +1,90 @@
+package plan
+
+import (
+	"fmt"
+	"strings"
+
+	"gopkg.in/src-d/go-mysql-server.v0/sql"
+)
+
+// Set configuration variables. Right now, only session variables are supported.
+type Set struct {
+	Variables []SetVariable
+}
+
+// SetVariable is a key-value pair to represent the value that will be set on
+// a variable.
+type SetVariable struct {
+	Name  string
+	Value sql.Expression
+}
+
+// NewSet creates a new Set node.
+func NewSet(vars ...SetVariable) *Set {
+	return &Set{vars}
+}
+
+// Resolved implements the sql.Node interface.
+func (s *Set) Resolved() bool {
+	for _, v := range s.Variables {
+		if !v.Value.Resolved() {
+			return false
+		}
+	}
+	return true
+}
+
+// Children implements the sql.Node interface.
+func (s *Set) Children() []sql.Node { return nil }
+
+// TransformUp implements the sql.Node interface.
+func (s *Set) TransformUp(f sql.TransformNodeFunc) (sql.Node, error) {
+	return f(s)
+}
+
+// TransformExpressionsUp implements the sql.Node interface.
+func (s *Set) TransformExpressionsUp(f sql.TransformExprFunc) (sql.Node, error) {
+	var vars = make([]SetVariable, len(s.Variables))
+	for i, v := range s.Variables {
+		val, err := v.Value.TransformUp(f)
+		if err != nil {
+			return nil, err
+		}
+
+		vars[i] = v
+		vars[i].Value = val
+	}
+
+	return NewSet(vars...), nil
+}
+
+// RowIter implements the sql.Node interface.
+func (s *Set) RowIter(ctx *sql.Context) (sql.RowIter, error) {
+	span, ctx := ctx.Span("plan.Set")
+	defer span.Finish()
+
+	for _, v := range s.Variables {
+		value, err := v.Value.Eval(ctx, nil)
+		if err != nil {
+			return nil, err
+		}
+
+		ctx.Set(strings.TrimLeft(v.Name, "@"), v.Value.Type(), value)
+	}
+
+	return sql.RowsToRowIter(), nil
+}
+
+// Schema implements the sql.Node interface.
+func (s *Set) Schema() sql.Schema { return nil }
+
+func (s *Set) String() string {
+	p := sql.NewTreePrinter()
+	_ = p.WriteNode("Set")
+	var children = make([]string, len(s.Variables))
+	for i, v := range s.Variables {
+		children[i] = fmt.Sprintf("%s = %s", v.Name, v.Value)
+	}
+	_ = p.WriteChildren(children...)
+	return p.String()
+}
diff --git a/sql/session.go b/sql/session.go
index 694c87e..3d1a733 100644
--- a/sql/session.go
+++ b/sql/session.go
@@ -3,6 +3,7 @@ package sql
 import (
 	"context"
 	"io"
+	"sync"
 	"time"
 
 	opentracing "github.com/opentracing/opentracing-go"
@@ -18,30 +19,66 @@ const (
 
 // Session holds the session data.
 type Session interface {
+	// Address of the server.
 	Address() string
+	// User of the session.
 	User() string
+	// Set session configuration.
+	Set(key string, typ Type, value interface{})
+	// Get session configuration.
+	Get(key string) (Type, interface{})
 }
 
 // BaseSession is the basic session type.
 type BaseSession struct {
-	addr string
-	user string
+	addr   string
+	user   string
+	mu     sync.RWMutex
+	config map[string]typedValue
 }
 
 // User returns the current user of the session.
-func (s BaseSession) User() string { return s.user }
+func (s *BaseSession) User() string { return s.user }
 
 // Address returns the server address.
-func (s BaseSession) Address() string { return s.addr }
+func (s *BaseSession) Address() string { return s.addr }
+
+// Set implements the Session interface.
+func (s *BaseSession) Set(key string, typ Type, value interface{}) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.config[key] = typedValue{typ, value}
+}
+
+// Get implements the Session interface.
+func (s *BaseSession) Get(key string) (Type, interface{}) {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+	v, ok := s.config[key]
+	if !ok {
+		return Null, nil
+	}
+
+	return v.typ, v.value
+}
+
+type typedValue struct {
+	typ   Type
+	value interface{}
+}
 
 // NewSession creates a new session with data.
 func NewSession(address string, user string) Session {
-	return &BaseSession{addr: address, user: user}
+	return &BaseSession{
+		addr:   address,
+		user:   user,
+		config: make(map[string]typedValue),
+	}
 }
 
 // NewBaseSession creates a new empty session.
 func NewBaseSession() Session {
-	return &BaseSession{}
+	return &BaseSession{config: make(map[string]typedValue)}
 }
 
 // Context of the query execution.


Test patch:
diff --git a/engine_test.go b/engine_test.go
index b9eb8b3..d1b6dfd 100644
--- a/engine_test.go
+++ b/engine_test.go
@@ -1046,6 +1046,24 @@ func TestReadOnly(t *testing.T) {
 	require.True(analyzer.ErrQueryNotAllowed.Is(err))
 }
 
+func TestSessionVariables(t *testing.T) {
+	require := require.New(t)
+
+	e := newEngine(t)
+
+	ctx := sql.NewEmptyContext()
+	_, _, err := e.Query(ctx, `SET autocommit=1, @@session.foo="bar"`)
+	require.NoError(err)
+
+	_, iter, err := e.Query(ctx, `SELECT @@autocommit, @@session.foo`)
+	require.NoError(err)
+
+	rows, err := sql.RowIterToRows(iter)
+	require.NoError(err)
+
+	require.Equal([]sql.Row{{int64(1), "bar"}}, rows)
+}
+
 func insertRows(t *testing.T, table sql.Inserter, rows ...sql.Row) {
 	t.Helper()
 
diff --git a/sql/analyzer/resolve_columns_test.go b/sql/analyzer/resolve_columns_test.go
index b89c8b4..3d42364 100644
--- a/sql/analyzer/resolve_columns_test.go
+++ b/sql/analyzer/resolve_columns_test.go
@@ -1,6 +1,7 @@
 package analyzer
 
 import (
+	"context"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -197,3 +198,31 @@ func TestQualifyColumnsQualifiedStar(t *testing.T) {
 	require.NoError(err)
 	require.Equal(expected, result)
 }
+
+func TestResolveColumnsSession(t *testing.T) {
+	require := require.New(t)
+
+	ctx := sql.NewContext(context.Background(), sql.WithSession(sql.NewBaseSession()))
+	ctx.Set("foo_bar", sql.Int64, int64(42))
+
+	node := plan.NewProject(
+		[]sql.Expression{
+			expression.NewUnresolvedColumn("@@foo_bar"),
+			expression.NewUnresolvedColumn("@@bar_baz"),
+		},
+		plan.NewResolvedTable(dualTable),
+	)
+
+	result, err := resolveColumns(ctx, NewDefault(nil), node)
+	require.NoError(err)
+
+	expected := plan.NewProject(
+		[]sql.Expression{
+			expression.NewGetSessionField("@@foo_bar", sql.Int64, int64(42)),
+			expression.NewGetSessionField("@@bar_baz", sql.Null, nil),
+		},
+		plan.NewResolvedTable(dualTable),
+	)
+
+	require.Equal(expected, result)
+}
diff --git a/sql/parse/parse_test.go b/sql/parse/parse_test.go
index 9a76752..9ddd46e 100644
--- a/sql/parse/parse_test.go
+++ b/sql/parse/parse_test.go
@@ -639,6 +639,29 @@ var fixtures = map[string]sql.Node{
 	),
 	`SHOW FULL PROCESSLIST`: plan.NewShowProcessList(),
 	`SHOW PROCESSLIST`:      plan.NewShowProcessList(),
+	`SELECT @@allowed_max_packet`: plan.NewProject([]sql.Expression{
+		expression.NewUnresolvedColumn("@@allowed_max_packet"),
+	}, plan.NewUnresolvedTable("dual")),
+	`SET autocommit=1, foo="bar"`: plan.NewSet(
+		plan.SetVariable{
+			Name:  "autocommit",
+			Value: expression.NewLiteral(int64(1), sql.Int64),
+		},
+		plan.SetVariable{
+			Name:  "foo",
+			Value: expression.NewLiteral("bar", sql.Text),
+		},
+	),
+	`SET @@session.autocommit=1, foo="bar"`: plan.NewSet(
+		plan.SetVariable{
+			Name:  "autocommit",
+			Value: expression.NewLiteral(int64(1), sql.Int64),
+		},
+		plan.SetVariable{
+			Name:  "foo",
+			Value: expression.NewLiteral("bar", sql.Text),
+		},
+	),
 }
 
 func TestParse(t *testing.T) {
diff --git a/sql/plan/set_test.go b/sql/plan/set_test.go
new file mode 100644
index 0000000..3f8b970
--- /dev/null
+++ b/sql/plan/set_test.go
@@ -0,0 +1,32 @@
+package plan
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"gopkg.in/src-d/go-mysql-server.v0/sql"
+	"gopkg.in/src-d/go-mysql-server.v0/sql/expression"
+)
+
+func TestSet(t *testing.T) {
+	require := require.New(t)
+
+	ctx := sql.NewContext(context.Background(), sql.WithSession(sql.NewBaseSession()))
+
+	s := NewSet(
+		SetVariable{"foo", expression.NewLiteral("bar", sql.Text)},
+		SetVariable{"@@baz", expression.NewLiteral(int64(1), sql.Int64)},
+	)
+
+	_, err := s.RowIter(ctx)
+	require.NoError(err)
+
+	typ, v := ctx.Get("foo")
+	require.Equal(sql.Text, typ)
+	require.Equal("bar", v)
+
+	typ, v = ctx.Get("baz")
+	require.Equal(sql.Int64, typ)
+	require.Equal(int64(1), v)
+}
diff --git a/sql/session_test.go b/sql/session_test.go
index 062accb..f50f82e 100644
--- a/sql/session_test.go
+++ b/sql/session_test.go
@@ -8,6 +8,22 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+func TestSessionConfig(t *testing.T) {
+	require := require.New(t)
+
+	sess := NewSession("foo", "bar")
+
+	typ, v := sess.Get("foo")
+	require.Equal(Null, typ)
+	require.Equal(nil, v)
+
+	sess.Set("foo", Int64, 1)
+
+	typ, v = sess.Get("foo")
+	require.Equal(Int64, typ)
+	require.Equal(1, v)
+}
+
 type testNode struct{}
 
 func (t *testNode) Resolved() bool {