diff --git a/ql/_dumps/Makefile b/ql/_dumps/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..cba56b0150415fb40b311ce885b8d59a497ff8b8 --- /dev/null +++ b/ql/_dumps/Makefile @@ -0,0 +1,3 @@ +all: + rm -f test.db + cat structs.sql | ql -db test.db diff --git a/ql/_dumps/structs.sql b/ql/_dumps/structs.sql new file mode 100644 index 0000000000000000000000000000000000000000..1d4da9ef191d5f3802f63251ec356b1524db0d1e --- /dev/null +++ b/ql/_dumps/structs.sql @@ -0,0 +1,32 @@ +BEGIN TRANSACTION; + +DROP TABLE IF EXISTS artist; + +CREATE TABLE artist ( + id int, + name string +); + +DROP TABLE IF EXISTS data_types; + +CREATE TABLE data_types ( + id int, + _uint int, + _uint8 int, + _uint16 int, + _uint32 int, + _uint64 int, + _int int, + _int8 int, + _int16 int, + _int32 int, + _int64 int, + _float32 float32, + _float64 float64, + _bool bool, + _string string, + _date time, + _time time +); + +COMMIT; diff --git a/ql/collection.go b/ql/collection.go new file mode 100644 index 0000000000000000000000000000000000000000..c22157c4f2a66ccb0a492b727a0ed6e944f32632 --- /dev/null +++ b/ql/collection.go @@ -0,0 +1,246 @@ +/* + 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. +*/ + +package ql + +import ( + "fmt" + "menteslibres.net/gosexy/to" + "strings" + "time" + "upper.io/db" + "upper.io/db/util/sqlutil" +) + +// Represents a QL table. +type Table struct { + source *Source + sqlutil.T +} + +func (self *Table) Find(terms ...interface{}) db.Result { + + queryChunks := sqlutil.NewQueryChunks() + + // No specific fields given. + if len(queryChunks.Fields) == 0 { + queryChunks.Fields = []string{`*`} + } + + // Compiling conditions + queryChunks.Conditions, queryChunks.Arguments = self.compileConditions(terms) + + if queryChunks.Conditions == "" { + queryChunks.Conditions = `1 = 1` + } + + // Creating a result handler. + result := &Result{ + self, + queryChunks, + nil, + } + + return result +} + +// Transforms conditions into arguments for sql.Exec/sql.Query +func (self *Table) compileConditions(term interface{}) (string, []string) { + sql := []string{} + args := []string{} + + 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...) + } + } + if len(sql) > 0 { + return `(` + strings.Join(sql, ` AND `) + `)`, args + } + case db.Cond: + return self.compileStatement(t) + } + + return "", args +} + +func (self *Table) compileStatement(where db.Cond) (string, []string) { + + str := make([]string, len(where)) + arg := make([]string, len(where)) + + i := 0 + + for key, _ := range where { + key = strings.Trim(key, ` `) + chunks := strings.SplitN(key, ` `, 2) + + op := `=` + + if len(chunks) > 1 { + op = chunks[1] + } + + str[i] = fmt.Sprintf(`%s %s ?`, chunks[0], op) + arg[i] = toInternal(where[key]) + + i++ + } + + switch len(str) { + case 1: + return str[0], arg + case 0: + return "", []string{} + } + + return `(` + strings.Join(str, ` AND `) + `)`, arg +} + +// Deletes all the rows within the collection. +func (self *Table) Truncate() error { + + _, err := self.source.doExec( + fmt.Sprintf(`TRUNCATE TABLE %s`, self.Name()), + ) + + return err +} + +// Appends an item (map or struct) into the collection. +func (self *Table) Append(item interface{}) (interface{}, error) { + + fields, values, err := self.FieldValues(item, toInternal) + + // Error ocurred, stop appending. + if err != nil { + return nil, err + } + + tx, err := self.source.session.Begin() + + if err != nil { + return nil, err + } + + res, err := self.source.doExec( + fmt.Sprintf(`INSERT INTO %s`, self.Name()), + sqlFields(fields), + `VALUES`, + sqlValues(values), + ) + + if err != nil { + return nil, err + } + + err = tx.Commit() + + if err != nil { + return nil, err + } + + var id int64 + + id, err = res.LastInsertId() + + if err != nil { + return nil, err + } + + return id, nil +} + +// Returns true if the collection exists. +func (self *Table) Exists() bool { + rows, err := self.source.session.Query(` + SELECT Name + FROM __Table + WHERE Name == ?`, + self.Name(), + ) + + if err != nil { + return false + } + + defer rows.Close() + + return rows.Next() +} + +func toInternalInterface(val interface{}) interface{} { + return toInternal(val) +} + +// Converts a Go value into internal database representation. +func toInternal(val interface{}) string { + + switch t := val.(type) { + case []byte: + return string(t) + case time.Time: + return t.Format(DateFormat) + case time.Duration: + return fmt.Sprintf(TimeFormat, int(t/time.Hour), int(t/time.Minute%60), int(t/time.Second%60), t%time.Second/time.Millisecond) + case bool: + if t == true { + return `1` + } else { + return `0` + } + } + + return to.String(val) +} + +// Convers a database representation (after auto-conversion) into a Go value. +func toNative(val interface{}) interface{} { + return val +} diff --git a/ql/database.go b/ql/database.go new file mode 100644 index 0000000000000000000000000000000000000000..e5fa42c87a63b8561e954cce2345d9e88fe96f39 --- /dev/null +++ b/ql/database.go @@ -0,0 +1,346 @@ +/* + 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. +*/ + +package ql + +import ( + "database/sql" + "fmt" + _ "github.com/cznic/ql/driver" + "reflect" + "regexp" + "strings" + "upper.io/db" +) + +var Debug = true + +// Format for saving dates. +var DateFormat = "2006-01-02 15:04:05" + +// Format for saving times. +var TimeFormat = "%d:%02d:%02d.%d" + +var columnPattern = regexp.MustCompile(`^([a-z]+)\(?([0-9,]+)?\)?\s?([a-z]*)?`) + +const driverName = `ql` + +func init() { + db.Register(driverName, &Source{}) +} + +type sqlValues_t []string + +type Source struct { + config db.Settings + session *sql.DB + name string + collections map[string]db.Collection +} + +type sqlQuery struct { + Query []string + Args []interface{} +} + +func sqlCompile(terms []interface{}) *sqlQuery { + q := &sqlQuery{} + + q.Query = []string{} + + for _, term := range terms { + switch t := term.(type) { + case string: + q.Query = append(q.Query, t) + case []string: + for _, arg := range t { + q.Args = append(q.Args, arg) + } + 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, `, `)+`)`) + } + } + + return q +} + +func sqlFields(names []string) string { + return `(` + strings.Join(names, `, `) + `)` +} + +func sqlValues(values []string) sqlValues_t { + ret := make(sqlValues_t, len(values)) + for i, _ := range values { + ret[i] = values[i] + } + return ret +} + +func (self *Source) doExec(terms ...interface{}) (sql.Result, error) { + if self.session == nil { + return nil, db.ErrNotConnected + } + + chunks := sqlCompile(terms) + + query := strings.Join(chunks.Query, ` `) + + for i := 0; i < len(chunks.Args); 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) + } + + return self.session.Exec(query, chunks.Args...) +} + +func (self *Source) doQuery(terms ...interface{}) (*sql.Rows, error) { + if self.session == nil { + return nil, db.ErrNotConnected + } + + chunks := sqlCompile(terms) + + query := strings.Join(chunks.Query, ` `) + + for i := 0; i < len(chunks.Args); 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) + } + + return self.session.Query(query, chunks.Args...) +} + +func (self *Source) doQueryRow(terms ...interface{}) (*sql.Row, error) { + if self.session == nil { + return nil, db.ErrNotConnected + } + + chunks := sqlCompile(terms) + + query := strings.Join(chunks.Query, ` `) + + for i := 0; i < len(chunks.Args); 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) + } + + return self.session.QueryRow(query, chunks.Args...), nil +} + +// Returns the string name of the database. +func (self *Source) Name() string { + return self.config.Database +} + +// Stores database settings. +func (self *Source) Setup(config db.Settings) error { + self.config = config + self.collections = make(map[string]db.Collection) + return self.Open() +} + +// Returns the underlying *sql.DB instance. +func (self *Source) Driver() interface{} { + return self.session +} + +// Attempts to connect to a database using the stored settings. +func (self *Source) Open() error { + var err error + + if self.config.Database == "" { + return db.ErrMissingDatabaseName + } + + self.session, err = sql.Open(`ql`, self.config.Database) + + if err != nil { + return err + } + + return nil +} + +// Closes the current database session. +func (self *Source) Close() error { + if self.session != nil { + return self.session.Close() + } + return nil +} + +// Changes the active database. +func (self *Source) Use(database string) error { + self.config.Database = database + 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 +} + +// Returns a list of all tables within the currently active database. +func (self *Source) Collections() ([]string, error) { + var collections []string + var collection string + + rows, err := self.session.Query(`SELECT Name FROM __Table`) + + if err != nil { + return nil, err + } + + defer rows.Close() + + for rows.Next() { + if err = rows.Scan(&collection); err != nil { + return nil, err + } + collections = append(collections, collection) + } + + err = rows.Err() + + if err != nil { + return nil, err + } + + return collections, nil +} + +// Returns a collection instance by name. +func (self *Source) Collection(name string) (db.Collection, error) { + + if collection, ok := self.collections[name]; ok == true { + return collection, nil + } + + table := &Table{} + + table.source = self + table.DB = self + table.PrimaryKey = `id` + + table.SetName = name + + // Table exists? + if table.Exists() == false { + return table, db.ErrCollectionDoesNotExists + } + + // Fetching table datatypes and mapping to internal gotypes. + rows, err := table.source.doQuery( + `SELECT + Name, Type + FROM __Column + WHERE + TableName = ?`, + []string{table.Name()}, + ) + + if err != nil { + return table, err + } + + columns := []struct { + ColumnName string + DataType string + }{} + + err = table.FetchRows(&columns, rows) + + if err != nil { + return nil, err + } + + table.ColumnTypes = make(map[string]reflect.Kind, len(columns)) + + for _, column := range columns { + + column.ColumnName = strings.ToLower(column.ColumnName) + column.DataType = strings.ToLower(column.DataType) + + results := columnPattern.FindStringSubmatch(column.DataType) + + // Default properties. + dextra := "" + dtype := `varchar` + + dtype = results[1] + + if len(results) > 3 { + dextra = results[3] + } + + ctype := reflect.String + + // Guessing datatypes. + switch dtype { + case `smallint`, `integer`, `bigint`, `serial`, `bigserial`: + if dextra == `unsigned` { + ctype = reflect.Uint64 + } else { + ctype = reflect.Int64 + } + case `real`, `double`: + ctype = reflect.Float64 + } + + table.ColumnTypes[column.ColumnName] = ctype + } + + self.collections[name] = table + + return table, nil +} diff --git a/ql/database_test.go b/ql/database_test.go new file mode 100644 index 0000000000000000000000000000000000000000..74d90d5ec6612d2bd54285a35a6207cf2ffa3db1 --- /dev/null +++ b/ql/database_test.go @@ -0,0 +1,702 @@ +/* + 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. + + cd _dumps + make + cd .. + go test +*/ +package ql + +import ( + "database/sql" + "fmt" + "menteslibres.net/gosexy/to" + "reflect" + "strings" + "testing" + "time" + "upper.io/db" +) + +// Wrapper. +const wrapperName = "ql" + +// Wrapper settings. +const dbname = "file://_dumps/test.db" + +// Global settings for tests. +var settings = db.Settings{ + Database: dbname, +} + +// Structure for testing conversions and datatypes. +type testValuesStruct struct { + Uint uint `field:"_uint"` + Uint8 uint8 `field:"_uint8"` + Uint16 uint16 `field:"_uint16"` + Uint32 uint32 `field:"_uint32"` + Uint64 uint64 `field:"_uint64"` + + Int int `field:"_int"` + Int8 int8 `field:"_int8"` + Int16 int16 `field:"_int16"` + Int32 int32 `field:"_int32"` + Int64 int64 `field:"_int64"` + + Float32 float32 `field:"_float32"` + Float64 float64 `field:"_float64"` + + Bool bool `field:"_bool"` + String string `field:"_string"` + + Date time.Time `field:"_date"` + Time time.Duration `field:"_time"` +} + +// Declaring some values to insert, we expect the same values to be returned. +var testValues = testValuesStruct{ + 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.Second * time.Duration(7331), +} + +// Enabling outputting some information to stdout (like the SQL query and its +// arguments), useful for development. +func TestEnableDebug(t *testing.T) { + Debug = true +} + +// Trying to open an empty datasource, it must fail. +func TestOpenFailed(t *testing.T) { + _, err := db.Open(wrapperName, db.Settings{}) + + if err == nil { + t.Errorf("Expecting an error.") + } +} + +// Truncates all collections. +func TestTruncate(t *testing.T) { + + var err error + + // Opening database. + sess, err := db.Open(wrapperName, settings) + + if err != nil { + t.Fatalf(err.Error()) + } + + // We should close the database when it's no longer in use. + defer sess.Close() + + // Getting a list of all collections in this database. + collections, err := sess.Collections() + fmt.Printf("%v\n", collections) + + if err != nil { + t.Fatalf(err.Error()) + } + + for _, name := range collections { + + // Pointing the collection. + col, err := sess.Collection(name) + if 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 { + t.Fatalf(err.Error()) + } + } + + } +} + +// This test appends some data into the "artist" table. +func TestAppend(t *testing.T) { + + var err error + var id interface{} + + // Opening database. + sess, err := db.Open(wrapperName, settings) + + if 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 { + t.Fatalf(err.Error()) + } + + // Appending a map. + id, err = artist.Append(map[string]string{ + "name": "Ozzie", + }) + + if to.Int64(id) == 0 { + t.Fatalf("Expecting an ID.") + } + + // Appending a struct. + id, err = artist.Append(struct { + Name string `field:name` + }{ + "Flea", + }) + + 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"` + }{ + "Slash", + }) + + if to.Int64(id) == 0 { + t.Fatalf("Expecting an ID.") + } + +} + +// This test tries to use an empty filter and count how many elements were +// added into the artist collection. +func TestResultCount(t *testing.T) { + + var err error + var res db.Result + + // Opening database. + sess, err := db.Open(wrapperName, settings) + + if 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") + + res = artist.Find() + + // Counting all the matching rows. + total, err := res.Count() + + if err != nil { + t.Fatalf(err.Error()) + } + + if total == 0 { + t.Fatalf("Should not be empty, we've just added some rows!") + } + +} + +// This test uses and result and tries to fetch items one by one. +func TestResultFetch(t *testing.T) { + + var err error + var res db.Result + + // Opening database. + sess, err := db.Open(wrapperName, settings) + + if 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 { + t.Fatalf(err.Error()) + } + + // Testing map + res = artist.Find() + + row_m := map[string]interface{}{} + + for { + err = res.Next(&row_m) + + if err == db.ErrNoMoreRows { + // No more row_ms left. + break + } + + if err == nil { + if to.Int64(row_m["id"]) == 0 { + t.Fatalf("Expecting a not null ID.") + } + if to.String(row_m["name"]) == "" { + t.Fatalf("Expecting a name.") + } + } else { + t.Fatalf(err.Error()) + } + } + + res.Close() + + // Testing struct + row_s := struct { + Id uint64 + Name string + }{} + + res = artist.Find() + + for { + err = res.Next(&row_s) + + if err == db.ErrNoMoreRows { + // No more row_s' left. + break + } + + if err == nil { + if row_s.Id == 0 { + t.Fatalf("Expecting a not null ID.") + } + if row_s.Name == "" { + t.Fatalf("Expecting a name.") + } + } else { + t.Fatalf(err.Error()) + } + } + + res.Close() + + // Testing tagged struct + row_t := struct { + Value1 uint64 `field:"id"` + Value2 string `field:"name"` + }{} + + res = artist.Find() + + for { + err = res.Next(&row_t) + + if err == db.ErrNoMoreRows { + // No more row_t's left. + break + } + + if err == nil { + if row_t.Value1 == 0 { + t.Fatalf("Expecting a not null ID.") + } + if row_t.Value2 == "" { + t.Fatalf("Expecting a name.") + } + } else { + t.Fatalf(err.Error()) + } + } + + res.Close() + + // Testing Result.All() with a slice of maps. + res = artist.Find() + + all_rows_m := []map[string]interface{}{} + err = res.All(&all_rows_m) + + if err != nil { + t.Fatalf(err.Error()) + } + + 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() + + all_rows_s := []struct { + Id uint64 + Name string + }{} + err = res.All(&all_rows_s) + + if err != nil { + t.Fatalf(err.Error()) + } + + 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() + + all_rows_t := []struct { + Value1 uint64 `field:"id"` + Value2 string `field:"name"` + }{} + err = res.All(&all_rows_t) + + if err != nil { + t.Fatalf(err.Error()) + } + + for _, single_row_t := range all_rows_t { + if single_row_t.Value1 == 0 { + t.Fatalf("Expecting a not null ID.") + } + } +} + +// This test tries to update some previously added rows. +func TestUpdate(t *testing.T) { + var err error + + // Opening database. + sess, err := db.Open(wrapperName, settings) + + if 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 { + t.Fatalf(err.Error()) + } + + // Value + value := struct { + Id uint64 + Name string + }{} + + // Getting the first artist. + res := artist.Find(db.Cond{"id !=": 0}).Limit(1) + + err = res.One(&value) + + if err != nil { + t.Fatalf(err.Error()) + } + + // Updating with a map + row_m := map[string]interface{}{ + "name": strings.ToUpper(value.Name), + } + + err = res.Update(row_m) + + if err != nil { + t.Fatalf(err.Error()) + } + + err = res.One(&value) + + if err != nil { + t.Fatalf(err.Error()) + } + + if value.Name != row_m["name"] { + t.Fatalf("Expecting a modification.") + } + + // Updating with a struct + row_s := struct { + Name string + }{strings.ToLower(value.Name)} + + err = res.Update(row_s) + + if err != nil { + t.Fatalf(err.Error()) + } + + err = res.One(&value) + + if err != nil { + t.Fatalf(err.Error()) + } + + if value.Name != row_s.Name { + t.Fatalf("Expecting a modification.") + } + + // Updating with a tagged struct + row_t := struct { + Value1 string `field:"name"` + }{strings.Replace(value.Name, "z", "Z", -1)} + + err = res.Update(row_t) + + if err != nil { + t.Fatalf(err.Error()) + } + + err = res.One(&value) + + if err != nil { + t.Fatalf(err.Error()) + } + + if value.Name != row_t.Value1 { + t.Fatalf("Expecting a modification.") + } + +} + +// This test tries to remove some previously added rows. +func TestRemove(t *testing.T) { + + var err error + + // Opening database. + sess, err := db.Open(wrapperName, settings) + + if 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 { + t.Fatalf(err.Error()) + } + + // Getting the artist with id = 1 + res := artist.Find(db.Cond{"id": 1}) + + // Trying to remove the row. + err = res.Remove() + + if err != nil { + t.Fatalf(err.Error()) + } +} + +// 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. +func TestDataTypes(t *testing.T) { + var res db.Result + + // Opening database. + sess, err := db.Open(wrapperName, settings) + + if 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() + + // Appending our test subject. + id, err := dataTypes.Append(testValues) + + if err != nil { + t.Fatalf(err.Error()) + } + + // Trying to get the same subject we added. + res = dataTypes.Find(db.Cond{"id": id}) + + exists, err := res.Count() + + if err != nil { + t.Fatalf(err.Error()) + } + + if exists == 0 { + t.Errorf("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.") + } +} + +// We are going to benchmark the engine, so this is no longed needed. +func TestDisableDebug(t *testing.T) { + Debug = false +} + +// Benchmarking raw database/sql. +func BenchmarkAppendRaw(b *testing.B) { + sess, err := db.Open(wrapperName, settings) + + if err != nil { + b.Fatalf(err.Error()) + } + + defer sess.Close() + + artist, err := sess.Collection("artist") + artist.Truncate() + + driver := sess.Driver().(*sql.DB) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := driver.Exec(`INSERT INTO artist (name) VALUES('Hayao Miyazaki')`) + if 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) + + if err != nil { + b.Fatalf(err.Error()) + } + + defer sess.Close() + + artist, err := sess.Collection("artist") + artist.Truncate() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err = artist.Append(map[string]string{"name": "Leonardo DaVinci"}) + if 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) + + if err != nil { + b.Fatalf(err.Error()) + } + + defer sess.Close() + + artist, err := sess.Collection("artist") + artist.Truncate() + + err = sess.Begin() + if err != nil { + b.Fatalf(err.Error()) + } + + for i := 0; i < b.N; i++ { + _, err = artist.Append(map[string]string{"name": "Isaac Asimov"}) + if err != nil { + b.Fatalf(err.Error()) + } + } + + err = sess.End() + if err != nil { + b.Fatalf(err.Error()) + } +} + +// Benchmarking Append with a struct. +func BenchmarkAppendStruct(b *testing.B) { + sess, err := db.Open(wrapperName, settings) + + if err != nil { + b.Fatalf(err.Error()) + } + + defer sess.Close() + + artist, err := sess.Collection("artist") + artist.Truncate() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err = artist.Append(struct{ Name string }{"John Lennon"}) + if err != nil { + b.Fatalf(err.Error()) + } + } +} diff --git a/ql/result.go b/ql/result.go new file mode 100644 index 0000000000000000000000000000000000000000..cb8c3dfb41769c4981ebefd02182b450173a3b8f --- /dev/null +++ b/ql/result.go @@ -0,0 +1,251 @@ +/* + 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. +*/ + +package ql + +import ( + "database/sql" + "fmt" + "strings" + "upper.io/db" + "upper.io/db/util/sqlutil" +) + +type counter struct { + Total uint64 `field:"total"` +} + +type Result struct { + table *Table + queryChunks *sqlutil.QueryChunks + // This is the main query cursor. It starts as a nil value. + cursor *sql.Rows +} + +// Executes a SELECT statement that can feed Next(), All() or One(). +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, + ) + } + 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) + 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) + return self +} + +// Determines sorting of results according to the provided names. Fields may be +// 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)) + + for _, field := range fields { + if strings.HasPrefix(field, `-`) == true { + sort = append(sort, field[1:]+` DESC`) + } else { + sort = append(sort, field+` ASC`) + } + } + + self.queryChunks.Sort = `ORDER BY ` + strings.Join(sort, `, `) + + return self +} + +// Retrieves only the given fields. +func (self *Result) Select(fields ...string) db.Result { + self.queryChunks.Fields = fields + return self +} + +// Dumps all results into a pointer to an slice of structs or maps. +func (self *Result) All(dst interface{}) error { + var err error + + if self.cursor != nil { + return db.ErrQueryIsPending + } + + // Current cursor. + err = self.setCursor() + + if err != nil { + return err + } + + defer self.Close() + + // Fetching all results within the cursor. + err = self.table.T.FetchRows(dst, self.cursor) + + return err +} + +// Fetches only one result from the resultset. +func (self *Result) One(dst interface{}) error { + var err error + + if self.cursor != nil { + return db.ErrQueryIsPending + } + + defer self.Close() + + err = self.Next(dst) + + return err +} + +// Fetches the next result from the resultset. +func (self *Result) Next(dst interface{}) error { + + var err error + + // Current cursor. + err = self.setCursor() + + if err != nil { + self.Close() + } + + // Fetching the next result from the cursor. + err = self.table.T.FetchRow(dst, self.cursor) + + if err != nil { + self.Close() + } + + 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, + ) + return err + +} + +// Updates matching items from the collection with values of the given map or +// struct. +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([]string, total) + + for i := 0; i < total; i++ { + updateFields[i] = fmt.Sprintf(`%s = ?`, ff[i]) + updateArgs[i] = vv[i] + } + + _, 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, + ) + + return err +} + +// Closes the result set. +func (self *Result) Close() error { + var err error + if self.cursor != nil { + err = self.cursor.Close() + self.cursor = nil + } + return err +} + +// 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, + ) + + if err != nil { + return 0, err + } + + dst := counter{} + self.table.T.FetchRow(&dst, rows) + + rows.Close() + + return dst.Total, nil +}