From c01d95d57566a36825b98ff52026c75bf1ae23b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Nieto?= <jose.carlos@menteslibres.net> Date: Tue, 10 Jun 2014 20:26:39 -0500 Subject: [PATCH] Upgrading MySQL adapter to use templates. --- mysql/_dumps/structs.sql | 20 ++ mysql/_example/main.go | 12 +- mysql/_example/test.sh | 2 +- mysql/collection.go | 355 +++++++++---------- mysql/database.go | 407 ++++++++++++--------- mysql/database_test.go | 742 ++++++++++++++++++++++++++------------- mysql/layout.go | 124 +++++++ mysql/result.go | 193 +++++----- mysql/tx.go | 34 ++ 9 files changed, 1193 insertions(+), 696 deletions(-) create mode 100644 mysql/layout.go create mode 100644 mysql/tx.go diff --git a/mysql/_dumps/structs.sql b/mysql/_dumps/structs.sql index 1058dfcd..a6046cbe 100644 --- a/mysql/_dumps/structs.sql +++ b/mysql/_dumps/structs.sql @@ -8,6 +8,26 @@ CREATE TABLE artist ( name VARCHAR(60) ); +DROP TABLE IF EXISTS publication; + +CREATE TABLE publication ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + PRIMARY KEY(id), + title VARCHAR(80), + author_id BIGINT(20) +); + +DROP TABLE IF EXISTS review; + +CREATE TABLE review ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + PRIMARY KEY(id), + publication_id BIGINT(20), + name VARCHAR(80), + comments TEXT, + created DATETIME +); + DROP TABLE IF EXISTS data_types; CREATE TABLE data_types ( diff --git a/mysql/_example/main.go b/mysql/_example/main.go index 0d5df5f3..f7d963f6 100644 --- a/mysql/_example/main.go +++ b/mysql/_example/main.go @@ -9,17 +9,17 @@ import ( ) var settings = db.Settings{ - Database: `upperio_tests`, // Database name - Socket: `/var/run/mysqld/mysqld.sock`, // Using unix sockets. - User: `upperio`, // Database username. - Password: `upperio`, // Database password. + Database: `upperio_tests`, // Database name + Host: `testserver.local`, + User: `upperio`, // Database username. + Password: `upperio`, // Database password. } type Birthday struct { // Maps the "Name" property to the "name" column of the "birthdays" table. - Name string `field:"name"` + Name string `db:"name"` // Maps the "Born" property to the "born" column of the "birthdays" table. - Born time.Time `field:"born"` + Born time.Time `db:"born"` } func main() { diff --git a/mysql/_example/test.sh b/mysql/_example/test.sh index d61a8716..518f1cf6 100755 --- a/mysql/_example/test.sh +++ b/mysql/_example/test.sh @@ -1,2 +1,2 @@ -cat example.sql | mysql -uupperio -pupperio upperio_tests +cat example.sql | mysql -uupperio -pupperio upperio_tests -htestserver.local go run main.go diff --git a/mysql/collection.go b/mysql/collection.go index cf4baea2..a358e0ae 100644 --- a/mysql/collection.go +++ b/mysql/collection.go @@ -1,204 +1,249 @@ -/* - Copyright (c) 2012-2014 JosĂŠ Carlos Nieto, https://menteslibres.net/xiam - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ +// Copyright (c) 2012-2014 JosĂŠ Carlos Nieto, https://menteslibres.net/xiam +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package mysql import ( "fmt" - _ "github.com/go-sql-driver/mysql" "menteslibres.net/gosexy/to" "reflect" "strings" "time" "upper.io/db" + "upper.io/db/util/sqlgen" "upper.io/db/util/sqlutil" ) -// Mysql table/collection. +const defaultOperator = `=` + type Table struct { - source *Source sqlutil.T + source *Source + names []string } -// Creates a filter with the given terms. -func (self *Table) Find(terms ...interface{}) db.Result { +func whereValues(term interface{}) (where sqlgen.Where, args []interface{}) { - queryChunks := sqlutil.NewQueryChunks() + args = []interface{}{} - // No specific fields given. - if len(queryChunks.Fields) == 0 { - queryChunks.Fields = []string{`*`} + switch t := term.(type) { + case []interface{}: + l := len(t) + where = make(sqlgen.Where, 0, l) + for _, cond := range t { + w, v := whereValues(cond) + args = append(args, v...) + where = append(where, w...) + } + case db.And: + and := make(sqlgen.And, 0, len(t)) + for _, cond := range t { + k, v := whereValues(cond) + args = append(args, v...) + and = append(and, k...) + } + where = append(where, and) + case db.Or: + or := make(sqlgen.Or, 0, len(t)) + for _, cond := range t { + k, v := whereValues(cond) + args = append(args, v...) + or = append(or, k...) + } + where = append(where, or) + case db.Raw: + if s, ok := t.Value.(string); ok == true { + where = append(where, sqlgen.Raw{s}) + } + case db.Cond: + k, v := conditionValues(t) + args = append(args, v...) + for _, kk := range k { + where = append(where, kk) + } } - // Compiling conditions - queryChunks.Conditions, queryChunks.Arguments = self.compileConditions(terms) + return where, args +} - if queryChunks.Conditions == "" { - queryChunks.Conditions = `1 = 1` - } +func interfaceArgs(value interface{}) (args []interface{}) { - // Creating a result handler. - result := &Result{ - self, - queryChunks, - nil, + if value == nil { + return nil } - return result -} + value_v := reflect.ValueOf(value) -// Transforms conditions into arguments for sql.Exec/sql.Query -func (self *Table) compileConditions(term interface{}) (string, []interface{}) { - sql := []string{} - args := []interface{}{} + switch value_v.Type().Kind() { + case reflect.Slice: + var i, total int - switch t := term.(type) { - case []interface{}: - for i := range t { - rsql, rargs := self.compileConditions(t[i]) - if rsql != "" { - sql = append(sql, rsql) - args = append(args, rargs...) - } - } - if len(sql) > 0 { - return `(` + strings.Join(sql, ` AND `) + `)`, args - } - case db.Or: - for i := range t { - rsql, rargs := self.compileConditions(t[i]) - if rsql != "" { - sql = append(sql, rsql) - args = append(args, rargs...) - } - } - if len(sql) > 0 { - return `(` + strings.Join(sql, ` OR `) + `)`, args - } - case db.And: - for i := range t { - rsql, rargs := self.compileConditions(t[i]) - if rsql != "" { - sql = append(sql, rsql) - args = append(args, rargs...) + total = value_v.Len() + if total > 0 { + args = make([]interface{}, total) + + for i = 0; i < total; i++ { + args[i] = toInternal(value_v.Index(i).Interface()) } + + return args + } else { + return nil } - if len(sql) > 0 { - return `(` + strings.Join(sql, ` AND `) + `)`, args - } - case db.Cond: - return self.compileStatement(t) + default: + args = []interface{}{toInternal(value)} } - return "", args + return args } -func (self *Table) compileStatement(cond db.Cond) (string, []interface{}) { +func conditionValues(cond db.Cond) (columnValues sqlgen.ColumnValues, args []interface{}) { - total := len(cond) + args = []interface{}{} - str := make([]string, 0, total) - arg := make([]interface{}, 0, total) + for column, value := range cond { + var columnValue sqlgen.ColumnValue - // Walking over conditions - for field, value := range cond { - // Removing leading or trailing spaces. - field = strings.TrimSpace(field) + // Guessing operator from input, or using a default one. + column := strings.TrimSpace(column) + chunks := strings.SplitN(column, ` `, 2) - chunks := strings.SplitN(field, ` `, 2) - - // Default operator. - op := `=` + columnValue.Column = sqlgen.Column{chunks[0]} if len(chunks) > 1 { - // User has defined a different operator. - op = chunks[1] + columnValue.Operator = chunks[1] + } else { + columnValue.Operator = defaultOperator } switch value := value.(type) { case db.Func: + // Catches functions. value_i := interfaceArgs(value.Args) + columnValue.Operator = value.Name + if value_i == nil { - str = append(str, fmt.Sprintf(`%s %s ()`, chunks[0], value.Name)) + // A function with no arguments. + columnValue.Value = sqlgen.Value{sqlgen.Raw{`()`}} } else { - str = append(str, fmt.Sprintf(`%s %s (?%s)`, chunks[0], value.Name, strings.Repeat(`,?`, len(value_i)-1))) - arg = append(arg, value_i...) + // A function with one or more arguments. + columnValue.Value = sqlgen.Value{sqlgen.Raw{fmt.Sprintf(`(?%s)`, strings.Repeat(`, ?`, len(value_i)-1))}} } + + args = append(args, value_i...) default: + // Catches everything else. value_i := interfaceArgs(value) - if value_i == nil { - str = append(str, fmt.Sprintf(`%s %s ()`, chunks[0], op)) + l := len(value_i) + if value_i == nil || l == 0 { + // Nil value given. + columnValue.Value = sqlgen.Value{sqlgen.Raw{`NULL`}} } else { - str = append(str, fmt.Sprintf(`%s %s (?%s)`, chunks[0], op, strings.Repeat(`,?`, len(value_i)-1))) - arg = append(arg, value_i...) + if l > 1 { + // Array value given. + columnValue.Value = sqlgen.Value{sqlgen.Raw{fmt.Sprintf(`(?%s)`, strings.Repeat(`, ?`, len(value_i)-1))}} + } else { + // Single value given. + columnValue.Value = sqlPlaceholder + } + args = append(args, value_i...) } } + + columnValues = append(columnValues, columnValue) } - switch len(str) { - case 1: - return str[0], arg - case 0: - return "", []interface{}{} + return columnValues, args +} + +func (self *Table) Find(terms ...interface{}) db.Result { + where, arguments := whereValues(terms) + + result := &Result{ + table: self, + where: where, + arguments: arguments, } - return `(` + strings.Join(str, ` AND `) + `)`, arg + return result +} + +func (self *Table) tableN(i int) string { + if len(self.names) > i { + chunks := strings.SplitN(self.names[i], " ", 2) + if len(chunks) > 0 { + return chunks[0] + } + } + return "" } // Deletes all the rows within the collection. func (self *Table) Truncate() error { - _, err := self.source.doExec( - fmt.Sprintf("TRUNCATE TABLE `%s`", self.Name()), - ) + _, err := self.source.doExec(sqlgen.Statement{ + Type: sqlgen.SqlTruncate, + Table: sqlgen.Table{self.tableN(0)}, + }) + + if err != nil { + return err + } - return err + return nil } // Appends an item (map or struct) into the collection. func (self *Table) Append(item interface{}) (interface{}, error) { - var id interface{} - fields, values, err := self.FieldValues(item, toInternal) + cols, vals, err := self.FieldValues(item, toInternal) + + var columns sqlgen.Columns + var values sqlgen.Values + + for _, col := range cols { + columns = append(columns, sqlgen.Column{col}) + } + + for i := 0; i < len(vals); i++ { + values = append(values, sqlPlaceholder) + } // Error ocurred, stop appending. if err != nil { return nil, err } - res, err := self.source.doExec( - fmt.Sprintf("INSERT INTO `%s`", self.Name()), - sqlFields(fields), - "VALUES", - sqlValues(values), - ) + res, err := self.source.doExec(sqlgen.Statement{ + Type: sqlgen.SqlInsert, + Table: sqlgen.Table{self.tableN(0)}, + Columns: columns, + Values: values, + }, vals...) - // Error ocurred, stop appending. if err != nil { return nil, err } - // Last inserted ID could be zero too. + var id int64 id, _ = res.LastInsertId() return id, nil @@ -206,33 +251,18 @@ func (self *Table) Append(item interface{}) (interface{}, error) { // Returns true if the collection exists. func (self *Table) Exists() bool { - rows, err := self.source.doQuery( - fmt.Sprintf(` - SELECT table_name - FROM information_schema.tables - WHERE table_schema = '%s' AND table_name = '%s' - `, - self.source.Name(), - self.Name(), - ), - ) - - if err != nil { + if err := self.source.tableExists(self.names...); err != nil { return false } - - defer rows.Close() - - return rows.Next() + return true } -func toInternalInterface(val interface{}) interface{} { - return toInternal(val) +func (self *Table) Name() string { + return strings.Join(self.names, `, `) } // Converts a Go value into internal database representation. func toInternal(val interface{}) interface{} { - switch t := val.(type) { case []byte: return string(t) @@ -242,47 +272,10 @@ func toInternal(val interface{}) interface{} { return fmt.Sprintf(TimeFormat, int(t/time.Hour), int(t/time.Minute%60), int(t/time.Second%60), t%time.Second/time.Millisecond) case bool: if t == true { - return "1" + return `1` } else { - return "0" + return `0` } } - return to.String(val) } - -// Converts a database representation (after auto-conversion) into a Go value. -func toNative(val interface{}) interface{} { - return val -} - -func interfaceArgs(value interface{}) (args []interface{}) { - - if value == nil { - return nil - } - - value_v := reflect.ValueOf(value) - - switch value_v.Type().Kind() { - case reflect.Slice: - var i, total int - - total = value_v.Len() - if total > 0 { - args = make([]interface{}, total) - - for i = 0; i < total; i++ { - args[i] = toInternal(value_v.Index(i).Interface()) - } - - return args - } else { - return nil - } - default: - args = []interface{}{toInternal(value)} - } - - return args -} diff --git a/mysql/database.go b/mysql/database.go index 77eeef41..378affda 100644 --- a/mysql/database.go +++ b/mysql/database.go @@ -1,25 +1,23 @@ -/* - Copyright (c) 2012-2014 JosĂŠ Carlos Nieto, https://menteslibres.net/xiam - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ +// Copyright (c) 2012-2014 JosĂŠ Carlos Nieto, https://menteslibres.net/xiam +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package mysql @@ -27,35 +25,39 @@ import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" - "log" "os" "reflect" "regexp" "strings" "upper.io/db" + "upper.io/db/util/sqlgen" + "upper.io/db/util/sqlutil" ) -// Format for saving dates. -const DateFormat = "2006-01-02 15:04:05.000" +const Driver = `mysql` -// Format for saving times. -const TimeFormat = "%d:%02d:%02d.%03d" - -var columnPattern = regexp.MustCompile(`^([a-z]+)\(?([0-9,]+)?\)?\s?([a-z]*)?`) - -const driverName = `mysql` +var ( + // Format for saving dates. + DateFormat = "2006-01-02 15:04:05.000" + // Format for saving times. + TimeFormat = "%d:%02d:%02d.%03d" +) -type sqlValues_t []interface{} +var ( + columnPattern = regexp.MustCompile(`^([a-z]+)\(?([0-9,]+)?\)?\s?([a-z]*)?`) + sqlPlaceholder = sqlgen.Value{sqlgen.Raw{`?`}} +) type Source struct { - session *sql.DB config db.Settings + session *sql.DB collections map[string]db.Collection + tx *sql.Tx } -type sqlQuery struct { - Query []string - Args []interface{} +type columnSchema_t struct { + ColumnName string `db:"column_name"` + DataType string `db:"data_type"` } func debugEnabled() bool { @@ -66,89 +68,93 @@ func debugEnabled() bool { } func init() { - db.Register(driverName, &Source{}) -} -func debugLogQuery(s string, q *sqlQuery) { - log.Printf("SQL: %s\nARGS: %v\n", strings.TrimSpace(s), q.Args) + sqlgen.SetTemplate(sqlgen.Template{ + mysqlColumnSeparator, + mysqlIdentifierSeparator, + mysqlIdentifierQuote, + mysqlValueSeparator, + mysqlValueQuote, + mysqlAndKeyword, + mysqlOrKeyword, + mysqlNotKeyword, + mysqlDescKeyword, + mysqlAscKeyword, + mysqlDefaultOperator, + mysqlClauseGroup, + mysqlClauseOperator, + mysqlColumnValue, + mysqlTableAliasLayout, + mysqlColumnAliasLayout, + mysqlSortByColumnLayout, + mysqlWhereLayout, + mysqlOrderByLayout, + mysqlInsertLayout, + mysqlSelectLayout, + mysqlUpdateLayout, + mysqlDeleteLayout, + mysqlTruncateLayout, + mysqlDropDatabaseLayout, + mysqlDropTableLayout, + mysqlSelectCountLayout, + }) + + db.Register(Driver, &Source{}) } -func sqlCompile(terms []interface{}) *sqlQuery { - q := &sqlQuery{} - - q.Query = []string{} +func (self *Source) doExec(stmt sqlgen.Statement, args ...interface{}) (sql.Result, error) { - for _, term := range terms { - switch t := term.(type) { - case sqlValues_t: - args := make([]string, len(t)) - for i, arg := range t { - args[i] = `?` - q.Args = append(q.Args, arg) - } - q.Query = append(q.Query, `(`+strings.Join(args, `, `)+`)`) - case string: - q.Query = append(q.Query, t) - default: - if reflect.TypeOf(t).Kind() == reflect.Slice { - var v = reflect.ValueOf(t) - for i := 0; i < v.Len(); i++ { - q.Args = append(q.Args, v.Index(i).Interface()) - } - } else { - q.Args = append(q.Args, t) - } - } + if self.session == nil { + return nil, db.ErrNotConnected } - return q -} + query := stmt.Compile() -func sqlFields(names []string) string { - for i, _ := range names { - names[i] = strings.Replace(names[i], "`", "``", -1) + if debugEnabled() == true { + sqlutil.DebugQuery(query, args) } - return "(`" + strings.Join(names, "`, `") + "`)" -} -func sqlValues(values []interface{}) sqlValues_t { - ret := make(sqlValues_t, len(values)) - for i, _ := range values { - ret[i] = values[i] + if self.tx != nil { + return self.tx.Exec(query, args...) } - return ret + + return self.session.Exec(query, args...) } -func (self *Source) doExec(terms ...interface{}) (sql.Result, error) { +func (self *Source) doQuery(stmt sqlgen.Statement, args ...interface{}) (*sql.Rows, error) { if self.session == nil { return nil, db.ErrNotConnected } - chunks := sqlCompile(terms) - - query := strings.Join(chunks.Query, ` `) + query := stmt.Compile() if debugEnabled() == true { - debugLogQuery(query, chunks) + sqlutil.DebugQuery(query, args) + } + + if self.tx != nil { + return self.tx.Query(query, args...) } - return self.session.Exec(query, chunks.Args...) + return self.session.Query(query, args...) } -func (self *Source) doQuery(terms ...interface{}) (*sql.Rows, error) { +func (self *Source) doQueryRow(stmt sqlgen.Statement, args ...interface{}) (*sql.Row, error) { if self.session == nil { return nil, db.ErrNotConnected } - chunks := sqlCompile(terms) - - query := strings.Join(chunks.Query, " ") + query := stmt.Compile() if debugEnabled() == true { - debugLogQuery(query, chunks) + sqlutil.DebugQuery(query, args) + } + + if self.tx != nil { + return self.tx.QueryRow(query, args...), nil } - return self.session.Query(query, chunks.Args...) + return self.session.QueryRow(query, args...), nil } // Returns the string name of the database. @@ -156,10 +162,50 @@ func (self *Source) Name() string { return self.config.Database } +// Ping verifies a connection to the database is still alive, +// establishing a connection if necessary. +func (self *Source) Ping() error { + return self.session.Ping() +} + +func (self *Source) clone() (*Source, error) { + src := &Source{} + src.Setup(self.config) + + if err := src.Open(); err != nil { + return nil, err + } + + return src, nil +} + +func (self *Source) Clone() (db.Database, error) { + return self.clone() +} + +func (self *Source) Transaction() (db.Tx, error) { + var err error + var clone *Source + var sqlTx *sql.Tx + + if sqlTx, err = self.session.Begin(); err != nil { + return nil, err + } + + if clone, err = self.clone(); err != nil { + return nil, err + } + + tx := &Tx{clone} + + clone.tx = sqlTx + + return tx, nil +} + // Stores database settings. func (self *Source) Setup(config db.Settings) error { self.config = config - self.session = nil self.collections = make(map[string]db.Collection) return self.Open() } @@ -223,25 +269,17 @@ func (self *Source) Close() error { // Changes the active database. func (self *Source) Use(database string) error { self.config.Database = database - _, err := self.session.Exec(fmt.Sprintf("USE `%s`", database)) - return err -} - -// Starts a transaction block. -func (self *Source) Begin() error { - _, err := self.session.Exec(`START TRANSACTION`) - return err -} - -// Ends a transaction block. -func (self *Source) End() error { - _, err := self.session.Exec(`COMMIT`) - return err + return self.Open() } // Drops the currently active database. func (self *Source) Drop() error { - _, err := self.session.Exec(fmt.Sprintf("DROP DATABASE `%s`", self.config.Database)) + + _, err := self.doQuery(sqlgen.Statement{ + Type: sqlgen.SqlDropDatabase, + Database: sqlgen.Database{self.config.Database}, + }) + return err } @@ -250,7 +288,16 @@ func (self *Source) Collections() ([]string, error) { var collections []string var collection string - rows, err := self.session.Query(`SHOW TABLES`) + rows, err := self.doQuery(sqlgen.Statement{ + Type: sqlgen.SqlSelect, + Table: sqlgen.Table{`information_schema.tables`}, + Columns: sqlgen.Columns{ + {`table_name`}, + }, + Where: sqlgen.Where{ + sqlgen.ColumnValue{sqlgen.Column{`table_schema`}, `=`, sqlPlaceholder}, + }, + }, self.config.Database) if err != nil { return nil, err @@ -266,85 +313,121 @@ func (self *Source) Collections() ([]string, error) { return collections, nil } -// Returns a collection instance by name. -func (self *Source) Collection(name string) (db.Collection, error) { +func (self *Source) tableExists(names ...string) error { + for _, name := range names { + + rows, err := self.doQuery(sqlgen.Statement{ + Type: sqlgen.SqlSelect, + Table: sqlgen.Table{`information_schema.tables`}, + Columns: sqlgen.Columns{ + {`table_name`}, + }, + Where: sqlgen.Where{ + sqlgen.ColumnValue{sqlgen.Column{`table_schema`}, `=`, sqlPlaceholder}, + sqlgen.ColumnValue{sqlgen.Column{`table_name`}, `=`, sqlPlaceholder}, + }, + }, self.config.Database, name) + + if err != nil { + return db.ErrCollectionDoesNotExists + } + + defer rows.Close() - if col, ok := self.collections[name]; ok == true { - return col, nil + if rows.Next() == false { + return db.ErrCollectionDoesNotExists + } } - table := &Table{} + return nil +} - table.source = self - table.DB = self +// Returns a collection instance by name. +func (self *Source) Collection(names ...string) (db.Collection, error) { - table.SetName = name + if len(names) == 0 { + return nil, db.ErrMissingCollectionName + } - // Table exists? - if table.Exists() == false { - return table, db.ErrCollectionDoesNotExists + col := &Table{ + source: self, + names: names, } - // Fetching table datatypes and mapping to internal gotypes. - rows, err := table.source.doQuery( - fmt.Sprintf( - "SHOW COLUMNS FROM `%s`", - table.Name(), - ), - ) + col.PrimaryKey = `id` - if err != nil { - return table, err - } + columns_t := []columnSchema_t{} - columns := []struct { - Field string - Type string - }{} + for _, name := range names { + chunks := strings.SplitN(name, " ", 2) - err = table.FetchRows(&columns, rows) + if len(chunks) > 0 { - if err != nil { - return nil, err - } + name = chunks[0] + + if err := self.tableExists(name); err != nil { + return nil, err + } - table.ColumnTypes = make(map[string]reflect.Kind, len(columns)) + rows, err := self.doQuery(sqlgen.Statement{ + Type: sqlgen.SqlSelect, + Table: sqlgen.Table{`information_schema.columns`}, + Columns: sqlgen.Columns{ + {`column_name`}, + {`data_type`}, + }, + Where: sqlgen.Where{ + sqlgen.ColumnValue{sqlgen.Column{`table_schema`}, `=`, sqlPlaceholder}, + sqlgen.ColumnValue{sqlgen.Column{`table_name`}, `=`, sqlPlaceholder}, + }, + }, self.config.Database, name) + + if err != nil { + return nil, err + } - for _, column := range columns { + if err = col.FetchRows(&columns_t, rows); err != nil { + return nil, err + } - column.Field = strings.ToLower(column.Field) - column.Type = strings.ToLower(column.Type) + col.ColumnTypes = make(map[string]reflect.Kind, len(columns_t)) - results := columnPattern.FindStringSubmatch(column.Type) + for _, column := range columns_t { - // Default properties. - dextra := "" - dtype := `varchar` + column.ColumnName = strings.ToLower(column.ColumnName) + column.DataType = strings.ToLower(column.DataType) - dtype = results[1] + results := columnPattern.FindStringSubmatch(column.DataType) - if len(results) > 3 { - dextra = results[3] - } + // Default properties. + dextra := "" + dtype := `varchar` + + dtype = results[1] + + if len(results) > 3 { + dextra = results[3] + } - ctype := reflect.String + ctype := reflect.String + + // Guessing datatypes. + switch dtype { + case `tinyint`, `smallint`, `mediumint`, `int`, `bigint`: + if dextra == `unsigned` { + ctype = reflect.Uint64 + } else { + ctype = reflect.Int64 + } + case `decimal`, `float`, `double`: + ctype = reflect.Float64 + } - // Guessing datatypes. - switch dtype { - case `tinyint`, `smallint`, `mediumint`, `int`, `bigint`: - if dextra == `unsigned` { - ctype = reflect.Uint64 - } else { - ctype = reflect.Int64 + col.ColumnTypes[column.ColumnName] = ctype } - case `decimal`, `float`, `double`: - ctype = reflect.Float64 - } - table.ColumnTypes[column.Field] = ctype + } } - self.collections[name] = table - - return table, nil + return col, nil } diff --git a/mysql/database_test.go b/mysql/database_test.go index f50e1ca7..9f41024a 100644 --- a/mysql/database_test.go +++ b/mysql/database_test.go @@ -1,40 +1,36 @@ -/* - Copyright (c) 2012-2014 JosĂŠ Carlos Nieto, https://menteslibres.net/xiam - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -/* - Tests for the mysql wrapper. - - Execute the Makefile in ./_dumps/ to create the expected database structure. - - cd _dumps - make - cd .. - go test -*/ +// Copyright (c) 2012-2014 JosĂŠ Carlos Nieto, https://menteslibres.net/xiam +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + package mysql +// In order to execute these tests you must initialize the database first: +// +// cd _dumps +// make +// cd .. +// go test + import ( "database/sql" + "flag" "menteslibres.net/gosexy/to" "os" "reflect" @@ -44,25 +40,20 @@ import ( "upper.io/db" ) -// Wrapper. -const wrapperName = "mysql" - -// Wrapper settings. const ( - host = "testserver.local" - dbname = "upperio_tests" + database = "upperio_tests" username = "upperio" password = "upperio" ) -// Global settings for tests. var settings = db.Settings{ - Database: dbname, - Host: host, + Database: database, User: username, Password: password, } +var host = flag.String("host", "testserver.local", "Testing server address.") + // Structure for testing conversions and datatypes. type testValuesStruct struct { Uint uint `field:"_uint"` @@ -98,30 +89,37 @@ var testValues = testValuesStruct{ time.Second * time.Duration(7331), } -// Enabling outputting some information to stdout (like the SQL query and its +func init() { + flag.Parse() + settings.Host = *host +} + +// Loggin some information to stdout (like the SQL query and its // arguments), useful for development. func TestEnableDebug(t *testing.T) { os.Setenv(db.EnvEnableDebug, "TRUE") } -// Trying to open an empty datasource, it must fail. +// Attempts to open an empty datasource. func TestOpenFailed(t *testing.T) { - _, err := db.Open(wrapperName, db.Settings{}) + var err error - if err == nil { - t.Errorf("Expecting an error.") + // Attempt to open an empty database. + if _, err = db.Open(Driver, db.Settings{}); err == nil { + // Must fail. + t.Fatalf("Expecting an error.") } } -// Truncates all collections. +// Attempts to get all collections and truncate each one of them. func TestTruncate(t *testing.T) { - var err error + var sess db.Database + var collections []string + var col db.Collection // Opening database. - sess, err := db.Open(wrapperName, settings) - - if err != nil { + if sess, err = db.Open(Driver, settings); err != nil { t.Fatalf(err.Error()) } @@ -129,158 +127,162 @@ func TestTruncate(t *testing.T) { defer sess.Close() // Getting a list of all collections in this database. - collections, err := sess.Collections() - - if err != nil { + if collections, err = sess.Collections(); err != nil { t.Fatalf(err.Error()) } + if len(collections) == 0 { + t.Fatalf("Expecting some collections.") + } + + // Walking over collections. for _, name := range collections { - // Pointing the collection. - col, err := sess.Collection(name) - if err != nil { + // Getting a collection. + if col, err = sess.Collection(name); err != nil { t.Fatalf(err.Error()) } - // Since this is a SQL collection (table), the structure must exists before - // we can use it. - exists := col.Exists() - - if exists == true { - // Truncating the structure, if exists. - err = col.Truncate() - - if err != nil { + // Table must exists before we can use it. + if col.Exists() == true { + // Truncating the table. + if err = col.Truncate(); err != nil { t.Fatalf(err.Error()) } } - } } -// This test appends some data into the "artist" table. +// Attempts to append some data into the "artist" table. func TestAppend(t *testing.T) { - var err error var id interface{} + var sess db.Database + var artist db.Collection + var total uint64 - // Opening database. - sess, err := db.Open(wrapperName, settings) - - if err != nil { + if sess, err = db.Open(Driver, settings); err != nil { t.Fatalf(err.Error()) } - // We should close the database when it's no longer in use. defer sess.Close() - // Getting a pointer to the "artist" collection. - artist, err := sess.Collection("artist") - - if err != nil { + if artist, err = sess.Collection("artist"); err != nil { t.Fatalf(err.Error()) } - // Appending a map. - id, err = artist.Append(map[string]string{ + // Attempt to append a map. + item_m := map[string]string{ "name": "Ozzie", - }) + } + + if id, err = artist.Append(item_m); err != nil { + t.Fatalf(err.Error()) + } if to.Int64(id) == 0 { t.Fatalf("Expecting an ID.") } - // Appending a struct. - id, err = artist.Append(struct { - Name string `field:name` + // Attempt to append a struct. + item_s := struct { + Name string `db:"name"` }{ "Flea", - }) + } + + if id, err = artist.Append(item_s); err != nil { + t.Fatalf(err.Error()) + } if to.Int64(id) == 0 { t.Fatalf("Expecting an ID.") } - // Appending a struct (using tags to specify the field name). - id, err = artist.Append(struct { - ArtistName string `field:"name"` + // Append to append a tagged struct. + item_t := struct { + ArtistName string `db:"name"` }{ "Slash", - }) + } + + if id, err = artist.Append(item_t); err != nil { + t.Fatalf(err.Error()) + } if to.Int64(id) == 0 { t.Fatalf("Expecting an ID.") } + // Counting elements, must be exactly 3 elements. + if total, err = artist.Find().Count(); err != nil { + t.Fatalf(err.Error()) + } + + if total != 3 { + t.Fatalf("Expecting exactly 3 rows.") + } + } -// This test tries to use an empty filter and count how many elements were -// added into the artist collection. +// Attempts to count all rows in our newly defined set. func TestResultCount(t *testing.T) { - var err error var res db.Result + var sess db.Database + var artist db.Collection + var total uint64 - // Opening database. - sess, err := db.Open(wrapperName, settings) - - if err != nil { + if sess, err = db.Open(Driver, settings); err != nil { t.Fatalf(err.Error()) } defer sess.Close() // We should close the database when it's no longer in use. - artist, _ := sess.Collection("artist") + if artist, err = sess.Collection("artist"); err != nil { + t.Fatalf(err.Error()) + } + // Defining a set with no conditions. res = artist.Find() // Counting all the matching rows. - total, err := res.Count() - - if err != nil { + if total, err = res.Count(); err != nil { t.Fatalf(err.Error()) } if total == 0 { - t.Fatalf("Should not be empty, we've just added some rows!") + t.Fatalf("Counter should not be zero, we've just added some rows!") } - } -// This test uses and result and tries to fetch items one by one. +// Attempts to fetch results one by one. func TestResultFetch(t *testing.T) { - var err error var res db.Result + var sess db.Database + var artist db.Collection - // Opening database. - sess, err := db.Open(wrapperName, settings) - - if err != nil { + if sess, err = db.Open(Driver, settings); err != nil { t.Fatalf(err.Error()) } - // We should close the database when it's no longer in use. defer sess.Close() - artist, err := sess.Collection("artist") - - if err != nil { + if artist, err = sess.Collection("artist"); err != nil { t.Fatalf(err.Error()) } - // Testing map - res = artist.Find() - + // Dumping into a map. row_m := map[string]interface{}{} + res = artist.Find() + for { err = res.Next(&row_m) if err == db.ErrNoMoreRows { - // No more row_ms left. break } @@ -298,7 +300,7 @@ func TestResultFetch(t *testing.T) { res.Close() - // Testing struct + // Dumping into an struct with no tags. row_s := struct { Id uint64 Name string @@ -310,7 +312,6 @@ func TestResultFetch(t *testing.T) { err = res.Next(&row_s) if err == db.ErrNoMoreRows { - // No more row_s' left. break } @@ -328,7 +329,7 @@ func TestResultFetch(t *testing.T) { res.Close() - // Testing tagged struct + // Dumping into a tagged struct. row_t := struct { Value1 uint64 `field:"id"` Value2 string `field:"name"` @@ -340,7 +341,6 @@ func TestResultFetch(t *testing.T) { err = res.Next(&row_t) if err == db.ErrNoMoreRows { - // No more row_t's left. break } @@ -358,54 +358,62 @@ func TestResultFetch(t *testing.T) { res.Close() - // Testing Result.All() with a slice of maps. - res = artist.Find() - + // Dumping into an slice of maps. all_rows_m := []map[string]interface{}{} - err = res.All(&all_rows_m) - if err != nil { + res = artist.Find() + if err = res.All(&all_rows_m); err != nil { t.Fatalf(err.Error()) } + if len(all_rows_m) != 3 { + t.Fatalf("Expecting 3 items.") + } + for _, single_row_m := range all_rows_m { if to.Int64(single_row_m["id"]) == 0 { t.Fatalf("Expecting a not null ID.") } } - // Testing Result.All() with a slice of structs. - res = artist.Find() + // Dumping into an slice of structs. all_rows_s := []struct { Id uint64 Name string }{} - err = res.All(&all_rows_s) - if err != nil { + res = artist.Find() + if err = res.All(&all_rows_s); err != nil { t.Fatalf(err.Error()) } + if len(all_rows_s) != 3 { + t.Fatalf("Expecting 3 items.") + } + for _, single_row_s := range all_rows_s { if single_row_s.Id == 0 { t.Fatalf("Expecting a not null ID.") } } - // Testing Result.All() with a slice of tagged structs. - res = artist.Find() - + // Dumping into an slice of tagged structs. all_rows_t := []struct { Value1 uint64 `field:"id"` Value2 string `field:"name"` }{} - err = res.All(&all_rows_t) - if err != nil { + res = artist.Find() + + if err = res.All(&all_rows_t); err != nil { t.Fatalf(err.Error()) } + if len(all_rows_t) != 3 { + t.Fatalf("Expecting 3 items.") + } + for _, single_row_t := range all_rows_t { if single_row_t.Value1 == 0 { t.Fatalf("Expecting a not null ID.") @@ -413,28 +421,23 @@ func TestResultFetch(t *testing.T) { } } -// This test tries to update some previously added rows. +// Attempts to modify previously added rows. func TestUpdate(t *testing.T) { var err error + var sess db.Database + var artist db.Collection - // Opening database. - sess, err := db.Open(wrapperName, settings) - - if err != nil { + if sess, err = db.Open(Driver, settings); err != nil { t.Fatalf(err.Error()) } - // We should close the database when it's no longer in use. defer sess.Close() - // Getting a pointer to the "artist" collection. - artist, err := sess.Collection("artist") - - if err != nil { + if artist, err = sess.Collection("artist"); err != nil { t.Fatalf(err.Error()) } - // Value + // Defining destination struct value := struct { Id uint64 Name string @@ -443,96 +446,83 @@ func TestUpdate(t *testing.T) { // Getting the first artist. res := artist.Find(db.Cond{"id !=": 0}).Limit(1) - err = res.One(&value) - - if err != nil { + if err = res.One(&value); err != nil { t.Fatalf(err.Error()) } - // Updating with a map + // Updating set with a map row_m := map[string]interface{}{ "name": strings.ToUpper(value.Name), } - err = res.Update(row_m) - - if err != nil { + if err = res.Update(row_m); err != nil { t.Fatalf(err.Error()) } - err = res.One(&value) - - if err != nil { + // Pulling it again. + if err = res.One(&value); err != nil { t.Fatalf(err.Error()) } + // Verifying. if value.Name != row_m["name"] { t.Fatalf("Expecting a modification.") } - // Updating with a struct + // Updating set with a struct row_s := struct { Name string }{strings.ToLower(value.Name)} - err = res.Update(row_s) - - if err != nil { + if err = res.Update(row_s); err != nil { t.Fatalf(err.Error()) } - err = res.One(&value) - - if err != nil { + // Pulling it again. + if err = res.One(&value); err != nil { t.Fatalf(err.Error()) } + // Verifying if value.Name != row_s.Name { t.Fatalf("Expecting a modification.") } - // Updating with a tagged struct + // Updating set with a tagged struct row_t := struct { - Value1 string `field:"name"` + Value1 string `db:"name"` }{strings.Replace(value.Name, "z", "Z", -1)} - err = res.Update(row_t) - - if err != nil { + if err = res.Update(row_t); err != nil { t.Fatalf(err.Error()) } - err = res.One(&value) - - if err != nil { + // Pulling it again. + if err = res.One(&value); err != nil { t.Fatalf(err.Error()) } + // Verifying if value.Name != row_t.Value1 { t.Fatalf("Expecting a modification.") } - } -// Test database functions +// Attempts to use functions within database queries. func TestFunction(t *testing.T) { var err error var res db.Result + var sess db.Database + var artist db.Collection + var total uint64 - // Opening database. - sess, err := db.Open(wrapperName, settings) - - if err != nil { + if sess, err = db.Open(Driver, settings); err != nil { t.Fatalf(err.Error()) } - // We should close the database when it's no longer in use. defer sess.Close() - // Getting a pointer to the "artist" collection. - artist, err := sess.Collection("artist") - - if err != nil { + if artist, err = sess.Collection("artist"); err != nil { t.Fatalf(err.Error()) } @@ -544,98 +534,289 @@ func TestFunction(t *testing.T) { res = artist.Find(db.Cond{"id NOT IN": []int{0, -1}}) if err = res.One(&row_s); err != nil { - t.Fatalf("One: %q", err) + t.Fatalf(err.Error()) + } + + if total, err = res.Count(); err != nil { + t.Fatalf(err.Error()) + } + + if total != 3 { + t.Fatalf("Expecting 3 items.") } res = artist.Find(db.Cond{"id": db.Func{"NOT IN", []int{0, -1}}}) if err = res.One(&row_s); err != nil { - t.Fatalf("One: %q", err) + t.Fatalf(err.Error()) + } + + if total, err = res.Count(); err != nil { + t.Fatalf(err.Error()) + } + + if total != 3 { + t.Fatalf("Expecting 3 items.") } res.Close() } -// This test tries to remove some previously added rows. +// Attempts to delete previously added rows. func TestRemove(t *testing.T) { + var err error + var res db.Result + var sess db.Database + var artist db.Collection + + if sess, err = db.Open(Driver, settings); err != nil { + t.Fatalf(err.Error()) + } + + defer sess.Close() + + if artist, err = sess.Collection("artist"); err != nil { + t.Fatalf(err.Error()) + } + // Getting the artist with id = 1 + res = artist.Find(db.Cond{"id": 1}) + + // Trying to remove the row. + if err = res.Remove(); err != nil { + t.Fatalf(err.Error()) + } +} + +// Attempts to use SQL raw statements. +func TestRawRelations(t *testing.T) { + var sess db.Database var err error - // Opening database. - sess, err := db.Open(wrapperName, settings) + var artist db.Collection + var publication db.Collection + var review db.Collection - if err != nil { + type artist_t struct { + Id int64 `db:"id,omitempty"` + Name string `db:"name"` + } + + type publication_t struct { + Id int64 `db:"id,omitempty"` + Title string `db:"title"` + AuthorId int64 `db:"author_id"` + } + + type review_t struct { + Id int64 `db:"id,omitempty"` + PublicationId int64 `db:"publication_id"` + Name string `db:"name"` + Comments string `db:"comments"` + Created time.Time `db:"created"` + } + + if sess, err = db.Open(Driver, settings); err != nil { t.Fatalf(err.Error()) } - // We should close the database when it's no longer in use. defer sess.Close() - // Getting a pointer to the "artist" collection. - artist, err := sess.Collection("artist") + // Artist collection. + if artist, err = sess.Collection("artist"); err != nil { + t.Fatalf(err.Error()) + } - if err != nil { + if err = artist.Truncate(); err != nil { t.Fatalf(err.Error()) } - // Getting the artist with id = 1 - res := artist.Find(db.Cond{"id": 1}) + // Publication collection. + if publication, err = sess.Collection("publication"); err != nil { + t.Fatalf(err.Error()) + } - // Trying to remove the row. - err = res.Remove() + if err = publication.Truncate(); err != nil { + t.Fatalf(err.Error()) + } + + // Review collection. + if review, err = sess.Collection("review"); err != nil { + t.Fatalf(err.Error()) + } + + if err = review.Truncate(); err != nil { + t.Fatalf(err.Error()) + } + + // Adding some artists. + var miyazakiId interface{} + miyazaki := artist_t{Name: `Hayao Miyazaki`} + if miyazakiId, err = artist.Append(miyazaki); err != nil { + t.Fatalf(err.Error()) + } + miyazaki.Id = miyazakiId.(int64) + + var asimovId interface{} + asimov := artist_t{Name: `Isaac Asimov`} + if asimovId, err = artist.Append(asimov); err != nil { + t.Fatalf(err.Error()) + } + + var marquezId interface{} + marquez := artist_t{Name: `Gabriel GarcĂa MĂĄrquez`} + if marquezId, err = artist.Append(marquez); err != nil { + t.Fatalf(err.Error()) + } + + // Adding some publications. + publication.Append(publication_t{ + Title: `Tonari no Totoro`, + AuthorId: miyazakiId.(int64), + }) + + publication.Append(publication_t{ + Title: `Howl's Moving Castle`, + AuthorId: miyazakiId.(int64), + }) + + publication.Append(publication_t{ + Title: `Ponyo`, + AuthorId: miyazakiId.(int64), + }) + + publication.Append(publication_t{ + Title: `Memoria de mis Putas Tristes`, + AuthorId: marquezId.(int64), + }) + + publication.Append(publication_t{ + Title: `El Coronel no tiene quien le escriba`, + AuthorId: marquezId.(int64), + }) + publication.Append(publication_t{ + Title: `El Amor en los tiempos del CĂłlera`, + AuthorId: marquezId.(int64), + }) + + publication.Append(publication_t{ + Title: `I, Robot`, + AuthorId: asimovId.(int64), + }) + + var foundationId interface{} + foundationId, err = publication.Append(publication_t{ + Title: `Foundation`, + AuthorId: asimovId.(int64), + }) if err != nil { t.Fatalf(err.Error()) } + + publication.Append(publication_t{ + Title: `The Robots of Dawn`, + AuthorId: asimovId.(int64), + }) + + // Adding reviews for foundation. + review.Append(review_t{ + PublicationId: foundationId.(int64), + Name: "John Doe", + Comments: "I love The Foundation series.", + Created: time.Now(), + }) + + review.Append(review_t{ + PublicationId: foundationId.(int64), + Name: "Edr Pls", + Comments: "The Foundation series made me fall in love with Isaac Asimov.", + Created: time.Now(), + }) + + // Exec'ing a raw query. + var artistPublication db.Collection + if artistPublication, err = sess.Collection(`artist AS a, publication AS p`); err != nil { + t.Fatalf(err.Error()) + } + + res := artistPublication.Find( + db.Raw{`a.id = p.author_id`}, + ).Select( + "p.id", + "p.title as publication_title", + "a.name AS artist_name", + ) + + type artistPublication_t struct { + Id int64 `db:"id"` + PublicationTitle string `db:"publication_title"` + ArtistName string `db:"artist_name"` + } + + all := []artistPublication_t{} + + if err = res.All(&all); err != nil { + t.Fatalf(err.Error()) + } + + if len(all) != 9 { + t.Fatalf("Expecting some rows.") + } + } -// This test tries to add many different datatypes to a single row in a -// collection, then it tries to get the stored datatypes and check if the -// stored and the original values match. +// Attempts to add many different datatypes to a single row in a collection, +// then it tries to get the stored datatypes and check if the stored and the +// original values match. func TestDataTypes(t *testing.T) { var res db.Result + var sess db.Database + var dataTypes db.Collection + var err error + var id interface{} + var exists uint64 - // Opening database. - sess, err := db.Open(wrapperName, settings) - - if err != nil { + if sess, err = db.Open(Driver, settings); err != nil { t.Fatalf(err.Error()) } - // We should close the database when it's no longer in use. defer sess.Close() // Getting a pointer to the "data_types" collection. - dataTypes, err := sess.Collection("data_types") - dataTypes.Truncate() + if dataTypes, err = sess.Collection("data_types"); err != nil { + t.Fatalf(err.Error()) + } - // Appending our test subject. - id, err := dataTypes.Append(testValues) + // Removing all data. + if err = dataTypes.Truncate(); err != nil { + t.Fatalf(err.Error()) + } - if err != nil { + // Appending our test subject. + if id, err = dataTypes.Append(testValues); err != nil { t.Fatalf(err.Error()) } - // Trying to get the same subject we added. + // Defining our set. res = dataTypes.Find(db.Cond{"id": id}) - exists, err := res.Count() - - if err != nil { + if exists, err = res.Count(); err != nil { t.Fatalf(err.Error()) } if exists == 0 { - t.Errorf("Expecting an item.") + t.Fatalf("Expecting an item.") } // Trying to dump the subject into an empty structure of the same type. var item testValuesStruct + res.One(&item) // The original value and the test subject must match. if reflect.DeepEqual(item, testValues) == false { - t.Errorf("Struct is different.") + t.Fatalf("Struct is different.") } } @@ -645,24 +826,25 @@ func TestDisableDebug(t *testing.T) { } // Benchmarking raw database/sql. -func BenchmarkAppendRaw(b *testing.B) { - sess, err := db.Open(wrapperName, settings) +func BenchmarkAppendRawSQL(b *testing.B) { + var err error + var sess db.Database - if err != nil { + if sess, err = db.Open(Driver, settings); err != nil { b.Fatalf(err.Error()) } defer sess.Close() - artist, err := sess.Collection("artist") - artist.Truncate() - driver := sess.Driver().(*sql.DB) + if _, err = driver.Exec("TRUNCATE TABLE `artist`"); err != nil { + b.Fatalf(err.Error()) + } + b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := driver.Exec(`INSERT INTO artist (name) VALUES('Hayao Miyazaki')`) - if err != nil { + if _, err = driver.Exec("INSERT INTO `artist` (`name`) VALUES('Hayao Miyazaki')"); err != nil { b.Fatalf(err.Error()) } } @@ -672,8 +854,8 @@ func BenchmarkAppendRaw(b *testing.B) { // // Contributed by wei2912 // See: https://github.com/gosexy/db/issues/20#issuecomment-20097801 -func BenchmarkAppendDbItem(b *testing.B) { - sess, err := db.Open(wrapperName, settings) +func BenchmarkAppendUpper(b *testing.B) { + sess, err := db.Open(Driver, settings) if err != nil { b.Fatalf(err.Error()) @@ -684,68 +866,128 @@ func BenchmarkAppendDbItem(b *testing.B) { artist, err := sess.Collection("artist") artist.Truncate() + item := struct { + Name string `db:"name"` + }{"Hayao Miyazaki"} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err = artist.Append(item); err != nil { + b.Fatalf(err.Error()) + } + } +} + +// Benchmarking raw database/sql. +func BenchmarkAppendTxRawSQL(b *testing.B) { + var err error + var sess db.Database + var tx *sql.Tx + + if sess, err = db.Open(Driver, settings); err != nil { + b.Fatalf(err.Error()) + } + + defer sess.Close() + + driver := sess.Driver().(*sql.DB) + + if tx, err = driver.Begin(); err != nil { + b.Fatalf(err.Error()) + } + + if _, err = tx.Exec("TRUNCATE TABLE `artist`"); err != nil { + b.Fatalf(err.Error()) + } + b.ResetTimer() for i := 0; i < b.N; i++ { - _, err = artist.Append(map[string]string{"name": "Leonardo DaVinci"}) - if err != nil { + if _, err = tx.Exec("INSERT INTO `artist` (`name`) VALUES('Hayao Miyazaki')"); err != nil { b.Fatalf(err.Error()) } } + + if err = tx.Commit(); err != nil { + b.Fatalf(err.Error()) + } } // Benchmarking Append() with transactions. -// -// Contributed by wei2912 -// See: https://github.com/gosexy/db/issues/20#issuecomment-20167939 -// Applying the BEGIN and END transaction optimizations. -func BenchmarkAppendDbItem_Transaction(b *testing.B) { - sess, err := db.Open(wrapperName, settings) +func BenchmarkAppendTxUpper(b *testing.B) { + var sess db.Database + var err error - if err != nil { + if sess, err = db.Open(Driver, settings); err != nil { b.Fatalf(err.Error()) } defer sess.Close() - artist, err := sess.Collection("artist") - artist.Truncate() + var tx db.Tx + if tx, err = sess.Transaction(); err != nil { + b.Fatalf(err.Error()) + } - err = sess.Begin() - if err != nil { + var artist db.Collection + if artist, err = tx.Collection("artist"); err != nil { b.Fatalf(err.Error()) } + if err = artist.Truncate(); err != nil { + b.Fatalf(err.Error()) + } + + item := struct { + Name string `db:"name"` + }{"Hayao Miyazaki"} + + b.ResetTimer() for i := 0; i < b.N; i++ { - _, err = artist.Append(map[string]string{"name": "Isaac Asimov"}) - if err != nil { + if _, err = artist.Append(item); err != nil { b.Fatalf(err.Error()) } } - err = sess.End() - if err != nil { + if err = tx.Commit(); err != nil { b.Fatalf(err.Error()) } } -// Benchmarking Append with a struct. -func BenchmarkAppendStruct(b *testing.B) { - sess, err := db.Open(wrapperName, settings) +// Benchmarking Append() with map. +func BenchmarkAppendTxUpperMap(b *testing.B) { + var sess db.Database + var err error - if err != nil { + if sess, err = db.Open(Driver, settings); err != nil { b.Fatalf(err.Error()) } defer sess.Close() - artist, err := sess.Collection("artist") - artist.Truncate() + var tx db.Tx + if tx, err = sess.Transaction(); err != nil { + b.Fatalf(err.Error()) + } + + var artist db.Collection + if artist, err = tx.Collection("artist"); err != nil { + b.Fatalf(err.Error()) + } + + if err = artist.Truncate(); err != nil { + b.Fatalf(err.Error()) + } + + item := map[string]string{"name": "Hayao Miyazaki"} b.ResetTimer() for i := 0; i < b.N; i++ { - _, err = artist.Append(struct{ Name string }{"John Lennon"}) - if err != nil { + if _, err = artist.Append(item); err != nil { b.Fatalf(err.Error()) } } + + if err = tx.Commit(); err != nil { + b.Fatalf(err.Error()) + } } diff --git a/mysql/layout.go b/mysql/layout.go new file mode 100644 index 00000000..98568de3 --- /dev/null +++ b/mysql/layout.go @@ -0,0 +1,124 @@ +// Copyright (c) 2012-2014 JosĂŠ Carlos Nieto, https://menteslibres.net/xiam +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package mysql + +const ( + mysqlColumnSeparator = `.` + mysqlIdentifierSeparator = `, ` + mysqlIdentifierQuote = "`{{.Raw}}`" + mysqlValueSeparator = `, ` + mysqlValueQuote = `'{{.}}'` + mysqlAndKeyword = `AND` + mysqlOrKeyword = `OR` + mysqlNotKeyword = `NOT` + mysqlDescKeyword = `DESC` + mysqlAscKeyword = `ASC` + mysqlDefaultOperator = `=` + mysqlClauseGroup = `({{.}})` + mysqlClauseOperator = ` {{.}} ` + mysqlColumnValue = `{{.Column}} {{.Operator}} {{.Value}}` + mysqlTableAliasLayout = `{{.Name}}{{if .Alias}} AS {{.Alias}}{{end}}` + mysqlColumnAliasLayout = `{{.Name}}{{if .Alias}} AS {{.Alias}}{{end}}` + mysqlSortByColumnLayout = `{{.Column}} {{.Sort}}` + + mysqlOrderByLayout = ` + {{if .SortColumns}} + ORDER BY {{.SortColumns}} + {{end}} + ` + + mysqlWhereLayout = ` + {{if .Conds}} + WHERE {{.Conds}} + {{end}} + ` + + mysqlSelectLayout = ` + SELECT + + {{if .Columns}} + {{.Columns}} + {{else}} + * + {{end}} + + FROM {{.Table}} + + {{.Where}} + + {{.OrderBy}} + + {{if .Limit}} + LIMIT {{.Limit}} + {{end}} + + {{if .Offset}} + OFFSET {{.Offset}} + {{end}} + ` + mysqlDeleteLayout = ` + DELETE + FROM {{.Table}} + {{.Where}} + ` + mysqlUpdateLayout = ` + UPDATE + {{.Table}} + SET {{.ColumnValues}} + {{ .Where }} + ` + + mysqlSelectCountLayout = ` + SELECT + COUNT(1) AS _t + FROM {{.Table}} + {{.Where}} + + {{if .Limit}} + LIMIT {{.Limit}} + {{end}} + + {{if .Offset}} + OFFSET {{.Offset}} + {{end}} + ` + + mysqlInsertLayout = ` + INSERT INTO {{.Table}} + ({{.Columns}}) + VALUES + ({{.Values}}) + {{.Extra}} + ` + + mysqlTruncateLayout = ` + TRUNCATE TABLE {{.Table}} + ` + + mysqlDropDatabaseLayout = ` + DROP DATABASE {{.Database}} + ` + + mysqlDropTableLayout = ` + DROP TABLE {{.Table}} + ` +) diff --git a/mysql/result.go b/mysql/result.go index b12fbbff..0564f8f9 100644 --- a/mysql/result.go +++ b/mysql/result.go @@ -1,44 +1,46 @@ -/* - Copyright (c) 2012-2014 JosĂŠ Carlos Nieto, https://menteslibres.net/xiam - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ +// Copyright (c) 2012-2014 JosĂŠ Carlos Nieto, https://menteslibres.net/xiam +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package mysql import ( "database/sql" - "fmt" "strings" "upper.io/db" - "upper.io/db/util/sqlutil" + "upper.io/db/util/sqlgen" ) type counter struct { - Total uint64 `field:"total"` + Total uint64 `db:"_t"` } type Result struct { - table *Table - queryChunks *sqlutil.QueryChunks - cursor *sql.Rows // This query cursor keeps results for Next(). + table *Table + cursor *sql.Rows // This is the main query cursor. It starts as a nil value. + limit sqlgen.Limit + offset sqlgen.Offset + columns sqlgen.Columns + where sqlgen.Where + orderBy sqlgen.OrderBy + arguments []interface{} } // Executes a SELECT statement that can feed Next(), All() or One(). @@ -46,38 +48,28 @@ func (self *Result) setCursor() error { var err error // We need a cursor, if the cursor does not exists yet then we create one. if self.cursor == nil { - self.cursor, err = self.table.source.doQuery( - // Mandatory SQL. - fmt.Sprintf( - "SELECT %s FROM `%s` WHERE %s", - // Fields. - strings.Join(self.queryChunks.Fields, `, `), - // Table name - self.table.Name(), - // Conditions - self.queryChunks.Conditions, - ), - // Arguments - self.queryChunks.Arguments, - // Optional SQL - self.queryChunks.Sort, - self.queryChunks.Limit, - self.queryChunks.Offset, - ) + self.cursor, err = self.table.source.doQuery(sqlgen.Statement{ + Type: sqlgen.SqlSelect, + Table: sqlgen.Table{self.table.Name()}, + Columns: self.columns, + Limit: self.limit, + Offset: self.offset, + Where: self.where, + }, self.arguments...) } return err } // Determines the maximum limit of results to be returned. func (self *Result) Limit(n uint) db.Result { - self.queryChunks.Limit = fmt.Sprintf(`LIMIT %d`, n) + self.limit = sqlgen.Limit(n) return self } // Determines how many documents will be skipped before starting to grab // results. func (self *Result) Skip(n uint) db.Result { - self.queryChunks.Offset = fmt.Sprintf(`OFFSET %d`, n) + self.offset = sqlgen.Offset(n) return self } @@ -85,24 +77,43 @@ func (self *Result) Skip(n uint) db.Result { // prefixed by - (minus) which means descending order, ascending order would be // used otherwise. func (self *Result) Sort(fields ...string) db.Result { - sort := make([]string, 0, len(fields)) + + sortColumns := make(sqlgen.SortColumns, 0, len(fields)) for _, field := range fields { - if strings.HasPrefix(field, `-`) == true { - sort = append(sort, field[1:]+` DESC`) + var sort sqlgen.SortColumn + + if strings.HasPrefix(field, `-`) { + // Explicit descending order. + sort = sqlgen.SortColumn{ + sqlgen.Column{field[1:]}, + sqlgen.SqlSortDesc, + } } else { - sort = append(sort, field+` ASC`) + // Ascending order. + sort = sqlgen.SortColumn{ + sqlgen.Column{field}, + sqlgen.SqlSortAsc, + } } + + sortColumns = append(sortColumns, sort) } - self.queryChunks.Sort = `ORDER BY ` + strings.Join(sort, `, `) + self.orderBy.SortColumns = sortColumns return self } // Retrieves only the given fields. func (self *Result) Select(fields ...string) db.Result { - self.queryChunks.Fields = fields + self.columns = make(sqlgen.Columns, 0, len(fields)) + + l := len(fields) + for i := 0; i < l; i++ { + self.columns = append(self.columns, sqlgen.Column{fields[i]}) + } + return self } @@ -115,11 +126,13 @@ func (self *Result) All(dst interface{}) error { } // Current cursor. - if err = self.setCursor(); err != nil { + err = self.setCursor() + + if err != nil { return err } - defer self.Close() // Make sure the result set closes. + defer self.Close() // Fetching all results within the cursor. err = self.table.T.FetchRows(dst, self.cursor) @@ -127,7 +140,7 @@ func (self *Result) All(dst interface{}) error { return err } -// Fetches only one result from the result set. +// Fetches only one result from the resultset. func (self *Result) One(dst interface{}) error { var err error @@ -148,13 +161,16 @@ func (self *Result) Next(dst interface{}) error { var err error // Current cursor. - if err = self.setCursor(); err != nil { + err = self.setCursor() + + if err != nil { self.Close() } // Fetching the next result from the cursor. - if err = self.table.T.FetchRow(dst, self.cursor); err != nil { - // Closing result set on error. + err = self.table.T.FetchRow(dst, self.cursor) + + if err != nil { self.Close() } @@ -164,14 +180,11 @@ func (self *Result) Next(dst interface{}) error { // Removes the matching items from the collection. func (self *Result) Remove() error { var err error - _, err = self.table.source.doExec( - fmt.Sprintf( - "DELETE FROM `%s` WHERE %s", - self.table.Name(), - self.queryChunks.Conditions, - ), - self.queryChunks.Arguments, - ) + _, err = self.table.source.doExec(sqlgen.Statement{ + Type: sqlgen.SqlDelete, + Table: sqlgen.Table{self.table.Name()}, + Where: self.where, + }, self.arguments...) return err } @@ -182,30 +195,19 @@ func (self *Result) Update(values interface{}) error { ff, vv, err := self.table.FieldValues(values, toInternal) - if err != nil { - return err - } - total := len(ff) - updateFields := make([]string, total) - updateArgs := make([]interface{}, total) + cvs := make(sqlgen.ColumnValues, 0, total) for i := 0; i < total; i++ { - updateFields[i] = fmt.Sprintf(`%s = ?`, ff[i]) - updateArgs[i] = vv[i] + cvs = append(cvs, sqlgen.ColumnValue{sqlgen.Column{ff[i]}, "=", sqlPlaceholder}) } - _, err = self.table.source.doExec( - fmt.Sprintf( - "UPDATE `%s` SET %s WHERE %s", - self.table.Name(), - strings.Join(updateFields, `, `), - self.queryChunks.Conditions, - ), - updateArgs, - self.queryChunks.Arguments, - ) + _, err = self.table.source.doExec(sqlgen.Statement{ + Type: sqlgen.SqlUpdate, + Table: sqlgen.Table{self.table.Name()}, + ColumnValues: cvs, + }, vv...) return err } @@ -223,23 +225,22 @@ func (self *Result) Close() error { // Counts matching elements. func (self *Result) Count() (uint64, error) { - rows, err := self.table.source.doQuery( - fmt.Sprintf( - "SELECT COUNT(1) AS total FROM `%s` WHERE %s", - self.table.Name(), - self.queryChunks.Conditions, - ), - self.queryChunks.Arguments, - ) + rows, err := self.table.source.doQuery(sqlgen.Statement{ + Type: sqlgen.SqlSelectCount, + Table: sqlgen.Table{self.table.Name()}, + Where: self.where, + Limit: self.limit, + Offset: self.offset, + }, self.arguments...) if err != nil { return 0, err } + defer rows.Close() + dst := counter{} self.table.T.FetchRow(&dst, rows) - rows.Close() - return dst.Total, nil } diff --git a/mysql/tx.go b/mysql/tx.go new file mode 100644 index 00000000..37ae6825 --- /dev/null +++ b/mysql/tx.go @@ -0,0 +1,34 @@ +// Copyright (c) 2012-2014 JosĂŠ Carlos Nieto, https://menteslibres.net/xiam +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package mysql + +type Tx struct { + *Source +} + +func (self *Tx) Commit() error { + return self.Source.tx.Commit() +} + +func (self *Tx) Rollback() error { + return self.Source.tx.Rollback() +} -- GitLab