From 2f11ac2d1b4c436bfc7d242aec76934ba484b9d9 Mon Sep 17 00:00:00 2001 From: Fayvor Love Date: Wed, 13 May 2026 19:32:01 -0400 Subject: [PATCH 1/2] Add percentile aggregation types Added p25, p50, p75, p90, p95 measure types that generate PERCENTILE_CONT SQL for multi-stage aggregations. Enables benchmarking use cases like "median reports per org". Changes: - CubeValidator.ts: Added types to validation lists - BaseQuery.js: Added SQL generation for percentiles Co-Authored-By: Claude Opus 4.5 --- .../src/adapter/BaseQuery.js | 20 +++++++++++++++++++ .../src/compiler/CubeValidator.ts | 8 +++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index 24bc94423b54b..c256a14afd2c4 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -3793,6 +3793,16 @@ export class BaseQuery { funDef = this.countDistinctApprox(evaluateSql); } else if (symbol.type === 'countDistinct' || symbol.type === 'count' && !symbol.sql && multiplied) { funDef = `count(distinct ${evaluateSql})`; + } else if (symbol.type === 'p25') { + funDef = `PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY ${evaluateSql})`; + } else if (symbol.type === 'p50') { + funDef = `PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY ${evaluateSql})`; + } else if (symbol.type === 'p75') { + funDef = `PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY ${evaluateSql})`; + } else if (symbol.type === 'p90') { + funDef = `PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY ${evaluateSql})`; + } else if (symbol.type === 'p95') { + funDef = `PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY ${evaluateSql})`; } else if (CubeSymbols.isCalculatedMeasureType(symbol.type)) { // TODO calculated measure type will be ungrouped // if (this.multiStageDimensions.length !== this.dimensions.length) { @@ -3813,6 +3823,16 @@ export class BaseQuery { return `count(distinct ${evaluateSql})`; } else if (symbol.type === 'runningTotal') { return `sum(${evaluateSql})`; // TODO + } else if (symbol.type === 'p25') { + return `PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY ${evaluateSql})`; + } else if (symbol.type === 'p50') { + return `PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY ${evaluateSql})`; + } else if (symbol.type === 'p75') { + return `PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY ${evaluateSql})`; + } else if (symbol.type === 'p90') { + return `PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY ${evaluateSql})`; + } else if (symbol.type === 'p95') { + return `PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY ${evaluateSql})`; } if (multiplied) { if (symbol.type === 'number' && evaluateSql === 'count(*)') { diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts index b4d5b90b8572a..90b39584dd07e 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts @@ -784,16 +784,18 @@ const CubeRefreshKeySchema = condition( ); const measureType = Joi.string().valid( - 'number', 'string', 'boolean', 'time', 'sum', 'avg', 'min', 'max', 'countDistinct', 'runningTotal', 'countDistinctApprox' + 'number', 'string', 'boolean', 'time', 'sum', 'avg', 'min', 'max', 'countDistinct', 'runningTotal', 'countDistinctApprox', + 'p25', 'p50', 'p75', 'p90', 'p95' ); const measureTypeWithCount = Joi.string().valid( - 'count', 'number', 'string', 'boolean', 'time', 'sum', 'avg', 'min', 'max', 'countDistinct', 'runningTotal', 'countDistinctApprox' + 'count', 'number', 'string', 'boolean', 'time', 'sum', 'avg', 'min', 'max', 'countDistinct', 'runningTotal', 'countDistinctApprox', + 'p25', 'p50', 'p75', 'p90', 'p95' ); const multiStageMeasureType = Joi.string().valid( 'count', 'number', 'string', 'boolean', 'time', 'sum', 'avg', 'min', 'max', 'countDistinct', 'runningTotal', 'countDistinctApprox', 'numberAgg', - 'rank' + 'rank', 'p25', 'p50', 'p75', 'p90', 'p95' ); const timeShiftItemRequired = Joi.object({ From b7bf6e72daae3dfe5ce7328bf600474271659ed6 Mon Sep 17 00:00:00 2001 From: Fayvor Love Date: Wed, 13 May 2026 20:45:36 -0400 Subject: [PATCH 2/2] Add p99 percentile aggregation type Extends the percentile measure types to include p99 (99th percentile) for capturing extreme outliers in distributions. Co-Authored-By: Claude Opus 4.5 --- packages/cubejs-schema-compiler/src/adapter/BaseQuery.js | 4 ++++ .../cubejs-schema-compiler/src/compiler/CubeValidator.ts | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index c256a14afd2c4..f17a9b3819d10 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -3803,6 +3803,8 @@ export class BaseQuery { funDef = `PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY ${evaluateSql})`; } else if (symbol.type === 'p95') { funDef = `PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY ${evaluateSql})`; + } else if (symbol.type === 'p99') { + funDef = `PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY ${evaluateSql})`; } else if (CubeSymbols.isCalculatedMeasureType(symbol.type)) { // TODO calculated measure type will be ungrouped // if (this.multiStageDimensions.length !== this.dimensions.length) { @@ -3833,6 +3835,8 @@ export class BaseQuery { return `PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY ${evaluateSql})`; } else if (symbol.type === 'p95') { return `PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY ${evaluateSql})`; + } else if (symbol.type === 'p99') { + return `PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY ${evaluateSql})`; } if (multiplied) { if (symbol.type === 'number' && evaluateSql === 'count(*)') { diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts index 90b39584dd07e..39d9905588409 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts @@ -785,17 +785,17 @@ const CubeRefreshKeySchema = condition( const measureType = Joi.string().valid( 'number', 'string', 'boolean', 'time', 'sum', 'avg', 'min', 'max', 'countDistinct', 'runningTotal', 'countDistinctApprox', - 'p25', 'p50', 'p75', 'p90', 'p95' + 'p25', 'p50', 'p75', 'p90', 'p95', 'p99' ); const measureTypeWithCount = Joi.string().valid( 'count', 'number', 'string', 'boolean', 'time', 'sum', 'avg', 'min', 'max', 'countDistinct', 'runningTotal', 'countDistinctApprox', - 'p25', 'p50', 'p75', 'p90', 'p95' + 'p25', 'p50', 'p75', 'p90', 'p95', 'p99' ); const multiStageMeasureType = Joi.string().valid( 'count', 'number', 'string', 'boolean', 'time', 'sum', 'avg', 'min', 'max', 'countDistinct', 'runningTotal', 'countDistinctApprox', 'numberAgg', - 'rank', 'p25', 'p50', 'p75', 'p90', 'p95' + 'rank', 'p25', 'p50', 'p75', 'p90', 'p95', 'p99' ); const timeShiftItemRequired = Joi.object({