diff --git a/ql/_dumps/structs.sql b/ql/_dumps/structs.sql index 7c340904ec1baf611a9068aed683d14651a8e4fd..997b8165a367ddc20afc4e8849d2988979c5e78e 100644 --- a/ql/_dumps/structs.sql +++ b/ql/_dumps/structs.sql @@ -6,6 +6,22 @@ CREATE TABLE artist ( name string ); +DROP TABLE IF EXISTS publication; + +CREATE TABLE publication ( + title string, + author_id int +); + +DROP TABLE IF EXISTS review; + +CREATE TABLE review ( + publication_id int, + name string, + comments string, + created time +); + DROP TABLE IF EXISTS data_types; CREATE TABLE data_types ( diff --git a/ql/collection.go b/ql/collection.go index 4adcc8a5776a78b71f567b7aa8bb419a60f50e1d..aa8c776804f8079b4dd82065388e057d0e310610 100644 --- a/ql/collection.go +++ b/ql/collection.go @@ -1,25 +1,23 @@ -/* - Copyright (c) 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 ql @@ -28,232 +26,240 @@ import ( "reflect" "strings" "upper.io/db" + "upper.io/db/util/sqlgen" "upper.io/db/util/sqlutil" ) -// Represents a QL table. +const defaultOperator = `==` + type Table struct { - source *Source sqlutil.T + source *Source + names []string } -func mirrorFn(a interface{}) interface{} { - return a -} - -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, - &t{&self.T}, + 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, ` && `) + `)`, 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, ` || `) + `)`, 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] = value_v.Index(i).Interface() } + + return args + } else { + return nil } - if len(sql) > 0 { - return `(` + strings.Join(sql, ` && `) + `)`, args - } - case db.Cond: - return self.compileStatement(t) + default: + args = []interface{}{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, + t: &t{&self.T}, } - 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() (err error) { +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)}, + }) - return err + if err != nil { + return err + } + + return nil } // Appends an item (map or struct) into the collection. func (self *Table) Append(item interface{}) (interface{}, error) { - fields, values, err := self.FieldValues(item, mirrorFn) + cols, vals, err := self.FieldValues(item, toInternal) - // Error ocurred, stop appending. - if err != nil { - return nil, err + var columns sqlgen.Columns + var values sqlgen.Values + + for _, col := range cols { + columns = append(columns, sqlgen.Column{col}) } - res, err := self.source.doExec( - fmt.Sprintf(`INSERT INTO %s`, self.Name()), - sqlFields(fields), - `VALUES`, - sqlValues(values), - ) + for i := 0; i < len(vals); i++ { + values = append(values, sqlPlaceholder) + } + // Error ocurred, stop appending. if err != nil { return nil, err } - var id int64 - - id, err = res.LastInsertId() + res, err := self.source.doExec(sqlgen.Statement{ + Type: sqlgen.SqlInsert, + Table: sqlgen.Table{self.tableN(0)}, + Columns: columns, + Values: values, + }, vals...) if err != nil { return nil, err } + var id int64 + id, _ = res.LastInsertId() + return id, nil } // Returns true if the collection exists. func (self *Table) Exists() bool { - rows, err := self.source.doQuery( - `SELECT Name - FROM __Table - WHERE Name == ? - `, - []interface{}{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 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] = value_v.Index(i).Interface() - } - - return args - } else { - return nil - } - default: - args = []interface{}{value} - } +func (self *Table) Name() string { + return strings.Join(self.names, `, `) +} - return args +func toInternal(v interface{}) interface{} { + return v } diff --git a/ql/database.go b/ql/database.go index 9d4db014538bd67c174e4bc21667e5c82aade866..aa0efd74d50b69a028013c75abe270be367050ce 100644 --- a/ql/database.go +++ b/ql/database.go @@ -1,25 +1,23 @@ -/* - Copyright (c) 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 ql @@ -27,38 +25,41 @@ import ( "database/sql" "fmt" _ "github.com/cznic/ql/driver" - "log" "os" "reflect" "strings" "time" "upper.io/db" + "upper.io/db/util/sqlgen" + "upper.io/db/util/sqlutil" ) -var Debug = false +const Driver = `ql` -// Format for saving dates. -var DateFormat = "2006-01-02 15:04:05" - -// Format for saving times. -var TimeFormat = "%d:%02d:%02d.%d" - -var timeType = reflect.TypeOf(time.Time{}).Kind() +var ( + // Format for saving dates. + DateFormat = "2006-01-02 15:04:05.000" + // Format for saving times. + TimeFormat = "%d:%02d:%02d.%03d" + timeType = reflect.TypeOf(time.Time{}).Kind() +) -const driverName = `ql` +var template *sqlgen.Template -type sqlValues_t []interface{} +var ( + sqlPlaceholder = sqlgen.Value{sqlgen.Raw{`?`}} +) type Source struct { config db.Settings session *sql.DB - name string collections map[string]db.Collection + tx *sql.Tx } -type sqlQuery struct { - Query []string - Args []interface{} +type columnSchema_t struct { + ColumnName string `db:"name"` + DataType string `db:"type"` } func debugEnabled() bool { @@ -69,129 +70,156 @@ 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) -} - -func sqlCompile(terms []interface{}) *sqlQuery { - q := &sqlQuery{} - q.Query = []string{} - - 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) - } - } + template = &sqlgen.Template{ + qlColumnSeparator, + qlIdentifierSeparator, + qlIdentifierQuote, + qlValueSeparator, + qlValueQuote, + qlAndKeyword, + qlOrKeyword, + qlNotKeyword, + qlDescKeyword, + qlAscKeyword, + qlDefaultOperator, + qlClauseGroup, + qlClauseOperator, + qlColumnValue, + qlTableAliasLayout, + qlColumnAliasLayout, + qlSortByColumnLayout, + qlWhereLayout, + qlOrderByLayout, + qlInsertLayout, + qlSelectLayout, + qlUpdateLayout, + qlDeleteLayout, + qlTruncateLayout, + qlDropDatabaseLayout, + qlDropTableLayout, + qlSelectCountLayout, } - return q -} - -func sqlFields(names []string) string { - return `(` + strings.Join(names, `, `) + `)` + db.Register(Driver, &Source{}) } -func sqlValues(values []interface{}) sqlValues_t { - ret := make(sqlValues_t, len(values)) - for i, _ := range values { - ret[i] = values[i] - } - return ret -} - -func (self *Source) doExec(terms ...interface{}) (res sql.Result, err error) { - var tx *sql.Tx +func (self *Source) doExec(stmt sqlgen.Statement, args ...interface{}) (sql.Result, error) { if self.session == nil { return nil, db.ErrNotConnected } - chunks := sqlCompile(terms) - - query := strings.Join(chunks.Query, ` `) + query := stmt.Compile(template) - for i := 0; i < len(chunks.Args); i++ { + l := len(args) + for i := 0; i < l; i++ { query = strings.Replace(query, `?`, fmt.Sprintf(`$%d`, i+1), 1) } if debugEnabled() == true { - debugLogQuery(query, chunks) + sqlutil.DebugQuery(query, args) } - if tx, err = self.session.Begin(); err != nil { - return nil, err - } + if self.tx == nil { + var tx *sql.Tx + var err error + var res sql.Result - if res, err = tx.Exec(query, chunks.Args...); err != nil { - return nil, err - } + if tx, err = self.session.Begin(); err != nil { + return nil, err + } - if err = tx.Commit(); err != nil { - return nil, err + if res, err = tx.Exec(query, args...); err != nil { + return nil, err + } + + if err = tx.Commit(); err != nil { + return nil, err + } + + return res, nil } - return res, nil + return self.tx.Exec(query, args...) } -func (self *Source) doQuery(terms ...interface{}) (*sql.Rows, 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 := stmt.Compile(template) - query := strings.Join(chunks.Query, ` `) - - for i := 0; i < len(chunks.Args); i++ { + l := len(args) + for i := 0; i < l; i++ { query = strings.Replace(query, `?`, fmt.Sprintf(`$%d`, i+1), 1) } if debugEnabled() == true { - debugLogQuery(query, chunks) + sqlutil.DebugQuery(query, args) + } + + if self.tx == nil { + var tx *sql.Tx + var err error + var rows *sql.Rows + + if tx, err = self.session.Begin(); err != nil { + return nil, err + } + + if rows, err = tx.Query(query, args...); err != nil { + return nil, err + } + + if err = tx.Commit(); err != nil { + return nil, err + } + + return rows, nil } - return self.session.Query(query, chunks.Args...) + return self.tx.Query(query, args...) } -func (self *Source) doQueryRow(terms ...interface{}) (*sql.Row, 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 := stmt.Compile(template) - query := strings.Join(chunks.Query, ` `) - - for i := 0; i < len(chunks.Args); i++ { + l := len(args) + for i := 0; i < l; i++ { query = strings.Replace(query, `?`, fmt.Sprintf(`$%d`, i+1), 1) } - if Debug == true { - fmt.Printf("Q: %s\n", query) - fmt.Printf("A: %v\n", chunks.Args) + if debugEnabled() == true { + sqlutil.DebugQuery(query, args) } - return self.session.QueryRow(query, chunks.Args...), nil + if self.tx == nil { + var tx *sql.Tx + var err error + var row *sql.Row + + if tx, err = self.session.Begin(); err != nil { + return nil, err + } + + if row = tx.QueryRow(query, args...); err != nil { + return nil, err + } + + if err = tx.Commit(); err != nil { + return nil, err + } + + return row, nil + } else { + return self.tx.QueryRow(query, args...), nil + } } // Returns the string name of the database. @@ -199,6 +227,47 @@ 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 clone, err = self.clone(); err != nil { + return nil, err + } + + if sqlTx, err = clone.session.Begin(); 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 @@ -242,22 +311,15 @@ func (self *Source) Use(database string) error { return self.Open() } -// Starts a transaction block. -func (self *Source) Begin() error { - _, err := self.session.Exec(`BEGIN`) - return err -} - -// Ends a transaction block. -func (self *Source) End() error { - _, err := self.session.Exec(`END`) - return err -} - // Drops the currently active database. func (self *Source) Drop() error { - self.session.Query(fmt.Sprintf(`DROP DATABASE "%s"`, self.config.Database)) - return nil + + _, err := self.doQuery(sqlgen.Statement{ + Type: sqlgen.SqlDropDatabase, + Database: sqlgen.Database{self.config.Database}, + }) + + return err } // Returns a list of all tables within the currently active database. @@ -265,7 +327,13 @@ func (self *Source) Collections() ([]string, error) { var collections []string var collection string - rows, err := self.session.Query(`SELECT Name FROM __Table`) + rows, err := self.doQuery(sqlgen.Statement{ + Type: sqlgen.SqlSelect, + Table: sqlgen.Table{`__Table`}, + Columns: sqlgen.Columns{ + {`Name`}, + }, + }) if err != nil { return nil, err @@ -274,114 +342,136 @@ func (self *Source) Collections() ([]string, error) { defer rows.Close() for rows.Next() { - if err = rows.Scan(&collection); err != nil { - return nil, err - } + rows.Scan(&collection) collections = append(collections, collection) } - err = rows.Err() + return collections, nil +} - if err != nil { - return nil, err +func (self *Source) tableExists(names ...string) error { + for _, name := range names { + + rows, err := self.doQuery(sqlgen.Statement{ + Type: sqlgen.SqlSelect, + Table: sqlgen.Table{`__Table`}, + Columns: sqlgen.Columns{ + {`Name`}, + }, + Where: sqlgen.Where{ + sqlgen.ColumnValue{sqlgen.Column{`Name`}, `==`, sqlPlaceholder}, + }, + }, name) + + if err != nil { + return db.ErrCollectionDoesNotExists + } + + defer rows.Close() + + if rows.Next() == false { + return db.ErrCollectionDoesNotExists + } } - return collections, nil + return nil } // Returns a collection instance by name. -func (self *Source) Collection(name string) (db.Collection, error) { +func (self *Source) Collection(names ...string) (db.Collection, error) { - if collection, ok := self.collections[name]; ok == true { - return collection, nil + if len(names) == 0 { + return nil, db.ErrMissingCollectionName } - table := &Table{} + col := &Table{ + source: self, + names: names, + } - table.source = self - table.DB = self - table.PrimaryKey = `id` + col.PrimaryKey = `id` - table.SetName = name + columns_t := []columnSchema_t{} - // Table exists? - if table.Exists() == false { - return table, db.ErrCollectionDoesNotExists - } + for _, name := range names { + chunks := strings.SplitN(name, " ", 2) - // Fetching table datatypes and mapping to internal gotypes. - rows, err := table.source.doQuery( - `SELECT - Name, Type - FROM __Column - WHERE - TableName == ?`, - []string{table.Name()}, - ) + if len(chunks) > 0 { - if err != nil { - return table, err - } + name = chunks[0] - columns := []struct { - Name string - Type string - }{} + if err := self.tableExists(name); err != nil { + return nil, err + } - err = table.FetchRows(&columns, rows) + rows, err := self.doQuery(sqlgen.Statement{ + Type: sqlgen.SqlSelect, + Table: sqlgen.Table{`__Column`}, + Columns: sqlgen.Columns{ + {`Name`}, + {`Type`}, + }, + Where: sqlgen.Where{ + sqlgen.ColumnValue{sqlgen.Column{`TableName`}, `==`, sqlPlaceholder}, + }, + }, name) + + if err != nil { + return nil, err + } - if err != nil { - return nil, err - } + if err = col.FetchRows(&columns_t, rows); err != nil { + return nil, err + } - table.ColumnTypes = make(map[string]reflect.Kind, len(columns)) - - for _, column := range columns { - - column.Name = strings.ToLower(column.Name) - column.Type = strings.ToLower(column.Type) - - // Default properties. - dtype := column.Type - - ctype := reflect.String - - // Guessing datatypes. - switch dtype { - case `int`: - ctype = reflect.Int - case `int8`: - ctype = reflect.Int8 - case `int16`: - ctype = reflect.Int16 - case `int32`, `rune`: - ctype = reflect.Int32 - case `int64`: - ctype = reflect.Int64 - case `uint`: - ctype = reflect.Uint - case `uint8`: - ctype = reflect.Uint8 - case `uint16`: - ctype = reflect.Uint16 - case `uint32`: - ctype = reflect.Uint32 - case `uint64`: - ctype = reflect.Uint64 - case `float64`: - ctype = reflect.Float64 - case `float32`: - ctype = reflect.Float32 - case `time`: - ctype = timeType - default: - ctype = reflect.String - } + col.ColumnTypes = make(map[string]reflect.Kind, len(columns_t)) + + for _, column := range columns_t { + + column.ColumnName = strings.ToLower(column.ColumnName) + column.DataType = strings.ToLower(column.DataType) + + // Default properties. + dtype := column.DataType + ctype := reflect.String + + // Guessing datatypes. + switch dtype { + case `int`: + ctype = reflect.Int + case `int8`: + ctype = reflect.Int8 + case `int16`: + ctype = reflect.Int16 + case `int32`, `rune`: + ctype = reflect.Int32 + case `int64`: + ctype = reflect.Int64 + case `uint`: + ctype = reflect.Uint + case `uint8`: + ctype = reflect.Uint8 + case `uint16`: + ctype = reflect.Uint16 + case `uint32`: + ctype = reflect.Uint32 + case `uint64`: + ctype = reflect.Uint64 + case `float64`: + ctype = reflect.Float64 + case `float32`: + ctype = reflect.Float32 + case `time`: + ctype = timeType + default: + ctype = reflect.String + } - table.ColumnTypes[column.Name] = ctype - } + col.ColumnTypes[column.ColumnName] = ctype + } - self.collections[name] = table + } + } - return table, nil + return col, nil } diff --git a/ql/database_test.go b/ql/database_test.go index 668a059a6a3ec0e7388a79e863f5cb67631b04b8..d0693a9daaaf98572189ba47b7c62d3abdb6cc51 100644 --- a/ql/database_test.go +++ b/ql/database_test.go @@ -1,57 +1,50 @@ -/* - Copyright (c) 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 ql wrapper. - - Execute the Makefile in ./_dumps/ to create the expected database structure. +// 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. - cd _dumps - make - cd .. - go test -*/ package ql +// In order to execute these tests you must initialize the database first: +// +// cd _dumps +// make +// cd .. +// go test + import ( - "database/sql" + //"database/sql" "menteslibres.net/gosexy/to" "os" + //"reflect" "strings" "testing" "time" "upper.io/db" ) -// Wrapper. -const wrapperName = "ql" - -// Wrapper settings. -const dbname = "file://_dumps/test.db" +const ( + database = `_dumps/test.db` +) -// Global settings for tests. var settings = db.Settings{ - Database: dbname, + Database: database, } // Structure for testing conversions and datatypes. @@ -80,39 +73,41 @@ type testValuesStruct struct { // Declaring some values to insert, we expect the same values to be returned. var testValues = testValuesStruct{ - uint(1), uint8(1), uint16(1), uint32(1), uint64(1), - int(-1), int8(-1), int16(-1), int32(-1), int64(-1), - float32(1.337), float64(1.337), + 1, 1, 1, 1, 1, + -1, -1, -1, -1, -1, + 1.337, 1.337, true, "Hello world!", - time.Date(2012, 7, 28, 1, 2, 3, 0, time.UTC), + time.Date(2012, 7, 28, 1, 2, 3, 0, time.Local), time.Second * time.Duration(7331), } -// Enabling outputting some information to stdout (like the SQL query and its +// 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()) } @@ -120,158 +115,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().Select("id() AS id", "name") - + // Dumping into a map. row_m := map[string]interface{}{} + res = artist.Find().Select("id() as id", "name") + for { err = res.Next(&row_m) if err == db.ErrNoMoreRows { - // No more row_ms left. break } @@ -289,19 +288,18 @@ func TestResultFetch(t *testing.T) { res.Close() - // Testing struct + // Dumping into an struct with no tags. row_s := struct { Id uint64 Name string }{} - res = artist.Find().Select("id() AS id", "name") + res = artist.Find().Select("id() as id", "name") for { err = res.Next(&row_s) if err == db.ErrNoMoreRows { - // No more row_s' left. break } @@ -319,19 +317,18 @@ 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"` }{} - res = artist.Find().Select("id() AS id", "name") + res = artist.Find().Select("id() as id", "name") for { err = res.Next(&row_t) if err == db.ErrNoMoreRows { - // No more row_t's left. break } @@ -349,54 +346,62 @@ func TestResultFetch(t *testing.T) { res.Close() - // Testing Result.All() with a slice of maps. - res = artist.Find().Select("id() AS id", "name") - + // Dumping into an slice of maps. all_rows_m := []map[string]interface{}{} - err = res.All(&all_rows_m) - if err != nil { + res = artist.Find().Select("id() as id", "name") + 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().Select("id() AS id", "name") + // 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().Select("id() as id", "name") + 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().Select("id() AS id", "name") - + // 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().Select("id() as id", "name") + + 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.") @@ -404,28 +409,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 @@ -434,96 +434,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()) } @@ -535,99 +522,296 @@ 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(), + }) + + /* + // TODO: Not supported by QL. + + // 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() as 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. +// TODO: Not supported by QL. + +// 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.") } } */ @@ -637,36 +821,40 @@ func TestDisableDebug(t *testing.T) { os.Setenv(db.EnvEnableDebug, "") } +/* +// TODO: Unsupported by QL // 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()) } } } +*/ // Benchmarking Append(). // // 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()) @@ -677,68 +865,132 @@ 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()) + } + } +} + +/* +// TODO: QL still has some issues here. + +// 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/ql/layout.go b/ql/layout.go new file mode 100644 index 0000000000000000000000000000000000000000..15f9a44f8c0e0b2d427671c232e4c1c5d53dc428 --- /dev/null +++ b/ql/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 ql + +const ( + qlColumnSeparator = `.` + qlIdentifierSeparator = `, ` + qlIdentifierQuote = `{{.Raw}}` + qlValueSeparator = `, ` + qlValueQuote = `"{{.}}"` + qlAndKeyword = `&&` + qlOrKeyword = `||` + qlNotKeyword = `!=` + qlDescKeyword = `DESC` + qlAscKeyword = `ASC` + qlDefaultOperator = `==` + qlClauseGroup = `({{.}})` + qlClauseOperator = ` {{.}} ` + qlColumnValue = `{{.Column}} {{.Operator}} {{.Value}}` + qlTableAliasLayout = `{{.Name}}{{if .Alias}} AS {{.Alias}}{{end}}` + qlColumnAliasLayout = `{{.Name}}{{if .Alias}} AS {{.Alias}}{{end}}` + qlSortByColumnLayout = `{{.Column}} {{.Sort}}` + + qlOrderByLayout = ` + {{if .SortColumns}} + ORDER BY {{.SortColumns}} + {{end}} + ` + + qlWhereLayout = ` + {{if .Conds}} + WHERE {{.Conds}} + {{end}} + ` + + qlSelectLayout = ` + SELECT + + {{if .Columns}} + {{.Columns}} + {{else}} + * + {{end}} + + FROM {{.Table}} + + {{.Where}} + + {{.OrderBy}} + + {{if .Limit}} + LIMIT {{.Limit}} + {{end}} + + {{if .Offset}} + OFFSET {{.Offset}} + {{end}} + ` + qlDeleteLayout = ` + DELETE + FROM {{.Table}} + {{.Where}} + ` + qlUpdateLayout = ` + UPDATE + {{.Table}} + SET {{.ColumnValues}} + {{ .Where }} + ` + + qlSelectCountLayout = ` + SELECT + count(1) AS total + FROM {{.Table}} + {{.Where}} + + {{if .Limit}} + LIMIT {{.Limit}} + {{end}} + + {{if .Offset}} + OFFSET {{.Offset}} + {{end}} + ` + + qlInsertLayout = ` + INSERT INTO {{.Table}} + ({{.Columns}}) + VALUES + ({{.Values}}) + {{.Extra}} + ` + + qlTruncateLayout = ` + TRUNCATE TABLE {{.Table}} + ` + + qlDropDatabaseLayout = ` + DROP DATABASE {{.Database}} + ` + + qlDropTableLayout = ` + DROP TABLE {{.Table}} + ` +) diff --git a/ql/result.go b/ql/result.go index 5ee594c26fb2190459c315cb56a90123fe756fdd..bcdfb3c4b4867b8371a1391c65acb800e7051a0e 100644 --- a/ql/result.go +++ b/ql/result.go @@ -1,46 +1,47 @@ -/* - Copyright (c) 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 ql 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"` +type counter_t struct { + Total uint64 `db:"total"` } type Result struct { - table *Table - queryChunks *sqlutil.QueryChunks - // This is the main query cursor. It starts as a nil value. - cursor *sql.Rows - t *t + 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{} + t *t } // Executes a SELECT statement that can feed Next(), All() or One(). @@ -48,38 +49,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 } @@ -87,24 +78,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 } @@ -132,7 +142,9 @@ func (self *Result) All(dst interface{}) error { } // Fetches only one result from the resultset. -func (self *Result) One(dst interface{}) (err error) { +func (self *Result) One(dst interface{}) error { + var err error + if self.cursor != nil { return db.ErrQueryIsPending } @@ -146,34 +158,34 @@ func (self *Result) One(dst interface{}) (err error) { // Fetches the next result from the resultset. 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() - return err } // Fetching the next result from the cursor. - if err = self.t.qlFetchRow(dst, self.cursor); err != nil { + err = self.t.qlFetchRow(dst, self.cursor) + + if err != nil { self.Close() - return err } - return nil + return err } // 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,32 +194,21 @@ func (self *Result) Remove() error { // struct. func (self *Result) Update(values interface{}) error { - ff, vv, err := self.table.FieldValues(values, mirrorFn) - - if err != nil { - return err - } + ff, vv, err := self.table.FieldValues(values, toInternal) 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 %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 } @@ -225,23 +226,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 } - dst := counter{} - self.t.FetchRow(&dst, rows) + defer rows.Close() - rows.Close() + dst := counter_t{} + self.t.FetchRow(&dst, rows) return dst.Total, nil } diff --git a/ql/tx.go b/ql/tx.go new file mode 100644 index 0000000000000000000000000000000000000000..3c6b5bedcde5f7c28ca80ff616ee3e9ce627852a --- /dev/null +++ b/ql/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 ql + +type Tx struct { + *Source +} + +func (self *Tx) Commit() error { + return self.Source.tx.Commit() +} + +func (self *Tx) Rollback() error { + return self.Source.tx.Rollback() +}