diff options
author | aneporada <aneporada@yandex-team.ru> | 2022-02-24 00:54:02 +0300 |
---|---|---|
committer | aneporada <aneporada@yandex-team.ru> | 2022-02-24 00:54:02 +0300 |
commit | 3d4ff3fedcc8246cef9be0ddcc1f6825fbaf7875 (patch) | |
tree | a1a73de4e8e902848cff0c96dbafca7d009dfa56 | |
parent | 9f906d188714c5bd1dfc2d29117ba02ee1859730 (diff) | |
download | ydb-3d4ff3fedcc8246cef9be0ddcc1f6825fbaf7875.tar.gz |
[YQL-10265] Generate WinOnGroups/WinOnRanges in parser
ref:80d7724ff4e722f4fdf2de2cfa38fc4bc1ec0fc8
-rw-r--r-- | ydb/library/yql/core/type_ann/type_ann_core.cpp | 5 | ||||
-rw-r--r-- | ydb/library/yql/core/type_ann/type_ann_list.cpp | 18 | ||||
-rw-r--r-- | ydb/library/yql/core/type_ann/type_ann_list.h | 3 | ||||
-rw-r--r-- | ydb/library/yql/sql/v1/node.cpp | 44 | ||||
-rw-r--r-- | ydb/library/yql/sql/v1/sql.cpp | 137 | ||||
-rw-r--r-- | ydb/library/yql/sql/v1/sql_ut.cpp | 13 |
6 files changed, 148 insertions, 72 deletions
diff --git a/ydb/library/yql/core/type_ann/type_ann_core.cpp b/ydb/library/yql/core/type_ann/type_ann_core.cpp index 743692061a..9596c5942b 100644 --- a/ydb/library/yql/core/type_ann/type_ann_core.cpp +++ b/ydb/library/yql/core/type_ann/type_ann_core.cpp @@ -13018,8 +13018,9 @@ template <NKikimr::NUdf::EDataSlot DataSlot> Functions["MultiAggregate"] = &MultiAggregateWrapper; Functions["Aggregate"] = &AggregateWrapper; Functions["SqlAggregateAll"] = &SqlAggregateAllWrapper; - Functions["WinOnRows"] = &WinOnRowsWrapper; - Functions["WinOnRange"] = &WinOnRangeWrapper; + Functions["WinOnRows"] = &WinOnWrapper; + Functions["WinOnGroups"] = &WinOnWrapper; + Functions["WinOnRange"] = &WinOnWrapper; Functions["WindowTraits"] = &WindowTraitsWrapper; Functions["CalcOverWindow"] = &CalcOverWindowWrapper; Functions["CalcOverSessionWindow"] = &CalcOverWindowWrapper; diff --git a/ydb/library/yql/core/type_ann/type_ann_list.cpp b/ydb/library/yql/core/type_ann/type_ann_list.cpp index 1fa47b7600..4724ac20b5 100644 --- a/ydb/library/yql/core/type_ann/type_ann_list.cpp +++ b/ydb/library/yql/core/type_ann/type_ann_list.cpp @@ -4941,7 +4941,17 @@ namespace { return IGraphTransformer::TStatus::Ok; } - IGraphTransformer::TStatus WinOnRowsWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TContext& ctx) { + IGraphTransformer::TStatus WinOnWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TContext& ctx) { + if (input->IsCallable("WinOnGroups")) { + ctx.Expr.AddError(TIssue(ctx.Expr.GetPosition(input->Pos()), "GROUPS in frame specification are not supported yet")); + return IGraphTransformer::TStatus::Error; + } + if (input->IsCallable("WinOnRange")) { + ctx.Expr.AddError(TIssue(ctx.Expr.GetPosition(input->Pos()), "RANGE in frame specification is not supported yet")); + return IGraphTransformer::TStatus::Error; + } + YQL_ENSURE(input->IsCallable("WinOnRows")); + if (!EnsureMinArgsCount(*input, 1, ctx.Expr)) { return IGraphTransformer::TStatus::Error; } @@ -4996,12 +5006,6 @@ namespace { return IGraphTransformer::TStatus::Ok; } - IGraphTransformer::TStatus WinOnRangeWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TContext& ctx) { - Y_UNUSED(output); - ctx.Expr.AddError(TIssue(ctx.Expr.GetPosition(input->Pos()), "WinOnRange is not supported yet")); - return IGraphTransformer::TStatus::Error; - } - IGraphTransformer::TStatus WindowTraitsWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TContext& ctx) { if (!EnsureArgsCount(*input, 6, ctx.Expr)) { return IGraphTransformer::TStatus::Error; diff --git a/ydb/library/yql/core/type_ann/type_ann_list.h b/ydb/library/yql/core/type_ann/type_ann_list.h index c56b4879ee..4b84ae9846 100644 --- a/ydb/library/yql/core/type_ann/type_ann_list.h +++ b/ydb/library/yql/core/type_ann/type_ann_list.h @@ -97,8 +97,7 @@ namespace NTypeAnnImpl { IGraphTransformer::TStatus SkipNullMembersWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TContext& ctx); IGraphTransformer::TStatus FilterNullElementsWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TContext& ctx); IGraphTransformer::TStatus SkipNullElementsWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TContext& ctx); - IGraphTransformer::TStatus WinOnRowsWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TContext& ctx); - IGraphTransformer::TStatus WinOnRangeWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TContext& ctx); + IGraphTransformer::TStatus WinOnWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TContext& ctx); IGraphTransformer::TStatus WindowTraitsWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TContext& ctx); IGraphTransformer::TStatus CalcOverWindowWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TContext& ctx); IGraphTransformer::TStatus CalcOverWindowGroupWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TContext& ctx); diff --git a/ydb/library/yql/sql/v1/node.cpp b/ydb/library/yql/sql/v1/node.cpp index 364c1ec946..2e46537327 100644 --- a/ydb/library/yql/sql/v1/node.cpp +++ b/ydb/library/yql/sql/v1/node.cpp @@ -1833,14 +1833,34 @@ private: TSourcePtr FakeSource; }; -TNodePtr BuildFrameNode(const TFrameBound& frame) { - YQL_ENSURE(frame.Settings != FrameUndefined); +TNodePtr BuildFrameNode(const TFrameBound& frame, EFrameType frameType) { + TString settingStr; + switch (frame.Settings) { + case FramePreceding: settingStr = "preceding"; break; + case FrameCurrentRow: settingStr = "currentRow"; break; + case FrameFollowing: settingStr = "following"; break; + default: YQL_ENSURE(false, "Unexpected frame setting"); + } TNodePtr node = frame.Bound; TPosition pos = frame.Pos; - if (frame.Settings == FrameCurrentRow) { - node = BuildQuotedAtom(pos, "currentRow", TNodeFlags::Default); - } else if (!node) { + if (frameType != EFrameType::FrameByRows) { + TVector<TNodePtr> settings; + settings.push_back(BuildQuotedAtom(pos, settingStr, TNodeFlags::Default)); + if (frame.Settings != FrameCurrentRow) { + if (!node) { + node = BuildQuotedAtom(pos, "unbounded", TNodeFlags::Default); + } else if (!node->IsLiteral()) { + node = new TYqlFrameBound(pos, node); + } + settings.push_back(std::move(node)); + } + return BuildTuple(pos, std::move(settings)); + } + + // TODO: switch FrameByRows to common format above + YQL_ENSURE(frame.Settings != FrameCurrentRow, "Should be already replaced by 0 preceding/following"); + if (!node) { node = BuildLiteralVoid(pos); } else if (node->IsLiteral()) { YQL_ENSURE(node->GetLiteralType() == "Int32"); @@ -1860,13 +1880,12 @@ TNodePtr BuildFrameNode(const TFrameBound& frame) { } TNodePtr ISource::BuildWindowFrame(const TFrameSpecification& spec, bool isCompact) { - YQL_ENSURE(spec.FrameType == FrameByRows); YQL_ENSURE(spec.FrameExclusion == FrameExclNone); YQL_ENSURE(spec.FrameBegin); YQL_ENSURE(spec.FrameEnd); - auto frameBeginNode = BuildFrameNode(*spec.FrameBegin); - auto frameEndNode = BuildFrameNode(*spec.FrameEnd); + auto frameBeginNode = BuildFrameNode(*spec.FrameBegin, spec.FrameType); + auto frameEndNode = BuildFrameNode(*spec.FrameEnd, spec.FrameType); auto begin = Q(Y(Q("begin"), frameBeginNode)); auto end = Q(Y(Q("end"), frameEndNode)); @@ -1937,7 +1956,14 @@ TNodePtr ISource::BuildCalcOverWindow(TContext& ctx, const TString& label) { const auto& funcs = (funcsIter == FuncOverWindow.end()) ? TVector<TNodePtr>() : funcsIter->second; auto frames = Y(); - auto callOnFrame = Y("WinOnRows", BuildWindowFrame(*spec->Frame, spec->IsCompact)); + TString frameType; + switch (spec->Frame->FrameType) { + case EFrameType::FrameByRows: frameType = "WinOnRows"; break; + case EFrameType::FrameByRange: frameType = "WinOnRange"; break; + case EFrameType::FrameByGroups: frameType = "WinOnGroups"; break; + } + YQL_ENSURE(frameType); + auto callOnFrame = Y(frameType, BuildWindowFrame(*spec->Frame, spec->IsCompact)); for (auto& agg : aggs) { auto winTraits = agg->WindowTraits(listType); callOnFrame = L(callOnFrame, winTraits); diff --git a/ydb/library/yql/sql/v1/sql.cpp b/ydb/library/yql/sql/v1/sql.cpp index b3f12bb4e3..19a01db6df 100644 --- a/ydb/library/yql/sql/v1/sql.cpp +++ b/ydb/library/yql/sql/v1/sql.cpp @@ -859,11 +859,11 @@ protected: bool RoleNameClause(const TRule_role_name& node, TDeferredAtom& result, bool allowSystemRoles); bool RoleParameters(const TRule_create_user_option& node, TRoleParameters& result) ; private: - static bool IsValidFrameSettings(TContext& ctx, const TFrameBound& begin, const TFrameBound& end); + static bool IsValidFrameSettings(TContext& ctx, const TFrameSpecification& frameSpec, size_t sortSpecSize); static TString FrameSettingsToString(EFrameSettings settings, bool isUnbounded); bool FrameBound(const TRule_window_frame_bound& rule, TFrameBoundPtr& bound); - bool FrameClause(const TRule_window_frame_clause& node, TFrameSpecificationPtr& frameSpec); + bool FrameClause(const TRule_window_frame_clause& node, TFrameSpecificationPtr& frameSpec, size_t sortSpecSize); bool SortSpecification(const TRule_sort_specification& node, TVector<TSortSpecificationPtr>& sortSpecs); bool ClusterExpr(const TRule_cluster_expr& node, bool allowWildcard, bool allowBinding, TString& service, TDeferredAtom& cluster, bool& isBinding); @@ -6746,8 +6746,10 @@ bool CheckFrameBoundLiteral(TContext& ctx, const TFrameBound& bound, TMaybe<i32> return true; } -bool TSqlTranslation::IsValidFrameSettings(TContext& ctx, const TFrameBound& begin, const TFrameBound& end) { - Y_UNUSED(ctx); +bool TSqlTranslation::IsValidFrameSettings(TContext& ctx, const TFrameSpecification& frameSpec, size_t sortSpecSize) { + const TFrameBound& begin = *frameSpec.FrameBegin; + const TFrameBound& end = *frameSpec.FrameEnd; + YQL_ENSURE(begin.Settings != FrameUndefined); YQL_ENSURE(end.Settings != FrameUndefined); @@ -6770,11 +6772,25 @@ bool TSqlTranslation::IsValidFrameSettings(TContext& ctx, const TFrameBound& beg return false; } + if (frameSpec.FrameType == FrameByRange && sortSpecSize != 1) { + TStringBuf msg = "RANGE with <offset> PRECEDING/FOLLOWING requires exactly one expression in ORDER BY partition clause"; + if (begin.Bound) { + ctx.Error(begin.Bound->GetPos()) << msg; + return false; + } + if (end.Bound) { + ctx.Error(end.Bound->GetPos()) << msg; + return false; + } + } + TMaybe<i32> beginValue; TMaybe<i32> endValue; - if (!CheckFrameBoundLiteral(ctx, begin, beginValue) || !CheckFrameBoundLiteral(ctx, end, endValue)) { - return false; + if (frameSpec.FrameType != EFrameType::FrameByRange) { + if (!CheckFrameBoundLiteral(ctx, begin, beginValue) || !CheckFrameBoundLiteral(ctx, end, endValue)) { + return false; + } } if (beginValue.Defined() && endValue.Defined()) { @@ -6840,25 +6856,21 @@ bool TSqlTranslation::FrameBound(const TRule_window_frame_bound& rule, TFrameBou return true; } -bool TSqlTranslation::FrameClause(const TRule_window_frame_clause& rule, TFrameSpecificationPtr& frameSpec) { +bool TSqlTranslation::FrameClause(const TRule_window_frame_clause& rule, TFrameSpecificationPtr& frameSpec, size_t sortSpecSize) { + // window_frame_clause: window_frame_units window_frame_extent window_frame_exclusion?; frameSpec = new TFrameSpecification; const TString frameUnitStr = to_lower(Token(rule.GetRule_window_frame_units1().GetToken1())); if (frameUnitStr == "rows") { frameSpec->FrameType = EFrameType::FrameByRows; } else if (frameUnitStr == "range") { frameSpec->FrameType = EFrameType::FrameByRange; - } else if (frameUnitStr == "groups") { - frameSpec->FrameType = EFrameType::FrameByGroups; } else { - Ctx.Error() << "Unknown frame type in window specification: " << frameUnitStr; - return false; - } - if (frameSpec->FrameType != EFrameType::FrameByRows) { - Ctx.Error() << "Only ROWS is supported as window frame unit"; - return false; + YQL_ENSURE(frameUnitStr == "groups"); + frameSpec->FrameType = EFrameType::FrameByGroups; } auto frameExtent = rule.GetRule_window_frame_extent2(); + // window_frame_extent: window_frame_bound | window_frame_between; switch (frameExtent.Alt_case()) { case TRule_window_frame_extent::kAltWindowFrameExtent1: { auto start = frameExtent.GetAlt_window_frame_extent1().GetRule_window_frame_bound1(); @@ -6866,12 +6878,14 @@ bool TSqlTranslation::FrameClause(const TRule_window_frame_clause& rule, TFrameS return false; } + // frame end is CURRENT ROW frameSpec->FrameEnd = new TFrameBound; frameSpec->FrameEnd->Pos = frameSpec->FrameBegin->Pos; frameSpec->FrameEnd->Settings = FrameCurrentRow; break; } case TRule_window_frame_extent::kAltWindowFrameExtent2: { + // window_frame_between: BETWEEN window_frame_bound AND window_frame_bound; auto between = frameExtent.GetAlt_window_frame_extent2().GetRule_window_frame_between1(); if (!FrameBound(between.GetRule_window_frame_bound2(), frameSpec->FrameBegin)) { return false; @@ -6886,13 +6900,12 @@ bool TSqlTranslation::FrameClause(const TRule_window_frame_clause& rule, TFrameS } YQL_ENSURE(frameSpec->FrameBegin); YQL_ENSURE(frameSpec->FrameEnd); - // check for literal - if (!IsValidFrameSettings(Ctx, *frameSpec->FrameBegin, *frameSpec->FrameEnd)) - { + if (!IsValidFrameSettings(Ctx, *frameSpec, sortSpecSize)) { return false; } if (rule.HasBlock3()) { + // window_frame_exclusion: EXCLUDE CURRENT ROW | EXCLUDE GROUP | EXCLUDE TIES | EXCLUDE NO OTHERS; switch (rule.GetBlock3().GetRule_window_frame_exclusion1().Alt_case()) { case TRule_window_frame_exclusion::kAltWindowFrameExclusion1: frameSpec->FrameExclusion = FrameExclCurRow; @@ -6916,22 +6929,6 @@ bool TSqlTranslation::FrameClause(const TRule_window_frame_clause& rule, TFrameS return false; } - if (frameSpec->FrameType != EFrameType::FrameByRange) { - // replace FrameCurrentRow for ROWS/GROUPS with 0 preceding/following - // FrameCurrentRow has special meaning ( = first/last peer row) - auto replaceCurrent = [](TFrameBound& frame, bool preceding) { - frame.Settings = preceding ? EFrameSettings::FramePreceding : EFrameSettings::FrameFollowing; - frame.Bound = new TLiteralNumberNode<i32>(frame.Pos, "Int32", "0"); - }; - if (frameSpec->FrameBegin->Settings == EFrameSettings::FrameCurrentRow) { - replaceCurrent(*frameSpec->FrameBegin, true); - } - - if (frameSpec->FrameEnd->Settings == EFrameSettings::FrameCurrentRow) { - replaceCurrent(*frameSpec->FrameEnd, false); - } - } - return true; } @@ -6975,37 +6972,73 @@ TWindowSpecificationPtr TSqlTranslation::WindowSpecification(const TRule_window_ return {}; } } + const bool ordered = !winSpecPtr->OrderBy.empty(); if (rule.HasBlock4()) { - if (!FrameClause(rule.GetBlock4().GetRule_window_frame_clause1(), winSpecPtr->Frame)) { + if (!FrameClause(rule.GetBlock4().GetRule_window_frame_clause1(), winSpecPtr->Frame, winSpecPtr->OrderBy.size())) { return {}; } } else { winSpecPtr->Frame = new TFrameSpecification; - winSpecPtr->Frame->FrameType = EFrameType::FrameByRows; - winSpecPtr->Frame->FrameExclusion = EFrameExclusions::FrameExclNone; - - // BETWEEN UNBOUNDED PRECEDING AND ... winSpecPtr->Frame->FrameBegin = new TFrameBound; - winSpecPtr->Frame->FrameBegin->Settings = EFrameSettings::FramePreceding; - - const bool ordered = !winSpecPtr->OrderBy.empty(); winSpecPtr->Frame->FrameEnd = new TFrameBound; winSpecPtr->Frame->FrameBegin->Pos = winSpecPtr->Frame->FrameEnd->Pos = Ctx.Pos(); - if (ordered) { - if (Ctx.AnsiCurrentRow) { - // RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - // We emit FrameByRows here, but FrameCurrentRow works specially for ROWS - winSpecPtr->Frame->FrameEnd->Settings = EFrameSettings::FrameCurrentRow; - } else { - // ROWS BETWEEN UNBOUNDED PRECEDING AND 0 FOLLOWING - winSpecPtr->Frame->FrameEnd->Settings = EFrameSettings::FrameFollowing; - winSpecPtr->Frame->FrameEnd->Bound = new TLiteralNumberNode<i32>(Ctx.Pos(), "Int32", "0"); - } + winSpecPtr->Frame->FrameExclusion = EFrameExclusions::FrameExclNone; + + winSpecPtr->Frame->FrameBegin->Settings = EFrameSettings::FramePreceding; + if (Ctx.AnsiCurrentRow) { + // RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + winSpecPtr->Frame->FrameType = EFrameType::FrameByRange; + winSpecPtr->Frame->FrameEnd->Settings = EFrameSettings::FrameCurrentRow; + } else if (ordered) { + // legacy behavior + // ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + winSpecPtr->Frame->FrameType = EFrameType::FrameByRows; + winSpecPtr->Frame->FrameEnd->Settings = EFrameSettings::FrameCurrentRow; } else { // ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + winSpecPtr->Frame->FrameType = EFrameType::FrameByRows; winSpecPtr->Frame->FrameEnd->Settings = EFrameSettings::FrameFollowing; } } + + // Normalize and simplify + auto replaceCurrentWith = [](TFrameBound& frame, bool preceding, TNodePtr value ) { + frame.Settings = preceding ? EFrameSettings::FramePreceding : EFrameSettings::FrameFollowing; + frame.Bound = value; + }; + + const auto frameSpec = winSpecPtr->Frame; + if (frameSpec->FrameType == EFrameType::FrameByRange) { + if (!ordered) { + // CURRENT ROW -> UNBOUNDED + if (frameSpec->FrameBegin->Settings == EFrameSettings::FrameCurrentRow) { + replaceCurrentWith(*frameSpec->FrameBegin, true, nullptr); + } + if (frameSpec->FrameEnd->Settings == EFrameSettings::FrameCurrentRow) { + replaceCurrentWith(*frameSpec->FrameBegin, false, nullptr); + } + } + + // RANGE UNBOUNDED -> ROWS UNBOUNDED + if (frameSpec->FrameBegin->Settings == EFrameSettings::FramePreceding && !frameSpec->FrameBegin->Bound && + frameSpec->FrameEnd->Settings == EFrameSettings::FrameFollowing && !frameSpec->FrameEnd->Bound) + { + frameSpec->FrameType = EFrameType::FrameByRows; + } + } else { + // replace FrameCurrentRow for ROWS/GROUPS with 0 preceding/following + // FrameCurrentRow has special meaning ( = first/last peer row) + if (frameSpec->FrameBegin->Settings == EFrameSettings::FrameCurrentRow) { + TNodePtr zero = new TLiteralNumberNode<i32>(winSpecPtr->Frame->FrameBegin->Pos, "Int32", "0"); + replaceCurrentWith(*frameSpec->FrameBegin, true, zero); + } + + if (frameSpec->FrameEnd->Settings == EFrameSettings::FrameCurrentRow) { + TNodePtr zero = new TLiteralNumberNode<i32>(winSpecPtr->Frame->FrameEnd->Pos, "Int32", "0"); + replaceCurrentWith(*frameSpec->FrameEnd, false, zero); + } + } + return winSpecPtr; } diff --git a/ydb/library/yql/sql/v1/sql_ut.cpp b/ydb/library/yql/sql/v1/sql_ut.cpp index 1fe963dc96..25b656d4aa 100644 --- a/ydb/library/yql/sql/v1/sql_ut.cpp +++ b/ydb/library/yql/sql/v1/sql_ut.cpp @@ -2629,6 +2629,19 @@ select FormatType($f()); check("ROWS BETWEEN 5 FOLLOWING AND 5 PRECEDING", "<main>:2:14: Error: Frame cannot start from FOLLOWING and end with PRECEDING\n"); } + Y_UNIT_TEST(BlockedRangeValueWithoutSingleOrderBy) { + UNIT_ASSERT(SqlToYql("SELECT COUNT(*) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM plato.Input").IsOk()); + UNIT_ASSERT(SqlToYql("SELECT COUNT(*) OVER (RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM plato.Input").IsOk()); + + auto res = SqlToYql("SELECT COUNT(*) OVER (RANGE 5 PRECEDING) FROM plato.Input"); + UNIT_ASSERT(!res.Root); + UNIT_ASSERT_NO_DIFF(Err2Str(res), "<main>:1:29: Error: RANGE with <offset> PRECEDING/FOLLOWING requires exactly one expression in ORDER BY partition clause\n"); + + res = SqlToYql("SELECT COUNT(*) OVER (ORDER BY key, value RANGE 5 PRECEDING) FROM plato.Input"); + UNIT_ASSERT(!res.Root); + UNIT_ASSERT_NO_DIFF(Err2Str(res), "<main>:1:49: Error: RANGE with <offset> PRECEDING/FOLLOWING requires exactly one expression in ORDER BY partition clause\n"); + } + Y_UNIT_TEST(NoColumnsInFrameBounds) { NYql::TAstParseResult res = SqlToYql( "SELECT SUM(x) OVER w FROM plato.Input WINDOW w AS (ROWS BETWEEN\n" |