Implement abstract raw query execution in Golang

K Srijeyanthan
4 min readMay 28, 2021

Golang is becoming a de facto standard to implement more advanced application backends like distributed microservices, streaming services, etc. For instance, Docker is fully crafted in Go programming. This article is not mainly explaining the features of Go, rather it explains writing SQL abstraction layer to generalize raw query execution.

There are various libraries available to work with RDBMS systems in Go, like gorm(https://gorm.io/index.html) or xorm (https://xorm.io/xorm/), these libraries are providing full-featured ORM to work with RDBMS systems, but there are few use cases where you need to have raw SQL access and process data.

For example, assume that we need to execute a random query that is constructed from user HTTP request dynamic parameters like below

The query was constructed based on a request

How can we use these ORM libraries to process queries like this, and execute them in a general way, here I am going to explain two different functions that provide convert row results in a Go map and an array of the map?

Convert entire table result into an array of map

Here is the idea is to execute the raw query and iterate the results to build the array of interface maps, then that can be marshaled using JSON encoder libraries to build JSON response.

type DbLayer struct{         dbConnection          *gorm.DB}//  since we don't know the result structure, we will keep it in map of interface structuretype Params map[string]interface{}func (dl *DbLayer) QueryRowsToListOfMap(rawQuery string) (int64, error, []Params) {

var arrayOfMaps []util.Params
//execute the raw query
rs, err := dl.dbConnection.Raw(rawQuery).Rows()

if err != nil {
return 0, err, arrayOfMaps
}

var (
refs []interface{}
cnt int64
)

for rs.Next() {
var rowMapData util.Params
rowMapData = make(util.Params)
columns, _ := rs.Columns()
refs = make([]interface{}, len(columns))
for i := range refs {
var ref sql.NullString
refs[i] = &ref
}

if err := rs.Scan(refs...); err != nil {
return 0, err, arrayOfMaps
}
for i := range refs {
value := reflect.Indirect(reflect.ValueOf(refs[i])).Interface().(sql.NullString)
// keep the results in key value
rowMapData[columns[i]] = value.String

}
// constructing the array of map
arrayOfMaps = append(arrayOfMaps, rowMapData)
cnt++
}

return cnt, nil, arrayOfMaps
}
Assume that we need to get the only col and value only

In this function, the user can define the interesting column they need, then it will be queried using a raw query to build the response map.

func (dl *DbLayer) queryRowsToMap(rawQuery string, container interface{}, keyCol, valueCol string) (int64, error) {
var (
maps util.Params
ind *reflect.Value
)

var typ int
switch container.(type) {
case *util.Params:
typ = 1
default:
typ = 2
vl := reflect.ValueOf(container)
id := reflect.Indirect(vl)
if vl.Kind() != reflect.Ptr || id.Kind() != reflect.Struct {
panic(fmt.Errorf("<RawSeter> RowsTo unsupport type `%T` need ptr struct", container))
}

ind = &id
}

rs, err := dl.dbConnectio.Raw(rawQuery).Rows()

if err != nil {
return 0, err
}

var (
refs []interface{}
cnt int64
cols []string
)

var (
keyIndex = -1
valueIndex = -1
)

for rs.Next() {
if cnt == 0 {
columns, err := rs.Columns()
if err != nil {
return 0, err
}
cols = columns
refs = make([]interface{}, len(cols))
for i := range refs {
fmt.Println("cols :", cols[i])
if keyCol == cols[i] {
keyIndex = i
}
if typ == 1 || keyIndex == i {
var ref sql.NullString
refs[i] = &ref
} else {
var ref interface{}
refs[i] = &ref
}
if valueCol == cols[i] {
valueIndex = i
}
}
if keyIndex == -1 || valueIndex == -1 {
panic(fmt.Errorf("<RawSeter> RowsTo unknown key, value column name `%s: %s`", keyCol, valueCol))
}
}

if err := rs.Scan(refs...); err != nil {
return 0, err
}

if cnt == 0 {
switch typ {
case 1:
maps = make(util.Params)
}
}
key := reflect.Indirect(reflect.ValueOf(refs[keyIndex])).Interface().(sql.NullString).String
switch typ {
case 1:
value := reflect.Indirect(reflect.ValueOf(refs[valueIndex])).Interface().(sql.NullString)
if value.Valid {
maps[key] = value.String
} else {
maps[key] = nil
}

default:
if id := ind.FieldByName(util.CamelString(key)); id.IsValid() {
ab.setFieldValue(id, reflect.ValueOf(refs[valueIndex]).Elem().Interface())
}
}

cnt++
}

if typ == 1 {
v, _ := container.(*util.Params)
*v = maps
}

return cnt, nil
}
// helper function to set the field value based on data typefunc (ab *AssetBase) setFieldValue(ind reflect.Value, value interface{}) {
switch ind.Kind() {
case reflect.Bool:
if value == nil {
ind.SetBool(false)
} else if v, ok := value.(bool); ok {
ind.SetBool(v)
} else {
v, _ := util.StrTo(ToStr(value)).Bool()
ind.SetBool(v)
}

case reflect.String:
if value == nil {
ind.SetString("")
} else {
ind.SetString(ToStr(value))
}

case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if value == nil {
ind.SetInt(0)
} else {
val := reflect.ValueOf(value)
switch val.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
ind.SetInt(val.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
ind.SetInt(int64(val.Uint()))
default:
v, _ := util.StrTo(ToStr(value)).Int64()
ind.SetInt(v)
}
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if value == nil {
ind.SetUint(0)
} else {
val := reflect.ValueOf(value)
switch val.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
ind.SetUint(uint64(val.Int()))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
ind.SetUint(val.Uint())
default:
v, _ := util.StrTo(ToStr(value)).Uint64()
ind.SetUint(v)
}
}
case reflect.Float64, reflect.Float32:
if value == nil {
ind.SetFloat(0)
} else {
val := reflect.ValueOf(value)
switch val.Kind() {
case reflect.Float64:
ind.SetFloat(val.Float())
default:
v, _ := util.StrTo(ToStr(value)).Float64()
ind.SetFloat(v)
}
}

case sql.NullString, sql.NullInt64, sql.NullFloat64, sql.NullBool:
indi := reflect.New(ind.Type()).Interface()
sc, ok := indi.(sql.Scanner)
if !ok {
return
}
err := sc.Scan(value)
if err == nil {
ind.Set(reflect.Indirect(reflect.ValueOf(sc)))
}
}

case reflect.Ptr:
if value == nil {
ind.Set(reflect.Zero(ind.Type()))
break
}
ind.Set(reflect.New(ind.Type().Elem()))
ab.setFieldValue(reflect.Indirect(ind), value)
}
}

We can execute the above function like below

responseMap Params
numberOfRecords, err := dbLayer.queryRowsToMap("select A,B from Table 1",responseMap, "A","B")
fmt.Println(responseMap)

as shown in this example, we can use these functions to generalize our raw queries apart from ORM functionalities.

References : https://gorm.io/docs/sql_builder.html

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

K Srijeyanthan
K Srijeyanthan

Written by K Srijeyanthan

I am Sri, an experienced Software Engineer and Entrepreneur, Passionate about distributed systems, and low latency application developments.

No responses yet

Write a response