From 62273ba51d590d6aaf43573fc7a63b7c4d680368 Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Fri, 24 May 2013 22:53:25 -0400 Subject: [PATCH 01/20] remove TypeConverter, move much of DbMap out to its own file; mapper will contain struct <-> db mappings, and a new query.go will contain query related functionality --- gorp.go | 264 +++----------------------------------------------------- 1 file changed, 11 insertions(+), 253 deletions(-) diff --git a/gorp.go b/gorp.go index 779369e..1fc8f37 100644 --- a/gorp.go +++ b/gorp.go @@ -16,7 +16,6 @@ import ( "database/sql" "errors" "fmt" - "log" "reflect" "strings" ) @@ -54,25 +53,6 @@ func (e OptimisticLockError) Error() string { return fmt.Sprintf("gorp: OptimisticLockError no row found for table=%s keys=%v", e.TableName, e.Keys) } -// The TypeConverter interface provides a way to map a value of one -// type to another type when persisting to, or loading from, a database. -// -// Example use cases: Implement type converter to convert bool types to "y"/"n" strings, -// or serialize a struct member as a JSON blob. -type TypeConverter interface { - // ToDb converts val to another type. Called before INSERT/UPDATE operations - ToDb(val interface{}) (interface{}, error) - - // FromDb returns a CustomScanner appropriate for this type. This will be used - // to hold values returned from SELECT queries. - // - // In particular the CustomScanner returned should implement a Binder - // function appropriate for the Go type you wish to convert the db value to - // - // If bool==false, then no custom scanner will be used for this field. - FromDb(target interface{}) (CustomScanner, bool) -} - // CustomScanner binds a database column value to a Go type type CustomScanner struct { // After a row is scanned, Holder will contain the value from the database column. @@ -93,29 +73,6 @@ func (me CustomScanner) Bind() error { return me.Binder(me.Holder, me.Target) } -// DbMap is the root gorp mapping object. Create one of these for each -// database schema you wish to map. Each DbMap contains a list of -// mapped tables. -// -// Example: -// -// dialect := gorp.MySQLDialect{"InnoDB", "UTF8"} -// dbmap := &gorp.DbMap{Db: db, Dialect: dialect} -// -type DbMap struct { - // Db handle to use with this map - Db *sql.DB - - // Dialect implementation to use with this map - Dialect Dialect - - TypeConverter TypeConverter - - tables []*TableMap - logger *log.Logger - logPrefix string -} - // TableMap represents a mapping between a Go struct and a database table // Use dbmap.AddTable() or dbmap.AddTableWithName() to create these type TableMap struct { @@ -203,14 +160,12 @@ type bindPlan struct { autoIncrIdx int } -func (plan bindPlan) createBindInstance(elem reflect.Value, conv TypeConverter) (bindInstance, error) { +func (plan bindPlan) createBindInstance(elem reflect.Value) bindInstance { bi := bindInstance{query: plan.query, autoIncrIdx: plan.autoIncrIdx, versField: plan.versField} if plan.versField != "" { bi.existingVersion = elem.FieldByName(plan.versField).Int() } - var err error - for i := 0; i < len(plan.argFields); i++ { k := plan.argFields[i] if k == versFieldConst { @@ -221,12 +176,6 @@ func (plan bindPlan) createBindInstance(elem reflect.Value, conv TypeConverter) } } else { val := elem.FieldByName(k).Interface() - if conv != nil { - val, err = conv.ToDb(val) - if err != nil { - return bindInstance{}, err - } - } bi.args = append(bi.args, val) } } @@ -234,16 +183,10 @@ func (plan bindPlan) createBindInstance(elem reflect.Value, conv TypeConverter) for i := 0; i < len(plan.keyFields); i++ { k := plan.keyFields[i] val := elem.FieldByName(k).Interface() - if conv != nil { - val, err = conv.ToDb(val) - if err != nil { - return bindInstance{}, err - } - } bi.keys = append(bi.keys, val) } - return bi, nil + return bi } type bindInstance struct { @@ -255,7 +198,7 @@ type bindInstance struct { autoIncrIdx int } -func (t *TableMap) bindInsert(elem reflect.Value) (bindInstance, error) { +func (t *TableMap) bindInsert(elem reflect.Value) bindInstance { plan := t.insertPlan if plan.query == "" { plan.autoIncrIdx = -1 @@ -306,10 +249,10 @@ func (t *TableMap) bindInsert(elem reflect.Value) (bindInstance, error) { t.insertPlan = plan } - return plan.createBindInstance(elem, t.dbmap.TypeConverter) + return plan.createBindInstance(elem) } -func (t *TableMap) bindUpdate(elem reflect.Value) (bindInstance, error) { +func (t *TableMap) bindUpdate(elem reflect.Value) bindInstance { plan := t.updatePlan if plan.query == "" { @@ -364,10 +307,10 @@ func (t *TableMap) bindUpdate(elem reflect.Value) (bindInstance, error) { t.updatePlan = plan } - return plan.createBindInstance(elem, t.dbmap.TypeConverter) + return plan.createBindInstance(elem) } -func (t *TableMap) bindDelete(elem reflect.Value) (bindInstance, error) { +func (t *TableMap) bindDelete(elem reflect.Value) bindInstance { plan := t.deletePlan if plan.query == "" { @@ -410,7 +353,7 @@ func (t *TableMap) bindDelete(elem reflect.Value) (bindInstance, error) { t.deletePlan = plan } - return plan.createBindInstance(elem, t.dbmap.TypeConverter) + return plan.createBindInstance(elem) } func (t *TableMap) bindGet() bindPlan { @@ -538,171 +481,6 @@ type SqlExecutor interface { queryRow(query string, args ...interface{}) *sql.Row } -// TraceOn turns on SQL statement logging for this DbMap. After this is -// called, all SQL statements will be sent to the logger. If prefix is -// a non-empty string, it will be written to the front of all logged -// strings, which can aid in filtering log lines. -// -// Use TraceOn if you want to spy on the SQL statements that gorp -// generates. -func (m *DbMap) TraceOn(prefix string, logger *log.Logger) { - m.logger = logger - if prefix == "" { - m.logPrefix = prefix - } else { - m.logPrefix = fmt.Sprintf("%s ", prefix) - } -} - -// TraceOff turns off tracing. It is idempotent. -func (m *DbMap) TraceOff() { - m.logger = nil - m.logPrefix = "" -} - -// AddTable registers the given interface type with gorp. The table name -// will be given the name of the TypeOf(i). You must call this function, -// or AddTableWithName, for any struct type you wish to persist with -// the given DbMap. -// -// This operation is idempotent. If i's type is already mapped, the -// existing *TableMap is returned -func (m *DbMap) AddTable(i interface{}) *TableMap { - return m.AddTableWithName(i, "") -} - -// AddTableWithName has the same behavior as AddTable, but sets -// table.TableName to name. -func (m *DbMap) AddTableWithName(i interface{}, name string) *TableMap { - t := reflect.TypeOf(i) - if name == "" { - name = t.Name() - } - - // check if we have a table for this type already - // if so, update the name and return the existing pointer - for i := range m.tables { - table := m.tables[i] - if table.gotype == t { - table.TableName = name - return table - } - } - - tmap := &TableMap{gotype: t, TableName: name, dbmap: m} - - n := t.NumField() - tmap.columns = make([]*ColumnMap, 0, n) - for i := 0; i < n; i++ { - f := t.Field(i) - columnName := f.Tag.Get("db") - if columnName == "" { - columnName = f.Name - } - - cm := &ColumnMap{ - ColumnName: columnName, - Transient: columnName == "-", - fieldName: f.Name, - gotype: f.Type, - } - tmap.columns = append(tmap.columns, cm) - if cm.fieldName == "Version" { - tmap.version = tmap.columns[len(tmap.columns)-1] - } - } - m.tables = append(m.tables, tmap) - - return tmap -} - -// CreateTables iterates through TableMaps registered to this DbMap and -// executes "create table" statements against the database for each. -// -// This is particularly useful in unit tests where you want to create -// and destroy the schema automatically. -func (m *DbMap) CreateTables() error { - return m.createTables(false) -} - -// CreateTablesIfNotExists is similar to CreateTables, but starts -// each statement with "create table if not exists" so that existing -// tables do not raise errors -func (m *DbMap) CreateTablesIfNotExists() error { - return m.createTables(true) -} - -func (m *DbMap) createTables(ifNotExists bool) error { - var err error - for i := range m.tables { - table := m.tables[i] - - create := "create table" - if ifNotExists { - create += " if not exists" - } - s := bytes.Buffer{} - s.WriteString(fmt.Sprintf("%s %s (", create, m.Dialect.QuoteField(table.TableName))) - x := 0 - for _, col := range table.columns { - if !col.Transient { - if x > 0 { - s.WriteString(", ") - } - stype := m.Dialect.ToSqlType(col.gotype, col.MaxSize, col.isAutoIncr) - s.WriteString(fmt.Sprintf("%s %s", m.Dialect.QuoteField(col.ColumnName), stype)) - - if col.isPK { - s.WriteString(" not null") - if len(table.keys) == 1 { - s.WriteString(" primary key") - } - } - if col.Unique { - s.WriteString(" unique") - } - if col.isAutoIncr { - s.WriteString(fmt.Sprintf(" %s", m.Dialect.AutoIncrStr())) - } - - x++ - } - } - if len(table.keys) > 1 { - s.WriteString(", primary key (") - for x := range table.keys { - if x > 0 { - s.WriteString(", ") - } - s.WriteString(m.Dialect.QuoteField(table.keys[x].ColumnName)) - } - s.WriteString(")") - } - s.WriteString(") ") - s.WriteString(m.Dialect.CreateTableSuffix()) - s.WriteString(";") - _, err = m.Exec(s.String()) - if err != nil { - break - } - } - return err -} - -// DropTables iterates through TableMaps registered to this DbMap and -// executes "drop table" statements against the database for each. -func (m *DbMap) DropTables() error { - var err error - for i := range m.tables { - table := m.tables[i] - _, e := m.Exec(fmt.Sprintf("drop table %s;", m.Dialect.QuoteField(table.TableName))) - if e != nil { - err = e - } - } - return err -} - // Insert runs a SQL INSERT statement for each element in list. List // items must be pointers. // @@ -1133,8 +911,6 @@ func rawselect(m *DbMap, exec SqlExecutor, i interface{}, query string, } } - conv := m.TypeConverter - // Add results to one of these two slices. var ( list = make([]interface{}, 0) @@ -1158,13 +934,6 @@ func rawselect(m *DbMap, exec SqlExecutor, i interface{}, query string, for x := range cols { f := v.Elem().Field(colToFieldOffset[x]) target := f.Addr().Interface() - if conv != nil { - scanner, ok := conv.FromDb(target) - if ok { - target = scanner.Holder - custScan = append(custScan, scanner) - } - } dest[x] = target } @@ -1269,19 +1038,11 @@ func get(m *DbMap, exec SqlExecutor, i interface{}, v := reflect.New(t) dest := make([]interface{}, len(plan.argFields)) - conv := m.TypeConverter custScan := make([]CustomScanner, 0) for x, fieldName := range plan.argFields { f := v.Elem().FieldByName(fieldName) target := f.Addr().Interface() - if conv != nil { - scanner, ok := conv.FromDb(target) - if ok { - target = scanner.Holder - custScan = append(custScan, scanner) - } - } dest[x] = target } @@ -1324,10 +1085,7 @@ func delete(m *DbMap, exec SqlExecutor, list ...interface{}) (int64, error) { return -1, err } - bi, err := table.bindDelete(elem) - if err != nil { - return -1, err - } + bi := table.bindDelete(elem) res, err := exec.Exec(bi.query, bi.args...) if err != nil { @@ -1369,7 +1127,7 @@ func update(m *DbMap, exec SqlExecutor, list ...interface{}) (int64, error) { return -1, err } - bi, err := table.bindUpdate(elem) + bi := table.bindUpdate(elem) if err != nil { return -1, err } @@ -1417,7 +1175,7 @@ func insert(m *DbMap, exec SqlExecutor, list ...interface{}) error { return err } - bi, err := table.bindInsert(elem) + bi := table.bindInsert(elem) if err != nil { return err } From d9ad5b182ff45619af8bcae785bb4cc1167eab67 Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Fri, 24 May 2013 23:05:15 -0400 Subject: [PATCH 02/20] remove SelectVal helpers --- gorp.go | 107 --------------------------------------------------- gorp_test.go | 95 --------------------------------------------- 2 files changed, 202 deletions(-) diff --git a/gorp.go b/gorp.go index 1fc8f37..bcac3bb 100644 --- a/gorp.go +++ b/gorp.go @@ -577,26 +577,6 @@ func (m *DbMap) Exec(query string, args ...interface{}) (sql.Result, error) { return m.Db.Exec(query, args...) } -// SelectInt is a convenience wrapper around the gorp.SelectInt function -func (m *DbMap) SelectInt(query string, args ...interface{}) (int64, error) { - return SelectInt(m, query, args...) -} - -// SelectNullInt is a convenience wrapper around the gorp.SelectNullInt function -func (m *DbMap) SelectNullInt(query string, args ...interface{}) (sql.NullInt64, error) { - return SelectNullInt(m, query, args...) -} - -// SelectStr is a convenience wrapper around the gorp.SelectStr function -func (m *DbMap) SelectStr(query string, args ...interface{}) (string, error) { - return SelectStr(m, query, args...) -} - -// SelectNullStr is a convenience wrapper around the gorp.SelectNullStr function -func (m *DbMap) SelectNullStr(query string, args ...interface{}) (sql.NullString, error) { - return SelectNullStr(m, query, args...) -} - // Begin starts a gorp Transaction func (m *DbMap) Begin() (*Transaction, error) { tx, err := m.Db.Begin() @@ -701,26 +681,6 @@ func (t *Transaction) Exec(query string, args ...interface{}) (sql.Result, error return stmt.Exec(args...) } -// SelectInt is a convenience wrapper around the gorp.SelectInt function -func (t *Transaction) SelectInt(query string, args ...interface{}) (int64, error) { - return SelectInt(t, query, args...) -} - -// SelectNullInt is a convenience wrapper around the gorp.SelectNullInt function -func (t *Transaction) SelectNullInt(query string, args ...interface{}) (sql.NullInt64, error) { - return SelectNullInt(t, query, args...) -} - -// SelectStr is a convenience wrapper around the gorp.SelectStr function -func (t *Transaction) SelectStr(query string, args ...interface{}) (string, error) { - return SelectStr(t, query, args...) -} - -// SelectNullStr is a convenience wrapper around the gorp.SelectNullStr function -func (t *Transaction) SelectNullStr(query string, args ...interface{}) (sql.NullString, error) { - return SelectNullStr(t, query, args...) -} - // Commits the underlying database transaction func (t *Transaction) Commit() error { return t.tx.Commit() @@ -743,73 +703,6 @@ func (t *Transaction) query(query string, args ...interface{}) (*sql.Rows, error /////////////// -// SelectInt executes the given query, which should be a SELECT statement for a single -// integer column, and returns the value of the first row returned. If no rows are -// found, zero is returned. -func SelectInt(e SqlExecutor, query string, args ...interface{}) (int64, error) { - var h int64 - err := selectVal(e, &h, query, args...) - if err != nil { - return 0, err - } - return h, nil -} - -// SelectNullInt executes the given query, which should be a SELECT statement for a single -// integer column, and returns the value of the first row returned. If no rows are -// found, the empty sql.NullInt64 value is returned. -func SelectNullInt(e SqlExecutor, query string, args ...interface{}) (sql.NullInt64, error) { - var h sql.NullInt64 - err := selectVal(e, &h, query, args...) - if err != nil { - return h, err - } - return h, nil -} - -// SelectStr executes the given query, which should be a SELECT statement for a single -// char/varchar column, and returns the value of the first row returned. If no rows are -// found, an empty string is returned. -func SelectStr(e SqlExecutor, query string, args ...interface{}) (string, error) { - var h string - err := selectVal(e, &h, query, args...) - if err != nil { - return "", err - } - return h, nil -} - -// SelectStr executes the given query, which should be a SELECT statement for a single -// char/varchar column, and returns the value of the first row returned. If no rows are -// found, the empty sql.NullString is returned. -func SelectNullStr(e SqlExecutor, query string, args ...interface{}) (sql.NullString, error) { - var h sql.NullString - err := selectVal(e, &h, query, args...) - if err != nil { - return h, err - } - return h, nil -} - -func selectVal(e SqlExecutor, holder interface{}, query string, args ...interface{}) error { - rows, err := e.query(query, args...) - if err != nil { - return err - } - defer rows.Close() - - if rows.Next() { - err = rows.Scan(holder) - if err != nil { - return err - } - } - - return nil -} - -/////////////// - func hookedselect(m *DbMap, exec SqlExecutor, i interface{}, query string, args ...interface{}) ([]interface{}, error) { diff --git a/gorp_test.go b/gorp_test.go index 4dd16cb..2561ca9 100644 --- a/gorp_test.go +++ b/gorp_test.go @@ -613,65 +613,6 @@ func TestTypeConversionExample(t *testing.T) { } -func TestSelectVal(t *testing.T) { - dbmap := initDbMapNulls() - defer dbmap.DropTables() - - bindVar := dbmap.Dialect.BindVar(0) - - t1 := TableWithNull{Str: sql.NullString{"abc", true}, - Int64: sql.NullInt64{78, true}, - Float64: sql.NullFloat64{32.2, true}, - Bool: sql.NullBool{true, true}, - Bytes: []byte("hi")} - _insert(dbmap, &t1) - - // SelectInt - i64 := selectInt(dbmap, "select Int64 from TableWithNull where Str='abc'") - if i64 != 78 { - t.Errorf("int64 %d != 78", i64) - } - i64 = selectInt(dbmap, "select count(*) from TableWithNull") - if i64 != 1 { - t.Errorf("int64 count %d != 1", i64) - } - i64 = selectInt(dbmap, "select count(*) from TableWithNull where Str="+bindVar, "asdfasdf") - if i64 != 0 { - t.Errorf("int64 no rows %d != 0", i64) - } - - // SelectNullInt - n := selectNullInt(dbmap, "select Int64 from TableWithNull where Str='notfound'") - if !reflect.DeepEqual(n, sql.NullInt64{0, false}) { - t.Errorf("nullint %v != 0,false", n) - } - - n = selectNullInt(dbmap, "select Int64 from TableWithNull where Str='abc'") - if !reflect.DeepEqual(n, sql.NullInt64{78, true}) { - t.Errorf("nullint %v != 78, true", n) - } - - // SelectStr - s := selectStr(dbmap, "select Str from TableWithNull where Int64="+bindVar, 78) - if s != "abc" { - t.Errorf("s %s != abc", s) - } - s = selectStr(dbmap, "select Str from TableWithNull where Str='asdfasdf'") - if s != "" { - t.Errorf("s no rows %s != ''", s) - } - - // SelectNullStr - ns := selectNullStr(dbmap, "select Str from TableWithNull where Int64="+bindVar, 78) - if !reflect.DeepEqual(ns, sql.NullString{"abc", true}) { - t.Errorf("nullstr %v != abc,true", ns) - } - ns = selectNullStr(dbmap, "select Str from TableWithNull where Str='asdfasdf'") - if !reflect.DeepEqual(ns, sql.NullString{"", false}) { - t.Errorf("nullstr no rows %v != '',false", ns) - } -} - func TestVersionMultipleRows(t *testing.T) { dbmap := initDbMap() defer dbmap.DropTables() @@ -901,42 +842,6 @@ func _get(dbmap *DbMap, i interface{}, keys ...interface{}) interface{} { return obj } -func selectInt(dbmap *DbMap, query string, args ...interface{}) int64 { - i64, err := SelectInt(dbmap, query, args...) - if err != nil { - panic(err) - } - - return i64 -} - -func selectNullInt(dbmap *DbMap, query string, args ...interface{}) sql.NullInt64 { - i64, err := SelectNullInt(dbmap, query, args...) - if err != nil { - panic(err) - } - - return i64 -} - -func selectStr(dbmap *DbMap, query string, args ...interface{}) string { - s, err := SelectStr(dbmap, query, args...) - if err != nil { - panic(err) - } - - return s -} - -func selectNullStr(dbmap *DbMap, query string, args ...interface{}) sql.NullString { - s, err := SelectNullStr(dbmap, query, args...) - if err != nil { - panic(err) - } - - return s -} - func _rawexec(dbmap *DbMap, query string, args ...interface{}) sql.Result { res, err := dbmap.Exec(query, args...) if err != nil { From 037a8e0960c5c2495f9cc00dca518560c365203f Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Fri, 24 May 2013 23:05:34 -0400 Subject: [PATCH 03/20] add back DbMap in mapper.go --- mapper.go | 208 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 mapper.go diff --git a/mapper.go b/mapper.go new file mode 100644 index 0000000..2ac1a06 --- /dev/null +++ b/mapper.go @@ -0,0 +1,208 @@ +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// +package gorp + +import ( + "bytes" + "database/sql" + "fmt" + "github.com/jmoiron/sqlx" + "log" + "reflect" + "strings" +) + +// DbMap is the root gorp mapping object. Create one of these for each +// database schema you wish to map. Each DbMap contains a list of +// mapped tables. +// +// Example: +// +// dialect := gorp.MySQLDialect{"InnoDB", "UTF8"} +// dbmap := &gorp.DbMap{Db: db, Dialect: dialect} +// +type DbMap struct { + // Db handle to use with this map + Db *sql.DB + Dbx *sqlx.DB + + // Dialect implementation to use with this map + Dialect Dialect + + tables []*TableMap + logger *log.Logger + logPrefix string +} + +// Return a new DbMap using the db connection and dialect +func NewDbMap(db *sql.DB, dialect Dialect) *DbMap { + return &DbMap{Db: db, Dialect: dialect, Dbx: &sqlx.DB{*db}} +} + +// TraceOn turns on SQL statement logging for this DbMap. After this is +// called, all SQL statements will be sent to the logger. If prefix is +// a non-empty string, it will be written to the front of all logged +// strings, which can aid in filtering log lines. +// +// Use TraceOn if you want to spy on the SQL statements that gorp +// generates. +func (m *DbMap) TraceOn(prefix string, logger *log.Logger) { + m.logger = logger + if len(prefix) == 0 { + m.logPrefix = prefix + } else { + m.logPrefix = prefix + " " + } +} + +// TraceOff turns off tracing. It is idempotent. +func (m *DbMap) TraceOff() { + m.logger = nil + m.logPrefix = "" +} + +// AddTable registers the given interface type with gorp. The table name +// will be given the name of the TypeOf(i), lowercased. +// +// This operation is idempotent. If i's type is already mapped, the +// existing *TableMap is returned +func (m *DbMap) AddTable(i interface{}, name ...string) *TableMap { + Name := "" + if len(name) > 0 { + Name = name[0] + } + + t := reflect.TypeOf(i) + if len(Name) == 0 { + Name = strings.ToLower(t.Name()) + } + + // check if we have a table for this type already + // if so, update the name and return the existing pointer + for i := range m.tables { + table := m.tables[i] + if table.gotype == t { + table.TableName = Name + return table + } + } + + tmap := &TableMap{gotype: t, TableName: Name, dbmap: m} + + n := t.NumField() + tmap.columns = make([]*ColumnMap, 0, n) + for i := 0; i < n; i++ { + f := t.Field(i) + columnName := f.Tag.Get("db") + if columnName == "" { + columnName = f.Name + } + + cm := &ColumnMap{ + ColumnName: columnName, + Transient: columnName == "-", + fieldName: f.Name, + gotype: f.Type, + } + tmap.columns = append(tmap.columns, cm) + if cm.fieldName == "Version" { + tmap.version = tmap.columns[len(tmap.columns)-1] + } + } + m.tables = append(m.tables, tmap) + + return tmap + +} + +func (m *DbMap) AddTableWithName(i interface{}, name string) *TableMap { + return m.AddTable(i, name) +} + +// CreateTables iterates through TableMaps registered to this DbMap and +// executes "create table" statements against the database for each. +// +// This is particularly useful in unit tests where you want to create +// and destroy the schema automatically. +func (m *DbMap) CreateTables() error { + return m.createTables(false) +} + +// CreateTablesIfNotExists is similar to CreateTables, but starts +// each statement with "create table if not exists" so that existing +// tables do not raise errors +func (m *DbMap) CreateTablesIfNotExists() error { + return m.createTables(true) +} + +func (m *DbMap) createTables(ifNotExists bool) error { + var err error + for i := range m.tables { + table := m.tables[i] + + create := "create table" + if ifNotExists { + create += " if not exists" + } + s := bytes.Buffer{} + s.WriteString(fmt.Sprintf("%s %s (", create, m.Dialect.QuoteField(table.TableName))) + x := 0 + for _, col := range table.columns { + if !col.Transient { + if x > 0 { + s.WriteString(", ") + } + stype := m.Dialect.ToSqlType(col.gotype, col.MaxSize, col.isAutoIncr) + s.WriteString(fmt.Sprintf("%s %s", m.Dialect.QuoteField(col.ColumnName), stype)) + + if col.isPK { + s.WriteString(" not null") + if len(table.keys) == 1 { + s.WriteString(" primary key") + } + } + if col.Unique { + s.WriteString(" unique") + } + if col.isAutoIncr { + s.WriteString(fmt.Sprintf(" %s", m.Dialect.AutoIncrStr())) + } + + x++ + } + } + if len(table.keys) > 1 { + s.WriteString(", primary key (") + for x := range table.keys { + if x > 0 { + s.WriteString(", ") + } + s.WriteString(m.Dialect.QuoteField(table.keys[x].ColumnName)) + } + s.WriteString(")") + } + s.WriteString(") ") + s.WriteString(m.Dialect.CreateTableSuffix()) + s.WriteString(";") + _, err = m.Exec(s.String()) + if err != nil { + break + } + } + return err +} + +// DropTables iterates through TableMaps registered to this DbMap and +// executes "drop table" statements against the database for each. +func (m *DbMap) DropTables() error { + var err error + for i := range m.tables { + table := m.tables[i] + _, e := m.Exec(fmt.Sprintf("drop table %s;", m.Dialect.QuoteField(table.TableName))) + if e != nil { + err = e + } + } + return err +} From 3017cc9f12ee87737733274fa524b41121b6801e Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Sat, 25 May 2013 14:09:11 -0400 Subject: [PATCH 04/20] remove type conversion stuff (this is handled by the database/sql/driver Scanner interface) and get tests working on pg and sqlite --- gorp_test.go | 88 ---------------------------------------------------- 1 file changed, 88 deletions(-) diff --git a/gorp_test.go b/gorp_test.go index 2561ca9..03c4751 100644 --- a/gorp_test.go +++ b/gorp_test.go @@ -2,7 +2,6 @@ package gorp import ( "database/sql" - "encoding/json" "errors" "fmt" _ "github.com/lib/pq" @@ -63,61 +62,6 @@ type WithStringPk struct { type CustomStringType string -type TypeConversionExample struct { - Id int64 - PersonJSON Person - Name CustomStringType -} - -type testTypeConverter struct{} - -func (me testTypeConverter) ToDb(val interface{}) (interface{}, error) { - - switch t := val.(type) { - case Person: - b, err := json.Marshal(t) - if err != nil { - return "", err - } - return string(b), nil - case CustomStringType: - return string(t), nil - } - - return val, nil -} - -func (me testTypeConverter) FromDb(target interface{}) (CustomScanner, bool) { - switch target.(type) { - case *Person: - binder := func(holder, target interface{}) error { - s, ok := holder.(*string) - if !ok { - return errors.New("FromDb: Unable to convert Person to *string") - } - b := []byte(*s) - return json.Unmarshal(b, target) - } - return CustomScanner{new(string), target, binder}, true - case *CustomStringType: - binder := func(holder, target interface{}) error { - s, ok := holder.(*string) - if !ok { - return errors.New("FromDb: Unable to convert CustomStringType to *string") - } - st, ok := target.(*CustomStringType) - if !ok { - return errors.New(fmt.Sprint("FromDb: Unable to convert target to *CustomStringType: ", reflect.TypeOf(target))) - } - *st = CustomStringType(*s) - return nil - } - return CustomScanner{new(string), target, binder}, true - } - - return CustomScanner{}, false -} - func (p *Person) PreInsert(s SqlExecutor) error { p.Created = time.Now().UnixNano() p.Updated = p.Created @@ -583,36 +527,6 @@ func TestWithIgnoredColumn(t *testing.T) { } } -func TestTypeConversionExample(t *testing.T) { - dbmap := initDbMap() - defer dbmap.DropTables() - - p := Person{FName: "Bob", LName: "Smith"} - tc := &TypeConversionExample{-1, p, CustomStringType("hi")} - _insert(dbmap, tc) - - expected := &TypeConversionExample{1, p, CustomStringType("hi")} - tc2 := _get(dbmap, TypeConversionExample{}, tc.Id).(*TypeConversionExample) - if !reflect.DeepEqual(expected, tc2) { - t.Errorf("tc2 %v != %v", expected, tc2) - } - - tc2.Name = CustomStringType("hi2") - tc2.PersonJSON = Person{FName: "Jane", LName: "Doe"} - _update(dbmap, tc2) - - expected = &TypeConversionExample{1, tc2.PersonJSON, CustomStringType("hi2")} - tc3 := _get(dbmap, TypeConversionExample{}, tc.Id).(*TypeConversionExample) - if !reflect.DeepEqual(expected, tc3) { - t.Errorf("tc3 %v != %v", expected, tc3) - } - - if _del(dbmap, tc) != 1 { - t.Errorf("Did not delete row with Id: %d", tc.Id) - } - -} - func TestVersionMultipleRows(t *testing.T) { dbmap := initDbMap() defer dbmap.DropTables() @@ -758,8 +672,6 @@ func initDbMap() *DbMap { dbmap.AddTableWithName(Invoice{}, "invoice_test").SetKeys(true, "Id") dbmap.AddTableWithName(Person{}, "person_test").SetKeys(true, "Id") dbmap.AddTableWithName(WithIgnoredColumn{}, "ignored_column_test").SetKeys(true, "Id") - dbmap.AddTableWithName(TypeConversionExample{}, "type_conv_test").SetKeys(true, "Id") - dbmap.TypeConverter = testTypeConverter{} err := dbmap.CreateTables() if err != nil { panic(err) From 0d53cffa805b920851cab764e46f42e59704baf6 Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Sun, 26 May 2013 12:03:08 -0400 Subject: [PATCH 05/20] replace most of the hook system with a simpler one that uses interfaces to pre-calculate hook support and uses a lot less reflect --- gorp.go | 105 ++++++++++++++++++++++++++++++++++----------------- gorp_test.go | 12 +++--- hooks.go | 52 +++++++++++++++++++++++++ mapper.go | 1 + test_all.sh | 14 +++---- todo.md | 6 +++ 6 files changed, 142 insertions(+), 48 deletions(-) create mode 100644 hooks.go create mode 100644 todo.md diff --git a/gorp.go b/gorp.go index bcac3bb..249941e 100644 --- a/gorp.go +++ b/gorp.go @@ -87,6 +87,14 @@ type TableMap struct { deletePlan bindPlan getPlan bindPlan dbmap *DbMap + // Cached capabilities for the struct mapped to this table + CanPreInsert bool + CanPostInsert bool + CanPostGet bool + CanPreUpdate bool + CanPostUpdate bool + CanPreDelete bool + CanPostDelete bool } // ResetSql removes cached insert/update/select/delete SQL strings @@ -711,6 +719,10 @@ func hookedselect(m *DbMap, exec SqlExecutor, i interface{}, query string, return nil, err } + // FIXME: should PostGet hooks be run on regular selects? a PostGet + // hook has access to the object and the database, and I'd hate for + // a query to execute SQL on every row of a queryset. + // Determine where the results are: written to i, or returned in list if t := toSliceType(i); t == nil { for _, v := range list { @@ -731,8 +743,7 @@ func hookedselect(m *DbMap, exec SqlExecutor, i interface{}, query string, return list, nil } -func rawselect(m *DbMap, exec SqlExecutor, i interface{}, query string, - args ...interface{}) ([]interface{}, error) { +func rawselect(m *DbMap, exec SqlExecutor, i interface{}, query string, args ...interface{}) ([]interface{}, error) { appendToSlice := false // Write results to i directly? // get type for i, verifying it's a struct or a pointer-to-slice @@ -955,27 +966,35 @@ func get(m *DbMap, exec SqlExecutor, i interface{}, } } - err = runHook("PostGet", v, hookArg(exec)) - if err != nil { - return nil, err + vi := v.Interface() + + if table.CanPostGet { + err = vi.(PostGetter).PostGet(exec) + if err != nil { + return nil, err + } } - return v.Interface(), nil + return vi, nil } func delete(m *DbMap, exec SqlExecutor, list ...interface{}) (int64, error) { - hookarg := hookArg(exec) - count := int64(0) + var err error + var table *TableMap + var elem reflect.Value + var count int64 + for _, ptr := range list { - table, elem, err := m.tableForPointer(ptr, true) + table, elem, err = m.tableForPointer(ptr, true) if err != nil { return -1, err } - eptr := elem.Addr() - err = runHook("PreDelete", eptr, hookarg) - if err != nil { - return -1, err + if table.CanPreDelete { + err = ptr.(PreDeleter).PreDelete(exec) + if err != nil { + return -1, err + } } bi := table.bindDelete(elem) @@ -984,6 +1003,7 @@ func delete(m *DbMap, exec SqlExecutor, list ...interface{}) (int64, error) { if err != nil { return -1, err } + rows, err := res.RowsAffected() if err != nil { return -1, err @@ -996,9 +1016,11 @@ func delete(m *DbMap, exec SqlExecutor, list ...interface{}) (int64, error) { count += rows - err = runHook("PostDelete", eptr, hookarg) - if err != nil { - return -1, err + if table.CanPostDelete { + err = ptr.(PostDeleter).PostDelete(exec) + if err != nil { + return -1, err + } } } @@ -1006,18 +1028,22 @@ func delete(m *DbMap, exec SqlExecutor, list ...interface{}) (int64, error) { } func update(m *DbMap, exec SqlExecutor, list ...interface{}) (int64, error) { - hookarg := hookArg(exec) - count := int64(0) + var err error + var table *TableMap + var elem reflect.Value + var count int64 + for _, ptr := range list { - table, elem, err := m.tableForPointer(ptr, true) + table, elem, err = m.tableForPointer(ptr, true) if err != nil { return -1, err } - eptr := elem.Addr() - err = runHook("PreUpdate", eptr, hookarg) - if err != nil { - return -1, err + if table.CanPreUpdate { + err = ptr.(PreUpdater).PreUpdate(exec) + if err != nil { + return -1, err + } } bi := table.bindUpdate(elem) @@ -1046,26 +1072,33 @@ func update(m *DbMap, exec SqlExecutor, list ...interface{}) (int64, error) { count += rows - err = runHook("PostUpdate", eptr, hookarg) - if err != nil { - return -1, err + if table.CanPostUpdate { + err = ptr.(PostUpdater).PostUpdate(exec) + + if err != nil { + return -1, err + } } } return count, nil } func insert(m *DbMap, exec SqlExecutor, list ...interface{}) error { - hookarg := hookArg(exec) + var err error + var table *TableMap + var elem reflect.Value + for _, ptr := range list { - table, elem, err := m.tableForPointer(ptr, false) + table, elem, err = m.tableForPointer(ptr, false) if err != nil { return err } - eptr := elem.Addr() - err = runHook("PreInsert", eptr, hookarg) - if err != nil { - return err + if table.CanPreInsert { + err = ptr.(PreInserter).PreInsert(exec) + if err != nil { + return err + } } bi := table.bindInsert(elem) @@ -1092,9 +1125,11 @@ func insert(m *DbMap, exec SqlExecutor, list ...interface{}) error { } } - err = runHook("PostInsert", eptr, hookarg) - if err != nil { - return err + if table.CanPostInsert { + err = ptr.(PostInserter).PostInsert(exec) + if err != nil { + return err + } } } return nil diff --git a/gorp_test.go b/gorp_test.go index 03c4751..2558512 100644 --- a/gorp_test.go +++ b/gorp_test.go @@ -7,7 +7,7 @@ import ( _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" _ "github.com/ziutek/mymysql/godrv" - "log" + //"log" "os" "reflect" "testing" @@ -120,7 +120,7 @@ func TestCreateTablesIfNotExists(t *testing.T) { func TestPersistentUser(t *testing.T) { dbmap := newDbMap() dbmap.Exec("drop table if exists PersistentUser") - dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) + //dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) table := dbmap.AddTable(PersistentUser{}).SetKeys(false, "Key") table.ColMap("Key").Rename("mykey") err := dbmap.CreateTablesIfNotExists() @@ -300,7 +300,7 @@ func TestNullValues(t *testing.T) { func TestColumnProps(t *testing.T) { dbmap := newDbMap() - dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) + //dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) t1 := dbmap.AddTable(Invoice{}).SetKeys(true, "Id") t1.ColMap("Created").Rename("date_created") t1.ColMap("Updated").SetTransient(true) @@ -548,7 +548,7 @@ func TestVersionMultipleRows(t *testing.T) { func TestWithStringPk(t *testing.T) { dbmap := newDbMap() - dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) + //dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) dbmap.AddTableWithName(WithStringPk{}, "string_pk_test").SetKeys(true, "Id") _, err := dbmap.Exec("create table string_pk_test (Id varchar(255), Name varchar(255));") if err != nil { @@ -668,7 +668,7 @@ func initDbMapBench() *DbMap { func initDbMap() *DbMap { dbmap := newDbMap() - dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) + //dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) dbmap.AddTableWithName(Invoice{}, "invoice_test").SetKeys(true, "Id") dbmap.AddTableWithName(Person{}, "person_test").SetKeys(true, "Id") dbmap.AddTableWithName(WithIgnoredColumn{}, "ignored_column_test").SetKeys(true, "Id") @@ -682,7 +682,7 @@ func initDbMap() *DbMap { func initDbMapNulls() *DbMap { dbmap := newDbMap() - dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) + //dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) dbmap.AddTable(TableWithNull{}).SetKeys(false, "Id") err := dbmap.CreateTables() if err != nil { diff --git a/hooks.go b/hooks.go new file mode 100644 index 0000000..1fec0fb --- /dev/null +++ b/hooks.go @@ -0,0 +1,52 @@ +package gorp + +import ( + "reflect" +) + +type PreInserter interface { + PreInsert(SqlExecutor) error +} + +type PostInserter interface { + PostInsert(SqlExecutor) error +} + +type PostGetter interface { + PostGet(SqlExecutor) error +} + +type PreUpdater interface { + PreUpdate(SqlExecutor) error +} + +type PostUpdater interface { + PostUpdate(SqlExecutor) error +} + +type PreDeleter interface { + PreDelete(SqlExecutor) error +} + +type PostDeleter interface { + PostDelete(SqlExecutor) error +} + +// Determine which hooks are supported by the mapper struct i +func (t *TableMap) setupHooks(i interface{}) { + // These hooks must be implemented on a pointer, so if a value is passed in + // we have to get a pointer for a new value of that type in order for the + // type assertions to pass. + ptr := i + if reflect.ValueOf(i).Kind() == reflect.Struct { + ptr = reflect.New(reflect.ValueOf(i).Type()).Interface() + } + + _, t.CanPreInsert = ptr.(PreInserter) + _, t.CanPostInsert = ptr.(PostInserter) + _, t.CanPostGet = ptr.(PostGetter) + _, t.CanPreUpdate = ptr.(PreUpdater) + _, t.CanPostUpdate = ptr.(PostUpdater) + _, t.CanPreDelete = ptr.(PreDeleter) + _, t.CanPostDelete = ptr.(PostDeleter) +} diff --git a/mapper.go b/mapper.go index 2ac1a06..a3e685b 100644 --- a/mapper.go +++ b/mapper.go @@ -89,6 +89,7 @@ func (m *DbMap) AddTable(i interface{}, name ...string) *TableMap { } tmap := &TableMap{gotype: t, TableName: Name, dbmap: m} + tmap.setupHooks(i) n := t.NumField() tmap.columns = make([]*ColumnMap, 0, n) diff --git a/test_all.sh b/test_all.sh index 21796ca..555253e 100755 --- a/test_all.sh +++ b/test_all.sh @@ -1,15 +1,15 @@ #!/bin/sh -set -e +set -e -export GORP_TEST_DSN=gorptest/gorptest/gorptest -export GORP_TEST_DIALECT=mysql +export GORP_TEST_DSN="gorptest/gorptest/" +export GORP_TEST_DIALECT="mysql" go test -export GORP_TEST_DSN="user=gorptest password=gorptest dbname=gorptest sslmode=disable" -export GORP_TEST_DIALECT=postgres +export GORP_TEST_DSN="user=$USER dbname=gorptest sslmode=disable" +export GORP_TEST_DIALECT="postgres" go test -export GORP_TEST_DSN=/tmp/gorptest.bin -export GORP_TEST_DIALECT=sqlite +export GORP_TEST_DSN="/tmp/gorptest.bin" +export GORP_TEST_DIALECT="sqlite" go test diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..1e74a52 --- /dev/null +++ b/todo.md @@ -0,0 +1,6 @@ +- replace hook calling process with one that uses interfaces and reflect.CanInterface +- remove list return support +- replace reflect struct filling with structscan +- cache/store as much reflect stuff as possible +- add query builder + From d926ea9acbf144fa34ded9e4ac72f711112f7361 Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Sun, 26 May 2013 15:19:41 -0400 Subject: [PATCH 06/20] add bench_all.sh with some benchmark output, fix benchmarks for all dialects, add a ReBind function which takes a query and a dialect and formats its bindvars according to that dialect (given the basic '?' bindvar scheme) --- bench_all.sh | 30 ++++++++++++++++++++++++++++++ dialect.go | 15 +++++++++++++++ gorp.go | 2 ++ gorp_test.go | 45 ++++++++++++++++++++++++++++++++++----------- 4 files changed, 81 insertions(+), 11 deletions(-) create mode 100755 bench_all.sh diff --git a/bench_all.sh b/bench_all.sh new file mode 100755 index 0000000..1825b6b --- /dev/null +++ b/bench_all.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +#set -e + +export GORP_TEST_DSN="gorptest/gorptest/" +export GORP_TEST_DIALECT="mysql" +go test -bench . -benchmem + +export GORP_TEST_DSN="user=$USER dbname=gorptest sslmode=disable" +export GORP_TEST_DIALECT="postgres" +go test -bench . -benchmem + +export GORP_TEST_DSN="/tmp/gorptest.bin" +export GORP_TEST_DIALECT="sqlite" +go test -bench . -benchmem + +# PASS +# BenchmarkNativeCrud 2000 872061 ns/op 5364 B/op 106 allocs/op +# BenchmarkGorpCrud 2000 918331 ns/op 7257 B/op 159 allocs/op +# ok _/Users/jmoiron/dev/go/gorp 4.413s +# PASS +# BenchmarkNativeCrud 1000 1468223 ns/op 8833 B/op 335 allocs/op +# BenchmarkGorpCrud 1000 1485000 ns/op 11585 B/op 390 allocs/op +# ok _/Users/jmoiron/dev/go/gorp 3.664s +# PASS +# BenchmarkNativeCrud 1000 1761324 ns/op 1446 B/op 51 allocs/op +# BenchmarkGorpCrud 1000 1733306 ns/op 2942 B/op 99 allocs/op +# ok _/Users/jmoiron/dev/go/gorp 3.977s +# + diff --git a/dialect.go b/dialect.go index d14dfaa..f2fc9a2 100644 --- a/dialect.go +++ b/dialect.go @@ -288,3 +288,18 @@ func (m MySQLDialect) InsertAutoIncr(exec SqlExecutor, insertSql string, params func (d MySQLDialect) QuoteField(f string) string { return "`" + f + "`" } + +// Formats the bindvars in the query string (these are '?') for the dialect. +func ReBind(query string, dialect Dialect) string { + + binder := dialect.BindVar(0) + if binder == "?" { + return query + } + + for i, j := 0, strings.Index(query, "?"); j >= 0; i++ { + query = strings.Replace(query, "?", dialect.BindVar(i), 1) + j = strings.Index(query, "?") + } + return query +} diff --git a/gorp.go b/gorp.go index 249941e..9c5c855 100644 --- a/gorp.go +++ b/gorp.go @@ -573,6 +573,8 @@ func (m *DbMap) Select(i interface{}, query string, args ...interface{}) ([]inte return hookedselect(m, m, i, query, args...) } +// FIXME: this comment is wrong, query preparation is commented out + // Exec runs an arbitrary SQL statement. args represent the bind parameters. // This is equivalent to running: Prepare(), Exec() using database/sql func (m *DbMap) Exec(query string, args ...interface{}) (sql.Result, error) { diff --git a/gorp_test.go b/gorp_test.go index 2558512..3e29874 100644 --- a/gorp_test.go +++ b/gorp_test.go @@ -564,6 +564,8 @@ func TestWithStringPk(t *testing.T) { } func BenchmarkNativeCrud(b *testing.B) { + var err error + b.StopTimer() dbmap := initDbMapBench() defer dbmap.DropTables() @@ -574,24 +576,44 @@ func BenchmarkNativeCrud(b *testing.B) { update := "update invoice_test set Created=?, Updated=?, Memo=?, PersonId=? where Id=?" delete := "delete from invoice_test where Id=?" + suffix := dbmap.Dialect.AutoIncrInsertSuffix(&ColumnMap{ColumnName: "Id"}) + insert = ReBind(insert, dbmap.Dialect) + suffix + sel = ReBind(sel, dbmap.Dialect) + update = ReBind(update, dbmap.Dialect) + delete = ReBind(delete, dbmap.Dialect) + inv := &Invoice{0, 100, 200, "my memo", 0, false} for i := 0; i < b.N; i++ { - res, err := dbmap.Db.Exec(insert, inv.Created, inv.Updated, - inv.Memo, inv.PersonId) - if err != nil { - panic(err) - } + if len(suffix) == 0 { + res, err := dbmap.Db.Exec(insert, inv.Created, inv.Updated, inv.Memo, inv.PersonId) + if err != nil { + panic(err) + } + + newid, err := res.LastInsertId() + if err != nil { + panic(err) + } + inv.Id = newid + } else { + rows, err := dbmap.Db.Query(insert, inv.Created, inv.Updated, inv.Memo, inv.PersonId) + if err != nil { + panic(err) + } + + if rows.Next() { + err = rows.Scan(&inv.Id) + if err != nil { + panic(err) + } + } + rows.Close() - newid, err := res.LastInsertId() - if err != nil { - panic(err) } - inv.Id = newid row := dbmap.Db.QueryRow(sel, inv.Id) - err = row.Scan(&inv.Id, &inv.Created, &inv.Updated, &inv.Memo, - &inv.PersonId) + err = row.Scan(&inv.Id, &inv.Created, &inv.Updated, &inv.Memo, &inv.PersonId) if err != nil { panic(err) } @@ -619,6 +641,7 @@ func BenchmarkGorpCrud(b *testing.B) { b.StopTimer() dbmap := initDbMapBench() defer dbmap.DropTables() + //dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) b.StartTimer() inv := &Invoice{0, 100, 200, "my memo", 0, true} From 85841bec2499adee62a588e6988c013e33c4468d Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Mon, 27 May 2013 01:18:40 -0400 Subject: [PATCH 07/20] WIP replacement of Get internals with StructScan; this had implications with the TableMap, and I had to replace the ability to Rename colmaps, but since you can control table field names through struct tags I decided that having only One Place to do that was best, and doing it in the tags has advantages. Get currently doesn't return errors as I thought it should, and the Optimistic Locking is purposefully broken at the moment --- gorp.go | 127 +++++++++++++++++++++++++-------------------------- gorp_test.go | 92 +++++++++++++++++++------------------ todo.md | 9 +++- 3 files changed, 116 insertions(+), 112 deletions(-) diff --git a/gorp.go b/gorp.go index 9c5c855..8968264 100644 --- a/gorp.go +++ b/gorp.go @@ -16,6 +16,7 @@ import ( "database/sql" "errors" "fmt" + "github.com/jmoiron/sqlx" "reflect" "strings" ) @@ -160,6 +161,8 @@ func (t *TableMap) SetVersionCol(field string) *ColumnMap { return c } +// A bindPlan saves a query type (insert, get, updated, delete) so it doesn't +// have to be re-created every time it's executed. type bindPlan struct { query string argFields []string @@ -431,14 +434,18 @@ type ColumnMap struct { isAutoIncr bool } +// This mapping should be known ahead of time, and this is the one case where +// I think I want things to actually be done in the struct tags instead of +// being changed at runtime where other systems then do not have access to them + // Rename allows you to specify the column name in the table // // Example: table.ColMap("Updated").Rename("date_updated") // -func (c *ColumnMap) Rename(colname string) *ColumnMap { - c.ColumnName = colname - return c -} +//func (c *ColumnMap) Rename(colname string) *ColumnMap { +// c.ColumnName = colname +// return c +//} // SetTransient allows you to mark the column as transient. If true // this column will be skipped when SQL statements are generated @@ -468,7 +475,7 @@ func (c *ColumnMap) SetMaxSize(size int) *ColumnMap { // a call to Commit() or Rollback() type Transaction struct { dbmap *DbMap - tx *sql.Tx + tx *sqlx.Tx } // SqlExecutor exposes gorp operations that can be run from Pre/Post @@ -478,15 +485,15 @@ type Transaction struct { // See the DbMap function docs for each of the functions below for more // information. type SqlExecutor interface { - Get(i interface{}, keys ...interface{}) (interface{}, error) + Get(dest interface{}, keys ...interface{}) error Insert(list ...interface{}) error Update(list ...interface{}) (int64, error) Delete(list ...interface{}) (int64, error) Exec(query string, args ...interface{}) (sql.Result, error) - Select(i interface{}, query string, - args ...interface{}) ([]interface{}, error) + Select(i interface{}, query string, args ...interface{}) ([]interface{}, error) query(query string, args ...interface{}) (*sql.Rows, error) queryRow(query string, args ...interface{}) *sql.Row + queryRowx(query string, args ...interface{}) *sqlx.Row } // Insert runs a SQL INSERT statement for each element in list. List @@ -546,8 +553,8 @@ func (m *DbMap) Delete(list ...interface{}) (int64, error) { // // Returns an error if SetKeys has not been called on the TableMap // Panics if any interface in the list has not been registered with AddTable -func (m *DbMap) Get(i interface{}, keys ...interface{}) (interface{}, error) { - return get(m, m, i, keys...) +func (m *DbMap) Get(dest interface{}, keys ...interface{}) error { + return get(m, m, dest, keys...) } // Select runs an arbitrary SQL query, binding the columns in the result @@ -593,7 +600,7 @@ func (m *DbMap) Begin() (*Transaction, error) { if err != nil { return nil, err } - return &Transaction{m, tx}, nil + return &Transaction{m, &sqlx.Tx{*tx}}, nil } func (m *DbMap) tableFor(t reflect.Type, checkPK bool) (*TableMap, error) { @@ -643,6 +650,11 @@ func (m *DbMap) queryRow(query string, args ...interface{}) *sql.Row { return m.Db.QueryRow(query, args...) } +func (m *DbMap) queryRowx(query string, args ...interface{}) *sqlx.Row { + m.trace(query, args) + return m.Dbx.QueryRowx(query, args...) +} + func (m *DbMap) query(query string, args ...interface{}) (*sql.Rows, error) { m.trace(query, args) return m.Db.Query(query, args...) @@ -672,8 +684,8 @@ func (t *Transaction) Delete(list ...interface{}) (int64, error) { } // Same behavior as DbMap.Get(), but runs in a transaction -func (t *Transaction) Get(i interface{}, keys ...interface{}) (interface{}, error) { - return get(t.dbmap, t, i, keys...) +func (t *Transaction) Get(dest interface{}, keys ...interface{}) error { + return get(t.dbmap, t, dest, keys...) } // Same behavior as DbMap.Select(), but runs in a transaction @@ -706,6 +718,11 @@ func (t *Transaction) queryRow(query string, args ...interface{}) *sql.Row { return t.tx.QueryRow(query, args...) } +func (t *Transaction) queryRowx(query string, args ...interface{}) *sqlx.Row { + t.dbmap.trace(query, args) + return t.tx.QueryRowx(query, args...) +} + func (t *Transaction) query(query string, args ...interface{}) (*sql.Rows, error) { t.dbmap.trace(query, args) return t.tx.Query(query, args...) @@ -745,6 +762,21 @@ func hookedselect(m *DbMap, exec SqlExecutor, i interface{}, query string, return list, nil } +func toType(i interface{}) (reflect.Type, error) { + t := reflect.TypeOf(i) + + // If a Pointer to a type, follow + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + + if t.Kind() != reflect.Struct { + return nil, errors.New("Cannot select into non-struct type.") + } + return t, nil + +} + func rawselect(m *DbMap, exec SqlExecutor, i interface{}, query string, args ...interface{}) ([]interface{}, error) { appendToSlice := false // Write results to i directly? @@ -762,6 +794,7 @@ func rawselect(m *DbMap, exec SqlExecutor, i interface{}, query string, args ... if err != nil { return nil, err } + defer rows.Close() // Fetch the column names as returned from db @@ -912,72 +945,33 @@ func toSliceType(i interface{}) reflect.Type { return t } -func toType(i interface{}) (reflect.Type, error) { - t := reflect.TypeOf(i) - - // If a Pointer to a type, follow - for t.Kind() == reflect.Ptr { - t = t.Elem() - } - - if t.Kind() != reflect.Struct { - return nil, errors.New(fmt.Sprintf("gorp: Cannot SELECT into non-struct type: %v", reflect.TypeOf(i))) - } - return t, nil -} - -func get(m *DbMap, exec SqlExecutor, i interface{}, - keys ...interface{}) (interface{}, error) { +func get(m *DbMap, exec SqlExecutor, dest interface{}, keys ...interface{}) error { - t, err := toType(i) + t, err := sqlx.BaseStructType(reflect.TypeOf(dest)) if err != nil { - return nil, err + return err } table, err := m.tableFor(t, true) if err != nil { - return nil, err + return err } plan := table.bindGet() - - v := reflect.New(t) - dest := make([]interface{}, len(plan.argFields)) - - custScan := make([]CustomScanner, 0) - - for x, fieldName := range plan.argFields { - f := v.Elem().FieldByName(fieldName) - target := f.Addr().Interface() - dest[x] = target - } - - row := exec.queryRow(plan.query, keys...) - err = row.Scan(dest...) + row := exec.queryRowx(plan.query, keys...) + err = row.StructScan(dest) if err != nil { - if err == sql.ErrNoRows { - err = nil - } - return nil, err - } - - for _, c := range custScan { - err = c.Bind() - if err != nil { - return nil, err - } + return err } - vi := v.Interface() - if table.CanPostGet { - err = vi.(PostGetter).PostGet(exec) + err = dest.(PostGetter).PostGet(exec) if err != nil { - return nil, err + return err } } - return vi, nil + return nil } func delete(m *DbMap, exec SqlExecutor, list ...interface{}) (int64, error) { @@ -1157,14 +1151,15 @@ func lockError(m *DbMap, exec SqlExecutor, tableName string, existingVer int64, elem reflect.Value, keys ...interface{}) (int64, error) { - existing, err := get(m, exec, elem.Interface(), keys...) + dest := reflect.New(elem.Type()) + err := get(m, exec, dest, keys...) if err != nil { return -1, err } ole := OptimisticLockError{tableName, keys, true, existingVer} - if existing == nil { - ole.RowExists = false - } + //if dest == nil { + // ole.RowExists = false + //} return -1, ole } diff --git a/gorp_test.go b/gorp_test.go index 3e29874..edbfbda 100644 --- a/gorp_test.go +++ b/gorp_test.go @@ -7,16 +7,18 @@ import ( _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" _ "github.com/ziutek/mymysql/godrv" - //"log" + "log" "os" "reflect" "testing" "time" ) +var _ = log.Fatal + type Invoice struct { Id int64 - Created int64 + Created int64 `db:"date_created"` Updated int64 Memo string PersonId int64 @@ -102,7 +104,7 @@ func (p *Person) PostGet(s SqlExecutor) error { } type PersistentUser struct { - Key int32 + Key int32 `db:"mykey"` Id string PassedTraining bool } @@ -121,8 +123,7 @@ func TestPersistentUser(t *testing.T) { dbmap := newDbMap() dbmap.Exec("drop table if exists PersistentUser") //dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) - table := dbmap.AddTable(PersistentUser{}).SetKeys(false, "Key") - table.ColMap("Key").Rename("mykey") + dbmap.AddTable(PersistentUser{}).SetKeys(false, "mykey") err := dbmap.CreateTablesIfNotExists() if err != nil { panic(err) @@ -135,7 +136,8 @@ func TestPersistentUser(t *testing.T) { } // prove we can pass a pointer into Get - pu2, err := dbmap.Get(pu, pu.Key) + pu2 := &PersistentUser{} + err = dbmap.Get(pu2, pu.Key) if err != nil { panic(err) } @@ -145,17 +147,17 @@ func TestPersistentUser(t *testing.T) { arr, err := dbmap.Select(pu, "select * from PersistentUser") if err != nil { - panic(err) + t.Error(err) } if !reflect.DeepEqual(pu, arr[0]) { t.Errorf("%v!=%v", pu, arr[0]) } // prove we can get the results back in a slice - var puArr []*PersistentUser + puArr := []*PersistentUser{} _, err = dbmap.Select(&puArr, "select * from PersistentUser") if err != nil { - panic(err) + t.Error(err) } if len(puArr) != 1 { t.Errorf("Expected one persistentuser, found none") @@ -205,6 +207,7 @@ func TestOverrideVersionCol(t *testing.T) { } func TestOptimisticLocking(t *testing.T) { + var err error dbmap := initDbMap() defer dbmap.DropTables() @@ -219,13 +222,14 @@ func TestOptimisticLocking(t *testing.T) { return } - obj, err := dbmap.Get(Person{}, p1.Id) + p2 := Person{} + err = dbmap.Get(&p2, p1.Id) if err != nil { panic(err) } - p2 := obj.(*Person) p2.LName = "Edwards" dbmap.Update(p2) // Version is now 2 + if p2.Version != 2 { t.Errorf("Update didn't incr Version: %d != %d", 2, p2.Version) } @@ -269,8 +273,8 @@ func TestNullValues(t *testing.T) { // try to load it expected := &TableWithNull{Id: 10} - obj := _get(dbmap, TableWithNull{}, 10) - t1 := obj.(*TableWithNull) + t1 := &TableWithNull{} + MustGet(dbmap, t1, 10) if !reflect.DeepEqual(expected, t1) { t.Errorf("%v != %v", expected, t1) } @@ -288,8 +292,7 @@ func TestNullValues(t *testing.T) { expected.Bytes = t1.Bytes _update(dbmap, t1) - obj = _get(dbmap, TableWithNull{}, 10) - t1 = obj.(*TableWithNull) + MustGet(dbmap, t1, 10) if t1.Str.String != "hi" { t.Errorf("%s != hi", t1.Str.String) } @@ -302,7 +305,7 @@ func TestColumnProps(t *testing.T) { dbmap := newDbMap() //dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) t1 := dbmap.AddTable(Invoice{}).SetKeys(true, "Id") - t1.ColMap("Created").Rename("date_created") + //t1.ColMap("Created").Rename("date_created") t1.ColMap("Updated").SetTransient(true) t1.ColMap("Memo").SetMaxSize(10) t1.ColMap("PersonId").SetUnique(true) @@ -316,15 +319,15 @@ func TestColumnProps(t *testing.T) { // test transient inv := &Invoice{0, 0, 1, "my invoice", 0, true} _insert(dbmap, inv) - obj := _get(dbmap, Invoice{}, inv.Id) - inv = obj.(*Invoice) - if inv.Updated != 0 { + inv2 := Invoice{} + MustGet(dbmap, &inv2, inv.Id) + if inv2.Updated != 0 { t.Errorf("Saved transient column 'Updated'") } // test max size - inv.Memo = "this memo is too long" - err = dbmap.Insert(inv) + inv2.Memo = "this memo is too long" + err = dbmap.Insert(inv2) if err == nil { t.Errorf("max size exceeded, but Insert did not fail.") } @@ -372,8 +375,7 @@ func TestHooks(t *testing.T) { t.Errorf("p1.PostInsert() didn't run: %v", p1) } - obj := _get(dbmap, Person{}, p1.Id) - p1 = obj.(*Person) + MustGet(dbmap, p1, p1.Id) if p1.LName != "postget" { t.Errorf("p1.PostGet() didn't run: %v", p1) } @@ -424,14 +426,15 @@ func TestTransaction(t *testing.T) { panic(err) } - obj, err := dbmap.Get(Invoice{}, inv1.Id) + obj := &Invoice{} + err = dbmap.Get(obj, inv1.Id) if err != nil { panic(err) } if !reflect.DeepEqual(inv1, obj) { t.Errorf("%v != %v", inv1, obj) } - obj, err = dbmap.Get(Invoice{}, inv2.Id) + err = dbmap.Get(obj, inv2.Id) if err != nil { panic(err) } @@ -472,8 +475,8 @@ func TestCrud(t *testing.T) { } // SELECT row - obj := _get(dbmap, Invoice{}, inv.Id) - inv2 := obj.(*Invoice) + inv2 := &Invoice{} + MustGet(dbmap, inv2, inv.Id) if !reflect.DeepEqual(inv, inv2) { t.Errorf("%v != %v", inv, inv2) } @@ -486,8 +489,8 @@ func TestCrud(t *testing.T) { if count != 1 { t.Errorf("update 1 != %d", count) } - obj = _get(dbmap, Invoice{}, inv.Id) - inv2 = obj.(*Invoice) + + MustGet(dbmap, inv2, inv.Id) if !reflect.DeepEqual(inv, inv2) { t.Errorf("%v != %v", inv, inv2) } @@ -500,8 +503,8 @@ func TestCrud(t *testing.T) { } // VERIFY deleted - obj = _get(dbmap, Invoice{}, inv.Id) - if obj != nil { + err := dbmap.Get(inv2, inv.Id) + if err != nil { t.Errorf("Found invoice with id: %d after Delete()", inv.Id) } } @@ -513,17 +516,22 @@ func TestWithIgnoredColumn(t *testing.T) { ic := &WithIgnoredColumn{-1, 0, 1} _insert(dbmap, ic) expected := &WithIgnoredColumn{0, 1, 1} - ic2 := _get(dbmap, WithIgnoredColumn{}, ic.Id).(*WithIgnoredColumn) + + ic2 := &WithIgnoredColumn{} + MustGet(dbmap, ic2, ic.Id) if !reflect.DeepEqual(expected, ic2) { t.Errorf("%v != %v", expected, ic2) } + if _del(dbmap, ic) != 1 { t.Errorf("Did not delete row with Id: %d", ic.Id) return } - if _get(dbmap, WithIgnoredColumn{}, ic.Id) != nil { - t.Errorf("Found id: %d after Delete()", ic.Id) + + err := dbmap.Get(ic2, ic.Id) + if err != nil { + t.Errorf("Found id: %d after Delete() (%#v)", ic.Id, ic2) } } @@ -651,16 +659,12 @@ func BenchmarkGorpCrud(b *testing.B) { panic(err) } - obj, err := dbmap.Get(Invoice{}, inv.Id) + inv2 := Invoice{} + err = dbmap.Get(&inv2, inv.Id) if err != nil { panic(err) } - inv2, ok := obj.(*Invoice) - if !ok { - panic(fmt.Sprintf("expected *Invoice, got: %v", obj)) - } - inv2.Created = 1000 inv2.Updated = 2000 inv2.Memo = "my memo 2" @@ -716,7 +720,7 @@ func initDbMapNulls() *DbMap { func newDbMap() *DbMap { dialect, driver := dialectAndDriver() - return &DbMap{Db: connect(driver), Dialect: dialect} + return NewDbMap(connect(driver), dialect) } func connect(driver string) *sql.DB { @@ -768,13 +772,11 @@ func _del(dbmap *DbMap, list ...interface{}) int64 { return count } -func _get(dbmap *DbMap, i interface{}, keys ...interface{}) interface{} { - obj, err := dbmap.Get(i, keys...) +func MustGet(dbmap *DbMap, i interface{}, keys ...interface{}) { + err := dbmap.Get(i, keys...) if err != nil { panic(err) } - - return obj } func _rawexec(dbmap *DbMap, query string, args ...interface{}) sql.Result { diff --git a/todo.md b/todo.md index 1e74a52..976b82d 100644 --- a/todo.md +++ b/todo.md @@ -1,6 +1,13 @@ -- replace hook calling process with one that uses interfaces and reflect.CanInterface +Todo: + - remove list return support - replace reflect struct filling with structscan - cache/store as much reflect stuff as possible - add query builder +Almost Done: + +- replace hook calling process with one that uses interfaces + +Done: + From 8c37ae15df91472c8df1f4b678b6892e36b82388 Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Mon, 27 May 2013 15:16:27 -0400 Subject: [PATCH 08/20] all mappers strings.ToLower() field & db names by default; must use tags and TableWithName for different behavior. I prefer this default because InitiialCaps is part of go's visibility and not a style guideline, and trying to CamelCase->snake_case is beset by weird behavior. --- gorp.go | 4 ++-- gorp_test.go | 24 ++++++++++++------------ mapper.go | 2 +- test_all.sh | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/gorp.go b/gorp.go index 8968264..20debe0 100644 --- a/gorp.go +++ b/gorp.go @@ -116,7 +116,7 @@ func (t *TableMap) ResetSql() { func (t *TableMap) SetKeys(isAutoIncr bool, fieldNames ...string) *TableMap { t.keys = make([]*ColumnMap, 0) for _, name := range fieldNames { - colmap := t.ColMap(name) + colmap := t.ColMap(strings.ToLower(name)) colmap.isPK = true colmap.isAutoIncr = isAutoIncr t.keys = append(t.keys, colmap) @@ -828,7 +828,7 @@ func rawselect(m *DbMap, exec SqlExecutor, i interface{}, query string, args ... if fieldName == "-" { continue } else if fieldName == "" { - fieldName = field.Name + fieldName = strings.ToLower(field.Name) } if tableMapped { colMap := colMapOrNil(table, fieldName) diff --git a/gorp_test.go b/gorp_test.go index edbfbda..fb35535 100644 --- a/gorp_test.go +++ b/gorp_test.go @@ -121,7 +121,7 @@ func TestCreateTablesIfNotExists(t *testing.T) { func TestPersistentUser(t *testing.T) { dbmap := newDbMap() - dbmap.Exec("drop table if exists PersistentUser") + dbmap.Exec("drop table if exists persistentuser") //dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) dbmap.AddTable(PersistentUser{}).SetKeys(false, "mykey") err := dbmap.CreateTablesIfNotExists() @@ -145,7 +145,7 @@ func TestPersistentUser(t *testing.T) { t.Errorf("%v!=%v", pu, pu2) } - arr, err := dbmap.Select(pu, "select * from PersistentUser") + arr, err := dbmap.Select(pu, "select * from persistentuser") if err != nil { t.Error(err) } @@ -155,7 +155,7 @@ func TestPersistentUser(t *testing.T) { // prove we can get the results back in a slice puArr := []*PersistentUser{} - _, err = dbmap.Select(&puArr, "select * from PersistentUser") + _, err = dbmap.Select(&puArr, "select * from persistentuser") if err != nil { t.Error(err) } @@ -187,15 +187,15 @@ func TestReturnsNonNilSlice(t *testing.T) { func TestOverrideVersionCol(t *testing.T) { dbmap := initDbMap() dbmap.DropTables() - t1 := dbmap.AddTable(InvoicePersonView{}).SetKeys(false, "InvoiceId", "PersonId") + t1 := dbmap.AddTable(InvoicePersonView{}).SetKeys(false, "invoiceid", "personid") err := dbmap.CreateTables() if err != nil { panic(err) } defer dbmap.DropTables() - c1 := t1.SetVersionCol("LegacyVersion") - if c1.ColumnName != "LegacyVersion" { + c1 := t1.SetVersionCol("legacyversion") + if c1.ColumnName != "legacyversion" { t.Errorf("Wrong col returned: %v", c1) } @@ -268,7 +268,7 @@ func TestNullValues(t *testing.T) { defer dbmap.DropTables() // insert a row directly - _rawexec(dbmap, "insert into TableWithNull values (10, null, "+ + _rawexec(dbmap, "insert into tablewithnull values (10, null, "+ "null, null, null, null)") // try to load it @@ -685,7 +685,7 @@ func BenchmarkGorpCrud(b *testing.B) { func initDbMapBench() *DbMap { dbmap := newDbMap() dbmap.Db.Exec("drop table if exists invoice_test") - dbmap.AddTableWithName(Invoice{}, "invoice_test").SetKeys(true, "Id") + dbmap.AddTableWithName(Invoice{}, "invoice_test").SetKeys(true, "id") err := dbmap.CreateTables() if err != nil { panic(err) @@ -696,9 +696,9 @@ func initDbMapBench() *DbMap { func initDbMap() *DbMap { dbmap := newDbMap() //dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) - dbmap.AddTableWithName(Invoice{}, "invoice_test").SetKeys(true, "Id") - dbmap.AddTableWithName(Person{}, "person_test").SetKeys(true, "Id") - dbmap.AddTableWithName(WithIgnoredColumn{}, "ignored_column_test").SetKeys(true, "Id") + dbmap.AddTableWithName(Invoice{}, "invoice_test").SetKeys(true, "id") + dbmap.AddTableWithName(Person{}, "person_test").SetKeys(true, "id") + dbmap.AddTableWithName(WithIgnoredColumn{}, "ignored_column_test").SetKeys(true, "id") err := dbmap.CreateTables() if err != nil { panic(err) @@ -710,7 +710,7 @@ func initDbMap() *DbMap { func initDbMapNulls() *DbMap { dbmap := newDbMap() //dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) - dbmap.AddTable(TableWithNull{}).SetKeys(false, "Id") + dbmap.AddTable(TableWithNull{}).SetKeys(false, "id") err := dbmap.CreateTables() if err != nil { panic(err) diff --git a/mapper.go b/mapper.go index a3e685b..1eae250 100644 --- a/mapper.go +++ b/mapper.go @@ -97,7 +97,7 @@ func (m *DbMap) AddTable(i interface{}, name ...string) *TableMap { f := t.Field(i) columnName := f.Tag.Get("db") if columnName == "" { - columnName = f.Name + columnName = strings.ToLower(f.Name) } cm := &ColumnMap{ diff --git a/test_all.sh b/test_all.sh index 555253e..48b05dc 100755 --- a/test_all.sh +++ b/test_all.sh @@ -2,7 +2,7 @@ set -e -export GORP_TEST_DSN="gorptest/gorptest/" +export GORP_TEST_DSN="gorptest/gorptest/gorptest" export GORP_TEST_DIALECT="mysql" go test From 605458c81775bde2a43f012fba4b94362d9bc15b Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Wed, 29 May 2013 21:37:13 -0400 Subject: [PATCH 09/20] all tests passing, performance isn't that hot --- gorp.go | 16 +++++++--------- gorp_test.go | 14 +++++++++----- todo.md | 7 +++++-- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/gorp.go b/gorp.go index 20debe0..9917c83 100644 --- a/gorp.go +++ b/gorp.go @@ -960,6 +960,7 @@ func get(m *DbMap, exec SqlExecutor, dest interface{}, keys ...interface{}) erro plan := table.bindGet() row := exec.queryRowx(plan.query, keys...) err = row.StructScan(dest) + if err != nil { return err } @@ -1006,8 +1007,7 @@ func delete(m *DbMap, exec SqlExecutor, list ...interface{}) (int64, error) { } if rows == 0 && bi.existingVersion > 0 { - return lockError(m, exec, table.TableName, - bi.existingVersion, elem, bi.keys...) + return lockError(m, exec, table.TableName, bi.existingVersion, elem, bi.keys...) } count += rows @@ -1147,19 +1147,17 @@ func runHook(name string, eptr reflect.Value, arg []reflect.Value) error { return nil } -func lockError(m *DbMap, exec SqlExecutor, tableName string, - existingVer int64, elem reflect.Value, - keys ...interface{}) (int64, error) { +func lockError(m *DbMap, exec SqlExecutor, tableName string, existingVer int64, elem reflect.Value, keys ...interface{}) (int64, error) { - dest := reflect.New(elem.Type()) + dest := reflect.New(elem.Type()).Interface() err := get(m, exec, dest, keys...) if err != nil { return -1, err } ole := OptimisticLockError{tableName, keys, true, existingVer} - //if dest == nil { - // ole.RowExists = false - //} + if dest == nil { + ole.RowExists = false + } return -1, ole } diff --git a/gorp_test.go b/gorp_test.go index fb35535..079f215 100644 --- a/gorp_test.go +++ b/gorp_test.go @@ -222,13 +222,17 @@ func TestOptimisticLocking(t *testing.T) { return } - p2 := Person{} - err = dbmap.Get(&p2, p1.Id) + p2 := &Person{} + err = dbmap.Get(p2, p1.Id) if err != nil { panic(err) } p2.LName = "Edwards" - dbmap.Update(p2) // Version is now 2 + _, err = dbmap.Update(p2) // Version is now 2 + + if err != nil { + panic(err) + } if p2.Version != 2 { t.Errorf("Update didn't incr Version: %d != %d", 2, p2.Version) @@ -504,7 +508,7 @@ func TestCrud(t *testing.T) { // VERIFY deleted err := dbmap.Get(inv2, inv.Id) - if err != nil { + if err != sql.ErrNoRows { t.Errorf("Found invoice with id: %d after Delete()", inv.Id) } } @@ -530,7 +534,7 @@ func TestWithIgnoredColumn(t *testing.T) { } err := dbmap.Get(ic2, ic.Id) - if err != nil { + if err != sql.ErrNoRows { t.Errorf("Found id: %d after Delete() (%#v)", ic.Id, ic2) } } diff --git a/todo.md b/todo.md index 976b82d..2bd36d0 100644 --- a/todo.md +++ b/todo.md @@ -1,9 +1,11 @@ Todo: -- remove list return support -- replace reflect struct filling with structscan +- remove list & new struct support form in favor of filling pointers and slices +- replace reflect struct filling with structscan from sqlx - cache/store as much reflect stuff as possible - add query builder +- update docs with new examples and + Almost Done: @@ -11,3 +13,4 @@ Almost Done: Done: +- use strings.ToLower on table & field names by default, aligning behavior w/ sqlx From 0b31b8a2b2a3da10bc9edbe86b064ba7e7a8c7c4 Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Wed, 29 May 2013 22:45:20 -0400 Subject: [PATCH 10/20] new test-all script which uses ./environ file for normal testing but is based off some env variables so it fits in well with dronio CI --- .gitignore | 1 + test-all | 41 +++++++++++++++++++++++++++++++++++++++++ test_all.sh | 15 --------------- 3 files changed, 42 insertions(+), 15 deletions(-) create mode 100755 test-all delete mode 100755 test_all.sh diff --git a/.gitignore b/.gitignore index 98f74b0..43337f2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ _obj *~ *.6 6.out +environ diff --git a/test-all b/test-all new file mode 100755 index 0000000..34ff0f8 --- /dev/null +++ b/test-all @@ -0,0 +1,41 @@ +#!/bin/sh + +if [ -f "./environ" ]; then + . ./environ +fi + +set -e + +if [ -n "$GORP_MYSQL_DSN" ]; then + export GORP_TEST_DSN="$GORP_MYSQL_DSN" + export GORP_TEST_DIALECT="mysql" + go test +else + echo "Skipping MySQL, \$GORP_MYSQL_DSN=$GORP_MYSQL_DSN" + if [ -n "$GORP_FAIL_ON_SKIP" ]; then + exit -1 + fi +fi + +if [ -n "$GORP_POSTGRES_DSN" ]; then + export GORP_TEST_DSN="$GORP_POSTGRES_DSN" + export GORP_TEST_DIALECT="postgres" + go test +else + echo "Skipping PostgreSQL, \$GORP_POSTGRES_DSN=$GORP_POSTGRES_DSN" + if [ -n "$GORP_FAIL_ON_SKIP" ]; then + exit -1 + fi +fi + +if [ -n "$GORP_SQLITE_DSN" ]; then + export GORP_TEST_DSN="$GORP_SQLITE_DSN" + export GORP_TEST_DIALECT="sqlite" + go test +else + echo "Skipping SQLite, \$GORP_SQLITE_DSN=$GORP_SQLITE_DSN" + if [ -n "$GORP_FAIL_ON_SKIP" ]; then + exit -1 + fi +fi + diff --git a/test_all.sh b/test_all.sh deleted file mode 100755 index 48b05dc..0000000 --- a/test_all.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -set -e - -export GORP_TEST_DSN="gorptest/gorptest/gorptest" -export GORP_TEST_DIALECT="mysql" -go test - -export GORP_TEST_DSN="user=$USER dbname=gorptest sslmode=disable" -export GORP_TEST_DIALECT="postgres" -go test - -export GORP_TEST_DSN="/tmp/gorptest.bin" -export GORP_TEST_DIALECT="sqlite" -go test From 4f72cf84a403af3b6bf32f4a9027b6606aeb2d19 Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Wed, 29 May 2013 22:51:06 -0400 Subject: [PATCH 11/20] add drone status and a note about the fork --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 177ce2c..8163dd2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,13 @@ -# Go Relational Persistence # +# Go Relational Persistence + +[![Build Status](https://drone.io/github.com/jmoiron/gorp/status.png)](https://drone.io/github.com/jmoiron/gorp/latest) + +**Note**: This fork is under heavy development. I plan to discuss the reasons +for this fork and some of the guiding philosophies behind what's been added and +what's been removed. For now, read the todo.md in this directory. The original +README.md follows: + +--------------------------------------------------------------------- I hesitate to call gorp an ORM. Go doesn't really have objects, at least not in the classic Smalltalk/Java sense. There goes the "O". gorp doesn't From 9ad13f8dc6863b7f2ecd411bfe4ff00d2fdd69e6 Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Wed, 29 May 2013 22:54:57 -0400 Subject: [PATCH 12/20] add some notes about test-all usage --- test-all | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test-all b/test-all index 34ff0f8..35c693b 100755 --- a/test-all +++ b/test-all @@ -1,4 +1,17 @@ #!/bin/sh +# +# To use this script, set the following environment variables: +# +# GORP_MYSQL_DSN - mysql connect DSN, like "gorptest/gorptest/gorptest" +# GORP_POSTGRES_DSN - postgres connect DSN, eg: +# "username=gorptest password=gorptest dbname=gorptest ssl-mode=disable" +# GORP_SQLITE_DSN - sqlite connect DSN, which is a path to a sqlite file. +# GORP_FAIL_ON_SKIP - optional, will fail if any DBs are skipped (for CI, mostly) +# +# In addition to this, you can create an `environ` file in this directory which +# will be sourced and ignored by git. +# + if [ -f "./environ" ]; then . ./environ From 85f775e3acea1018de7a9fcba35b211ad8d71de5 Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Wed, 29 May 2013 22:57:19 -0400 Subject: [PATCH 13/20] add some test-all explanations to the readme --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8163dd2..f515d6b 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,23 @@ **Note**: This fork is under heavy development. I plan to discuss the reasons for this fork and some of the guiding philosophies behind what's been added and -what's been removed. For now, read the todo.md in this directory. The original -README.md follows: +what's been removed. For now, read the todo.md in this directory. + +To use the `test-all` script, set the following environment variables: + + `GORP_MYSQL_DSN` - mysql connect DSN, like "gorptest/gorptest/gorptest" + `GORP_POSTGRES_DSN` - postgres connect DSN, eg: + "username=gorptest password=gorptest dbname=gorptest ssl-mode=disable" + `GORP_SQLITE_DSN` - sqlite connect DSN, which is a path to a sqlite file. + `GORP_FAIL_ON_SKIP` - optional, will fail if any DBs are skipped (for CI, mostly) + +In addition to this, you can create an `environ` file in this directory which +will be sourced and ignored by git. + +The original README.md follows: + + + --------------------------------------------------------------------- From 74f434087a46371f4af4289260ed9a3eec06b188 Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Thu, 30 May 2013 00:01:19 -0300 Subject: [PATCH 14/20] updating more readme to make it less hideous --- README.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f515d6b..0c43486 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,26 @@ what's been removed. For now, read the todo.md in this directory. To use the `test-all` script, set the following environment variables: - `GORP_MYSQL_DSN` - mysql connect DSN, like "gorptest/gorptest/gorptest" - `GORP_POSTGRES_DSN` - postgres connect DSN, eg: - "username=gorptest password=gorptest dbname=gorptest ssl-mode=disable" - `GORP_SQLITE_DSN` - sqlite connect DSN, which is a path to a sqlite file. - `GORP_FAIL_ON_SKIP` - optional, will fail if any DBs are skipped (for CI, mostly) +```sh +# mysql DSN, like "gorptest/gorptest/gorptest" +GORP_MYSQL_DSN="dbname/username/password" -In addition to this, you can create an `environ` file in this directory which -will be sourced and ignored by git. +# postgres DSN, like: +GORP_POSTGRES_DSN="username=dbname password=pw dbname=dbname ssl-node=disable" -The original README.md follows: +# sqlite DSN, which is a path +GORP_SQLITE_DSN="/dev/shm/gorptest.db" +# optional, will fail the test if any DBs are skipped (for CI, mostly) +GORP_FAIL_ON_SKIP=true +``` +In addition to this, you can create an `environ` file in this directory which +will be sourced and ignored by git. You can continue to use the `GORP_TEST_DSN` +and `GORP_TEST_DIALECT` variables if you want to manually run `go test` or if +you want to run the benchmarks, as described below. + +The original README.md follows: --------------------------------------------------------------------- From 8ccfe824ccc754894b27a02a9a9d03eeaddf2d58 Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Sat, 1 Jun 2013 13:50:02 -0400 Subject: [PATCH 15/20] finish removal of hookArg and runHook in favor of using interfaces, which will end up not using select after the list/struct return support is removed --- gorp.go | 63 +++++++++++++++++++++++++++++---------------------------- todo.md | 13 ++++++------ 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/gorp.go b/gorp.go index 9917c83..0989087 100644 --- a/gorp.go +++ b/gorp.go @@ -730,10 +730,20 @@ func (t *Transaction) query(query string, args ...interface{}) (*sql.Rows, error /////////////// -func hookedselect(m *DbMap, exec SqlExecutor, i interface{}, query string, - args ...interface{}) ([]interface{}, error) { +func hookedselect(m *DbMap, exec SqlExecutor, dest interface{}, query string, args ...interface{}) ([]interface{}, error) { - list, err := rawselect(m, exec, i, query, args...) + t, err := sqlx.BaseStructType(reflect.TypeOf(dest)) + switch t.Kind() { + case reflect.Slice: + t, err = sqlx.BaseStructType(t.Elem()) + } + if err != nil { + return nil, err + } + + table := tableOrNil(m, t) + + list, err := rawselect(m, exec, dest, query, args...) if err != nil { return nil, err } @@ -743,19 +753,26 @@ func hookedselect(m *DbMap, exec SqlExecutor, i interface{}, query string, // a query to execute SQL on every row of a queryset. // Determine where the results are: written to i, or returned in list - if t := toSliceType(i); t == nil { - for _, v := range list { - err = runHook("PostGet", reflect.ValueOf(v), hookArg(exec)) - if err != nil { - return nil, err + if table != nil { + if t := toSliceType(dest); t == nil { + if table.CanPostGet { + for _, v := range list { + err = v.(PostGetter).PostGet(exec) + if err != nil { + return nil, err + } + } } - } - } else { - resultsValue := reflect.Indirect(reflect.ValueOf(i)) - for i := 0; i < resultsValue.Len(); i++ { - err = runHook("PostGet", resultsValue.Index(i), hookArg(exec)) - if err != nil { - return nil, err + } else { + resultsValue := reflect.Indirect(reflect.ValueOf(dest)) + if table.CanPostGet { + for i := 0; i < resultsValue.Len(); i++ { + v := resultsValue.Index(i).Interface() + err = v.(PostGetter).PostGet(exec) + if err != nil { + return nil, err + } + } } } } @@ -1131,22 +1148,6 @@ func insert(m *DbMap, exec SqlExecutor, list ...interface{}) error { return nil } -func hookArg(exec SqlExecutor) []reflect.Value { - execval := reflect.ValueOf(exec) - return []reflect.Value{execval} -} - -func runHook(name string, eptr reflect.Value, arg []reflect.Value) error { - hook := eptr.MethodByName(name) - if hook != zeroVal { - ret := hook.Call(arg) - if len(ret) > 0 && !ret[0].IsNil() { - return ret[0].Interface().(error) - } - } - return nil -} - func lockError(m *DbMap, exec SqlExecutor, tableName string, existingVer int64, elem reflect.Value, keys ...interface{}) (int64, error) { dest := reflect.New(elem.Type()).Interface() diff --git a/todo.md b/todo.md index 2bd36d0..c5f24b0 100644 --- a/todo.md +++ b/todo.md @@ -1,16 +1,17 @@ Todo: -- remove list & new struct support form in favor of filling pointers and slices -- replace reflect struct filling with structscan from sqlx - cache/store as much reflect stuff as possible - add query builder -- update docs with new examples and +- update docs with new examples +- add better interfaces to control underlying types to TableMap +In Progress: -Almost Done: - -- replace hook calling process with one that uses interfaces +- remove list & new struct support form in favor of filling pointers and slices (removed in Get, not Select) +- replace reflect struct filling with structscan from sqlx (done in Get, not Select) Done: - use strings.ToLower on table & field names by default, aligning behavior w/ sqlx +- replace hook calling process with one that uses interfaces + From c1032fb9633a8d60415c8e719b73044122385e7a Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Sat, 1 Jun 2013 15:57:44 -0400 Subject: [PATCH 16/20] use sqlx.StructScan in rawquery, remove slice return for Select --- gorp.go | 191 ++++++++++----------------------------------------- gorp_test.go | 42 ++++------- 2 files changed, 50 insertions(+), 183 deletions(-) diff --git a/gorp.go b/gorp.go index 0989087..da880ac 100644 --- a/gorp.go +++ b/gorp.go @@ -490,7 +490,7 @@ type SqlExecutor interface { Update(list ...interface{}) (int64, error) Delete(list ...interface{}) (int64, error) Exec(query string, args ...interface{}) (sql.Result, error) - Select(i interface{}, query string, args ...interface{}) ([]interface{}, error) + Select(dest interface{}, query string, args ...interface{}) error query(query string, args ...interface{}) (*sql.Rows, error) queryRow(query string, args ...interface{}) *sql.Row queryRowx(query string, args ...interface{}) *sqlx.Row @@ -576,14 +576,12 @@ func (m *DbMap) Get(dest interface{}, keys ...interface{}) error { // and nil returned. // // i does NOT need to be registered with AddTable() -func (m *DbMap) Select(i interface{}, query string, args ...interface{}) ([]interface{}, error) { +func (m *DbMap) Select(i interface{}, query string, args ...interface{}) error { return hookedselect(m, m, i, query, args...) } -// FIXME: this comment is wrong, query preparation is commented out - // Exec runs an arbitrary SQL statement. args represent the bind parameters. -// This is equivalent to running: Prepare(), Exec() using database/sql +// This is equivalent to running Exec() using database/sql func (m *DbMap) Exec(query string, args ...interface{}) (sql.Result, error) { m.trace(query, args) //stmt, err := m.Db.Prepare(query) @@ -596,11 +594,11 @@ func (m *DbMap) Exec(query string, args ...interface{}) (sql.Result, error) { // Begin starts a gorp Transaction func (m *DbMap) Begin() (*Transaction, error) { - tx, err := m.Db.Begin() + tx, err := m.Dbx.Beginx() if err != nil { return nil, err } - return &Transaction{m, &sqlx.Tx{*tx}}, nil + return &Transaction{m, tx}, nil } func (m *DbMap) tableFor(t reflect.Type, checkPK bool) (*TableMap, error) { @@ -610,8 +608,7 @@ func (m *DbMap) tableFor(t reflect.Type, checkPK bool) (*TableMap, error) { } if checkPK && len(table.keys) < 1 { - e := fmt.Sprintf("gorp: No keys defined for table: %s", - table.TableName) + e := fmt.Sprintf("gorp: No keys defined for table: %s", table.TableName) return nil, errors.New(e) } @@ -689,8 +686,8 @@ func (t *Transaction) Get(dest interface{}, keys ...interface{}) error { } // Same behavior as DbMap.Select(), but runs in a transaction -func (t *Transaction) Select(i interface{}, query string, args ...interface{}) ([]interface{}, error) { - return hookedselect(t.dbmap, t, i, query, args...) +func (t *Transaction) Select(dest interface{}, query string, args ...interface{}) error { + return hookedselect(t.dbmap, t, dest, query, args...) } // Same behavior as DbMap.Exec(), but runs in a transaction @@ -730,7 +727,7 @@ func (t *Transaction) query(query string, args ...interface{}) (*sql.Rows, error /////////////// -func hookedselect(m *DbMap, exec SqlExecutor, dest interface{}, query string, args ...interface{}) ([]interface{}, error) { +func hookedselect(m *DbMap, exec SqlExecutor, dest interface{}, query string, args ...interface{}) error { t, err := sqlx.BaseStructType(reflect.TypeOf(dest)) switch t.Kind() { @@ -738,45 +735,41 @@ func hookedselect(m *DbMap, exec SqlExecutor, dest interface{}, query string, ar t, err = sqlx.BaseStructType(t.Elem()) } if err != nil { - return nil, err + return err } table := tableOrNil(m, t) - list, err := rawselect(m, exec, dest, query, args...) + err = rawselect(m, exec, dest, query, args...) if err != nil { - return nil, err + return err } + // FIXME: In order to run hooks here we have to use reflect to loop over dest + // If we used sqlx.Rows.StructScan to pull one row at a time, we could do it + // all at once, but then we would lose some of the efficiencies of StructScan(sql.Rows) + // FIXME: should PostGet hooks be run on regular selects? a PostGet // hook has access to the object and the database, and I'd hate for // a query to execute SQL on every row of a queryset. - // Determine where the results are: written to i, or returned in list - if table != nil { - if t := toSliceType(dest); t == nil { - if table.CanPostGet { - for _, v := range list { - err = v.(PostGetter).PostGet(exec) - if err != nil { - return nil, err - } - } - } - } else { - resultsValue := reflect.Indirect(reflect.ValueOf(dest)) - if table.CanPostGet { - for i := 0; i < resultsValue.Len(); i++ { - v := resultsValue.Index(i).Interface() - err = v.(PostGetter).PostGet(exec) - if err != nil { - return nil, err - } - } + if table != nil && table.CanPostGet { + var x interface{} + v := reflect.ValueOf(dest) + if v.Kind() == reflect.Ptr { + v = reflect.Indirect(v) + } + l := v.Len() + for i := 0; i < l; i++ { + x = v.Index(i).Interface() + err = x.(PostGetter).PostGet(exec) + if err != nil { + return err } + } } - return list, nil + return nil } func toType(i interface{}) (reflect.Type, error) { @@ -794,129 +787,19 @@ func toType(i interface{}) (reflect.Type, error) { } -func rawselect(m *DbMap, exec SqlExecutor, i interface{}, query string, args ...interface{}) ([]interface{}, error) { - appendToSlice := false // Write results to i directly? - - // get type for i, verifying it's a struct or a pointer-to-slice - t, err := toType(i) - if err != nil { - if t = toSliceType(i); t == nil { - return nil, err - } - appendToSlice = true - } +func rawselect(m *DbMap, exec SqlExecutor, dest interface{}, query string, args ...interface{}) error { + // FIXME: we need to verify dest is a pointer-to-slice // Run the query - rows, err := exec.query(query, args...) + sqlrows, err := exec.query(query, args...) if err != nil { - return nil, err - } - - defer rows.Close() - - // Fetch the column names as returned from db - cols, err := rows.Columns() - if err != nil { - return nil, err - } - - // check if type t is a mapped table - if so we'll - // check the table for column aliasing below - tableMapped := false - table := tableOrNil(m, t) - if table != nil { - tableMapped = true - } - - colToFieldOffset := make([]int, len(cols)) - - numField := t.NumField() - - // Loop over column names and find field in i to bind to - // based on column name. all returned columns must match - // a field in the i struct - for x := range cols { - colToFieldOffset[x] = -1 - colName := strings.ToLower(cols[x]) - for y := 0; y < numField; y++ { - field := t.Field(y) - fieldName := field.Tag.Get("db") - - if fieldName == "-" { - continue - } else if fieldName == "" { - fieldName = strings.ToLower(field.Name) - } - if tableMapped { - colMap := colMapOrNil(table, fieldName) - if colMap != nil { - fieldName = colMap.ColumnName - } - } - fieldName = strings.ToLower(fieldName) - - if fieldName == colName { - colToFieldOffset[x] = y - break - } - } - if colToFieldOffset[x] == -1 { - e := fmt.Sprintf("gorp: No field %s in type %s (query: %s)", - colName, t.Name(), query) - return nil, errors.New(e) - } - } - - // Add results to one of these two slices. - var ( - list = make([]interface{}, 0) - sliceValue = reflect.Indirect(reflect.ValueOf(i)) - ) - - for { - if !rows.Next() { - // if error occured return rawselect - if rows.Err() != nil { - return nil, rows.Err() - } - // time to exit from outer "for" loop - break - } - v := reflect.New(t) - dest := make([]interface{}, len(cols)) - - custScan := make([]CustomScanner, 0) - - for x := range cols { - f := v.Elem().Field(colToFieldOffset[x]) - target := f.Addr().Interface() - dest[x] = target - } - - err = rows.Scan(dest...) - if err != nil { - return nil, err - } - - for _, c := range custScan { - err = c.Bind() - if err != nil { - return nil, err - } - } - - if appendToSlice { - sliceValue.Set(reflect.Append(sliceValue, v)) - } else { - list = append(list, v.Interface()) - } + return err } - if appendToSlice && sliceValue.IsNil() { - sliceValue.Set(reflect.MakeSlice(sliceValue.Type(), 0, 0)) - } + defer sqlrows.Close() + err = sqlx.StructScan(sqlrows, dest) + return err - return list, nil } func fieldByName(val reflect.Value, fieldName string) *reflect.Value { diff --git a/gorp_test.go b/gorp_test.go index 079f215..e5dd5de 100644 --- a/gorp_test.go +++ b/gorp_test.go @@ -145,7 +145,8 @@ func TestPersistentUser(t *testing.T) { t.Errorf("%v!=%v", pu, pu2) } - arr, err := dbmap.Select(pu, "select * from persistentuser") + arr := []*PersistentUser{} + err = dbmap.Select(&arr, "select * from persistentuser") if err != nil { t.Error(err) } @@ -154,36 +155,19 @@ func TestPersistentUser(t *testing.T) { } // prove we can get the results back in a slice - puArr := []*PersistentUser{} - _, err = dbmap.Select(&puArr, "select * from persistentuser") + puArr := []PersistentUser{} + err = dbmap.Select(&puArr, "select * from persistentuser") if err != nil { t.Error(err) } if len(puArr) != 1 { t.Errorf("Expected one persistentuser, found none") } - if !reflect.DeepEqual(pu, puArr[0]) { + if !reflect.DeepEqual(pu, &puArr[0]) { t.Errorf("%v!=%v", pu, puArr[0]) } } -// Ensure that the slices containing SQL results are non-nil when the result set is empty. -func TestReturnsNonNilSlice(t *testing.T) { - dbmap := initDbMap() - defer dbmap.DropTables() - noResultsSQL := "select * from invoice_test where id=99999" - var r1 []*Invoice - _rawselect(dbmap, &r1, noResultsSQL) - if r1 == nil { - t.Errorf("r1==nil") - } - - r2 := _rawselect(dbmap, Invoice{}, noResultsSQL) - if r2 == nil { - t.Errorf("r2==nil") - } -} - func TestOverrideVersionCol(t *testing.T) { dbmap := initDbMap() dbmap.DropTables() @@ -356,13 +340,14 @@ func TestRawSelect(t *testing.T) { expected := &InvoicePersonView{inv1.Id, p1.Id, inv1.Memo, p1.FName, 0} - query := "select i.Id InvoiceId, p.Id PersonId, i.Memo, p.FName " + + query := "select i.id invoiceid, p.id personid, i.memo, p.fname " + "from invoice_test i, person_test p " + - "where i.PersonId = p.Id" - list := _rawselect(dbmap, InvoicePersonView{}, query) + "where i.personid = p.id" + list := []InvoicePersonView{} + MustSelect(dbmap, &list, query) if len(list) != 1 { t.Errorf("len(list) != 1: %d", len(list)) - } else if !reflect.DeepEqual(expected, list[0]) { + } else if !reflect.DeepEqual(expected, &list[0]) { t.Errorf("%v != %v", expected, list[0]) } } @@ -393,7 +378,7 @@ func TestHooks(t *testing.T) { var persons []*Person bindVar := dbmap.Dialect.BindVar(0) - _rawselect(dbmap, &persons, "select * from person_test where id = "+bindVar, p1.Id) + MustSelect(dbmap, &persons, "select * from person_test where id = "+bindVar, p1.Id) if persons[0].LName != "postget" { t.Errorf("p1.PostGet() didn't run after select: %v", p1) } @@ -791,10 +776,9 @@ func _rawexec(dbmap *DbMap, query string, args ...interface{}) sql.Result { return res } -func _rawselect(dbmap *DbMap, i interface{}, query string, args ...interface{}) []interface{} { - list, err := dbmap.Select(i, query, args...) +func MustSelect(dbmap *DbMap, dest interface{}, query string, args ...interface{}) { + err := dbmap.Select(dest, query, args...) if err != nil { panic(err) } - return list } From aea7f79141f40459dbf6171cfef0cc426161b743 Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Sat, 1 Jun 2013 18:07:45 -0400 Subject: [PATCH 17/20] remove a lot of old introspective stuff which is no longer necessary --- gorp.go | 58 --------------------------------------------------------- 1 file changed, 58 deletions(-) diff --git a/gorp.go b/gorp.go index da880ac..7058269 100644 --- a/gorp.go +++ b/gorp.go @@ -772,21 +772,6 @@ func hookedselect(m *DbMap, exec SqlExecutor, dest interface{}, query string, ar return nil } -func toType(i interface{}) (reflect.Type, error) { - t := reflect.TypeOf(i) - - // If a Pointer to a type, follow - for t.Kind() == reflect.Ptr { - t = t.Elem() - } - - if t.Kind() != reflect.Struct { - return nil, errors.New("Cannot select into non-struct type.") - } - return t, nil - -} - func rawselect(m *DbMap, exec SqlExecutor, dest interface{}, query string, args ...interface{}) error { // FIXME: we need to verify dest is a pointer-to-slice @@ -802,49 +787,6 @@ func rawselect(m *DbMap, exec SqlExecutor, dest interface{}, query string, args } -func fieldByName(val reflect.Value, fieldName string) *reflect.Value { - // try to find field by exact match - f := val.FieldByName(fieldName) - - if f != zeroVal { - return &f - } - - // try to find by case insensitive match - only the Postgres driver - // seems to require this - in the case where columns are aliased in the sql - fieldNameL := strings.ToLower(fieldName) - fieldCount := val.NumField() - t := val.Type() - for i := 0; i < fieldCount; i++ { - sf := t.Field(i) - if strings.ToLower(sf.Name) == fieldNameL { - f := val.Field(i) - return &f - } - } - - return nil -} - -// toSliceType returns the element type of the given object, if the object is a -// "*[]*Element". If not, returns nil. -func toSliceType(i interface{}) reflect.Type { - t := reflect.TypeOf(i) - if t.Kind() != reflect.Ptr { - return nil - } - if t = t.Elem(); t.Kind() != reflect.Slice { - return nil - } - if t = t.Elem(); t.Kind() != reflect.Ptr { - return nil - } - if t = t.Elem(); t.Kind() != reflect.Struct { - return nil - } - return t -} - func get(m *DbMap, exec SqlExecutor, dest interface{}, keys ...interface{}) error { t, err := sqlx.BaseStructType(reflect.TypeOf(dest)) From 7fc58dcd277cdf37aeef16922e36fcd38cd7033a Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Sat, 1 Jun 2013 18:10:10 -0400 Subject: [PATCH 18/20] remove used-once _rawexec test function --- gorp_test.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/gorp_test.go b/gorp_test.go index e5dd5de..f767748 100644 --- a/gorp_test.go +++ b/gorp_test.go @@ -256,8 +256,10 @@ func TestNullValues(t *testing.T) { defer dbmap.DropTables() // insert a row directly - _rawexec(dbmap, "insert into tablewithnull values (10, null, "+ - "null, null, null, null)") + _, err := dbmap.Exec(`insert into tablewithnull values (10, null, null, null, null, null)`) + if err != nil { + panic(err) + } // try to load it expected := &TableWithNull{Id: 10} @@ -768,14 +770,6 @@ func MustGet(dbmap *DbMap, i interface{}, keys ...interface{}) { } } -func _rawexec(dbmap *DbMap, query string, args ...interface{}) sql.Result { - res, err := dbmap.Exec(query, args...) - if err != nil { - panic(err) - } - return res -} - func MustSelect(dbmap *DbMap, dest interface{}, query string, args ...interface{}) { err := dbmap.Select(dest, query, args...) if err != nil { From fae5be90ef67923d55ad0671ee98794229d41e8b Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Sat, 1 Jun 2013 19:31:41 -0400 Subject: [PATCH 19/20] remove panics when tables are not found, error channels are good enough for that case, move the remaining SqlExecutor interface for dbmap into mapper.go --- gorp.go | 232 +++++++++------------------------------------------ gorp_test.go | 11 +++ mapper.go | 153 +++++++++++++++++++++++++++++++++ todo.md | 5 +- 4 files changed, 205 insertions(+), 196 deletions(-) diff --git a/gorp.go b/gorp.go index 7058269..0f03a78 100644 --- a/gorp.go +++ b/gorp.go @@ -21,7 +21,14 @@ import ( "strings" ) -var zeroVal reflect.Value +type NoKeysErr struct { + Table *TableMap +} + +func (n NoKeysErr) Error() string { + return fmt.Sprintf("Could not find keys for table %v", n.Table) +} + var versFieldConst = "[gorp_ver_field]" // OptimisticLockError is returned by Update() or Delete() if the @@ -496,173 +503,6 @@ type SqlExecutor interface { queryRowx(query string, args ...interface{}) *sqlx.Row } -// Insert runs a SQL INSERT statement for each element in list. List -// items must be pointers. -// -// Any interface whose TableMap has an auto-increment primary key will -// have its last insert id bound to the PK field on the struct. -// -// Hook functions PreInsert() and/or PostInsert() will be executed -// before/after the INSERT statement if the interface defines them. -// -// Panics if any interface in the list has not been registered with AddTable -func (m *DbMap) Insert(list ...interface{}) error { - return insert(m, m, list...) -} - -// Update runs a SQL UPDATE statement for each element in list. List -// items must be pointers. -// -// Hook functions PreUpdate() and/or PostUpdate() will be executed -// before/after the UPDATE statement if the interface defines them. -// -// Returns number of rows updated -// -// Returns an error if SetKeys has not been called on the TableMap -// Panics if any interface in the list has not been registered with AddTable -func (m *DbMap) Update(list ...interface{}) (int64, error) { - return update(m, m, list...) -} - -// Delete runs a SQL DELETE statement for each element in list. List -// items must be pointers. -// -// Hook functions PreDelete() and/or PostDelete() will be executed -// before/after the DELETE statement if the interface defines them. -// -// Returns number of rows deleted -// -// Returns an error if SetKeys has not been called on the TableMap -// Panics if any interface in the list has not been registered with AddTable -func (m *DbMap) Delete(list ...interface{}) (int64, error) { - return delete(m, m, list...) -} - -// Get runs a SQL SELECT to fetch a single row from the table based on the -// primary key(s) -// -// i should be an empty value for the struct to load -// keys should be the primary key value(s) for the row to load. If -// multiple keys exist on the table, the order should match the column -// order specified in SetKeys() when the table mapping was defined. -// -// Hook function PostGet() will be executed -// after the SELECT statement if the interface defines them. -// -// Returns a pointer to a struct that matches or nil if no row is found -// -// Returns an error if SetKeys has not been called on the TableMap -// Panics if any interface in the list has not been registered with AddTable -func (m *DbMap) Get(dest interface{}, keys ...interface{}) error { - return get(m, m, dest, keys...) -} - -// Select runs an arbitrary SQL query, binding the columns in the result -// to fields on the struct specified by i. args represent the bind -// parameters for the SQL statement. -// -// Column names on the SELECT statement should be aliased to the field names -// on the struct i. Returns an error if one or more columns in the result -// do not match. It is OK if fields on i are not part of the SQL -// statement. -// -// Hook function PostGet() will be executed -// after the SELECT statement if the interface defines them. -// -// Values are returned in one of two ways: -// 1. If i is a struct or a pointer to a struct, returns a slice of pointers to -// matching rows of type i. -// 2. If i is a pointer to a slice, the results will be appended to that slice -// and nil returned. -// -// i does NOT need to be registered with AddTable() -func (m *DbMap) Select(i interface{}, query string, args ...interface{}) error { - return hookedselect(m, m, i, query, args...) -} - -// Exec runs an arbitrary SQL statement. args represent the bind parameters. -// This is equivalent to running Exec() using database/sql -func (m *DbMap) Exec(query string, args ...interface{}) (sql.Result, error) { - m.trace(query, args) - //stmt, err := m.Db.Prepare(query) - //if err != nil { - // return nil, err - //} - //fmt.Println("Exec", query, args) - return m.Db.Exec(query, args...) -} - -// Begin starts a gorp Transaction -func (m *DbMap) Begin() (*Transaction, error) { - tx, err := m.Dbx.Beginx() - if err != nil { - return nil, err - } - return &Transaction{m, tx}, nil -} - -func (m *DbMap) tableFor(t reflect.Type, checkPK bool) (*TableMap, error) { - table := tableOrNil(m, t) - if table == nil { - panic(fmt.Sprintf("No table found for type: %v", t.Name())) - } - - if checkPK && len(table.keys) < 1 { - e := fmt.Sprintf("gorp: No keys defined for table: %s", table.TableName) - return nil, errors.New(e) - } - - return table, nil -} - -func tableOrNil(m *DbMap, t reflect.Type) *TableMap { - for i := range m.tables { - table := m.tables[i] - if table.gotype == t { - return table - } - } - return nil -} - -func (m *DbMap) tableForPointer(ptr interface{}, checkPK bool) (*TableMap, reflect.Value, error) { - ptrv := reflect.ValueOf(ptr) - if ptrv.Kind() != reflect.Ptr { - e := fmt.Sprintf("gorp: passed non-pointer: %v (kind=%v)", ptr, - ptrv.Kind()) - return nil, reflect.Value{}, errors.New(e) - } - elem := ptrv.Elem() - etype := reflect.TypeOf(elem.Interface()) - t, err := m.tableFor(etype, checkPK) - if err != nil { - return nil, reflect.Value{}, err - } - - return t, elem, nil -} - -func (m *DbMap) queryRow(query string, args ...interface{}) *sql.Row { - m.trace(query, args) - return m.Db.QueryRow(query, args...) -} - -func (m *DbMap) queryRowx(query string, args ...interface{}) *sqlx.Row { - m.trace(query, args) - return m.Dbx.QueryRowx(query, args...) -} - -func (m *DbMap) query(query string, args ...interface{}) (*sql.Rows, error) { - m.trace(query, args) - return m.Db.Query(query, args...) -} - -func (m *DbMap) trace(query string, args ...interface{}) { - if m.logger != nil { - m.logger.Printf("%s%s %v", m.logPrefix, query, args) - } -} - /////////////// // Same behavior as DbMap.Insert(), but runs in a transaction @@ -729,18 +569,10 @@ func (t *Transaction) query(query string, args ...interface{}) (*sql.Rows, error func hookedselect(m *DbMap, exec SqlExecutor, dest interface{}, query string, args ...interface{}) error { - t, err := sqlx.BaseStructType(reflect.TypeOf(dest)) - switch t.Kind() { - case reflect.Slice: - t, err = sqlx.BaseStructType(t.Elem()) - } - if err != nil { - return err - } - - table := tableOrNil(m, t) + // select can use arbitrary structs for join queries, so we needn't find a table + table := m.TableFor(dest) - err = rawselect(m, exec, dest, query, args...) + err := rawselect(m, exec, dest, query, args...) if err != nil { return err } @@ -789,19 +621,18 @@ func rawselect(m *DbMap, exec SqlExecutor, dest interface{}, query string, args func get(m *DbMap, exec SqlExecutor, dest interface{}, keys ...interface{}) error { - t, err := sqlx.BaseStructType(reflect.TypeOf(dest)) - if err != nil { - return err - } + table := m.TableFor(dest) - table, err := m.tableFor(t, true) - if err != nil { - return err + if table == nil { + return fmt.Errorf("Could not find table for %v", dest) + } + if len(table.keys) < 1 { + return &NoKeysErr{table} } plan := table.bindGet() row := exec.queryRowx(plan.query, keys...) - err = row.StructScan(dest) + err := row.StructScan(dest) if err != nil { return err @@ -817,6 +648,24 @@ func get(m *DbMap, exec SqlExecutor, dest interface{}, keys ...interface{}) erro return nil } +// Return a table for a pointer; error if i is not a pointer or if the +// table is not found +func tableForPointer(m *DbMap, i interface{}, checkPk bool) (*TableMap, reflect.Value, error) { + v := reflect.ValueOf(i) + if v.Kind() != reflect.Ptr { + return nil, v, fmt.Errorf("Value %v not a pointer", v) + } + v = v.Elem() + t := m.TableForType(v.Type()) + if t == nil { + return nil, v, fmt.Errorf("Could not find table for %v", t) + } + if checkPk && len(t.keys) < 1 { + return t, v, &NoKeysErr{t} + } + return t, v, nil +} + func delete(m *DbMap, exec SqlExecutor, list ...interface{}) (int64, error) { var err error var table *TableMap @@ -824,7 +673,7 @@ func delete(m *DbMap, exec SqlExecutor, list ...interface{}) (int64, error) { var count int64 for _, ptr := range list { - table, elem, err = m.tableForPointer(ptr, true) + table, elem, err = tableForPointer(m, ptr, true) if err != nil { return -1, err } @@ -872,7 +721,7 @@ func update(m *DbMap, exec SqlExecutor, list ...interface{}) (int64, error) { var count int64 for _, ptr := range list { - table, elem, err = m.tableForPointer(ptr, true) + table, elem, err = tableForPointer(m, ptr, true) if err != nil { return -1, err } @@ -927,7 +776,7 @@ func insert(m *DbMap, exec SqlExecutor, list ...interface{}) error { var elem reflect.Value for _, ptr := range list { - table, elem, err = m.tableForPointer(ptr, false) + table, elem, err = tableForPointer(m, ptr, false) if err != nil { return err } @@ -940,9 +789,6 @@ func insert(m *DbMap, exec SqlExecutor, list ...interface{}) error { } bi := table.bindInsert(elem) - if err != nil { - return err - } if bi.autoIncrIdx > -1 { id, err := m.Dialect.InsertAutoIncr(exec, bi.query, bi.args...) diff --git a/gorp_test.go b/gorp_test.go index f767748..e7068f5 100644 --- a/gorp_test.go +++ b/gorp_test.go @@ -190,6 +190,17 @@ func TestOverrideVersionCol(t *testing.T) { } } +func TestDontPanicOnInsert(t *testing.T) { + var err error + dbmap := initDbMap() + defer dbmap.DropTables() + + err = dbmap.Insert(&TableWithNull{Id: 10}) + if err == nil { + t.Errorf("Should have received an error for inserting without a known table.") + } +} + func TestOptimisticLocking(t *testing.T) { var err error dbmap := initDbMap() diff --git a/mapper.go b/mapper.go index 1eae250..a11386b 100644 --- a/mapper.go +++ b/mapper.go @@ -207,3 +207,156 @@ func (m *DbMap) DropTables() error { } return err } + +// Insert runs a SQL INSERT statement for each element in list. List +// items must be pointers, because any interface whose TableMap has an +// auto-increment PK will have its insert Id bound to the PK struct field, +// +// Hook functions PreInsert() and/or PostInsert() will be executed +// before/after the INSERT statement if the interface defines them. +func (m *DbMap) Insert(list ...interface{}) error { + return insert(m, m, list...) +} + +// Update runs a SQL UPDATE statement for each element in list. List +// items must be pointers. +// +// Hook functions PreUpdate() and/or PostUpdate() will be executed +// before/after the UPDATE statement if the interface defines them. +// +// Returns number of rows updated +// +// Returns an error if SetKeys has not been called on the TableMap or if +// any interface in the list has not been registered with AddTable +func (m *DbMap) Update(list ...interface{}) (int64, error) { + return update(m, m, list...) +} + +// Delete runs a SQL DELETE statement for each element in list. List +// items must be pointers. +// +// Hook functions PreDelete() and/or PostDelete() will be executed +// before/after the DELETE statement if the interface defines them. +// +// Returns number of rows deleted +// +// Returns an error if SetKeys has not been called on the TableMap or if +// any interface in the list has not been registered with AddTable +func (m *DbMap) Delete(list ...interface{}) (int64, error) { + return delete(m, m, list...) +} + +// Get runs a SQL SELECT to fetch a single row from the table based on the +// primary key(s) +// +// i should be an empty value for the struct to load +// keys should be the primary key value(s) for the row to load. If +// multiple keys exist on the table, the order should match the column +// order specified in SetKeys() when the table mapping was defined. +// +// Hook function PostGet() will be executed +// after the SELECT statement if the interface defines them. +// +// Returns a pointer to a struct that matches or nil if no row is found +// +// Returns an error if SetKeys has not been called on the TableMap or +// if any interface in the list has not been registered with AddTable +func (m *DbMap) Get(dest interface{}, keys ...interface{}) error { + return get(m, m, dest, keys...) +} + +// Select runs an arbitrary SQL query, binding the columns in the result +// to fields on the struct specified by i. args represent the bind +// parameters for the SQL statement. +// +// Column names on the SELECT statement should be aliased to the field names +// on the struct i. Returns an error if one or more columns in the result +// do not match. It is OK if fields on i are not part of the SQL +// statement. +// +// Hook function PostGet() will be executed +// after the SELECT statement if the interface defines them. +// +// Values are returned in one of two ways: +// 1. If i is a struct or a pointer to a struct, returns a slice of pointers to +// matching rows of type i. +// 2. If i is a pointer to a slice, the results will be appended to that slice +// and nil returned. +// +// i does NOT need to be registered with AddTable() +func (m *DbMap) Select(i interface{}, query string, args ...interface{}) error { + return hookedselect(m, m, i, query, args...) +} + +// Exec runs an arbitrary SQL statement. args represent the bind parameters. +// This is equivalent to running Exec() using database/sql +func (m *DbMap) Exec(query string, args ...interface{}) (sql.Result, error) { + m.trace(query, args) + //stmt, err := m.Db.Prepare(query) + //if err != nil { + // return nil, err + //} + //fmt.Println("Exec", query, args) + return m.Db.Exec(query, args...) +} + +// Begin starts a gorp Transaction +func (m *DbMap) Begin() (*Transaction, error) { + tx, err := m.Dbx.Beginx() + if err != nil { + return nil, err + } + return &Transaction{m, tx}, nil +} + +// Returns any matching tables for the interface i or nil if not found +// If i is a slice, then the table is given for the base slice type +func (m *DbMap) TableFor(i interface{}) *TableMap { + var t reflect.Type + v := reflect.ValueOf(i) +start: + switch v.Kind() { + case reflect.Ptr: + // dereference pointer and try again; we never want to store pointer + // types anywhere, that way we always know how to do lookups + v = v.Elem() + goto start + case reflect.Slice: + // if this is a slice of X's, we're interested in the type of X + t = v.Type().Elem() + default: + t = v.Type() + } + return m.TableForType(t) +} + +// Returns any matching tables for the type t or nil if not found +func (m *DbMap) TableForType(t reflect.Type) *TableMap { + for _, table := range m.tables { + if table.gotype == t { + return table + } + } + return nil +} + +func (m *DbMap) queryRow(query string, args ...interface{}) *sql.Row { + m.trace(query, args) + return m.Db.QueryRow(query, args...) +} + +func (m *DbMap) queryRowx(query string, args ...interface{}) *sqlx.Row { + m.trace(query, args) + return m.Dbx.QueryRowx(query, args...) +} + +func (m *DbMap) query(query string, args ...interface{}) (*sql.Rows, error) { + m.trace(query, args) + return m.Db.Query(query, args...) +} + +func (m *DbMap) trace(query string, args ...interface{}) { + if m.logger != nil { + m.logger.Printf("%s%s %v", m.logPrefix, query, args) + } +} diff --git a/todo.md b/todo.md index c5f24b0..827c94d 100644 --- a/todo.md +++ b/todo.md @@ -7,11 +7,10 @@ Todo: In Progress: -- remove list & new struct support form in favor of filling pointers and slices (removed in Get, not Select) -- replace reflect struct filling with structscan from sqlx (done in Get, not Select) - Done: +- remove list & new struct support form in favor of filling pointers and slices +- replace reflect struct filling with structscan from sqlx - use strings.ToLower on table & field names by default, aligning behavior w/ sqlx - replace hook calling process with one that uses interfaces From 2cbbb9c17481557d3d054b57701dd525e636dbec Mon Sep 17 00:00:00 2001 From: Jason Moiron Date: Sun, 2 Jun 2013 13:57:45 -0400 Subject: [PATCH 20/20] allow ColumnMaps to set an sqltype which is not used yet but will be used in sql creation --- mapper.go => dbmap.go | 0 gorp.go | 40 ++++++++++++++++++---------------------- todo.md | 1 + 3 files changed, 19 insertions(+), 22 deletions(-) rename mapper.go => dbmap.go (100%) diff --git a/mapper.go b/dbmap.go similarity index 100% rename from mapper.go rename to dbmap.go diff --git a/gorp.go b/gorp.go index 0f03a78..48ce646 100644 --- a/gorp.go +++ b/gorp.go @@ -437,23 +437,11 @@ type ColumnMap struct { fieldName string gotype reflect.Type + sqltype string isPK bool isAutoIncr bool } -// This mapping should be known ahead of time, and this is the one case where -// I think I want things to actually be done in the struct tags instead of -// being changed at runtime where other systems then do not have access to them - -// Rename allows you to specify the column name in the table -// -// Example: table.ColMap("Updated").Rename("date_updated") -// -//func (c *ColumnMap) Rename(colname string) *ColumnMap { -// c.ColumnName = colname -// return c -//} - // SetTransient allows you to mark the column as transient. If true // this column will be skipped when SQL statements are generated func (c *ColumnMap) SetTransient(b bool) *ColumnMap { @@ -468,6 +456,14 @@ func (c *ColumnMap) SetUnique(b bool) *ColumnMap { return c } +// Set the column's sql type. This is a string, such as 'varchar(32)' or +// 'text', which will be used by CreateTable and nothing else. It is the +// caller's responsibility to ensure this will map cleanly to the struct +func (c *ColumnMap) SetSqlType(t string) *ColumnMap { + c.sqltype = t + return c +} + // SetMaxSize specifies the max length of values of this column. This is // passed to the dialect.ToSqlType() function, which can use the value // to alter the generated type for "create table" statements @@ -476,15 +472,6 @@ func (c *ColumnMap) SetMaxSize(size int) *ColumnMap { return c } -// Transaction represents a database transaction. -// Insert/Update/Delete/Get/Exec operations will be run in the context -// of that transaction. Transactions should be terminated with -// a call to Commit() or Rollback() -type Transaction struct { - dbmap *DbMap - tx *sqlx.Tx -} - // SqlExecutor exposes gorp operations that can be run from Pre/Post // hooks. This hides whether the current operation that triggered the // hook is in a transaction. @@ -505,6 +492,15 @@ type SqlExecutor interface { /////////////// +// Transaction represents a database transaction. +// Insert/Update/Delete/Get/Exec operations will be run in the context +// of that transaction. Transactions should be terminated with +// a call to Commit() or Rollback() +type Transaction struct { + dbmap *DbMap + tx *sqlx.Tx +} + // Same behavior as DbMap.Insert(), but runs in a transaction func (t *Transaction) Insert(list ...interface{}) error { return insert(t.dbmap, t, list...) diff --git a/todo.md b/todo.md index 827c94d..b8f1d4f 100644 --- a/todo.md +++ b/todo.md @@ -1,5 +1,6 @@ Todo: +- benchmarks that can compare mainline gorp to this fork - cache/store as much reflect stuff as possible - add query builder - update docs with new examples