# sq (Structured Query)
[one-page documentation](https://bokwoon.neocities.org/sq.html)
sq is a type-safe data mapper and query builder for Go. Its concept is simple: you provide a callback function that maps a row to a struct, generics ensure that you get back a slice of structs at the end. Additionally, mentioning a column in the callback function automatically adds it to the SELECT clause so you don't even have to explicitly mention what columns you want to select: the [act of mapping a column is the same as selecting it](#select-example-raw-sql). This eliminates a source of errors where you have specify the columns twice (once in the query itself, once to the call to rows.Scan) and end up missing a column, getting the column order wrong or mistyping a column name.
Notable features:
- Works across SQLite, Postgres, MySQL and SQL Server. [[more info](https://bokwoon.neocities.org/sq.html#set-query-dialect)]
- Each dialect has its own query builder, allowing you to use dialect-specific features. [[more info](https://bokwoon.neocities.org/sq.html#dialect-specific-features)]
- Declarative schema migrations. [[more info](https://bokwoon.neocities.org/sq.html#declarative-schema)]
- Supports arrays, enums, JSON and UUID. [[more info](https://bokwoon.neocities.org/sq.html#arrays-enums-json-uuid)]
- Query logging. [[more info](https://bokwoon.neocities.org/sq.html#logging)]
# Installation
This package only supports Go 1.19 and above.
```shell
$ go get github.com/bokwoon95/sq
$ go install -tags=fts5 github.com/bokwoon95/sqddl@latest
```
# Features
- IN
- [In Slice](https://bokwoon.neocities.org/sq.html#in-slice) - `a IN (1, 2, 3)`
- [In RowValues](https://bokwoon.neocities.org/sq.html#in-rowvalues) - `(a, b, c) IN ((1, 2, 3), (4, 5, 6), (7, 8, 9))`
- [In Subquery](https://bokwoon.neocities.org/sq.html#in-subquery) - `(a, b) IN (SELECT a, b FROM tbl WHERE condition)`
- CASE
- [Predicate Case](https://bokwoon.neocities.org/sq.html#predicate-case) - `CASE WHEN a THEN b WHEN c THEN d ELSE e END`
- [Simple case](https://bokwoon.neocities.org/sq.html#simple-case) - `CASE expr WHEN a THEN b WHEN c THEN d ELSE e END`
- EXISTS
- [Where Exists](https://bokwoon.neocities.org/sq.html#where-exists)
- [Where Not Exists](https://bokwoon.neocities.org/sq.html#where-not-exists)
- [Select Exists](https://bokwoon.neocities.org/sq.html#querybuilder-fetch-exists)
- [Subqueries](https://bokwoon.neocities.org/sq.html#subqueries)
- [WITH (Common Table Expressions)](https://bokwoon.neocities.org/sq.html#common-table-expressions)
- [Aggregate functions](https://bokwoon.neocities.org/sq.html#aggregate-functions)
- [Window functions](https://bokwoon.neocities.org/sq.html#window-functions)
- [UNION, INTERSECT, EXCEPT](https://bokwoon.neocities.org/sq.html#union-intersect-except)
- [INSERT from SELECT](https://bokwoon.neocities.org/sq.html#querybuilder-insert-from-select)
- RETURNING
- [SQLite RETURNING](https://bokwoon.neocities.org/sq.html#sqlite-returning)
- [Postgres RETURNING](https://bokwoon.neocities.org/sq.html#postgres-returning)
- LastInsertId
- [SQLite LastInsertId](https://bokwoon.neocities.org/sq.html#sqlite-last-insert-id)
- [MySQL LastInsertId](https://bokwoon.neocities.org/sq.html#mysql-last-insert-id)
- Insert ignore duplicates
- [SQLite Insert ignore duplicates](https://bokwoon.neocities.org/sq.html#sqlite-insert-ignore-duplicates)
- [Postgres Insert ignore duplicates](https://bokwoon.neocities.org/sq.html#postgres-insert-ignore-duplicates)
- [MySQL Insert ignore duplicates](https://bokwoon.neocities.org/sq.html#mysql-insert-ignore-duplicates)
- [SQL Server Insert ignore duplicates](https://bokwoon.neocities.org/sq.html#sqlserver-insert-ignore-duplicates)
- Upsert
- [SQLite Upsert](https://bokwoon.neocities.org/sq.html#sqlite-upsert)
- [Postgres Upsert](https://bokwoon.neocities.org/sq.html#postgres-upsert)
- [MySQL Upsert](https://bokwoon.neocities.org/sq.html#mysql-upsert)
- [SQL Server Upsert](https://bokwoon.neocities.org/sq.html#sqlserver-upsert)
- Update with Join
- [SQLite Update with Join](https://bokwoon.neocities.org/sq.html#sqlite-update-with-join)
- [Postgres Update with Join](https://bokwoon.neocities.org/sq.html#postgres-update-with-join)
- [MySQL Update with Join](https://bokwoon.neocities.org/sq.html#mysql-update-with-join)
- [SQL Server Update with Join](https://bokwoon.neocities.org/sq.html#sqlserver-update-with-join)
- Delete with Join
- [SQLite Delete with Join](https://bokwoon.neocities.org/sq.html#sqlite-delete-with-join)
- [Postgres Delete with Join](https://bokwoon.neocities.org/sq.html#postgres-delete-with-join)
- [MySQL Delete with Join](https://bokwoon.neocities.org/sq.html#mysql-delete-with-join)
- [SQL Server Delete with Join](https://bokwoon.neocities.org/sq.html#sqlserver-delete-with-join)
- Bulk Update
- [SQLite Bulk Update](https://bokwoon.neocities.org/sq.html#sqlite-bulk-update)
- [Postgres Bulk Update](https://bokwoon.neocities.org/sq.html#postgres-bulk-update)
- [MySQL Bulk Update](https://bokwoon.neocities.org/sq.html#mysql-bulk-update)
- [SQL Server Bulk Update](https://bokwoon.neocities.org/sq.html#sqlserver-bulk-update)
## SELECT example (Raw SQL)
```go
db, err := sql.Open("postgres", "postgres://username:password@localhost:5432/sakila?sslmode=disable")
actors, err := sq.FetchAll(db, sq.
Queryf("SELECT {*} FROM actor AS a WHERE a.actor_id IN ({})",
[]int{1, 2, 3, 4, 5},
).
SetDialect(sq.DialectPostgres),
func(row *sq.Row) Actor {
return Actor{
ActorID: row.Int("a.actor_id"),
FirstName: row.String("a.first_name"),
LastName: row.String("a.last_name"),
LastUpdate: row.Time("a.last_update"),
}
},
)
```
## SELECT example (Query Builder)
To use the query builder, you must first [define your table structs](https://bokwoon.neocities.org/sq.html#table-structs).
```go
type ACTOR struct {
sq.TableStruct
ACTOR_ID sq.NumberField
FIRST_NAME sq.StringField
LAST_NAME sq.StringField
LAST_UPDATE sq.TimeField
}
db, err := sql.Open("postgres", "postgres://username:password@localhost:5432/sakila?sslmode=disable")
a := sq.New[ACTOR]("a")
actors, err := sq.FetchAll(db, sq.
From(a).
Where(a.ACTOR_ID.In([]int{1, 2, 3, 4, 5})).
SetDialect(sq.DialectPostgres),
func(row *sq.Row) Actor {
return Actor{
ActorID: row.IntField(a.ACTOR_ID),
FirstName: row.StringField(a.FIRST_NAME),
LastName: row.StringField(a.LAST_NAME),
LastUpdate: row.TimeField(a.LAST_UPDATE),
}
},
)
```
## INSERT example (Raw SQL)
```go
db, err := sql.Open("postgres", "postgres://username:password@localhost:5432/sakila?sslmode=disable")
_, err := sq.Exec(db, sq.
Queryf("INSERT INTO actor (actor_id, first_name, last_name) VALUES {}", sq.RowValues{
{18, "DAN", "TORN"},
{56, "DAN", "HARRIS"},
{166, "DAN", "STREEP"},
}).
SetDialect(sq.DialectPostgres),
)
```
## INSERT example (Query Builder)
To use the query builder, you must first [define your table structs](https://bokwoon.neocities.org/sq.html#table-structs).
```go
type ACTOR struct {
sq.TableStruct
ACTOR_ID sq.NumberField
FIRST_NAME sq.StringField
LAST_NAME sq.StringField
LAST_UPDATE sq.TimeField
}
db, err := sql.Open("postgres", "postgres://username:password@localhost:5432/sakila?sslmode=disable")
a := sq.New[ACTOR]("a")
_, err := sq.Exec(db, sq.
InsertInto(a).
Columns(a.ACTOR_ID, a.FIRST_NAME, a.LAST_NAME).
Values(18, "DAN", "TORN").
Values(56, "DAN", "HARRIS").
Values(166, "DAN", "STREEP").
SetDialect(sq.DialectPostgres),
)
```
For a more detailed overview, look at the [Quickstart](https://bokwoon.neocities.org/sq.html#quickstart).
## Project Status
sq is done for my use case (hence it may seem inactive, but it's just complete). At this point I'm just waiting for people to ask questions or file feature requests under [discussions](https://github.com/bokwoon95/sq/discussions).
## Contributing
See [START\_HERE.md](https://github.com/bokwoon95/sq/blob/main/START_HERE.md).
================================================
FILE: START_HERE.md
================================================
This document describes how the codebase is organized. It is meant for people who are contributing to the codebase (or are just casually browsing).
Files are written in such a way that **each successive file in the list below only depends on files that come before it**. This self-enforced restriction makes deep architectural changes trivial because you can essentially blow away the entire codebase and rewrite it from scratch file-by-file, complete with working tests every step of the way. Please adhere to this file order when submitting pull requests.
- [**sq.go**](https://github.com/bokwoon95/sq/blob/main/sq.go)
- Core interfaces: SQLWriter, DB, Query, Table, PolicyTable, Window, Field, Predicate, Assignment, Any, Array, Binary, Boolean, Enum, JSON, Number, String, UUID, Time, Enumeration, DialectValuer,
- Data types: Result, TableStruct, ViewStruct.
- Misc utility functions.
- [**fmt.go**](https://github.com/bokwoon95/sq/blob/main/fmt.go)
- Two important string building functions that everything else is built on: [Writef](https://pkg.go.dev/github.com/bokwoon95/sq#Writef) and [WriteValue](https://pkg.go.dev/github.com/bokwoon95/sq#WriteValue).
- Data types: Parameter, BinaryParameter, BooleanParameter, NumberParameter, StringParameter, TimeParameter.
- Utility functions: QuoteIdentifier, EscapeQuote, Sprintf, Sprint.
- [**builtins.go**](https://github.com/bokwoon95/sq/blob/main/builtins.go)
- Builtin data types that are built on top of Writef and WriteValue: Expression (Expr), CustomQuery (Queryf), VariadicPredicate, assignment, RowValue, RowValues, Fields.
- Builtin functions that are built on top of Writef and WriteValue: Eq, Ne, Lt, Le, Gt, Ge, Exists, NotExists, In.
- [**fields.go**](https://github.com/bokwoon95/sq/blob/main/fields.go)
- All of the field types: AnyField, ArrayField, BinaryField, BooleanField, EnumField, JSONField, NumberField, StringField, UUIDField, TimeField.
- Data types: Identifier, Timestamp.
- Functions: [New](https://pkg.go.dev/github.com/bokwoon95/sq#New), ArrayValue, EnumValue, JSONValue, UUIDValue.
- [**cte.go**](https://github.com/bokwoon95/sq/blob/main/cte.go)
- CTE represents an SQL common table expression (CTE).
- UNION, INTERSECT, EXCEPT.
- [**joins.go**](https://github.com/bokwoon95/sq/blob/main/joins.go)
- The various SQL joins.
- [**row_column.go**](https://github.com/bokwoon95/sq/blob/main/row_column.go)
- Row and Column methods.
- [**window.go**](https://github.com/bokwoon95/sq/blob/main/window.go)
- SQL windows and window functions.
- [**select_query.go**](https://github.com/bokwoon95/sq/blob/main/select_query.go)
- SQL SELECT query builder.
- [**insert_query.go**](https://github.com/bokwoon95/sq/blob/main/insert_query.go)
- SQL INSERT query builder.
- [**update_query.go**](https://github.com/bokwoon95/sq/blob/main/update_query.go)
- SQL UPDATE query builder.
- [**delete_query.go**](https://github.com/bokwoon95/sq/blob/main/delete_query.go)
- SQL DELETE query builder.
- [**logger.go**](https://github.com/bokwoon95/sq/blob/main/logger.go)
- sq.Log and sq.VerboseLog.
- [**fetch_exec.go**](https://github.com/bokwoon95/sq/blob/main/fetch_exec.go)
- FetchCursor, FetchOne, FetchAll, Exec.
- CompiledFetch, CompiledExec.
- PreparedFetch, PreparedExec.
- [**misc.go**](https://github.com/bokwoon95/sq/blob/main/misc.go)
- Misc SQL constructs.
- ValueExpression, LiteralValue, DialectExpression, CaseExpression, SimpleCaseExpression.
- SelectValues (`SELECT ... UNION ALL SELECT ... UNION ALL SELECT ...`)
- TableValues (`VALUES (...), (...), (...)`).
- [**integration_test.go**](https://github.com/bokwoon95/sq/blob/main/integration_test.go)
- Tests that interact with a live database i.e. SQLite, Postgres, MySQL and SQL Server.
## Testing
Add tests if you add code.
To run tests, use:
```shell
$ go test . # -failfast -shuffle=on -coverprofile=coverage
```
There are tests that require a live database connection. They will only run if you provide the corresponding database URL in the test flags:
```shell
$ go test . -postgres $POSTGRES_URL -mysql $MYSQL_URL -sqlserver $SQLSERVER_URL # -failfast -shuffle=on -coverprofile=coverage
```
You can consider using the [docker-compose.yml defined in the sqddl repo](https://github.com/bokwoon95/sqddl/blob/main/docker-compose.yml) to spin up Postgres, MySQL and SQL Server databases that are reachable at the following URLs:
```shell
# docker-compose up -d
POSTGRES_URL='postgres://user1:Hunter2!@localhost:5456/sakila?sslmode=disable'
MYSQL_URL='root:Hunter2!@tcp(localhost:3330)/sakila?multiStatements=true&parseTime=true'
MARIADB_URL='root:Hunter2!@tcp(localhost:3340)/sakila?multiStatements=true&parseTime=true'
SQLSERVER_URL='sqlserver://sa:Hunter2!@localhost:1447'
```
## Documentation
Documentation is contained entirely within [sq.md](https://github.com/bokwoon95/sq/blob/main/sq.md) in the project root directory. You can view the output at [https://bokwoon.neocities.org/sq.html](https://bokwoon.neocities.org/sq.html). The documentation is regenerated everytime a new commit is pushed to the main branch, so to change the documentation just change sq.md and submit a pull request.
You can preview the output of sq.md locally by installing [github.com/bokwoon95/mddocs](https://github.com/bokwoon95/mddocs) and running it with sq.md as the argument.
```shell
$ go install github/bokwoon95/mddocs@latest
$ mddocs
Usage:
mddocs project.md # serves project.md on a localhost connection
mddocs project.md project.html # render project.md into project.html
$ mddocs sq.md
serving sq.md at localhost:6060
```
To add a new section and register it in the table of contents, append a `#headerID` to the end of a header (replace `headerID` with the actual header ID). The header ID should only contain unicode letters, digits, hyphen `-` and underscore `_`.
```text
## This is a header.
## This is a header with a headerID. #header-id <-- added to table of contents
```
================================================
FILE: builtins.go
================================================
package sq
import (
"bytes"
"context"
"fmt"
"strings"
)
// Expression is an SQL expression that satisfies the Table, Field, Predicate,
// Binary, Boolean, Number, String and Time interfaces.
type Expression struct {
format string
values []any
alias string
}
var _ interface {
Table
Field
Predicate
Any
Assignment
} = (*Expression)(nil)
// Expr creates a new Expression using Writef syntax.
func Expr(format string, values ...any) Expression {
return Expression{format: format, values: values}
}
// WriteSQL implements the SQLWriter interface.
func (expr Expression) WriteSQL(ctx context.Context, dialect string, buf *bytes.Buffer, args *[]any, params map[string][]int) error {
err := Writef(ctx, dialect, buf, args, params, expr.format, expr.values)
if err != nil {
return err
}
return nil
}
// As returns a new Expression with the given alias.
func (expr Expression) As(alias string) Expression {
expr.alias = alias
return expr
}
// In returns an 'expr IN (value)' Predicate.
func (expr Expression) In(value any) Predicate { return In(expr, value) }
// In returns an 'expr NOT IN (value)' Predicate.
func (expr Expression) NotIn(value any) Predicate { return NotIn(expr, value) }
// Eq returns an 'expr = value' Predicate.
func (expr Expression) Eq(value any) Predicate { return cmp("=", expr, value) }
// Ne returns an 'expr <> value' Predicate.
func (expr Expression) Ne(value any) Predicate { return cmp("<>", expr, value) }
// Lt returns an 'expr < value' Predicate.
func (expr Expression) Lt(value any) Predicate { return cmp("<", expr, value) }
// Le returns an 'expr <= value' Predicate.
func (expr Expression) Le(value any) Predicate { return cmp("<=", expr, value) }
// Gt returns an 'expr > value' Predicate.
func (expr Expression) Gt(value any) Predicate { return cmp(">", expr, value) }
// Ge returns an 'expr >= value' Predicate.
func (expr Expression) Ge(value any) Predicate { return cmp(">=", expr, value) }
// GetAlias returns the alias of the Expression.
func (expr Expression) GetAlias() string { return expr.alias }
// IsTable implements the Table interface.
func (expr Expression) IsTable() {}
// IsField implements the Field interface.
func (expr Expression) IsField() {}
// IsArray implements the Array interface.
func (expr Expression) IsArray() {}
// IsBinary implements the Binary interface.
func (expr Expression) IsBinary() {}
// IsBoolean implements the Boolean interface.
func (expr Expression) IsBoolean() {}
// IsEnum implements the Enum interface.
func (expr Expression) IsEnum() {}
// IsJSON implements the JSON interface.
func (expr Expression) IsJSON() {}
// IsNumber implements the Number interface.
func (expr Expression) IsNumber() {}
// IsString implements the String interface.
func (expr Expression) IsString() {}
// IsTime implements the Time interface.
func (expr Expression) IsTime() {}
// IsUUID implements the UUID interface.
func (expr Expression) IsUUID() {}
func (e Expression) IsAssignment() {}
// CustomQuery represents a user-defined query.
type CustomQuery struct {
Dialect string
Format string
Values []any
fields []Field
}
var _ Query = (*CustomQuery)(nil)
// Queryf creates a new query using Writef syntax.
func Queryf(format string, values ...any) CustomQuery {
return CustomQuery{Format: format, Values: values}
}
// Queryf creates a new SQLite query using Writef syntax.
func (b sqliteQueryBuilder) Queryf(format string, values ...any) CustomQuery {
return CustomQuery{Dialect: DialectSQLite, Format: format, Values: values}
}
// Queryf creates a new Postgres query using Writef syntax.
func (b postgresQueryBuilder) Queryf(format string, values ...any) CustomQuery {
return CustomQuery{Dialect: DialectPostgres, Format: format, Values: values}
}
// Queryf creates a new MySQL query using Writef syntax.
func (b mysqlQueryBuilder) Queryf(format string, values ...any) CustomQuery {
return CustomQuery{Dialect: DialectMySQL, Format: format, Values: values}
}
// Queryf creates a new SQL Server query using Writef syntax.
func (b sqlserverQueryBuilder) Queryf(format string, values ...any) CustomQuery {
return CustomQuery{Dialect: DialectSQLServer, Format: format, Values: values}
}
// Append returns a new CustomQuery with the format string and values slice
// appended to the current CustomQuery.
func (q CustomQuery) Append(format string, values ...any) CustomQuery {
q.Format += " " + format
q.Values = append(q.Values, values...)
return q
}
// WriteSQL implements the SQLWriter interface.
func (q CustomQuery) WriteSQL(ctx context.Context, dialect string, buf *bytes.Buffer, args *[]any, params map[string][]int) error {
var err error
format := q.Format
splitAt := -1
for i := strings.IndexByte(format, '{'); i >= 0; i = strings.IndexByte(format, '{') {
if i+2 <= len(format) && format[i:i+2] == "{{" {
format = format[i+2:]
continue
}
if i+3 <= len(format) && format[i:i+3] == "{*}" {
splitAt = len(q.Format) - len(format[i:])
break
}
format = format[i+1:]
}
if splitAt < 0 {
return Writef(ctx, dialect, buf, args, params, q.Format, q.Values)
}
runningValuesIndex := 0
ordinalIndices := make(map[int]int)
err = writef(ctx, dialect, buf, args, params, q.Format[:splitAt], q.Values, &runningValuesIndex, ordinalIndices)
if err != nil {
return err
}
err = writeFields(ctx, dialect, buf, args, params, q.fields, true)
if err != nil {
return err
}
err = writef(ctx, dialect, buf, args, params, q.Format[splitAt+3:], q.Values, &runningValuesIndex, ordinalIndices)
if err != nil {
return err
}
return nil
}
// SetFetchableFields sets the fetchable fields of the query.
func (q CustomQuery) SetFetchableFields(fields []Field) (query Query, ok bool) {
format := q.Format
for i := strings.IndexByte(format, '{'); i >= 0; i = strings.IndexByte(format, '{') {
if i+2 <= len(format) && format[i:i+2] == "{{" {
format = format[i+2:]
continue
}
if i+3 <= len(format) && format[i:i+3] == "{*}" {
q.fields = fields
return q, true
}
format = format[i+1:]
}
return q, false
}
// GetFetchableFields gets the fetchable fields of the query.
func (q CustomQuery) GetFetchableFields() []Field {
return q.fields
}
// GetDialect gets the dialect of the query.
func (q CustomQuery) GetDialect() string { return q.Dialect }
// SetDialect sets the dialect of the query.
func (q CustomQuery) SetDialect(dialect string) CustomQuery {
q.Dialect = dialect
return q
}
// VariadicPredicate represents the 'x AND y AND z...' or 'x OR Y OR z...' SQL
// construct.
type VariadicPredicate struct {
// Toplevel indicates if the VariadicPredicate can skip writing the
// (surrounding brackets).
Toplevel bool
alias string
// If IsDisjunction is true, the Predicates are joined using OR. If false,
// the Predicates are joined using AND. The default is AND.
IsDisjunction bool
// Predicates holds the predicates inside the VariadicPredicate
Predicates []Predicate
}
var _ Predicate = (*VariadicPredicate)(nil)
// And joins the predicates together with the AND operator.
func And(predicates ...Predicate) VariadicPredicate {
return VariadicPredicate{IsDisjunction: false, Predicates: predicates}
}
// Or joins the predicates together with the OR operator.
func Or(predicates ...Predicate) VariadicPredicate {
return VariadicPredicate{IsDisjunction: true, Predicates: predicates}
}
// WriteSQL implements the SQLWriter interface.
func (p VariadicPredicate) WriteSQL(ctx context.Context, dialect string, buf *bytes.Buffer, args *[]any, params map[string][]int) error {
var err error
if len(p.Predicates) == 0 {
return fmt.Errorf("VariadicPredicate empty")
}
if len(p.Predicates) == 1 {
switch p1 := p.Predicates[0].(type) {
case nil:
return fmt.Errorf("predicate #1 is nil")
case VariadicPredicate:
p1.Toplevel = p.Toplevel
err = p1.WriteSQL(ctx, dialect, buf, args, params)
if err != nil {
return err
}
default:
err = p.Predicates[0].WriteSQL(ctx, dialect, buf, args, params)
if err != nil {
return err
}
}
return nil
}
if !p.Toplevel {
buf.WriteString("(")
}
for i, predicate := range p.Predicates {
if i > 0 {
if p.IsDisjunction {
buf.WriteString(" OR ")
} else {
buf.WriteString(" AND ")
}
}
switch predicate := predicate.(type) {
case nil:
return fmt.Errorf("predicate #%d is nil", i+1)
case VariadicPredicate:
predicate.Toplevel = false
err = predicate.WriteSQL(ctx, dialect, buf, args, params)
if err != nil {
return fmt.Errorf("predicate #%d: %w", i+1, err)
}
default:
err = predicate.WriteSQL(ctx, dialect, buf, args, params)
if err != nil {
return fmt.Errorf("predicate #%d: %w", i+1, err)
}
}
}
if !p.Toplevel {
buf.WriteString(")")
}
return nil
}
// As returns a new VariadicPredicate with the given alias.
func (p VariadicPredicate) As(alias string) VariadicPredicate {
p.alias = alias
return p
}
// GetAlias returns the alias of the VariadicPredicate.
func (p VariadicPredicate) GetAlias() string { return p.alias }
// IsField implements the Field interface.
func (p VariadicPredicate) IsField() {}
// IsBooleanType implements the Predicate interface.
func (p VariadicPredicate) IsBoolean() {}
// assignment represents assigning a value to a Field.
type assignment struct {
field Field
value any
}
var _ Assignment = (*assignment)(nil)
// Set creates a new Assignment assigning the value to a field.
func Set(field Field, value any) Assignment {
return assignment{field: field, value: value}
}
// Setf creates a new Assignment assigning a custom expression to a Field.
func Setf(field Field, format string, values ...any) Assignment {
return assignment{field: field, value: Expr(format, values...)}
}
// WriteSQL implements the SQLWriter interface.
func (a assignment) WriteSQL(ctx context.Context, dialect string, buf *bytes.Buffer, args *[]any, params map[string][]int) error {
if a.field == nil {
return fmt.Errorf("field is nil")
}
var err error
if dialect == DialectMySQL {
err = a.field.WriteSQL(ctx, dialect, buf, args, params)
if err != nil {
return err
}
} else {
err = withPrefix(a.field, "").WriteSQL(ctx, dialect, buf, args, params)
if err != nil {
return err
}
}
buf.WriteString(" = ")
_, isQuery := a.value.(Query)
if isQuery {
buf.WriteString("(")
}
err = WriteValue(ctx, dialect, buf, args, params, a.value)
if err != nil {
return err
}
if isQuery {
buf.WriteString(")")
}
return nil
}
// IsAssignment implements the Assignment interface.
func (a assignment) IsAssignment() {}
// Assignments represents a list of Assignments e.g. x = 1, y = 2, z = 3.
type Assignments []Assignment
// WriteSQL implements the SQLWriter interface.
func (as Assignments) WriteSQL(ctx context.Context, dialect string, buf *bytes.Buffer, args *[]any, params map[string][]int) error {
var err error
for i, assignment := range as {
if assignment == nil {
return fmt.Errorf("assignment #%d is nil", i+1)
}
if i > 0 {
buf.WriteString(", ")
}
err = assignment.WriteSQL(ctx, dialect, buf, args, params)
if err != nil {
return fmt.Errorf("assignment #%d: %w", i+1, err)
}
}
return nil
}
// RowValue represents an SQL row value expression e.g. (x, y, z).
type RowValue []any
// WriteSQL implements the SQLWriter interface.
func (r RowValue) WriteSQL(ctx context.Context, dialect string, buf *bytes.Buffer, args *[]any, params map[string][]int) error {
buf.WriteString("(")
var err error
for i, value := range r {
if i > 0 {
buf.WriteString(", ")
}
_, isQuery := value.(Query)
if isQuery {
buf.WriteString("(")
}
err = WriteValue(ctx, dialect, buf, args, params, value)
if err != nil {
return fmt.Errorf("rowvalue #%d: %w", i+1, err)
}
if isQuery {
buf.WriteString(")")
}
}
buf.WriteString(")")
return nil
}
// In returns an 'rowvalue IN (value)' Predicate.
func (r RowValue) In(v any) Predicate { return In(r, v) }
// NotIn returns an 'rowvalue NOT IN (value)' Predicate.
func (r RowValue) NotIn(v any) Predicate { return NotIn(r, v) }
// Eq returns an 'rowvalue = value' Predicate.
func (r RowValue) Eq(v any) Predicate { return cmp("=", r, v) }
// RowValues represents a list of RowValues e.g. (x, y, z), (a, b, c).
type RowValues []RowValue
// WriteSQL implements the SQLWriter interface.
func (rs RowValues) WriteSQL(ctx context.Context, dialect string, buf *bytes.Buffer, args *[]any, params map[string][]int) error {
var err error
for i, r := range rs {
if i > 0 {
buf.WriteString(", ")
}
err = r.WriteSQL(ctx, dialect, buf, args, params)
if err != nil {
return fmt.Errorf("rowvalues #%d: %w", i+1, err)
}
}
return nil
}
// Fields represents a list of Fields e.g. tbl.field1, tbl.field2, tbl.field3.
type Fields []Field
// WriteSQL implements the SQLWriter interface.
func (fs Fields) WriteSQL(ctx context.Context, dialect string, buf *bytes.Buffer, args *[]any, params map[string][]int) error {
var err error
for i, field := range fs {
if field == nil {
return fmt.Errorf("field #%d is nil", i+1)
}
if i > 0 {
buf.WriteString(", ")
}
_, isQuery := field.(Query)
if isQuery {
buf.WriteString("(")
}
err = field.WriteSQL(ctx, dialect, buf, args, params)
if err != nil {
return fmt.Errorf("field #%d: %w", i+1, err)
}
if isQuery {
buf.WriteString(")")
}
}
return nil
}
type (
sqliteQueryBuilder struct{ ctes []CTE }
postgresQueryBuilder struct{ ctes []CTE }
mysqlQueryBuilder struct{ ctes []CTE }
sqlserverQueryBuilder struct{ ctes []CTE }
)
// Dialect-specific query builder variables.
var (
SQLite sqliteQueryBuilder
Postgres postgresQueryBuilder
MySQL mysqlQueryBuilder
SQLServer sqlserverQueryBuilder
)
// With sets the CTEs in the SQLiteQueryBuilder.
func (b sqliteQueryBuilder) With(ctes ...CTE) sqliteQueryBuilder {
b.ctes = ctes
return b
}
// With sets the CTEs in the PostgresQueryBuilder.
func (b postgresQueryBuilder) With(ctes ...CTE) postgresQueryBuilder {
b.ctes = ctes
return b
}
// With sets the CTEs in the MySQLQueryBuilder.
func (b mysqlQueryBuilder) With(ctes ...CTE) mysqlQueryBuilder {
b.ctes = ctes
return b
}
// With sets the CTEs in the SQLServerQueryBuilder.
func (b sqlserverQueryBuilder) With(ctes ...CTE) sqlserverQueryBuilder {
b.ctes = ctes
return b
}
// ToSQL converts an SQLWriter into a query string and args slice.
//
// The params map is used to hold the mappings between named parameters in the
// query to the corresponding index in the args slice and is used for rebinding
// args by their parameter name. If you don't need to track this, you can pass
// in a nil map.
func ToSQL(dialect string, w SQLWriter, params map[string][]int) (query string, args []any, err error) {
return ToSQLContext(context.Background(), dialect, w, params)
}
// ToSQLContext is like ToSQL but additionally requires a context.Context.
func ToSQLContext(ctx context.Context, dialect string, w SQLWriter, params map[string][]int) (query string, args []any, err error) {
if w == nil {
return "", nil, fmt.Errorf("SQLWriter is nil")
}
if dialect == "" {
if q, ok := w.(Query); ok {
dialect = q.GetDialect()
}
}
buf := bufpool.Get().(*bytes.Buffer)
buf.Reset()
defer bufpool.Put(buf)
err = w.WriteSQL(ctx, dialect, buf, &args, params)
query = buf.String()
if err != nil {
return query, args, err
}
return query, args, nil
}
// Eq returns an 'x = y' Predicate.
func Eq(x, y any) Predicate { return cmp("=", x, y) }
// Ne returns an 'x <> y' Predicate.
func Ne(x, y any) Predicate { return cmp("<>", x, y) }
// Lt returns an 'x < y' Predicate.
func Lt(x, y any) Predicate { return cmp("<", x, y) }
// Le returns an 'x <= y' Predicate.
func Le(x, y any) Predicate { return cmp("<=", x, y) }
// Gt returns an 'x > y' Predicate.
func Gt(x, y any) Predicate { return cmp(">", x, y) }
// Ge returns an 'x >= y' Predicate.
func Ge(x, y any) Predicate { return cmp(">=", x, y) }
// Exists returns an 'EXISTS (query)' Predicate.
func Exists(query Query) Predicate { return Expr("EXISTS ({})", query) }
// NotExists returns a 'NOT EXISTS (query)' Predicate.
func NotExists(query Query) Predicate { return Expr("NOT EXISTS ({})", query) }
// In returns an 'x IN (y)' Predicate.
func In(x, y any) Predicate {
_, isQueryA := x.(Query)
_, isRowValueB := y.(RowValue)
if !isQueryA && !isRowValueB {
return Expr("{} IN ({})", x, y)
} else if !isQueryA && isRowValueB {
return Expr("{} IN {}", x, y)
} else if isQueryA && !isRowValueB {
return Expr("({}) IN ({})", x, y)
} else {
return Expr("({}) IN {}", x, y)
}
}
// NotIn returns an 'x NOT IN (y)' Predicate.
func NotIn(x, y any) Predicate {
_, isQueryA := x.(Query)
_, isRowValueB := y.(RowValue)
if !isQueryA && !isRowValueB {
return Expr("{} NOT IN ({})", x, y)
} else if !isQueryA && isRowValueB {
return Expr("{} NOT IN {}", x, y)
} else if isQueryA && !isRowValueB {
return Expr("({}) NOT IN ({})", x, y)
} else {
return Expr("({}) NOT IN {}", x, y)
}
}
// cmp returns an 'x | Parameter | Description |
|---|---|
sql.Named(name string, value any) |
database/sql's named parameter type |
sq.Param(name string, value any) |
same as sql.Named, but satisfies the Field interface |
sq.BinaryParam(name string, b []byte) |
same as sql.Named, but satisfies the Binary interface |
sq.BooleanParam(name string, b bool) |
same as sql.Named, but satisfies the Boolean interface |
sq.IntParam(name string, num int) |
same as sql.Named, but satisfies the Number interface |
sq.Int64Param(name string, num int64) |
same as sql.Named, but satisfies the Number interface |
sq.Float64Param(name string, num float64) |
same as sql.Named, but satisfies the Number interface |
sq.StringParam(name string, s string) |
same as sql.Named, but satisfies the String interface |
sq.TimeParam(name string, t time.Time) |
same as sql.Named, but satisfies the Time interface |