Skip to content

Commit a7a7604

Browse files
committed
refactor(granularities): resolve and apply global config in CubeSymbols instead of a bespoke CompilerApi compile flow
1 parent cc08f4a commit a7a7604

9 files changed

Lines changed: 229 additions & 213 deletions

File tree

packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { UserError } from './UserError';
2020
import { BaseQuery, PreAggregationDefinitionExtended } from '../adapter';
2121
import type { CubeValidator } from './CubeValidator';
2222
import type { ErrorReporter } from './ErrorReporter';
23+
import type { GlobalGranularitiesConfig } from './GlobalGranularitiesConfig';
2324
import { FinishedJoinTree } from './JoinGraph';
2425

2526
export type SegmentDefinition = {
@@ -210,13 +211,14 @@ export class CubeEvaluator extends CubeSymbols {
210211
private isRbacEnabledCache: boolean | null = null;
211212

212213
public constructor(
213-
protected readonly cubeValidator: CubeValidator
214+
protected readonly cubeValidator: CubeValidator,
215+
granularitiesResolver?: () => Promise<GlobalGranularitiesConfig>,
214216
) {
215-
super(true);
217+
super(true, granularitiesResolver);
216218
}
217219

218-
public compile(cubes: any[], errorReporter: ErrorReporter) {
219-
super.compile(cubes, errorReporter);
220+
public async compile(cubes: any[], errorReporter: ErrorReporter) {
221+
await super.compile(cubes, errorReporter);
220222
const validCubes = this.cubeList.filter(cube => this.cubeValidator.isCubeValid(cube)).sort((a, b) => {
221223
if (a.isView) {
222224
return 1;

packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ import { camelize } from 'inflection';
55
import { UserError } from './UserError';
66
import { DynamicReference } from './DynamicReference';
77
import { camelizeCube } from './utils';
8-
import { normalizeGranularitiesBlock, NormalizedGranularitiesBlock } from './GranularityResolver';
8+
import {
9+
normalizeGranularitiesBlock,
10+
NormalizedGranularitiesBlock,
11+
resolveDimensionGranularities,
12+
} from './GranularityResolver';
13+
import {
14+
buildBuiltInsCatalog,
15+
DEFAULT_GRANULARITIES_CONFIG,
16+
GlobalGranularitiesConfig,
17+
} from './GlobalGranularitiesConfig';
918

1019
import type { ErrorReporter } from './ErrorReporter';
1120
import { TranspilerSymbolResolver } from './transpilers';
@@ -297,13 +306,30 @@ export class CubeSymbols implements TranspilerSymbolResolver, CompilerInterface
297306

298307
private resolveSymbolsCallContext: any;
299308

300-
public constructor(evaluateViews = false) {
309+
// Resolves the global granularities config; shared across the compilers of one prepareCompiler so
310+
// a `config.granularities` function runs once per compile.
311+
private readonly granularitiesResolver?: () => Promise<GlobalGranularitiesConfig>;
312+
313+
// Global granularities resolved at the start of compile(); the default catalog until then.
314+
private globalGranularities: GlobalGranularitiesConfig = DEFAULT_GRANULARITIES_CONFIG;
315+
316+
public constructor(
317+
evaluateViews = false,
318+
granularitiesResolver?: () => Promise<GlobalGranularitiesConfig>,
319+
) {
301320
this.symbols = {};
302321
this.builtCubes = {};
303322
this.cubeDefinitions = {};
304323
this.funcArgumentsValues = {};
305324
this.cubeList = [];
306325
this.evaluateViews = evaluateViews;
326+
this.granularitiesResolver = granularitiesResolver;
327+
}
328+
329+
// The resolved global config for this compile. Read by CubeToMetaTransformer (for
330+
// `effectiveGranularities`) and by the /v1/granularities catalog.
331+
public get globalGranularitiesConfig(): GlobalGranularitiesConfig {
332+
return this.globalGranularities;
307333
}
308334

309335
public free() {
@@ -315,7 +341,14 @@ export class CubeSymbols implements TranspilerSymbolResolver, CompilerInterface
315341
this.resolveSymbolsCallContext = undefined;
316342
}
317343

318-
public compile(cubes: CubeDefinition[], errorReporter: ErrorReporter) {
344+
public async compile(cubes: CubeDefinition[], errorReporter: ErrorReporter) {
345+
// Resolve the global granularities config before any cube is transformed: the merge below writes
346+
// global customs onto time dimensions, and the SQL layer resolves them from that same map.
347+
// The phase driver chains each compile() through `.then()`, so awaiting here is safe.
348+
if (this.granularitiesResolver) {
349+
this.globalGranularities = await this.granularitiesResolver();
350+
}
351+
319352
this.cubeDefinitions = Object.fromEntries(
320353
cubes.map((c): [string, CubeDefinition] => [c.name, c])
321354
);
@@ -609,28 +642,73 @@ export class CubeSymbols implements TranspilerSymbolResolver, CompilerInterface
609642
} as CubeSymbolsDefinition;
610643
}
611644

612-
// Stores the canonical `granularitiesBlock` on each time dimension and rewrites
613-
// `granularities` to the dict of locally-defined customs only — preserving the legacy shape
614-
// that BaseQuery, prepare-annotation, and CubeToMetaTransformer already read.
645+
// Stores the canonical `granularitiesBlock` on each time dimension, rewrites `granularities` to
646+
// the locally-defined customs — preserving the legacy shape that BaseQuery, prepare-annotation
647+
// and CubeToMetaTransformer read — then layers the effective global customs on top so they are
648+
// resolvable in SQL and visible to pre-aggregation matching.
615649
private normalizeDimensionGranularities(dimensions: Record<string, any> | undefined) {
616650
if (!dimensions) {
617651
return;
618652
}
619653

654+
// Every time dimension participates: global customs apply even to one that declares no
655+
// `granularities` of its own.
620656
for (const dim of Object.values(dimensions)) {
621-
// A view's included dimension already carries a propagated granularitiesBlock (with the
622-
// source dimension's includes/excludes) alongside the custom-only `granularities` map.
623-
// Re-normalizing the custom-only map here would reset includes to '*' and drop the source's
624-
// includes/excludes, so only normalize dimensions that haven't been normalized yet.
625-
if (dim && dim.type === 'time' && 'granularities' in dim && !dim.granularitiesBlock) {
626-
// Keep the raw user value for the validator (it runs after this and would otherwise only
627-
// see the extracted customs, never the includes/excludes/custom dict).
628-
dim.rawGranularities = dim.granularities;
629-
const block: NormalizedGranularitiesBlock = normalizeGranularitiesBlock(dim.granularities);
630-
dim.granularitiesBlock = block;
631-
dim.granularities = block.custom;
657+
if (dim && dim.type === 'time') {
658+
// A view's included dimension already carries a propagated granularitiesBlock (with the
659+
// source dimension's includes/excludes) alongside the custom-only `granularities` map.
660+
// Re-normalizing the custom-only map here would reset includes to '*' and drop the source's
661+
// includes/excludes, so only normalize dimensions that haven't been normalized yet. A
662+
// dimension declaring no `granularities` needs no block — it takes the global config as-is,
663+
// and attaching one would leak an empty block into the compiled model.
664+
if (!dim.granularitiesBlock && 'granularities' in dim) {
665+
// Keep the raw user value for the validator (it runs after this and would otherwise only
666+
// see the extracted customs, never the includes/excludes/custom dict).
667+
dim.rawGranularities = dim.granularities;
668+
const block: NormalizedGranularitiesBlock = normalizeGranularitiesBlock(dim.granularities);
669+
dim.granularitiesBlock = block;
670+
dim.granularities = block.custom;
671+
}
672+
673+
this.mergeGlobalCustomsIntoDimension(dim);
674+
}
675+
}
676+
}
677+
678+
// Layer the dimension's effective global customs onto its `granularities` map (locals win on a
679+
// name collision). Reassigns rather than mutating, so a view dimension sharing its source's map
680+
// by reference isn't contaminated.
681+
private mergeGlobalCustomsIntoDimension(dim: any) {
682+
const globalCustom = this.globalGranularities.customGranularities;
683+
if (Object.keys(globalCustom).length === 0) {
684+
return;
685+
}
686+
687+
// `granularitiesBlock.custom` is the model's own customs, unaffected by the merge below.
688+
const locals: Record<string, GranularityDefinition> = dim.granularitiesBlock?.custom ?? {};
689+
const resolved = resolveDimensionGranularities(
690+
dim.granularitiesBlock ?? normalizeGranularitiesBlock(undefined),
691+
this.globalGranularities.enabledBuiltIns,
692+
globalCustom,
693+
buildBuiltInsCatalog(this.globalGranularities),
694+
);
695+
696+
// Only genuine global customs are baked in: built-ins resolve by name without a definition, and
697+
// a local of the same name already wins.
698+
const merged: Record<string, GranularityDefinition> = {};
699+
for (const [name, def] of Object.entries(resolved)) {
700+
if (def.type === 'custom' &&
701+
Object.prototype.hasOwnProperty.call(globalCustom, name) &&
702+
!Object.prototype.hasOwnProperty.call(locals, name)
703+
) {
704+
merged[name] = { ...def };
705+
delete (merged[name] as any).type;
632706
}
633707
}
708+
709+
if (Object.keys(merged).length > 0) {
710+
dim.granularities = { ...merged, ...locals };
711+
}
634712
}
635713

636714
private camelCaseTypes(obj: Object | Array<any> | undefined) {
@@ -1582,8 +1660,11 @@ export class CubeSymbols implements TranspilerSymbolResolver, CompilerInterface
15821660
return { interval: `1 ${granName}` };
15831661
}
15841662

1585-
// Custom granularities (local + baked-in globals) resolve from the compiled cube symbols.
1586-
return cube?.[dimName]?.[gr]?.[granName];
1663+
// A local custom wins; otherwise fall back to a global custom from the resolved config.
1664+
return cube?.[dimName]?.[gr]?.[granName] ??
1665+
(Object.prototype.hasOwnProperty.call(this.globalGranularities.customGranularities, granName)
1666+
? this.globalGranularities.customGranularities[granName]
1667+
: undefined);
15871668
}
15881669

15891670
protected cubeDependenciesProxy(parentIndex, cubeName) {

packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.ts

Lines changed: 18 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ import { resolveNamedNumericFormat, STANDARD_FORMAT_SPECIFIERS, DEFAULT_FORMAT_S
2323
import {
2424
EffectiveGranularity,
2525
NormalizedGranularitiesBlock,
26-
ResolvedGranularitySet,
27-
GRANULARITY_STRING_FIELDS,
2826
normalizeGranularitiesBlock,
2927
resolveDimensionGranularities,
3028
serializeEffectiveGranularities,
@@ -217,18 +215,12 @@ export class CubeToMetaTransformer implements CompilerInterface {
217215
*/
218216
public queries: TransformedCube[];
219217

220-
// Resolved-once global granularities config for this appId, baked into the compiled model.
221-
// CompilerApi resolves all config forms (env / static / function) before compile and passes the
222-
// result here — the transformer never sees a function.
223-
private readonly granularitiesConfig?: GlobalGranularitiesConfig;
224-
225218
// Precomputed once in compile() so plain dimensions need no per-dimension resolution: `defaultSet`
226-
// and `defaultGlobalCustoms` are shared by reference across every dimension without a local block.
219+
// is shared by reference across every dimension without a local block.
227220
private granularityState!: {
228221
config: GlobalGranularitiesConfig;
229222
catalog: Record<string, GranularityDefinition>;
230223
defaultSet: EffectiveGranularity[];
231-
defaultGlobalCustoms: Record<string, GranularityDefinition>;
232224
};
233225

234226
public constructor(
@@ -237,44 +229,33 @@ export class CubeToMetaTransformer implements CompilerInterface {
237229
contextEvaluator: ContextEvaluator,
238230
viewGroupEvaluator: ViewGroupEvaluator,
239231
joinGraph: JoinGraph,
240-
granularitiesConfig?: GlobalGranularitiesConfig
241232
) {
242233
this.cubeValidator = cubeValidator;
243234
this.cubeSymbols = cubeEvaluator;
244235
this.cubeEvaluator = cubeEvaluator;
245236
this.contextEvaluator = contextEvaluator;
246237
this.viewGroupEvaluator = viewGroupEvaluator;
247238
this.joinGraph = joinGraph;
248-
this.granularitiesConfig = granularitiesConfig;
249239
this.cubes = [];
250240
this.queries = [];
251241
}
252242

253-
// The resolved global config baked into this compiled model. Exposed for the /v1/granularities
254-
// endpoint, which serves the per-appId catalog from the compiled model rather than re-resolving.
255-
public get globalGranularitiesConfig(): GlobalGranularitiesConfig | undefined {
256-
return this.granularityState?.config;
257-
}
258-
259243
public get viewGroups(): CompiledViewGroup[] {
260244
return this.viewGroupEvaluator.compiledViewGroups;
261245
}
262246

263247
public compile(_cubes: any[], errorReporter: ErrorReporter): void {
264-
// The config is already resolved (env / static / function) by CompilerApi at compile time and
265-
// baked in here — a missing config means the default catalog.
266-
const config = this.granularitiesConfig ?? DEFAULT_GRANULARITIES_CONFIG;
248+
// CubeSymbols resolved the global config at the start of its own compile phase.
249+
const config = this.cubeEvaluator.globalGranularitiesConfig ?? DEFAULT_GRANULARITIES_CONFIG;
267250
const catalog = buildBuiltInsCatalog(config);
268-
// Resolve the no-local-block ("default") set once; every plain time dimension shares both its
269-
// serialized wire form and its global-custom map by reference (no per-dimension resolution).
270-
const defaultResolved = resolveDimensionGranularities(
271-
normalizeGranularitiesBlock(undefined), config.enabledBuiltIns, config.customGranularities, catalog,
272-
);
251+
// Resolve the no-local-block ("default") set once; every plain time dimension shares the
252+
// serialized wire form by reference (no per-dimension resolution).
273253
this.granularityState = {
274254
config,
275255
catalog,
276-
defaultSet: serializeEffectiveGranularities(defaultResolved),
277-
defaultGlobalCustoms: this.globalCustomsOf(defaultResolved, config, {}),
256+
defaultSet: serializeEffectiveGranularities(resolveDimensionGranularities(
257+
normalizeGranularitiesBlock(undefined), config.enabledBuiltIns, config.customGranularities, catalog,
258+
)),
278259
};
279260

280261
this.cubes = this.cubeSymbols.cubeList
@@ -359,11 +340,11 @@ export class CubeToMetaTransformer implements CompilerInterface {
359340
const dimensionVisibility = isCubeVisible
360341
? this.isVisible(extendedDimDef, !extendedDimDef.primaryKey)
361342
: false;
362-
// Snapshot the dimension's LOCAL customs before any merge below: the deprecated
363-
// `granularities` meta field must keep listing only the model's own custom granularities.
364-
const localCustoms = extendedDimDef.granularities;
365-
const localCustomEntries = localCustoms ? Object.entries(localCustoms) : [];
366343
const { granularitiesBlock } = extendedDimDef as any;
344+
// The deprecated `granularities` meta field lists only the model's own custom
345+
// granularities — `granularitiesBlock.custom`, which the global merge leaves untouched.
346+
const localCustoms = granularitiesBlock?.custom ?? extendedDimDef.granularities;
347+
const localCustomEntries = localCustoms ? Object.entries(localCustoms) : [];
367348
const dimType = this.dimensionDataType(extendedDimDef.type || 'string');
368349
const dimFormat = this.transformDimensionFormat(extendedDimDef);
369350
const dimCurrency = extendedDimDef.currency?.toUpperCase();
@@ -372,23 +353,13 @@ export class CubeToMetaTransformer implements CompilerInterface {
372353
if (dimType === 'time') {
373354
const s = this.granularityState;
374355
const inputs = this.granularityInputsForDimension(cubeTitle, localCustoms, granularitiesBlock);
375-
// Dimensions with a local block resolve individually; plain ones reuse the shared default
376-
// (both the serialized set and the global-custom map) computed once in compile().
377-
let globalCustoms: Record<string, GranularityDefinition>;
378-
if (inputs) {
379-
const resolved = resolveDimensionGranularities(
356+
// Dimensions with a local block resolve individually; plain ones reuse the shared
357+
// default set computed once in compile().
358+
effectiveGranularities = inputs
359+
? serializeEffectiveGranularities(resolveDimensionGranularities(
380360
inputs, s.config.enabledBuiltIns, s.config.customGranularities, s.catalog,
381-
);
382-
effectiveGranularities = serializeEffectiveGranularities(resolved);
383-
globalCustoms = this.globalCustomsOf(resolved, s.config, localCustoms ?? {});
384-
} else {
385-
effectiveGranularities = s.defaultSet;
386-
globalCustoms = s.defaultGlobalCustoms;
387-
}
388-
389-
// Bake the effective GLOBAL customs into the dimension's `granularities` map (SQL resolves
390-
// customs by name from this map, and pre-agg matching reads it). Locals win over globals.
391-
this.mergeGlobalCustomsIntoDimension(cubeName, dimensionName, extendedDimDef, localCustoms, globalCustoms);
361+
))
362+
: s.defaultSet;
392363
}
393364

394365
return {
@@ -486,64 +457,6 @@ export class CubeToMetaTransformer implements CompilerInterface {
486457
return { includes: block.includes, excludes: block.excludes, custom };
487458
}
488459

489-
// From an already-resolved set, extract the GLOBAL customs a dimension exposes: entries that are
490-
// custom, defined in the global config, and not shadowed by a local of the same name. Projected
491-
// through GRANULARITY_STRING_FIELDS (the shared field list, so it can't drift from serialize/hash).
492-
private globalCustomsOf(
493-
resolved: ResolvedGranularitySet,
494-
config: GlobalGranularitiesConfig,
495-
localCustoms: Record<string, GranularityDefinition>,
496-
): Record<string, GranularityDefinition> {
497-
const out: Record<string, GranularityDefinition> = {};
498-
for (const [name, def] of Object.entries(resolved)) {
499-
if (def.type === 'custom' &&
500-
Object.prototype.hasOwnProperty.call(config.customGranularities, name) &&
501-
!Object.prototype.hasOwnProperty.call(localCustoms, name)
502-
) {
503-
const projected: GranularityDefinition = {} as GranularityDefinition;
504-
for (const field of GRANULARITY_STRING_FIELDS) {
505-
if (def[field] !== undefined) {
506-
(projected as any)[field] = def[field];
507-
}
508-
}
509-
out[name] = projected;
510-
}
511-
}
512-
return out;
513-
}
514-
515-
// Bake global customs into a dimension's `granularities` map (locals win). Must write BOTH the
516-
// `dimDef` object (pre-agg matching) and the distinct `symbols[cube][dim]` object (SQL
517-
// resolveGranularity) — writing one leaves the other unable to resolve the custom. Reassign, never
518-
// mutate in place, so a view dim sharing its source's map by reference isn't contaminated.
519-
private mergeGlobalCustomsIntoDimension(
520-
cubeName: string,
521-
dimensionName: string,
522-
dimDef: ExtendedCubeSymbolDefinition,
523-
localCustoms: Record<string, GranularityDefinition> | undefined,
524-
globalCustoms: Record<string, GranularityDefinition>,
525-
): void {
526-
if (Object.keys(globalCustoms).length === 0) {
527-
return;
528-
}
529-
const hasLocals = !!localCustoms && Object.keys(localCustoms).length > 0;
530-
531-
// With no locals the baked map IS the shared globalCustoms — assign it by reference (every plain
532-
// dimension then shares one object). Copy-on-write only when locals must be layered on top.
533-
const write = (existing: Record<string, GranularityDefinition> | undefined) => (
534-
existing && Object.keys(existing).length > 0
535-
? { ...globalCustoms, ...existing } // globals first, locals last so locals win on collisions
536-
: globalCustoms
537-
);
538-
539-
dimDef.granularities = write(hasLocals ? localCustoms : undefined);
540-
541-
const symbolDim = (this.cubeEvaluator as any).symbols?.[cubeName]?.[dimensionName];
542-
if (symbolDim && symbolDim !== dimDef) {
543-
symbolDim.granularities = write(symbolDim.granularities as Record<string, GranularityDefinition> | undefined);
544-
}
545-
}
546-
547460
public queriesForContext(contextId: string | null | undefined): TransformedCube[] {
548461
// return All queries if no context pass
549462
if (contextId == null || contextId.length === 0) {

packages/cubejs-schema-compiler/src/compiler/DataSchemaCompiler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export class DataSchemaCompiler {
224224
try {
225225
return compileServices
226226
.map((compileService) => (() => compileService.compile(objects, errorsReport)))
227-
.reduce((p, fn) => p.then(fn), Promise.resolve())
227+
.reduce<Promise<void>>((p, fn) => p.then(() => fn()).then(() => undefined), Promise.resolve())
228228
.catch((error) => {
229229
errorsReport.error(error);
230230
});

0 commit comments

Comments
 (0)