diff --git a/builder.go b/builder.go index 61d0cbf27fc3a01f2032e0cd078ca11a91ab000e..cbd692aefa410cc9914b3fbf0ac13dbc967a333f 100644 --- a/builder.go +++ b/builder.go @@ -19,8 +19,6 @@ // 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 builder provides tools to compose, execute and map SQL queries to Go -// structs and maps. package db import ( @@ -28,8 +26,8 @@ import ( "fmt" ) -// Builder defines methods that can serve as starting points for SQL queries. -type Builder interface { +// SQLBuilder defines methods that can serve as starting points for SQL queries. +type SQLBuilder interface { // Select initializes and returns a Selector pointed at the given columns. // @@ -279,8 +277,8 @@ type Selector interface { // results. Getter - // Fetcher provides methods to retrieve and map results. - Fetcher + // ResultMapper provides methods to retrieve and map results. + ResultMapper // fmt.Stringer provides `String() string`, you can use `String()` to compile // the `Selector` into a string. @@ -386,7 +384,8 @@ type Getter interface { QueryRow() (*sql.Row, error) } -type Fetcher interface { +// ResultMapper defined methods for a result mapper. +type ResultMapper interface { // All dumps all the results into the given slice, All() expects a pointer to // slice of maps or structs. // @@ -411,8 +410,8 @@ type Fetcher interface { // Iterator provides methods for iterating over query results. type Iterator interface { - // Fetcher provides methods to retrieve and map results. - Fetcher + // ResultMapper provides methods to retrieve and map results. + ResultMapper // Scan dumps the current result into the given pointer variable pointers. Scan(dest ...interface{}) error diff --git a/db.go b/db.go index 93b02420f89d5ba0ac0411132e4f0aede0602e11..36104d406f246f6fa34eb9616c346063f2e1ee26 100644 --- a/db.go +++ b/db.go @@ -395,23 +395,7 @@ type Database interface { ClearCache() } -// Tx is an interface that enhaces the Database interface with additional -// methods for transactional operations. -// -// Example: -// sess, err = postgresql.Open(settings) -// ... -// -// tx, err = sess.NewTransaction() -// ... -// -// artist, err = tx.Collection("artist") -// ... -// -// id, err = artist.Insert(item) -// ... -// -// err = tx.Commit() +// Tx represents transactions that can be either committed or rolled back. type Tx interface { // Rollback discards all the instructions on the current transaction. Rollback() error @@ -422,10 +406,12 @@ 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 a map or a - // struct as argument and returns the ID of the newly added element. The type - // of this ID depends on the database adapter. The ID returned by Insert() - // can be passed directly to Find() to retrieve the recently added element. + // 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. Insert(interface{}) (interface{}, error) // InsertReturning is like Insert() but it updates the passed pointer to map @@ -521,34 +507,32 @@ type ConnectionURL interface { // String returns the connection string that is going to be passed to the // adapter. String() string - - // Adapter returns the name of the adapter associated with the connection - // URL, this name can be used as argument by the db.Adapter function to - // retrieve an imported adapter. - Adapter() string } -// SQLDatabase represents a SQL database capable of creating transactions and -// use builder methods. +// SQLDatabase represents a Database which is capable of both creating +// transactions and use SQL builder methods. type SQLDatabase interface { Database - Builder + SQLBuilder - // NewTx returns a SQLTx in case the database supports transactions and the - // transaction can be initialized. + // NewTx returns a new session that lives within a transaction. This session + // is completely independent from its parent. NewTx() (SQLTx, error) - // Tx accepts a function which first argument is a SQLDatabase which runs - // within a transaction. If the fn function returns nil, the transaction is - // commited and the Tx function returns nil too, if fn returns an error, then - // the transaction is rolled back and the error is returned by Tx. + // Tx creates a new transaction that is passed as context to the fn function. + // The fn function defines a transaction operation. If the fn function + // returns nil, the transaction is commited, otherwise the transaction is + // rolled back. The transaction session is closed after the function exists, + // regardless of the error value returned by fn. Tx(fn func(sess SQLTx) error) error } -// SQLTx represents a transaction on a SQL database. +// SQLTx represents transaction on a SQL database. Transactions can only accept +// intructions until being commited or rolled back, they become useless +// afterwards and are automatically closed. type SQLTx interface { Database - Builder + SQLBuilder Tx } diff --git a/errors.go b/errors.go index 965f6066779c73b3669260f2acdf910c13a5e7cb..c492ef8148d444f3427ded626c437cedf172da87 100644 --- a/errors.go +++ b/errors.go @@ -27,25 +27,25 @@ import ( // Shared error messages. var ( - ErrNoMoreRows = errors.New(`upper: no more rows in this result set.`) - ErrNotConnected = errors.New(`upper: you're currently not connected.`) - ErrMissingDatabaseName = errors.New(`upper: missing database name.`) - ErrMissingCollectionName = errors.New(`upper: missing collection name.`) - ErrCollectionDoesNotExist = errors.New(`upper: collection does not exist.`) - ErrSockerOrHost = errors.New(`upper: you may connect either to a unix socket or a tcp address, but not both.`) - ErrQueryLimitParam = errors.New(`upper: a query can accept only one db.Limit() parameter.`) - ErrQuerySortParam = errors.New(`upper: a query can accept only one db.Sort{} parameter.`) - ErrQueryOffsetParam = errors.New(`upper: a query can accept only one db.Offset() parameter.`) - ErrMissingConditions = errors.New(`upper: missing selector conditions.`) - ErrUnsupported = errors.New(`upper: this action is currently unsupported on this database.`) - ErrUndefined = errors.New(`upper: this value is undefined.`) - ErrQueryIsPending = errors.New(`upper: can't execute this instruction while the result set is still open.`) - ErrUnsupportedDestination = errors.New(`upper: unsupported destination type.`) - ErrUnsupportedType = errors.New(`upper: this type does not support marshaling.`) - ErrUnsupportedValue = errors.New(`upper: this value does not support unmarshaling.`) - ErrUnknownConditionType = errors.New(`upper: arguments of type %T can't be used as constraints.`) - ErrTooManyClients = errors.New(`upper: can't connect to database server: too many clients.`) - ErrGivingUpTryingToConnect = errors.New(`upper: giving up trying to connect: too many clients.`) + ErrNoMoreRows = errors.New(`upper: no more rows in this result set`) + ErrNotConnected = errors.New(`upper: you're currently not connected`) + ErrMissingDatabaseName = errors.New(`upper: missing database name`) + ErrMissingCollectionName = errors.New(`upper: missing collection name`) + ErrCollectionDoesNotExist = errors.New(`upper: collection does not exist`) + ErrSockerOrHost = errors.New(`upper: you may connect either to a unix socket or a tcp address, but not both`) + ErrQueryLimitParam = errors.New(`upper: a query can accept only one dbLimit() parameter`) + ErrQuerySortParam = errors.New(`upper: a query can accept only one dbSort{} parameter`) + ErrQueryOffsetParam = errors.New(`upper: a query can accept only one dbOffset() parameter`) + ErrMissingConditions = errors.New(`upper: missing selector conditions`) + ErrUnsupported = errors.New(`upper: this action is currently unsupported on this database`) + ErrUndefined = errors.New(`upper: this value is undefined`) + ErrQueryIsPending = errors.New(`upper: can't execute this instruction while the result set is still open`) + ErrUnsupportedDestination = errors.New(`upper: unsupported destination type`) + ErrUnsupportedType = errors.New(`upper: this type does not support marshaling`) + ErrUnsupportedValue = errors.New(`upper: this value does not support unmarshaling`) + ErrUnknownConditionType = errors.New(`upper: arguments of type %T can't be used as constraints`) + ErrTooManyClients = errors.New(`upper: can't connect to database server: too many clients`) + ErrGivingUpTryingToConnect = errors.New(`upper: giving up trying to connect: too many clients`) ErrMissingConnURL = errors.New(`upper: missing DSN`) ErrNotImplemented = errors.New(`upper: call not implemented`) ErrAlreadyWithinTransaction = errors.New(`upper: already within a transaction`) diff --git a/internal/sqladapter/database.go b/internal/sqladapter/database.go index 3e0c6c6d942c46449a793f63cb5107b1a067acac..3b3cf8d54afb64d602a61aaa69a98749c38ad489 100644 --- a/internal/sqladapter/database.go +++ b/internal/sqladapter/database.go @@ -30,7 +30,7 @@ type Database interface { // PartialDatabase defines all the methods an adapter must provide. type PartialDatabase interface { - db.Builder + db.SQLBuilder Collections() ([]string, error) Open(db.ConnectionURL) error diff --git a/internal/sqladapter/result.go b/internal/sqladapter/result.go index 8bddc76ba5f0b21582a359f88c6e7fa40daecdd7..7dc49358ce55c646353cb4d2e2647f5edc1a0bcb 100644 --- a/internal/sqladapter/result.go +++ b/internal/sqladapter/result.go @@ -29,7 +29,7 @@ import ( // Result represents a delimited set of items bound by a condition. type Result struct { - b db.Builder + b db.SQLBuilder table string iter db.Iterator limit int @@ -50,7 +50,7 @@ func filter(conds []interface{}) []interface{} { // NewResult creates and Results a new Result set on the given table, this set // is limited by the given exql.Where conditions. -func NewResult(b db.Builder, table string, conds []interface{}) *Result { +func NewResult(b db.SQLBuilder, table string, conds []interface{}) *Result { return &Result{ b: b, table: table, diff --git a/internal/sqladapter/testing/adapter.go.tpl b/internal/sqladapter/testing/adapter.go.tpl index 37c34ebc483db5f838738602217539b3686f3a34..5ac78a8f19d19bd0f5bc2215776ef355bbc63646 100644 --- a/internal/sqladapter/testing/adapter.go.tpl +++ b/internal/sqladapter/testing/adapter.go.tpl @@ -38,7 +38,7 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func mustOpen() Database { +func mustOpen() db.SQLDatabase { sess, err := Open(settings) if err != nil { panic(err.Error()) @@ -162,7 +162,7 @@ func TestInsertReturningWithinTransaction(t *testing.T) { err := sess.Collection("artist").Truncate() assert.NoError(t, err) - tx, err := sess.NewTransaction() + tx, err := sess.NewTx() assert.NoError(t, err) defer tx.Close() @@ -871,7 +871,7 @@ func TestTransactionsAndRollback(t *testing.T) { defer sess.Close() // Simple transaction that should not fail. - tx, err := sess.NewTransaction() + tx, err := sess.NewTx() assert.NoError(t, err) artist := tx.Collection("artist") @@ -893,7 +893,7 @@ func TestTransactionsAndRollback(t *testing.T) { assert.NoError(t, err) // Use another transaction. - tx, err = sess.NewTransaction() + tx, err = sess.NewTx() artist = tx.Collection("artist") @@ -925,7 +925,7 @@ func TestTransactionsAndRollback(t *testing.T) { assert.NoError(t, err) // Attempt to add some rows. - tx, err = sess.NewTransaction() + tx, err = sess.NewTx() assert.NoError(t, err) artist = tx.Collection("artist") @@ -956,7 +956,7 @@ func TestTransactionsAndRollback(t *testing.T) { assert.NoError(t, err) // Attempt to add some rows. - tx, err = sess.NewTransaction() + tx, err = sess.NewTx() assert.NoError(t, err) artist = tx.Collection("artist") @@ -1108,7 +1108,7 @@ func TestBuilder(t *testing.T) { assert.NoError(t, err) assert.NotZero(t, all) - tx, err := sess.NewTransaction() + tx, err := sess.NewTx() assert.NoError(t, err) assert.NotZero(t, tx) defer tx.Close() @@ -1153,7 +1153,7 @@ func TestExhaustConnectionPool(t *testing.T) { // Requesting a new transaction session. start := time.Now() - tx, err := sess.NewTransaction() + tx, err := sess.NewTx() if err != nil { tFatal(err) } diff --git a/internal/sqladapter/testing/adapter_benchmark.go.tpl b/internal/sqladapter/testing/adapter_benchmark.go.tpl index da9b31419c6de626df63a30ac5f16cc0626980ef..b4a8d0ddfb418d01acb6f7b7374a353acdfe8dd1 100644 --- a/internal/sqladapter/testing/adapter_benchmark.go.tpl +++ b/internal/sqladapter/testing/adapter_benchmark.go.tpl @@ -93,7 +93,7 @@ func BenchmarkUpperInsertTransaction(b *testing.B) { sess := mustOpen() defer sess.Close() - tx, err := sess.NewTransaction() + tx, err := sess.NewTx() if err != nil { b.Fatal(err) } @@ -125,7 +125,7 @@ func BenchmarkUpperInsertTransactionWithMap(b *testing.B) { sess := mustOpen() defer sess.Close() - tx, err := sess.NewTransaction() + tx, err := sess.NewTx() if err != nil { b.Fatal(err) } @@ -276,8 +276,8 @@ func BenchmarkUpperCommitManyTransactions(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - var tx Tx - if tx, err = sess.NewTransaction(); err != nil { + var tx db.SQLTx + if tx, err = sess.NewTx(); err != nil { b.Fatal(err) } @@ -314,8 +314,8 @@ func BenchmarkUpperRollbackManyTransactions(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - var tx db.Tx - if tx, err = sess.NewTransaction(); err != nil { + var tx db.SQLTx + if tx, err = sess.NewTx(); err != nil { b.Fatal(err) } diff --git a/internal/sqladapter/tx.go b/internal/sqladapter/tx.go index bd73ab3cc3cc316cbe4b07986d68c180c54e3a04..9b1972ef265f4df19b2f7ad77b0a3d169cb1dfa4 100644 --- a/internal/sqladapter/tx.go +++ b/internal/sqladapter/tx.go @@ -85,10 +85,12 @@ func (t *sqlTx) Commit() (err error) { } func (t *txWrapper) Commit() error { + defer t.Database.Close() // Automatic close on commit. return t.BaseTx.Commit() } func (t *txWrapper) Rollback() error { + defer t.Database.Close() // Automatic close on rollback. return t.BaseTx.Rollback() } diff --git a/postgresql/connection.go b/postgresql/connection.go index 041a3050a073ce78e0ef6ec5d2e1933ec7fb59fa..07c0fb54e8bf820545402e776b4a10a9aae5eae7 100644 --- a/postgresql/connection.go +++ b/postgresql/connection.go @@ -90,10 +90,6 @@ type ConnectionURL struct { var escaper = strings.NewReplacer(` `, `\ `, `'`, `\'`, `\`, `\\`) -func (c ConnectionURL) Adapter() string { - return Adapter -} - func (c ConnectionURL) String() (s string) { u := make([]string, 0, 6) diff --git a/postgresql/database.go b/postgresql/database.go index b55eac9d4494726d492254957c46690ba6242d0d..6d8bd6b7f1c69059bed0fcd43b8e2dd44bf53ce2 100644 --- a/postgresql/database.go +++ b/postgresql/database.go @@ -36,11 +36,10 @@ import ( // database is the actual implementation of Database type database struct { sqladapter.BaseDatabase // Leveraged by sqladapter - db.Builder + db.SQLBuilder - txMu sync.Mutex - tx sqladapter.DatabaseTx - cloned bool + txMu sync.Mutex + tx sqladapter.DatabaseTx connURL db.ConnectionURL } @@ -69,6 +68,7 @@ func Open(settings db.ConnectionURL) (db.SQLDatabase, error) { return d, nil } +// NewTx returns a transaction session. func NewTx(sqlTx *sql.Tx) (db.SQLTx, error) { d, err := newDatabase(nil) if err != nil { @@ -83,7 +83,7 @@ func NewTx(sqlTx *sql.Tx) (db.SQLTx, error) { if err != nil { return nil, err } - d.Builder = b + d.SQLBuilder = b if err := d.BaseDatabase.BindTx(sqlTx); err != nil { return nil, err @@ -109,7 +109,7 @@ func New(sess *sql.DB) (db.SQLDatabase, error) { if err != nil { return nil, err } - d.Builder = b + d.SQLBuilder = b if err := d.BaseDatabase.BindSession(sess); err != nil { return nil, err @@ -142,7 +142,7 @@ func (d *database) NewTx() (db.SQLTx, error) { // Collections returns a list of non-system tables from the database. func (d *database) Collections() (collections []string, err error) { - q := d.Builder.Select("table_name"). + q := d.Select("table_name"). From("information_schema.tables"). Where("table_schema = ?", "public") @@ -169,7 +169,7 @@ func (d *database) open() error { if err != nil { return err } - d.Builder = b + d.SQLBuilder = b connFn := func() error { sess, err := sql.Open("postgres", d.ConnectionURL().String()) @@ -191,7 +191,6 @@ func (d *database) clone() (*database, error) { if err != nil { return nil, err } - clone.cloned = true if err := clone.open(); err != nil { return nil, err @@ -266,7 +265,7 @@ func (d *database) NewLocalTransaction() (sqladapter.DatabaseTx, error) { // FindDatabaseName allows sqladapter look up the database's name. func (d *database) FindDatabaseName() (string, error) { - q := d.Builder.Select(db.Raw("CURRENT_DATABASE() AS name")) + q := d.Select(db.Raw("CURRENT_DATABASE() AS name")) iter := q.Iterator() defer iter.Close() @@ -283,7 +282,7 @@ func (d *database) FindDatabaseName() (string, error) { // TableExists allows sqladapter check whether a table exists and returns an // error in case it doesn't. func (d *database) TableExists(name string) error { - q := d.Builder.Select("table_name"). + q := d.Select("table_name"). From("information_schema.tables"). Where("table_catalog = ? AND table_name = ?", d.BaseDatabase.Name(), name) @@ -302,7 +301,7 @@ func (d *database) TableExists(name string) error { // FindTablePrimaryKeys allows sqladapter find a table's primary keys. func (d *database) FindTablePrimaryKeys(tableName string) ([]string, error) { - q := d.Builder.Select("pg_attribute.attname AS pkey"). + q := d.Select("pg_attribute.attname AS pkey"). From("pg_index", "pg_class", "pg_attribute"). Where(` pg_class.oid = '"` + tableName + `"'::regclass diff --git a/postgresql/postgresql.go b/postgresql/postgresql.go index c4ec35199aaeb42dac0f8eda3016d6c18d2eac46..2db921a24a96a8b549ac1104df433d94f1d42892 100644 --- a/postgresql/postgresql.go +++ b/postgresql/postgresql.go @@ -31,7 +31,7 @@ const sqlDriver = `postgres` const Adapter = `postgresql` func init() { - db.RegisterSQLAdapter(Adapter, &db.SQLAdapter{ + db.RegisterSQLAdapter(Adapter, &db.SQLAdapterFuncMap{ New: New, NewTx: NewTx, Open: Open, diff --git a/postgresql/template_test.go b/postgresql/template_test.go index 962865eed6750444cae51f373be6fe30bab67226..41a360f5f98bf117d3b564bc14d28a2bf3f58360 100644 --- a/postgresql/template_test.go +++ b/postgresql/template_test.go @@ -10,7 +10,7 @@ import ( func TestTemplateSelect(t *testing.T) { - b := builder.NewBuilderWithTemplate(template) + b := builder.NewSQLBuilder(template) assert := assert.New(t) assert.Equal( @@ -148,7 +148,7 @@ func TestTemplateSelect(t *testing.T) { } func TestTemplateInsert(t *testing.T) { - b := builder.NewBuilderWithTemplate(template) + b := builder.NewSQLBuilder(template) assert := assert.New(t) assert.Equal( @@ -190,7 +190,7 @@ func TestTemplateInsert(t *testing.T) { } func TestTemplateUpdate(t *testing.T) { - b := builder.NewBuilderWithTemplate(template) + b := builder.NewSQLBuilder(template) assert := assert.New(t) assert.Equal( @@ -232,7 +232,7 @@ func TestTemplateUpdate(t *testing.T) { } func TestTemplateDelete(t *testing.T) { - b := builder.NewBuilderWithTemplate(template) + b := builder.NewSQLBuilder(template) assert := assert.New(t) assert.Equal( diff --git a/sqlbuilder/builder.go b/sqlbuilder/builder.go index aad21449a2797389ee8e3f28a9b03bfd56b4d11a..a06d8ff16ae74e428792ae22b4062ea43a7a54d0 100644 --- a/sqlbuilder/builder.go +++ b/sqlbuilder/builder.go @@ -55,7 +55,7 @@ type sqlBuilder struct { } // New returns a query builder that is bound to the given database session. -func New(sess interface{}, t *exql.Template) (db.Builder, error) { +func New(sess interface{}, t *exql.Template) (db.SQLBuilder, error) { switch v := sess.(type) { case *sql.DB: sess = newSqlgenProxy(v, t) @@ -71,8 +71,8 @@ func New(sess interface{}, t *exql.Template) (db.Builder, error) { }, nil } -// NewBuilderWithTemplate returns a builder that is based on the given template. -func NewBuilderWithTemplate(t *exql.Template) db.Builder { +// NewSQLBuilder returns a builder that is based on the given template. +func NewSQLBuilder(t *exql.Template) db.SQLBuilder { return &sqlBuilder{ t: newTemplateWithUtils(t), } @@ -436,6 +436,6 @@ func (p *exprProxy) StatementQueryRow(stmt *exql.Statement, args ...interface{}) } var ( - _ = db.Builder(&sqlBuilder{}) + _ = db.SQLBuilder(&sqlBuilder{}) _ = exprDB(&exprProxy{}) ) diff --git a/wrapper.go b/wrapper.go index 4861334ae8b2081d9e58842ec10520b5ac5bf79c..eb637aad109b6222d6a095775126c6f3ff2b07a8 100644 --- a/wrapper.go +++ b/wrapper.go @@ -26,38 +26,38 @@ import ( "fmt" ) -var adapters map[string]*SQLAdapter +var sqlAdapters map[string]*SQLAdapterFuncMap func init() { - adapters = make(map[string]*SQLAdapter) + sqlAdapters = make(map[string]*SQLAdapterFuncMap) } -type SQLAdapter struct { +type SQLAdapterFuncMap struct { New func(sqlDB *sql.DB) (SQLDatabase, error) NewTx func(sqlTx *sql.Tx) (SQLTx, error) Open func(settings ConnectionURL) (SQLDatabase, error) } -func RegisterSQLAdapter(name string, fn *SQLAdapter) { +func RegisterSQLAdapter(name string, adapter *SQLAdapterFuncMap) { if name == "" { panic(`Missing adapter name`) } - if _, ok := adapters[name]; ok { + if _, ok := sqlAdapters[name]; ok { panic(`db.RegisterSQLAdapter() called twice for adapter: ` + name) } - adapters[name] = fn + sqlAdapters[name] = adapter } -func Adapter(name string) SQLAdapter { - if fn, ok := adapters[name]; ok { +func SQLAdapter(name string) SQLAdapterFuncMap { + if fn, ok := sqlAdapters[name]; ok { return *fn } - return missingAdapter(name) + return missingSQLAdapter(name) } -func missingAdapter(name string) SQLAdapter { +func missingSQLAdapter(name string) SQLAdapterFuncMap { err := fmt.Errorf("upper: Missing adapter %q, forgot to import?", name) - return SQLAdapter{ + return SQLAdapterFuncMap{ New: func(*sql.DB) (SQLDatabase, error) { return nil, err },