Skip to content

Commit

Permalink
Merge pull request #7 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 Jul 23, 2019
2 parents b4b6fbd + 9821e5d commit 05311b8
Show file tree
Hide file tree
Showing 84 changed files with 9,633 additions and 954 deletions.
15 changes: 9 additions & 6 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,7 @@ jobs:
go get github.com/davecgh/go-spew/spew
go get github.com/jstemmer/go-junit-report
- run: mkdir -p $TEST_RESULTS/unit-tests
- run: mkdir -p $TEST_RESULTS/integration-tests

- run: go test -v 2>&1 | go-junit-report > $TEST_RESULTS/unit-tests/results.xml
go install github.com/go-jet/jet/cmd/jet
- run:
name: Waiting for Postgres to be ready
Expand All @@ -51,13 +48,19 @@ jobs:
echo Failed waiting for Postgres && exit 1
- run:
name: Run integration tests
name: Init Postgres database
command: |
cd tests
go run ./init/init.go
go test -v 2>&1 | go-junit-report > $TEST_RESULTS/integration-tests/results.xml
cd ..
- run: mkdir -p $TEST_RESULTS
- run: go test -v . ./tests -coverpkg=github.com/go-jet/jet,github.com/go-jet/jet/execution/...,github.com/go-jet/jet/generator/...,github.com/go-jet/jet/internal/... -coverprofile=cover.out 2>&1 | go-junit-report > $TEST_RESULTS/results.xml

