Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
28262: sql: add support for optional frame exclusion r=yuzefovich a=yuzefovich

Adds support for optional frame exclusion clause in the specification
of window frames. This feature is described in
https://www.postgresql.org/docs/12/sql-expressions.html.

Whenever a non-default frame exclusion is present, it forces the
execution to fall back to a naive quadratic approach. This means
that for every row in a window frame we will compute the window
function from scratch, without making use of any previous computations.
Additionally, the presence of a non-default frame exclusion breaks
necessary assumptions for utilizing a sliding window approach for
min, max, sum, and avg functions, so those also fall back to a naive
quadratic approach.

Closes: cockroachdb#27100.

Release note (sql change): CockroachDB now supports optional frame
exclusion clause in the specification of window frames.

Co-authored-by: Yahor Yuzefovich <yahor@cockroachlabs.com>
  • Loading branch information
craig[bot] and yuzefovich committed Aug 19, 2019
2 parents 903fdf2 + b347575 commit 79d97a7
Show file tree
Hide file tree
Showing 29 changed files with 2,049 additions and 329 deletions.
16 changes: 13 additions & 3 deletions docs/generated/sql/bnf/stmt_block.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@ unreserved_keyword ::=
| 'ENCODING'
| 'ENUM'
| 'ESCAPE'
| 'EXCLUDE'
| 'EXECUTE'
| 'EXPERIMENTAL'
| 'EXPERIMENTAL_AUDIT'
Expand Down Expand Up @@ -722,6 +723,7 @@ unreserved_keyword ::=
| 'OPTION'
| 'OPTIONS'
| 'ORDINALITY'
| 'OTHERS'
| 'OVER'
| 'OWNED'
| 'PARENT'
Expand Down Expand Up @@ -808,6 +810,7 @@ unreserved_keyword ::=
| 'TEMPORARY'
| 'TESTING_RELOCATE'
| 'TEXT'
| 'TIES'
| 'TRACE'
| 'TRANSACTION'
| 'TRIGGER'
Expand Down Expand Up @@ -2309,9 +2312,9 @@ opt_partition_clause ::=
|

opt_frame_clause ::=
'RANGE' frame_extent
| 'ROWS' frame_extent
| 'GROUPS' frame_extent
'RANGE' frame_extent opt_frame_exclusion
| 'ROWS' frame_extent opt_frame_exclusion
| 'GROUPS' frame_extent opt_frame_exclusion
|

extract_list ::=
Expand Down Expand Up @@ -2346,6 +2349,13 @@ frame_extent ::=
frame_bound
| 'BETWEEN' frame_bound 'AND' frame_bound

opt_frame_exclusion ::=
'EXCLUDE' 'CURRENT' 'ROW'
| 'EXCLUDE' 'GROUP'
| 'EXCLUDE' 'TIES'
| 'EXCLUDE' 'NO' 'OTHERS'
|

extract_arg ::=
'identifier'
| 'YEAR'
Expand Down
124 changes: 96 additions & 28 deletions pkg/sql/distsqlpb/processors.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
"github.com/cockroachdb/cockroach/pkg/util/duration"
"github.com/pkg/errors"
"github.com/cockroachdb/errors"
)

// Equals returns true if two aggregation specifiers are identical (and thus
Expand Down Expand Up @@ -45,7 +45,7 @@ func (a AggregatorSpec_Aggregation) Equals(b AggregatorSpec_Aggregation) bool {
return true
}

