diff --git a/config.go b/config.go index 26cb1523f8c12786bae5734c41736c0e3d6bb16c..f0e34e8249e83f3c79ea5a6dc0f7f5e8e8f5e27c 100644 --- a/config.go +++ b/config.go @@ -1,3 +1,24 @@ +// Copyright (c) 2012-present The upper.io/db authors. All rights reserved. +// +// 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 db import ( @@ -5,8 +26,7 @@ import ( "sync/atomic" ) -// Settings defines methods to get or set configuration settings, use db.Conf -// to get or set global configuration settings. +// Settings defines methods to get or set configuration values. type Settings interface { // SetLogging enables or disables logging. SetLogging(bool) @@ -15,7 +35,7 @@ type Settings interface { // SetLogger defines which logger to use. SetLogger(Logger) - // Returns the configured logger. + // Returns the currently configured logger. Logger() Logger } @@ -60,5 +80,5 @@ func (c *conf) LoggingEnabled() bool { return false } -// Conf has global configuration settings for upper-db. +// Conf provides global configuration settings for upper-db. var Conf Settings = &conf{} diff --git a/db.go b/db.go index 877c300d4e7944115697da0ab3f94d4cfce95fb8..a5de8ad5f7b162cb5beb21030c470746ab4f410b 100644 --- a/db.go +++ b/db.go @@ -19,12 +19,12 @@ // 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 db provides a common interface to work with different data sources -// using adapters that wrap mature database drivers. +// Package db (or upper-db) provides a common interface to work with different +// data sources using adapters that wrap mature database drivers. // -// The main purpose of db is to abstract common database operations and +// The main purpose of upper-db is to abstract common database operations and // encourage users perform advanced operations directly using the underlying -// driver. db supports the MySQL, PostgreSQL, SQLite and QL databases and +// driver. upper-db supports the MySQL, PostgreSQL, SQLite and QL databases and // provides partial support (CRUD, no transactions) for MongoDB. // // go get upper.io/db.v2 @@ -73,7 +73,7 @@ // } // // See more usage examples and documentation for users at -// http://beta.upper.io/db.v2. +// https://upper.io/db.v2. package db // import "upper.io/db.v2" import ( @@ -82,22 +82,38 @@ import ( "time" ) -// Constraint interface represents a condition. +// Constraint interface represents a single condition, like "a = 1". where `a` +// is the key and `1` is the value. This is an exported interface but it's +// rarely used directly, you may want to use the `db.Cond{}` map instead. type Constraint interface { + // Key is the leftmost part of the constraint and usually contains a column + // name. Key() interface{} + + // Value if the rightmost part of the constraint and usually contains a + // column value. Value() interface{} } -// Constraints interface represents an array of constraints. +// Constraints interface represents an array or constraints, like "a = 1, b = +// 2, c = 3". type Constraints interface { + // Constraints returns an array of constraints. Constraints() []Constraint } -// Compound represents an statement that has one or many sentences joined by a -// compound operator, like AND or OR. +// Compound represents an statement that has one or many sentences joined by by +// an operator like "AND" or "OR". This is an exported interface but it's +// rarely used directly, you may want to use the `db.And()` or `db.Or()` +// functions instead. type Compound interface { + // Sentences returns child sentences. Sentences() []Compound + + // Operator returns the operator that joins the compound's child sentences. Operator() CompoundOperator + + // Empty returns true if the compound has zero children, false otherwise. Empty() bool } @@ -111,34 +127,49 @@ const ( OperatorOr ) -// RawValue interface represents values that can bypass SQL filters. Use with -// care. +// RawValue interface represents values that can bypass SQL filters. This is an +// exported interface but it's rarely used directly, you may want to use +// the `db.Raw()` function instead. type RawValue interface { fmt.Stringer Compound + + // Raw returns the string representation of the value that the user wants to + // pass without any escaping. Raw() string + + // Arguments returns the arguments to be replaced on the query. Arguments() []interface{} } // Function interface defines methods for representing database functions. +// This is an exported interface but it's rarely used directly, you may want to +// use the `db.Func()` function instead. type Function interface { + // Name returns the function name. Name() string + + // Argument returns the function arguments. Arguments() []interface{} } -// Marshaler is the interface implemented by struct fields that can marshal -// themselves into a value that can be saved to a database. +// Marshaler is the interface implemented by struct fields that can transform +// themselves into values that can be stored on a database. type Marshaler interface { + // MarshalDB returns the internal database representation of the Go value. MarshalDB() (interface{}, error) } -// Unmarshaler is the interface implemented by struct fields that can unmarshal -// themselves from database data into a Go value. +// Unmarshaler is the interface implemented by struct fields that can transform +// themselves from stored database values into Go values. type Unmarshaler interface { + // UnmarshalDB receives an internal database representation of a value and + // must transform that into a Go value. UnmarshalDB(interface{}) error } -// Cond is a map that defines conditions for a query. +// Cond is a map that defines conditions for a query and satisfies the +// Constraints and Compound interfaces. // // Each entry of the map represents a condition (a column-value relation bound // by a comparison operator). The comparison operator is optional and can be @@ -155,7 +186,8 @@ type Unmarshaler interface { // // Where id is in a list of ids. // db.Cond{"id IN": []{1, 2, 3}} // -// // Where age is lower than 18 (mongodb-like operator). +// // Where age is lower than 18 (you could use this syntax when using +// // mongodb). // db.Cond{"age $lt": 18} // // // Where age > 32 and age < 35 @@ -253,7 +285,7 @@ func (c *compound) push(a ...Compound) *compound { return c } -// Union represents an OR compound. +// Union represents a compound joined by OR. type Union struct { *compound } @@ -285,7 +317,7 @@ func (a *Intersection) Empty() bool { return a.compound.Empty() } -// Intersection represents an AND compound. +// Intersection represents a compound joined by AND. type Intersection struct { *compound } @@ -313,7 +345,7 @@ func NewConstraint(key interface{}, value interface{}) Constraint { return constraint{k: key, v: value} } -// Func represents a database function. +// Func represents a database function and satisfies the db.Function interface. // // Examples: // @@ -398,6 +430,8 @@ func Or(conds ...Compound) *Union { // // // SOUNDEX('Hello') // Raw("SOUNDEX('Hello')") +// +// Raw returns a value that satifies the db.RawValue interface. func Raw(value string, args ...interface{}) RawValue { r := rawValue{v: value, a: nil} if len(args) > 0 { @@ -460,23 +494,24 @@ type Tx interface { // Collection is an interface that defines methods useful for handling tables. type Collection interface { // Insert inserts a new item into the collection, it accepts one argument - // that can be either a map or a struct. When the call is successful, it - // returns the ID of the newly added element as an interface (the underlying - // type of this ID depends on the database adapter). The ID returned by - // Insert() can be passed directly to Find() in order to retrieve the newly - // added element. + // that can be either a map or a struct. If the call suceeds, it returns the + // ID of the newly added element as an `interface{}` (the underlying type of + // this ID is unknown and depends on the database adapter). The ID returned + // by Insert() could be passed directly to Find() to retrieve the newly added + // element. Insert(interface{}) (interface{}, error) // InsertReturning is like Insert() but it updates the passed pointer to map - // or struct with the newly inserted element. This is all done automically - // within a transaction. If the database does not support transactions this - // method returns db.ErrUnsupported. + // or struct with the newly inserted element (and with automatic fields, like + // IDs, timestamps, etc). This is all done atomically within a transaction. + // If the database does not support transactions this method returns + // db.ErrUnsupported. InsertReturning(interface{}) error - // Exists returns true if the collection exists. + // Exists returns true if the collection exists, false otherwise. Exists() bool - // Find returns a result set with the given filters. + // Find defines a new result set with elements from the collection. Find(...interface{}) Result // Truncate removes all elements on the collection and resets the @@ -511,7 +546,7 @@ type Result interface { // set. Select(...interface{}) Result - // Where discards the initial filtering conditions and sets new ones. + // Where resets the initial filtering conditions and sets new ones. Where(...interface{}) Result // Group is used to group results that have the same value in the same column @@ -551,18 +586,24 @@ type Result interface { // using All(). All(sliceOfStructs interface{}) error - // Close closes the result set. + // Close closes the result set and frees all locked resources. Close() error } -// ConnectionURL represents a connection string +// ConnectionURL represents a connection string. type ConnectionURL interface { // String returns the connection string that is going to be passed to the // adapter. String() string } -// Ensure interface compatibility +// Default limits for database/sql limit methods. +var ( + DefaultConnMaxLifetime = time.Duration(0) // 0 means reuse forever. + DefaultMaxIdleConns = 10 // Keep 10 idle connections. + DefaultMaxOpenConns = 0 // 0 means unlimited. +) + var ( _ Function = &dbFunc{} _ Constraints = Cond{} @@ -570,10 +611,3 @@ var ( _ Constraint = &constraint{} _ RawValue = &rawValue{} ) - -// Default limits for database/sql limit methods. -var ( - DefaultConnMaxLifetime = time.Duration(0) // 0 means reuse forever. - DefaultMaxIdleConns = 10 // Keep 10 idle connections. - DefaultMaxOpenConns = 0 // 0 means unlimited. -) diff --git a/env.go b/env.go index 6ff0cd39ad4ac724a61d31ba6f0821a2bbe9c8a4..70cbd63c07d4a211b63fa1954cb5bf9713dc349a 100644 --- a/env.go +++ b/env.go @@ -1,3 +1,24 @@ +// Copyright (c) 2012-present The upper.io/db authors. All rights reserved. +// +// 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 db import ( diff --git a/errors.go b/errors.go index afccfc4c5e0609c3f288c3fd610df0f06f9398fb..e6b160110b6631ead0830c476a147ea507c2193b 100644 --- a/errors.go +++ b/errors.go @@ -25,7 +25,7 @@ import ( "errors" ) -// Shared error messages. +// Error messages. var ( ErrNoMoreRows = errors.New(`upper: no more rows in this result set`) ErrNotConnected = errors.New(`upper: you're currently not connected`) diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 826a5902434244f2dffc5d7a04707d062493f1ab..d81cebe246f6ff0f768cee0d986dbd4d66afbf71 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -83,8 +83,8 @@ func (c *Cache) Read(h Hashable) (string, bool) { // does not exists returns nil and false. func (c *Cache) ReadRaw(h Hashable) (interface{}, bool) { c.mu.RLock() + defer c.mu.RUnlock() data, ok := c.cache[h.Hash()] - c.mu.RUnlock() if ok { return data.Value.(*item).value, true } @@ -106,7 +106,7 @@ func (c *Cache) Write(h Hashable, value interface{}) { c.cache[key] = c.li.PushFront(&item{key, value}) - if c.li.Len() > c.capacity { + for c.li.Len() > c.capacity { el := c.li.Remove(c.li.Back()) delete(c.cache, el.(*item).key) if p, ok := el.(*item).value.(HasOnPurge); ok { diff --git a/internal/sqladapter/database.go b/internal/sqladapter/database.go index 26e7291fc782b6af2c1bcc75c3120a2c58d435cf..9d7370fd826287eb0ded04893827e7662b98ff44 100644 --- a/internal/sqladapter/database.go +++ b/internal/sqladapter/database.go @@ -19,7 +19,8 @@ var ( lastTxID uint64 ) -// HasCleanUp +// HasCleanUp is implemented by structs that have a clean up routine that needs +// to be called before Close(). type HasCleanUp interface { CleanUp() error } @@ -257,8 +258,8 @@ func (d *database) StatementExec(stmt *exql.Statement, args ...interface{}) (res status.RowsAffected = &rowsAffected } - if lastInsertId, err := res.LastInsertId(); err == nil { - status.LastInsertID = &lastInsertId + if lastInsertID, err := res.LastInsertId(); err == nil { + status.LastInsertID = &lastInsertID } } @@ -266,13 +267,14 @@ func (d *database) StatementExec(stmt *exql.Statement, args ...interface{}) (res }(time.Now()) } - var p *sql.Stmt + var p *Stmt if p, query, err = d.prepareStatement(stmt); err != nil { return nil, err } + defer p.Close() if execer, ok := d.PartialDatabase.(HasStatementExec); ok { - res, err = execer.StatementExec(p, args...) + res, err = execer.StatementExec(p.Stmt, args...) return } @@ -298,10 +300,11 @@ func (d *database) StatementQuery(stmt *exql.Statement, args ...interface{}) (ro }(time.Now()) } - var p *sql.Stmt + var p *Stmt if p, query, err = d.prepareStatement(stmt); err != nil { return nil, err } + defer p.Close() rows, err = p.Query(args...) return @@ -326,10 +329,11 @@ func (d *database) StatementQueryRow(stmt *exql.Statement, args ...interface{}) }(time.Now()) } - var p *sql.Stmt + var p *Stmt if p, query, err = d.prepareStatement(stmt); err != nil { return nil, err } + defer p.Close() row, err = p.QueryRow(args...), nil return @@ -347,37 +351,33 @@ func (d *database) Driver() interface{} { // prepareStatement converts a *exql.Statement representation into an actual // *sql.Stmt. This method will attempt to used a cached prepared statement, if // available. -func (d *database) prepareStatement(stmt *exql.Statement) (*sql.Stmt, string, error) { +func (d *database) prepareStatement(stmt *exql.Statement) (*Stmt, string, error) { if d.sess == nil && d.Transaction() == nil { return nil, "", db.ErrNotConnected } pc, ok := d.cachedStatements.ReadRaw(stmt) - if ok { // The statement was cached. - ps := pc.(*cachedStatement) - return ps.Stmt, ps.query, nil + ps := pc.(*Stmt).Open() + return ps, ps.query, nil } // Plain SQL query. query := d.PartialDatabase.CompileStatement(stmt) - var p *sql.Stmt - var err error - - if d.Transaction() != nil { - p, err = d.Transaction().(*sqlTx).Prepare(query) - } else { - p, err = d.sess.Prepare(query) - } - + sqlStmt, err := func() (*sql.Stmt, error) { + if d.Transaction() != nil { + return d.Transaction().(*sqlTx).Prepare(query) + } + return d.sess.Prepare(query) + }() if err != nil { return nil, query, err } - d.cachedStatements.Write(stmt, &cachedStatement{p, query}) - + p := NewStatement(sqlStmt, query) + d.cachedStatements.Write(stmt, p) return p, query, nil } diff --git a/internal/sqladapter/statement.go b/internal/sqladapter/statement.go index 51e4f1d7f0fbecf3996a29483e09b2c504750d83..be520be8fbf69f4f0b7bda247d944a3784f355a3 100644 --- a/internal/sqladapter/statement.go +++ b/internal/sqladapter/statement.go @@ -2,15 +2,51 @@ package sqladapter import ( "database/sql" + "sync/atomic" ) -// cachedStatement represents a *sql.Stmt that is cached and provides the +// Stmt represents a *sql.Stmt that is cached and provides the // OnPurge method to allow it to clean after itself. -type cachedStatement struct { +type Stmt struct { *sql.Stmt + query string + + count int64 + dead int32 +} + +// NewStatement creates an returns an opened statement +func NewStatement(stmt *sql.Stmt, query string) *Stmt { + return &Stmt{ + Stmt: stmt, + query: query, + count: 1, + } +} + +// Open marks the statement as in-use +func (c *Stmt) Open() *Stmt { + atomic.AddInt64(&c.count, 1) + return c +} + +// Close closes the underlying statement if no other go-routine is using it. +func (c *Stmt) Close() { + if atomic.AddInt64(&c.count, -1) > 0 { + // There are another goroutines using this statement so we don't want to + // close it for real. + return + } + if atomic.LoadInt32(&c.dead) > 0 { + // Statement is dead and we can close it for real. + c.Stmt.Close() + } } -func (c *cachedStatement) OnPurge() { - c.Stmt.Close() +// OnPurge marks the statement as ready to be cleaned up. +func (c *Stmt) OnPurge() { + // Mark as dead, you can continue using it but it will be closed for real + // when c.count reaches 0. + atomic.StoreInt32(&c.dead, 1) } diff --git a/internal/sqladapter/testing/adapter.go.tpl b/internal/sqladapter/testing/adapter.go.tpl index 92b2e89974a7cf18ef88c23e1444cbc9f8467527..6cc8dc27c03e8259bb792de77705bc029077d353 100644 --- a/internal/sqladapter/testing/adapter.go.tpl +++ b/internal/sqladapter/testing/adapter.go.tpl @@ -1370,6 +1370,34 @@ func TestBuilder(t *testing.T) { assert.NotZero(t, all) } +func TestStressPreparedStatementCache(t *testing.T) { + sess := mustOpen() + defer sess.Close() + + var tMu sync.Mutex + tFatal := func(err error) { + tMu.Lock() + defer tMu.Unlock() + t.Fatal(err) + } + + var wg sync.WaitGroup + + for i := 1; i < 1000; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + res := sess.Collection("artist").Find().Select(db.Raw(fmt.Sprintf("COUNT(%d)", i%5))) + var data map[string]interface{} + if err := res.One(&data); err != nil { + tFatal(err) + } + }(i) + } + + wg.Wait() +} + func TestExhaustConnectionPool(t *testing.T) { if Adapter == "ql" { t.Skip("Currently not supported.") diff --git a/internal/sqladapter/tx.go b/internal/sqladapter/tx.go index ee34446a746884e51c29c14eef21ab26b674b197..e1a5dcd8d518f563cc312a68831b5b301d46b4f6 100644 --- a/internal/sqladapter/tx.go +++ b/internal/sqladapter/tx.go @@ -29,7 +29,7 @@ import ( "upper.io/db.v2/lib/sqlbuilder" ) -// Tx represents a database session within a transaction. +// DatabaseTx represents a database session within a transaction. type DatabaseTx interface { BaseDatabase BaseTx @@ -98,6 +98,7 @@ func (t *txWrapper) Rollback() error { return t.BaseTx.Rollback() } +// RunTx creates a transaction context and runs fn within it. func RunTx(d sqlbuilder.Database, fn func(tx sqlbuilder.Tx) error) error { tx, err := d.NewTx() if err != nil { diff --git a/lib/sqlbuilder/builder.go b/lib/sqlbuilder/builder.go index 03abd592bc7d9b8637c3ccbc10ab3e0c8d6acb2e..9130dba003c7845566997f3a3c58ae247c54abad 100644 --- a/lib/sqlbuilder/builder.go +++ b/lib/sqlbuilder/builder.go @@ -15,6 +15,7 @@ import ( "upper.io/db.v2/lib/reflectx" ) +// MapOptions represents options for the mapper. type MapOptions struct { IncludeZeroed bool IncludeNil bool diff --git a/lib/sqlbuilder/wrapper.go b/lib/sqlbuilder/wrapper.go index b3d9f1883572ed28393e4f2ddefed8b2272633f5..6214a6c6aa05a7cfb7711d2ed0d3ce917de08fea 100644 --- a/lib/sqlbuilder/wrapper.go +++ b/lib/sqlbuilder/wrapper.go @@ -69,6 +69,8 @@ type Database interface { Tx(fn func(sess Tx) error) error } +// AdapterFuncMap is a struct that defines a set of functions that adapters +// need to provide. type AdapterFuncMap struct { New func(sqlDB *sql.DB) (Database, error) NewTx func(sqlTx *sql.Tx) (Tx, error) @@ -108,14 +110,17 @@ func adapter(name string) AdapterFuncMap { return missingAdapter(name) } +// Open opens a SQL database. func Open(adapterName string, settings db.ConnectionURL) (Database, error) { return adapter(adapterName).Open(settings) } +// New wraps an active *sql.DB session. func New(adapterName string, sqlDB *sql.DB) (Database, error) { return adapter(adapterName).New(sqlDB) } +// NewTx wraps an active *sql.Tx transation. func NewTx(adapterName string, sqlTx *sql.Tx) (Tx, error) { return adapter(adapterName).NewTx(sqlTx) } diff --git a/logger.go b/logger.go index f346598ea6ab8d94df698688ce184a73caf844cc..1703fb5828c3ceaaee5f4b75c63fd70aea38256c 100644 --- a/logger.go +++ b/logger.go @@ -1,3 +1,24 @@ +// Copyright (c) 2012-present The upper.io/db authors. All rights reserved. +// +// 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 db import ( @@ -24,7 +45,7 @@ var ( reColumnCompareExclude = regexp.MustCompile(`[^a-zA-Z0-9]`) ) -// QueryStatus represents a query after being executed. +// QueryStatus represents the status of a query after being executed. type QueryStatus struct { SessID uint64 TxID uint64 @@ -41,41 +62,42 @@ type QueryStatus struct { End time.Time } +// String returns a formatted log message. func (q *QueryStatus) String() string { - s := make([]string, 0, 8) + lines := make([]string, 0, 8) if q.SessID > 0 { - s = append(s, fmt.Sprintf(fmtLogSessID, q.SessID)) + lines = append(lines, fmt.Sprintf(fmtLogSessID, q.SessID)) } if q.TxID > 0 { - s = append(s, fmt.Sprintf(fmtLogTxID, q.TxID)) + lines = append(lines, fmt.Sprintf(fmtLogTxID, q.TxID)) } - if qry := q.Query; qry != "" { - qry = reInvisibleChars.ReplaceAllString(qry, ` `) - qry = strings.TrimSpace(qry) - s = append(s, fmt.Sprintf(fmtLogQuery, qry)) + if query := q.Query; query != "" { + query = reInvisibleChars.ReplaceAllString(query, ` `) + query = strings.TrimSpace(query) + lines = append(lines, fmt.Sprintf(fmtLogQuery, query)) } if len(q.Args) > 0 { - s = append(s, fmt.Sprintf(fmtLogArgs, q.Args)) + lines = append(lines, fmt.Sprintf(fmtLogArgs, q.Args)) } if q.RowsAffected != nil { - s = append(s, fmt.Sprintf(fmtLogRowsAffected, *q.RowsAffected)) + lines = append(lines, fmt.Sprintf(fmtLogRowsAffected, *q.RowsAffected)) } if q.LastInsertID != nil { - s = append(s, fmt.Sprintf(fmtLogLastInsertID, *q.LastInsertID)) + lines = append(lines, fmt.Sprintf(fmtLogLastInsertID, *q.LastInsertID)) } if q.Err != nil { - s = append(s, fmt.Sprintf(fmtLogError, q.Err)) + lines = append(lines, fmt.Sprintf(fmtLogError, q.Err)) } - s = append(s, fmt.Sprintf(fmtLogTimeTaken, float64(q.End.UnixNano()-q.Start.UnixNano())/float64(1e9))) + lines = append(lines, fmt.Sprintf(fmtLogTimeTaken, float64(q.End.UnixNano()-q.Start.UnixNano())/float64(1e9))) - return strings.Join(s, "\n") + return strings.Join(lines, "\n") } // EnvEnableDebug can be used by adapters to determine if the user has enabled @@ -94,15 +116,9 @@ const ( EnvEnableDebug = `UPPERIO_DB_DEBUG` ) -func init() { - if envEnabled(EnvEnableDebug) { - Conf.SetLogging(true) - } -} - // Logger represents a logging collector. You can pass a logging collector to // db.Conf.SetLogger(myCollector) to make it collect db.QueryStatus messages -// after every query. +// after executing a query. type Logger interface { Log(*QueryStatus) } @@ -119,4 +135,10 @@ func (lg *defaultLogger) Log(m *QueryStatus) { log.Printf("\n\t%s\n\n", strings.Replace(m.String(), "\n", "\n\t", -1)) } -var _ = Logger(&defaultLogger{}) +var _ Logger = &defaultLogger{} + +func init() { + if envEnabled(EnvEnableDebug) { + Conf.SetLogging(true) + } +}