implement left join (fb-1888) (#2420)

* implement left join

* fixed failing test

(cherry picked from commit 7386f13159)
This commit is contained in:
Pat Okeeffe 2023-01-19 10:05:34 -06:00 committed by Joe Friedrich
parent 1046e28338
commit b3a552e0d3
9 changed files with 165 additions and 97 deletions

View file

@ -10,6 +10,7 @@ import (
const (
ErrInternal errors.Code = "ErrInternal"
ErrUnsupported errors.Code = "ErrUnsupported"
ErrCacheKeyNotFound errors.Code = "ErrCacheKeyNotFound"
ErrDuplicateColumn errors.Code = "ErrDuplicateColumn"
@ -172,6 +173,17 @@ func NewErrInternalf(format string, a ...interface{}) error {
)
}
func NewErrUnsupported(line, col int, is bool, thing string) error {
msg := fmt.Sprintf("[%d:%d] %s are not supported", line, col, thing)
if is {
msg = fmt.Sprintf("[%d:%d] %s is not supported", line, col, thing)
}
return errors.New(
ErrUnknownIdentifier,
msg,
)
}
func NewErrCacheKeyNotFound(key uint64) error {
return errors.New(
ErrCacheKeyNotFound,

View file

@ -3893,13 +3893,14 @@ func (c *JoinClause) SourceFromAlias(alias string) Source {
}
type JoinOperator struct {
Comma Pos // position of comma
Natural Pos // position of NATURAL keyword
Left Pos // position of LEFT keyword
Outer Pos // position of OUTER keyword
Inner Pos // position of INNER keyword
Cross Pos // position of CROSS keyword
Join Pos // position of JOIN keyword
Comma Pos // position of comma
Left Pos // position of LEFT keyword
Right Pos // position of RIGHT keyword
Full Pos // position of FULL keyword
Outer Pos // position of OUTER keyword
Inner Pos // position of INNER keyword
// Cross Pos // position of CROSS keyword // TODO(pok) - add cross back when we do it
Join Pos // position of JOIN keyword
}
// Clone returns a deep copy of op.
@ -3918,9 +3919,6 @@ func (op *JoinOperator) String() string {
}
var buf bytes.Buffer
if op.Natural.IsValid() {
buf.WriteString(" NATURAL")
}
if op.Left.IsValid() {
buf.WriteString(" LEFT")
if op.Outer.IsValid() {
@ -3928,8 +3926,8 @@ func (op *JoinOperator) String() string {
}
} else if op.Inner.IsValid() {
buf.WriteString(" INNER")
} else if op.Cross.IsValid() {
buf.WriteString(" CROSS")
// } else if op.Cross.IsValid() {
// buf.WriteString(" CROSS")
}
buf.WriteString(" JOIN ")

View file

@ -831,18 +831,6 @@ func TestSelectStatement_String(t *testing.T) {
},
}, `SELECT * FROM x JOIN y ON TRUE`)
AssertStatementStringer(t, &parser.SelectStatement{
Columns: []*parser.ResultColumn{{Star: pos(0)}},
Source: &parser.JoinClause{
X: &parser.QualifiedTableName{Name: &parser.Ident{Name: "x"}},
Operator: &parser.JoinOperator{Natural: pos(0), Inner: pos(0)},
Y: &parser.QualifiedTableName{Name: &parser.Ident{Name: "y"}},
Constraint: &parser.UsingConstraint{
Columns: []*parser.Ident{{Name: "a"}, {Name: "b"}},
},
},
}, `SELECT * FROM x NATURAL INNER JOIN y USING (a, b)`)
AssertStatementStringer(t, &parser.SelectStatement{
Columns: []*parser.ResultColumn{{Star: pos(0)}},
Source: &parser.JoinClause{
@ -861,14 +849,14 @@ func TestSelectStatement_String(t *testing.T) {
},
}, `SELECT * FROM x LEFT OUTER JOIN y`)
AssertStatementStringer(t, &parser.SelectStatement{
Columns: []*parser.ResultColumn{{Star: pos(0)}},
Source: &parser.JoinClause{
X: &parser.QualifiedTableName{Name: &parser.Ident{Name: "x"}},
Operator: &parser.JoinOperator{Cross: pos(0)},
Y: &parser.QualifiedTableName{Name: &parser.Ident{Name: "y"}},
},
}, `SELECT * FROM x CROSS JOIN y`)
// AssertStatementStringer(t, &parser.SelectStatement{
// Columns: []*parser.ResultColumn{{Star: pos(0)}},
// Source: &parser.JoinClause{
// X: &parser.QualifiedTableName{Name: &parser.Ident{Name: "x"}},
// Operator: &parser.JoinOperator{Cross: pos(0)},
// Y: &parser.QualifiedTableName{Name: &parser.Ident{Name: "y"}},
// },
// }, `SELECT * FROM x CROSS JOIN y`)
}
func TestUpdateStatement_String(t *testing.T) {

View file

@ -2274,7 +2274,7 @@ func (p *Parser) parseSource() (source Source, err error) {
for {
// Exit immediately if not part of a join operator.
switch p.peek() {
case COMMA, NATURAL, LEFT, INNER, CROSS, JOIN:
case COMMA, LEFT, RIGHT, FULL, INNER /*CROSS, */, JOIN:
default:
return source, nil
}
@ -2340,21 +2340,28 @@ func (p *Parser) parseJoinOperator() (*JoinOperator, error) {
return &op, nil
}
if p.peek() == NATURAL {
op.Natural, _, _ = p.scan()
}
// Parse "LEFT", "LEFT OUTER", "INNER", or "CROSS"
// Parse "INNER", "LEFT [OUTER]", "RIGHT [OUTER]", "FULL [OUTER]", or "CROSS"
switch p.peek() {
case LEFT:
op.Left, _, _ = p.scan()
if p.peek() == OUTER {
op.Outer, _, _ = p.scan()
}
case RIGHT:
op.Right, _, _ = p.scan()
if p.peek() == OUTER {
op.Outer, _, _ = p.scan()
}
case FULL:
op.Full, _, _ = p.scan()
if p.peek() == OUTER {
op.Outer, _, _ = p.scan()
}
case INNER:
op.Inner, _, _ = p.scan()
case CROSS:
op.Cross, _, _ = p.scan()
// case CROSS:
// op.Cross, _, _ = p.scan()
}
// Parse final JOIN.
@ -2362,7 +2369,6 @@ func (p *Parser) parseJoinOperator() (*JoinOperator, error) {
return &op, p.errorExpected(p.pos, p.tok, "JOIN")
}
op.Join, _, _ = p.scan()
return &op, nil
}

View file

@ -1879,22 +1879,6 @@ func TestParser_ParseStatement(t *testing.T) {
},
},
})
AssertParseStatement(t, `SELECT * FROM foo NATURAL JOIN bar`, &parser.SelectStatement{
Select: pos(0),
Columns: []*parser.ResultColumn{
{Star: pos(7)},
},
From: pos(9),
Source: &parser.JoinClause{
X: &parser.QualifiedTableName{
Name: &parser.Ident{NamePos: pos(14), Name: "foo"},
},
Operator: &parser.JoinOperator{Natural: pos(18), Join: pos(26)},
Y: &parser.QualifiedTableName{
Name: &parser.Ident{NamePos: pos(31), Name: "bar"},
},
},
})
AssertParseStatement(t, `SELECT * FROM foo INNER JOIN bar ON true`, &parser.SelectStatement{
Select: pos(0),
Columns: []*parser.ResultColumn{
@ -1986,22 +1970,22 @@ func TestParser_ParseStatement(t *testing.T) {
},
},
})
AssertParseStatement(t, `SELECT * FROM foo CROSS JOIN bar`, &parser.SelectStatement{
Select: pos(0),
Columns: []*parser.ResultColumn{
{Star: pos(7)},
},
From: pos(9),
Source: &parser.JoinClause{
X: &parser.QualifiedTableName{
Name: &parser.Ident{NamePos: pos(14), Name: "foo"},
},
Operator: &parser.JoinOperator{Cross: pos(18), Join: pos(24)},
Y: &parser.QualifiedTableName{
Name: &parser.Ident{NamePos: pos(29), Name: "bar"},
},
},
})
// AssertParseStatement(t, `SELECT * FROM foo CROSS JOIN bar`, &parser.SelectStatement{
// Select: pos(0),
// Columns: []*parser.ResultColumn{
// {Star: pos(7)},
// },
// From: pos(9),
// Source: &parser.JoinClause{
// X: &parser.QualifiedTableName{
// Name: &parser.Ident{NamePos: pos(14), Name: "foo"},
// },
// Operator: &parser.JoinOperator{Cross: pos(18), Join: pos(24)},
// Y: &parser.QualifiedTableName{
// Name: &parser.Ident{NamePos: pos(29), Name: "bar"},
// },
// },
// })
/*AssertParseStatement(t, `WITH cte (foo, bar) AS (SELECT baz), xxx AS (SELECT yyy) SELECT bat`, &parser.SelectStatement{
WithClause: &parser.WithClause{
@ -2234,10 +2218,12 @@ func TestParser_ParseStatement(t *testing.T) {
AssertParseStatementError(t, `SELECT foo FROM foo INDEXED BY`, `1:30: expected index name, found 'EOF'`)*/
/*AssertParseStatementError(t, `SELECT foo FROM foo NOT`, `1:23: expected INDEXED, found 'EOF'`)*/
AssertParseStatementError(t, `SELECT * FROM foo INNER`, `1:23: expected JOIN, found 'EOF'`)
AssertParseStatementError(t, `SELECT * FROM foo CROSS`, `1:23: expected JOIN, found 'EOF'`)
AssertParseStatementError(t, `SELECT * FROM foo NATURAL`, `1:25: expected JOIN, found 'EOF'`)
AssertParseStatementError(t, `SELECT * FROM foo LEFT`, `1:22: expected JOIN, found 'EOF'`)
AssertParseStatementError(t, `SELECT * FROM foo LEFT OUTER`, `1:28: expected JOIN, found 'EOF'`)
AssertParseStatementError(t, `SELECT * FROM foo RIGHT`, `1:23: expected JOIN, found 'EOF'`)
AssertParseStatementError(t, `SELECT * FROM foo RIGHT OUTER`, `1:29: expected JOIN, found 'EOF'`)
AssertParseStatementError(t, `SELECT * FROM foo FULL`, `1:22: expected JOIN, found 'EOF'`)
AssertParseStatementError(t, `SELECT * FROM foo FULL OUTER`, `1:28: expected JOIN, found 'EOF'`)
AssertParseStatementError(t, `SELECT * FROM foo,`, `1:18: expected table name or left paren, found 'EOF'`)
AssertParseStatementError(t, `SELECT * FROM foo JOIN bar ON`, `1:29: expected expression, found 'EOF'`)
AssertParseStatementError(t, `SELECT * FROM foo JOIN bar USING`, `1:32: expected left paren, found 'EOF'`)

View file

@ -139,6 +139,7 @@ const (
FOR
FOREIGN
FROM
FULL
FUNCTION
GLOB
GROUP
@ -170,7 +171,6 @@ const (
MATCH
MAX
MIN
NATURAL
NO
NOT
NOTBETWEEN
@ -208,6 +208,7 @@ const (
RESTRICT
RETURNS
RETURN
RIGHT
ROLLBACK
ROW
ROWS
@ -362,6 +363,7 @@ var tokens = [...]string{
FOR: "FOR",
FOREIGN: "FOREIGN",
FROM: "FROM",
FULL: "FULL",
FUNCTION: "FUNCTION",
GLOB: "GLOB",
GROUP: "GROUP",
@ -394,7 +396,6 @@ var tokens = [...]string{
MAX: "MAX",
MIN: "MIN",
NO: "NO",
NATURAL: "NATURAL",
NOT: "NOT",
NOTBETWEEN: "NOTBETWEEN",
NOTEXISTS: "NOTEXISTS",
@ -431,6 +432,7 @@ var tokens = [...]string{
RESTRICT: "RESTRICT",
RETURNS: "RETURNS",
RETURN: "RETURN",
RIGHT: "RIGHT",
ROLLBACK: "ROLLBACK",
ROW: "ROW",
ROWS: "ROWS",

View file

@ -268,6 +268,17 @@ func (p *ExecutionPlanner) compileSource(scope *PlanOpQuery, source parser.Sourc
case *parser.JoinClause:
scope.AddWarning("🦖 here there be dragons! JOINS are experimental.")
// what sort of join is it?
jType := joinTypeInner
if sourceExpr.Operator.Left.IsValid() {
jType = joinTypeLeft
} else if sourceExpr.Operator.Right.IsValid() {
return nil, sql3.NewErrUnsupported(sourceExpr.Operator.Right.Line, sourceExpr.Operator.Right.Column, false, "RIGHT join types")
} else if sourceExpr.Operator.Full.IsValid() {
return nil, sql3.NewErrUnsupported(sourceExpr.Operator.Full.Line, sourceExpr.Operator.Full.Column, false, "FULL join types")
}
// handle the join condition
var joinCondition types.PlanExpression
if sourceExpr.Constraint == nil {
scope.AddWarning("⚠️ cartesian products are never a good idea - are you missing a join constraint?")
@ -285,6 +296,7 @@ func (p *ExecutionPlanner) compileSource(scope *PlanOpQuery, source parser.Sourc
}
}
// compile top and bottom child ops
topOp, err := p.compileSource(scope, sourceExpr.X)
if err != nil {
return nil, err
@ -293,7 +305,7 @@ func (p *ExecutionPlanner) compileSource(scope *PlanOpQuery, source parser.Sourc
if err != nil {
return nil, err
}
return NewPlanOpNestedLoops(topOp, bottomOp, joinCondition), nil
return NewPlanOpNestedLoops(topOp, bottomOp, jType, joinCondition), nil
case *parser.QualifiedTableName:

View file

@ -16,14 +16,16 @@ type PlanOpNestedLoops struct {
top types.PlanOperator
bottom types.PlanOperator
cond types.PlanExpression
jType joinType
warnings []string
}
func NewPlanOpNestedLoops(top, bottom types.PlanOperator, condition types.PlanExpression) *PlanOpNestedLoops {
func NewPlanOpNestedLoops(top, bottom types.PlanOperator, jType joinType, condition types.PlanExpression) *PlanOpNestedLoops {
return &PlanOpNestedLoops{
top: top,
bottom: bottom,
cond: condition,
jType: jType,
warnings: make([]string, 0),
}
}
@ -71,14 +73,14 @@ func (p *PlanOpNestedLoops) Iterator(ctx context.Context, row types.Row) (types.
}
rowWidth := len(row) + len(p.top.Schema()) + len(p.bottom.Schema())
return newNestedLoopsIter(ctx, joinTypeInner, topIter, p.bottom, row, p.cond, rowWidth, row), nil
return newNestedLoopsIter(ctx, p.jType, topIter, p.bottom, row, p.cond, rowWidth, row), nil
}
func (p *PlanOpNestedLoops) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) {
if len(children) != 2 {
return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children))
}
return NewPlanOpNestedLoops(children[0], children[1], p.cond), nil
return NewPlanOpNestedLoops(children[0], children[1], p.jType, p.cond), nil
}
func (p *PlanOpNestedLoops) Expressions() []types.PlanExpression {
@ -101,9 +103,10 @@ func (p *PlanOpNestedLoops) WithUpdatedExpressions(exprs ...types.PlanExpression
type joinType byte
const (
joinTypeInner joinType = iota
joinTypeLeft
joinTypeRight
joinTypeInner joinType = iota // records that have matching values in both tables
joinTypeLeft // all records from the left table, and the matched records from the right table
joinTypeRight // all records from the right table, and the matched records from the left table
joinTypeFull // all records when there is a match in either left or right table
)
type nestedLoopsIter struct {
@ -178,7 +181,7 @@ func (i *nestedLoopsIter) loadBottom(ctx context.Context) (row types.Row, err er
return rightRow, nil
}
func (i *nestedLoopsIter) buildRow(primary, secondary types.Row) types.Row {
func (i *nestedLoopsIter) buildRow(primary, secondary types.Row) (types.Row, error) {
row := make(types.Row, i.rowSize)
primary = primary[len(i.originalRow):]
@ -186,19 +189,23 @@ func (i *nestedLoopsIter) buildRow(primary, secondary types.Row) types.Row {
var first, second types.Row
var secondOffset int
switch i.typ {
case joinTypeRight:
first = secondary
second = primary
secondOffset = len(row) - len(second)
default:
case joinTypeLeft:
first = primary
second = secondary
secondOffset = len(first)
case joinTypeInner:
first = primary
second = secondary
secondOffset = len(first)
default:
return nil, sql3.NewErrInternalf("unsupported join type %d", i.typ)
}
copy(row, first)
copy(row[secondOffset:], second)
return row
return row, nil
}
func conditionIsTrue(ctx context.Context, row types.Row, cond types.PlanExpression) (bool, error) {
@ -222,16 +229,38 @@ func (i *nestedLoopsIter) Next(ctx context.Context) (types.Row, error) {
secondary, err := i.loadBottom(ctx)
if err != nil {
if err == types.ErrNoMoreRows {
if !i.foundMatch && (i.typ == joinTypeLeft || i.typ == joinTypeRight) {
row := i.buildRow(primary, nil)
return row, nil
// no more rows from secondary
switch i.typ {
case joinTypeInner:
continue
case joinTypeLeft:
if !i.foundMatch {
row, err := i.buildRow(primary, nil)
if err != nil {
return nil, err
}
return row, nil
}
continue
case joinTypeRight:
return nil, sql3.NewErrInternalf("unhandled join type %v", i.typ)
case joinTypeFull:
return nil, sql3.NewErrInternalf("unhandled join type %v", i.typ)
default:
return nil, sql3.NewErrInternalf("unhandled join type %v", i.typ)
}
continue
}
return nil, err
}
row := i.buildRow(primary, secondary)
row, err := i.buildRow(primary, secondary)
if err != nil {
return nil, err
}
matches, err := conditionIsTrue(ctx, row, i.cond)
if err != nil {
return nil, err

View file

@ -17,6 +17,7 @@ var joinTestsUsers = TableTest{
srcRow(int64(1), string("b"), int64(18)),
srcRow(int64(2), string("c"), int64(28)),
srcRow(int64(3), string("d"), int64(34)),
srcRow(int64(4), string("e"), int64(36)),
),
),
SQLTests: nil,
@ -44,7 +45,7 @@ var joinTestsOrders = TableTest{
}
var joinTests = TableTest{
name: "innerjointest",
name: "joinTests",
SQLTests: []SQLTest{
{
name: "innerjoin-aggregate-groupby",
@ -102,6 +103,40 @@ var joinTests = TableTest{
),
Compare: CompareExactOrdered,
},
{
name: "leftjoin",
SQLs: sqls(
"select u._id , o.userid from users u left join orders o on o.userid = u._id;",
),
ExpHdrs: hdrs(
hdr("_id", fldTypeID),
hdr("userid", fldTypeInt),
),
ExpRows: rows(
row(int64(0), int64(0)),
row(int64(1), int64(1)),
row(int64(1), int64(1)),
row(int64(2), int64(2)),
row(int64(2), int64(2)),
row(int64(3), int64(3)),
row(int64(4), nil),
),
Compare: CompareExactOrdered,
},
{
name: "fulljoin",
SQLs: sqls(
"select u._id , o.userid from users u full join orders o on o.userid = u._id;",
),
ExpErr: "FULL join types are not supported",
},
{
name: "outerjoin",
SQLs: sqls(
"select u._id , o.userid from users u right join orders o on o.userid = u._id;",
),
ExpErr: "RIGHT join types are not supported",
},
},
PQLTests: []PQLTest{
{