177 lines
5.0 KiB
Go
177 lines
5.0 KiB
Go
package batcher
|
|
|
|
import (
|
|
"strings"
|
|
"errors"
|
|
"strconv"
|
|
|
|
"btchrr/models"
|
|
)
|
|
|
|
// Batcher - encapsulates the batch size for splitting a slice into batches
|
|
type Batcher struct {
|
|
batchSize int
|
|
placeholders map[string]struct{}
|
|
}
|
|
|
|
// NewBatcher - Creates a new batcher instance with the specified batch size.
|
|
// Returns an error if the batch size is invalid.
|
|
func NewBatcher(batchSize int) (*Batcher, error) {
|
|
if batchSize <= 0 {
|
|
return nil, models.ErrInvalidBatchSize
|
|
}
|
|
b := &Batcher{
|
|
batchSize: batchSize,
|
|
placeholders: make(map[string]struct{}),
|
|
}
|
|
for _, ph := range []string{"?", "$", ":"} {
|
|
b.placeholders[ph] = struct{}{}
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// BuildBatch - Builds a batch query from a single query
|
|
func (b *Batcher) BuildBatches(singleQuery string, items []any) ([]models.BatchedQuery, error) {
|
|
batchedItems, err := b.batchItems(items)
|
|
if err != nil {
|
|
return []models.BatchedQuery{}, err
|
|
}
|
|
|
|
batches, err := b.BuildBatchQuery(singleQuery, b.batchSize, batchedItems, len(batchedItems))
|
|
if err != nil {
|
|
return []models.BatchedQuery{}, err
|
|
}
|
|
|
|
return batches, nil
|
|
}
|
|
|
|
// Batch - Splits the input slice into batches of the specified size.
|
|
// Returns an error if the input slice is empty.
|
|
func (b *Batcher) batchItems(items []any) ([][]any, error) {
|
|
if len(items) == 0 {
|
|
return nil, models.ErrNoItems
|
|
}
|
|
|
|
totalItems := len(items)
|
|
numOfBatches := (totalItems + b.batchSize - 1) / b.batchSize // round up
|
|
|
|
batches := make([][]any, 0, numOfBatches)
|
|
|
|
for i := 0; i < totalItems; i += b.batchSize {
|
|
end := i + b.batchSize
|
|
if end > totalItems {
|
|
end = totalItems
|
|
}
|
|
// Copy elements to avoid possible side effects if the original slice is modified
|
|
batch := make([]any, end-i)
|
|
copy(batch, items[i:end])
|
|
batches = append(batches, batch)
|
|
}
|
|
|
|
return batches, nil
|
|
}
|
|
|
|
// BuildBatchQuery - builds a batch query from a single query
|
|
func (b *Batcher) BuildBatchQuery(singleQuery string, batchSize int, batchedItems [][]any, numOfBatches int) (batchedQueries []models.BatchedQuery, err error) {
|
|
placeholder, err := b.detectPlaceholders(singleQuery)
|
|
if err != nil {
|
|
return []models.BatchedQuery{}, err
|
|
}
|
|
|
|
switch placeholder {
|
|
case "?":
|
|
for i :=0; i < numOfBatches; i++ {
|
|
batchQuery, err := b.buildSqliteQuery(singleQuery, batchedItems[i])
|
|
if err != nil {
|
|
return []models.BatchedQuery{}, err
|
|
}
|
|
batchedQueries = append(batchedQueries, batchQuery)
|
|
}
|
|
return
|
|
case "$":
|
|
for i :=0; i < numOfBatches; i++ {
|
|
batchQuery, err := b.buildPostgresQuery(singleQuery, batchedItems[i])
|
|
if err != nil {
|
|
return []models.BatchedQuery{}, err
|
|
}
|
|
batchedQueries = append(batchedQueries, batchQuery)
|
|
}
|
|
return
|
|
default:
|
|
return b.buildQueryWithNamedPlaceholders(singleQuery, placeholder, batchedItems)
|
|
}
|
|
}
|
|
|
|
// detectPlaceholder определяет тип плейсхолдера в SQL-запросе, ищет плейсхолдеры окружённые пробелами (" ? ", " $ ", " : ")
|
|
func (b *Batcher) detectPlaceholders(query string) (string, error) {
|
|
for _, s := range query {
|
|
if _, ok := b.placeholders[string(s)]; ok {
|
|
return string(s), nil
|
|
}
|
|
}
|
|
return "", models.ErrCannotDetectPlaceholder
|
|
}
|
|
|
|
func (b *Batcher) buildSqliteQuery(singleQuery string, batchedItems []any) (batchedQueries models.BatchedQuery, err error) {
|
|
// INSERT INTO users VALUES (?, ?);
|
|
idx := strings.Index(strings.ToLower(singleQuery), "values")
|
|
if idx == -1 {
|
|
return "", errors.New("no values found in query")
|
|
}
|
|
|
|
prefix := singleQuery[:idx+6] // +6 - values
|
|
valuesPart := singleQuery[idx+6:]
|
|
|
|
numOfInserts := strings.Count(valuesPart, "?")
|
|
if numOfInserts == 0 {
|
|
return "", errors.New("no placeholders found in query")
|
|
}
|
|
|
|
singleItemValues := "(" + strings.Repeat("?, ", numOfInserts - 1) + "?)"
|
|
//INSERT INTO users (name, age) VALUES (?, ?), (?, ?), (?, ?);
|
|
newValues := strings.Repeat(singleItemValues + ", ", b.batchSize - 1) + singleItemValues
|
|
newQuery := prefix + " " + newValues + ";"
|
|
|
|
|
|
return models.BatchedQuery(newQuery), nil
|
|
}
|
|
|
|
func (b *Batcher) buildPostgresQuery(singleQuery string, batchedItems []any) (batchedQueries models.BatchedQuery, err error) {
|
|
// INSERT INTO users VALUES ($1, $2);
|
|
idx := strings.Index(strings.ToLower(singleQuery), "values")
|
|
if idx == -1 {
|
|
return "", errors.New("no values found in query")
|
|
}
|
|
|
|
prefix := singleQuery[:idx+6] // +6 - values
|
|
valuesPart := singleQuery[idx+6:]
|
|
|
|
numOfInserts := strings.Count(valuesPart, "$")
|
|
if numOfInserts == 0 {
|
|
return "", errors.New("no placeholders found in query")
|
|
}
|
|
|
|
//INSERT INTO users VALUES ($1, $2), ($3, $4), ($5, $6);
|
|
placeholderNum := 1
|
|
query := ""
|
|
for i := 0; i < b.batchSize ; i++ {
|
|
if i > 0 {
|
|
query += ", "
|
|
}
|
|
query += "("
|
|
for j := 0; j < numOfInserts; j++ {
|
|
query += "$" + strconv.Itoa(placeholderNum) + ", "
|
|
placeholderNum++
|
|
}
|
|
query += ")"
|
|
}
|
|
|
|
query = prefix + " " + query + ";"
|
|
|
|
return models.BatchedQuery(query), nil
|
|
}
|
|
|
|
func (b *Batcher) buildQueryWithNamedPlaceholders(singleQuery string, placeholder string, batchedItems [][]any) (batchedQueries []models.BatchedQuery, err error) {
|
|
return []models.BatchedQuery{}, errors.New("not implemented")
|
|
}
|