Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release-18.0] Handle Nullability for Columns from Outer Tables (#16174) #16184

Closed
wants to merge 1 commit into from
Closed
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
171 changes: 171 additions & 0 deletions go/test/endtoend/vtgate/queries/misc/misc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,174 @@ func TestAnalyze(t *testing.T) {
})
}
}
<<<<<<< HEAD
=======

// TestTransactionModeVar executes SELECT on `transaction_mode` variable
func TestTransactionModeVar(t *testing.T) {
utils.SkipIfBinaryIsBelowVersion(t, 19, "vtgate")

mcmp, closer := start(t)
defer closer()

tcases := []struct {
setStmt string
expRes string
}{{
expRes: `[[VARCHAR("MULTI")]]`,
}, {
setStmt: `set transaction_mode = single`,
expRes: `[[VARCHAR("SINGLE")]]`,
}, {
setStmt: `set transaction_mode = multi`,
expRes: `[[VARCHAR("MULTI")]]`,
}, {
setStmt: `set transaction_mode = twopc`,
expRes: `[[VARCHAR("TWOPC")]]`,
}}

for _, tcase := range tcases {
mcmp.Run(tcase.setStmt, func(mcmp *utils.MySQLCompare) {
if tcase.setStmt != "" {
utils.Exec(t, mcmp.VtConn, tcase.setStmt)
}
utils.AssertMatches(t, mcmp.VtConn, "select @@transaction_mode", tcase.expRes)
})
}
}

// TestAliasesInOuterJoinQueries tests that aliases work in queries that have outer join clauses.
func TestAliasesInOuterJoinQueries(t *testing.T) {
utils.SkipIfBinaryIsBelowVersion(t, 20, "vtgate")

mcmp, closer := start(t)
defer closer()

// Insert data into the 2 tables
mcmp.Exec("insert into t1(id1, id2) values (1,2), (42,5), (5, 42)")
mcmp.Exec("insert into tbl(id, unq_col, nonunq_col) values (1,2,3), (2,5,3), (3, 42, 2)")

// Check that the select query works as intended and verifying the column names as well.
mcmp.ExecWithColumnCompare("select t1.id1 as t0, t1.id1 as t1, tbl.unq_col as col from t1 left outer join tbl on t1.id2 = tbl.nonunq_col")
mcmp.ExecWithColumnCompare("select t1.id1 as t0, t1.id1 as t1, tbl.unq_col as col from t1 left outer join tbl on t1.id2 = tbl.nonunq_col order by t1.id2 limit 2")
mcmp.ExecWithColumnCompare("select t1.id1 as t0, t1.id1 as t1, tbl.unq_col as col from t1 left outer join tbl on t1.id2 = tbl.nonunq_col order by t1.id2 limit 2 offset 2")
mcmp.ExecWithColumnCompare("select t1.id1 as t0, t1.id1 as t1, count(*) as leCount from t1 left outer join tbl on t1.id2 = tbl.nonunq_col group by 1, t1")
mcmp.ExecWithColumnCompare("select t.id1, t.id2, derived.unq_col from t1 t join (select id, unq_col, nonunq_col from tbl) as derived on t.id2 = derived.nonunq_col")
}

func TestAlterTableWithView(t *testing.T) {
utils.SkipIfBinaryIsBelowVersion(t, 20, "vtgate")
mcmp, closer := start(t)
defer closer()

// Test that create/alter view works and the output is as expected
mcmp.Exec(`use ks_misc`)
mcmp.Exec(`create view v1 as select * from t1`)
var viewDef string
utils.WaitForVschemaCondition(t, clusterInstance.VtgateProcess, keyspaceName, func(t *testing.T, ksMap map[string]any) bool {
views, ok := ksMap["views"]
if !ok {
return false
}
viewsMap := views.(map[string]any)
view, ok := viewsMap["v1"]
if ok {
viewDef = view.(string)
}
return ok
}, "Waiting for view creation")
mcmp.Exec(`insert into t1(id1, id2) values (1, 1)`)
mcmp.AssertMatches("select * from v1", `[[INT64(1) INT64(1)]]`)

// alter table add column
mcmp.Exec(`alter table t1 add column test bigint`)
time.Sleep(10 * time.Second)
mcmp.Exec(`alter view v1 as select * from t1`)

waitForChange := func(t *testing.T, ksMap map[string]any) bool {
// wait for the view definition to change
views := ksMap["views"]
viewsMap := views.(map[string]any)
newView := viewsMap["v1"]
if newView.(string) == viewDef {
return false
}
viewDef = newView.(string)
return true
}
utils.WaitForVschemaCondition(t, clusterInstance.VtgateProcess, keyspaceName, waitForChange, "Waiting for alter view")

mcmp.AssertMatches("select * from v1", `[[INT64(1) INT64(1) NULL]]`)

// alter table remove column
mcmp.Exec(`alter table t1 drop column test`)
mcmp.Exec(`alter view v1 as select * from t1`)

utils.WaitForVschemaCondition(t, clusterInstance.VtgateProcess, keyspaceName, waitForChange, "Waiting for alter view")

mcmp.AssertMatches("select * from v1", `[[INT64(1) INT64(1)]]`)
}

