2019-09-27 11:46:31 +02:00
|
|
|
package qrm
|
2019-03-05 18:55:47 +01:00
|
|
|
|
|
|
|
|
import (
|
2019-06-20 12:22:19 +02:00
|
|
|
"context"
|
2021-05-16 18:46:50 +02:00
|
|
|
"database/sql"
|
2025-03-08 19:01:37 +01:00
|
|
|
"encoding/json"
|
2019-10-18 10:15:08 +02:00
|
|
|
"errors"
|
2021-05-16 18:46:50 +02:00
|
|
|
"fmt"
|
2019-03-05 18:55:47 +01:00
|
|
|
"reflect"
|
2025-12-29 23:27:27 +09:00
|
|
|
|
|
|
|
|
"github.com/go-jet/jet/v2/internal/utils/must"
|
2019-03-05 18:55:47 +01:00
|
|
|
)
|
|
|
|
|
|
2025-03-11 10:50:06 +01:00
|
|
|
// Config holds the configuration settings for QRM scanning behavior.
|
|
|
|
|
type Config struct {
|
|
|
|
|
// StrictScan, when true, causes the scanning function to panic if it encounters any
|
|
|
|
|
// unused columns in the SQL query result. This ensures that every column is mapped
|
|
|
|
|
// to a field in the destination struct.
|
|
|
|
|
// Does not apply to statements build with SELECT_JSON_OBJ or SELECT_JSON_ARR
|
|
|
|
|
StrictScan bool
|
2025-03-11 13:33:39 +01:00
|
|
|
|
2025-12-29 23:27:27 +09:00
|
|
|
// StrictFieldMapping, when true, causes the scanning function to panic if it encounters any
|
|
|
|
|
// destination struct fields that do not have matching columns in the SQL query result.
|
2026-01-12 18:24:20 +09:00
|
|
|
//
|
|
|
|
|
// Optional fields:
|
|
|
|
|
// If a destination field (including struct/slice fields) is not always selected by a query,
|
|
|
|
|
// it can be marked as optional using `qrm:"optional"`. When StrictFieldMapping is enabled,
|
|
|
|
|
// unmapped fields under an optional field will not trigger a panic.
|
2025-12-29 23:27:27 +09:00
|
|
|
// Does not apply to statements build with SELECT_JSON_OBJ or SELECT_JSON_ARR
|
|
|
|
|
StrictFieldMapping bool
|
|
|
|
|
|
2025-03-11 13:33:39 +01:00
|
|
|
// JsonUnmarshalFunc is called by the Query method to unmarshal JSON query results created by
|
|
|
|
|
// SELECT_JSON_OBJ and SELECT_JSON_ARR statements.
|
|
|
|
|
// It can be replaced with any implementation that matches the standard "encoding/json" `Unmarshal` function signature.
|
|
|
|
|
// By default, it uses the `Unmarshal` function from Go's standard `encoding/json` package.
|
|
|
|
|
JsonUnmarshalFunc func(data []byte, v any) error
|
2025-03-11 10:50:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GlobalConfig is the package-wide configuration for SQL scanning.
|
2025-03-11 13:33:39 +01:00
|
|
|
// This variable is not thread safe, and it should be modified only once, for instance, during application initialization.
|
2025-03-11 10:50:06 +01:00
|
|
|
var GlobalConfig = Config{
|
2025-12-29 23:27:27 +09:00
|
|
|
StrictScan: false,
|
|
|
|
|
StrictFieldMapping: false,
|
|
|
|
|
JsonUnmarshalFunc: json.Unmarshal,
|
2025-03-11 10:50:06 +01:00
|
|
|
}
|
|
|
|
|
|
2019-10-18 10:15:08 +02:00
|
|
|
// ErrNoRows is returned by Query when query result set is empty
|
|
|
|
|
var ErrNoRows = errors.New("qrm: no rows in result set")
|
|
|
|
|
|
2025-02-21 19:55:01 +01:00
|
|
|
// QueryJsonObj executes a SQL query that returns a JSON object, unmarshals the result into the provided destination,
|
|
|
|
|
// and returns the number of rows processed.
|
|
|
|
|
//
|
|
|
|
|
// The query must return exactly one row with a single column; otherwise, an error is returned.
|
|
|
|
|
//
|
|
|
|
|
// Parameters:
|
|
|
|
|
//
|
|
|
|
|
// ctx - The context for managing query execution (timeouts, cancellations).
|
|
|
|
|
// db - The database connection or transaction that implements the Queryable interface.
|
|
|
|
|
// query - The SQL query string to be executed.
|
|
|
|
|
// args - A slice of arguments to be used with the query.
|
|
|
|
|
// destPtr - A pointer to the variable where the unmarshaled JSON result will be stored.
|
|
|
|
|
// The destination should be a pointer to a struct or map[string]any.
|
|
|
|
|
//
|
|
|
|
|
// Returns:
|
|
|
|
|
//
|
|
|
|
|
// rowsProcessed - The number of rows processed by the query execution.
|
|
|
|
|
// err - An error if query execution or unmarshaling fails.
|
|
|
|
|
func QueryJsonObj(ctx context.Context, db Queryable, query string, args []interface{}, destPtr interface{}) (rowsProcessed int64, err error) {
|
|
|
|
|
must.BeInitializedPtr(destPtr, "jet: destination is nil")
|
|
|
|
|
must.BeTypeKind(destPtr, reflect.Ptr, jsonDestObjErr)
|
|
|
|
|
destType := reflect.TypeOf(destPtr).Elem()
|
|
|
|
|
must.BeTrue(destType.Kind() == reflect.Struct || destType.Kind() == reflect.Map, jsonDestObjErr)
|
|
|
|
|
|
|
|
|
|
return queryJson(ctx, db, query, args, destPtr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// QueryJsonArr executes a SQL query that returns a JSON array, unmarshals the result into the provided destination,
|
|
|
|
|
// and returns the number of rows processed.
|
|
|
|
|
//
|
|
|
|
|
// The query must return exactly one row with a single column; otherwise, an error is returned.
|
|
|
|
|
//
|
|
|
|
|
// Parameters:
|
|
|
|
|
//
|
|
|
|
|
// ctx - The context for managing query execution (timeouts, cancellations).
|
|
|
|
|
// db - The database connection or transaction that implements the Queryable interface.
|
|
|
|
|
// query - The SQL query string to be executed.
|
|
|
|
|
// args - A slice of arguments to be used with the query.
|
|
|
|
|
// destPtr - A pointer to the variable where the unmarshaled JSON array will be stored.
|
|
|
|
|
// The destination should be a pointer to a slice of structs or []map[string]any.
|
|
|
|
|
//
|
|
|
|
|
// Returns:
|
|
|
|
|
//
|
|
|
|
|
// rowsProcessed - The number of rows processed by the query execution.
|
|
|
|
|
// err - An error if query execution or unmarshaling fails.
|
|
|
|
|
func QueryJsonArr(ctx context.Context, db Queryable, query string, args []interface{}, destPtr interface{}) (rowsProcessed int64, err error) {
|
|
|
|
|
must.BeInitializedPtr(destPtr, "jet: destination is nil")
|
|
|
|
|
must.BeTypeKind(destPtr, reflect.Ptr, jsonDestArrErr)
|
|
|
|
|
destType := reflect.TypeOf(destPtr).Elem()
|
|
|
|
|
must.BeTrue(destType.Kind() == reflect.Slice, jsonDestArrErr)
|
|
|
|
|
|
|
|
|
|
return queryJson(ctx, db, query, args, destPtr)
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-08 19:01:37 +01:00
|
|
|
var jsonDestObjErr = "jet: SELECT_JSON_OBJ destination has to be a pointer to struct or pointer to map[string]any"
|
|
|
|
|
var jsonDestArrErr = "jet: SELECT_JSON_ARR destination has to be a pointer to slice of struct or pointer to []map[string]any"
|
2025-02-21 19:55:01 +01:00
|
|
|
|
|
|
|
|
func queryJson(ctx context.Context, db Queryable, query string, args []interface{}, destPtr interface{}) (rowsProcessed int64, err error) {
|
|
|
|
|
must.BeInitializedPtr(db, "jet: db is nil")
|
|
|
|
|
|
|
|
|
|
var rows *sql.Rows
|
|
|
|
|
rows, err = db.QueryContext(ctx, query, args...)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
|
|
if !rows.Next() {
|
|
|
|
|
err = rows.Err()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
return 0, ErrNoRows
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var jsonData []byte
|
|
|
|
|
err = rows.Scan(&jsonData)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 1, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if jsonData == nil {
|
|
|
|
|
return 1, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-11 13:33:39 +01:00
|
|
|
err = GlobalConfig.JsonUnmarshalFunc(jsonData, &destPtr)
|
2025-02-21 19:55:01 +01:00
|
|
|
|
|
|
|
|
if err != nil {
|
2025-03-09 17:46:34 +01:00
|
|
|
return 1, fmt.Errorf("jet: invalid json, %w", err)
|
2025-02-21 19:55:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if rows.Next() {
|
|
|
|
|
return 1, fmt.Errorf("jet: query returned more then one row")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = rows.Close()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 1, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 1, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Query executes a Query Result Mapping (QRM) of the provided SQL `query` with a list of parameterized arguments `args`
|
|
|
|
|
// over the database connection `db` using the provided context `ctx` and stores the result in the destination `destPtr`.
|
|
|
|
|
//
|
|
|
|
|
// The destination must be a pointer to either a struct or a slice of structs
|
|
|
|
|
// If the destination is a pointer to a struct and no rows are returned, the method returns qrm.ErrNoRows.
|
|
|
|
|
//
|
|
|
|
|
// Parameters:
|
|
|
|
|
//
|
|
|
|
|
// ctx - The context for managing query execution (timeouts, cancellations).
|
|
|
|
|
// db - The database connection or transaction implementing the Queryable interface.
|
|
|
|
|
// query - The SQL query string to be executed.
|
|
|
|
|
// args - A slice of arguments to be used with the query.
|
|
|
|
|
// destPtr - A pointer to the variable where the query result will be stored. This can be a pointer to a struct or a slice of structs.
|
|
|
|
|
//
|
|
|
|
|
// Returns:
|
|
|
|
|
//
|
|
|
|
|
// rowsProcessed - The number of rows processed by the query execution.
|
|
|
|
|
// err - An error if query execution or result mapping fails, or if no rows are found when a struct is expected.
|
2022-05-13 14:04:11 +02:00
|
|
|
func Query(ctx context.Context, db Queryable, query string, args []interface{}, destPtr interface{}) (rowsProcessed int64, err error) {
|
2019-05-20 17:37:55 +02:00
|
|
|
|
2023-07-21 14:11:31 +02:00
|
|
|
must.BeInitializedPtr(db, "jet: db is nil")
|
|
|
|
|
must.BeInitializedPtr(destPtr, "jet: destination is nil")
|
|
|
|
|
must.BeTypeKind(destPtr, reflect.Ptr, "jet: destination has to be a pointer to slice or pointer to struct")
|
2019-05-20 17:37:55 +02:00
|
|
|
|
2019-09-27 11:46:31 +02:00
|
|
|
destinationPtrType := reflect.TypeOf(destPtr)
|
2019-05-20 17:37:55 +02:00
|
|
|
|
|
|
|
|
if destinationPtrType.Elem().Kind() == reflect.Slice {
|
2022-01-12 19:03:50 +01:00
|
|
|
rowsProcessed, err := queryToSlice(ctx, db, query, args, destPtr)
|
2021-10-15 17:43:10 +02:00
|
|
|
if err != nil {
|
2022-01-12 19:03:50 +01:00
|
|
|
return rowsProcessed, fmt.Errorf("jet: %w", err)
|
2021-10-15 17:43:10 +02:00
|
|
|
}
|
2022-01-12 19:03:50 +01:00
|
|
|
return rowsProcessed, nil
|
2019-05-20 17:37:55 +02:00
|
|
|
} else if destinationPtrType.Elem().Kind() == reflect.Struct {
|
|
|
|
|
tempSlicePtrValue := reflect.New(reflect.SliceOf(destinationPtrType))
|
|
|
|
|
tempSliceValue := tempSlicePtrValue.Elem()
|
|
|
|
|
|
2019-10-12 18:45:09 +02:00
|
|
|
rowsProcessed, err := queryToSlice(ctx, db, query, args, tempSlicePtrValue.Interface())
|
2019-05-20 17:37:55 +02:00
|
|
|
|
|
|
|
|
if err != nil {
|
2022-01-12 19:03:50 +01:00
|
|
|
return rowsProcessed, fmt.Errorf("jet: %w", err)
|
2019-05-20 17:37:55 +02:00
|
|
|
}
|
|
|
|
|
|
2019-10-12 18:45:09 +02:00
|
|
|
if rowsProcessed == 0 {
|
2022-01-12 19:03:50 +01:00
|
|
|
return 0, ErrNoRows
|
2019-05-20 17:37:55 +02:00
|
|
|
}
|
|
|
|
|
|
2019-10-12 18:45:09 +02:00
|
|
|
// edge case when row result set contains only NULLs.
|
|
|
|
|
if tempSliceValue.Len() == 0 {
|
2022-01-12 19:03:50 +01:00
|
|
|
return rowsProcessed, nil
|
2019-10-12 18:45:09 +02:00
|
|
|
}
|
|
|
|
|
|
2019-09-27 11:46:31 +02:00
|
|
|
structValue := reflect.ValueOf(destPtr).Elem()
|
2019-05-20 17:37:55 +02:00
|
|
|
firstTempStruct := tempSliceValue.Index(0).Elem()
|
|
|
|
|
|
|
|
|
|
if structValue.Type().AssignableTo(firstTempStruct.Type()) {
|
|
|
|
|
structValue.Set(tempSliceValue.Index(0).Elem())
|
|
|
|
|
}
|
2022-01-12 19:03:50 +01:00
|
|
|
return rowsProcessed, nil
|
2019-05-20 17:37:55 +02:00
|
|
|
} else {
|
2019-08-13 13:57:26 +02:00
|
|
|
panic("jet: destination has to be a pointer to slice or pointer to struct")
|
2019-05-20 17:37:55 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-16 19:10:43 +02:00
|
|
|
// ScanOneRowToDest will scan one row into struct destination
|
2022-02-04 12:31:08 +01:00
|
|
|
func ScanOneRowToDest(scanContext *ScanContext, rows *sql.Rows, destPtr interface{}) error {
|
2023-07-21 14:11:31 +02:00
|
|
|
must.BeInitializedPtr(destPtr, "jet: destination is nil")
|
|
|
|
|
must.BeTypeKind(destPtr, reflect.Ptr, "jet: destination has to be a pointer to slice or pointer to struct")
|
2021-05-16 18:46:50 +02:00
|
|
|
|
|
|
|
|
if len(scanContext.row) == 0 {
|
|
|
|
|
return errors.New("empty row slice")
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-04 12:31:08 +01:00
|
|
|
err := rows.Scan(scanContext.row...)
|
2021-05-16 18:46:50 +02:00
|
|
|
|
|
|
|
|
if err != nil {
|
2022-02-09 12:34:10 +01:00
|
|
|
return fmt.Errorf("jet: rows scan error, %w", err)
|
2021-05-16 18:46:50 +02:00
|
|
|
}
|
|
|
|
|
|
2022-02-09 12:34:10 +01:00
|
|
|
destValuePtr := reflect.ValueOf(destPtr)
|
2021-05-16 18:46:50 +02:00
|
|
|
|
2025-03-11 10:50:06 +01:00
|
|
|
scanContext.rowNum++
|
|
|
|
|
|
2022-02-09 12:34:10 +01:00
|
|
|
_, err = mapRowToStruct(scanContext, "", destValuePtr, nil)
|
2021-05-16 18:46:50 +02:00
|
|
|
|
|
|
|
|
if err != nil {
|
2022-02-09 12:34:10 +01:00
|
|
|
return fmt.Errorf("jet: failed to scan a row into destination, %w", err)
|
2021-05-16 18:46:50 +02:00
|
|
|
}
|
|
|
|
|
|
2025-03-11 10:50:06 +01:00
|
|
|
scanContext.EnsureEveryColumnRead() // can panic
|
2025-12-29 23:27:27 +09:00
|
|
|
if GlobalConfig.StrictFieldMapping {
|
|
|
|
|
scanContext.EnsureEveryFieldMapped() // can panic
|
|
|
|
|
}
|
2025-03-11 10:50:06 +01:00
|
|
|
|
2021-05-16 18:46:50 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-13 14:04:11 +02:00
|
|
|
func queryToSlice(ctx context.Context, db Queryable, query string, args []interface{}, slicePtr interface{}) (rowsProcessed int64, err error) {
|
2019-06-20 12:22:19 +02:00
|
|
|
if ctx == nil {
|
|
|
|
|
ctx = context.Background()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rows, err := db.QueryContext(ctx, query, args...)
|
2019-03-05 18:55:47 +01:00
|
|
|
|
|
|
|
|
if err != nil {
|
2019-10-12 18:45:09 +02:00
|
|
|
return
|
2019-03-05 18:55:47 +01:00
|
|
|
}
|
2019-03-09 14:20:44 +01:00
|
|
|
defer rows.Close()
|
2019-03-05 18:55:47 +01:00
|
|
|
|
2022-02-04 12:31:08 +01:00
|
|
|
scanContext, err := NewScanContext(rows)
|
2019-03-14 09:18:23 +01:00
|
|
|
|
2019-05-20 17:37:55 +02:00
|
|
|
if err != nil {
|
2019-10-12 18:45:09 +02:00
|
|
|
return
|
2019-03-14 09:18:23 +01:00
|
|
|
}
|
2019-03-05 18:55:47 +01:00
|
|
|
|
2019-05-20 17:37:55 +02:00
|
|
|
if len(scanContext.row) == 0 {
|
2019-10-12 18:45:09 +02:00
|
|
|
return
|
2019-05-20 17:37:55 +02:00
|
|
|
}
|
|
|
|
|
|
2019-06-12 16:21:50 +02:00
|
|
|
slicePtrValue := reflect.ValueOf(slicePtr)
|
|
|
|
|
|
2019-03-05 18:55:47 +01:00
|
|
|
for rows.Next() {
|
2019-10-12 18:45:09 +02:00
|
|
|
err = rows.Scan(scanContext.row...)
|
2019-03-05 18:55:47 +01:00
|
|
|
|
|
|
|
|
if err != nil {
|
2022-01-12 19:03:50 +01:00
|
|
|
return scanContext.rowNum, err
|
2019-03-05 18:55:47 +01:00
|
|
|
}
|
|
|
|
|
|
2019-03-15 21:55:43 +01:00
|
|
|
scanContext.rowNum++
|
|
|
|
|
|
2022-02-09 12:34:10 +01:00
|
|
|
_, err = mapRowToSlice(scanContext, "", slicePtrValue, nil)
|
2019-05-20 17:37:55 +02:00
|
|
|
|
|
|
|
|
if err != nil {
|
2022-01-12 19:03:50 +01:00
|
|
|
return scanContext.rowNum, err
|
2019-03-05 18:55:47 +01:00
|
|
|
}
|
2025-03-11 10:50:06 +01:00
|
|
|
|
|
|
|
|
if scanContext.rowNum == 1 && GlobalConfig.StrictScan {
|
|
|
|
|
scanContext.EnsureEveryColumnRead()
|
|
|
|
|
}
|
2025-12-29 23:27:27 +09:00
|
|
|
if scanContext.rowNum == 1 && GlobalConfig.StrictFieldMapping {
|
|
|
|
|
scanContext.EnsureEveryFieldMapped()
|
|
|
|
|
}
|
2019-03-05 18:55:47 +01:00
|
|
|
}
|
|
|
|
|
|
2019-06-21 16:16:57 +02:00
|
|
|
err = rows.Close()
|
2019-03-05 18:55:47 +01:00
|
|
|
if err != nil {
|
2022-01-12 19:03:50 +01:00
|
|
|
return scanContext.rowNum, err
|
2019-03-05 18:55:47 +01:00
|
|
|
}
|
|
|
|
|
|
2022-01-12 19:03:50 +01:00
|
|
|
return scanContext.rowNum, rows.Err()
|
2019-03-05 18:55:47 +01:00
|
|
|
}
|
|
|
|
|
|
2021-12-07 17:16:10 +01:00
|
|
|
func mapRowToSlice(
|
2022-02-04 12:31:08 +01:00
|
|
|
scanContext *ScanContext,
|
2021-12-07 17:16:10 +01:00
|
|
|
groupKey string,
|
|
|
|
|
slicePtrValue reflect.Value,
|
|
|
|
|
field *reflect.StructField) (updated bool, err error) {
|
2019-04-04 13:07:21 +02:00
|
|
|
|
2019-05-20 17:37:55 +02:00
|
|
|
sliceElemType := getSliceElemType(slicePtrValue)
|
2019-03-14 09:18:23 +01:00
|
|
|
|
2019-07-29 18:08:53 +02:00
|
|
|
if isSimpleModelType(sliceElemType) {
|
2019-07-13 13:17:28 +02:00
|
|
|
updated, err = mapRowToBaseTypeSlice(scanContext, slicePtrValue, field)
|
2019-05-20 17:37:55 +02:00
|
|
|
return
|
2019-03-14 09:18:23 +01:00
|
|
|
}
|
|
|
|
|
|
2023-07-21 14:11:31 +02:00
|
|
|
must.TypeBeOfKind(sliceElemType, reflect.Struct, "jet: unsupported slice element type"+fieldToString(field))
|
2019-03-14 09:18:23 +01:00
|
|
|
|
2019-07-13 13:17:28 +02:00
|
|
|
structGroupKey := scanContext.getGroupKey(sliceElemType, field)
|
2019-03-30 09:59:24 +01:00
|
|
|
|
2022-02-09 12:34:10 +01:00
|
|
|
groupKey = concat(groupKey, ",", structGroupKey)
|
2019-03-30 09:59:24 +01:00
|
|
|
|
2019-06-20 11:19:23 +02:00
|
|
|
index, ok := scanContext.uniqueDestObjectsMap[groupKey]
|
2019-03-30 09:59:24 +01:00
|
|
|
|
2019-05-20 17:37:55 +02:00
|
|
|
if ok {
|
|
|
|
|
structPtrValue := getSliceElemPtrAt(slicePtrValue, index)
|
2019-03-30 09:59:24 +01:00
|
|
|
|
2022-02-09 12:34:10 +01:00
|
|
|
return mapRowToStruct(scanContext, groupKey, structPtrValue, field, true)
|
2019-07-18 18:42:03 +02:00
|
|
|
}
|
2019-03-30 09:59:24 +01:00
|
|
|
|
2019-07-18 18:42:03 +02:00
|
|
|
destinationStructPtr := newElemPtrValueForSlice(slicePtrValue)
|
2019-03-30 09:59:24 +01:00
|
|
|
|
2022-02-09 12:34:10 +01:00
|
|
|
updated, err = mapRowToStruct(scanContext, groupKey, destinationStructPtr, field)
|
2019-05-20 17:37:55 +02:00
|
|
|
|
2019-07-18 18:42:03 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-05-22 11:28:32 +02:00
|
|
|
|
2019-07-18 18:42:03 +02:00
|
|
|
if updated {
|
|
|
|
|
scanContext.uniqueDestObjectsMap[groupKey] = slicePtrValue.Elem().Len()
|
|
|
|
|
err = appendElemToSlice(slicePtrValue, destinationStructPtr)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
2019-05-20 17:37:55 +02:00
|
|
|
}
|
2019-03-30 09:59:24 +01:00
|
|
|
}
|
|
|
|
|
|
2019-05-20 17:37:55 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-04 12:31:08 +01:00
|
|
|
func mapRowToBaseTypeSlice(scanContext *ScanContext, slicePtrValue reflect.Value, field *reflect.StructField) (updated bool, err error) {
|
2019-07-13 13:17:28 +02:00
|
|
|
index := 0
|
|
|
|
|
if field != nil {
|
2025-02-21 19:55:01 +01:00
|
|
|
typeName, columnName, _ := getTypeAndFieldName("", *field)
|
2019-07-13 13:17:28 +02:00
|
|
|
if index = scanContext.typeToColumnIndex(typeName, columnName); index < 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-02-09 12:34:10 +01:00
|
|
|
rowElemPtr := scanContext.rowElemValueClonePtr(index)
|
2019-03-14 09:18:23 +01:00
|
|
|
|
2021-09-15 09:03:05 -04:00
|
|
|
if rowElemPtr.IsValid() && !rowElemPtr.IsNil() {
|
2019-07-13 13:17:28 +02:00
|
|
|
updated = true
|
2021-09-15 09:03:05 -04:00
|
|
|
err = appendElemToSlice(slicePtrValue, rowElemPtr)
|
2019-07-13 13:17:28 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-03-14 09:18:23 +01:00
|
|
|
}
|
|
|
|
|
|
2019-07-13 13:17:28 +02:00
|
|
|
return
|
2019-03-14 09:18:23 +01:00
|
|
|
}
|
|
|
|
|
|
2021-12-07 17:16:10 +01:00
|
|
|
func mapRowToStruct(
|
2022-02-04 12:31:08 +01:00
|
|
|
scanContext *ScanContext,
|
2021-12-07 17:16:10 +01:00
|
|
|
groupKey string,
|
|
|
|
|
structPtrValue reflect.Value,
|
|
|
|
|
parentField *reflect.StructField,
|
|
|
|
|
onlySlices ...bool, // small optimization, not to assign to already assigned struct fields
|
|
|
|
|
) (updated bool, err error) {
|
|
|
|
|
|
2019-10-12 18:45:09 +02:00
|
|
|
mapOnlySlices := len(onlySlices) > 0
|
2019-07-13 13:17:28 +02:00
|
|
|
structType := structPtrValue.Type().Elem()
|
2019-03-14 09:18:23 +01:00
|
|
|
|
2022-02-09 12:34:10 +01:00
|
|
|
if scanContext.typesVisited.contains(&structType) {
|
2021-12-07 17:16:10 +01:00
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-09 12:34:10 +01:00
|
|
|
scanContext.typesVisited.push(&structType)
|
|
|
|
|
defer scanContext.typesVisited.pop()
|
2021-12-07 17:16:10 +01:00
|
|
|
|
2019-07-13 13:17:28 +02:00
|
|
|
typeInf := scanContext.getTypeInfo(structType, parentField)
|
|
|
|
|
|
|
|
|
|
structValue := structPtrValue.Elem()
|
|
|
|
|
|
|
|
|
|
for i := 0; i < structValue.NumField(); i++ {
|
|
|
|
|
field := structType.Field(i)
|
|
|
|
|
fieldValue := structValue.Field(i)
|
|
|
|
|
|
2019-07-30 15:04:36 +02:00
|
|
|
if !fieldValue.CanSet() { // private field
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-21 19:55:01 +01:00
|
|
|
fieldMappingInfo := typeInf.fieldMappings[i]
|
|
|
|
|
|
|
|
|
|
switch fieldMappingInfo.Type {
|
2019-07-13 13:17:28 +02:00
|
|
|
|
2025-02-21 19:55:01 +01:00
|
|
|
case complexType:
|
2019-07-13 13:17:28 +02:00
|
|
|
var changed bool
|
2023-04-13 10:45:08 +02:00
|
|
|
changed, err = mapRowToDestinationValue(scanContext, concat(groupKey, ":", field.Name), fieldValue, &field)
|
2019-07-13 13:17:28 +02:00
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if changed {
|
|
|
|
|
updated = true
|
|
|
|
|
}
|
2025-02-21 19:55:01 +01:00
|
|
|
default:
|
|
|
|
|
if mapOnlySlices || fieldMappingInfo.rowIndex == -1 {
|
2019-07-13 13:17:28 +02:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-21 19:55:01 +01:00
|
|
|
scannedValue := scanContext.rowElemValue(fieldMappingInfo.rowIndex)
|
2019-07-13 13:17:28 +02:00
|
|
|
|
2022-02-09 12:34:10 +01:00
|
|
|
if !scannedValue.IsValid() {
|
|
|
|
|
setZeroValue(fieldValue) // scannedValue is nil, destination should be set to zero value
|
2019-10-12 18:45:09 +02:00
|
|
|
continue
|
|
|
|
|
}
|
2019-07-13 13:17:28 +02:00
|
|
|
|
2019-10-12 18:45:09 +02:00
|
|
|
updated = true
|
2019-07-13 13:17:28 +02:00
|
|
|
|
2025-02-21 19:55:01 +01:00
|
|
|
switch fieldMappingInfo.Type {
|
|
|
|
|
case implementsScanner:
|
2022-02-09 12:34:10 +01:00
|
|
|
initializeValueIfNilPtr(fieldValue)
|
|
|
|
|
fieldScanner := getScanner(fieldValue)
|
2019-07-13 13:17:28 +02:00
|
|
|
|
2022-02-09 12:34:10 +01:00
|
|
|
value := scannedValue.Interface()
|
|
|
|
|
|
|
|
|
|
err := fieldScanner.Scan(value)
|
2019-07-13 13:17:28 +02:00
|
|
|
|
|
|
|
|
if err != nil {
|
2025-02-21 19:55:01 +01:00
|
|
|
return updated, qrmAssignError(scannedValue, field, err)
|
|
|
|
|
}
|
|
|
|
|
case jsonUnmarshal:
|
|
|
|
|
value, ok := scannedValue.Interface().([]byte)
|
|
|
|
|
|
|
|
|
|
if !ok {
|
|
|
|
|
return updated, qrmAssignError(scannedValue, field, fmt.Errorf("value not convertable to []byte"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fieldInterface := fieldValue.Addr().Interface()
|
|
|
|
|
|
|
|
|
|
err := json.Unmarshal(value, fieldInterface)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
2025-03-09 17:46:34 +01:00
|
|
|
return updated, qrmAssignError(scannedValue, field, fmt.Errorf("invalid json, %w", err))
|
2019-07-13 13:17:28 +02:00
|
|
|
}
|
2025-02-21 19:55:01 +01:00
|
|
|
default: // simple type
|
2022-02-09 12:34:10 +01:00
|
|
|
err := assign(scannedValue, fieldValue)
|
2021-10-15 17:43:10 +02:00
|
|
|
|
|
|
|
|
if err != nil {
|
2025-02-21 19:55:01 +01:00
|
|
|
return updated, qrmAssignError(scannedValue, field, err)
|
2021-10-15 17:43:10 +02:00
|
|
|
}
|
2019-07-13 13:17:28 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return
|
2019-03-14 09:18:23 +01:00
|
|
|
}
|
|
|
|
|
|
2025-02-21 19:55:01 +01:00
|
|
|
func qrmAssignError(scannedValue reflect.Value, field reflect.StructField, err error) error {
|
|
|
|
|
return fmt.Errorf(`can't assign %T(%q) to '%s %s': %w`, scannedValue.Interface(), scannedValue.Interface(),
|
|
|
|
|
field.Name, field.Type.String(), err)
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-07 17:16:10 +01:00
|
|
|
func mapRowToDestinationValue(
|
2022-02-04 12:31:08 +01:00
|
|
|
scanContext *ScanContext,
|
2021-12-07 17:16:10 +01:00
|
|
|
groupKey string,
|
|
|
|
|
dest reflect.Value,
|
|
|
|
|
structField *reflect.StructField) (updated bool, err error) {
|
2019-03-14 09:18:23 +01:00
|
|
|
|
2019-05-20 17:37:55 +02:00
|
|
|
var destPtrValue reflect.Value
|
2019-03-14 09:18:23 +01:00
|
|
|
|
2019-05-20 17:37:55 +02:00
|
|
|
if dest.Kind() != reflect.Ptr {
|
|
|
|
|
destPtrValue = dest.Addr()
|
2019-08-13 13:57:26 +02:00
|
|
|
} else {
|
2019-05-20 17:37:55 +02:00
|
|
|
if dest.IsNil() {
|
|
|
|
|
destPtrValue = reflect.New(dest.Type().Elem())
|
2019-03-14 09:18:23 +01:00
|
|
|
} else {
|
2019-05-20 17:37:55 +02:00
|
|
|
destPtrValue = dest
|
2019-03-14 09:18:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-09 12:34:10 +01:00
|
|
|
updated, err = mapRowToDestinationPtr(scanContext, groupKey, destPtrValue, structField)
|
2019-05-20 17:37:55 +02:00
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if dest.Kind() == reflect.Ptr && dest.IsNil() && updated {
|
|
|
|
|
dest.Set(destPtrValue)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return
|
2019-03-05 18:55:47 +01:00
|
|
|
}
|
2019-10-12 18:45:09 +02:00
|
|
|
|
2021-12-07 17:16:10 +01:00
|
|
|
func mapRowToDestinationPtr(
|
2022-02-04 12:31:08 +01:00
|
|
|
scanContext *ScanContext,
|
2021-12-07 17:16:10 +01:00
|
|
|
groupKey string,
|
|
|
|
|
destPtrValue reflect.Value,
|
|
|
|
|
structField *reflect.StructField) (updated bool, err error) {
|
2019-10-12 18:45:09 +02:00
|
|
|
|
2023-07-21 14:11:31 +02:00
|
|
|
must.ValueBeOfTypeKind(destPtrValue, reflect.Ptr, "jet: internal error. Destination is not pointer.")
|
2019-10-12 18:45:09 +02:00
|
|
|
|
|
|
|
|
destValueKind := destPtrValue.Elem().Kind()
|
|
|
|
|
|
|
|
|
|
if destValueKind == reflect.Struct {
|
2022-02-09 12:34:10 +01:00
|
|
|
return mapRowToStruct(scanContext, groupKey, destPtrValue, structField)
|
2019-10-12 18:45:09 +02:00
|
|
|
} else if destValueKind == reflect.Slice {
|
2022-02-09 12:34:10 +01:00
|
|
|
return mapRowToSlice(scanContext, groupKey, destPtrValue, structField)
|
2019-10-12 18:45:09 +02:00
|
|
|
} else {
|
|
|
|
|
panic("jet: unsupported dest type: " + structField.Name + " " + structField.Type.String())
|
|
|
|
|
}
|
|
|
|
|
}
|