Skip to content

Commit

Permalink
Merge pull request #13 from go-jet/develop
Browse files Browse the repository at this point in the history
Merge develop to master
  • Loading branch information
go-jet authored Sep 21, 2019
2 parents fbdf056 + e965aaa commit fbf3b6d
Show file tree
Hide file tree
Showing 48 changed files with 1,897 additions and 506 deletions.
4 changes: 4 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ https://github.com/dropbox/godropbox/tree/master/database/sqlbuilder (BSD-3)

This product contains a modified portion of 'snaker' which can be obtained at:
https://github.com/serenize/snaker (MIT)


This product contains `FormatTimestamp` function from 'pq' which can be obtained at:
https://github.com/lib/pq (MIT)
55 changes: 32 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ convert database query result into desired arbitrary object structure.
Jet currently supports `PostgreSQL`, `MySQL` and `MariaDB`. Future releases will add support for additional databases.

![jet](https://github.com/go-jet/jet/wiki/image/jet.png)
Jet is the easiest and fastest way to write complex SQL queries and map database query result
Jet is the easiest and the fastest way to write complex SQL queries and map database query result
into complex object composition. __It is not an ORM.__

## Motivation
Expand Down Expand Up @@ -46,7 +46,7 @@ https://medium.com/@go.jet/jet-5f3667efa0cc
* UPDATE `(SET, WHERE)`,
* DELETE `(WHERE, ORDER_BY, LIMIT)`,
* LOCK `(READ, WRITE)`
2) Auto-generated Data Model types - Go types mapped to database type (table or enum), used to store
2) Auto-generated Data Model types - Go types mapped to database type (table, view or enum), used to store
result of database queries. Can be combined to create desired query result destination.
3) Query execution with result mapping to arbitrary destination structure.

Expand Down Expand Up @@ -88,12 +88,13 @@ jet -source=PostgreSQL -host=localhost -port=5432 -user=jetuser -password=jetpas
```sh
Connecting to postgres database: host=localhost port=5432 user=jetuser password=jetpass dbname=jetdb sslmode=disable
Retrieving schema information...
FOUND 15 table(s), 1 enum(s)
Destination directory: ./gen/jetdb/dvds
Cleaning up schema destination directory...
FOUND 15 table(s), 7 view(s), 1 enum(s)
Cleaning up destination directory...
Generating table sql builder files...
Generating table model files...
Generating view sql builder files...
Generating enum sql builder files...
Generating table model files...
Generating view model files...
Generating enum model files...
Done
```
Expand All @@ -102,30 +103,34 @@ be omitted (both databases doesn't have schema support).
_*User has to have a permission to read information schema tables._

As command output suggest, Jet will:
- connect to postgres database and retrieve information about the _tables_ and _enums_ of `dvds` schema
- connect to postgres database and retrieve information about the _tables_, _views_ and _enums_ of `dvds` schema
- delete everything in schema destination folder - `./gen/jetdb/dvds`,
- and finally generate SQL Builder and Model files for each schema table and enum.
- and finally generate SQL Builder and Model files for each schema table, view and enum.


Generated files folder structure will look like this:
```sh
|-- gen # -path
| `-- jetdb # database name
| `-- dvds # schema name
| |-- enum # sql builder folder for enums
| |-- enum # sql builder package for enums
| | |-- mpaa_rating.go
| |-- table # sql builder folder for tables
| |-- table # sql builder package for tables
| |-- actor.go
| |-- address.go
| |-- category.go
| ...
| |-- model # model files for each table and enum
| |-- view # sql builder package for views
| |-- actor_info.go
| |-- film_list.go
| ...
| |-- model # data model types for each table, view and enum
| | |-- actor.go
| | |-- address.go
| | |-- mpaa_rating.go
| | ...
```
Types from `table` and `enum` are used to write type safe SQL in Go, and `model` types can be combined to store
Types from `table`, `view` and `enum` are used to write type safe SQL in Go, and `model` types can be combined to store
results of the SQL queries.


Expand Down Expand Up @@ -167,7 +172,8 @@ stmt := SELECT(
Film.FilmID.ASC(),
)
```
Package(dot) import is used so that statement would resemble as much as possible as native SQL. Note that every column has a type. String column `Language.Name` and `Category.Name` can be compared only with
_Package(dot) import is used so that statement would resemble as much as possible as native SQL._
Note that every column has a type. String column `Language.Name` and `Category.Name` can be compared only with
string columns and expressions. `Actor.ActorID`, `FilmActor.ActorID`, `Film.Length` are integer columns
and can be compared only with integer columns and expressions.

Expand Down Expand Up @@ -268,11 +274,12 @@ ORDER BY actor.actor_id ASC, film.film_id ASC;

#### Execute query and store result

Well formed SQL is just a first half the job. Lets see how can we make some sense of result set returned executing
Well formed SQL is just a first half of the job. Lets see how can we make some sense of result set returned executing
above statement. Usually this is the most complex and tedious work, but with Jet it is the easiest.

First we have to create desired structure to store query result set.
This is done be combining autogenerated model types or it can be done manually(see [wiki](https://github.com/go-jet/jet/wiki/Scan-to-arbitrary-destination) for more information).
First we have to create desired structure to store query result.
This is done be combining autogenerated model types or it can be done
manually(see [wiki](https://github.com/go-jet/jet/wiki/Query-Result-Mapping-(QRM)) for more information).

Let's say this is our desired structure:
```go
Expand All @@ -287,8 +294,8 @@ var dest []struct {
}
}
```
Because one actor can act in multiple films, `Films` field is a slice, and because each film belongs to one language
`Langauge` field is just a single model struct.
`Films` field is a slice because one actor can act in multiple films, and because each film belongs to one language
`Langauge` field is just a single model struct. `Film` can belong to multiple categories.
_*There is no limitation of how big or nested destination can be._

Now lets execute a above statement on open database connection (or transaction) db and store result into `dest`.
Expand Down Expand Up @@ -504,12 +511,14 @@ The biggest benefit is speed. Speed is improved in 3 major areas:

##### Speed of development

Writing SQL queries is much easier, because programmer has the help of SQL code completion and SQL type safety directly in Go.
Writing code is much faster and code is more robust. Automatic scan to arbitrary structure removes a lot of headache and
boilerplate code needed to structure database query result.
Writing SQL queries is faster and easier, because the developers have help of SQL code completion and SQL type safety directly from Go.
Automatic scan to arbitrary structure removes a lot of headache and boilerplate code needed to structure database query result.

##### Speed of execution

While ORM libraries can introduce significant performance penalties due to number of round-trips to the database,
Jet will always perform much better, because of the single database call.

Common web and database server usually are not on the same physical machine, and there is some latency between them.
Latency can vary from 5ms to 50+ms. In majority of cases query executed on database is simple query lasting no more than 1ms.
In those cases web server handler execution time is directly proportional to latency between server and database.
Expand All @@ -521,14 +530,14 @@ With Jet, handler time lost on latency between server and database is constant.
return result in one database call. Handler execution will be only proportional to the number of rows returned from database.
ORM example replaced with jet will take just 30ms + 'result scan time' = 31ms (rough estimate).

With Jet you can even join the whole database and store the whole structured result in in one query call.
With Jet you can even join the whole database and store the whole structured result in one database call.
This is exactly what is being done in one of the tests: [TestJoinEverything](/tests/postgres/chinook_db_test.go#L40).
The whole test database is joined and query result(~10,000 rows) is stored in a structured variable in less than 0.7s.

##### How quickly bugs are found

The most expensive bugs are the one on the production and the least expensive are those found during development.
With automatically generated type safe SQL not only queries are written faster but bugs are found sooner.
With automatically generated type safe SQL, not only queries are written faster but bugs are found sooner.
Lets return to quick start example, and take closer look at a line:
```go
AND(Film.Length.GT(Int(180))),
Expand Down
4 changes: 1 addition & 3 deletions examples/quick-start/quick-start.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,7 @@ func jsonSave(path string, v interface{}) {

err := ioutil.WriteFile(path, jsonText, 0644)

if err != nil {
panic(err)
}
panicOnError(err)
}

