Dialect refactor improvements and clean up.
This commit is contained in:
parent
23fd973699
commit
647ef21aaf
52 changed files with 1097 additions and 671 deletions
|
|
@ -1,547 +0,0 @@
|
|||
# Jet
|
||||
|
||||
[](https://circleci.com/gh/go-jet/jet/tree/develop)
|
||||
[](https://codecov.io/gh/go-jet/jet)
|
||||
[](https://goreportcard.com/report/github.com/go-jet/jet)
|
||||
[](http://godoc.org/github.com/go-jet/jet)
|
||||
[](https://github.com/go-jet/jet/releases)
|
||||
|
||||
|
||||
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._
|
||||
|
||||
|
||||
## Contents
|
||||
- [Features](#features)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation](#installation)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Generate sql builder and model files](#generate-sql-builder-and-model-files)
|
||||
- [Lets write some SQL queries in Go](#lets-write-some-sql-queries-in-go)
|
||||
- [Execute query and store result](#execute-query-and-store-result)
|
||||
- [Benefits](#benefits)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Versioning](#versioning)
|
||||
- [License](#license)
|
||||
|
||||
## Features
|
||||
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:
|
||||
* SELECT (DISTINCT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET, FOR, UNION, INTERSECT, EXCEPT, sub-queries)
|
||||
* INSERT (VALUES, query, RETURNING),
|
||||
* UPDATE (SET, WHERE, RETURNING),
|
||||
* DELETE (WHERE, RETURNING),
|
||||
* LOCK (IN, NOWAIT)
|
||||
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
|
||||
|
||||
### Prerequisites
|
||||
|
||||
To install Jet package, you need to install Go and set your Go workspace first.
|
||||
|
||||
[Go](https://golang.org/) **version 1.8+ is required**
|
||||
|
||||
### Installation
|
||||
|
||||
Use the bellow command to install jet
|
||||
```sh
|
||||
$ go get -u github.com/go-jet/jet
|
||||
```
|
||||
|
||||
Install jet generator to GOPATH bin folder. This will allow generating jet files from the command line.
|
||||
|
||||
```sh
|
||||
go install github.com/go-jet/jet/cmd/jet
|
||||
```
|
||||
|
||||
Make sure GOPATH bin folder is added to the PATH environment variable.
|
||||
|
||||
### Quick Start
|
||||
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
|
||||
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 `jetuser`, user password `jetpass`, database `jetdb` and
|
||||
schema `dvds` we will use this command:
|
||||
```sh
|
||||
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=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...
|
||||
Generating table sql builder files...
|
||||
Generating table model files...
|
||||
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 table 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
|
||||
| | |-- mpaa_rating.go
|
||||
| |-- table # sql builder folder for tables
|
||||
| |-- actor.go
|
||||
| |-- address.go
|
||||
| |-- category.go
|
||||
| ...
|
||||
| |-- 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 can be combined to store
|
||||
results of the SQL queries.
|
||||
|
||||
#### Lets write some SQL queries in Go
|
||||
|
||||
First we need to import jet and generated files from previous step:
|
||||
```go
|
||||
import (
|
||||
// dot import so that Go code would resemble as much as native SQL
|
||||
// dot import is not mandatory
|
||||
. "github.com/go-jet/jet"
|
||||
. "github.com/go-jet/jet/examples/quick-start/gen/jetdb/dvds/table"
|
||||
|
||||
"github.com/go-jet/jet/examples/quick-start/gen/jetdb/dvds/model"
|
||||
)
|
||||
```
|
||||
Lets 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'.
|
||||
```go
|
||||
stmt := SELECT(
|
||||
Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate, // or just Actor.AllColumns
|
||||
Film.AllColumns,
|
||||
Language.AllColumns,
|
||||
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(),
|
||||
)
|
||||
```
|
||||
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 from statement?__
|
||||
```go
|
||||
query, args, err := stmt.Sql()
|
||||
```
|
||||
query - parametrized query\
|
||||
args - parameters for the query
|
||||
|
||||
<details>
|
||||
<summary>Click to see `query` and `args`</summary>
|
||||
|
||||
```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 = $1) AND (category.name != $2)) AND (film.length > $3)
|
||||
ORDER BY actor.actor_id ASC, film.film_id ASC;
|
||||
```
|
||||
```sh
|
||||
[English Action 180]
|
||||
```
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
__How to get debug SQL from statement?__
|
||||
```go
|
||||
debugSql, err := stmt.DebugSql()
|
||||
```
|
||||
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>
|
||||
|
||||
```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;
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
#### 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
|
||||
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).
|
||||
|
||||
Let's say this is our desired structure:
|
||||
```go
|
||||
var dest []struct {
|
||||
model.Actor
|
||||
|
||||
Films []struct {
|
||||
model.Film
|
||||
|
||||
Language model.Language
|
||||
Categories []model.Category
|
||||
}
|
||||
}
|
||||
```
|
||||
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`.
|
||||
|
||||
```go
|
||||
err := stmt.Query(db, &dest)
|
||||
handleError(err)
|
||||
```
|
||||
|
||||
__And thats 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:
|
||||
```go
|
||||
jsonText, _ := json.MarshalIndent(dest, "", "\t")
|
||||
fmt.Println(string(jsonText))
|
||||
```
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
"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": "2006-02-15T10:02:19Z"
|
||||
},
|
||||
"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": "2006-02-15T10:02:19Z"
|
||||
},
|
||||
"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:
|
||||
|
||||
```go
|
||||
var dest2 []struct {
|
||||
model.Category
|
||||
|
||||
Films []model.Film
|
||||
Actors []model.Actor
|
||||
}
|
||||
|
||||
err = stmt.Query(db, &dest2)
|
||||
handleError(err)
|
||||
```
|
||||
<details>
|
||||
<summary>Click to see `dest2` json</summary>
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
//...
|
||||
]
|
||||
```
|
||||
</details>
|
||||
|
||||
Complete code example can be found at [./examples/quick-start/quick-start.go](./examples/quick-start/quick-start.go)
|
||||
|
||||
|
||||
This example represent probably the most common use case. Detail info about additional features and use cases can be
|
||||
found at project [wiki](https://github.com/go-jet/jet/wiki) page.
|
||||
|
||||
## Benefits
|
||||
|
||||
What are the benefits of writing SQL in Go using Jet? The biggest benefit is speed.
|
||||
Speed is improved in 3 major areas:
|
||||
|
||||
##### Speed of development
|
||||
|
||||
Writing SQL queries is much easier directly from Go, 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.
|
||||
|
||||
##### Speed of execution
|
||||
|
||||
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.
|
||||
This is not such a big problem if handler calls database couple of times, but what if web server is using ORM to retrieve data from database.
|
||||
ORM sometimes can access the database once for every object needed. Now lets say latency is 30ms and there are 100
|
||||
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 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/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.
|
||||
Lets return to quick start example, and take closer look at a line:
|
||||
```go
|
||||
AND(Film.Length.GT(Int(180))),
|
||||
```
|
||||
Lets 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.
|
||||
|
||||
Lets 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 compered to other integer columns and expressions.
|
||||
|
||||
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/google/uuid` _(Used for debug purposes and in data model files)_
|
||||
- `github.com/lib/pq` _(Used by Jet to read information about database schema types)_
|
||||
|
||||
To run the tests, additional dependencies are required:
|
||||
- `github.com/pkg/profile`
|
||||
- `gotest.tools/assert`
|
||||
|
||||
|
||||
## Versioning
|
||||
|
||||
[SemVer](http://semver.org/) is used for versioning. For the versions available, see the [releases](https://github.com/go-jet/jet/releases).
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2019 Goran Bjelanovic
|
||||
Licensed under the Apache License, Version 2.0.
|
||||
|
|
@ -11,25 +11,25 @@ func TestBoolExpressionEQ(t *testing.T) {
|
|||
|
||||
func TestBoolExpressionNOT_EQ(t *testing.T) {
|
||||
assertClauseSerialize(t, table1ColBool.NOT_EQ(table2ColBool), "(table1.col_bool != table2.col_bool)")
|
||||
assertClauseSerialize(t, table1ColBool.NOT_EQ(Bool(true)), "(table1.col_bool != TRUE)")
|
||||
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 FALSE)")
|
||||
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 FALSE)")
|
||||
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(),
|
||||
`(2 = table1.col_int) IS TRUE`)
|
||||
`($1 = table1.col_int) IS TRUE`, int64(2))
|
||||
assertClauseSerialize(t, (Int(2).EQ(table1ColInt)).IS_TRUE().AND(Int(4).EQ(table2ColInt)),
|
||||
`((2 = table1.col_int) IS TRUE AND (4 = table2.col_int))`)
|
||||
`(($1 = table1.col_int) IS TRUE AND ($2 = table2.col_int))`, int64(2), int64(4))
|
||||
}
|
||||
|
||||
func TestBoolExpressionIS_NOT_TRUE(t *testing.T) {
|
||||
|
|
@ -55,20 +55,20 @@ func TestBoolExpressionIS_NOT_UNKNOWN(t *testing.T) {
|
|||
func TestBinaryBoolExpression(t *testing.T) {
|
||||
boolExpression := Int(2).EQ(Int(3))
|
||||
|
||||
assertClauseSerialize(t, boolExpression, "(2 = 3)")
|
||||
assertClauseSerialize(t, boolExpression, "($1 = $2)", int64(2), int64(3))
|
||||
|
||||
assertProjectionSerialize(t, boolExpression, "2 = 3")
|
||||
assertProjectionSerialize(t, boolExpression, "$1 = $2", int64(2), int64(3))
|
||||
assertProjectionSerialize(t, boolExpression.AS("alias_eq_expression"),
|
||||
`(2 = 3) AS "alias_eq_expression"`)
|
||||
`($1 = $2) AS "alias_eq_expression"`, int64(2), int64(3))
|
||||
assertClauseSerialize(t, boolExpression.AND(Int(4).EQ(Int(5))),
|
||||
"((2 = 3) AND (4 = 5))")
|
||||
"(($1 = $2) AND ($3 = $4))", int64(2), int64(3), int64(4), int64(5))
|
||||
assertClauseSerialize(t, boolExpression.OR(Int(4).EQ(Int(5))),
|
||||
"((2 = 3) OR (4 = 5))")
|
||||
"(($1 = $2) OR ($3 = $4))", int64(2), int64(3), int64(4), int64(5))
|
||||
}
|
||||
|
||||
func TestBoolLiteral(t *testing.T) {
|
||||
assertClauseSerialize(t, Bool(true), "TRUE", true)
|
||||
assertClauseSerialize(t, Bool(false), "FALSE", false)
|
||||
assertClauseSerialize(t, Bool(true), "$1", true)
|
||||
assertClauseSerialize(t, Bool(false), "$1", false)
|
||||
}
|
||||
|
||||
func TestExists(t *testing.T) {
|
||||
|
|
@ -78,13 +78,13 @@ func TestExists(t *testing.T) {
|
|||
WHERE(table1Col1.EQ(table2Col3)),
|
||||
),
|
||||
`(EXISTS (
|
||||
SELECT 1
|
||||
SELECT $1
|
||||
FROM db.table2
|
||||
WHERE table1.col1 = table2.col3
|
||||
))`)
|
||||
))`, int64(1))
|
||||
}
|
||||
|
||||
func TestBoolExp(t *testing.T) {
|
||||
assertClauseSerialize(t, BoolExp(String("true")), "'true'")
|
||||
assertClauseSerialize(t, BoolExp(String("true")).IS_TRUE(), "'true' IS TRUE")
|
||||
assertClauseSerialize(t, BoolExp(String("true")), "$1", "true")
|
||||
assertClauseSerialize(t, BoolExp(String("true")).IS_TRUE(), "$1 IS TRUE", "true")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ func TestArgToString(t *testing.T) {
|
|||
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(float64(1.11)), "1.11")
|
||||
|
||||
assert.Equal(t, argToString("john"), "'john'")
|
||||
assert.Equal(t, argToString([]byte("john")), "'john'")
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
package jet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var ANSII = NewDialect(DialectParams{ // just for tests
|
||||
AliasQuoteChar: '"',
|
||||
AliasQuoteChar: '"',
|
||||
IdentifierQuoteChar: '"',
|
||||
ArgumentPlaceholder: func(ord int) string {
|
||||
return "#"
|
||||
return "$" + strconv.Itoa(ord)
|
||||
},
|
||||
SupportsReturning: true,
|
||||
})
|
||||
|
||||
type Dialect interface {
|
||||
|
|
@ -15,7 +22,7 @@ type Dialect interface {
|
|||
AliasQuoteChar() byte
|
||||
IdentifierQuoteChar() byte
|
||||
ArgumentPlaceholder() QueryPlaceholderFunc
|
||||
UpdateAssigment() func(columns []IColumn, values []Clause, out *SqlBuilder) (err error)
|
||||
SetClause() func(columns []IColumn, values []Clause, out *SqlBuilder) (err error)
|
||||
SupportsReturning() bool
|
||||
}
|
||||
|
||||
|
|
@ -35,7 +42,7 @@ type DialectParams struct {
|
|||
AliasQuoteChar byte
|
||||
IdentifierQuoteChar byte
|
||||
ArgumentPlaceholder QueryPlaceholderFunc
|
||||
UpdateAssigment func(columns []IColumn, values []Clause, out *SqlBuilder) (err error)
|
||||
SetClause func(columns []IColumn, values []Clause, out *SqlBuilder) (err error)
|
||||
|
||||
SupportsReturning bool
|
||||
}
|
||||
|
|
@ -49,7 +56,7 @@ func NewDialect(params DialectParams) Dialect {
|
|||
aliasQuoteChar: params.AliasQuoteChar,
|
||||
identifierQuoteChar: params.IdentifierQuoteChar,
|
||||
argumentPlaceholder: params.ArgumentPlaceholder,
|
||||
updateAssigment: params.UpdateAssigment,
|
||||
setClause: params.SetClause,
|
||||
supportsReturning: params.SupportsReturning,
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +69,7 @@ type dialectImpl struct {
|
|||
aliasQuoteChar byte
|
||||
identifierQuoteChar byte
|
||||
argumentPlaceholder QueryPlaceholderFunc
|
||||
updateAssigment UpdateAssigmentFunc
|
||||
setClause UpdateAssigmentFunc
|
||||
|
||||
supportsReturning bool
|
||||
}
|
||||
|
|
@ -95,10 +102,40 @@ func (d *dialectImpl) ArgumentPlaceholder() QueryPlaceholderFunc {
|
|||
return d.argumentPlaceholder
|
||||
}
|
||||
|
||||
func (d *dialectImpl) UpdateAssigment() func(columns []IColumn, values []Clause, out *SqlBuilder) (err error) {
|
||||
return d.updateAssigment
|
||||
func (d *dialectImpl) SetClause() func(columns []IColumn, values []Clause, out *SqlBuilder) (err error) {
|
||||
if d.setClause != nil {
|
||||
return d.setClause
|
||||
}
|
||||
return setClause
|
||||
}
|
||||
|
||||
func (d *dialectImpl) SupportsReturning() bool {
|
||||
return d.supportsReturning
|
||||
}
|
||||
|
||||
func setClause(columns []IColumn, values []Clause, out *SqlBuilder) (err error) {
|
||||
|
||||
if len(columns) != len(values) {
|
||||
return errors.New("jet: mismatch in numers of columns and values")
|
||||
}
|
||||
|
||||
for i, column := range columns {
|
||||
if i > 0 {
|
||||
out.WriteString(", ")
|
||||
}
|
||||
|
||||
if column == nil {
|
||||
return errors.New("jet: nil column in columns list")
|
||||
}
|
||||
|
||||
out.WriteString(column.Name())
|
||||
|
||||
out.WriteString(" = ")
|
||||
|
||||
if err = Serialize(values[i], UpdateStatementType, out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -204,14 +204,13 @@ func CHR(integerExpression IntegerExpression) StringExpression {
|
|||
return newStringFunc("CHR", integerExpression)
|
||||
}
|
||||
|
||||
//
|
||||
//func CONCAT(expressions ...Expression) StringExpression {
|
||||
// return newStringFunc("CONCAT", expressions...)
|
||||
//}
|
||||
//
|
||||
//func CONCAT_WS(expressions ...Expression) StringExpression {
|
||||
// return newStringFunc("CONCAT_WS", expressions...)
|
||||
//}
|
||||
func CONCAT(expressions ...Expression) StringExpression {
|
||||
return newStringFunc("CONCAT", expressions...)
|
||||
}
|
||||
|
||||
func CONCAT_WS(expressions ...Expression) StringExpression {
|
||||
return newStringFunc("CONCAT_WS", expressions...)
|
||||
}
|
||||
|
||||
// CONVERT converts string to dest_encoding. The original encoding is
|
||||
// specified by src_encoding. The string must be valid in this encoding.
|
||||
|
|
@ -243,11 +242,11 @@ func DECODE(data StringExpression, format StringExpression) StringExpression {
|
|||
return newStringFunc("DECODE", data, format)
|
||||
}
|
||||
|
||||
//func FORMAT(formatStr StringExpression, formatArgs ...expressions) StringExpression {
|
||||
// args := []expressions{formatStr}
|
||||
// args = append(args, formatArgs...)
|
||||
// return newStringFunc("FORMAT", args...)
|
||||
//}
|
||||
func FORMAT(formatStr StringExpression, formatArgs ...Expression) StringExpression {
|
||||
args := []Expression{formatStr}
|
||||
args = append(args, formatArgs...)
|
||||
return newStringFunc("FORMAT", args...)
|
||||
}
|
||||
|
||||
// INITCAP converts the first letter of each word to upper case
|
||||
// and the rest to lower case. Words are sequences of alphanumeric
|
||||
|
|
@ -344,6 +343,14 @@ func TO_HEX(number IntegerExpression) StringExpression {
|
|||
return newStringFunc("TO_HEX", number)
|
||||
}
|
||||
|
||||
func REGEXP_LIKE(stringExp StringExpression, pattern StringExpression, matchType ...string) BoolExpression {
|
||||
if len(matchType) > 0 {
|
||||
return newBoolFunc("REGEXP_LIKE", stringExp, pattern, String(matchType[0], true))
|
||||
}
|
||||
|
||||
return newBoolFunc("REGEXP_LIKE", stringExp, pattern)
|
||||
}
|
||||
|
||||
//----------Data Type Formatting Functions ----------------------//
|
||||
|
||||
// TO_CHAR converts expression to string with format
|
||||
|
|
@ -425,9 +432,9 @@ func LOCALTIMESTAMP(precision ...int) TimestampExpression {
|
|||
var timestampFunc *timestampFunc
|
||||
|
||||
if len(precision) > 0 {
|
||||
timestampFunc = newTimestampFunc("LOCALTIMESTAMP", constLiteral(precision[0]))
|
||||
timestampFunc = NewTimestampFunc("LOCALTIMESTAMP", constLiteral(precision[0]))
|
||||
} else {
|
||||
timestampFunc = newTimestampFunc("LOCALTIMESTAMP")
|
||||
timestampFunc = NewTimestampFunc("LOCALTIMESTAMP")
|
||||
}
|
||||
|
||||
timestampFunc.noBrackets = true
|
||||
|
|
@ -506,6 +513,12 @@ func (f *funcExpressionImpl) serialize(statement StatementType, out *SqlBuilder,
|
|||
return errors.New("jet: Function expressions is nil. ")
|
||||
}
|
||||
|
||||
if serializeOverride := out.Dialect.SerializeOverride(f.name); serializeOverride != nil {
|
||||
|
||||
serializeOverrideFunc := serializeOverride(f.expressions...)
|
||||
return serializeOverrideFunc(statement, out, options...)
|
||||
}
|
||||
|
||||
addBrackets := !f.noBrackets || len(f.expressions) > 0
|
||||
|
||||
if addBrackets {
|
||||
|
|
@ -629,7 +642,7 @@ type timestampFunc struct {
|
|||
timestampInterfaceImpl
|
||||
}
|
||||
|
||||
func newTimestampFunc(name string, expressions ...Expression) *timestampFunc {
|
||||
func NewTimestampFunc(name string, expressions ...Expression) *timestampFunc {
|
||||
timestampFunc := ×tampFunc{}
|
||||
|
||||
timestampFunc.funcExpressionImpl = *newFunc(name, expressions, timestampFunc)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
package jet
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Representation of an escaped literal
|
||||
type literalExpression struct {
|
||||
type LiteralExpression interface {
|
||||
Expression
|
||||
|
||||
Value() interface{}
|
||||
SetConstant(constant bool)
|
||||
}
|
||||
|
||||
type literalExpressionImpl struct {
|
||||
expressionInterfaceImpl
|
||||
noOpVisitorImpl
|
||||
|
||||
|
|
@ -11,8 +22,8 @@ type literalExpression struct {
|
|||
constant bool
|
||||
}
|
||||
|
||||
func literal(value interface{}, optionalConstant ...bool) *literalExpression {
|
||||
exp := literalExpression{value: value}
|
||||
func literal(value interface{}, optionalConstant ...bool) *literalExpressionImpl {
|
||||
exp := literalExpressionImpl{value: value}
|
||||
|
||||
if len(optionalConstant) > 0 {
|
||||
exp.constant = optionalConstant[0]
|
||||
|
|
@ -23,14 +34,14 @@ func literal(value interface{}, optionalConstant ...bool) *literalExpression {
|
|||
return &exp
|
||||
}
|
||||
|
||||
func constLiteral(value interface{}) *literalExpression {
|
||||
func constLiteral(value interface{}) *literalExpressionImpl {
|
||||
exp := literal(value)
|
||||
exp.constant = true
|
||||
|
||||
return exp
|
||||
}
|
||||
|
||||
func (l literalExpression) serialize(statement StatementType, out *SqlBuilder, options ...SerializeOption) error {
|
||||
func (l *literalExpressionImpl) serialize(statement StatementType, out *SqlBuilder, options ...SerializeOption) error {
|
||||
if l.constant {
|
||||
out.insertConstantArgument(l.value)
|
||||
} else {
|
||||
|
|
@ -40,29 +51,102 @@ func (l literalExpression) serialize(statement StatementType, out *SqlBuilder, o
|
|||
return nil
|
||||
}
|
||||
|
||||
func (l *literalExpressionImpl) Value() interface{} {
|
||||
return l.value
|
||||
}
|
||||
|
||||
func (l *literalExpressionImpl) SetConstant(constant bool) {
|
||||
l.constant = constant
|
||||
}
|
||||
|
||||
type integerLiteralExpression struct {
|
||||
literalExpressionImpl
|
||||
integerInterfaceImpl
|
||||
}
|
||||
|
||||
// Int is constructor for integer expressions literals.
|
||||
func Int(value int64, constant ...bool) IntegerExpression {
|
||||
return IntExp(literal(value, constant...))
|
||||
numLiteral := &integerLiteralExpression{}
|
||||
|
||||
numLiteral.literalExpressionImpl = *literal(value)
|
||||
if len(constant) > 0 && constant[0] == true {
|
||||
numLiteral.constant = true
|
||||
}
|
||||
|
||||
numLiteral.literalExpressionImpl.parent = numLiteral
|
||||
numLiteral.integerInterfaceImpl.parent = numLiteral
|
||||
|
||||
return numLiteral
|
||||
}
|
||||
|
||||
//---------------------------------------------------//
|
||||
type boolLiteralExpression struct {
|
||||
boolInterfaceImpl
|
||||
literalExpressionImpl
|
||||
}
|
||||
|
||||
// Bool creates new bool literal expression
|
||||
func Bool(value bool) BoolExpression {
|
||||
return BoolExp(literal(value))
|
||||
boolLiteralExpression := boolLiteralExpression{}
|
||||
|
||||
boolLiteralExpression.literalExpressionImpl = *literal(value)
|
||||
boolLiteralExpression.boolInterfaceImpl.parent = &boolLiteralExpression
|
||||
|
||||
return &boolLiteralExpression
|
||||
}
|
||||
|
||||
//---------------------------------------------------//
|
||||
type floatLiteral struct {
|
||||
floatInterfaceImpl
|
||||
literalExpressionImpl
|
||||
}
|
||||
|
||||
// Float creates new float literal expression
|
||||
func Float(value float64) FloatExpression {
|
||||
return FloatExp(literal(value))
|
||||
floatLiteral := floatLiteral{}
|
||||
floatLiteral.literalExpressionImpl = *literal(value)
|
||||
|
||||
floatLiteral.floatInterfaceImpl.parent = &floatLiteral
|
||||
|
||||
return &floatLiteral
|
||||
}
|
||||
|
||||
//---------------------------------------------------//
|
||||
type stringLiteral struct {
|
||||
stringInterfaceImpl
|
||||
literalExpressionImpl
|
||||
}
|
||||
|
||||
// String creates new string literal expression
|
||||
func String(value string) StringExpression {
|
||||
return StringExp(literal(value))
|
||||
func String(value string, constant ...bool) StringExpression {
|
||||
stringLiteral := stringLiteral{}
|
||||
stringLiteral.literalExpressionImpl = *literal(value)
|
||||
if len(constant) > 0 && constant[0] == true {
|
||||
stringLiteral.constant = true
|
||||
}
|
||||
|
||||
stringLiteral.stringInterfaceImpl.parent = &stringLiteral
|
||||
|
||||
return &stringLiteral
|
||||
}
|
||||
|
||||
func formatMilliseconds(milliseconds ...int) string {
|
||||
if len(milliseconds) > 0 {
|
||||
if milliseconds[0] < 1000 {
|
||||
return fmt.Sprintf(".%03d", milliseconds[0])
|
||||
} else {
|
||||
return "." + strconv.Itoa(milliseconds[0])
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Time creates new time literal expression
|
||||
func Time(hour, minute, second, milliseconds int) TimeExpression {
|
||||
timeStr := fmt.Sprintf("%02d:%02d:%02d.%03d", hour, minute, second, milliseconds)
|
||||
func Time(hour, minute, second int, milliseconds ...int) TimeExpression {
|
||||
timeStr := fmt.Sprintf("%02d:%02d:%02d", hour, minute, second)
|
||||
|
||||
timeStr += formatMilliseconds(milliseconds...)
|
||||
|
||||
return TimeExp(literal(timeStr))
|
||||
}
|
||||
|
|
@ -75,8 +159,10 @@ func Timez(hour, minute, second, milliseconds, timezone int) TimezExpression {
|
|||
}
|
||||
|
||||
// Timestamp creates new timestamp literal expression
|
||||
func Timestamp(year, month, day, hour, minute, second, milliseconds int) TimestampExpression {
|
||||
timeStr := fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d.%03d", year, month, day, hour, minute, second, milliseconds)
|
||||
func Timestamp(year int, month time.Month, day, hour, minute, second int, milliseconds ...int) TimestampExpression {
|
||||
timeStr := fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, minute, second)
|
||||
|
||||
timeStr += formatMilliseconds(milliseconds...)
|
||||
|
||||
return TimestampExp(literal(timeStr))
|
||||
}
|
||||
|
|
@ -90,7 +176,7 @@ func Timestampz(year, month, day, hour, minute, second, milliseconds, timezone i
|
|||
}
|
||||
|
||||
//Date creates new date expression
|
||||
func Date(year, month, day int) DateExpression {
|
||||
func Date(year int, month time.Month, day int) DateExpression {
|
||||
timeStr := fmt.Sprintf("%04d-%02d-%02d", year, month, day)
|
||||
|
||||
return DateExp(literal(timeStr))
|
||||
|
|
|
|||
|
|
@ -19,16 +19,17 @@ FROM db.table2;
|
|||
|
||||
func TestSelectLiterals(t *testing.T) {
|
||||
assertStatement(t, SELECT(Int(1), Float(2.2), Bool(false)).FROM(table1), `
|
||||
SELECT 1,
|
||||
2.2,
|
||||
FALSE
|
||||
SELECT $1,
|
||||
$2,
|
||||
$3
|
||||
FROM db.table1;
|
||||
`)
|
||||
`, int64(1), 2.2, false)
|
||||
}
|
||||
|
||||
func TestSelectDistinct(t *testing.T) {
|
||||
assertStatement(t, SELECT(table1ColBool).DISTINCT(), `
|
||||
SELECT DISTINCT table1.col_bool AS "table1.col_bool";
|
||||
assertStatement(t, SELECT(table1ColBool).DISTINCT().FROM(table1), `
|
||||
SELECT DISTINCT table1.col_bool AS "table1.col_bool"
|
||||
FROM db.table1;
|
||||
`)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
package jet
|
||||
|
||||
const (
|
||||
StringConcatOperator = "||"
|
||||
)
|
||||
|
||||
// StringExpression interface
|
||||
type StringExpression interface {
|
||||
Expression
|
||||
|
|
@ -18,8 +22,8 @@ type StringExpression interface {
|
|||
|
||||
LIKE(pattern StringExpression) BoolExpression
|
||||
NOT_LIKE(pattern StringExpression) BoolExpression
|
||||
SIMILAR_TO(pattern StringExpression) BoolExpression
|
||||
NOT_SIMILAR_TO(pattern StringExpression) BoolExpression
|
||||
|
||||
REGEXP_LIKE(pattern StringExpression, matchType ...string) BoolExpression
|
||||
}
|
||||
|
||||
type stringInterfaceImpl struct {
|
||||
|
|
@ -59,7 +63,7 @@ func (s *stringInterfaceImpl) LT_EQ(rhs StringExpression) BoolExpression {
|
|||
}
|
||||
|
||||
func (s *stringInterfaceImpl) CONCAT(rhs Expression) StringExpression {
|
||||
return newBinaryStringExpression(s.parent, rhs, "||")
|
||||
return newBinaryStringExpression(s.parent, rhs, StringConcatOperator)
|
||||
}
|
||||
|
||||
func (s *stringInterfaceImpl) LIKE(pattern StringExpression) BoolExpression {
|
||||
|
|
@ -70,12 +74,8 @@ func (s *stringInterfaceImpl) NOT_LIKE(pattern StringExpression) BoolExpression
|
|||
return newBinaryBoolOperator(s.parent, pattern, "NOT LIKE")
|
||||
}
|
||||
|
||||
func (s *stringInterfaceImpl) SIMILAR_TO(pattern StringExpression) BoolExpression {
|
||||
return newBinaryBoolOperator(s.parent, pattern, "SIMILAR TO")
|
||||
}
|
||||
|
||||
func (s *stringInterfaceImpl) NOT_SIMILAR_TO(pattern StringExpression) BoolExpression {
|
||||
return newBinaryBoolOperator(s.parent, pattern, "NOT SIMILAR TO")
|
||||
func (s *stringInterfaceImpl) REGEXP_LIKE(pattern StringExpression, matchType ...string) BoolExpression {
|
||||
return REGEXP_LIKE(s.parent, pattern, matchType...)
|
||||
}
|
||||
|
||||
//---------------------------------------------------//
|
||||
|
|
|
|||
|
|
@ -66,14 +66,9 @@ func TestStringNOT_LIKE(t *testing.T) {
|
|||
assertClauseSerialize(t, table3StrCol.NOT_LIKE(String("JOHN")), "(table3.col2 NOT LIKE $1)", "JOHN")
|
||||
}
|
||||
|
||||
func TestStringSIMILAR_TO(t *testing.T) {
|
||||
assertClauseSerialize(t, table3StrCol.SIMILAR_TO(table2ColStr), "(table3.col2 SIMILAR TO table2.col_str)")
|
||||
assertClauseSerialize(t, table3StrCol.SIMILAR_TO(String("JOHN")), "(table3.col2 SIMILAR TO $1)", "JOHN")
|
||||
}
|
||||
|
||||
func TestStringNOT_SIMILAR_TO(t *testing.T) {
|
||||
assertClauseSerialize(t, table3StrCol.NOT_SIMILAR_TO(table2ColStr), "(table3.col2 NOT SIMILAR TO table2.col_str)")
|
||||
assertClauseSerialize(t, table3StrCol.NOT_SIMILAR_TO(String("JOHN")), "(table3.col2 NOT SIMILAR TO $1)", "JOHN")
|
||||
func TestStringREGEXP_LIKE(t *testing.T) {
|
||||
assertClauseSerialize(t, table3StrCol.REGEXP_LIKE(table2ColStr), "REGEXP_LIKE(table3.col2, table2.col_str)")
|
||||
assertClauseSerialize(t, table3StrCol.REGEXP_LIKE(String("JOHN"), "c"), "REGEXP_LIKE(table3.col2, $1, 'c')", "JOHN")
|
||||
}
|
||||
|
||||
func TestStringExp(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -79,12 +79,8 @@ func assertClauseSerialize(t *testing.T, clause Clause, query string, args ...in
|
|||
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.DeepEqual(t, out.DebugSQL(), query)
|
||||
|
||||
if len(args) > 0 {
|
||||
assert.DeepEqual(t, out.Args, args)
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, out.Buff.String(), query)
|
||||
assert.DeepEqual(t, out.Args, args)
|
||||
}
|
||||
|
||||
func assertClauseSerializeErr(t *testing.T, clause Clause, errString string) {
|
||||
|
|
@ -102,22 +98,19 @@ func assertProjectionSerialize(t *testing.T, projection Projection, query string
|
|||
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.DeepEqual(t, out.DebugSQL(), query)
|
||||
|
||||
if len(args) > 0 {
|
||||
assert.DeepEqual(t, out.Args, args)
|
||||
}
|
||||
assert.DeepEqual(t, out.Buff.String(), query)
|
||||
assert.DeepEqual(t, out.Args, args)
|
||||
}
|
||||
|
||||
func assertStatement(t *testing.T, query Statement, expectedQuery string, expectedArgs ...interface{}) {
|
||||
queryStr, err := query.DebugSql()
|
||||
queryStr, args, err := query.Sql()
|
||||
assert.NilError(t, err)
|
||||
|
||||
fmt.Println(queryStr)
|
||||
|
||||
//fmt.Println(queryStr)
|
||||
assert.Equal(t, queryStr, expectedQuery)
|
||||
//assert.DeepEqual(t, args, expectedArgs)
|
||||
assert.DeepEqual(t, args, expectedArgs)
|
||||
}
|
||||
|
||||
func assertStatementErr(t *testing.T, stmt Statement, errorStr string) {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ func (u *updateStatementImpl) Sql(dialect ...Dialect) (query string, args []inte
|
|||
out.newLine()
|
||||
out.WriteString("SET")
|
||||
|
||||
if err = out.Dialect.UpdateAssigment()(u.columns, u.values, out); err != nil {
|
||||
if err = out.Dialect.SetClause()(u.columns, u.values, out); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ WHERE table1.col_int >= $2;
|
|||
func TestUpdateWithValues(t *testing.T) {
|
||||
expectedSQL := `
|
||||
UPDATE db.table1
|
||||
SET (col_int, col_float) = ($1, $2)
|
||||
SET col_int = $1, col_float = $2
|
||||
WHERE table1.col_int >= $3;
|
||||
`
|
||||
stmt := table1.UPDATE(table1ColInt, table1ColFloat).
|
||||
|
|
@ -51,26 +51,7 @@ RETURNING table1.col1 AS "table1.col1";
|
|||
assertStatement(t, stmt, expectedSQL, int64(2))
|
||||
}
|
||||
|
||||
func TestUpdateColumnsWithSelect(t *testing.T) {
|
||||
expectedSQL := `
|
||||
UPDATE db.table1
|
||||
SET (col1, col_float) = (
|
||||
SELECT table1.col_float AS "table1.col_float",
|
||||
table2.col3 AS "table2.col3"
|
||||
FROM db.table1
|
||||
)
|
||||
WHERE table1.col1 = $1
|
||||
RETURNING table1.col1 AS "table1.col1";
|
||||
`
|
||||
stmt := table1.UPDATE(table1Col1, table1ColFloat).
|
||||
SET(table1.SELECT(table1ColFloat, table2Col3)).
|
||||
WHERE(table1Col1.EQ(Int(2))).
|
||||
RETURNING(table1Col1)
|
||||
|
||||
assertStatement(t, stmt, expectedSQL, int64(2))
|
||||
}
|
||||
|
||||
func TestInvalidInputs(t *testing.T) {
|
||||
assertStatementErr(t, table1.UPDATE(table1ColInt).SET(1, 2), "jet: WHERE clause not set")
|
||||
assertStatementErr(t, table1.UPDATE(nil).SET(1, 2), "jet: nil column in columns list")
|
||||
assertStatementErr(t, table1.UPDATE(table1ColInt).SET(1), "jet: WHERE clause not set")
|
||||
assertStatementErr(t, table1.UPDATE(nil).SET(1), "jet: nil column in columns list")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,11 +84,14 @@ func AssertDebugStatementSql(t *testing.T, query jet.Statement, expectedQuery st
|
|||
_, args, err := query.Sql()
|
||||
assert.NilError(t, err)
|
||||
//assert.Equal(t, queryStr, expectedQuery)
|
||||
assert.DeepEqual(t, args, expectedArgs)
|
||||
if len(expectedArgs) > 0 {
|
||||
assert.DeepEqual(t, args, expectedArgs)
|
||||
}
|
||||
|
||||
debuqSql, err := query.DebugSql()
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, debuqSql, expectedQuery)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue