From 792f89a8571cf054c02daecc9341a634d567d557 Mon Sep 17 00:00:00 2001 From: Karl Blomster Date: Fri, 3 Jun 2022 16:11:24 +0200 Subject: [PATCH 01/11] mysql: export some conditional functions COALESCE, NULLIF, GREATEST and LEAST already existed and were available in the postgres dialect, but not in MySQL. --- mysql/functions.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/mysql/functions.go b/mysql/functions.go index 2a8e2278..24cd73cb 100644 --- a/mysql/functions.go +++ b/mysql/functions.go @@ -261,10 +261,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 From 0425e8895c398eaf649f2dc7c4b4be472b52af72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Zahradn=C3=ADk?= Date: Sat, 4 Jun 2022 14:12:08 +0200 Subject: [PATCH 02/11] add postgres json literal --- postgres/literal.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/postgres/literal.go b/postgres/literal.go index 7b1bd197..929e3941 100644 --- a/postgres/literal.go +++ b/postgres/literal.go @@ -65,6 +65,11 @@ func String(value string) StringExpression { return CAST(jet.String(value)).AS_TEXT() } +// Json creates new json literal expression +func Json(value string) StringExpression { + return StringExp(CAST(jet.String(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 From 11b0a6858a8a84c5a4a4bbbe4bf401a32503b517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Zahradn=C3=ADk?= Date: Sat, 4 Jun 2022 14:22:33 +0200 Subject: [PATCH 03/11] Better json func, and tests --- postgres/literal.go | 9 +++++++-- postgres/literal_test.go | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/postgres/literal.go b/postgres/literal.go index 929e3941..e3a95b3b 100644 --- a/postgres/literal.go +++ b/postgres/literal.go @@ -66,8 +66,13 @@ func String(value string) StringExpression { } // Json creates new json literal expression -func Json(value string) StringExpression { - return StringExp(CAST(jet.String(value)).AS("json")) +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 diff --git a/postgres/literal_test.go b/postgres/literal_test.go index 5c5160ed..9fef0556 100644 --- a/postgres/literal_test.go +++ b/postgres/literal_test.go @@ -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`) From b4a0964fcead38415eb3b65144f017d51a815c94 Mon Sep 17 00:00:00 2001 From: go-jet Date: Tue, 16 Aug 2022 12:47:52 +0200 Subject: [PATCH 04/11] Update badge links. --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e1f808f0..84cee3bb 100644 --- a/README.md +++ b/README.md @@ -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. From f93636eab0105ab08a995fc73716dec456b1dbcb Mon Sep 17 00:00:00 2001 From: go-jet Date: Wed, 17 Aug 2022 13:03:03 +0200 Subject: [PATCH 05/11] Add missing sqlite conditional functions. --- sqlite/functions.go | 8 +++- tests/mysql/select_test.go | 86 +++++++++++++++++++++++++++++++++++ tests/postgres/select_test.go | 86 +++++++++++++++++++++++++++++++++++ tests/sqlite/select_test.go | 72 +++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+), 1 deletion(-) diff --git a/sqlite/functions.go b/sqlite/functions.go index d7142747..47a0a5b7 100644 --- a/sqlite/functions.go +++ b/sqlite/functions.go @@ -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 diff --git a/tests/mysql/select_test.go b/tests/mysql/select_test.go index 024d9417..d6f7bcb2 100644 --- a/tests/mysql/select_test.go +++ b/tests/mysql/select_test.go @@ -1103,3 +1103,89 @@ 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" + } +] +`) +} diff --git a/tests/postgres/select_test.go b/tests/postgres/select_test.go index dce9b87b..5f34daae 100644 --- a/tests/postgres/select_test.go +++ b/tests/postgres/select_test.go @@ -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, diff --git a/tests/sqlite/select_test.go b/tests/sqlite/select_test.go index 657fb3a8..5f093ce3 100644 --- a/tests/sqlite/select_test.go +++ b/tests/sqlite/select_test.go @@ -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" + } +] +`) +} From 92254c35df5159f92cbb235535e61d7a77060854 Mon Sep 17 00:00:00 2001 From: Nikita Konin Date: Thu, 18 Aug 2022 13:21:34 +0300 Subject: [PATCH 06/11] add missing SET method to ColumnTimez interface it has been already implemented, just missing from interface --- internal/jet/column_types.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/jet/column_types.go b/internal/jet/column_types.go index a606a4e7..4748b2c4 100644 --- a/internal/jet/column_types.go +++ b/internal/jet/column_types.go @@ -208,6 +208,7 @@ type ColumnTimez interface { Column From(subQuery SelectTable) ColumnTimez + SET(timeExp TimezExpression) ColumnAssigment } type timezColumnImpl struct { From 4e1ff6502323776efd256581f475bb875bf35234 Mon Sep 17 00:00:00 2001 From: go-jet Date: Tue, 23 Aug 2022 12:23:46 +0200 Subject: [PATCH 07/11] [MySQL] Add NEW alias for the rows to be inserted. --- generator/template/file_templates.go | 78 +--------------------------- generator/template/process.go | 34 ++++++------ internal/jet/clause.go | 7 +++ mysql/insert_statement.go | 8 ++- tests/mysql/generator_test.go | 54 ++++++++++++++----- tests/mysql/insert_test.go | 61 +++++++++++++++++++++- 6 files changed, 135 insertions(+), 107 deletions(-) diff --git a/generator/template/file_templates.go b/generator/template/file_templates.go index d1f12603..35b4dcd4 100644 --- a/generator/template/file_templates.go +++ b/generator/template/file_templates.go @@ -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}}", ""), } } diff --git a/generator/template/process.go b/generator/template/process.go index 46a598de..09636dc4 100644 --- a/generator/template/process.go +++ b/generator/template/process.go @@ -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) { diff --git a/internal/jet/clause.go b/internal/jet/clause.go index aa450055..85dd5341 100644 --- a/internal/jet/clause.go +++ b/internal/jet/clause.go @@ -392,6 +392,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 +418,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) } diff --git a/mysql/insert_statement.go b/mysql/insert_statement.go index 8495e040..273374f5 100644 --- a/mysql/insert_statement.go +++ b/mysql/insert_statement.go @@ -12,6 +12,7 @@ type InsertStatement interface { // 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 @@ -52,6 +53,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 +85,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) diff --git a/tests/mysql/generator_test.go b/tests/mysql/generator_test.go index e8f8d8f7..acbe04bc 100644 --- a/tests/mysql/generator_test.go +++ b/tests/mysql/generator_test.go @@ -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 diff --git a/tests/mysql/insert_test.go b/tests/mysql/insert_test.go index 10887f5a..431862f8 100644 --- a/tests/mysql/insert_test.go +++ b/tests/mysql/insert_test.go @@ -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) From a2ea1892e52acea3195b34fd08f5375f18ccf8dd Mon Sep 17 00:00:00 2001 From: go-jet Date: Tue, 23 Aug 2022 12:38:16 +0200 Subject: [PATCH 08/11] Go fmt. --- doc.go | 30 +++++++++++++++++------------- internal/jet/bool_expression.go | 6 +----- internal/jet/column_list.go | 8 ++++---- internal/jet/float_expression.go | 2 +- internal/jet/integer_expression.go | 3 --- internal/jet/literal_expression.go | 8 ++++---- internal/jet/statement.go | 2 +- internal/jet/string_expression.go | 2 +- mysql/cast.go | 2 +- mysql/functions.go | 3 ++- mysql/interval.go | 7 ++++--- postgres/expressions.go | 2 +- postgres/functions.go | 3 ++- postgres/interval_expression.go | 3 ++- postgres/select_statement.go | 2 +- sqlite/columns.go | 2 +- sqlite/select_statement.go | 2 +- 17 files changed, 44 insertions(+), 43 deletions(-) diff --git a/doc.go b/doc.go index 44a3e893..9f637bb5 100644 --- a/doc.go +++ b/doc.go @@ -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", diff --git a/internal/jet/bool_expression.go b/internal/jet/bool_expression.go index b4015b27..41bdcc44 100644 --- a/internal/jet/bool_expression.go +++ b/internal/jet/bool_expression.go @@ -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 diff --git a/internal/jet/column_list.go b/internal/jet/column_list.go index 2fdb3588..a4a0b663 100644 --- a/internal/jet/column_list.go +++ b/internal/jet/column_list.go @@ -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{} diff --git a/internal/jet/float_expression.go b/internal/jet/float_expression.go index 3fb30fed..52c97ebc 100644 --- a/internal/jet/float_expression.go +++ b/internal/jet/float_expression.go @@ -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 diff --git a/internal/jet/integer_expression.go b/internal/jet/integer_expression.go index 32d15e04..0bce229b 100644 --- a/internal/jet/integer_expression.go +++ b/internal/jet/integer_expression.go @@ -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 diff --git a/internal/jet/literal_expression.go b/internal/jet/literal_expression.go index 450b0abc..c71cc150 100644 --- a/internal/jet/literal_expression.go +++ b/internal/jet/literal_expression.go @@ -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 } diff --git a/internal/jet/statement.go b/internal/jet/statement.go index 11d8c95f..58c3dfa4 100644 --- a/internal/jet/statement.go +++ b/internal/jet/statement.go @@ -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{}) diff --git a/internal/jet/string_expression.go b/internal/jet/string_expression.go index 4e7efa62..29b24472 100644 --- a/internal/jet/string_expression.go +++ b/internal/jet/string_expression.go @@ -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)) } diff --git a/mysql/cast.go b/mysql/cast.go index 5a3e481e..83a0578b 100644 --- a/mysql/cast.go +++ b/mysql/cast.go @@ -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")) } diff --git a/mysql/functions.go b/mysql/functions.go index 24cd73cb..e2e67766 100644 --- a/mysql/functions.go +++ b/mysql/functions.go @@ -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)) } diff --git a/mysql/interval.go b/mysql/interval.go index ea246329..23dcd36e 100644 --- a/mysql/interval.go +++ b/mysql/interval.go @@ -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 { diff --git a/postgres/expressions.go b/postgres/expressions.go index c5c20653..60b0ee7b 100644 --- a/postgres/expressions.go +++ b/postgres/expressions.go @@ -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 diff --git a/postgres/functions.go b/postgres/functions.go index 7cc03353..5b07b452 100644 --- a/postgres/functions.go +++ b/postgres/functions.go @@ -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)) } diff --git a/postgres/interval_expression.go b/postgres/interval_expression.go index d1c47887..323aa315 100644 --- a/postgres/interval_expression.go +++ b/postgres/interval_expression.go @@ -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 { diff --git a/postgres/select_statement.go b/postgres/select_statement.go index ff553fb6..d44d6aad 100644 --- a/postgres/select_statement.go +++ b/postgres/select_statement.go @@ -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...)) } diff --git a/sqlite/columns.go b/sqlite/columns.go index 88ae4f6b..2941b8d6 100644 --- a/sqlite/columns.go +++ b/sqlite/columns.go @@ -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 diff --git a/sqlite/select_statement.go b/sqlite/select_statement.go index b5a75669..5e92d52e 100644 --- a/sqlite/select_statement.go +++ b/sqlite/select_statement.go @@ -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...)) } From 16c9ee428e86e9104002ee954aa9e5363ea5a93a Mon Sep 17 00:00:00 2001 From: Stefan <25141842+Hoonyyhoon@users.noreply.github.com> Date: Wed, 7 Sep 2022 00:51:24 +0900 Subject: [PATCH 09/11] Add omitted reserved word 'right' on postgresql --- postgres/dialect.go | 1 + 1 file changed, 1 insertion(+) diff --git a/postgres/dialect.go b/postgres/dialect.go index 86799293..9484ab1a 100644 --- a/postgres/dialect.go +++ b/postgres/dialect.go @@ -167,6 +167,7 @@ var reservedWords = []string{ "PRIMARY", "REFERENCES", "RETURNING", + "RIGHT", "SELECT", "SESSION_USER", "SOME", From f772f90336fa995a801227cc0fc4ccbb22fc1258 Mon Sep 17 00:00:00 2001 From: go-jet Date: Thu, 29 Sep 2022 13:33:00 +0200 Subject: [PATCH 10/11] [MySQL] Optimizer hints --- internal/jet/clause.go | 52 +++++++++++++++++++++++++++++------- mysql/delete_statement.go | 15 ++++++++--- mysql/insert_statement.go | 12 ++++++++- mysql/optimizer_hints.go | 19 +++++++++++++ mysql/select_statement.go | 23 +++++++++++++--- mysql/update_statement.go | 7 +++++ postgres/delete_statement.go | 5 ++-- sqlite/delete_statement.go | 5 ++-- tests/mysql/delete_test.go | 20 ++++++++++++++ tests/mysql/insert_test.go | 20 ++++++++++++++ tests/mysql/select_test.go | 22 +++++++++++++++ tests/mysql/update_test.go | 34 ++++++++++++++++++++--- 12 files changed, 207 insertions(+), 27 deletions(-) create mode 100644 mysql/optimizer_hints.go diff --git a/internal/jet/clause.go b/internal/jet/clause.go index 85dd5341..3708607d 100644 --- a/internal/jet/clause.go +++ b/internal/jet/clause.go @@ -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 { @@ -449,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)...) } diff --git a/mysql/delete_statement.go b/mysql/delete_statement.go index 0d39cde5..8b2c5651 100644 --- a/mysql/delete_statement.go +++ b/mysql/delete_statement.go @@ -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 diff --git a/mysql/insert_statement.go b/mysql/insert_statement.go index 273374f5..4a3fdb40 100644 --- a/mysql/insert_statement.go +++ b/mysql/insert_statement.go @@ -6,6 +6,8 @@ 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. @@ -22,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 @@ -38,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 diff --git a/mysql/optimizer_hints.go b/mysql/optimizer_hints.go new file mode 100644 index 00000000..21a0147d --- /dev/null +++ b/mysql/optimizer_hints.go @@ -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)) +} diff --git a/mysql/select_statement.go b/mysql/select_statement.go index 1c3a88a1..45c77825 100644 --- a/mysql/select_statement.go +++ b/mysql/select_statement.go @@ -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 diff --git a/mysql/update_statement.go b/mysql/update_statement.go index f053afe8..b0acaef4 100644 --- a/mysql/update_statement.go +++ b/mysql/update_statement.go @@ -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) diff --git a/postgres/delete_statement.go b/postgres/delete_statement.go index e4ecc49b..3bc1321f 100644 --- a/postgres/delete_statement.go +++ b/postgres/delete_statement.go @@ -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 diff --git a/sqlite/delete_statement.go b/sqlite/delete_statement.go index e9c06106..89626a38 100644 --- a/sqlite/delete_statement.go +++ b/sqlite/delete_statement.go @@ -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 diff --git a/tests/mysql/delete_test.go b/tests/mysql/delete_test.go index 2c92367f..7bb24226 100644 --- a/tests/mysql/delete_test.go +++ b/tests/mysql/delete_test.go @@ -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) + }) +} diff --git a/tests/mysql/insert_test.go b/tests/mysql/insert_test.go index 431862f8..b05c91db 100644 --- a/tests/mysql/insert_test.go +++ b/tests/mysql/insert_test.go @@ -370,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) + }) +} diff --git a/tests/mysql/select_test.go b/tests/mysql/select_test.go index d6f7bcb2..f07f8105 100644 --- a/tests/mysql/select_test.go +++ b/tests/mysql/select_test.go @@ -1189,3 +1189,25 @@ ORDER BY film.film_id; ] `) } + +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) +} diff --git a/tests/mysql/update_test.go b/tests/mysql/update_test.go index c03d4240..76844ebe 100644 --- a/tests/mysql/update_test.go +++ b/tests/mysql/update_test.go @@ -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) + }) +} From 59f9df9b7e999f3c7342c31d073ab8f1b670f8f1 Mon Sep 17 00:00:00 2001 From: go-jet Date: Thu, 29 Sep 2022 14:31:08 +0200 Subject: [PATCH 11/11] [postgres] Add Json literal test. --- tests/postgres/alltypes_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/postgres/alltypes_test.go b/tests/postgres/alltypes_test.go index 2a1e0e2d..8a7cc419 100644 --- a/tests/postgres/alltypes_test.go +++ b/tests/postgres/alltypes_test.go @@ -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,