- run:
name: Upload code coverage
command: bash <(curl -s https://codecov.io/bash)

- store_artifacts: # Upload test summary for display in Artifacts: https://circleci.com/docs/2.0/artifacts/
path: /tmp/test-results
destination: raw-test-output
Expand Down
64 changes: 36 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# Jet

[![Go Report Card](https://goreportcard.com/badge/github.com/go-jet/jet)](https://goreportcard.com/report/github.com/go-jet/jet)
[![Documentation](https://godoc.org/github.com/go-jet/jet?status.svg)](http://godoc.org/github.com/go-jet/jet)
[![codecov](https://codecov.io/gh/go-jet/jet/branch/develop/graph/badge.svg)](https://codecov.io/gh/go-jet/jet)
[![CircleCI](https://circleci.com/gh/go-jet/jet/tree/develop.svg?style=svg&circle-token=97f255c6a4a3ab6590ea2e9195eb3ebf9f97b4a7)](https://circleci.com/gh/go-jet/jet/tree/develop)

Jet is a framework for writing type-safe SQL queries for PostgreSQL in Go, with ability to easily
convert database query result to desired arbitrary structure.
_Support for additional databases will be added in future jet releases._
_*Support for additional databases will be added in future jet releases._


## Contents
Expand All @@ -22,7 +25,7 @@ _Support for additional databases will be added in future jet releases._
- [License](#license)

## Features
1) Type-safe SQL Builder
1) Auto-generated type-safe SQL Builder
- Types - boolean, integers(smallint, integer, bigint), floats(real, numeric, decimal, double precision),
strings(text, character, character varying), date, time(z), timestamp(z) and enums.
- Statements:
Expand All @@ -31,10 +34,9 @@ _Support for additional databases will be added in future jet releases._
* UPDATE (SET, WHERE, RETURNING),
* DELETE (WHERE, RETURNING),
* LOCK (IN, NOWAIT)
2) Auto-generated Data Model types - Go struct mapped to database type (table or enum), used to store
result of database queries.
3) Query execution with mapping to arbitrary destination structure - destination structure can be
created by combining auto-generated data model types.
2) Auto-generated Data Model types - Go types mapped to database type (table 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.

## Getting Started

Expand Down Expand Up @@ -63,15 +65,16 @@ Make sure GOPATH bin folder is added to the PATH environment variable.
For this quick start example we will use sample _dvd rental_ database. Full database dump can be found in [./tests/init/data/dvds.sql](./tests/init/data/dvds.sql).
Schema diagram of interest for example can be found [here](./examples/quick-start/diagram.png).

#### Generate sql builder and model files
#### Generate SQL Builder and Model files
To generate jet SQL Builder and Data Model files from postgres database we need to call `jet` generator with postgres
connection parameters and root destination folder path for generated files.\
Assuming we are running local postgres database, with user `jet`, database `jetdb` and schema `dvds` we will use this command:
Assuming we are running local postgres database, with user `jetuser`, user password `jetpass`, database `jetdb` and
schema `dvds` we will use this command:
```sh
jet -host=localhost -port=5432 -user=jet -password=jet -dbname=jetdb -schema=dvds -path=./gen
jet -host=localhost -port=5432 -user=jetuser -password=jetpass -dbname=jetdb -schema=dvds -path=./gen
```
```sh
Connecting to postgres database: host=localhost port=5432 user=jet password=jet dbname=jetdb sslmode=disable
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
Expand All @@ -82,11 +85,12 @@ Generating enum sql builder files...
Generating enum model files...
Done
```
_*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
- delete everything in schema destination folder - `./gen/jetdb/dvds`,
- and finally generate sql builder and model files for each schema tables and enums.
- and finally generate SQL Builder and Model files for each schema table and enum.


Generated files folder structure will look like this:
Expand All @@ -101,13 +105,13 @@ Generated files folder structure will look like this:
| |-- address.go
| |-- category.go
| ...
| |-- model # Plain Old Data for every table and enum
| |-- model # model files for each table 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 are combined to store
Types from `table` and `enum` are used to write type safe SQL in Go, and `model` types can be combined to store
results of the SQL queries.

#### Lets write some SQL queries in Go
Expand Down Expand Up @@ -147,20 +151,19 @@ stmt := SELECT(
Film.FilmID.ASC(),
)
```
With package(dot) import above statements looks almost the same as native SQL.
Note that every column has a type. String column `Language.Name` and `Category.Name` can be compared only with
With package(dot) import above statements looks almost the same 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.

__How to get parametrized SQL query?__
__How to get parametrized SQL query from statement?__
```go
query, args, err := stmt.Sql()
```
query - parametrized query\
args - parameters for the query

<details>
<summary>Click to see `query` and `arg`</summary>
<summary>Click to see `query` and `args`</summary>

```sql
SELECT actor.actor_id AS "actor.actor_id",
Expand Down Expand Up @@ -202,11 +205,12 @@ ORDER BY actor.actor_id ASC, film.film_id ASC;

</details>

__How to get debug SQL that can be copy pasted to sql editor and executed?__
```go
__How to get debug SQL from statement?__
```go
debugSql, err := stmt.DebugSql()
```
debugSql - parametrized query where every parameter is replaced with appropriate string argument representation
debugSql - query string that can be copy pasted to sql editor and executed. It's not intended to be used in production.

<details>
<summary>Click to see debug sql</summary>

Expand Down Expand Up @@ -252,20 +256,24 @@ Well formed SQL is just a first half the job. Lets see how can we make some sens
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 for more information).
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).

Let's say this is our desired structure:
Let's say this is our desired structure, created by combining auto-generated model types:
```go
var dest []struct {
model.Actor

Films []struct {
model.Film
Language model.Language
Categories []model.Category

Language model.Language
Categories []model.Category
}
}
```
_There is no limitation for how big or nested destination structure can be._
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.
_*There is no limitation of how big or nested destination structure can be._

Now lets execute a above statement on open database connection db and store result into `dest`.

Expand Down Expand Up @@ -494,12 +502,12 @@ ORM sometimes can access the database once for every object needed. Now lets say
different objects required from the database. This handler will last 3 seconds !!!.

With Jet, handler time lost on latency between server and database is constant. Because we can write complex query and
return result in one database call. Handler execution will be proportional to the number of rows returned from database.
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.
This is exactly what is being done in one of the tests: [TestJoinEverything](/tests/chinook_db_test.go#L40).
The whole test database is joined and query result is stored in a structured variable in less than 1s.
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

Expand Down Expand Up @@ -534,4 +542,4 @@ To run the tests, additional dependencies are required:
## License

Copyright 2019 Goran Bjelanovic
Licensed under the Apache License, Version 2.0.
Licensed under the Apache License, Version 2.0.
2 changes: 1 addition & 1 deletion alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func newAlias(expression Expression, aliasName string) projection {
}
}

func (a *alias) from(subQuery ExpressionTable) projection {
func (a *alias) from(subQuery SelectTable) projection {
column := newColumn(a.alias, "", nil)
column.parent = &column
column.subQuery = subQuery
Expand Down
4 changes: 4 additions & 0 deletions bool_expresion.go → bool_expression.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package jet

//BoolExpression interface
type BoolExpression interface {
Expression

Expand Down Expand Up @@ -150,6 +151,9 @@ func newBoolExpressionWrap(expression Expression) BoolExpression {
return &boolExpressionWrap
}

// BoolExp is bool expression wrapper around arbitrary expression.
// Allows go compiler to see any expression as bool expression.
// Does not add sql cast to generated sql builder output.
func BoolExp(expression Expression) BoolExpression {
return newBoolExpressionWrap(expression)
}
10 changes: 10 additions & 0 deletions bool_expression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ func TestBoolExpressionNOT_EQ(t *testing.T) {
assertClauseSerialize(t, table1ColBool.NOT_EQ(Bool(true)), "(table1.col_bool != $1)", true)
}

func TestBoolExpressionIS_DISTINCT_FROM(t *testing.T) {
assertClauseSerialize(t, table1ColBool.IS_DISTINCT_FROM(table2ColBool), "(table1.col_bool IS DISTINCT FROM table2.col_bool)")
assertClauseSerialize(t, table1ColBool.IS_DISTINCT_FROM(Bool(false)), "(table1.col_bool IS DISTINCT FROM $1)", false)
}

func TestBoolExpressionIS_NOT_DISTINCT_FROM(t *testing.T) {
assertClauseSerialize(t, table1ColBool.IS_NOT_DISTINCT_FROM(table2ColBool), "(table1.col_bool IS NOT DISTINCT FROM table2.col_bool)")
assertClauseSerialize(t, table1ColBool.IS_NOT_DISTINCT_FROM(Bool(false)), "(table1.col_bool IS NOT DISTINCT FROM $1)", false)
}

func TestBoolExpressionIS_TRUE(t *testing.T) {
assertClauseSerialize(t, table1ColBool.IS_TRUE(), "table1.col_bool IS TRUE")
assertClauseSerialize(t, (Int(2).EQ(table1ColInt)).IS_TRUE(),
Expand Down
2 changes: 2 additions & 0 deletions cast.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type castImpl struct {
castType string
}

// CAST wraps expression for casting.
// For instance: CAST(table.column).AS_BOOL()
func CAST(expression Expression) cast {
return &castImpl{
Expression: expression,
Expand Down
32 changes: 13 additions & 19 deletions clause.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ type sqlBuilder struct {
type statementType string

const (
select_statement statementType = "SELECT"
insert_statement statementType = "INSERT"
update_statement statementType = "UPDATE"
delete_statement statementType = "DELETE"
set_statement statementType = "SET"
lock_statement statementType = "LOCK"
selectStatement statementType = "SELECT"
insertStatement statementType = "INSERT"
updateStatement statementType = "UPDATE"
deleteStatement statementType = "DELETE"
setStatement statementType = "SET"
lockStatement statementType = "LOCK"
)

const defaultIdent = 5
Expand Down Expand Up @@ -102,7 +102,7 @@ func (q *sqlBuilder) writeGroupBy(statement statementType, groupBy []groupByClau
return err
}

func (q *sqlBuilder) writeOrderBy(statement statementType, orderBy []OrderByClause) error {
func (q *sqlBuilder) writeOrderBy(statement statementType, orderBy []orderByClause) error {
q.newLine()
q.writeString("ORDER BY")

Expand Down Expand Up @@ -189,33 +189,27 @@ func (q *sqlBuilder) finalize() (string, []interface{}) {
}

func (q *sqlBuilder) insertConstantArgument(arg interface{}) {
q.writeString(ArgToString(arg))
q.writeString(argToString(arg))
}

func (q *sqlBuilder) insertPreparedArgument(arg interface{}) {
func (q *sqlBuilder) insertParametrizedArgument(arg interface{}) {
q.args = append(q.args, arg)
argPlaceholder := "$" + strconv.Itoa(len(q.args))

q.writeString(argPlaceholder)
}

func (q *sqlBuilder) reset() {
q.buff.Reset()
q.args = []interface{}{}
}

func ArgToString(value interface{}) string {
if isNil(value) {
func argToString(value interface{}) string {
if utils.IsNil(value) {
return "NULL"
}

switch bindVal := value.(type) {
case bool:
if bindVal {
return "TRUE"
} else {
return "FALSE"
}
return "FALSE"
case int8:
return strconv.FormatInt(int64(bindVal), 10)
case int:
Expand Down Expand Up @@ -252,7 +246,7 @@ func ArgToString(value interface{}) string {
case time.Time:
return stringQuote(string(utils.FormatTimestamp(bindVal)))
default:
return "[Unknown type]"
return "[Unsupported type]"
}
}

Expand Down
33 changes: 33 additions & 0 deletions clause_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package jet

import (
"github.com/google/uuid"
"gotest.tools/assert"
"testing"
"time"
)

func TestArgToString(t *testing.T) {
assert.Equal(t, argToString(true), "TRUE")
assert.Equal(t, argToString(false), "FALSE")

assert.Equal(t, argToString(int8(-8)), "-8")
assert.Equal(t, argToString(int16(-16)), "-16")
assert.Equal(t, argToString(int(-32)), "-32")
assert.Equal(t, argToString(int32(-32)), "-32")
assert.Equal(t, argToString(int64(-64)), "-64")
assert.Equal(t, argToString(uint8(8)), "8")
assert.Equal(t, argToString(uint16(16)), "16")
assert.Equal(t, argToString(uint(32)), "32")
assert.Equal(t, argToString(uint32(32)), "32")
assert.Equal(t, argToString(uint64(64)), "64")

assert.Equal(t, argToString("john"), "'john'")
assert.Equal(t, argToString([]byte("john")), "'john'")
assert.Equal(t, argToString(uuid.MustParse("b68dbff4-a87d-11e9-a7f2-98ded00c39c6")), "'b68dbff4-a87d-11e9-a7f2-98ded00c39c6'")

time, err := time.Parse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Jan 2 15:04:05 -0700 MST 2006")
assert.NilError(t, err)
assert.Equal(t, argToString(time), "'2006-01-02 15:04:05-07:00'")
assert.Equal(t, argToString(map[string]bool{}), "[Unsupported type]")
}
4 changes: 2 additions & 2 deletions cmd/jet/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"flag"
"fmt"
"github.com/go-jet/jet/generator/postgres"
_ "github.com/lib/pq"
"os"
"strconv"
)

var (
Expand Down Expand Up @@ -70,7 +70,7 @@ Usage of jet:

genData := postgres.DBConnection{
Host: host,
Port: strconv.Itoa(port),
Port: port,
User: user,
Password: password,
SslMode: sslmode,
Expand Down
Loading

0 comments on commit 05311b8

Please sign in to comment.