docs: @Nullable
    public TimeseriesQuery toTimeseriesQuery()
    {
      if (grouping == null || grouping.getHavingFilter() != null) {
        return null;
      }
  
      final Granularity queryGranularity;
      final boolean descending;
      int timeseriesLimit = 0;
      if (grouping.getDimensions().isEmpty()) {
        queryGranularity = Granularities.ALL;
        descending = false;
      } else if (grouping.getDimensions().size() == 1) {
        final DimensionExpression dimensionExpression = Iterables.getOnlyElement(grouping.getDimensions());
        queryGranularity = Expressions.toQueryGranularity(
            dimensionExpression.getDruidExpression(),
            plannerContext.getExprMacroTable()
        );
  
        if (queryGranularity == null) {
          // Timeseries only applies if the single dimension is granular __time.
          return null;
        }
        if (limitSpec != null) {
          // If there is a limit spec, set timeseriesLimit to given value if less than Integer.Max_VALUE
          if (limitSpec.isLimited()) {
            timeseriesLimit = limitSpec.getLimit();
          }
  
          if (limitSpec.getColumns().isEmpty()) {
            descending = false;
          } else {
            // We're ok if the first order by is time (since every time value is distinct, the rest of the columns
            // wouldn't matter anyway).
            final OrderByColumnSpec firstOrderBy = limitSpec.getColumns().get(0);
  
            if (firstOrderBy.getDimension().equals(dimensionExpression.getOutputName())) {
              // Order by time.
              descending = firstOrderBy.getDirection() == OrderByColumnSpec.Direction.DESCENDING;
            } else {
              // Order by something else.
              return null;
            }
          }
        } else {
          // No limitSpec.
          descending = false;
        }
      } else {
        // More than one dimension, timeseries cannot handle.
        return null;
      }
  
      final Filtration filtration = Filtration.create(filter).optimize(sourceQuerySignature);
  
      final List<PostAggregator> postAggregators = new ArrayList<>(grouping.getPostAggregators());
      if (sortProject != null) {
        postAggregators.addAll(sortProject.getPostAggregators());
      }
      final Map<String, Object> theContext = new HashMap<>();
      theContext.put("skipEmptyBuckets", true);
      theContext.putAll(plannerContext.getQueryContext());
  
      return new TimeseriesQuery(
          dataSource,
          filtration.getQuerySegmentSpec(),
          descending,
          getVirtualColumns(false),
          filtration.getDimFilter(),
          queryGranularity,
          grouping.getAggregatorFactories(),
          postAggregators,
          timeseriesLimit,
          ImmutableSortedMap.copyOf(theContext)
      );
    }