Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v5
with:
go-version: "1.24.x"
go-version: "1.26.x"

- name: Check out code
uses: actions/checkout@v4
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
postgres-test:
strategy:
matrix:
go: [1.24.x, 1.23.x] # when updating versions, update it below too.
go: [1.26.x, 1.25.x] # when updating versions, update it below too.
runs-on: ubuntu-latest
services:
postgres:
Expand Down Expand Up @@ -42,7 +42,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24.x'
go-version: '1.26.x'
- name: Run unit tests
run: |
go test -v -race -count 1 -covermode atomic -coverprofile=profile.cov ./...
Expand All @@ -51,7 +51,7 @@ jobs:
working-directory: integration
run: go test -v
- name: Code coverage
if: ${{ github.event_name != 'pull_request' && matrix.go == '1.24.x' }}
if: ${{ github.event_name != 'pull_request' && matrix.go == '1.26.x' }}
uses: shogo82148/actions-goveralls@v1
with:
path-to-profile: profile.cov
1 change: 0 additions & 1 deletion delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@ func (b DeleteBuilder) Returning(columns ...string) DeleteBuilder {

// ReturningSelect adds a RETURNING expressions to the query similar to Using, but takes a Select statement.
func (b DeleteBuilder) ReturningSelect(from SelectBuilder, alias string) DeleteBuilder {
from.placeholder = questionPlaceholder
b.returning = append(b.returning, Alias{Expr: from, As: alias})
return b
}
Expand Down
8 changes: 8 additions & 0 deletions delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ func TestDeleteBuilderSQL(t *testing.T) {
wantSQL: "DELETE FROM films USING (SELECT id FROM producers WHERE name = $1) AS p",
wantArgs: []any{"foo"},
},
{
name: "delete_using_select_params",
b: Delete("films").
UsingSelect(Select("id").From("producers").Where("name = ?", "foo"), "p").
Where("status = ?", "active"),
wantSQL: "DELETE FROM films USING (SELECT id FROM producers WHERE name = $1) AS p WHERE status = $2",
wantArgs: []any{"foo", "active"},
},
{
name: "delete_with_cte",
b: Delete("orders").
Expand Down
43 changes: 38 additions & 5 deletions expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (e expr) SQL() (sql string, args []any, err error) {

if as, ok := ap[0].(SQLizer); ok {
// sqlizer argument; expand it and append the result
isql, iargs, err = as.SQL()
isql, iargs, err = nestedSQL(as)
buf.WriteString(sp[:i])
buf.WriteString(isql)
args = append(args, iargs...)
Expand Down Expand Up @@ -95,7 +95,7 @@ func ConcatSQL(ce ...any) (sql string, args []any, err error) {
case string:
sql += p
case SQLizer:
pSQL, pArgs, err := p.SQL()
pSQL, pArgs, err := nestedSQL(p)
if err != nil {
return "", nil, err
}
Expand All @@ -120,7 +120,7 @@ type Alias struct {

// AliasExprSQL returns a SQL query based on the alias.
func (a Alias) SQL() (sql string, args []any, err error) {
sql, args, err = a.Expr.SQL()
sql, args, err = nestedSQL(a.Expr)
if err == nil {
sql = fmt.Sprintf("(%s) AS %s", sql, a.As)
}
Expand Down Expand Up @@ -162,6 +162,17 @@ func (eq Eq) toSQL(useNotOpr bool) (sql string, args []any, err error) {
if val, err = v.Value(); err != nil {
return
}
case SQLizer:
var vsql string
var vargs []any
vsql, vargs, err = nestedSQL(v)
if err != nil {
return
}
expr = fmt.Sprintf("%s %s (%s)", key, equalOpr, vsql)
args = append(args, vargs...)
exprs = append(exprs, expr)
continue
}

r := reflect.ValueOf(val)
Expand Down Expand Up @@ -215,7 +226,7 @@ func (neq NotEq) SQL() (sql string, args []any, err error) {
// Like is syntactic sugar for use with LIKE conditions.
// Ex:
//
// .Where(Like{"name": "%irrel"})
// .Where(Like{"name": "%elephant"})
type Like map[string]any

func (lk Like) toSQL(opr string) (sql string, args []any, err error) {
Expand All @@ -228,6 +239,17 @@ func (lk Like) toSQL(opr string) (sql string, args []any, err error) {
if val, err = v.Value(); err != nil {
return
}
case SQLizer:
var vsql string
var vargs []any
vsql, vargs, err = nestedSQL(v)
if err != nil {
return
}
expr = fmt.Sprintf("%s %s (%s)", key, opr, vsql)
args = append(args, vargs...)
exprs = append(exprs, expr)
continue
}

if val == nil {
Expand Down Expand Up @@ -255,7 +277,7 @@ func (lk Like) SQL() (sql string, args []any, err error) {
// NotLike is syntactic sugar for use with LIKE conditions.
// Ex:
//
// .Where(NotLike{"name": "%irrel"})
// .Where(NotLike{"name": "%elephant"})
type NotLike Like

func (nlk NotLike) SQL() (sql string, args []any, err error) {
Expand Down Expand Up @@ -312,6 +334,17 @@ func (lt Lt) toSQL(opposite, orEq bool) (sql string, args []any, err error) {
if val, err = v.Value(); err != nil {
return
}
case SQLizer:
var vsql string
var vargs []any
vsql, vargs, err = nestedSQL(v)
if err != nil {
return
}
expr = fmt.Sprintf("%s %s (%s)", key, opr, vsql)
args = append(args, vargs...)
exprs = append(exprs, expr)
continue
}

if val == nil {
Expand Down
75 changes: 75 additions & 0 deletions expr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,44 @@ func TestNotEqSQL(t *testing.T) {
}
}

func TestEqSubquerySQL(t *testing.T) {
t.Parallel()
b := Eq{"id": Select("id").From("other").Where("x = ?", 1)}
sql, args, err := b.SQL()
if err != nil {
t.Errorf("unexpected error: %v", err)
}

want := "id = (SELECT id FROM other WHERE x = ?)"
if want != sql {
t.Errorf("expected SQL to be %q, got %q instead", want, sql)
}

expectedArgs := []any{1}
if !reflect.DeepEqual(expectedArgs, args) {
t.Errorf("wanted %v, got %v instead", expectedArgs, args)
}
}

func TestNotEqSubquerySQL(t *testing.T) {
t.Parallel()
b := NotEq{"id": Select("id").From("other").Where("x = ?", 1)}
sql, args, err := b.SQL()
if err != nil {
t.Errorf("unexpected error: %v", err)
}

want := "id <> (SELECT id FROM other WHERE x = ?)"
if want != sql {
t.Errorf("expected SQL to be %q, got %q instead", want, sql)
}

expectedArgs := []any{1}
if !reflect.DeepEqual(expectedArgs, args) {
t.Errorf("wanted %v, got %v instead", expectedArgs, args)
}
}

func TestEqNotInSQL(t *testing.T) {
t.Parallel()
b := NotEq{"id": []int{1, 2, 3}}
Expand Down Expand Up @@ -203,6 +241,43 @@ func TestLtSQL(t *testing.T) {
}
}

func TestLtSubquerySQL(t *testing.T) {
t.Parallel()
b := Lt{"score": Select("avg(score)").From("results").Where("active = ?", true)}
sql, args, err := b.SQL()
if err != nil {
t.Errorf("unexpected error: %v", err)
}

want := "score < (SELECT avg(score) FROM results WHERE active = ?)"
if want != sql {
t.Errorf("expected SQL to be %q, got %q instead", want, sql)
}

expectedArgs := []any{true}
if !reflect.DeepEqual(expectedArgs, args) {
t.Errorf("wanted %v, got %v instead", expectedArgs, args)
}
}

func TestGtSubquerySQL(t *testing.T) {
t.Parallel()
b := Gt{"score": Select("avg(score)").From("results")}
sql, args, err := b.SQL()
if err != nil {
t.Errorf("unexpected error: %v", err)
}

want := "score > (SELECT avg(score) FROM results)"
if want != sql {
t.Errorf("expected SQL to be %q, got %q instead", want, sql)
}

if len(args) != 0 {
t.Errorf("wanted 0 arguments, got %d instead", len(args))
}
}

func TestLtOrEqSQL(t *testing.T) {
t.Parallel()
b := LtOrEq{"id": 1}
Expand Down
8 changes: 5 additions & 3 deletions insert.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,13 @@ func (b InsertBuilder) appendValuesToSQL(w io.Writer, args []any) ([]any, error)
valueStrings := make([]string, len(row))
for v, val := range row {
if vs, ok := val.(SQLizer); ok {
vsql, vargs, err := vs.SQL()
vsql, vargs, err := nestedSQL(vs)
if err != nil {
return nil, err
}
if _, ok := vs.(rawSQLizer); ok {
vsql = fmt.Sprintf("(%s)", vsql)
}
valueStrings[v] = vsql
args = append(args, vargs...)
} else {
Expand All @@ -147,7 +150,7 @@ func (b InsertBuilder) appendSelectToSQL(w io.Writer, args []any) ([]any, error)
return args, errors.New("select clause for insert statements are not set")
}

selectClause, sArgs, err := b.selectBuilder.SQL()
selectClause, sArgs, err := b.selectBuilder.unfinalizedSQL()
if err != nil {
return args, err
}
Expand Down Expand Up @@ -221,7 +224,6 @@ func (b InsertBuilder) Returning(columns ...string) InsertBuilder {

// ReturningSelect adds a RETURNING expressions to the query similar to Using, but takes a Select statement.
func (b InsertBuilder) ReturningSelect(from SelectBuilder, alias string) InsertBuilder {
from.placeholder = questionPlaceholder
b.returning = append(b.returning, Alias{Expr: from, As: alias})
return b
}
Expand Down
59 changes: 41 additions & 18 deletions insert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,45 @@ func TestInsertBuilderSQL(t *testing.T) {
"SELECT s.id, s.data FROM source s JOIN tree ON tree.id = s.parent_id) " +
"INSERT INTO archive (id,data) SELECT id, data FROM tree",
},
{
name: "insert_select_params",
b: Insert("films").
Columns("id", "title").
Select(Select("id", "title").From("producers").Where("name = ?", "foo")),
wantSQL: "INSERT INTO films (id,title) SELECT id, title FROM producers WHERE name = $1",
wantArgs: []any{"foo"},
},
{
name: "insert_values_select_params",
b: Insert("films").
Columns("id", "title").
Values(1, Select("title").From("other").Where("id = ?", 2)).
Suffix("RETURNING id, ?", 3),
wantSQL: "INSERT INTO films (id,title) VALUES ($1,(SELECT title FROM other WHERE id = $2)) RETURNING id, $3",
wantArgs: []any{1, 2, 3},
},
{
name: "insert_values_union_params",
b: Insert("films").
Columns("id", "title").
Values(1, Union(
Select("title").From("other").Where("id = ?", 2),
Select("title").From("another").Where("id = ?", 3),
)),
wantSQL: "INSERT INTO films (id,title) VALUES ($1,(SELECT title FROM other WHERE id = $2 UNION SELECT title FROM another WHERE id = $3))",
wantArgs: []any{1, 2, 3},
},
{
name: "insert_values_union",
b: Insert("t").
Columns("id").
Values(Union(
Select("id").From("a").Where("x = ?", 1),
Select("id").From("b").Where("x = ?", 2),
)),
wantSQL: "INSERT INTO t (id) VALUES ((SELECT id FROM a WHERE x = $1 UNION SELECT id FROM b WHERE x = $2))",
wantArgs: []any{1, 2},
},
}
for _, tc := range testCases {
tc := tc
Expand Down Expand Up @@ -242,27 +281,11 @@ func TestInsertBuilderSelect(t *testing.T) {
}
}

func TestInsertBuilderReplace(t *testing.T) {
t.Parallel()
b := Replace("table").Values(1)

want := "REPLACE INTO table VALUES ($1)"

sql, _, err := b.SQL()
if err != nil {
t.Errorf("unexpected error: %v", err)
}

if want != sql {
t.Errorf("expected SQL to be %q, got %q instead", want, sql)
}
}

func TestInsertBuilderVerb(t *testing.T) {
t.Parallel()
b := Insert("table").Verb("REPLACE").Values(1)
b := Insert("table").Verb("UPSERT").Values(1)

want := "REPLACE INTO table VALUES ($1)"
want := "UPSERT INTO table VALUES ($1)"

sql, _, err := b.SQL()
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pgq.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type rawSQLizer interface {
// not try very hard to ensure it. Additionally, executing the output of this
// function with any untrusted user input is certainly insecure.
func Debug(s SQLizer) string {
sql, args, err := s.SQL()
sql, args, err := nestedSQL(s)
if err != nil {
return fmt.Sprintf("[SQL error: %s]", err)
}
Expand Down
9 changes: 9 additions & 0 deletions pgq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ func TestDebug(t *testing.T) {
}
}

func TestDebugSelect(t *testing.T) {
t.Parallel()
sqlizer := Select("id", "name").From("users").Where("id = ?", 42).Where("active = ?", true)
want := "SELECT id, name FROM users WHERE id = '42' AND active = 'true'"
if got := Debug(sqlizer); got != want {
t.Errorf("expected %q, got %q instead", want, got)
}
}

func TestDebugSQLizerErrors(t *testing.T) {
t.Parallel()
var errorMessages = []struct {
Expand Down
Loading
Loading