Skip to content

Commit cb9e7b0

Browse files
author
Reinaldy Rafli
authored
Merge pull request #3 from aldy505/feat/upsert
Feat/upsert (WIP)
2 parents 50c8d98 + b5df4ae commit cb9e7b0

14 files changed

+505
-48
lines changed

README.md

+40-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![Go Reference](https://pkg.go.dev/badge/github.com/aldy505/bob.svg)](https://pkg.go.dev/github.com/aldy505/bob) [![Go Report Card](https://goreportcard.com/badge/github.com/aldy505/bob)](https://goreportcard.com/report/github.com/aldy505/bob) ![GitHub](https://img.shields.io/github/license/aldy505/bob) [![CodeFactor](https://www.codefactor.io/repository/github/aldy505/bob/badge)](https://www.codefactor.io/repository/github/aldy505/bob) [![codecov](https://codecov.io/gh/aldy505/bob/branch/master/graph/badge.svg?token=Noeexg5xEJ)](https://codecov.io/gh/aldy505/bob) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/9b78970127c74c1a923533e05f65848d)](https://www.codacy.com/gh/aldy505/bob/dashboard?utm_source=github.com&utm_medium=referral&utm_content=aldy505/bob&utm_campaign=Badge_Grade) [![Build test](https://github.com/aldy505/bob/actions/workflows/build.yml/badge.svg)](https://github.com/aldy505/bob/actions/workflows/build.yml) [![Test and coverage](https://github.com/aldy505/bob/actions/workflows/coverage.yml/badge.svg)](https://github.com/aldy505/bob/actions/workflows/coverage.yml)
44

5-
Think of this as an extension of [Squirrel](https://github.com/Masterminds/squirrel) with functionability like [Knex](https://knexjs.org/). I still use Squirrel for other types of queries (insert, select, and all that), but I needed some SQL builder for create table and some other stuffs.
5+
Think of this as an extension of [Squirrel](https://github.com/Masterminds/squirrel) with functionability like [Knex](https://knexjs.org/). I still use Squirrel for other types of queries (insert, select, and all that), but I needed some SQL builder for create table and some other stuffs. Including database creation & upsert.
66

77
Oh, and of course, heavily inspired by [Bob the Builder](https://en.wikipedia.org/wiki/Bob_the_Builder).
88

@@ -116,6 +116,44 @@ func main() {
116116
}
117117
```
118118

119+
### Upsert
120+
121+
```go
122+
func main() {
123+
sql, args, err := bob.
124+
// Notice that you should give database dialect on the second params.
125+
// Available database dialect are MySQL, PostgreSQL, SQLite, and MSSQL.
126+
Upsert("users", bob.MySQL).
127+
Columns("name", "email", "age").
128+
// You could do multiple Values() call, but I'd suggest to not do it.
129+
// Because this is an upsert function, not an insert one.
130+
Values("Thomas Mueler", "[email protected]", 25).
131+
Replace("age", 25).
132+
PlaceholderFormat(bob.Question).
133+
ToSql()
134+
135+
// Another example for PostgreSQL
136+
sql, args, err = bob.
137+
Upsert("users", bob.PostgreSQL).
138+
Columns("name", "email", "age").
139+
Values("Billy Urtha", "[email protected]", 30).
140+
Key("email").
141+
Replace("age", 40).
142+
PlaceholderFormat(bob.Dollar).
143+
ToSql()
144+
145+
// One more time, for MSSQL / SQL Server.
146+
sql, args, err = bob.
147+
Upsert("users", bob.MSSQL).
148+
Columns("name", "email", "age").
149+
Values("George Rust", "[email protected]", 19).
150+
Key("email", "[email protected]").
151+
Replace("age", 18).
152+
PlaceholderFormat(bob.AtP).
153+
ToSql()
154+
}
155+
```
156+
119157
### Placeholder format / Dialect
120158

121159
Default placeholder is a question mark (MySQL-like). If you want to change it, simply use something like this:
@@ -222,12 +260,12 @@ func main() {
222260
* `bob.DropTableIfExists(tableName)` - Drop a table if exists (`drop table if exists "users"`)
223261
* `bob.RenameTable(currentTable, desiredName)` - Rename a table (`rename table "users" to "people"`)
224262
* `bob.Truncate(tableName)` - Truncate a table (`truncate "users"`)
263+
* `bob.Upsert(tableName, dialect)` - UPSERT function (`insert into "users" ("name", "email") values (?, ?) on duplicate key update email = ?`)
225264

226265
### TODO
227266

228267
Meaning these are some ideas for the future development of Bob.
229268

230-
* `bob.Upsert(tableName)` - UPSERT function (`insert into "users" ("name", "email") values (?, ?) on duplicate key update email = ?`)
231269
* `bob.ExecWith()` - Just like Squirrel's [ExecWith](https://pkg.go.dev/github.com/Masterminds/squirrel?utm_source=godoc#ExecWith)
232270
* `bob.Count(tableName, columnName)` - Count query (`select count("active") from "users"`)
233271

bob.go

+78-7
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,20 @@ import (
88

99
// ErrEmptyTable is a common database/sql error if a table is empty or no rows is returned by the query.
1010
var ErrEmptyTable = errors.New("sql: no rows in result set")
11+
1112
// ErrEmptyTable is a common pgx error if a table is empty or no rows is returned by the query.
1213
var ErrEmptyTablePgx = errors.New("no rows in result set")
1314

15+
// ErrDialectNotSupported tells you whether the dialect is supported or not.
16+
var ErrDialectNotSupported = errors.New("provided database dialect is not supported")
17+
18+
const (
19+
MySQL int = iota
20+
PostgreSQL
21+
SQLite
22+
MSSQL
23+
)
24+
1425
// BobBuilderType is the type for BobBuilder
1526
type BobBuilderType builder.Builder
1627

@@ -21,12 +32,12 @@ type BobBuilder interface {
2132

2233
// CreateTable creates a table with CreateBuilder interface
2334
func (b BobBuilderType) CreateTable(table string) CreateBuilder {
24-
return CreateBuilder(b).Name(table)
35+
return CreateBuilder(b).name(table)
2536
}
2637

2738
// CreateTableIfNotExists creates a table with CreateBuilder interface, if the table doesn't exists.
2839
func (b BobBuilderType) CreateTableIfNotExists(table string) CreateBuilder {
29-
return CreateBuilder(b).Name(table).IfNotExists()
40+
return CreateBuilder(b).name(table).ifNotExists()
3041
}
3142

3243
// HasTable checks if a table exists with HasBuilder interface
@@ -41,28 +52,49 @@ func (b BobBuilderType) HasColumn(column string) HasBuilder {
4152

4253
// DropTable drops (delete contents & remove) a table from the database.
4354
func (b BobBuilderType) DropTable(table string) DropBuilder {
44-
return DropBuilder(b).DropTable(table)
55+
return DropBuilder(b).dropTable(table)
4556
}
4657

4758
// DropTable drops (delete contents & remove) a table from the database if the table exists.
4859
func (b BobBuilderType) DropTableIfExists(table string) DropBuilder {
49-
return DropBuilder(b).DropTable(table).IfExists()
60+
return DropBuilder(b).dropTable(table).ifExists()
5061
}
5162

5263
// RenameTable simply renames an exisisting table.
5364
func (b BobBuilderType) RenameTable(from, to string) RenameBuilder {
54-
return RenameBuilder(b).From(from).To(to)
65+
return RenameBuilder(b).from(from).to(to)
5566
}
5667

5768
// Truncate performs TRUNCATE function. It deletes all contents from a table but not deleting the table.
5869
func (b BobBuilderType) Truncate(table string) TruncateBuilder {
59-
return TruncateBuilder(b).Truncate(table)
70+
return TruncateBuilder(b).truncate(table)
71+
}
72+
73+
func (b BobBuilderType) Upsert(table string, dialect int) UpsertBuilder {
74+
return UpsertBuilder(b).dialect(dialect).into(table)
6075
}
6176

6277
// BobStmtBuilder is the parent builder for BobBuilderType
6378
var BobStmtBuilder = BobBuilderType(builder.EmptyBuilder)
6479

6580
// CreateTable creates a table with CreateBuilder interface.
81+
// Refer to README for available column definition types.
82+
//
83+
// // Note that CREATE TABLE doesn't returns args params.
84+
// sql, _, err := bob.
85+
// CreateTable("tableName").
86+
// // The first parameter is the column's name.
87+
// // The second parameters and so on forth are extras.
88+
// StringColumn("id", "NOT NULL", "PRIMARY KEY", "AUTOINCREMENT").
89+
// StringColumn("email", "NOT NULL", "UNIQUE").
90+
// // See the list of available column definition types through pkg.go.dev or README.
91+
// TextColumn("password").
92+
// // Or add your custom type.
93+
// AddColumn(bob.ColumnDef{Name: "tableName", Type: "customType", Extras: []string{"NOT NULL"}}).
94+
// ToSql()
95+
// if err != nil {
96+
// // handle your error
97+
// }
6698
func CreateTable(table string) CreateBuilder {
6799
return BobStmtBuilder.CreateTable(table)
68100
}
@@ -100,4 +132,43 @@ func RenameTable(from, to string) RenameBuilder {
100132
// Truncate performs TRUNCATE function. It deletes all contents from a table but not deleting the table.
101133
func Truncate(table string) TruncateBuilder {
102134
return BobStmtBuilder.Truncate(table)
103-
}
135+
}
136+
137+
// Upsert performs a UPSERT query with specified database dialect.
138+
// Supported database includes MySQL, PostgreSQL, SQLite and MSSQL.
139+
//
140+
// // MySQL example:
141+
// sql, args, err := bob.
142+
// // Notice that you should give database dialect on the second params.
143+
// // Available database dialect are MySQL, PostgreSQL, SQLite, and MSSQL.
144+
// Upsert("users", bob.MySQL).
145+
// Columns("name", "email", "age").
146+
// // You could do multiple Values() call, but I'd suggest to not do it.
147+
// // Because this is an upsert function, not an insert one.
148+
// Values("Thomas Mueler", "[email protected]", 25).
149+
// Replace("age", 25).
150+
// PlaceholderFormat(bob.Question).
151+
// ToSql()
152+
//
153+
// // Another example for PostgreSQL:
154+
// sql, args, err = bob.
155+
// Upsert("users", bob.PostgreSQL).
156+
// Columns("name", "email", "age").
157+
// Values("Billy Urtha", "[email protected]", 30).
158+
// Key("email").
159+
// Replace("age", 40).
160+
// PlaceholderFormat(bob.Dollar).
161+
// ToSql()
162+
//
163+
// // One more time, for MSSQL / SQL Server:
164+
// sql, args, err = bob.
165+
// Upsert("users", bob.MSSQL).
166+
// Columns("name", "email", "age").
167+
// Values("George Rust", "[email protected]", 19).
168+
// Key("email", "[email protected]").
169+
// Replace("age", 18).
170+
// PlaceholderFormat(bob.AtP).
171+
// ToSql()
172+
func Upsert(table string, dialect int) UpsertBuilder {
173+
return BobStmtBuilder.Upsert(table, dialect)
174+
}

create.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ func init() {
2727
builder.Register(CreateBuilder{}, createData{})
2828
}
2929

30-
// Name sets the table name
31-
func (b CreateBuilder) Name(name string) CreateBuilder {
30+
// name sets the table name
31+
func (b CreateBuilder) name(name string) CreateBuilder {
3232
return builder.Set(b, "TableName", name).(CreateBuilder)
3333
}
3434

35-
// IfNotExists adds IF NOT EXISTS to the query
36-
func (b CreateBuilder) IfNotExists() CreateBuilder {
35+
// ifNotExists adds IF NOT EXISTS to the query
36+
func (b CreateBuilder) ifNotExists() CreateBuilder {
3737
return builder.Set(b, "IfNotExists", true).(CreateBuilder)
3838
}
3939

drop.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,19 @@ type DropBuilder builder.Builder
1111

1212
type dropData struct {
1313
TableName string
14-
IfExists bool
14+
IfExists bool
1515
}
1616

1717
func init() {
1818
builder.Register(DropBuilder{}, dropData{})
1919
}
2020

2121
// DropTable sets which table to be dropped
22-
func (b DropBuilder) DropTable(name string) DropBuilder {
22+
func (b DropBuilder) dropTable(name string) DropBuilder {
2323
return builder.Set(b, "TableName", name).(DropBuilder)
2424
}
2525

26-
func (b DropBuilder) IfExists() DropBuilder {
26+
func (b DropBuilder) ifExists() DropBuilder {
2727
return builder.Set(b, "IfExists", true).(DropBuilder)
2828
}
2929

@@ -45,9 +45,9 @@ func (d *dropData) ToSql() (sqlStr string, args []interface{}, err error) {
4545
if d.IfExists {
4646
sql.WriteString("IF EXISTS ")
4747
}
48-
49-
sql.WriteString("\""+d.TableName+"\";")
48+
49+
sql.WriteString("\"" + d.TableName + "\";")
5050

5151
sqlStr = sql.String()
5252
return
53-
}
53+
}

drop_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
)
88

99
func TestDrop(t *testing.T) {
10-
t.Run("should be able to create regular drop query", func (t *testing.T) {
10+
t.Run("should be able to create regular drop query", func(t *testing.T) {
1111
sql, _, err := bob.DropTable("users").ToSql()
1212
if err != nil {
1313
t.Error(err)
@@ -37,4 +37,4 @@ func TestDrop(t *testing.T) {
3737
t.Error(err)
3838
}
3939
})
40-
}
40+
}

has.go

-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ import (
88
"github.com/lann/builder"
99
)
1010

11-
// TODO - The whole file is a todo
12-
// Meant to find two things: HasTable and HasColumn(s)
13-
1411
type HasBuilder builder.Builder
1512

1613
type hasData struct {

has_test.go

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
package bob_test
22

33
import (
4+
"reflect"
45
"testing"
56

67
"github.com/aldy505/bob"
78
)
89

9-
// TODO - do more test
10-
1110
func TestHas(t *testing.T) {
1211
t.Run("should be able to create a hasTable query", func(t *testing.T) {
1312
sql, args, err := bob.HasTable("users").ToSql()
@@ -19,8 +18,8 @@ func TestHas(t *testing.T) {
1918
if sql != result {
2019
t.Fatal("sql is not equal with result:", sql)
2120
}
22-
23-
if len(args) != 1 {
21+
argsResult := []interface{}{"users"}
22+
if !reflect.DeepEqual(args, argsResult) {
2423
t.Fatal("args is not equal with argsResult:", args)
2524
}
2625
})
@@ -36,7 +35,8 @@ func TestHas(t *testing.T) {
3635
t.Fatal("sql is not equal with result:", sql)
3736
}
3837

39-
if len(args) != 2 {
38+
argsResult := []interface{}{"users", "name"}
39+
if !reflect.DeepEqual(args, argsResult) {
4040
t.Fatal("args is not equal with argsResult:", args)
4141
}
4242
})
@@ -52,7 +52,8 @@ func TestHas(t *testing.T) {
5252
t.Fatal("sql is not equal with result:", sql)
5353
}
5454

55-
if len(args) != 2 {
55+
argsResult := []interface{}{"users", "name"}
56+
if !reflect.DeepEqual(args, argsResult) {
5657
t.Fatal("args is not equal with argsResult:", args)
5758
}
5859
})
@@ -68,12 +69,13 @@ func TestHas(t *testing.T) {
6869
t.Fatal("sql is not equal with result:", sql)
6970
}
7071

71-
if len(args) != 2 {
72+
argsResult := []interface{}{"users", "private"}
73+
if !reflect.DeepEqual(args, argsResult) {
7274
t.Fatal("args is not equal with argsResult:", args)
7375
}
7476
})
7577

76-
t.Run("should be able to have a different placeholder", func(t *testing.T) {
78+
t.Run("should be able to have a different placeholder - dollar", func(t *testing.T) {
7779
sql, args, err := bob.HasTable("users").HasColumn("name").PlaceholderFormat(bob.Dollar).ToSql()
7880
if err != nil {
7981
t.Fatal(err.Error())
@@ -84,7 +86,8 @@ func TestHas(t *testing.T) {
8486
t.Fatal("sql is not equal with result:", sql)
8587
}
8688

87-
if len(args) != 2 {
89+
argsResult := []interface{}{"users", "name"}
90+
if !reflect.DeepEqual(args, argsResult) {
8891
t.Fatal("args is not equal with argsResult:", args)
8992
}
9093
})

rename.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,20 @@ type RenameBuilder builder.Builder
1010

1111
type renameData struct {
1212
From string
13-
To string
13+
To string
1414
}
1515

1616
func init() {
1717
builder.Register(RenameBuilder{}, renameData{})
1818
}
1919

20-
// From sets existing table name
21-
func (b RenameBuilder) From(name string) RenameBuilder {
20+
// from sets existing table name
21+
func (b RenameBuilder) from(name string) RenameBuilder {
2222
return builder.Set(b, "From", name).(RenameBuilder)
2323
}
2424

25-
// To sets desired table name
26-
func (b RenameBuilder) To(name string) RenameBuilder {
25+
// to sets desired table name
26+
func (b RenameBuilder) to(name string) RenameBuilder {
2727
return builder.Set(b, "To", name).(RenameBuilder)
2828
}
2929

@@ -38,6 +38,6 @@ func (d *renameData) ToSql() (sqlStr string, args []interface{}, err error) {
3838
if len(d.From) == 0 || d.From == "" || len(d.To) == 0 || d.To == "" {
3939
err = errors.New("rename statement must specify a table")
4040
}
41-
sqlStr = "RENAME TABLE \""+d.From+"\" TO \""+d.To+"\";"
41+
sqlStr = "RENAME TABLE \"" + d.From + "\" TO \"" + d.To + "\";"
4242
return
43-
}
43+
}

0 commit comments

Comments
 (0)