mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-17 19:09:19 +02:00
158 lines
4.3 KiB
Go
158 lines
4.3 KiB
Go
// Package sqlutil provides helper utilities for SQL operations.
|
|
package sqlutil
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
sq "github.com/Masterminds/squirrel"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/oxynote/oxynote/server/core/pkg/httpserver"
|
|
)
|
|
|
|
// DB is an interface that should be implemented by all
|
|
// database managers.
|
|
type DB interface {
|
|
// BeginTx should begin a transaction and set the active Tx
|
|
// object to the dest parameter.
|
|
// Example:
|
|
// type CustomTx interface {
|
|
// Tx // embed core transaction interface
|
|
// FetchUser() User
|
|
// }
|
|
//
|
|
// var tx CustomTx
|
|
// db.BeginTx(context.Background(), &tx)
|
|
//
|
|
// NOTE #1: when go1.18 with the new type parameter feature is
|
|
// released, the signature of this method, as well as all of its
|
|
// implementations will need to be rewritten.
|
|
// NOTE #2: since the only way to set a transaction object to the
|
|
// destination parameter is by using reflection, this may not
|
|
// be safe at compile-time, so destination interfaces need to be
|
|
// thoroughly checked. However, this will not be a problem when
|
|
// type parameters are introduced.
|
|
BeginTx(ctx context.Context, dest any) error
|
|
}
|
|
|
|
// Tx is an interface that should be implemented by all
|
|
// transactions created by any database manager.
|
|
type Tx interface {
|
|
// Commit should commit the active transaction.
|
|
Commit() error
|
|
|
|
// Rollback should rollback the active transaction.
|
|
Rollback() error
|
|
}
|
|
|
|
// Select validates the provided query and uses it to start a SELECT SQL
|
|
// query.
|
|
// Filter and sort functions are called even when there are no filters
|
|
// or sort keys in the query. In these cases, the input is made of zero
|
|
// values (useful for default filter/sort settings).
|
|
// 'page_count' column is added and indicates the total count of pages
|
|
// that is available with the provided filters and limits.
|
|
// All dashes in query keys are transformed into underscores before
|
|
// filter/sort functions are called:
|
|
//
|
|
// created-at -> created_at.
|
|
// redeem-time-from -> redeem_time_from.
|
|
func Select(
|
|
qr httpserver.Query,
|
|
filter func(b sq.SelectBuilder, k, v string) (sq.SelectBuilder, error),
|
|
sort func(b sq.SelectBuilder, k httpserver.SortKey) (sq.SelectBuilder, error),
|
|
pageCount func(b sq.SelectBuilder, limit uint64) sq.SelectBuilder,
|
|
) (sq.SelectBuilder, error) {
|
|
if err := qr.Validate(); err != nil {
|
|
return sq.SelectBuilder{}, err
|
|
}
|
|
|
|
b := sq.Select().Limit(qr.Limit).Offset(qr.Limit * (qr.Page - 1))
|
|
b = pageCount(b, qr.Limit)
|
|
|
|
var err error
|
|
|
|
for k, v := range qr.Filters {
|
|
b, err = filter(b, strings.ReplaceAll(k, "-", "_"), v)
|
|
if err != nil {
|
|
return sq.SelectBuilder{}, err
|
|
}
|
|
}
|
|
|
|
// make sure that filter func is called at least once
|
|
if len(qr.Filters) == 0 {
|
|
b, err = filter(b, "", "")
|
|
if err != nil {
|
|
return sq.SelectBuilder{}, err
|
|
}
|
|
}
|
|
|
|
for _, v := range qr.SortKeys {
|
|
v.Key = strings.ReplaceAll(v.Key, "-", "_")
|
|
|
|
b, err = sort(b, v)
|
|
if err != nil {
|
|
return sq.SelectBuilder{}, err
|
|
}
|
|
}
|
|
|
|
// make sure that sort func is called at least once
|
|
if len(qr.SortKeys) == 0 {
|
|
b, err = sort(b, httpserver.SortKey{})
|
|
if err != nil {
|
|
return sq.SelectBuilder{}, err
|
|
}
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
// PageCount is the default way to calculate page count.
|
|
func PageCount(b sq.SelectBuilder, limit uint64) sq.SelectBuilder {
|
|
return b.Column(`CAST(CEIL(COUNT(*) OVER() / (? * 1.0)) AS INTEGER) AS page_count`, limit).
|
|
PlaceholderFormat(sq.Dollar)
|
|
}
|
|
|
|
// SortString transforms sort key type into a valid sorting clause string.
|
|
func SortString(k httpserver.SortKey) string {
|
|
ord := "DESC"
|
|
if k.Asc {
|
|
ord = "ASC"
|
|
}
|
|
|
|
return fmt.Sprintf("%s %s", k.Key, ord)
|
|
}
|
|
|
|
// WrapTx executes the provided function in a transaction.
|
|
// NOTE: Agent should be a valid *sqlx.Tx or *sqlx.DB pointer.
|
|
func WrapTx(ctx context.Context, agent any, fn func(*sqlx.Tx) error) (err error) {
|
|
var tx *sqlx.Tx
|
|
|
|
switch tp := agent.(type) {
|
|
case *sqlx.DB:
|
|
tx, err = tp.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if rerr := recover(); rerr != nil {
|
|
tx.Rollback() //nolint:errcheck,gosec // error provides no meaningful info
|
|
panic(rerr) // re-throw panic after tx rollback
|
|
} else if err != nil {
|
|
tx.Rollback() //nolint:errcheck,gosec // error provides no meaningful info
|
|
return
|
|
}
|
|
|
|
err = tx.Commit()
|
|
}()
|
|
case *sqlx.Tx:
|
|
tx = tp
|
|
default:
|
|
panic("invalid agent type")
|
|
}
|
|
|
|
return fn(tx)
|
|
}
|