Skip to content

Commit 5335db7

Browse files
committed
Range, EqNotEmpty
1 parent 8c60a41 commit 5335db7

File tree

3 files changed

+129
-5
lines changed

3 files changed

+129
-5
lines changed

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ sq.Case("id").When(1, 2).When(2, "text").Else(4)
3333

3434
## New features
3535

36-
- Add subquery support for `WHERE` clause (e.g. `sq.Eq{"id": sq.Select("id").From("other_table")}`).
37-
- Add support for integer values in `CASE THEN/ELSE` clause (e.g. `sq.Case("id").When(1, 2).When(2, 3).Else(4)`).
38-
- Add support for aggregate functions `SUM`, `COUNT`, `AVG`, `MIN`, `MAX` (e.g. `sq.Sum(subQuery)`).
39-
- Add support for using slice as argument for `Column` function (e.g. `Column(sq.Expr("id = ANY(?)", []int{1,2,3}))`).
40-
- Add support for `IN` and `NOT IN` clause (e.g. `In("id", []int{1, 2, 3})`, `NotIn("id", subQuery)`).
36+
- Subquery support for `WHERE` clause (e.g. `sq.Eq{"id": sq.Select("id").From("other_table")}`).
37+
- Support for integer values in `CASE THEN/ELSE` clause (e.g. `sq.Case("id").When(1, 2).When(2, 3).Else(4)`).
38+
- Support for aggregate functions `SUM`, `COUNT`, `AVG`, `MIN`, `MAX` (e.g. `sq.Sum(subQuery)`).
39+
- Support for using slice as argument for `Column` function (e.g. `Column(sq.Expr("id = ANY(?)", []int{1,2,3}))`).
40+
- Support for `IN` and `NOT IN` clause (e.g. `In("id", []int{1, 2, 3})`, `NotIn("id", subQuery)`).
41+
- Range function: `sq.Range("id", 1, 10)` -> `id BETWEEN 1 AND 10`. `sq.Range("id", 1, nil)` -> `id >= 1`. `sq.Range("id", nil, 10)` -> `id <= 10`.
42+
- EqNotEmpty function: ignores empty and zero values in Eq map. Useful for filtering. `EqNotEmpty{"id1": 1, "name": nil, id2: 0, "desc": ""}` -> `id1 = 1`.
4143

4244
## Miscellaneous
4345

expr.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,3 +627,89 @@ func (e notInExpr) ToSql() (sql string, args []any, err error) {
627627

628628
return sql, args, err
629629
}
630+
631+
// rangeExpr helps to use BETWEEN in SQL query
632+
type rangeExpr struct {
633+
column string
634+
start any
635+
end any
636+
}
637+
638+
// Range allows to use range in SQL query
639+
// Ex: SelectBuilder.Where(Range("id", 1, 3)) -> "id BETWEEN 1 AND 3"
640+
// If start or end is nil, it will be omitted from the query.
641+
// Ex: SelectBuilder.Where(Range("id", 1, nil)) -> "id >= 1"
642+
// Ex: SelectBuilder.Where(Range("id", nil, 3)) -> "id <= 3"
643+
func Range(column string, start, end any) rangeExpr {
644+
return rangeExpr{column, start, end}
645+
}
646+
647+
// ToSql builds the query into a SQL string and bound args.
648+
func (e rangeExpr) ToSql() (sql string, args []any, err error) {
649+
hasStart := e.start != nil && !reflect.ValueOf(e.start).IsZero()
650+
hasEnd := e.end != nil && !reflect.ValueOf(e.end).IsZero()
651+
652+
if !hasStart && !hasEnd {
653+
return "", nil, nil
654+
}
655+
656+
var s Sqlizer
657+
if hasStart && hasEnd {
658+
s = Expr(fmt.Sprintf("%s BETWEEN ? AND ?", e.column), e.start, e.end)
659+
} else if hasStart {
660+
s = GtOrEq{e.column: e.start}
661+
} else {
662+
s = LtOrEq{e.column: e.end}
663+
}
664+
665+
return s.ToSql()
666+
}
667+
668+
// EqNotEmpty ignores empty and zero values in Eq map.
669+
// Ex: EqNotEmpty{"id1": 1, "name": nil, id2: 0, "desc": ""} -> "id1 = 1".
670+
type EqNotEmpty map[string]any
671+
672+
// ToSql builds the query into a SQL string and bound args.
673+
func (eq EqNotEmpty) ToSql() (sql string, args []any, err error) {
674+
vals := make(Eq, len(eq))
675+
for k, v := range eq {
676+
v = clearEmptyValue(v)
677+
if v != nil {
678+
vals[k] = v
679+
}
680+
}
681+
682+
return vals.ToSql()
683+
}
684+
685+
// clearEmptyValue recursively clears empty and zero values in any type.
686+
func clearEmptyValue(v any) any {
687+
if v == nil {
688+
return nil
689+
}
690+
691+
t := reflect.ValueOf(v)
692+
switch t.Kind() { //nolint:exhaustive
693+
case reflect.Array, reflect.Slice:
694+
if t.Len() != 0 {
695+
newSlice := reflect.MakeSlice(t.Type(), 0, t.Len())
696+
for i := 0; i < t.Len(); i++ {
697+
itemVal := clearEmptyValue(t.Index(i).Interface())
698+
if itemVal != nil {
699+
newSlice = reflect.Append(newSlice, t.Index(i))
700+
}
701+
}
702+
703+
if newSlice.Len() != 0 {
704+
return newSlice.Interface()
705+
}
706+
}
707+
708+
default:
709+
if !t.IsZero() {
710+
return v
711+
}
712+
}
713+
714+
return nil
715+
}

expr_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,42 @@ func TestIn(t *testing.T) {
554554
assert.Equal(t, []any{[]int{1, 2, 3}}, args)
555555
}
556556

557+
func Test_Range(t *testing.T) {
558+
sql, args, err := Range("id", 1, 10).ToSql()
559+
assert.NoError(t, err)
560+
assert.Equal(t, "id BETWEEN ? AND ?", sql)
561+
assert.Equal(t, []any{1, 10}, args)
562+
563+
sql, args, err = Range("id", 1, nil).ToSql()
564+
assert.NoError(t, err)
565+
assert.Equal(t, "id >= ?", sql)
566+
assert.Equal(t, []any{1}, args)
567+
568+
sql, args, err = Range("id", nil, 10).ToSql()
569+
assert.NoError(t, err)
570+
assert.Equal(t, "id <= ?", sql)
571+
assert.Equal(t, []any{10}, args)
572+
573+
sql, args, err = Range("id", nil, nil).ToSql()
574+
assert.NoError(t, err)
575+
assert.Equal(t, "", sql)
576+
assert.Empty(t, args)
577+
}
578+
579+
func Test_EqNotEmpty(t *testing.T) {
580+
sql, args, err := EqNotEmpty{
581+
"col1": 1,
582+
"col2": 0,
583+
"col3": "",
584+
"col4": nil,
585+
"col5": []int{2, 0, 3},
586+
"col6": []any{0, 0},
587+
}.ToSql()
588+
assert.NoError(t, err)
589+
assert.Equal(t, "col1 = ? AND col5 IN (?,?)", sql)
590+
assert.Equal(t, []any{1, 2, 3}, args)
591+
}
592+
557593
func ExampleEq() {
558594
Select("id", "created", "first_name").From("users").Where(Eq{
559595
"company": 20,

0 commit comments

Comments
 (0)