diff --git a/Makefile b/Makefile index 7fe2b3b..e2c4b63 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ test: tail -n +2 $(COVERAGE_PROFILE) >> $(COVERAGE_REPORT); \ rm $(COVERAGE_PROFILE); \ fi; \ - for dir in `find . -name "*.go" | grep -o '.*/' | sort -u | grep -v './tests/' | grep -v './fixtures/'`; do \ + for dir in `find . -name "*.go" | grep -o '.*/' | sort -u | grep -v './tests/' | grep -v './fixtures/' | grep -v './benchmarks/'`; do \ go test $$dir -coverprofile=$(COVERAGE_PROFILE) -covermode=$(COVERAGE_MODE); \ if [ $$? != 0 ]; then \ exit 2; \ diff --git a/README.md b/README.md index c15fa9f..3e77f7a 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Support for arrays of all basic Go types and all JSON and arrays operators is pr * [Query with relationships](#query-with-relationships) * [Querying JSON](#querying-json) * [Transactions](#transactions) +* [Benchmarks](#benchmarks) * [Contributing](#contributing) ## Installation @@ -600,6 +601,40 @@ store.Transaction(func(s *UserStore) error { * `time.Time` and `url.URL` need to be used as is. That is, you can not use a type `Foo` being `type Foo time.Time`. `time.Time` and `url.URL` are types that are treated in a special way, if you do that, it would be the same as saying `type Foo struct { ... }` and kallax would no longer be able to identify the correct type. * Multidimensional arrays or slices are **not supported** except inside a JSON field. +## Benchmarks + +Here are some benchmarks against [GORM](https://github.com/jinzhu/gorm) and `database/sql`, which is one of the most popular ORMs for Go. In the future we might add benchmarks for some more complex cases and other available ORMs. + +``` +BenchmarkKallaxInsertWithRelationships-4 300 4767574 ns/op 19130 B/op 441 allocs/op +BenchmarkRawSQLInsertWithRelationships-4 300 4467652 ns/op 3997 B/op 114 allocs/op +BenchmarkGORMInsertWithRelationships-4 300 4813566 ns/op 34550 B/op 597 allocs/op + +BenchmarkKallaxInsert-4 500 3650913 ns/op 3569 B/op 85 allocs/op +BenchmarkRawSQLInsert-4 500 3530908 ns/op 901 B/op 24 allocs/op +BenchmarkGORMInsert-4 300 3716373 ns/op 4558 B/op 104 allocs/op + +BenchmarkKallaxQueryRelationships/query-4 1000 1535928 ns/op 59335 B/op 1557 allocs/op +BenchmarkRawSQLQueryRelationships/query-4 30 44225743 ns/op 201288 B/op 6021 allocs/op +BenchmarkGORMQueryRelationships/query-4 300 4012112 ns/op 1068887 B/op 20827 allocs/op + +BenchmarkKallaxQuery/query-4 3000 433453 ns/op 50697 B/op 1893 allocs/op +BenchmarkRawSQLQuery/query-4 5000 368947 ns/op 37392 B/op 1522 allocs/op +BenchmarkGORMQuery/query-4 2000 1311137 ns/op 427308 B/op 7065 allocs/op + +PASS +ok gopkg.in/src-d/go-kallax.v1/benchmarks 31.313s +``` + +As we can see on the benchmark, the performance loss is not very much compared to raw `database/sql`, while GORMs performance loss is very big and the memory consumption is way higher. + +Source code of the benchmarks can be found on the [benchmarks](https://github.com/src-d/go-kallax/tree/master/benchmarks) folder. + +**Notes:** + +* Benchmarks were run on a 2015 MacBook Pro with i5 and 8GB of RAM and 128GB SSD hard drive running fedora 25. +* Benchmark of `database/sql` for querying with relationships is implemented with a very naive 1+n solution. That's why the result is that bad. + ## Contributing ### Reporting bugs diff --git a/benchmarks/bench_test.go b/benchmarks/bench_test.go new file mode 100644 index 0000000..20569d5 --- /dev/null +++ b/benchmarks/bench_test.go @@ -0,0 +1,379 @@ +package benchmark + +import ( + "database/sql" + "fmt" + "os" + "testing" + + "github.com/jinzhu/gorm" +) + +func envOrDefault(key string, def string) string { + v := os.Getenv(key) + if v == "" { + v = def + } + return v +} + +func dbURL() string { + return fmt.Sprintf( + "postgres://%s:%s@0.0.0.0:5432/%s?sslmode=disable", + envOrDefault("DBUSER", "testing"), + envOrDefault("DBPASS", "testing"), + envOrDefault("DBNAME", "testing"), + ) +} + +func openTestDB(b *testing.B) *sql.DB { + db, err := sql.Open("postgres", dbURL()) + if err != nil { + b.Fatalf("error opening db: %s", err) + } + return db +} + +func openGormTestDB(b *testing.B) *gorm.DB { + db, err := gorm.Open("postgres", dbURL()) + if err != nil { + b.Fatalf("error opening db: %s", err) + } + return db +} + +var schemas = []string{ + `CREATE TABLE IF NOT EXISTS people ( + id serial primary key, + name text + )`, + `CREATE TABLE IF NOT EXISTS pets ( + id serial primary key, + name text, + kind text, + person_id integer references people(id) + )`, +} + +var tables = []string{"pets", "people"} + +func setupDB(b *testing.B, db *sql.DB) *sql.DB { + for _, s := range schemas { + _, err := db.Exec(s) + if err != nil { + b.Fatalf("error creating schema: %s", err) + } + } + + return db +} + +func teardownDB(b *testing.B, db *sql.DB) { + for _, t := range tables { + _, err := db.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", t)) + if err != nil { + b.Fatalf("error dropping table: %s", err) + } + } + + if err := db.Close(); err != nil { + b.Fatalf("error closing db: %s", err) + } +} + +func mkPersonWithRels() *Person { + return &Person{ + Name: "Dolan", + Pets: []*Pet{ + {Name: "Garfield", Kind: Cat}, + {Name: "Oddie", Kind: Dog}, + {Name: "Reptar", Kind: Fish}, + }, + } +} + +func mkGormPersonWithRels() *GORMPerson { + return &GORMPerson{ + Name: "Dolan", + Pets: []*GORMPet{ + {Name: "Garfield", Kind: string(Cat)}, + {Name: "Oddie", Kind: string(Dog)}, + {Name: "Reptar", Kind: string(Fish)}, + }, + } +} + +func BenchmarkKallaxInsertWithRelationships(b *testing.B) { + db := setupDB(b, openTestDB(b)) + defer teardownDB(b, db) + + store := NewPersonStore(db) + for i := 0; i < b.N; i++ { + if err := store.Insert(mkPersonWithRels()); err != nil { + b.Fatalf("error inserting: %s", err) + } + } +} + +func BenchmarkRawSQLInsertWithRelationships(b *testing.B) { + db := setupDB(b, openTestDB(b)) + defer teardownDB(b, db) + + for i := 0; i < b.N; i++ { + p := mkPersonWithRels() + tx, err := db.Begin() + + err = tx.QueryRow("INSERT INTO people (name) VALUES ($1) RETURNING id", p.Name). + Scan(&p.ID) + if err != nil { + b.Fatalf("error inserting: %s", err) + } + + for _, pet := range p.Pets { + err := tx.QueryRow( + "INSERT INTO pets (name, kind, person_id) VALUES ($1, $2, $3) RETURNING id", + pet.Name, string(pet.Kind), p.ID, + ).Scan(&pet.ID) + if err != nil { + b.Fatalf("error inserting rel: %s", err) + } + } + + if err := tx.Commit(); err != nil { + b.Fatalf("error committing transaction: %s", err) + } + } +} + +func BenchmarkGORMInsertWithRelationships(b *testing.B) { + store := openGormTestDB(b) + setupDB(b, store.DB()) + defer teardownDB(b, store.DB()) + + for i := 0; i < b.N; i++ { + if db := store.Create(mkGormPersonWithRels()); db.Error != nil { + b.Fatalf("error inserting: %s", db.Error) + } + } +} + +func BenchmarkKallaxInsert(b *testing.B) { + db := setupDB(b, openTestDB(b)) + defer teardownDB(b, db) + + store := NewPersonStore(db) + for i := 0; i < b.N; i++ { + if err := store.Insert(&Person{Name: "foo"}); err != nil { + b.Fatalf("error inserting: %s", err) + } + } +} + +func BenchmarkRawSQLInsert(b *testing.B) { + db := setupDB(b, openTestDB(b)) + defer teardownDB(b, db) + + for i := 0; i < b.N; i++ { + p := &Person{Name: "foo"} + + err := db.QueryRow("INSERT INTO people (name) VALUES ($1) RETURNING id", p.Name). + Scan(&p.ID) + if err != nil { + b.Fatalf("error inserting: %s", err) + } + } +} + +func BenchmarkGORMInsert(b *testing.B) { + store := openGormTestDB(b) + setupDB(b, store.DB()) + defer teardownDB(b, store.DB()) + + for i := 0; i < b.N; i++ { + if db := store.Create(&GORMPerson{Name: "foo"}); db.Error != nil { + b.Fatalf("error inserting: %s", db.Error) + } + } +} + +func BenchmarkKallaxQueryRelationships(b *testing.B) { + db := openTestDB(b) + setupDB(b, db) + defer teardownDB(b, db) + + store := NewPersonStore(db) + for i := 0; i < 200; i++ { + if err := store.Insert(mkPersonWithRels()); err != nil { + b.Fatalf("error inserting: %s", err) + } + } + + b.Run("query", func(b *testing.B) { + for i := 0; i < b.N; i++ { + rs, err := store.Find(NewPersonQuery().WithPets(nil).Limit(100)) + if err != nil { + b.Fatalf("error retrieving persons: %s", err) + } + + _, err = rs.All() + if err != nil { + b.Fatalf("error retrieving persons: %s", err) + } + } + }) +} + +func BenchmarkRawSQLQueryRelationships(b *testing.B) { + db := openTestDB(b) + setupDB(b, db) + defer teardownDB(b, db) + + store := NewPersonStore(db) + for i := 0; i < 200; i++ { + if err := store.Insert(mkPersonWithRels()); err != nil { + b.Fatalf("error inserting: %s", err) + } + } + + b.Run("query", func(b *testing.B) { + for i := 0; i < b.N; i++ { + rows, err := db.Query("SELECT * FROM people") + if err != nil { + b.Fatalf("error querying: %s", err) + } + + var people []*GORMPerson + for rows.Next() { + var p GORMPerson + if err := rows.Scan(&p.ID, &p.Name); err != nil { + b.Fatalf("error scanning: %s", err) + } + + r, err := db.Query("SELECT * FROM pets WHERE person_id = $1", p.ID) + if err != nil { + b.Fatalf("error querying relationships: %s", err) + } + + for r.Next() { + var pet GORMPet + if err := r.Scan(&pet.ID, &pet.Name, &pet.Kind, &pet.PersonID); err != nil { + b.Fatalf("error scanning relationship: %s", err) + } + p.Pets = append(p.Pets, &pet) + } + + r.Close() + people = append(people, &p) + } + + _ = people + rows.Close() + } + }) +} + +func BenchmarkGORMQueryRelationships(b *testing.B) { + store := openGormTestDB(b) + setupDB(b, store.DB()) + defer teardownDB(b, store.DB()) + + for i := 0; i < 300; i++ { + if db := store.Create(mkGormPersonWithRels()); db.Error != nil { + b.Fatalf("error inserting: %s", db.Error) + } + } + + b.Run("query", func(b *testing.B) { + for i := 0; i < b.N; i++ { + var persons []*GORMPerson + db := store.Preload("Pets").Limit(100).Find(&persons) + if db.Error != nil { + b.Fatalf("error retrieving persons: %s", db.Error) + } + } + }) +} + +func BenchmarkKallaxQuery(b *testing.B) { + db := openTestDB(b) + setupDB(b, db) + defer teardownDB(b, db) + + store := NewPersonStore(db) + for i := 0; i < 300; i++ { + if err := store.Insert(&Person{Name: "foo"}); err != nil { + b.Fatalf("error inserting: %s", err) + } + } + + b.Run("query", func(b *testing.B) { + for i := 0; i < b.N; i++ { + rs, err := store.Find(NewPersonQuery()) + if err != nil { + b.Fatalf("error retrieving persons: %s", err) + } + + _, err = rs.All() + if err != nil { + b.Fatalf("error retrieving persons: %s", err) + } + } + }) +} + +func BenchmarkRawSQLQuery(b *testing.B) { + db := openTestDB(b) + setupDB(b, db) + defer teardownDB(b, db) + + store := NewPersonStore(db) + for i := 0; i < 300; i++ { + if err := store.Insert(&Person{Name: "foo"}); err != nil { + b.Fatalf("error inserting: %s", err) + } + } + + b.Run("query", func(b *testing.B) { + for i := 0; i < b.N; i++ { + rows, err := db.Query("SELECT * FROM people") + if err != nil { + b.Fatalf("error querying: %s", err) + } + + var people []*Person + for rows.Next() { + var p Person + err := rows.Scan(&p.ID, &p.Name) + if err != nil { + b.Fatalf("error scanning: %s", err) + } + people = append(people, &p) + } + + _ = people + rows.Close() + } + }) +} + +func BenchmarkGORMQuery(b *testing.B) { + store := openGormTestDB(b) + setupDB(b, store.DB()) + defer teardownDB(b, store.DB()) + + for i := 0; i < 200; i++ { + if db := store.Create(&GORMPerson{Name: "foo"}); db.Error != nil { + b.Fatalf("error inserting: %s", db.Error) + } + } + + b.Run("query", func(b *testing.B) { + for i := 0; i < b.N; i++ { + var persons []*GORMPerson + db := store.Find(&persons) + if db.Error != nil { + b.Fatalf("error retrieving persons:", db.Error) + } + } + }) +} diff --git a/benchmarks/kallax.go b/benchmarks/kallax.go new file mode 100644 index 0000000..374d36b --- /dev/null +++ b/benchmarks/kallax.go @@ -0,0 +1,1034 @@ +// IMPORTANT! This is auto generated code by https://github.com/src-d/go-kallax +// Please, do not touch the code below, and if you do, do it under your own +// risk. Take into account that all the code you write here will be completely +// erased from earth the next time you generate the kallax models. +package benchmark + +import ( + "database/sql" + "fmt" + + "gopkg.in/src-d/go-kallax.v1" + "gopkg.in/src-d/go-kallax.v1/types" +) + +var _ types.SQLType +var _ fmt.Formatter + +// NewPerson returns a new instance of Person. +func NewPerson() (record *Person) { + return new(Person) +} + +// GetID returns the primary key of the model. +func (r *Person) GetID() kallax.Identifier { + return (*kallax.NumericID)(&r.ID) +} + +// ColumnAddress returns the pointer to the value of the given column. +func (r *Person) ColumnAddress(col string) (interface{}, error) { + switch col { + case "id": + return (*kallax.NumericID)(&r.ID), nil + case "name": + return &r.Name, nil + + default: + return nil, fmt.Errorf("kallax: invalid column in Person: %s", col) + } +} + +// Value returns the value of the given column. +func (r *Person) Value(col string) (interface{}, error) { + switch col { + case "id": + return r.ID, nil + case "name": + return r.Name, nil + + default: + return nil, fmt.Errorf("kallax: invalid column in Person: %s", col) + } +} + +// NewRelationshipRecord returns a new record for the relatiobship in the given +// field. +func (r *Person) NewRelationshipRecord(field string) (kallax.Record, error) { + switch field { + case "Pets": + return new(Pet), nil + + } + return nil, fmt.Errorf("kallax: model Person has no relationship %s", field) +} + +// SetRelationship sets the given relationship in the given field. +func (r *Person) SetRelationship(field string, rel interface{}) error { + switch field { + case "Pets": + records, ok := rel.([]kallax.Record) + if !ok { + return fmt.Errorf("kallax: relationship field %s needs a collection of records, not %T", field, rel) + } + + r.Pets = make([]*Pet, len(records)) + for i, record := range records { + rel, ok := record.(*Pet) + if !ok { + return fmt.Errorf("kallax: element of type %T cannot be added to relationship %s", record, field) + } + r.Pets[i] = rel + } + return nil + + } + return fmt.Errorf("kallax: model Person has no relationship %s", field) +} + +// PersonStore is the entity to access the records of the type Person +// in the database. +type PersonStore struct { + *kallax.Store +} + +// NewPersonStore creates a new instance of PersonStore +// using a SQL database. +func NewPersonStore(db *sql.DB) *PersonStore { + return &PersonStore{kallax.NewStore(db)} +} + +func (s *PersonStore) relationshipRecords(record *Person) []kallax.RecordWithSchema { + record.ClearVirtualColumns() + var records []kallax.RecordWithSchema + + for _, rec := range record.Pets { + rec.ClearVirtualColumns() + rec.AddVirtualColumn("person_id", record.GetID()) + records = append(records, kallax.RecordWithSchema{ + Schema.Pet.BaseSchema, + rec, + }) + } + + return records +} + +// Insert inserts a Person in the database. A non-persisted object is +// required for this operation. +func (s *PersonStore) Insert(record *Person) error { + + records := s.relationshipRecords(record) + if len(records) > 0 { + return s.Store.Transaction(func(s *kallax.Store) error { + if err := s.Insert(Schema.Person.BaseSchema, record); err != nil { + return err + } + + for _, r := range records { + if err := kallax.ApplyBeforeEvents(r.Record); err != nil { + return err + } + persisted := r.Record.IsPersisted() + + if _, err := s.Save(r.Schema, r.Record); err != nil { + return err + } + + if err := kallax.ApplyAfterEvents(r.Record, persisted); err != nil { + return err + } + } + + return nil + }) + } + + return s.Store.Insert(Schema.Person.BaseSchema, record) + +} + +// Update updates the given record on the database. If the columns are given, +// only these columns will be updated. Otherwise all of them will be. +// Be very careful with this, as you will have a potentially different object +// in memory but not on the database. +// Only writable records can be updated. Writable objects are those that have +// been just inserted or retrieved using a query with no custom select fields. +func (s *PersonStore) Update(record *Person, cols ...kallax.SchemaField) (updated int64, err error) { + + records := s.relationshipRecords(record) + if len(records) > 0 { + err = s.Store.Transaction(func(s *kallax.Store) error { + updated, err = s.Update(Schema.Person.BaseSchema, record, cols...) + if err != nil { + return err + } + + for _, r := range records { + if err := kallax.ApplyBeforeEvents(r.Record); err != nil { + return err + } + persisted := r.Record.IsPersisted() + + if _, err := s.Save(r.Schema, r.Record); err != nil { + return err + } + + if err := kallax.ApplyAfterEvents(r.Record, persisted); err != nil { + return err + } + } + + return nil + }) + if err != nil { + return 0, err + } + + return updated, nil + } + + return s.Store.Update(Schema.Person.BaseSchema, record, cols...) + +} + +// Save inserts the object if the record is not persisted, otherwise it updates +// it. Same rules of Update and Insert apply depending on the case. +func (s *PersonStore) Save(record *Person) (updated bool, err error) { + if !record.IsPersisted() { + return false, s.Insert(record) + } + + rowsUpdated, err := s.Update(record) + if err != nil { + return false, err + } + + return rowsUpdated > 0, nil +} + +// Delete removes the given record from the database. +func (s *PersonStore) Delete(record *Person) error { + + return s.Store.Delete(Schema.Person.BaseSchema, record) + +} + +// Find returns the set of results for the given query. +func (s *PersonStore) Find(q *PersonQuery) (*PersonResultSet, error) { + rs, err := s.Store.Find(q) + if err != nil { + return nil, err + } + + return NewPersonResultSet(rs), nil +} + +// MustFind returns the set of results for the given query, but panics if there +// is any error. +func (s *PersonStore) MustFind(q *PersonQuery) *PersonResultSet { + return NewPersonResultSet(s.Store.MustFind(q)) +} + +// Count returns the number of rows that would be retrieved with the given +// query. +func (s *PersonStore) Count(q *PersonQuery) (int64, error) { + return s.Store.Count(q) +} + +// MustCount returns the number of rows that would be retrieved with the given +// query, but panics if there is an error. +func (s *PersonStore) MustCount(q *PersonQuery) int64 { + return s.Store.MustCount(q) +} + +// FindOne returns the first row returned by the given query. +// `ErrNotFound` is returned if there are no results. +func (s *PersonStore) FindOne(q *PersonQuery) (*Person, error) { + q.Limit(1) + q.Offset(0) + rs, err := s.Find(q) + if err != nil { + return nil, err + } + + if !rs.Next() { + return nil, kallax.ErrNotFound + } + + record, err := rs.Get() + if err != nil { + return nil, err + } + + if err := rs.Close(); err != nil { + return nil, err + } + + return record, nil +} + +// MustFindOne returns the first row retrieved by the given query. It panics +// if there is an error or if there are no rows. +func (s *PersonStore) MustFindOne(q *PersonQuery) *Person { + record, err := s.FindOne(q) + if err != nil { + panic(err) + } + return record +} + +// Reload refreshes the Person with the data in the database and +// makes it writable. +func (s *PersonStore) Reload(record *Person) error { + return s.Store.Reload(Schema.Person.BaseSchema, record) +} + +// Transaction executes the given callback in a transaction and rollbacks if +// an error is returned. +// The transaction is only open in the store passed as a parameter to the +// callback. +func (s *PersonStore) Transaction(callback func(*PersonStore) error) error { + if callback == nil { + return kallax.ErrInvalidTxCallback + } + + return s.Store.Transaction(func(store *kallax.Store) error { + return callback(&PersonStore{store}) + }) +} + +// RemovePets removes the given items of the Pets field of the +// model. If no items are given, it removes all of them. +// The items will also be removed from the passed record inside this method. +func (s *PersonStore) RemovePets(record *Person, deleted ...*Pet) error { + var updated []*Pet + var clear bool + if len(deleted) == 0 { + clear = true + deleted = record.Pets + if len(deleted) == 0 { + return nil + } + } + + if len(deleted) > 1 { + err := s.Store.Transaction(func(s *kallax.Store) error { + for _, d := range deleted { + var r kallax.Record = d + + if beforeDeleter, ok := r.(kallax.BeforeDeleter); ok { + if err := beforeDeleter.BeforeDelete(); err != nil { + return err + } + } + + if err := s.Delete(Schema.Pet.BaseSchema, d); err != nil { + return err + } + + if afterDeleter, ok := r.(kallax.AfterDeleter); ok { + if err := afterDeleter.AfterDelete(); err != nil { + return err + } + } + } + return nil + }) + + if err != nil { + return err + } + + if clear { + record.Pets = nil + return nil + } + } else { + var r kallax.Record = deleted[0] + if beforeDeleter, ok := r.(kallax.BeforeDeleter); ok { + if err := beforeDeleter.BeforeDelete(); err != nil { + return err + } + } + + var err error + if afterDeleter, ok := r.(kallax.AfterDeleter); ok { + err = s.Store.Transaction(func(s *kallax.Store) error { + err := s.Delete(Schema.Pet.BaseSchema, r) + if err != nil { + return err + } + + return afterDeleter.AfterDelete() + }) + } else { + err = s.Store.Delete(Schema.Pet.BaseSchema, deleted[0]) + } + + if err != nil { + return err + } + } + + for _, r := range record.Pets { + var found bool + for _, d := range deleted { + if d.GetID().Equals(r.GetID()) { + found = true + break + } + } + if !found { + updated = append(updated, r) + } + } + record.Pets = updated + return nil +} + +// PersonQuery is the object used to create queries for the Person +// entity. +type PersonQuery struct { + *kallax.BaseQuery +} + +// NewPersonQuery returns a new instance of PersonQuery. +func NewPersonQuery() *PersonQuery { + return &PersonQuery{ + BaseQuery: kallax.NewBaseQuery(Schema.Person.BaseSchema), + } +} + +// Select adds columns to select in the query. +func (q *PersonQuery) Select(columns ...kallax.SchemaField) *PersonQuery { + if len(columns) == 0 { + return q + } + q.BaseQuery.Select(columns...) + return q +} + +// SelectNot excludes columns from being selected in the query. +func (q *PersonQuery) SelectNot(columns ...kallax.SchemaField) *PersonQuery { + q.BaseQuery.SelectNot(columns...) + return q +} + +// Copy returns a new identical copy of the query. Remember queries are mutable +// so make a copy any time you need to reuse them. +func (q *PersonQuery) Copy() *PersonQuery { + return &PersonQuery{ + BaseQuery: q.BaseQuery.Copy(), + } +} + +// Order adds order clauses to the query for the given columns. +func (q *PersonQuery) Order(cols ...kallax.ColumnOrder) *PersonQuery { + q.BaseQuery.Order(cols...) + return q +} + +// BatchSize sets the number of items to fetch per batch when there are 1:N +// relationships selected in the query. +func (q *PersonQuery) BatchSize(size uint64) *PersonQuery { + q.BaseQuery.BatchSize(size) + return q +} + +// Limit sets the max number of items to retrieve. +func (q *PersonQuery) Limit(n uint64) *PersonQuery { + q.BaseQuery.Limit(n) + return q +} + +// Offset sets the number of items to skip from the result set of items. +func (q *PersonQuery) Offset(n uint64) *PersonQuery { + q.BaseQuery.Offset(n) + return q +} + +// Where adds a condition to the query. All conditions added are concatenated +// using a logical AND. +func (q *PersonQuery) Where(cond kallax.Condition) *PersonQuery { + q.BaseQuery.Where(cond) + return q +} + +func (q *PersonQuery) WithPets(cond kallax.Condition) *PersonQuery { + q.AddRelation(Schema.Pet.BaseSchema, "Pets", kallax.OneToMany, cond) + return q +} + +// FindByID adds a new filter to the query that will require that +// the ID property is equal to one of the passed values; if no passed values, it will do nothing +func (q *PersonQuery) FindByID(v ...int64) *PersonQuery { + if len(v) == 0 { + return q + } + values := make([]interface{}, len(v)) + for i, val := range v { + values[i] = val + } + return q.Where(kallax.In(Schema.Person.ID, values...)) +} + +// FindByName adds a new filter to the query that will require that +// the Name property is equal to the passed value +func (q *PersonQuery) FindByName(v string) *PersonQuery { + return q.Where(kallax.Eq(Schema.Person.Name, v)) +} + +// PersonResultSet is the set of results returned by a query to the +// database. +type PersonResultSet struct { + ResultSet kallax.ResultSet + last *Person + lastErr error +} + +// NewPersonResultSet creates a new result set for rows of the type +// Person. +func NewPersonResultSet(rs kallax.ResultSet) *PersonResultSet { + return &PersonResultSet{ResultSet: rs} +} + +// Next fetches the next item in the result set and returns true if there is +// a next item. +// The result set is closed automatically when there are no more items. +func (rs *PersonResultSet) Next() bool { + if !rs.ResultSet.Next() { + rs.lastErr = rs.ResultSet.Close() + rs.last = nil + return false + } + + var record kallax.Record + record, rs.lastErr = rs.ResultSet.Get(Schema.Person.BaseSchema) + if rs.lastErr != nil { + rs.last = nil + } else { + var ok bool + rs.last, ok = record.(*Person) + if !ok { + rs.lastErr = fmt.Errorf("kallax: unable to convert record to *Person") + rs.last = nil + } + } + + return true +} + +// Get retrieves the last fetched item from the result set and the last error. +func (rs *PersonResultSet) Get() (*Person, error) { + return rs.last, rs.lastErr +} + +// ForEach iterates over the complete result set passing every record found to +// the given callback. It is possible to stop the iteration by returning +// `kallax.ErrStop` in the callback. +// Result set is always closed at the end. +func (rs *PersonResultSet) ForEach(fn func(*Person) error) error { + for rs.Next() { + record, err := rs.Get() + if err != nil { + return err + } + + if err := fn(record); err != nil { + if err == kallax.ErrStop { + return rs.Close() + } + + return err + } + } + return nil +} + +// All returns all records on the result set and closes the result set. +func (rs *PersonResultSet) All() ([]*Person, error) { + var result []*Person + for rs.Next() { + record, err := rs.Get() + if err != nil { + return nil, err + } + result = append(result, record) + } + return result, nil +} + +// One returns the first record on the result set and closes the result set. +func (rs *PersonResultSet) One() (*Person, error) { + if !rs.Next() { + return nil, kallax.ErrNotFound + } + + record, err := rs.Get() + if err != nil { + return nil, err + } + + if err := rs.Close(); err != nil { + return nil, err + } + + return record, nil +} + +// Err returns the last error occurred. +func (rs *PersonResultSet) Err() error { + return rs.lastErr +} + +// Close closes the result set. +func (rs *PersonResultSet) Close() error { + return rs.ResultSet.Close() +} + +// NewPet returns a new instance of Pet. +func NewPet() (record *Pet) { + return new(Pet) +} + +// GetID returns the primary key of the model. +func (r *Pet) GetID() kallax.Identifier { + return (*kallax.NumericID)(&r.ID) +} + +// ColumnAddress returns the pointer to the value of the given column. +func (r *Pet) ColumnAddress(col string) (interface{}, error) { + switch col { + case "id": + return (*kallax.NumericID)(&r.ID), nil + case "name": + return &r.Name, nil + case "kind": + return &r.Kind, nil + + default: + return nil, fmt.Errorf("kallax: invalid column in Pet: %s", col) + } +} + +// Value returns the value of the given column. +func (r *Pet) Value(col string) (interface{}, error) { + switch col { + case "id": + return r.ID, nil + case "name": + return r.Name, nil + case "kind": + return (string)(r.Kind), nil + + default: + return nil, fmt.Errorf("kallax: invalid column in Pet: %s", col) + } +} + +// NewRelationshipRecord returns a new record for the relatiobship in the given +// field. +func (r *Pet) NewRelationshipRecord(field string) (kallax.Record, error) { + return nil, fmt.Errorf("kallax: model Pet has no relationships") +} + +// SetRelationship sets the given relationship in the given field. +func (r *Pet) SetRelationship(field string, rel interface{}) error { + return fmt.Errorf("kallax: model Pet has no relationships") +} + +// PetStore is the entity to access the records of the type Pet +// in the database. +type PetStore struct { + *kallax.Store +} + +// NewPetStore creates a new instance of PetStore +// using a SQL database. +func NewPetStore(db *sql.DB) *PetStore { + return &PetStore{kallax.NewStore(db)} +} + +// Insert inserts a Pet in the database. A non-persisted object is +// required for this operation. +func (s *PetStore) Insert(record *Pet) error { + + return s.Store.Insert(Schema.Pet.BaseSchema, record) + +} + +// Update updates the given record on the database. If the columns are given, +// only these columns will be updated. Otherwise all of them will be. +// Be very careful with this, as you will have a potentially different object +// in memory but not on the database. +// Only writable records can be updated. Writable objects are those that have +// been just inserted or retrieved using a query with no custom select fields. +func (s *PetStore) Update(record *Pet, cols ...kallax.SchemaField) (updated int64, err error) { + + return s.Store.Update(Schema.Pet.BaseSchema, record, cols...) + +} + +// Save inserts the object if the record is not persisted, otherwise it updates +// it. Same rules of Update and Insert apply depending on the case. +func (s *PetStore) Save(record *Pet) (updated bool, err error) { + if !record.IsPersisted() { + return false, s.Insert(record) + } + + rowsUpdated, err := s.Update(record) + if err != nil { + return false, err + } + + return rowsUpdated > 0, nil +} + +// Delete removes the given record from the database. +func (s *PetStore) Delete(record *Pet) error { + + return s.Store.Delete(Schema.Pet.BaseSchema, record) + +} + +// Find returns the set of results for the given query. +func (s *PetStore) Find(q *PetQuery) (*PetResultSet, error) { + rs, err := s.Store.Find(q) + if err != nil { + return nil, err + } + + return NewPetResultSet(rs), nil +} + +// MustFind returns the set of results for the given query, but panics if there +// is any error. +func (s *PetStore) MustFind(q *PetQuery) *PetResultSet { + return NewPetResultSet(s.Store.MustFind(q)) +} + +// Count returns the number of rows that would be retrieved with the given +// query. +func (s *PetStore) Count(q *PetQuery) (int64, error) { + return s.Store.Count(q) +} + +// MustCount returns the number of rows that would be retrieved with the given +// query, but panics if there is an error. +func (s *PetStore) MustCount(q *PetQuery) int64 { + return s.Store.MustCount(q) +} + +// FindOne returns the first row returned by the given query. +// `ErrNotFound` is returned if there are no results. +func (s *PetStore) FindOne(q *PetQuery) (*Pet, error) { + q.Limit(1) + q.Offset(0) + rs, err := s.Find(q) + if err != nil { + return nil, err + } + + if !rs.Next() { + return nil, kallax.ErrNotFound + } + + record, err := rs.Get() + if err != nil { + return nil, err + } + + if err := rs.Close(); err != nil { + return nil, err + } + + return record, nil +} + +// MustFindOne returns the first row retrieved by the given query. It panics +// if there is an error or if there are no rows. +func (s *PetStore) MustFindOne(q *PetQuery) *Pet { + record, err := s.FindOne(q) + if err != nil { + panic(err) + } + return record +} + +// Reload refreshes the Pet with the data in the database and +// makes it writable. +func (s *PetStore) Reload(record *Pet) error { + return s.Store.Reload(Schema.Pet.BaseSchema, record) +} + +// Transaction executes the given callback in a transaction and rollbacks if +// an error is returned. +// The transaction is only open in the store passed as a parameter to the +// callback. +func (s *PetStore) Transaction(callback func(*PetStore) error) error { + if callback == nil { + return kallax.ErrInvalidTxCallback + } + + return s.Store.Transaction(func(store *kallax.Store) error { + return callback(&PetStore{store}) + }) +} + +// PetQuery is the object used to create queries for the Pet +// entity. +type PetQuery struct { + *kallax.BaseQuery +} + +// NewPetQuery returns a new instance of PetQuery. +func NewPetQuery() *PetQuery { + return &PetQuery{ + BaseQuery: kallax.NewBaseQuery(Schema.Pet.BaseSchema), + } +} + +// Select adds columns to select in the query. +func (q *PetQuery) Select(columns ...kallax.SchemaField) *PetQuery { + if len(columns) == 0 { + return q + } + q.BaseQuery.Select(columns...) + return q +} + +// SelectNot excludes columns from being selected in the query. +func (q *PetQuery) SelectNot(columns ...kallax.SchemaField) *PetQuery { + q.BaseQuery.SelectNot(columns...) + return q +} + +// Copy returns a new identical copy of the query. Remember queries are mutable +// so make a copy any time you need to reuse them. +func (q *PetQuery) Copy() *PetQuery { + return &PetQuery{ + BaseQuery: q.BaseQuery.Copy(), + } +} + +// Order adds order clauses to the query for the given columns. +func (q *PetQuery) Order(cols ...kallax.ColumnOrder) *PetQuery { + q.BaseQuery.Order(cols...) + return q +} + +// BatchSize sets the number of items to fetch per batch when there are 1:N +// relationships selected in the query. +func (q *PetQuery) BatchSize(size uint64) *PetQuery { + q.BaseQuery.BatchSize(size) + return q +} + +// Limit sets the max number of items to retrieve. +func (q *PetQuery) Limit(n uint64) *PetQuery { + q.BaseQuery.Limit(n) + return q +} + +// Offset sets the number of items to skip from the result set of items. +func (q *PetQuery) Offset(n uint64) *PetQuery { + q.BaseQuery.Offset(n) + return q +} + +// Where adds a condition to the query. All conditions added are concatenated +// using a logical AND. +func (q *PetQuery) Where(cond kallax.Condition) *PetQuery { + q.BaseQuery.Where(cond) + return q +} + +// FindByID adds a new filter to the query that will require that +// the ID property is equal to one of the passed values; if no passed values, it will do nothing +func (q *PetQuery) FindByID(v ...int64) *PetQuery { + if len(v) == 0 { + return q + } + values := make([]interface{}, len(v)) + for i, val := range v { + values[i] = val + } + return q.Where(kallax.In(Schema.Pet.ID, values...)) +} + +// FindByName adds a new filter to the query that will require that +// the Name property is equal to the passed value +func (q *PetQuery) FindByName(v string) *PetQuery { + return q.Where(kallax.Eq(Schema.Pet.Name, v)) +} + +// FindByKind adds a new filter to the query that will require that +// the Kind property is equal to the passed value +func (q *PetQuery) FindByKind(v PetKind) *PetQuery { + return q.Where(kallax.Eq(Schema.Pet.Kind, v)) +} + +// PetResultSet is the set of results returned by a query to the +// database. +type PetResultSet struct { + ResultSet kallax.ResultSet + last *Pet + lastErr error +} + +// NewPetResultSet creates a new result set for rows of the type +// Pet. +func NewPetResultSet(rs kallax.ResultSet) *PetResultSet { + return &PetResultSet{ResultSet: rs} +} + +// Next fetches the next item in the result set and returns true if there is +// a next item. +// The result set is closed automatically when there are no more items. +func (rs *PetResultSet) Next() bool { + if !rs.ResultSet.Next() { + rs.lastErr = rs.ResultSet.Close() + rs.last = nil + return false + } + + var record kallax.Record + record, rs.lastErr = rs.ResultSet.Get(Schema.Pet.BaseSchema) + if rs.lastErr != nil { + rs.last = nil + } else { + var ok bool + rs.last, ok = record.(*Pet) + if !ok { + rs.lastErr = fmt.Errorf("kallax: unable to convert record to *Pet") + rs.last = nil + } + } + + return true +} + +// Get retrieves the last fetched item from the result set and the last error. +func (rs *PetResultSet) Get() (*Pet, error) { + return rs.last, rs.lastErr +} + +// ForEach iterates over the complete result set passing every record found to +// the given callback. It is possible to stop the iteration by returning +// `kallax.ErrStop` in the callback. +// Result set is always closed at the end. +func (rs *PetResultSet) ForEach(fn func(*Pet) error) error { + for rs.Next() { + record, err := rs.Get() + if err != nil { + return err + } + + if err := fn(record); err != nil { + if err == kallax.ErrStop { + return rs.Close() + } + + return err + } + } + return nil +} + +// All returns all records on the result set and closes the result set. +func (rs *PetResultSet) All() ([]*Pet, error) { + var result []*Pet + for rs.Next() { + record, err := rs.Get() + if err != nil { + return nil, err + } + result = append(result, record) + } + return result, nil +} + +// One returns the first record on the result set and closes the result set. +func (rs *PetResultSet) One() (*Pet, error) { + if !rs.Next() { + return nil, kallax.ErrNotFound + } + + record, err := rs.Get() + if err != nil { + return nil, err + } + + if err := rs.Close(); err != nil { + return nil, err + } + + return record, nil +} + +// Err returns the last error occurred. +func (rs *PetResultSet) Err() error { + return rs.lastErr +} + +// Close closes the result set. +func (rs *PetResultSet) Close() error { + return rs.ResultSet.Close() +} + +type schema struct { + Person *schemaPerson + Pet *schemaPet +} + +type schemaPerson struct { + *kallax.BaseSchema + ID kallax.SchemaField + Name kallax.SchemaField +} + +type schemaPet struct { + *kallax.BaseSchema + ID kallax.SchemaField + Name kallax.SchemaField + Kind kallax.SchemaField +} + +var Schema = &schema{ + Person: &schemaPerson{ + BaseSchema: kallax.NewBaseSchema( + "people", + "__person", + kallax.NewSchemaField("id"), + kallax.ForeignKeys{ + "Pets": kallax.NewForeignKey("person_id", false), + }, + func() kallax.Record { + return new(Person) + }, + true, + kallax.NewSchemaField("id"), + kallax.NewSchemaField("name"), + ), + ID: kallax.NewSchemaField("id"), + Name: kallax.NewSchemaField("name"), + }, + Pet: &schemaPet{ + BaseSchema: kallax.NewBaseSchema( + "pets", + "__pet", + kallax.NewSchemaField("id"), + kallax.ForeignKeys{}, + func() kallax.Record { + return new(Pet) + }, + true, + kallax.NewSchemaField("id"), + kallax.NewSchemaField("name"), + kallax.NewSchemaField("kind"), + ), + ID: kallax.NewSchemaField("id"), + Name: kallax.NewSchemaField("name"), + Kind: kallax.NewSchemaField("kind"), + }, +} diff --git a/benchmarks/models_gorm.go b/benchmarks/models_gorm.go new file mode 100644 index 0000000..2ef4735 --- /dev/null +++ b/benchmarks/models_gorm.go @@ -0,0 +1,22 @@ +package benchmark + +type GORMPerson struct { + ID int64 `gorm:"primary_key"` + Name string + Pets []*GORMPet `gorm:"ForeignKey:PersonID"` +} + +func (GORMPerson) TableName() string { + return "people" +} + +type GORMPet struct { + ID int64 `gorm:"primary_key"` + PersonID int64 + Name string + Kind string +} + +func (GORMPet) TableName() string { + return "pets" +} diff --git a/benchmarks/models_kallax.go b/benchmarks/models_kallax.go new file mode 100644 index 0000000..62f89c3 --- /dev/null +++ b/benchmarks/models_kallax.go @@ -0,0 +1,25 @@ +package benchmark + +import kallax "gopkg.in/src-d/go-kallax.v1" + +type Person struct { + kallax.Model `table:"people"` + ID int64 `pk:"autoincr"` + Name string + Pets []*Pet +} + +type Pet struct { + kallax.Model `table:"pets"` + ID int64 `pk:"autoincr"` + Name string + Kind PetKind +} + +type PetKind string + +const ( + Cat PetKind = "cat" + Dog PetKind = "dog" + Fish PetKind = "fish" +)