package db import ( "reflect" "strings" "testing" "unsafe" "github.com/stretchr/testify/assert" ) func TestPaths(t *testing.T) { type CustomInt int type S2 struct { B bool `db:"B"` // field 0 PB *bool `db:"PB"` // field 1 NoTag string // field 2 } type S struct { I int `db:"I"` // field 0 PI *int `db:"PI"` // field 1 CI CustomInt `db:"CI"` // field 2 PCI *CustomInt `db:"PCI"` // field 3 S2 `db:"S2"` // field 4 (embedded!) PS2 *S2 `db:"PS2"` // field 5 NoTag int // field 6 } type Nested struct { S S `db:"S"` // field 0 PS *S `db:"PS"` // field 1 NoTag S // field 2 } type Embedded struct { NoTag S // field 0 Nested // field 1 } names, paths := getColumnNamesAndPaths(reflect.TypeOf(Embedded{}), nil, nil) assert.Equal(t, []columnName{ {"S", "I"}, {"S", "PI"}, {"S", "CI"}, {"S", "PCI"}, {"S", "S2", "B"}, {"S", "S2", "PB"}, {"S", "PS2", "B"}, {"S", "PS2", "PB"}, {"PS", "I"}, {"PS", "PI"}, {"PS", "CI"}, {"PS", "PCI"}, {"PS", "S2", "B"}, {"PS", "S2", "PB"}, {"PS", "PS2", "B"}, {"PS", "PS2", "PB"}, }, names) assert.Equal(t, []fieldPath{ {1, 0, 0}, {1, 0, 1}, // Nested.S.I, Nested.S.PI {1, 0, 2}, {1, 0, 3}, // Nested.S.CI, Nested.S.PCI {1, 0, 4, 0}, {1, 0, 4, 1}, // Nested.S.S2.B, Nested.S.S2.PB {1, 0, 5, 0}, {1, 0, 5, 1}, // Nested.S.PS2.B, Nested.S.PS2.PB {1, 1, 0}, {1, 1, 1}, // Nested.PS.I, Nested.PS.PI {1, 1, 2}, {1, 1, 3}, // Nested.PS.CI, Nested.PS.PCI {1, 1, 4, 0}, {1, 1, 4, 1}, // Nested.PS.S2.B, Nested.PS.S2.PB {1, 1, 5, 0}, {1, 1, 5, 1}, // Nested.PS.PS2.B, Nested.PS.PS2.PB }, paths) assert.True(t, len(names) == len(paths)) testStruct := Embedded{} for i, path := range paths { val, field := followPathThroughStructs(reflect.ValueOf(&testStruct), path) assert.True(t, val.IsValid()) assert.True(t, strings.Contains(names[i][len(names[i])-1], field.Name)) } } func TestCompileQuery(t *testing.T) { t.Run("simple struct", func(t *testing.T) { type Dest struct { Foo int `db:"foo"` Bar bool `db:"bar"` Nope string // no tag } compiled := compileQuery("SELECT $columns FROM greeblies", reflect.TypeOf(Dest{})) assert.Equal(t, "SELECT foo, bar FROM greeblies", compiled.query) }) t.Run("complex structs", func(t *testing.T) { type CustomInt int type S2 struct { B bool `db:"B"` PB *bool `db:"PB"` NoTag string } type S struct { I int `db:"I"` PI *int `db:"PI"` CI CustomInt `db:"CI"` PCI *CustomInt `db:"PCI"` S2 `db:"S2"` // embedded! PS2 *S2 `db:"PS2"` NoTag int } type Nested struct { S S `db:"S"` PS *S `db:"PS"` NoTag S } type Dest struct { NoTag S Nested } compiled := compileQuery("SELECT $columns FROM greeblies", reflect.TypeOf(Dest{})) assert.Equal(t, "SELECT S.I, S.PI, S.CI, S.PCI, S_S2.B, S_S2.PB, S_PS2.B, S_PS2.PB, PS.I, PS.PI, PS.CI, PS.PCI, PS_S2.B, PS_S2.PB, PS_PS2.B, PS_PS2.PB FROM greeblies", compiled.query) }) t.Run("int", func(t *testing.T) { type Dest int // There should be no error here because we do not need to extract columns from // the destination type. There may be errors down the line in value iteration, but // that is always the case if the Go types don't match the query. compiled := compileQuery("SELECT id FROM greeblies", reflect.TypeOf(Dest(0))) assert.Equal(t, "SELECT id FROM greeblies", compiled.query) }) t.Run("just one table", func(t *testing.T) { type Dest struct { Foo int `db:"foo"` Bar bool `db:"bar"` Nope string // no tag } // The prefix is necessary because otherwise we would have to provide a struct with // a db tag in order to provide the query with the `greeblies.` prefix in the // final query. This comes up a lot when we do a JOIN to help with a condition, but // don't actually care about any of the data we joined to. compiled := compileQuery( "SELECT $columns{greeblies} FROM greeblies NATURAL JOIN props", reflect.TypeOf(Dest{}), ) assert.Equal(t, "SELECT greeblies.foo, greeblies.bar FROM greeblies NATURAL JOIN props", compiled.query) }) t.Run("using $columns without a struct is not allowed", func(t *testing.T) { type Dest int assert.Panics(t, func() { compileQuery("SELECT $columns FROM greeblies", reflect.TypeOf(Dest(0))) }) }) } func TestQueryBuilder(t *testing.T) { t.Run("happy time", func(t *testing.T) { var qb QueryBuilder qb.Add("SELECT stuff FROM thing WHERE foo = $? AND bar = $?", 3, "hello") qb.Add("AND (baz = $?)", true) assert.Equal(t, "SELECT stuff FROM thing WHERE foo = $1 AND bar = $2\nAND (baz = $3)\n", qb.String()) assert.Equal(t, []interface{}{3, "hello", true}, qb.Args()) }) t.Run("too few arguments", func(t *testing.T) { var qb QueryBuilder assert.Panics(t, func() { qb.Add("HELLO $? $? $?", 1, 2) }) }) t.Run("too many arguments", func(t *testing.T) { var qb QueryBuilder assert.Panics(t, func() { qb.Add("HELLO $? $? $?", 1, 2, 3, 4) }) }) } func TestSetValueFromDB(t *testing.T) { t.Run("ints", func(t *testing.T) { t.Run("int to int", func(t *testing.T) { var dest int var value int = 3 setValueFromDB(reflectPtr(&dest), reflect.ValueOf(value)) assert.Equal(t, 3, dest) }) t.Run("int32 to int", func(t *testing.T) { var dest int var value int32 = 3 setValueFromDB(reflectPtr(&dest), reflect.ValueOf(value)) assert.Equal(t, 3, dest) }) t.Run("int to *int", func(t *testing.T) { var dest *int var value int = 3 setValueFromDB(reflectPtr(&dest), reflect.ValueOf(value)) assert.Equal(t, 3, *dest) }) t.Run("int32 to *int", func(t *testing.T) { var dest *int var value int32 = 3 setValueFromDB(reflectPtr(&dest), reflect.ValueOf(value)) assert.Equal(t, 3, *dest) }) t.Run("pointer nil to *int", func(t *testing.T) { var dest *int var value *int = nil setValueFromDB(reflectPtr(&dest), reflect.ValueOf(value)) assert.Nil(t, dest) }) t.Run("interface nil to *int", func(t *testing.T) { var dest *int var value interface{} = nil setValueFromDB(reflectPtr(&dest), reflect.ValueOf(value)) assert.Nil(t, dest) }) }) t.Run("strings", func(t *testing.T) { type myString string t.Run("string to string", func(t *testing.T) { var dest string var value string = "handmade" setValueFromDB(reflectPtr(&dest), reflect.ValueOf(value)) assert.Equal(t, "handmade", dest) }) t.Run("custom string to string", func(t *testing.T) { var dest string var value myString = "handmade" setValueFromDB(reflectPtr(&dest), reflect.ValueOf(value)) assert.Equal(t, "handmade", dest) }) t.Run("string to *string", func(t *testing.T) { var dest *string var value string = "handmade" setValueFromDB(reflectPtr(&dest), reflect.ValueOf(value)) assert.Equal(t, "handmade", *dest) }) t.Run("custom string to *int", func(t *testing.T) { var dest *string var value myString = "handmade" setValueFromDB(reflectPtr(&dest), reflect.ValueOf(value)) assert.Equal(t, "handmade", *dest) }) t.Run("pointer nil to *string", func(t *testing.T) { var dest *string var value *string = nil setValueFromDB(reflectPtr(&dest), reflect.ValueOf(value)) assert.Nil(t, dest) }) t.Run("interface nil to *string", func(t *testing.T) { var dest *string var value interface{} = nil setValueFromDB(reflectPtr(&dest), reflect.ValueOf(value)) assert.Nil(t, dest) }) }) } func reflectPtr[T any](dest *T) reflect.Value { return reflect.NewAt(reflect.TypeOf(*dest), unsafe.Pointer(dest)).Elem() }