Giter Site home page Giter Site logo

go-jet / jet Goto Github PK

View Code? Open in Web Editor NEW
2.0K 22.0 106.0 5.26 MB

Type safe SQL builder with code generation and automatic query result data mapping

License: Apache License 2.0

Go 99.75% Makefile 0.25%
golang sql sqlbuilder postgresql code-generator database sql-query sql-query-builder postgres typesafe

jet's Introduction

Jet

go-jet codecov Go Report Card Documentation GitHub release

Jet is a complete solution for efficient and high performance database access, consisting of type-safe SQL builder with code generation and automatic query result data mapping.
Jet currently supports PostgreSQL, MySQL, CockroachDB, MariaDB and SQLite. Future releases will add support for additional databases.

jet
Jet is the easiest, and the fastest way to write complex type-safe SQL queries as a Go code and map database query result into complex object composition. It is not an ORM.

Motivation

https://medium.com/@go.jet/jet-5f3667efa0cc

Contents

Features

  1. Auto-generated type-safe SQL Builder. Statements supported:

    • SELECT (DISTINCT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET, FOR, LOCK_IN_SHARE_MODE, UNION, INTERSECT, EXCEPT, WINDOW, sub-queries)
    • INSERT (VALUES, MODEL, MODELS, QUERY, ON_CONFLICT/ON_DUPLICATE_KEY_UPDATE, RETURNING),
    • UPDATE (SET, MODEL, WHERE, RETURNING),
    • DELETE (WHERE, ORDER_BY, LIMIT, RETURNING),
    • LOCK (IN, NOWAIT), (READ, WRITE)
    • WITH
  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 complex query result destination.

  3. Query execution with result mapping to arbitrary destination.

Getting Started

Prerequisites

To install Jet package, you need to install Go and set your Go workspace first.

Go version 1.18+ is required

Installation

Use the command bellow to add jet as a dependency into go.mod project:

$ go get -u github.com/go-jet/jet/v2

Jet generator can be installed in one of the following ways:

  • (Go1.16+) Install jet generator using go install:
go install github.com/go-jet/jet/v2/cmd/jet@latest

Jet generator is installed to the directory named by the GOBIN environment variable, which defaults to $GOPATH/bin or $HOME/go/bin if the GOPATH environment variable is not set.

  • Install jet generator to specific folder:
git clone https://github.com/go-jet/jet.git
cd jet && go build -o dir_path ./cmd/jet

Make sure dir_path folder is added to the PATH environment variable.

Quick Start

For this quick start example we will use PostgreSQL sample 'dvd rental' database. Full database dump can be found in ./tests/testdata/init/postgres/dvds.sql. Schema diagram of interest can be found here.

Generate SQL Builder and Model types

To generate jet SQL Builder and Data Model types from running postgres database, we need to call jet generator with postgres connection parameters and destination folder path. Assuming we are running local postgres database, with user user, user password pass, database jetdb and schema dvds we will use this command:

jet -dsn=postgresql://user:pass@localhost:5432/jetdb?sslmode=disable -schema=dvds -path=./.gen
Connecting to postgres database: postgresql://user:pass@localhost:5432/jetdb?sslmode=disable 
Retrieving schema information...
	FOUND 15 table(s), 7 view(s), 1 enum(s)
Cleaning up destination directory...
Generating table sql builder files...
Generating view sql builder files...
Generating enum sql builder files...
Generating table model files...
Generating view model files...
Generating enum model files...
Done

Procedure is similar for MySQL, CockroachDB, MariaDB and SQLite. For example:

jet -source=mysql -dsn="user:pass@tcp(localhost:3306)/dbname" -path=./.gen
jet -dsn=postgres://user:pass@localhost:26257/jetdb?sslmode=disable -schema=dvds -path=./.gen  #cockroachdb
jet -dsn="mariadb://user:pass@tcp(localhost:3306)/dvds" -path=./.gen              # source flag can be omitted if data source appears in dsn
jet -source=sqlite -dsn="/path/to/sqlite/database/file" -schema=dvds -path=./.gen
jet -dsn="file:///path/to/sqlite/database/file" -schema=dvds -path=./.gen         # sqlite database assumed for 'file' data sources

*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, views and enums of dvds schema
  • delete everything in schema destination folder - ./.gen/jetdb/dvds,
  • and finally generate SQL Builder and Model types for each schema table, view and enum.

Generated files folder structure will look like this:

|-- .gen                              # path
|   -- jetdb                          # database name
|       -- dvds                       # schema name
|           |-- enum                  # sql builder package for enums
|           |   |-- mpaa_rating.go
|           |-- table                 # sql builder package for tables
|               |-- actor.go
|               |-- address.go
|               |-- category.go
|               ...
|           |-- 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, view and enum are used to write type safe SQL in Go, and model types are combined to store results of the SQL queries.

Let's write some SQL queries in Go

First we need to import postgres SQLBuilder and generated packages from the previous step:

import (
	// dot import so go code would resemble as much as native SQL
	// dot import is not mandatory
	. "github.com/go-jet/jet/v2/examples/quick-start/.gen/jetdb/dvds/table"
	. "github.com/go-jet/jet/v2/postgres"

	"github.com/go-jet/jet/v2/examples/quick-start/.gen/jetdb/dvds/model"
)

Let's say we want to retrieve the list of all actors that acted in films longer than 180 minutes, film language is 'English' and film category is not 'Action'.

