query.go

Summary

Maintainability
C
1 day
Test Coverage
A
100%
package rel

import (
    "strconv"
    "strings"
)

// Querier interface defines contract to be used for query builder.
type Querier interface {
    Build(*Query)
}

type QueryPopulator interface {
    Populate(*Query, DocumentMeta)
}

// Build for given table using given queriers.
func Build(table string, queriers ...Querier) Query {
    var (
        query = newQuery()
    )

    if len(queriers) > 0 {
        _, query.empty = queriers[0].(Query)
    }

    for _, querier := range queriers {
        // avoid using indirect call to avoid heap allocation
        switch q := querier.(type) {
        case Query:
            q.Build(&query)
        case JoinQuery:
            q.Build(&query)
        case FilterQuery:
            q.Build(&query)
        case GroupQuery:
            q.Build(&query)
        case SortQuery:
            q.Build(&query)
        case Offset:
            q.Build(&query)
        case Limit:
            q.Build(&query)
        case Lock:
            q.Build(&query)
        case Unscoped:
            q.Build(&query)
        case Reload:
            q.Build(&query)
        case SQLQuery:
            q.Build(&query)
        case Preload:
            q.Build(&query)
        case Cascade:
            q.Build(&query)
        }
    }

    if query.Table == "" {
        query.Table = table
    }

    return query
}

// Query defines information about query generated by query builder.
type Query struct {
    empty           bool // TODO: use bitmask to mark what is updated and use it when merging two queries
    Table           string
    SelectQuery     SelectQuery
    JoinQuery       []JoinQuery
    WhereQuery      FilterQuery
    GroupQuery      GroupQuery
    SortQuery       []SortQuery
    OffsetQuery     Offset
    LimitQuery      Limit
    LockQuery       Lock
    SQLQuery        SQLQuery
    UnscopedQuery   Unscoped
    ReloadQuery     Reload
    CascadeQuery    Cascade
    PreloadQuery    []string
    UsePrimaryDb    bool
    queryPopulators []QueryPopulator
}

// Build query.
func (q Query) Build(query *Query) {
    if query.empty {
        *query = q
    } else {
        // manual merge
        if q.Table != "" {
            query.Table = q.Table
        }

        if q.SelectQuery.Fields != nil {
            query.SelectQuery = q.SelectQuery
        }

        query.JoinQuery = append(query.JoinQuery, q.JoinQuery...)

        if !q.WhereQuery.None() {
            query.WhereQuery = query.WhereQuery.And(q.WhereQuery)
        }

        if q.GroupQuery.Fields != nil {
            query.GroupQuery = q.GroupQuery
        }

        query.SortQuery = append(query.SortQuery, q.SortQuery...)

        if q.OffsetQuery != 0 {
            query.OffsetQuery = q.OffsetQuery
        }

        if q.LimitQuery != 0 {
            query.LimitQuery = q.LimitQuery
        }

        if q.LockQuery != "" {
            query.LockQuery = q.LockQuery
        }

        query.ReloadQuery = query.ReloadQuery || q.ReloadQuery
        query.CascadeQuery = query.CascadeQuery || q.CascadeQuery
        query.UsePrimaryDb = query.UsePrimaryDb || q.UsePrimaryDb
    }
}

func (q Query) Populate(documentMeta DocumentMeta) Query {
    for i := range q.queryPopulators {
        q.queryPopulators[i].Populate(&q, documentMeta)
    }

    return q
}

func (q *Query) AddPopulator(populator QueryPopulator) {
    q.queryPopulators = append(q.queryPopulators, populator)
}

// Select filter fields to be selected from database.
func (q Query) Select(fields ...string) Query {
    q.SelectQuery = NewSelect(fields...)
    return q
}

// From set the table to be used for query.
func (q Query) From(table string) Query {
    q.Table = table
    return q
}

// Distinct sets select query to be distinct.
func (q Query) Distinct() Query {
    q.SelectQuery.OnlyDistinct = true
    return q
}

// Join current table with other table.
func (q Query) Join(table string, filter ...FilterQuery) Query {
    return q.JoinOn(table, "", "", filter...)
}

// JoinOn current table with other table.
func (q Query) JoinOn(table string, from string, to string, filter ...FilterQuery) Query {
    return q.JoinWith("JOIN", table, from, to, filter...)
}

// JoinWith current table with other table with custom join mode.
func (q Query) JoinWith(mode string, table string, from string, to string, filter ...FilterQuery) Query {
    NewJoinWith(mode, table, from, to, filter...).Build(&q) // TODO: ensure this always called last

    return q
}

// Joinf create join query using a raw query.
func (q Query) Joinf(expr string, args ...any) Query {
    NewJoinFragment(expr, args...).Build(&q) // TODO: ensure this always called last

    return q
}

// JoinAssoc current table with other table based on association field.
func (q Query) JoinAssoc(assoc string, filter ...FilterQuery) Query {
    return q.JoinAssocWith("JOIN", assoc, filter...)
}

// JoinAssocWith current table with other table based on association field.
func (q Query) JoinAssocWith(mode string, assoc string, filter ...FilterQuery) Query {
    NewJoinAssocWith(mode, assoc, filter...).Build(&q)

    return q
}

