{"record":{"id":"5ee66acbf8b76b46","repo":"cube-js/cube","slug":"multiple-rollups-found-that-can-be-used-for-rollup","errorCode":null,"errorMessage":"Multiple rollups found that can be used for rollup join ${JSON.stringify(join)}: ${fromPreAggObj.map(p => this.preAggregationId(p)).join(', ')}","messagePattern":"Multiple rollups found that can be used for rollup join (.+?): (.+?)","errorType":"validation","errorClass":"UserError","httpStatus":null,"severity":"error","filePath":"packages/cubejs-schema-compiler/src/adapter/PreAggregations.ts","lineNumber":1102,"sourceCode":"  }\n\n  private preAggObjForJoin(\n    preAggObjsToJoin: PreAggregationForQuery[],\n    joinMembers: string[],\n    join: JoinEdgeWithMembers,\n    rollupJoinPreAggName: string,\n  ): PreAggregationForQuery {\n    const fromPreAggObj = preAggObjsToJoin\n      .filter(p => joinMembers.every(m => !!p.references.dimensions.find(d => m === d)));\n    if (!fromPreAggObj.length) {\n      const msg = `No rollups found that can be used for a rollup join from \"${\n        join.from}\" (fromMembers: ${JSON.stringify(join.fromMembers)}) to \"${join.to}\" (toMembers: ${\n        JSON.stringify(join.toMembers)}). Check the \"${\n        rollupJoinPreAggName}\" pre-aggregation definition — you may have forgotten to specify the full dimension paths`;\n      throw new UserError(msg);\n    }\n    if (fromPreAggObj.length > 1) {\n      throw new UserError(\n        `Multiple rollups found that can be used for rollup join ${JSON.stringify(join)}: ${fromPreAggObj.map(p => this.preAggregationId(p)).join(', ')}`,\n      );\n    }\n    return fromPreAggObj[0];\n  }\n\n  private resolveJoinMembers(join: FinishedJoinTree): JoinEdgeWithMembers[] {\n    const joinMap = new Set<string>();\n\n    return join.joins.map(j => {\n      joinMap.add(j.originalFrom);\n\n      const memberPaths = this.query.collectMemberNamesFor(() => this.query.evaluateSql(j.originalFrom, j.join.sql)).map(m => m.split('.'));\n\n      const invalidMembers = memberPaths.filter(m => !joinMap.has(m[0]) && m[0] !== j.originalTo);\n      if (invalidMembers.length) {\n        throw new UserError(`Members ${invalidMembers.join(', ')} in join from '${j.originalFrom}' to '${j.originalTo}' doesn't reference join cubes`);\n      }","sourceCodeStart":1084,"sourceCodeEnd":1120,"githubUrl":"https://github.com/cube-js/cube/blob/7d981676b36392fec34088b9afab6bdcad40207c/packages/cubejs-schema-compiler/src/adapter/PreAggregations.ts#L1084-L1120","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Make the rollupJoin's fromMembers specific enough (use full dimension paths) so only one pre-aggregation matches.","Remove or rename one of the competing pre-aggregations listed in the error message.","Add distinguishing dimensions/timeDimensions to one of the candidate pre-aggregations so only one is eligible.","Restructure the rollupJoin to reference the exact pre-aggregation intended by name where possible."],"exampleFix":"// before: two pre-aggs on Orders both match join.fromMembers\npreAggregations: {\n  ordersByDay: { measures: [CUBE.count], dimensions: [Orders.status], timeDimension: Orders.createdAt, granularity: day },\n  ordersByDayWide: { measures: [CUBE.count], dimensions: [Orders.status], timeDimension: Orders.createdAt, granularity: day } // duplicate coverage\n}\n// after: remove/renamed the duplicate so exactly one rollup matches\npreAggregations: {\n  ordersByDay: { measures: [CUBE.count], dimensions: [Orders.status], timeDimension: Orders.createdAt, granularity: day }\n}","handlingStrategy":"validation","validationCode":"// Before defining a rollupJoin, list candidate pre-aggs per source cube and ensure exactly one covers each fromMembers set\nfunction uniqueRollupForJoin(preAggs, fromMembers) {\n  const matches = preAggs.filter(p => fromMembers.every(m => p.references.dimensions.includes(m)));\n  if (matches.length > 1) throw new Error(`Ambiguous rollupJoin source: ${matches.map(p => p.name).join(', ')}`);\n  return matches[0];\n}","typeGuard":"function hasUniqueRollupMatch(preAggs, fromMembers): boolean { return preAggs.filter(p => fromMembers.every(m => p.references.dimensions.includes(m))).length === 1; }","tryCatchPattern":"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; }","preventionTips":["Keep one pre-aggregation per (measures, dimensions, granularity) combination per cube","Use full dimension paths in rollupJoin fromMembers","Review pre-aggregation additions on joined cubes for overlap with rollupJoin sources","Add a unit test compiling your rollupJoins to catch ambiguity early"],"tags":["pre-aggregation","rollup-join","ambiguity","schema-compiler"],"backgroundTag":"ambiguous-rollup-join-source","analyzedSha":"7d981676b36392fec34088b9afab6bdcad40207c","analyzedAt":"2026-09-02T03:45:10.400Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}