stmt := SELECT(
    Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate,  // or just Actor.AllColumns
    Film.AllColumns,                                                  
    Language.AllColumns.Except(Language.LastUpdate),  // all language columns except last_update 
    Category.AllColumns,
).FROM(
    Actor.
        INNER_JOIN(FilmActor, Actor.ActorID.EQ(FilmActor.ActorID)).  
        INNER_JOIN(Film, Film.FilmID.EQ(FilmActor.FilmID)).          
        INNER_JOIN(Language, Language.LanguageID.EQ(Film.LanguageID)).
        INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID)).
        INNER_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)),
).WHERE(
    Language.Name.EQ(String("English")).             
        AND(Category.Name.NOT_EQ(String("Action"))).  
        AND(Film.Length.GT(Int(180))),               
).ORDER_BY(
    Actor.ActorID.ASC(),
    Film.FilmID.ASC(),
)

Package(dot) import is used, so the statements look as close as possible to the 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 a parametrized SQL query from the statement?

query, args := stmt.Sql()

query - parametrized query
args - query parameters

Click to see `query` and `args`
SELECT actor.actor_id AS "actor.actor_id",
     actor.first_name AS "actor.first_name",
     actor.last_name AS "actor.last_name",
     actor.last_update AS "actor.last_update",
     film.film_id AS "film.film_id",
     film.title AS "film.title",
     film.description AS "film.description",
     film.release_year AS "film.release_year",
     film.language_id AS "film.language_id",
     film.rental_duration AS "film.rental_duration",
     film.rental_rate AS "film.rental_rate",
     film.length AS "film.length",
     film.replacement_cost AS "film.replacement_cost",
     film.rating AS "film.rating",
     film.last_update AS "film.last_update",
     film.special_features AS "film.special_features",
     film.fulltext AS "film.fulltext",
     language.language_id AS "language.language_id",
     language.name AS "language.name",
     language.last_update AS "language.last_update",
     category.category_id AS "category.category_id",
     category.name AS "category.name",
     category.last_update AS "category.last_update"
FROM dvds.actor
     INNER JOIN dvds.film_actor ON (actor.actor_id = film_actor.actor_id)
     INNER JOIN dvds.film ON (film.film_id = film_actor.film_id)
     INNER JOIN dvds.language ON (language.language_id = film.language_id)
     INNER JOIN dvds.film_category ON (film_category.film_id = film.film_id)
     INNER JOIN dvds.category ON (category.category_id = film_category.category_id)
WHERE ((language.name = $1) AND (category.name != $2)) AND (film.length > $3)
ORDER BY actor.actor_id ASC, film.film_id ASC;
[English Action 180]

How to get debug SQL from statement?

debugSql := stmt.DebugSql()

debugSql - this query string can be copy-pasted to sql editor and executed. It is not intended to be used in production. For debug purposes only!!!

Click to see debug sql
SELECT actor.actor_id AS "actor.actor_id",
     actor.first_name AS "actor.first_name",
     actor.last_name AS "actor.last_name",
     actor.last_update AS "actor.last_update",
     film.film_id AS "film.film_id",
     film.title AS "film.title",
     film.description AS "film.description",
     film.release_year AS "film.release_year",
     film.language_id AS "film.language_id",
     film.rental_duration AS "film.rental_duration",
     film.rental_rate AS "film.rental_rate",
     film.length AS "film.length",
     film.replacement_cost AS "film.replacement_cost",
     film.rating AS "film.rating",
     film.last_update AS "film.last_update",
     film.special_features AS "film.special_features",
     film.fulltext AS "film.fulltext",
     language.language_id AS "language.language_id",
     language.name AS "language.name",
     language.last_update AS "language.last_update",
     category.category_id AS "category.category_id",
     category.name AS "category.name",
     category.last_update AS "category.last_update"
FROM dvds.actor
     INNER JOIN dvds.film_actor ON (actor.actor_id = film_actor.actor_id)
     INNER JOIN dvds.film ON (film.film_id = film_actor.film_id)
     INNER JOIN dvds.language ON (language.language_id = film.language_id)
     INNER JOIN dvds.film_category ON (film_category.film_id = film.film_id)
     INNER JOIN dvds.category ON (category.category_id = film_category.category_id)
WHERE ((language.name = 'English') AND (category.name != 'Action')) AND (film.length > 180)
ORDER BY actor.actor_id ASC, film.film_id ASC;

Execute query and store result

Well-formed SQL is just a first half of the job. Let's 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. This is done be combining autogenerated model types, or it can be done by combining custom model types(see wiki for more information).

Note that it's possible to overwrite default jet generator behavior. All the aspects of generated model and SQLBuilder types can be tailor-made(wiki).

Let's say this is our desired structure made of autogenerated types:

var dest []struct {
    model.Actor
    
    Films []struct {
        model.Film
        
        Language    model.Language
        Categories  []model.Category
    }
}

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 let's execute above statement on open database connection (or transaction) db and store result into dest.

err := stmt.Query(db, &dest)
handleError(err)

And that's it.

dest now contains the list of all actors(with list of films acted, where each film has information about language and list of belonging categories) that acted in films longer than 180 minutes, film language is 'English' and film category is not 'Action'.

Lets print dest as a json to see:

