diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 95302334c..000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @rashiq @meiji163 @timvaillancourt diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 820d4ef96..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,13 +0,0 @@ -> This is the place to report a bug, ask a question, or suggest an enhancement. - -> This is also the place to make a discussion before creating a PR. - -> If this is a bug report, please provide a test case (e.g., your table definition and gh-ost command) and the error output. - -> Please use markdown to format code or SQL: https://guides.github.com/features/mastering-markdown/ - -> Please label the issue on the right (bug, enhancement, question, etc.). - -> And please understand if this issue is not addressed immediately or in a timeframe you were expecting. - -> Thank you! diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index c0c246463..000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,19 +0,0 @@ -## A Pull Request should be associated with an Issue. - -> We wish to have discussions in Issues. A single issue may be targeted by multiple PRs. -> If you're offering a new feature or fixing anything, we'd like to know beforehand in Issues, -> and potentially we'll be able to point development in a particular direction. - -Related issue: https://github.com/github/gh-ost/issues/0123456789 - -> Further notes in https://github.com/github/gh-ost/blob/master/.github/CONTRIBUTING.md -> Thank you! We are open to PRs, but please understand if for technical reasons we are unable to accept each and any PR - -### Description - -This PR [briefly explain what it does] - -> In case this PR introduced Go code changes: - -- [ ] contributed code is using same conventions as original code -- [ ] `script/cibuild` returns with no formatting errors, build errors or unit test errors. diff --git a/build.sh b/build.sh index 64d5b1584..c932624d0 100755 --- a/build.sh +++ b/build.sh @@ -2,8 +2,8 @@ # # -RELEASE_VERSION= -buildpath= +RELEASE_VERSION=2 +buildpath=/tmp function setuptree() { b=$( mktemp -d $buildpath/gh-ostXXXXXX ) || return 1 diff --git a/go/base/context.go b/go/base/context.go index 59227ea2d..8c4d540fe 100644 --- a/go/base/context.go +++ b/go/base/context.go @@ -123,6 +123,7 @@ type MigrationContext struct { MaxLagMillisecondsThrottleThreshold int64 throttleControlReplicaKeys *mysql.InstanceKeyMap ThrottleFlagFile string + copyWhereClause string ThrottleAdditionalFlagFile string throttleQuery string throttleHTTP string @@ -725,6 +726,19 @@ func (this *MigrationContext) GetCriticalLoad() LoadMap { return this.criticalLoad.Duplicate() } +func (this *MigrationContext) GetWhereClause() string { + this.throttleMutex.Lock() + defer this.throttleMutex.Unlock() + + return this.copyWhereClause +} + +func (this *MigrationContext) SetWhereClause(WhereClause string) { + this.throttleMutex.Lock() + defer this.throttleMutex.Unlock() + this.copyWhereClause = WhereClause +} + func (this *MigrationContext) GetNiceRatio() float64 { this.throttleMutex.Lock() defer this.throttleMutex.Unlock() diff --git a/go/cmd/gh-ost/main.go b/go/cmd/gh-ost/main.go index 3e6057995..a32a87b49 100644 --- a/go/cmd/gh-ost/main.go +++ b/go/cmd/gh-ost/main.go @@ -115,6 +115,7 @@ func main() { throttleControlReplicas := flag.String("throttle-control-replicas", "", "List of replicas on which to check for lag; comma delimited. Example: myhost1.com:3306,myhost2.com,myhost3.com:3307") throttleQuery := flag.String("throttle-query", "", "when given, issued (every second) to check if operation should throttle. Expecting to return zero for no-throttle, >0 for throttle. Query is issued on the migrated server. Make sure this query is lightweight") throttleHTTP := flag.String("throttle-http", "", "when given, gh-ost checks given URL via HEAD request; any response code other than 200 (OK) causes throttling; make sure it has low latency response") + copyWhereClause := flag.String("where-clause", "1=1", "added where clause for the insert query, filtering table rows") flag.Int64Var(&migrationContext.ThrottleHTTPIntervalMillis, "throttle-http-interval-millis", 100, "Number of milliseconds to wait before triggering another HTTP throttle check") flag.Int64Var(&migrationContext.ThrottleHTTPTimeoutMillis, "throttle-http-timeout-millis", 1000, "Number of milliseconds to use as an HTTP throttle check timeout") ignoreHTTPErrors := flag.Bool("ignore-http-errors", false, "ignore HTTP connection errors during throttle check") @@ -303,6 +304,7 @@ func main() { migrationContext.SetThrottleHTTP(*throttleHTTP) migrationContext.SetIgnoreHTTPErrors(*ignoreHTTPErrors) migrationContext.SetDefaultNumRetries(*defaultRetries) + migrationContext.SetWhereClause(*copyWhereClause) migrationContext.ApplyCredentials() if err := migrationContext.SetupTLS(); err != nil { migrationContext.Log.Fatale(err) diff --git a/go/logic/applier.go b/go/logic/applier.go index 990fbe720..2cefde216 100644 --- a/go/logic/applier.go +++ b/go/logic/applier.go @@ -11,6 +11,7 @@ import ( "strings" "sync/atomic" "time" + "context" "github.com/github/gh-ost/go/base" "github.com/github/gh-ost/go/binlog" @@ -631,13 +632,20 @@ func (this *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected this.migrationContext.MigrationIterationRangeMaxValues.AbstractValues(), this.migrationContext.GetIteration() == 0, this.migrationContext.IsTransactionalTable(), + this.migrationContext.GetWhereClause(), ) if err != nil { return chunkSize, rowsAffected, duration, err } sqlResult, err := func() (gosql.Result, error) { - tx, err := this.db.Begin() + var conn *gosql.Conn + conn, err = this.db.Conn(context.Background()) + if (conn == nil || err != nil) { + fmt.Sprintf("failed to get connection") + return nil, err + } + tx, err := conn.BeginTx(context.Background(), nil) if err != nil { return nil, err } @@ -646,16 +654,17 @@ func (this *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected sessionQuery := fmt.Sprintf(`SET SESSION time_zone = '%s'`, this.migrationContext.ApplierTimeZone) sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery()) - if _, err := tx.Exec(sessionQuery); err != nil { + if _, err := tx.ExecContext(context.Background(), sessionQuery); err != nil { return nil, err } - result, err := tx.Exec(query, explodedArgs...) + result, err := tx.ExecContext(context.Background(), query, explodedArgs...) if err != nil { return nil, err } if err := tx.Commit(); err != nil { return nil, err } + conn.Close() return result, nil }() diff --git a/go/logic/inspect.go b/go/logic/inspect.go index 9d414a43e..b5a7c79e9 100644 --- a/go/logic/inspect.go +++ b/go/logic/inspect.go @@ -554,7 +554,7 @@ func (this *Inspector) CountTableRows(ctx context.Context) error { return err } - query := fmt.Sprintf(`select /* gh-ost */ count(*) as count_rows from %s.%s`, sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName)) + query := fmt.Sprintf(`select /* gh-ost */ table_rows as count_rows from information_schema.tables where table_schema="%s" and table_name="%s"`, sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName)) var rowsEstimate int64 if err := conn.QueryRowContext(ctx, query).Scan(&rowsEstimate); err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { diff --git a/go/logic/migrator.go b/go/logic/migrator.go index 3fc897cd3..d15464184 100644 --- a/go/logic/migrator.go +++ b/go/logic/migrator.go @@ -842,13 +842,14 @@ func (this *Migrator) printMigrationStatusHint(writers ...io.Writer) { ) maxLoad := this.migrationContext.GetMaxLoad() criticalLoad := this.migrationContext.GetCriticalLoad() - fmt.Fprintf(w, "# chunk-size: %+v; max-lag-millis: %+vms; dml-batch-size: %+v; max-load: %s; critical-load: %s; nice-ratio: %f\n", + fmt.Fprintf(w, "# chunk-size: %+v; max-lag-millis: %+vms; dml-batch-size: %+v; max-load: %s; critical-load: %s; nice-ratio: %f; where-clause: %s\n", atomic.LoadInt64(&this.migrationContext.ChunkSize), atomic.LoadInt64(&this.migrationContext.MaxLagMillisecondsThrottleThreshold), atomic.LoadInt64(&this.migrationContext.DMLBatchSize), maxLoad.String(), criticalLoad.String(), this.migrationContext.GetNiceRatio(), + this.migrationContext.GetWhereClause(), ) if this.migrationContext.ThrottleFlagFile != "" { setIndicator := "" diff --git a/go/sql/builder.go b/go/sql/builder.go index 7be428f93..37b0f2699 100644 --- a/go/sql/builder.go +++ b/go/sql/builder.go @@ -182,7 +182,7 @@ func BuildRangePreparedComparison(columns *ColumnList, args []interface{}, compa return BuildRangeComparison(columns.Names(), values, args, comparisonSign) } -func BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName string, sharedColumns []string, mappedSharedColumns []string, uniqueKey string, uniqueKeyColumns *ColumnList, rangeStartValues, rangeEndValues []string, rangeStartArgs, rangeEndArgs []interface{}, includeRangeStartValues bool, transactionalTable bool) (result string, explodedArgs []interface{}, err error) { +func BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName string, sharedColumns []string, mappedSharedColumns []string, uniqueKey string, uniqueKeyColumns *ColumnList, rangeStartValues, rangeEndValues []string, rangeStartArgs, rangeEndArgs []interface{}, includeRangeStartValues bool, transactionalTable bool, whereClause string) (result string, explodedArgs []interface{}, err error) { if len(sharedColumns) == 0 { return "", explodedArgs, fmt.Errorf("Got 0 shared columns in BuildRangeInsertQuery") } @@ -232,19 +232,19 @@ func BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName strin %s.%s force index (%s) where - (%s and %s) + (%s and %s and %s) %s )`, databaseName, originalTableName, databaseName, ghostTableName, mappedSharedColumnsListing, sharedColumnsListing, databaseName, originalTableName, uniqueKey, - rangeStartComparison, rangeEndComparison, transactionalClause) + rangeStartComparison, rangeEndComparison, whereClause, transactionalClause) return result, explodedArgs, nil } -func BuildRangeInsertPreparedQuery(databaseName, originalTableName, ghostTableName string, sharedColumns []string, mappedSharedColumns []string, uniqueKey string, uniqueKeyColumns *ColumnList, rangeStartArgs, rangeEndArgs []interface{}, includeRangeStartValues bool, transactionalTable bool) (result string, explodedArgs []interface{}, err error) { +func BuildRangeInsertPreparedQuery(databaseName, originalTableName, ghostTableName string, sharedColumns []string, mappedSharedColumns []string, uniqueKey string, uniqueKeyColumns *ColumnList, rangeStartArgs, rangeEndArgs []interface{}, includeRangeStartValues bool, transactionalTable bool, whereClause string) (result string, explodedArgs []interface{}, err error) { rangeStartValues := buildColumnsPreparedValues(uniqueKeyColumns) rangeEndValues := buildColumnsPreparedValues(uniqueKeyColumns) - return BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, mappedSharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, includeRangeStartValues, transactionalTable) + return BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, mappedSharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, includeRangeStartValues, transactionalTable, whereClause) } func BuildUniqueKeyRangeEndPreparedQueryViaOffset(databaseName, tableName string, uniqueKeyColumns *ColumnList, rangeStartArgs, rangeEndArgs []interface{}, chunkSize int64, includeRangeStartValues bool, hint string) (result string, explodedArgs []interface{}, err error) { diff --git a/go/sql/builder_test.go b/go/sql/builder_test.go index 574e8bb1b..3766146b5 100644 --- a/go/sql/builder_test.go +++ b/go/sql/builder_test.go @@ -163,6 +163,7 @@ func TestBuildRangeInsertQuery(t *testing.T) { databaseName := "mydb" originalTableName := "tbl" ghostTableName := "ghost" + whereClause := "1=1" sharedColumns := []string{"id", "name", "position"} { uniqueKey := "PRIMARY" @@ -172,7 +173,7 @@ func TestBuildRangeInsertQuery(t *testing.T) { rangeStartArgs := []interface{}{3} rangeEndArgs := []interface{}{103} - query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false) + query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false, whereClause) test.S(t).ExpectNil(err) expected := ` insert /* gh-ost mydb.tbl */ ignore @@ -186,7 +187,7 @@ func TestBuildRangeInsertQuery(t *testing.T) { force index (PRIMARY) where (((id > @v1s) or ((id = @v1s))) - and ((id < @v1e) or ((id = @v1e)))) + and ((id < @v1e) or ((id = @v1e))) and 1=1) )` test.S(t).ExpectEquals(normalizeQuery(query), normalizeQuery(expected)) test.S(t).ExpectTrue(reflect.DeepEqual(explodedArgs, []interface{}{3, 3, 103, 103})) @@ -199,7 +200,7 @@ func TestBuildRangeInsertQuery(t *testing.T) { rangeStartArgs := []interface{}{3, 17} rangeEndArgs := []interface{}{103, 117} - query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false) + query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false, whereClause) test.S(t).ExpectNil(err) expected := ` insert /* gh-ost mydb.tbl */ ignore @@ -219,17 +220,110 @@ func TestBuildRangeInsertQuery(t *testing.T) { and ((name < @v1e) or (((name = @v1e)) AND (position < @v2e)) - or ((name = @v1e) and (position = @v2e)))) + or ((name = @v1e) and (position = @v2e))) and 1=1) )` test.S(t).ExpectEquals(normalizeQuery(query), normalizeQuery(expected)) test.S(t).ExpectTrue(reflect.DeepEqual(explodedArgs, []interface{}{3, 3, 17, 3, 17, 103, 103, 117, 103, 117})) } } + +func TestBuildRangeInsertQueryWhereClauseFiltering(t *testing.T) { + databaseName := "mydb" + originalTableName := "tbl" + ghostTableName := "ghost" + sharedColumns := []string{"id", "name", "position"} + { + uniqueKey := "PRIMARY" + uniqueKeyColumns := NewColumnList([]string{"id"}) + rangeStartValues := []string{"@v1s"} + rangeEndValues := []string{"@v1e"} + rangeStartArgs := []interface{}{3} + rangeEndArgs := []interface{}{103} + whereClause := "id = 1" + + query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false, whereClause) + test.S(t).ExpectNil(err) + expected := ` + insert /* gh-ost mydb.tbl */ ignore + into + mydb.ghost + (id, name, position) + ( + select id, name, position + from + mydb.tbl + force index (PRIMARY) + where + (((id > @v1s) or ((id = @v1s))) + and ((id < @v1e) or ((id = @v1e))) and id = 1) + )` + test.S(t).ExpectEquals(normalizeQuery(query), normalizeQuery(expected)) + test.S(t).ExpectTrue(reflect.DeepEqual(explodedArgs, []interface{}{3, 3, 103, 103})) + } + { + uniqueKey := "PRIMARY" + uniqueKeyColumns := NewColumnList([]string{"id"}) + rangeStartValues := []string{"@v1s"} + rangeEndValues := []string{"@v1e"} + rangeStartArgs := []interface{}{3} + rangeEndArgs := []interface{}{103} + whereClause := "id not in (1,2,3)" + + query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false, whereClause) + test.S(t).ExpectNil(err) + expected := ` + insert /* gh-ost mydb.tbl */ ignore + into + mydb.ghost + (id, name, position) + ( + select id, name, position + from + mydb.tbl + force index (PRIMARY) + where + (((id > @v1s) or ((id = @v1s))) + and ((id < @v1e) or ((id = @v1e))) and id not in (1,2,3)) + )` + test.S(t).ExpectEquals(normalizeQuery(query), normalizeQuery(expected)) + test.S(t).ExpectTrue(reflect.DeepEqual(explodedArgs, []interface{}{3, 3, 103, 103})) + } + { + uniqueKey := "PRIMARY" + uniqueKeyColumns := NewColumnList([]string{"id"}) + rangeStartValues := []string{"@v1s"} + rangeEndValues := []string{"@v1e"} + rangeStartArgs := []interface{}{3} + rangeEndArgs := []interface{}{103} + whereClause := "id in (select id from ids)" + + query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false, whereClause) + test.S(t).ExpectNil(err) + expected := ` + insert /* gh-ost mydb.tbl */ ignore + into + mydb.ghost + (id, name, position) + ( + select id, name, position + from + mydb.tbl + force index (PRIMARY) + where + (((id > @v1s) or ((id = @v1s))) + and ((id < @v1e) or ((id = @v1e))) and id in (select id from ids)) + )` + test.S(t).ExpectEquals(normalizeQuery(query), normalizeQuery(expected)) + test.S(t).ExpectTrue(reflect.DeepEqual(explodedArgs, []interface{}{3, 3, 103, 103})) + } +} + func TestBuildRangeInsertQueryRenameMap(t *testing.T) { databaseName := "mydb" originalTableName := "tbl" ghostTableName := "ghost" + whereClause := "1=1" sharedColumns := []string{"id", "name", "position"} mappedSharedColumns := []string{"id", "name", "location"} { @@ -240,7 +334,7 @@ func TestBuildRangeInsertQueryRenameMap(t *testing.T) { rangeStartArgs := []interface{}{3} rangeEndArgs := []interface{}{103} - query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, mappedSharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false) + query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, mappedSharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false, whereClause) test.S(t).ExpectNil(err) expected := ` insert /* gh-ost mydb.tbl */ ignore @@ -255,7 +349,7 @@ func TestBuildRangeInsertQueryRenameMap(t *testing.T) { where (((id > @v1s) or ((id = @v1s))) and - ((id < @v1e) or ((id = @v1e)))) + ((id < @v1e) or ((id = @v1e))) and 1=1) )` test.S(t).ExpectEquals(normalizeQuery(query), normalizeQuery(expected)) test.S(t).ExpectTrue(reflect.DeepEqual(explodedArgs, []interface{}{3, 3, 103, 103})) @@ -268,7 +362,7 @@ func TestBuildRangeInsertQueryRenameMap(t *testing.T) { rangeStartArgs := []interface{}{3, 17} rangeEndArgs := []interface{}{103, 117} - query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, mappedSharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false) + query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, mappedSharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false, whereClause) test.S(t).ExpectNil(err) expected := ` insert /* gh-ost mydb.tbl */ ignore @@ -284,7 +378,7 @@ func TestBuildRangeInsertQueryRenameMap(t *testing.T) { (((name > @v1s) or (((name = @v1s)) AND (position > @v2s)) or ((name = @v1s) and (position = @v2s))) and ((name < @v1e) or (((name = @v1e)) AND (position < @v2e)) - or ((name = @v1e) and (position = @v2e)))) + or ((name = @v1e) and (position = @v2e))) and 1=1) )` test.S(t).ExpectEquals(normalizeQuery(query), normalizeQuery(expected)) test.S(t).ExpectTrue(reflect.DeepEqual(explodedArgs, []interface{}{3, 3, 17, 3, 17, 103, 103, 117, 103, 117})) @@ -295,6 +389,7 @@ func TestBuildRangeInsertPreparedQuery(t *testing.T) { databaseName := "mydb" originalTableName := "tbl" ghostTableName := "ghost" + whereClause := "1=1" sharedColumns := []string{"id", "name", "position"} { uniqueKey := "name_position_uidx" @@ -302,7 +397,7 @@ func TestBuildRangeInsertPreparedQuery(t *testing.T) { rangeStartArgs := []interface{}{3, 17} rangeEndArgs := []interface{}{103, 117} - query, explodedArgs, err := BuildRangeInsertPreparedQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartArgs, rangeEndArgs, true, true) + query, explodedArgs, err := BuildRangeInsertPreparedQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartArgs, rangeEndArgs, true, true, whereClause) test.S(t).ExpectNil(err) expected := ` insert /* gh-ost mydb.tbl */ ignore @@ -314,7 +409,7 @@ func TestBuildRangeInsertPreparedQuery(t *testing.T) { from mydb.tbl force index (name_position_uidx) - where (((name > ?) or (((name = ?)) AND (position > ?)) or ((name = ?) and (position = ?))) and ((name < ?) or (((name = ?)) AND (position < ?)) or ((name = ?) and (position = ?)))) + where (((name > ?) or (((name = ?)) AND (position > ?)) or ((name = ?) and (position = ?))) and ((name < ?) or (((name = ?)) AND (position < ?)) or ((name = ?) and (position = ?))) and 1=1) lock in share mode )` test.S(t).ExpectEquals(normalizeQuery(query), normalizeQuery(expected))