Skip to content

Commit 0eadbe8

Browse files
Fixing Column aliases in outer join queries (#15384)
Signed-off-by: Manan Gupta <manan@planetscale.com> Signed-off-by: Florent Poinsard <35779988+frouioui@users.noreply.github.com> Co-authored-by: Florent Poinsard <35779988+frouioui@users.noreply.github.com>
1 parent 259d963 commit 0eadbe8

14 files changed

+167
-56
lines changed

go/test/endtoend/vtgate/queries/misc/misc_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,3 +352,22 @@ func TestTransactionModeVar(t *testing.T) {
352352
})
353353
}
354354
}
355+
356+
// TestAliasesInOuterJoinQueries tests that aliases work in queries that have outer join clauses.
357+
func TestAliasesInOuterJoinQueries(t *testing.T) {
358+
utils.SkipIfBinaryIsBelowVersion(t, 20, "vtgate")
359+
360+
mcmp, closer := start(t)
361+
defer closer()
362+
363+
// Insert data into the 2 tables
364+
mcmp.Exec("insert into t1(id1, id2) values (1,2), (42,5), (5, 42)")
365+
mcmp.Exec("insert into tbl(id, unq_col, nonunq_col) values (1,2,3), (2,5,3), (3, 42, 2)")
366+
367+
// Check that the select query works as intended and then run it again verifying the column names as well.
368+
mcmp.AssertMatches("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", `[[INT64(1) INT64(1) INT64(42)] [INT64(5) INT64(5) NULL] [INT64(42) INT64(42) NULL]]`)
369+
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")
370+
371+
mcmp.AssertMatches("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", `[[INT64(1) INT64(1) INT64(42)] [INT64(42) INT64(42) NULL]]`)
372+
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")
373+
}

go/vt/vtgate/engine/cached_size.go

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go/vt/vtgate/engine/simple_projection.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package engine
1919
import (
2020
"context"
2121

22+
"google.golang.org/protobuf/proto"
23+
2224
"vitess.io/vitess/go/sqltypes"
2325
querypb "vitess.io/vitess/go/vt/proto/query"
2426
)
@@ -29,8 +31,10 @@ var _ Primitive = (*SimpleProjection)(nil)
2931
type SimpleProjection struct {
3032
// Cols defines the column numbers from the underlying primitive
3133
// to be returned.
32-
Cols []int
33-
Input Primitive
34+
Cols []int
35+
// ColNames are the column names to use for the columns.
36+
ColNames []string
37+
Input Primitive
3438
}
3539

3640
// NeedsTransaction implements the Primitive interface
@@ -104,8 +108,13 @@ func (sc *SimpleProjection) buildFields(inner *sqltypes.Result) []*querypb.Field
104108
return nil
105109
}
106110
fields := make([]*querypb.Field, 0, len(sc.Cols))
107-
for _, col := range sc.Cols {
108-
fields = append(fields, inner.Fields[col])
111+
for idx, col := range sc.Cols {
112+
field := inner.Fields[col]
113+
if sc.ColNames[idx] != "" {
114+
field = proto.Clone(field).(*querypb.Field)
115+
field.Name = sc.ColNames[idx]
116+
}
117+
fields = append(fields, field)
109118
}
110119
return fields
111120
}
@@ -114,6 +123,16 @@ func (sc *SimpleProjection) description() PrimitiveDescription {
114123
other := map[string]any{
115124
"Columns": sc.Cols,
116125
}
126+
emptyColNames := true
127+
for _, cName := range sc.ColNames {
128+
if cName != "" {
129+
emptyColNames = false
130+
break
131+
}
132+
}
133+
if !emptyColNames {
134+
other["ColumnNames"] = sc.ColNames
135+
}
117136
return PrimitiveDescription{
118137
OperatorType: "SimpleProjection",
119138
Other: other,

go/vt/vtgate/engine/simple_projection_test.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ func TestSubqueryExecute(t *testing.T) {
4444
}
4545

4646
sq := &SimpleProjection{
47-
Cols: []int{0, 2},
48-
Input: prim,
47+
Cols: []int{0, 2},
48+
ColNames: []string{"", ""},
49+
Input: prim,
4950
}
5051

5152
bv := map[string]*querypb.BindVariable{
@@ -93,8 +94,9 @@ func TestSubqueryStreamExecute(t *testing.T) {
9394
}
9495

9596
sq := &SimpleProjection{
96-
Cols: []int{0, 2},
97-
Input: prim,
97+
Cols: []int{0, 2},
98+
ColNames: []string{"", ""},
99+
Input: prim,
98100
}
99101

100102
bv := map[string]*querypb.BindVariable{
@@ -142,8 +144,9 @@ func TestSubqueryGetFields(t *testing.T) {
142144
}
143145

144146
sq := &SimpleProjection{
145-
Cols: []int{0, 2},
146-
Input: prim,
147+
Cols: []int{0, 2},
148+
ColNames: []string{"", ""},
149+
Input: prim,
147150
}
148151

149152
bv := map[string]*querypb.BindVariable{

go/vt/vtgate/planbuilder/operator_transformers.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -355,10 +355,10 @@ func transformProjection(ctx *plancontext.PlanningContext, op *operators.Project
355355
return nil, err
356356
}
357357

358-
if cols := op.AllOffsets(); cols != nil {
358+
if cols, colNames := op.AllOffsets(); cols != nil {
359359
// if all this op is doing is passing through columns from the input, we
360360
// can use the faster SimpleProjection
361-
return useSimpleProjection(ctx, op, cols, src)
361+
return useSimpleProjection(ctx, op, cols, colNames, src)
362362
}
363363

364364
ap, err := op.GetAliasedProjections()
@@ -403,7 +403,7 @@ func getEvalEngingeExpr(ctx *plancontext.PlanningContext, pe *operators.ProjExpr
403403

404404
// useSimpleProjection uses nothing at all if the output is already correct,
405405
// or SimpleProjection when we have to reorder or truncate the columns
406-
func useSimpleProjection(ctx *plancontext.PlanningContext, op *operators.Projection, cols []int, src logicalPlan) (logicalPlan, error) {
406+
func useSimpleProjection(ctx *plancontext.PlanningContext, op *operators.Projection, cols []int, colNames []string, src logicalPlan) (logicalPlan, error) {
407407
columns := op.Source.GetColumns(ctx)
408408
if len(columns) == len(cols) && elementsMatchIndices(cols) {
409409
// the columns are already in the right order. we don't need anything at all here
@@ -412,7 +412,8 @@ func useSimpleProjection(ctx *plancontext.PlanningContext, op *operators.Project
412412
return &simpleProjection{
413413
logicalPlanCommon: newBuilderCommon(src),
414414
eSimpleProj: &engine.SimpleProjection{
415-
Cols: cols,
415+
Cols: cols,
416+
ColNames: colNames,
416417
},
417418
}, nil
418419
}

go/vt/vtgate/planbuilder/operators/projection.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -399,18 +399,23 @@ func (p *Projection) GetOrdering(ctx *plancontext.PlanningContext) []OrderBy {
399399

400400
// AllOffsets returns a slice of integer offsets for all columns in the Projection
401401
// if all columns are of type Offset. If any column is not of type Offset, it returns nil.
402-
func (p *Projection) AllOffsets() (cols []int) {
402+
func (p *Projection) AllOffsets() (cols []int, colNames []string) {
403403
ap, err := p.GetAliasedProjections()
404404
if err != nil {
405-
return nil
405+
return nil, nil
406406
}
407407
for _, c := range ap {
408408
offset, ok := c.Info.(Offset)
409409
if !ok {
410-
return nil
410+
return nil, nil
411+
}
412+
colName := ""
413+
if c.Original.As.NotEmpty() {
414+
colName = c.Original.As.String()
411415
}
412416

413417
cols = append(cols, int(offset))
418+
colNames = append(colNames, colName)
414419
}
415420
return
416421
}
@@ -445,7 +450,7 @@ func (p *Projection) Compact(ctx *plancontext.PlanningContext) (Operator, *Apply
445450
needed := false
446451
for i, projection := range ap {
447452
e, ok := projection.Info.(Offset)
448-
if !ok || int(e) != i {
453+
if !ok || int(e) != i || projection.Original.As.NotEmpty() {
449454
needed = true
450455
break
451456
}
@@ -475,6 +480,9 @@ func (p *Projection) compactWithJoin(ctx *plancontext.PlanningContext, join *App
475480
for _, col := range ap {
476481
switch colInfo := col.Info.(type) {
477482
case Offset:
483+
if col.Original.As.NotEmpty() {
484+
return p, NoRewrite
485+
}
478486
newColumns = append(newColumns, join.Columns[colInfo])
479487
newColumnsAST.add(join.JoinColumns.columns[colInfo])
480488
case nil:

go/vt/vtgate/planbuilder/testdata/ddl_cases_no_default_keyspace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@
144144
"Name": "user",
145145
"Sharded": true
146146
},
147-
"Query": "create view view_a as select a.user_id as user_id, a.col1 as col1, a.col2 as col2, b.user_id as user_id, b.col1 as col1, b.col2 as col2 from authoritative as a join authoritative as b on a.user_id = b.user_id"
147+
"Query": "create view view_a as select a.user_id, a.col1, a.col2, b.user_id, b.col1, b.col2 from authoritative as a join authoritative as b on a.user_id = b.user_id"
148148
},
149149
"TablesUsed": [
150150
"user.view_a"
@@ -201,7 +201,7 @@
201201
"Name": "user",
202202
"Sharded": true
203203
},
204-
"Query": "create view view_a as select `user`.id, a.user_id as user_id, a.col1 as col1, a.col2 as col2, `user`.col1 from authoritative as a join `user` on a.user_id = `user`.id"
204+
"Query": "create view view_a as select `user`.id, a.user_id, a.col1, a.col2, `user`.col1 from authoritative as a join `user` on a.user_id = `user`.id"
205205
},
206206
"TablesUsed": [
207207
"user.view_a"

go/vt/vtgate/planbuilder/testdata/from_cases.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3760,8 +3760,8 @@
37603760
"Name": "user",
37613761
"Sharded": true
37623762
},
3763-
"FieldQuery": "select authoritative.col1 as col1, authoritative.user_id as user_id, authoritative.col2 as col2 from authoritative where 1 != 1",
3764-
"Query": "select authoritative.col1 as col1, authoritative.user_id as user_id, authoritative.col2 as col2 from authoritative",
3763+
"FieldQuery": "select authoritative.col1, authoritative.user_id, authoritative.col2 from authoritative where 1 != 1",
3764+
"Query": "select authoritative.col1, authoritative.user_id, authoritative.col2 from authoritative",
37653765
"Table": "authoritative"
37663766
},
37673767
{
@@ -3771,8 +3771,8 @@
37713771
"Name": "main",
37723772
"Sharded": false
37733773
},
3774-
"FieldQuery": "select unsharded_authoritative.col2 as col2 from unsharded_authoritative where 1 != 1",
3775-
"Query": "select unsharded_authoritative.col2 as col2 from unsharded_authoritative where unsharded_authoritative.col1 = :authoritative_col1",
3774+
"FieldQuery": "select unsharded_authoritative.col2 from unsharded_authoritative where 1 != 1",
3775+
"Query": "select unsharded_authoritative.col2 from unsharded_authoritative where unsharded_authoritative.col1 = :authoritative_col1",
37763776
"Table": "unsharded_authoritative"
37773777
}
37783778
]

go/vt/vtgate/planbuilder/testdata/info_schema57_cases.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -750,8 +750,8 @@
750750
"Name": "main",
751751
"Sharded": false
752752
},
753-
"FieldQuery": "select a.VARIABLE_NAME as VARIABLE_NAME, a.VARIABLE_VALUE as VARIABLE_VALUE, b.CHARACTER_SET_NAME as CHARACTER_SET_NAME, b.DEFAULT_COLLATE_NAME as DEFAULT_COLLATE_NAME, b.DESCRIPTION as DESCRIPTION, b.MAXLEN as MAXLEN from information_schema.GLOBAL_STATUS as a, information_schema.CHARACTER_SETS as b where 1 != 1",
754-
"Query": "select a.VARIABLE_NAME as VARIABLE_NAME, a.VARIABLE_VALUE as VARIABLE_VALUE, b.CHARACTER_SET_NAME as CHARACTER_SET_NAME, b.DEFAULT_COLLATE_NAME as DEFAULT_COLLATE_NAME, b.DESCRIPTION as DESCRIPTION, b.MAXLEN as MAXLEN from information_schema.GLOBAL_STATUS as a, information_schema.CHARACTER_SETS as b",
753+
"FieldQuery": "select a.VARIABLE_NAME, a.VARIABLE_VALUE, b.CHARACTER_SET_NAME, b.DEFAULT_COLLATE_NAME, b.DESCRIPTION, b.MAXLEN from information_schema.GLOBAL_STATUS as a, information_schema.CHARACTER_SETS as b where 1 != 1",
754+
"Query": "select a.VARIABLE_NAME, a.VARIABLE_VALUE, b.CHARACTER_SET_NAME, b.DEFAULT_COLLATE_NAME, b.DESCRIPTION, b.MAXLEN from information_schema.GLOBAL_STATUS as a, information_schema.CHARACTER_SETS as b",
755755
"Table": "information_schema.CHARACTER_SETS, information_schema.GLOBAL_STATUS"
756756
}
757757
}

go/vt/vtgate/planbuilder/testdata/info_schema80_cases.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -815,8 +815,8 @@
815815
"Name": "main",
816816
"Sharded": false
817817
},
818-
"FieldQuery": "select a.CONSTRAINT_CATALOG as CONSTRAINT_CATALOG, a.CONSTRAINT_SCHEMA as CONSTRAINT_SCHEMA, a.CONSTRAINT_NAME as CONSTRAINT_NAME, a.CHECK_CLAUSE as CHECK_CLAUSE, b.CHARACTER_SET_NAME as CHARACTER_SET_NAME, b.DEFAULT_COLLATE_NAME as DEFAULT_COLLATE_NAME, b.DESCRIPTION as DESCRIPTION, b.MAXLEN as MAXLEN from information_schema.CHECK_CONSTRAINTS as a, information_schema.CHARACTER_SETS as b where 1 != 1",
819-
"Query": "select a.CONSTRAINT_CATALOG as CONSTRAINT_CATALOG, a.CONSTRAINT_SCHEMA as CONSTRAINT_SCHEMA, a.CONSTRAINT_NAME as CONSTRAINT_NAME, a.CHECK_CLAUSE as CHECK_CLAUSE, b.CHARACTER_SET_NAME as CHARACTER_SET_NAME, b.DEFAULT_COLLATE_NAME as DEFAULT_COLLATE_NAME, b.DESCRIPTION as DESCRIPTION, b.MAXLEN as MAXLEN from information_schema.CHECK_CONSTRAINTS as a, information_schema.CHARACTER_SETS as b",
818+
"FieldQuery": "select a.CONSTRAINT_CATALOG, a.CONSTRAINT_SCHEMA, a.CONSTRAINT_NAME, a.CHECK_CLAUSE, b.CHARACTER_SET_NAME, b.DEFAULT_COLLATE_NAME, b.DESCRIPTION, b.MAXLEN from information_schema.CHECK_CONSTRAINTS as a, information_schema.CHARACTER_SETS as b where 1 != 1",
819+
"Query": "select a.CONSTRAINT_CATALOG, a.CONSTRAINT_SCHEMA, a.CONSTRAINT_NAME, a.CHECK_CLAUSE, b.CHARACTER_SET_NAME, b.DEFAULT_COLLATE_NAME, b.DESCRIPTION, b.MAXLEN from information_schema.CHECK_CONSTRAINTS as a, information_schema.CHARACTER_SETS as b",
820820
"Table": "information_schema.CHARACTER_SETS, information_schema.CHECK_CONSTRAINTS"
821821
}
822822
}

go/vt/vtgate/planbuilder/testdata/rails_cases.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@
5050
"Name": "user",
5151
"Sharded": true
5252
},
53-
"FieldQuery": "select author5s.id as id, author5s.`name` as `name`, author5s.created_at as created_at, author5s.updated_at as updated_at, book6s.supplier5_id, book6s.id from author5s, book6s where 1 != 1",
54-
"Query": "select author5s.id as id, author5s.`name` as `name`, author5s.created_at as created_at, author5s.updated_at as updated_at, book6s.supplier5_id, book6s.id from author5s, book6s where book6s.author5_id = author5s.id",
53+
"FieldQuery": "select author5s.id, author5s.`name`, author5s.created_at, author5s.updated_at, book6s.supplier5_id, book6s.id from author5s, book6s where 1 != 1",
54+
"Query": "select author5s.id, author5s.`name`, author5s.created_at, author5s.updated_at, book6s.supplier5_id, book6s.id from author5s, book6s where book6s.author5_id = author5s.id",
5555
"Table": "author5s, book6s"
5656
},
5757
{

go/vt/vtgate/planbuilder/testdata/select_cases.json

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,8 @@
361361
"Name": "user",
362362
"Sharded": true
363363
},
364-
"FieldQuery": "select a.user_id as user_id, a.col1 as col1, a.col2 as col2, b.user_id as user_id, b.col1 as col1, b.col2 as col2 from authoritative as a, authoritative as b where 1 != 1",
365-
"Query": "select a.user_id as user_id, a.col1 as col1, a.col2 as col2, b.user_id as user_id, b.col1 as col1, b.col2 as col2 from authoritative as a, authoritative as b where a.user_id = b.user_id",
364+
"FieldQuery": "select a.user_id, a.col1, a.col2, b.user_id, b.col1, b.col2 from authoritative as a, authoritative as b where 1 != 1",
365+
"Query": "select a.user_id, a.col1, a.col2, b.user_id, b.col1, b.col2 from authoritative as a, authoritative as b where a.user_id = b.user_id",
366366
"Table": "authoritative"
367367
},
368368
"TablesUsed": [
@@ -433,8 +433,8 @@
433433
"Name": "user",
434434
"Sharded": true
435435
},
436-
"FieldQuery": "select `user`.id, a.user_id as user_id, a.col1 as col1, a.col2 as col2, `user`.col1 from authoritative as a, `user` where 1 != 1",
437-
"Query": "select `user`.id, a.user_id as user_id, a.col1 as col1, a.col2 as col2, `user`.col1 from authoritative as a, `user` where a.user_id = `user`.id",
436+
"FieldQuery": "select `user`.id, a.user_id, a.col1, a.col2, `user`.col1 from authoritative as a, `user` where 1 != 1",
437+
"Query": "select `user`.id, a.user_id, a.col1, a.col2, `user`.col1 from authoritative as a, `user` where a.user_id = `user`.id",
438438
"Table": "`user`, authoritative"
439439
},
440440
"TablesUsed": [
@@ -4997,5 +4997,63 @@
49974997
"user.user"
49984998
]
49994999
}
5000+
},
5001+
{
5002+
"comment": "column name aliases in outer join queries",
5003+
"query": "select name as t0, name as t1 from user left outer join user_extra on user.cola = user_extra.cola",
5004+
"plan": {
5005+
"QueryType": "SELECT",
5006+
"Original": "select name as t0, name as t1 from user left outer join user_extra on user.cola = user_extra.cola",
5007+
"Instructions": {
5008+
"OperatorType": "SimpleProjection",
5009+
"ColumnNames": [
5010+
"t0",
5011+
"t1"
5012+
],
5013+
"Columns": [
5014+
0,
5015+
0
5016+
],
5017+
"Inputs": [
5018+
{
5019+
"OperatorType": "Join",
5020+
"Variant": "LeftJoin",
5021+
"JoinColumnIndexes": "L:0",
5022+
"JoinVars": {
5023+
"user_cola": 1
5024+
},
5025+
"TableName": "`user`_user_extra",
5026+
"Inputs": [
5027+
{
5028+
"OperatorType": "Route",
5029+
"Variant": "Scatter",
5030+
"Keyspace": {
5031+
"Name": "user",
5032+
"Sharded": true
5033+
},
5034+
"FieldQuery": "select `name`, `user`.cola from `user` where 1 != 1",
5035+
"Query": "select `name`, `user`.cola from `user`",
5036+
"Table": "`user`"
5037+
},
5038+
{
5039+
"OperatorType": "Route",
5040+
"Variant": "Scatter",
5041+
"Keyspace": {
5042+
"Name": "user",
5043+
"Sharded": true
5044+
},
5045+
"FieldQuery": "select 1 from user_extra where 1 != 1",
5046+
"Query": "select 1 from user_extra where user_extra.cola = :user_cola",
5047+
"Table": "user_extra"
5048+
}
5049+
]
5050+
}
5051+
]
5052+
},
5053+
"TablesUsed": [
5054+
"user.user",
5055+
"user.user_extra"
5056+
]
5057+
}
50005058
}
50015059
]

go/vt/vtgate/semantics/early_rewriter.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1237,17 +1237,13 @@ type expanderState struct {
12371237
// addColumn adds columns to the expander state. If we have vschema info about the query,
12381238
// we also store which columns were expanded
12391239
func (e *expanderState) addColumn(col ColumnInfo, tbl TableInfo, tblName sqlparser.TableName) {
1240-
withQualifier := e.needsQualifier
12411240
var colName *sqlparser.ColName
12421241
var alias sqlparser.IdentifierCI
1243-
if withQualifier {
1242+
if e.needsQualifier {
12441243
colName = sqlparser.NewColNameWithQualifier(col.Name, tblName)
12451244
} else {
12461245
colName = sqlparser.NewColName(col.Name)
12471246
}
1248-
if e.needsQualifier {
1249-
alias = sqlparser.NewIdentifierCI(col.Name)
1250-
}
12511247
e.colNames = append(e.colNames, &sqlparser.AliasedExpr{Expr: colName, As: alias})
12521248
e.storeExpandInfo(tbl, tblName, colName)
12531249
}

0 commit comments

Comments
 (0)