diff --git a/boil/_fixtures/11.sql b/boil/_fixtures/11.sql index 8f6f4b3..818874c 100644 --- a/boil/_fixtures/11.sql +++ b/boil/_fixtures/11.sql @@ -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; \ No newline at end of file +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; \ No newline at end of file diff --git a/boil/qm/query_mods.go b/boil/qm/query_mods.go index e7fc934..f085046 100644 --- a/boil/qm/query_mods.go +++ b/boil/qm/query_mods.go @@ -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) } } diff --git a/boil/query_builders.go b/boil/query_builders.go index 7173e2a..3896108 100644 --- a/boil/query_builders.go +++ b/boil/query_builders.go @@ -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 $ // where 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 diff --git a/boil/query_builders_test.go b/boil/query_builders_test.go index 3b31095..e2c372a 100644 --- a/boil/query_builders_test.go +++ b/boil/query_builders_test.go @@ -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) + } } } diff --git a/strmangle/strmangle.go b/strmangle/strmangle.go index 56206ef..703c304 100644 --- a/strmangle/strmangle.go +++ b/strmangle/strmangle.go @@ -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()