// TestStraightJoin tests that Vitess respects the ordering of join in a STRAIGHT JOIN query.
func TestStraightJoin(t *testing.T) {
utils.SkipIfBinaryIsBelowVersion(t, 20, "vtgate")
mcmp, closer := start(t)
defer closer()

mcmp.Exec("insert into tbl(id, unq_col, nonunq_col) values (1,0,10), (2,10,10), (3,4,20), (4,30,20), (5,40,10)")
mcmp.Exec(`insert into t1(id1, id2) values (10, 11), (20, 13)`)

mcmp.AssertMatchesNoOrder("select tbl.unq_col, tbl.nonunq_col, t1.id2 from t1 join tbl where t1.id1 = tbl.nonunq_col",
`[[INT64(0) INT64(10) INT64(11)] [INT64(10) INT64(10) INT64(11)] [INT64(4) INT64(20) INT64(13)] [INT64(40) INT64(10) INT64(11)] [INT64(30) INT64(20) INT64(13)]]`,
)
// Verify that in a normal join query, vitess joins tbl with t1.
res, err := mcmp.VtConn.ExecuteFetch("vexplain plan select tbl.unq_col, tbl.nonunq_col, t1.id2 from t1 join tbl where t1.id1 = tbl.nonunq_col", 100, false)
require.NoError(t, err)
require.Contains(t, fmt.Sprintf("%v", res.Rows), "tbl_t1")

// Test the same query with a straight join
mcmp.AssertMatchesNoOrder("select tbl.unq_col, tbl.nonunq_col, t1.id2 from t1 straight_join tbl where t1.id1 = tbl.nonunq_col",
`[[INT64(0) INT64(10) INT64(11)] [INT64(10) INT64(10) INT64(11)] [INT64(4) INT64(20) INT64(13)] [INT64(40) INT64(10) INT64(11)] [INT64(30) INT64(20) INT64(13)]]`,
)
// Verify that in a straight join query, vitess joins t1 with tbl.
res, err = mcmp.VtConn.ExecuteFetch("vexplain plan select tbl.unq_col, tbl.nonunq_col, t1.id2 from t1 straight_join tbl where t1.id1 = tbl.nonunq_col", 100, false)
require.NoError(t, err)
require.Contains(t, fmt.Sprintf("%v", res.Rows), "t1_tbl")
}

func TestColumnAliases(t *testing.T) {
utils.SkipIfBinaryIsBelowVersion(t, 20, "vtgate")
mcmp, closer := start(t)
defer closer()

mcmp.Exec("insert into t1(id1, id2) values (0,0), (1,1)")
mcmp.ExecWithColumnCompare(`select a as k from (select count(*) as a from t1) t`)
}

func TestHandleNullableColumn(t *testing.T) {
utils.SkipIfBinaryIsBelowVersion(t, 21, "vtgate")
require.NoError(t,
utils.WaitForAuthoritative(t, keyspaceName, "tbl", clusterInstance.VtgateProcess.ReadVSchema))
mcmp, closer := start(t)
defer closer()

mcmp.Exec("insert into t1(id1, id2) values (0,0), (1,1), (2,2)")
mcmp.Exec("insert into tbl(id, unq_col, nonunq_col) values (0,0,0), (1,1,6)")
// This query tests that we handle nullable columns correctly
// tbl.nonunq_col is not nullable according to the schema, but because of the left join, it can be NULL
mcmp.ExecWithColumnCompare(`select * from t1 left join tbl on t1.id2 = tbl.id where t1.id1 = 6 or tbl.nonunq_col = 6`)
}

