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)
[![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)
[![Documentation](https://godoc.org/github.com/go-jet/jet?status.svg)](http://godoc.org/github.com/go-jet/jet)
[![GitHub release](https://img.shields.io/github/release/go-jet/jet.svg)](https://github.com/go-jet/jet/v2/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)
[![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/v2)
[![GitHub release](https://img.shields.io/github/release/go-jet/jet.svg)](https://github.com/go-jet/jet/releases)
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.

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.
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:
$ go get -u github.com/go-jet/jet/v2
Jet generator can be installed in one of the following ways:
1) (Go1.16+) Install jet generator using go install:
go install github.com/go-jet/jet/v2/cmd/jet@latest
1. (Go1.16+) Install jet generator using go install:
go install github.com/go-jet/jet/v2/cmd/jet@latest
2) Install jet generator to GOPATH/bin folder:
cd $GOPATH/src/ && GO111MODULE=off go get -u github.com/go-jet/jet/cmd/jet
2. Install jet generator to GOPATH/bin folder:
cd $GOPATH/src/ && GO111MODULE=off go get -u github.com/go-jet/jet/cmd/jet
3) Install jet generator into specific folder:
git clone https://github.com/go-jet/jet.git
cd jet && go build -o dir_path ./cmd/jet
3. Install jet generator into specific folder:
git clone https://github.com/go-jet/jet.git
cd jet && go build -o dir_path ./cmd/jet
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
and Model files. File generation is very fast, and can be added as every pre-build step.
Sample command:
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:
import . "some_path/.gen/jetdb/dvds/table"
import "some_path/.gen/jetdb/dvds/model"
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.
Then we can write the SQL query:
// sub-query
rRatingFilms :=
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:
var dest []struct {
model.Film
@ -81,9 +83,11 @@ Now we can run the statement and store the result into desired destination:
err := stmt.Query(db, &dest)
We can print a statement to see SQL query and arguments sent to postgres server:
fmt.Println(stmt.Sql())
Output:
SELECT "rFilms"."film.film_id" AS "film.film_id",
"rFilms"."film.title" AS "film.title",
"rFilms"."film.rating" AS "film.rating",

View file

@ -26,80 +26,6 @@ import (
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 {
{{dialect.PackageName}}.Table
@ -116,7 +42,7 @@ type {{structImplName}} struct {
type {{tableTemplate.TypeName}} struct {
{{structImplName}}
EXCLUDED {{structImplName}}
{{toUpper insertedRowAlias}} {{structImplName}}
}
// 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}} {
return &{{tableTemplate.TypeName}}{
{{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 {
var tableSQLBuilderTemplate TableSQLBuilder
var tableSQLBuilder TableSQLBuilder
if fileTypes == "view" {
tableSQLBuilderTemplate = sqlBuilderTemplate.View(tableMetaData)
tableSQLBuilder = sqlBuilderTemplate.View(tableMetaData)
} else {
tableSQLBuilderTemplate = sqlBuilderTemplate.Table(tableMetaData)
tableSQLBuilder = sqlBuilderTemplate.Table(tableMetaData)
}
if tableSQLBuilderTemplate.Skip {
if tableSQLBuilder.Skip {
continue
}
tableSQLBuilderPath := path.Join(dirPath, tableSQLBuilderTemplate.Path)
tableSQLBuilderPath := path.Join(dirPath, tableSQLBuilder.Path)
err := utils.EnsureDirPath(tableSQLBuilderPath)
throw.OnError(err)
text, err := generateTemplate(
autoGenWarningTemplate+getTableSQLBuilderTemplate(dialect),
autoGenWarningTemplate+tableSQLBuilderTemplate,
tableMetaData,
template.FuncMap{
"package": func() string {
return tableSQLBuilderTemplate.PackageName()
return tableSQLBuilder.PackageName()
},
"dialect": func() jet.Dialect {
return dialect
@ -151,29 +151,33 @@ func processTableSQLBuilder(fileTypes, dirPath string,
return schemaMetaData.Name
},
"tableTemplate": func() TableSQLBuilder {
return tableSQLBuilderTemplate
return tableSQLBuilder
},
"structImplName": func() string { // postgres only
structName := tableSQLBuilderTemplate.TypeName
structName := tableSQLBuilder.TypeName
return string(strings.ToLower(structName)[0]) + structName[1:]
},
"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)
err = utils.SaveGoFile(tableSQLBuilderPath, tableSQLBuilderTemplate.FileName, text)
err = utils.SaveGoFile(tableSQLBuilderPath, tableSQLBuilder.FileName, text)
throw.OnError(err)
}
}
func getTableSQLBuilderTemplate(dialect jet.Dialect) string {
if dialect.Name() == "PostgreSQL" || dialect.Name() == "SQLite" {
return tableSQLBuilderTemplateWithEXCLUDED
func insertedRowAlias(dialect jet.Dialect) string {
if dialect.Name() == "MySQL" {
return "new"
}
return tableSQLBuilderTemplate
return "excluded"
}
func processTableModels(fileTypes, modelDirPath string, tablesMetaData []metadata.Table, modelTemplate Model) {

View file

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

View file

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

View file

@ -4,10 +4,10 @@ package jet
type ColumnList []ColumnExpression
// 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 {
return columnAssigmentImpl{
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
// Address.AllColumns.Except(Address.PostalCode, Address.Phone)
//
// Address.AllColumns.Except(Address.PostalCode, Address.Phone)
func (cl ColumnList) Except(excludedColumns ...Column) ColumnList {
excludedColumnList := UnwidColumnList(excludedColumns)
excludedColumnNames := map[string]bool{}

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ import (
"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 {
// Sql returns parametrized sql query with list of arguments.
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]))
}
//---------------------------------------------------//
// ---------------------------------------------------//
func newBinaryStringOperatorExpression(lhs, rhs Expression, operator string) StringExpression {
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"))
}
// AS_DATE casts expression AS DATE type
// AS_DATE casts expression AS DATE type
func (c *castImpl) AS_DATE() DateExpression {
return DateExp(c.AS("DATE"))
}

View file

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

View file

@ -225,7 +225,8 @@ var REGEXP_LIKE = jet.REGEXP_LIKE
//----------------- Date/Time Functions and Operators ------------//
// 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 {
return IntExp(jet.EXTRACT(string(field), from))
}
@ -261,10 +262,22 @@ func UNIX_TIMESTAMP(str StringExpression) TimestampExpression {
return jet.NewTimestampFunc("UNIX_TIMESTAMP", str)
}
//----------- Comparison operators ---------------//
// --------------- Conditional Expressions Functions -------------//
// EXISTS checks for existence of the rows in subQuery
var EXISTS = jet.EXISTS
// CASE create CASE operator with optional list of expressions
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 {
Statement
OPTIMIZER_HINTS(hints ...OptimizerHint) InsertStatement
// Insert row of values
VALUES(value interface{}, values ...interface{}) InsertStatement
// 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.
MODEL(data interface{}) InsertStatement
MODELS(data interface{}) InsertStatement
AS_NEW() InsertStatement
ON_DUPLICATE_KEY_UPDATE(assigments ...ColumnAssigment) InsertStatement
@ -21,7 +24,10 @@ type InsertStatement interface {
func newInsertStatement(table Table, columns []jet.Column) InsertStatement {
newInsert := &insertStatementImpl{}
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.Columns = columns
@ -37,6 +43,11 @@ type insertStatementImpl struct {
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 {
is.ValuesQuery.Rows = append(is.ValuesQuery.Rows, jet.UnwindRowFromValues(value, values))
return is
@ -52,6 +63,11 @@ func (is *insertStatementImpl) MODELS(data interface{}) InsertStatement {
return is
}
func (is *insertStatementImpl) AS_NEW() InsertStatement {
is.ValuesQuery.As = "new"
return is
}
func (is *insertStatementImpl) ON_DUPLICATE_KEY_UPDATE(assigments ...ColumnAssigment) InsertStatement {
is.OnDuplicateKey = assigments
return is
@ -79,7 +95,7 @@ func (s onDuplicateKeyUpdateClause) Serialize(statementType jet.StatementType, o
out.NewLine()
}
jet.Serialize(assigment, statementType, out, jet.ShortName.WithFallTrough(options)...)
jet.Serialize(assigment, statementType, out, jet.FallTrough(options)...)
}
out.DecreaseIdent(24)

View file

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

View file

@ -6,6 +6,8 @@ import "github.com/go-jet/jet/v2/internal/jet"
type UpdateStatement interface {
jet.Statement
OPTIMIZER_HINTS(hints ...OptimizerHint) UpdateStatement
SET(value interface{}, values ...interface{}) UpdateStatement
MODEL(data interface{}) UpdateStatement
@ -36,6 +38,11 @@ func newUpdateStatement(table Table, columns []jet.Column) UpdateStatement {
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 {
columnAssigment, isColumnAssigment := value.(ColumnAssigment)

View file

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

View file

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

View file

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

View file

@ -296,7 +296,8 @@ const (
)
// 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 {
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(1, DAY, 3, MINUTE)
//
// INTERVAL(1, DAY, 3, MINUTE)
func INTERVAL(quantityAndUnit ...quantityAndUnit) IntervalExpression {
quantityAndUnitLen := len(quantityAndUnit)
if quantityAndUnitLen == 0 || quantityAndUnitLen%2 != 0 {

View file

@ -65,6 +65,16 @@ func String(value string) StringExpression {
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
// value can be any uuid type with a String method
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"))
}
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) {
assertSerialize(t, Date(2014, time.January, 2), `$1::date`, "2014-01-02")
assertSerialize(t, DateT(time.Now()), `$1::date`)

View file

@ -65,7 +65,7 @@ type SelectStatement interface {
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 {
return newSelectStatement(nil, append([]Projection{projection}, projections...))
}

View file

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

View file

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

View file

@ -342,7 +342,13 @@ func UNIX_TIMESTAMP(str StringExpression) TimestampExpression {
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
var EXISTS = jet.EXISTS

View file

@ -58,7 +58,7 @@ type SelectStatement interface {
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 {
return newSelectStatement(nil, append([]Projection{projection}, projections...))
}

View file

@ -2,6 +2,7 @@ package mysql
import (
"context"
"database/sql"
"github.com/go-jet/jet/v2/internal/testutils"
. "github.com/go-jet/jet/v2/mysql"
"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)
}
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", "")
type ActorTable struct {
type actorTable struct {
mysql.Table
//Columns
@ -251,27 +251,40 @@ type ActorTable struct {
MutableColumns mysql.ColumnList
}
type ActorTable struct {
actorTable
NEW actorTable
}
// 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)
}
// 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())
}
// 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())
}
// 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())
}
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 (
ActorIDColumn = mysql.IntegerColumn("actor_id")
FirstNameColumn = mysql.StringColumn("first_name")
@ -281,7 +294,7 @@ func newActorTable(schemaName, tableName, alias string) ActorTable {
mutableColumns = mysql.ColumnList{FirstNameColumn, LastNameColumn, LastUpdateColumn}
)
return ActorTable{
return actorTable{
Table: mysql.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
@ -334,7 +347,7 @@ import (
var ActorInfo = newActorInfoTable("dvds", "actor_info", "")
type ActorInfoTable struct {
type actorInfoTable struct {
mysql.Table
//Columns
@ -347,27 +360,40 @@ type ActorInfoTable struct {
MutableColumns mysql.ColumnList
}
type ActorInfoTable struct {
actorInfoTable
NEW actorInfoTable
}
// 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)
}
// 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())
}
// 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())
}
// 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())
}
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 (
ActorIDColumn = mysql.IntegerColumn("actor_id")
FirstNameColumn = mysql.StringColumn("first_name")
@ -377,7 +403,7 @@ func newActorInfoTable(schemaName, tableName, alias string) ActorInfoTable {
mutableColumns = mysql.ColumnList{ActorIDColumn, FirstNameColumn, LastNameColumn, FilmInfoColumn}
)
return ActorInfoTable{
return actorInfoTable{
Table: mysql.NewTable(schemaName, tableName, alias, allColumns...),
//Columns

View file

@ -255,7 +255,7 @@ func TestInsertOnDuplicateKey(t *testing.T) {
INSERT INTO test_sample.link
VALUES (?, ?, ?, DEFAULT),
(?, ?, ?, DEFAULT)
ON DUPLICATE KEY UPDATE id = (id + ?),
ON DUPLICATE KEY UPDATE id = (link.id + ?),
name = ?;
`, 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) {
stmt := Link.INSERT().
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")
}
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))
}
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"
"github.com/go-jet/jet/v2/internal/testutils"
. "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/table"
"github.com/stretchr/testify/require"
@ -260,10 +260,10 @@ func TestUpdateExecContext(t *testing.T) {
}
func TestUpdateWithJoin(t *testing.T) {
statement := table.Staff.INNER_JOIN(table.Address, table.Address.AddressID.EQ(table.Staff.AddressID)).
UPDATE(table.Staff.LastName).
statement := Staff.INNER_JOIN(Address, Address.AddressID.EQ(Staff.AddressID)).
UPDATE(Staff.LastName).
SET(String("New staff name")).
WHERE(table.Staff.StaffID.EQ(Int(1)))
WHERE(Staff.StaffID.EQ(Int(1)))
testutils.AssertStatementSql(t, statement, `
UPDATE dvds.staff
@ -274,3 +274,29 @@ WHERE staff.staff_id = ?;
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)
}
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{
SmallIntPtr: testutils.Int16Ptr(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{
CustomerID: 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.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"
}
]
`)
}