├── LICENSE ├── README.md ├── adapters ├── database.go ├── database_mysql.go ├── database_psql.go └── database_sqlite.go ├── database.go ├── query.go ├── query_test.go ├── tests ├── query_test.sqlite ├── query_test_mysql.sql ├── query_test_pq.sql └── query_test_sqlite.sql └── textual.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mechanism Design Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Query [![GoDoc](https://godoc.org/github.com/fragmenta/query?status.svg)](https://godoc.org/github.com/fragmenta/query) [![Go Report Card](https://goreportcard.com/badge/github.com/fragmenta/query)](https://goreportcard.com/report/github.com/fragmenta/query) 2 | ===== 3 | 4 | 5 | 6 | Query lets you build SQL queries with chainable methods, and defer execution of SQL until you wish to extract a count or array of models. It will probably remain limited in scope - it is not intended to be a full ORM with strict mapping between db tables and structs, but a tool for querying the database with minimum friction, and performing CRUD operations linked to models; simplifying your use of SQL to store model data without getting in the way. Full or partial SQL queries are of course also available, and full control over sql. Model creation and column are delegated to the model, to avoid dictating any particular model structure or interface, however a suggested interface is given (see below and in tests), which makes usage painless in your handlers without any boilerplate. 7 | 8 | Supported databases: PostgreSQL, SQLite, MySQL. Bug fixes, suggestions and contributions welcome. 9 | 10 | Usage 11 | ===== 12 | 13 | 14 | ```go 15 | 16 | // In your app - open a database with options 17 | options := map[string]string{"adapter":"postgres","db":"query_test"} 18 | err := query.OpenDatabase(options) 19 | defer query.CloseDatabase() 20 | 21 | ... 22 | 23 | // In your model 24 | type Page struct { 25 | ID int64 26 | CreatedAt time.Time 27 | UpdatedAt time.Time 28 | MyField myStruct 29 | ... 30 | // Models can have any structure, any PK, here an int is used 31 | } 32 | 33 | // Normally you'd define helpers on your model class to load rows from the database 34 | // Query does not attempt to read data into columns with reflection or tags - 35 | // that is left to your model so you can read as little or as much as you want from queries 36 | 37 | func Find(ID int64) (*Page, error) { 38 | result, err := PagesQuery().Where("id=?", ID).FirstResult() 39 | if err != nil { 40 | return nil, err 41 | } 42 | return NewWithColumns(result), nil 43 | } 44 | 45 | func FindAll(q *Query) ([]*Page, error) { 46 | results, err := q.Results() 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | var models []*Page 52 | for _, r := range results { 53 | m := NewWithColumns(r) 54 | models = append(models, m) 55 | } 56 | 57 | return models, nil 58 | } 59 | 60 | ... 61 | 62 | // In your handlers, construct queries and ask your models for the data 63 | 64 | // Find a simple model by id 65 | page, err := pages.Find(1) 66 | 67 | // Start querying the database using chained finders 68 | q := page.Query().Where("id IN (?,?)",4,5).Order("id desc").Limit(30) 69 | 70 | // Build up chains depending on other app logic, still no db requests 71 | if shouldRestrict { 72 | q.Where("id > ?",3).OrWhere("keywords ~* ?","Page") 73 | } 74 | 75 | // Pass the relation around, until you are ready to retrieve models from the db 76 | results, err := pages.FindAll(q) 77 | ``` 78 | 79 | What it does 80 | ============ 81 | 82 | * Builds chainable queries including where, orwhere,group,having,order,limit,offset or plain sql 83 | * Allows any Primary Key/Table name or model fields (query.New lets you define this) 84 | * Allows Delete and Update operations on queried records, without creating objects 85 | * Defers SQL requests until full query is built and results requested 86 | * Provide helpers and return results for join ids, counts, single rows, or multiple rows 87 | 88 | 89 | What it doesn't do 90 | ================== 91 | 92 | * Attempt to read your models with reflection or struct tags 93 | * Require changes to your structs like tagging fields or specific fields 94 | * Cause problems with untagged fields, embedding, and fields not in the database 95 | * Provide hooks after/before update etc - your models are fully in charge of queries and their lifecycle 96 | 97 | 98 | 99 | Tests 100 | ================== 101 | 102 | All 3 databases supported have a test suite - to run the tests, create a database called query_test in mysql and psql then run go test at the root of the package. The sqlite tests are disabled by default because enabling them prohibits cross compilation, which is useful if you don't want to install go on your server but just upload a binary compiled locally. 103 | 104 | ```bash 105 | go test 106 | ``` 107 | 108 | 109 | 110 | Versions 111 | ================== 112 | 113 | - 1.0 - First version with interfaces and chainable finders 114 | - 1.0.1 - Updated to quote table names and fields, for use of reserved words, bug fix for mysql concurrency 115 | - 1.3 - updated API, now shifted instantiation to models instead, to avoid use of reflection 116 | - 1.3.1 - Fixed bugs in Mysql import, updated tests 117 | -------------------------------------------------------------------------------- /adapters/database.go: -------------------------------------------------------------------------------- 1 | // Package adapters offers adapters for pouplar databases 2 | package adapters 3 | 4 | import ( 5 | "database/sql" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | // Database provides an interface for database adapters to conform to 11 | type Database interface { 12 | 13 | // Open and close 14 | Open(opts map[string]string) error 15 | Close() error 16 | SQLDB() *sql.DB 17 | 18 | // Execute queries with or without returned rows 19 | Exec(query string, args ...interface{}) (sql.Result, error) 20 | Query(query string, args ...interface{}) (*sql.Rows, error) 21 | 22 | // Insert a record, returning id 23 | Insert(sql string, args ...interface{}) (id int64, err error) 24 | 25 | // Return extra SQL for insert statement (see psql) 26 | InsertSQL(pk string) string 27 | 28 | // A format string for the arg placeholder 29 | Placeholder(i int) string 30 | 31 | // Quote Table and Column names 32 | QuoteField(name string) string 33 | 34 | // Convert a time to a string 35 | TimeString(t time.Time) string 36 | 37 | // Convert a string to a time 38 | ParseTime(s string) (time.Time, error) 39 | } 40 | 41 | // Adapter is a struct defining a few functions used by all adapters 42 | type Adapter struct { 43 | queries map[string]interface{} 44 | } 45 | 46 | // ReplaceArgPlaceholder does no replacements by default, and use default ? placeholder for args 47 | // psql requires a different placeholder numericall labelled 48 | func (db *Adapter) ReplaceArgPlaceholder(sql string, args []interface{}) string { 49 | return sql 50 | } 51 | 52 | // Placeholder is the argument placeholder for this adapter 53 | func (db *Adapter) Placeholder(i int) string { 54 | return "?" 55 | } 56 | 57 | // TimeString - given a time, return the standard string representation 58 | func (db *Adapter) TimeString(t time.Time) string { 59 | return t.Format("2006-01-02 15:04:05.000 -0700") 60 | } 61 | 62 | // ParseTime - given a string, return a time object built from it 63 | func (db *Adapter) ParseTime(s string) (time.Time, error) { 64 | 65 | // Deal with broken mysql dates - deal better with this? 66 | if s == "0000-00-00 00:00:00" { 67 | return time.Now(), nil 68 | } 69 | 70 | // Try to choose the right format for date string 71 | format := "2006-01-02 15:04:05" 72 | if len(s) > len(format) { 73 | format = "2006-01-02 15:04:05.000" 74 | } 75 | if len(s) > len(format) { 76 | format = "2006-01-02 15:04:05.000 -0700" 77 | } 78 | 79 | t, err := time.Parse(format, s) 80 | if err != nil { 81 | fmt.Println("Unhandled field type:", s, "\n", err) 82 | } 83 | 84 | return t, err 85 | } 86 | 87 | // QuoteField quotes a table name or column name 88 | func (db *Adapter) QuoteField(name string) string { 89 | return fmt.Sprintf(`"%s"`, name) 90 | } 91 | 92 | // InsertSQL provides extra SQL for end of insert statement (RETURNING for psql) 93 | func (db *Adapter) InsertSQL(pk string) string { 94 | return "" 95 | } 96 | 97 | // performQuery executes Query SQL on the given sqlDB and return the rows. 98 | // NB caller must call use defer rows.Close() with rows returned 99 | func (db *Adapter) performQuery(sqlDB *sql.DB, debug bool, query string, args ...interface{}) (*sql.Rows, error) { 100 | 101 | if sqlDB == nil { 102 | return nil, fmt.Errorf("No database available.") 103 | } 104 | 105 | if debug { 106 | fmt.Println("QUERY:", query, "ARGS", args) 107 | } 108 | 109 | // This should be cached, perhaps hold a map in memory of queries strings and compiled queries? 110 | // use queries map to store this 111 | stmt, err := sqlDB.Prepare(query) 112 | if err != nil { 113 | return nil, err 114 | } 115 | defer stmt.Close() 116 | 117 | rows, err := stmt.Query(args...) 118 | 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | // Caller is responsible for closing rows with defer rows.Close() 124 | return rows, err 125 | } 126 | 127 | // performExec executes Query SQL on the given sqlDB with no rows returned, just result 128 | func (db *Adapter) performExec(sqlDB *sql.DB, debug bool, query string, args ...interface{}) (sql.Result, error) { 129 | 130 | if sqlDB == nil { 131 | return nil, fmt.Errorf("No database available.") 132 | } 133 | 134 | if debug { 135 | fmt.Println("QUERY:", query, "ARGS", args) 136 | } 137 | 138 | stmt, err := sqlDB.Prepare(query) 139 | if err != nil { 140 | return nil, err 141 | } 142 | defer stmt.Close() 143 | 144 | result, err := stmt.Exec(args...) 145 | 146 | if err != nil { 147 | return result, err 148 | } 149 | 150 | // Caller is responsible for closing rows with defer rows.Close() 151 | return result, err 152 | } 153 | -------------------------------------------------------------------------------- /adapters/database_mysql.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | // Mysql driver 8 | _ "github.com/go-sql-driver/mysql" 9 | ) 10 | 11 | // MysqlAdapter conforms to the query.Database interface 12 | type MysqlAdapter struct { 13 | *Adapter 14 | options map[string]string 15 | sqlDB *sql.DB 16 | debug bool 17 | } 18 | 19 | // Open this database 20 | func (db *MysqlAdapter) Open(opts map[string]string) error { 21 | 22 | db.debug = false 23 | db.options = map[string]string{ 24 | "adapter": "mysql", 25 | "user": "root", // sub your user 26 | "password": "", 27 | "db": "query_test", 28 | "protocol": "tcp", 29 | "host": "localhost", 30 | "port": "3306", 31 | "params": "charset=utf8&parseTime=true", 32 | } 33 | 34 | if opts["debug"] == "true" { 35 | db.debug = true 36 | } 37 | 38 | // Merge options 39 | for k, v := range opts { 40 | db.options[k] = v 41 | } 42 | 43 | // A typical connection string is of the form: 44 | //"user:password@tcp(localhost:3306)/dbname?charset=utf8&parseTime=true") 45 | options := fmt.Sprintf("%s:%s@%s(%s:%s)/%s?%s", 46 | db.options["user"], 47 | db.options["password"], 48 | db.options["protocol"], 49 | db.options["host"], 50 | db.options["port"], 51 | db.options["db"], 52 | db.options["params"]) 53 | 54 | var err error 55 | db.sqlDB, err = sql.Open(db.options["adapter"], options) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | if db.sqlDB == nil { 61 | fmt.Printf("Mysql options:%s", options) 62 | return fmt.Errorf("\nError creating database with options: %v", db.options) 63 | } 64 | 65 | // Call ping on the db to check it does actually exist! 66 | err = db.sqlDB.Ping() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return err 72 | 73 | } 74 | 75 | // Close the database 76 | func (db *MysqlAdapter) Close() error { 77 | if db.sqlDB != nil { 78 | return db.sqlDB.Close() 79 | } 80 | return nil 81 | } 82 | 83 | // SQLDB returns the internal db.sqlDB pointer 84 | func (db *MysqlAdapter) SQLDB() *sql.DB { 85 | return db.sqlDB 86 | } 87 | 88 | // Query SQL execute - NB caller must call use defer rows.Close() with rows returned 89 | func (db *MysqlAdapter) Query(query string, args ...interface{}) (*sql.Rows, error) { 90 | return db.performQuery(db.sqlDB, db.debug, query, args...) 91 | } 92 | 93 | // Exec - use this for non-select statements 94 | func (db *MysqlAdapter) Exec(query string, args ...interface{}) (sql.Result, error) { 95 | return db.performExec(db.sqlDB, db.debug, query, args...) 96 | } 97 | 98 | // QuoteField quotes a table name or column name 99 | func (db *MysqlAdapter) QuoteField(name string) string { 100 | return fmt.Sprintf("`%s`", name) 101 | } 102 | 103 | // Insert a record with params and return the id - psql behaves differently 104 | func (db *MysqlAdapter) Insert(query string, args ...interface{}) (id int64, err error) { 105 | 106 | tx, err := db.sqlDB.Begin() 107 | if err != nil { 108 | return 0, err 109 | } 110 | 111 | // Execute the sql using db 112 | result, err := db.Exec(query, args...) 113 | if err != nil { 114 | return 0, err 115 | } 116 | 117 | // TODO - check this works on mysql under load with concurrent connections 118 | // fine if connection not shared 119 | id, err = result.LastInsertId() 120 | if err != nil { 121 | return 0, err 122 | } 123 | 124 | err = tx.Commit() 125 | if err != nil { 126 | return 0, err 127 | } 128 | 129 | return id, nil 130 | 131 | } 132 | -------------------------------------------------------------------------------- /adapters/database_psql.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | // psql driver 8 | _ "github.com/lib/pq" 9 | ) 10 | 11 | // PostgresqlAdapter conforms to the query.Database interface 12 | type PostgresqlAdapter struct { 13 | *Adapter 14 | options map[string]string 15 | sqlDB *sql.DB 16 | debug bool 17 | } 18 | 19 | // Open this database with the given options 20 | // opts map keys:adapter, user, password, db, host, port, params (give extra parameters in the params option) 21 | // Additional options available are detailed in the pq driver docs at 22 | // https://godoc.org/github.com/lib/pq 23 | func (db *PostgresqlAdapter) Open(opts map[string]string) error { 24 | 25 | db.debug = false 26 | db.options = map[string]string{ 27 | "adapter": "postgres", 28 | "user": "", 29 | "password": "", 30 | "db": "", 31 | "host": "localhost", // for unix instead of tcp use path - see driver 32 | "port": "5432", // default PSQL port 33 | "params": "sslmode=disable connect_timeout=60", // disable sslmode for localhost, set timeout 34 | } 35 | 36 | if opts["debug"] == "true" { 37 | db.debug = true 38 | } 39 | 40 | // Merge options 41 | for k, v := range opts { 42 | db.options[k] = v 43 | } 44 | 45 | // Default to psql database on localhost on port 5432, typical connection string: 46 | // user=server password=p host=localhost port=5432 dbname=db sslmode=disable 47 | // See https://godoc.org/github.com/lib/pq for options, use params to override defaults if required 48 | optionString := fmt.Sprintf("user=%s %s host=%s port=%s dbname=%s %s", 49 | db.options["user"], 50 | paramOrBlank("password", db.options["password"]), 51 | db.options["host"], 52 | db.options["port"], 53 | db.options["db"], 54 | db.options["params"]) 55 | 56 | var err error 57 | db.sqlDB, err = sql.Open(db.options["adapter"], optionString) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | // Call ping on the db to check it does actually exist! 63 | err = db.sqlDB.Ping() 64 | if err != nil { 65 | return err 66 | } 67 | 68 | if db.sqlDB != nil && db.debug { 69 | fmt.Printf("Database %s opened using %s\n", db.options["db"], db.options["adapter"]) 70 | } 71 | 72 | return nil 73 | 74 | } 75 | 76 | func paramOrBlank(k, v string) string { 77 | if len(v) > 0 { 78 | return fmt.Sprintf("%s=%s", k, v) 79 | } 80 | return "" 81 | } 82 | 83 | // Close the database 84 | func (db *PostgresqlAdapter) Close() error { 85 | if db.sqlDB != nil { 86 | return db.sqlDB.Close() 87 | } 88 | return nil 89 | } 90 | 91 | // SQLDB returns the internal db.sqlDB pointer 92 | func (db *PostgresqlAdapter) SQLDB() *sql.DB { 93 | return db.sqlDB 94 | } 95 | 96 | // Query executes query SQL - NB caller must call use defer rows.Close() with rows returned 97 | func (db *PostgresqlAdapter) Query(query string, args ...interface{}) (*sql.Rows, error) { 98 | return db.performQuery(db.sqlDB, db.debug, query, args...) 99 | } 100 | 101 | // Exec - use this for non-select statements 102 | func (db *PostgresqlAdapter) Exec(query string, args ...interface{}) (sql.Result, error) { 103 | return db.performExec(db.sqlDB, db.debug, query, args...) 104 | } 105 | 106 | // Placeholder returns the db placeholder 107 | func (db *PostgresqlAdapter) Placeholder(i int) string { 108 | return fmt.Sprintf("$%d", i) 109 | } 110 | 111 | // InsertSQL is extra SQL for end of insert statement (RETURNING for psql) 112 | func (db *PostgresqlAdapter) InsertSQL(pk string) string { 113 | return fmt.Sprintf("RETURNING %s", pk) 114 | } 115 | 116 | // Insert a record with params and return the id 117 | func (db *PostgresqlAdapter) Insert(sql string, args ...interface{}) (id int64, err error) { 118 | 119 | // TODO - handle different types of id, not just int 120 | // Execute the sql using db and retrieve new row id 121 | row := db.sqlDB.QueryRow(sql, args...) 122 | err = row.Scan(&id) 123 | return id, err 124 | } 125 | -------------------------------------------------------------------------------- /adapters/database_sqlite.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | // FIXME: Sqlite drivers are broken compiling with cgo at present 4 | // therefore we don't use this adapter 5 | 6 | import ( 7 | "database/sql" 8 | "fmt" 9 | // Unfortunately can't cross compile with sqlite support enabled - 10 | // see https://github.com/mattn/go-sqlite3/issues/106 11 | // For now for we just turn off sqlite as we don't use it in production... 12 | // pure go version of sqlite, or ditch sqlite and find some other pure go simple db 13 | // would be nice not to require a db at all for very simple usage 14 | //_ "github.com/mattn/go-sqlite3" 15 | ) 16 | 17 | // SqliteAdapter conforms to the query.Database interface 18 | type SqliteAdapter struct { 19 | *Adapter 20 | options map[string]string 21 | sqlDB *sql.DB 22 | debug bool 23 | } 24 | 25 | // Open this database 26 | func (db *SqliteAdapter) Open(opts map[string]string) error { 27 | 28 | db.debug = false 29 | db.options = map[string]string{ 30 | "adapter": "sqlite3", 31 | "db": "./tests/query_test.sqlite", 32 | } 33 | 34 | if opts["debug"] == "true" { 35 | db.debug = true 36 | } 37 | 38 | for k, v := range opts { 39 | db.options[k] = v 40 | } 41 | 42 | var err error 43 | db.sqlDB, err = sql.Open(db.options["adapter"], db.options["db"]) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | if db.sqlDB != nil && db.debug { 49 | fmt.Printf("Database %s opened using %s\n", db.options["db"], db.options["adapter"]) 50 | } 51 | 52 | // Call ping on the db to check it does actually exist! 53 | err = db.sqlDB.Ping() 54 | if err != nil { 55 | return err 56 | } 57 | 58 | return err 59 | 60 | } 61 | 62 | // Close the database 63 | func (db *SqliteAdapter) Close() error { 64 | if db.sqlDB != nil { 65 | return db.sqlDB.Close() 66 | } 67 | return nil 68 | } 69 | 70 | // SQLDB returns the internal db.sqlDB pointer 71 | func (db *SqliteAdapter) SQLDB() *sql.DB { 72 | return db.sqlDB 73 | } 74 | 75 | // Query execute Query SQL - NB caller must call use defer rows.Close() with rows returned 76 | func (db *SqliteAdapter) Query(query string, args ...interface{}) (*sql.Rows, error) { 77 | return db.performQuery(db.sqlDB, db.debug, query, args...) 78 | } 79 | 80 | // Exec - use this for non-select statements 81 | func (db *SqliteAdapter) Exec(query string, args ...interface{}) (sql.Result, error) { 82 | return db.performExec(db.sqlDB, db.debug, query, args...) 83 | } 84 | 85 | // Insert a record with params and return the id - psql behaves differently 86 | func (db *SqliteAdapter) Insert(query string, args ...interface{}) (id int64, err error) { 87 | 88 | // Execute the sql using db 89 | result, err := db.Exec(query, args...) 90 | if err != nil { 91 | return 0, err 92 | } 93 | 94 | id, err = result.LastInsertId() 95 | if err != nil { 96 | return 0, err 97 | } 98 | 99 | return id, nil 100 | 101 | } 102 | -------------------------------------------------------------------------------- /database.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/fragmenta/query/adapters" 9 | ) 10 | 11 | // database is the package global db - this reference is not exported outside the package. 12 | var database adapters.Database 13 | 14 | // OpenDatabase opens the database with the given options 15 | func OpenDatabase(opts map[string]string) error { 16 | 17 | // If we already have a db, return it 18 | if database != nil { 19 | return fmt.Errorf("query: database already open - %s", database) 20 | } 21 | 22 | // Assign the db global in query package 23 | switch opts["adapter"] { 24 | case "sqlite3": 25 | database = &adapters.SqliteAdapter{} 26 | case "mysql": 27 | database = &adapters.MysqlAdapter{} 28 | case "postgres": 29 | database = &adapters.PostgresqlAdapter{} 30 | default: 31 | database = nil // fail 32 | } 33 | 34 | if database == nil { 35 | return fmt.Errorf("query: database adapter not recognised - %s", opts) 36 | } 37 | 38 | // Ask the db adapter to open 39 | return database.Open(opts) 40 | } 41 | 42 | // CloseDatabase closes the database opened by OpenDatabase 43 | func CloseDatabase() error { 44 | var err error 45 | if database != nil { 46 | err = database.Close() 47 | database = nil 48 | } 49 | 50 | return err 51 | } 52 | 53 | // SetMaxOpenConns sets the maximum number of open connections 54 | func SetMaxOpenConns(max int) { 55 | database.SQLDB().SetMaxOpenConns(max) 56 | } 57 | 58 | // QuerySQL executes the given sql Query against our database, with arbitrary args 59 | func QuerySQL(query string, args ...interface{}) (*sql.Rows, error) { 60 | if database == nil { 61 | return nil, fmt.Errorf("query: QuerySQL called with nil database") 62 | } 63 | results, err := database.Query(query, args...) 64 | return results, err 65 | } 66 | 67 | // ExecSQL executes the given sql against our database with arbitrary args 68 | // NB returns sql.Result - not to be used when rows expected 69 | func ExecSQL(query string, args ...interface{}) (sql.Result, error) { 70 | if database == nil { 71 | return nil, fmt.Errorf("query: ExecSQL called with nil database") 72 | } 73 | results, err := database.Exec(query, args...) 74 | return results, err 75 | } 76 | 77 | // TimeString returns a string formatted as a time for this db 78 | // if the database is nil, an empty string is returned. 79 | func TimeString(t time.Time) string { 80 | if database != nil { 81 | return database.TimeString(t) 82 | } 83 | return "" 84 | } 85 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | // Package query lets you build and execute SQL chainable queries against a database of your choice, and defer execution of SQL until you wish to extract a count or array of models. 2 | 3 | // NB in order to allow cross-compilation, we exlude sqlite drivers by default 4 | // uncomment them to allow use of sqlite 5 | 6 | package query 7 | 8 | import ( 9 | "database/sql" 10 | "fmt" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | // FIXME - this package global should in theory be protected by a mutex, even if it is only for debugging 17 | 18 | // Debug sets whether we output debug statements for SQL 19 | var Debug bool 20 | 21 | func init() { 22 | Debug = false // default to false 23 | } 24 | 25 | // Result holds the results of a query as map[string]interface{} 26 | type Result map[string]interface{} 27 | 28 | // Func is a function which applies effects to queries 29 | type Func func(q *Query) *Query 30 | 31 | // Query provides all the chainable relational query builder methods 32 | type Query struct { 33 | 34 | // Database - database name and primary key, set with New() 35 | tablename string 36 | primarykey string 37 | 38 | // SQL - Private fields used to store sql before building sql query 39 | sql string 40 | sel string 41 | join string 42 | where string 43 | group string 44 | having string 45 | order string 46 | offset string 47 | limit string 48 | 49 | // Extra args to be substituted in the *where* clause 50 | args []interface{} 51 | } 52 | 53 | // New builds a new Query, given the table and primary key 54 | func New(t string, pk string) *Query { 55 | 56 | // If we have no db, return nil 57 | if database == nil { 58 | return nil 59 | } 60 | 61 | q := &Query{ 62 | tablename: t, 63 | primarykey: pk, 64 | } 65 | 66 | return q 67 | } 68 | 69 | // Exec the given sql and args against the database directly 70 | // Returning sql.Result (NB not rows) 71 | func Exec(sql string, args ...interface{}) (sql.Result, error) { 72 | results, err := database.Exec(sql, args...) 73 | return results, err 74 | } 75 | 76 | // Rows executes the given sql and args against the database directly 77 | // Returning sql.Rows 78 | func Rows(sql string, args ...interface{}) (*sql.Rows, error) { 79 | results, err := database.Query(sql, args...) 80 | return results, err 81 | } 82 | 83 | // Copy returns a new copy of this query which can be mutated without affecting the original 84 | func (q *Query) Copy() *Query { 85 | return &Query{ 86 | tablename: q.tablename, 87 | primarykey: q.primarykey, 88 | sql: q.sql, 89 | sel: q.sel, 90 | join: q.join, 91 | where: q.where, 92 | group: q.group, 93 | having: q.having, 94 | order: q.order, 95 | offset: q.offset, 96 | limit: q.limit, 97 | args: q.args, 98 | } 99 | } 100 | 101 | // TODO: These should instead be something like query.New("table_name").Join(a,b).Insert() and just have one multiple function? 102 | 103 | // InsertJoin inserts a join clause on the query 104 | func (q *Query) InsertJoin(a int64, b int64) error { 105 | return q.InsertJoins([]int64{a}, []int64{b}) 106 | } 107 | 108 | // InsertJoins using an array of ids (more general version of above) 109 | // This inserts joins for every possible relation between the ids 110 | func (q *Query) InsertJoins(a []int64, b []int64) error { 111 | 112 | // Make sure we have some data 113 | if len(a) == 0 || len(b) == 0 { 114 | return fmt.Errorf("query: null data for joins insert %s", q.table()) 115 | } 116 | 117 | values := "" 118 | for _, av := range a { 119 | for _, bv := range b { 120 | // NB no zero values allowed, we simply ignore zero values 121 | if av != 0 && bv != 0 { 122 | values += fmt.Sprintf("(%d,%d),", av, bv) 123 | } 124 | 125 | } 126 | } 127 | 128 | values = strings.TrimRight(values, ",") 129 | 130 | sql := fmt.Sprintf("INSERT into %s VALUES %s;", q.table(), values) 131 | 132 | if Debug { 133 | fmt.Printf("JOINS SQL:%s\n", sql) 134 | } 135 | 136 | _, err := database.Exec(sql) 137 | if err != nil { 138 | return fmt.Errorf("query: insert joins:%s", err) 139 | } 140 | return nil 141 | } 142 | 143 | // UpdateJoins updates the given joins, using the given id to clear joins first 144 | func (q *Query) UpdateJoins(id int64, a []int64, b []int64) error { 145 | 146 | if Debug { 147 | fmt.Printf("SetJoins %s %s=%d: %v %v \n", q.table(), q.pk(), id, a, b) 148 | } 149 | 150 | // First delete any existing joins 151 | err := q.Where(fmt.Sprintf("%s=?", q.pk()), id).Delete() 152 | if err != nil { 153 | return fmt.Errorf("query: update join delete error:%s", err) 154 | } 155 | 156 | // Now join all a's with all b's by generating joins for each possible combination 157 | 158 | // Make sure we have data in both cases, otherwise do not attempt insert any joins 159 | if len(a) > 0 && len(b) > 0 { 160 | // Now insert all new ids - NB the order of arguments here MUST match the order in the table 161 | err = q.InsertJoins(a, b) 162 | if err != nil { 163 | return err 164 | } 165 | } 166 | 167 | return nil 168 | } 169 | 170 | // Insert inserts a record in the database 171 | func (q *Query) Insert(params map[string]string) (int64, error) { 172 | 173 | // Insert and retrieve ID in one step from db 174 | sql := q.insertSQL(params) 175 | 176 | if Debug { 177 | fmt.Printf("INSERT SQL:%s %v\n", sql, valuesFromParams(params)) 178 | } 179 | 180 | id, err := database.Insert(sql, valuesFromParams(params)...) 181 | if err != nil { 182 | return 0, err 183 | } 184 | 185 | return id, nil 186 | } 187 | 188 | // insertSQL sets the insert sql for update statements, turn params into sql i.e. "col"=? 189 | // NB we always use parameterized queries, never string values. 190 | func (q *Query) insertSQL(params map[string]string) string { 191 | var cols, vals []string 192 | 193 | for i, k := range sortedParamKeys(params) { 194 | cols = append(cols, database.QuoteField(k)) 195 | vals = append(vals, database.Placeholder(i+1)) 196 | } 197 | query := fmt.Sprintf("INSERT INTO %s (%s) VALUES(%s) %s;", q.table(), strings.Join(cols, ","), strings.Join(vals, ","), database.InsertSQL(q.pk())) 198 | 199 | return query 200 | } 201 | 202 | // Update one model specified in this query - the column names MUST be verified in the model 203 | func (q *Query) Update(params map[string]string) error { 204 | // We should check the query has a where limitation to avoid updating all? 205 | // pq unfortunately does not accept limit(1) here 206 | return q.UpdateAll(params) 207 | } 208 | 209 | // Delete one model specified in this relation 210 | func (q *Query) Delete() error { 211 | // We should check the query has a where limitation? 212 | return q.DeleteAll() 213 | } 214 | 215 | // UpdateAll updates all models specified in this relation 216 | func (q *Query) UpdateAll(params map[string]string) error { 217 | 218 | // Build query SQL, allowing for null fields 219 | var output []string 220 | for _, key := range sortedParamKeys(params) { 221 | v := params[key] 222 | // Special case the value 'null' 223 | if v == "null" { 224 | // Set the value to null in the db, rather than a value 225 | output = append(output, fmt.Sprintf("%s=null", database.QuoteField(key))) 226 | // Remove from params as we have added to the update statement and don't require an argument 227 | delete(params, key) 228 | } else { 229 | // Set the value using a placeholder 230 | output = append(output, fmt.Sprintf("%s=?", database.QuoteField(key))) 231 | } 232 | } 233 | querySQL := strings.Join(output, ",") 234 | 235 | // Create sql for update from all params except null params using placeholders for args 236 | q.Select(fmt.Sprintf("UPDATE %s SET %s", q.table(), querySQL)) 237 | 238 | // Execute, after PREpending params to args 239 | // In an update statement, the where comes at the end so update args come first 240 | values := valuesFromParams(params) 241 | q.args = append(values, q.args...) 242 | 243 | // If debug mode, output a string representation 244 | if Debug { 245 | fmt.Printf("UPDATE SQL:%s\n%v\n", q.QueryString(), values) 246 | } 247 | 248 | // Return the result of execution 249 | _, err := q.Result() 250 | return err 251 | } 252 | 253 | // DeleteAll delets *all* models specified in this relation 254 | func (q *Query) DeleteAll() error { 255 | 256 | q.Select(fmt.Sprintf("DELETE FROM %s", q.table())) 257 | 258 | if Debug { 259 | fmt.Printf("DELETE SQL:%s <= %v\n", q.QueryString(), q.args) 260 | } 261 | 262 | // Execute 263 | _, err := q.Result() 264 | 265 | return err 266 | } 267 | 268 | // Count fetches a count of model objects (executes SQL). 269 | func (q *Query) Count() (int64, error) { 270 | 271 | // In order to get consistent results, we use the same query builder 272 | // but reset select to simple count select 273 | 274 | // Store the previous select and set 275 | s := q.sel 276 | countSelect := fmt.Sprintf("SELECT COUNT(distinct %s.%s) FROM %s", q.table(), q.pk(), q.table()) 277 | q.Select(countSelect) 278 | 279 | // Store the previous order (minus order by) and set to empty 280 | // Order must be blank on count because of limited select 281 | o := strings.Replace(q.order, "ORDER BY ", "", 1) 282 | q.order = "" 283 | 284 | // Fetch count from db for our sql with count select and no order set 285 | var count int64 286 | rows, err := q.Rows() 287 | if err != nil { 288 | return 0, fmt.Errorf("query: error querying database for count: %s\nQuery:%s", err, q.QueryString()) 289 | } 290 | 291 | // We expect just one row, with one column (count) 292 | defer rows.Close() 293 | for rows.Next() { 294 | err = rows.Scan(&count) 295 | if err != nil { 296 | return 0, err 297 | } 298 | } 299 | 300 | // Reset select after getting count query 301 | q.Select(s) 302 | q.Order(o) 303 | q.reset() 304 | 305 | return count, err 306 | } 307 | 308 | // Result executes the query against the database, returning sql.Result, and error (no rows) 309 | // (Executes SQL) 310 | func (q *Query) Result() (sql.Result, error) { 311 | results, err := database.Exec(q.QueryString(), q.args...) 312 | return results, err 313 | } 314 | 315 | // Rows executes the query against the database, and return the sql rows result for this query 316 | // (Executes SQL) 317 | func (q *Query) Rows() (*sql.Rows, error) { 318 | results, err := database.Query(q.QueryString(), q.args...) 319 | return results, err 320 | } 321 | 322 | // FirstResult executes the SQL and returrns the first result 323 | func (q *Query) FirstResult() (Result, error) { 324 | 325 | // Set a limit on the query 326 | q.Limit(1) 327 | 328 | // Fetch all results (1) 329 | results, err := q.Results() 330 | if err != nil { 331 | return nil, err 332 | } 333 | 334 | if len(results) == 0 { 335 | return nil, fmt.Errorf("No results found for Query:%s", q.QueryString()) 336 | } 337 | 338 | // Return the first result 339 | return results[0], nil 340 | } 341 | 342 | // ResultInt64 returns the first result from a query stored in the column named col as an int64. 343 | func (q *Query) ResultInt64(c string) (int64, error) { 344 | result, err := q.FirstResult() 345 | if err != nil || result[c] == nil { 346 | return 0, err 347 | } 348 | var i int64 349 | switch result[c].(type) { 350 | case int64: 351 | i = result[c].(int64) 352 | case int: 353 | i = int64(result[c].(int)) 354 | case float64: 355 | i = int64(result[c].(float64)) 356 | case string: 357 | f, err := strconv.ParseFloat(result[c].(string), 64) 358 | if err != nil { 359 | return i, err 360 | } 361 | i = int64(f) 362 | } 363 | 364 | return i, nil 365 | } 366 | 367 | // ResultFloat64 returns the first result from a query stored in the column named col as a float64. 368 | func (q *Query) ResultFloat64(c string) (float64, error) { 369 | result, err := q.FirstResult() 370 | if err != nil || result[c] == nil { 371 | return 0, err 372 | } 373 | var f float64 374 | switch result[c].(type) { 375 | case float64: 376 | f = result[c].(float64) 377 | case int: 378 | f = float64(result[c].(int)) 379 | case int64: 380 | f = float64(result[c].(int)) 381 | case string: 382 | f, err = strconv.ParseFloat(result[c].(string), 64) 383 | if err != nil { 384 | return f, err 385 | } 386 | } 387 | 388 | return f, nil 389 | } 390 | 391 | // Results returns an array of results 392 | func (q *Query) Results() ([]Result, error) { 393 | 394 | // Make an empty result set map 395 | var results []Result 396 | 397 | // Fetch rows from db for our sql 398 | rows, err := q.Rows() 399 | 400 | if err != nil { 401 | return results, fmt.Errorf("Error querying database for rows: %s\nQUERY:%s", err, q) 402 | } 403 | 404 | // Close rows before returning 405 | defer rows.Close() 406 | 407 | // Fetch the columns from the database 408 | cols, err := rows.Columns() 409 | if err != nil { 410 | return results, fmt.Errorf("Error fetching columns: %s\nQUERY:%s\nCOLS:%s", err, q, cols) 411 | } 412 | 413 | // For each row, construct an entry in results with a map of column string keys to values 414 | for rows.Next() { 415 | result, err := scanRow(cols, rows) 416 | if err != nil { 417 | return results, fmt.Errorf("Error fetching row: %s\nQUERY:%s\nCOLS:%s", err, q, cols) 418 | } 419 | results = append(results, result) 420 | } 421 | 422 | return results, nil 423 | } 424 | 425 | // ResultIDs returns an array of ids as the result of a query 426 | // FIXME - this should really use the query primary key, not "id" hardcoded 427 | func (q *Query) ResultIDs() []int64 { 428 | var ids []int64 429 | if Debug { 430 | fmt.Printf("#info ResultIDs:%s\n", q.DebugString()) 431 | } 432 | results, err := q.Results() 433 | if err != nil { 434 | return ids 435 | } 436 | 437 | for _, r := range results { 438 | if r["id"] != nil { 439 | ids = append(ids, r["id"].(int64)) 440 | } 441 | } 442 | 443 | return ids 444 | } 445 | 446 | // ResultIDSets returns a map from a values to arrays of b values, the order of a,b is respected not the table key order 447 | func (q *Query) ResultIDSets(a, b string) map[int64][]int64 { 448 | idSets := make(map[int64][]int64, 0) 449 | 450 | results, err := q.Results() 451 | if err != nil { 452 | return idSets 453 | } 454 | 455 | for _, r := range results { 456 | if r[a] != nil && r[b] != nil { 457 | av := r[a].(int64) 458 | bv := r[b].(int64) 459 | idSets[av] = append(idSets[av], bv) 460 | } 461 | } 462 | if Debug { 463 | fmt.Printf("#info ResultIDSets:%s\n", q.DebugString()) 464 | } 465 | return idSets 466 | } 467 | 468 | // QueryString builds a query string to use for results 469 | func (q *Query) QueryString() string { 470 | 471 | if q.sql == "" { 472 | 473 | // if we have arguments override the selector 474 | if q.sel == "" { 475 | // Note q.table() etc perform quoting on field names 476 | q.sel = fmt.Sprintf("SELECT %s.* FROM %s", q.table(), q.table()) 477 | } 478 | 479 | q.sql = fmt.Sprintf("%s %s %s %s %s %s %s %s", q.sel, q.join, q.where, q.group, q.having, q.order, q.offset, q.limit) 480 | q.sql = strings.TrimRight(q.sql, " ") 481 | q.sql = strings.Replace(q.sql, " ", " ", -1) 482 | q.sql = strings.Replace(q.sql, " ", " ", -1) 483 | 484 | // Replace ? with whatever placeholder db prefers 485 | q.replaceArgPlaceholders() 486 | 487 | q.sql = q.sql + ";" 488 | } 489 | 490 | return q.sql 491 | } 492 | 493 | // CHAINABLE FINDERS 494 | 495 | // Apply the Func to this query, and return the modified Query 496 | // This allows chainable finders from other packages 497 | // e.g. q.Apply(status.Published) where status.Published is a Func 498 | func (q *Query) Apply(f Func) *Query { 499 | return f(q) 500 | } 501 | 502 | // Conditions applies a series of query funcs to a query 503 | func (q *Query) Conditions(funcs ...Func) *Query { 504 | for _, f := range funcs { 505 | q = f(q) 506 | } 507 | return q 508 | } 509 | 510 | // SQL defines sql manually and overrides all other setters 511 | // Completely replaces all stored sql 512 | func (q *Query) SQL(sql string) *Query { 513 | q.sql = sql 514 | q.reset() 515 | return q 516 | } 517 | 518 | // Limit sets the sql LIMIT with an int 519 | func (q *Query) Limit(limit int) *Query { 520 | q.limit = fmt.Sprintf("LIMIT %d", limit) 521 | q.reset() 522 | return q 523 | } 524 | 525 | // Offset sets the sql OFFSET with an int 526 | func (q *Query) Offset(offset int) *Query { 527 | q.offset = fmt.Sprintf("OFFSET %d", offset) 528 | q.reset() 529 | return q 530 | } 531 | 532 | // Where defines a WHERE clause on SQL - Additional calls add WHERE () AND () clauses 533 | func (q *Query) Where(sql string, args ...interface{}) *Query { 534 | 535 | if len(q.where) > 0 { 536 | q.where = fmt.Sprintf("%s AND (%s)", q.where, sql) 537 | } else { 538 | q.where = fmt.Sprintf("WHERE (%s)", sql) 539 | } 540 | 541 | // NB this assumes that args are only supplied for where clauses 542 | // this may be an incorrect assumption! 543 | if args != nil { 544 | if q.args == nil { 545 | q.args = args 546 | } else { 547 | q.args = append(q.args, args...) 548 | } 549 | } 550 | 551 | q.reset() 552 | return q 553 | } 554 | 555 | // OrWhere defines a where clause on SQL - Additional calls add WHERE () OR () clauses 556 | func (q *Query) OrWhere(sql string, args ...interface{}) *Query { 557 | 558 | if len(q.where) > 0 { 559 | q.where = fmt.Sprintf("%s OR (%s)", q.where, sql) 560 | } else { 561 | q.where = fmt.Sprintf("WHERE (%s)", sql) 562 | } 563 | 564 | if args != nil { 565 | if q.args == nil { 566 | q.args = args 567 | } else { 568 | q.args = append(q.args, args...) 569 | } 570 | } 571 | 572 | q.reset() 573 | return q 574 | } 575 | 576 | // WhereIn adds a Where clause which selects records IN() the given array 577 | // If IDs is an empty array, the query limit is set to 0 578 | func (q *Query) WhereIn(col string, IDs []int64) *Query { 579 | // Return no results, so that when chaining callers 580 | // don't have to check for empty arrays 581 | if len(IDs) == 0 { 582 | q.Limit(0) 583 | q.reset() 584 | return q 585 | } 586 | 587 | in := "" 588 | for _, ID := range IDs { 589 | in = fmt.Sprintf("%s%d,", in, ID) 590 | } 591 | in = strings.TrimRight(in, ",") 592 | sql := fmt.Sprintf("%s IN (%s)", col, in) 593 | 594 | if len(q.where) > 0 { 595 | q.where = fmt.Sprintf("%s AND (%s)", q.where, sql) 596 | } else { 597 | q.where = fmt.Sprintf("WHERE (%s)", sql) 598 | } 599 | 600 | q.reset() 601 | return q 602 | } 603 | 604 | // Define a join clause on SQL - we create an inner join like this: 605 | // INNER JOIN extras_seasons ON extras.id = extra_id 606 | // q.Select("SELECT units.* FROM units INNER JOIN sites ON units.site_id = sites.id") 607 | 608 | // rails join example 609 | // INNER JOIN "posts_tags" ON "posts_tags"."tag_id" = "tags"."id" WHERE "posts_tags"."post_id" = 111 610 | 611 | // Join adds an inner join to the query 612 | func (q *Query) Join(otherModel string) *Query { 613 | modelTable := q.tablename 614 | 615 | tables := []string{ 616 | modelTable, 617 | ToPlural(otherModel), 618 | } 619 | sort.Strings(tables) 620 | joinTable := fmt.Sprintf("%s_%s", tables[0], tables[1]) 621 | 622 | sql := fmt.Sprintf("INNER JOIN %s ON %s.id = %s.%s_id", database.QuoteField(joinTable), database.QuoteField(modelTable), database.QuoteField(joinTable), ToSingular(modelTable)) 623 | 624 | if len(q.join) > 0 { 625 | q.join = fmt.Sprintf("%s %s", q.join, sql) 626 | } else { 627 | q.join = fmt.Sprintf("%s", sql) 628 | } 629 | 630 | q.reset() 631 | return q 632 | } 633 | 634 | // AddJoinString appends a fully specified join string to the list of joins 635 | func (q *Query) AddJoinString(joinSQL string) *Query { 636 | q.join = fmt.Sprintf("%s %s", q.join, joinSQL) 637 | q.reset() 638 | return q 639 | } 640 | 641 | // Order defines ORDER BY sql 642 | func (q *Query) Order(sql string) *Query { 643 | if sql == "" { 644 | q.order = "" 645 | } else { 646 | q.order = fmt.Sprintf("ORDER BY %s", sql) 647 | } 648 | q.reset() 649 | 650 | return q 651 | } 652 | 653 | // Group defines GROUP BY sql 654 | func (q *Query) Group(sql string) *Query { 655 | if sql == "" { 656 | q.group = "" 657 | } else { 658 | q.group = fmt.Sprintf("GROUP BY %s", sql) 659 | } 660 | q.reset() 661 | return q 662 | } 663 | 664 | // Having defines HAVING sql 665 | func (q *Query) Having(sql string) *Query { 666 | if sql == "" { 667 | q.having = "" 668 | } else { 669 | q.having = fmt.Sprintf("HAVING %s", sql) 670 | } 671 | q.reset() 672 | return q 673 | } 674 | 675 | // Select defines SELECT sql 676 | func (q *Query) Select(sql string) *Query { 677 | q.sel = sql 678 | q.reset() 679 | return q 680 | } 681 | 682 | // DebugString returns a query representation string useful for debugging 683 | func (q *Query) DebugString() string { 684 | return fmt.Sprintf("--\nQuery-SQL:%s\nARGS:%s\n--", q.QueryString(), q.argString()) 685 | } 686 | 687 | // Clear sql/query caches 688 | func (q *Query) reset() { 689 | // Perhaps later clear cached compiled representation of query too 690 | 691 | // clear stored sql 692 | q.sql = "" 693 | } 694 | 695 | // Return an arg string (for debugging) 696 | func (q *Query) argString() string { 697 | output := "-" 698 | 699 | for _, a := range q.args { 700 | output = output + fmt.Sprintf("'%s',", q.argToString(a)) 701 | } 702 | output = strings.TrimRight(output, ",") 703 | output = output + "" 704 | 705 | return output 706 | } 707 | 708 | // Convert arguments to string - used only for debug argument strings 709 | // Not to be exported or used to try to escape strings... 710 | func (q *Query) argToString(arg interface{}) string { 711 | switch arg.(type) { 712 | case string: 713 | return arg.(string) 714 | case []byte: 715 | return string(arg.([]byte)) 716 | case int, int8, int16, int32, uint, uint8, uint16, uint32: 717 | return fmt.Sprintf("%d", arg) 718 | case int64, uint64: 719 | return fmt.Sprintf("%d", arg) 720 | case float32, float64: 721 | return fmt.Sprintf("%f", arg) 722 | case bool: 723 | return fmt.Sprintf("%d", arg) 724 | default: 725 | return fmt.Sprintf("%v", arg) 726 | 727 | } 728 | 729 | } 730 | 731 | // Ask model for primary key name to use 732 | func (q *Query) pk() string { 733 | return database.QuoteField(q.primarykey) 734 | } 735 | 736 | // Ask model for table name to use 737 | func (q *Query) table() string { 738 | return database.QuoteField(q.tablename) 739 | } 740 | 741 | // Replace ? with whatever database prefers (psql uses numbered args) 742 | func (q *Query) replaceArgPlaceholders() { 743 | // Match ? and replace with argument placeholder from database 744 | for i := range q.args { 745 | q.sql = strings.Replace(q.sql, "?", database.Placeholder(i+1), 1) 746 | } 747 | } 748 | 749 | // Sorts the param names given - map iteration order is explicitly random in Go 750 | // but we need params in a defined order to avoid unexpected results. 751 | func sortedParamKeys(params map[string]string) []string { 752 | sortedKeys := make([]string, len(params)) 753 | i := 0 754 | for k := range params { 755 | sortedKeys[i] = k 756 | i++ 757 | } 758 | sort.Strings(sortedKeys) 759 | 760 | return sortedKeys 761 | } 762 | 763 | // Generate a set of values for the params in order 764 | func valuesFromParams(params map[string]string) []interface{} { 765 | 766 | // NB DO NOT DEPEND ON PARAMS ORDER - see note on SortedParamKeys 767 | var values []interface{} 768 | for _, key := range sortedParamKeys(params) { 769 | values = append(values, params[key]) 770 | } 771 | return values 772 | } 773 | 774 | func scanRow(cols []string, rows *sql.Rows) (Result, error) { 775 | 776 | // We return a map[string]interface{} for each row scanned 777 | result := Result{} 778 | 779 | values := make([]interface{}, len(cols)) 780 | for i := 0; i < len(cols); i++ { 781 | var col interface{} 782 | values[i] = &col 783 | } 784 | 785 | // Scan results into these interfaces 786 | err := rows.Scan(values...) 787 | if err != nil { 788 | return nil, fmt.Errorf("Error scanning row: %s", err) 789 | } 790 | 791 | // Make a string => interface map and hand off to caller 792 | // We fix up a few types which the pq driver returns as less handy equivalents 793 | // We enforce usage of int64 at all times as all our records use int64 794 | for i := 0; i < len(cols); i++ { 795 | v := *values[i].(*interface{}) 796 | if values[i] != nil { 797 | switch v.(type) { 798 | default: 799 | result[cols[i]] = v 800 | case bool: 801 | result[cols[i]] = v.(bool) 802 | case int: 803 | result[cols[i]] = int64(v.(int)) 804 | case []byte: // text cols are given as bytes 805 | result[cols[i]] = string(v.([]byte)) 806 | case int64: 807 | result[cols[i]] = v.(int64) 808 | } 809 | } 810 | 811 | } 812 | 813 | return result, nil 814 | } 815 | -------------------------------------------------------------------------------- /query_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | // At present psql and mysql are tested, sqlite is disabled due to cross-compilation requirements 15 | 16 | // Pages is a simple example model for testing the query package which stores some fields in the db. 17 | // All functions prefixed with Pages here - normally the model would be in a separate function 18 | // so pages.Find(1) etc 19 | 20 | // Page model 21 | type Page struct { 22 | ID int64 23 | UpdatedAt time.Time 24 | CreatedAt time.Time 25 | 26 | OtherField map[string]string 27 | Title string 28 | Summary string 29 | Text string 30 | 31 | UnusedField int8 32 | } 33 | 34 | // Create a model object, called from actions. 35 | func (p *Page) Create(params map[string]string) (int64, error) { 36 | params["created_at"] = TimeString(time.Now().UTC()) 37 | params["updated_at"] = TimeString(time.Now().UTC()) 38 | return PagesQuery().Insert(params) 39 | } 40 | 41 | // Update this model object, called from actions. 42 | func (p *Page) Update(params map[string]string) error { 43 | params["updated_at"] = TimeString(time.Now().UTC()) 44 | return PagesQuery().Where("id=?", p.ID).Update(params) 45 | } 46 | 47 | // Delete this page 48 | func (p *Page) Delete() error { 49 | return PagesQuery().Where("id=?", p.ID).Delete() 50 | } 51 | 52 | // NewWithColumns creates a new page instance and fills it with data from the database cols provided 53 | func PagesNewWithColumns(cols map[string]interface{}) *Page { 54 | 55 | page := PagesNew() 56 | 57 | // Normally you'd validate col values with something like the model/validate pkg 58 | // we'll use a simple dummy function instead 59 | page.ID = cols["id"].(int64) 60 | if cols["created_at"] != nil { 61 | page.CreatedAt = cols["created_at"].(time.Time) 62 | } 63 | if cols["updated_at"] != nil { 64 | page.UpdatedAt = cols["updated_at"].(time.Time) 65 | } 66 | 67 | if cols["title"] != nil { 68 | page.Title = cols["title"].(string) 69 | } 70 | if cols["summary"] != nil { 71 | page.Summary = cols["summary"].(string) 72 | } 73 | if cols["text"] != nil { 74 | page.Text = cols["text"].(string) 75 | } 76 | 77 | return page 78 | } 79 | 80 | // New initialises and returns a new Page 81 | func PagesNew() *Page { 82 | page := &Page{} 83 | return page 84 | } 85 | 86 | // Query returns a new query for pages 87 | func PagesQuery() *Query { 88 | return New("pages", "id") 89 | } 90 | 91 | func PagesFind(ID int64) (*Page, error) { 92 | result, err := PagesQuery().Where("id=?", ID).FirstResult() 93 | if err != nil { 94 | return nil, err 95 | } 96 | return PagesNewWithColumns(result), nil 97 | } 98 | 99 | func PagesFindAll(q *Query) ([]*Page, error) { 100 | results, err := q.Results() 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | var models []*Page 106 | for _, r := range results { 107 | m := PagesNewWithColumns(r) 108 | models = append(models, m) 109 | } 110 | 111 | return models, nil 112 | } 113 | 114 | // ---------------------------------- 115 | // Test Helpers 116 | // ---------------------------------- 117 | 118 | var User = os.ExpandEnv("$USER") 119 | var Password = os.ExpandEnv("$QUERY_TEST_PASS") // may be blank 120 | 121 | var Format = "\n---\nFAILURE\n---\ninput: %q\nexpected: %q\noutput: %q" 122 | 123 | // ---------------------------------- 124 | // PSQL TESTS 125 | // ---------------------------------- 126 | 127 | func TestPQSetup(t *testing.T) { 128 | 129 | fmt.Println("\n---\nTESTING POSTRGRESQL\n---") 130 | 131 | // First execute sql 132 | cmd := exec.Command("psql", "-dquery_test", "-f./tests/query_test_pq.sql") 133 | stdout, _ := cmd.StdoutPipe() 134 | stderr, _ := cmd.StderrPipe() 135 | err := cmd.Start() 136 | if err != nil { 137 | t.Fatalf("DB Test Error %v", err) 138 | } 139 | io.Copy(os.Stdout, stdout) 140 | io.Copy(os.Stderr, stderr) 141 | cmd.Wait() 142 | 143 | if err == nil { 144 | // Open the database 145 | options := map[string]string{ 146 | "adapter": "postgres", 147 | "user": User, // Valid username required for databases 148 | "password": Password, 149 | "db": "query_test", 150 | "debug": "true", 151 | } 152 | 153 | err = OpenDatabase(options) 154 | if err != nil { 155 | t.Fatalf("DB Error %v", err) 156 | } 157 | 158 | fmt.Printf("---\nQuery Testing Postgres - query_test DB setup complete as user %s\n---", User) 159 | } 160 | 161 | } 162 | 163 | func TestPQFind(t *testing.T) { 164 | 165 | // This should fail, as there is no such page 166 | p, err := PagesFind(11) 167 | if err == nil { 168 | t.Fatalf(Format, "Find(11)", "nil", p, err) 169 | } 170 | 171 | // This should work 172 | _, err = PagesFind(1) 173 | if err != nil { 174 | t.Fatalf(Format, "Find(1)", "Model object", err) 175 | } 176 | 177 | } 178 | 179 | func TestPQCount(t *testing.T) { 180 | 181 | // This should return 3 182 | count, err := PagesQuery().Count() 183 | if err != nil || count != 3 { 184 | t.Fatalf(Format, "Count failed", "3", fmt.Sprintf("%d", count)) 185 | } 186 | 187 | // This should return 2 - test limit ignored 188 | count, err = PagesQuery().Where("id < 3").Order("id desc").Limit(100).Count() 189 | if err != nil || count != 2 { 190 | t.Fatalf(Format, "Count id < 3 failed", "2", fmt.Sprintf("%d", count)) 191 | } 192 | 193 | // This should return 0 194 | count, err = PagesQuery().Where("id > 3").Count() 195 | if err != nil || count != 0 { 196 | t.Fatalf(Format, "Count id > 3 failed", "0", fmt.Sprintf("%d", count)) 197 | } 198 | 199 | // Test retrieving an array, then counting, then where 200 | // This should work 201 | q := PagesQuery().Where("id > ?", 1).Order("id desc") 202 | 203 | count, err = q.Count() 204 | if err != nil || count != 2 { 205 | t.Fatalf(Format, "Count id > 1 failed", "2", fmt.Sprintf("%d", count), err) 206 | } 207 | 208 | // Reuse same query to get array after count 209 | models, err := PagesFindAll(q) 210 | if err != nil || len(models) != 2 { 211 | t.Fatalf(Format, "Where Array after count", "len 2", err) 212 | } 213 | 214 | } 215 | func TestPQWhere(t *testing.T) { 216 | 217 | q := PagesQuery().Where("id > ?", 1) 218 | models, err := PagesFindAll(q) 219 | if err != nil || len(models) != 2 { 220 | t.Fatalf(Format, "Where Array", "len 2", fmt.Sprintf("%d", len(models))) 221 | } 222 | 223 | } 224 | 225 | func TestPQOrder(t *testing.T) { 226 | 227 | // Look for pages in reverse order 228 | q := PagesQuery().Where("id > 1").Order("id desc") 229 | models, err := PagesFindAll(q) 230 | if err != nil || len(models) == 0 { 231 | t.Fatalf(Format, "Order test id desc", "3", fmt.Sprintf("%d", len(models))) 232 | return 233 | } 234 | 235 | p := models[0] 236 | if p.ID != 3 { 237 | t.Fatalf(Format, "Order test id desc", "3", fmt.Sprintf("%d", p.ID)) 238 | 239 | } 240 | 241 | // Look for pages in right order 242 | q = PagesQuery().Where("id < ?", 10).Where("id < ?", 100).Order("id asc") 243 | models, err = PagesFindAll(q) 244 | if err != nil || models == nil { 245 | t.Fatalf(Format, "Order test id asc", "1", err) 246 | } 247 | 248 | p = models[0] 249 | // Check id and created at time are correct 250 | if p.ID != 1 || time.Since(p.CreatedAt) > time.Second { 251 | t.Fatalf(Format, "Order test id asc", "1", fmt.Sprintf("%d", p.ID)) 252 | } 253 | 254 | } 255 | 256 | func TestPQSelect(t *testing.T) { 257 | 258 | var models []*Page 259 | q := PagesQuery().Select("SELECT id,title from pages").Order("id asc") 260 | models, err := PagesFindAll(q) 261 | if err != nil || len(models) == 0 { 262 | t.Fatalf(Format, "Select error on id,title", "id,title", err) 263 | } 264 | p := models[0] 265 | // Check id and title selected, other values to be zero values 266 | if p.ID != 1 || p.Title != "Title 1." || len(p.Text) > 0 || p.CreatedAt.Year() > 1 { 267 | t.Fatalf(Format, "Select id,title", "id,title only", p) 268 | } 269 | 270 | } 271 | 272 | // Some more damaging operations we execute at the end, 273 | // to avoid having to reload the db for each test 274 | 275 | func TestPQUpdateAll(t *testing.T) { 276 | 277 | err := PagesQuery().UpdateAll(map[string]string{"title": "test me"}) 278 | if err != nil { 279 | t.Fatalf(Format, "UPDATE ALL err", "udpate all records", err) 280 | } 281 | 282 | // Check we have all pages with same title 283 | count, err := PagesQuery().Where("title=?", "test me").Count() 284 | 285 | if err != nil || count != 3 { 286 | t.Fatalf(Format, "Count after update all", "3", fmt.Sprintf("%d", count)) 287 | } 288 | 289 | } 290 | 291 | func TestPQUpdate(t *testing.T) { 292 | 293 | p, err := PagesFind(3) 294 | if err != nil { 295 | t.Fatalf(Format, "Update could not find model err", "id-3", err) 296 | } 297 | 298 | // Should really test updates with several strings here 299 | // Update each model with a different string 300 | // This does also check if AllowedParams is working properly to clean params 301 | err = p.Update(map[string]string{"title": "UPDATE 1"}) 302 | if err != nil { 303 | t.Fatalf(Format, "Error after update", "updated", err) 304 | } 305 | // Check it is modified 306 | p, err = PagesFind(3) 307 | 308 | if err != nil { 309 | t.Fatalf(Format, "Error after update 1", "updated", err) 310 | } 311 | 312 | // Check we have an update and the updated at time was set 313 | if p.Title != "UPDATE 1" || time.Since(p.UpdatedAt) > time.Second { 314 | t.Fatalf(Format, "Error after update 1 - Not updated properly", "UPDATE 1", p.Title) 315 | } 316 | 317 | } 318 | 319 | func TestPQCreate(t *testing.T) { 320 | 321 | params := map[string]string{ 322 | // "id": "", 323 | "title": "Test 98", 324 | "text": "My text", 325 | "created_at": "REPLACE ME", 326 | "summary": "This is my summary", 327 | } 328 | 329 | // if your model is in a package, it could be pages.Create() 330 | // For now to mock we just use an empty page 331 | id, err := (&Page{}).Create(params) 332 | if err != nil { 333 | t.Fatalf(Format, "Err on create", err) 334 | } 335 | 336 | // Now find the page and test it 337 | p, err := PagesFind(id) 338 | if err != nil { 339 | t.Fatalf(Format, "Err on create find", err) 340 | } 341 | 342 | if p.Title != "Test 98" { 343 | t.Fatalf(Format, "Create page params mismatch", "Creation", p.Title) 344 | } 345 | 346 | // Check we have one left 347 | count, err := PagesQuery().Count() 348 | 349 | if err != nil || count != 4 { 350 | t.Fatalf(Format, "Count after create", "4", fmt.Sprintf("%d", count)) 351 | } 352 | 353 | } 354 | 355 | func TestPQDelete(t *testing.T) { 356 | 357 | p, err := PagesFind(3) 358 | if err != nil { 359 | t.Fatalf(Format, "Could not find model err", "id-3", err) 360 | } 361 | 362 | err = p.Delete() 363 | if err != nil { 364 | t.Fatalf(Format, "Error after delete", "deleted", err) 365 | } 366 | 367 | // Check it is gone and we get an error on next find 368 | p, err = PagesFind(3) 369 | 370 | if !strings.Contains(fmt.Sprintf("%s", err), "No results found") { 371 | t.Fatalf(Format, "Error after delete 1", "1", err) 372 | } 373 | 374 | } 375 | 376 | func TestPQDeleteAll(t *testing.T) { 377 | 378 | err := PagesQuery().Where("id > 1").DeleteAll() 379 | if err != nil { 380 | t.Fatalf(Format, "DELETE ALL err", "delete al above 1 records", err) 381 | } 382 | 383 | // Check we have one left 384 | count, err := PagesQuery().Count() 385 | 386 | if err != nil || count != 1 { 387 | t.Fatalf(Format, "Count after delete all above 1", "1", fmt.Sprintf("%d", count)) 388 | } 389 | 390 | } 391 | 392 | // This test takes some time, so only enable for speed testing 393 | func BenchmarkPQSpeed(t *testing.B) { 394 | 395 | fmt.Println("\n---\nSpeed testing PSQL\n---") 396 | 397 | for i := 0; i < 100000; i++ { 398 | // ok github.com/fragmenta/query 20.238s 399 | 400 | var models []*Page 401 | q := PagesQuery().Select("SELECT id,title from pages").Where("id < i").Order("id asc") 402 | models, err := PagesFindAll(q) 403 | if err != nil && models != nil { 404 | 405 | } 406 | 407 | // ok github.com/fragmenta/query 21.680s 408 | q = PagesQuery().Select("SELECT id,title from pages").Where("id < i").Order("id asc") 409 | r, err := q.Results() 410 | if err != nil && r != nil { 411 | 412 | } 413 | 414 | } 415 | 416 | fmt.Println("\n---\nSpeed testing PSQL END\n---") 417 | 418 | } 419 | 420 | // NB this test must come last, any tests after this will try to use an invalid database reference 421 | func TestPQTeardown(t *testing.T) { 422 | 423 | err := CloseDatabase() 424 | if err != nil { 425 | fmt.Println("Close DB ERROR ", err) 426 | } 427 | } 428 | 429 | // ---------------------------------- 430 | // MYSQL TESTS 431 | // ---------------------------------- 432 | 433 | func TestMysqlSetup(t *testing.T) { 434 | 435 | fmt.Println("\n---\nTESTING Mysql\n---") 436 | 437 | // First execute sql 438 | 439 | // read whole the file 440 | bytes, err := ioutil.ReadFile("./tests/query_test_mysql.sql") 441 | if err != nil { 442 | t.Fatalf("MYSQL DB ERROR: %s", err) 443 | } 444 | s := string(bytes) 445 | 446 | cmd := exec.Command("mysql", "-u", "root", "--init-command", s, "query_test") 447 | stdout, _ := cmd.StdoutPipe() 448 | stderr, _ := cmd.StderrPipe() 449 | err = cmd.Start() 450 | if err != nil { 451 | t.Fatalf("MYSQL DB ERROR: %s", err) 452 | } 453 | io.Copy(os.Stdout, stdout) 454 | io.Copy(os.Stderr, stderr) 455 | cmd.Wait() 456 | 457 | if err == nil { 458 | 459 | // Open the database 460 | options := map[string]string{ 461 | "adapter": "mysql", 462 | "db": "query_test", 463 | "debug": "true", 464 | } 465 | 466 | err = OpenDatabase(options) 467 | if err != nil { 468 | t.Fatalf("\n\n----\nMYSQL DB ERROR:\n%s\n----\n\n", err) 469 | } 470 | 471 | fmt.Println("---\nQuery Testing Mysql - DB setup complete\n---") 472 | } 473 | 474 | } 475 | 476 | func TestMysqlFind(t *testing.T) { 477 | 478 | // This should work 479 | p, err := PagesFind(1) 480 | if err != nil { 481 | t.Fatalf(Format, "Find(1)", "Model object", p) 482 | } 483 | 484 | // This should fail, so we check that 485 | p, err = PagesFind(11) 486 | if err == nil { 487 | t.Fatalf(Format, "Find(1)", "Model object", p) 488 | } 489 | 490 | } 491 | 492 | func TestMysqlCount(t *testing.T) { 493 | 494 | // This should return 3 495 | count, err := PagesQuery().Count() 496 | if err != nil || count != 3 { 497 | t.Fatalf(Format, "Count failed", "3", fmt.Sprintf("%d", count)) 498 | } 499 | 500 | // This should return 2 - test limit ignored 501 | count, err = PagesQuery().Where("id < 3").Order("id desc").Limit(100).Count() 502 | if err != nil || count != 2 { 503 | t.Fatalf(Format, "Count id < 3 failed", "2", fmt.Sprintf("%d", count)) 504 | } 505 | 506 | // This should return 0 507 | count, err = PagesQuery().Where("id > 3").Count() 508 | if err != nil || count != 0 { 509 | t.Fatalf(Format, "Count id > 3 failed", "0", fmt.Sprintf("%d", count)) 510 | } 511 | 512 | // Test retrieving an array, then counting, then where 513 | // This should work 514 | q := PagesQuery().Where("id > ?", 1).Order("id desc") 515 | 516 | count, err = q.Count() 517 | if err != nil || count != 2 { 518 | t.Fatalf(Format, "Count id > 1 failed", "2", fmt.Sprintf("%d", count), err) 519 | } 520 | 521 | // Reuse same query to get array after count 522 | var models []*Page 523 | models, err = PagesFindAll(q) 524 | if err != nil || len(models) != 2 { 525 | t.Fatalf(Format, "Where Array after count", "len 2", err) 526 | } 527 | 528 | } 529 | 530 | func TestMysqlWhere(t *testing.T) { 531 | 532 | var models []*Page 533 | q := PagesQuery().Where("id > ?", 1) 534 | models, err := PagesFindAll(q) 535 | if err != nil || len(models) != 2 { 536 | t.Fatalf(Format, "Where Array", "len 2", fmt.Sprintf("%d", len(models))) 537 | } 538 | 539 | } 540 | 541 | func TestMysqlOrder(t *testing.T) { 542 | 543 | // Look for pages in reverse order 544 | var models []*Page 545 | q := PagesQuery().Where("id > 1").Order("id desc") 546 | models, err := PagesFindAll(q) 547 | if err != nil || len(models) == 0 { 548 | t.Fatalf(Format, "Order test id desc", "3", fmt.Sprintf("%d", len(models))) 549 | return 550 | } 551 | 552 | p := models[0] 553 | if p.ID != 3 { 554 | t.Fatalf(Format, "Order test id desc", "3", fmt.Sprintf("%d", p.ID)) 555 | 556 | } 557 | 558 | // Look for pages in right order 559 | q = PagesQuery().Where("id < ?", 10).Where("id < ?", 100).Order("id asc") 560 | models, err = PagesFindAll(q) 561 | if err != nil || models == nil { 562 | t.Fatalf(Format, "Order test id asc", "1", err) 563 | } 564 | 565 | p = models[0] 566 | if p.ID != 1 { 567 | t.Fatalf(Format, "Order test id asc", "1", fmt.Sprintf("%d", p.ID)) 568 | 569 | } 570 | 571 | } 572 | 573 | func TestMysqlSelect(t *testing.T) { 574 | 575 | var models []*Page 576 | q := PagesQuery().Select("SELECT id,title from pages").Order("id asc") 577 | models, err := PagesFindAll(q) 578 | if err != nil || len(models) == 0 { 579 | t.Fatalf(Format, "Select error on id,title", "id,title", err) 580 | } 581 | p := models[0] 582 | if p.ID != 1 || p.Title != "Title 1." || len(p.Text) > 0 { 583 | t.Fatalf(Format, "Select id,title", "id,title only", p) 584 | } 585 | 586 | } 587 | 588 | func TestMysqlUpdate(t *testing.T) { 589 | 590 | p, err := PagesFind(3) 591 | if err != nil { 592 | t.Fatalf(Format, "Update could not find model err", "id-3", err) 593 | } 594 | 595 | // Should really test updates with several strings here 596 | // Update each model with a different string 597 | // This does also check if AllowedParams is working properly to clean params 598 | err = p.Update(map[string]string{"title": "UPDATE 1"}) 599 | if err != nil { 600 | t.Fatalf(Format, "Error after update", "updated", err) 601 | } 602 | 603 | // Check it is modified 604 | p, err = PagesFind(3) 605 | 606 | if err != nil { 607 | t.Fatalf(Format, "Error after update 1", "updated", err) 608 | } 609 | 610 | if p.Title != "UPDATE 1" { 611 | t.Fatalf(Format, "Error after update 1 - Not updated properly", "UPDATE 1", p.Title) 612 | } 613 | 614 | } 615 | 616 | // Some more damaging operations we execute at the end, 617 | // to avoid having to reload the db for each test 618 | 619 | func TestMysqlUpdateAll(t *testing.T) { 620 | 621 | err := PagesQuery().UpdateAll(map[string]string{"title": "test me"}) 622 | if err != nil { 623 | t.Fatalf(Format, "UPDATE ALL err", "udpate all records", err) 624 | } 625 | 626 | // Check we have all pages with same title 627 | count, err := PagesQuery().Where("title=?", "test me").Count() 628 | 629 | if err != nil || count != 3 { 630 | t.Fatalf(Format, "Count after update all", "3", fmt.Sprintf("%d", count)) 631 | } 632 | 633 | } 634 | 635 | func TestMysqlCreate(t *testing.T) { 636 | 637 | params := map[string]string{ 638 | "title": "Test 98", 639 | "text": "My text", 640 | "created_at": "REPLACE ME", 641 | "summary": "me", 642 | } 643 | 644 | // if your model is in a package, it could be pages.Create() 645 | // For now to mock we just use an empty page 646 | id, err := (&Page{}).Create(params) 647 | if err != nil { 648 | t.Fatalf(Format, "Err on create", err) 649 | } 650 | 651 | // Now find the page and test it 652 | p, err := PagesFind(id) 653 | if err != nil { 654 | t.Fatalf(Format, "Err on create find", err) 655 | } 656 | 657 | if p.Text != "My text" { 658 | t.Fatalf(Format, "Create page params mismatch", "Creation", p.ID) 659 | } 660 | 661 | // Check we have one left 662 | count, err := PagesQuery().Count() 663 | 664 | if err != nil || count != 4 { 665 | t.Fatalf(Format, "Count after create", "4", fmt.Sprintf("%d", count)) 666 | } 667 | 668 | } 669 | 670 | func TestMysqlDelete(t *testing.T) { 671 | 672 | p, err := PagesFind(3) 673 | if err != nil { 674 | t.Fatalf(Format, "Could not find model err", "id-3", err) 675 | } 676 | err = p.Delete() 677 | if err != nil { 678 | t.Fatalf(Format, "Error after delete", "deleted", err) 679 | } 680 | 681 | // Check it is gone and we get an error on next find 682 | p, err = PagesFind(3) 683 | if !strings.Contains(fmt.Sprintf("%s", err), "No results found") { 684 | t.Fatalf(Format, "Error after delete 1", "1", err) 685 | } 686 | 687 | } 688 | 689 | func TestMysqlDeleteAll(t *testing.T) { 690 | 691 | err := PagesQuery().Where("id > 1").DeleteAll() 692 | if err != nil { 693 | t.Fatalf(Format, "DELETE ALL err", "delete 2 records", err) 694 | } 695 | 696 | // Check we have one left 697 | count, err := PagesQuery().Where("id > 0").Count() 698 | 699 | if err != nil || count != 1 { 700 | t.Fatalf(Format, "Count after delete all", "1", fmt.Sprintf("%d", count)) 701 | } 702 | 703 | } 704 | 705 | func TestMysqlTeardown(t *testing.T) { 706 | 707 | err := CloseDatabase() 708 | if err != nil { 709 | fmt.Println("Close DB ERROR ", err) 710 | } 711 | } 712 | 713 | /* 714 | // See note in adapters/database_sqlite.go for reasons this is disabled 715 | // ---------------------------------- 716 | // SQLITE TESTS 717 | // ---------------------------------- 718 | 719 | func TestSQSetup(t *testing.T) { 720 | 721 | fmt.Println("\n---\nTESTING SQLITE\n---") 722 | 723 | // NB we use binary named sqlite3 - this is the default on OS X 724 | // NB this requires sqlite3 version > 3.7.15 for init alternative would be to echo sql file at end 725 | cmd := exec.Command("sqlite3", "--init", "tests/query_test_sqlite.sql", "tests/query_test.sqlite") 726 | stdout, _ := cmd.StdoutPipe() 727 | stderr, _ := cmd.StderrPipe() 728 | err = cmd.Start() 729 | if err != nil { 730 | fmt.Println("Could not set up sqlite db - ERROR ", err) 731 | os.Exit(1) 732 | } 733 | go io.Copy(os.Stdout, stdout) 734 | go io.Copy(os.Stderr, stderr) 735 | cmd.Wait() 736 | 737 | if err == nil { 738 | _ = strings.Replace("", "", "", -1) 739 | 740 | // Open the database 741 | options := map[string]string{ 742 | "adapter": "sqlite3", 743 | "db": "tests/query_test.sqlite", 744 | "debug": "true", // for more detail on failure, enable debug mode on db 745 | } 746 | 747 | err = OpenDatabase(options) 748 | if err != nil { 749 | fmt.Println("Open database ERROR ", err) 750 | os.Exit(1) 751 | } 752 | 753 | fmt.Println("---\nQuery Testing Sqlite3 - DB setup complete\n---") 754 | } 755 | 756 | } 757 | 758 | func TestSQFind(t *testing.T) { 759 | 760 | // This should work - NB in normal usage this would be query.New 761 | p, err := PagesFind(1) 762 | if err != nil { 763 | t.Fatalf(Format, "Find(1)", "Model object", err) 764 | } 765 | // Check we got the page we expect 766 | if p.ID != 1 { 767 | t.Fatalf(Format, "Find(1) p", "Model object", p) 768 | } 769 | 770 | // This should fail, so we check that 771 | p, err = PagesFind(11) 772 | if err == nil || p != nil { 773 | t.Fatalf(Format, "Find(11)", "Model object", err) 774 | } 775 | 776 | } 777 | 778 | func TestSQCount(t *testing.T) { 779 | 780 | // This should return 3 781 | count, err := PagesQuery().Count() 782 | if err != nil || count != 3 { 783 | t.Fatalf(Format, "Count failed", "3", fmt.Sprintf("%d", count)) 784 | } 785 | 786 | // This should return 2 - test limit ignored 787 | count, err = PagesQuery().Where("id in (?,?)", 1, 2).Order("id desc").Limit(100).Count() 788 | if err != nil || count != 2 { 789 | t.Fatalf(Format, "Count id < 3 failed", "2", fmt.Sprintf("%d", count)) 790 | } 791 | 792 | // This should return 0 793 | count, err = PagesQuery().Where("id > 3").Count() 794 | if err != nil || count != 0 { 795 | t.Fatalf(Format, "Count id > 3 failed", "0", fmt.Sprintf("%d", count)) 796 | } 797 | 798 | // Test retrieving an array, then counting, then where 799 | // This should work 800 | q := PagesQuery().Where("id > ?", 1).Order("id desc") 801 | 802 | count, err = q.Count() 803 | if err != nil || count != 2 { 804 | t.Fatalf(Format, "Count id > 1 failed", "2", fmt.Sprintf("%d", count), err) 805 | } 806 | 807 | // Reuse same query to get array after count 808 | results, err := q.Results() 809 | if err != nil || len(results) != 2 { 810 | t.Fatalf(Format, "Where Array after count", "len 2", err) 811 | } 812 | 813 | } 814 | 815 | func TestSQWhere(t *testing.T) { 816 | 817 | q := PagesQuery().Where("id > ?", 1) 818 | pages, err := PagesFindAll(q) 819 | 820 | if err != nil || len(pages) != 2 { 821 | t.Fatalf(Format, "Where Array", "len 2", fmt.Sprintf("%d", len(pages))) 822 | } 823 | 824 | } 825 | 826 | func TestSQOrder(t *testing.T) { 827 | 828 | // Look for pages in reverse order 829 | var models []*Page 830 | q := PagesQuery().Where("id > 0").Order("id desc") 831 | models, err := PagesFindAll(q) 832 | 833 | if err != nil || len(models) == 0 { 834 | t.Fatalf(Format, "Order count test id desc", "3", fmt.Sprintf("%d", len(models))) 835 | return 836 | } 837 | 838 | p := models[0] 839 | if p.ID != 3 { 840 | t.Fatalf(Format, "Order test id desc 1", "3", fmt.Sprintf("%d", p.ID)) 841 | return 842 | } 843 | 844 | // Look for pages in right order - reset models 845 | q = PagesQuery().Where("id < ?", 10).Where("id < ?", 100).Order("id asc") 846 | models, err = PagesFindAll(q) 847 | // fmt.Println("TESTING MODELS %v",models) 848 | 849 | if err != nil || models == nil { 850 | t.Fatalf(Format, "Order test id asc count", "1", err) 851 | } 852 | 853 | p = models[0] 854 | if p.ID != 1 { 855 | t.Fatalf(Format, "Order test id asc 1", "1", fmt.Sprintf("%d", p.ID)) 856 | return 857 | } 858 | 859 | } 860 | 861 | func TestSQSelect(t *testing.T) { 862 | 863 | var models []*Page 864 | q := PagesQuery().Select("SELECT id,title from pages").Order("id asc") 865 | models, err := PagesFindAll(q) 866 | if err != nil || len(models) == 0 { 867 | t.Fatalf(Format, "Select error on id,title", "id,title", err) 868 | } 869 | p := models[0] 870 | if p.ID != 1 || p.Title != "Title 1." || len(p.Text) > 0 { 871 | t.Fatalf(Format, "Select id,title", "id,title only", p) 872 | } 873 | 874 | } 875 | 876 | func TestSQUpdate(t *testing.T) { 877 | 878 | p, err := PagesFind(3) 879 | if err != nil { 880 | t.Fatalf(Format, "Update could not find model err", "id-3", err) 881 | } 882 | 883 | // Should really test updates with several strings here 884 | err = p.Update(map[string]string{"title": "UPDATE 1", "summary": "Test summary"}) 885 | 886 | // Check it is modified 887 | p, err = PagesFind(3) 888 | 889 | if err != nil { 890 | t.Fatalf(Format, "Error after update 1", "updated", err) 891 | } 892 | 893 | if p.Title != "UPDATE 1" { 894 | t.Fatalf(Format, "Error after update 1 - Not updated properly", "UPDATE 1", p.Title) 895 | } 896 | 897 | } 898 | 899 | // Some more damaging operations we execute at the end, 900 | // to avoid having to reload the db for each test 901 | 902 | func TestSQUpdateAll(t *testing.T) { 903 | 904 | err := PagesQuery().UpdateAll(map[string]string{"title": "test me"}) 905 | if err != nil { 906 | t.Fatalf(Format, "UPDATE ALL err", "udpate all records", err) 907 | } 908 | 909 | // Check we have all pages with same title 910 | count, err := PagesQuery().Where("title=?", "test me").Count() 911 | 912 | if err != nil || count != 3 { 913 | t.Fatalf(Format, "Count after update all", "3", fmt.Sprintf("%d", count)) 914 | } 915 | 916 | } 917 | 918 | func TestSQCreate(t *testing.T) { 919 | 920 | params := map[string]string{ 921 | "title": "Test 98", 922 | "text": "My text", 923 | "created_at": "REPLACE ME", 924 | "summary": "me", 925 | } 926 | 927 | // if your model is in a package, it could be pages.Create() 928 | // For now to mock we just use an empty page 929 | id, err := (&Page{}).Create(params) 930 | if err != nil { 931 | t.Fatalf(Format, "Err on create", err) 932 | } 933 | 934 | // Now find the page and test it 935 | p, err := PagesFind(id) 936 | if err != nil { 937 | t.Fatalf(Format, "Err on create find", err) 938 | } 939 | 940 | if p.Title != "Test 98" { 941 | t.Fatalf(Format, "Create page params mismatch", "Creation", p.ID) 942 | } 943 | 944 | // Check we have one left 945 | count, err := PagesQuery().Count() 946 | 947 | if err != nil || count != 4 { 948 | t.Fatalf(Format, "Count after create", "4", fmt.Sprintf("%d", count)) 949 | } 950 | 951 | } 952 | 953 | func TestSQDelete(t *testing.T) { 954 | 955 | p, err := PagesFind(3) 956 | if err != nil { 957 | t.Fatalf(Format, "Could not find model err", "id-3", err) 958 | } 959 | 960 | err = p.Delete() 961 | 962 | // Check it is gone and we get an error on next find 963 | p, err = PagesFind(3) 964 | 965 | if !strings.Contains(fmt.Sprintf("%s", err), "No results found") { 966 | t.Fatalf(Format, "Error after delete 1", "1", err) 967 | } 968 | 969 | } 970 | 971 | func TestSQDeleteAll(t *testing.T) { 972 | 973 | err := PagesQuery().Where("id > 1").DeleteAll() 974 | if err != nil { 975 | t.Fatalf(Format, "DELETE ALL err", "delete 2 records", err) 976 | } 977 | 978 | // Check we have one left 979 | count, err := PagesQuery().Where("id > 0").Count() 980 | 981 | if err != nil || count != 1 { 982 | t.Fatalf(Format, "Count after delete all", "1", fmt.Sprintf("%d", count)) 983 | } 984 | 985 | } 986 | 987 | func TestSQTeardown(t *testing.T) { 988 | 989 | err := CloseDatabase() 990 | if err != nil { 991 | fmt.Println("Close DB ERROR ", err) 992 | } 993 | } 994 | */ 995 | -------------------------------------------------------------------------------- /tests/query_test.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fragmenta/query/3f6e1c2131ff6086f96c0342da6f0b111541dc6d/tests/query_test.sqlite -------------------------------------------------------------------------------- /tests/query_test_mysql.sql: -------------------------------------------------------------------------------- 1 | /* Database created using mysql for tests - query_test */ 2 | DROP TABLE pages; 3 | CREATE TABLE pages ( 4 | id integer NOT NULL PRIMARY KEY AUTO_INCREMENT, 5 | title text, 6 | text text, 7 | keywords text, 8 | status integer, 9 | created_at timestamp NOT NULL, 10 | updated_at timestamp NOT NULL, 11 | url text, 12 | summary text 13 | ); 14 | 15 | insert into pages VALUES(1,'Title 1.','test 1 text','keywords1',100,NOW(),NOW(),'test.example.com',''); 16 | insert into pages VALUES(2,'Title 2','test 2 text','keywords 2',100,NOW(),NOW(),'test.example.com',''); 17 | insert into pages VALUES(3,'Title 3 here','test 3 text','keywords,3',100,NOW(),NOW(),'test.example.com',''); -------------------------------------------------------------------------------- /tests/query_test_pq.sql: -------------------------------------------------------------------------------- 1 | /* Database created using psql for tests - query_test */ 2 | DROP TABLE pages; 3 | CREATE TABLE pages ( 4 | id SERIAL NOT NULL, 5 | title text, 6 | text text, 7 | keywords text, 8 | status integer, 9 | created_at timestamp NOT NULL, 10 | updated_at timestamp NOT NULL, 11 | url text, 12 | summary text 13 | ); 14 | 15 | 16 | insert into pages (title,text,keywords,status,created_at,updated_at,url,summary) VALUES('Title 1.','test 1 text','keywords1',100,NOW(),NOW(),'test.example.com',''); 17 | insert into pages (title,text,keywords,status,created_at,updated_at,url,summary) VALUES('Title 2','test 2 text','keywords 2',100,NOW(),NOW(),'test.example.com',''); 18 | insert into pages (title,text,keywords,status,created_at,updated_at,url,summary) VALUES('Title 3 here','test 3 text','keywords,3',100,NOW(),NOW(),'test.example.com',''); 19 | -------------------------------------------------------------------------------- /tests/query_test_sqlite.sql: -------------------------------------------------------------------------------- 1 | /* Database created using sqlite for tests - query_test */ 2 | DROP TABLE pages; 3 | CREATE TABLE pages ( 4 | id integer NOT NULL PRIMARY KEY, 5 | title text, 6 | text text, 7 | keywords text, 8 | status integer, 9 | created_at timestamp NOT NULL, 10 | updated_at timestamp NOT NULL, 11 | url text, 12 | summary text 13 | ); 14 | 15 | 16 | insert into pages VALUES(1,'Title 1.','test 1 text','keywords1',100,'2013-03-18 12:18:50.447 +0000','2013-03-18 12:18:50.447 +0000','test.example.com',''); 17 | insert into pages VALUES(2,'Title 2','test 2 text','keywords 2',100,'2013-03-18 12:18:50.447 +0000','2013-03-18 12:18:50.447 +0000','test.example.com',''); 18 | insert into pages VALUES(3,'Title 3 here','test 3 text','keywords,3',100,'2013-03-18 12:18:50.447 +0000','2013-03-18 12:18:50.447 +0000','test.example.com',''); -------------------------------------------------------------------------------- /textual.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | ) 7 | 8 | // Truncate the given string to length using … as ellipsis. 9 | func Truncate(s string, length int) string { 10 | return TruncateWithEllipsis(s, length, "…") 11 | } 12 | 13 | // TruncateWithEllipsis truncates the given string to length using provided ellipsis. 14 | func TruncateWithEllipsis(s string, length int, ellipsis string) string { 15 | 16 | l := len(s) 17 | el := len(ellipsis) 18 | if l+el > length { 19 | s = string(s[0:length-el]) + ellipsis 20 | } 21 | return s 22 | } 23 | 24 | // ToPlural returns the plural version of an English word 25 | // using some simple rules and a table of exceptions. 26 | func ToPlural(text string) (plural string) { 27 | 28 | // We only deal with lowercase 29 | word := strings.ToLower(text) 30 | 31 | // Check translations first, and return a direct translation if there is one 32 | if translations[word] != "" { 33 | return translations[word] 34 | } 35 | 36 | // If we have no translation, just follow some basic rules - avoid new rules if possible 37 | if strings.HasSuffix(word, "s") || strings.HasSuffix(word, "z") || strings.HasSuffix(word, "h") { 38 | plural = word + "es" 39 | } else if strings.HasSuffix(word, "y") { 40 | plural = strings.TrimRight(word, "y") + "ies" 41 | } else if strings.HasSuffix(word, "um") { 42 | plural = strings.TrimRight(word, "um") + "a" 43 | } else { 44 | plural = word + "s" 45 | } 46 | 47 | return plural 48 | } 49 | 50 | // common transformations from singular to plural 51 | // Which irregulars are important or correct depends on your usage of English 52 | // Some of those below are now considered old-fashioned and many more could be added 53 | // As this is used for database models, it only needs a limited subset of all irregulars 54 | // NB you should not attempt to reverse and singularize, but just use the singular provided 55 | var translations = map[string]string{ 56 | "hero": "heroes", 57 | "supernova": "supernovae", 58 | "day": "days", 59 | "monkey": "monkeys", 60 | "money": "monies", 61 | "chassis": "chassis", 62 | "sheep": "sheep", 63 | "aircraft": "aircraft", 64 | "fish": "fish", 65 | "nucleus": "nuclei", 66 | "mouse": "mice", 67 | "buffalo": "buffalo", 68 | "species": "species", 69 | "information": "information", 70 | "wife": "wives", 71 | "shelf": "shelves", 72 | "index": "indices", 73 | "matrix": "matrices", 74 | "formula": "formulae", 75 | "millennium": "millennia", 76 | "ganglion": "ganglia", 77 | "octopus": "octopodes", 78 | "man": "men", 79 | "woman": "women", 80 | "person": "people", 81 | "axis": "axes", 82 | "die": "dice", 83 | // ..etc 84 | } 85 | 86 | // ToSingular converts a word to singular. 87 | // NB reversal from plurals may fail 88 | func ToSingular(word string) (singular string) { 89 | 90 | if strings.HasSuffix(word, "ses") || strings.HasSuffix(word, "zes") || strings.HasSuffix(word, "hes") { 91 | singular = strings.TrimRight(word, "es") 92 | } else if strings.HasSuffix(word, "ies") { 93 | singular = strings.TrimRight(word, "ies") + "y" 94 | } else if strings.HasSuffix(word, "a") { 95 | singular = strings.TrimRight(word, "a") + "um" 96 | } else { 97 | singular = strings.TrimRight(word, "s") 98 | } 99 | 100 | return singular 101 | } 102 | 103 | // ToSnake converts a string from struct field names to corresponding database column names (e.g. FieldName to field_name). 104 | func ToSnake(text string) string { 105 | b := bytes.NewBufferString("") 106 | for i, c := range text { 107 | if i > 0 && c >= 'A' && c <= 'Z' { 108 | b.WriteRune('_') 109 | } 110 | b.WriteRune(c) 111 | } 112 | return strings.ToLower(b.String()) 113 | } 114 | 115 | // ToCamel converts a string from database column names to corresponding struct field names (e.g. field_name to FieldName). 116 | func ToCamel(text string, private ...bool) string { 117 | lowerCamel := false 118 | if private != nil { 119 | lowerCamel = private[0] 120 | } 121 | b := bytes.NewBufferString("") 122 | s := strings.Split(text, "_") 123 | for i, v := range s { 124 | if len(v) > 0 { 125 | s := v[:1] 126 | if i > 0 || lowerCamel == false { 127 | s = strings.ToUpper(s) 128 | } 129 | b.WriteString(s) 130 | b.WriteString(v[1:]) 131 | } 132 | } 133 | return b.String() 134 | } 135 | --------------------------------------------------------------------------------