jsonText, _ := json.MarshalIndent(dest, "", "\t")
fmt.Println(string(jsonText))
[
	{
		"ActorID": 1,
		"FirstName": "Penelope",
		"LastName": "Guiness",
		"LastUpdate": "2013-05-26T14:47:57.62Z",
		"Films": [
			{
				"FilmID": 499,
				"Title": "King Evolution",
				"Description": "A Action-Packed Tale of a Boy And a Lumberjack who must Chase a Madman in A Baloon",
				"ReleaseYear": 2006,
				"LanguageID": 1,
				"RentalDuration": 3,
				"RentalRate": 4.99,
				"Length": 184,
				"ReplacementCost": 24.99,
				"Rating": "NC-17",
				"LastUpdate": "2013-05-26T14:50:58.951Z",
				"SpecialFeatures": "{Trailers,\"Deleted Scenes\",\"Behind the Scenes\"}",
				"Fulltext": "'action':5 'action-pack':4 'baloon':21 'boy':10 'chase':16 'evolut':2 'king':1 'lumberjack':13 'madman':18 'must':15 'pack':6 'tale':7",
				"Language": {
					"LanguageID": 1,
					"Name": "English             ",
					"LastUpdate": "0001-01-01T00:00:00Z"
				},
				"Categories": [
					{
						"CategoryID": 8,
						"Name": "Family",
						"LastUpdate": "2006-02-15T09:46:27Z"
					}
				]
			}
		]
	},
	{
		"ActorID": 3,
		"FirstName": "Ed",
		"LastName": "Chase",
		"LastUpdate": "2013-05-26T14:47:57.62Z",
		"Films": [
			{
				"FilmID": 996,
				"Title": "Young Language",
				"Description": "A Unbelieveable Yarn of a Boat And a Database Administrator who must Meet a Boy in The First Manned Space Station",
				"ReleaseYear": 2006,
				"LanguageID": 1,
				"RentalDuration": 6,
				"RentalRate": 0.99,
				"Length": 183,
				"ReplacementCost": 9.99,
				"Rating": "G",
				"LastUpdate": "2013-05-26T14:50:58.951Z",
				"SpecialFeatures": "{Trailers,\"Behind the Scenes\"}",
				"Fulltext": "'administr':12 'boat':8 'boy':17 'databas':11 'first':20 'languag':2 'man':21 'meet':15 'must':14 'space':22 'station':23 'unbeliev':4 'yarn':5 'young':1",
				"Language": {
					"LanguageID": 1,
					"Name": "English             ",
					"LastUpdate": "0001-01-01T00:00:00Z"
				},
				"Categories": [
					{
						"CategoryID": 6,
						"Name": "Documentary",
						"LastUpdate": "2006-02-15T09:46:27Z"
					}
				]
			}
		]
	},
	//...(125 more items)
]

What if, we also want to have list of films per category and actors per category, where films are longer than 180 minutes, film language is 'English' and film category is not 'Action'.
In that case we can reuse above statement stmt, and just change our destination:

var dest2 []struct {
    model.Category

    Films []model.Film
    Actors []model.Actor
}

err = stmt.Query(db, &dest2)
handleError(err)
Click to see `dest2` json
[
	{
		"CategoryID": 8,
		"Name": "Family",
		"LastUpdate": "2006-02-15T09:46:27Z",
		"Films": [
			{
				"FilmID": 499,
				"Title": "King Evolution",
				"Description": "A Action-Packed Tale of a Boy And a Lumberjack who must Chase a Madman in A Baloon",
				"ReleaseYear": 2006,
				"LanguageID": 1,
				"RentalDuration": 3,
				"RentalRate": 4.99,
				"Length": 184,
				"ReplacementCost": 24.99,
				"Rating": "NC-17",
				"LastUpdate": "2013-05-26T14:50:58.951Z",
				"SpecialFeatures": "{Trailers,\"Deleted Scenes\",\"Behind the Scenes\"}",
				"Fulltext": "'action':5 'action-pack':4 'baloon':21 'boy':10 'chase':16 'evolut':2 'king':1 'lumberjack':13 'madman':18 'must':15 'pack':6 'tale':7"
			},
			{
				"FilmID": 50,
				"Title": "Baked Cleopatra",
				"Description": "A Stunning Drama of a Forensic Psychologist And a Husband who must Overcome a Waitress in A Monastery",
				"ReleaseYear": 2006,
				"LanguageID": 1,
				"RentalDuration": 3,
				"RentalRate": 2.99,
				"Length": 182,
				"ReplacementCost": 20.99,
				"Rating": "G",
				"LastUpdate": "2013-05-26T14:50:58.951Z",
				"SpecialFeatures": "{Commentaries,\"Behind the Scenes\"}",
				"Fulltext": "'bake':1 'cleopatra':2 'drama':5 'forens':8 'husband':12 'monasteri':20 'must':14 'overcom':15 'psychologist':9 'stun':4 'waitress':17"
			}
		],
		"Actors": [
			{
				"ActorID": 1,
				"FirstName": "Penelope",
				"LastName": "Guiness",
				"LastUpdate": "2013-05-26T14:47:57.62Z"
			},
			{
				"ActorID": 20,
				"FirstName": "Lucille",
				"LastName": "Tracy",
				"LastUpdate": "2013-05-26T14:47:57.62Z"
			},
			{
				"ActorID": 36,
				"FirstName": "Burt",
				"LastName": "Dukakis",
				"LastUpdate": "2013-05-26T14:47:57.62Z"
			},
			{
				"ActorID": 70,
				"FirstName": "Michelle",
				"LastName": "Mcconaughey",
				"LastUpdate": "2013-05-26T14:47:57.62Z"
			},
			{
				"ActorID": 118,
				"FirstName": "Cuba",
				"LastName": "Allen",
				"LastUpdate": "2013-05-26T14:47:57.62Z"
			},
			{
				"ActorID": 187,
				"FirstName": "Renee",
				"LastName": "Ball",
				"LastUpdate": "2013-05-26T14:47:57.62Z"
			},
			{
				"ActorID": 198,
				"FirstName": "Mary",
				"LastName": "Keitel",
				"LastUpdate": "2013-05-26T14:47:57.62Z"
			}
		]
	},
    //...
]

