diff --git a/go/test/endtoend/vtgate/queries/misc/misc_test.go b/go/test/endtoend/vtgate/queries/misc/misc_test.go index 1776d489bbf..e5dd8927245 100644 --- a/go/test/endtoend/vtgate/queries/misc/misc_test.go +++ b/go/test/endtoend/vtgate/queries/misc/misc_test.go @@ -36,7 +36,7 @@ func start(t *testing.T) (utils.MySQLCompare, func()) { require.NoError(t, err) deleteAll := func() { - tables := []string{"t1", "tbl", "unq_idx", "nonunq_idx", "tbl_enum_set", "uks.unsharded"} + tables := []string{"t1", "tbl", "unq_idx", "nonunq_idx", "tbl_enum_set", "uks.unsharded", "all_types"} for _, table := range tables { _, _ = mcmp.ExecAndIgnore("delete from " + table) } @@ -596,3 +596,48 @@ func TestSemiJoin(t *testing.T) { }) } } + +// TestTabletTypeRouting tests that the tablet type routing works as intended. +func TestTabletTypeRouting(t *testing.T) { + // We are gonna configure the routing rules to send the + // query for a replica tablet in ks_misc.t1 to go to a table that doesn't exist. + // I know this doesn't make much practical sense, but makes testing really easy. + routingRules := `{"rules": [ + { + "from_table": "ks_misc.t1@replica", + "to_tables": ["uks.unknown"] + } +]}` + err := clusterInstance.VtctldClientProcess.ApplyRoutingRules(routingRules) + require.NoError(t, err) + defer func() { + // Clear the routing rules after the test. + err = clusterInstance.VtctldClientProcess.ApplyRoutingRules("{}") + require.NoError(t, err) + }() + + mcmp, closer := start(t) + defer closer() + + mcmp.Exec("insert into t1(id1, id2) values (0,0)") + + vtConn := mcmp.VtConn + // We first verify that querying the primary tablet goes to the t1 table. + utils.Exec(t, vtConn, "use ks_misc@primary") + utils.AssertMatches(t, vtConn, "select * from ks_misc.t1", `[[INT64(0) INT64(0)]]`) + // Now we change the connection's target + utils.Exec(t, vtConn, "use ks_misc@replica") + // We verify that querying the replica tablet creates an unknown table error. + _, err = utils.ExecAllowError(t, vtConn, "select * from ks_misc.t1") + require.ErrorContains(t, err, "table unknown not found") +} + +// TestJoinMixedCaseExpr tests that join condition with expression from both table having in clause is handled correctly. +func TestJoinMixedCaseExpr(t *testing.T) { + mcmp, closer := start(t) + defer closer() + + mcmp.Exec(`insert into all_types(id, int_unsigned) values (1, 1), (2, 2), (3,3), (4,4), (10,5), (20, 6)`) + mcmp.Exec(`prepare prep_pk from 'SELECT t1.id from all_types t1 join all_types t2 on t1.int_unsigned = (case when t2.int_unsigned in (1, 2, 3) then 1 when t2.int_unsigned = 4 then 10 else 20 end)'`) + mcmp.AssertMatches(`execute prep_pk`, `[[INT64(1)] [INT64(1)] [INT64(1)]]`) +} diff --git a/go/vt/vtgate/evalengine/translate_simplify.go b/go/vt/vtgate/evalengine/translate_simplify.go index 6af7f7646a0..18be4841de5 100644 --- a/go/vt/vtgate/evalengine/translate_simplify.go +++ b/go/vt/vtgate/evalengine/translate_simplify.go @@ -138,10 +138,24 @@ func simplifyExpr(env *ExpressionEnv, e IR) (IR, error) { if err != nil { return nil, err } - return &Literal{inner: simplified}, nil + return evalToIR(simplified), nil } if err := e.simplify(env); err != nil { return nil, err } return e, nil } + +// evalToIR turns an internal eval result into an IR: +// - if it’s an evalTuple, it recurses into a TupleExpr +// - otherwise it wraps the single value in a Literal +func evalToIR(simplified eval) IR { + if tuple, isTuple := simplified.(*evalTuple); isTuple { + te := make(TupleExpr, len(tuple.t)) + for i, t := range tuple.t { + te[i] = evalToIR(t) + } + return te + } + return &Literal{inner: simplified} +} diff --git a/go/vt/vtgate/planbuilder/testdata/from_cases.json b/go/vt/vtgate/planbuilder/testdata/from_cases.json index 3bc9eae96c1..d63b506e66f 100644 --- a/go/vt/vtgate/planbuilder/testdata/from_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/from_cases.json @@ -4720,5 +4720,49 @@ "user.user_extra" ] } + }, + { + "comment": "case statement in join condition for evaluation - tuple‐expr simplification in CASE", + "query": "select 1 from user u1 join user u2 on u1.col = (case when u2.col = 2 then 0 when u2.col in (1, 2) then 1 else 2 end)", + "plan": { + "QueryType": "SELECT", + "Original": "select 1 from user u1 join user u2 on u1.col = (case when u2.col = 2 then 0 when u2.col in (1, 2) then 1 else 2 end)", + "Instructions": { + "OperatorType": "Join", + "Variant": "Join", + "JoinColumnIndexes": "L:0", + "JoinVars": { + "u1_col": 1 + }, + "TableName": "`user`_`user`", + "Inputs": [ + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select 1, u1.col from `user` as u1 where 1 != 1", + "Query": "select 1, u1.col from `user` as u1", + "Table": "`user`" + }, + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select 1 from `user` as u2 where 1 != 1", + "Query": "select 1 from `user` as u2 where :u1_col = case when u2.col = 2 then 0 when u2.col in (1, 2) then 1 else 2 end", + "Table": "`user`" + } + ] + }, + "TablesUsed": [ + "user.user" + ] + } } ]