Skip to content

Commit

Permalink
[MySQL] Optimizer hints
Browse files Browse the repository at this point in the history
  • Loading branch information
go-jet committed Sep 29, 2022
1 parent c9967d1 commit f772f90
Show file tree
Hide file tree
Showing 12 changed files with 207 additions and 27 deletions.
52 changes: 43 additions & 9 deletions internal/jet/clause.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)...)
}

Expand Down
15 changes: 11 additions & 4 deletions mysql/delete_statement.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
12 changes: 11 additions & 1 deletion mysql/insert_statement.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions mysql/optimizer_hints.go
Original file line number Diff line number Diff line change
@@ -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))
}
23 changes: 20 additions & 3 deletions mysql/select_statement.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions mysql/update_statement.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand Down
5 changes: 2 additions & 3 deletions postgres/delete_statement.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
5 changes: 2 additions & 3 deletions sqlite/delete_statement.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
20 changes: 20 additions & 0 deletions tests/mysql/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
})
}
20 changes: 20 additions & 0 deletions tests/mysql/insert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
22 changes: 22 additions & 0 deletions tests/mysql/select_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading

0 comments on commit f772f90

Please sign in to comment.