v0.0.383 sq.InsertMultiple
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m15s

This commit is contained in:
Mike Schwörer 2024-02-09 15:17:51 +01:00
parent 885bb53244
commit 30ce8c4b60
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
7 changed files with 423 additions and 29 deletions

View File

@ -1,5 +1,5 @@
package goext package goext
const GoextVersion = "0.0.382" const GoextVersion = "0.0.383"
const GoextVersionTimestamp = "2024-02-09T12:25:01+0100" const GoextVersionTimestamp = "2024-02-09T15:17:51+0100"

View File

@ -479,3 +479,33 @@ func JoinString(arr []string, delimiter string) string {
return str return str
} }
// ArrChunk splits the array into buckets of max-size `chunkSize`
// order is being kept.
// The last chunk may contain less than length elements.
//
// (chunkSize == -1) means no chunking
//
// see https://www.php.net/manual/en/function.array-chunk.php
func ArrChunk[T any](arr []T, chunkSize int) [][]T {
if chunkSize == -1 {
return [][]T{arr}
}
res := make([][]T, 0, 1+len(arr)/chunkSize)
i := 0
for i < len(arr) {
right := i + chunkSize
if right >= len(arr) {
right = len(arr)
}
res = append(res, arr[i:right])
i = right
}
return res
}

View File

