2021-06-24 07:43:28 +00:00
|
|
|
# Bob - SQL Query Builder
|
|
|
|
|
2021-12-10 09:44:15 +00:00
|
|
|
[![Go Reference](pkg-go-dev-badge)](pkg-go-dev-link)
|
|
|
|
[![Go Report Card](go-report-badge)](go-report-link)
|
|
|
|
![GitHub](license-badge)
|
|
|
|
[![CodeFactor](codefactor-badge)](codefactor-link)
|
|
|
|
[![codecov](codecov-badge)](codecov-link)
|
|
|
|
[![Codacy Badge](codacy-badge)](codacy-link)
|
|
|
|
[![Test and coverage](actions-badge)](actions-link)
|
|
|
|
|
|
|
|
Bob is an SQL builder library initially made as an extension for [Squirrel](squirrel-url)
|
|
|
|
with functionality like [Knex](knex-url) (from the Node.js world). Squirrel itself
|
|
|
|
doesn't provide other types of queries for creating a table, upsert,
|
|
|
|
and some other things. Bob is meant to fill those gaps.
|
|
|
|
|
|
|
|
The different between Bob and Squirrel is that Bob is solely a query builder.
|
|
|
|
The users have to execute and manage the SQL connection themself.
|
|
|
|
Meaning there are no ExecWith() function implemented on Bob, as you can
|
|
|
|
find it on Squirrel.
|
|
|
|
|
|
|
|
The purpose of an SQL query builder is to prevent any typo or mistypes
|
|
|
|
on the SQL queries. Although also with that reason, Bob might not always
|
|
|
|
have the right query for you, depending on what you are doing with the
|
|
|
|
SQL query. It might sometimes be better for you to write the SQL query
|
|
|
|
yourself, if your problem is specific and needs some micro-tweaks.
|
|
|
|
|
|
|
|
With that being said, I hope you enjoy using Bob and consider starring or
|
|
|
|
reporting any issues regarding the usage of Bob in your projects.
|
|
|
|
|
|
|
|
Oh, and of course, heavily inspired by [Bob the Builder](bob-wikipedia).
|
2021-06-25 07:32:12 +00:00
|
|
|
|
2021-12-10 09:44:15 +00:00
|
|
|
## Usage
|
2021-06-24 07:43:28 +00:00
|
|
|
|
2021-07-09 06:47:26 +00:00
|
|
|
```go
|
|
|
|
import "github.com/aldy505/bob"
|
|
|
|
```
|
|
|
|
|
2021-12-10 09:44:15 +00:00
|
|
|
Like any other Go projects when you're using Go modules, just put that
|
|
|
|
text right there on the top of your projects, do `go mod tidy` and
|
|
|
|
you are good to go.
|
2021-06-24 07:43:28 +00:00
|
|
|
|
2021-12-10 09:44:15 +00:00
|
|
|
Either way, I'm not 100% confident enough to say that this thing is
|
|
|
|
production ready. But, the way I see it, it's good enough to be used
|
|
|
|
on a production-level applications. In fact, I'm using it on one of my
|
|
|
|
current projects that's getting around 100-200 hits per day.
|
|
|
|
|
|
|
|
If you have any feature request or improvement ideas for the project,
|
|
|
|
please kindly open an issue
|
2021-07-21 10:15:30 +00:00
|
|
|
|
|
|
|
### Create a table
|
|
|
|
|
|
|
|
```go
|
|
|
|
import "github.com/aldy505/bob"
|
|
|
|
|
|
|
|
func main() {
|
2021-07-21 18:47:19 +00:00
|
|
|
// Note that CREATE TABLE doesn't returns args params.
|
2021-07-21 10:15:30 +00:00
|
|
|
sql, _, err := bob.
|
|
|
|
CreateTable("tableName").
|
|
|
|
// The first parameter is the column's name.
|
|
|
|
// The second parameters and so on forth are extras.
|
|
|
|
StringColumn("id", "NOT NULL", "PRIMARY KEY", "AUTOINCREMENT").
|
|
|
|
StringColumn("email", "NOT NULL", "UNIQUE").
|
2021-07-21 18:47:19 +00:00
|
|
|
// See the list of available column definition types through pkg.go.dev or scroll down below.
|
2021-07-21 10:15:30 +00:00
|
|
|
TextColumn("password").
|
2021-07-21 18:47:19 +00:00
|
|
|
// Or add your custom type.
|
2021-07-21 10:24:05 +00:00
|
|
|
AddColumn(bob.ColumnDef{Name: "tableName", Type: "customType", Extras: []string{"NOT NULL"}}).
|
2021-07-21 10:15:30 +00:00
|
|
|
ToSql()
|
|
|
|
if err != nil {
|
|
|
|
// handle your error
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2021-07-21 18:47:19 +00:00
|
|
|
Available column definition types (please be aware that some only works on certain database):
|
2021-07-21 10:24:05 +00:00
|
|
|
* `StringColumn()` - Default to `VARCHAR(255)`
|
|
|
|
* `TextColumn()` - Default to `TEXT`
|
|
|
|
* `UUIDColumn()` - Defaults to `UUID`
|
|
|
|
* `BooleanColumn()` - Defaults to `BOOLEAN`
|
|
|
|
* `IntegerColumn()` - Defaults to `INTEGER`. Postgres and SQLite only.
|
|
|
|
* `IntColumn()` - Defaults to `INT`. MySQL and MSSQL only.
|
|
|
|
* `RealColumn()` - Defaults to `REAL`. Postgres, MSSQL, and SQLite only.
|
|
|
|
* `FloatColumn()` - Defaults to `FLOAT`. Postgres and SQLite only.
|
|
|
|
* `DateTimeColumn()` - Defaults to `DATETIME`.
|
|
|
|
* `TimeStampColumn()` - Defaults to `TIMESTAMP`.
|
|
|
|
* `TimeColumn()` - Defaults to `TIME`.
|
|
|
|
* `DateColumn()` - Defaults to `DATE`.
|
|
|
|
* `JSONColumn()` - Dafults to `JSON`. MySQL and Postgres only.
|
|
|
|
* `JSONBColumn()` - Defaults to `JSONB`. Postgres only.
|
|
|
|
* `BlobColumn()` - Defaults to `BLOB`. MySQL and SQLite only.
|
|
|
|
|
|
|
|
For any other types, please use `AddColumn()`.
|
|
|
|
|
2021-07-21 10:15:30 +00:00
|
|
|
Another builder of `bob.CreateTableIfNotExists()` is also available.
|
|
|
|
|
2021-11-09 05:52:32 +00:00
|
|
|
### Create index
|
|
|
|
|
|
|
|
```go
|
|
|
|
func main() {
|
|
|
|
sql, _, err := bob.
|
2021-11-10 04:21:13 +00:00
|
|
|
CreateIndex("idx_email").
|
|
|
|
On("users").
|
2021-11-09 05:52:32 +00:00
|
|
|
// To create a CREATE UNIQUE INDEX ...
|
|
|
|
Unique().
|
|
|
|
// Method "Spatial()" and "FullText()" are also available.
|
|
|
|
// You can specify as many columns as you like.
|
2021-11-10 04:21:13 +00:00
|
|
|
Columns(bob.IndexColumn{Name: "email", Collate: "DEFAULT", Extras: []string{"ASC"}}).
|
|
|
|
ToSql()
|
2021-11-09 05:52:32 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
Another builder of `bob.CreateIndexIfNotExists()` is also available.
|
|
|
|
|
2021-07-21 10:15:30 +00:00
|
|
|
### Check if a table exists
|
|
|
|
|
|
|
|
```go
|
|
|
|
func main() {
|
|
|
|
sql, args, err := bob.HasTable("users").ToSql()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Check if a column exists
|
|
|
|
|
|
|
|
```go
|
|
|
|
func main() {
|
|
|
|
sql, args, err := bob.HasColumn("email").ToSql()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2021-07-24 18:47:02 +00:00
|
|
|
### Drop table
|
|
|
|
|
|
|
|
```go
|
|
|
|
func main() {
|
|
|
|
sql, _, err := bob.DropTable("users").ToSql()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2021-11-05 05:06:47 +00:00
|
|
|
// sql = "DROP TABLE users;"
|
|
|
|
|
|
|
|
sql, _, err = bob.DropTableIfExists("users").ToSql()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
// sql = "DROP TABLE IF EXISTS users;"
|
|
|
|
|
|
|
|
sql, _, err = bob.DropTable("users").Cascade().ToSql()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
// sql = "DROP TABLE users CASCADE;"
|
|
|
|
|
|
|
|
sql, _, err = bob.DropTable("users").Restrict().ToSql()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
// sql = "DROP TABLE users RESTRICT;"
|
2021-07-24 18:47:02 +00:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Truncate table
|
|
|
|
|
|
|
|
```go
|
|
|
|
func main() {
|
|
|
|
sql, _, err := bob.Truncate("users").ToSql()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Rename table
|
|
|
|
|
|
|
|
```go
|
|
|
|
func main() {
|
|
|
|
sql, _, err := bob.RenameTable("users", "people").ToSql()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2021-07-31 04:44:21 +00:00
|
|
|
### Upsert
|
|
|
|
|
|
|
|
```go
|
|
|
|
func main() {
|
|
|
|
sql, args, err := bob.
|
|
|
|
// Notice that you should give database dialect on the second params.
|
|
|
|
// Available database dialect are MySQL, PostgreSQL, SQLite, and MSSQL.
|
|
|
|
Upsert("users", bob.MySQL).
|
|
|
|
Columns("name", "email", "age").
|
|
|
|
// You could do multiple Values() call, but I'd suggest to not do it.
|
|
|
|
// Because this is an upsert function, not an insert one.
|
|
|
|
Values("Thomas Mueler", "tmueler@something.com", 25).
|
|
|
|
Replace("age", 25).
|
|
|
|
ToSql()
|
|
|
|
|
|
|
|
// Another example for PostgreSQL
|
|
|
|
sql, args, err = bob.
|
|
|
|
Upsert("users", bob.PostgreSQL).
|
|
|
|
Columns("name", "email", "age").
|
|
|
|
Values("Billy Urtha", "billu@something.com", 30).
|
|
|
|
Key("email").
|
|
|
|
Replace("age", 40).
|
|
|
|
ToSql()
|
2021-12-10 09:44:15 +00:00
|
|
|
|
2021-07-31 04:44:21 +00:00
|
|
|
// One more time, for MSSQL / SQL Server.
|
|
|
|
sql, args, err = bob.
|
|
|
|
Upsert("users", bob.MSSQL).
|
|
|
|
Columns("name", "email", "age").
|
|
|
|
Values("George Rust", "georgee@something.com", 19).
|
|
|
|
Key("email", "georgee@something.com").
|
|
|
|
Replace("age", 18).
|
|
|
|
ToSql()
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2021-07-21 18:47:19 +00:00
|
|
|
### Placeholder format / Dialect
|
2021-07-21 10:15:30 +00:00
|
|
|
|
|
|
|
Default placeholder is a question mark (MySQL-like). If you want to change it, simply use something like this:
|
|
|
|
|
|
|
|
```go
|
|
|
|
func main() {
|
|
|
|
// Option 1
|
|
|
|
sql, args, err := bob.HasTable("users").PlaceholderFormat(bob.Dollar).ToSql()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Option 2
|
|
|
|
sql, args, err = bob.HasTable("users").ToSql()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
correctPlaceholder := bob.ReplacePlaceholder(sql, bob.Dollar)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
Available placeholder formats:
|
|
|
|
* `bob.Question` - `INSERT INTO "users" (name) VALUES (?)`
|
|
|
|
* `bob.Dollar` - `INSERT INTO "users" (name) VALUES ($1)`
|
2021-07-31 05:03:31 +00:00
|
|
|
* `bob.Colon` - `INSERT INTO "users" (name) VALUES (:1)`
|
2021-07-21 10:15:30 +00:00
|
|
|
* `bob.AtP` - `INSERT INTO "users" (name) VALUES (@p1)`
|
|
|
|
|
|
|
|
### With pgx (PostgreSQL)
|
2021-06-24 07:43:28 +00:00
|
|
|
|
|
|
|
```go
|
2021-06-30 15:58:25 +00:00
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"log"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/aldy505/bob"
|
|
|
|
"github.com/jackc/pgx/v4"
|
|
|
|
)
|
2021-06-24 07:43:28 +00:00
|
|
|
|
|
|
|
func main() {
|
2021-06-30 15:58:25 +00:00
|
|
|
db := pgx.Connect()
|
|
|
|
|
|
|
|
// Check if a table is exists
|
2021-07-09 06:41:34 +00:00
|
|
|
sql, args, err = bob.HasTable("users").PlaceholderFormat(bob.Dollar).ToSql()
|
2021-06-30 16:01:13 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2021-06-30 15:58:25 +00:00
|
|
|
|
|
|
|
var hasTableUsers bool
|
|
|
|
err = db.QueryRow(context.Background(), sql, args...).Scan(&hasTableUsers)
|
2021-06-24 07:43:28 +00:00
|
|
|
if err != nil {
|
2021-07-21 09:00:05 +00:00
|
|
|
if err == bob.ErrEmptyTablePg {
|
2021-06-30 16:01:13 +00:00
|
|
|
hasTableUsers = false
|
|
|
|
} else {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2021-06-24 07:43:28 +00:00
|
|
|
}
|
|
|
|
|
2021-06-30 15:58:25 +00:00
|
|
|
if !hasTableUsers {
|
|
|
|
// Create "users" table
|
2021-07-21 09:00:05 +00:00
|
|
|
sql, _, err := bob.
|
|
|
|
CreateTable("users").
|
|
|
|
IntegerColumn("id", "PRIMARY KEY", "SERIAL").
|
|
|
|
StringColumn("name", "NOT NULL").
|
|
|
|
TextColumn("password", "NOT NULL").
|
|
|
|
DateColumn("created_at").
|
2021-07-09 06:41:34 +00:00
|
|
|
ToSql()
|
2021-06-30 15:58:25 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2021-07-21 09:00:05 +00:00
|
|
|
_, err = db.Query(context.Background(), splitQuery[i])
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
2021-06-30 15:58:25 +00:00
|
|
|
}
|
2021-07-09 06:47:26 +00:00
|
|
|
|
|
|
|
// Create another table, this time with CREATE TABLE IF NOT EXISTS
|
2021-07-21 09:00:05 +00:00
|
|
|
sql, _, err := bob.
|
|
|
|
CreateTableIfNotExists("inventory").
|
|
|
|
UUIDColumn("id", "PRIMARY KEY").
|
|
|
|
IntegerColumn("userID", "FOREIGN KEY REFERENCES users(id)").
|
|
|
|
JSONColumn("items").
|
|
|
|
IntegerColumn("quantity").
|
2021-07-09 06:47:26 +00:00
|
|
|
ToSql()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2021-12-10 09:44:15 +00:00
|
|
|
|
2021-07-21 09:00:05 +00:00
|
|
|
_, err = db.Query(context.Background(), inventoryQuery[i])
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
2021-07-09 06:47:26 +00:00
|
|
|
}
|
2021-06-30 15:58:25 +00:00
|
|
|
}
|
2021-06-24 07:43:28 +00:00
|
|
|
}
|
2021-07-09 06:47:26 +00:00
|
|
|
```
|
|
|
|
|
2021-07-21 09:00:05 +00:00
|
|
|
## Features
|
|
|
|
|
|
|
|
* `bob.CreateTable(tableName)` - Basic SQL create table
|
|
|
|
* `bob.CreateTableIfNotExists(tableName)` - Create table if not exists
|
2021-11-09 05:52:32 +00:00
|
|
|
* `bob.CreateIndex(indexName)` - Basic SQL create index
|
|
|
|
* `bob.CreateIndexIfNotExists(tableName)` - Create index if not exists
|
2021-07-21 09:00:05 +00:00
|
|
|
* `bob.HasTable(tableName)` - Checks if column exists (return error if false, check example above for error handling)
|
|
|
|
* `bob.HasColumn(columnName)` - Check if a column exists on current table
|
2021-07-24 18:47:02 +00:00
|
|
|
* `bob.DropTable(tableName)` - Drop a table (`drop table "users"`)
|
|
|
|
* `bob.DropTableIfExists(tableName)` - Drop a table if exists (`drop table if exists "users"`)
|
|
|
|
* `bob.RenameTable(currentTable, desiredName)` - Rename a table (`rename table "users" to "people"`)
|
|
|
|
* `bob.Truncate(tableName)` - Truncate a table (`truncate "users"`)
|
2021-07-31 04:44:21 +00:00
|
|
|
* `bob.Upsert(tableName, dialect)` - UPSERT function (`insert into "users" ("name", "email") values (?, ?) on duplicate key update email = ?`)
|
2021-07-21 09:00:05 +00:00
|
|
|
|
2021-07-09 06:47:26 +00:00
|
|
|
## Contributing
|
|
|
|
|
|
|
|
Contributions are always welcome! As long as you add a test for your changes.
|
|
|
|
|
|
|
|
## License
|
|
|
|
|
2021-07-21 18:47:19 +00:00
|
|
|
Bob is licensed under [MIT license](./LICENSE)
|
2021-12-10 09:44:15 +00:00
|
|
|
|
|
|
|
[squirrel-url]: https://github.com/Masterminds/squirrel
|
|
|
|
[knex-url]: https://knexjs.org/
|
|
|
|
[bob-wikipedia]: https://en.wikipedia.org/wiki/Bob_the_Builder
|
|
|
|
[pkg-go-dev-badge]: https://pkg.go.dev/badge/github.com/aldy505/bob.svg
|
|
|
|
[pkg-go-dev-link]: https://pkg.go.dev/github.com/aldy505/bob
|
|
|
|
[go-report-badge]: https://goreportcard.com/badge/github.com/aldy505/bob
|
|
|
|
[go-report-link]: https://goreportcard.com/report/github.com/aldy505/bob
|
|
|
|
[license-badge]: https://img.shields.io/github/license/aldy505/bob
|
|
|
|
[codefactor-link]: https://www.codefactor.io/repository/github/aldy505/bob
|
|
|
|
[codefactor-badge]: https://www.codefactor.io/repository/github/aldy505/bob/badge
|
|
|
|
[codecov-badge]: https://codecov.io/gh/aldy505/bob/branch/master/graph/badge.svg?token=Noeexg5xEJ
|
|
|
|
[codecov-link]: https://codecov.io/gh/aldy505/bob
|
|
|
|
[codacy-badge]: https://app.codacy.com/project/badge/Grade/9b78970127c74c1a923533e05f65848d
|
|
|
|
[codacy-link]: https://www.codacy.com/gh/aldy505/bob/dashboard?utm_source=github.com&utm_medium=referral&utm_content=aldy505/bob&utm_campaign=Badge_Grade
|
|
|
|
[actions-badge]: https://github.com/aldy505/bob/actions/workflows/coverage.yml/badge.svg
|
|
|
|
[actions-link]: https://github.com/aldy505/bob/actions/workflows/coverage.yml
|