mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 02:44:59 +00:00
implement left join (fb-1888) (#2420)
* implement left join
* fixed failing test
(cherry picked from commit 7386f13159)
This commit is contained in:
parent
1046e28338
commit
b3a552e0d3
9 changed files with 165 additions and 97 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 ")
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'`)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue