Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ebc1e8d
fix(plan): preserve enum and set types in views
ck89119 Aug 3, 2026
1d83ee3
fix(plan): preserve special types across view binding
ck89119 Aug 3, 2026
59ae5b9
Merge branch 'main' into issue-26226-main
ck89119 Aug 3, 2026
e476ffe
fix(plan): preserve view distinct display semantics
ck89119 Aug 3, 2026
eaf3bf0
fix(plan): preserve set bitmap across view assignments
ck89119 Aug 3, 2026
e50d8e3
fix(plan): limit set passthrough to bare columns
ck89119 Aug 3, 2026
5ecd810
Merge remote-tracking branch 'mo/main' into issue-26226-main
ck89119 Aug 3, 2026
bb144fc
fix(plan): guard missing set projection columns
ck89119 Aug 3, 2026
037e846
fix(plan): preserve view catalog type provenance
ck89119 Aug 3, 2026
e21c569
Merge remote-tracking branch 'mo/main' into issue-26226-main
ck89119 Aug 3, 2026
d1c7cdf
Merge remote-tracking branch 'mo/main' into issue-26226-main
ck89119 Aug 3, 2026
630e72a
fix(plan): restore view types through transparent queries
ck89119 Aug 3, 2026
d46864f
Merge remote-tracking branch 'mo/main' into issue-26226-main
ck89119 Aug 4, 2026
88258ec
fix(plan): preserve set values across view ordering
ck89119 Aug 4, 2026
2cf1961
Merge remote-tracking branch 'mo/main' into issue-26226-main
ck89119 Aug 4, 2026
17a8c49
Merge remote-tracking branch 'mo/main' into issue-26226-main
ck89119 Aug 4, 2026
63a401c
fix(plan): generalize output column provenance
ck89119 Aug 4, 2026
2e8ec9f
Merge remote-tracking branch 'mo/main' into issue-26226-main
ck89119 Aug 4, 2026
6b1f393
refactor(plan): narrow output provenance metadata
ck89119 Aug 4, 2026
c7388c3
Merge remote-tracking branch 'mo/main' into issue-26226-main
ck89119 Aug 4, 2026
5e242cc
fix(plan): canonicalize semantic view values
ck89119 Aug 4, 2026
8e90d5d
fix(plan): preserve canonical view result types
ck89119 Aug 4, 2026
80108da
fix(plan): clear CTAS defaults at query boundaries
ck89119 Aug 4, 2026
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
6 changes: 5 additions & 1 deletion pkg/sql/plan/base_binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,11 @@ func (b *baseBinder) baseBindColRef(astExpr *tree.UnresolvedName, depth int32, i
return
}

