Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 222 additions & 2 deletions pkg/frontend/mysql_cmd_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1312,7 +1312,7 @@ func doPrepareString(ses *Session, execCtx *ExecCtx, st *tree.PrepareString) (*P
return nil, err
}

stmts, err := mysql.Parse(execCtx.reqCtx, st.Sql, v.(int64))
stmts, err := mysql.ParseWithSQLMode(execCtx.reqCtx, st.Sql, v.(int64), sessionSQLModeForParser(ses))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -2508,7 +2508,227 @@ func parseSql(execCtx *ExecCtx, p *mysql.MySQLParser) (stmts []tree.Statement, e
lctn = vv
}
}
return p.Parse(execCtx.reqCtx, execCtx.input.getSql(), lctn)
sql := execCtx.input.getSql()
initialSQLMode := sessionSQLModeForParser(execCtx.ses)
stmts, err = p.ParseWithSQLMode(execCtx.reqCtx, sql, lctn, initialSQLMode)
if err != nil {
incrementalStmts, incrementalErr := parseStatementsWithStaticSQLModeSet(execCtx, p, sql, lctn, initialSQLMode)
if incrementalErr == nil && hasStaticSQLModeSet(incrementalStmts) {
return incrementalStmts, nil
}
if incrementalErr == nil {
freeStatements(incrementalStmts)
}
return nil, err
}
if !hasStaticSQLModeSet(stmts) {
return stmts, nil
}
freeStatements(stmts)
return parseStatementsWithStaticSQLModeSet(execCtx, p, sql, lctn, initialSQLMode)
}

func sessionSQLModeForParser(ses FeSession) string {
v, err := ses.GetSessionSysVar("sql_mode")
if err != nil {
return ""
}
mode, ok := v.(string)
if !ok {
return ""
}
return mysql.SessionSQLModeForParser(mode)
}

func parseStatementsWithStaticSQLModeSet(
execCtx *ExecCtx,
p *mysql.MySQLParser,
sql string,
lctn int64,
initialSQLMode string,
) ([]tree.Statement, error) {
remaining := sql
parserSQLMode := initialSQLMode
stmts := make([]tree.Statement, 0)
for {
stmt, rest, hasStmt, err := parseFirstStatementWithSQLMode(execCtx, p, remaining, lctn, parserSQLMode)
if err != nil {
freeStatements(stmts)
return nil, err
}
remaining = rest
if !hasStmt {
if remaining == "" {
break
}
continue
}
stmts = append(stmts, stmt)
if nextSQLMode, ok := staticSQLModeSetValue(stmt); ok {
parserSQLMode = nextSQLMode
}
if remaining == "" {
break
}
}
if len(stmts) == 0 {
return []tree.Statement{&tree.EmptyStmt{}}, nil
}
return stmts, nil
}

func parseFirstStatementWithSQLMode(
execCtx *ExecCtx,
p *mysql.MySQLParser,
sql string,
lctn int64,
parserSQLMode string,
) (tree.Statement, string, bool, error) {
if sql == "" {
return nil, "", false, nil
}
scanner := mysql.NewScannerWithSQLMode(dialect.MYSQL, sql, mysql.ParseSQLModeFlags(parserSQLMode))
defer mysql.PutScanner(scanner)
for scanner.Pos < len(sql) {
typ, _ := scanner.Scan()
switch typ {
case int(';'):
fragment := sql[:scanner.Pos-1]
rest := sql[scanner.Pos:]
if !fragmentHasStatement(fragment, parserSQLMode) {
return nil, rest, false, nil
}
stmt, err := parseSingleStatementWithSQLMode(execCtx, p, fragment, lctn, parserSQLMode)
if err == nil {
return stmt, rest, true, nil
}
case mysql.EofChar():
return parseLastStatementWithSQLMode(execCtx, p, sql, lctn, parserSQLMode)
case mysql.LEX_ERROR:
return parseLastStatementWithSQLMode(execCtx, p, sql, lctn, parserSQLMode)
}
}
return parseLastStatementWithSQLMode(execCtx, p, sql, lctn, parserSQLMode)
}

func parseLastStatementWithSQLMode(
execCtx *ExecCtx,
p *mysql.MySQLParser,
sql string,
lctn int64,
parserSQLMode string,
) (tree.Statement, string, bool, error) {
if !fragmentHasStatement(sql, parserSQLMode) {
return nil, "", false, nil
}
stmt, err := parseSingleStatementWithSQLMode(execCtx, p, sql, lctn, parserSQLMode)
if err != nil {
return nil, "", false, err
}
return stmt, "", true, nil
}

func parseSingleStatementWithSQLMode(
execCtx *ExecCtx,
p *mysql.MySQLParser,
sql string,
lctn int64,
parserSQLMode string,
) (tree.Statement, error) {
parsed, err := p.ParseWithSQLMode(execCtx.reqCtx, sql, lctn, parserSQLMode)
if err != nil {
return nil, err
}
if len(parsed) != 1 {
freeStatements(parsed)
return nil, moerr.NewParseError(execCtx.reqCtx, "syntax error, or too many sql to parse")
}
return parsed[0], nil
}

