Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for specifying tables to be locked in ForUpdate, ForNoKeyUpdate, ForKeyShare, ForShare #299

Merged
merged 1 commit into from
Oct 6, 2021
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ New features and/or enhancements are great and I encourage you to either submit
1. The use case
2. A short example

If you are issuing a PR also also include the following
If you are issuing a PR also include the following

1. Tests - otherwise the PR will not be merged
2. Documentation - otherwise the PR will not be merged
Expand All @@ -297,7 +297,7 @@ go test -v -race ./...
You can also run the tests in a container using [docker-compose](https://docs.docker.com/compose/).

```sh
GO_VERSION=latest docker-compose run goqu
MYSQL_VERSION=8 POSTGRES_VERSION=13.4 SQLSERVER_VERSION=2017-CU8-ubuntu GO_VERSION=latest docker-compose run goqu
```

## License
Expand Down
1 change: 1 addition & 0 deletions dialect/sqlite3/sqlite3.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func DialectOptions() *goqu.SQLDialectOptions {
opts.ConflictDoUpdateFragment = []byte(" DO UPDATE SET ")
opts.ConflictDoNothingFragment = []byte(" DO NOTHING ")
opts.ForUpdateFragment = []byte("")
opts.OfFragment = []byte("")
opts.NowaitFragment = []byte("")
return opts
}
Expand Down
1 change: 1 addition & 0 deletions dialect/sqlserver/sqlserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func DialectOptions() *goqu.SQLDialectOptions {
0x1a: []byte("\\x1a"),
}

opts.OfFragment = []byte("")
opts.ConflictFragment = []byte("")
opts.ConflictDoUpdateFragment = []byte("")
opts.ConflictDoNothingFragment = []byte("")
Expand Down
26 changes: 26 additions & 0 deletions docs/selecting.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* [`Window`](#window)
* [`With`](#with)
* [`SetError`](#seterror)
* [`ForUpdate`](#forupdate)
* Executing Queries
* [`ScanStructs`](#scan-structs) - Scans rows into a slice of structs
* [`ScanStruct`](#scan-struct) - Scans a row into a slice a struct, returns false if a row wasnt found
Expand Down Expand Up @@ -875,6 +876,31 @@ name is empty
name is empty
```

<a name="forupdate"></a>
**[`ForUpdate`](https://godoc.org/github.com/doug-martin/goqu/#SelectDataset.ForUpdate)**

```go
sql, _, _ := goqu.From("test").ForUpdate(exp.Wait).ToSQL()
fmt.Println(sql)
```

Output:
```sql
SELECT * FROM "test" FOR UPDATE
```

If your dialect supports FOR UPDATE OF you provide tables to be locked as variable arguments to the ForUpdate method.

```go
sql, _, _ := goqu.From("test").ForUpdate(exp.Wait, goqu.T("test")).ToSQL()
fmt.Println(sql)
```

Output:
```sql
SELECT * FROM "test" FOR UPDATE OF "test"
```

## Executing Queries

To execute your query use [`goqu.Database#From`](https://godoc.org/github.com/doug-martin/goqu/#Database.From) to create your dataset
Expand Down
9 changes: 8 additions & 1 deletion exp/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ type (
Lock interface {
Strength() LockStrength
WaitOption() WaitOption
Of() []IdentifierExpression
}
lock struct {
strength LockStrength
waitOption WaitOption
of []IdentifierExpression
}
)

Expand All @@ -25,10 +27,11 @@ const (
SkipLocked
)

func NewLock(strength LockStrength, option WaitOption) Lock {
func NewLock(strength LockStrength, option WaitOption, of ...IdentifierExpression) Lock {
return lock{
strength: strength,
waitOption: option,
of: of,
}
}

Expand All @@ -39,3 +42,7 @@ func (l lock) Strength() LockStrength {
func (l lock) WaitOption() WaitOption {
return l.waitOption
}

func (l lock) Of() []IdentifierExpression {
return l.of
}
20 changes: 10 additions & 10 deletions select_dataset.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,27 +359,27 @@ func (sd *SelectDataset) ClearWhere() *SelectDataset {
}

// Adds a FOR UPDATE clause. See examples.
func (sd *SelectDataset) ForUpdate(waitOption exp.WaitOption) *SelectDataset {
return sd.withLock(exp.ForUpdate, waitOption)
func (sd *SelectDataset) ForUpdate(waitOption exp.WaitOption, of ...exp.IdentifierExpression) *SelectDataset {
return sd.withLock(exp.ForUpdate, waitOption, of...)
}

// Adds a FOR NO KEY UPDATE clause. See examples.
func (sd *SelectDataset) ForNoKeyUpdate(waitOption exp.WaitOption) *SelectDataset {
return sd.withLock(exp.ForNoKeyUpdate, waitOption)
func (sd *SelectDataset) ForNoKeyUpdate(waitOption exp.WaitOption, of ...exp.IdentifierExpression) *SelectDataset {
return sd.withLock(exp.ForNoKeyUpdate, waitOption, of...)
}

// Adds a FOR KEY SHARE clause. See examples.
func (sd *SelectDataset) ForKeyShare(waitOption exp.WaitOption) *SelectDataset {
return sd.withLock(exp.ForKeyShare, waitOption)
func (sd *SelectDataset) ForKeyShare(waitOption exp.WaitOption, of ...exp.IdentifierExpression) *SelectDataset {
return sd.withLock(exp.ForKeyShare, waitOption, of...)
}

// Adds a FOR SHARE clause. See examples.
func (sd *SelectDataset) ForShare(waitOption exp.WaitOption) *SelectDataset {
return sd.withLock(exp.ForShare, waitOption)
func (sd *SelectDataset) ForShare(waitOption exp.WaitOption, of ...exp.IdentifierExpression) *SelectDataset {
return sd.withLock(exp.ForShare, waitOption, of...)
}

func (sd *SelectDataset) withLock(strength exp.LockStrength, option exp.WaitOption) *SelectDataset {
return sd.copy(sd.clauses.SetLock(exp.NewLock(strength, option)))
func (sd *SelectDataset) withLock(strength exp.LockStrength, option exp.WaitOption, of ...exp.IdentifierExpression) *SelectDataset {
return sd.copy(sd.clauses.SetLock(exp.NewLock(strength, option, of...)))
}

// Adds a GROUP BY clause. See examples.
Expand Down
32 changes: 32 additions & 0 deletions select_dataset_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"github.com/doug-martin/goqu/v9"
"github.com/doug-martin/goqu/v9/exp"
"github.com/lib/pq"
)

Expand Down Expand Up @@ -1651,3 +1652,34 @@ func ExampleSelectDataset_Executor_scannerScanVal() {
// Sally
// Vinita
}

func ExampleForUpdate() {
sql, args, _ := goqu.From("test").ForUpdate(exp.Wait).ToSQL()
fmt.Println(sql, args)

// Output:
// SELECT * FROM "test" FOR UPDATE []
}

func ExampleForUpdate_of() {
sql, args, _ := goqu.From("test").ForUpdate(exp.Wait, goqu.T("test")).ToSQL()
fmt.Println(sql, args)

// Output:
// SELECT * FROM "test" FOR UPDATE OF "test" []
}

func ExampleForUpdate_ofMultiple() {
sql, args, _ := goqu.From("table1").Join(
goqu.T("table2"),
goqu.On(goqu.I("table2.id").Eq(goqu.I("table1.id"))),
).ForUpdate(
exp.Wait,
goqu.T("table1"),
goqu.T("table2"),
).ToSQL()
fmt.Println(sql, args)

// Output:
// SELECT * FROM "table1" INNER JOIN "table2" ON ("table2"."id" = "table1"."id") FOR UPDATE OF "table1", "table2" []
}
48 changes: 48 additions & 0 deletions select_dataset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,18 @@ func (sds *selectDatasetSuite) TestForUpdate() {
SetFrom(exp.NewColumnListExpression("test")).
SetLock(exp.NewLock(exp.ForUpdate, goqu.NoWait)),
},
selectTestCase{
ds: bd.ForUpdate(goqu.NoWait, goqu.T("table1")),
clauses: exp.NewSelectClauses().
SetFrom(exp.NewColumnListExpression("test")).
SetLock(exp.NewLock(exp.ForUpdate, goqu.NoWait, goqu.T("table1"))),
},
selectTestCase{
ds: bd.ForUpdate(goqu.NoWait, goqu.T("table1"), goqu.T("table2")),
clauses: exp.NewSelectClauses().
SetFrom(exp.NewColumnListExpression("test")).
SetLock(exp.NewLock(exp.ForUpdate, goqu.NoWait, goqu.T("table1"), goqu.T("table2"))),
},
selectTestCase{
ds: bd,
clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")),
Expand All @@ -690,6 +702,18 @@ func (sds *selectDatasetSuite) TestForNoKeyUpdate() {
SetFrom(exp.NewColumnListExpression("test")).
SetLock(exp.NewLock(exp.ForNoKeyUpdate, goqu.NoWait)),
},
selectTestCase{
ds: bd.ForNoKeyUpdate(goqu.NoWait, goqu.T("table1")),
clauses: exp.NewSelectClauses().
SetFrom(exp.NewColumnListExpression("test")).
SetLock(exp.NewLock(exp.ForNoKeyUpdate, goqu.NoWait, goqu.T("table1"))),
},
selectTestCase{
ds: bd.ForNoKeyUpdate(goqu.NoWait, goqu.T("table1"), goqu.T("table2")),
clauses: exp.NewSelectClauses().
SetFrom(exp.NewColumnListExpression("test")).
SetLock(exp.NewLock(exp.ForNoKeyUpdate, goqu.NoWait, goqu.T("table1"), goqu.T("table2"))),
},
selectTestCase{
ds: bd,
clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")),
Expand All @@ -706,6 +730,18 @@ func (sds *selectDatasetSuite) TestForKeyShare() {
SetFrom(exp.NewColumnListExpression("test")).
SetLock(exp.NewLock(exp.ForKeyShare, goqu.NoWait)),
},
selectTestCase{
ds: bd.ForKeyShare(goqu.NoWait, goqu.T("table1")),
clauses: exp.NewSelectClauses().
SetFrom(exp.NewColumnListExpression("test")).
SetLock(exp.NewLock(exp.ForKeyShare, goqu.NoWait, goqu.T("table1"))),
},
selectTestCase{
ds: bd.ForKeyShare(goqu.NoWait, goqu.T("table1"), goqu.T("table2")),
clauses: exp.NewSelectClauses().
SetFrom(exp.NewColumnListExpression("test")).
SetLock(exp.NewLock(exp.ForKeyShare, goqu.NoWait, goqu.T("table1"), goqu.T("table2"))),
},
selectTestCase{
ds: bd,
clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")),
Expand All @@ -722,6 +758,18 @@ func (sds *selectDatasetSuite) TestForShare() {
SetFrom(exp.NewColumnListExpression("test")).
SetLock(exp.NewLock(exp.ForShare, goqu.NoWait)),
},
selectTestCase{
ds: bd.ForShare(goqu.NoWait, goqu.T("table1")),
clauses: exp.NewSelectClauses().
SetFrom(exp.NewColumnListExpression("test")).
SetLock(exp.NewLock(exp.ForShare, goqu.NoWait, goqu.T("table1"))),
},
selectTestCase{
ds: bd.ForShare(goqu.NoWait, goqu.T("table1"), goqu.T("table2")),
clauses: exp.NewSelectClauses().
SetFrom(exp.NewColumnListExpression("test")).
SetLock(exp.NewLock(exp.ForShare, goqu.NoWait, goqu.T("table1"), goqu.T("table2"))),
},
selectTestCase{
ds: bd,
clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")),
Expand Down
17 changes: 16 additions & 1 deletion sqlgen/select_sql_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,23 @@ func (ssg *selectSQLGenerator) ForSQL(b sb.SQLBuilder, lockingClause exp.Lock) {
case exp.ForKeyShare:
b.Write(ssg.DialectOptions().ForKeyShareFragment)
}

of := lockingClause.Of()
if ofLen := len(of); ofLen > 0 {
if ofFragment := ssg.DialectOptions().OfFragment; len(ofFragment) > 0 {
b.Write(ofFragment)
for i, table := range of {
ssg.ExpressionSQLGenerator().Generate(b, table)
if i < ofLen-1 {
b.WriteRunes(ssg.DialectOptions().CommaRune, ssg.DialectOptions().SpaceRune)
}
}
b.WriteRunes(ssg.DialectOptions().SpaceRune)
}
}

// the WAIT case is the default in Postgres, and is what you get if you don't specify NOWAIT or
// SKIP LOCKED. There's no special syntax for it in PG, so we don't do anything for it here
// SKIP LOCKED. There's no special syntax for it in PG, so we don't do anything for it here
switch lockingClause.WaitOption() {
case exp.Wait:
return
Expand Down
13 changes: 13 additions & 0 deletions sqlgen/select_sql_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sqlgen_test
import (
"testing"

"github.com/doug-martin/goqu/v9"
"github.com/doug-martin/goqu/v9/exp"
"github.com/doug-martin/goqu/v9/internal/errors"
"github.com/doug-martin/goqu/v9/internal/sb"
Expand Down Expand Up @@ -506,17 +507,21 @@ func (ssgs *selectSQLGeneratorSuite) TestToSelectSQL_withFor() {
opts.ForNoKeyUpdateFragment = []byte(" for no key update ")
opts.ForShareFragment = []byte(" for share ")
opts.ForKeyShareFragment = []byte(" for key share ")
opts.OfFragment = []byte("of ")
opts.NowaitFragment = []byte("nowait")
opts.SkipLockedFragment = []byte("skip locked")

sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test"))
scFnW := sc.SetLock(exp.NewLock(exp.ForNolock, exp.Wait))
scFnNw := sc.SetLock(exp.NewLock(exp.ForNolock, exp.NoWait))
scFnSl := sc.SetLock(exp.NewLock(exp.ForNolock, exp.SkipLocked))
scFnSlOf := sc.SetLock(exp.NewLock(exp.ForNolock, exp.SkipLocked, goqu.T("my_table")))

scFsW := sc.SetLock(exp.NewLock(exp.ForShare, exp.Wait))
scFsNw := sc.SetLock(exp.NewLock(exp.ForShare, exp.NoWait))
scFsSl := sc.SetLock(exp.NewLock(exp.ForShare, exp.SkipLocked))
scFsSlOf := sc.SetLock(exp.NewLock(exp.ForShare, exp.SkipLocked, goqu.T("my_table")))
scFsSlOfMulti := sc.SetLock(exp.NewLock(exp.ForShare, exp.SkipLocked, goqu.T("my_table"), goqu.T("table2")))

scFksW := sc.SetLock(exp.NewLock(exp.ForKeyShare, exp.Wait))
scFksNw := sc.SetLock(exp.NewLock(exp.ForKeyShare, exp.NoWait))
Expand All @@ -539,6 +544,8 @@ func (ssgs *selectSQLGeneratorSuite) TestToSelectSQL_withFor() {

selectTestCase{clause: scFnSl, sql: `SELECT * FROM "test"`},
selectTestCase{clause: scFnSl, sql: `SELECT * FROM "test"`, isPrepared: true},
selectTestCase{clause: scFnSlOf, sql: `SELECT * FROM "test"`},
selectTestCase{clause: scFnSlOf, sql: `SELECT * FROM "test"`, isPrepared: true, args: []interface{}{}},

selectTestCase{clause: scFsW, sql: `SELECT * FROM "test" for share `},
selectTestCase{clause: scFsW, sql: `SELECT * FROM "test" for share `, isPrepared: true},
Expand All @@ -549,6 +556,12 @@ func (ssgs *selectSQLGeneratorSuite) TestToSelectSQL_withFor() {
selectTestCase{clause: scFsSl, sql: `SELECT * FROM "test" for share skip locked`},
selectTestCase{clause: scFsSl, sql: `SELECT * FROM "test" for share skip locked`, isPrepared: true},

selectTestCase{clause: scFsSlOf, sql: `SELECT * FROM "test" for share of "my_table" skip locked`},
selectTestCase{clause: scFsSlOf, sql: `SELECT * FROM "test" for share of "my_table" skip locked`, isPrepared: true},

selectTestCase{clause: scFsSlOfMulti, sql: `SELECT * FROM "test" for share of "my_table", "table2" skip locked`},
selectTestCase{clause: scFsSlOfMulti, sql: `SELECT * FROM "test" for share of "my_table", "table2" skip locked`, isPrepared: true},

selectTestCase{clause: scFksW, sql: `SELECT * FROM "test" for key share `},
selectTestCase{clause: scFksW, sql: `SELECT * FROM "test" for key share `, isPrepared: true},

Expand Down
3 changes: 3 additions & 0 deletions sqlgen/sql_dialect_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ type (
ForNoKeyUpdateFragment []byte
// The SQL FOR SHARE fragment(DEFAULT=[]byte(" FOR SHARE "))
ForShareFragment []byte
// The SQL OF fragment(DEFAULT=[]byte("OF "))
OfFragment []byte
// The SQL FOR KEY SHARE fragment(DEFAULT=[]byte(" FOR KEY SHARE "))
ForKeyShareFragment []byte
// The SQL NOWAIT fragment(DEFAULT=[]byte("NOWAIT"))
Expand Down Expand Up @@ -450,6 +452,7 @@ func DefaultDialectOptions() *SQLDialectOptions {
ForNoKeyUpdateFragment: []byte(" FOR NO KEY UPDATE "),
ForShareFragment: []byte(" FOR SHARE "),
ForKeyShareFragment: []byte(" FOR KEY SHARE "),
OfFragment: []byte("OF "),
NowaitFragment: []byte("NOWAIT"),
SkipLockedFragment: []byte("SKIP LOCKED"),
LateralFragment: []byte("LATERAL "),
Expand Down