Merge pull request #180 from go-jet/develop

Release v.2.9.0
This commit is contained in:
go-jet 2022-09-30 13:53:33 +02:00 committed by GitHub
commit 24857bc067
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 692 additions and 183 deletions

View file

@ -2,10 +2,9 @@
[![CircleCI](https://circleci.com/gh/go-jet/jet/tree/master.svg?style=svg&circle-token=97f255c6a4a3ab6590ea2e9195eb3ebf9f97b4a7)](https://circleci.com/gh/go-jet/jet/tree/develop) [![CircleCI](https://circleci.com/gh/go-jet/jet/tree/master.svg?style=svg&circle-token=97f255c6a4a3ab6590ea2e9195eb3ebf9f97b4a7)](https://circleci.com/gh/go-jet/jet/tree/develop)
[![codecov](https://codecov.io/gh/go-jet/jet/branch/master/graph/badge.svg)](https://codecov.io/gh/go-jet/jet) [![codecov](https://codecov.io/gh/go-jet/jet/branch/master/graph/badge.svg)](https://codecov.io/gh/go-jet/jet)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-jet/jet)](https://goreportcard.com/report/github.com/go-jet/jet) [![Go Report Card](https://goreportcard.com/badge/github.com/go-jet/jet)](https://goreportcard.com/report/github.com/go-jet/jet/v2)
[![Documentation](https://godoc.org/github.com/go-jet/jet?status.svg)](http://godoc.org/github.com/go-jet/jet) [![Documentation](https://godoc.org/github.com/go-jet/jet?status.svg)](http://godoc.org/github.com/go-jet/jet/v2)
[![GitHub release](https://img.shields.io/github/release/go-jet/jet.svg)](https://github.com/go-jet/jet/v2/releases) [![GitHub release](https://img.shields.io/github/release/go-jet/jet.svg)](https://github.com/go-jet/jet/releases)
[![Gitter](https://badges.gitter.im/go-jet/community.svg)](https://gitter.im/go-jet/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
Jet is a complete solution for efficient and high performance database access, consisting of type-safe SQL builder 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. with code generation and automatic query result data mapping.

30
doc.go
View file

@ -3,44 +3,45 @@ Package jet is a complete solution for efficient and high performance database a
with code generation and automatic query result data mapping. with code generation and automatic query result data mapping.
Jet currently supports PostgreSQL, MySQL, MariaDB and SQLite. Future releases will add support for additional databases. Jet currently supports PostgreSQL, MySQL, MariaDB and SQLite. Future releases will add support for additional databases.
# Installation
Installation
Use the command bellow to add jet as a dependency into go.mod project: Use the command bellow to add jet as a dependency into go.mod project:
$ go get -u github.com/go-jet/jet/v2 $ go get -u github.com/go-jet/jet/v2
Jet generator can be installed in one of the following ways: Jet generator can be installed in one of the following ways:
1) (Go1.16+) Install jet generator using go install: 1. (Go1.16+) Install jet generator using go install:
go install github.com/go-jet/jet/v2/cmd/jet@latest go install github.com/go-jet/jet/v2/cmd/jet@latest
2) Install jet generator to GOPATH/bin folder: 2. Install jet generator to GOPATH/bin folder:
cd $GOPATH/src/ && GO111MODULE=off go get -u github.com/go-jet/jet/cmd/jet cd $GOPATH/src/ && GO111MODULE=off go get -u github.com/go-jet/jet/cmd/jet
3) Install jet generator into specific folder: 3. Install jet generator into specific folder:
git clone https://github.com/go-jet/jet.git git clone https://github.com/go-jet/jet.git
cd jet && go build -o dir_path ./cmd/jet cd jet && go build -o dir_path ./cmd/jet
Make sure that the destination folder is added to the PATH environment variable. Make sure that the destination folder is added to the PATH environment variable.
# Usage
Usage
Jet requires already defined database schema(with tables, enums etc), so that jet generator can generate SQL Builder Jet requires already defined database schema(with tables, enums etc), so that jet generator can generate SQL Builder
and Model files. File generation is very fast, and can be added as every pre-build step. and Model files. File generation is very fast, and can be added as every pre-build step.
Sample command: Sample command:
jet -dsn=postgresql://user:pass@localhost:5432/jetdb -schema=dvds -path=./.gen jet -dsn=postgresql://user:pass@localhost:5432/jetdb -schema=dvds -path=./.gen
Before we can write SQL queries in Go, we need to import generated SQL builder and model types: Before we can write SQL queries in Go, we need to import generated SQL builder and model types:
import . "some_path/.gen/jetdb/dvds/table" import . "some_path/.gen/jetdb/dvds/table"
import "some_path/.gen/jetdb/dvds/model" import "some_path/.gen/jetdb/dvds/model"
To write postgres SQL queries we import: To write postgres SQL queries we import:
. "github.com/go-jet/jet/v2/postgres" // Dot import is used so that Go code resemble as much as native SQL. It is not mandatory. . "github.com/go-jet/jet/v2/postgres" // Dot import is used so that Go code resemble as much as native SQL. It is not mandatory.
Then we can write the SQL query: Then we can write the SQL query:
// sub-query // sub-query
rRatingFilms := rRatingFilms :=
SELECT( SELECT(
@ -72,6 +73,7 @@ Then we can write the SQL query:
) )
Now we can run the statement and store the result into desired destination: Now we can run the statement and store the result into desired destination:
var dest []struct { var dest []struct {
model.Film model.Film
@ -81,9 +83,11 @@ Now we can run the statement and store the result into desired destination:
err := stmt.Query(db, &dest) err := stmt.Query(db, &dest)
We can print a statement to see SQL query and arguments sent to postgres server: We can print a statement to see SQL query and arguments sent to postgres server:
fmt.Println(stmt.Sql()) fmt.Println(stmt.Sql())
Output: Output:
SELECT "rFilms"."film.film_id" AS "film.film_id", SELECT "rFilms"."film.film_id" AS "film.film_id",
"rFilms"."film.title" AS "film.title", "rFilms"."film.title" AS "film.title",
"rFilms"."film.rating" AS "film.rating", "rFilms"."film.rating" AS "film.rating",

View file

@ -26,80 +26,6 @@ import (
var {{tableTemplate.InstanceName}} = new{{tableTemplate.TypeName}}("{{schemaName}}", "{{.Name}}", "") var {{tableTemplate.InstanceName}} = new{{tableTemplate.TypeName}}("{{schemaName}}", "{{.Name}}", "")
type {{tableTemplate.TypeName}} struct {
{{dialect.PackageName}}.Table
//Columns
{{- range $i, $c := .Columns}}
{{- $field := columnField $c}}
{{$field.Name}} {{dialect.PackageName}}.Column{{$field.Type}}
{{- end}}
AllColumns {{dialect.PackageName}}.ColumnList
MutableColumns {{dialect.PackageName}}.ColumnList
}
// AS creates new {{tableTemplate.TypeName}} with assigned alias
func (a {{tableTemplate.TypeName}}) AS(alias string) {{tableTemplate.TypeName}} {
return new{{tableTemplate.TypeName}}(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new {{tableTemplate.TypeName}} with assigned schema name
func (a {{tableTemplate.TypeName}}) FromSchema(schemaName string) {{tableTemplate.TypeName}} {
return new{{tableTemplate.TypeName}}(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new {{tableTemplate.TypeName}} with assigned table prefix
func (a {{tableTemplate.TypeName}}) WithPrefix(prefix string) {{tableTemplate.TypeName}} {
return new{{tableTemplate.TypeName}}(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new {{tableTemplate.TypeName}} with assigned table suffix
func (a {{tableTemplate.TypeName}}) WithSuffix(suffix string) {{tableTemplate.TypeName}} {
return new{{tableTemplate.TypeName}}(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func new{{tableTemplate.TypeName}}(schemaName, tableName, alias string) {{tableTemplate.TypeName}} {
var (
{{- range $i, $c := .Columns}}
{{- $field := columnField $c}}
{{$field.Name}}Column = {{dialect.PackageName}}.{{$field.Type}}Column("{{$c.Name}}")
{{- end}}
allColumns = {{dialect.PackageName}}.ColumnList{ {{template "column-list" .Columns}} }
mutableColumns = {{dialect.PackageName}}.ColumnList{ {{template "column-list" .MutableColumns}} }
)
return {{tableTemplate.TypeName}}{
Table: {{dialect.PackageName}}.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
{{- range $i, $c := .Columns}}
{{- $field := columnField $c}}
{{$field.Name}}: {{$field.Name}}Column,
{{- end}}
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}
`
var tableSQLBuilderTemplateWithEXCLUDED = `
{{define "column-list" -}}
{{- range $i, $c := . }}
{{- $field := columnField $c}}
{{- if gt $i 0 }}, {{end}}{{$field.Name}}Column
{{- end}}
{{- end}}
package {{package}}
import (
"github.com/go-jet/jet/v2/{{dialect.PackageName}}"
)
var {{tableTemplate.InstanceName}} = new{{tableTemplate.TypeName}}("{{schemaName}}", "{{.Name}}", "")
type {{structImplName}} struct { type {{structImplName}} struct {
{{dialect.PackageName}}.Table {{dialect.PackageName}}.Table
@ -116,7 +42,7 @@ type {{structImplName}} struct {
type {{tableTemplate.TypeName}} struct { type {{tableTemplate.TypeName}} struct {
{{structImplName}} {{structImplName}}
EXCLUDED {{structImplName}} {{toUpper insertedRowAlias}} {{structImplName}}
} }
// AS creates new {{tableTemplate.TypeName}} with assigned alias // AS creates new {{tableTemplate.TypeName}} with assigned alias
@ -142,7 +68,7 @@ func (a {{tableTemplate.TypeName}}) WithSuffix(suffix string) *{{tableTemplate.T
func new{{tableTemplate.TypeName}}(schemaName, tableName, alias string) *{{tableTemplate.TypeName}} { func new{{tableTemplate.TypeName}}(schemaName, tableName, alias string) *{{tableTemplate.TypeName}} {
return &{{tableTemplate.TypeName}}{ return &{{tableTemplate.TypeName}}{
{{structImplName}}: new{{tableTemplate.TypeName}}Impl(schemaName, tableName, alias), {{structImplName}}: new{{tableTemplate.TypeName}}Impl(schemaName, tableName, alias),
EXCLUDED: new{{tableTemplate.TypeName}}Impl("", "excluded", ""), {{toUpper insertedRowAlias}}: new{{tableTemplate.TypeName}}Impl("", "{{insertedRowAlias}}", ""),
} }
} }

View file

@ -120,29 +120,29 @@ func processTableSQLBuilder(fileTypes, dirPath string,
for _, tableMetaData := range tablesMetaData { for _, tableMetaData := range tablesMetaData {
var tableSQLBuilderTemplate TableSQLBuilder var tableSQLBuilder TableSQLBuilder
if fileTypes == "view" { if fileTypes == "view" {
tableSQLBuilderTemplate = sqlBuilderTemplate.View(tableMetaData) tableSQLBuilder = sqlBuilderTemplate.View(tableMetaData)
} else { } else {
tableSQLBuilderTemplate = sqlBuilderTemplate.Table(tableMetaData) tableSQLBuilder = sqlBuilderTemplate.Table(tableMetaData)
} }
if tableSQLBuilderTemplate.Skip { if tableSQLBuilder.Skip {
continue continue
} }
tableSQLBuilderPath := path.Join(dirPath, tableSQLBuilderTemplate.Path) tableSQLBuilderPath := path.Join(dirPath, tableSQLBuilder.Path)
err := utils.EnsureDirPath(tableSQLBuilderPath) err := utils.EnsureDirPath(tableSQLBuilderPath)
throw.OnError(err) throw.OnError(err)
text, err := generateTemplate( text, err := generateTemplate(
autoGenWarningTemplate+getTableSQLBuilderTemplate(dialect), autoGenWarningTemplate+tableSQLBuilderTemplate,
tableMetaData, tableMetaData,
template.FuncMap{ template.FuncMap{
"package": func() string { "package": func() string {
return tableSQLBuilderTemplate.PackageName() return tableSQLBuilder.PackageName()
}, },
"dialect": func() jet.Dialect { "dialect": func() jet.Dialect {
return dialect return dialect
@ -151,29 +151,33 @@ func processTableSQLBuilder(fileTypes, dirPath string,
return schemaMetaData.Name return schemaMetaData.Name
}, },
"tableTemplate": func() TableSQLBuilder { "tableTemplate": func() TableSQLBuilder {
return tableSQLBuilderTemplate return tableSQLBuilder
}, },
"structImplName": func() string { // postgres only "structImplName": func() string { // postgres only
structName := tableSQLBuilderTemplate.TypeName structName := tableSQLBuilder.TypeName
return string(strings.ToLower(structName)[0]) + structName[1:] return string(strings.ToLower(structName)[0]) + structName[1:]
}, },
"columnField": func(columnMetaData metadata.Column) TableSQLBuilderColumn { "columnField": func(columnMetaData metadata.Column) TableSQLBuilderColumn {
return tableSQLBuilderTemplate.Column(columnMetaData) return tableSQLBuilder.Column(columnMetaData)
},
"toUpper": strings.ToUpper,
"insertedRowAlias": func() string {
return insertedRowAlias(dialect)
}, },
}) })
throw.OnError(err) throw.OnError(err)
err = utils.SaveGoFile(tableSQLBuilderPath, tableSQLBuilderTemplate.FileName, text) err = utils.SaveGoFile(tableSQLBuilderPath, tableSQLBuilder.FileName, text)
throw.OnError(err) throw.OnError(err)
} }
} }
func getTableSQLBuilderTemplate(dialect jet.Dialect) string { func insertedRowAlias(dialect jet.Dialect) string {
if dialect.Name() == "PostgreSQL" || dialect.Name() == "SQLite" { if dialect.Name() == "MySQL" {
return tableSQLBuilderTemplateWithEXCLUDED return "new"
} }
return tableSQLBuilderTemplate return "excluded"
} }
func processTableModels(fileTypes, modelDirPath string, tablesMetaData []metadata.Table, modelTemplate Model) { func processTableModels(fileTypes, modelDirPath string, tablesMetaData []metadata.Table, modelTemplate Model) {

View file

@ -1,6 +1,6 @@
package jet package jet
//BoolExpression interface // BoolExpression interface
type BoolExpression interface { type BoolExpression interface {
Expression Expression
@ -84,22 +84,18 @@ func (b *boolInterfaceImpl) IS_NOT_UNKNOWN() BoolExpression {
return newPostfixBoolOperatorExpression(b.parent, "IS NOT UNKNOWN") return newPostfixBoolOperatorExpression(b.parent, "IS NOT UNKNOWN")
} }
//---------------------------------------------------//
func newBinaryBoolOperatorExpression(lhs, rhs Expression, operator string, additionalParams ...Expression) BoolExpression { func newBinaryBoolOperatorExpression(lhs, rhs Expression, operator string, additionalParams ...Expression) BoolExpression {
return BoolExp(NewBinaryOperatorExpression(lhs, rhs, operator, additionalParams...)) return BoolExp(NewBinaryOperatorExpression(lhs, rhs, operator, additionalParams...))
} }
//---------------------------------------------------//
func newPrefixBoolOperatorExpression(expression Expression, operator string) BoolExpression { func newPrefixBoolOperatorExpression(expression Expression, operator string) BoolExpression {
return BoolExp(newPrefixOperatorExpression(expression, operator)) return BoolExp(newPrefixOperatorExpression(expression, operator))
} }
//---------------------------------------------------//
func newPostfixBoolOperatorExpression(expression Expression, operator string) BoolExpression { func newPostfixBoolOperatorExpression(expression Expression, operator string) BoolExpression {
return BoolExp(newPostfixOperatorExpression(expression, operator)) return BoolExp(newPostfixOperatorExpression(expression, operator))
} }
//---------------------------------------------------//
type boolExpressionWrapper struct { type boolExpressionWrapper struct {
boolInterfaceImpl boolInterfaceImpl
Expression Expression

View file

@ -16,11 +16,35 @@ type ClauseWithProjections interface {
Projections() ProjectionList Projections() ProjectionList
} }
// OptimizerHint provides a way to optimize query execution per-statement basis
type OptimizerHint string
type optimizerHints []OptimizerHint
func (o optimizerHints) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
if len(o) == 0 {
return
}
out.WriteString("/*+")
for i, hint := range o {
if i > 0 {
out.WriteByte(' ')
}
out.WriteString(string(hint))
}
out.WriteString("*/")
}
// ClauseSelect struct // ClauseSelect struct
type ClauseSelect struct { type ClauseSelect struct {
Distinct bool Distinct bool
DistinctOnColumns []ColumnExpression DistinctOnColumns []ColumnExpression
ProjectionList []Projection ProjectionList []Projection
// MySQL only
OptimizerHints optimizerHints
} }
// Projections returns list of projections for select clause // Projections returns list of projections for select clause
@ -32,6 +56,7 @@ func (s *ClauseSelect) Projections() ProjectionList {
func (s *ClauseSelect) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) { func (s *ClauseSelect) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
out.NewLine() out.NewLine()
out.WriteString("SELECT") out.WriteString("SELECT")
s.OptimizerHints.Serialize(statementType, out, options...)
if s.Distinct { if s.Distinct {
out.WriteString("DISTINCT") out.WriteString("DISTINCT")
@ -286,12 +311,16 @@ func (s *ClauseSetStmtOperator) Serialize(statementType StatementType, out *SQLB
// ClauseUpdate struct // ClauseUpdate struct
type ClauseUpdate struct { type ClauseUpdate struct {
Table SerializerTable Table SerializerTable
// MySQL only
OptimizerHints optimizerHints
} }
// Serialize serializes clause into SQLBuilder // Serialize serializes clause into SQLBuilder
func (u *ClauseUpdate) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) { func (u *ClauseUpdate) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
out.NewLine() out.NewLine()
out.WriteString("UPDATE") out.WriteString("UPDATE")
u.OptimizerHints.Serialize(statementType, out, options...)
if utils.IsNil(u.Table) { if utils.IsNil(u.Table) {
panic("jet: table to update is nil") panic("jet: table to update is nil")
@ -342,6 +371,9 @@ func (s *SetClause) Serialize(statementType StatementType, out *SQLBuilder, opti
type ClauseInsert struct { type ClauseInsert struct {
Table SerializerTable Table SerializerTable
Columns []Column Columns []Column
// MySQL only
OptimizerHints optimizerHints
} }
// GetColumns gets list of columns for insert // GetColumns gets list of columns for insert
@ -355,13 +387,15 @@ func (i *ClauseInsert) GetColumns() []Column {
// Serialize serializes clause into SQLBuilder // Serialize serializes clause into SQLBuilder
func (i *ClauseInsert) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) { func (i *ClauseInsert) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
out.NewLine()
out.WriteString("INSERT INTO")
if utils.IsNil(i.Table) { if utils.IsNil(i.Table) {
panic("jet: table is nil for INSERT clause") panic("jet: table is nil for INSERT clause")
} }
out.NewLine()
out.WriteString("INSERT")
i.OptimizerHints.Serialize(statementType, out, options...)
out.WriteString("INTO")
i.Table.serialize(statementType, out) i.Table.serialize(statementType, out)
if len(i.Columns) > 0 { if len(i.Columns) > 0 {
@ -392,6 +426,7 @@ func (v *ClauseValuesQuery) Serialize(statementType StatementType, out *SQLBuild
// ClauseValues struct // ClauseValues struct
type ClauseValues struct { type ClauseValues struct {
Rows [][]Serializer Rows [][]Serializer
As string
} }
// Serialize serializes clause into SQLBuilder // Serialize serializes clause into SQLBuilder
@ -417,6 +452,12 @@ func (v *ClauseValues) Serialize(statementType StatementType, out *SQLBuilder, o
out.WriteByte(')') out.WriteByte(')')
} }
if len(v.As) > 0 {
out.WriteString("AS")
out.WriteIdentifier(v.As)
}
out.DecreaseIdent(7) out.DecreaseIdent(7)
} }
@ -442,17 +483,17 @@ func (v *ClauseQuery) Serialize(statementType StatementType, out *SQLBuilder, op
// ClauseDelete struct // ClauseDelete struct
type ClauseDelete struct { type ClauseDelete struct {
Table SerializerTable Table SerializerTable
// MySQL only
OptimizerHints optimizerHints
} }
// Serialize serializes clause into SQLBuilder // Serialize serializes clause into SQLBuilder
func (d *ClauseDelete) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) { func (d *ClauseDelete) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
out.NewLine() out.NewLine()
out.WriteString("DELETE FROM") out.WriteString("DELETE")
d.OptimizerHints.Serialize(statementType, out, options...)
if d.Table == nil { out.WriteString("FROM")
panic("jet: nil table in DELETE clause")
}
d.Table.serialize(statementType, out, FallTrough(options)...) d.Table.serialize(statementType, out, FallTrough(options)...)
} }

View file

@ -4,10 +4,10 @@ package jet
type ColumnList []ColumnExpression type ColumnList []ColumnExpression
// SET creates column assigment for each column in column list. expression should be created by ROW function // SET creates column assigment for each column in column list. expression should be created by ROW function
// Link.UPDATE().
// SET(Link.MutableColumns.SET(ROW(String("github.com"), Bool(false))).
// WHERE(Link.ID.EQ(Int(0)))
// //
// Link.UPDATE().
// SET(Link.MutableColumns.SET(ROW(String("github.com"), Bool(false))).
// WHERE(Link.ID.EQ(Int(0)))
func (cl ColumnList) SET(expression Expression) ColumnAssigment { func (cl ColumnList) SET(expression Expression) ColumnAssigment {
return columnAssigmentImpl{ return columnAssigmentImpl{
column: cl, column: cl,
@ -16,8 +16,8 @@ func (cl ColumnList) SET(expression Expression) ColumnAssigment {
} }
// Except will create new column list in which columns contained in list of excluded column names are removed // Except will create new column list in which columns contained in list of excluded column names are removed
// Address.AllColumns.Except(Address.PostalCode, Address.Phone)
// //
// Address.AllColumns.Except(Address.PostalCode, Address.Phone)
func (cl ColumnList) Except(excludedColumns ...Column) ColumnList { func (cl ColumnList) Except(excludedColumns ...Column) ColumnList {
excludedColumnList := UnwidColumnList(excludedColumns) excludedColumnList := UnwidColumnList(excludedColumns)
excludedColumnNames := map[string]bool{} excludedColumnNames := map[string]bool{}

View file

@ -208,6 +208,7 @@ type ColumnTimez interface {
Column Column
From(subQuery SelectTable) ColumnTimez From(subQuery SelectTable) ColumnTimez
SET(timeExp TimezExpression) ColumnAssigment
} }
type timezColumnImpl struct { type timezColumnImpl struct {

View file

@ -1,6 +1,6 @@
package jet package jet
//FloatExpression is interface for SQL float columns // FloatExpression is interface for SQL float columns
type FloatExpression interface { type FloatExpression interface {
Expression Expression
numericExpression numericExpression

View file

@ -120,17 +120,14 @@ func (i *integerInterfaceImpl) BIT_SHIFT_RIGHT(intExpression IntegerExpression)
return newBinaryIntegerOperatorExpression(i.parent, intExpression, ">>") return newBinaryIntegerOperatorExpression(i.parent, intExpression, ">>")
} }
//---------------------------------------------------//
func newBinaryIntegerOperatorExpression(lhs, rhs IntegerExpression, operator string) IntegerExpression { func newBinaryIntegerOperatorExpression(lhs, rhs IntegerExpression, operator string) IntegerExpression {
return IntExp(NewBinaryOperatorExpression(lhs, rhs, operator)) return IntExp(NewBinaryOperatorExpression(lhs, rhs, operator))
} }
//---------------------------------------------------//
func newPrefixIntegerOperatorExpression(expression IntegerExpression, operator string) IntegerExpression { func newPrefixIntegerOperatorExpression(expression IntegerExpression, operator string) IntegerExpression {
return IntExp(newPrefixOperatorExpression(expression, operator)) return IntExp(newPrefixOperatorExpression(expression, operator))
} }
//---------------------------------------------------//
type integerExpressionWrapper struct { type integerExpressionWrapper struct {
integerInterfaceImpl integerInterfaceImpl

View file

@ -118,7 +118,7 @@ func Uint64(value uint64) IntegerExpression {
return intLiteral(value) return intLiteral(value)
} }
//---------------------------------------------------// // ---------------------------------------------------//
type boolLiteralExpression struct { type boolLiteralExpression struct {
boolInterfaceImpl boolInterfaceImpl
literalExpressionImpl literalExpressionImpl
@ -134,7 +134,7 @@ func Bool(value bool) BoolExpression {
return &boolLiteralExpression return &boolLiteralExpression
} }
//---------------------------------------------------// // ---------------------------------------------------//
type floatLiteral struct { type floatLiteral struct {
floatInterfaceImpl floatInterfaceImpl
literalExpressionImpl literalExpressionImpl
@ -160,7 +160,7 @@ func Decimal(value string) FloatExpression {
return &floatLiteral return &floatLiteral
} }
//---------------------------------------------------// // ---------------------------------------------------//
type stringLiteral struct { type stringLiteral struct {
stringInterfaceImpl stringInterfaceImpl
literalExpressionImpl literalExpressionImpl
@ -351,7 +351,7 @@ func (n *nullLiteral) serialize(statement StatementType, out *SQLBuilder, option
out.WriteString("NULL") out.WriteString("NULL")
} }
//--------------------------------------------------// // --------------------------------------------------//
type starLiteral struct { type starLiteral struct {
ExpressionInterfaceImpl ExpressionInterfaceImpl
} }

View file

@ -7,7 +7,7 @@ import (
"time" "time"
) )
//Statement is common interface for all statements(SELECT, INSERT, UPDATE, DELETE, LOCK) // Statement is common interface for all statements(SELECT, INSERT, UPDATE, DELETE, LOCK)
type Statement interface { type Statement interface {
// Sql returns parametrized sql query with list of arguments. // Sql returns parametrized sql query with list of arguments.
Sql() (query string, args []interface{}) Sql() (query string, args []interface{})

View file

@ -89,7 +89,7 @@ func (s *stringInterfaceImpl) NOT_REGEXP_LIKE(pattern StringExpression, caseSens
return newBinaryBoolOperatorExpression(s.parent, pattern, StringNotRegexpLikeOperator, Bool(len(caseSensitive) > 0 && caseSensitive[0])) return newBinaryBoolOperatorExpression(s.parent, pattern, StringNotRegexpLikeOperator, Bool(len(caseSensitive) > 0 && caseSensitive[0]))
} }
//---------------------------------------------------// // ---------------------------------------------------//
func newBinaryStringOperatorExpression(lhs, rhs Expression, operator string) StringExpression { func newBinaryStringOperatorExpression(lhs, rhs Expression, operator string) StringExpression {
return StringExp(NewBinaryOperatorExpression(lhs, rhs, operator)) return StringExp(NewBinaryOperatorExpression(lhs, rhs, operator))
} }

View file

@ -68,7 +68,7 @@ func (c *castImpl) AS_CHAR(length ...int) StringExpression {
return StringExp(c.AS("CHAR")) return StringExp(c.AS("CHAR"))
} }
// AS_DATE casts expression AS DATE type // AS_DATE casts expression AS DATE type
func (c *castImpl) AS_DATE() DateExpression { func (c *castImpl) AS_DATE() DateExpression {
return DateExp(c.AS("DATE")) return DateExp(c.AS("DATE"))
} }

View file

@ -6,6 +6,8 @@ import "github.com/go-jet/jet/v2/internal/jet"
type DeleteStatement interface { type DeleteStatement interface {
Statement Statement
OPTIMIZER_HINTS(hints ...OptimizerHint) DeleteStatement
USING(tables ...ReadableTable) DeleteStatement USING(tables ...ReadableTable) DeleteStatement
WHERE(expression BoolExpression) DeleteStatement WHERE(expression BoolExpression) DeleteStatement
ORDER_BY(orderByClauses ...OrderByClause) DeleteStatement ORDER_BY(orderByClauses ...OrderByClause) DeleteStatement
@ -15,7 +17,7 @@ type DeleteStatement interface {
type deleteStatementImpl struct { type deleteStatementImpl struct {
jet.SerializerStatement jet.SerializerStatement
Delete jet.ClauseStatementBegin Delete jet.ClauseDelete
Using jet.ClauseFrom Using jet.ClauseFrom
Where jet.ClauseWhere Where jet.ClauseWhere
OrderBy jet.ClauseOrderBy OrderBy jet.ClauseOrderBy
@ -29,17 +31,22 @@ func newDeleteStatement(table Table) DeleteStatement {
&newDelete.Using, &newDelete.Using,
&newDelete.Where, &newDelete.Where,
&newDelete.OrderBy, &newDelete.OrderBy,
&newDelete.Limit) &newDelete.Limit,
)
newDelete.Delete.Name = "DELETE FROM" newDelete.Delete.Table = table
newDelete.Using.Name = "USING" newDelete.Using.Name = "USING"
newDelete.Delete.Tables = append(newDelete.Delete.Tables, table)
newDelete.Where.Mandatory = true newDelete.Where.Mandatory = true
newDelete.Limit.Count = -1 newDelete.Limit.Count = -1
return newDelete return newDelete
} }
func (d *deleteStatementImpl) OPTIMIZER_HINTS(hints ...OptimizerHint) DeleteStatement {
d.Delete.OptimizerHints = hints
return d
}
func (d *deleteStatementImpl) USING(tables ...ReadableTable) DeleteStatement { func (d *deleteStatementImpl) USING(tables ...ReadableTable) DeleteStatement {
d.Using.Tables = readableTablesToSerializerList(tables) d.Using.Tables = readableTablesToSerializerList(tables)
return d return d

View file

@ -225,7 +225,8 @@ var REGEXP_LIKE = jet.REGEXP_LIKE
//----------------- Date/Time Functions and Operators ------------// //----------------- Date/Time Functions and Operators ------------//
// EXTRACT function retrieves subfields such as year or hour from date/time values // EXTRACT function retrieves subfields such as year or hour from date/time values
// EXTRACT(DAY, User.CreatedAt) //
// EXTRACT(DAY, User.CreatedAt)
func EXTRACT(field unitType, from Expression) IntegerExpression { func EXTRACT(field unitType, from Expression) IntegerExpression {
return IntExp(jet.EXTRACT(string(field), from)) return IntExp(jet.EXTRACT(string(field), from))
} }
@ -261,10 +262,22 @@ func UNIX_TIMESTAMP(str StringExpression) TimestampExpression {
return jet.NewTimestampFunc("UNIX_TIMESTAMP", str) return jet.NewTimestampFunc("UNIX_TIMESTAMP", str)
} }
//----------- Comparison operators ---------------// // --------------- Conditional Expressions Functions -------------//
// EXISTS checks for existence of the rows in subQuery // EXISTS checks for existence of the rows in subQuery
var EXISTS = jet.EXISTS var EXISTS = jet.EXISTS
// CASE create CASE operator with optional list of expressions // CASE create CASE operator with optional list of expressions
var CASE = jet.CASE var CASE = jet.CASE
// COALESCE function returns the first of its arguments that is not null.
var COALESCE = jet.COALESCE
// NULLIF function returns a null value if value1 equals value2; otherwise it returns value1.
var NULLIF = jet.NULLIF
// GREATEST selects the largest value from a list of expressions, or null if any of the expressions is null.
var GREATEST = jet.GREATEST
// LEAST selects the smallest value from a list of expressions, or null if any of the expressions is null.
var LEAST = jet.LEAST

View file

@ -6,12 +6,15 @@ import "github.com/go-jet/jet/v2/internal/jet"
type InsertStatement interface { type InsertStatement interface {
Statement Statement
OPTIMIZER_HINTS(hints ...OptimizerHint) InsertStatement
// Insert row of values // Insert row of values
VALUES(value interface{}, values ...interface{}) InsertStatement VALUES(value interface{}, values ...interface{}) InsertStatement
// Insert row of values, where value for each column is extracted from filed of structure data. // Insert row of values, where value for each column is extracted from filed of structure data.
// If data is not struct or there is no field for every column selected, this method will panic. // If data is not struct or there is no field for every column selected, this method will panic.
MODEL(data interface{}) InsertStatement MODEL(data interface{}) InsertStatement
MODELS(data interface{}) InsertStatement MODELS(data interface{}) InsertStatement
AS_NEW() InsertStatement
ON_DUPLICATE_KEY_UPDATE(assigments ...ColumnAssigment) InsertStatement ON_DUPLICATE_KEY_UPDATE(assigments ...ColumnAssigment) InsertStatement
@ -21,7 +24,10 @@ type InsertStatement interface {
func newInsertStatement(table Table, columns []jet.Column) InsertStatement { func newInsertStatement(table Table, columns []jet.Column) InsertStatement {
newInsert := &insertStatementImpl{} newInsert := &insertStatementImpl{}
newInsert.SerializerStatement = jet.NewStatementImpl(Dialect, jet.InsertStatementType, newInsert, newInsert.SerializerStatement = jet.NewStatementImpl(Dialect, jet.InsertStatementType, newInsert,
&newInsert.Insert, &newInsert.ValuesQuery, &newInsert.OnDuplicateKey) &newInsert.Insert,
&newInsert.ValuesQuery,
&newInsert.OnDuplicateKey,
)
newInsert.Insert.Table = table newInsert.Insert.Table = table
newInsert.Insert.Columns = columns newInsert.Insert.Columns = columns
@ -37,6 +43,11 @@ type insertStatementImpl struct {
OnDuplicateKey onDuplicateKeyUpdateClause OnDuplicateKey onDuplicateKeyUpdateClause
} }
func (is *insertStatementImpl) OPTIMIZER_HINTS(hints ...OptimizerHint) InsertStatement {
is.Insert.OptimizerHints = hints
return is
}
func (is *insertStatementImpl) VALUES(value interface{}, values ...interface{}) InsertStatement { func (is *insertStatementImpl) VALUES(value interface{}, values ...interface{}) InsertStatement {
is.ValuesQuery.Rows = append(is.ValuesQuery.Rows, jet.UnwindRowFromValues(value, values)) is.ValuesQuery.Rows = append(is.ValuesQuery.Rows, jet.UnwindRowFromValues(value, values))
return is return is
@ -52,6 +63,11 @@ func (is *insertStatementImpl) MODELS(data interface{}) InsertStatement {
return is return is
} }
func (is *insertStatementImpl) AS_NEW() InsertStatement {
is.ValuesQuery.As = "new"
return is
}
func (is *insertStatementImpl) ON_DUPLICATE_KEY_UPDATE(assigments ...ColumnAssigment) InsertStatement { func (is *insertStatementImpl) ON_DUPLICATE_KEY_UPDATE(assigments ...ColumnAssigment) InsertStatement {
is.OnDuplicateKey = assigments is.OnDuplicateKey = assigments
return is return is
@ -79,7 +95,7 @@ func (s onDuplicateKeyUpdateClause) Serialize(statementType jet.StatementType, o
out.NewLine() out.NewLine()
} }
jet.Serialize(assigment, statementType, out, jet.ShortName.WithFallTrough(options)...) jet.Serialize(assigment, statementType, out, jet.FallTrough(options)...)
} }
out.DecreaseIdent(24) out.DecreaseIdent(24)

View file

@ -39,10 +39,11 @@ const (
type Interval = jet.Interval type Interval = jet.Interval
// INTERVAL creates new temporal interval. // INTERVAL creates new temporal interval.
// In a case of MICROSECOND, SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR unit type //
// value parameter has to be a number. // In a case of MICROSECOND, SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR unit type
// value parameter has to be a number.
// INTERVAL(1, DAY) // INTERVAL(1, DAY)
// In a case of other unit types, value should be string with appropriate format. // In a case of other unit types, value should be string with appropriate format.
// INTERVAL("10:08:50", HOUR_SECOND) // INTERVAL("10:08:50", HOUR_SECOND)
func INTERVAL(value interface{}, unitType unitType) Interval { func INTERVAL(value interface{}, unitType unitType) Interval {
switch unitType { switch unitType {

19
mysql/optimizer_hints.go Normal file
View file

@ -0,0 +1,19 @@
package mysql
import (
"fmt"
"github.com/go-jet/jet/v2/internal/jet"
)
// OptimizerHint provides a way to optimize query execution per-statement basis
type OptimizerHint = jet.OptimizerHint
// MAX_EXECUTION_TIME limits statement execution time
func MAX_EXECUTION_TIME(miliseconds int) OptimizerHint {
return OptimizerHint(fmt.Sprintf("MAX_EXECUTION_TIME(%d)", miliseconds))
}
// QB_NAME assigns name to query block
func QB_NAME(name string) OptimizerHint {
return OptimizerHint(fmt.Sprintf("QB_NAME(%s)", name))
}

View file

@ -40,6 +40,8 @@ type SelectStatement interface {
jet.HasProjections jet.HasProjections
Expression Expression
OPTIMIZER_HINTS(hints ...OptimizerHint) SelectStatement
DISTINCT() SelectStatement DISTINCT() SelectStatement
FROM(tables ...ReadableTable) SelectStatement FROM(tables ...ReadableTable) SelectStatement
WHERE(expression BoolExpression) SelectStatement WHERE(expression BoolExpression) SelectStatement
@ -65,9 +67,19 @@ func SELECT(projection Projection, projections ...Projection) SelectStatement {
func newSelectStatement(table ReadableTable, projections []Projection) SelectStatement { func newSelectStatement(table ReadableTable, projections []Projection) SelectStatement {
newSelect := &selectStatementImpl{} newSelect := &selectStatementImpl{}
newSelect.ExpressionStatement = jet.NewExpressionStatementImpl(Dialect, jet.SelectStatementType, newSelect, &newSelect.Select, newSelect.ExpressionStatement = jet.NewExpressionStatementImpl(Dialect, jet.SelectStatementType, newSelect,
&newSelect.From, &newSelect.Where, &newSelect.GroupBy, &newSelect.Having, &newSelect.Window, &newSelect.OrderBy, &newSelect.Select,
&newSelect.Limit, &newSelect.Offset, &newSelect.For, &newSelect.ShareLock) &newSelect.From,
&newSelect.Where,
&newSelect.GroupBy,
&newSelect.Having,
&newSelect.Window,
&newSelect.OrderBy,
&newSelect.Limit,
&newSelect.Offset,
&newSelect.For,
&newSelect.ShareLock,
)
newSelect.Select.ProjectionList = projections newSelect.Select.ProjectionList = projections
if table != nil { if table != nil {
@ -100,6 +112,11 @@ type selectStatementImpl struct {
ShareLock jet.ClauseOptional ShareLock jet.ClauseOptional
} }
func (s *selectStatementImpl) OPTIMIZER_HINTS(hints ...OptimizerHint) SelectStatement {
s.Select.OptimizerHints = hints
return s
}
func (s *selectStatementImpl) DISTINCT() SelectStatement { func (s *selectStatementImpl) DISTINCT() SelectStatement {
s.Select.Distinct = true s.Select.Distinct = true
return s return s

View file

@ -6,6 +6,8 @@ import "github.com/go-jet/jet/v2/internal/jet"
type UpdateStatement interface { type UpdateStatement interface {
jet.Statement jet.Statement
OPTIMIZER_HINTS(hints ...OptimizerHint) UpdateStatement
SET(value interface{}, values ...interface{}) UpdateStatement SET(value interface{}, values ...interface{}) UpdateStatement
MODEL(data interface{}) UpdateStatement MODEL(data interface{}) UpdateStatement
@ -36,6 +38,11 @@ func newUpdateStatement(table Table, columns []jet.Column) UpdateStatement {
return update return update
} }
func (u *updateStatementImpl) OPTIMIZER_HINTS(hints ...OptimizerHint) UpdateStatement {
u.Update.OptimizerHints = hints
return u
}
func (u *updateStatementImpl) SET(value interface{}, values ...interface{}) UpdateStatement { func (u *updateStatementImpl) SET(value interface{}, values ...interface{}) UpdateStatement {
columnAssigment, isColumnAssigment := value.(ColumnAssigment) columnAssigment, isColumnAssigment := value.(ColumnAssigment)

View file

@ -14,7 +14,7 @@ type DeleteStatement interface {
type deleteStatementImpl struct { type deleteStatementImpl struct {
jet.SerializerStatement jet.SerializerStatement
Delete jet.ClauseStatementBegin Delete jet.ClauseDelete
Using jet.ClauseFrom Using jet.ClauseFrom
Where jet.ClauseWhere Where jet.ClauseWhere
Returning jet.ClauseReturning Returning jet.ClauseReturning
@ -28,8 +28,7 @@ func newDeleteStatement(table WritableTable) DeleteStatement {
&newDelete.Where, &newDelete.Where,
&newDelete.Returning) &newDelete.Returning)
newDelete.Delete.Name = "DELETE FROM" newDelete.Delete.Table = table
newDelete.Delete.Tables = append(newDelete.Delete.Tables, table)
newDelete.Using.Name = "USING" newDelete.Using.Name = "USING"
newDelete.Where.Mandatory = true newDelete.Where.Mandatory = true

View file

@ -167,6 +167,7 @@ var reservedWords = []string{
"PRIMARY", "PRIMARY",
"REFERENCES", "REFERENCES",
"RETURNING", "RETURNING",
"RIGHT",
"SELECT", "SELECT",
"SESSION_USER", "SESSION_USER",
"SOME", "SOME",

View file

@ -18,7 +18,7 @@ type NumericExpression = jet.NumericExpression
// IntegerExpression interface // IntegerExpression interface
type IntegerExpression = jet.IntegerExpression type IntegerExpression = jet.IntegerExpression
//FloatExpression is interface // FloatExpression is interface
type FloatExpression = jet.FloatExpression type FloatExpression = jet.FloatExpression
// TimeExpression interface // TimeExpression interface

View file

@ -296,7 +296,8 @@ const (
) )
// EXTRACT function retrieves subfields such as year or hour from date/time values // EXTRACT function retrieves subfields such as year or hour from date/time values
// EXTRACT(DAY, User.CreatedAt) //
// EXTRACT(DAY, User.CreatedAt)
func EXTRACT(field unit, from Expression) FloatExpression { func EXTRACT(field unit, from Expression) FloatExpression {
return FloatExp(jet.EXTRACT(unitToString(field), from)) return FloatExp(jet.EXTRACT(unitToString(field), from))
} }

View file

@ -120,7 +120,8 @@ type intervalExpression struct {
} }
// INTERVAL creates new interval expression from the list of quantity-unit pairs. // INTERVAL creates new interval expression from the list of quantity-unit pairs.
// INTERVAL(1, DAY, 3, MINUTE) //
// INTERVAL(1, DAY, 3, MINUTE)
func INTERVAL(quantityAndUnit ...quantityAndUnit) IntervalExpression { func INTERVAL(quantityAndUnit ...quantityAndUnit) IntervalExpression {
quantityAndUnitLen := len(quantityAndUnit) quantityAndUnitLen := len(quantityAndUnit)
if quantityAndUnitLen == 0 || quantityAndUnitLen%2 != 0 { if quantityAndUnitLen == 0 || quantityAndUnitLen%2 != 0 {

View file

@ -65,6 +65,16 @@ func String(value string) StringExpression {
return CAST(jet.String(value)).AS_TEXT() return CAST(jet.String(value)).AS_TEXT()
} }
// Json creates new json literal expression
func Json(value interface{}) StringExpression {
switch value.(type) {
case string, []byte:
default:
panic("Bytea parameter value has to be of the type string or []byte")
}
return StringExp(CAST(jet.Literal(value)).AS("json"))
}
// UUID is a helper function to create string literal expression from uuid object // UUID is a helper function to create string literal expression from uuid object
// value can be any uuid type with a String method // value can be any uuid type with a String method
var UUID = jet.UUID var UUID = jet.UUID

View file

@ -67,6 +67,11 @@ func TestBytea(t *testing.T) {
assertSerialize(t, Bytea([]byte("Some byte array")), `$1::bytea`, []byte("Some byte array")) assertSerialize(t, Bytea([]byte("Some byte array")), `$1::bytea`, []byte("Some byte array"))
} }
func TestJson(t *testing.T) {
assertSerialize(t, Json("{\"key\": \"value\"}"), `$1::json`, "{\"key\": \"value\"}")
assertSerialize(t, Json([]byte("{\"key\": \"value\"}")), `$1::json`, []byte("{\"key\": \"value\"}"))
}
func TestDate(t *testing.T) { func TestDate(t *testing.T) {
assertSerialize(t, Date(2014, time.January, 2), `$1::date`, "2014-01-02") assertSerialize(t, Date(2014, time.January, 2), `$1::date`, "2014-01-02")
assertSerialize(t, DateT(time.Now()), `$1::date`) assertSerialize(t, DateT(time.Now()), `$1::date`)

View file

@ -65,7 +65,7 @@ type SelectStatement interface {
AsTable(alias string) SelectTable AsTable(alias string) SelectTable
} }
//SELECT creates new SelectStatement with list of projections // SELECT creates new SelectStatement with list of projections
func SELECT(projection Projection, projections ...Projection) SelectStatement { func SELECT(projection Projection, projections ...Projection) SelectStatement {
return newSelectStatement(nil, append([]Projection{projection}, projections...)) return newSelectStatement(nil, append([]Projection{projection}, projections...))
} }

View file

@ -51,7 +51,7 @@ type ColumnDateTime = jet.ColumnTimestamp
// DateTimeColumn creates named timestamp column // DateTimeColumn creates named timestamp column
var DateTimeColumn = jet.TimestampColumn var DateTimeColumn = jet.TimestampColumn
//ColumnTimestamp is interface of SQL timestamp columns. // ColumnTimestamp is interface of SQL timestamp columns.
type ColumnTimestamp = jet.ColumnTimestamp type ColumnTimestamp = jet.ColumnTimestamp
// TimestampColumn creates named timestamp column // TimestampColumn creates named timestamp column

View file

@ -15,7 +15,7 @@ type DeleteStatement interface {
type deleteStatementImpl struct { type deleteStatementImpl struct {
jet.SerializerStatement jet.SerializerStatement
Delete jet.ClauseStatementBegin Delete jet.ClauseDelete
Where jet.ClauseWhere Where jet.ClauseWhere
OrderBy jet.ClauseOrderBy OrderBy jet.ClauseOrderBy
Limit jet.ClauseLimit Limit jet.ClauseLimit
@ -32,8 +32,7 @@ func newDeleteStatement(table Table) DeleteStatement {
&newDelete.Returning, &newDelete.Returning,
) )
newDelete.Delete.Name = "DELETE FROM" newDelete.Delete.Table = table
newDelete.Delete.Tables = append(newDelete.Delete.Tables, table)
newDelete.Where.Mandatory = true newDelete.Where.Mandatory = true
newDelete.Limit.Count = -1 newDelete.Limit.Count = -1

View file

@ -342,7 +342,13 @@ func UNIX_TIMESTAMP(str StringExpression) TimestampExpression {
return jet.NewTimestampFunc("UNIX_TIMESTAMP", str) return jet.NewTimestampFunc("UNIX_TIMESTAMP", str)
} }
//----------- Comparison operators ---------------// // --------------- Conditional Expressions Functions -------------//
// COALESCE function returns the first of its arguments that is not null.
var COALESCE = jet.COALESCE
// NULLIF function returns a null value if value1 equals value2; otherwise it returns value1.
var NULLIF = jet.NULLIF
// EXISTS checks for existence of the rows in subQuery // EXISTS checks for existence of the rows in subQuery
var EXISTS = jet.EXISTS var EXISTS = jet.EXISTS

View file

@ -58,7 +58,7 @@ type SelectStatement interface {
AsTable(alias string) SelectTable AsTable(alias string) SelectTable
} }
//SELECT creates new SelectStatement with list of projections // SELECT creates new SelectStatement with list of projections
func SELECT(projection Projection, projections ...Projection) SelectStatement { func SELECT(projection Projection, projections ...Projection) SelectStatement {
return newSelectStatement(nil, append([]Projection{projection}, projections...)) return newSelectStatement(nil, append([]Projection{projection}, projections...))
} }

View file

@ -2,6 +2,7 @@ package mysql
import ( import (
"context" "context"
"database/sql"
"github.com/go-jet/jet/v2/internal/testutils" "github.com/go-jet/jet/v2/internal/testutils"
. "github.com/go-jet/jet/v2/mysql" . "github.com/go-jet/jet/v2/mysql"
"github.com/go-jet/jet/v2/tests/.gentestdata/mysql/dvds/table" "github.com/go-jet/jet/v2/tests/.gentestdata/mysql/dvds/table"
@ -98,3 +99,22 @@ WHERE (staff.staff_id != ?) AND (rental.rental_id < ?);
testutils.AssertExecAndRollback(t, stmt, db) testutils.AssertExecAndRollback(t, stmt, db)
} }
func TestDeleteOptimizerHints(t *testing.T) {
stmt := Link.DELETE().
OPTIMIZER_HINTS(QB_NAME("deleteIns"), "MRR(link)").
WHERE(
Link.Name.IN(String("Gmail"), String("Outlook")),
)
testutils.AssertDebugStatementSql(t, stmt, `
DELETE /*+ QB_NAME(deleteIns) MRR(link) */ FROM test_sample.link
WHERE link.name IN ('Gmail', 'Outlook');
`)
testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) {
_, err := stmt.Exec(tx)
require.NoError(t, err)
})
}

View file

@ -238,7 +238,7 @@ import (
var Actor = newActorTable("dvds", "actor", "") var Actor = newActorTable("dvds", "actor", "")
type ActorTable struct { type actorTable struct {
mysql.Table mysql.Table
//Columns //Columns
@ -251,27 +251,40 @@ type ActorTable struct {
MutableColumns mysql.ColumnList MutableColumns mysql.ColumnList
} }
type ActorTable struct {
actorTable
NEW actorTable
}
// AS creates new ActorTable with assigned alias // AS creates new ActorTable with assigned alias
func (a ActorTable) AS(alias string) ActorTable { func (a ActorTable) AS(alias string) *ActorTable {
return newActorTable(a.SchemaName(), a.TableName(), alias) return newActorTable(a.SchemaName(), a.TableName(), alias)
} }
// Schema creates new ActorTable with assigned schema name // Schema creates new ActorTable with assigned schema name
func (a ActorTable) FromSchema(schemaName string) ActorTable { func (a ActorTable) FromSchema(schemaName string) *ActorTable {
return newActorTable(schemaName, a.TableName(), a.Alias()) return newActorTable(schemaName, a.TableName(), a.Alias())
} }
// WithPrefix creates new ActorTable with assigned table prefix // WithPrefix creates new ActorTable with assigned table prefix
func (a ActorTable) WithPrefix(prefix string) ActorTable { func (a ActorTable) WithPrefix(prefix string) *ActorTable {
return newActorTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) return newActorTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
} }
// WithSuffix creates new ActorTable with assigned table suffix // WithSuffix creates new ActorTable with assigned table suffix
func (a ActorTable) WithSuffix(suffix string) ActorTable { func (a ActorTable) WithSuffix(suffix string) *ActorTable {
return newActorTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) return newActorTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
} }
func newActorTable(schemaName, tableName, alias string) ActorTable { func newActorTable(schemaName, tableName, alias string) *ActorTable {
return &ActorTable{
actorTable: newActorTableImpl(schemaName, tableName, alias),
NEW: newActorTableImpl("", "new", ""),
}
}
func newActorTableImpl(schemaName, tableName, alias string) actorTable {
var ( var (
ActorIDColumn = mysql.IntegerColumn("actor_id") ActorIDColumn = mysql.IntegerColumn("actor_id")
FirstNameColumn = mysql.StringColumn("first_name") FirstNameColumn = mysql.StringColumn("first_name")
@ -281,7 +294,7 @@ func newActorTable(schemaName, tableName, alias string) ActorTable {
mutableColumns = mysql.ColumnList{FirstNameColumn, LastNameColumn, LastUpdateColumn} mutableColumns = mysql.ColumnList{FirstNameColumn, LastNameColumn, LastUpdateColumn}
) )
return ActorTable{ return actorTable{
Table: mysql.NewTable(schemaName, tableName, alias, allColumns...), Table: mysql.NewTable(schemaName, tableName, alias, allColumns...),
//Columns //Columns
@ -334,7 +347,7 @@ import (
var ActorInfo = newActorInfoTable("dvds", "actor_info", "") var ActorInfo = newActorInfoTable("dvds", "actor_info", "")
type ActorInfoTable struct { type actorInfoTable struct {
mysql.Table mysql.Table
//Columns //Columns
@ -347,27 +360,40 @@ type ActorInfoTable struct {
MutableColumns mysql.ColumnList MutableColumns mysql.ColumnList
} }
type ActorInfoTable struct {
actorInfoTable
NEW actorInfoTable
}
// AS creates new ActorInfoTable with assigned alias // AS creates new ActorInfoTable with assigned alias
func (a ActorInfoTable) AS(alias string) ActorInfoTable { func (a ActorInfoTable) AS(alias string) *ActorInfoTable {
return newActorInfoTable(a.SchemaName(), a.TableName(), alias) return newActorInfoTable(a.SchemaName(), a.TableName(), alias)
} }
// Schema creates new ActorInfoTable with assigned schema name // Schema creates new ActorInfoTable with assigned schema name
func (a ActorInfoTable) FromSchema(schemaName string) ActorInfoTable { func (a ActorInfoTable) FromSchema(schemaName string) *ActorInfoTable {
return newActorInfoTable(schemaName, a.TableName(), a.Alias()) return newActorInfoTable(schemaName, a.TableName(), a.Alias())
} }
// WithPrefix creates new ActorInfoTable with assigned table prefix // WithPrefix creates new ActorInfoTable with assigned table prefix
func (a ActorInfoTable) WithPrefix(prefix string) ActorInfoTable { func (a ActorInfoTable) WithPrefix(prefix string) *ActorInfoTable {
return newActorInfoTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) return newActorInfoTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
} }
// WithSuffix creates new ActorInfoTable with assigned table suffix // WithSuffix creates new ActorInfoTable with assigned table suffix
func (a ActorInfoTable) WithSuffix(suffix string) ActorInfoTable { func (a ActorInfoTable) WithSuffix(suffix string) *ActorInfoTable {
return newActorInfoTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) return newActorInfoTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
} }
func newActorInfoTable(schemaName, tableName, alias string) ActorInfoTable { func newActorInfoTable(schemaName, tableName, alias string) *ActorInfoTable {
return &ActorInfoTable{
actorInfoTable: newActorInfoTableImpl(schemaName, tableName, alias),
NEW: newActorInfoTableImpl("", "new", ""),
}
}
func newActorInfoTableImpl(schemaName, tableName, alias string) actorInfoTable {
var ( var (
ActorIDColumn = mysql.IntegerColumn("actor_id") ActorIDColumn = mysql.IntegerColumn("actor_id")
FirstNameColumn = mysql.StringColumn("first_name") FirstNameColumn = mysql.StringColumn("first_name")
@ -377,7 +403,7 @@ func newActorInfoTable(schemaName, tableName, alias string) ActorInfoTable {
mutableColumns = mysql.ColumnList{ActorIDColumn, FirstNameColumn, LastNameColumn, FilmInfoColumn} mutableColumns = mysql.ColumnList{ActorIDColumn, FirstNameColumn, LastNameColumn, FilmInfoColumn}
) )
return ActorInfoTable{ return actorInfoTable{
Table: mysql.NewTable(schemaName, tableName, alias, allColumns...), Table: mysql.NewTable(schemaName, tableName, alias, allColumns...),
//Columns //Columns

View file

@ -255,7 +255,7 @@ func TestInsertOnDuplicateKey(t *testing.T) {
INSERT INTO test_sample.link INSERT INTO test_sample.link
VALUES (?, ?, ?, DEFAULT), VALUES (?, ?, ?, DEFAULT),
(?, ?, ?, DEFAULT) (?, ?, ?, DEFAULT)
ON DUPLICATE KEY UPDATE id = (id + ?), ON DUPLICATE KEY UPDATE id = (link.id + ?),
name = ?; name = ?;
`, randId, "http://www.postgresqltutorial.com", "PostgreSQL Tutorial", `, randId, "http://www.postgresqltutorial.com", "PostgreSQL Tutorial",
randId, "http://www.postgresqltutorial.com", "PostgreSQL Tutorial", randId, "http://www.postgresqltutorial.com", "PostgreSQL Tutorial",
@ -283,6 +283,65 @@ ON DUPLICATE KEY UPDATE id = (id + ?),
}) })
} }
func TestInsertOnDuplicateKeyUpdateNEW(t *testing.T) {
skipForMariaDB(t)
randId := rand.Int31()
stmt := Link.INSERT().
MODELS([]model.Link{
{
ID: randId,
URL: "https://www.postgresqltutorial.com",
Name: "PostgreSQL Tutorial",
Description: nil,
},
{
ID: randId,
URL: "https://www.yahoo.com",
Name: "Yahoo",
Description: testutils.StringPtr("web portal and search engine"),
},
}).AS_NEW().
ON_DUPLICATE_KEY_UPDATE(
Link.ID.SET(Link.ID.ADD(Int(11))),
Link.URL.SET(Link.NEW.URL),
Link.Name.SET(Link.NEW.Name),
Link.Description.SET(Link.NEW.Description),
)
testutils.AssertStatementSql(t, stmt, `
INSERT INTO test_sample.link
VALUES (?, ?, ?, ?),
(?, ?, ?, ?) AS new
ON DUPLICATE KEY UPDATE id = (link.id + ?),
url = new.url,
name = new.name,
description = new.description;
`)
testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) {
_, err := stmt.Exec(tx)
require.NoError(t, err)
stmt := SELECT(Link.AllColumns).
FROM(Link).
WHERE(Link.ID.EQ(Int32(randId + 11)))
var dest model.Link
err = stmt.Query(tx, &dest)
require.NoError(t, err)
testutils.AssertDeepEqual(t, dest, model.Link{
ID: randId + 11,
URL: "https://www.yahoo.com",
Name: "Yahoo",
Description: testutils.StringPtr("web portal and search engine"),
})
})
}
func TestInsertWithQueryContext(t *testing.T) { func TestInsertWithQueryContext(t *testing.T) {
stmt := Link.INSERT(). stmt := Link.INSERT().
VALUES(1100, "http://www.postgresqltutorial.com", "PostgreSQL Tutorial", DEFAULT) VALUES(1100, "http://www.postgresqltutorial.com", "PostgreSQL Tutorial", DEFAULT)
@ -311,3 +370,23 @@ func TestInsertWithExecContext(t *testing.T) {
require.Error(t, err, "context deadline exceeded") require.Error(t, err, "context deadline exceeded")
} }
func TestInsertOptimizerHints(t *testing.T) {
stmt := Link.INSERT(Link.MutableColumns).
OPTIMIZER_HINTS(QB_NAME("qbIns"), "NO_ICP(link)").
MODEL(model.Link{
URL: "http://www.google.com",
Name: "Google",
})
testutils.AssertDebugStatementSql(t, stmt, `
INSERT /*+ QB_NAME(qbIns) NO_ICP(link) */ INTO test_sample.link (url, name, description)
VALUES ('http://www.google.com', 'Google', NULL);
`)
testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) {
_, err := stmt.Exec(tx)
require.NoError(t, err)
})
}

View file

@ -1103,3 +1103,111 @@ func TestScanIntoCustomBaseTypes(t *testing.T) {
require.Equal(t, testutils.ToJSON(films), testutils.ToJSON(myFilms)) require.Equal(t, testutils.ToJSON(films), testutils.ToJSON(myFilms))
} }
func TestConditionalFunctions(t *testing.T) {
stmt := SELECT(
EXISTS(
Film.SELECT(Film.FilmID).WHERE(Film.RentalDuration.GT(Int(100))),
).AS("exists"),
CASE(Film.Length.GT(Int(120))).
WHEN(Bool(true)).THEN(String("long film")).
ELSE(String("short film")).AS("case"),
COALESCE(Film.Description, String("none")).AS("coalesce"),
NULLIF(Film.ReleaseYear, Int(200)).AS("null_if"),
GREATEST(Film.RentalDuration, Int(4), Int(5)).AS("greatest"),
LEAST(Film.RentalDuration, Int(7), Int(6)).AS("least"),
).FROM(
Film,
).WHERE(
Film.FilmID.LT(Int(5)),
).ORDER_BY(
Film.FilmID,
)
testutils.AssertDebugStatementSql(t, stmt, `
SELECT (EXISTS (
SELECT film.film_id AS "film.film_id"
FROM dvds.film
WHERE film.rental_duration > 100
)) AS "exists",
(CASE (film.length > 120) WHEN TRUE THEN 'long film' ELSE 'short film' END) AS "case",
COALESCE(film.description, 'none') AS "coalesce",
NULLIF(film.release_year, 200) AS "null_if",
GREATEST(film.rental_duration, 4, 5) AS "greatest",
LEAST(film.rental_duration, 7, 6) AS "least"
FROM dvds.film
WHERE film.film_id < 5
ORDER BY film.film_id;
`)
var res []struct {
Exists string
Case string
Coalesce string
NullIf string
Greatest string
Least string
}
err := stmt.Query(db, &res)
require.NoError(t, err)
testutils.AssertJSON(t, res, `
[
{
"Exists": "0",
"Case": "short film",
"Coalesce": "A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies",
"NullIf": "2006",
"Greatest": "6",
"Least": "6"
},
{
"Exists": "0",
"Case": "short film",
"Coalesce": "A Astounding Epistle of a Database Administrator And a Explorer who must Find a Car in Ancient China",
"NullIf": "2006",
"Greatest": "5",
"Least": "3"
},
{
"Exists": "0",
"Case": "short film",
"Coalesce": "A Astounding Reflection of a Lumberjack And a Car who must Sink a Lumberjack in A Baloon Factory",
"NullIf": "2006",
"Greatest": "7",
"Least": "6"
},
{
"Exists": "0",
"Case": "short film",
"Coalesce": "A Fanciful Documentary of a Frisbee And a Lumberjack who must Chase a Monkey in A Shark Tank",
"NullIf": "2006",
"Greatest": "5",
"Least": "5"
}
]
`)
}
func TestSelectOptimizerHints(t *testing.T) {
stmt := SELECT(Actor.AllColumns).
OPTIMIZER_HINTS(MAX_EXECUTION_TIME(1), QB_NAME("mainQueryBlock"), "NO_ICP(actor)").
DISTINCT().
FROM(Actor)
testutils.AssertDebugStatementSql(t, stmt, `
SELECT /*+ MAX_EXECUTION_TIME(1) QB_NAME(mainQueryBlock) NO_ICP(actor) */ DISTINCT 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"
FROM dvds.actor;
`)
var actors []model.Actor
err := stmt.QueryContext(context.Background(), db, &actors)
require.NoError(t, err)
require.Len(t, actors, 200)
}

View file

@ -5,7 +5,7 @@ import (
"database/sql" "database/sql"
"github.com/go-jet/jet/v2/internal/testutils" "github.com/go-jet/jet/v2/internal/testutils"
. "github.com/go-jet/jet/v2/mysql" . "github.com/go-jet/jet/v2/mysql"
"github.com/go-jet/jet/v2/tests/.gentestdata/mysql/dvds/table" . "github.com/go-jet/jet/v2/tests/.gentestdata/mysql/dvds/table"
"github.com/go-jet/jet/v2/tests/.gentestdata/mysql/test_sample/model" "github.com/go-jet/jet/v2/tests/.gentestdata/mysql/test_sample/model"
. "github.com/go-jet/jet/v2/tests/.gentestdata/mysql/test_sample/table" . "github.com/go-jet/jet/v2/tests/.gentestdata/mysql/test_sample/table"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -260,10 +260,10 @@ func TestUpdateExecContext(t *testing.T) {
} }
func TestUpdateWithJoin(t *testing.T) { func TestUpdateWithJoin(t *testing.T) {
statement := table.Staff.INNER_JOIN(table.Address, table.Address.AddressID.EQ(table.Staff.AddressID)). statement := Staff.INNER_JOIN(Address, Address.AddressID.EQ(Staff.AddressID)).
UPDATE(table.Staff.LastName). UPDATE(Staff.LastName).
SET(String("New staff name")). SET(String("New staff name")).
WHERE(table.Staff.StaffID.EQ(Int(1))) WHERE(Staff.StaffID.EQ(Int(1)))
testutils.AssertStatementSql(t, statement, ` testutils.AssertStatementSql(t, statement, `
UPDATE dvds.staff UPDATE dvds.staff
@ -274,3 +274,29 @@ WHERE staff.staff_id = ?;
testutils.AssertExecAndRollback(t, statement, db) testutils.AssertExecAndRollback(t, statement, db)
} }
func TestUpdateOptimizerHints(t *testing.T) {
stmt := Link.UPDATE(Link.AllColumns).
OPTIMIZER_HINTS(QB_NAME("qbInsert"), "MRR(link)").
MODEL(model.Link{
ID: 501,
URL: "http://www.duckduckgo.com",
Name: "DuckDuckGo",
}).
WHERE(Link.Name.EQ(String("Bing")))
testutils.AssertDebugStatementSql(t, stmt, `
UPDATE /*+ QB_NAME(qbInsert) MRR(link) */ test_sample.link
SET id = 501,
url = 'http://www.duckduckgo.com',
name = 'DuckDuckGo',
description = NULL
WHERE link.name = 'Bing';
`)
testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) {
_, err := stmt.Exec(tx)
require.NoError(t, err)
})
}

View file

@ -1257,6 +1257,28 @@ LIMIT $6;
requireLogged(t, query) requireLogged(t, query)
} }
func TestJsonLiteral(t *testing.T) {
stmt := AllTypes.UPDATE().
SET(AllTypes.JSON.SET(Json(`{"firstName": "John", "lastName": "Doe"}`))).
WHERE(AllTypes.SmallInt.EQ(Int(14))).
RETURNING(AllTypes.JSON)
testutils.AssertDebugStatementSql(t, stmt, `
UPDATE test_sample.all_types
SET json = '{"firstName": "John", "lastName": "Doe"}'::json
WHERE all_types.small_int = 14
RETURNING all_types.json AS "all_types.json";
`)
testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) {
var res model.AllTypes
err := stmt.Query(tx, &res)
require.NoError(t, err)
require.Equal(t, res.JSON, `{"firstName": "John", "lastName": "Doe"}`)
})
}
var allTypesRow0 = model.AllTypes{ var allTypesRow0 = model.AllTypes{
SmallIntPtr: testutils.Int16Ptr(14), SmallIntPtr: testutils.Int16Ptr(14),
SmallInt: 14, SmallInt: 14,

View file

@ -2723,6 +2723,92 @@ func TestScanUsingConn(t *testing.T) {
}) })
} }
func TestConditionalFunctions(t *testing.T) {
stmt := SELECT(
EXISTS(
Film.SELECT(Film.FilmID).WHERE(Film.RentalDuration.GT(Int(100))),
).AS("exists"),
CASE(Film.Length.GT(Int(120))).
WHEN(Bool(true)).THEN(String("long film")).
ELSE(String("short film")).AS("case"),
COALESCE(Film.Description, String("none")).AS("coalesce"),
NULLIF(Film.ReleaseYear, Int(200)).AS("null_if"),
GREATEST(Film.RentalDuration, Int(4), Int(5)).AS("greatest"),
LEAST(Film.RentalDuration, Int(7), Int(6)).AS("least"),
).FROM(
Film,
).WHERE(
Film.FilmID.LT(Int(5)),
).ORDER_BY(
Film.FilmID,
)
testutils.AssertDebugStatementSql(t, stmt, `
SELECT (EXISTS (
SELECT film.film_id AS "film.film_id"
FROM dvds.film
WHERE film.rental_duration > 100
)) AS "exists",
(CASE (film.length > 120) WHEN TRUE::boolean THEN 'long film'::text ELSE 'short film'::text END) AS "case",
COALESCE(film.description, 'none'::text) AS "coalesce",
NULLIF(film.release_year, 200) AS "null_if",
GREATEST(film.rental_duration, 4, 5) AS "greatest",
LEAST(film.rental_duration, 7, 6) AS "least"
FROM dvds.film
WHERE film.film_id < 5
ORDER BY film.film_id;
`)
var res []struct {
Exists bool
Case string
Coalesce string
NullIf string
Greatest string
Least string
}
err := stmt.Query(db, &res)
require.NoError(t, err)
testutils.AssertJSON(t, res, `
[
{
"Exists": false,
"Case": "short film",
"Coalesce": "A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies",
"NullIf": "2006",
"Greatest": "6",
"Least": "6"
},
{
"Exists": false,
"Case": "short film",
"Coalesce": "A Astounding Epistle of a Database Administrator And a Explorer who must Find a Car in Ancient China",
"NullIf": "2006",
"Greatest": "5",
"Least": "3"
},
{
"Exists": false,
"Case": "short film",
"Coalesce": "A Astounding Reflection of a Lumberjack And a Car who must Sink a Lumberjack in A Baloon Factory",
"NullIf": "2006",
"Greatest": "7",
"Least": "6"
},
{
"Exists": false,
"Case": "short film",
"Coalesce": "A Fanciful Documentary of a Frisbee And a Lumberjack who must Chase a Monkey in A Shark Tank",
"NullIf": "2006",
"Greatest": "5",
"Least": "5"
}
]
`)
}
var customer0 = model.Customer{ var customer0 = model.Customer{
CustomerID: 1, CustomerID: 1,
StoreID: 1, StoreID: 1,

View file

@ -810,3 +810,75 @@ func TestScanNumericToNumber(t *testing.T) {
require.Equal(t, number.Float32, float32(1.234568e+09)) require.Equal(t, number.Float32, float32(1.234568e+09))
require.Equal(t, number.Float64, float64(1.234567890111e+09)) require.Equal(t, number.Float64, float64(1.234567890111e+09))
} }
func TestConditionalFunctions(t *testing.T) {
stmt := SELECT(
EXISTS(
Film.SELECT(Film.FilmID).WHERE(Film.RentalDuration.GT(Int(5))),
).AS("exists"),
CASE(Film.Length.GT(Int(120))).
WHEN(Bool(true)).THEN(String("long film")).
ELSE(String("short film")).AS("case"),
COALESCE(Film.Description, String("none")).AS("coalesce"),
NULLIF(Film.ReleaseYear, Int(200)).AS("null_if"),
).FROM(
Film,
).WHERE(
Film.FilmID.LT(Int(5)),
).ORDER_BY(
Film.FilmID,
)
testutils.AssertDebugStatementSql(t, stmt, `
SELECT (EXISTS (
SELECT film.film_id AS "film.film_id"
FROM film
WHERE film.rental_duration > 5
)) AS "exists",
(CASE (film.length > 120) WHEN TRUE THEN 'long film' ELSE 'short film' END) AS "case",
COALESCE(film.description, 'none') AS "coalesce",
NULLIF(film.release_year, 200) AS "null_if"
FROM film
WHERE film.film_id < 5
ORDER BY film.film_id;
`)
var res []struct {
Exists bool
Case string
Coalesce string
NullIf string
}
err := stmt.Query(db, &res)
require.NoError(t, err)
testutils.AssertJSON(t, res, `
[
{
"Exists": true,
"Case": "short film",
"Coalesce": "A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies",
"NullIf": "2006"
},
{
"Exists": true,
"Case": "short film",
"Coalesce": "A Astounding Epistle of a Database Administrator And a Explorer who must Find a Car in Ancient China",
"NullIf": "2006"
},
{
"Exists": true,
"Case": "short film",
"Coalesce": "A Astounding Reflection of a Lumberjack And a Car who must Sink a Lumberjack in A Baloon Factory",
"NullIf": "2006"
},
{
"Exists": true,
"Case": "short film",
"Coalesce": "A Fanciful Documentary of a Frisbee And a Lumberjack who must Chase a Monkey in A Shark Tank",
"NullIf": "2006"
}
]
`)
}