From f772f90336fa995a801227cc0fc4ccbb22fc1258 Mon Sep 17 00:00:00 2001 From: go-jet Date: Thu, 29 Sep 2022 13:33:00 +0200 Subject: [PATCH] [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 85dd534..3708607 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 0d39cde..8b2c565 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 273374f..4a3fdb4 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 0000000..21a0147 --- /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 1c3a88a..45c7782 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 f053afe..b0acaef 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 e4ecc49..3bc1321 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 e9c0610..89626a3 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 2c92367..7bb2422 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 431862f..b05c91d 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 d6f7bcb..f07f810 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 c03d424..76844eb 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) + }) +}