docs: public static Pair<Grouper<RowBasedKey>, Accumulator<AggregateResult, Row>> createGrouperAccumulatorPair(
        final GroupByQuery query,
        final boolean isInputRaw,
        final Map<String, ValueType> rawInputRowSignature,
        final GroupByQueryConfig config,
        final Supplier<ByteBuffer> bufferSupplier,
        @Nullable final ReferenceCountingResourceHolder<ByteBuffer> combineBufferHolder,
        final int concurrencyHint,
        final LimitedTemporaryStorage temporaryStorage,
        final ObjectMapper spillMapper,
        final AggregatorFactory[] aggregatorFactories,
        @Nullable final ListeningExecutorService grouperSorter,
        final int priority,
        final boolean hasQueryTimeout,
        final long queryTimeoutAt,
        final int mergeBufferSize
    )
    {
      // concurrencyHint >= 1 for concurrent groupers, -1 for single-threaded
      Preconditions.checkArgument(concurrencyHint >= 1 || concurrencyHint == -1, "invalid concurrencyHint");
  
      final List<ValueType> valueTypes = DimensionHandlerUtils.getValueTypesFromDimensionSpecs(query.getDimensions());
  
      final GroupByQueryConfig querySpecificConfig = config.withOverrides(query);
      final boolean includeTimestamp = GroupByStrategyV2.getUniversalTimestamp(query) == null;
  
      final ThreadLocal<Row> columnSelectorRow = new ThreadLocal<>();
      final ColumnSelectorFactory columnSelectorFactory = query.getVirtualColumns().wrap(
          RowBasedColumnSelectorFactory.create(
              columnSelectorRow,
              rawInputRowSignature
          )
      );
  
      final boolean willApplyLimitPushDown = query.isApplyLimitPushDown();
      final DefaultLimitSpec limitSpec = willApplyLimitPushDown ? (DefaultLimitSpec) query.getLimitSpec() : null;
      boolean sortHasNonGroupingFields = false;
      if (willApplyLimitPushDown) {
        sortHasNonGroupingFields = DefaultLimitSpec.sortingOrderHasNonGroupingFields(
            limitSpec,
            query.getDimensions()
        );
      }
  
      final Grouper.KeySerdeFactory<RowBasedKey> keySerdeFactory = new RowBasedKeySerdeFactory(
          includeTimestamp,
          query.getContextSortByDimsFirst(),
          query.getDimensions(),
          querySpecificConfig.getMaxMergingDictionarySize() / (concurrencyHint == -1 ? 1 : concurrencyHint),
          valueTypes,
          aggregatorFactories,
          limitSpec
      );
  
      final Grouper<RowBasedKey> grouper;
      if (concurrencyHint == -1) {
        grouper = new SpillingGrouper<>(
            bufferSupplier,
            keySerdeFactory,
            columnSelectorFactory,
            aggregatorFactories,
            querySpecificConfig.getBufferGrouperMaxSize(),
            querySpecificConfig.getBufferGrouperMaxLoadFactor(),
            querySpecificConfig.getBufferGrouperInitialBuckets(),
            temporaryStorage,
            spillMapper,
            true,
            limitSpec,
            sortHasNonGroupingFields,
            mergeBufferSize
        );
      } else {
        final Grouper.KeySerdeFactory<RowBasedKey> combineKeySerdeFactory = new RowBasedKeySerdeFactory(
            includeTimestamp,
            query.getContextSortByDimsFirst(),
            query.getDimensions(),
            querySpecificConfig.getMaxMergingDictionarySize(), // use entire dictionary space for combining key serde
            valueTypes,
            aggregatorFactories,
            limitSpec
        );
  
        grouper = new ConcurrentGrouper<>(
            querySpecificConfig,
            bufferSupplier,
            combineBufferHolder,
            keySerdeFactory,
            combineKeySerdeFactory,
            columnSelectorFactory,
            aggregatorFactories,
            temporaryStorage,
            spillMapper,
            concurrencyHint,
            limitSpec,
            sortHasNonGroupingFields,
            grouperSorter,
            priority,
            hasQueryTimeout,
            queryTimeoutAt
        );
      }
  
      final int keySize = includeTimestamp ? query.getDimensions().size() + 1 : query.getDimensions().size();
      final ValueExtractFunction valueExtractFn = makeValueExtractFunction(
          query,
          isInputRaw,
          includeTimestamp,
          columnSelectorFactory,
          valueTypes
      );
  
      final Accumulator<AggregateResult, Row> accumulator = new Accumulator<AggregateResult, Row>()
      {
        @Override
        public AggregateResult accumulate(
            final AggregateResult priorResult,
            final Row row
        )
        {
          BaseQuery.checkInterrupted();
  
          if (priorResult != null && !priorResult.isOk()) {
            // Pass-through error returns without doing more work.
            return priorResult;
          }
  
          if (!grouper.isInitialized()) {
            grouper.init();
          }
  
          columnSelectorRow.set(row);
  
          final Comparable[] key = new Comparable[keySize];
          valueExtractFn.apply(row, key);
  
          final AggregateResult aggregateResult = grouper.aggregate(new RowBasedKey(key));
          columnSelectorRow.set(null);
  
          return aggregateResult;
        }
      };
  
      return new Pair<>(grouper, accumulator);
    }