Complete code example can be found at ./examples/quick-start/quick-start.go

This example represent probably the most common use case. Detail info about additional statements, features and use cases can be found at project Wiki page.

Benefits

What are the benefits of writing SQL in Go using Jet?
The biggest benefit is speed. Speed is being improved in 3 major areas:

Speed of development

Writing SQL queries is faster and easier, as developers will have help of SQL code completion and SQL type safety directly from Go code. 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(N+1 query problem), jet will always perform better as developers can write complex query and retrieve result with a single database call. Thus handler time lost on latency between server and database can be constant. Handler execution will be proportional only to the query complexity and the number of rows returned from database.

With Jet, it is even possible to 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. The whole test database is joined and query result(~10,000 rows) is stored in a structured variable in less than 0.5s.

How quickly bugs are found

The most expensive bugs are the one discovered 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.
Let's return to quick start example, and take closer look at a line:

AND(Film.Length.GT(Int(180))),

Let's say someone changes column length to duration from film table. The next go build will fail at that line, and the bug will be caught at compile time.

Let's say someone changes the type of length column to some non integer type. Build will also fail at the same line because integer columns and expressions can be only compared to other integer columns and expressions.

Build will also fail if someone removes length column from film table. Film field will be omitted from SQL Builder and Model types, next time jet generator is run.

Without Jet these bugs will have to be either caught by some test or by manual testing.

Dependencies

At the moment Jet dependence only of:

  • github.com/lib/pq (Used by jet generator to read PostgreSQL database information)
  • github.com/go-sql-driver/mysql (Used by jet generator to read MySQL and MariaDB database information)
  • github.com/mattn/go-sqlite3 (Used by jet generator to read SQLite database information)
  • github.com/google/uuid (Used in data model files and for debug purposes)

To run the tests, additional dependencies are required:

  • github.com/pkg/profile
  • github.com/stretchr/testify
  • github.com/google/go-cmp
  • github.com/jackc/pgx/v4
  • github.com/shopspring/decimal
  • github.com/volatiletech/null/v8

Versioning

SemVer is used for versioning. For the versions available, take a look at the releases.

License

Copyright 2019-2024 Goran Bjelanovic
Licensed under the Apache License, Version 2.0.

jet's People

Contributors

anisjonischkeit avatar billbuilt avatar cking-vonix avatar crazydadz avatar dependabot[bot] avatar emarj avatar fourdim avatar gkdr avatar go-jet avatar hoonyyhoon avatar joonashaapsaari avatar josephbuchma avatar jupp0r avatar kblomster avatar kvii avatar masterkidan avatar mattdowdell avatar mlaflamm avatar quirell avatar rangzen avatar realbucksavage avatar ryym avatar sarkan-ag5 avatar vetcher avatar wexder avatar yngfoxx avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

jet's Issues

How to properly mock qrm.DB so that it works with jet Query method

Hello,

I have started using the library for a pet project of mine and I am really excited about it. It removes a lot of the boilerplate that I need to write for anything related to databases. However, I had some issues when I tried to unit test some my code.

To be more precise, I use the qrm.DB interface to pass it to any Query made from jet and mock it for the unit tests, but I couldn't find a way to mok it work properly. When jet calls Query it's seems that internally calls the QueryContext method of the DB interface which has the following signature QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error).
It may either return a sql.Rows pointer or an err, but sql.Rows is a struct that doesn't expose any of its internal fields and I cannot pass anyting else except a nil pointer, which makes QueryContext to panic and propagates to jet Query method.

Can you suggest a solution or an alternative solution to this issue?

Transactions ?!

Hello and thank you for publishing this.

I' been looking through the docs & samples and can't find any reference to transactions. Are transactions possible with go-jet (MySQL driver) ?

Unsigned integers?!

Given a database table mapped by go-jet to:

type Host struct {
	IDHost uint64 `sql:"primary_key"`
	Name string
	Addr string
	Port uint16
}

and


obj := model.Host {
    IDHost: 9980677793214571132,
    Name: "a-long-hash",
    Addr: "127.0.0.1",
    Port: 8080,
}

I should be able to do this


	stm := Host.INSERT(Host.AllColumns).
		MODEL(obj).
		ON_DUPLICATE_KEY_UPDATE(
			Host.IDHost.SET(UInt64(obj.IDHost)),
			Host.Name.SET(String(obj.Name)),
			Host.Addr.SET(String(obj.Addr)),
			Host.Port.SET(UInt16(obj.Port)),
			)

however jet-go has no UIntXX IntegerExpression. What am I missing?

Numerical enum fails

Hi,