// Where query.
func (q Query) Where(filters ...FilterQuery) Query {
    q.WhereQuery = q.WhereQuery.And(filters...)
    return q
}

// Wheref create where query using a raw query.
func (q Query) Wheref(expr string, args ...any) Query {
    q.WhereQuery = q.WhereQuery.And(FilterFragment(expr, args...))
    return q
}

// OrWhere query.
func (q Query) OrWhere(filters ...FilterQuery) Query {
    q.WhereQuery = q.WhereQuery.Or(And(filters...))
    return q
}

// OrWheref create where query using a raw query.
func (q Query) OrWheref(expr string, args ...any) Query {
    q.WhereQuery = q.WhereQuery.Or(FilterFragment(expr, args...))
    return q
}

// Group query.
func (q Query) Group(fields ...string) Query {
    q.GroupQuery.Fields = fields
    return q
}

// Having query.
func (q Query) Having(filters ...FilterQuery) Query {
    q.GroupQuery.Filter = q.GroupQuery.Filter.And(filters...)
    return q
}

// Havingf create having query using a raw query.
func (q Query) Havingf(expr string, args ...any) Query {
    q.GroupQuery.Filter = q.GroupQuery.Filter.And(FilterFragment(expr, args...))
    return q
}

// OrHaving query.
func (q Query) OrHaving(filters ...FilterQuery) Query {
    q.GroupQuery.Filter = q.GroupQuery.Filter.Or(And(filters...))
    return q
}

// OrHavingf create having query using a raw query.
func (q Query) OrHavingf(expr string, args ...any) Query {
    q.GroupQuery.Filter = q.GroupQuery.Filter.Or(FilterFragment(expr, args...))
    return q
}

// Sort query.
func (q Query) Sort(fields ...string) Query {
    return q.SortAsc(fields...)
}

// SortAsc query.
func (q Query) SortAsc(fields ...string) Query {
    var (
        offset = len(q.SortQuery)
    )

    q.SortQuery = append(q.SortQuery, make([]SortQuery, len(fields))...)
    for i := range fields {
        q.SortQuery[offset+i] = NewSortAsc(fields[i])
    }

    return q
}

// SortDesc query.
func (q Query) SortDesc(fields ...string) Query {
    var (
        offset = len(q.SortQuery)
    )

    q.SortQuery = append(q.SortQuery, make([]SortQuery, len(fields))...)
    for i := range fields {
        q.SortQuery[offset+i] = NewSortDesc(fields[i])
    }

    return q
}

// Offset the result returned by database.
func (q Query) Offset(offset int) Query {
    q.OffsetQuery = Offset(offset)
    return q
}

// Limit result returned by database.
func (q Query) Limit(limit int) Query {
    q.LimitQuery = Limit(limit)
    return q
}

// Lock query expression.
func (q Query) Lock(lock string) Query {
    q.LockQuery = Lock(lock)
    return q
}

// Unscoped allows soft-delete to be ignored.
func (q Query) Unscoped() Query {
    q.UnscopedQuery = true
    return q
}

// Reload force reloading association on preload.
func (q Query) Reload() Query {
    q.ReloadQuery = true
    return q
}

// Cascade enable/disable autoload association on Find and FindAll query.
func (q Query) Cascade(c bool) Query {
    q.CascadeQuery = Cascade(c)
    return q
}

// Preload field association.
func (q Query) Preload(field string) Query {
    q.PreloadQuery = append(q.PreloadQuery, field)
    return q
}

// UsePrimary database.
func (q Query) UsePrimary() Query {
    q.UsePrimaryDb = true
    return q
}