func TestEnumSetVals(t *testing.T) {
utils.SkipIfBinaryIsBelowVersion(t, 20, "vtgate")

mcmp, closer := start(t)
defer closer()
require.NoError(t, utils.WaitForAuthoritative(t, keyspaceName, "tbl_enum_set", clusterInstance.VtgateProcess.ReadVSchema))

mcmp.Exec("insert into tbl_enum_set(id, enum_col, set_col) values (1, 'medium', 'a,b,e'), (2, 'small', 'e,f,g'), (3, 'large', 'c'), (4, 'xsmall', 'a,b'), (5, 'medium', 'a,d')")

mcmp.AssertMatches("select id, enum_col, cast(enum_col as signed) from tbl_enum_set order by enum_col, id", `[[INT64(4) ENUM("xsmall") INT64(1)] [INT64(2) ENUM("small") INT64(2)] [INT64(1) ENUM("medium") INT64(3)] [INT64(5) ENUM("medium") INT64(3)] [INT64(3) ENUM("large") INT64(4)]]`)
mcmp.AssertMatches("select id, set_col, cast(set_col as unsigned) from tbl_enum_set order by set_col, id", `[[INT64(4) SET("a,b") UINT64(3)] [INT64(3) SET("c") UINT64(4)] [INT64(5) SET("a,d") UINT64(9)] [INT64(1) SET("a,b,e") UINT64(19)] [INT64(2) SET("e,f,g") UINT64(112)]]`)
}
>>>>>>> 5a6f3868c5 (Handle Nullability for Columns from Outer Tables (#16174))
43 changes: 42 additions & 1 deletion go/test/endtoend/vtgate/queries/misc/schema.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,46 @@
<<<<<<< HEAD
create table if not exists t1(
id1 bigint,
id2 bigint,
primary key(id1)
) Engine=InnoDB;
) Engine=InnoDB;
=======
create table t1
(
id1 bigint,
id2 bigint,
primary key (id1)
) Engine=InnoDB;

create table unq_idx
(
unq_col bigint,
keyspace_id varbinary(20),
primary key (unq_col)
) Engine = InnoDB;

create table nonunq_idx
(
nonunq_col bigint,
id bigint,
keyspace_id varbinary(20),
primary key (nonunq_col, id)
) Engine = InnoDB;

create table tbl
(
id bigint,
unq_col bigint,
nonunq_col bigint not null,
primary key (id),
unique (unq_col)
) Engine = InnoDB;

create table tbl_enum_set
(
id bigint,
enum_col enum('xsmall', 'small', 'medium', 'large', 'xlarge'),
set_col set('a', 'b', 'c', 'd', 'e', 'f', 'g'),
primary key (id)
) Engine = InnoDB;
>>>>>>> 5a6f3868c5 (Handle Nullability for Columns from Outer Tables (#16174))
128 changes: 128 additions & 0 deletions go/vt/vtgate/evalengine/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,135 @@ type ctype struct {
Col collations.TypedCollation
}

<<<<<<< HEAD
func (ct ctype) nullable() bool {
=======
type Type struct {
typ sqltypes.Type
collation collations.ID
nullable bool
init bool
size, scale int32
values *EnumSetValues
}

func (v *EnumSetValues) Equal(other *EnumSetValues) bool {
if v == nil && other == nil {
return true
}
if v == nil || other == nil {
return false
}
return slices.Equal(*v, *other)
}

func NewType(t sqltypes.Type, collation collations.ID) Type {
// New types default to being nullable
return NewTypeEx(t, collation, true, 0, 0, nil)
}

func NewTypeEx(t sqltypes.Type, collation collations.ID, nullable bool, size, scale int32, values *EnumSetValues) Type {
return Type{
typ: t,
collation: collation,
nullable: nullable,
init: true,
size: size,
scale: scale,
values: values,
}
}

func NewTypeFromField(f *querypb.Field) Type {
return Type{
typ: f.Type,
collation: collations.ID(f.Charset),
nullable: f.Flags&uint32(querypb.MySqlFlag_NOT_NULL_FLAG) == 0,
init: true,
size: int32(f.ColumnLength),
scale: int32(f.Decimals),
}
}

func (t *Type) ToField(name string) *querypb.Field {
// need to get the proper flags for the type; usually leaving flags
// to 0 is OK, because Vitess' MySQL client will generate the right
// ones for the column's type, but here we're also setting the NotNull
// flag, so it needs to be set with the full flags for the column
_, flags := sqltypes.TypeToMySQL(t.typ)
if !t.nullable {
flags |= int64(querypb.MySqlFlag_NOT_NULL_FLAG)
}

f := &querypb.Field{
Name: name,
Type: t.typ,
Charset: uint32(t.collation),
ColumnLength: uint32(t.size),
Decimals: uint32(t.scale),
Flags: uint32(flags),
}
return f
}

func (t *Type) Type() sqltypes.Type {
if t.init {
return t.typ
}
return sqltypes.Unknown
}

func (t *Type) Collation() collations.ID {
return t.collation
}

func (t *Type) Size() int32 {
return t.size
}

func (t *Type) Scale() int32 {
return t.scale
}

func (t *Type) Nullable() bool {
if t.init {
return t.nullable
}
return true // nullable by default for unknown types
}

func (t *Type) SetNullability(n bool) {
t.nullable = n
}

func (t *Type) Values() *EnumSetValues {
return t.values
}

func (t *Type) Valid() bool {
return t.init
}

func (t *Type) Equal(other *Type) bool {
return t.typ == other.typ &&
t.collation == other.collation &&
t.nullable == other.nullable &&
t.size == other.size &&
t.scale == other.scale &&
t.values.Equal(other.values)
}

func (ct *ctype) equal(other ctype) bool {
return ct.Type == other.Type &&
ct.Flag == other.Flag &&
ct.Size == other.Size &&
ct.Scale == other.Scale &&
ct.Col == other.Col &&
ct.Values.Equal(other.Values)
}

func (ct *ctype) nullable() bool {
>>>>>>> 5a6f3868c5 (Handle Nullability for Columns from Outer Tables (#16174))
return ct.Flag&flagNullable != 0
}

Expand Down
Loading
Loading