func printStatementInfo(stmt SelectStatement) {
Expand Down
40 changes: 35 additions & 5 deletions execution/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ func (s *scanContext) getGroupKeyInfo(structType reflect.Type, parentField *refl
if len(subType.indexes) != 0 || len(subType.subTypes) != 0 {
ret.subTypes = append(ret.subTypes, subType)
}
} else if isPrimaryKey(field) {
} else if isPrimaryKey(field, parentField) {
index := s.typeToColumnIndex(newTypeName, fieldName)

if index < 0 {
Expand Down Expand Up @@ -813,9 +813,7 @@ func (s *scanContext) rowElem(index int) interface{} {

value, err := valuer.Value()

if err != nil {
panic(err)
}
utils.PanicOnError(err)

return value
}
Expand All @@ -837,13 +835,45 @@ func (s *scanContext) rowElemValuePtr(index int) reflect.Value {
return newElem
}

func isPrimaryKey(field reflect.StructField) bool {
func isPrimaryKey(field reflect.StructField, parentField *reflect.StructField) bool {

if hasOverwrite, isPrimaryKey := primaryKeyOvewrite(field.Name, parentField); hasOverwrite {
return isPrimaryKey
}

sqlTag := field.Tag.Get("sql")

return sqlTag == "primary_key"
}

func primaryKeyOvewrite(columnName string, parentField *reflect.StructField) (hasOverwrite, primaryKey bool) {
if parentField == nil {
return
}

sqlTag := parentField.Tag.Get("sql")

if !strings.HasPrefix(sqlTag, "primary_key") {
return
}

parts := strings.Split(sqlTag, "=")

if len(parts) < 2 {
return
}

primaryKeyColumns := strings.Split(parts[1], ",")

for _, primaryKeyCol := range primaryKeyColumns {
if toCommonIdentifier(columnName) == toCommonIdentifier(primaryKeyCol) {
return true, true
}
}

return true, false
}

func indirectType(reflectType reflect.Type) reflect.Type {
if reflectType.Kind() != reflect.Ptr {
return reflectType
Expand Down
2 changes: 1 addition & 1 deletion execution/internal/null_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (nt *NullTime) Scan(value interface{}) (err error) {
nt.Time, nt.Valid = parseTime(v)
return
default:
return fmt.Errorf("can't scan time from %v", value)
return fmt.Errorf("can't scan time.Time from %v", value)
}
}

Expand Down
147 changes: 147 additions & 0 deletions execution/internal/null_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package internal

import (
"fmt"
"gotest.tools/assert"
"testing"
"time"
)

func TestNullByteArray(t *testing.T) {
var array NullByteArray

assert.NilError(t, array.Scan(nil))
assert.Equal(t, array.Valid, false)

assert.NilError(t, array.Scan([]byte("bytea")))
assert.Equal(t, array.Valid, true)
assert.Equal(t, string(array.ByteArray), string([]byte("bytea")))

assert.Error(t, array.Scan(12), "can't scan []byte from 12")
}

func TestNullTime(t *testing.T) {
var array NullTime

assert.NilError(t, array.Scan(nil))
assert.Equal(t, array.Valid, false)

time := time.Now()
assert.NilError(t, array.Scan(time))
assert.Equal(t, array.Valid, true)
value, _ := array.Value()
assert.Equal(t, value, time)

assert.NilError(t, array.Scan([]byte("13:10:11")))
assert.Equal(t, array.Valid, true)
value, _ = array.Value()
assert.Equal(t, fmt.Sprintf("%v", value), "0000-01-01 13:10:11 +0000 UTC")

assert.NilError(t, array.Scan("13:10:11"))
assert.Equal(t, array.Valid, true)
value, _ = array.Value()
assert.Equal(t, fmt.Sprintf("%v", value), "0000-01-01 13:10:11 +0000 UTC")

assert.Error(t, array.Scan(12), "can't scan time.Time from 12")
}

func TestNullInt8(t *testing.T) {
var array NullInt8

assert.NilError(t, array.Scan(nil))
assert.Equal(t, array.Valid, false)

assert.NilError(t, array.Scan(int64(11)))
assert.Equal(t, array.Valid, true)
value, _ := array.Value()
assert.Equal(t, value, int8(11))

assert.Error(t, array.Scan("text"), "can't scan int8 from text")
}

func TestNullInt16(t *testing.T) {
var array NullInt16

assert.NilError(t, array.Scan(nil))
assert.Equal(t, array.Valid, false)

assert.NilError(t, array.Scan(int64(11)))
assert.Equal(t, array.Valid, true)
value, _ := array.Value()
assert.Equal(t, value, int16(11))

assert.NilError(t, array.Scan(int16(20)))
assert.Equal(t, array.Valid, true)
value, _ = array.Value()
assert.Equal(t, value, int16(20))

assert.NilError(t, array.Scan(int8(30)))
assert.Equal(t, array.Valid, true)
value, _ = array.Value()
assert.Equal(t, value, int16(30))

assert.NilError(t, array.Scan(uint8(30)))
assert.Equal(t, array.Valid, true)
value, _ = array.Value()
assert.Equal(t, value, int16(30))

assert.Error(t, array.Scan("text"), "can't scan int16 from text")
}

func TestNullInt32(t *testing.T) {
var array NullInt32

assert.NilError(t, array.Scan(nil))
assert.Equal(t, array.Valid, false)

assert.NilError(t, array.Scan(int64(11)))
assert.Equal(t, array.Valid, true)
value, _ := array.Value()
assert.Equal(t, value, int32(11))

assert.NilError(t, array.Scan(int32(32)))
assert.Equal(t, array.Valid, true)
value, _ = array.Value()
assert.Equal(t, value, int32(32))

assert.NilError(t, array.Scan(int16(20)))
assert.Equal(t, array.Valid, true)
value, _ = array.Value()
assert.Equal(t, value, int32(20))

assert.NilError(t, array.Scan(uint16(16)))
assert.Equal(t, array.Valid, true)
value, _ = array.Value()
assert.Equal(t, value, int32(16))

assert.NilError(t, array.Scan(int8(30)))
assert.Equal(t, array.Valid, true)
value, _ = array.Value()
assert.Equal(t, value, int32(30))

assert.NilError(t, array.Scan(uint8(30)))
assert.Equal(t, array.Valid, true)
value, _ = array.Value()
assert.Equal(t, value, int32(30))

assert.Error(t, array.Scan("text"), "can't scan int32 from text")
}

func TestNullFloat32(t *testing.T) {
var array NullFloat32

assert.NilError(t, array.Scan(nil))
assert.Equal(t, array.Valid, false)

assert.NilError(t, array.Scan(float64(64)))
assert.Equal(t, array.Valid, true)
value, _ := array.Value()
assert.Equal(t, value, float32(64))

assert.NilError(t, array.Scan(float32(32)))
assert.Equal(t, array.Valid, true)
value, _ = array.Value()
assert.Equal(t, value, float32(32))

assert.Error(t, array.Scan(12), "can't scan float32 from 12")
}
Loading

0 comments on commit fbf3b6d

Please sign in to comment.