Skip to content

Commit

Permalink
Make Foreign_key_checks a Vitess Aware variable (#14484)
Browse files Browse the repository at this point in the history
Signed-off-by: Manan Gupta <[email protected]>
  • Loading branch information
GuptaManan100 authored Nov 20, 2023
1 parent 81777e5 commit 543e761
Show file tree
Hide file tree
Showing 34 changed files with 3,440 additions and 123 deletions.
138 changes: 85 additions & 53 deletions go/test/endtoend/vtgate/foreignkey/fk_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"vitess.io/vitess/go/sqltypes"
"vitess.io/vitess/go/test/endtoend/utils"
"vitess.io/vitess/go/vt/log"
"vitess.io/vitess/go/vt/sqlparser"
)

type QueryFormat string
Expand All @@ -52,6 +53,7 @@ type fuzzer struct {
updateShare int
concurrency int
queryFormat QueryFormat
fkState *bool

// shouldStop is an internal state variable, that tells the fuzzer
// whether it should stop or not.
Expand All @@ -71,7 +73,7 @@ type debugInfo struct {
}

// newFuzzer creates a new fuzzer struct.
func newFuzzer(concurrency int, maxValForId int, maxValForCol int, insertShare int, deleteShare int, updateShare int, queryFormat QueryFormat) *fuzzer {
func newFuzzer(concurrency int, maxValForId int, maxValForCol int, insertShare int, deleteShare int, updateShare int, queryFormat QueryFormat, fkState *bool) *fuzzer {
fz := &fuzzer{
concurrency: concurrency,
maxValForId: maxValForId,
Expand All @@ -80,6 +82,7 @@ func newFuzzer(concurrency int, maxValForId int, maxValForCol int, insertShare i
deleteShare: deleteShare,
updateShare: updateShare,
queryFormat: queryFormat,
fkState: fkState,
wg: sync.WaitGroup{},
}
// Initially the fuzzer thread is stopped.
Expand Down Expand Up @@ -131,17 +134,18 @@ func (fz *fuzzer) generateInsertDMLQuery(insertType string) string {
tableId := rand.Intn(len(fkTables))
idValue := 1 + rand.Intn(fz.maxValForId)
tableName := fkTables[tableId]
setVarFkChecksVal := fz.getSetVarFkChecksVal()
if tableName == "fk_t20" {
colValue := rand.Intn(1 + fz.maxValForCol)
col2Value := rand.Intn(1 + fz.maxValForCol)
return fmt.Sprintf("%s into %v (id, col, col2) values (%v, %v, %v)", insertType, tableName, idValue, convertIntValueToString(colValue), convertIntValueToString(col2Value))
return fmt.Sprintf("%s %vinto %v (id, col, col2) values (%v, %v, %v)", insertType, setVarFkChecksVal, tableName, idValue, convertIntValueToString(colValue), convertIntValueToString(col2Value))
} else if isMultiColFkTable(tableName) {
colaValue := rand.Intn(1 + fz.maxValForCol)
colbValue := rand.Intn(1 + fz.maxValForCol)
return fmt.Sprintf("%s into %v (id, cola, colb) values (%v, %v, %v)", insertType, tableName, idValue, convertIntValueToString(colaValue), convertIntValueToString(colbValue))
return fmt.Sprintf("%s %vinto %v (id, cola, colb) values (%v, %v, %v)", insertType, setVarFkChecksVal, tableName, idValue, convertIntValueToString(colaValue), convertIntValueToString(colbValue))
} else {
colValue := rand.Intn(1 + fz.maxValForCol)
return fmt.Sprintf("%s into %v (id, col) values (%v, %v)", insertType, tableName, idValue, convertIntValueToString(colValue))
return fmt.Sprintf("%s %vinto %v (id, col) values (%v, %v)", insertType, setVarFkChecksVal, tableName, idValue, convertIntValueToString(colValue))
}
}

Expand All @@ -150,10 +154,11 @@ func (fz *fuzzer) generateUpdateDMLQuery() string {
tableId := rand.Intn(len(fkTables))
idValue := 1 + rand.Intn(fz.maxValForId)
tableName := fkTables[tableId]
setVarFkChecksVal := fz.getSetVarFkChecksVal()
if tableName == "fk_t20" {
colValue := convertIntValueToString(rand.Intn(1 + fz.maxValForCol))
col2Value := convertIntValueToString(rand.Intn(1 + fz.maxValForCol))
return fmt.Sprintf("update %v set col = %v, col2 = %v where id = %v", tableName, colValue, col2Value, idValue)
return fmt.Sprintf("update %v%v set col = %v, col2 = %v where id = %v", setVarFkChecksVal, tableName, colValue, col2Value, idValue)
} else if isMultiColFkTable(tableName) {
if rand.Intn(2) == 0 {
colaValue := convertIntValueToString(rand.Intn(1 + fz.maxValForCol))
Expand All @@ -162,23 +167,24 @@ func (fz *fuzzer) generateUpdateDMLQuery() string {
colaValue = fz.generateExpression(rand.Intn(4)+1, "cola", "colb", "id")
colbValue = fz.generateExpression(rand.Intn(4)+1, "cola", "colb", "id")
}
return fmt.Sprintf("update %v set cola = %v, colb = %v where id = %v", tableName, colaValue, colbValue, idValue)
return fmt.Sprintf("update %v%v set cola = %v, colb = %v where id = %v", setVarFkChecksVal, tableName, colaValue, colbValue, idValue)
} else {
colValue := fz.generateExpression(rand.Intn(4)+1, "cola", "colb", "id")
colToUpdate := []string{"cola", "colb"}[rand.Intn(2)]
return fmt.Sprintf("update %v set %v = %v where id = %v", tableName, colToUpdate, colValue, idValue)
}
} else {
colValue := fz.generateExpression(rand.Intn(4)+1, "col", "id")
return fmt.Sprintf("update %v set col = %v where id = %v", tableName, colValue, idValue)
return fmt.Sprintf("update %v%v set col = %v where id = %v", setVarFkChecksVal, tableName, colValue, idValue)
}
}

// generateDeleteDMLQuery generates a DELETE query from the parameters for the fuzzer.
func (fz *fuzzer) generateDeleteDMLQuery() string {
tableId := rand.Intn(len(fkTables))
idValue := 1 + rand.Intn(fz.maxValForId)
query := fmt.Sprintf("delete from %v where id = %v", fkTables[tableId], idValue)
setVarFkChecksVal := fz.getSetVarFkChecksVal()
query := fmt.Sprintf("delete %vfrom %v where id = %v", setVarFkChecksVal, fkTables[tableId], idValue)
return query
}

Expand All @@ -204,6 +210,9 @@ func (fz *fuzzer) runFuzzerThread(t *testing.T, sharded bool, fuzzerThreadId int
// Create a MySQL Compare that connects to both Vitess and MySQL and runs the queries against both.
mcmp, err := utils.NewMySQLCompare(t, vtParams, mysqlParams)
require.NoError(t, err)
if fz.fkState != nil {
mcmp.Exec(fmt.Sprintf("SET FOREIGN_KEY_CHECKS=%v", sqlparser.FkChecksStateString(fz.fkState)))
}
var vitessDb, mysqlDb *sql.DB
if fz.queryFormat == PreparedStatementPacket {
// Open another connection to Vitess using the go-sql-driver so that we can send prepared statements as COM_STMT_PREPARE packets.
Expand Down Expand Up @@ -464,6 +473,21 @@ func (fz *fuzzer) generateParameterizedDeleteQuery() (query string, params []any
return fmt.Sprintf("delete from %v where id = ?", fkTables[tableId]), []any{idValue}
}

// getSetVarFkChecksVal generates an optimizer hint to randomly set the foreign key checks to on or off or leave them unaltered.
func (fz *fuzzer) getSetVarFkChecksVal() string {
if fz.concurrency != 1 {
return ""
}
val := rand.Intn(3)
if val == 0 {
return ""
}
if val == 1 {
return "/*+ SET_VAR(foreign_key_checks=On) */ "
}
return "/*+ SET_VAR(foreign_key_checks=Off) */ "
}

// TestFkFuzzTest is a fuzzer test that works by querying the database concurrently.
// We have a pre-written set of query templates that we will use, but the data in the queries will
// be randomly generated. The intent is that we hammer the database as a real-world application would
Expand Down Expand Up @@ -615,57 +639,65 @@ func TestFkFuzzTest(t *testing.T) {
updateShare: 50,
}}

for _, tt := range testcases {
for _, testSharded := range []bool{false, true} {
for _, queryFormat := range []QueryFormat{OlapSQLQueries, SQLQueries, PreparedStatmentQueries, PreparedStatementPacket} {
t.Run(getTestName(tt.name, testSharded)+fmt.Sprintf(" QueryFormat - %v", queryFormat), func(t *testing.T) {
mcmp, closer := start(t)
defer closer()
// Set the correct keyspace to use from VtGates.
if testSharded {
t.Skip("Skip test since we don't have sharded foreign key support yet")
_ = utils.Exec(t, mcmp.VtConn, "use `ks`")
} else {
_ = utils.Exec(t, mcmp.VtConn, "use `uks`")
valTrue := true
valFalse := false
for _, fkState := range []*bool{nil, &valTrue, &valFalse} {
for _, tt := range testcases {
for _, testSharded := range []bool{false, true} {
for _, queryFormat := range []QueryFormat{OlapSQLQueries, SQLQueries, PreparedStatmentQueries, PreparedStatementPacket} {
if fkState != nil && (queryFormat != SQLQueries || tt.concurrency != 1) {
continue
}
// Ensure that the Vitess database is originally empty
ensureDatabaseState(t, mcmp.VtConn, true)
ensureDatabaseState(t, mcmp.MySQLConn, true)

// Create the fuzzer.
fz := newFuzzer(tt.concurrency, tt.maxValForId, tt.maxValForCol, tt.insertShare, tt.deleteShare, tt.updateShare, queryFormat)

// Start the fuzzer.
fz.start(t, testSharded)

// Wait for the timeForTesting so that the threads continue to run.
totalTime := time.After(tt.timeForTesting)
done := false
for !done {
select {
case <-totalTime:
done = true
case <-time.After(10 * time.Millisecond):
validateReplication(t)
t.Run(getTestName(tt.name, testSharded)+fmt.Sprintf(" FkState - %v QueryFormat - %v", sqlparser.FkChecksStateString(fkState), queryFormat), func(t *testing.T) {
mcmp, closer := start(t)
defer closer()
// Set the correct keyspace to use from VtGates.
if testSharded {
t.Skip("Skip test since we don't have sharded foreign key support yet")
_ = utils.Exec(t, mcmp.VtConn, "use `ks`")
} else {
_ = utils.Exec(t, mcmp.VtConn, "use `uks`")
}

// Ensure that the Vitess database is originally empty
ensureDatabaseState(t, mcmp.VtConn, true)
ensureDatabaseState(t, mcmp.MySQLConn, true)

// Create the fuzzer.
fz := newFuzzer(tt.concurrency, tt.maxValForId, tt.maxValForCol, tt.insertShare, tt.deleteShare, tt.updateShare, queryFormat, fkState)

// Start the fuzzer.
fz.start(t, testSharded)

// Wait for the timeForTesting so that the threads continue to run.
totalTime := time.After(tt.timeForTesting)
done := false
for !done {
select {
case <-totalTime:
done = true
case <-time.After(10 * time.Millisecond):
validateReplication(t)
}
}
}

fz.stop()
fz.stop()

// We encountered an error while running the fuzzer. Let's print out the information!
if fz.firstFailureInfo != nil {
log.Errorf("Failing query - %v", fz.firstFailureInfo.queryToFail)
for idx, table := range fkTables {
log.Errorf("MySQL data for %v -\n%v", table, fz.firstFailureInfo.mysqlState[idx].Rows)
log.Errorf("Vitess data for %v -\n%v", table, fz.firstFailureInfo.vitessState[idx].Rows)
// We encountered an error while running the fuzzer. Let's print out the information!
if fz.firstFailureInfo != nil {
log.Errorf("Failing query - %v", fz.firstFailureInfo.queryToFail)
for idx, table := range fkTables {
log.Errorf("MySQL data for %v -\n%v", table, fz.firstFailureInfo.mysqlState[idx].Rows)
log.Errorf("Vitess data for %v -\n%v", table, fz.firstFailureInfo.vitessState[idx].Rows)
}
}
}

// ensure Vitess database has some data. This ensures not all the commands failed.
ensureDatabaseState(t, mcmp.VtConn, false)
// Verify the consistency of the data.
verifyDataIsCorrect(t, mcmp, tt.concurrency)
})
// ensure Vitess database has some data. This ensures not all the commands failed.
ensureDatabaseState(t, mcmp.VtConn, false)
// Verify the consistency of the data.
verifyDataIsCorrect(t, mcmp, tt.concurrency)
})
}
}
}
}
Expand Down
22 changes: 22 additions & 0 deletions go/test/endtoend/vtgate/foreignkey/fk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"fmt"
"io"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -888,6 +889,22 @@ func TestFkQueries(t *testing.T) {
"update fk_t15 set col = col * (col - (col))",
},
},
{
name: "Update a child table which doesn't cause an update, but parent doesn't have that value",
queries: []string{
"insert into fk_t10 (id, col) values (1,1),(2,2)",
"insert /*+ SET_VAR(foreign_key_checks=0) */ into fk_t11 (id, col) values (1,1),(2,2),(5,5)",
"update fk_t11 set col = id where id in (1, 5)",
},
},
{
name: "Update a child table from a null to a value that parent doesn't have",
queries: []string{
"insert into fk_t10 (id, col) values (1,1),(2,2)",
"insert into fk_t11 (id, col) values (1,1),(2,2),(5,NULL)",
"update fk_t11 set col = id where id in (1, 5)",
},
},
}

for _, testcase := range testcases {
Expand Down Expand Up @@ -954,6 +971,11 @@ func TestFkOneCase(t *testing.T) {
ensureDatabaseState(t, mcmp.MySQLConn, true)

for _, query := range queries {
if strings.HasPrefix(query, "vexplain") {
res := utils.Exec(t, mcmp.VtConn, query)
log.Errorf("Query %v, Result - %v", query, res.Rows)
continue
}
_, _ = mcmp.ExecAllowAndCompareError(query)
if t.Failed() {
log.Errorf("Query failed - %v", query)
Expand Down
21 changes: 13 additions & 8 deletions go/test/vschemawrapper/vschema_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@ import (
var _ plancontext.VSchema = (*VSchemaWrapper)(nil)

type VSchemaWrapper struct {
V *vindexes.VSchema
Keyspace *vindexes.Keyspace
TabletType_ topodatapb.TabletType
Dest key.Destination
SysVarEnabled bool
Version plancontext.PlannerVersion
EnableViews bool
TestBuilder func(query string, vschema plancontext.VSchema, keyspace string) (*engine.Plan, error)
V *vindexes.VSchema
Keyspace *vindexes.Keyspace
TabletType_ topodatapb.TabletType
Dest key.Destination
SysVarEnabled bool
ForeignKeyChecksState *bool
Version plancontext.PlannerVersion
EnableViews bool
TestBuilder func(query string, vschema plancontext.VSchema, keyspace string) (*engine.Plan, error)
}

func (vw *VSchemaWrapper) GetPrepareData(stmtName string) *vtgatepb.PrepareData {
Expand Down Expand Up @@ -140,6 +141,10 @@ func (vw *VSchemaWrapper) KeyspaceError(keyspace string) error {
return nil
}

func (vw *VSchemaWrapper) GetForeignKeyChecksState() *bool {
return vw.ForeignKeyChecksState
}

func (vw *VSchemaWrapper) AllKeyspace() ([]*vindexes.Keyspace, error) {
if vw.Keyspace == nil {
return nil, vterrors.VT13001("keyspace not available")
Expand Down
4 changes: 4 additions & 0 deletions go/vt/schemadiff/semantics.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ func (si *declarativeSchemaInformation) KeyspaceError(keyspace string) error {
return nil
}

func (si *declarativeSchemaInformation) GetForeignKeyChecksState() *bool {
return nil
}

// addTable adds a fake table with an empty column list
func (si *declarativeSchemaInformation) addTable(tableName string) {
tbl := &vindexes.Table{
Expand Down
14 changes: 14 additions & 0 deletions go/vt/sqlparser/ast_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,20 @@ func (node *ParsedComments) AddQueryHint(queryHint string) (Comments, error) {
return newComments, nil
}

// FkChecksStateString prints the foreign key checks state.
func FkChecksStateString(state *bool) string {
if state == nil {
return ""
}
switch *state {
case false:
return "Off"
case true:
return "On"
}
return ""
}

// ParseParams parses the vindex parameter list, pulling out the special-case
// "owner" parameter
func (node *VindexSpec) ParseParams() (string, map[string]string) {
Expand Down
Loading

0 comments on commit 543e761

Please sign in to comment.