Skip to content

Commit

Permalink
Add range clauses ([NOT] BETWEEN) support (#25)
Browse files Browse the repository at this point in the history
* add range clauses support

* range expression's comment typo

* add range methods to LiteralExpressions

* replace magic string with cleaner variable

* expressions map support for range clauses

* Replace both rhs1 and rhs2 for a single RangeVal struct with explicit fields
  • Loading branch information
denisvm authored and doug-martin committed Sep 14, 2016
1 parent e3d9d23 commit e051d5c
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 1 deletion.
4 changes: 4 additions & 0 deletions adapters.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ type (
//
//buf: The current SqlBuilder to write the sql to
BooleanExpressionSql(buf *SqlBuilder, operator BooleanExpression) error
//Generates SQL value for a RangeExpression
//
//buf: The current SqlBuilder to write the sql to
RangeExpressionSql(buf *SqlBuilder, operator RangeExpression) error
//Generates SQL value for an OrderedExpression
//
//buf: The current SqlBuilder to write the sql to
Expand Down
9 changes: 9 additions & 0 deletions adapters/mysql/mysql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ func (me *mysqlTest) TestQuery() {
assert.True(t, entry.Int <= 4)
}

entries = entries[0:0]
assert.NoError(t, ds.Where(goqu.I("int").Between(goqu.RangeVal{Start:3,End:6})).Order(goqu.I("id").Asc()).ScanStructs(&entries))
assert.Len(t, entries, 4)
assert.NoError(t, err)
for _, entry := range entries {
assert.True(t, entry.Int >= 3)
assert.True(t, entry.Int <= 6)
}

entries = entries[0:0]
assert.NoError(t, ds.Where(goqu.I("string").Eq("0.100000")).Order(goqu.I("id").Asc()).ScanStructs(&entries))
assert.Len(t, entries, 1)
Expand Down
9 changes: 9 additions & 0 deletions adapters/postgres/postgres_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,15 @@ func (me *postgresTest) TestQuery() {
assert.True(t, entry.Int <= 4)
}

entries = entries[0:0]
assert.NoError(t, ds.Where(goqu.I("int").Between(goqu.RangeVal{Start:3,End:6})).Order(goqu.I("id").Asc()).ScanStructs(&entries))
assert.Len(t, entries, 4)
assert.NoError(t, err)
for _, entry := range entries {
assert.True(t, entry.Int >= 3)
assert.True(t, entry.Int <= 6)
}

entries = entries[0:0]
assert.NoError(t, ds.Where(goqu.I("string").Eq("0.100000")).Order(goqu.I("id").Asc()).ScanStructs(&entries))
assert.Len(t, entries, 1)
Expand Down
9 changes: 9 additions & 0 deletions adapters/sqlite3/sqlite3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,15 @@ func (me *sqlite3Test) TestQuery() {
assert.True(t, entry.Int <= 4)
}

entries = entries[0:0]
assert.NoError(t, ds.Where(goqu.I("int").Between(goqu.RangeVal{Start:3,End:6})).Order(goqu.I("id").Asc()).ScanStructs(&entries))
assert.Len(t, entries, 4)
assert.NoError(t, err)
for _, entry := range entries {
assert.True(t, entry.Int >= 3)
assert.True(t, entry.Int <= 6)
}

entries = entries[0:0]
assert.NoError(t, ds.Where(goqu.I("string").Eq("0.100000")).Order(goqu.I("id").Asc()).ScanStructs(&entries))
assert.Len(t, entries, 1)
Expand Down
2 changes: 2 additions & 0 deletions dataset.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ func (me *Dataset) expressionSql(buf *SqlBuilder, expression Expression) error {
return me.adapter.AliasedExpressionSql(buf, e)
} else if e, ok := expression.(BooleanExpression); ok {
return me.adapter.BooleanExpressionSql(buf, e)
} else if e, ok := expression.(RangeExpression); ok {
return me.adapter.RangeExpressionSql(buf, e)
} else if e, ok := expression.(OrderedExpression); ok {
return me.adapter.OrderedExpressionSql(buf, e)
} else if e, ok := expression.(UpdateExpression); ok {
Expand Down
32 changes: 32 additions & 0 deletions dataset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,29 @@ func (me *datasetTest) TestBooleanExpression() {
assert.NoError(t, ds.Literal(me.Truncate(buf), I("a").NotILike(regexp.MustCompile("(a|b)"))))
assert.Equal(t, buf.args, []interface{}{"(a|b)"})
assert.Equal(t, buf.String(), `("a" !~* ?)`)
}

func (me *datasetTest) TestRangeExpression() {
t := me.T()
buf := NewSqlBuilder(false)
ds := From("test")
assert.NoError(t, ds.Literal(me.Truncate(buf), I("a").Between(RangeVal{Start:1,End:2})))
assert.Equal(t, buf.String(), `("a" BETWEEN 1 AND 2)`)
assert.NoError(t, ds.Literal(me.Truncate(buf), I("a").NotBetween(RangeVal{Start:1,End:2})))
assert.Equal(t, buf.String(), `("a" NOT BETWEEN 1 AND 2)`)
assert.NoError(t, ds.Literal(me.Truncate(buf), I("a").Between(RangeVal{Start:"aaa",End:"zzz"})))
assert.Equal(t, buf.String(), `("a" BETWEEN 'aaa' AND 'zzz')`)

buf = NewSqlBuilder(true)
assert.NoError(t, ds.Literal(me.Truncate(buf), I("a").Between(RangeVal{Start:1,End:2})))
assert.Equal(t, buf.args, []interface{}{1, 2})
assert.Equal(t, buf.String(), `("a" BETWEEN ? AND ?)`)
assert.NoError(t, ds.Literal(me.Truncate(buf), I("a").NotBetween(RangeVal{Start:1,End:2})))
assert.Equal(t, buf.args, []interface{}{1, 2})
assert.Equal(t, buf.String(), `("a" NOT BETWEEN ? AND ?)`)
assert.NoError(t, ds.Literal(me.Truncate(buf), I("a").Between(RangeVal{Start:"aaa",End:"zzz"})))
assert.Equal(t, buf.args, []interface{}{"aaa", "zzz"})
assert.Equal(t, buf.String(), `("a" BETWEEN ? AND ?)`)
}

func (me *datasetTest) TestLiteralOrderedExpression() {
Expand Down Expand Up @@ -748,6 +770,10 @@ func (me *datasetTest) TestLiteralExpressionMap() {
assert.Equal(t, buf.String(), `("a" NOT IN ('a', 'b', 'c'))`)
assert.NoError(t, ds.Literal(me.Truncate(buf), Ex{"a": Op{"is": nil, "eq": 10}}))
assert.Equal(t, buf.String(), `(("a" = 10) OR ("a" IS NULL))`)
assert.NoError(t, ds.Literal(me.Truncate(buf), Ex{"a": Op{"between": RangeVal{Start:1,End:10}}}))
assert.Equal(t, buf.String(), `("a" BETWEEN 1 AND 10)`)
assert.NoError(t, ds.Literal(me.Truncate(buf), Ex{"a": Op{"notbetween": RangeVal{Start:1,End:10}}}))
assert.Equal(t, buf.String(), `("a" NOT BETWEEN 1 AND 10)`)

buf = NewSqlBuilder(true)
assert.NoError(t, ds.Literal(me.Truncate(buf), Ex{"a": 1}))
Expand Down Expand Up @@ -796,6 +822,12 @@ func (me *datasetTest) TestLiteralExpressionMap() {
assert.NoError(t, ds.Literal(me.Truncate(buf), Ex{"a": Op{"is": nil, "eq": 10}}))
assert.Equal(t, buf.args, []interface{}{10, nil})
assert.Equal(t, buf.String(), `(("a" = ?) OR ("a" IS ?))`)
assert.NoError(t, ds.Literal(me.Truncate(buf), Ex{"a": Op{"between": RangeVal{Start:1,End:10}}}))
assert.Equal(t, buf.args, []interface{}{1, 10})
assert.Equal(t, buf.String(), `("a" BETWEEN ? AND ?)`)
assert.NoError(t, ds.Literal(me.Truncate(buf), Ex{"a": Op{"notbetween": RangeVal{Start:1,End:10}}}))
assert.Equal(t, buf.args, []interface{}{1, 10})
assert.Equal(t, buf.String(), `("a" NOT BETWEEN ? AND ?)`)
}

func (me *datasetTest) TestLiteralExpressionOrMap() {
Expand Down
33 changes: 33 additions & 0 deletions default_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ var (
REGEXP_I_LIKE_OP: []byte("~*"),
REGEXP_NOT_I_LIKE_OP: []byte("!~*"),
}
default_rangeop_lookup = map[RangeOperation][]byte{
BETWEEN_OP: []byte("BETWEEN"),
NBETWEEN_OP: []byte("NOT BETWEEN"),
}
default_join_lookup = map[JoinType][]byte{
INNER_JOIN: []byte(" INNER JOIN "),
FULL_OUTER_JOIN: []byte(" FULL OUTER JOIN "),
Expand Down Expand Up @@ -182,6 +186,8 @@ type (
TimeFormat string
//A map used to look up BooleanOperations and their SQL equivalents
BooleanOperatorLookup map[BooleanOperation][]byte
//A map used to look up RangeOperations and their SQL equivalents
RangeOperatorLookup map[RangeOperation][]byte
//A map used to look up JoinTypes and their SQL equivalents
JoinTypeLookup map[JoinType][]byte
//Whether or not to use literal TRUE or FALSE for IS statements (e.g. IS TRUE or IS 0)
Expand Down Expand Up @@ -233,6 +239,7 @@ func NewDefaultAdapter(ds *Dataset) Adapter {
IntersectAllFragment: default_intersect_all_fragment,
PlaceHolderRune: default_place_holder_rune,
BooleanOperatorLookup: default_operator_lookup,
RangeOperatorLookup: default_rangeop_lookup,
JoinTypeLookup: default_join_lookup,
TimeFormat: time.RFC3339Nano,
UseLiteralIsBools: true,
Expand Down Expand Up @@ -726,6 +733,32 @@ func (me *DefaultAdapter) BooleanExpressionSql(buf *SqlBuilder, operator Boolean
return nil
}

//Generates SQL for a RangeExpresion (e.g. I("a").Between(RangeVal{Start:2,End:5}) -> "a" BETWEEN 2 AND 5)
func (me *DefaultAdapter) RangeExpressionSql(buf *SqlBuilder, operator RangeExpression) error {
buf.WriteRune(left_paren_rune)
if err := me.Literal(buf, operator.Lhs()); err != nil {
return err
}
buf.WriteRune(space_rune)
operatorOp := operator.Op()
if val, ok := me.RangeOperatorLookup[operatorOp]; ok {
buf.Write(val)
} else {
return NewGoquError("Range operator %+v not supported", operatorOp)
}
rhs := operator.Rhs()
buf.WriteRune(space_rune)
if err := me.Literal(buf, rhs.Start); err != nil {
return err
}
buf.Write(default_and_fragment)
if err := me.Literal(buf, rhs.End); err != nil {
return err
}
buf.WriteRune(right_paren_rune)
return nil
}

//Generates SQL for an OrderedExpression (e.g. I("a").Asc() -> "a" ASC)
func (me *DefaultAdapter) OrderedExpressionSql(buf *SqlBuilder, order OrderedExpression) error {
if err := me.Literal(buf, order.SortExpression()); err != nil {
Expand Down
23 changes: 22 additions & 1 deletion example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ func ExampleComparisonMethods() {
sql, _, _ = db.From("test").Where(goqu.L("(a + b)").Lte(10)).ToSql()
fmt.Println(sql)

sql, _, _ = db.From("test").Where(goqu.L("(a + b)").Between(goqu.RangeVal{Start:10,End:100})).ToSql()
fmt.Println(sql)

sql, _, _ = db.From("test").Where(goqu.L("(a + b)").NotBetween(goqu.RangeVal{Start:10,End:100})).ToSql()
fmt.Println(sql)

//used with Ex expression map
sql, _, _ = db.From("test").Where(goqu.Ex{
"a": 10,
Expand All @@ -208,6 +214,8 @@ func ExampleComparisonMethods() {
// SELECT * FROM "test" WHERE ((a + b) >= 10)
// SELECT * FROM "test" WHERE ((a + b) < 10)
// SELECT * FROM "test" WHERE ((a + b) <= 10)
// SELECT * FROM "test" WHERE ((a + b) BETWEEN 10 AND 100)
// SELECT * FROM "test" WHERE ((a + b) NOT BETWEEN 10 AND 100)
// SELECT * FROM "test" WHERE (("a" = 10) AND ("b" != 10) AND ("c" >= 10) AND ("d" < 10) AND ("e" <= 10))
}

Expand Down Expand Up @@ -1511,12 +1519,18 @@ func ExampleEx_withOpPrepared() {
}).ToSql()
fmt.Println(sql, args)

sql, args, _ = db.From("items").Prepared(true).Where(goqu.Ex{
"col1": goqu.Op{"between": goqu.RangeVal{Start:1,End:10}},
"col2": goqu.Op{"notbetween": goqu.RangeVal{Start:1,End:10}},
}).ToSql()
fmt.Println(sql, args)

// Output:
// SELECT * FROM "items" WHERE (("col1" != ?) AND ("col3" IS NOT TRUE) AND ("col6" NOT IN (?, ?, ?))) [a a b c]
// SELECT * FROM "items" WHERE (("col1" > ?) AND ("col2" >= ?) AND ("col3" < ?) AND ("col4" <= ?)) [1 1 1 1]
// SELECT * FROM "items" WHERE (("col1" LIKE ?) AND ("col2" NOT LIKE ?) AND ("col3" ILIKE ?) AND ("col4" NOT ILIKE ?)) [a% a% a% a%]
// SELECT * FROM "items" WHERE (("col1" ~ ?) AND ("col2" !~ ?) AND ("col3" ~* ?) AND ("col4" !~* ?)) [^(a|b) ^(a|b) ^(a|b) ^(a|b)]

// SELECT * FROM "items" WHERE (("col1" BETWEEN ? AND ?) AND ("col2" NOT BETWEEN ? AND ?)) [1 10 1 10]
}

func ExampleOp() {
Expand Down Expand Up @@ -1552,11 +1566,18 @@ func ExampleOp() {
}).ToSql()
fmt.Println(sql)

sql, _, _ = db.From("items").Where(goqu.Ex{
"col1": goqu.Op{"between": goqu.RangeVal{Start:1,End:10}},
"col2": goqu.Op{"notbetween": goqu.RangeVal{Start:1,End:10}},
}).ToSql()
fmt.Println(sql)

// Output:
// SELECT * FROM "items" WHERE (("col1" != 'a') AND ("col3" IS NOT TRUE) AND ("col6" NOT IN ('a', 'b', 'c')))
// SELECT * FROM "items" WHERE (("col1" > 1) AND ("col2" >= 1) AND ("col3" < 1) AND ("col4" <= 1))
// SELECT * FROM "items" WHERE (("col1" LIKE 'a%') AND ("col2" NOT LIKE 'a%') AND ("col3" ILIKE 'a%') AND ("col4" NOT ILIKE 'a%'))
// SELECT * FROM "items" WHERE (("col1" ~ '^(a|b)') AND ("col2" !~ '^(a|b)') AND ("col3" ~* '^(a|b)') AND ("col4" !~* '^(a|b)'))
// SELECT * FROM "items" WHERE (("col1" BETWEEN 1 AND 10) AND ("col2" NOT BETWEEN 1 AND 10))
}

func ExampleOp_withMultipleKeys() {
Expand Down
92 changes: 92 additions & 0 deletions expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ func mapToExpressionList(ex map[string]interface{}, eType ExpressionListType) (E
ored = lhs.ILike(op[opKey])
case "notilike":
ored = lhs.NotILike(op[opKey])
case "between":
rangeVal, ok := op[opKey].(RangeVal)
if ok {
ored = lhs.Between(rangeVal)
}
case "notbetween":
rangeVal, ok := op[opKey].(RangeVal)
if ok {
ored = lhs.NotBetween(rangeVal)
}
default:
return nil, NewGoquError("Unsupported expression type %s", op)
}
Expand Down Expand Up @@ -444,6 +454,14 @@ type (
// I("col").Lte(1) //("col" <= 1)
Lte(interface{}) BooleanExpression
}
RangeMethods interface {
//Creates a Range expression for between comparisons
// I("col").Between(RangeVal{Start:1, End:10}) //("col" BETWEEN 1 AND 10)
Between(RangeVal) RangeExpression
//Creates a Range expression for between comparisons
// I("col").NotBetween(RangeVal{Start:1, End:10}) //("col" NOT BETWEEN 1 AND 10)
NotBetween(RangeVal) RangeExpression
}
//Interface that an expression should implement if it can be used in an IN expression
InMethods interface {
//Creates a Boolean expression for IN clauses
Expand Down Expand Up @@ -526,6 +544,7 @@ type (
Expression
AliasMethods
ComparisonMethods
RangeMethods
InMethods
StringMethods
BooleanMethods
Expand Down Expand Up @@ -683,6 +702,12 @@ func (me identifier) Desc() OrderedExpression { return desc(
func (me identifier) Distinct() SqlFunctionExpression { return DISTINCT(me) }
func (me identifier) Cast(t string) CastExpression { return Cast(me, t) }

//Returns a RangeExpression for checking that a identifier is between two values (e.g "my_col" BETWEEN 1 AND 10)
func (me identifier) Between(val RangeVal) RangeExpression { return between(me, val) }

//Returns a RangeExpression for checking that a identifier is between two values (e.g "my_col" BETWEEN 1 AND 10)
func (me identifier) NotBetween(val RangeVal) RangeExpression { return notBetween(me, val) }

type (
//Expression for representing "literal" sql.
// L("col = 1") -> col = 1)
Expand All @@ -691,6 +716,7 @@ type (
Expression
AliasMethods
ComparisonMethods
RangeMethods
OrderedMethods
//Returns the literal sql
Literal() string
Expand Down Expand Up @@ -751,6 +777,8 @@ func (me literal) Lt(val interface{}) BooleanExpression { return lt(me, val) }
func (me literal) Lte(val interface{}) BooleanExpression { return lte(me, val) }
func (me literal) Asc() OrderedExpression { return asc(me) }
func (me literal) Desc() OrderedExpression { return desc(me) }
func (me literal) Between(val RangeVal) RangeExpression { return between(me, val) }
func (me literal) NotBetween(val RangeVal) RangeExpression { return notBetween(me, val) }

type (
UpdateExpression interface {
Expand Down Expand Up @@ -1004,6 +1032,66 @@ func checkBoolExpType(op BooleanOperation, lhs Expression, rhs interface{}, inve
return boolean{op: op, lhs: lhs, rhs: rhs}
}


type (
RangeOperation int
RangeExpression interface {
Expression
//Returns the operator for the expression
Op() RangeOperation
//The left hand side of the expression (e.g. I("a")
Lhs() Expression
//The right hand side of the expression could be a primitive value, dataset, or expression
Rhs() RangeVal
}
ranged struct {
lhs Expression
rhs RangeVal
op RangeOperation
}
RangeVal struct {
Start interface{}
End interface{}
}
)

const (
//BETWEEN
BETWEEN_OP RangeOperation = iota
//NOT BETWEEN
NBETWEEN_OP
)

func (me ranged) Clone() Expression {
return ranged{op: me.op, lhs: me.lhs.Clone(), rhs: me.rhs}
}

func (me ranged) Expression() Expression {
return me
}

func (me ranged) Rhs() RangeVal {
return me.rhs
}

func (me ranged) Lhs() Expression {
return me.lhs
}

func (me ranged) Op() RangeOperation {
return me.op
}

//used internally to create an BETWEEN comparison RangeExpression
func between(lhs Expression, rhs RangeVal) RangeExpression {
return ranged{op: BETWEEN_OP, lhs: lhs, rhs: rhs}
}

//used internally to create an NOT BETWEEN comparison RangeExpression
func notBetween(lhs Expression, rhs RangeVal) RangeExpression {
return ranged{op: NBETWEEN_OP, lhs: lhs, rhs: rhs}
}

type (
//Expression for Aliased expressions
// I("a").As("b") -> "a" AS "b"
Expand Down Expand Up @@ -1217,6 +1305,8 @@ func (me sqlFunctionExpression) Gt(val interface{}) BooleanExpression { return
func (me sqlFunctionExpression) Gte(val interface{}) BooleanExpression { return gte(me, val) }
func (me sqlFunctionExpression) Lt(val interface{}) BooleanExpression { return lt(me, val) }
func (me sqlFunctionExpression) Lte(val interface{}) BooleanExpression { return lte(me, val) }
func (me sqlFunctionExpression) Between(val RangeVal) RangeExpression { return between(me, val) }
func (me sqlFunctionExpression) NotBetween(val RangeVal) RangeExpression { return notBetween(me, val) }

type (
//An Expression that represents another Expression casted to a SQL type
Expand Down Expand Up @@ -1283,6 +1373,8 @@ func (me cast) IsNotTrue() BooleanExpression { return isNot(me, true
func (me cast) IsFalse() BooleanExpression { return is(me, false) }
func (me cast) IsNotFalse() BooleanExpression { return isNot(me, nil) }
func (me cast) Distinct() SqlFunctionExpression { return DISTINCT(me) }
func (me cast) Between(val RangeVal) RangeExpression { return between(me, val) }
func (me cast) NotBetween(val RangeVal) RangeExpression{ return notBetween(me, val) }

type (
compoundType int
Expand Down

0 comments on commit e051d5c

Please sign in to comment.