Skip to content

Commit

Permalink
Make Builder more strict (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
cristaloleg authored Nov 22, 2022
1 parent 4723412 commit 45ae46f
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 4 deletions.
21 changes: 21 additions & 0 deletions GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@ Examples in [example_test.go](example_test.go) explicitly show that query argume

The `%s` verb should be used with an extra care, no user input should be passed through it.

## Compile-time queries

To enforce compile-time queries `builq.Builder` accepts only constant strings:

```go
var sb builq.Builder
sb.Addf("SELECT %s FROM %s", cols, "table")
sb.Addf("WHERE id = %$", 123)

// this WILL NOT complile, orClause isn't const
// var orClause = "OR id = %$"
// sb.Addf(orClause, 42)

// WILL compile, orClause2 is known at compile-time
const orClause2 = "OR id = %$"
sb.Addf(orClause2, 42)
```

The reason behing this API is to improve security and to prevent bad runtime queries.
Also, some projects require constant queries due to security policies (precise definition might be different but you get the idea).

## String placeholder

To write just a string there is the `%s` formatting verb. Works the same as in the `fmt` package.
Expand Down
7 changes: 5 additions & 2 deletions builq.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ type Builder struct {
placeholder rune // a placeholder used to build the query.
}

type constString string

// Addf formats according to a format specifier, writes to query and appends args.
func (b *Builder) Addf(format string, args ...any) *Builder {
// Format param must be a constant string.
func (b *Builder) Addf(format constString, args ...any) *Builder {
if b.err != nil {
return b
}
Expand All @@ -46,7 +49,7 @@ func (b *Builder) Addf(format string, args ...any) *Builder {
wargs[i] = &argument{value: args[i], builder: b}
}

_, err := fmt.Fprintf(&b.query, format, wargs...)
_, err := fmt.Fprintf(&b.query, string(format), wargs...)
if b.err == nil && err != nil {
b.err = err
}
Expand Down
4 changes: 2 additions & 2 deletions builq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestBuilder(t *testing.T) {
t.Run(name, func(t *testing.T) {
t.Helper()
var b Builder
b.Addf(format, args...)
b.Addf(constString(format), args...)
_, _, err := b.Build()
if !errors.Is(err, wantErr) {
t.Errorf("\nhave: %v\nwant: %v", err, wantErr)
Expand Down Expand Up @@ -61,7 +61,7 @@ func FuzzBuilder(f *testing.F) {
}

var b Builder
b.Addf(format, arg1, arg2)
b.Addf(constString(format), arg1, arg2)
_, args, err := b.Build()

if err != nil {
Expand Down
30 changes: 30 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,36 @@ import (
"github.com/cristalhq/builq"
)

func ExampleBuilder() {
cols := builq.Columns{"foo", "bar"}

var sb builq.Builder
sb.Addf("SELECT %s FROM %s", cols, "table")
sb.Addf("WHERE id = %$", 123)

// this WILL NOT complile
// var orClause = "OR id = %$"
// sb.Addf(orClause, 42)

// WILL compile
const orClause2 = "OR id = %$"
sb.Addf(orClause2, 42)

query, args, err := sb.Build()
panicIf(err)

fmt.Printf("query:\n%v", query)
fmt.Printf("args:\n%v", args)

// Output:
// query:
// SELECT foo, bar FROM table
// WHERE id = $1
// OR id = $2
// args:
// [123 42]
}

func ExampleQuery1() {
cols := builq.Columns{"foo, bar"}

Expand Down

0 comments on commit 45ae46f

Please sign in to comment.