Finish WHERE IN feature

* Fix bug with appendOr/And ordering
This commit is contained in:
Patrick O'brien 2016-08-19 03:21:53 +10:00
parent 3cdfc6237a
commit d28b3f4b62
5 changed files with 252 additions and 174 deletions

View file

@ -1 +1 @@
UPDATE thing happy, "fun", "stuff" SET ("col2", "fun"."col3", "col1") = ($1,$2,$3) WHERE (aa=$4 or bb=$5 or cc=$6) OR (dd=$7 or ee=$8 or ff=$9 and gg=$10) LIMIT 5;
UPDATE thing happy, "fun", "stuff" SET ("col2", "fun"."col3", "col1") = ($1,$2,$3) WHERE (aa=$4 or bb=$5 or cc=$6) AND (dd=$7 or ee=$8 or ff=$9 and gg=$10) LIMIT 5;

View file

@ -63,8 +63,8 @@ func And(clause string, args ...interface{}) QueryMod {
// Or allows you to specify a where clause seperated by an OR for your statement
func Or(clause string, args ...interface{}) QueryMod {
return func(q *boil.Query) {
boil.SetLastWhereAsOr(q)
boil.AppendWhere(q, clause, args...)
boil.SetLastWhereAsOr(q)
}
}
@ -90,8 +90,8 @@ func AndIn(clause string, args ...interface{}) QueryMod {
// an OR for your where statement
func OrIn(clause string, args ...interface{}) QueryMod {
return func(q *boil.Query) {
boil.SetLastInAsOr(q)
boil.AppendIn(q, clause, args...)
boil.SetLastInAsOr(q)
}
}

View file

@ -12,6 +12,7 @@ import (
var (
rgxIdentifier = regexp.MustCompile(`^(?i)"?[a-z_][_a-z0-9]*"?(?:\."?[_a-z][_a-z0-9]*"?)*$`)
rgxInClause = regexp.MustCompile(`^(?i)(.*[\s|\)|\?])IN([\s|\(|\?].*)$`)
)
func buildQuery(q *Query) (string, []interface{}) {
@ -77,7 +78,8 @@ func buildSelectQuery(q *Query) (*bytes.Buffer, []interface{}) {
fmt.Fprintf(joinBuf, " INNER JOIN %s", j.clause)
args = append(args, j.args...)
}
fmt.Fprintf(buf, convertQuestionMarks(joinBuf.String(), argsLen+1))
resp, _ := convertQuestionMarks(joinBuf.String(), argsLen+1)
fmt.Fprintf(buf, resp)
strmangle.PutBuffer(joinBuf)
}
@ -188,7 +190,8 @@ func writeModifiers(q *Query, buf *bytes.Buffer, args *[]interface{}) {
fmt.Fprintf(havingBuf, j.clause)
*args = append(*args, j.args...)
}
fmt.Fprintf(buf, convertQuestionMarks(havingBuf.String(), argsLen+1))
resp, _ := convertQuestionMarks(havingBuf.String(), argsLen+1)
fmt.Fprintf(buf, resp)
strmangle.PutBuffer(havingBuf)
}
@ -267,25 +270,26 @@ func whereClause(q *Query, startAt int) (string, []interface{}) {
var args []interface{}
buf.WriteString(" WHERE ")
for i := 0; i < len(q.where); i++ {
buf.WriteString(fmt.Sprintf("(%s)", q.where[i].clause))
args = append(args, q.where[i].args...)
// break on the last loop
if i == len(q.where)-1 {
break
for i, where := range q.where {
if i != 0 {
if where.orSeparator {
buf.WriteString(" OR ")
} else {
buf.WriteString(" AND ")
}
}
if q.where[i].orSeparator {
buf.WriteString(" OR ")
} else {
buf.WriteString(" AND ")
}
buf.WriteString(fmt.Sprintf("(%s)", where.clause))
args = append(args, where.args...)
}
return convertQuestionMarks(buf.String(), startAt), args
resp, _ := convertQuestionMarks(buf.String(), startAt)
return resp, args
}
// inClause parses an in slice and converts it into a
// single IN clause, like:
// WHERE ("a", "b") IN (($1,$2),($3,$4)).
func inClause(q *Query, startAt int) (string, []interface{}) {
if len(q.in) == 0 {
return "", nil
@ -298,28 +302,93 @@ func inClause(q *Query, startAt int) (string, []interface{}) {
if len(q.where) == 0 {
buf.WriteString(" WHERE ")
}
for i := 0; i < len(q.in); i++ {
for i, in := range q.in {
ln := len(in.args)
// We only prefix the OR and AND separators after the first
// clause has been generated UNLESS there is already a where
// clause that we have to add on to.
if i != 0 || len(q.where) > 0 {
if in.orSeparator {
buf.WriteString(" OR ")
} else {
buf.WriteString(" AND ")
}
}
matches := rgxInClause.FindStringSubmatch(in.clause)
// If we can't find any matches attempt a simple replace with 1 group.
// Clauses that fit this criteria will not be able to contain ? in their
// column name side, however if this case is being hit then the regexp
// probably needs adjustment, or the user is passing in invalid clauses.
if matches == nil {
clause, count := convertInQuestionMarks(in.clause, startAt, 1, ln)
buf.WriteString(clause)
startAt = startAt + count
} else {
leftSide := strings.TrimSpace(matches[1])
rightSide := strings.TrimSpace(matches[2])
// If matches are found, we have to parse the left side (column side)
// of the clause to determine how many columns they are using.
// This number determines the groupAt for the convert function.
cols := strings.Split(leftSide, ",")
cols = strmangle.IdentQuoteSlice(cols)
groupAt := len(cols)
leftClause, leftCount := convertQuestionMarks(strings.Join(cols, ","), startAt)
rightClause, rightCount := convertInQuestionMarks(rightSide, startAt+leftCount, groupAt, ln-leftCount)
buf.WriteString(leftClause)
buf.WriteString(" IN ")
buf.WriteString(rightClause)
startAt = startAt + leftCount + rightCount
}
args = append(args, in.args...)
}
// regexp split thing so we have left side and right side
// split on )IN( / \sIN\s, combine them
// buf.WriteString(convertQuestionMarks(leftSide, startAt))
// buf.WriteString(" IN ")
// buf.WriteString(convertInQuestionMarks(rightSide, total, group, startAt+offset))
return "", args
return buf.String(), args
}
func convertInQuestionMarks(clause string, total, groupAt, startAt int) string {
return ""
// convertInQuestionMarks finds the first unescaped occurence of ? and swaps it
// with a list of numbered placeholders, starting at startAt.
// It uses groupAt to determine how many placeholders should be in each group,
// for example, groupAt 2 would result in: (($1,$2),($3,$4))
// and groupAt 1 would result in ($1,$2,$3,$4)
func convertInQuestionMarks(clause string, startAt, groupAt, total int) (string, int) {
if startAt == 0 || len(clause) == 0 {
panic("Not a valid start number.")
}
paramBuf := strmangle.GetBuffer()
defer strmangle.PutBuffer(paramBuf)
foundAt := -1
for i := 0; i < len(clause); i++ {
if (clause[i] == '?' && i == 0) || (clause[i] == '?' && clause[i-1] != '\\') {
foundAt = i
break
}
}
if foundAt == -1 {
return strings.Replace(clause, `\?`, "?", -1), 0
}
paramBuf.WriteString(clause[:foundAt])
paramBuf.WriteByte('(')
paramBuf.WriteString(strmangle.Placeholders(total, startAt, groupAt))
paramBuf.WriteByte(')')
paramBuf.WriteString(clause[foundAt+1:])
// Remove all backslashes from escaped question-marks
ret := strings.Replace(paramBuf.String(), `\?`, "?", -1)
return ret, total
}
// convertQuestionMarks converts each occurence of ? with $<number>
// where <number> is an incrementing digit starting at startAt.
// If question-mark (?) is escaped using back-slash (\), it will be ignored.
func convertQuestionMarks(clause string, startAt int) string {
func convertQuestionMarks(clause string, startAt int) (string, int) {
if startAt == 0 {
panic("Not a valid start number.")
}
@ -327,6 +396,7 @@ func convertQuestionMarks(clause string, startAt int) string {
paramBuf := strmangle.GetBuffer()
defer strmangle.PutBuffer(paramBuf)
paramIndex := 0
total := 0
for {
if paramIndex >= len(clause) {
@ -349,11 +419,12 @@ func convertQuestionMarks(clause string, startAt int) string {
}
paramBuf.WriteString(clause[:paramIndex] + fmt.Sprintf("$%d", startAt))
total++
startAt++
paramIndex++
}
return paramBuf.String()
return paramBuf.String(), total
}
// identifierMapping creates a map of all identifiers to potential model names

View file

@ -247,8 +247,8 @@ func TestWhereClause(t *testing.T) {
{
q: Query{
where: []where{
where{clause: "(a=?)", orSeparator: true},
where{clause: "(b=?)"},
where{clause: "(a=?)"},
where{clause: "(b=?)", orSeparator: true},
},
},
expect: " WHERE ((a=$1)) OR ((b=$2))",
@ -318,11 +318,11 @@ func TestWhereClause(t *testing.T) {
q: Query{
where: []where{
where{clause: "a=? or b=?"},
where{clause: "c=? and d=?"},
where{clause: "c=? and d=?", orSeparator: true},
where{clause: "e=? or f=?"},
},
},
expect: " WHERE (a=$1 or b=$2) AND (c=$3 and d=$4) AND (e=$5 or f=$6)",
expect: " WHERE (a=$1 or b=$2) OR (c=$3 and d=$4) AND (e=$5 or f=$6)",
},
}
@ -340,129 +340,132 @@ func TestInClause(t *testing.T) {
tests := []struct {
q Query
expect string
args []interface{}
}{
// Or("a=?")
{
q: Query{
in: []in{{clause: "a in ?", args: []interface{}{}, orSeparator: true}},
},
expect: ` WHERE "a" IN ()`,
},
{
q: Query{
in: []in{{clause: "a in ?", args: []interface{}{1}, orSeparator: true}},
},
expect: " WHERE a IN ($1)",
expect: ` WHERE "a" IN ($1)`,
args: []interface{}{1},
},
{
q: Query{
in: []in{{clause: "a in ?", args: []interface{}{1, 2, 3}}},
},
expect: " WHERE a IN ($1,$2,$3)",
expect: ` WHERE "a" IN ($1,$2,$3)`,
args: []interface{}{1, 2, 3},
},
{
q: Query{
in: []in{{clause: "? in ?", args: []interface{}{1, 2, 3}}},
},
expect: " WHERE $1 IN ($2,$3)",
args: []interface{}{1, 2, 3},
},
{
q: Query{
in: []in{{clause: "( ? , ? ) in ( ? )", orSeparator: true, args: []interface{}{"a", "b", 1, 2, 3, 4}}},
},
expect: " WHERE ( $1 , $2 ) IN ( (($3,$4),($5,$6)) )",
args: []interface{}{"a", "b", 1, 2, 3, 4},
},
{
q: Query{
in: []in{{clause: `("a")in(?)`, orSeparator: true, args: []interface{}{1, 2, 3}}},
},
expect: ` WHERE ("a") IN (($1,$2,$3))`,
args: []interface{}{1, 2, 3},
},
{
q: Query{
in: []in{{clause: `("a")in?`, args: []interface{}{1}}},
},
expect: ` WHERE ("a") IN ($1)`,
args: []interface{}{1},
},
{
q: Query{
where: []where{
{clause: "a=?", args: []interface{}{1}},
},
in: []in{
{clause: `?,?,"name" in ?`, orSeparator: true, args: []interface{}{"c", "d", 3, 4, 5, 6, 7, 8}},
{clause: `?,?,"name" in ?`, orSeparator: true, args: []interface{}{"e", "f", 9, 10, 11, 12, 13, 14}},
},
},
expect: ` OR $1,$2,"name" IN (($3,$4,$5),($6,$7,$8)) OR $9,$10,"name" IN (($11,$12,$13),($14,$15,$16))`,
args: []interface{}{"c", "d", 3, 4, 5, 6, 7, 8, "e", "f", 9, 10, 11, 12, 13, 14},
},
{
q: Query{
in: []in{
{clause: `("a")in`, args: []interface{}{1}},
{clause: `("a")in?`, orSeparator: true, args: []interface{}{1}},
},
},
expect: ` WHERE ("a")in OR ("a") IN ($1)`,
args: []interface{}{1, 1},
},
{
q: Query{
in: []in{
{clause: `\?,\? in \?`, args: []interface{}{1}},
{clause: `\?,\?in \?`, orSeparator: true, args: []interface{}{1}},
},
},
expect: ` WHERE ?,? IN ? OR ?,? IN ?`,
args: []interface{}{1, 1},
},
{
q: Query{
in: []in{
{clause: `("a")in`, args: []interface{}{1}},
{clause: `("a") in thing`, args: []interface{}{1, 2, 3}},
{clause: `("a")in?`, orSeparator: true, args: []interface{}{4, 5, 6}},
},
},
expect: ` WHERE ("a")in AND ("a") IN thing OR ("a") IN ($1,$2,$3)`,
args: []interface{}{1, 1, 2, 3, 4, 5, 6},
},
{
q: Query{
in: []in{
{clause: `("a")in?`, orSeparator: true, args: []interface{}{4, 5, 6}},
{clause: `("a") in thing`, args: []interface{}{1, 2, 3}},
{clause: `("a")in`, args: []interface{}{1}},
},
},
expect: ` WHERE ("a") IN ($1,$2,$3) AND ("a") IN thing AND ("a")in`,
args: []interface{}{4, 5, 6, 1, 2, 3, 1},
},
{
q: Query{
in: []in{
{clause: `("a")in?`, orSeparator: true, args: []interface{}{4, 5, 6}},
{clause: `("a")in`, args: []interface{}{1}},
{clause: `("a") in thing`, args: []interface{}{1, 2, 3}},
},
},
expect: ` WHERE ("a") IN ($1,$2,$3) AND ("a")in AND ("a") IN thing`,
args: []interface{}{4, 5, 6, 1, 1, 2, 3},
},
// // Where("a=?")
// {
// q: Query{
// where: []where{where{clause: "a=?"}},
// },
// expect: " WHERE (a=$1)",
// },
// // Where("(a=?)")
// {
// q: Query{
// where: []where{where{clause: "(a=?)"}},
// },
// expect: " WHERE ((a=$1))",
// },
// // Where("((a=? OR b=?))")
// {
// q: Query{
// where: []where{where{clause: "((a=? OR b=?))"}},
// },
// expect: " WHERE (((a=$1 OR b=$2)))",
// },
// // Where("(a=?)", Or("(b=?)")
// {
// q: Query{
// where: []where{
// where{clause: "(a=?)", orSeparator: true},
// where{clause: "(b=?)"},
// },
// },
// expect: " WHERE ((a=$1)) OR ((b=$2))",
// },
// // Where("a=? OR b=?")
// {
// q: Query{
// where: []where{where{clause: "a=? OR b=?"}},
// },
// expect: " WHERE (a=$1 OR b=$2)",
// },
// // Where("a=?"), Where("b=?")
// {
// q: Query{
// where: []where{where{clause: "a=?"}, where{clause: "b=?"}},
// },
// expect: " WHERE (a=$1) AND (b=$2)",
// },
// // Where("(a=? AND b=?) OR c=?")
// {
// q: Query{
// where: []where{where{clause: "(a=? AND b=?) OR c=?"}},
// },
// expect: " WHERE ((a=$1 AND b=$2) OR c=$3)",
// },
// // Where("a=? OR b=?"), Where("c=? OR d=? OR e=?")
// {
// q: Query{
// where: []where{
// where{clause: "(a=? OR b=?)"},
// where{clause: "(c=? OR d=? OR e=?)"},
// },
// },
// expect: " WHERE ((a=$1 OR b=$2)) AND ((c=$3 OR d=$4 OR e=$5))",
// },
// // Where("(a=? AND b=?) OR (c=? AND d=? AND e=?) OR f=? OR f=?")
// {
// q: Query{
// where: []where{
// where{clause: "(a=? AND b=?) OR (c=? AND d=? AND e=?) OR f=? OR g=?"},
// },
// },
// expect: " WHERE ((a=$1 AND b=$2) OR (c=$3 AND d=$4 AND e=$5) OR f=$6 OR g=$7)",
// },
// // Where("(a=? AND b=?) OR (c=? AND d=? OR e=?) OR f=? OR g=?")
// {
// q: Query{
// where: []where{
// where{clause: "(a=? AND b=?) OR (c=? AND d=? OR e=?) OR f=? OR g=?"},
// },
// },
// expect: " WHERE ((a=$1 AND b=$2) OR (c=$3 AND d=$4 OR e=$5) OR f=$6 OR g=$7)",
// },
// // Where("a=? or b=?"), Or("c=? and d=?"), Or("e=? or f=?")
// {
// q: Query{
// where: []where{
// where{clause: "a=? or b=?", orSeparator: true},
// where{clause: "c=? and d=?", orSeparator: true},
// where{clause: "e=? or f=?", orSeparator: true},
// },
// },
// expect: " WHERE (a=$1 or b=$2) OR (c=$3 and d=$4) OR (e=$5 or f=$6)",
// },
// // Where("a=? or b=?"), Or("c=? and d=?"), Or("e=? or f=?")
// {
// q: Query{
// where: []where{
// where{clause: "a=? or b=?"},
// where{clause: "c=? and d=?"},
// where{clause: "e=? or f=?"},
// },
// },
// expect: " WHERE (a=$1 or b=$2) AND (c=$3 and d=$4) AND (e=$5 or f=$6)",
// },
}
for i, test := range tests {
result, _ := inClause(&test.q, 1)
result, args := inClause(&test.q, 1)
if result != test.expect {
t.Errorf("%d) Mismatch between expect and result:\n%s\n%s\n", i, test.expect, result)
}
if !reflect.DeepEqual(args, test.args) {
t.Errorf("%d) Mismatch between expected args:\n%#v\n%#v\n", i, test.args, args)
}
}
}
@ -473,35 +476,41 @@ func TestConvertQuestionMarks(t *testing.T) {
clause string
start int
expect string
count int
}{
{clause: "hello friend", start: 1, expect: "hello friend"},
{clause: "thing=?", start: 2, expect: "thing=$2"},
{clause: "thing=? and stuff=? and happy=?", start: 2, expect: "thing=$2 and stuff=$3 and happy=$4"},
{clause: `thing \? stuff`, start: 2, expect: `thing ? stuff`},
{clause: `thing \? stuff and happy \? fun`, start: 2, expect: `thing ? stuff and happy ? fun`},
{clause: "hello friend", start: 1, expect: "hello friend", count: 0},
{clause: "thing=?", start: 2, expect: "thing=$2", count: 1},
{clause: "thing=? and stuff=? and happy=?", start: 2, expect: "thing=$2 and stuff=$3 and happy=$4", count: 3},
{clause: `thing \? stuff`, start: 2, expect: `thing ? stuff`, count: 0},
{clause: `thing \? stuff and happy \? fun`, start: 2, expect: `thing ? stuff and happy ? fun`, count: 0},
{
clause: `thing \? stuff ? happy \? and mad ? fun \? \? \?`,
start: 2,
expect: `thing ? stuff $2 happy ? and mad $3 fun ? ? ?`,
count: 2,
},
{
clause: `thing ? stuff ? happy \? fun \? ? ?`,
start: 1,
expect: `thing $1 stuff $2 happy ? fun ? $3 $4`,
count: 4,
},
{clause: `?`, start: 1, expect: `$1`},
{clause: `???`, start: 1, expect: `$1$2$3`},
{clause: `?`, start: 1, expect: `$1`, count: 1},
{clause: `???`, start: 1, expect: `$1$2$3`, count: 3},
{clause: `\?`, start: 1, expect: `?`},
{clause: `\?\?\?`, start: 1, expect: `???`},
{clause: `\??\??\??`, start: 1, expect: `?$1?$2?$3`},
{clause: `?\??\??\?`, start: 1, expect: `$1?$2?$3?`},
{clause: `\??\??\??`, start: 1, expect: `?$1?$2?$3`, count: 3},
{clause: `?\??\??\?`, start: 1, expect: `$1?$2?$3?`, count: 3},
}
for i, test := range tests {
res := convertQuestionMarks(test.clause, test.start)
res, count := convertQuestionMarks(test.clause, test.start)
if res != test.expect {
t.Errorf("%d) Mismatch between expect and result:\n%s\n%s\n", i, test.expect, res)
}
if count != test.count {
t.Errorf("%d) Expected count %d, got %d", i, test.count, count)
}
}
}
@ -511,36 +520,34 @@ func TestConvertInQuestionMarks(t *testing.T) {
tests := []struct {
clause string
start int
group int
total int
expect string
}{
{clause: "hello friend", start: 1, expect: "hello friend"},
{clause: "thing=?", start: 2, expect: "thing=$2"},
{clause: "thing=? and stuff=? and happy=?", start: 2, expect: "thing=$2 and stuff=$3 and happy=$4"},
{clause: `thing \? stuff`, start: 2, expect: `thing ? stuff`},
{clause: `thing \? stuff and happy \? fun`, start: 2, expect: `thing ? stuff and happy ? fun`},
{
clause: `thing \? stuff ? happy \? and mad ? fun \? \? \?`,
start: 2,
expect: `thing ? stuff $2 happy ? and mad $3 fun ? ? ?`,
},
{
clause: `thing ? stuff ? happy \? fun \? ? ?`,
start: 1,
expect: `thing $1 stuff $2 happy ? fun ? $3 $4`,
},
{clause: `?`, start: 1, expect: `$1`},
{clause: `???`, start: 1, expect: `$1$2$3`},
{clause: `\?`, start: 1, expect: `?`},
{clause: `\?\?\?`, start: 1, expect: `???`},
{clause: `\??\??\??`, start: 1, expect: `?$1?$2?$3`},
{clause: `?\??\??\?`, start: 1, expect: `$1?$2?$3?`},
{clause: "?", expect: "(($1,$2,$3),($4,$5,$6),($7,$8,$9))", start: 1, total: 9, group: 3},
{clause: "?", expect: "(($2,$3),($4))", start: 2, total: 3, group: 2},
{clause: "hello friend", start: 1, expect: "hello friend", total: 0, group: 1},
{clause: "thing ? thing", start: 2, expect: "thing ($2,$3) thing", total: 2, group: 1},
{clause: "thing?thing", start: 2, expect: "thing($2)thing", total: 1, group: 1},
{clause: `thing \? stuff`, start: 2, expect: `thing ? stuff`, total: 0, group: 1},
{clause: `thing \? stuff and happy \? fun`, start: 2, expect: `thing ? stuff and happy ? fun`, total: 0, group: 1},
{clause: "thing ? thing ? thing", start: 1, expect: "thing ($1,$2,$3) thing ? thing", total: 3, group: 1},
{clause: `?`, start: 1, expect: `($1)`, total: 1, group: 1},
{clause: `???`, start: 1, expect: `($1,$2,$3)??`, total: 3, group: 1},
{clause: `\?`, start: 1, expect: `?`, total: 0, group: 1},
{clause: `\?\?\?`, start: 1, expect: `???`, total: 0, group: 1},
{clause: `\??\??\??`, start: 1, expect: `?($1,$2,$3)????`, total: 3, group: 1},
{clause: `?\??\??\?`, start: 1, expect: `($1,$2,$3)?????`, total: 3, group: 1},
}
for i, test := range tests {
res := convertQuestionMarks(test.clause, test.start)
res, count := convertInQuestionMarks(test.clause, test.start, test.group, test.total)
if res != test.expect {
t.Errorf("%d) Mismatch between expect and result:\n%s\n%s\n", i, test.expect, res)
}
if count != test.total {
t.Errorf("%d) Expected %d, got %d", i, test.total, count)
}
}
}

View file

@ -237,7 +237,7 @@ func PrefixStringSlice(str string, strs []string) []string {
}
// Placeholders generates the SQL statement placeholders for in queries.
// For example, ($1, $2, $3), ($4, $5, $6) etc.
// For example, ($1,$2,$3),($4,$5,$6) etc.
// It will start counting placeholders at "start".
func Placeholders(count int, start int, group int) string {
buf := GetBuffer()