@@ -12,101 +12,106 @@ import (
1212// The zero value is ready to use.
1313// Do not copy a non-zero Builder.
1414type Builder struct {
15+ // TODO: prealloc?
1516 query strings.Builder
1617 args []any
1718 counter int
1819 placeholder rune
1920}
2021
2122// Appendf formats according to the given format and appends the result to the query.
22- // It works like [fmt.Appendf], i.e. all rules from the [fmt] package are applied.
23- // In addition, Appendf supports %?, %$, and %@ verbs, which are automatically expanded to the query placeholders ?, $N, and @pN,
24- // where N is the auto-incrementing counter.
25- // The corresponding arguments can then be accessed with the [Builder.Args] method.
23+ // It works like [fmt.Appendf], meaning all the rules from the [fmt] package are applied.
24+ // In addition, Appendf supports special verbs that automatically expand to database placeholders.
2625//
27- // IMPORTANT: to avoid SQL injections, make sure to pass arguments from user input with placeholder verbs.
26+ // ---------------------------------------------
27+ // | Database | Verb | Placeholder |
28+ // |----------------------|------|-------------|
29+ // | MySQL, SQLite | %? | ? |
30+ // | PostgreSQL | %$ | $N |
31+ // | Microsoft SQL Server | %@ | @pN |
32+ // ---------------------------------------------
2833//
29- // Placeholder verbs map to the following database placeholders:
30- // - MySQL, SQLite: %? -> ?
31- // - PostgreSQL: %$ -> $N
32- // - MSSQL: %@ -> @pN
34+ // Here, N is an auto-incrementing counter.
35+ // For example, "%$, %$, %$" expands to "$1, $2, $3".
3336//
34- // TODO: document slice arguments usage.
35- func (b * Builder ) Appendf (format string , args ... any ) {
36- a := make ([]any , len (args ))
37- for i , arg := range args {
38- a [i ] = argument {value : arg , builder : b }
37+ // If a special verb includes the "+" flag, it automatically expands to multiple placeholders.
38+ // For example, given the verb "%+?" and the argument []int{1, 2, 3},
39+ // Appendf writes "?, ?, ?" to the query and appends 1, 2, and 3 to the arguments.
40+ // You may want to use this flag to build "WHERE IN (...)" clauses.
41+ //
42+ // Make sure to always pass arguments from user input with placeholder verbs to avoid SQL injections.
43+ func (b * Builder ) Appendf (format string , a ... any ) {
44+ fs := make ([]any , len (a ))
45+ for i := range a {
46+ fs [i ] = formatter {arg : a [i ], builder : b }
3947 }
40- fmt .Fprintf (& b .query , format , a ... )
48+ fmt .Fprintf (& b .query , format , fs ... )
4149}
4250
43- // Query returns the query string.
44- func (b * Builder ) Query () string { return b .query .String () }
51+ // Build returns the query and its arguments.
52+ func (b * Builder ) Build () (query string , args []any ) {
53+ return b .query .String (), b .args
54+ }
4555
46- // Args returns the query arguments.
47- func (b * Builder ) Args () []any { return b .args }
56+ // Build is a shorthand for a new [Builder] + [Builder.Appendf] + [Builder.Build].
57+ func Build (format string , a ... any ) (query string , args []any ) {
58+ var b Builder
59+ b .Appendf (format , a ... )
60+ return b .Build ()
61+ }
4862
49- type argument struct {
50- value any
63+ type formatter struct {
64+ arg any
5165 builder * Builder
5266}
5367
5468// Format implements [fmt.Formatter].
55- func (a argument ) Format (s fmt.State , verb rune ) {
69+ func (f formatter ) Format (s fmt.State , verb rune ) {
5670 switch verb {
5771 case '?' , '$' , '@' :
58- if a .builder .placeholder == 0 {
59- a .builder .placeholder = verb
72+ if f .builder .placeholder == 0 {
73+ f .builder .placeholder = verb
6074 }
61- if a .builder .placeholder != verb {
75+ if f .builder .placeholder != verb {
6276 panic ("unexpected placeholder" )
6377 }
78+ if s .Flag ('+' ) {
79+ appendAll (s , f .builder , verb , f .arg )
80+ } else {
81+ appendOne (s , f .builder , verb , f .arg )
82+ }
6483 default :
6584 format := fmt .FormatString (s , verb )
66- fmt .Fprintf (s , format , a .value )
67- return
68- }
69-
70- if s .Flag ('+' ) {
71- a .writeSlice (s , verb )
72- } else {
73- a .writePlaceholder (s , verb )
74- a .builder .args = append (a .builder .args , a .value )
85+ fmt .Fprintf (s , format , f .arg )
7586 }
7687}
7788
78- func ( a argument ) writePlaceholder ( w io.Writer , verb rune ) {
89+ func appendOne ( w io.Writer , b * Builder , verb rune , arg any ) {
7990 switch verb {
80- case '?' : // MySQL, SQLite
91+ case '?' :
8192 fmt .Fprint (w , "?" )
82- case '$' : // PostgreSQL
83- a . builder .counter ++
84- fmt .Fprintf (w , "$%d" , a . builder .counter )
85- case '@' : // MSSQL
86- a . builder .counter ++
87- fmt .Fprintf (w , "@p%d" , a . builder .counter )
93+ case '$' :
94+ b .counter ++
95+ fmt .Fprintf (w , "$%d" , b .counter )
96+ case '@' :
97+ b .counter ++
98+ fmt .Fprintf (w , "@p%d" , b .counter )
8899 }
100+ b .args = append (b .args , arg )
89101}
90102
91- func ( a argument ) writeSlice ( w io.Writer , verb rune ) {
92- slice := reflect .ValueOf (a . value )
103+ func appendAll ( w io.Writer , b * Builder , verb rune , arg any ) {
104+ slice := reflect .ValueOf (arg )
93105 if slice .Kind () != reflect .Slice {
94106 panic ("non-slice argument" )
95107 }
96-
97108 if slice .Len () == 0 {
98- // TODO: revisit.
99- // "WHERE IN (NULL)" will always result in an empty result set,
100- // which may be undesirable in some situations.
101- fmt .Fprint (w , "NULL" )
102- return
109+ panic ("zero-length slice argument" )
103110 }
104-
105111 for i := range slice .Len () {
106112 if i > 0 {
107113 fmt .Fprint (w , ", " )
108114 }
109- a .writePlaceholder (w , verb )
110- a .builder .args = append (a .builder .args , slice .Index (i ).Interface ())
115+ appendOne (w , b , verb , slice .Index (i ).Interface ())
111116 }
112117}
0 commit comments