diff --git a/sql3/errors.go b/sql3/errors.go index aab3a4218..6e069bd18 100644 --- a/sql3/errors.go +++ b/sql3/errors.go @@ -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, diff --git a/sql3/parser/ast.go b/sql3/parser/ast.go index c071164c1..b508e918f 100644 --- a/sql3/parser/ast.go +++ b/sql3/parser/ast.go @@ -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 ") diff --git a/sql3/parser/ast_test.go b/sql3/parser/ast_test.go index c326e055b..b963f2161 100644 --- a/sql3/parser/ast_test.go +++ b/sql3/parser/ast_test.go @@ -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) { diff --git a/sql3/parser/parser.go b/sql3/parser/parser.go index af27ab6e0..8b69d807f 100644 --- a/sql3/parser/parser.go +++ b/sql3/parser/parser.go @@ -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 } diff --git a/sql3/parser/parser_test.go b/sql3/parser/parser_test.go index 748ddf0f4..67d9316ac 100644 --- a/sql3/parser/parser_test.go +++ b/sql3/parser/parser_test.go @@ -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'`) diff --git a/sql3/parser/token.go b/sql3/parser/token.go index bc765a6a1..af6721666 100644 --- a/sql3/parser/token.go +++ b/sql3/parser/token.go @@ -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", diff --git a/sql3/planner/compileselect.go b/sql3/planner/compileselect.go index 0a112bed1..5682d24b1 100644 --- a/sql3/planner/compileselect.go +++ b/sql3/planner/compileselect.go @@ -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: diff --git a/sql3/planner/opnestedloops.go b/sql3/planner/opnestedloops.go index 71df5894b..d4116baa5 100644 --- a/sql3/planner/opnestedloops.go +++ b/sql3/planner/opnestedloops.go @@ -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 diff --git a/sql3/test/defs/defs_join.go b/sql3/test/defs/defs_join.go index d1269fb18..bd23e95de 100644 --- a/sql3/test/defs/defs_join.go +++ b/sql3/test/defs/defs_join.go @@ -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{ {