diff --git a/sqlite/_dumps/structs.sql b/sqlite/_dumps/structs.sql index 6d886dd3896d739944cf253284f204f8752d7a67..207788225b2d8c7084a5e5c408c7846f68702096 100644 --- a/sqlite/_dumps/structs.sql +++ b/sqlite/_dumps/structs.sql @@ -1,11 +1,33 @@ PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; +DROP TABLE IF EXISTS artist; + CREATE TABLE artist ( id integer primary key, name varchar(60) ); +DROP TABLE IF EXISTS publication; + +CREATE TABLE publication ( + id integer primary key, + title varchar(80), + author_id integer +); + +DROP TABLE IF EXISTS review; + +CREATE TABLE review ( + id integer primary key, + publication_id integer, + name varchar(80), + comments text, + created varchar(20) +); + +DROP TABLE IF EXISTS data_types; + CREATE TABLE data_types ( id integer primary key, _uint integer, diff --git a/sqlite/collection.go b/sqlite/collection.go index 4a633e9f0b2d3742e088587c4155d7e6d4e93741..21250066d7b44ce4106e7c5b365116c1f985ff71 100644 --- a/sqlite/collection.go +++ b/sqlite/collection.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 sqlite @@ -30,206 +28,241 @@ import ( "strings" "time" "upper.io/db" + "upper.io/db/util/sqlgen" "upper.io/db/util/sqlutil" ) -// Represents a SQLite table. +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(`DELETE FROM '%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) { - 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), - ) + row, 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. - id, _ := res.LastInsertId() + var id int64 + id, _ = row.LastInsertId() return id, nil } // Returns true if the collection exists. func (self *Table) Exists() bool { - rows, err := self.source.doQuery( - fmt.Sprintf(` - SELECT name - FROM sqlite_master - WHERE type = 'table' AND name = '%s' - `, - 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) @@ -244,42 +277,5 @@ func toInternal(val interface{}) interface{} { return `0` } } - return to.String(val) } - -// Convers 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/sqlite/database.go b/sqlite/database.go index 24f42c2b8f277e1e9ade83fa3c4119e2e4e61911..1572115c241efa8c5c9d33a53ae52fba027b2869 100644 --- a/sqlite/database.go +++ b/sqlite/database.go @@ -1,65 +1,66 @@ -/* - 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 sqlite import ( "database/sql" "fmt" - // This hack is not required anymore. - // See: https://github.com/mattn/go-sqlite3/issues/40 - //_ "github.com/xiam/gosqlite3" _ "github.com/mattn/go-sqlite3" - "log" "os" "reflect" "regexp" "strings" "upper.io/db" + "upper.io/db/util/sqlgen" + "upper.io/db/util/sqlutil" ) -// Format for saving dates. -var DateFormat = `2006-01-02 15:04:05` +const Driver = `sqlite` -// Format for saving times. -var TimeFormat = `%d:%02d:%02d.%d` - -var columnPattern = regexp.MustCompile(`^([a-zA-Z]+)\(?([0-9,]+)?\)?\s?([a-zA-Z]*)?`) +var ( + // Format for saving dates. + DateFormat = "2006-01-02 15:04:05" + // Format for saving times. + TimeFormat = "%d:%02d:%02d.%d" + SSLMode = "disable" +) -const driverName = `sqlite` +var template *sqlgen.Template -type sqlValues_t []interface{} +var ( + columnPattern = regexp.MustCompile(`^([a-zA-Z]+)\(?([0-9,]+)?\)?\s?([a-zA-Z]*)?`) + 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 { @@ -70,90 +71,109 @@ 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) -} + template = &sqlgen.Template{ + sqlColumnSeparator, + sqlIdentifierSeparator, + sqlIdentifierQuote, + sqlValueSeparator, + sqlValueQuote, + sqlAndKeyword, + sqlOrKeyword, + sqlNotKeyword, + sqlDescKeyword, + sqlAscKeyword, + sqlDefaultOperator, + sqlClauseGroup, + sqlClauseOperator, + sqlColumnValue, + sqlTableAliasLayout, + sqlColumnAliasLayout, + sqlSortByColumnLayout, + sqlWhereLayout, + sqlOrderByLayout, + sqlInsertLayout, + sqlSelectLayout, + sqlUpdateLayout, + sqlDeleteLayout, + sqlTruncateLayout, + sqlDropDatabaseLayout, + sqlDropTableLayout, + sqlSelectCountLayout, + } -func sqlCompile(terms []interface{}) *sqlQuery { - q := &sqlQuery{} + db.Register(Driver, &Source{}) +} - 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(template) -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(template) if debugEnabled() == true { - debugLogQuery(query, chunks) + sqlutil.DebugQuery(query, args) } - return self.session.Exec(query, chunks.Args...) -} + if self.tx != nil { + return self.tx.Query(query, args...) + } -func (self *Source) doQuery(terms ...interface{}) (*sql.Rows, error) { + return self.session.Query(query, args...) +} +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) + + if debugEnabled() == true { + sqlutil.DebugQuery(query, args) + } + + if self.tx != nil { + return self.tx.QueryRow(query, args...), nil + } + + return self.session.QueryRow(query, args...), nil +} - query := strings.Join(chunks.Query, ` `) +func (self *Source) doRawQuery(query string, args ...interface{}) (*sql.Rows, error) { + if self.session == nil { + return nil, db.ErrNotConnected + } if debugEnabled() == true { - debugLogQuery(query, chunks) + sqlutil.DebugQuery(query, args) + } + + if self.tx != nil { + return self.tx.Query(query, args...) } - return self.session.Query(query, chunks.Args...) + return self.session.Query(query, args...) } // Returns the string name of the database. @@ -161,6 +181,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 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 @@ -181,9 +242,7 @@ func (self *Source) Open() error { return db.ErrMissingDatabaseName } - self.session, err = sql.Open(`sqlite3`, fmt.Sprintf(`file:%s?cache=shared`, self.config.Database)) - - if err != nil { + if self.session, err = sql.Open(`sqlite3`, fmt.Sprintf(`file:%s?cache=shared`, self.config.Database)); err != nil { return err } @@ -204,21 +263,14 @@ 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 { - _, 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 } @@ -227,7 +279,16 @@ func (self *Source) Collections() ([]string, error) { var collections []string var collection string - rows, err := self.session.Query(`SELECT tbl_name FROM sqlite_master WHERE type = ?`, `table`) + rows, err := self.doQuery(sqlgen.Statement{ + Type: sqlgen.SqlSelect, + Columns: sqlgen.Columns{ + {"tbl_name"}, + }, + Table: sqlgen.Table{"sqlite_master"}, + Where: sqlgen.Where{ + sqlgen.ColumnValue{sqlgen.Column{"type"}, "=", sqlgen.Value{"table"}}, + }, + }) if err != nil { return nil, err @@ -243,82 +304,112 @@ 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{`sqlite_master`}, + Columns: sqlgen.Columns{ + {`tbl_name`}, + }, + Where: sqlgen.Where{ + sqlgen.ColumnValue{sqlgen.Column{`type`}, `=`, sqlPlaceholder}, + sqlgen.ColumnValue{sqlgen.Column{`tbl_name`}, `=`, sqlPlaceholder}, + }, + }, `table`, name) + + if err != nil { + return db.ErrCollectionDoesNotExists + } - if collection, ok := self.collections[name]; ok == true { - return collection, nil + defer rows.Close() + + 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.session.Query(fmt.Sprintf(`PRAGMA TABLE_INFO('%s')`, table.Name())) + col.PrimaryKey = `id` - if err != nil { - return table, err - } + columns_t := []columnSchema_t{} - columns := []struct { - Name 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.doRawQuery(fmt.Sprintf(`PRAGMA TABLE_INFO('%s')`, name)) - for _, column := range columns { + if err != nil { + return nil, err + } - column.Name = strings.ToLower(column.Name) - column.Type = strings.ToLower(column.Type) + if err = col.FetchRows(&columns_t, rows); err != nil { + return nil, err + } - results := columnPattern.FindStringSubmatch(column.Type) + col.ColumnTypes = make(map[string]reflect.Kind, len(columns_t)) - // Default properties. - dextra := "" - dtype := `text` + for _, column := range columns_t { - dtype = results[1] + column.ColumnName = strings.ToLower(column.ColumnName) + column.DataType = strings.ToLower(column.DataType) - if len(results) > 3 { - dextra = results[3] - } + results := columnPattern.FindStringSubmatch(column.DataType) - ctype := reflect.String + // Default properties. + dextra := `` + dtype := `text` + + dtype = results[1] + + if len(results) > 3 { + dextra = results[3] + } - // Guessing datatypes. - switch dtype { - case `integer`: - if dextra == `unsigned` { - ctype = reflect.Uint64 - } else { - ctype = reflect.Int64 + ctype := reflect.String + + // Guessing datatypes. + switch dtype { + case `integer`: + if dextra == `unsigned` { + ctype = reflect.Uint64 + } else { + ctype = reflect.Int64 + } + case `real`, `numeric`: + ctype = reflect.Float64 + default: + ctype = reflect.String + } + + col.ColumnTypes[column.ColumnName] = ctype } - case `real`, `numeric`: - ctype = reflect.Float64 - default: - ctype = reflect.String - } - table.ColumnTypes[column.Name] = ctype + } } - self.collections[name] = table - - return table, nil + return col, nil } diff --git a/sqlite/database_test.go b/sqlite/database_test.go index b640f832b7fb2056e864933f1d282a0d102cfb67..0a34c8c8c1c0fe361122d3cba969318256731129 100644 --- a/sqlite/database_test.go +++ b/sqlite/database_test.go @@ -1,38 +1,33 @@ -/* - 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 sqlite 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 sqlite +// In order to execute these tests you must initialize the database first: +// +// cd _dumps +// make +// cd .. +// go test + import ( "database/sql" "menteslibres.net/gosexy/to" @@ -44,15 +39,12 @@ import ( "upper.io/db" ) -// Wrapper. -const wrapperName = "sqlite" - -// Wrapper settings. -const databaseFilename = "./_dumps/gotest.sqlite3.db" +const ( + database = `_dumps/gotest.sqlite3.db` +) -// Global settings for tests. var settings = db.Settings{ - Database: databaseFilename, + Database: database, } // Structure for testing conversions and datatypes. @@ -90,30 +82,32 @@ var testValues = testValuesStruct{ 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()) } @@ -121,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() - + // 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 } @@ -290,7 +288,7 @@ func TestResultFetch(t *testing.T) { res.Close() - // Testing struct + // Dumping into an struct with no tags. row_s := struct { Id uint64 Name string @@ -302,7 +300,6 @@ func TestResultFetch(t *testing.T) { err = res.Next(&row_s) if err == db.ErrNoMoreRows { - // No more row_s' left. break } @@ -320,7 +317,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"` @@ -332,7 +329,6 @@ func TestResultFetch(t *testing.T) { err = res.Next(&row_t) if err == db.ErrNoMoreRows { - // No more row_t's left. break } @@ -350,54 +346,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.") @@ -405,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 @@ -435,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()) } @@ -536,98 +522,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.") } } @@ -637,24 +814,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(`DELETE FROM "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()) } } @@ -664,8 +842,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()) @@ -676,68 +854,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++ { - _, err = artist.Append(map[string]string{"name": "Leonardo DaVinci"}) - if err != nil { + 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(`DELETE FROM "artist"`); err != nil { + b.Fatalf(err.Error()) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + 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/sqlite/layout.go b/sqlite/layout.go new file mode 100644 index 0000000000000000000000000000000000000000..17f7c10a037eec50838787ff7c0ef57d1b57c817 --- /dev/null +++ b/sqlite/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 sqlite + +const ( + sqlColumnSeparator = `.` + sqlIdentifierSeparator = `, ` + sqlIdentifierQuote = `"{{.Raw}}"` + sqlValueSeparator = `, ` + sqlValueQuote = `'{{.}}'` + sqlAndKeyword = `AND` + sqlOrKeyword = `OR` + sqlNotKeyword = `NOT` + sqlDescKeyword = `DESC` + sqlAscKeyword = `ASC` + sqlDefaultOperator = `=` + sqlClauseGroup = `({{.}})` + sqlClauseOperator = ` {{.}} ` + sqlColumnValue = `{{.Column}} {{.Operator}} {{.Value}}` + sqlTableAliasLayout = `{{.Name}}{{if .Alias}} AS {{.Alias}}{{end}}` + sqlColumnAliasLayout = `{{.Name}}{{if .Alias}} AS {{.Alias}}{{end}}` + sqlSortByColumnLayout = `{{.Column}} {{.Sort}}` + + sqlOrderByLayout = ` + {{if .SortColumns}} + ORDER BY {{.SortColumns}} + {{end}} + ` + + sqlWhereLayout = ` + {{if .Conds}} + WHERE {{.Conds}} + {{end}} + ` + + sqlSelectLayout = ` + SELECT + + {{if .Columns}} + {{.Columns}} + {{else}} + * + {{end}} + + FROM {{.Table}} + + {{.Where}} + + {{.OrderBy}} + + {{if .Limit}} + LIMIT {{.Limit}} + {{end}} + + {{if .Offset}} + OFFSET {{.Offset}} + {{end}} + ` + sqlDeleteLayout = ` + DELETE + FROM {{.Table}} + {{.Where}} + ` + sqlUpdateLayout = ` + UPDATE + {{.Table}} + SET {{.ColumnValues}} + {{ .Where }} + ` + + sqlSelectCountLayout = ` + SELECT + COUNT(1) AS _t + FROM {{.Table}} + {{.Where}} + + {{if .Limit}} + LIMIT {{.Limit}} + {{end}} + + {{if .Offset}} + OFFSET {{.Offset}} + {{end}} + ` + + sqlInsertLayout = ` + INSERT INTO {{.Table}} + ({{.Columns}}) + VALUES + ({{.Values}}) + {{.Extra}} + ` + + sqlTruncateLayout = ` + DELETE FROM {{.Table}} + ` + + sqlDropDatabaseLayout = ` + DROP DATABASE {{.Database}} + ` + + sqlDropTableLayout = ` + DROP TABLE {{.Table}} + ` +) diff --git a/sqlite/result.go b/sqlite/result.go index 5354308b3a9ce0739dd3f48c3e20631297ece640..04a1dd985d5f146606eaba7bcff3f18254eb9c37 100644 --- a/sqlite/result.go +++ b/sqlite/result.go @@ -1,45 +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 sqlite 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:"_t"` } type Result struct { - table *Table - queryChunks *sqlutil.QueryChunks - // This is the main query cursor. It starts as a nil value. - cursor *sql.Rows + 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(). @@ -47,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 } @@ -86,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 } @@ -170,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 } @@ -188,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 } @@ -226,26 +222,25 @@ func (self *Result) Close() error { return err } -// Counts matching elements. +// Counting the elements that will be returned. 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.table.T.FetchRow(&dst, rows) + defer rows.Close() - rows.Close() + dst := counter_t{} + self.table.T.FetchRow(&dst, rows) return dst.Total, nil } diff --git a/sqlite/tx.go b/sqlite/tx.go new file mode 100644 index 0000000000000000000000000000000000000000..75faadf00524d09cfecd1940b630cced0cba35e8 --- /dev/null +++ b/sqlite/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 sqlite + +type Tx struct { + *Source +} + +func (self *Tx) Commit() error { + return self.Source.tx.Commit() +} + +func (self *Tx) Rollback() error { + return self.Source.tx.Rollback() +}