func fragmentHasStatement(fragment string, parserSQLMode string) bool {
if strings.TrimSpace(fragment) == "" {
return false
}
scanner := mysql.NewScannerWithSQLMode(dialect.MYSQL, fragment, mysql.ParseSQLModeFlags(parserSQLMode))
defer mysql.PutScanner(scanner)
typ, _ := scanner.Scan()
return typ != 0 && typ != mysql.EofChar()
}

func hasStaticSQLModeSet(stmts []tree.Statement) bool {
for _, stmt := range stmts {
if _, ok := staticSQLModeSetValue(stmt); ok {
return true
}
}
return false
}

func staticSQLModeSetValue(stmt tree.Statement) (string, bool) {
setVar, ok := stmt.(*tree.SetVar)
if !ok {
return "", false
}

var parserSQLMode string
found := false
for _, assignment := range setVar.Assignments {
if assignment == nil || !assignment.System || assignment.Global || !strings.EqualFold(assignment.Name, "sql_mode") {
continue
}
value, ok := staticSQLModeExprValue(assignment.Value)
if !ok {
continue
}
converted, err := gSysVarsDefs["sql_mode"].GetType().Convert(value)
if err != nil {
continue
}
mode, ok := converted.(string)
if !ok {
continue
}
parserSQLMode = mysql.SessionSQLModeForParser(mode)
found = true
}
return parserSQLMode, found
}

func staticSQLModeExprValue(expr tree.Expr) (interface{}, bool) {
switch v := expr.(type) {
case *tree.DefaultVal:
return gSysVarsDefs["sql_mode"].Default, true
case *tree.UnresolvedName:
return v.ColName(), true
case *tree.NumVal:
switch v.Kind() {
case tree.Str:
return v.String(), true
case tree.Int:
if i, ok := v.Int64(); ok {
return i, true
}
if u, ok := v.Uint64(); ok {
return u, true
}
case tree.Float:
if f, ok := v.Float64(); ok {
return f, true
}
case tree.Bool:
return v.Bool(), true
}
}
return nil, false
}

func freeStatements(stmts []tree.Statement) {
for _, stmt := range stmts {
if stmt != nil {
stmt.Free()
}
}
}

func incTransactionCounter(tenant string, tenantId uint32) {
Expand Down
153 changes: 153 additions & 0 deletions pkg/frontend/mysql_cmd_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,159 @@ func Test_HandlePrepareStmt(t *testing.T) {
})
}

func Test_HandlePrepareStringUsesSessionSQLMode(t *testing.T) {
ctx := defines.AttachAccountId(context.TODO(), catalog.System_Account)
setSessionAlloc("", NewLeakCheckAllocator())
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ec := newTestExecCtx(ctx, ctrl)

runTestHandle("handlePrepareStringUsesSessionSQLMode", t, func(ses *Session) error {
require.NoError(t, ses.SetSessionSysVar(ctx, "sql_mode", "PIPES_AS_CONCAT"))
ec.resper = ses.respr
preStmt, err := handlePrepareString(ses, ec, tree.NewPrepareString("stmt_sql_mode", "select 'a'||'b'"))
if err != nil {
return err
}
defer preStmt.Close()
requirePreparedSelectConcat(t, preStmt)
return nil
})
}

func Test_HandlePrepareVarUsesSessionSQLMode(t *testing.T) {
ctx := defines.AttachAccountId(context.TODO(), catalog.System_Account)
setSessionAlloc("", NewLeakCheckAllocator())
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ec := newTestExecCtx(ctx, ctrl)

runTestHandle("handlePrepareVarUsesSessionSQLMode", t, func(ses *Session) error {
require.NoError(t, ses.SetSessionSysVar(ctx, "sql_mode", "PIPES_AS_CONCAT"))
require.NoError(t, ses.SetUserDefinedVar("stmt_var_sql", "select 'a'||'b'", "set @stmt_var_sql = 'select ''a''||''b'''"))
ec.resper = ses.respr
preStmt, err := handlePrepareVar(ses, ec, tree.NewPrepareVar("stmt_sql_mode_var", tree.NewVarExpr("stmt_var_sql", false, false, nil)))
if err != nil {
return err
}
defer preStmt.Close()
requirePreparedSelectConcat(t, preStmt)
return nil
})
}

func requirePreparedSelectConcat(t *testing.T, preStmt *PrepareStmt) {
t.Helper()
selectStmt, ok := preStmt.PrepareStmt.(*tree.Select)
require.True(t, ok)
selectClause, ok := selectStmt.Select.(*tree.SelectClause)
require.True(t, ok)
require.Len(t, selectClause.Exprs, 1)
fn, ok := selectClause.Exprs[0].Expr.(*tree.FuncExpr)
require.True(t, ok)
name, ok := fn.Func.FunctionReference.(*tree.UnresolvedName)
require.True(t, ok)
require.Equal(t, "concat", name.ColName())
}

