-
Notifications
You must be signed in to change notification settings - Fork 2.1k
fix(schema-compiler,query-orchestrator): bound the rollupLambda source query #11708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -266,8 +266,17 @@ export class PreAggregationPartitionRangeLoader { | |||||
| if (this.preAggregation.rollupLambdaId) { | ||||||
| if (this.lambdaQuery && loadResults.length > 0) { | ||||||
| const { buildRangeEnd, targetTableName } = loadResults[loadResults.length - 1]; | ||||||
| const lambdaTypes = await this.loadCache.getTableColumnTypes(this.preAggregation, targetTableName); | ||||||
| lambdaTable = await this.downloadLambdaTable(buildRangeEnd, lambdaTypes); | ||||||
| if (this.lambdaSourceDataCovered(buildRangeEnd)) { | ||||||
| this.logger('Skipping lambda source query', { | ||||||
| preAggregationId: this.preAggregation.preAggregationId, | ||||||
| requestId: this.requestId, | ||||||
| buildRangeEnd, | ||||||
| matchedTimeDimensionDateRange: this.preAggregation.matchedTimeDimensionDateRange, | ||||||
| }); | ||||||
| } else { | ||||||
| const lambdaTypes = await this.loadCache.getTableColumnTypes(this.preAggregation, targetTableName); | ||||||
| lambdaTable = await this.downloadLambdaTable(buildRangeEnd, lambdaTypes); | ||||||
| } | ||||||
| } | ||||||
| const rollupLambdaResults = this.preAggregationsTablesToTempTables.filter(tempTableResult => tempTableResult[1].rollupLambdaId === this.preAggregation.rollupLambdaId); | ||||||
| const filteredResults = loadResults.filter( | ||||||
|
|
@@ -368,6 +377,20 @@ export class PreAggregationPartitionRangeLoader { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * The lambda query is lower bounded by `buildRangeEnd` (exclusive, via | ||||||
| * `afterDate FROM_PARTITION_RANGE`), so it can only contribute rows strictly after it. | ||||||
| * @see https://github.com/cube-js/cube/issues/11682 | ||||||
| */ | ||||||
| private lambdaSourceDataCovered(buildRangeEnd?: string): boolean { | ||||||
| const matchedRangeEnd = this.preAggregation.matchedTimeDimensionDateRange?.[1]; | ||||||
| if (!matchedRangeEnd || !buildRangeEnd) { | ||||||
| return false; | ||||||
| } | ||||||
| // buildRangeEnd comes back from the DB and may carry a `Z` suffix. | ||||||
| return reformatInIsoLocal(matchedRangeEnd) <= reformatInIsoLocal(buildRangeEnd); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Downloads the lambda table from the source DB. | ||||||
| */ | ||||||
|
|
@@ -399,8 +422,9 @@ export class PreAggregationPartitionRangeLoader { | |||||
| lambdaTypes, | ||||||
| } | ||||||
| ); | ||||||
| if (data.rowCount === this.options.maxSourceRowLimit) { | ||||||
| throw new Error(`The maximum number of source rows ${this.options.maxSourceRowLimit} was reached for ${this.preAggregation.preAggregationId}`); | ||||||
| const appliedRowLimit = this.lambdaQuery.maxSourceRowLimit ?? this.options.maxSourceRowLimit; | ||||||
| if (data.rowCount === appliedRowLimit) { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor, and the PR already flags it as out of scope: now that the limit is inlined into the SQL by the compiler rather than substituted at runtime, an
Suggested change
|
||||||
| throw new Error(`The maximum number of source rows ${appliedRowLimit} was reached for ${this.preAggregation.preAggregationId}`); | ||||||
| } | ||||||
| return { | ||||||
| name: `${LAMBDA_TABLE_PREFIX}_${this.preAggregation.tableName.replace('.', '_')}`, | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1093,26 +1093,41 @@ export class BaseQuery { | |
| const lambdaPreAgg = preAggForQuery.referencedPreAggregations[preAggForQuery.referencedPreAggregations.length - 1]; | ||
| // TODO(cristipp) Use source query instead of preaggregation references. | ||
| const references = this.cubeEvaluator.evaluatePreAggregationReferences(lambdaPreAgg.cube, lambdaPreAgg.preAggregation); | ||
| const [timeDimension] = references.timeDimensions; | ||
| // @see https://github.com/cube-js/cube/issues/11682 | ||
| const sourceDateRange = timeDimension && | ||
| this.preAggregations.lambdaSourceDateRange(lambdaPreAgg, preAggForQuery); | ||
| // Tesseract renders LIMIT as an inline number and can't carry the MAX_SOURCE_ROW_LIMIT | ||
| // placeholder (it parses to None, emitting no LIMIT at all), so resolve it here. | ||
| const resolvedRowLimit = this.options.maxSourceRowLimit ?? getEnv('maxSourceRowLimit'); | ||
| const maxSourceRowLimit = typeof resolvedRowLimit === 'number' && resolvedRowLimit > 0 | ||
| ? resolvedRowLimit | ||
|
Comment on lines
+1100
to
+1104
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Baking the limit here silently drops support for the Two things line up badly:
So for a user with Either plumb |
||
| : undefined; | ||
| const lambdaQuery = this.newSubQuery( | ||
| { | ||
| measures: references.measures, | ||
| dimensions: references.dimensions, | ||
| timeDimensions: references.timeDimensions, | ||
| filters: [ | ||
| ...this.options.filters ?? [], | ||
| references.timeDimensions.length > 0 | ||
| ? { | ||
| member: references.timeDimensions[0].dimension, | ||
| operator: 'afterDate', | ||
| values: [FROM_PARTITION_RANGE] | ||
| } | ||
| : [], | ||
| ...(timeDimension ? [{ | ||
| member: timeDimension.dimension, | ||
| operator: 'afterDate', | ||
| values: [FROM_PARTITION_RANGE] | ||
| }] : []), | ||
| // Kept separate from the afterDate filter on purpose: inDateRange's lower bound is | ||
| // inclusive and would double count rows sitting exactly at the partition end. | ||
| ...(sourceDateRange ? [{ | ||
| member: timeDimension.dimension, | ||
| operator: 'inDateRange', | ||
| values: sourceDateRange | ||
| }] : []), | ||
| ], | ||
| segments: this.options.segments, | ||
| order: [], | ||
| limit: undefined, | ||
| offset: undefined, | ||
| rowLimit: MAX_SOURCE_ROW_LIMIT, | ||
| rowLimit: maxSourceRowLimit ?? MAX_SOURCE_ROW_LIMIT, | ||
| preAggregationQuery: true, | ||
| } | ||
| ); | ||
|
|
@@ -1121,7 +1136,11 @@ export class BaseQuery { | |
| () => this.cacheKeyQueries(), | ||
| { preAggregationQuery: true } | ||
| ); | ||
| result[this.preAggregations.preAggregationId(lambdaPreAgg)] = { sqlAndParams, cacheKeyQueries }; | ||
| result[this.preAggregations.preAggregationId(lambdaPreAgg)] = { | ||
| sqlAndParams, | ||
| cacheKeyQueries, | ||
| maxSourceRowLimit, | ||
| }; | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -313,20 +313,24 @@ export class PreAggregations { | |
| return []; | ||
| } | ||
|
|
||
| private preAggregationDescriptionFor(cube: string, foundPreAggregation: PreAggregationForQuery): FullPreAggregationDescription { | ||
| const { preAggregationName, preAggregation, references } = foundPreAggregation; | ||
| /** | ||
| * Requested time dimension range this pre-aggregation was matched by, as local | ||
| * (timezone naked) ISO strings, e.g. ['2024-02-01T00:00:00.000', '2024-02-29T23:59:59.999']. | ||
| * Drives partition selection, and bounds the rollupLambda source query so both halves of | ||
| * the union agree. | ||
| */ | ||
| public matchedTimeDimensionDateRangeFor(foundPreAggregation: PreAggregationForQuery): [string, string] | undefined { | ||
| const { preAggregation } = foundPreAggregation; | ||
|
|
||
| const tableName = this.preAggregationTableName(cube, preAggregationName, preAggregation); | ||
| const invalidateKeyQueries = this.query.preAggregationInvalidateKeyQueries(cube, preAggregation, preAggregationName); | ||
| const queryForSqlEvaluation = this.query.preAggregationQueryForSqlEvaluation(cube, preAggregation); | ||
| // Atm this is only defined in KsqlQuery but without it partitions are recreated on every refresh | ||
| const partitionInvalidateKeyQueries = queryForSqlEvaluation.partitionInvalidateKeyQueries?.(cube, preAggregation); | ||
| if (!preAggregation.partitionGranularity) { | ||
| return undefined; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PR description says "so nothing is recomputed", but extracting this into a public method actually adds a computation: Worth memoizing either |
||
|
|
||
| const allBackAliasMembers = this.query.allBackAliasMembers(); | ||
|
|
||
| let matchedTimeDimension: BaseTimeDimension | undefined; | ||
|
|
||
| if (preAggregation.partitionGranularity && !this.hasCumulativeMeasures()) { | ||
| if (!this.hasCumulativeMeasures()) { | ||
| matchedTimeDimension = this.query.timeDimensions.find(td => { | ||
| if (!td.dateRange) { | ||
| return false; | ||
|
|
@@ -349,24 +353,77 @@ export class PreAggregations { | |
| }); | ||
| } | ||
|
|
||
| let filters: BaseFilter[] | undefined; | ||
| const filters = this.query.filters?.filter((td): td is BaseFilter => { | ||
| // TODO support all date operators | ||
| if (td.isDateOperator() && 'camelizeOperator' in td && td.camelizeOperator === 'inDateRange') { | ||
| if (td.dimension === foundPreAggregation.references.timeDimensions[0].dimension) { | ||
| return true; | ||
| } | ||
|
|
||
| if (preAggregation.partitionGranularity) { | ||
| filters = this.query.filters?.filter((td): td is BaseFilter => { | ||
| // TODO support all date operators | ||
| if (td.isDateOperator() && 'camelizeOperator' in td && td.camelizeOperator === 'inDateRange') { | ||
| if (td.dimension === foundPreAggregation.references.timeDimensions[0].dimension) { | ||
| return true; | ||
| } | ||
| // Handling for views | ||
| return td.dimension === allBackAliasMembers[foundPreAggregation.references.timeDimensions[0].dimension]; | ||
| } | ||
|
|
||
| // Handling for views | ||
| return td.dimension === allBackAliasMembers[foundPreAggregation.references.timeDimensions[0].dimension]; | ||
| } | ||
| return false; | ||
| }); | ||
|
|
||
| return false; | ||
| }); | ||
| return matchedTimeDimension?.boundaryDateRangeFormatted() || | ||
| filters?.[0]?.formattedDateRange() || // TODO intersect all date ranges | ||
| undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Mirrors the merge done in preAggregationDescriptionsForUsageInfos(), otherwise a forward | ||
| * shifted usage would be bounded tighter than the partitions it unions with and lose | ||
| * source rows. | ||
| */ | ||
| public lambdaSourceDateRange( | ||
| lambdaPreAggregation: PreAggregationForQuery, | ||
| rollupLambda: PreAggregationForQuery | ||
| ): [string, string] | undefined { | ||
| const matchedDateRange = this.matchedTimeDimensionDateRangeFor(lambdaPreAggregation); | ||
|
|
||
| if (!matchedDateRange) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const usageInfos = (this.preAggregationUsageInfos || []).filter( | ||
| usageInfo => usageInfo.cubeName === rollupLambda.cube && | ||
| usageInfo.preAggregationName === rollupLambda.preAggregationName | ||
| ); | ||
|
|
||
| if (usageInfos.length === 0) { | ||
| return matchedDateRange; | ||
| } | ||
|
|
||
| let merged: [string, string] | undefined; | ||
|
|
||
| for (const usageInfo of usageInfos) { | ||
| const usageDateRange = PreAggregations.mergeUsageDateRanges(usageInfo.usages); | ||
| if (!usageDateRange) { | ||
| // An unknown usage range may need anything, so bound nothing. | ||
| return undefined; | ||
| } | ||
| merged = merged | ||
| ? [ | ||
| usageDateRange[0] < merged[0] ? usageDateRange[0] : merged[0], | ||
| usageDateRange[1] > merged[1] ? usageDateRange[1] : merged[1], | ||
| ] | ||
| : usageDateRange; | ||
| } | ||
|
|
||
| return merged; | ||
| } | ||
|
|
||
| private preAggregationDescriptionFor(cube: string, foundPreAggregation: PreAggregationForQuery): FullPreAggregationDescription { | ||
| const { preAggregationName, preAggregation, references } = foundPreAggregation; | ||
|
|
||
| const tableName = this.preAggregationTableName(cube, preAggregationName, preAggregation); | ||
| const invalidateKeyQueries = this.query.preAggregationInvalidateKeyQueries(cube, preAggregation, preAggregationName); | ||
| const queryForSqlEvaluation = this.query.preAggregationQueryForSqlEvaluation(cube, preAggregation); | ||
| // Atm this is only defined in KsqlQuery but without it partitions are recreated on every refresh | ||
| const partitionInvalidateKeyQueries = queryForSqlEvaluation.partitionInvalidateKeyQueries?.(cube, preAggregation); | ||
|
|
||
| const uniqueKeyColumnsDefault = () => null; | ||
| const uniqueKeyColumns = ({ | ||
| rollup: () => queryForSqlEvaluation.preAggregationUniqueKeyColumns(cube, preAggregation), | ||
|
|
@@ -403,11 +460,7 @@ export class PreAggregations { | |
| preAggregationStartEndQueries: | ||
| (preAggregation.partitionGranularity || references.timeDimensions[0]?.granularity) && | ||
| this.refreshRangeQuery(cube).preAggregationStartEndQueries(cube, preAggregation), | ||
| matchedTimeDimensionDateRange: | ||
| preAggregation.partitionGranularity && ( | ||
| matchedTimeDimension?.boundaryDateRangeFormatted() || | ||
| filters?.[0]?.formattedDateRange() // TODO intersect all date ranges | ||
| ), | ||
| matchedTimeDimensionDateRange: this.matchedTimeDimensionDateRangeFor(foundPreAggregation), | ||
| indexesSql: Object.keys(preAggregation.indexes || {}) | ||
| .map( | ||
| index => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The skip decision and the SQL bound can disagree in the multi-usage case, and the disagreement is in the unsafe direction.
lambdaSourceDateRange()bounds the SQL by the union of every usage range for the rollupLambda, but this check readsthis.preAggregation.matchedTimeDimensionDateRange, which comes from one description. With several usage infos,preAggregationDescriptionsForUsageInfos()produces one description per usage (each with its own merged range) and thenpreAggregationsDescription()collapses them withR.uniqBy(desc => desc.tableName)(schema-compiler/src/adapter/PreAggregations.ts:145) — the first one wins.So if usage A ends inside the built range and usage B extends past it, the surviving description carries A's range,
lambdaSourceDataCovered()returnstrue, and the fresh tail that B's SQL bound was explicitly widened to fetch is never downloaded. Before this change the download always ran.Given the PR already notes that multi-usage descriptions don't add the lambda table to their per-usage unions, this may be masked today — but the two computations should be fed from the same value rather than relying on that. Consider having the schema compiler stamp the effective source range onto
LambdaQuery(next tomaxSourceRowLimit) and comparing against that here, so the skip can never be wider than the bound actually rendered into the SQL.