minus5 / gofreetds

Go Sql Server database driver.
MIT License
113 stars 48 forks source link

Support Scan for single column by it's name #45

Closed daliborfilus closed 7 years ago

daliborfilus commented 7 years ago

Used for cases where you don't exactly know what fields will be returned (maybe the DBA adds new columns in the middle, or someone changes single table which affects tens of views/procedures).

Scan to struct should solve that too, but Scan with struct has a limitation in that the column name must begin with upper case (because exports...) and cannot contain spaces and other common problems.

Other DB adapters try to use tags for those cases, but we don't have support for that and this was way simpler. And it uses less magic.

Usage (as can be seen in tests):

                var s string
        err = r.ScanColumn("some_column", &s)
        if err != nil {
            return err
        }

P.S. I am little worried about performance of this. As you can see in result.go in new method FindColumn, there is a TODO for using hash map as a cache of column name to it's array index in r.Columns. But I tried that and it was 2 times slower than simple array seq scan, so the TODO is now obsolete.

What do you think?

P.P.S. Also, add CurrentRow() helper method for those guys who actually want to know what row we are on. With that, people can implement your own Row parsing and can use Next() at the same time. (Without CurrentRow, we would need to use for i, row := range r.Rows ...)

daliborfilus commented 7 years ago

For more ergonomic use here is a wrapper for handling failure of this:

package db

import (
    "github.com/NoICE/gofreetds"
    "errors"
)

type ScanMap struct {
    ColumnMapping map[string]interface{}
}

func NewScanMap() *ScanMap {
    var r = new(ScanMap)
    r.ColumnMapping = make(map[string]interface{})
    return r
}

// Map registers new mapping for given column to given destination for Scan to use.
func (s *ScanMap) Map(name string, dest interface{}) {
    s.ColumnMapping[name] = dest
}

// Scan all defined fields by mappings registered with Map.
// Returns error if any single scan fails.
// Result has to be initialized first (using Next() or similar).
func (s *ScanMap) Scan(r *freetds.Result) (err error) {
    if s.ColumnMapping == nil {
        return errors.New("ScanMap.Scan() called without adding any mappings")
    }

    if r == nil {
        return errors.New("ScanMap.Scan() called with nil result")
    }

    for name, dest := range s.ColumnMapping {
        err = r.ScanColumn(name, dest)
        if err != nil {
            return
        }
    }
    return
}

Which is then used like this:

func ParseSomething(result *freetds.Result) (tmp Something, err error) {
    var sm = db.NewScanMap()
    sm.Map("RatingName", &tmp.Rating.NameValue)
    sm.Map("ImageSource", &tmp.ImageSource.ImageSource)
    sm.Map("ImageName", &tmp.ImageSource.Name)
    sm.Map("ImageAlterativeText", &tmp.ImageSource.AlterativeText)
    sm.Map("UserRating", &tmp.UserRating)
    err = sm.Scan(result)
    return
}