Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/nullism/bqb
BQB is a lightweight and easy to use query builder that works with sqlite, mysql, mariadb, postgres, and others.
https://github.com/nullism/bqb
go golang mysql postgresql query-builder sql sqlite
Last synced: about 2 months ago
JSON representation
BQB is a lightweight and easy to use query builder that works with sqlite, mysql, mariadb, postgres, and others.
- Host: GitHub
- URL: https://github.com/nullism/bqb
- Owner: nullism
- License: mit
- Created: 2021-07-31T17:41:45.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2024-04-29T21:21:21.000Z (8 months ago)
- Last Synced: 2024-07-31T20:49:53.021Z (5 months ago)
- Topics: go, golang, mysql, postgresql, query-builder, sql, sqlite
- Language: Go
- Homepage:
- Size: 151 KB
- Stars: 136
- Watchers: 2
- Forks: 10
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.txt
Awesome Lists containing this project
- awesome-go - bqb - Lightweight and easy to learn query builder. (Database / SQL Query Builders)
- awesome-go-extra - bqb - 07-31T17:41:45Z|2022-04-07T13:26:51Z| (Generators / SQL Query Builders)
README
# Basic Query Builder
[![Tests Status](https://github.com/nullism/bqb/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/nullism/bqb/actions/workflows/tests.yml) [![GoDoc](https://godoc.org/github.com/nullism/bqb?status.svg)](https://godoc.org/github.com/nullism/bqb) [![code coverage](coverage.svg)](https://github.com/nullism/bqb/actions/workflows/tests.yml) [![Go Report Card](https://img.shields.io/badge/go%20report-A+-brightgreen.svg?style=flat)](https://goreportcard.com/report/github.com/nullism/bqb) [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
# Compatibility
This has been tested with sqlite, PostGres, and MySQL, using `database/sql`, `pq`, `pgx`, and `sqlx`.
By the nature of how it works it should be fully compatible with any DB interface and database that uses `?` or `$` parameter syntax._Note: Go `v1.20+` is required for BQB `>= v1.4.0`. Go `v1.17+` is required for BQB `<= v1.3.0`._
# Why
1. Simple, lightweight, and fast
2. Supports any and all syntax by the nature of how it works
3. Doesn't require learning special syntax or operators
4. 100% test coverage# Examples
## Basic
```golang
q := bqb.New("SELECT * FROM places WHERE id = ?", 1234)
sql, params, err := q.ToSql()
```Produces
```sql
SELECT * FROM places WHERE id = ?
``````
PARAMS: [1234]
```### Escaping `?`
Use the double question mark `??` value to escape the `?` in Postgres queries.
For example:```golang
q := bqb.New("SELECT * FROM places WHERE json_obj_column ?? 'key'")
sql, params, err := q.ToPgsql()
```This query uses the `?` operator for jsonb types in Postgres to test an object
for the presence of a key. It should not be interpreted as an escaped value by
bqb.```sql
SELECT * FROM places WHERE json_obj_column ? 'key'
``````
PARAMS: []
```## Postgres - ToPgsql()
Just call the `ToPgsql()` method instead of `ToSql()` to convert the query to Postgres syntax
```golang
q := bqb.New("DELETE FROM users").
Space("WHERE id = ? OR name IN (?)", 7, []string{"delete", "remove"}).
Space("LIMIT ?", 5)
sql, params, err := q.ToPgsql()
```Produces
```sql
DELETE FROM users WHERE id = $1 OR name IN ($2, $3) LIMIT $4
``````
PARAMS: [7, "delete", "remove", 5]
```## Raw - ToRaw()
_Obvious warning: You should not use this for user input_
The `ToRaw()` call returns a string with the values filled in rather than parameterized
```golang
q := New("a = ?, b = ?, c = ?", "my a", 1234, nil)
sql, err := q.ToRaw()
```Produces
```
a = 'my a', b = 1234, c = NULL
```## Types
```golang
q := bqb.New(
"int:? string:? []int:? []string:? Query:? JsonMap:? nil:? []intf:?",
1, "2", []int{3, 3}, []string{"4", "4"}, bqb.New("5"), bqb.JsonMap{"6": 6}, nil, []interface{}{"a",1,true},
)
sql, _ := q.ToRaw()
```Produces
```
int:1 string:'2' []int:3,3 []string:'4','4' Query:5 JsonMap:'{"6":6}' nil:NULL []intf:'a',1,true
```### driver.Valuer
The [driver.Valuer](https://pkg.go.dev/database/sql/driver#Valuer) interface is supported for types that are able to convert
themselves to a sql driver value. See [examples/main.go:valuer](./examples/main.go#L102).```
q := bqb.New("?", valuer)
```### Embedder
BQB provides an Embedder interface for directly replacing `?` with a string returned by the `RawValue` method on the Embedder implementation.
This can be useful for changing sort direction or embedding table and column names. See [examples/main.go:embedder](./examples/main.go#L122) for an example.
_Note: Since this is a raw value, special attention should be paid to ensure user-input is checked and sanitized._
## Query IN
Arguments of type `[]string`,`[]*string`, `[]int`,`[]*int`, or `[]interface{}` are automatically expanded.
```golang
q := bqb.New(
"strs:(?) *strs:(?) ints:(?) *ints:(?) intfs:(?)",
[]string{"a", "b"}, []*string{}, []int{1, 2}, []*int{}, []interface{}{3, true},
)
sql, params, _ := q.ToSql()
```Produces
```
SQL: strs:(?,?) *strs:(?) ints:(?,?) *ints:(?) intfs:(?,?)
PARAMS: [a b 1 2 3 true]
```## Json Arguments
There are two helper structs, `JsonMap` and `JsonList` to make JSON conversion a little simpler.
```golang
sql, err := bqb.New(
"INSERT INTO my_table (json_map, json_list) VALUES (?, ?)",
bqb.JsonMap{"a": 1, "b": []string{"a","b","c"}},
bqb.JsonList{"string",1,true,nil},
).ToRaw()
```Produces
```sql
INSERT INTO my_table (json_map, json_list)
VALUES ('{"a": 1, "b": ["a","b","c"]}', '["string",1,true,null]')
```## Query Building
Since queries are built in an additive way by reference rather than value, it's easy to mutate a query without
having to reassign the result.### Basic Example
```golang
sel := bqb.New("SELECT")...
// later
sel.Space("id")...
// even later
sel.Comma("age").Comma("email")
```Produces
```sql
SELECT id,age,email
```### Advanced Example
The `Optional(string)` function returns a query that resolves to an empty string if no query parts have
been added via methods on the query instance, and joins with a space to the next query part.
For example `q := Optional("SELECT")` will resolve to an empty string unless parts have been added by one of the methods,
e.g `q.Space("* FROM my_table")` would make `q.ToSql()` resolve to `SELECT * FROM my_table`.```golang
sel := bqb.Optional("SELECT")
if getName {
sel.Comma("name")
}if getId {
sel.Comma("id")
}if !getName && !getId {
sel.Comma("*")
}from := bqb.New("FROM my_table")
where := bqb.Optional("WHERE")
if filterAdult {
adultCond := bqb.New("name = ?", "adult")
if ageCheck {
adultCond.And("age > ?", 20)
}
where.And("(?)", adultCond)
}if filterChild {
where.Or("(name = ? AND age < ?)", "youth", 21)
}q := bqb.New("? ? ?", sel, from, where).Space("LIMIT ?", 10)
```Assuming all values are true, the query would look like:
```sql
SELECT name,id FROM my_table WHERE (name = 'adult' AND age > 20) OR (name = 'youth' AND age < 21) LIMIT 10
```If `getName` and `getId` are false, the query would be
```sql
SELECT * FROM my_table WHERE (name = 'adult' AND age > 20) OR (name = 'youth' AND age < 21) LIMIT 10
```If `filterAdult` is `false`, the query would be:
```sql
SELECT name,id FROM my_table WHERE (name = 'youth' AND age < 21) LIMIT 10
```If all values are `false`, the query would be:
```sql
SELECT * FROM my_table LIMIT 10
```## Methods
Methods on the bqb `Query` struct follow the same pattern.
All query-modifying methods take a string (the query text) and variable length interface (the query args).
For example `q.And("abc")` will add `AND abc` to the query.
Take the following
```golang
q := bqb.Optional("WHERE")
q.Empty() // returns true
q.Len() // returns 0
q.Space("1 = 2") // query is now WHERE 1 = 2
q.Empty() // returns false
q.Len() // returns 1
q.And("b") // query is now WHERE 1 = 2 AND b
q.Or("c") // query is now WHERE 1 = 2 AND b OR c
q.Concat("d") // query is now WHERE 1 = 2 AND b OR cd
q.Comma("e") // query is now WHERE 1 = 2 AND b OR cd,e
q.Join("+", "f") // query is now WHERE 1 = 2 AND b OR cd,e+f
```Valid `args` include `string`, `int`, `floatN`, `*Query`, `[]int`, `Embedder`, `Embedded`, `driver.Valuer` or `[]string`.
# Frequently Asked Questions
## Is there more documentation?
It's not really necessary because the API is so tiny and public methods are documented in code,
see [godoc](https://godoc.org/github.com/nullism/bqb).
However, you can check out the [tests](./query_test.go) and [examples](./examples/main.go) to see the variety of usages.## Why not just use a string builder?
Bqb provides several benefits over a string builder:
For example let's say we use the string builder way to build the following:
```golang
var params []interface{}
var whereParts []string
q := "SELECT * FROM my_table "
if filterAge {
params = append(params, 21)
whereParts = append(whereParts, fmt.Sprintf("age > $%d ", len(params)))
}if filterBobs {
params = append(params, "Bob%")
whereParts = append(whereParts, fmt.Sprintf("name LIKE $%d ", len(params)))
}if len(whereParts) > 0 {
q += "WHERE " + strings.Join(whereParts, " AND ") + " "
}if limit != nil {
params = append(params, limit)
q += fmt.Sprintf("LIMIT $%d", len(params))
}// SELECT * FROM my_table WHERE age > $1 AND name LIKE $2 LIMIT $3
```Some problems with that approach
1. You must perform a string join for the various parts of the where clause
2. You must remember to include a trailing or leading space for each clause
3. You have to keep track of parameter count (for Postgres anyway)
4. It's kind of uglyThe same logic can be achieved with `bqb` a bit more cleanly
```golang
q := bqb.New("SELECT * FROM my_table")
where := bqb.Optional("WHERE")
if filterAge {
where.And("age > ?", 21)
}if filterBobs {
where.And("name LIKE ?", "Bob%")
}q.Space("?", where)
if limit != nil {
q.Space("LIMIT ?", limit)
}// SELECT * FROM my_table WHERE age > $1 AND name LIKE $2 LIMIT $3
```Both methods will allow you to remain close to the SQL, however the `bqb` approach will
1. Easily adapt to MySQL or Postgres without changing parameters
2. Hide the "WHERE" clause if both `filterBobs` and `filterAge` are false## Why not use a full query builder?
Take the following _typical_ query example:
```golang
q := qb.Select("*").From("users").Where(qb.And{qb.Eq{"name": "Ed"}, qb.Gt{"age": 21}})
```Vs the bqb way:
```golang
q := bqb.New("SELECT * FROM users WHERE name = ? AND age > ?", "ed", 21)
```## Okay, so a simple query it might make sense to use something like `bqb`, but what about grouped queries?
A query builder can handle this in multiple ways, a fairly common pattern might be:
```golang
q := qb.Select("name").From("users")and := qb.And{}
if checkAge {
and = append(and, qb.Gt{"age": 21})
}if checkName {
or := qb.Or{qb.Eq{"name":"trusted"}}
if nullNameOkay {
or = append(or, qb.Is{"name": nil})
}
and = append(and, or)
}q = q.Where(and)
// SELECT name FROM users WHERE age > 21 AND (name = 'trusted' OR name IS NULL)
```Contrast that with the `bqb` approach:
```golang
q := bqb.New("SELECT name FROM users")
where := bqb.Optional("WHERE")
if checkAge {
where.And("age > ?", 21)
}if checkName {
or := bqb.New("name = ?", "trusted")
if nullNameOkay {
or.Or("name IS ?", nil)
}
where.And("(?)", or)
}q.Space("?", where)
// SELECT name FROM users WHERE age > 21 AND (name = 'trusted' OR name IS NULL)
```It seems to be a matter of taste as to which method appears cleaner.