package sqlbuilder import ( "fmt" "math/rand" "reflect" "slices" "sync" "testing" ) type fuzzState struct { data []byte dataIndex int callchainRepresentation string currentBuilder reflect.Value usedMethods map[string]bool } func (fs *fuzzState) consumeData(size int) []byte { if len(fs.data) <= fs.dataIndex+size { return []byte{} } result := make([]byte, size) copy(result, fs.data[fs.dataIndex:fs.dataIndex+size]) fs.dataIndex += size return result } func (fs *fuzzState) updateCallchain(method string, args []reflect.Value) { fs.callchainRepresentation += "." + method + "(" for i, arg := range args { if i > 0 { fs.callchainRepresentation += ", " } fs.callchainRepresentation += fmt.Sprintf("%q", arg) } fs.callchainRepresentation += ")" } func getSelectBuilderMethods() (map[string]reflect.Type, []string) { sb := NewSelectBuilder() sbType := reflect.TypeOf(sb) // Skip methods that are likely to cause issues or don't return builders skipMethods := []string{ "Build", "String", "BuildWithFlavor", "Flavor", "NumCol", "NumValue", "NumAssignment", "TableNames", "Var", } methodList := make(map[string]reflect.Type) methodNames := make([]string, 0, sbType.NumMethod()) for i := 0; i < sbType.NumMethod(); i++ { method := sbType.Method(i) if slices.Contains(skipMethods, method.Name) { continue } methodList[method.Name] = method.Type methodNames = append(methodNames, method.Name) } return methodList, methodNames } func generateMethodArgs(methodType reflect.Type, state *fuzzState) ([]reflect.Value, bool) { numArgs := methodType.NumIn() - 1 // Skip receiver isVariadic := methodType.IsVariadic() if isVariadic { return generateVariadicArgs(methodType, numArgs, state) } return generateFixedArgs(methodType, numArgs, state) } func generateFixedArgs(methodType reflect.Type, numArgs int, state *fuzzState) ([]reflect.Value, bool) { args := make([]reflect.Value, numArgs) for i := 0; i < numArgs; i++ { argType := methodType.In(i + 1) // Skip receiver argData := state.consumeData(16) args[i] = generateArgumentForType(argType, argData) if !args[i].IsValid() { return nil, false } // Additional type compatibility check for complex types if argType.Kind() == reflect.Ptr && args[i].Kind() == reflect.Ptr { if argType != args[i].Type() { return nil, false } } } return args, true } func generateVariadicArgs(methodType reflect.Type, numArgs int, state *fuzzState) ([]reflect.Value, bool) { numFixedArgs := numArgs - 1 // Last parameter is the variadic slice // Generate fixed arguments first args := make([]reflect.Value, numFixedArgs) for i := 0; i < numFixedArgs; i++ { argType := methodType.In(i + 1) // Skip receiver argData := state.consumeData(16) args[i] = generateArgumentForType(argType, argData) if !args[i].IsValid() { return nil, false } } // Generate variadic arguments (0-3 arguments to keep it reasonable) if numFixedArgs < numArgs { variadicType := methodType.In(numArgs).Elem() // Get the element type of the slice numVariadicArgs := 0 if len(state.data) > state.dataIndex { // 0-3 variadic args, keep the number small while still exercising multiple values // TODO: Possible fuzz improvement to allow for more variadic args. Not sure it's worth it. numVariadicArgs = int(state.data[state.dataIndex] % 4) state.dataIndex++ } for j := 0; j < numVariadicArgs; j++ { argData := state.consumeData(16) varArg := generateArgumentForType(variadicType, argData) if !varArg.IsValid() { return nil, false } args = append(args, varArg) } } return args, true } func tryCallMethod(methodName string, methodType reflect.Type, state *fuzzState, t *testing.T) bool { // Check if method exists on current builder callableMethod := state.currentBuilder.MethodByName(methodName) if !callableMethod.IsValid() { return false } // Generate arguments args, canCall := generateMethodArgs(methodType, state) if !canCall { return false } // Update call chain representation and log it state.updateCallchain(methodName, args) t.Log("callchain:", state.callchainRepresentation) // Mark this method as used state.usedMethods[methodName] = true // Call method and capture result for chaining result := callableMethod.Call(args) // Only chain if method returns the same builder type (SelectBuilder) if len(result) > 0 && result[0].IsValid() { resultType := result[0].Type() if resultType.Kind() == reflect.Ptr && resultType.String() == "*sqlbuilder.SelectBuilder" && !result[0].IsNil() { state.currentBuilder = result[0] } } return true } func executeMethodChain(methodList map[string]reflect.Type, methodNames []string, state *fuzzState, maxChains uint8, t *testing.T) { for nbFunc := uint8(0); nbFunc < maxChains; nbFunc++ { methodCalled := false // Try to find a method we haven't used yet to create more varied chains for _, method := range methodNames { // Skip methods we've already used to create more variety if state.usedMethods[method] && len(state.usedMethods) < len(methodNames) { continue } if tryCallMethod(method, methodList[method], state, t) { methodCalled = true break // Move to next chain iteration } } // If no method could be called, break the chain if !methodCalled { break } } } func finalizeBuild(state *fuzzState) { // Always try to build the final SQL to ensure it doesn't panic if state.currentBuilder.IsValid() { buildMethod := state.currentBuilder.MethodByName("Build") if buildMethod.IsValid() { buildMethod.Call([]reflect.Value{}) } } } func FuzzSelect(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte, seed int64, numberOfChainedFunction uint8) { if len(data) == 0 { return } // Get all available methods for SelectBuilder methodList, methodNames := getSelectBuilderMethods() // Randomize method order deterministically based on seed r := rand.New(rand.NewSource(seed)) r.Shuffle(len(methodNames), func(i, j int) { methodNames[i], methodNames[j] = methodNames[j], methodNames[i] }) // Initialize fuzzing state state := &fuzzState{ data: data, dataIndex: 0, callchainRepresentation: "NewSelectBuilder()", currentBuilder: reflect.ValueOf(NewSelectBuilder()), usedMethods: make(map[string]bool), } // Limit the number of chained functions to prevent infinite loops maxChains := numberOfChainedFunction if maxChains > 10 { maxChains = 10 } // Execute method chain executeMethodChain(methodList, methodNames, state, maxChains, t) t.Logf("Final callchain: %s", state.callchainRepresentation) // Try to build the final result finalizeBuild(state) }) } // generateArgumentForType generates a reflect.Value for the given type based on the provided data. // It will consume the data slice to create a value of the specified type. // It handles specific custom types like JoinOption and Flavor, and Go will consider them disntinct types than their aliases. func generateArgumentForType(argType reflect.Type, data []byte) reflect.Value { switch argType.Kind() { case reflect.String: // Handle specific custom string types first if argType.String() == "sqlbuilder.JoinOption" { joinOptions := []JoinOption{ FullJoin, FullOuterJoin, InnerJoin, LeftJoin, LeftOuterJoin, RightJoin, RightOuterJoin, } if len(data) > 0 { return reflect.ValueOf(joinOptions[int(data[0])%len(joinOptions)]) } return reflect.ValueOf(InnerJoin) } // Use remaining data as string return reflect.ValueOf(string(data)) case reflect.Int: // Handle specific custom int types first if argType.String() == "sqlbuilder.Flavor" { return reflect.ValueOf(DefaultFlavor) } if len(data) > 0 { return reflect.ValueOf(int(data[0])) } return reflect.ValueOf(0) case reflect.Bool: if len(data) > 0 { return reflect.ValueOf(data[0]%2 == 0) } return reflect.ValueOf(false) case reflect.Int8: if len(data) > 0 { return reflect.ValueOf(int8(data[0])) } return reflect.ValueOf(int8(0)) case reflect.Int16: if len(data) >= 2 { return reflect.ValueOf(int16(data[0])<<8 | int16(data[1])) } return reflect.ValueOf(int16(0)) case reflect.Int32: if len(data) >= 4 { return reflect.ValueOf(int32(data[0])<<24 | int32(data[1])<<16 | int32(data[2])<<8 | int32(data[3])) } return reflect.ValueOf(int32(0)) case reflect.Int64: if len(data) >= 8 { val := int64(data[0])<<56 | int64(data[1])<<48 | int64(data[2])<<40 | int64(data[3])<<32 | int64(data[4])<<24 | int64(data[5])<<16 | int64(data[6])<<8 | int64(data[7]) return reflect.ValueOf(val) } return reflect.ValueOf(int64(0)) case reflect.Uint: if len(data) > 0 { return reflect.ValueOf(uint(data[0])) } return reflect.ValueOf(uint(0)) case reflect.Uint8: if len(data) > 0 { return reflect.ValueOf(uint8(data[0])) } return reflect.ValueOf(uint8(0)) case reflect.Uint16: if len(data) >= 2 { return reflect.ValueOf(uint16(data[0])<<8 | uint16(data[1])) } return reflect.ValueOf(uint16(0)) case reflect.Uint32: if len(data) >= 4 { return reflect.ValueOf(uint32(data[0])<<24 | uint32(data[1])<<16 | uint32(data[2])<<8 | uint32(data[3])) } return reflect.ValueOf(uint32(0)) case reflect.Uint64: if len(data) >= 8 { val := uint64(data[0])<<56 | uint64(data[1])<<48 | uint64(data[2])<<40 | uint64(data[3])<<32 | uint64(data[4])<<24 | uint64(data[5])<<16 | uint64(data[6])<<8 | uint64(data[7]) return reflect.ValueOf(val) } return reflect.ValueOf(uint64(0)) case reflect.Float32: if len(data) >= 4 { bits := uint32(data[0])<<24 | uint32(data[1])<<16 | uint32(data[2])<<8 | uint32(data[3]) return reflect.ValueOf(float32(bits)) } return reflect.ValueOf(float32(0)) case reflect.Float64: if len(data) >= 8 { bits := uint64(data[0])<<56 | uint64(data[1])<<48 | uint64(data[2])<<40 | uint64(data[3])<<32 | uint64(data[4])<<24 | uint64(data[5])<<16 | uint64(data[6])<<8 | uint64(data[7]) return reflect.ValueOf(float64(bits)) } return reflect.ValueOf(float64(0)) case reflect.Slice: if argType.Elem().Kind() == reflect.String { return reflect.ValueOf([]string{string(data)}) } if argType.Elem().Kind() == reflect.Interface { return reflect.ValueOf([]interface{}{string(data)}) } return reflect.ValueOf([]interface{}{string(data)}) case reflect.Ptr: // Handle pointer types by creating a pointer to the underlying type // Handle specific pointer types if argType == reflect.TypeOf((*WhereClause)(nil)) { return reflect.ValueOf(NewWhereClause()) } if argType == reflect.TypeOf((*SelectBuilder)(nil)) { return reflect.ValueOf(NewSelectBuilder()) } if argType == reflect.TypeOf((*Args)(nil)) { return reflect.ValueOf(&Args{}) } if argType == reflect.TypeOf((*CTEBuilder)(nil)) { return reflect.ValueOf(DefaultFlavor.NewCTEBuilder()) } if argType == reflect.TypeOf((*InsertBuilder)(nil)) { return reflect.ValueOf(DefaultFlavor.NewInsertBuilder()) } if argType == reflect.TypeOf((*UpdateBuilder)(nil)) { return reflect.ValueOf(DefaultFlavor.NewUpdateBuilder()) } if argType == reflect.TypeOf((*DeleteBuilder)(nil)) { return reflect.ValueOf(DefaultFlavor.NewDeleteBuilder()) } // For other pointer types, create a pointer to the underlying type str := string(data) return reflect.ValueOf(&str) case reflect.Interface: // Handle specific interface types if argType.String() == "sqlbuilder.Builder" { // Create a simple SelectBuilder for Builder interface return reflect.ValueOf(NewSelectBuilder()) } return reflect.ValueOf(string(data)) default: // For other types, use zero value return reflect.Zero(argType) } } // FuzzSelectClone fuzzes SelectBuilder.Clone behavior under concurrent usage // and ensures cloned instances are independent and safe to mutate. func FuzzSelectClone(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte, seed int64, numberOfChainedFunction uint8) { if len(data) == 0 { return } methodList, methodNames := getSelectBuilderMethods() r := rand.New(rand.NewSource(seed)) r.Shuffle(len(methodNames), func(i, j int) { methodNames[i], methodNames[j] = methodNames[j], methodNames[i] }) // Build a base template SelectBuilder via fuzzed method chains. base := NewSelectBuilder() baseState := &fuzzState{ data: data, dataIndex: 0, callchainRepresentation: "NewSelectBuilder()", currentBuilder: reflect.ValueOf(base), usedMethods: make(map[string]bool), } maxChains := numberOfChainedFunction if maxChains > 10 { maxChains = 10 } executeMethodChain(methodList, methodNames, baseState, maxChains, t) baseSQLBefore, baseArgsBefore := base.Build() // Clone concurrently and mutate clones with fuzzed chains. cloneCount := int(r.Uint32()%4) + 1 // 1..4 clones var wg sync.WaitGroup wg.Add(cloneCount) start := make(chan struct{}) type result struct { sql string args []interface{} } results := make(chan result, cloneCount) for i := 0; i < cloneCount; i++ { // Use different offsets into the same fuzz data for variety. offset := 0 if len(data) > 0 { offset = (i * 17) % len(data) } go func(off int) { defer wg.Done() <-start // start all goroutines roughly at the same time c := base.Clone() st := &fuzzState{ data: data, dataIndex: off, callchainRepresentation: "Clone()", currentBuilder: reflect.ValueOf(c), usedMethods: make(map[string]bool), } executeMethodChain(methodList, methodNames, st, maxChains, t) finalizeBuild(st) // ensure no panic on Build s, a := c.Build() results <- result{sql: s, args: a} }(offset) } close(start) wg.Wait() close(results) // Ensure base builder stays unchanged after concurrent cloning/mutation of clones. baseSQLAfter, baseArgsAfter := base.Build() if baseSQLBefore != baseSQLAfter || !reflect.DeepEqual(baseArgsBefore, baseArgsAfter) { t.Fatalf("base builder mutated by clones:\n before: %s %v\n after: %s %v", baseSQLBefore, baseArgsBefore, baseSQLAfter, baseArgsAfter) } // Independence check: mutating one clone does not affect another clone. cloneA := base.Clone() sA1, aA1 := cloneA.Build() done := make(chan struct{}) go func() { defer close(done) c2 := base.Clone() // Apply a deterministic small change; should not affect cloneA. c2.OrderBy("id").Desc().Limit(1).Offset(0) _, _ = c2.Build() }() sA2, aA2 := cloneA.Build() if sA1 != sA2 || !reflect.DeepEqual(aA1, aA2) { t.Fatalf("cloneA changed after mutating another clone") } <-done // Further independence: modifying cloneA should not affect the base. cloneA.Limit(3).Asc() _ = cloneA.String() baseSQLFinal, baseArgsFinal := base.Build() if baseSQLFinal != baseSQLAfter || !reflect.DeepEqual(baseArgsFinal, baseArgsAfter) { t.Fatalf("base changed after modifying a clone") } // Drain results to ensure all builds completed; mainly to use the values and avoid lints. for range results { } }) }