From ef2002a6f13c9861d2db5e45867a911a695cd522 Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Fri, 28 May 2021 17:09:28 -0700 Subject: [PATCH 1/7] Support for value args for limit and offset --- engine.go | 12 ++++----- enginetest/queries.go | 15 ++++++++++++ go.mod | 2 ++ sql/expression/literal.go | 4 +++ sql/parse/parse.go | 22 ++++++++--------- sql/parse/parse_test.go | 30 +++++++++++------------ sql/parse/warnings.go | 5 ++-- sql/plan/limit.go | 51 ++++++++++++++++++++++++++++++++------- sql/plan/offset.go | 15 ++++++++---- 9 files changed, 106 insertions(+), 50 deletions(-) diff --git a/engine.go b/engine.go index 57e2e3db8b..01349c8ccb 100644 --- a/engine.go +++ b/engine.go @@ -152,18 +152,18 @@ func (e *Engine) QueryNodeWithBindings( return nil, nil, err } - analyzed, err = e.Analyzer.Analyze(ctx, parsed, nil) - if err != nil { - return nil, nil, err - } - if len(bindings) > 0 { - analyzed, err = plan.ApplyBindings(analyzed, bindings) + parsed, err = plan.ApplyBindings(parsed, bindings) if err != nil { return nil, nil, err } } + analyzed, err = e.Analyzer.Analyze(ctx, parsed, nil) + if err != nil { + return nil, nil, err + } + transactionDatabase, err := e.beginTransaction(ctx, parsed) if err != nil { return nil, nil, err diff --git a/enginetest/queries.go b/enginetest/queries.go index 712a10abaf..1a88dcad6a 100644 --- a/enginetest/queries.go +++ b/enginetest/queries.go @@ -1225,6 +1225,21 @@ var QueryTests = []QueryTest{ Query: "SELECT i FROM mytable ORDER BY i LIMIT 1 OFFSET 1;", Expected: []sql.Row{{int64(2)}}, }, + { + Query: "SELECT i FROM mytable WHERE s = 'first row' ORDER BY i DESC LIMIT ?;", + Bindings: map[string]sql.Expression{ + "v1": expression.NewLiteral(1, sql.Int8), + }, + Expected: []sql.Row{{int64(1)}}, + }, + { + Query: "SELECT i FROM mytable ORDER BY i LIMIT ? OFFSET 2;", + Bindings: map[string]sql.Expression{ + "v1": expression.NewLiteral(1, sql.Int8), + "v2": expression.NewLiteral(1, sql.Int8), + }, + Expected: []sql.Row{{int64(3)}}, + }, { Query: "SELECT i FROM mytable WHERE i NOT IN (SELECT i FROM (SELECT * FROM (SELECT i as i, s as s FROM mytable) f) s)", Expected: []sql.Row{}, diff --git a/go.mod b/go.mod index 577fc6d895..1f58907359 100644 --- a/go.mod +++ b/go.mod @@ -32,4 +32,6 @@ require ( gopkg.in/src-d/go-errors.v1 v1.0.0 ) +replace github.com/dolthub/vitess => ../vitess + go 1.13 diff --git a/sql/expression/literal.go b/sql/expression/literal.go index d393f74be3..0ff20dc5fe 100644 --- a/sql/expression/literal.go +++ b/sql/expression/literal.go @@ -60,6 +60,10 @@ func (p *Literal) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { func (p *Literal) String() string { switch v := p.value.(type) { + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return fmt.Sprintf("%d", v) + case float32, float64: + return fmt.Sprintf("%f", v) case string: return fmt.Sprintf("%q", v) case []byte: diff --git a/sql/parse/parse.go b/sql/parse/parse.go index 11cf3ea2dc..93dbe558c4 100644 --- a/sql/parse/parse.go +++ b/sql/parse/parse.go @@ -627,7 +627,7 @@ func convertSelect(ctx *sql.Context, s *sqlparser.Select) (sql.Node, error) { } } else if ok, val := sql.HasDefaultValue(ctx, ctx.Session, "sql_select_limit"); !ok { limit := mustCastNumToInt64(val) - node = plan.NewLimit(limit, node) + node = plan.NewLimit(expression.NewLiteral(limit, sql.Int64), node) } // Finally, if common table expressions were provided, wrap the top-level node in a With node to capture them @@ -1869,15 +1869,11 @@ func limitToLimit( limit sqlparser.Expr, child sql.Node, ) (*plan.Limit, error) { - rowCount, err := getInt64Value(ctx, limit, "LIMIT with non-integer literal") + rowCount, err := ExprToExpression(ctx, limit) if err != nil { return nil, err } - if rowCount < 0 { - return nil, ErrUnsupportedSyntax.New("LIMIT must be >= 0") - } - return plan.NewLimit(rowCount, child), nil } @@ -1895,16 +1891,12 @@ func offsetToOffset( offset sqlparser.Expr, child sql.Node, ) (*plan.Offset, error) { - o, err := getInt64Value(ctx, offset, "OFFSET with non-integer literal") + rowCount, err := ExprToExpression(ctx, offset) if err != nil { return nil, err } - if o < 0 { - return nil, ErrUnsupportedSyntax.New("OFFSET must be >= 0") - } - - return plan.NewOffset(o, child), nil + return plan.NewOffset(rowCount, child), nil } // getInt64Literal returns an int64 *expression.Literal for the value given, or an unsupported error with the string @@ -1915,6 +1907,12 @@ func getInt64Literal(ctx *sql.Context, expr sqlparser.Expr, errStr string) (*exp return nil, err } + switch e := e.(type) { + case *expression.Literal: + if !sql.IsInteger(e.Type()) { + return nil, ErrUnsupportedFeature.New(errStr) + } + } nl, ok := e.(*expression.Literal) if !ok || !sql.IsInteger(nl.Type()) { return nil, ErrUnsupportedFeature.New(errStr) diff --git a/sql/parse/parse_test.go b/sql/parse/parse_test.go index 2fda5672c9..215d3108b1 100644 --- a/sql/parse/parse_test.go +++ b/sql/parse/parse_test.go @@ -1226,7 +1226,7 @@ CREATE TABLE t2 }), "a"), ), - `SELECT column_0 FROM (values row(1,2), row(3,4)) a limit 1`: plan.NewLimit(1, + `SELECT column_0 FROM (values row(1,2), row(3,4)) a limit 1`: plan.NewLimit(expression.NewLiteral(int8(1), sql.Int8), plan.NewProject( []sql.Expression{ expression.NewUnresolvedColumn("column_0"), @@ -1283,7 +1283,7 @@ CREATE TABLE t2 plan.NewUnresolvedTable("foo", ""), ), ), - `SELECT foo, bar FROM foo LIMIT 10;`: plan.NewLimit(10, + `SELECT foo, bar FROM foo LIMIT 10;`: plan.NewLimit(expression.NewLiteral(int8(10), sql.Int8), plan.NewProject( []sql.Expression{ expression.NewUnresolvedColumn("foo"), @@ -1302,7 +1302,7 @@ CREATE TABLE t2 plan.NewUnresolvedTable("foo", ""), ), ), - `SELECT foo, bar FROM foo WHERE foo = bar LIMIT 10;`: plan.NewLimit(10, + `SELECT foo, bar FROM foo WHERE foo = bar LIMIT 10;`: plan.NewLimit(expression.NewLiteral(int8(10), sql.Int8), plan.NewProject( []sql.Expression{ expression.NewUnresolvedColumn("foo"), @@ -1317,7 +1317,7 @@ CREATE TABLE t2 ), ), ), - `SELECT foo, bar FROM foo ORDER BY baz DESC LIMIT 1;`: plan.NewLimit(1, + `SELECT foo, bar FROM foo ORDER BY baz DESC LIMIT 1;`: plan.NewLimit(expression.NewLiteral(int8(1), sql.Int8), plan.NewSort( []sql.SortField{{Column: expression.NewUnresolvedColumn("baz"), Order: sql.Descending, NullOrdering: sql.NullsFirst}}, plan.NewProject( @@ -1329,7 +1329,7 @@ CREATE TABLE t2 ), ), ), - `SELECT foo, bar FROM foo WHERE qux = 1 ORDER BY baz DESC LIMIT 1;`: plan.NewLimit(1, + `SELECT foo, bar FROM foo WHERE qux = 1 ORDER BY baz DESC LIMIT 1;`: plan.NewLimit(expression.NewLiteral(int8(1), sql.Int8), plan.NewSort( []sql.SortField{{Column: expression.NewUnresolvedColumn("baz"), Order: sql.Descending, NullOrdering: sql.NullsFirst}}, plan.NewProject( @@ -1545,8 +1545,8 @@ CREATE TABLE t2 }, plan.NewUnresolvedTable("foo", ""), ), - `SELECT foo, bar FROM foo LIMIT 2 OFFSET 5;`: plan.NewLimit(2, - plan.NewOffset(5, plan.NewProject( + `SELECT foo, bar FROM foo LIMIT 2 OFFSET 5;`: plan.NewLimit(expression.NewLiteral(int8(2), sql.Int8), + plan.NewOffset(expression.NewLiteral(int8(5), sql.Int8), plan.NewProject( []sql.Expression{ expression.NewUnresolvedColumn("foo"), expression.NewUnresolvedColumn("bar"), @@ -1554,8 +1554,8 @@ CREATE TABLE t2 plan.NewUnresolvedTable("foo", ""), )), ), - `SELECT foo, bar FROM foo LIMIT 5,2;`: plan.NewLimit(2, - plan.NewOffset(5, plan.NewProject( + `SELECT foo, bar FROM foo LIMIT 5,2;`: plan.NewLimit(expression.NewLiteral(int8(2), sql.Int8), + plan.NewOffset(expression.NewLiteral(int8(5), sql.Int8), plan.NewProject( []sql.Expression{ expression.NewUnresolvedColumn("foo"), expression.NewUnresolvedColumn("bar"), @@ -2151,7 +2151,7 @@ CREATE TABLE t2 }, plan.NewUnresolvedTable("foo", ""), ), - `SELECT /*+ JOIN_ORDER(a,b) */ * FROM b join a on c = d limit 5`: plan.NewLimit(5, + `SELECT /*+ JOIN_ORDER(a,b) */ * FROM b join a on c = d limit 5`: plan.NewLimit(expression.NewLiteral(int8(5), sql.Int8), plan.NewProject( []sql.Expression{ expression.NewStar(), @@ -2277,9 +2277,9 @@ CREATE TABLE t2 `SHOW CREATE SCHEMA foo`: plan.NewShowCreateDatabase(sql.UnresolvedDatabase("foo"), false), `SHOW CREATE DATABASE IF NOT EXISTS foo`: plan.NewShowCreateDatabase(sql.UnresolvedDatabase("foo"), true), `SHOW CREATE SCHEMA IF NOT EXISTS foo`: plan.NewShowCreateDatabase(sql.UnresolvedDatabase("foo"), true), - `SHOW WARNINGS`: plan.NewOffset(0, plan.ShowWarnings(sql.NewEmptyContext().Warnings())), - `SHOW WARNINGS LIMIT 10`: plan.NewLimit(10, plan.NewOffset(0, plan.ShowWarnings(sql.NewEmptyContext().Warnings()))), - `SHOW WARNINGS LIMIT 5,10`: plan.NewLimit(10, plan.NewOffset(5, plan.ShowWarnings(sql.NewEmptyContext().Warnings()))), + `SHOW WARNINGS`: plan.NewOffset(expression.NewLiteral(0, sql.Int64), plan.ShowWarnings(sql.NewEmptyContext().Warnings())), + `SHOW WARNINGS LIMIT 10`: plan.NewLimit(expression.NewLiteral(10, sql.Int64), plan.NewOffset(expression.NewLiteral(0, sql.Int64), plan.ShowWarnings(sql.NewEmptyContext().Warnings()))), + `SHOW WARNINGS LIMIT 5,10`: plan.NewLimit(expression.NewLiteral(10, sql.Int64), plan.NewOffset(expression.NewLiteral(5, sql.Int64), plan.ShowWarnings(sql.NewEmptyContext().Warnings()))), "SHOW CREATE DATABASE `foo`": plan.NewShowCreateDatabase(sql.UnresolvedDatabase("foo"), false), "SHOW CREATE SCHEMA `foo`": plan.NewShowCreateDatabase(sql.UnresolvedDatabase("foo"), false), "SHOW CREATE DATABASE IF NOT EXISTS `foo`": plan.NewShowCreateDatabase(sql.UnresolvedDatabase("foo"), true), @@ -3159,8 +3159,6 @@ var fixturesErrors = map[string]*errors.Kind{ `SHOW METHEMONEY`: ErrUnsupportedFeature, `LOCK TABLES foo AS READ`: errUnexpectedSyntax, `LOCK TABLES foo LOW_PRIORITY READ`: errUnexpectedSyntax, - `SELECT * FROM mytable LIMIT -100`: ErrUnsupportedSyntax, - `SELECT * FROM mytable LIMIT 100 OFFSET -1`: ErrUnsupportedSyntax, `SELECT INTERVAL 1 DAY - '2018-05-01'`: ErrUnsupportedSyntax, `SELECT INTERVAL 1 DAY * '2018-05-01'`: ErrUnsupportedSyntax, `SELECT '2018-05-01' * INTERVAL 1 DAY`: ErrUnsupportedSyntax, @@ -3182,7 +3180,7 @@ func TestParseErrors(t *testing.T) { ctx := sql.NewEmptyContext() _, err := Parse(ctx, query) require.Error(err) - require.True(expectedError.Is(err)) + require.True(expectedError.Is(err), "Expected %T but got %T", expectedError, err) }) } } diff --git a/sql/parse/warnings.go b/sql/parse/warnings.go index 3012b39000..e406858b54 100644 --- a/sql/parse/warnings.go +++ b/sql/parse/warnings.go @@ -22,6 +22,7 @@ import ( "strings" "unicode" + "github.com/dolthub/go-mysql-server/sql/expression" errors "gopkg.in/src-d/go-errors.v1" "github.com/dolthub/go-mysql-server/sql" @@ -78,7 +79,7 @@ func parseShowWarnings(ctx *sql.Context, s string) (sql.Node, error) { return nil, errInvalidIndex.New("offset", offset) } } - node = plan.NewOffset(int64(offset), node) + node = plan.NewOffset(expression.NewLiteral(offset, sql.Int64), node) if cntstr != "" { if count, err = strconv.Atoi(cntstr); err != nil { return nil, err @@ -87,7 +88,7 @@ func parseShowWarnings(ctx *sql.Context, s string) (sql.Node, error) { return nil, errInvalidIndex.New("count", count) } if count > 0 { - node = plan.NewLimit(int64(count), node) + node = plan.NewLimit(expression.NewLiteral(count, sql.Int64), node) } } diff --git a/sql/plan/limit.go b/sql/plan/limit.go index 51f0e8eaee..30c9fb5cb0 100644 --- a/sql/plan/limit.go +++ b/sql/plan/limit.go @@ -15,6 +15,7 @@ package plan import ( + "fmt" "io" opentracing "github.com/opentracing/opentracing-go" @@ -25,12 +26,12 @@ import ( // Limit is a node that only allows up to N rows to be retrieved. type Limit struct { UnaryNode - Limit int64 + Limit sql.Expression CalcFoundRows bool } // NewLimit creates a new Limit node with the given size. -func NewLimit(size int64, child sql.Node) *Limit { +func NewLimit(size sql.Expression, child sql.Node) *Limit { return &Limit{ UnaryNode: UnaryNode{Child: child}, Limit: size, @@ -39,24 +40,55 @@ func NewLimit(size int64, child sql.Node) *Limit { // Resolved implements the Resolvable interface. func (l *Limit) Resolved() bool { - return l.UnaryNode.Child.Resolved() + return l.UnaryNode.Child.Resolved() && l.Limit.Resolved() } // RowIter implements the Node interface. func (l *Limit) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter, error) { span, ctx := ctx.Span("plan.Limit", opentracing.Tag{Key: "limit", Value: l.Limit}) - li, err := l.Child.RowIter(ctx, row) + limit, err := getInt64Value(ctx, l.Limit) + if err != nil { + return nil, err + } + + childIter, err := l.Child.RowIter(ctx, row) if err != nil { span.Finish() return nil, err } return sql.NewSpanIter(span, &limitIter{ - l: l, - childIter: li, + l: l, + limit: limit, + childIter: childIter, }), nil } +// getInt64Value returns the int64 literal value in the expression given, or an error with the errStr given if it +// cannot. +func getInt64Value(ctx *sql.Context, expr sql.Expression) (int64, error) { + i, err := expr.Eval(ctx, nil) + if err != nil { + return 0, err + } + + switch i := i.(type) { + case int: + return int64(i), nil + case int8: + return int64(i), nil + case int16: + return int64(i), nil + case int32: + return int64(i), nil + case int64: + return i, nil + default: + // analyzer should catch this already + panic(fmt.Sprintf("Unsupported type for limit %T", i)) + } +} + // WithChildren implements the Node interface. func (l *Limit) WithChildren(children ...sql.Node) (sql.Node, error) { if len(children) != 1 { @@ -70,14 +102,14 @@ func (l *Limit) WithChildren(children ...sql.Node) (sql.Node, error) { func (l Limit) String() string { pr := sql.NewTreePrinter() - _ = pr.WriteNode("Limit(%d)", l.Limit) + _ = pr.WriteNode("Limit(%s)", l.Limit) _ = pr.WriteChildren(l.Child.String()) return pr.String() } func (l Limit) DebugString() string { pr := sql.NewTreePrinter() - _ = pr.WriteNode("Limit(%d)", l.Limit) + _ = pr.WriteNode("Limit(%s)", l.Limit) _ = pr.WriteChildren(sql.DebugString(l.Child)) return pr.String() } @@ -86,10 +118,11 @@ type limitIter struct { l *Limit currentPos int64 childIter sql.RowIter + limit int64 } func (li *limitIter) Next() (sql.Row, error) { - if li.currentPos >= li.l.Limit { + if li.currentPos >= li.limit { // If we were asked to calc all found rows, then when we are past the limit we iterate over the rest of the // result set to count it if li.l.CalcFoundRows { diff --git a/sql/plan/offset.go b/sql/plan/offset.go index a74bd4717a..1b8b0903a7 100644 --- a/sql/plan/offset.go +++ b/sql/plan/offset.go @@ -23,11 +23,11 @@ import ( // Offset is a node that skips the first N rows. type Offset struct { UnaryNode - Offset int64 + Offset sql.Expression } // NewOffset creates a new Offset node. -func NewOffset(n int64, child sql.Node) *Offset { +func NewOffset(n sql.Expression, child sql.Node) *Offset { return &Offset{ UnaryNode: UnaryNode{Child: child}, Offset: n, @@ -36,19 +36,24 @@ func NewOffset(n int64, child sql.Node) *Offset { // Resolved implements the Resolvable interface. func (o *Offset) Resolved() bool { - return o.Child.Resolved() + return o.Child.Resolved() && o.Offset.Resolved() } // RowIter implements the Node interface. func (o *Offset) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter, error) { span, ctx := ctx.Span("plan.Offset", opentracing.Tag{Key: "offset", Value: o.Offset}) + offset, err := getInt64Value(ctx, o.Offset) + if err != nil { + return nil, err + } + it, err := o.Child.RowIter(ctx, row) if err != nil { span.Finish() return nil, err } - return sql.NewSpanIter(span, &offsetIter{o.Offset, it}), nil + return sql.NewSpanIter(span, &offsetIter{offset, it}), nil } // WithChildren implements the Node interface. @@ -61,7 +66,7 @@ func (o *Offset) WithChildren(children ...sql.Node) (sql.Node, error) { func (o Offset) String() string { pr := sql.NewTreePrinter() - _ = pr.WriteNode("Offset(%d)", o.Offset) + _ = pr.WriteNode("Offset(%s)", o.Offset) _ = pr.WriteChildren(o.Child.String()) return pr.String() } From 8b1a07f1bf388acb1915a917da9f614b3b37fab3 Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Fri, 28 May 2021 17:35:00 -0700 Subject: [PATCH 2/7] Added validation for limit and offset values --- enginetest/enginetests.go | 21 ++++++++++++- enginetest/queries.go | 29 +++++++++++++++++ sql/analyzer/rules.go | 1 + sql/analyzer/validation_rules.go | 54 ++++++++++++++++++++++++++++++++ sql/errors.go | 4 +++ sql/plan/limit.go | 13 ++++++++ sql/plan/offset.go | 13 ++++++++ 7 files changed, 134 insertions(+), 1 deletion(-) diff --git a/enginetest/enginetests.go b/enginetest/enginetests.go index e20a6533ab..e6829dd9ce 100644 --- a/enginetest/enginetests.go +++ b/enginetest/enginetests.go @@ -435,7 +435,7 @@ func TestQueryErrors(t *testing.T, harness Harness) { t.Skipf("skipping query %s", tt.Query) } } - AssertErr(t, engine, harness, tt.Query, tt.ExpectedErr) + AssertErrWithBindings(t, engine, harness, tt.Query, tt.Bindings, tt.ExpectedErr) }) } } @@ -2864,6 +2864,25 @@ func AssertErr(t *testing.T, e *sqle.Engine, harness Harness, query string, expe AssertErrWithCtx(t, e, NewContext(harness), query, expectedErrKind, errStrs...) } +// AssertErrWithBindings asserts that the given query returns an error during its execution, optionally specifying a +// type of error. +func AssertErrWithBindings(t *testing.T, e *sqle.Engine, harness Harness, query string, bindings map[string]sql.Expression, expectedErrKind *errors.Kind, errStrs ...string) { + ctx := NewContext(harness) + _, iter, err := e.QueryWithBindings(ctx, query, bindings) + if err == nil { + _, err = sql.RowIterToRows(ctx, iter) + } + require.Error(t, err) + if expectedErrKind != nil { + require.True(t, expectedErrKind.Is(err), "Expected error of type %s but got %s", expectedErrKind, err) + } + // If there are multiple error strings then we only match against the first + if len(errStrs) >= 1 { + require.Equal(t, errStrs[0], err.Error()) + } + +} + // AssertErrWithCtx is the same as AssertErr, but uses the context given instead of creating one from a harness func AssertErrWithCtx(t *testing.T, e *sqle.Engine, ctx *sql.Context, query string, expectedErrKind *errors.Kind, errStrs ...string) { _, iter, err := e.Query(ctx, query) diff --git a/enginetest/queries.go b/enginetest/queries.go index 1a88dcad6a..549685c2d9 100644 --- a/enginetest/queries.go +++ b/enginetest/queries.go @@ -5807,6 +5807,7 @@ var ExplodeQueries = []QueryTest{ type QueryErrorTest struct { Query string + Bindings map[string]sql.Expression ExpectedErr *errors.Kind } @@ -6010,6 +6011,34 @@ var errorQueries = []QueryErrorTest{ Query: "SELECT a FROM (select i,s FROM mytable) mt (a,b,c) order by a desc;", ExpectedErr: sql.ErrColumnCountMismatch, }, + { + Query: "SELECT i FROM mytable limit ?", + ExpectedErr: sql.ErrInvalidSyntax, + Bindings: map[string]sql.Expression{ + "v1": expression.NewLiteral(-100, sql.Int8), + }, + }, + { + Query: "SELECT i FROM mytable limit ?", + ExpectedErr: sql.ErrInvalidType, + Bindings: map[string]sql.Expression{ + "v1": expression.NewLiteral("100", sql.LongText), + }, + }, + { + Query: "SELECT i FROM mytable limit 10, ?", + ExpectedErr: sql.ErrInvalidSyntax, + Bindings: map[string]sql.Expression{ + "v1": expression.NewLiteral(-100, sql.Int8), + }, + }, + { + Query: "SELECT i FROM mytable limit 10, ?", + ExpectedErr: sql.ErrInvalidType, + Bindings: map[string]sql.Expression{ + "v1": expression.NewLiteral("100", sql.LongText), + }, + }, } // WriteQueryTest is a query test for INSERT, UPDATE, etc. statements. It has a query to run and a select query to diff --git a/sql/analyzer/rules.go b/sql/analyzer/rules.go index eb5ab2952a..be6b464349 100644 --- a/sql/analyzer/rules.go +++ b/sql/analyzer/rules.go @@ -21,6 +21,7 @@ import ( // OnceBeforeDefault contains the rules to be applied just once before the // DefaultRules. var OnceBeforeDefault = []Rule{ + {"validate_offset_and_limit", validateLimitAndOffset}, {"load_stored_procedures", loadStoredProcedures}, {"resolve_views", resolveViews}, {"lift_common_table_expressions", liftCommonTableExpressions}, diff --git a/sql/analyzer/validation_rules.go b/sql/analyzer/validation_rules.go index b0db0c4b5c..1587227a61 100644 --- a/sql/analyzer/validation_rules.go +++ b/sql/analyzer/validation_rules.go @@ -105,6 +105,60 @@ var DefaultValidationRules = []Rule{ {validateUnionSchemasMatchRule, validateUnionSchemasMatch}, } +// validateLimitAndOffset ensures that only integer literals are used for limit and offset values +func validateLimitAndOffset(ctx *sql.Context, a *Analyzer, n sql.Node, scope *Scope) (sql.Node, error) { + return plan.TransformUp(n, func(n sql.Node) (sql.Node, error) { + switch n := n.(type) { + case *plan.Limit: + switch e := n.Limit.(type) { + case *expression.Literal: + if !sql.IsInteger(e.Type()) { + return nil, sql.ErrInvalidType.New(e.Type().String()) + } + i, err := e.Eval(ctx, nil) + if err != nil { + return nil, err + } + + i64, err := sql.Int64.Convert(i) + if err != nil { + return nil, err + } + if i64.(int64) < 0 { + return nil, sql.ErrInvalidSyntax.New("negative limit") + } + default: + return nil, sql.ErrInvalidType.New(e.Type().String()) + } + return n, nil + case *plan.Offset: + switch e := n.Offset.(type) { + case *expression.Literal: + if !sql.IsInteger(e.Type()) { + return nil, sql.ErrInvalidType.New(e.Type().String()) + } + i, err := e.Eval(ctx, nil) + if err != nil { + return nil, err + } + + i64, err := sql.Int64.Convert(i) + if err != nil { + return nil, err + } + if i64.(int64) < 0 { + return nil, sql.ErrInvalidSyntax.New("negative offset") + } + default: + return nil, sql.ErrInvalidType.New(e.Type().String()) + } + return n, nil + default: + return n, nil + } + }) +} + func validateIsResolved(ctx *sql.Context, a *Analyzer, n sql.Node, scope *Scope) (sql.Node, error) { span, _ := ctx.Span("validate_is_resolved") defer span.Finish() diff --git a/sql/errors.go b/sql/errors.go index 6a069d0a7b..b0c00d40be 100644 --- a/sql/errors.go +++ b/sql/errors.go @@ -281,6 +281,10 @@ var ( // ErrSavepointDoesNotExist is returned when a RELEASE SAVEPOINT or ROLLBACK TO SAVEPOINT statement references a // non-existent savepoint identifier ErrSavepointDoesNotExist = errors.NewKind("SAVEPOINT %s does not exist") + + // ErrInvalidSyntax is returned for syntax errors that aren't picked up by the parser, e.g. the wrong type of + // expression used in part of statement. + ErrInvalidSyntax = errors.NewKind("Invalid syntax: %s") ) func CastSQLError(err error) (*mysql.SQLError, bool) { diff --git a/sql/plan/limit.go b/sql/plan/limit.go index 30c9fb5cb0..c215ef28b3 100644 --- a/sql/plan/limit.go +++ b/sql/plan/limit.go @@ -38,6 +38,19 @@ func NewLimit(size sql.Expression, child sql.Node) *Limit { } } +// Expressions implements sql.Expressioner +func (o *Limit) Expressions() []sql.Expression { + return []sql.Expression{o.Limit} +} + +// WithExpressions implements sql.Expressioner +func (o *Limit) WithExpressions(exprs ...sql.Expression) (sql.Node, error) { + if len(exprs) != 1 { + return nil, sql.ErrInvalidChildrenNumber.New(o, len(exprs), 1) + } + return NewLimit(exprs[0], o.Child), nil +} + // Resolved implements the Resolvable interface. func (l *Limit) Resolved() bool { return l.UnaryNode.Child.Resolved() && l.Limit.Resolved() diff --git a/sql/plan/offset.go b/sql/plan/offset.go index 1b8b0903a7..b641cba370 100644 --- a/sql/plan/offset.go +++ b/sql/plan/offset.go @@ -34,6 +34,19 @@ func NewOffset(n sql.Expression, child sql.Node) *Offset { } } +// Expressions implements sql.Expressioner +func (o *Offset) Expressions() []sql.Expression { + return []sql.Expression{o.Offset} +} + +// WithExpressions implements sql.Expressioner +func (o *Offset) WithExpressions(exprs ...sql.Expression) (sql.Node, error) { + if len(exprs) != 1 { + return nil, sql.ErrInvalidChildrenNumber.New(o, len(exprs), 1) + } + return NewOffset(exprs[0], o.Child), nil +} + // Resolved implements the Resolvable interface. func (o *Offset) Resolved() bool { return o.Child.Resolved() && o.Offset.Resolved() From 4b5037fe3bb5ebdd62b425b6128b84d1d1d0a751 Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Fri, 28 May 2021 17:46:32 -0700 Subject: [PATCH 3/7] Fixed a couple bugs introduced while fixing the other bug --- enginetest/queries.go | 4 ++++ sql/expression/literal.go | 2 -- sql/plan/limit.go | 14 ++++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/enginetest/queries.go b/enginetest/queries.go index 549685c2d9..01ea798f51 100644 --- a/enginetest/queries.go +++ b/enginetest/queries.go @@ -1221,6 +1221,10 @@ var QueryTests = []QueryTest{ Query: "SELECT i FROM mytable WHERE s = 'first row' ORDER BY i DESC LIMIT 1;", Expected: []sql.Row{{int64(1)}}, }, + { + Query: "SELECT i FROM mytable WHERE s = 'first row' ORDER BY i DESC LIMIT 0;", + Expected: []sql.Row{}, + }, { Query: "SELECT i FROM mytable ORDER BY i LIMIT 1 OFFSET 1;", Expected: []sql.Row{{int64(2)}}, diff --git a/sql/expression/literal.go b/sql/expression/literal.go index 0ff20dc5fe..8597628437 100644 --- a/sql/expression/literal.go +++ b/sql/expression/literal.go @@ -62,8 +62,6 @@ func (p *Literal) String() string { switch v := p.value.(type) { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: return fmt.Sprintf("%d", v) - case float32, float64: - return fmt.Sprintf("%f", v) case string: return fmt.Sprintf("%q", v) case []byte: diff --git a/sql/plan/limit.go b/sql/plan/limit.go index c215ef28b3..eb68c5ed6b 100644 --- a/sql/plan/limit.go +++ b/sql/plan/limit.go @@ -39,16 +39,18 @@ func NewLimit(size sql.Expression, child sql.Node) *Limit { } // Expressions implements sql.Expressioner -func (o *Limit) Expressions() []sql.Expression { - return []sql.Expression{o.Limit} +func (l *Limit) Expressions() []sql.Expression { + return []sql.Expression{l.Limit} } // WithExpressions implements sql.Expressioner -func (o *Limit) WithExpressions(exprs ...sql.Expression) (sql.Node, error) { +func (l Limit) WithExpressions(exprs ...sql.Expression) (sql.Node, error) { if len(exprs) != 1 { - return nil, sql.ErrInvalidChildrenNumber.New(o, len(exprs), 1) + return nil, sql.ErrInvalidChildrenNumber.New(l, len(exprs), 1) } - return NewLimit(exprs[0], o.Child), nil + nl := &l + nl.Limit = exprs[0] + return nl, nil } // Resolved implements the Resolvable interface. @@ -71,7 +73,7 @@ func (l *Limit) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter, error) { return nil, err } return sql.NewSpanIter(span, &limitIter{ - l: l, + l: l, limit: limit, childIter: childIter, }), nil From 055f24efe6261a90cac69fcbc2cc5fa0362754dd Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Sun, 30 May 2021 14:45:38 -0700 Subject: [PATCH 4/7] Upgrade vitess --- go.mod | 4 +--- go.sum | 7 ++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 1f58907359..fbef4041b8 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ require ( github.com/VividCortex/gohistogram v1.0.0 // indirect github.com/cespare/xxhash v1.1.0 github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81 - github.com/dolthub/vitess v0.0.0-20210524220733-7b048c544267 + github.com/dolthub/vitess v0.0.0-20210530214338-7755381e6501 github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect github.com/go-kit/kit v0.9.0 github.com/go-sql-driver/mysql v1.6.0 @@ -32,6 +32,4 @@ require ( gopkg.in/src-d/go-errors.v1 v1.0.0 ) -replace github.com/dolthub/vitess => ../vitess - go 1.13 diff --git a/go.sum b/go.sum index b13edcea22..b29eae8161 100755 --- a/go.sum +++ b/go.sum @@ -14,15 +14,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81 h1:7/v8q9XGFa6q5Ap4Z/OhNkAMBaK5YeuEzwJt+NZdhiE= github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81/go.mod h1:siLfyv2c92W1eN/R4QqG/+RjjX5W2+gCTRjZxBjI3TY= -github.com/dolthub/vitess v0.0.0-20210524220733-7b048c544267 h1:g3KWBmLtSWlEbwUF4NV4a4jzE5aec8n2ZHWwSDy9IGY= -github.com/dolthub/vitess v0.0.0-20210524220733-7b048c544267/go.mod h1:hUE8oSk2H5JZnvtlLBhJPYC8WZCA5AoSntdLTcBvdBM= +github.com/dolthub/vitess v0.0.0-20210530214338-7755381e6501 h1:QO+maZZoP4PUwS5Clk/lo5AvZ8J5jHevbC/tTAfLe70= +github.com/dolthub/vitess v0.0.0-20210530214338-7755381e6501/go.mod h1:hUE8oSk2H5JZnvtlLBhJPYC8WZCA5AoSntdLTcBvdBM= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw= github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -127,12 +126,10 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190830154057-c17b040389b9 h1:5/jaG/gKlo3xxvUn85ReNyTlN7BvlPPsxC6sHZKjGEE= golang.org/x/tools v0.0.0-20190830154057-c17b040389b9/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= From bb0655434a59a49a4edd7b1369390ef091935b6f Mon Sep 17 00:00:00 2001 From: zachmu Date: Sun, 30 May 2021 21:47:59 +0000 Subject: [PATCH 5/7] [ga-format-pr] Run ./format_repo.sh to fix formatting --- enginetest/queries.go | 4 ++-- go.sum | 1 + sql/parse/warnings.go | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/enginetest/queries.go b/enginetest/queries.go index 01ea798f51..94b0b16426 100644 --- a/enginetest/queries.go +++ b/enginetest/queries.go @@ -1230,14 +1230,14 @@ var QueryTests = []QueryTest{ Expected: []sql.Row{{int64(2)}}, }, { - Query: "SELECT i FROM mytable WHERE s = 'first row' ORDER BY i DESC LIMIT ?;", + Query: "SELECT i FROM mytable WHERE s = 'first row' ORDER BY i DESC LIMIT ?;", Bindings: map[string]sql.Expression{ "v1": expression.NewLiteral(1, sql.Int8), }, Expected: []sql.Row{{int64(1)}}, }, { - Query: "SELECT i FROM mytable ORDER BY i LIMIT ? OFFSET 2;", + Query: "SELECT i FROM mytable ORDER BY i LIMIT ? OFFSET 2;", Bindings: map[string]sql.Expression{ "v1": expression.NewLiteral(1, sql.Int8), "v2": expression.NewLiteral(1, sql.Int8), diff --git a/go.sum b/go.sum index b29eae8161..4a27cf71d6 100755 --- a/go.sum +++ b/go.sum @@ -126,6 +126,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190830154057-c17b040389b9 h1:5/jaG/gKlo3xxvUn85ReNyTlN7BvlPPsxC6sHZKjGEE= golang.org/x/tools v0.0.0-20190830154057-c17b040389b9/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= diff --git a/sql/parse/warnings.go b/sql/parse/warnings.go index e406858b54..d4d6522d2c 100644 --- a/sql/parse/warnings.go +++ b/sql/parse/warnings.go @@ -22,10 +22,10 @@ import ( "strings" "unicode" - "github.com/dolthub/go-mysql-server/sql/expression" errors "gopkg.in/src-d/go-errors.v1" "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/plan" ) From 5195b80de1783b0694f265bbf0ffa3847b8a4e9c Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Sun, 30 May 2021 15:11:14 -0700 Subject: [PATCH 6/7] Fixed broken tests --- sql/analyzer/parallelize_test.go | 4 ++-- sql/plan/limit.go | 10 ++++++++++ sql/plan/limit_test.go | 7 ++++--- sql/plan/offset_test.go | 5 +++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/sql/analyzer/parallelize_test.go b/sql/analyzer/parallelize_test.go index 502e4e2041..34568bf847 100644 --- a/sql/analyzer/parallelize_test.go +++ b/sql/analyzer/parallelize_test.go @@ -198,7 +198,7 @@ func TestIsParallelizable(t *testing.T) { { "limit", plan.NewLimit( - 5, + expression.NewLiteral(5, sql.Int8), plan.NewResolvedTable(nil, nil, nil), ), false, @@ -206,7 +206,7 @@ func TestIsParallelizable(t *testing.T) { { "offset", plan.NewOffset( - 5, + expression.NewLiteral(5, sql.Int8), plan.NewResolvedTable(nil, nil, nil), ), false, diff --git a/sql/plan/limit.go b/sql/plan/limit.go index eb68c5ed6b..2a7f8c98a6 100644 --- a/sql/plan/limit.go +++ b/sql/plan/limit.go @@ -98,6 +98,16 @@ func getInt64Value(ctx *sql.Context, expr sql.Expression) (int64, error) { return int64(i), nil case int64: return i, nil + case uint: + return int64(i), nil + case uint8: + return int64(i), nil + case uint16: + return int64(i), nil + case uint32: + return int64(i), nil + case uint64: + return int64(i), nil default: // analyzer should catch this already panic(fmt.Sprintf("Unsupported type for limit %T", i)) diff --git a/sql/plan/limit_test.go b/sql/plan/limit_test.go index 57d8d041f3..c547dd31b1 100644 --- a/sql/plan/limit_test.go +++ b/sql/plan/limit_test.go @@ -20,6 +20,7 @@ import ( "reflect" "testing" + "github.com/dolthub/go-mysql-server/sql/expression" "github.com/stretchr/testify/require" "github.com/dolthub/go-mysql-server/memory" @@ -32,7 +33,7 @@ var testingTableSize int func TestLimitPlan(t *testing.T) { require := require.New(t) table, _ := getTestingTable(t) - limitPlan := NewLimit(0, NewResolvedTable(table, nil, nil)) + limitPlan := NewLimit(expression.NewLiteral(0, sql.Int8), NewResolvedTable(table, nil, nil)) require.Equal(1, len(limitPlan.Children())) iterator, err := getLimitedIterator(t, 1) @@ -43,7 +44,7 @@ func TestLimitPlan(t *testing.T) { func TestLimitImplementsNode(t *testing.T) { require := require.New(t) table, _ := getTestingTable(t) - limitPlan := NewLimit(0, NewResolvedTable(table, nil, nil)) + limitPlan := NewLimit(expression.NewLiteral(0, sql.Int8), NewResolvedTable(table, nil, nil)) childSchema := table.Schema() nodeSchema := limitPlan.Schema() require.True(reflect.DeepEqual(childSchema, nodeSchema)) @@ -122,7 +123,7 @@ func getLimitedIterator(t *testing.T, limitSize int64) (sql.RowIter, error) { t.Helper() ctx := sql.NewEmptyContext() table, _ := getTestingTable(t) - limitPlan := NewLimit(limitSize, NewResolvedTable(table, nil, nil)) + limitPlan := NewLimit(expression.NewLiteral(limitSize, sql.Int64), NewResolvedTable(table, nil, nil)) return limitPlan.RowIter(ctx, nil) } diff --git a/sql/plan/offset_test.go b/sql/plan/offset_test.go index 5c46ca13b3..1573667305 100644 --- a/sql/plan/offset_test.go +++ b/sql/plan/offset_test.go @@ -17,6 +17,7 @@ package plan import ( "testing" + "github.com/dolthub/go-mysql-server/sql/expression" "github.com/stretchr/testify/require" "github.com/dolthub/go-mysql-server/sql" @@ -27,7 +28,7 @@ func TestOffsetPlan(t *testing.T) { ctx := sql.NewEmptyContext() table, _ := getTestingTable(t) - offset := NewOffset(0, NewResolvedTable(table, nil, nil)) + offset := NewOffset(expression.NewLiteral(0, sql.Int8), NewResolvedTable(table, nil, nil)) require.Equal(1, len(offset.Children())) iter, err := offset.RowIter(ctx, nil) @@ -40,7 +41,7 @@ func TestOffset(t *testing.T) { ctx := sql.NewEmptyContext() table, n := getTestingTable(t) - offset := NewOffset(1, NewResolvedTable(table, nil, nil)) + offset := NewOffset(expression.NewLiteral(1, sql.Int8), NewResolvedTable(table, nil, nil)) iter, err := offset.RowIter(ctx, nil) require.NoError(err) From ca21092995d545b981673c24455e27711b06b73b Mon Sep 17 00:00:00 2001 From: zachmu Date: Sun, 30 May 2021 22:12:05 +0000 Subject: [PATCH 7/7] [ga-format-pr] Run ./format_repo.sh to fix formatting --- sql/plan/limit_test.go | 2 +- sql/plan/offset_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sql/plan/limit_test.go b/sql/plan/limit_test.go index c547dd31b1..782d564e99 100644 --- a/sql/plan/limit_test.go +++ b/sql/plan/limit_test.go @@ -20,11 +20,11 @@ import ( "reflect" "testing" - "github.com/dolthub/go-mysql-server/sql/expression" "github.com/stretchr/testify/require" "github.com/dolthub/go-mysql-server/memory" "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" ) var testingTable *memory.Table diff --git a/sql/plan/offset_test.go b/sql/plan/offset_test.go index 1573667305..c30024f85d 100644 --- a/sql/plan/offset_test.go +++ b/sql/plan/offset_test.go @@ -17,10 +17,10 @@ package plan import ( "testing" - "github.com/dolthub/go-mysql-server/sql/expression" "github.com/stretchr/testify/require" "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" ) func TestOffsetPlan(t *testing.T) {