I just noticed that when an enum has a numerical numerator, the thing crashes quite ambiguously:

Generating enum sql builder files...
14:18: expected expression (and 2 more errors)

Also a feature request: can you add a flag say -overwrite which will only overwrite files and keep other things intact, for now I've commented:

	// fmt.Println("Cleaning up destination directory...")
	// err := utils.CleanUpGeneratedFiles(destDir)
	// utils.PanicOnError(err)

Cheers

Custom Go-lang types for DB models (Monetary values)

We are currently storing monetary values as type Numeric in our PostgreSQL database and we perform all types of numeric operations. Because of this, the default golang Float64 datatype does not work for our needs as we cannot work with numerical approximations.

Is there any way to customize the model currently?

Efficient combination of jet and gRPC

Thanks for this great repo.

I'm wondering what the best way to use this repo is with gRPC. I have some proto messages that will be stored in some PG tables, and i'm wondering what the most efficient way to use jet would be in such a situation. Is it possible for me to unpack query results directly into structs generated via gRPC?

Here's an example struct and its associated proto message:

generated:

type Dataset struct {
	// A server generated ID that is unique across the system.
	Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
	// The name of the Dataset
	Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
	// The account ID that this Dataset belongs to.
	AccountId string `protobuf:"bytes,3,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"`
	// This is the time the Dataset was created, in seconds from the epoch.
	CreateTime           *time.Time `protobuf:"bytes,4,opt,name=create_time,json=createTime,proto3,stdtime" json:"create_time,omitempty"`
	XXX_NoUnkeyedLiteral struct{}   `json:"-"`
	XXX_unrecognized     []byte     `json:"-"`
	XXX_sizecache        int32      `json:"-"`
}

proto message:

message Dataset {
    // A server generated ID that is unique across the system.
    string id = 1;

    // The name of the Dataset
    string name = 2;

    // The account ID that this Dataset belongs to.
    string account_id = 3;

    // This is the time the Dataset was created, in seconds from the epoch.
    google.protobuf.Timestamp create_time = 4 [(gogoproto.stdtime) = true];

}

To me, it seems like the only real option is to treat model.Dataset that Jet generates as a storage layer, and have functions that cast between model.Dataset and the Dataset struct that gRPC generates.

Any thoughts?

Support for passwords with spaces

At present jet doesn't support passwords with spaces in them.

I was able to workaround the issue by doing a sql-injection like attack:

	err := postgres.Generate("./gen/db", postgres.DBConnection{
		Host:     "localhost",
		Port:     5432,
		User:     "dbuser",
		SslMode:  "disable",
		Password: "null password='password with spaces'",
		DBName:   "mydb",
	})

Controlling tags of a go-jet model

Hello,

I checked the documentation but I couldn't find anything saying about custom tags in the auto-generated models. Let's say that I have this table in my database:

CREATE TABLE IF NOT EXISTS users (
      id int GENERATED ALWAYS AS IDENTITY,
      username VARCHAR(255),
      email VARCHAR(255),
      password VARCHAR(255),
      balance float8,
      created_at TIMESTAMPTZ,
      updated_at TIMESTAMPTZ
);

At the moment I get this struct

type Users struct {
	ID        int32 `sql:"primary_key"`
	Username  string
	Email     string
	Password  string
	Balance   *float64
	CreatedAt *time.Time
	UpdatedAt *time.Time
}

but I want to get something like this:

type Users struct {
	ID        int32 `json:"id" sql:"primary_key"`
	Username  string `json:"username"`
	Email     string `json:"email"`
	Password  string `json:"password"`
	Balance   *float64 `json:"balance"`
	CreatedAt *time.Time `json:"createdAt"`
	UpdatedAt *time.Time `json:"updatedAt"`
}

I use my models as a result of an HTTP response, so it would be really handy if I had some control over the resulted struct. Is there a way to control this behaviour or its a feature that may be added in a later stage?

SQLite support

@go-jet , issues #43 and #76 mention SQLite support coming soon. Are there any plans on making this happen in the foreseeable future?

SQLServer Support

Hey there, very interested in trying out this project.

Wondering where MS SQLServer support would be on the roadmap?

Thanks.

Name conflicts in generation

We have an enum column Status in our table Node and also have a table named node_status. The (table-)model NodeStatus gets overwritten by the Enum which also resolves to NodeStatus. As we removed the enum from the node table, everything works fine (but removing or renaming is no option).

A possible solution we could think of, is passing a renaming argument (or config file) for tables or specific attributes. Or maybe just resolve automatically to a prefixed/suffixed name.

Support for dynamic schema names

At the moment, the schema name is baked into the generated code. It would be helpful if the schema name could be globally overridden, so that a compiled program could be configured to hit different databases in different environments without changing code (or adding FromSchema to all queries).

Possible to have expressions with custom arguments?

Is there support for e.g. if I have a custom function, foo(text) returns text, can I call it with a placeholder value? e.g.:

...
.WHERE(
    CALL("foo(?)", MyTable.Column).EQ("bar"),
)

I see there's a RAW() function but it doesn't look like it can accept placeholders.

Add raw helper with parameters

Currently the Raw() function takes a string, but sometimes functions require parameters from the user. Currently you have to sanitize yourself the input and use a fmt.Sprintf which is prone to SQL injections.
It would be nice if we could something like: Raw("my_function($1)", String("test")).

Deleting with joins

