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

Code before:
	column, ok := argColumn.(*Column)
	if !ok {
		return nil
	}

	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")
	}

Review comment (suggestion):
The type assertions in lines 187-188 are unsafe and will panic if the expression is not of the expected type. Specifically, `scalarFunc.Function.(*builtinFtsMysqlMatchAgainstSig)` (line 187) and `scalarFunc.GetArgs()[0].(*Constant)` (line 188) will panic if the types don't match. These should be checked with safe type assertions (ok pattern) to return proper errors instead of panicking.
```suggestion
	builtin, ok := scalarFunc.Function.(*builtinFtsMysqlMatchAgainstSig)
	if !ok {
		return expr, false, errors.Errorf("unexpected function type for %s", ast.FTSMysqlMatchAgainst)
	}
	if builtin.modifier == ast.FulltextSearchModifierBooleanMode {
		args := scalarFunc.GetArgs()
		if len(args) == 0 {
			return expr, false, errors.Errorf("missing pattern argument for %s", ast.FTSMysqlMatchAgainst)
		}
		patternConst, ok := args[0].(*Constant)
		if !ok {
			return expr, false, errors.Errorf("unexpected argument type for MATCH AGAINST pattern")
		}
		patternStr := patternConst.Value.GetString()
```

Code after:
	column, ok := argColumn.(*Column)
	if !ok {
		return nil
	}

	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]