func Test_ParseSqlUsesSQLModeSetEarlierInSamePacket(t *testing.T) {
ctx := defines.AttachAccountId(context.TODO(), catalog.System_Account)
setPu("", config.NewParameterUnit(&config.FrontendParameters{}, nil, nil, nil))
ses := NewSession(ctx, "", &testMysqlWriter{}, nil)

t.Run("ansi quotes", func(t *testing.T) {
stmts := parseSQLModePacket(t, ctx, ses, `set sql_mode=ANSI_QUOTES; select "c"`)
defer freeStatements(stmts)
require.Len(t, stmts, 2)

selectStmt, ok := stmts[1].(*tree.Select)
require.True(t, ok)
selectClause, ok := selectStmt.Select.(*tree.SelectClause)
require.True(t, ok)
require.Len(t, selectClause.Exprs, 1)
name, ok := selectClause.Exprs[0].Expr.(*tree.UnresolvedName)
require.True(t, ok)
require.Equal(t, "c", name.ColName())
})

t.Run("real as float", func(t *testing.T) {
require.NoError(t, ses.SetSessionSysVar(ctx, "sql_mode", ""))
stmts := parseSQLModePacket(t, ctx, ses, "set sql_mode=REAL_AS_FLOAT; create table t(r real)")
defer freeStatements(stmts)
require.Len(t, stmts, 2)

createStmt, ok := stmts[1].(*tree.CreateTable)
require.True(t, ok)
require.Len(t, createStmt.Defs, 1)
col, ok := createStmt.Defs[0].(*tree.ColumnTableDef)
require.True(t, ok)
require.Equal(t, uint32(defines.MYSQL_TYPE_FLOAT), col.Type.(*tree.T).InternalType.Oid)
})

t.Run("no backslash escapes", func(t *testing.T) {
stmts := parseSQLModePacket(t, ctx, ses, `set sql_mode=NO_BACKSLASH_ESCAPES; select 'a\'; select 1`)
defer freeStatements(stmts)
require.Len(t, stmts, 3)

selectStmt, ok := stmts[1].(*tree.Select)
require.True(t, ok)
selectClause, ok := selectStmt.Select.(*tree.SelectClause)
require.True(t, ok)
require.Len(t, selectClause.Exprs, 1)
lit, ok := selectClause.Exprs[0].Expr.(*tree.NumVal)
require.True(t, ok)
require.Equal(t, `a\`, lit.String())
})

t.Run("disable no backslash escapes", func(t *testing.T) {
require.NoError(t, ses.SetSessionSysVar(ctx, "sql_mode", "NO_BACKSLASH_ESCAPES"))
execCtx := &ExecCtx{
reqCtx: ctx,
ses: ses,
input: &UserInput{
sql: `set sql_mode=''; select 'a\'; select 1`,
},
}
_, err := parseSql(execCtx, ses.GetMySQLParser())
require.Error(t, err)
})

t.Run("compound statement keeps internal semicolons", func(t *testing.T) {
stmts := parseSQLModePacket(t, ctx, ses, `set sql_mode=ANSI_QUOTES; begin select if(1, 2, 3); end; select "c"`)
defer freeStatements(stmts)
require.Len(t, stmts, 3)

compoundStmt, ok := stmts[1].(*tree.CompoundStmt)
require.True(t, ok)
require.NotEmpty(t, compoundStmt.Stmts)
require.NotNil(t, compoundStmt.Stmts[0])

selectStmt, ok := stmts[2].(*tree.Select)
require.True(t, ok)
selectClause, ok := selectStmt.Select.(*tree.SelectClause)
require.True(t, ok)
require.Len(t, selectClause.Exprs, 1)
name, ok := selectClause.Exprs[0].Expr.(*tree.UnresolvedName)
require.True(t, ok)
require.Equal(t, "c", name.ColName())
})
}

func parseSQLModePacket(t *testing.T, ctx context.Context, ses *Session, sql string) []tree.Statement {
t.Helper()
require.NoError(t, ses.SetSessionSysVar(ctx, "sql_mode", ""))
execCtx := &ExecCtx{
reqCtx: ctx,
ses: ses,
input: &UserInput{
sql: sql,
},
}
stmts, err := parseSql(execCtx, ses.GetMySQLParser())
require.NoError(t, err)
return stmts
}

func Test_HandleDeallocate(t *testing.T) {
ctx := defines.AttachAccountId(context.TODO(), catalog.System_Account)
stmt, err := parsers.ParseOne(ctx, dialect.MYSQL, "deallocate Prepare stmt1", 1)
Expand Down
Loading
Loading