Is it currently possible to execute a delete query that includes a join? I'd like to be able to do that without running a sub query.

Here is my ideal sql:

DELETE posts
FROM posts
INNER JOIN projects ON projects.project_id = posts.project_id
WHERE projects.client_id = ?

This what I am stuck with now:

DELETE FROM posts
WHERE posts.project_id IN ((
          SELECT posts.project_id
          FROM posts
               INNER JOIN projects ON projects.project_id = posts.project_id
          WHERE project.client_id = ?
     ));

Postgres Citext Support

If there is no Citext support, it would be great if jet can use string instead of Citext when generating.

Escape reserved words used as identifier in generated sql

Hi there!

I love this idea, I'm building a test app with this library and I found a problem. I'm using a "user" table on PostgreSQL to store my users. This table is on a schema (image_gallery), and if I run:

 statement := SELECT(Picture.AllColumns).
		FROM(
			Picture.INNER_JOIN(User, Picture.UserID.EQ(User.UserID)),
		)

the query fails to execute. The problem is that schema is not being chained to the fields, so the query looks like this:

SELECT picture.picture_id AS "picture.picture_id",
     picture.user_id AS "picture.user_id",
     picture.image_id AS "picture.image_id",
     picture.title AS "picture.title",
     picture.description AS "picture.description",
     picture.created AS "picture.created",
     picture.num_likes AS "picture.num_likes",
     picture.num_comments AS "picture.num_comments"
FROM image_gallery.picture
     INNER JOIN image_gallery.user ON (picture.user_id = user.user_id); 

PostgreSQL complains about a wrong syntax on the "user.user_id". I guess "user" is some reserved word. I see that the solution is to scape the table names always like this:

SELECT "picture".picture_id AS "picture.picture_id",
       "picture".user_id AS "picture.user_id",
       "picture".image_id AS "picture.image_id",
       "picture".title AS "picture.title",
       "picture".description AS "picture.description",
       "picture".created AS "picture.created",
       "picture".num_likes AS "picture.num_likes",
       "picture".num_comments AS "picture.num_comments"
FROM image_gallery."picture"
         INNER JOIN image_gallery."user" ON ("picture".user_id = "user".user_id);

I think it is an easy solution and will help a lot to avoid collision with reserved words, don't you think? I think I can contribute to the project so if you see this solution as correct (I think for Mysql and MariaDB we can use ` scaping char) I can check if I can contribute to the project.

Cheers!

Dynamically construct ORDER BY

Hi, I have a question.

Based on a selection of fields, I am dynamically joining tables by conditionally calling LEFT_JOIN and appending the joined table's columns to a []mysql.Projection which I use in the final SELECT(). This works pretty well.

Now I want to dynamically construct the ORDER_BY() based on the columns I joined, but I fail to see how. The function takes arguments of the internal type jet.OrderByClause, and I have not found an exported type (or interface) I can use to have a slice of these I can append to.

Did I miss something, or is there currently no way to do this? I would really prefer to avoid using subselects in the joined tables just for the ordering.

Thanks in advance!

Return *; equivalent?

Hi,

I am looking for the proper way to do an insert and fetch back the returned value from the insert.

For instance, how would I execute a query like the following using Jet?

INSERT INTO public.users (user_id,
                          email,
                          first_name,
                          last_name,
                          oidc_subject,
                          password_hash,
                          created_time,
                          updated_time)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
RETURNING *;

Select returns empty data for custom models

Hi nice job building this query builder ๐Ÿ‘

Currently when I try to use custom models, when executing the SELECT statement for postgres an empty data is returned. Example ๐Ÿ‘‡

db, err := config.GetDb()
defer db.Close()
if err != nil {
    return nil, err
}

var users = []User{} // User here is a custom model, not the one generated by jet

t := tbl.Users // Table generated for User by jet
stmt, _ := t.SELECT(t.AllColumns).FROM(t)
err = t.Query(db, &users)

fmt.Println(users) // Returns []

One reason for the custom model is: the table names are in plural but I want the model name to be singular, but jet generates the models as plurals just exactly from the db tables.

Is there a way I can write this to work explicitly with custom models instead of the ones generated by jet?

Allow to forbid-list tables from generation