// String describe query as string.
func (q Query) String() string {
    if q.SQLQuery.Statement != "" {
        return q.SQLQuery.String()
    }

    var builder strings.Builder
    builder.WriteString("rel")

    if q.UsePrimaryDb {
        builder.WriteString(".UsePrimary()")
    }

    if q.Table != "" {
        builder.WriteString(".From(\"")
        builder.WriteString(q.Table)
        builder.WriteString("\")")
    }

    if len(q.SelectQuery.Fields) != 0 {
        builder.WriteString(".Select(\"")
        builder.WriteString(strings.Join(q.SelectQuery.Fields, "\", \""))
        builder.WriteString("\")")
    }

    if q.SelectQuery.OnlyDistinct {
        builder.WriteString(".Distinct()")
    }

    for i := range q.JoinQuery {
        builder.WriteString(".JoinWith(\"")
        builder.WriteString(q.JoinQuery[i].Mode)
        builder.WriteString("\", \"")
        builder.WriteString(q.JoinQuery[i].Table)
        builder.WriteString("\", \"")
        builder.WriteString(q.JoinQuery[i].From)
        builder.WriteString("\", \"")
        builder.WriteString(q.JoinQuery[i].To)
        builder.WriteString("\")")
    }

    if !q.WhereQuery.None() {
        builder.WriteString(".Where(")
        builder.WriteString(q.WhereQuery.String())
        builder.WriteByte(')')
    }

    if len(q.GroupQuery.Fields) != 0 {
        builder.WriteString(".Group(\"")
        builder.WriteString(strings.Join(q.GroupQuery.Fields, "\", \""))
        builder.WriteString("\")")

        if !q.GroupQuery.Filter.None() {
            builder.WriteString(".Having(")
            builder.WriteString(q.GroupQuery.Filter.String())
            builder.WriteByte(')')
        }
    }

    for _, sq := range q.SortQuery {
        if sq.Asc() {
            builder.WriteString(".SortAsc(\"")
        } else {
            builder.WriteString(".SortDesc(\"")
        }
        builder.WriteString(sq.Field)
        builder.WriteString("\")")
    }

    if q.LimitQuery > 0 {
        builder.WriteString(".Limit(")
        builder.WriteString(strconv.Itoa(int(q.LimitQuery)))
        builder.WriteString(")")
    }

    if q.OffsetQuery > 0 {
        builder.WriteString(".Offset(")
        builder.WriteString(strconv.Itoa(int(q.OffsetQuery)))
        builder.WriteString(")")
    }

    if q.LockQuery != "" {
        builder.WriteString(".Lock(\"")
        builder.WriteString(string(q.LockQuery))
        builder.WriteString("\")")
    }

    if q.UnscopedQuery {
        builder.WriteString(".Unscoped()")
    }

    if q.ReloadQuery {
        builder.WriteString(".Reload()")
    }

    if !q.CascadeQuery {
        builder.WriteString(".Cascade(false)")
    }

    if len(q.PreloadQuery) != 0 {
        builder.WriteString(".Preload(\"")
        builder.WriteString(strings.Join(q.PreloadQuery, "\", \""))
        builder.WriteString("\")")
    }

    if str := builder.String(); str != "rel" {
        return str
    }
    return ""
}

func newQuery() Query {
    return Query{
        CascadeQuery: true,
    }
}

// Select query create a query with chainable syntax, using select as the starting point.
func Select(fields ...string) Query {
    query := newQuery()
    query.SelectQuery.Fields = fields
    return query
}

// From create a query with chainable syntax, using from as the starting point.
func From(table string) Query {
    query := newQuery()
    query.Table = table
    return query
}

// Join create a query with chainable syntax, using join as the starting point.
func Join(table string, filter ...FilterQuery) Query {
    return JoinOn(table, "", "", filter...)
}

// JoinOn create a query with chainable syntax, using join as the starting point.
func JoinOn(table string, from string, to string, filter ...FilterQuery) Query {
    return JoinWith("JOIN", table, from, to, filter...)
}

// JoinWith create a query with chainable syntax, using join as the starting point.
func JoinWith(mode string, table string, from string, to string, filter ...FilterQuery) Query {
    query := newQuery()
    query.JoinQuery = []JoinQuery{
        NewJoinWith(mode, table, from, to, filter...),
    }
    return query
}

// JoinAssoc create a query with chainable syntax, using join as the starting point.
func JoinAssoc(assoc string, filter ...FilterQuery) Query {
    return JoinAssocWith("JOIN", assoc, filter...)
}

// JoinAssocWith create a query with chainable syntax, using join as the starting point.
func JoinAssocWith(mode string, assoc string, filter ...FilterQuery) Query {
    query := newQuery()
    query.JoinQuery = []JoinQuery{
        NewJoinAssocWith(mode, assoc, filter...),
    }
    query.AddPopulator(&query.JoinQuery[0])
    return query
}

// Joinf create a query with chainable syntax, using join as the starting point.
func Joinf(expr string, args ...any) Query {
    query := newQuery()
    query.JoinQuery = []JoinQuery{
        NewJoinFragment(expr, args...),
    }
    return query
}

// Where create a query with chainable syntax, using where as the starting point.
func Where(filters ...FilterQuery) Query {
    query := newQuery()
    query.WhereQuery = And(filters...)
    return query
}

func UsePrimary() Query {
    query := newQuery()
    query.UsePrimaryDb = true
    return query
}

// Offset  Query.
type Offset int

// Build query.
func (o Offset) Build(query *Query) {
    query.OffsetQuery = o
}

// Limit options.
// When passed as query, it limits returned result from database.
// When passed as column option, it sets the maximum size of the string/text/binary/integer columns.
type Limit int

// Build query.
func (l Limit) Build(query *Query) {
    query.LimitQuery = l
}

func (l Limit) applyColumn(column *Column) {
    column.Limit = int(l)
}

// Lock query.
// This query will be ignored if used outside of transaction.
type Lock string

// Build query.
func (l Lock) Build(query *Query) {
    query.LockQuery = l
}

// ForUpdate lock query.
func ForUpdate() Lock {
    return "FOR UPDATE"
}

// Unscoped query.
type Unscoped bool

// Build query.
func (u Unscoped) Build(query *Query) {
    query.UnscopedQuery = u
}

// Apply mutation.
func (u Unscoped) Apply(doc *Document, mutation *Mutation) {
    mutation.Unscoped = u
}

// Preload query.
type Preload string

// Build query.
func (p Preload) Build(query *Query) {
    query.PreloadQuery = append(query.PreloadQuery, string(p))
}