445 lines
12 KiB
Go
445 lines
12 KiB
Go
// Copyright 2016 Qiang Xue. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package dbx
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
)
|
|
|
|
// BuildHookFunc defines a callback function that is executed on Query creation.
|
|
type BuildHookFunc func(q *Query)
|
|
|
|
// SelectQuery represents a DB-agnostic SELECT query.
|
|
// It can be built into a DB-specific query by calling the Build() method.
|
|
type SelectQuery struct {
|
|
// FieldMapper maps struct field names to DB column names.
|
|
FieldMapper FieldMapFunc
|
|
// TableMapper maps structs to DB table names.
|
|
TableMapper TableMapFunc
|
|
|
|
builder Builder
|
|
ctx context.Context
|
|
buildHook BuildHookFunc
|
|
|
|
preFragment string
|
|
postFragment string
|
|
selects []string
|
|
distinct bool
|
|
selectOption string
|
|
from []string
|
|
where Expression
|
|
join []JoinInfo
|
|
orderBy []string
|
|
groupBy []string
|
|
having Expression
|
|
union []UnionInfo
|
|
limit int64
|
|
offset int64
|
|
params Params
|
|
}
|
|
|
|
// JoinInfo contains the specification for a JOIN clause.
|
|
type JoinInfo struct {
|
|
Join string
|
|
Table string
|
|
On Expression
|
|
}
|
|
|
|
// UnionInfo contains the specification for a UNION clause.
|
|
type UnionInfo struct {
|
|
All bool
|
|
Query *Query
|
|
}
|
|
|
|
// NewSelectQuery creates a new SelectQuery instance.
|
|
func NewSelectQuery(builder Builder, db *DB) *SelectQuery {
|
|
return &SelectQuery{
|
|
builder: builder,
|
|
selects: []string{},
|
|
from: []string{},
|
|
join: []JoinInfo{},
|
|
orderBy: []string{},
|
|
groupBy: []string{},
|
|
union: []UnionInfo{},
|
|
limit: -1,
|
|
params: Params{},
|
|
ctx: db.ctx,
|
|
FieldMapper: db.FieldMapper,
|
|
TableMapper: db.TableMapper,
|
|
}
|
|
}
|
|
|
|
// WithBuildHook runs the provided hook function with the query created on Build().
|
|
func (q *SelectQuery) WithBuildHook(fn BuildHookFunc) *SelectQuery {
|
|
q.buildHook = fn
|
|
return q
|
|
}
|
|
|
|
// Context returns the context associated with the query.
|
|
func (q *SelectQuery) Context() context.Context {
|
|
return q.ctx
|
|
}
|
|
|
|
// WithContext associates a context with the query.
|
|
func (q *SelectQuery) WithContext(ctx context.Context) *SelectQuery {
|
|
q.ctx = ctx
|
|
return q
|
|
}
|
|
|
|
// PreFragment sets SQL fragment that should be prepended before the select query (e.g. WITH clause).
|
|
func (s *SelectQuery) PreFragment(fragment string) *SelectQuery {
|
|
s.preFragment = fragment
|
|
return s
|
|
}
|
|
|
|
// PostFragment sets SQL fragment that should be appended at the end of the select query.
|
|
func (s *SelectQuery) PostFragment(fragment string) *SelectQuery {
|
|
s.postFragment = fragment
|
|
return s
|
|
}
|
|
|
|
// Select specifies the columns to be selected.
|
|
// Column names will be automatically quoted.
|
|
func (s *SelectQuery) Select(cols ...string) *SelectQuery {
|
|
s.selects = cols
|
|
return s
|
|
}
|
|
|
|
// AndSelect adds additional columns to be selected.
|
|
// Column names will be automatically quoted.
|
|
func (s *SelectQuery) AndSelect(cols ...string) *SelectQuery {
|
|
s.selects = append(s.selects, cols...)
|
|
return s
|
|
}
|
|
|
|
// Distinct specifies whether to select columns distinctively.
|
|
// By default, distinct is false.
|
|
func (s *SelectQuery) Distinct(v bool) *SelectQuery {
|
|
s.distinct = v
|
|
return s
|
|
}
|
|
|
|
// SelectOption specifies additional option that should be append to "SELECT".
|
|
func (s *SelectQuery) SelectOption(option string) *SelectQuery {
|
|
s.selectOption = option
|
|
return s
|
|
}
|
|
|
|
// From specifies which tables to select from.
|
|
// Table names will be automatically quoted.
|
|
func (s *SelectQuery) From(tables ...string) *SelectQuery {
|
|
s.from = tables
|
|
return s
|
|
}
|
|
|
|
// Where specifies the WHERE condition.
|
|
func (s *SelectQuery) Where(e Expression) *SelectQuery {
|
|
s.where = e
|
|
return s
|
|
}
|
|
|
|
// AndWhere concatenates a new WHERE condition with the existing one (if any) using "AND".
|
|
func (s *SelectQuery) AndWhere(e Expression) *SelectQuery {
|
|
s.where = And(s.where, e)
|
|
return s
|
|
}
|
|
|
|
// OrWhere concatenates a new WHERE condition with the existing one (if any) using "OR".
|
|
func (s *SelectQuery) OrWhere(e Expression) *SelectQuery {
|
|
s.where = Or(s.where, e)
|
|
return s
|
|
}
|
|
|
|
// Join specifies a JOIN clause.
|
|
// The "typ" parameter specifies the JOIN type (e.g. "INNER JOIN", "LEFT JOIN").
|
|
func (s *SelectQuery) Join(typ string, table string, on Expression) *SelectQuery {
|
|
s.join = append(s.join, JoinInfo{typ, table, on})
|
|
return s
|
|
}
|
|
|
|
// InnerJoin specifies an INNER JOIN clause.
|
|
// This is a shortcut method for Join.
|
|
func (s *SelectQuery) InnerJoin(table string, on Expression) *SelectQuery {
|
|
return s.Join("INNER JOIN", table, on)
|
|
}
|
|
|
|
// LeftJoin specifies a LEFT JOIN clause.
|
|
// This is a shortcut method for Join.
|
|
func (s *SelectQuery) LeftJoin(table string, on Expression) *SelectQuery {
|
|
return s.Join("LEFT JOIN", table, on)
|
|
}
|
|
|
|
// RightJoin specifies a RIGHT JOIN clause.
|
|
// This is a shortcut method for Join.
|
|
func (s *SelectQuery) RightJoin(table string, on Expression) *SelectQuery {
|
|
return s.Join("RIGHT JOIN", table, on)
|
|
}
|
|
|
|
// OrderBy specifies the ORDER BY clause.
|
|
// Column names will be properly quoted. A column name can contain "ASC" or "DESC" to indicate its ordering direction.
|
|
func (s *SelectQuery) OrderBy(cols ...string) *SelectQuery {
|
|
s.orderBy = cols
|
|
return s
|
|
}
|
|
|
|
// AndOrderBy appends additional columns to the existing ORDER BY clause.
|
|
// Column names will be properly quoted. A column name can contain "ASC" or "DESC" to indicate its ordering direction.
|
|
func (s *SelectQuery) AndOrderBy(cols ...string) *SelectQuery {
|
|
s.orderBy = append(s.orderBy, cols...)
|
|
return s
|
|
}
|
|
|
|
// GroupBy specifies the GROUP BY clause.
|
|
// Column names will be properly quoted.
|
|
func (s *SelectQuery) GroupBy(cols ...string) *SelectQuery {
|
|
s.groupBy = cols
|
|
return s
|
|
}
|
|
|
|
// AndGroupBy appends additional columns to the existing GROUP BY clause.
|
|
// Column names will be properly quoted.
|
|
func (s *SelectQuery) AndGroupBy(cols ...string) *SelectQuery {
|
|
s.groupBy = append(s.groupBy, cols...)
|
|
return s
|
|
}
|
|
|
|
// Having specifies the HAVING clause.
|
|
func (s *SelectQuery) Having(e Expression) *SelectQuery {
|
|
s.having = e
|
|
return s
|
|
}
|
|
|
|
// AndHaving concatenates a new HAVING condition with the existing one (if any) using "AND".
|
|
func (s *SelectQuery) AndHaving(e Expression) *SelectQuery {
|
|
s.having = And(s.having, e)
|
|
return s
|
|
}
|
|
|
|
// OrHaving concatenates a new HAVING condition with the existing one (if any) using "OR".
|
|
func (s *SelectQuery) OrHaving(e Expression) *SelectQuery {
|
|
s.having = Or(s.having, e)
|
|
return s
|
|
}
|
|
|
|
// Union specifies a UNION clause.
|
|
func (s *SelectQuery) Union(q *Query) *SelectQuery {
|
|
s.union = append(s.union, UnionInfo{false, q})
|
|
return s
|
|
}
|
|
|
|
// UnionAll specifies a UNION ALL clause.
|
|
func (s *SelectQuery) UnionAll(q *Query) *SelectQuery {
|
|
s.union = append(s.union, UnionInfo{true, q})
|
|
return s
|
|
}
|
|
|
|
// Limit specifies the LIMIT clause.
|
|
// A negative limit means no limit.
|
|
func (s *SelectQuery) Limit(limit int64) *SelectQuery {
|
|
s.limit = limit
|
|
return s
|
|
}
|
|
|
|
// Offset specifies the OFFSET clause.
|
|
// A negative offset means no offset.
|
|
func (s *SelectQuery) Offset(offset int64) *SelectQuery {
|
|
s.offset = offset
|
|
return s
|
|
}
|
|
|
|
// Bind specifies the parameter values to be bound to the query.
|
|
func (s *SelectQuery) Bind(params Params) *SelectQuery {
|
|
s.params = params
|
|
return s
|
|
}
|
|
|
|
// AndBind appends additional parameters to be bound to the query.
|
|
func (s *SelectQuery) AndBind(params Params) *SelectQuery {
|
|
if len(s.params) == 0 {
|
|
s.params = params
|
|
} else {
|
|
for k, v := range params {
|
|
s.params[k] = v
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Build builds the SELECT query and returns an executable Query object.
|
|
func (s *SelectQuery) Build() *Query {
|
|
params := Params{}
|
|
for k, v := range s.params {
|
|
params[k] = v
|
|
}
|
|
|
|
qb := s.builder.QueryBuilder()
|
|
|
|
clauses := []string{
|
|
s.preFragment,
|
|
qb.BuildSelect(s.selects, s.distinct, s.selectOption),
|
|
qb.BuildFrom(s.from),
|
|
qb.BuildJoin(s.join, params),
|
|
qb.BuildWhere(s.where, params),
|
|
qb.BuildGroupBy(s.groupBy),
|
|
qb.BuildHaving(s.having, params),
|
|
}
|
|
|
|
sql := ""
|
|
for _, clause := range clauses {
|
|
if clause != "" {
|
|
if sql == "" {
|
|
sql = clause
|
|
} else {
|
|
sql += " " + clause
|
|
}
|
|
}
|
|
}
|
|
|
|
sql = qb.BuildOrderByAndLimit(sql, s.orderBy, s.limit, s.offset)
|
|
|
|
if s.postFragment != "" {
|
|
sql += " " + s.postFragment
|
|
}
|
|
|
|
if union := qb.BuildUnion(s.union, params); union != "" {
|
|
sql = fmt.Sprintf("(%v) %v", sql, union)
|
|
}
|
|
|
|
query := s.builder.NewQuery(sql).Bind(params).WithContext(s.ctx)
|
|
|
|
if s.buildHook != nil {
|
|
s.buildHook(query)
|
|
}
|
|
|
|
return query
|
|
}
|
|
|
|
// One executes the SELECT query and populates the first row of the result into the specified variable.
|
|
//
|
|
// If the query does not specify a "from" clause, the method will try to infer the name of the table
|
|
// to be selected from by calling getTableName() which will return either the variable type name
|
|
// or the TableName() method if the variable implements the TableModel interface.
|
|
//
|
|
// Note that when the query has no rows in the result set, an sql.ErrNoRows will be returned.
|
|
func (s *SelectQuery) One(a interface{}) error {
|
|
if len(s.from) == 0 {
|
|
if tableName := s.TableMapper(a); tableName != "" {
|
|
s.from = []string{tableName}
|
|
}
|
|
}
|
|
|
|
return s.Build().One(a)
|
|
}
|
|
|
|
// Model selects the row with the specified primary key and populates the model with the row data.
|
|
//
|
|
// The model variable should be a pointer to a struct. If the query does not specify a "from" clause,
|
|
// it will use the model struct to determine which table to select data from. It will also use the model
|
|
// to infer the name of the primary key column. Only simple primary key is supported. For composite primary keys,
|
|
// please use Where() to specify the filtering condition.
|
|
func (s *SelectQuery) Model(pk, model interface{}) error {
|
|
t := reflect.TypeOf(model)
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
if t.Kind() != reflect.Struct {
|
|
return VarTypeError("must be a pointer to a struct")
|
|
}
|
|
|
|
si := getStructInfo(t, s.FieldMapper)
|
|
if len(si.pkNames) == 1 {
|
|
return s.AndWhere(HashExp{si.nameMap[si.pkNames[0]].dbName: pk}).One(model)
|
|
}
|
|
if len(si.pkNames) == 0 {
|
|
return MissingPKError
|
|
}
|
|
|
|
return CompositePKError
|
|
}
|
|
|
|
// All executes the SELECT query and populates all rows of the result into a slice.
|
|
//
|
|
// Note that the slice must be passed in as a pointer.
|
|
//
|
|
// If the query does not specify a "from" clause, the method will try to infer the name of the table
|
|
// to be selected from by calling getTableName() which will return either the type name of the slice elements
|
|
// or the TableName() method if the slice element implements the TableModel interface.
|
|
func (s *SelectQuery) All(slice interface{}) error {
|
|
if len(s.from) == 0 {
|
|
if tableName := s.TableMapper(slice); tableName != "" {
|
|
s.from = []string{tableName}
|
|
}
|
|
}
|
|
|
|
return s.Build().All(slice)
|
|
}
|
|
|
|
// Rows builds and executes the SELECT query and returns a Rows object for data retrieval purpose.
|
|
// This is a shortcut to SelectQuery.Build().Rows()
|
|
func (s *SelectQuery) Rows() (*Rows, error) {
|
|
return s.Build().Rows()
|
|
}
|
|
|
|
// Row builds and executes the SELECT query and populates the first row of the result into the specified variables.
|
|
// This is a shortcut to SelectQuery.Build().Row()
|
|
func (s *SelectQuery) Row(a ...interface{}) error {
|
|
return s.Build().Row(a...)
|
|
}
|
|
|
|
// Column builds and executes the SELECT statement and populates the first column of the result into a slice.
|
|
// Note that the parameter must be a pointer to a slice.
|
|
// This is a shortcut to SelectQuery.Build().Column()
|
|
func (s *SelectQuery) Column(a interface{}) error {
|
|
return s.Build().Column(a)
|
|
}
|
|
|
|
// QueryInfo represents a debug/info struct with exported SelectQuery fields.
|
|
type QueryInfo struct {
|
|
PreFragment string
|
|
PostFragment string
|
|
Builder Builder
|
|
Selects []string
|
|
Distinct bool
|
|
SelectOption string
|
|
From []string
|
|
Where Expression
|
|
Join []JoinInfo
|
|
OrderBy []string
|
|
GroupBy []string
|
|
Having Expression
|
|
Union []UnionInfo
|
|
Limit int64
|
|
Offset int64
|
|
Params Params
|
|
Context context.Context
|
|
BuildHook BuildHookFunc
|
|
}
|
|
|
|
// Info exports common SelectQuery fields allowing to inspect the
|
|
// current select query options.
|
|
func (s *SelectQuery) Info() *QueryInfo {
|
|
return &QueryInfo{
|
|
Builder: s.builder,
|
|
PreFragment: s.preFragment,
|
|
PostFragment: s.postFragment,
|
|
Selects: s.selects,
|
|
Distinct: s.distinct,
|
|
SelectOption: s.selectOption,
|
|
From: s.from,
|
|
Where: s.where,
|
|
Join: s.join,
|
|
OrderBy: s.orderBy,
|
|
GroupBy: s.groupBy,
|
|
Having: s.having,
|
|
Union: s.union,
|
|
Limit: s.limit,
|
|
Offset: s.offset,
|
|
Params: s.params,
|
|
Context: s.ctx,
|
|
BuildHook: s.buildHook,
|
|
}
|
|
}
|