if isEnumOrSetPlanType(typ) {
preserveSpecialValue := typ != nil && b.mysqlSpecialTargetType != nil &&
typ.Enumvalues == b.mysqlSpecialTargetType.Enumvalues &&
((isEnumPlanType(typ) && isEnumPlanType(b.mysqlSpecialTargetType)) ||
(isSetPlanType(typ) && isSetPlanType(b.mysqlSpecialTargetType)))
if isEnumOrSetPlanType(typ) && !preserveSpecialValue {
if err != nil {
errutil.ReportError(b.GetContext(), err)
return
Expand Down
19 changes: 8 additions & 11 deletions pkg/sql/plan/build_ddl.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,15 @@ func genViewTableDef(ctx CompilerContext, stmt *tree.Select, colNames tree.Ident
originName = string(colNames[idx])
name = originName
}
typ := &expr.Typ
if sourceType, ok := mysqlSpecialTypeSourceType(expr); ok {
typ = sourceType
}
cols[idx] = &plan.ColDef{
Name: strings.ToLower(name),
OriginName: originName,
Alg: plan.CompressType_Lz4,
Typ: expr.Typ,
Typ: *typ,
Default: &plan.Default{
NullAbility: !expr.Typ.NotNullable,
Expr: nil,
Expand Down Expand Up @@ -275,16 +279,9 @@ func genAsSelectCols(ctx CompilerContext, stmt *tree.Select, isPrepareStmt bool)
if binding, ok := bindCtx.bindingByTable[tblName]; ok {
defaultVal = binding.defaults[binding.colIdByName[colName]]
}
case *plan.Expr_F:
// enum
if e.F.Func.ObjName == moEnumCastIndexToValueFun || e.F.Func.ObjName == moSetCastIndexToValueFun {
// cast_index_to_value('apple,banana,orange', cast(col_name as T_uint16))
colRef := e.F.Args[1].Expr.(*plan.Expr_Col).Col
tblName, colName := getTblAndColName(colRef.RelPos, colRef.ColPos)
if binding, ok := bindCtx.bindingByTable[tblName]; ok {
typ = binding.types[binding.colIdByName[colName]]
}
}
}
if sourceType, ok := mysqlSpecialTypeSourceType(expr); ok {
typ = sourceType
}

cols[i] = &plan.ColDef{
Expand Down
281 changes: 281 additions & 0 deletions pkg/sql/plan/build_ddl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,287 @@ func TestBuildCreateViewExplicitColumnList(t *testing.T) {
})
}

func addMySQLSpecialTypeColumns(ctx *MockCompilerContext) {
ctx.tables["nation"].Cols = append(ctx.tables["nation"].Cols,
&plan.ColDef{
Name: "priority",
Typ: plan.Type{
Id: int32(types.T_enum),
Enumvalues: "low,medium,high",
NotNullable: true,
},
},
&plan.ColDef{
Name: "flags",
Typ: plan.Type{
Id: int32(types.T_uint64),
Enumvalues: "red,green,blue",
},
},
)
}

func TestBuildCreateViewPreservesMySQLSpecialColumnTypes(t *testing.T) {
const rootSQL = "create view v (renamed_priority, renamed_flags, renamed_name) as " +
"select priority, flags, n_name from nation"
ctx := &rootSQLCompilerContext{
MockCompilerContext: NewMockCompilerContext(false),
rootSQL: rootSQL,
}
addMySQLSpecialTypeColumns(ctx.MockCompilerContext)

stmt, err := parsers.ParseOne(t.Context(), dialect.MYSQL, rootSQL, 1)
require.NoError(t, err)
defer stmt.Free()

p, err := BuildPlan(ctx, stmt, false)
require.NoError(t, err)
cols := p.GetDdl().GetCreateView().GetTableDef().GetCols()
require.Len(t, cols, 3)
priorityType := cols[0].GetTyp()
flagsType := cols[1].GetTyp()
nameType := cols[2].GetTyp()
require.Equal(t, "renamed_priority", cols[0].GetName())
require.Equal(t, int32(types.T_enum), priorityType.GetId())
require.Equal(t, "low,medium,high", priorityType.GetEnumvalues())
require.True(t, priorityType.GetNotNullable())
require.Equal(t, "renamed_flags", cols[1].GetName())
require.Equal(t, int32(types.T_uint64), flagsType.GetId())
require.Equal(t, "red,green,blue", flagsType.GetEnumvalues())
require.False(t, flagsType.GetNotNullable())
require.Equal(t, "renamed_name", cols[2].GetName())
require.Equal(t, int32(types.T_varchar), nameType.GetId())
}

func TestBuildCTASPreservesMySQLSpecialColumnTypes(t *testing.T) {
const sql = "create table copied as select priority, flags, n_name from nation"
ctx := NewMockCompilerContext(false)
addMySQLSpecialTypeColumns(ctx)
stmt, err := parsers.ParseOne(t.Context(), dialect.MYSQL, sql, 1)
require.NoError(t, err)
defer stmt.Free()

p, err := BuildPlan(ctx, stmt, false)
require.NoError(t, err)
cols := p.GetDdl().GetCreateTable().GetTableDef().GetCols()
require.GreaterOrEqual(t, len(cols), 3)
require.True(t, isEnumPlanType(&cols[0].Typ))
require.Equal(t, "low,medium,high", cols[0].Typ.GetEnumvalues())
require.True(t, isSetPlanType(&cols[1].Typ))
require.Equal(t, "red,green,blue", cols[1].Typ.GetEnumvalues())
require.Equal(t, int32(types.T_varchar), cols[2].Typ.GetId())
}

func TestViewRebindPreservesMySQLSpecialColumnSemantics(t *testing.T) {
const createViewSQL = "create view v_enum_set as select priority, flags, n_name from nation"
ctx := NewMockCompilerContext(false)
addMySQLSpecialTypeColumns(ctx)
createCtx := &rootSQLCompilerContext{MockCompilerContext: ctx, rootSQL: createViewSQL}
stmt, err := parsers.ParseOne(t.Context(), dialect.MYSQL, createViewSQL, 1)
require.NoError(t, err)
createPlan, err := BuildPlan(createCtx, stmt, false)
stmt.Free()
require.NoError(t, err)

viewDef := DeepCopyTableDef(createPlan.GetDdl().GetCreateView().GetTableDef(), true)
viewDef.Name = "v_enum_set"
viewDef.DbName = "tpch"
viewDef.TableType = catalog.SystemViewRel
ctx.tables["v_enum_set"] = viewDef
ctx.objects["v_enum_set"] = &plan.ObjectRef{SchemaName: "tpch", ObjName: "v_enum_set"}

stmt, err = parsers.ParseOne(t.Context(), dialect.MYSQL,
"select priority from v_enum_set order by priority", 1)
require.NoError(t, err)
selectPlan, err := BuildPlan(ctx, stmt, false)
stmt.Free()
require.NoError(t, err)

var sortKey *plan.Expr
for _, node := range selectPlan.GetQuery().GetNodes() {
if node.GetNodeType() == plan.Node_SORT {
require.Len(t, node.GetOrderBy(), 1)
sortKey = node.GetOrderBy()[0].GetExpr()
break
}
}
require.NotNil(t, sortKey)
sortType := sortKey.GetTyp()
require.Equal(t, int32(types.T_enum), sortType.GetId())
require.Equal(t, "low,medium,high", sortType.GetEnumvalues())
query := selectPlan.GetQuery()
require.Len(t, query.GetSteps(), 1)
resultNode := query.GetNodes()[query.GetSteps()[0]]
require.Len(t, resultNode.GetProjectList(), 1)
resultType := resultNode.GetProjectList()[0].GetTyp()
require.Equal(t, int32(types.T_varchar), resultType.GetId())

stmt, err = parsers.ParseOne(t.Context(), dialect.MYSQL,
"select flags from v_enum_set", 1)
require.NoError(t, err)
rawSetPlan, err := BuildPlan(ctx, stmt, false)
stmt.Free()
require.NoError(t, err)
setDisplayFound := false
for _, node := range rawSetPlan.GetQuery().GetNodes() {
for _, project := range node.GetProjectList() {
fn := project.GetF()
if fn == nil {
continue
}
require.NotEqual(t, moSetCastValueToIndexFun, fn.GetFunc().GetObjName(),
"a direct view projection must not round-trip a SET bitmap through its display string")
if fn.GetFunc().GetObjName() == moSetCastIndexToValueFun {
setDisplayFound = true
require.Len(t, fn.GetArgs(), 2)
require.True(t, isSetPlanType(&fn.GetArgs()[1].Typ))
}
}
}
require.True(t, setDisplayFound)

stmt, err = parsers.ParseOne(t.Context(), dialect.MYSQL,
"create table copied_from_view as select priority, flags, n_name from v_enum_set", 1)
require.NoError(t, err)
ctasPlan, err := BuildPlan(ctx, stmt, false)
stmt.Free()
require.NoError(t, err)
cols := ctasPlan.GetDdl().GetCreateTable().GetTableDef().GetCols()
require.GreaterOrEqual(t, len(cols), 3)
require.True(t, isEnumPlanType(&cols[0].Typ))
require.Equal(t, "low,medium,high", cols[0].Typ.GetEnumvalues())
require.True(t, isSetPlanType(&cols[1].Typ))
require.Equal(t, "red,green,blue", cols[1].Typ.GetEnumvalues())
require.Equal(t, int32(types.T_varchar), cols[2].Typ.GetId())

ctasDef := DeepCopyTableDef(ctasPlan.GetDdl().GetCreateTable().GetTableDef(), true)
ctasDef.Name = "copied_from_view"
ctasDef.DbName = "tpch"
ctx.tables[ctasDef.Name] = ctasDef
ctx.objects[ctasDef.Name] = &plan.ObjectRef{SchemaName: "tpch", ObjName: ctasDef.Name}
stmt, err = parsers.ParseOne(t.Context(), dialect.MYSQL,
ctasPlan.GetDdl().GetCreateTable().GetCreateAsSelectSql(), 1)
require.NoError(t, err)
insertPlan, err := BuildPlan(ctx, stmt, false)
stmt.Free()
require.NoError(t, err)
for _, node := range insertPlan.GetQuery().GetNodes() {
for _, project := range node.GetProjectList() {
if fn := project.GetF(); fn != nil {
require.NotEqual(t, moSetCastValueToIndexFun, fn.GetFunc().GetObjName(),
"CTAS INSERT must retain the projected SET bitmap: node=%d type=%s expr=%s",
node.GetNodeId(), node.GetNodeType().String(), project.String())
}
}
}

stmt, err = parsers.ParseOne(t.Context(), dialect.MYSQL,
"insert into copied_from_view (priority, flags, n_name) "+
"select priority, concat(flags, ',green'), n_name from v_enum_set", 1)
require.NoError(t, err)
nestedPlan, err := BuildPlan(ctx, stmt, false)
stmt.Free()
require.NoError(t, err)
nestedDisplayFound := false
for _, node := range nestedPlan.GetQuery().GetNodes() {
for _, project := range node.GetProjectList() {
walkPlanExpr(project, func(expr *plan.Expr) {
if fn := expr.GetF(); fn != nil && fn.GetFunc().GetObjName() == moSetCastIndexToValueFun {
nestedDisplayFound = true
}
})
}
}
require.True(t, nestedDisplayFound,
"a SET column nested in CONCAT must keep its SQL-visible string semantics")
}

func TestViewSpecialTypeBoundaryPreservesDistinctVisibleValues(t *testing.T) {
const createViewSQL = "create view v_distinct_set as select distinct flags from nation"
ctx := NewMockCompilerContext(false)
addMySQLSpecialTypeColumns(ctx)
createCtx := &rootSQLCompilerContext{MockCompilerContext: ctx, rootSQL: createViewSQL}
stmt, err := parsers.ParseOne(t.Context(), dialect.MYSQL, createViewSQL, 1)
require.NoError(t, err)
createPlan, err := BuildPlan(createCtx, stmt, false)
stmt.Free()
require.NoError(t, err)

viewDef := DeepCopyTableDef(createPlan.GetDdl().GetCreateView().GetTableDef(), true)
viewDef.Name = "v_distinct_set"
viewDef.DbName = "tpch"
viewDef.TableType = catalog.SystemViewRel
ctx.tables[viewDef.Name] = viewDef
ctx.objects[viewDef.Name] = &plan.ObjectRef{SchemaName: "tpch", ObjName: viewDef.Name}

stmt, err = parsers.ParseOne(t.Context(), dialect.MYSQL, "select flags from v_distinct_set", 1)
require.NoError(t, err)
queryPlan, err := BuildPlan(ctx, stmt, false)
stmt.Free()
require.NoError(t, err)

query := queryPlan.GetQuery()
var viewBoundary *plan.Node
setDisplayProjects := 0
var distinctGroupType *plan.Type
for _, node := range query.GetNodes() {
if node.GetNodeType() == plan.Node_AGG && len(node.GetGroupBy()) == 1 {
distinctGroupType = &node.GetGroupBy()[0].Typ
}
if node.GetNodeType() != plan.Node_PROJECT || len(node.GetProjectList()) != 1 {
continue
}
fn := node.GetProjectList()[0].GetF()
if fn == nil {
continue
}
switch fn.GetFunc().GetObjName() {
case moSetCastIndexToValueFun:
setDisplayProjects++
case moSetCastValueToIndexFun:
viewBoundary = node
}
}

require.NotNil(t, distinctGroupType)
require.Equal(t, int32(types.T_varchar), distinctGroupType.GetId(),
"DISTINCT must consume the SQL-visible SET value")
require.GreaterOrEqual(t, setDisplayProjects, 2,
"both the view's semantic projection and the outer result need display wrappers")
require.NotNil(t, viewBoundary)
require.True(t, isSetPlanType(&viewBoundary.GetProjectList()[0].Typ),
"only the completed view boundary should restore the SET type")
}

func TestMySQLSpecialTypeSourceTypeRejectsNonTransparentExpressions(t *testing.T) {
enumType := plan.Type{Id: int32(types.T_enum), Enumvalues: "low,high"}
valid := &plan.Expr{
Expr: &plan.Expr_F{F: &plan.Function{
Func: &plan.ObjectRef{ObjName: moEnumCastIndexToValueFun},
Args: []*plan.Expr{
{Typ: plan.Type{Id: int32(types.T_varchar)}},
{Typ: enumType, Expr: &plan.Expr_Col{Col: &plan.ColRef{RelPos: 1, ColPos: 2}}},
},
}},
}

got, ok := mysqlSpecialTypeSourceType(valid)
require.True(t, ok)
require.Equal(t, enumType, *got)

for _, mutate := range []func(*plan.Expr){
func(expr *plan.Expr) { expr.GetF().Args = expr.GetF().Args[:1] },
func(expr *plan.Expr) { expr.GetF().Args[1].Expr = nil },
func(expr *plan.Expr) { expr.GetF().Args[1].Typ.Id = int32(types.T_varchar) },
func(expr *plan.Expr) { expr.GetF().Func.ObjName = "concat" },
} {
expr := DeepCopyExpr(valid)
mutate(expr)
_, ok = mysqlSpecialTypeSourceType(expr)
require.False(t, ok)
}
}

func TestBuildCreateViewRejectsTemporaryTable(t *testing.T) {
tests := []string{
"create view v as select * from nation",
Expand Down
13 changes: 13 additions & 0 deletions pkg/sql/plan/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,19 @@ func TestInsertSelectProjectedSetUsesStoredBitmap(t *testing.T) {
require.True(t, planHasPlainUint64ColRef(logicPlan))
}

func TestInsertSelectSetTargetRejectsUnknownSourceColumn(t *testing.T) {
mock := NewMockOptimizer(true)
addSetBitmapDestinationForTest(mock)
mock.ctxt.tables["set_bitmap_destination"].Cols[1].Typ.Enumvalues = "a,b"

_, err := runOneStmt(
mock,
t,
"insert into set_bitmap_destination(id, bitmap) select n_nationkey, missing from nation",
)
require.ErrorContains(t, err, "column missing does not exist")
}

func addSetBitmapDestinationForTest(mock *MockOptimizer) {
const tableName = "set_bitmap_destination"
idType := plan.Type{Id: int32(types.T_int32), NotNullable: true}
Expand Down
31 changes: 31 additions & 0 deletions pkg/sql/plan/mysql_special_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,37 @@ func mysqlSpecialTypeFuncNames(typ *plan.Type) (string, string, string, error) {
}
}

// mysqlSpecialTypeSourceType returns the source ENUM/SET type for the transparent
// display wrapper inserted by the column binder. Persisted output schemas use
// this type instead of the wrapper's VARCHAR result type.
func mysqlSpecialTypeSourceType(expr *plan.Expr) (*plan.Type, bool) {
sourceExpr, ok := mysqlSpecialTypeSourceExpr(expr)
if !ok {
return nil, false
}
return &sourceExpr.Typ, true
}

func mysqlSpecialTypeSourceExpr(expr *plan.Expr) (*plan.Expr, bool) {
if expr == nil {
return nil, false
}
fn := expr.GetF()
if fn == nil || fn.Func == nil || len(fn.Args) != 2 || fn.Args[1] == nil || fn.Args[1].GetCol() == nil {
return nil, false
}

sourceExpr := fn.Args[1]
switch fn.Func.ObjName {
case moEnumCastIndexToValueFun:
return sourceExpr, isEnumPlanType(&sourceExpr.Typ)
case moSetCastIndexToValueFun:
return sourceExpr, isSetPlanType(&sourceExpr.Typ)
default:
return nil, false
}
}

// mysqlSpecialOrderTypeForExpr returns the storage type whose definition order
// belongs to a visible string expression. Provenance is deliberately narrow:
// an exact ENUM/SET display call originates it, and an exact ColRef may carry it
Expand Down
Loading
Loading