Repository: upper/db
Pull request #586 (2020-08-17 12:34:04)

v4: records and stores


Patch:
diff --git a/adapter/cockroachdb/connection.go b/adapter/cockroachdb/connection.go
index 4d04455..8859382 100644
--- a/adapter/cockroachdb/connection.go
+++ b/adapter/cockroachdb/connection.go
@@ -112,21 +112,23 @@ func (c ConnectionURL) String() (s string) {
 		u = append(u, "password="+escaper.Replace(c.Password))
 	}
 
-	if c.Host != "" {
-		host, port, err := net.SplitHostPort(c.Host)
-		if err == nil {
-			if host == "" {
-				host = "127.0.0.1"
-			}
-			if port == "" {
-				port = "26257"
-			}
-			u = append(u, "host="+escaper.Replace(host))
-			u = append(u, "port="+escaper.Replace(port))
-		} else {
-			u = append(u, "host="+escaper.Replace(c.Host))
-			u = append(u, "port=26257")
+	if c.Host == "" {
+		c.Host = "127.0.0.1:26257"
+	}
+
+	host, port, err := net.SplitHostPort(c.Host)
+	if err == nil {
+		if host == "" {
+			host = "127.0.0.1"
+		}
+		if port == "" {
+			port = "26257"
 		}
+		u = append(u, "host="+escaper.Replace(host))
+		u = append(u, "port="+escaper.Replace(port))
+	} else {
+		u = append(u, "host="+escaper.Replace(c.Host))
+		u = append(u, "port=26257")
 	}
 
 	if c.Socket != "" {
diff --git a/adapter/mongo/database.go b/adapter/mongo/database.go
index d46955e..8f08761 100644
--- a/adapter/mongo/database.go
+++ b/adapter/mongo/database.go
@@ -29,8 +29,6 @@ import (
 	"sync"
 	"time"
 
-	"github.com/upper/db/v4/internal/unsupported"
-
 	db "github.com/upper/db/v4"
 	mgo "gopkg.in/mgo.v2"
 )
@@ -183,11 +181,15 @@ func (s *Source) Collections() (cols []db.Collection, err error) {
 	return cols, nil
 }
 
-func (s *Source) Get(db.Model, interface{}) error {
+func (s *Source) Delete(db.Record) error {
+	return db.ErrNotImplemented
+}
+
+func (s *Source) Get(db.Record, interface{}) error {
 	return db.ErrNotImplemented
 }
 
-func (s *Source) Save(db.Model) error {
+func (s *Source) Save(db.Record) error {
 	return db.ErrNotImplemented
 }
 
@@ -235,7 +237,3 @@ func (s *Source) versionAtLeast(version ...int) bool {
 	}
 	return true
 }
-
-func (s *Source) Item(m db.Model) db.Item {
-	return &unsupported.Item{}
-}
diff --git a/internal/sqladapter/collection.go b/internal/sqladapter/collection.go
index 22d1e8e..2419c87 100644
--- a/internal/sqladapter/collection.go
+++ b/internal/sqladapter/collection.go
@@ -9,10 +9,6 @@ import (
 	"github.com/upper/db/v4/sqlbuilder"
 )
 
-type hasReflector interface {
-	Reflect(db.Model)
-}
-
 // CollectionAdapter defines methods to be implemented by SQL adapters.
 type CollectionAdapter interface {
 	// Insert prepares and executes an INSERT statament. When the item is
@@ -248,10 +244,6 @@ func (c *collection) InsertReturning(item interface{}) error {
 		goto cancel
 	}
 
-	if reflector, ok := item.(hasReflector); ok {
-		reflector.Reflect(item.(db.Model))
-	}
-
 	if !inTx {
 		// This is only executed if t.Session() was **not** a transaction and if
 		// sess was created with sess.NewTransaction().
@@ -344,10 +336,6 @@ func (c *collection) UpdateReturning(item interface{}) error {
 		panic("default")
 	}
 
-	if reflector, ok := item.(hasReflector); ok {
-		reflector.Reflect(item.(db.Model))
-	}
-
 	if !inTx {
 		// This is only executed if t.Session() was **not** a transaction and if
 		// sess was created with sess.NewTransaction().
diff --git a/internal/sqladapter/database.go b/internal/sqladapter/database.go
index a966c21..6c5618f 100644
--- a/internal/sqladapter/database.go
+++ b/internal/sqladapter/database.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"database/sql"
 	"math"
+	"reflect"
 	"strconv"
 	"sync"
 	"sync/atomic"
@@ -16,6 +17,10 @@ import (
 	"github.com/upper/db/v4/sqlbuilder"
 )
 
+type hasPrimaryKeys interface {
+	PrimaryKeys() []string
+}
+
 var (
 	lastSessID uint64
 	lastTxID   uint64
@@ -112,8 +117,11 @@ type Session interface {
 	// Driver returns the underlying driver the session is using
 	Driver() interface{}
 
-	Save(db.Model) error
-	Get(db.Model, interface{}) error
+	Save(db.Record) error
+
+	Get(db.Record, interface{}) error
+
+	Delete(db.Record) error
 
 	// WaitForConnection attempts to run the given connection function a fixed
 	// number of times before failing.
@@ -333,14 +341,150 @@ func (sess *session) Open() error {
 	return sess.BindDB(sqlDB)
 }
 
-func (sess *session) Get(m db.Model, id interface{}) error {
-	return m.Collection(sess).Find(id).One(m)
+func (sess *session) Get(record db.Record, id interface{}) error {
+	return record.Collection(sess).Find(id).One(record)
+}
+
+func (sess *session) Save(record db.Record) error {
+	if record == nil {
+		return db.ErrNilItem
+	}
+
+	if reflect.TypeOf(record).Kind() != reflect.Ptr {
+		return db.ErrExpectingPointerToStruct
+	}
+
+	_, fields := recordPrimaryKeyFieldValues(sess, record)
+	isCreate := true
+	for i := range fields {
+		if fields[i] != reflect.Zero(reflect.TypeOf(fields[i])).Interface() {
+			isCreate = false
+			break
+		}
+	}
+
+	if isCreate {
+		return recordCreate(sess, record)
+	}
+	return recordUpdate(sess, record)
+}
+
+func (sess *session) Delete(record db.Record) error {
+	if record == nil {
+		return db.ErrNilItem
+	}
+
+	if reflect.TypeOf(record).Kind() != reflect.Ptr {
+		return db.ErrExpectingPointerToStruct
+	}
+
+	conds, err := recordID(sess, record)
+	if err != nil {
+		return err
+	}
+
+	if hook, ok := record.(db.BeforeDeleteHook); ok {
+		if err := hook.BeforeDelete(sess); err != nil {
+			return err
+		}
+	}
+
+	if err := record.Collection(sess).Find(conds).Delete(); err != nil {
+		return err
+	}
+
+	if hook, ok := record.(db.AfterDeleteHook); ok {
+		if err := hook.AfterDelete(sess); err != nil {
+			return err
+		}
+	}
+
+	return nil
 }
 
-func (sess *session) Save(m db.Model) error {
-	item := sqlbuilder.NewItem(sess, m)
-	item.(hasReflector).Reflect(m)
-	return item.Save(sess)
+func recordID(sess db.Session, record db.Record) (db.Cond, error) {
+	if record == nil {
+		return nil, db.ErrNilItem
+	}
+
+	id := db.Cond{}
+
+	keys, fields := recordPrimaryKeyFieldValues(sess, record)
+	for i := range fields {
+		if fields[i] == reflect.Zero(reflect.TypeOf(fields[i])).Interface() {
+			return nil, db.ErrZeroItemID
+		}
+		id[keys[i]] = fields[i]
+	}
+	if len(id) < 1 {
+		return nil, db.ErrZeroItemID
+	}
+
+	return id, nil
+}
+
+func recordPrimaryKeyFieldValues(sess db.Session, record db.Record) ([]string, []interface{}) {
+	pKeys := record.Collection(sess).(hasPrimaryKeys).PrimaryKeys()
+	fields := sqlbuilder.Mapper.FieldsByName(reflect.ValueOf(record), pKeys)
+
+	values := make([]interface{}, 0, len(fields))
+	for i := range fields {
+		if fields[i].IsValid() {
+			values = append(values, fields[i].Interface())
+		}
+	}
+
+	return pKeys, values
+}
+
+func recordCreate(sess db.Session, record db.Record) error {
+	if validator, ok := record.(db.Validator); ok {
+		if err := validator.Validate(); err != nil {
+			return err
+		}
+	}
+
+	if hook, ok := record.(db.BeforeCreateHook); ok {
+		if err := hook.BeforeCreate(sess); err != nil {
+			return err
+		}
+	}
+
+	if err := record.Collection(sess).InsertReturning(record); err != nil {
+		return err
+	}
+
+	if hook, ok := record.(db.AfterCreateHook); ok {
+		if err := hook.AfterCreate(sess); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func recordUpdate(sess db.Session, record db.Record) error {
+	if validator, ok := record.(db.Validator); ok {
+		if err := validator.Validate(); err != nil {
+			return err
+		}
+	}
+
+	if hook, ok := record.(db.BeforeUpdateHook); ok {
+		if err := hook.BeforeUpdate(sess); err != nil {
+			return err
+		}
+	}
+
+	if err := record.Collection(sess).UpdateReturning(record); err != nil {
+		return err
+	}
+
+	if hook, ok := record.(db.AfterUpdateHook); ok {
+		if err := hook.AfterUpdate(sess); err != nil {
+			return err
+		}
+	}
+	return nil
 }
 
 func (sess *session) DB() *sql.DB {
diff --git a/internal/unsupported/item.go b/internal/unsupported/item.go
deleted file mode 100644
index 5ae5084..0000000
--- a/internal/unsupported/item.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// 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 unsupported
-
-import (
-	"github.com/upper/db/v4"
-)
-
-type Item struct {
-}
-
-func (*Item) Update(db.Session, db.M) error {
-	return db.ErrUnsupported
-}
-
-func (*Item) Delete(db.Session) error {
-	return db.ErrUnsupported
-}
-
-func (*Item) Save(db.Session) error {
-	return db.ErrUnsupported
-}
-
-func (*Item) Changes() db.M {
-	return db.M{}
-}
diff --git a/item.go b/item.go
deleted file mode 100644
index 9d3ec2c..0000000
--- a/item.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package db
-
-// Item defines additonal methods for db.Model objects.
-type Item interface {
-	Save(Session) error
-	Delete(Session) error
-	Update(Session, M) error
-	Changes() M
-}
diff --git a/model.go b/record.go
similarity index 79%
rename from model.go
rename to record.go
index f276439..6c1470d 100644
--- a/model.go
+++ b/record.go
@@ -20,12 +20,12 @@
 
 package db
 
-// Model is the equivalence between concrete database schemas and Go values.
-type Model interface {
+// Record is the equivalence between concrete database schemas and Go values.
+type Record interface {
 	Collection(sess Session) Collection
 }
 
-// HasSave is an interface that defines an (optional) Save function for models
+// HasSave is an interface that defines an (optional) Save function for records
 // that is called when persisting an item (creating or updating). If Save is
 // not defined, upper/db will attempt to either create or update the item based
 // on whether the values for item's primary key are defined.
@@ -34,50 +34,50 @@ type HasSave interface {
 }
 
 // Validator is an interface that defined an (optional) Validate function for
-// models that is called before persisting an item (creating or updating). If
+// records that is called before persisting an item (creating or updating). If
 // Validate returns an error the current operation is rolled back.
 type Validator interface {
 	Validate() error
 }
 
 // BeforeCreateHook is an interface that defines an BeforeCreate function for
-// models that is called before creating an item. If BeforeCreate returns an
+// records that is called before creating an item. If BeforeCreate returns an
 // error the create process is rolled back.
 type BeforeCreateHook interface {
 	BeforeCreate(Session) error
 }
 
 // AfterCreateHook is an interface that defines an AfterCreate function for
-// models that is called after creating an item. If AfterCreate returns an
+// records that is called after creating an item. If AfterCreate returns an
 // error the create process is rolled back.
 type AfterCreateHook interface {
 	AfterCreate(Session) error
 }
 
 // BeforeUpdateHook is an interface that defines a BeforeUpdate function for
-// models that is called before updating an item. If BeforeUpdate returns an
+// records that is called before updating an item. If BeforeUpdate returns an
 // error the update process is rolled back.
 type BeforeUpdateHook interface {
 	BeforeUpdate(Session) error
 }
 
 // AfterUpdateHook is an interface that defines an AfterUpdate function for
-// models that is called after updating an item. If AfterUpdate returns an
+// records that is called after updating an item. If AfterUpdate returns an
 // error the update process is rolled back.
 type AfterUpdateHook interface {
 	AfterUpdate(Session) error
 }
 
 // BeforeDeleteHook is an interface that defines a BeforeDelete function for
-// models that is called before removing an item. If BeforeDelete returns an
+// records that is called before removing an item. If BeforeDelete returns an
 // error the delete process is rolled back.
 type BeforeDeleteHook interface {
 	BeforeDelete(Session) error
 }
 
 // AfterDeleteHook is an interface that defines a AfterDelete function for
-// models that is called after removing an item. If AfterDelete returns an
-// error the delete process is rolled back.
+// records that is called after removing an item. If AfterDelete returns
+// an error the delete process is rolled back.
 type AfterDeleteHook interface {
 	AfterDelete(Session) error
 }
diff --git a/session.go b/session.go
index 09d1e6d..40fc3b1 100644
--- a/session.go
+++ b/session.go
@@ -40,8 +40,11 @@ type Session interface {
 	// database.
 	Collections() ([]Collection, error)
 
-	Save(model Model) error
-	Get(model Model, id interface{}) error
+	Save(record Record) error
+
+	Get(record Record, id interface{}) error
+
+	Delete(record Record) error
 
 	// Reset resets all the caching mechanisms the adapter is using.
 	Reset()
diff --git a/sqlbuilder/fetch.go b/sqlbuilder/fetch.go
index 60e1669..07dc0b5 100644
--- a/sqlbuilder/fetch.go
+++ b/sqlbuilder/fetch.go
@@ -32,10 +32,6 @@ type hasConvertValues interface {
 	ConvertValues(values []interface{}) []interface{}
 }
 
-type hasReflect interface {
-	Reflect(db.Model)
-}
-
 var Mapper = reflectx.NewMapper("db")
 
 // fetchRow receives a *sql.Rows value and tries to map all the rows into a
@@ -81,10 +77,6 @@ func fetchRow(iter *iterator, dst interface{}) error {
 		itemV.Set(reflect.Indirect(item))
 	}
 
-	if reflector, ok := dst.(hasReflect); ok {
-		reflector.Reflect(dst.(db.Model))
-	}
-
 	return nil
 }
 
diff --git a/sqlbuilder/item.go b/sqlbuilder/item.go
deleted file mode 100644
index 18c16ef..0000000
--- a/sqlbuilder/item.go
+++ /dev/null
@@ -1,229 +0,0 @@
-package sqlbuilder
-
-import (
-	"reflect"
-
-	db "github.com/upper/db/v4"
-)
-
-type hasPrimaryKeys interface {
-	PrimaryKeys() []string
-}
-
-type Item struct {
-	model    db.Model
-	pristine db.M
-}
-
-func (z *Item) Reflect(model db.Model) {
-	z.SetPristine(model)
-}
-
-func mapValues(model db.Model) db.M {
-	m := db.M{}
-	if model == nil {
-		return m
-	}
-	fieldMap := Mapper.FieldMap(reflect.ValueOf(model))
-	for column := range fieldMap {
-		m[column] = fieldMap[column].Interface()
-	}
-	return m
-}
-
-func NewItem(sess Session, model db.Model) db.Item {
-	item := &Item{}
-	item.SetPristine(model)
-	return item
-}
-
-func (z *Item) SetPristine(model db.Model) {
-	z.model = model               // current value
-	z.pristine = mapValues(model) // copy
-}
-
-func (z *Item) Changes() db.M {
-	diff := db.M{}
-	state := mapValues(z.model)
-	for column := range z.pristine {
-		// TODO: deep equality
-		if z.pristine[column] != state[column] {
-			diff[column] = state[column]
-		}
-	}
-	return diff
-}
-
-func (z *Item) getPrimaryKeyFieldValues(sess db.Session) ([]string, []interface{}) {
-	pKeys := z.model.Collection(sess).(hasPrimaryKeys).PrimaryKeys()
-	fields := Mapper.FieldsByName(reflect.ValueOf(z.model), pKeys)
-
-	values := make([]interface{}, 0, len(fields))
-	for i := range fields {
-		if fields[i].IsValid() {
-			values = append(values, fields[i].Interface())
-		}
-	}
-
-	return pKeys, values
-}
-
-func (z *Item) id(sess db.Session) (db.Cond, error) {
-	if z.model == nil {
-		return nil, db.ErrNilItem
-	}
-
-	id := db.Cond{}
-
-	keys, fields := z.getPrimaryKeyFieldValues(sess)
-	for i := range fields {
-		if fields[i] == reflect.Zero(reflect.TypeOf(fields[i])).Interface() {
-			return nil, db.ErrZeroItemID
-		}
-		id[keys[i]] = fields[i]
-	}
-	if len(id) < 1 {
-		return nil, db.ErrZeroItemID
-	}
-
-	return id, nil
-}
-
-func (z *Item) Save(sess db.Session) error {
-	if z.model == nil {
-		return db.ErrNilItem
-	}
-
-	if reflect.TypeOf(z).Kind() != reflect.Ptr {
-		return db.ErrExpectingPointerToStruct
-	}
-
-	_, fields := z.getPrimaryKeyFieldValues(sess)
-	isCreate := true
-	for i := range fields {
-		if fields[i] != reflect.Zero(reflect.TypeOf(fields[i])).Interface() {
-			isCreate = false
-		}
-	}
-
-	if isCreate {
-		return z.doCreate(sess)
-	}
-	return z.doUpdate(sess)
-}
-
-func (z *Item) doUpdate(sess db.Session) error {
-	if validator, ok := z.model.(db.Validator); ok {
-		if err := validator.Validate(); err != nil {
-			return err
-		}
-	}
-
-	if hook, ok := z.model.(db.BeforeUpdateHook); ok {
-		if err := hook.BeforeUpdate(sess); err != nil {
-			return err
-		}
-	}
-
-	if err := z.model.Collection(sess).UpdateReturning(z.model); err != nil {
-		return err
-	}
-
-	if hook, ok := z.model.(db.AfterUpdateHook); ok {
-		if err := hook.AfterUpdate(sess); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (z *Item) doCreate(sess db.Session) error {
-	if validator, ok := z.model.(db.Validator); ok {
-		if err := validator.Validate(); err != nil {
-			return err
-		}
-	}
-
-	if hook, ok := z.model.(db.BeforeCreateHook); ok {
-		if err := hook.BeforeCreate(sess); err != nil {
-			return err
-		}
-	}
-
-	if err := z.model.Collection(sess).InsertReturning(z.model); err != nil {
-		return err
-	}
-
-	if hook, ok := z.model.(db.AfterCreateHook); ok {
-		if err := hook.AfterCreate(sess); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (z *Item) Delete(sess db.Session) error {
-	if z.model == nil {
-		return db.ErrNilItem
-	}
-
-	if reflect.TypeOf(z.model).Kind() != reflect.Ptr {
-		return db.ErrExpectingPointerToStruct
-	}
-
-	conds, err := z.id(sess)
-	if err != nil {
-		return err
-	}
-
-	if hook, ok := z.model.(db.BeforeDeleteHook); ok {
-		if err := hook.BeforeDelete(sess); err != nil {
-			return err
-		}
-	}
-
-	if err := z.model.Collection(sess).Find(conds).Delete(); err != nil {
-		return err
-	}
-
-	if hook, ok := z.model.(db.AfterDeleteHook); ok {
-		if err := hook.AfterDelete(sess); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func (z *Item) Update(sess db.Session, m db.M) error {
-	if z.model == nil {
-		return db.ErrNilItem
-	}
-
-	if reflect.TypeOf(z.model).Kind() != reflect.Ptr {
-		return db.ErrExpectingPointerToStruct
-	}
-
-	conds, err := z.id(sess)
-	if err != nil {
-		return err
-	}
-
-	if hook, ok := z.model.(db.BeforeDeleteHook); ok {
-		if err := hook.BeforeDelete(sess); err != nil {
-			return err
-		}
-	}
-
-	if err := z.model.Collection(sess).Find(conds).Update(m); err != nil {
-		return err
-	}
-
-	if hook, ok := z.model.(db.AfterDeleteHook); ok {
-		if err := hook.AfterDelete(sess); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}


Test patch:
diff --git a/adapter/cockroachdb/connection_test.go b/adapter/cockroachdb/connection_test.go
index 0cadcc9..1911270 100644
--- a/adapter/cockroachdb/connection_test.go
+++ b/adapter/cockroachdb/connection_test.go
@@ -30,8 +30,8 @@ import (
 func TestConnectionURL(t *testing.T) {
 	c := ConnectionURL{}
 
-	// Default connection string is empty.
-	assert.Equal(t, "", c.String(), "Expecting default connectiong string to be empty")
+	// Default connection string.
+	assert.Equal(t, "host=127.0.0.1 port=26257 sslmode=require", c.String(), "Expecting default connectiong string to be empty")
 
 	// Adding a host with port.
 	c.Host = "localhost:1234"
diff --git a/adapter/cockroachdb/model_test.go b/adapter/cockroachdb/record_test.go
similarity index 89%
rename from adapter/cockroachdb/model_test.go
rename to adapter/cockroachdb/record_test.go
index a572c82..a740b65 100644
--- a/adapter/cockroachdb/model_test.go
+++ b/adapter/cockroachdb/record_test.go
@@ -28,14 +28,14 @@ import (
 	"github.com/upper/db/v4/internal/testsuite"
 )
 
-type BondTests struct {
-	testsuite.BondTestSuite
+type RecordTests struct {
+	testsuite.RecordTestSuite
 }
 
-func (s *BondTests) SetupSuite() {
+func (s *RecordTests) SetupSuite() {
 	s.Helper = &Helper{}
 }
 
-func TestBond(t *testing.T) {
-	suite.Run(t, &BondTests{})
+func TestRecord(t *testing.T) {
+	suite.Run(t, &RecordTests{})
 }
diff --git a/adapter/mssql/model_test.go b/adapter/mssql/record_test.go
similarity index 89%
rename from adapter/mssql/model_test.go
rename to adapter/mssql/record_test.go
index e573ab9..e3b6d65 100644
--- a/adapter/mssql/model_test.go
+++ b/adapter/mssql/record_test.go
@@ -28,15 +28,15 @@ import (
 	"github.com/upper/db/v4/internal/testsuite"
 )
 
-type BondTests struct {
-	testsuite.BondTestSuite
+type RecordTests struct {
+	testsuite.RecordTestSuite
 }
 
-func (s *BondTests) SetupSuite() {
+func (s *RecordTests) SetupSuite() {
 	s.Helper = &Helper{}
 }
 
-func SkipTestBond(t *testing.T) {
+func SkipTestRecord(t *testing.T) {
 	// mssql: Cannot update identity column 'id'.
-	suite.Run(t, &BondTests{})
+	suite.Run(t, &RecordTests{})
 }
diff --git a/adapter/mysql/model_test.go b/adapter/mysql/record_test.go
similarity index 89%
rename from adapter/mysql/model_test.go
rename to adapter/mysql/record_test.go
index 5a46cf9..7878ddd 100644
--- a/adapter/mysql/model_test.go
+++ b/adapter/mysql/record_test.go
@@ -28,14 +28,14 @@ import (
 	"github.com/upper/db/v4/internal/testsuite"
 )
 
-type BondTests struct {
-	testsuite.BondTestSuite
+type RecordTests struct {
+	testsuite.RecordTestSuite
 }
 
-func (s *BondTests) SetupSuite() {
+func (s *RecordTests) SetupSuite() {
 	s.Helper = &Helper{}
 }
 
-func TestBond(t *testing.T) {
-	suite.Run(t, &BondTests{})
+func TestRecord(t *testing.T) {
+	suite.Run(t, &RecordTests{})
 }
diff --git a/adapter/postgresql/model_test.go b/adapter/postgresql/record_test.go
similarity index 89%
rename from adapter/postgresql/model_test.go
rename to adapter/postgresql/record_test.go
index 5aa079e..8bdb070 100644
--- a/adapter/postgresql/model_test.go
+++ b/adapter/postgresql/record_test.go
@@ -28,14 +28,14 @@ import (
 	"github.com/upper/db/v4/internal/testsuite"
 )
 
-type BondTests struct {
-	testsuite.BondTestSuite
+type RecordTests struct {
+	testsuite.RecordTestSuite
 }
 
-func (s *BondTests) SetupSuite() {
+func (s *RecordTests) SetupSuite() {
 	s.Helper = &Helper{}
 }
 
-func TestBond(t *testing.T) {
-	suite.Run(t, &BondTests{})
+func TestRecord(t *testing.T) {
+	suite.Run(t, &RecordTests{})
 }
diff --git a/adapter/ql/model_test.go b/adapter/ql/record_test.go
similarity index 89%
rename from adapter/ql/model_test.go
rename to adapter/ql/record_test.go
index 7246f7b..bfefeda 100644
--- a/adapter/ql/model_test.go
+++ b/adapter/ql/record_test.go
@@ -28,14 +28,14 @@ import (
 	"github.com/upper/db/v4/internal/testsuite"
 )
 
-type BondTests struct {
-	testsuite.BondTestSuite
+type RecordTests struct {
+	testsuite.RecordTestSuite
 }
 
-func (s *BondTests) SetupSuite() {
+func (s *RecordTests) SetupSuite() {
 	s.Helper = &Helper{}
 }
 
-func SkipTestBond(t *testing.T) {
-	suite.Run(t, &BondTests{})
+func SkipTestRecord(t *testing.T) {
+	suite.Run(t, &RecordTests{})
 }
diff --git a/adapter/sqlite/model_test.go b/adapter/sqlite/record_test.go
similarity index 89%
rename from adapter/sqlite/model_test.go
rename to adapter/sqlite/record_test.go
index 77e3f90..9a88535 100644
--- a/adapter/sqlite/model_test.go
+++ b/adapter/sqlite/record_test.go
@@ -28,14 +28,14 @@ import (
 	"github.com/upper/db/v4/internal/testsuite"
 )
 
-type BondTests struct {
-	testsuite.BondTestSuite
+type RecordTests struct {
+	testsuite.RecordTestSuite
 }
 
-func (s *BondTests) SetupSuite() {
+func (s *RecordTests) SetupSuite() {
 	s.Helper = &Helper{}
 }
 
-func TestBond(t *testing.T) {
-	suite.Run(t, &BondTests{})
+func TestRecord(t *testing.T) {
+	suite.Run(t, &RecordTests{})
 }
diff --git a/internal/testsuite/model_suite.go b/internal/testsuite/record_suite.go
similarity index 69%
rename from internal/testsuite/model_suite.go
rename to internal/testsuite/record_suite.go
index 48eea60..19fd46f 100644
--- a/internal/testsuite/model_suite.go
+++ b/internal/testsuite/record_suite.go
@@ -1,120 +1,78 @@
 package testsuite
 
 import (
+	"database/sql"
 	"fmt"
 	"time"
 
-	"database/sql"
-
 	"github.com/stretchr/testify/suite"
 	"github.com/upper/db/v4"
 	"github.com/upper/db/v4/sqlbuilder"
 )
 
-type Session struct {
-	sqlbuilder.Session
-}
-
-func (d *Session) Accounts() *Accounts {
-	return NewAccounts(d.Session)
-}
-
-func (d *Session) UserCollection() *UserCollection {
-	return NewUserCollection(d.Session)
-}
-
-func (d *Session) LogCollection() *LogCollection {
-	return NewLogCollection(d.Session)
-}
-
-func NewSession(sess db.Session) *Session {
-	return &Session{Session: sess.(sqlbuilder.Session)}
-}
-
-func NewAccounts(sess db.Session) *Accounts {
-	return &Accounts{Collection: sess.Collection("accounts")}
+func Accounts(sess db.Session) db.Collection {
+	return sess.Collection("accounts")
 }
 
-func NewUserCollection(sess db.Session) *UserCollection {
-	return &UserCollection{Collection: sess.Collection("users")}
+func Users(sess db.Session) db.Collection {
+	return sess.Collection("users")
 }
 
-func NewLogCollection(sess db.Session) *LogCollection {
-	return &LogCollection{Collection: sess.Collection("logs")}
+func Logs(sess db.Session) db.Collection {
+	return sess.Collection("logs")
 }
 
 type Log struct {
-	sqlbuilder.Item
-
 	ID      uint64 `db:"id,omitempty"`
 	Message string `db:"message"`
 }
 
 type Account struct {
-	sqlbuilder.Item
-
 	ID        uint64     `db:"id,omitempty"`
 	Name      string     `db:"name"`
 	Disabled  bool       `db:"disabled"`
 	CreatedAt *time.Time `db:"created_at,omitempty"`
 }
 
-func (account *Account) Collection(sess db.Session) db.Collection {
-	return sess.Collection("accounts")
+func (*Account) Collection(sess db.Session) db.Collection {
+	return Accounts(sess)
 }
 
-func (account *Account) AfterCreate(sess db.Session) error {
-	message := fmt.Sprintf("Account %q was created.", account.Name)
+func (a *Account) AfterCreate(sess db.Session) error {
+	message := fmt.Sprintf("Account %q was created.", a.Name)
 	return sess.Save(&Log{Message: message})
 }
 
 type User struct {
-	sqlbuilder.Item
-
 	ID        uint64 `db:"id,omitempty"`
 	AccountID uint64 `db:"account_id"`
 	Username  string `db:"username"`
 }
 
-func (user *User) AfterCreate(sess db.Session) error {
-	message := fmt.Sprintf("User %q was created.", user.Username)
+func (u *User) AfterCreate(sess db.Session) error {
+	message := fmt.Sprintf("User %q was created.", u.Username)
 	return sess.Save(&Log{Message: message})
 }
 
-func (user *User) Collection(sess db.Session) db.Collection {
-	return sess.Collection("users")
-}
-
-func (l *Log) Collection(sess db.Session) db.Collection {
-	return sess.Collection("logs")
+func (*User) Collection(sess db.Session) db.Collection {
+	return Users(sess)
 }
 
-type LogCollection struct {
-	db.Collection
+func (*Log) Collection(sess db.Session) db.Collection {
+	return Logs(sess)
 }
 
-type Accounts struct {
-	db.Collection
-}
-
-type UserCollection struct {
-	db.Collection
-}
-
-type BondTestSuite struct {
-	sess *Session
-
+type RecordTestSuite struct {
 	suite.Suite
-
 	Helper
 }
 
-func (s *BondTestSuite) AfterTest(suiteName, testName string) {
+func (s *RecordTestSuite) AfterTest(suiteName, testName string) {
 	err := s.TearDown()
 	s.NoError(err)
 }
 
-func (s *BondTestSuite) BeforeTest(suiteName, testName string) {
+func (s *RecordTestSuite) BeforeTest(suiteName, testName string) {
 	err := s.TearUp()
 	s.NoError(err)
 
@@ -127,15 +85,9 @@ func (s *BondTestSuite) BeforeTest(suiteName, testName string) {
 		err = cols[i].Truncate()
 		s.NoError(err)
 	}
-
-	s.sess = NewSession(sess)
 }
 
-func (s *BondTestSuite) Session() *Session {
-	return s.sess
-}
-
-func (s *BondTestSuite) TestFindOne() {
+func (s *RecordTestSuite) TestFindOne() {
 	var err error
 	sess := s.Session()
 
@@ -144,44 +96,28 @@ func (s *BondTestSuite) TestFindOne() {
 	s.NoError(err)
 
 	s.NotZero(user.ID)
-
-	s.Equal(db.M{}, user.Changes())
-	user.Username = "jose-2"
-	s.Equal(db.M{"username": "jose-2"}, user.Changes())
+	userID := user.ID
 
 	user = User{}
-	err = sess.Get(&user, db.Cond{"username": "jose"})
-
+	err = Users(sess).Find(userID).One(&user)
 	s.NoError(err)
+	s.Equal("jose", user.Username)
 
-	s.Equal(db.M{}, user.Changes())
-	user.Username = "catalina"
-	s.Equal(db.M{"username": "catalina"}, user.Changes())
-
-	err = sess.Save(&user)
+	user = User{}
+	err = sess.Get(&user, db.Cond{"username": "jose"})
 	s.NoError(err)
+	s.Equal("jose", user.Username)
 
-	s.Equal(db.M{}, user.Changes())
-	user.Username = "nala"
-	s.Equal(db.M{"username": "nala"}, user.Changes())
-
+	user.Username = "Catalina"
 	err = sess.Save(&user)
 	s.NoError(err)
-	s.Equal(db.M{}, user.Changes())
-
-	userID := user.ID
-
-	user = User{}
-	err = sess.Get(&user, userID)
-
-	s.Equal("nala", user.Username)
 
 	user = User{}
 	err = sess.Get(&user, userID)
 	s.NoError(err)
-	s.Equal("nala", user.Username)
+	s.Equal("Catalina", user.Username)
 
-	err = user.Delete(sess)
+	err = sess.Delete(&user)
 	s.NoError(err)
 
 	err = sess.Get(&user, userID)
@@ -193,7 +129,7 @@ func (s *BondTestSuite) TestFindOne() {
 	s.Error(err)
 }
 
-func (s *BondTestSuite) TestAccounts() {
+func (s *RecordTestSuite) TestAccounts() {
 	sess := s.Session()
 
 	user := User{Username: "peter"}
@@ -205,7 +141,7 @@ func (s *BondTestSuite) TestAccounts() {
 	err = sess.Save(&user)
 	s.Error(err, "username should be unique")
 
-	account1 := Account{Name: "Pressly"}
+	account1 := Account{Name: "skywalker"}
 	err = sess.Save(&account1)
 	s.NoError(err)
 
@@ -230,19 +166,19 @@ func (s *BondTestSuite) TestAccounts() {
 	err = sess.Save(&account1)
 	s.NoError(err)
 
-	count, err := sess.Accounts().Count()
+	count, err := Accounts(sess).Count()
 	s.NoError(err)
 	s.Equal(uint64(1), count)
 
-	err = account1.Delete(sess)
+	err = sess.Delete(&account1)
 	s.NoError(err)
 
-	count, err = sess.Accounts().Find().Count()
+	count, err = Accounts(sess).Find().Count()
 	s.NoError(err)
 	s.Zero(count)
 }
 
-func (s *BondTestSuite) TestDelete() {
+func (s *RecordTestSuite) TestDelete() {
 	sess := s.Session()
 
 	account := Account{Name: "Pressly"}
@@ -251,16 +187,16 @@ func (s *BondTestSuite) TestDelete() {
 	s.NotZero(account.ID)
 
 	// Delete by query -- without callbacks
-	err = sess.Accounts().
+	err = Accounts(sess).
 		Find(account.ID).
 		Delete()
 	s.NoError(err)
 
-	count, err := sess.Accounts().Find(account.ID).Count()
+	count, err := Accounts(sess).Find(account.ID).Count()
 	s.Zero(count)
 }
 
-func (s *BondTestSuite) TestSlices() {
+func (s *RecordTestSuite) TestSlices() {
 	sess := s.Session()
 
 	err := sess.Save(&Account{Name: "Apple"})
@@ -270,14 +206,14 @@ func (s *BondTestSuite) TestSlices() {
 	s.NoError(err)
 
 	var accounts []*Account
-	err = sess.Accounts().
+	err = Accounts(sess).
 		Find(db.Cond{}).
 		All(&accounts)
 	s.NoError(err)
 	s.Len(accounts, 2)
 }
 
-func (s *BondTestSuite) TestSelectOnlyIDs() {
+func (s *RecordTestSuite) TestSelectOnlyIDs() {
 	sess := s.Session()
 
 	err := sess.Save(&Account{Name: "Apple"})
@@ -290,7 +226,7 @@ func (s *BondTestSuite) TestSelectOnlyIDs() {
 		Id int64 `db:"id"`
 	}
 
-	err = sess.Accounts().
+	err = Accounts(sess).
 		Find().
 		Select("id").All(&ids)
 	s.NoError(err)
@@ -298,8 +234,8 @@ func (s *BondTestSuite) TestSelectOnlyIDs() {
 	s.NotEmpty(ids[0])
 }
 
-func (s *BondTestSuite) TestTx() {
-	sess := s.Session()
+func (s *RecordTestSuite) TestTx() {
+	sess := s.Session().(sqlbuilder.Session)
 
 	user := User{Username: "peter"}
 	err := sess.Save(&user)
@@ -364,7 +300,7 @@ func (s *BondTestSuite) TestTx() {
 	s.Error(err)
 }
 
-func (s *BondTestSuite) TestInheritedTx() {
+func (s *RecordTestSuite) TestInheritedTx() {
 	sess := s.Session()
 
 	sqlDB := sess.Driver().(*sql.DB)
@@ -440,7 +376,7 @@ func (s *BondTestSuite) TestInheritedTx() {
 	s.NoError(err)
 }
 
-func (s *BondTestSuite) TestUnknownCollection() {
+func (s *RecordTestSuite) TestUnknownCollection() {
 	var err error
 	sess := s.Session()
 
diff --git a/internal/unsupported/interfaces_test.go b/internal/unsupported/interfaces_test.go
deleted file mode 100644
index 709213d..0000000
--- a/internal/unsupported/interfaces_test.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package unsupported
-
-import (
-	"github.com/upper/db/v4"
-)
-
-var _ db.Item = &Item{}
diff --git a/sqlbuilder/interfaces_test.go b/sqlbuilder/interfaces_test.go
deleted file mode 100644
index b06a131..0000000
--- a/sqlbuilder/interfaces_test.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package sqlbuilder
-
-import (
-	"github.com/upper/db/v4"
-)
-
-var _ db.Item = &Item{}