mirror of
https://github.com/VinGarcia/ksql.git
synced 2025-05-31 11:42:25 +00:00
As known, a map can't be accessed with read/write concurrently on multiple goroutines. This just replaces all uses of global maps for caches with sync.Map, which is safe to be used concurrently.
227 lines
5.5 KiB
Go
227 lines
5.5 KiB
Go
package kbuilder
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/vingarcia/ksql"
|
|
"github.com/vingarcia/ksql/internal/structs"
|
|
)
|
|
|
|
// Query is is the struct template for building SELECT queries.
|
|
type Query struct {
|
|
// Select expects either a struct using the `ksql` tags
|
|
// or a string listing the column names using SQL syntax,
|
|
// e.g.: `id, username, address`
|
|
Select interface{}
|
|
|
|
// From expects the FROM clause from an SQL query, e.g. `users JOIN posts USING(post_id)`
|
|
From string
|
|
|
|
// Where expects a list of WhereQuery instances built
|
|
// by the public Where() function.
|
|
Where WhereQueries
|
|
|
|
Limit int
|
|
Offset int
|
|
OrderBy OrderByQuery
|
|
}
|
|
|
|
// Build is a utility function for finding the dialect based on the driver and
|
|
// then calling BuildQuery(dialect)
|
|
func (q Query) Build(driver string) (sqlQuery string, params []interface{}, _ error) {
|
|
dialect, err := ksql.GetDriverDialect(driver)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
return q.BuildQuery(dialect)
|
|
}
|
|
|
|
// BuildQuery implements the queryBuilder interface
|
|
func (q Query) BuildQuery(dialect ksql.Dialect) (sqlQuery string, params []interface{}, _ error) {
|
|
var b strings.Builder
|
|
|
|
switch v := q.Select.(type) {
|
|
case string:
|
|
b.WriteString("SELECT " + v)
|
|
default:
|
|
selectQuery, err := buildSelectQuery(v, dialect)
|
|
if err != nil {
|
|
return "", nil, errors.Wrap(err, "error reading the Select field")
|
|
}
|
|
b.WriteString("SELECT " + selectQuery)
|
|
}
|
|
|
|
b.WriteString(" FROM " + q.From)
|
|
|
|
if len(q.Where) > 0 {
|
|
var whereQuery string
|
|
whereQuery, params = q.Where.build(dialect)
|
|
b.WriteString(" WHERE " + whereQuery)
|
|
}
|
|
|
|
if strings.TrimSpace(q.From) == "" {
|
|
return "", nil, fmt.Errorf("the From field is mandatory for every query")
|
|
}
|
|
|
|
if q.OrderBy.fields != "" {
|
|
b.WriteString(" ORDER BY " + q.OrderBy.fields)
|
|
if q.OrderBy.desc {
|
|
b.WriteString(" DESC")
|
|
}
|
|
}
|
|
|
|
if q.Limit > 0 {
|
|
b.WriteString(" LIMIT " + strconv.Itoa(q.Limit))
|
|
}
|
|
|
|
if q.Offset > 0 {
|
|
b.WriteString(" OFFSET " + strconv.Itoa(q.Offset))
|
|
}
|
|
|
|
return b.String(), params, nil
|
|
}
|
|
|
|
// WhereQuery represents a single condition in a WHERE expression.
|
|
type WhereQuery struct {
|
|
// Accepts any SQL boolean expression
|
|
// This expression may optionally contain
|
|
// string formatting directives %s and only %s.
|
|
//
|
|
// For each of these directives we expect a new param
|
|
// on the params list below.
|
|
//
|
|
// In the resulting query each %s will be properly replaced
|
|
// by placeholders according to the database driver, e.g. `$1`
|
|
// for postgres or `?` for sqlite3.
|
|
cond string
|
|
params []interface{}
|
|
}
|
|
|
|
// WhereQueries is the helper for creating complex WHERE queries
|
|
// in a dynamic way.
|
|
type WhereQueries []WhereQuery
|
|
|
|
func (w WhereQueries) build(dialect ksql.Dialect) (query string, params []interface{}) {
|
|
var conds []string
|
|
for _, whereQuery := range w {
|
|
var placeholders []interface{}
|
|
for i := range whereQuery.params {
|
|
placeholders = append(placeholders, dialect.Placeholder(len(params)+i))
|
|
}
|
|
|
|
conds = append(conds, fmt.Sprintf(whereQuery.cond, placeholders...))
|
|
params = append(params, whereQuery.params...)
|
|
}
|
|
|
|
return strings.Join(conds, " AND "), params
|
|
}
|
|
|
|
// Where adds a new boolean condition to an existing
|
|
// WhereQueries helper.
|
|
func (w WhereQueries) Where(cond string, params ...interface{}) WhereQueries {
|
|
return append(w, WhereQuery{
|
|
cond: cond,
|
|
params: params,
|
|
})
|
|
}
|
|
|
|
// WhereIf conditionally adds a new boolean expression to the WhereQueries helper.
|
|
func (w WhereQueries) WhereIf(cond string, param interface{}) WhereQueries {
|
|
if param == nil || reflect.ValueOf(param).IsNil() {
|
|
return w
|
|
}
|
|
|
|
return append(w, WhereQuery{
|
|
cond: cond,
|
|
params: []interface{}{param},
|
|
})
|
|
}
|
|
|
|
// Where adds a new boolean condition to an existing
|
|
// WhereQueries helper.
|
|
func Where(cond string, params ...interface{}) WhereQueries {
|
|
return WhereQueries{{
|
|
cond: cond,
|
|
params: params,
|
|
}}
|
|
}
|
|
|
|
// WhereIf conditionally adds a new boolean expression to the WhereQueries helper
|
|
func WhereIf(cond string, param interface{}) WhereQueries {
|
|
if param == nil || reflect.ValueOf(param).IsNil() {
|
|
return WhereQueries{}
|
|
}
|
|
|
|
return WhereQueries{{
|
|
cond: cond,
|
|
params: []interface{}{param},
|
|
}}
|
|
}
|
|
|
|
// OrderByQuery represents the ORDER BY part of the query
|
|
type OrderByQuery struct {
|
|
fields string
|
|
desc bool
|
|
}
|
|
|
|
// Desc is a setter function for configuring the
|
|
// ORDER BY part of the query as DESC
|
|
func (o OrderByQuery) Desc() OrderByQuery {
|
|
return OrderByQuery{
|
|
fields: o.fields,
|
|
desc: true,
|
|
}
|
|
}
|
|
|
|
// OrderBy is a helper for building the ORDER BY
|
|
// part of the query.
|
|
func OrderBy(fields string) OrderByQuery {
|
|
return OrderByQuery{
|
|
fields: fields,
|
|
desc: false,
|
|
}
|
|
}
|
|
|
|
var cachedSelectQueries = &sync.Map{}
|
|
|
|
// Builds the select query using cached info so that its efficient
|
|
func buildSelectQuery(obj interface{}, dialect ksql.Dialect) (string, error) {
|
|
t := reflect.TypeOf(obj)
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
|
|
if t.Kind() != reflect.Struct {
|
|
return "", fmt.Errorf("expected to receive a pointer to struct, but got: %T", obj)
|
|
}
|
|
|
|
if data, found := cachedSelectQueries.Load(t); found {
|
|
if query, ok := data.(string); !ok {
|
|
return "", fmt.Errorf("invalid cache entry, expected type string, found %T", data)
|
|
} else {
|
|
return query, nil
|
|
}
|
|
}
|
|
|
|
info, err := structs.GetTagInfo(t)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var escapedNames []string
|
|
for i := 0; i < info.NumFields(); i++ {
|
|
name := info.ByIndex(i).Name
|
|
escapedNames = append(escapedNames, dialect.Escape(name))
|
|
}
|
|
|
|
query := strings.Join(escapedNames, ", ")
|
|
cachedSelectQueries.Store(t, query)
|
|
return query, nil
|
|
}
|