Sometimes, database contain tables that is not relates to domain itself and are serving as technical information. So we want it to be ignored by jet.
E.g. applied migrations list (https://github.com/pressly/goose creates table goose_db_version)

I propose to add some flag to cli, so generator would ignore tables passed to this flag.

$ jet -ignore="goose_db_version;locks"

Schema rename support

Add support for renaming the schema for generated code. Currently there is no way to use the generated code for identical database structure using two different schema names.

Use cases for this includes multitenant systems and/or using different schema name in test database than in development in order to avoid date-destroying accidents in production database. The issues is even more serious in MySQL, as the schema name is in fact the name of the database.

Furthermore, it would be nice to have support for using the default schema (i.e. leave out the schema name part in the SQL queries) for the above use cases.

Support can be build just like the AS -function is supported in the generated code. This allows the same generated code to access tables in multiple schemas even simultaneously.

Postgres arrays returned as string

I have this table:

create table sessions (
    id uuid primary key default gen_random_uuid(),
    user_id uuid not null references users(id) on delete cascade,
    identifier text not null default '',
    scopes text[] not null default '{}'
);

However, it generates this code:

type Sessions struct {
	ID         uuid.UUID `sql:"primary_key"`
	UserID     uuid.UUID
	Identifier string
	Scopes     string
}

scopes has been emitted as string rather than []string, and results in the funny Postgres {foo, bar} string. Is it possible to scan directly into an array?

Support for conflict clause and with queries

Jet is awesome, but features are a bit limiting for my use cases. Any plan/timeline to add the following features (for postgres, I assume MySQL has an equivalent):

  1. Conflict clause such as:
    INSERT INTO table (col) VALUES (val) ON CONFLICT DO NOTHING
  2. WITH Queries (CTE) such as:
    WITH alias AS (
        SELECT x FROM y
    )
    SELECT * FROM z
    WHERE a IN (SELECT a FROM alias)

Thanks for the great work!

Changing default alias

I'm planning to use jet for query builder then I can map query results to my own Struct. Currently, jet generates alias with table.column format. For example:

stmt := SELECT(Clients.AllColumns).FROM(Clients).WHERE(expression)
fmt.Println(stmt.DebugSql())

will prints

SELECT clients.id AS "clients.id",
     clients.created_at AS "clients.created_at",
     clients.updated_at AS "clients.updated_at",
     clients.username AS "clients.username"
FROM mydb.clients
WHERE clients.id = '12345';

However, I defined my struct like this:

type Client struct {
	ID        uuid.UUID `db:"id"` // tag name is different with generated alias
	CreatedAt time.Time `db:"created_at"`
	UpdatedAt time.Time `db:"updated_at"`
	Username  string    `db:"username"`
}

This, of course, will cause error when I try to map the result. I could do either:

  1. Use the jet generated model Struct instead of my own Struct
  2. Change the tags to include table name (for example change db:"id" to db:"clients.id"), or
  3. Change query statement to this:
stmt := SELECT(
    Clients.ID.AS(Clients.ID.Name()),
    Clients.CreatedAt.AS(Clients.CreatedAt.Name()),
    Clients.UpdatedAt.AS(Clients.UpdatedAt.Name()),
    Clients.Username.AS(Clients.Username.Name())).
FROM(Clients).WHERE(expression)

I might go with the second solution, however it would be nicer if we have some way to control the default alias ๐Ÿ˜‰

func (c *columnImpl) defaultAlias() string {
if c.tableName != "" {
return c.tableName + "." + c.name
}
return c.name
}

Generated update SQL does NOT use backticks for reserved keywords

I am trying to do an update of a column with a reserved word as name in mysql.

Models und set value generate invalid SQL:

type Model struct { ID: int, Load: string }
table.UPDATE(table.Load).MODELS(model{Load: "test"}).Where(...

AND

table.UPDATE(table.Load).SET(mysql.String("test")).WHERE(...

generates:

UPDATE table
SET load = 'test'
WHERE ...;

set column with value generates valid SQL:

table.UPDATE().SET(table.Load.SET(mysql.String("test"))).WHERE(...

generates the correct SQL:

UPDATE uwaa.node_status
SET `load` = 'test'
WHERE ...;

Generated files idempotence

Thank you for your work on this library - a very nice design idea and very slick in use!

I'm driving the jet executable from go generate, and it works very well, except jet always adds the generation date which ends up being an only thing in the git diff after the generation step and requires me to then revert the jet generated files that are not actually changed.

//
// Code generated by go-jet DO NOT EDIT.
// Generated at Wednesday, 15-Jan-20 08:38:53 GMT
//

Would it be possible to remove or introduce a config switch for the date generation?

Thanks and well done on the library again!

I am confused about how to use v2

I am coming back to an older evaluation project of mine and thought I need to update the go.mod file to use v2 (github.com/go-jet/jet/v2). I did, and I updated jet itself too.

The jet command shows 2.3.0 as the version number.

When I generate the table schemas (MariaDB), it will use a non-v2 import in the generated sources, though! Building the project will ask me to add the non-v2 path to go.mod. When I do, it won't compile because of incompatible structures.

I have a clean go (1.16.3) installation with no GOPATH set (go env GOPATH shows ~/go). The project is a go.mod project and located at a different path.

When I don't use the v2 in the mod file, everything compiles fine (but probably not with the newest version of jet?).

When I use the v2 in the mod file, do a generate and manually add the v2 to all generated sources, it also compiles fine.

I also hat "one time" that jet actually generated v2/mysql imports, but I can't reproduce it.

It looks to me as if the jet tool does not correctly determine if the import paths have to use the v2 ending.

What am I doing wrong here?

Allow to connect via DSN

Hi.
Is it possible to allow connection via DSN (like postgres://username:password@localhost:5432/database_name), not separated flag params?
I see, that pgx and mysql driver understands this format.

I think it should looks like:

$ jet -conn="postgres://username:password@localhost:5432/jet" -schema=public -path=.

I can prepare pull request

IN doesn't accept []StringExpression...

Hi there!
Awesome job, I really like the lib for far :)
So I have this particular query:

err := SELECT(Type.Name.AS("parentType"), Field.AllColumns).
		FROM(Field).
		FROM(Field.INNER_JOIN(Type, Type.ID.EQ(Field.ParentTypeID))).
		WHERE(Field.ParentTypeID.IN(types...)).
		Query(s.db, &existingFields)

Where types is []StringExpression.
I would have expected it to be accepted but it gives me an error:

Cannot use 't' (type []StringExpression) as type []Expression 

No sure what is wrong. I am using Postgres.

ON DUPLICATE KEY UPDATE clause for batch INSERTS

In MySQL it is possible to use ON DUPLICATE KEY UPDATE clause in batch inserts, like in the below example:

INSERT INTO `buoy_stations` (`id`, `coords`, `name`, `owner`, `pgm`, `met`, `currents`)
VALUES 
('00922', 'Point(30,-90)','name 1','owner 1','pgm 1','y','y'),
('00923', 'Point(30,-90)','name 2','owner 2','pgm 2','y','y'),
('00924', 'Point(30,-90)','name 3','owner 3','pgm 3','y','y'),
('00925', 'Point(30,-90)','name 4','owner 4','pgm 4','y','y'),
('00926', 'Point(30,-90)','name 5','owner 5','pgm 5','y','y')
ON DUPLICATE KEY
        UPDATE coords=values(coords), name=values(name), owner=values(owner), pgm=values(pgm), met=values(met), currents=values(currents);

However, it appears that it is impossible to perform batch inserts with go-jet's current ON DUPLICATE KEY UPDATE implementation.

Pgx compatible?

Hi thanks for this repo.

It seems like the query interface is meant for libpq driver. Any chance this is compatible with pgx?

Feature request: error and latency logging

It would be nice if the LoggerFunc interface also had provided to it the outcome of the query, such as the number of rows returned, execution latency and the error. This could also be achieved by providing a middleware interceptor.

Change mapping of postgres `jsonb` to []byte

Hi
I know it would be backward incompatible, but I think jsonb and json columns should be mapped to go []byte instead of current string.

My points are:

  • for parsing (json.Unmarshal()) json field from string we need to convert it to []byte. It adds overhead like additional copying or using unsafe
  • Json is not a "text"

Optionally disable specific packages generation

This project is really what I needed to work with go and sql, so thank you.

Since destinations types could be any type and generated packages do not depend on each other, it would be great to have the opportunity to opt out of specific packages generation, for example to avoid generation of the model package.

I'm thinking about a cmd flag model ( with maybe a corresponding field in a config struct for generation from code ? ) with default value true which could be set to false to avoid generation of that specific package in case I want to take advantage of my custom types.
Same for enum.

I'd be glad to work on it myself if you like the idea, just let me know what you think about it.

Why is `SET()` not purely variadic?

Hi again,

I have a question regarding the signature you chose on some functions.
For example, after using UPDATE(columns ...jet.Column), you have to use SET(value interface{}, values ...interface{}). So in the first function, you can comfortably use a slice, but not in the second one. A similar case that comes to mind is SELECT(projection Projection, projections ...Projection). Without digging too deep, I can't really tell why it needs to be like this. Could you explain?

(In my specific case, I had either one or two items in my slice. I tried SET(vals[0], vals[1:]...), but (iirc) with only one item JET would complain that the lenghts of the parameter lists to the update and set clauses is different. If the interface needs to stay that way, maybe this is something that can be fixed instead.)

Access to foreign data via PostgreSql + FDW

Hello,

I've successfully used this library with PostgreSql connected to Oracle through a foreign data wrapper.

Only 4 lines were added to generator/internal/metadata/schema_meta_data.go, as follows:

const (
     baseTable    = "BASE TABLE"
+     foreignTable = "FOREIGN" 
     view         = "VIEW"
)
// GetSchemaMetaData returns schema information from db connection.
 func GetSchemaMetaData(db *sql.DB, schemaName string, querySet DialectQuerySet) (schemaInfo SchemaMetaData) {
        schemaInfo.TablesMetaData = getTablesMetaData(db, querySet, schemaName, baseTable)
 +      for _, s := range getTablesMetaData(db, querySet, schemaName, foreignTable) {
 +              schemaInfo.TablesMetaData = append(schemaInfo.TablesMetaData, s)
 +      }
        schemaInfo.ViewsMetaData = getTablesMetaData(db, querySet, schemaName, view)
        schemaInfo.EnumsMetaData = querySet.GetEnumsMetaData(db, schemaName)

After these modifications, all pre-existing tests were successful (even those specific to MySql/Mariadb).

I have not written any tests for FOREIGN tables since once generated in the Go project, they are no different from 'normal' tables.

Since FDWs are standardized, this (potentially) opens the door to a large number of databases through PostgreSql: https://wiki.postgresql.org/wiki/Foreign_data_wrappers

I have not made a PR because I don't know if this approach would be useful for you, but since the modification is very slight it could be made directly by you in the source code if you find it useful.

go-jet/jet: v2.1.0
Postgresql: 11.5
https://github.com/laurenz/oracle_fdw/releases/tag/ORACLE_FDW_2_1_0
Oracle 11.2
Ubuntu 16.04

How to select MIN/MAX date field?

I have a table of records that has a date DATETIME field. I need to select the earliest date and the latest date. In raw SQL this would be

SELECT MIN(date) AS first, MAX(date) AS last FROM table;

However with jet there are only min/max funcs for integers and floats and no apparent way to convert a date field to an integer (effectively render it as unix time). How can I do this with Jet?

PostgreSQL views should be queriyable

Hello,
excellent approach in the spirit of JOOQ (Java/Kotlin). Great project. Thank you.
Is there any specific reason why PostgreSQL "views" are excluded from SQL queries?

SELECT table_name FROM information_schema.tables where table_schema = $1 /* and table_type = 'BASE TABLE' */ ;

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.