go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/db/populate.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package db
     9  
    10  import (
    11  	"database/sql"
    12  	"fmt"
    13  	"reflect"
    14  )
    15  
    16  // PopulateByName sets the values of an object from the values of a sql.Rows object using column names.
    17  func PopulateByName(object any, row Rows, cols *TypeMeta) error {
    18  	rowColumns, err := row.Columns()
    19  	if err != nil {
    20  		return err
    21  	}
    22  
    23  	var values = make([]interface{}, len(rowColumns))
    24  	var columnLookup = cols.Lookup()
    25  	for i, name := range rowColumns {
    26  		if col, ok := columnLookup[name]; ok {
    27  			initColumnValue(i, values, col)
    28  		} else {
    29  			var value any
    30  			values[i] = &value
    31  		}
    32  	}
    33  
    34  	err = row.Scan(values...)
    35  	if err != nil {
    36  		return err
    37  	}
    38  
    39  	var colName string
    40  	var field *Column
    41  	var ok bool
    42  
    43  	objectValue := reflectValue(object)
    44  	for i, v := range values {
    45  		colName = rowColumns[i]
    46  		if field, ok = columnLookup[colName]; ok {
    47  			err = field.SetValueReflected(objectValue, v)
    48  			if err != nil {
    49  				return err
    50  			}
    51  		}
    52  	}
    53  
    54  	return nil
    55  }
    56  
    57  // IsPopulatable returns if an object is populatable
    58  func IsPopulatable(object any) (isPopulatable bool) {
    59  	_, isPopulatable = object.(Populatable)
    60  	return
    61  }
    62  
    63  // IsScanner returns if an scalar value is scannable
    64  func IsScanner(object any) (isScanner bool) {
    65  	_, isScanner = object.(sql.Scanner)
    66  	return
    67  }
    68  
    69  // PopulateInOrder sets the values of an object in order from a sql.Rows object.
    70  // Only use this method if you're certain of the column order. It is faster than populateByName.
    71  // Optionally if your object implements Populatable this process will be skipped completely, which is even faster.
    72  func PopulateInOrder(object any, row Scanner, cols []*Column) (err error) {
    73  	var values = make([]interface{}, len(cols))
    74  
    75  	for i, col := range cols {
    76  		initColumnValue(i, values, col)
    77  	}
    78  	if err = row.Scan(values...); err != nil {
    79  		return err
    80  	}
    81  
    82  	objectValue := reflectValue(object)
    83  	var field *Column
    84  	for i, v := range values {
    85  		field = cols[i]
    86  		if err = field.SetValueReflected(objectValue, v); err != nil {
    87  			return
    88  		}
    89  	}
    90  
    91  	return
    92  }
    93  
    94  // Zero resets an object.
    95  func Zero(object any) error {
    96  	objectValue := reflect.ValueOf(object)
    97  	if !objectValue.Elem().CanSet() {
    98  		return fmt.Errorf("zero; cannot set object, did you pass a reference?")
    99  	}
   100  	objectValue.Elem().Set(reflect.Zero(objectValue.Type().Elem()))
   101  	return nil
   102  }
   103  
   104  // initColumnValue inserts the correct placeholder in the scan array of values.
   105  // it will use `sql.Null` forms where appropriate.
   106  // JSON fields are implicitly nullable.
   107  func initColumnValue(index int, values []interface{}, col *Column) {
   108  	if col.IsJSON {
   109  		values[index] = &sql.NullString{}
   110  	} else if col.FieldType.Kind() == reflect.Ptr {
   111  		values[index] = reflect.New(col.FieldType).Interface()
   112  	} else {
   113  		values[index] = reflect.New(reflect.PtrTo(col.FieldType)).Interface()
   114  	}
   115  }