blob: 73182412231fc4c79cf7a751b93c60c0fbcb0ee8 [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.sql;
import org.apache.calcite.linq4j.Ord;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexWindowBound;
import org.apache.calcite.rex.RexWindowBounds;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.type.ReturnTypes;
import org.apache.calcite.sql.type.SqlTypeFamily;
import org.apache.calcite.sql.util.SqlBasicVisitor;
import org.apache.calcite.sql.util.SqlVisitor;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.apache.calcite.util.ControlFlowException;
import org.apache.calcite.util.ImmutableNullableList;
import org.apache.calcite.util.Litmus;
import org.apache.calcite.util.Util;
import com.google.common.collect.ImmutableList;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.dataflow.qual.Pure;
import java.math.BigDecimal;
import java.util.List;
import static org.apache.calcite.linq4j.Nullness.castNonNull;
import static org.apache.calcite.util.Static.RESOURCE;
/**
* SQL window specification.
*
* <p>For example, the query
*
* <blockquote>
* <pre>SELECT sum(a) OVER (w ROWS 3 PRECEDING)
* FROM t
* WINDOW w AS (PARTITION BY x, y ORDER BY z),
* w1 AS (w ROWS 5 PRECEDING UNBOUNDED FOLLOWING)</pre>
* </blockquote>
*
* <p>declares windows w and w1, and uses a window in an OVER clause. It thus
* contains 3 {@link SqlWindow} objects.
*/
public class SqlWindow extends SqlCall {
/**
* The FOLLOWING operator used exclusively in a window specification.
*/
public static final SqlPostfixOperator FOLLOWING_OPERATOR =
new SqlPostfixOperator("FOLLOWING", SqlKind.FOLLOWING, 20,
ReturnTypes.ARG0, null,
null);
/**
* The PRECEDING operator used exclusively in a window specification.
*/
public static final SqlPostfixOperator PRECEDING_OPERATOR =
new SqlPostfixOperator("PRECEDING", SqlKind.PRECEDING, 20,
ReturnTypes.ARG0, null,
null);
//~ Instance fields --------------------------------------------------------
/** The name of the window being declared. */
@Nullable SqlIdentifier declName;
/** The name of the window being referenced, or null. */
@Nullable SqlIdentifier refName;
/** The list of partitioning columns. */
SqlNodeList partitionList;
/** The list of ordering columns. */
SqlNodeList orderList;
/** Whether it is a physical (rows) or logical (values) range. */
SqlLiteral isRows;
/** The lower bound of the window. */
@Nullable SqlNode lowerBound;
/** The upper bound of the window. */
@Nullable SqlNode upperBound;
/** Whether to allow partial results. It may be null. */
@Nullable SqlLiteral allowPartial;
private @Nullable SqlCall windowCall = null;
//~ Constructors -----------------------------------------------------------
/**
* Creates a window.
*/
public SqlWindow(SqlParserPos pos, @Nullable SqlIdentifier declName,
@Nullable SqlIdentifier refName, SqlNodeList partitionList, SqlNodeList orderList,
SqlLiteral isRows, @Nullable SqlNode lowerBound, @Nullable SqlNode upperBound,
@Nullable SqlLiteral allowPartial) {
super(pos);
this.declName = declName;
this.refName = refName;
this.partitionList = partitionList;
this.orderList = orderList;
this.isRows = isRows;
this.lowerBound = lowerBound;
this.upperBound = upperBound;
this.allowPartial = allowPartial;
assert declName == null || declName.isSimple();
assert partitionList != null;
assert orderList != null;
}
public static SqlWindow create(@Nullable SqlIdentifier declName, @Nullable SqlIdentifier refName,
SqlNodeList partitionList, SqlNodeList orderList, SqlLiteral isRows,
@Nullable SqlNode lowerBound, @Nullable SqlNode upperBound, @Nullable SqlLiteral allowPartial,
SqlParserPos pos) {
// If there's only one bound and it's 'FOLLOWING', make it the upper
// bound.
if (upperBound == null
&& lowerBound != null
&& lowerBound.getKind() == SqlKind.FOLLOWING) {
upperBound = lowerBound;
lowerBound = null;
}
return new SqlWindow(pos, declName, refName, partitionList, orderList,
isRows, lowerBound, upperBound, allowPartial);
}
//~ Methods ----------------------------------------------------------------
@Override public SqlOperator getOperator() {
return SqlWindowOperator.INSTANCE;
}
@Override public SqlKind getKind() {
return SqlKind.WINDOW;
}
@SuppressWarnings("nullness")
@Override public List<SqlNode> getOperandList() {
return ImmutableNullableList.of(declName, refName, partitionList, orderList,
isRows, lowerBound, upperBound, allowPartial);
}
@SuppressWarnings("assignment.type.incompatible")
@Override public void setOperand(int i, @Nullable SqlNode operand) {
switch (i) {
case 0:
this.declName = (SqlIdentifier) operand;
break;
case 1:
this.refName = (SqlIdentifier) operand;
break;
case 2:
this.partitionList = (SqlNodeList) operand;
break;
case 3:
this.orderList = (SqlNodeList) operand;
break;
case 4:
this.isRows = (SqlLiteral) operand;
break;
case 5:
this.lowerBound = operand;
break;
case 6:
this.upperBound = operand;
break;
case 7:
this.allowPartial = (SqlLiteral) operand;
break;
default:
throw new AssertionError(i);
}
}
@Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
if (null != declName) {
declName.unparse(writer, 0, 0);
writer.keyword("AS");
}
// Override, so we don't print extra parentheses.
getOperator().unparse(writer, this, 0, 0);
}
public @Nullable SqlIdentifier getDeclName() {
return declName;
}
public void setDeclName(SqlIdentifier declName) {
assert declName.isSimple();
this.declName = declName;
}
public @Nullable SqlNode getLowerBound() {
return lowerBound;
}
public void setLowerBound(@Nullable SqlNode lowerBound) {
this.lowerBound = lowerBound;
}
public @Nullable SqlNode getUpperBound() {
return upperBound;
}
public void setUpperBound(@Nullable SqlNode upperBound) {
this.upperBound = upperBound;
}
/**
* Returns if the window is guaranteed to have rows.
* This is useful to refine data type of window aggregates.
* For instance sum(non-nullable) over (empty window) is NULL.
*
* @return true when the window is non-empty
*
* @see org.apache.calcite.rel.core.Window.Group#isAlwaysNonEmpty()
* @see SqlOperatorBinding#getGroupCount()
* @see org.apache.calcite.sql.validate.SqlValidatorImpl#resolveWindow(SqlNode, SqlValidatorScope)
*/
public boolean isAlwaysNonEmpty() {
final RexWindowBound lower;
final RexWindowBound upper;
if (lowerBound == null) {
if (upperBound == null) {
lower = RexWindowBounds.UNBOUNDED_PRECEDING;
} else {
lower = RexWindowBounds.CURRENT_ROW;
}
} else if (lowerBound instanceof SqlLiteral) {
lower = RexWindowBounds.create(lowerBound, null);
} else {
return false;
}
if (upperBound == null) {
upper = RexWindowBounds.CURRENT_ROW;
} else if (upperBound instanceof SqlLiteral) {
upper = RexWindowBounds.create(upperBound, null);
} else {
return false;
}
return isAlwaysNonEmpty(lower, upper);
}
public static boolean isAlwaysNonEmpty(RexWindowBound lower,
RexWindowBound upper) {
final int lowerKey = lower.getOrderKey();
final int upperKey = upper.getOrderKey();
return lowerKey > -1 && lowerKey <= upperKey;
}
public void setRows(SqlLiteral isRows) {
this.isRows = isRows;
}
@Pure
public boolean isRows() {
return isRows.booleanValue();
}
public SqlNodeList getOrderList() {
return orderList;
}
public void setOrderList(SqlNodeList orderList) {
this.orderList = orderList;
}
public SqlNodeList getPartitionList() {
return partitionList;
}
public void setPartitionList(SqlNodeList partitionList) {
this.partitionList = partitionList;
}
public @Nullable SqlIdentifier getRefName() {
return refName;
}
public void setWindowCall(@Nullable SqlCall windowCall) {
this.windowCall = windowCall;
assert windowCall == null
|| windowCall.getOperator() instanceof SqlAggFunction;
}
public @Nullable SqlCall getWindowCall() {
return windowCall;
}
// CHECKSTYLE: IGNORE 1
/** @see Util#deprecated(Object, boolean) */
static void checkSpecialLiterals(SqlWindow window, SqlValidator validator) {
final SqlNode lowerBound = window.getLowerBound();
final SqlNode upperBound = window.getUpperBound();
Object lowerLitType = null;
Object upperLitType = null;
SqlOperator lowerOp = null;
SqlOperator upperOp = null;
if (null != lowerBound) {
if (lowerBound.getKind() == SqlKind.LITERAL) {
lowerLitType = ((SqlLiteral) lowerBound).getValue();
if (Bound.UNBOUNDED_FOLLOWING == lowerLitType) {
throw validator.newValidationError(lowerBound,
RESOURCE.badLowerBoundary());
}
} else if (lowerBound instanceof SqlCall) {
lowerOp = ((SqlCall) lowerBound).getOperator();
}
}
if (null != upperBound) {
if (upperBound.getKind() == SqlKind.LITERAL) {
upperLitType = ((SqlLiteral) upperBound).getValue();
if (Bound.UNBOUNDED_PRECEDING == upperLitType) {
throw validator.newValidationError(upperBound,
RESOURCE.badUpperBoundary());
}
} else if (upperBound instanceof SqlCall) {
upperOp = ((SqlCall) upperBound).getOperator();
}
}
if (Bound.CURRENT_ROW == lowerLitType) {
if (null != upperOp) {
if (upperOp == PRECEDING_OPERATOR) {
throw validator.newValidationError(castNonNull(upperBound),
RESOURCE.currentRowPrecedingError());
}
}
} else if (null != lowerOp) {
if (lowerOp == FOLLOWING_OPERATOR) {
if (null != upperOp) {
if (upperOp == PRECEDING_OPERATOR) {
throw validator.newValidationError(castNonNull(upperBound),
RESOURCE.followingBeforePrecedingError());
}
} else if (null != upperLitType) {
if (Bound.CURRENT_ROW == upperLitType) {
throw validator.newValidationError(castNonNull(upperBound),
RESOURCE.currentRowFollowingError());
}
}
}
}
}
public static SqlNode createCurrentRow(SqlParserPos pos) {
return Bound.CURRENT_ROW.symbol(pos);
}
public static SqlNode createUnboundedFollowing(SqlParserPos pos) {
return Bound.UNBOUNDED_FOLLOWING.symbol(pos);
}
public static SqlNode createUnboundedPreceding(SqlParserPos pos) {
return Bound.UNBOUNDED_PRECEDING.symbol(pos);
}
public static SqlNode createFollowing(SqlNode e, SqlParserPos pos) {
return FOLLOWING_OPERATOR.createCall(pos, e);
}
public static SqlNode createPreceding(SqlNode e, SqlParserPos pos) {
return PRECEDING_OPERATOR.createCall(pos, e);
}
public static SqlNode createBound(SqlLiteral range) {
return range;
}
/**
* Returns whether an expression represents the "CURRENT ROW" bound.
*/
public static boolean isCurrentRow(SqlNode node) {
return (node instanceof SqlLiteral)
&& ((SqlLiteral) node).symbolValue(Bound.class) == Bound.CURRENT_ROW;
}
/**
* Returns whether an expression represents the "UNBOUNDED PRECEDING" bound.
*/
public static boolean isUnboundedPreceding(SqlNode node) {
return (node instanceof SqlLiteral)
&& ((SqlLiteral) node).symbolValue(Bound.class) == Bound.UNBOUNDED_PRECEDING;
}
/**
* Returns whether an expression represents the "UNBOUNDED FOLLOWING" bound.
*/
public static boolean isUnboundedFollowing(SqlNode node) {
return (node instanceof SqlLiteral)
&& ((SqlLiteral) node).symbolValue(Bound.class) == Bound.UNBOUNDED_FOLLOWING;
}
/**
* Creates a new window by combining this one with another.
*
* <p>For example,
*
* <blockquote><pre>WINDOW (w PARTITION BY x ORDER BY y)
* overlay
* WINDOW w AS (PARTITION BY z)</pre></blockquote>
*
* <p>yields
*
* <blockquote><pre>WINDOW (PARTITION BY z ORDER BY y)</pre></blockquote>
*
* <p>Does not alter this or the other window.
*
* @return A new window
*/
public SqlWindow overlay(SqlWindow that, SqlValidator validator) {
// check 7.11 rule 10c
final SqlNodeList partitions = getPartitionList();
if (0 != partitions.size()) {
throw validator.newValidationError(partitions.get(0),
RESOURCE.partitionNotAllowed());
}
// 7.11 rule 10d
final SqlNodeList baseOrder = getOrderList();
final SqlNodeList refOrder = that.getOrderList();
if ((0 != baseOrder.size()) && (0 != refOrder.size())) {
throw validator.newValidationError(baseOrder.get(0),
RESOURCE.orderByOverlap());
}
// 711 rule 10e
final SqlNode lowerBound = that.getLowerBound();
final SqlNode upperBound = that.getUpperBound();
if ((null != lowerBound) || (null != upperBound)) {
throw validator.newValidationError(that.isRows,
RESOURCE.refWindowWithFrame());
}
SqlIdentifier declNameNew = declName;
SqlIdentifier refNameNew = refName;
SqlNodeList partitionListNew = partitionList;
SqlNodeList orderListNew = orderList;
SqlLiteral isRowsNew = isRows;
SqlNode lowerBoundNew = lowerBound;
SqlNode upperBoundNew = upperBound;
SqlLiteral allowPartialNew = allowPartial;
// Clear the reference window, because the reference is now resolved.
// The overlaying window may have its own reference, of course.
refNameNew = null;
// Overlay other parameters.
if (setOperand(partitionListNew, that.partitionList, validator)) {
partitionListNew = that.partitionList;
}
if (setOperand(orderListNew, that.orderList, validator)) {
orderListNew = that.orderList;
}
if (setOperand(lowerBoundNew, that.lowerBound, validator)) {
lowerBoundNew = that.lowerBound;
}
if (setOperand(upperBoundNew, that.upperBound, validator)) {
upperBoundNew = that.upperBound;
}
return new SqlWindow(
SqlParserPos.ZERO,
declNameNew,
refNameNew,
partitionListNew,
orderListNew,
isRowsNew,
lowerBoundNew,
upperBoundNew,
allowPartialNew);
}
private static boolean setOperand(@Nullable SqlNode clonedOperand, @Nullable SqlNode thatOperand,
SqlValidator validator) {
if ((thatOperand != null) && !SqlNodeList.isEmptyList(thatOperand)) {
if ((clonedOperand == null)
|| SqlNodeList.isEmptyList(clonedOperand)) {
return true;
} else {
throw validator.newValidationError(clonedOperand,
RESOURCE.cannotOverrideWindowAttribute());
}
}
return false;
}
/**
* Overridden method to specifically check only the right subtree of a window
* definition.
*
* @param node The SqlWindow to compare to "this" window
* @param litmus What to do if an error is detected (nodes are not equal)
*
* @return boolean true if all nodes in the subtree are equal
*/
@Override public boolean equalsDeep(@Nullable SqlNode node, Litmus litmus) {
// This is the difference over super.equalsDeep. It skips
// operands[0] the declared name fo this window. We only want
// to check the window components.
return node == this
|| node instanceof SqlWindow
&& SqlNode.equalDeep(
Util.skip(getOperandList()),
Util.skip(((SqlWindow) node).getOperandList()), litmus);
}
/**
* Returns whether partial windows are allowed. If false, a partial window
* (for example, a window of size 1 hour which has only 45 minutes of data
* in it) will appear to windowed aggregate functions to be empty.
*/
@EnsuresNonNullIf(expression = "allowPartial", result = false)
public boolean isAllowPartial() {
// Default (and standard behavior) is to allow partial windows.
return allowPartial == null
|| allowPartial.booleanValue();
}
@Override public void validate(SqlValidator validator,
SqlValidatorScope scope) {
SqlValidatorScope operandScope = scope; // REVIEW
@SuppressWarnings("unused")
SqlIdentifier declName = this.declName;
SqlIdentifier refName = this.refName;
SqlNodeList partitionList = this.partitionList;
SqlNodeList orderList = this.orderList;
SqlLiteral isRows = this.isRows;
SqlNode lowerBound = this.lowerBound;
SqlNode upperBound = this.upperBound;
SqlLiteral allowPartial = this.allowPartial;
if (refName != null) {
SqlWindow win = validator.resolveWindow(this, operandScope);
partitionList = win.partitionList;
orderList = win.orderList;
isRows = win.isRows;
lowerBound = win.lowerBound;
upperBound = win.upperBound;
allowPartial = win.allowPartial;
}
for (SqlNode partitionItem : partitionList) {
try {
partitionItem.accept(Util.OverFinder.INSTANCE);
} catch (ControlFlowException e) {
throw validator.newValidationError(this,
RESOURCE.partitionbyShouldNotContainOver());
}
partitionItem.validateExpr(validator, operandScope);
}
for (SqlNode orderItem : orderList) {
boolean savedColumnReferenceExpansion =
validator.config().columnReferenceExpansion();
validator.transform(config -> config.withColumnReferenceExpansion(false));
try {
orderItem.accept(Util.OverFinder.INSTANCE);
} catch (ControlFlowException e) {
throw validator.newValidationError(this,
RESOURCE.orderbyShouldNotContainOver());
}
try {
orderItem.validateExpr(validator, scope);
} finally {
validator.transform(config ->
config.withColumnReferenceExpansion(savedColumnReferenceExpansion));
}
}
// 6.10 rule 6a Function RANK & DENSE_RANK require ORDER BY clause
if (orderList.size() == 0
&& !SqlValidatorUtil.containsMonotonic(scope)
&& windowCall != null
&& windowCall.getOperator().requiresOrder()) {
throw validator.newValidationError(this, RESOURCE.funcNeedsOrderBy());
}
// Run framing checks if there are any
if (upperBound != null || lowerBound != null) {
// 6.10 Rule 6a RANK & DENSE_RANK do not allow ROWS or RANGE
if (windowCall != null && !windowCall.getOperator().allowsFraming()) {
throw validator.newValidationError(isRows, RESOURCE.rankWithFrame());
}
SqlTypeFamily orderTypeFam = null;
// SQL03 7.10 Rule 11a
if (orderList.size() > 0) {
// if order by is a compound list then range not allowed
if (orderList.size() > 1
&& !isRows()
&& !onlySymbolBounds(lowerBound, upperBound)) {
throw validator.newValidationError(isRows,
RESOURCE.compoundOrderByProhibitsRange());
}
// get the type family for the sort key for Frame Boundary Val.
RelDataType orderType =
validator.deriveType(
operandScope,
orderList.get(0));
orderTypeFam = orderType.getSqlTypeName().getFamily();
} else {
// requires an ORDER BY clause if frame is logical(RANGE)
// We relax this requirement if the table appears to be
// sorted already
if (!onlySymbolBounds(lowerBound, upperBound)
&& !isRows()
&& !SqlValidatorUtil.containsMonotonic(scope)) {
throw validator.newValidationError(this,
RESOURCE.overMissingOrderBy());
}
}
// Let the bounds validate themselves
validateFrameBoundary(
lowerBound,
isRows(),
orderTypeFam,
validator,
operandScope);
validateFrameBoundary(
upperBound,
isRows(),
orderTypeFam,
validator,
operandScope);
// Validate across boundaries. 7.10 Rule 8 a-d
checkSpecialLiterals(this, validator);
} else if (orderList.size() == 0
&& !SqlValidatorUtil.containsMonotonic(scope)
&& windowCall != null
&& windowCall.getOperator().requiresOrder()) {
throw validator.newValidationError(this, RESOURCE.overMissingOrderBy());
}
if (!isRows() && !isAllowPartial()) {
throw validator.newValidationError(castNonNull(allowPartial),
RESOURCE.cannotUseDisallowPartialWithRange());
}
}
private boolean onlySymbolBounds(@Nullable SqlNode lowerBound, @Nullable SqlNode upperBound) {
return lowerBound != null && upperBound != null
&& (isCurrentRow(lowerBound) || isUnboundedPreceding(lowerBound))
&& (isCurrentRow(upperBound) || isUnboundedFollowing(upperBound));
}
private static void validateFrameBoundary(
@Nullable SqlNode bound,
boolean isRows,
@Nullable SqlTypeFamily orderTypeFam,
SqlValidator validator,
SqlValidatorScope scope) {
if (null == bound) {
return;
}
bound.validate(validator, scope);
switch (bound.getKind()) {
case LITERAL:
// is there really anything to validate here? this covers
// "CURRENT_ROW","unbounded preceding" & "unbounded following"
break;
case OTHER:
case FOLLOWING:
case PRECEDING:
assert bound instanceof SqlCall;
final SqlNode boundVal = ((SqlCall) bound).operand(0);
// SQL03 7.10 rule 11b Physical ROWS must be a numeric constant. JR:
// actually it's SQL03 7.11 rule 11b "exact numeric with scale 0"
// means not only numeric constant but exact numeric integral
// constant. We also interpret the spec. to not allow negative
// values, but allow zero.
if (isRows) {
if (boundVal instanceof SqlNumericLiteral) {
final SqlNumericLiteral boundLiteral =
(SqlNumericLiteral) boundVal;
if (!boundLiteral.isExact()
|| (boundLiteral.getScale() != null
&& boundLiteral.getValueAs(BigDecimal.class).stripTrailingZeros().scale() > 0)
|| (0 > boundLiteral.longValue(true))) {
// true == throw if not exact (we just tested that - right?)
throw validator.newValidationError(boundVal,
RESOURCE.rowMustBeNonNegativeIntegral());
}
} else {
// Allow expressions in ROWS clause
}
}
// if this is a range spec check and make sure the boundary type
// and order by type are compatible
if (orderTypeFam != null && !isRows) {
final RelDataType boundType = validator.deriveType(scope, boundVal);
final SqlTypeFamily boundTypeFamily =
boundType.getSqlTypeName().getFamily();
final List<SqlTypeFamily> allowableBoundTypeFamilies =
orderTypeFam.allowableDifferenceTypes();
if (allowableBoundTypeFamilies.isEmpty()) {
throw validator.newValidationError(boundVal,
RESOURCE.orderByDataTypeProhibitsRange());
}
if (!allowableBoundTypeFamilies.contains(boundTypeFamily)) {
throw validator.newValidationError(boundVal,
RESOURCE.orderByRangeMismatch());
}
}
break;
default:
throw new AssertionError("Unexpected node type");
}
}
/**
* Creates a window <code>(RANGE <i>columnName</i> CURRENT ROW)</code>.
*
* @param columnName Order column
*/
public SqlWindow createCurrentRowWindow(final String columnName) {
return SqlWindow.create(
null,
null,
new SqlNodeList(SqlParserPos.ZERO),
new SqlNodeList(
ImmutableList.of(
new SqlIdentifier(columnName, SqlParserPos.ZERO)),
SqlParserPos.ZERO),
SqlLiteral.createBoolean(true, SqlParserPos.ZERO),
SqlWindow.createCurrentRow(SqlParserPos.ZERO),
SqlWindow.createCurrentRow(SqlParserPos.ZERO),
SqlLiteral.createBoolean(true, SqlParserPos.ZERO),
SqlParserPos.ZERO);
}
/**
* Creates a window <code>(RANGE <i>columnName</i> UNBOUNDED
* PRECEDING)</code>.
*
* @param columnName Order column
*/
public SqlWindow createUnboundedPrecedingWindow(final String columnName) {
return SqlWindow.create(
null,
null,
new SqlNodeList(SqlParserPos.ZERO),
new SqlNodeList(
ImmutableList.of(
new SqlIdentifier(columnName, SqlParserPos.ZERO)),
SqlParserPos.ZERO),
SqlLiteral.createBoolean(false, SqlParserPos.ZERO),
SqlWindow.createUnboundedPreceding(SqlParserPos.ZERO),
SqlWindow.createCurrentRow(SqlParserPos.ZERO),
SqlLiteral.createBoolean(false, SqlParserPos.ZERO),
SqlParserPos.ZERO);
}
@Deprecated // to be removed before 2.0
public void populateBounds() {
if (lowerBound == null && upperBound == null) {
setLowerBound(SqlWindow.createUnboundedPreceding(pos));
}
if (lowerBound == null) {
setLowerBound(SqlWindow.createCurrentRow(pos));
}
if (upperBound == null) {
setUpperBound(SqlWindow.createCurrentRow(pos));
}
}
/**
* An enumeration of types of bounds in a window: <code>CURRENT ROW</code>,
* <code>UNBOUNDED PRECEDING</code>, and <code>UNBOUNDED FOLLOWING</code>.
*/
enum Bound implements Symbolizable {
CURRENT_ROW("CURRENT ROW"),
UNBOUNDED_PRECEDING("UNBOUNDED PRECEDING"),
UNBOUNDED_FOLLOWING("UNBOUNDED FOLLOWING");
private final String sql;
Bound(String sql) {
this.sql = sql;
}
@Override public String toString() {
return sql;
}
}
/** An operator describing a window specification. */
private static class SqlWindowOperator extends SqlOperator {
private static final SqlWindowOperator INSTANCE = new SqlWindowOperator();
private SqlWindowOperator() {
super("WINDOW", SqlKind.WINDOW, 2, true, null, null, null);
}
@Override public SqlSyntax getSyntax() {
return SqlSyntax.SPECIAL;
}
@SuppressWarnings("argument.type.incompatible")
@Override public SqlCall createCall(
@Nullable SqlLiteral functionQualifier,
SqlParserPos pos,
@Nullable SqlNode... operands) {
assert functionQualifier == null;
assert operands.length == 8;
return create(
(SqlIdentifier) operands[0],
(SqlIdentifier) operands[1],
(SqlNodeList) operands[2],
(SqlNodeList) operands[3],
(SqlLiteral) operands[4],
operands[5],
operands[6],
(SqlLiteral) operands[7],
pos);
}
@Override public <R> void acceptCall(
SqlVisitor<R> visitor,
SqlCall call,
boolean onlyExpressions,
SqlBasicVisitor.ArgHandler<R> argHandler) {
if (onlyExpressions) {
for (Ord<SqlNode> operand : Ord.zip(call.getOperandList())) {
// if the second param is an Identifier then it's supposed to
// be a name from a window clause and isn't part of the
// group by check
if (operand.e == null) {
continue;
}
if (operand.i == 1 && operand.e instanceof SqlIdentifier) {
// skip refName
continue;
}
argHandler.visitChild(visitor, call, operand.i, operand.e);
}
} else {
super.acceptCall(visitor, call, onlyExpressions, argHandler);
}
}
@Override public void unparse(
SqlWriter writer,
SqlCall call,
int leftPrec,
int rightPrec) {
final SqlWindow window = (SqlWindow) call;
final SqlWriter.Frame frame =
writer.startList(SqlWriter.FrameTypeEnum.WINDOW, "(", ")");
if (window.refName != null) {
window.refName.unparse(writer, 0, 0);
}
if (window.partitionList.size() > 0) {
writer.sep("PARTITION BY");
final SqlWriter.Frame partitionFrame = writer.startList("", "");
window.partitionList.unparse(writer, 0, 0);
writer.endList(partitionFrame);
}
if (window.orderList.size() > 0) {
writer.sep("ORDER BY");
final SqlWriter.Frame orderFrame = writer.startList("", "");
window.orderList.unparse(writer, 0, 0);
writer.endList(orderFrame);
}
SqlNode lowerBound = window.lowerBound;
SqlNode upperBound = window.upperBound;
if (lowerBound == null) {
// No ROWS or RANGE clause
} else if (upperBound == null) {
if (window.isRows()) {
writer.sep("ROWS");
} else {
writer.sep("RANGE");
}
lowerBound.unparse(writer, 0, 0);
} else {
if (window.isRows()) {
writer.sep("ROWS BETWEEN");
} else {
writer.sep("RANGE BETWEEN");
}
lowerBound.unparse(writer, 0, 0);
writer.keyword("AND");
upperBound.unparse(writer, 0, 0);
}
// ALLOW PARTIAL/DISALLOW PARTIAL
if (window.allowPartial == null) {
// do nothing
} else if (window.isAllowPartial()) {
// We could output "ALLOW PARTIAL", but this syntax is
// non-standard. Omitting the clause has the same effect.
} else {
writer.keyword("DISALLOW PARTIAL");
}
writer.endList(frame);
}
}
}