Repository: pingcap/tidb
File: pkg/expression/fts_helper.go (Go)

Code before:
	return &FTSInfo{
		Query:  query.Value.GetString(),
		Column: column,
	}
}

// RewriteMySQLMatchAgainstRecursively rewrites the MySQL MATCH AGAINST function into a combination of TiDB's internal FTS functions.
func RewriteMySQLMatchAgainstRecursively(bctx BuildContext, expr Expression) (Expression, bool, error) {
	scalarFunc, ok := expr.(*ScalarFunction)
	if !ok {
		return expr, false, nil
	}
	if scalarFunc.FuncName.L != ast.FTSMysqlMatchAgainst {
		// If it's not a MySQL MATCH AGAINST function, we recursively rewrite its arguments.
		changed := false
		newArgs := make([]Expression, 0, len(scalarFunc.GetArgs()))
		for _, arg := range scalarFunc.GetArgs() {
			newArg, argChanged, err := RewriteMySQLMatchAgainstRecursively(bctx, arg)
			if err != nil {
				return expr, false, err
			}
			if argChanged {
				changed = true
			}
			newArgs = append(newArgs, newArg)
		}
		if changed {
			return NewFunctionInternal(bctx, scalarFunc.FuncName.L, scalarFunc.RetType, newArgs...), true, nil
		}
		return expr, false, nil
	}
	var (
		patternGroup matchagainst.StandardBooleanGroup
		err          error
	)
	if scalarFunc.Function.(*builtinFtsMysqlMatchAgainstSig).modifier == ast.FulltextSearchModifierBooleanMode {
		patternStr := scalarFunc.GetArgs()[0].(*Constant).Value.GetString()
		patternGroup, err = matchagainst.ParseStandardBooleanMode(patternStr)
		if err != nil {
			return expr, false, err
		}
	}
	// Current limitation of TiDB.
	if len(patternGroup.Must)+len(patternGroup.MustNot) > 0 && len(patternGroup.Should) > 0 {
		return expr, false, errors.Errorf("TiDB only supports multiple terms with +/- modifiers")
	}
	if len(patternGroup.Must)+len(patternGroup.MustNot) == 0 && len(patternGroup.Should) > 1 {
		return expr, false, errors.Errorf("TiDB only supports multiple terms with +/- modifiers")
	}
	argsBuffer := make([]Expression, len(scalarFunc.GetArgs()))
	searchFuncs := make([]Expression, 0, len(patternGroup.Must)+len(patternGroup.MustNot)+len(patternGroup.Should))

Review comment (bug):
The function does not handle the case when modifier is not ast.FulltextSearchModifierBooleanMode. In this case, `patternGroup` remains uninitialized (zero value) and the validation logic on lines 195-200 will pass with zero-length slices, but the rewrite logic that follows will produce an incorrect result. The function should either validate the modifier and return an error, or handle non-BOOLEAN modes explicitly.
```suggestion
	modifier := scalarFunc.Function.(*builtinFtsMysqlMatchAgainstSig).modifier
	if modifier != ast.FulltextSearchModifierBooleanMode {
		// Non-BOOLEAN modes are currently not rewritten; recursively rewrite arguments instead.
		changed := false
		newArgs := make([]Expression, 0, len(scalarFunc.GetArgs()))
		for _, arg := range scalarFunc.GetArgs() {
			newArg, argChanged, err := RewriteMySQLMatchAgainstRecursively(bctx, arg)
			if err != nil {
				return expr, false, err
			}
			if argChanged {
				changed = true
			}
			newArgs = append(newArgs, newArg)
		}
		if changed {
			return NewFunctionInternal(bctx, scalarFunc.FuncName.L, scalarFunc.RetType, newArgs...), true, nil
		}
		return expr, false, nil
	}
	patternStr := scalarFunc.GetArgs()[0].(*Constant).Value.GetString()
	patternGroup, err = matchagainst.ParseStandardBooleanMode(patternStr)
	if err != nil {
		return expr, false, err
	}
```

Code after:
	return &FTSInfo{
		Query:  query.Value.GetString(),
		Column: column,
	}
}

// RewriteMySQLMatchAgainst rewrites one MySQL MATCH AGAINST function into a combination of TiDB's internal FTS functions.
func RewriteMySQLMatchAgainst(bctx BuildContext, expr Expression) (Expression, error) {
	scalarFunc, ok := expr.(*ScalarFunction)
	if !ok {
		return expr, nil
	}
	if scalarFunc.FuncName.L != ast.FTSMysqlMatchAgainst {
		return expr, nil
	}
	var (
		patternGroup matchagainst.StandardBooleanGroup
		err          error
	)
	if scalarFunc.Function.(*builtinFtsMysqlMatchAgainstSig).modifier == ast.FulltextSearchModifierBooleanMode {
		patternStr := scalarFunc.GetArgs()[0].(*Constant).Value.GetString()
		patternGroup, err = matchagainst.ParseStandardBooleanMode(patternStr)
		if err != nil {
			return expr, err
		}
	}
	// Current limitation of TiDB.
	if len(patternGroup.Must)+len(patternGroup.MustNot) > 0 && len(patternGroup.Should) > 0 {
		return expr, errors.Errorf("TiDB only supports multiple terms with +/- modifiers")
	}
	if len(patternGroup.Must)+len(patternGroup.MustNot) == 0 && len(patternGroup.Should) > 1 {
		return expr, errors.Errorf("TiDB only supports multiple terms with +/- modifiers")
	}

	// Compatible with MySQL, if there is no valid term in the pattern or only negative search, we return a constant false.
	if (len(patternGroup.Must)+len(patternGroup.MustNot)+len(patternGroup.Should) == 0) ||
		(len(patternGroup.Must)+len(patternGroup.Should) == 0 && len(patternGroup.MustNot) > 0) {
		return &Constant{
			Value:   types.NewIntDatum(0),
			RetType: types.NewFieldType(mysql.TypeTiny),
		}, nil
	}
	argsBuffer := make([]Expression, len(scalarFunc.GetArgs()))
	searchFuncs := make([]Expression, 0, len(patternGroup.Must)+len(patternGroup.MustNot)+len(patternGroup.Should))
	for i := 1; i < len(scalarFunc.GetArgs()); i++ {
		argsBuffer[i] = scalarFunc.GetArgs()[i]
	}
	rewriteFunc := func(item matchagainst.StandardBooleanClause) (Expression, error) {
		patternConst := &Constant{
			Value:   types.NewStringDatum(item.Expr.Text()),
			RetType: types.NewFieldType(mysql.TypeString),