diff --git a/adapters.go b/adapters.go index 96016cbd..9ed9c6e6 100644 --- a/adapters.go +++ b/adapters.go @@ -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 diff --git a/adapters/mysql/mysql_test.go b/adapters/mysql/mysql_test.go index 6ed8ee42..fb7f18df 100644 --- a/adapters/mysql/mysql_test.go +++ b/adapters/mysql/mysql_test.go @@ -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) diff --git a/adapters/postgres/postgres_test.go b/adapters/postgres/postgres_test.go index 31e7d52e..9b5828e7 100644 --- a/adapters/postgres/postgres_test.go +++ b/adapters/postgres/postgres_test.go @@ -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) diff --git a/adapters/sqlite3/sqlite3_test.go b/adapters/sqlite3/sqlite3_test.go index 5afdd4e3..bd33fff1 100644 --- a/adapters/sqlite3/sqlite3_test.go +++ b/adapters/sqlite3/sqlite3_test.go @@ -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) diff --git a/dataset.go b/dataset.go index 4c144a06..9da2e65c 100644 --- a/dataset.go +++ b/dataset.go @@ -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 { diff --git a/dataset_test.go b/dataset_test.go index 68c28ce9..9c2aada0 100644 --- a/dataset_test.go +++ b/dataset_test.go @@ -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() { @@ -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})) @@ -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() { diff --git a/default_adapter.go b/default_adapter.go index b0eb1577..2ca2b16b 100644 --- a/default_adapter.go +++ b/default_adapter.go @@ -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 "), @@ -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) @@ -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, @@ -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 { diff --git a/example_test.go b/example_test.go index a82c85fd..72f8c8e1 100644 --- a/example_test.go +++ b/example_test.go @@ -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, @@ -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)) } @@ -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() { @@ -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() { diff --git a/expressions.go b/expressions.go index 1a94114d..d7db12b5 100644 --- a/expressions.go +++ b/expressions.go @@ -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) } @@ -443,6 +453,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 @@ -525,6 +543,7 @@ type ( Expression AliasMethods ComparisonMethods + RangeMethods InMethods StringMethods BooleanMethods @@ -682,6 +701,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) @@ -690,6 +715,7 @@ type ( Expression AliasMethods ComparisonMethods + RangeMethods OrderedMethods //Returns the literal sql Literal() string @@ -750,6 +776,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 { @@ -1003,6 +1031,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" @@ -1216,6 +1304,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 @@ -1282,6 +1372,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