diff --git a/page.go b/page.go index 39af090..3fbb287 100644 --- a/page.go +++ b/page.go @@ -3,7 +3,6 @@ package pgkit import ( "cmp" "context" - "fmt" "regexp" "slices" "strings" @@ -92,7 +91,7 @@ func (s Sort) String() string { if s.Column == "" { return "" } - return fmt.Sprintf("%s %s", s.Column, s.Order) + return s.Column + " " + string(s.Order) } var _MatcherOrderBy = regexp.MustCompile(`-?([a-zA-Z0-9]+)`) @@ -266,7 +265,7 @@ func (p Paginator[T]) getOrder(page *Page) []string { sort := page.GetOrder(p.settings.ColumnFunc, p.settings.Sort...) list := make([]string, len(sort)) for i := range sort { - list[i] = fmt.Sprintf("%s %s", sort[i].Column, sort[i].Order) + list[i] = sort[i].Column + " " + string(sort[i].Order) } return list } diff --git a/querier.go b/querier.go index 548ced7..382779b 100644 --- a/querier.go +++ b/querier.go @@ -1,9 +1,9 @@ package pgkit import ( - "bytes" "context" "fmt" + "strconv" "strings" sq "github.com/Masterminds/squirrel" @@ -260,14 +260,15 @@ func (r RawSQL) Prepare(query string) (string, int, error) { parts := strings.Split(query, "?") - q := bytes.Buffer{} + q := strings.Builder{} for i, p := range parts { if p == "" { continue } q.WriteString(p) if i < n { - q.WriteString(fmt.Sprintf("$%d", i+1)) + q.WriteByte('$') + q.WriteString(strconv.Itoa(i + 1)) } } diff --git a/stringconcat_bench_test.go b/stringconcat_bench_test.go new file mode 100644 index 0000000..ed9652c --- /dev/null +++ b/stringconcat_bench_test.go @@ -0,0 +1,47 @@ +package pgkit_test + +import ( + "testing" + + "github.com/goware/pgkit/v2" + "github.com/stretchr/testify/require" +) + +// TestStringConcatOutputUnchanged pins the exact strings produced by the hot +// query-building paths, so the fmt.Sprintf -> concatenation change is proven +// to be output-preserving (must pass before and after the fix). +func TestStringConcatOutputUnchanged(t *testing.T) { + // Columns are quoted by pgx.Identifier.Sanitize(). + require.Equal(t, `"created_at" DESC`, pgkit.Sort{Column: "created_at", Order: pgkit.Desc}.String()) + require.Equal(t, `"name" ASC`, pgkit.Sort{Column: "name", Order: pgkit.Asc}.String()) + // Unset order defaults to ASC via sanitize. + require.Equal(t, `"id" ASC`, pgkit.Sort{Column: "id"}.String()) + + stmt := pgkit.RawQuery("SELECT * FROM t WHERE a = ? AND b = ? AND c = ?") + require.NoError(t, stmt.Err()) + require.Equal(t, "SELECT * FROM t WHERE a = $1 AND b = $2 AND c = $3", stmt.GetQuery()) + require.Equal(t, 3, stmt.NumArgs()) +} + +// BenchmarkSortString measures Sort.String(), called once per ORDER BY column +// on every paginated and keyset query build. +func BenchmarkSortString(b *testing.B) { + s := pgkit.Sort{Column: "created_at", Order: pgkit.Desc} + b.ReportAllocs() + for b.Loop() { + _ = s.String() + } +} + +// BenchmarkRawQueryPrepare measures RawQuery, which runs the placeholder +// rewrite (?-> $N) in RawSQL.Prepare once per call. +func BenchmarkRawQueryPrepare(b *testing.B) { + const q = "SELECT * FROM t WHERE a = ? AND b = ? AND c = ? AND d = ? AND e = ?" + b.ReportAllocs() + for b.Loop() { + stmt := pgkit.RawQuery(q) + if stmt.Err() != nil { + b.Fatal(stmt.Err()) + } + } +}