RexWindow.java

/*
 * 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 static com.google.common.base.Preconditions.checkArgument;

import static java.util.Objects.requireNonNull;

/**
 * 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 RexWindowExclusion exclude;
  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,
      RexWindowExclusion exclude) {
    this.partitionKeys = ImmutableList.copyOf(partitionKeys);
    this.orderKeys = ImmutableList.copyOf(orderKeys);
    this.lowerBound = requireNonNull(lowerBound, "lowerBound");
    this.upperBound = requireNonNull(upperBound, "upperBound");
    this.exclude = exclude;
    this.isRows = isRows;
    this.nodeCount = computeCodeCount();
    this.digest = computeDigest();
    checkArgument(
        !(lowerBound.isUnboundedPreceding()
            && upperBound.isUnboundedFollowing()
            && 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.isEmpty()) {
      sb.append("PARTITION BY ");
      for (int i = 0; i < partitionKeys.size(); i++) {
        if (i > 0) {
          sb.append(", ");
        }
        sb.append(partitionKeys.get(i));
      }
    }
    if (!orderKeys.isEmpty()) {
      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 it is RANGE without ORDER BY (in which case all frames yield all
    //    rows),
    // 3. If it is an unbounded range
    //    ("RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING"
    //    or "ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING")
    //    with no ORDER BY.
    if (!allowFraming // 1
        || (!isRows && orderKeys.isEmpty()) // 2
        || (orderKeys.isEmpty()
            && lowerBound.isUnboundedPreceding() // 3
            && upperBound.isUnboundedFollowing())) {
      // Don't print a 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".
      //
      // 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.
      if (!isRows && lowerBound.isUnboundedPreceding()) {
        // OVER (ORDER BY x)
        // OVER (ORDER BY x RANGE UNBOUNDED PRECEDING)
        // OVER (ORDER BY x RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
        // are equivalent, so print the first (i.e. nothing).
      } else {
        sb.append(sb.length() > initialLength ? " " : "")
            .append(isRows ? "ROWS" : "RANGE")
            .append(' ')
            .append(lowerBound);
      }
    } else {
      sb.append(sb.length() > initialLength ? " " : "")
          .append(isRows ? "ROWS" : "RANGE")
          .append(" BETWEEN ")
          .append(lowerBound)
          .append(" AND ")
          .append(upperBound);
    }
    if (exclude != RexWindowExclusion.EXCLUDE_NO_OTHER) {
      sb.append(" ").append(exclude).append(" ");
    }
    return sb;
  }

  public RexWindowBound getLowerBound() {
    return lowerBound;
  }

  public RexWindowBound getUpperBound() {
    return upperBound;
  }

  public RexWindowExclusion getExclude() {
    return exclude;
  }

  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());
  }
}