cube-js/cube · error · UserError

Multiple rollups found that can be used for rollup join

Error message

Multiple rollups found that can be used for rollup join ${JSON.stringify(join)}: ${fromPreAggObj.map(p => this.preAggregationId(p)).join(', ')}

What it means

When resolving a rollupJoin pre-aggregation, Cube finds candidate pre-aggregations matching the join's 'from' side. If more than one pre-aggregation qualifies, the join becomes ambiguous and the compiler throws this UserError instead of guessing. It is thrown in PreAggregations.ts during buildJoinPreAggregationForQuery resolution.

Solutions

  1. Make the rollupJoin's fromMembers specific enough (use full dimension paths) so only one pre-aggregation matches.
  2. Remove or rename one of the competing pre-aggregations listed in the error message.
  3. Add distinguishing dimensions/timeDimensions to one of the candidate pre-aggregations so only one is eligible.
  4. Restructure the rollupJoin to reference the exact pre-aggregation intended by name where possible.

Example fix

// before: two pre-aggs on Orders both match join.fromMembers
preAggregations: {
  ordersByDay: { measures: [CUBE.count], dimensions: [Orders.status], timeDimension: Orders.createdAt, granularity: day },
  ordersByDayWide: { measures: [CUBE.count], dimensions: [Orders.status], timeDimension: Orders.createdAt, granularity: day } // duplicate coverage
}
// after: remove/renamed the duplicate so exactly one rollup matches
preAggregations: {
  ordersByDay: { measures: [CUBE.count], dimensions: [Orders.status], timeDimension: Orders.createdAt, granularity: day }
}
Defensive patterns

Strategy: validation

Validate before calling

// Before defining a rollupJoin, list candidate pre-aggs per source cube and ensure exactly one covers each fromMembers set
function uniqueRollupForJoin(preAggs, fromMembers) {
  const matches = preAggs.filter(p => fromMembers.every(m => p.references.dimensions.includes(m)));
  if (matches.length > 1) throw new Error(`Ambiguous rollupJoin source: ${matches.map(p => p.name).join(', ')}`);
  return matches[0];
}

Type guard

function hasUniqueRollupMatch(preAggs, fromMembers): boolean { return preAggs.filter(p => fromMembers.every(m => p.references.dimensions.includes(m))).length === 1; }

Try / catch

try { await cube.query(query); } catch (e) { if (/Multiple rollups found that can be used for rollup join/.test(e.message)) { console.error('Narrow fromMembers or remove a competing pre-aggregation:', e.message); } else throw e; }

Prevention

When it happens

Trigger: A pre-aggregation with type 'rollupJoin' whose fromMembers match two or more defined pre-aggregations on the source cube (e.g. two pre-aggregations covering the same dimension paths, or one plain and one multi-stage rollup both satisfying the join 'from' reference).

Common situations: Defining multiple overlapping pre-aggregations on a joined cube (e.g. one for daily and one covering all grain with the same dimensions); adding a new pre-aggregation that accidentally subsumes an existing rollupJoin source; forgetting to narrow fromMembers so several rollups match.

Related errors


AI-assisted analysis of cube-js/cube@7d981676b3 (2026-09-02). Data as JSON: /api/errors/5ee66acbf8b76b46. Report an issue: GitHub.

Appendix: source

Thrown at packages/cubejs-schema-compiler/src/adapter/PreAggregations.ts:1102

  }

  private preAggObjForJoin(
    preAggObjsToJoin: PreAggregationForQuery[],
    joinMembers: string[],
    join: JoinEdgeWithMembers,
    rollupJoinPreAggName: string,
  ): PreAggregationForQuery {
    const fromPreAggObj = preAggObjsToJoin
      .filter(p => joinMembers.every(m => !!p.references.dimensions.find(d => m === d)));
    if (!fromPreAggObj.length) {
      const msg = `No rollups found that can be used for a rollup join from "${
        join.from}" (fromMembers: ${JSON.stringify(join.fromMembers)}) to "${join.to}" (toMembers: ${
        JSON.stringify(join.toMembers)}). Check the "${
        rollupJoinPreAggName}" pre-aggregation definition — you may have forgotten to specify the full dimension paths`;
      throw new UserError(msg);
    }
    if (fromPreAggObj.length > 1) {
      throw new UserError(
        `Multiple rollups found that can be used for rollup join ${JSON.stringify(join)}: ${fromPreAggObj.map(p => this.preAggregationId(p)).join(', ')}`,
      );
    }
    return fromPreAggObj[0];
  }

  private resolveJoinMembers(join: FinishedJoinTree): JoinEdgeWithMembers[] {
    const joinMap = new Set<string>();

    return join.joins.map(j => {
      joinMap.add(j.originalFrom);

      const memberPaths = this.query.collectMemberNamesFor(() => this.query.evaluateSql(j.originalFrom, j.join.sql)).map(m => m.split('.'));

      const invalidMembers = memberPaths.filter(m => !joinMap.has(m[0]) && m[0] !== j.originalTo);
      if (invalidMembers.length) {
        throw new UserError(`Members ${invalidMembers.join(', ')} in join from '${j.originalFrom}' to '${j.originalTo}' doesn't reference join cubes`);
      }

View on GitHub (pinned to 7d981676b3)