@ -1,13 +1,14 @@
package sq package sq
import ( import (
"errors"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/exerr" "gogs.mikescher.com/BlackForestBytes/goext/exerr"
"reflect" "reflect"
"strings" "strings"
) )
func BuildUpdateStatement(q Queryable, tableName string, obj any, idColumn string) (string, PP, error) { func BuildUpdateStatement[TData any](q Queryable, tableName string, obj TData, idColumn string) (string, PP, error) {
rval := reflect.ValueOf(obj) rval := reflect.ValueOf(obj)
rtyp := rval.Type() rtyp := rval.Type()
@ -70,7 +71,7 @@ func BuildUpdateStatement(q Queryable, tableName string, obj any, idColumn strin
return fmt.Sprintf("UPDATE %s SET %s WHERE %s", tableName, strings.Join(setClauses, ", "), matchClause), params, nil return fmt.Sprintf("UPDATE %s SET %s WHERE %s", tableName, strings.Join(setClauses, ", "), matchClause), params, nil
} }
func BuildInsertStatement(q Queryable, tableName string, obj any) (string, PP, error) { func BuildInsertStatement[TData any](q Queryable, tableName string, obj TData) (string, PP, error) {
rval := reflect.ValueOf(obj) rval := reflect.ValueOf(obj)
rtyp := rval.Type() rtyp := rval.Type()
@ -118,3 +119,81 @@ func BuildInsertStatement(q Queryable, tableName string, obj any) (string, PP, e
//goland:noinspection SqlNoDataSourceInspection //goland:noinspection SqlNoDataSourceInspection
return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", tableName, strings.Join(fields, ", "), strings.Join(values, ", ")), params, nil return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", tableName, strings.Join(fields, ", "), strings.Join(values, ", ")), params, nil
} }
func BuildInsertMultipleStatement[TData any](q Queryable, tableName string, vArr []TData) (string, PP, error) {
if len(vArr) == 0 {
return "", nil, errors.New("no data supplied")
}
rtyp := reflect.ValueOf(vArr[0]).Type()
sqlPrefix := ""
{
columns := make([]string, 0)
for i := 0; i < rtyp.NumField(); i++ {
rsfield := rtyp.Field(i)
if !rsfield.IsExported() {
continue
}
columnName := rsfield.Tag.Get("db")
if columnName == "" || columnName == "-" {
continue
}
columns = append(columns, "\""+columnName+"\"")
}
sqlPrefix = fmt.Sprintf("INSERT"+" INTO \"%s\" (%s) VALUES", tableName, strings.Join(columns, ", "))
}
pp := PP{}
sqlValuesArr := make([]string, 0)
for _, v := range vArr {
rval := reflect.ValueOf(v)
params := make([]string, 0)
for i := 0; i < rtyp.NumField(); i++ {
rsfield := rtyp.Field(i)
rvfield := rval.Field(i)
if !rsfield.IsExported() {
continue
}
columnName := rsfield.Tag.Get("db")
if columnName == "" || columnName == "-" {
continue
}
if rsfield.Type.Kind() == reflect.Ptr && rvfield.IsNil() {
params = append(params, "NULL")
} else {
val, err := convertValueToDB(q, rvfield.Interface())
if err != nil {
return "", nil, err
}
params = append(params, ":"+pp.Add(val))
}
}
sqlValuesArr = append(sqlValuesArr, fmt.Sprintf("(%s)", strings.Join(params, ", ")))
}
sqlstr := fmt.Sprintf("%s %s", sqlPrefix, strings.Join(sqlValuesArr, ", "))
return sqlstr, pp, nil
}

15
sq/main_test.go Normal file
View File

@ -0,0 +1,15 @@
package sq
import (
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"os"
"testing"
)
func TestMain(m *testing.M) {
if !exerr.Initialized() {
exerr.Init(exerr.ErrorPackageConfigInit{ZeroLogErrTraces: langext.PFalse, ZeroLogAllTraces: langext.PFalse})
}
os.Exit(m.Run())
}

View File

@ -4,10 +4,9 @@ import (
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"fmt"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"reflect" "gogs.mikescher.com/BlackForestBytes/goext/exerr"
"strings" "gogs.mikescher.com/BlackForestBytes/goext/langext"
) )
type StructScanMode string type StructScanMode string
@ -26,42 +25,61 @@ const (
func InsertSingle[TData any](ctx context.Context, q Queryable, tableName string, v TData) (sql.Result, error) { func InsertSingle[TData any](ctx context.Context, q Queryable, tableName string, v TData) (sql.Result, error) {
rval := reflect.ValueOf(v) sqlstr, pp, err := BuildInsertStatement(q, tableName, v)
rtyp := rval.Type()
columns := make([]string, 0)
params := make([]string, 0)
pp := PP{}
for i := 0; i < rtyp.NumField(); i++ {
rsfield := rtyp.Field(i)
rvfield := rval.Field(i)
if !rsfield.IsExported() {
continue
}
columnName := rsfield.Tag.Get("db")
if columnName == "" || columnName == "-" {
continue
}
paramkey := fmt.Sprintf("_%s", columnName)
columns = append(columns, "\""+columnName+"\"")
params = append(params, ":"+paramkey)
val, err := convertValueToDB(q, rvfield.Interface())
if err != nil { if err != nil {
return nil, err return nil, err
} }
pp[paramkey] = val sqlr, err := q.Exec(ctx, sqlstr, pp)
if err != nil {
return nil, err
} }
sqlstr := fmt.Sprintf("INSERT"+" INTO \"%s\" (%s) VALUES (%s)", tableName, strings.Join(columns, ", "), strings.Join(params, ", ")) return sqlr, nil
}
func InsertMultiple[TData any](ctx context.Context, q Queryable, tableName string, vArr []TData, maxBatch int) ([]sql.Result, error) {
if len(vArr) == 0 {
return make([]sql.Result, 0), nil
}
chunks := langext.ArrChunk(vArr, maxBatch)
sqlstrArr := make([]string, 0)
ppArr := make([]PP, 0)
for _, chunk := range chunks {
sqlstr, pp, err := BuildInsertMultipleStatement(q, tableName, chunk)
if err != nil {
return nil, exerr.Wrap(err, "").Build()
}
sqlstrArr = append(sqlstrArr, sqlstr)
ppArr = append(ppArr, pp)
}
res := make([]sql.Result, 0, len(sqlstrArr))
for i := 0; i < len(sqlstrArr); i++ {
sqlr, err := q.Exec(ctx, sqlstrArr[i], ppArr[i])
if err != nil {
return nil, err
}
res = append(res, sqlr)
}
return res, nil
}
func UpdateSingle[TData any](ctx context.Context, q Queryable, tableName string, v TData, idColumn string) (sql.Result, error) {
sqlstr, pp, err := BuildUpdateStatement(q, tableName, v, idColumn)
if err != nil {
return nil, err
}
sqlr, err := q.Exec(ctx, sqlstr, pp) sqlr, err := q.Exec(ctx, sqlstr, pp)
if err != nil { if err != nil {

237
sq/scanner_test.go Normal file
View File

@ -0,0 +1,237 @@
package sq
import (
"context"
"database/sql"
"fmt"
"github.com/glebarez/go-sqlite"
"github.com/jmoiron/sqlx"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"path/filepath"
"testing"
)
func TestInsertSingle(t *testing.T) {
type request struct {
ID string `json:"id" db:"id"`
Timestamp int `json:"timestamp" db:"timestamp"`
StrVal string `json:"strVal" db:"str_val"`
FloatVal float64 `json:"floatVal" db:"float_val"`
Dummy bool `json:"dummyBool" db:"dummy_bool"`
JsonVal JsonObj `json:"jsonVal" db:"json_val"`
}
if !langext.InArray("sqlite3", sql.Drivers()) {
sqlite.RegisterAsSQLITE3()
}
ctx := context.Background()
dbdir := t.TempDir()
dbfile1 := filepath.Join(dbdir, langext.MustHexUUID()+".sqlite3")
url := fmt.Sprintf("file:%s?_pragma=journal_mode(%s)&_pragma=timeout(%d)&_pragma=foreign_keys(%s)&_pragma=busy_timeout(%d)", dbfile1, "DELETE", 1000, "true", 1000)
xdb := tst.Must(sqlx.Open("sqlite", url))(t)
db := NewDB(xdb)
db.RegisterDefaultConverter()
_, err := db.Exec(ctx, `
CREATE TABLE requests (
id TEXT NOT NULL,
timestamp INTEGER NOT NULL,
str_val TEXT NOT NULL,
float_val REAL NOT NULL,
dummy_bool INTEGER NOT NULL CHECK(dummy_bool IN (0, 1)),
json_val TEXT NOT NULL,
PRIMARY KEY (id)
) STRICT
`, PP{})
tst.AssertNoErr(t, err)
_, err = InsertSingle(ctx, db, "requests", request{
ID: "9927",
Timestamp: 12321,
StrVal: "hello world",
Dummy: true,
FloatVal: 3.14159,
JsonVal: JsonObj{
"firs": 1,
"second": true,
},
})
tst.AssertNoErr(t, err)
}
func TestUpdateSingle(t *testing.T) {
type request struct {
ID string `json:"id" db:"id"`
Timestamp int `json:"timestamp" db:"timestamp"`
StrVal string `json:"strVal" db:"str_val"`
FloatVal float64 `json:"floatVal" db:"float_val"`
Dummy bool `json:"dummyBool" db:"dummy_bool"`
JsonVal JsonObj `json:"jsonVal" db:"json_val"`
}
if !langext.InArray("sqlite3", sql.Drivers()) {
sqlite.RegisterAsSQLITE3()
}
ctx := context.Background()
dbdir := t.TempDir()
dbfile1 := filepath.Join(dbdir, langext.MustHexUUID()+".sqlite3")
url := fmt.Sprintf("file:%s?_pragma=journal_mode(%s)&_pragma=timeout(%d)&_pragma=foreign_keys(%s)&_pragma=busy_timeout(%d)", dbfile1, "DELETE", 1000, "true", 1000)
xdb := tst.Must(sqlx.Open("sqlite", url))(t)
db := NewDB(xdb)
db.RegisterDefaultConverter()
_, err := db.Exec(ctx, `
CREATE TABLE requests (
id TEXT NOT NULL,
timestamp INTEGER NOT NULL,
str_val TEXT NOT NULL,
float_val REAL NOT NULL,
dummy_bool INTEGER NOT NULL CHECK(dummy_bool IN (0, 1)),
json_val TEXT NOT NULL,
PRIMARY KEY (id)
) STRICT
`, PP{})
tst.AssertNoErr(t, err)
_, err = InsertSingle(ctx, db, "requests", request{
ID: "9927",
Timestamp: 12321,
StrVal: "hello world",
Dummy: true,
FloatVal: 3.14159,
JsonVal: JsonObj{
"first": 1,
"second": true,
},
})
tst.AssertNoErr(t, err)
v, err := QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '9927' LIMIT 1", PP{}, SModeExtended, Safe)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, v.Timestamp, 12321)
tst.AssertEqual(t, v.StrVal, "hello world")
tst.AssertEqual(t, v.Dummy, true)
tst.AssertEqual(t, v.FloatVal, 3.14159)
tst.AssertStrRepEqual(t, v.JsonVal["first"], 1)
tst.AssertStrRepEqual(t, v.JsonVal["second"], true)
_, err = UpdateSingle(ctx, db, "requests", request{
ID: "9927",
Timestamp: 9999,
StrVal: "9999 hello world",
Dummy: false,
FloatVal: 123.222,
JsonVal: JsonObj{
"first": 2,
"second": false,
},
}, "id")
v, err = QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '9927' LIMIT 1", PP{}, SModeExtended, Safe)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, v.Timestamp, 9999)
tst.AssertEqual(t, v.StrVal, "9999 hello world")
tst.AssertEqual(t, v.Dummy, false)
tst.AssertEqual(t, v.FloatVal, 123.222)
tst.AssertStrRepEqual(t, v.JsonVal["first"], 2)
tst.AssertStrRepEqual(t, v.JsonVal["second"], false)
}
func TestInsertMultiple(t *testing.T) {
type request struct {
ID string `json:"id" db:"id"`
Timestamp int `json:"timestamp" db:"timestamp"`
StrVal string `json:"strVal" db:"str_val"`
FloatVal float64 `json:"floatVal" db:"float_val"`
Dummy bool `json:"dummyBool" db:"dummy_bool"`
JsonVal JsonObj `json:"jsonVal" db:"json_val"`
}
if !langext.InArray("sqlite3", sql.Drivers()) {
sqlite.RegisterAsSQLITE3()
}
ctx := context.Background()
dbdir := t.TempDir()
dbfile1 := filepath.Join(dbdir, langext.MustHexUUID()+".sqlite3")
url := fmt.Sprintf("file:%s?_pragma=journal_mode(%s)&_pragma=timeout(%d)&_pragma=foreign_keys(%s)&_pragma=busy_timeout(%d)", dbfile1, "DELETE", 1000, "true", 1000)
xdb := tst.Must(sqlx.Open("sqlite", url))(t)
db := NewDB(xdb)
db.RegisterDefaultConverter()
_, err := db.Exec(ctx, `
CREATE TABLE requests (
id TEXT NOT NULL,
timestamp INTEGER NOT NULL,
str_val TEXT NOT NULL,
float_val REAL NOT NULL,
dummy_bool INTEGER NOT NULL CHECK(dummy_bool IN (0, 1)),
json_val TEXT NOT NULL,
PRIMARY KEY (id)
) STRICT
`, PP{})
tst.AssertNoErr(t, err)
_, err = InsertMultiple(ctx, db, "requests", []request{
{
ID: "1",
Timestamp: 1000,
StrVal: "one",
Dummy: true,
FloatVal: 0.1,
JsonVal: JsonObj{
"arr": []int{0},
},
},
{
ID: "2",
Timestamp: 2000,
StrVal: "two",
Dummy: true,
FloatVal: 0.2,
JsonVal: JsonObj{
"arr": []int{0, 0},
},
},
{
ID: "3",
Timestamp: 3000,
StrVal: "three",
Dummy: true,
FloatVal: 0.3,
JsonVal: JsonObj{
"arr": []int{0, 0, 0},
},
},
}, -1)
tst.AssertNoErr(t, err)
_, err = QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '1' LIMIT 1", PP{}, SModeExtended, Safe)
tst.AssertNoErr(t, err)
_, err = QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '2' LIMIT 1", PP{}, SModeExtended, Safe)
tst.AssertNoErr(t, err)
_, err = QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '3' LIMIT 1", PP{}, SModeExtended, Safe)
tst.AssertNoErr(t, err)
}

View File

@ -2,6 +2,7 @@ package tst
import ( import (
"encoding/hex" "encoding/hex"
"fmt"
"reflect" "reflect"
"runtime/debug" "runtime/debug"
"testing" "testing"
@ -125,3 +126,17 @@ func AssertNoErr(t *testing.T, anerr error) {
t.Error("Function returned an error: " + anerr.Error() + "\n" + string(debug.Stack())) t.Error("Function returned an error: " + anerr.Error() + "\n" + string(debug.Stack()))
} }
} }
func AssertStrRepEqual(t *testing.T, actual any, expected any) {
t.Helper()
if fmt.Sprintf("%v", actual) != fmt.Sprintf("%v", expected) {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)
}
}
func AssertStrRepNotEqual(t *testing.T, actual any, expected any) {
t.Helper()
if fmt.Sprintf("%v", actual) == fmt.Sprintf("%v", expected) {
t.Errorf("values do not differ: Actual: '%v', Expected: '%v'", actual, expected)
}
}