{"record":{"id":"b64e341679070245","repo":"cube-js/cube","slug":"data-blending-query-granularities-must-match","errorCode":null,"errorMessage":"Data blending query granularities must match","messagePattern":"Data blending query granularities must match","errorType":"validation","errorClass":"UserError","httpStatus":null,"severity":"error","filePath":"packages/cubejs-api-gateway/src/gateway.ts","lineNumber":1513,"sourceCode":"    this.log({\n      type: 'Query Rewrite completed',\n      queryRewriteId,\n      normalizedQueries: normalizedQueries.map(q => this.sanitizeQueryForLogging(q)),\n      duration: Date.now() - startTime,\n      query\n    }, context);\n\n    normalizedQueries = normalizedQueries.map(q => remapToQueryAdapterFormat(q));\n\n    if (normalizedQueries.some((currentQuery) => !currentQuery)) {\n      throw new Error('queryTransformer returned null query. Please check your queryTransformer implementation');\n    }\n\n    if (queryType === QueryTypeEnum.BLENDING_QUERY) {\n      const queryGranularity = getQueryGranularity(normalizedQueries);\n\n      if (queryGranularity.length > 1) {\n        throw new UserError('Data blending query granularities must match');\n      }\n      if (queryGranularity.length === 0) {\n        throw new UserError('Data blending query without granularity is not supported');\n      }\n    }\n\n    return [queryType, normalizedQueries, queryNormalizationResult.map((it) => remapToQueryAdapterFormat(it.normalizedQuery))];\n  }\n\n  protected async sql4sql({\n    query,\n    disablePostProcessing,\n    context,\n    res,\n  }: {query: string, disablePostProcessing: boolean} & BaseRequest) {\n    try {\n      await this.assertApiScope('sql', context.securityContext);\n","sourceCodeStart":1495,"sourceCodeEnd":1531,"githubUrl":"https://github.com/cube-js/cube/blob/7d981676b36392fec34088b9afab6bdcad40207c/packages/cubejs-api-gateway/src/gateway.ts#L1495-L1531","documentation":"For a data blending query (an array of queries blended into one result set), all sub-queries must share exactly one common date granularity (e.g. all 'day' or all 'month'). The gateway extracts the granularities of the normalized queries via getQueryGranularity and throws if more than one distinct granularity is found, because blending rows on differing time buckets is undefined.","triggerScenarios":"POST /cubejs-api/v1/load with an array query (QueryTypeEnum.BLENDING_QUERY) where e.g. query[0].timeDimensions[0].granularity = 'day' and query[1].timeDimensions[0].granularity = 'month', or one uses no granularity while another uses 'week'.","commonSituations":"Blending two charts with different zoom levels (daily vs monthly); building the query array programmatically and applying granularity only to some sub-queries; UIs that let users pick granularity per metric instead of globally.","solutions":["Set the same `granularity` on the timeDimension of every sub-query in the array.","Derive granularity once from a single UI control and apply it to all blended queries.","If different granularities are truly needed, issue separate /load requests and blend on the client instead."],"exampleFix":"// before\n[{ measures:['a.c'], timeDimensions:[{dimension:'a.date', granularity:'day', dateRange:['2024-01-01','2024-03-01']}] },\n { measures:['b.c'], timeDimensions:[{dimension:'b.date', granularity:'month', dateRange:['2024-01-01','2024-03-01']}] }]\n// after\n[{ measures:['a.c'], timeDimensions:[{dimension:'a.date', granularity:'month', dateRange:['2024-01-01','2024-03-01']}] },\n { measures:['b.c'], timeDimensions:[{dimension:'b.date', granularity:'month', dateRange:['2024-01-01','2024-03-01']}] }]","handlingStrategy":"validation","validationCode":"function blendGranularities(queries) {\n  return [...new Set(queries.flatMap(q => (q.timeDimensions ?? []).map(td => td.granularity).filter(Boolean)))];\n}\nif (Array.isArray(query) && blendGranularities(query).length > 1) throw new Error('All blended queries must use the same granularity');","typeGuard":"function hasMatchingGranularity(qs: any[]): boolean {\n  const gs = new Set(qs.flatMap(q => (q.timeDimensions ?? []).map(td => td.granularity).filter(Boolean)));\n  return gs.size <= 1;\n}","tryCatchPattern":"try {\n  return await cubeApi.load(queries);\n} catch (e) {\n  if (String(e?.message).includes('granularities must match')) {\n    const g = blendGranularities(queries)[0];\n    return await cubeApi.load(queries.map(q => alignGranularity(q, g)));\n  }\n  throw e;\n}","preventionTips":["Drive granularity from a single shared control in the UI, not per-query settings.","Unit-test blended query builders for granularity consistency.","Prefer the official client's blending support which normalizes query types."],"tags":["data-blending","query-validation","granularity"],"backgroundTag":"data-blending-granularity-mismatch","analyzedSha":"7d981676b36392fec34088b9afab6bdcad40207c","analyzedAt":"2026-09-02T03:45:10.400Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}