blob: ecf5391163d28bf4d7a068eedc937d47399fbd00 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.calcite.rex;
import org.apache.calcite.util.Pair;
import com.google.common.collect.ImmutableList;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.List;
import java.util.Objects;
import static com.google.common.base.Preconditions.checkArgument;
/**
* Specification of the window of rows over which a {@link RexOver} windowed
* aggregate is evaluated.
*
* <p>Treat it as immutable!
*/
public class RexWindow {
//~ Instance fields --------------------------------------------------------
public final ImmutableList<RexNode> partitionKeys;
public final ImmutableList<RexFieldCollation> orderKeys;
private final RexWindowBound lowerBound;
private final RexWindowBound upperBound;
private final boolean isRows;
private final String digest;
public final int nodeCount;
//~ Constructors -----------------------------------------------------------
/**
* Creates a window.
*
* <p>If you need to create a window from outside this package, use
* {@link RexBuilder#makeOver}.
*
* <p>If {@code orderKeys} is empty the bracket will usually be
* "BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING".
*
* <p>The digest assumes 'default' brackets, and does not print brackets or
* bounds that are the default.
*
* <p>If {@code orderKeys} is empty, assumes the bracket is "RANGE BETWEEN
* UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING" and does not print the
* bracket.
*
*<li>If {@code orderKeys} is not empty, the default top is "CURRENT ROW".
* The default bracket is "RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW",
* which will be printed as blank.
* "ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW" is different, and is
* printed as "ROWS UNBOUNDED PRECEDING".
* "ROWS BETWEEN 5 PRECEDING AND CURRENT ROW" is printed as
* "ROWS 5 PRECEDING".
*/
@SuppressWarnings("method.invocation.invalid")
RexWindow(
List<RexNode> partitionKeys,
List<RexFieldCollation> orderKeys,
RexWindowBound lowerBound,
RexWindowBound upperBound,
boolean isRows) {
this.partitionKeys = ImmutableList.copyOf(partitionKeys);
this.orderKeys = ImmutableList.copyOf(orderKeys);
this.lowerBound = Objects.requireNonNull(lowerBound, "lowerBound");
this.upperBound = Objects.requireNonNull(upperBound, "upperBound");
this.isRows = isRows;
this.nodeCount = computeCodeCount();
this.digest = computeDigest();
checkArgument(
!(lowerBound.isUnbounded() && lowerBound.isPreceding()
&& upperBound.isUnbounded() && upperBound.isFollowing() && isRows),
"use RANGE for unbounded, not ROWS");
}
//~ Methods ----------------------------------------------------------------
@Override public String toString() {
return digest;
}
@Override public int hashCode() {
return digest.hashCode();
}
@Override public boolean equals(@Nullable Object that) {
if (that instanceof RexWindow) {
RexWindow window = (RexWindow) that;
return digest.equals(window.digest);
}
return false;
}
private String computeDigest() {
return appendDigest_(new StringBuilder(), true).toString();
}
StringBuilder appendDigest(StringBuilder sb, boolean allowFraming) {
if (allowFraming) {
// digest was calculated with allowFraming=true; reuse it
return sb.append(digest);
} else {
return appendDigest_(sb, allowFraming);
}
}
private StringBuilder appendDigest_(StringBuilder sb, boolean allowFraming) {
final int initialLength = sb.length();
if (partitionKeys.size() > 0) {
sb.append("PARTITION BY ");
for (int i = 0; i < partitionKeys.size(); i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(partitionKeys.get(i));
}
}
if (orderKeys.size() > 0) {
sb.append(sb.length() > initialLength ? " ORDER BY " : "ORDER BY ");
for (int i = 0; i < orderKeys.size(); i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(orderKeys.get(i));
}
}
// There are 3 reasons to skip the ROWS/RANGE clause.
// 1. If this window is being used with a RANK-style function that does not
// allow framing, or
// 2. If there is no ORDER BY (in which case a frame is invalid), or
// 3. If the ROWS/RANGE clause is the default, "RANGE BETWEEN UNBOUNDED
// PRECEDING AND CURRENT ROW"
if (!allowFraming // 1
|| orderKeys.isEmpty() // 2
|| (lowerBound.isUnbounded() // 3
&& lowerBound.isPreceding()
&& upperBound.isCurrentRow()
&& !isRows)) {
// No ROWS or RANGE clause
} else if (upperBound.isCurrentRow()) {
// Per MSSQL: If ROWS/RANGE is specified and <window frame preceding>
// is used for <window frame extent> (short syntax) then this
// specification is used for the window frame boundary starting point and
// CURRENT ROW is used for the boundary ending point. For example
// "ROWS 5 PRECEDING" is equal to "ROWS BETWEEN 5 PRECEDING AND CURRENT
// ROW".
//
// By similar reasoning to (3) above, we print the shorter option if it is
// the default. If the RexWindow is, say, "ROWS BETWEEN 5 PRECEDING AND
// CURRENT ROW", we output "ROWS 5 PRECEDING" because it is equivalent and
// is shorter.
sb.append(sb.length() > initialLength
? (isRows ? " ROWS " : " RANGE ")
: (isRows ? "ROWS " : "RANGE "))
.append(lowerBound);
} else {
sb.append(sb.length() > initialLength
? (isRows ? " ROWS BETWEEN " : " RANGE BETWEEN ")
: (isRows ? "ROWS BETWEEN " : "RANGE BETWEEN "))
.append(lowerBound)
.append(" AND ")
.append(upperBound);
}
return sb;
}
public RexWindowBound getLowerBound() {
return lowerBound;
}
public RexWindowBound getUpperBound() {
return upperBound;
}
public boolean isRows() {
return isRows;
}
private int computeCodeCount() {
return RexUtil.nodeCount(partitionKeys)
+ RexUtil.nodeCount(Pair.left(orderKeys))
+ (lowerBound == null ? 0 : lowerBound.nodeCount())
+ (upperBound == null ? 0 : upperBound.nodeCount());
}
}