Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs-mintlify/reference/configuration/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,41 @@ different for multitenant setups.
| --------------- | ---------------------- | --------------------- |
| A valid string | `cubejs` | `cubejs` |

## `CUBEJS_AUTO_DRILL_MEMBERS`
Comment thread
igorlukanin marked this conversation as resolved.

If `true`, measures that declare no [`drill_members`][ref-drill-members] get a
default set computed when the data model is compiled: the cube's own dimensions,
primary key first, capped by
[`CUBEJS_AUTO_DRILL_MEMBERS_LIMIT`](#cubejs_auto_drill_members_limit). Without
this, a measure with no `drill_members` offers no [drill down][ref-drilldowns]
at all.

A measure that declares `drill_members` is never touched — including
`drill_members: []`, which is how you suppress the default for one measure.
Non-public dimensions are excluded, except the primary key; sub-query dimensions
are excluded too, since each one costs a join per drill.

For a view, the default names the view's own members, so a drill from a view
measure resolves rather than dead-ending. A view has no primary key of its own,
so there the set is simply the dimensions the view includes, in the order it
includes them.

| Possible Values | Default in Development | Default in Production |
| --------------- | ---------------------- | --------------------- |
| `true`, `false` | `false` | `false` |

## `CUBEJS_AUTO_DRILL_MEMBERS_LIMIT`

The most drill members
[`CUBEJS_AUTO_DRILL_MEMBERS`](#cubejs_auto_drill_members) will add to a measure.
Has no effect unless that variable is enabled. `0` disables the default set
entirely. With `CUBEJS_AUTO_DRILL_MEMBERS` enabled, a value that isn't a number
fails data model compilation.

| Possible Values | Default in Development | Default in Production |
| --------------- | ---------------------- | --------------------- |
| A number | `10` | `10` |

## `CUBEJS_AUTO_RUN_MODE`

The deployment-wide default for whether [Explore](/docs/explore-analyze/explore)
Expand Down Expand Up @@ -2211,6 +2246,8 @@ The port for a Cube deployment to listen to API connections on.
[link-tesseract]: https://cube.dev/blog/introducing-next-generation-data-modeling-engine
[ref-multi-stage-calculations]: /docs/data-modeling/measures#multi-stage-measures
[ref-folders]: /reference/data-modeling/view#folders
[ref-drill-members]: /reference/data-modeling/measures#drill_members
[ref-drilldowns]: /recipes/core-data-api/drilldowns
[ref-dataviz-tools]: /admin/connect-to-data/visualization-tools
[ref-context-to-app-id]: /reference/configuration/config#context_to_app_id
[ref-environments]: /admin/deployment/environments
Expand Down
13 changes: 12 additions & 1 deletion docs-mintlify/reference/data-modeling/measures.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,16 @@ cube(`orders`, {

</CodeGroup>

<Info>

A measure with no `drill_members` offers no drill down at all. Set
[`CUBEJS_AUTO_DRILL_MEMBERS`][ref-auto-drill-members] to give such measures a
default set — the cube's own dimensions, primary key first. Measures that
declare `drill_members` keep exactly what they declare, and `drill_members: []`
suppresses the default for a single measure.

</Info>


[ref-ai-context]: /docs/data-modeling/ai-context
[ref-ref-cubes]: /reference/data-modeling/cube
Expand All @@ -1509,4 +1519,5 @@ cube(`orders`, {
[link-d3-format]: https://d3js.org/d3-format
[link-iso-4217]: https://en.wikipedia.org/wiki/ISO_4217
[ref-calculated-measures]: /docs/data-modeling/measures#calculated-measures
[ref-schema-ref-preaggs-rollup]: /reference/data-modeling/pre-aggregations#rollup
[ref-schema-ref-preaggs-rollup]: /reference/data-modeling/pre-aggregations#rollup
[ref-auto-drill-members]: /reference/configuration/environment-variables#cubejs_auto_drill_members
8 changes: 8 additions & 0 deletions packages/cubejs-backend-shared/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,14 @@ const variables: Record<string, (...args: any) => any> = {
nestedFoldersDelimiter: () => get('CUBEJS_NESTED_FOLDERS_DELIMITER')
.default('')
.asString(),
// Measures that declare no drill members get a default set computed at
// compilation time. Declared drill members always take precedence.
autoDrillMembers: () => get('CUBEJS_AUTO_DRILL_MEMBERS')
.default('false')
.asBoolStrict(),
autoDrillMembersLimit: () => get('CUBEJS_AUTO_DRILL_MEMBERS_LIMIT')
.default('10')
.asInt(),
defaultTimezone: () => get('CUBEJS_DEFAULT_TIMEZONE')
.default('UTC')
.asString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export type DimensionDefinition = {
type: string;
sql(): string;
primaryKey?: true;
subQuery?: boolean;
ownedByCube: boolean;
fieldType?: string;
multiStage?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface ExtendedCubeSymbolDefinition extends CubeSymbolDefinition {
params?: Array<{ key: string; value: (...args: any[]) => string }>;
}>;
synthetic?: boolean;
subQuery?: boolean;
}

interface ExtendedCubeDefinition extends CubeDefinitionExtended {
Expand Down Expand Up @@ -236,6 +237,8 @@ export class CubeToMetaTransformer implements CompilerInterface {
const flatFolderSeparator = getEnv('nestedFoldersDelimiter');
const flatFolders: FlatFolder[] = [];

const autoDrillMembers = this.defaultDrillMembers(cubeName, extendedCube);

const processFolder = (folder: Folder, path: string[] = [], mergedMembers: string[] = []): NestedFolder => {
const flatMembers: string[] = [];
// After evaluation in CubeEvaluator, folder.includes contains resolved FolderMember items
Expand Down Expand Up @@ -290,7 +293,7 @@ export class CubeToMetaTransformer implements CompilerInterface {
const metricDef = nameToMetric[1] as ExtendedCubeSymbolDefinition;
const measureVisibility = isCubeVisible ? this.isVisible(metricDef, true) : false;
return {
...this.measureConfig(cubeName, cubeTitle, nameToMetric),
...this.measureConfig(cubeName, cubeTitle, nameToMetric, autoDrillMembers),
isVisible: measureVisibility,
public: measureVisibility,
};
Expand Down Expand Up @@ -419,17 +422,102 @@ export class CubeToMetaTransformer implements CompilerInterface {
return dimensionType === 'switch' ? 'string' : dimensionType;
}

private measureConfig(cubeName: string, cubeTitle: string, nameToMetric: [string, any]): Omit<MeasureConfig, 'isVisible' | 'public'> {
/**
* A view's included members don't carry `subQuery`, so a dimension reached
* through a view is resolved back to its source definition to classify it.
*/
private isSubQuery(extendedDimDef: ExtendedCubeSymbolDefinition): boolean {
if (extendedDimDef.subQuery) {
return true;
}

if (!extendedDimDef.aliasMember) {
return false;
}

try {
return !!this.cubeEvaluator.dimensionByPath(extendedDimDef.aliasMember)?.subQuery;
} catch (e) {
// An alias that no longer resolves is not this method's problem to report.
return false;
}
}

/**
* Drill members for measures that declare none: the cube's own dimensions,
* primary key first, capped.
*
* The primary key is included whatever its visibility — it is hidden by
* default, yet it is the member that identifies a row, and hand-written
* `drillMembers` name it routinely. Visibility governs the member picker,
* not what a drill query may reference. Everything else must be public.
*
* Views have no primary key (it is not propagated onto included members) and
* their dimensions span every source cube, so there the set is simply the
* view's own dimensions in include order.
*/
private defaultDrillMembers(cubeName: string, extendedCube: ExtendedCubeDefinition): string[] {
// The flag is checked before the limit is parsed: a malformed limit must not
// fail compilation for deployments that never enabled the feature.
if (!getEnv('autoDrillMembers')) {
return [];
}

const limit = getEnv('autoDrillMembersLimit');
if (limit <= 0) {
return [];
}

const primaryKeys: string[] = [];
const rest: string[] = [];

for (const [dimensionName, dimDef] of Object.entries(extendedCube.dimensions || {})) {
const extendedDimDef = dimDef as ExtendedCubeSymbolDefinition;

// Link helpers are generated, not authored — they would crowd out real
// attributes under the cap. Sub-query dimensions cost a join per drill.
const eligible = !extendedDimDef.synthetic && !this.isSubQuery(extendedDimDef);

if (eligible && extendedDimDef.primaryKey) {
primaryKeys.push(`${cubeName}.${dimensionName}`);
Comment on lines +481 to +482

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including a non-public primary key produces a drill member that clients can't resolve in production.

The reasoning ("visibility governs the member picker, not what a drill query may reference") holds for the query, but not for the meta document the picker is built from. Gateway.filterVisibleItemsInMeta() strips non-visible dimensions from the /meta response outside dev mode / playground auth:

// packages/cubejs-api-gateway/src/gateway.ts:645
dimensions: cube.config.dimensions?.filter(visibilityFilter),

So in production, orders.count.drillMembers[0] === 'orders.id' while orders.id is absent from the cube's dimensions. Meta.resolveMember() in cubejs-client-core then returns a not-found stub for it:

// packages/cubejs-client-core/src/Meta.ts:196
return { title: memberName, error: `Path not found '${memberName}'` };

The drill query still runs (ResultSet.drillDown() reads drillMembers off the load-response annotation, which is unfiltered), but any UI that renders the drill member list from /meta gets a raw path plus an error object as the first entry of every auto-generated set. Today that only happens for models whose authors explicitly named a hidden PK; with this flag it becomes the default for every cube in every model.

Options: gate the PK on this.isVisible(extendedDimDef, false) too, put it last instead of first, or keep it and document the interaction with production meta filtering explicitly. Either way it's worth a test asserting the shape a production client actually sees.

Fix this →

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked it and the reasoning holds: filterVisibleItemsInMeta does strip the hidden PK from /v1/meta, and resolveMember returns the not-found stub for it, so a picker built from meta shows a broken first entry for every automatic set. The drill itself still works, since drillDown() reads the unfiltered load-response annotation.

Holding it rather than fixing it, though: the ticket is on Igor > Check and the verification artifact is a meta before/after that demonstrates the primary-key-first ordering by name. Every fix here (gate the PK on visibility, or move it last) changes exactly what he is about to verify, so it needs his call first. Written up in the planning doc with the three options and the evidence; it will land as a follow-up rather than get resolved silently under him.

} else if (eligible && this.isVisible(extendedDimDef, true)) {
rest.push(`${cubeName}.${dimensionName}`);
}
}

return primaryKeys.concat(rest).slice(0, limit);
}

private measureConfig(
cubeName: string,
cubeTitle: string,
nameToMetric: [string, any],
autoDrillMembers: string[]
): Omit<MeasureConfig, 'isVisible' | 'public'> {
const [metricName, metricDef] = nameToMetric;
const extendedMetricDef = metricDef as ExtendedCubeSymbolDefinition;
const name = `${cubeName}.${metricName}`;

// Support both old 'drillMemberReferences' and new 'drillMembers' keys
const drillMembers = extendedMetricDef.drillMembers || extendedMetricDef.drillMemberReferences;

const drillMembersArray: string[] = (drillMembers && this.cubeEvaluator.evaluateReferences(
cubeName, drillMembers, { originalSorting: true }
)) || [];
// Keyed on whether drill members were declared at all, never on how many
// survive evaluation: `drill_members: []` is a deliberate opt-out, and a
// view including none of a declared set still evaluates to empty.
//
// The declared branch keeps its established shape verbatim, quirks included
// (a reference without an array literal evaluates to a bare string, which
// this field has always passed through). Normalizing it here would change
// emitted meta for models that never enabled the automatic set.
//
// The computed set is copied per measure: it is built once per cube and
// `metaConfig` is cached, so sharing the instance would let one consumer
// sorting in place reorder every other measure's list, for every request.
const drillMembersArray: string[] = drillMembers
? ((this.cubeEvaluator.evaluateReferences(
cubeName, drillMembers, { originalSorting: true }
) as string[]) || [])
Comment thread
igorlukanin marked this conversation as resolved.
: autoDrillMembers.slice();

const type = CubeSymbols.toMemberDataType(extendedMetricDef.type || 'number');
const isCumulative = extendedMetricDef.cumulative || BaseMeasure.isCumulative(extendedMetricDef);
Expand Down
Loading
Loading