func (spec *WindowerSpec_Frame_Mode) initFromAST(w tree.WindowFrameMode) {
func (spec *WindowerSpec_Frame_Mode) initFromAST(w tree.WindowFrameMode) error {
switch w {
case tree.RANGE:
*spec = WindowerSpec_Frame_RANGE
Expand All @@ -54,11 +54,12 @@ func (spec *WindowerSpec_Frame_Mode) initFromAST(w tree.WindowFrameMode) {
case tree.GROUPS:
*spec = WindowerSpec_Frame_GROUPS
default:
panic("unexpected WindowFrameMode")
return errors.AssertionFailedf("unexpected WindowFrameMode")
}
return nil
}

func (spec *WindowerSpec_Frame_BoundType) initFromAST(bt tree.WindowFrameBoundType) {
func (spec *WindowerSpec_Frame_BoundType) initFromAST(bt tree.WindowFrameBoundType) error {
switch bt {
case tree.UnboundedPreceding:
*spec = WindowerSpec_Frame_UNBOUNDED_PRECEDING
Expand All @@ -71,8 +72,25 @@ func (spec *WindowerSpec_Frame_BoundType) initFromAST(bt tree.WindowFrameBoundTy
case tree.UnboundedFollowing:
*spec = WindowerSpec_Frame_UNBOUNDED_FOLLOWING
default:
panic("unexpected WindowFrameBoundType")
return errors.AssertionFailedf("unexpected WindowFrameBoundType")
}
return nil
}

func (spec *WindowerSpec_Frame_Exclusion) initFromAST(e tree.WindowFrameExclusion) error {
switch e {
case tree.NoExclusion:
*spec = WindowerSpec_Frame_NO_EXCLUSION
case tree.ExcludeCurrentRow:
*spec = WindowerSpec_Frame_EXCLUDE_CURRENT_ROW
case tree.ExcludeGroup:
*spec = WindowerSpec_Frame_EXCLUDE_GROUP
case tree.ExcludeTies:
*spec = WindowerSpec_Frame_EXCLUDE_TIES
default:
return errors.AssertionFailedf("unexpected WindowerFrameExclusion")
}
return nil
}

// If offset exprs are present, we evaluate them and save the encoded results
Expand All @@ -84,7 +102,9 @@ func (spec *WindowerSpec_Frame_Bounds) initFromAST(
return errors.Errorf("unexpected: Start Bound is nil")
}
spec.Start = WindowerSpec_Frame_Bound{}
spec.Start.BoundType.initFromAST(b.StartBound.BoundType)
if err := spec.Start.BoundType.initFromAST(b.StartBound.BoundType); err != nil {
return err
}
if b.StartBound.HasOffset() {
typedStartOffset := b.StartBound.OffsetExpr.(tree.TypedExpr)
dStartOffset, err := typedStartOffset.Eval(evalCtx)
Expand Down Expand Up @@ -126,7 +146,9 @@ func (spec *WindowerSpec_Frame_Bounds) initFromAST(

if b.EndBound != nil {
spec.End = &WindowerSpec_Frame_Bound{}
spec.End.BoundType.initFromAST(b.EndBound.BoundType)
if err := spec.End.BoundType.initFromAST(b.EndBound.BoundType); err != nil {
return err
}
if b.EndBound.HasOffset() {
typedEndOffset := b.EndBound.OffsetExpr.(tree.TypedExpr)
dEndOffset, err := typedEndOffset.Eval(evalCtx)
Expand Down Expand Up @@ -189,55 +211,101 @@ func isNegative(evalCtx *tree.EvalContext, offset tree.Datum) bool {
// InitFromAST initializes the spec based on tree.WindowFrame. It will evaluate
// offset expressions if present in the frame.
func (spec *WindowerSpec_Frame) InitFromAST(f *tree.WindowFrame, evalCtx *tree.EvalContext) error {
spec.Mode.initFromAST(f.Mode)
if err := spec.Mode.initFromAST(f.Mode); err != nil {
return err
}
if err := spec.Exclusion.initFromAST(f.Exclusion); err != nil {
return err
}
return spec.Bounds.initFromAST(f.Bounds, f.Mode, evalCtx)
}

func (spec WindowerSpec_Frame_Mode) convertToAST() tree.WindowFrameMode {
func (spec WindowerSpec_Frame_Mode) convertToAST() (tree.WindowFrameMode, error) {
switch spec {
case WindowerSpec_Frame_RANGE:
return tree.RANGE
return tree.RANGE, nil
case WindowerSpec_Frame_ROWS:
return tree.ROWS
return tree.ROWS, nil
case WindowerSpec_Frame_GROUPS:
return tree.GROUPS
return tree.GROUPS, nil
default:
panic("unexpected WindowerSpec_Frame_Mode")
return tree.WindowFrameMode(0), errors.AssertionFailedf("unexpected WindowerSpec_Frame_Mode")
}
}

func (spec WindowerSpec_Frame_BoundType) convertToAST() tree.WindowFrameBoundType {
func (spec WindowerSpec_Frame_BoundType) convertToAST() (tree.WindowFrameBoundType, error) {
switch spec {
case WindowerSpec_Frame_UNBOUNDED_PRECEDING:
return tree.UnboundedPreceding
return tree.UnboundedPreceding, nil
case WindowerSpec_Frame_OFFSET_PRECEDING:
return tree.OffsetPreceding
return tree.OffsetPreceding, nil
case WindowerSpec_Frame_CURRENT_ROW:
return tree.CurrentRow
return tree.CurrentRow, nil
case WindowerSpec_Frame_OFFSET_FOLLOWING:
return tree.OffsetFollowing
return tree.OffsetFollowing, nil
case WindowerSpec_Frame_UNBOUNDED_FOLLOWING:
return tree.UnboundedFollowing
return tree.UnboundedFollowing, nil
default:
panic("unexpected WindowerSpec_Frame_BoundType")
return tree.WindowFrameBoundType(0), errors.AssertionFailedf("unexpected WindowerSpec_Frame_BoundType")
}
}

func (spec WindowerSpec_Frame_Exclusion) convertToAST() (tree.WindowFrameExclusion, error) {
switch spec {
case WindowerSpec_Frame_NO_EXCLUSION:
return tree.NoExclusion, nil
case WindowerSpec_Frame_EXCLUDE_CURRENT_ROW:
return tree.ExcludeCurrentRow, nil
case WindowerSpec_Frame_EXCLUDE_GROUP:
return tree.ExcludeGroup, nil
case WindowerSpec_Frame_EXCLUDE_TIES:
return tree.ExcludeTies, nil
default:
return tree.WindowFrameExclusion(0), errors.AssertionFailedf("unexpected WindowerSpec_Frame_Exclusion")
}
}

// convertToAST produces tree.WindowFrameBounds based on
// WindowerSpec_Frame_Bounds. Note that it might not be fully equivalent to
// original - if offsetExprs were present in original tree.WindowFrameBounds,
// they are not included.
func (spec WindowerSpec_Frame_Bounds) convertToAST() tree.WindowFrameBounds {
bounds := tree.WindowFrameBounds{StartBound: &tree.WindowFrameBound{
BoundType: spec.Start.BoundType.convertToAST(),
}}
func (spec WindowerSpec_Frame_Bounds) convertToAST() (tree.WindowFrameBounds, error) {
bounds := tree.WindowFrameBounds{}
startBoundType, err := spec.Start.BoundType.convertToAST()
if err != nil {
return bounds, err
}
bounds.StartBound = &tree.WindowFrameBound{
BoundType: startBoundType,
}

if spec.End != nil {
bounds.EndBound = &tree.WindowFrameBound{BoundType: spec.End.BoundType.convertToAST()}
endBoundType, err := spec.End.BoundType.convertToAST()
if err != nil {
return bounds, err
}
bounds.EndBound = &tree.WindowFrameBound{BoundType: endBoundType}
}
return bounds
return bounds, nil
}

// ConvertToAST produces a tree.WindowFrame given a WindoweSpec_Frame.
func (spec *WindowerSpec_Frame) ConvertToAST() *tree.WindowFrame {
return &tree.WindowFrame{Mode: spec.Mode.convertToAST(), Bounds: spec.Bounds.convertToAST()}
func (spec *WindowerSpec_Frame) ConvertToAST() (*tree.WindowFrame, error) {
mode, err := spec.Mode.convertToAST()
if err != nil {
return nil, err
}
bounds, err := spec.Bounds.convertToAST()
if err != nil {
return nil, err
}
exclusion, err := spec.Exclusion.convertToAST()
if err != nil {
return nil, err
}
return &tree.WindowFrame{
Mode: mode,
Bounds: bounds,
Exclusion: exclusion,
}, nil
}
Loading

0 comments on commit 79d97a7

Please sign in to comment.