diff --git a/pkg/sql/opt/optbuilder/testdata/with b/pkg/sql/opt/optbuilder/testdata/with index 02bf8f8533c7..8995274754d4 100644 --- a/pkg/sql/opt/optbuilder/testdata/with +++ b/pkg/sql/opt/optbuilder/testdata/with @@ -1143,6 +1143,40 @@ with &2 (cte) ├── "?column?":7 => a:9 └── "?column?":8 => b:10 +# Allow non-recursive CTE even when it has UNION. +build +WITH RECURSIVE cte(a, b) AS ( + SELECT 1, 2 + UNION + SELECT 3, 4 +) SELECT * FROM cte; +---- +with &2 (cte) + ├── columns: a:7!null b:8!null + ├── union + │ ├── columns: "?column?":5!null "?column?":6!null + │ ├── left columns: "?column?":1 "?column?":2 + │ ├── right columns: "?column?":3 "?column?":4 + │ ├── project + │ │ ├── columns: "?column?":1!null "?column?":2!null + │ │ ├── values + │ │ │ └── () + │ │ └── projections + │ │ ├── 1 [as="?column?":1] + │ │ └── 2 [as="?column?":2] + │ └── project + │ ├── columns: "?column?":3!null "?column?":4!null + │ ├── values + │ │ └── () + │ └── projections + │ ├── 3 [as="?column?":3] + │ └── 4 [as="?column?":4] + └── with-scan &2 (cte) + ├── columns: a:7!null b:8!null + └── mapping: + ├── "?column?":5 => a:7 + └── "?column?":6 => b:8 + # Error cases. build WITH RECURSIVE cte(a, b) AS ( @@ -1158,7 +1192,7 @@ WITH RECURSIVE cte(a, b) AS ( SELECT 1+a, 1+b FROM cte ) SELECT * FROM cte; ---- -error (42601): recursive query "cte" does not have the form non-recursive-term UNION ALL recursive-term +error (0A000): unimplemented: recursive query "cte" uses UNION which is not implemented (only UNION ALL is supported) build WITH RECURSIVE cte(a, b) AS ( diff --git a/pkg/sql/opt/optbuilder/with.go b/pkg/sql/opt/optbuilder/with.go index ccfa8ccf37ab..025fc28a7170 100644 --- a/pkg/sql/opt/optbuilder/with.go +++ b/pkg/sql/opt/optbuilder/with.go @@ -75,16 +75,25 @@ func (b *Builder) buildCTE( } cteScope.ctes = map[string]*cteSource{cte.Name.Alias.String(): cteSrc} - initial, recursive, ok := b.splitRecursiveCTE(cte.Stmt) - if !ok { + initial, recursive, isUnionAll, ok := b.splitRecursiveCTE(cte.Stmt) + // We don't currently support the UNION form (only UNION ALL). + if !ok || !isUnionAll { // Build this as a non-recursive CTE, but throw a proper error message if it // does have a recursive reference. cteSrc.onRef = func() { - panic(pgerror.Newf( - pgcode.Syntax, - "recursive query %q does not have the form non-recursive-term UNION ALL recursive-term", - cte.Name.Alias, - )) + if !ok { + panic(pgerror.Newf( + pgcode.Syntax, + "recursive query %q does not have the form non-recursive-term UNION ALL recursive-term", + cte.Name.Alias, + )) + } else { + panic(unimplementedWithIssueDetailf( + 46642, "", + "recursive query %q uses UNION which is not implemented (only UNION ALL is supported)", + cte.Name.Alias, + )) + } } return b.buildCTE(cte, cteScope, false /* recursive */) } @@ -228,16 +237,16 @@ func (b *Builder) getCTECols(cteScope *scope, name tree.AliasClause) physical.Pr // returns ok=false. func (b *Builder) splitRecursiveCTE( stmt tree.Statement, -) (initial, recursive *tree.Select, ok bool) { +) (initial, recursive *tree.Select, isUnionAll bool, ok bool) { sel, ok := stmt.(*tree.Select) // The form above doesn't allow for "outer" WITH, ORDER BY, or LIMIT // clauses. if !ok || sel.With != nil || sel.OrderBy != nil || sel.Limit != nil { - return nil, nil, false + return nil, nil, false, false } union, ok := sel.Select.(*tree.UnionClause) - if !ok || union.Type != tree.UnionOp || !union.All { - return nil, nil, false + if !ok || union.Type != tree.UnionOp { + return nil, nil, false, false } - return union.Left, union.Right, true + return union.Left, union.Right, union.All, true } diff --git a/pkg/util/errorutil/unimplemented/unimplemented.go b/pkg/util/errorutil/unimplemented/unimplemented.go index a16b551648ec..e31031ef9c76 100644 --- a/pkg/util/errorutil/unimplemented/unimplemented.go +++ b/pkg/util/errorutil/unimplemented/unimplemented.go @@ -65,8 +65,7 @@ func NewWithIssueDetail(issue int, detail, msg string) error { return unimplementedInternal(1 /*depth*/, issue, detail, false /*format*/, msg) } -// NewWithIssueDetailf is like UnimplementedWithIssueDetail -// but the message is formatted. +// NewWithIssueDetailf is like NewWithIssueDetail but the message is formatted. func NewWithIssueDetailf(issue int, detail, format string, args ...interface{}) error { return unimplementedInternal(1 /*depth*/, issue, detail, true /*format*/, format, args...) }