11 Commits

Author SHA1 Message Date
f915efb7e5 Merge pull request 'new/null-types' (#2) from new/null-types into main
Reviewed-on: #2
2026-01-23 19:59:05 +00:00
f67a02a9fe adding comments 2026-01-23 14:58:51 -05:00
88e67d69fc removing debug prints 2026-01-23 14:56:28 -05:00
9719cde88b cleanup
- removing test main
- adding files to gitignore to prevent future pushes
2026-01-23 14:54:28 -05:00
8f950e38b8 comment updated 2026-01-23 14:52:29 -05:00
e8ceac6f88 added checks for sql null types 2026-01-16 15:59:44 -05:00
7fd2f60168 simplifying logic 2026-01-16 10:34:05 -05:00
b4428e397e using proper struct for nulltypes 2026-01-16 10:31:40 -05:00
7ce271bdb7 [test] Adding test case for nulls
- ensuring nulls work
- will be undone
2026-01-16 10:27:57 -05:00
4ec738b130 type and bind updates
- using sql nullTypes
- adding additional bind type option
2026-01-16 10:18:24 -05:00
459eaca866 poc for sql null types 2026-01-15 22:53:05 -05:00
2 changed files with 110 additions and 105 deletions

10
.gitignore vendored
View File

@@ -15,15 +15,7 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
#ignore test files
cmd/
cmd/*

205
lazy.go
View File

@@ -1,12 +1,14 @@
package lazy
import (
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"math/rand/v2"
"reflect"
"regexp"
"time"
"github.com/jmoiron/sqlx"
)
@@ -15,71 +17,61 @@ const (
DB_TAG = "db" //tag used to parse sql fields in example struct
LAZY_TAG = "lazy" //tag label for KEY_VALUE
KEY_VALUE = "key" //tag value for LAZY_TAG
//bind values
BindQuestion Binds = 1
BindDollar Binds = 2
BindNamed Binds = 3
BindAt Binds = 4
)
var (
ErrBadExample = errors.New("example field must be a non-nil struct")
ErrMissingKeys = errors.New("there must be a key for each row requested with RowCount")
//contains placeholder to inject type on runtime
ErrUnsupportedType = errors.New("type: %s is not yet supported")
)
type Binds int
// mock data generated based on config
type RowMock struct {
type Mock struct {
Query string
Columns []string
Rows [][]driver.Value
}
// configuration for RandomGenerate
type RowConfig struct {
type Config struct {
Query string
Example any
Keys []any
Keys []any //length of keys must = RowCount, if set
RowCount int
}
type StructConfig struct {
Query string
Example any
RowCount int
}
type StructMock struct {
Query string
MockStructs []any
BindType Binds
}
// generates mock data based on configuration to be used for sqlmock
func RandomRows(r RowConfig) (*RowMock, error) {
func RandomGenerate(m Config) (*Mock, error) {
//example struct cannot be nil and must be a struct
if r.Example == nil {
return nil, ErrBadExample
if m.Example == nil {
return nil, errors.New("example value cannot be nil")
}
if reflect.ValueOf(r.Example).Kind() != reflect.Struct {
return nil, ErrBadExample
if reflect.ValueOf(m.Example).Kind() != reflect.Struct {
return nil, errors.New("example value must be a struct")
}
//any weirdness, just pull one row
if r.RowCount <= 0 {
r.RowCount = 1
if m.RowCount <= 0 {
m.RowCount = 1
}
//if keys are set, ensure there are enough to populate requested rows
primaryKey := false
if len(r.Keys) > 0 {
if len(m.Keys) > 0 {
primaryKey = true
if len(r.Keys) != r.RowCount {
return nil, ErrMissingKeys
if len(m.Keys) != m.RowCount {
return nil, errors.New("you must provide a key for each row")
}
}
retType := reflect.TypeOf(r.Example)
retType := reflect.TypeOf(m.Example)
maxFieldCount := retType.NumField()
columns := make([]string, 0, maxFieldCount)
rows := make([][]driver.Value, 0)
for y := 0; y < r.RowCount; y++ {
for y := 0; y < m.RowCount; y++ {
rows = append(rows, make([]driver.Value, 0))
for x := 0; x < maxFieldCount; x++ {
field := retType.Field(x)
@@ -95,86 +87,94 @@ func RandomRows(r RowConfig) (*RowMock, error) {
//if field has lazy:"key" tag and tags are present inject primary key
if field.Tag.Get(LAZY_TAG) == KEY_VALUE && primaryKey {
rows[y] = append(rows[y], r.Keys[y])
rows[y] = append(rows[y], m.Keys[y])
continue
}
//generate random values
nv := kindToRandom(field)
if nv == nil {
return nil, fmt.Errorf(ErrUnsupportedType.Error(), field.Type.Name())
return nil, fmt.Errorf("could not match type: %s", retType.Name())
}
rows[y] = append(rows[y], nv)
}
}
return &RowMock{
return &Mock{
//sql is rebound and escaped for sqlmock.ExpectQuery
Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(r.Query)),
Query: sqlx.Rebind(int(m.BindType), regexp.QuoteMeta(m.Query)),
Columns: columns,
Rows: rows,
}, nil
}
// generates mock data for insert statements to be used for sqlmock, it also returns a slice of
// example structs with mock data
func RandomStruct(c StructConfig) (*StructMock, error) {
//make sure we have an example struct
if c.Example == nil {
return nil, ErrBadExample
}
if reflect.ValueOf(c.Example).Kind() != reflect.Struct {
return nil, ErrBadExample
}
//we need at least one row
if c.RowCount <= 0 {
c.RowCount = 1
}
//get example type and number of fields
retType := reflect.TypeOf(c.Example)
maxFieldCount := retType.NumField()
//create slice of example structs
ft := reflect.SliceOf(retType)
filled := reflect.MakeSlice(ft, 0, c.RowCount)
for x := 0; x < c.RowCount; x++ {
//add empty example struct
filled = reflect.Append(filled, reflect.ValueOf(c.Example))
for y := 0; y < maxFieldCount; y++ {
field := retType.Field(y)
//get random value
nv := kindToRandom(field)
if nv == nil {
return nil, fmt.Errorf(ErrUnsupportedType.Error(), field.Type.Name())
}
if filled.Index(x).Field(y).CanSet() {
filled.Index(x).Field(y).Set(reflect.ValueOf(nv))
}
}
}
//convert reflect.Value to []any
fl := filled.Len()
retStructs := make([]any, fl)
for x := 0; x < fl; x++ {
retStructs[x] = filled.Index(x).Interface()
}
return &StructMock{
Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(c.Query)),
MockStructs: retStructs,
}, nil
}
// converts basic reflect Kind's to psuedo-random data, it is super basic and supports very basic types. Slices and arrays
// are supported, slices will get 1-10 entries and arrays will fill length
// converts basic reflect Kind's to psuedo-random data, slices are given a random amount of entries
func kindToRandom(field reflect.StructField) any {
//this isn't ideal, but since the sql types are structs they were being filled in a weird
//manner. For now we check those types first, and will clean this up later.
switch field.Type {
case reflect.TypeOf(sql.NullTime{}):
var temp sql.NullTime
if !isNull() {
temp.Valid = true
temp.Time = time.Now()
}
return temp
case reflect.TypeOf(sql.NullInt16{}):
var temp sql.NullInt16
if isNull() {
temp.Valid = false
} else {
temp.Valid = true
temp.Int16 = int16(rand.Int())
}
return temp
case reflect.TypeOf(sql.NullInt32{}):
var temp sql.NullInt32
if isNull() {
temp.Valid = false
} else {
temp.Valid = true
temp.Int32 = rand.Int32()
}
return temp
case reflect.TypeOf(sql.NullInt64{}):
var temp sql.NullInt64
if isNull() {
temp.Valid = false
} else {
temp.Valid = true
temp.Int64 = rand.Int64()
}
return temp
case reflect.TypeOf(sql.NullBool{}):
var temp sql.NullBool
if isNull() {
temp.Valid = false
} else {
temp.Valid = true
temp.Bool = (rand.Int()%2 == 0)
}
return temp
case reflect.TypeOf(sql.NullFloat64{}):
var temp sql.NullFloat64
if isNull() {
temp.Valid = false
} else {
temp.Valid = true
temp.Float64 = rand.Float64()
}
return temp
case reflect.TypeOf(sql.NullString{}):
var temp sql.NullString
if isNull() {
temp.Valid = false
} else {
temp.Valid = true
temp.String = fmt.Sprintf("random %d", rand.Int())
}
return temp
}
kind := field.Type.Kind()
switch kind {
case reflect.Int:
@@ -188,9 +188,11 @@ func kindToRandom(field reflect.StructField) any {
case reflect.Float64:
return rand.Float64()
case reflect.String:
return fmt.Sprintf("%d", rand.Int())
return fmt.Sprintf("random %d", rand.Int())
case reflect.Bool:
return rand.Int()%2 == 0
case reflect.TypeOf(time.Time{}).Kind():
return time.Now()
case reflect.Array:
underlying := field.Type.Elem().Kind()
count := reflect.ValueOf(field).Len() //fill entire length
@@ -275,7 +277,7 @@ func kindToRandom(field reflect.StructField) any {
case reflect.String:
var strslice []string
for x := 0; x < count; x++ {
strslice = append(strslice, fmt.Sprintf("%d", rand.Int()))
strslice = append(strslice, fmt.Sprintf("random %d", rand.Int()))
}
return strslice
case reflect.Bool:
@@ -289,3 +291,14 @@ func kindToRandom(field reflect.StructField) any {
return nil
}
// a quick and dirty rng function to determine if nulltypes are null or
// if we will generate a random value
func isNull() bool {
x := rand.IntN(2)
if x%2 == 0 {
return true
} else {
return false
}
}