├── .travis.yml ├── README.md ├── .gitignore ├── LICENSE ├── sqlstruct_test.go └── sqlstruct.go /.travis.yml: -------------------------------------------------------------------------------- 1 | arch: 2 | - amd64 3 | - ppc64le 4 | language: go 5 | 6 | go: 7 | - 1.2 8 | - tip 9 | jobs: 10 | exclude: 11 | - go: 1.2 12 | arch: ppc64le 13 | - go: 1.2 14 | arch: amd64 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sqlstruct 2 | ========= 3 | 4 | sqlstruct provides some convenience functions for using structs with go's database/sql package 5 | 6 | Documentation can be found at http://godoc.org/github.com/kisielk/sqlstruct 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Kamil Kisiel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /sqlstruct_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Kamil Kisiel. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license which can be found in the LICENSE file. 4 | package sqlstruct 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | type EmbeddedType struct { 12 | FieldE string `sql:"field_e"` 13 | } 14 | 15 | type testType struct { 16 | FieldA string `sql:"field_a"` 17 | FieldB string `sql:"-"` // Ignored 18 | FieldC string `sql:"field_C"` // Different letter case 19 | Field_D string // Field name is used 20 | EmbeddedType 21 | } 22 | 23 | type testType2 struct { 24 | FieldA string `sql:"field_a"` 25 | FieldSec string `sql:"field_sec"` 26 | } 27 | 28 | // testRows is a mock version of sql.Rows which can only scan strings 29 | type testRows struct { 30 | columns []string 31 | values []interface{} 32 | } 33 | 34 | func (r testRows) Scan(dest ...interface{}) error { 35 | for i := range r.values { 36 | v := reflect.ValueOf(dest[i]) 37 | if v.Kind() != reflect.Ptr { 38 | panic("Not a pointer!") 39 | } 40 | 41 | switch dest[i].(type) { 42 | case *string: 43 | *(dest[i].(*string)) = r.values[i].(string) 44 | default: 45 | // Do nothing. We assume the tests only use strings here 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | func (r testRows) Columns() ([]string, error) { 52 | return r.columns, nil 53 | } 54 | 55 | func (r *testRows) addValue(c string, v interface{}) { 56 | r.columns = append(r.columns, c) 57 | r.values = append(r.values, v) 58 | } 59 | 60 | func TestColumns(t *testing.T) { 61 | var v testType 62 | e := "field_a, field_c, field_d, field_e" 63 | c := Columns(v) 64 | 65 | if c != e { 66 | t.Errorf("expected %q got %q", e, c) 67 | } 68 | } 69 | 70 | func TestColumnsAliased(t *testing.T) { 71 | var t1 testType 72 | var t2 testType2 73 | 74 | expected := "t1.field_a AS t1_field_a, t1.field_c AS t1_field_c, " 75 | expected += "t1.field_d AS t1_field_d, t1.field_e AS t1_field_e" 76 | actual := ColumnsAliased(t1, "t1") 77 | 78 | if expected != actual { 79 | t.Errorf("Expected %q got %q", expected, actual) 80 | } 81 | 82 | expected = "t2.field_a AS t2_field_a, t2.field_sec AS t2_field_sec" 83 | actual = ColumnsAliased(t2, "t2") 84 | 85 | if expected != actual { 86 | t.Errorf("Expected %q got %q", expected, actual) 87 | } 88 | } 89 | 90 | func TestScan(t *testing.T) { 91 | rows := testRows{} 92 | rows.addValue("field_a", "a") 93 | rows.addValue("field_b", "b") 94 | rows.addValue("field_c", "c") 95 | rows.addValue("field_d", "d") 96 | rows.addValue("field_e", "e") 97 | 98 | e := testType{"a", "", "c", "d", EmbeddedType{"e"}} 99 | 100 | var r testType 101 | err := Scan(&r, rows) 102 | if err != nil { 103 | t.Errorf("unexpected error: %s", err) 104 | } 105 | 106 | if r != e { 107 | t.Errorf("expected %q got %q", e, r) 108 | } 109 | } 110 | 111 | func TestScanAliased(t *testing.T) { 112 | rows := testRows{} 113 | rows.addValue("t1_field_a", "a") 114 | rows.addValue("t1_field_b", "b") 115 | rows.addValue("t1_field_c", "c") 116 | rows.addValue("t1_field_d", "d") 117 | rows.addValue("t1_field_e", "e") 118 | rows.addValue("t2_field_a", "a2") 119 | rows.addValue("t2_field_sec", "sec") 120 | 121 | expected := testType{"a", "", "c", "d", EmbeddedType{"e"}} 122 | var actual testType 123 | err := ScanAliased(&actual, rows, "t1") 124 | if err != nil { 125 | t.Errorf("unexpected error: %s", err) 126 | } 127 | 128 | if expected != actual { 129 | t.Errorf("expected %q got %q", expected, actual) 130 | } 131 | 132 | expected2 := testType2{"a2", "sec"} 133 | var actual2 testType2 134 | 135 | err = ScanAliased(&actual2, rows, "t2") 136 | if err != nil { 137 | t.Errorf("unexpected error: %s", err) 138 | } 139 | 140 | if expected2 != actual2 { 141 | t.Errorf("expected %q got %q", expected2, actual2) 142 | } 143 | } 144 | 145 | func TestToSnakeCase(t *testing.T) { 146 | var s string 147 | s = ToSnakeCase("FirstName") 148 | if "first_name" != s { 149 | t.Errorf("expected first_name got %q", s) 150 | } 151 | 152 | s = ToSnakeCase("First") 153 | if "first" != s { 154 | t.Errorf("expected first got %q", s) 155 | } 156 | 157 | s = ToSnakeCase("firstName") 158 | if "first_name" != s { 159 | t.Errorf("expected first_name got %q", s) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /sqlstruct.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Kamil Kisiel. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license which can be found in the LICENSE file. 4 | 5 | /* 6 | Package sqlstruct provides some convenience functions for using structs with 7 | the Go standard library's database/sql package. 8 | 9 | The package matches struct field names to SQL query column names. A field can 10 | also specify a matching column with "sql" tag, if it's different from field 11 | name. Unexported fields or fields marked with `sql:"-"` are ignored, just like 12 | with "encoding/json" package. 13 | 14 | For example: 15 | 16 | type T struct { 17 | F1 string 18 | F2 string `sql:"field2"` 19 | F3 string `sql:"-"` 20 | } 21 | 22 | rows, err := db.Query(fmt.Sprintf("SELECT %s FROM tablename", sqlstruct.Columns(T{}))) 23 | ... 24 | 25 | for rows.Next() { 26 | var t T 27 | err = sqlstruct.Scan(&t, rows) 28 | ... 29 | } 30 | 31 | err = rows.Err() // get any errors encountered during iteration 32 | 33 | Aliased tables in a SQL statement may be scanned into a specific structure identified 34 | by the same alias, using the ColumnsAliased and ScanAliased functions: 35 | 36 | type User struct { 37 | Id int `sql:"id"` 38 | Username string `sql:"username"` 39 | Email string `sql:"address"` 40 | Name string `sql:"name"` 41 | HomeAddress *Address `sql:"-"` 42 | } 43 | 44 | type Address struct { 45 | Id int `sql:"id"` 46 | City string `sql:"city"` 47 | Street string `sql:"address"` 48 | } 49 | 50 | ... 51 | 52 | var user User 53 | var address Address 54 | sql := ` 55 | SELECT %s, %s FROM users AS u 56 | INNER JOIN address AS a ON a.id = u.address_id 57 | WHERE u.username = ? 58 | ` 59 | sql = fmt.Sprintf(sql, sqlstruct.ColumnsAliased(*user, "u"), sqlstruct.ColumnsAliased(*address, "a")) 60 | rows, err := db.Query(sql, "gedi") 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | defer rows.Close() 65 | if rows.Next() { 66 | err = sqlstruct.ScanAliased(&user, rows, "u") 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | err = sqlstruct.ScanAliased(&address, rows, "a") 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | user.HomeAddress = address 75 | } 76 | fmt.Printf("%+v", *user) 77 | // output: "{Id:1 Username:gedi Email:gediminas.morkevicius@gmail.com Name:Gedas HomeAddress:0xc21001f570}" 78 | fmt.Printf("%+v", *user.HomeAddress) 79 | // output: "{Id:2 City:Vilnius Street:Plento 34}" 80 | 81 | */ 82 | package sqlstruct 83 | 84 | import ( 85 | "bytes" 86 | "database/sql" 87 | "fmt" 88 | "reflect" 89 | "sort" 90 | "strings" 91 | "sync" 92 | ) 93 | 94 | // NameMapper is the function used to convert struct fields which do not have sql tags 95 | // into database column names. 96 | // 97 | // The default mapper converts field names to lower case. If instead you would prefer 98 | // field names converted to snake case, simply assign sqlstruct.ToSnakeCase to the variable: 99 | // 100 | // sqlstruct.NameMapper = sqlstruct.ToSnakeCase 101 | // 102 | // Alternatively for a custom mapping, any func(string) string can be used instead. 103 | var NameMapper func(string) string = strings.ToLower 104 | 105 | // A cache of fieldInfos to save reflecting every time. Inspried by encoding/xml 106 | var finfos map[reflect.Type]fieldInfo 107 | var finfoLock sync.RWMutex 108 | 109 | // TagName is the name of the tag to use on struct fields 110 | var TagName = "sql" 111 | 112 | // fieldInfo is a mapping of field tag values to their indices 113 | type fieldInfo map[string][]int 114 | 115 | func init() { 116 | finfos = make(map[reflect.Type]fieldInfo) 117 | } 118 | 119 | // Rows defines the interface of types that are scannable with the Scan function. 120 | // It is implemented by the sql.Rows type from the standard library 121 | type Rows interface { 122 | Scan(...interface{}) error 123 | Columns() ([]string, error) 124 | } 125 | 126 | // getFieldInfo creates a fieldInfo for the provided type. Fields that are not tagged 127 | // with the "sql" tag and unexported fields are not included. 128 | func getFieldInfo(typ reflect.Type) fieldInfo { 129 | finfoLock.RLock() 130 | finfo, ok := finfos[typ] 131 | finfoLock.RUnlock() 132 | if ok { 133 | return finfo 134 | } 135 | 136 | finfo = make(fieldInfo) 137 | 138 | n := typ.NumField() 139 | for i := 0; i < n; i++ { 140 | f := typ.Field(i) 141 | tag := f.Tag.Get(TagName) 142 | 143 | // Skip unexported fields or fields marked with "-" 144 | if f.PkgPath != "" || tag == "-" { 145 | continue 146 | } 147 | 148 | // Handle embedded structs 149 | if f.Anonymous && f.Type.Kind() == reflect.Struct { 150 | for k, v := range getFieldInfo(f.Type) { 151 | finfo[k] = append([]int{i}, v...) 152 | } 153 | continue 154 | } 155 | 156 | // Use field name for untagged fields 157 | if tag == "" { 158 | tag = f.Name 159 | } 160 | tag = NameMapper(tag) 161 | 162 | finfo[tag] = []int{i} 163 | } 164 | 165 | finfoLock.Lock() 166 | finfos[typ] = finfo 167 | finfoLock.Unlock() 168 | 169 | return finfo 170 | } 171 | 172 | // Scan scans the next row from rows in to a struct pointed to by dest. The struct type 173 | // should have exported fields tagged with the "sql" tag. Columns from row which are not 174 | // mapped to any struct fields are ignored. Struct fields which have no matching column 175 | // in the result set are left unchanged. 176 | func Scan(dest interface{}, rows Rows) error { 177 | return doScan(dest, rows, "") 178 | } 179 | 180 | // ScanAliased works like scan, except that it expects the results in the query to be 181 | // prefixed by the given alias. 182 | // 183 | // For example, if scanning to a field named "name" with an alias of "user" it will 184 | // expect to find the result in a column named "user_name". 185 | // 186 | // See ColumnAliased for a convenient way to generate these queries. 187 | func ScanAliased(dest interface{}, rows Rows, alias string) error { 188 | return doScan(dest, rows, alias) 189 | } 190 | 191 | // Columns returns a string containing a sorted, comma-separated list of column names as 192 | // defined by the type s. s must be a struct that has exported fields tagged with the "sql" tag. 193 | func Columns(s interface{}) string { 194 | return strings.Join(cols(s), ", ") 195 | } 196 | 197 | // ColumnsAliased works like Columns except it prefixes the resulting column name with the 198 | // given alias. 199 | // 200 | // For each field in the given struct it will generate a statement like: 201 | // alias.field AS alias_field 202 | // 203 | // It is intended to be used in conjunction with the ScanAliased function. 204 | func ColumnsAliased(s interface{}, alias string) string { 205 | names := cols(s) 206 | aliased := make([]string, 0, len(names)) 207 | for _, n := range names { 208 | aliased = append(aliased, alias+"."+n+" AS "+alias+"_"+n) 209 | } 210 | return strings.Join(aliased, ", ") 211 | } 212 | 213 | func cols(s interface{}) []string { 214 | v := reflect.ValueOf(s) 215 | fields := getFieldInfo(v.Type()) 216 | 217 | names := make([]string, 0, len(fields)) 218 | for f := range fields { 219 | names = append(names, f) 220 | } 221 | 222 | sort.Strings(names) 223 | return names 224 | } 225 | 226 | func doScan(dest interface{}, rows Rows, alias string) error { 227 | destv := reflect.ValueOf(dest) 228 | typ := destv.Type() 229 | 230 | if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Struct { 231 | panic(fmt.Errorf("dest must be pointer to struct; got %T", destv)) 232 | } 233 | fieldInfo := getFieldInfo(typ.Elem()) 234 | 235 | elem := destv.Elem() 236 | var values []interface{} 237 | 238 | cols, err := rows.Columns() 239 | if err != nil { 240 | return err 241 | } 242 | 243 | for _, name := range cols { 244 | if len(alias) > 0 { 245 | name = strings.Replace(name, alias+"_", "", 1) 246 | } 247 | idx, ok := fieldInfo[strings.ToLower(name)] 248 | var v interface{} 249 | if !ok { 250 | // There is no field mapped to this column so we discard it 251 | v = &sql.RawBytes{} 252 | } else { 253 | v = elem.FieldByIndex(idx).Addr().Interface() 254 | } 255 | values = append(values, v) 256 | } 257 | 258 | return rows.Scan(values...) 259 | } 260 | 261 | // ToSnakeCase converts a string to snake case, words separated with underscores. 262 | // It's intended to be used with NameMapper to map struct field names to snake case database fields. 263 | func ToSnakeCase(src string) string { 264 | thisUpper := false 265 | prevUpper := false 266 | 267 | buf := bytes.NewBufferString("") 268 | for i, v := range src { 269 | if v >= 'A' && v <= 'Z' { 270 | thisUpper = true 271 | } else { 272 | thisUpper = false 273 | } 274 | if i > 0 && thisUpper && !prevUpper { 275 | buf.WriteRune('_') 276 | } 277 | prevUpper = thisUpper 278 | buf.WriteRune(v) 279 | } 280 | return strings.ToLower(buf.String()) 281 | } 282 | --------------------------------------------------------------------------------