@oneuptime/common 11.3.9 → 11.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/Models/AnalyticsModels/MutableMetric.ts +333 -0
  2. package/Server/Services/AlertService.ts +167 -206
  3. package/Server/Services/AlertStateTimelineService.ts +121 -1
  4. package/Server/Services/AnalyticsDatabaseService.ts +19 -23
  5. package/Server/Services/IncidentService.ts +179 -252
  6. package/Server/Services/IncidentStateTimelineService.ts +120 -1
  7. package/Server/Services/Index.ts +2 -0
  8. package/Server/Services/MetricAggregationService.ts +73 -3
  9. package/Server/Services/MetricService.ts +228 -0
  10. package/Server/Services/MutableMetricService.ts +275 -0
  11. package/Server/Services/TelemetryAttributeService.ts +183 -1
  12. package/Server/Utils/AnalyticsDatabase/ClusterConfig.ts +2 -0
  13. package/Server/Utils/AnalyticsDatabase/Statement.ts +2 -0
  14. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +145 -0
  15. package/Tests/Types/Monitor/MonitorStepEntityScope.test.ts +0 -56
  16. package/Types/AnalyticsDatabase/AnalyticsTableEngine.ts +1 -0
  17. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +1 -0
  18. package/Types/Dashboard/DashboardTemplates.ts +267 -7
  19. package/Types/Monitor/MonitorStepMetricMonitor.ts +0 -27
  20. package/build/dist/Models/AnalyticsModels/MutableMetric.js +275 -0
  21. package/build/dist/Models/AnalyticsModels/MutableMetric.js.map +1 -0
  22. package/build/dist/Server/Services/AlertService.js +133 -170
  23. package/build/dist/Server/Services/AlertService.js.map +1 -1
  24. package/build/dist/Server/Services/AlertStateTimelineService.js +97 -0
  25. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  26. package/build/dist/Server/Services/AnalyticsDatabaseService.js +16 -15
  27. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  28. package/build/dist/Server/Services/IncidentService.js +142 -207
  29. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  30. package/build/dist/Server/Services/IncidentStateTimelineService.js +97 -0
  31. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  32. package/build/dist/Server/Services/Index.js +2 -0
  33. package/build/dist/Server/Services/Index.js.map +1 -1
  34. package/build/dist/Server/Services/MetricAggregationService.js +49 -3
  35. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  36. package/build/dist/Server/Services/MetricService.js +155 -0
  37. package/build/dist/Server/Services/MetricService.js.map +1 -1
  38. package/build/dist/Server/Services/MutableMetricService.js +182 -0
  39. package/build/dist/Server/Services/MutableMetricService.js.map +1 -0
  40. package/build/dist/Server/Services/TelemetryAttributeService.js +131 -6
  41. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  42. package/build/dist/Server/Utils/AnalyticsDatabase/ClusterConfig.js +2 -0
  43. package/build/dist/Server/Utils/AnalyticsDatabase/ClusterConfig.js.map +1 -1
  44. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +2 -0
  45. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  46. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableEngine.js +1 -0
  47. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableEngine.js.map +1 -1
  48. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +1 -0
  49. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
  50. package/build/dist/Types/Dashboard/DashboardTemplates.js +252 -6
  51. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  52. package/build/dist/Types/Monitor/MonitorStepMetricMonitor.js +0 -13
  53. package/build/dist/Types/Monitor/MonitorStepMetricMonitor.js.map +1 -1
  54. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  import CreateBy from "../Types/Database/CreateBy";
2
2
  import DeleteBy from "../Types/Database/DeleteBy";
3
- import { OnCreate, OnDelete } from "../Types/Database/Hooks";
3
+ import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
4
4
  import QueryHelper from "../Types/Database/QueryHelper";
5
+ import UpdateBy from "../Types/Database/UpdateBy";
5
6
  import DatabaseService from "./DatabaseService";
6
7
  import IncidentPublicNoteService from "./IncidentPublicNoteService";
7
8
  import IncidentService from "./IncidentService";
@@ -643,6 +644,113 @@ ${createdItem.rootCause}`,
643
644
  return createdItem;
644
645
  }
645
646
 
647
+ @CaptureSpan()
648
+ protected override async onBeforeUpdate(
649
+ updateBy: UpdateBy<IncidentStateTimeline>,
650
+ ): Promise<OnUpdate<IncidentStateTimeline>> {
651
+ const incidentIds: Array<ObjectID> =
652
+ await this.getIncidentIdsForTimelineQuery(updateBy);
653
+
654
+ return {
655
+ updateBy,
656
+ carryForward: incidentIds,
657
+ };
658
+ }
659
+
660
+ @CaptureSpan()
661
+ protected override async onUpdateSuccess(
662
+ onUpdate: OnUpdate<IncidentStateTimeline>,
663
+ updatedItemIds: ObjectID[],
664
+ ): Promise<OnUpdate<IncidentStateTimeline>> {
665
+ const incidentIds: Array<ObjectID> = [
666
+ ...(Array.isArray(onUpdate.carryForward)
667
+ ? (onUpdate.carryForward as Array<ObjectID>)
668
+ : []),
669
+ ];
670
+
671
+ if (updatedItemIds.length > 0) {
672
+ const updatedTimelines: Array<IncidentStateTimeline> = await this.findBy({
673
+ query: {
674
+ _id: QueryHelper.any(updatedItemIds),
675
+ },
676
+ select: {
677
+ incidentId: true,
678
+ },
679
+ skip: 0,
680
+ limit: LIMIT_PER_PROJECT,
681
+ props: {
682
+ isRoot: true,
683
+ ignoreHooks: true,
684
+ },
685
+ });
686
+
687
+ incidentIds.push(...this.getIncidentIdsFromTimelines(updatedTimelines));
688
+ }
689
+
690
+ this.refreshIncidentMetricsForIncidentIds(incidentIds);
691
+
692
+ return onUpdate;
693
+ }
694
+
695
+ private async getIncidentIdsForTimelineQuery(
696
+ updateBy: UpdateBy<IncidentStateTimeline>,
697
+ ): Promise<Array<ObjectID>> {
698
+ const timelines: Array<IncidentStateTimeline> = await this.findBy({
699
+ query: updateBy.query,
700
+ select: {
701
+ incidentId: true,
702
+ },
703
+ skip: updateBy.skip,
704
+ limit: updateBy.limit,
705
+ props: {
706
+ isRoot: true,
707
+ ignoreHooks: true,
708
+ },
709
+ });
710
+
711
+ return this.getIncidentIdsFromTimelines(timelines);
712
+ }
713
+
714
+ private getIncidentIdsFromTimelines(
715
+ timelines: Array<IncidentStateTimeline>,
716
+ ): Array<ObjectID> {
717
+ const incidentIds: Map<string, ObjectID> = new Map<string, ObjectID>();
718
+
719
+ for (const timeline of timelines) {
720
+ if (timeline.incidentId) {
721
+ incidentIds.set(timeline.incidentId.toString(), timeline.incidentId);
722
+ }
723
+ }
724
+
725
+ return Array.from(incidentIds.values());
726
+ }
727
+
728
+ private refreshIncidentMetricsForIncidentIds(
729
+ incidentIds: Array<ObjectID>,
730
+ ): void {
731
+ const uniqueIncidentIds: Map<string, ObjectID> = new Map<
732
+ string,
733
+ ObjectID
734
+ >();
735
+
736
+ for (const incidentId of incidentIds) {
737
+ uniqueIncidentIds.set(incidentId.toString(), incidentId);
738
+ }
739
+
740
+ for (const incidentId of uniqueIncidentIds.values()) {
741
+ IncidentService.refreshIncidentMetrics({
742
+ incidentId: incidentId,
743
+ }).catch((error: Error) => {
744
+ logger.error(`Error while refreshing incident metrics:`, {
745
+ incidentId: incidentId.toString(),
746
+ } as LogAttributes);
747
+ logger.error(error, {
748
+ incidentId: incidentId.toString(),
749
+ } as LogAttributes);
750
+ });
751
+ }
752
+ }
753
+
646
754
  private async isLastIncidentState(data: {
647
755
  projectId: ObjectID;
648
756
  incidentStateId: ObjectID;
@@ -1027,6 +1135,17 @@ ${createdItem.rootCause}`,
1027
1135
  },
1028
1136
  });
1029
1137
  }
1138
+
1139
+ IncidentService.refreshIncidentMetrics({
1140
+ incidentId: incidentId,
1141
+ }).catch((error: Error) => {
1142
+ logger.error(`Error while refreshing incident metrics:`, {
1143
+ incidentId: incidentId?.toString(),
1144
+ } as LogAttributes);
1145
+ logger.error(error, {
1146
+ incidentId: incidentId?.toString(),
1147
+ } as LogAttributes);
1148
+ });
1030
1149
  }
1031
1150
 
1032
1151
  return onDelete;
@@ -61,6 +61,7 @@ import MailService from "./MailService";
61
61
  import MetricService from "./MetricService";
62
62
  import MetricItemAggMV1mService from "./MetricItemAggMV1mService";
63
63
  import MetricItemAggMV1mByHostV2Service from "./MetricItemAggMV1mByHostV2Service";
64
+ import MutableMetricService from "./MutableMetricService";
64
65
  import MetricBaselineService from "./MetricBaselineService";
65
66
  import MonitorCustomFieldService from "./MonitorCustomFieldService";
66
67
  import MonitorGroupOwnerTeamService from "./MonitorGroupOwnerTeamService";
@@ -499,6 +500,7 @@ export const AnalyticsServices: Array<
499
500
  LogService,
500
501
  SpanService,
501
502
  MetricService,
503
+ MutableMetricService,
502
504
  /*
503
505
  * Materialized-view target tables. The auto-create flow runs
504
506
  * `CREATE TABLE IF NOT EXISTS` for these, idempotent with the legacy
@@ -1,6 +1,7 @@
1
1
  import { SQL, Statement } from "../Utils/AnalyticsDatabase/Statement";
2
2
  import { getQuerySettings } from "../Utils/AnalyticsDatabase/QuerySettingsHelper";
3
3
  import MetricService from "./MetricService";
4
+ import { MutableMetricService as MutableMetricServiceClass } from "./MutableMetricService";
4
5
  import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
5
6
  import { JSONObject } from "../../Types/JSON";
6
7
  import ObjectID from "../../Types/ObjectID";
@@ -40,6 +41,8 @@ export interface FacetRequest extends MetricFilters {
40
41
  export class MetricAggregationService {
41
42
  private static readonly DEFAULT_FACET_LIMIT: number = 500;
42
43
  private static readonly TABLE_NAME: string = AnalyticsTableName.Metric;
44
+ private static readonly MUTABLE_TABLE_NAME: string =
45
+ AnalyticsTableName.MutableMetric;
43
46
  private static readonly TOP_LEVEL_COLUMNS: Set<string> = new Set([
44
47
  "primaryEntityId",
45
48
  "name",
@@ -102,26 +105,34 @@ export class MetricAggregationService {
102
105
  const isTopLevelColumn: boolean =
103
106
  isResourceFacet ||
104
107
  MetricAggregationService.isTopLevelColumn(request.facetKey);
108
+ const useMutableMetricTable: boolean =
109
+ MetricAggregationService.shouldUseMutableMetricTable(request);
105
110
 
106
111
  const statement: Statement = new Statement();
107
112
 
108
113
  if (isResourceFacet) {
109
114
  statement.append(
110
- SQL`SELECT toString(primaryEntityId) AS val, count() AS cnt FROM ${MetricAggregationService.TABLE_NAME}`,
115
+ SQL`SELECT toString(primaryEntityId) AS val, count() AS cnt`,
111
116
  );
112
117
  } else if (isTopLevelColumn) {
113
118
  statement.append(
114
- SQL`SELECT toString(${request.facetKey}) AS val, count() AS cnt FROM ${MetricAggregationService.TABLE_NAME}`,
119
+ SQL`SELECT toString(${request.facetKey}) AS val, count() AS cnt`,
115
120
  );
116
121
  } else {
117
122
  statement.append(
118
123
  SQL`SELECT attributes[${{
119
124
  type: TableColumnType.Text,
120
125
  value: request.facetKey,
121
- }}] AS val, count() AS cnt FROM ${MetricAggregationService.TABLE_NAME}`,
126
+ }}] AS val, count() AS cnt`,
122
127
  );
123
128
  }
124
129
 
130
+ MetricAggregationService.appendFacetSourceTable(
131
+ statement,
132
+ request,
133
+ useMutableMetricTable,
134
+ );
135
+
125
136
  statement.append(
126
137
  SQL` WHERE projectId = ${{
127
138
  type: TableColumnType.ObjectID,
@@ -135,6 +146,10 @@ export class MetricAggregationService {
135
146
  }}`,
136
147
  );
137
148
 
149
+ if (useMutableMetricTable) {
150
+ statement.append(SQL` AND isDeleted = false`);
151
+ }
152
+
138
153
  if (isResourceFacet) {
139
154
  statement.append(
140
155
  SQL` AND primaryEntityType = ${{
@@ -187,6 +202,61 @@ export class MetricAggregationService {
187
202
  return statement;
188
203
  }
189
204
 
205
+ private static shouldUseMutableMetricTable(request: FacetRequest): boolean {
206
+ if (!request.metricNames || request.metricNames.length === 0) {
207
+ return false;
208
+ }
209
+
210
+ return request.metricNames.every((metricName: string): boolean => {
211
+ return MutableMetricServiceClass.isMutableMetricName(metricName);
212
+ });
213
+ }
214
+
215
+ private static appendFacetSourceTable(
216
+ statement: Statement,
217
+ request: FacetRequest,
218
+ useMutableMetricTable: boolean,
219
+ ): void {
220
+ if (!useMutableMetricTable) {
221
+ statement.append(SQL` FROM ${MetricAggregationService.TABLE_NAME}`);
222
+ return;
223
+ }
224
+
225
+ statement.append(
226
+ SQL` FROM (
227
+ SELECT
228
+ projectId,
229
+ name,
230
+ primaryEntityId,
231
+ primaryEntityType,
232
+ metricPointId,
233
+ argMax(time, version) AS time,
234
+ argMax(attributes, version) AS attributes,
235
+ argMax(retentionDate, version) AS retentionDate,
236
+ argMax(isDeleted, version) AS isDeleted
237
+ FROM ${MetricAggregationService.MUTABLE_TABLE_NAME}
238
+ WHERE projectId = ${{
239
+ type: TableColumnType.ObjectID,
240
+ value: request.projectId,
241
+ }}`,
242
+ );
243
+
244
+ if (request.metricNames && request.metricNames.length > 0) {
245
+ statement.append(
246
+ SQL` AND name IN (${{
247
+ type: TableColumnType.Text,
248
+ value: new Includes(request.metricNames),
249
+ }})`,
250
+ );
251
+ }
252
+
253
+ statement.append(
254
+ SQL`
255
+ GROUP BY projectId, name, primaryEntityId, primaryEntityType, metricPointId
256
+ )`,
257
+ );
258
+ }
259
+
190
260
  private static appendCommonFilters(
191
261
  statement: Statement,
192
262
  request: MetricFilters,
@@ -1,5 +1,6 @@
1
1
  import ClickhouseDatabase from "../Infrastructure/ClickhouseDatabase";
2
2
  import AnalyticsDatabaseService from "./AnalyticsDatabaseService";
3
+ import { MutableMetricService as MutableMetricServiceClass } from "./MutableMetricService";
3
4
  import Metric from "../../Models/AnalyticsModels/Metric";
4
5
  import AggregateBy, {
5
6
  AggregateUtil,
@@ -172,6 +173,15 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
172
173
  statement: Statement;
173
174
  columns: Array<string>;
174
175
  } {
176
+ const mutableMetricStatement: {
177
+ statement: Statement;
178
+ columns: Array<string>;
179
+ } | null = this.tryBuildMutableMetricAggregateStatement(aggregateBy);
180
+
181
+ if (mutableMetricStatement) {
182
+ return mutableMetricStatement;
183
+ }
184
+
175
185
  if (!isPercentileAggregation(aggregateBy.aggregationType)) {
176
186
  /*
177
187
  * Try the per-host MV first — host detail pages are the
@@ -387,6 +397,224 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
387
397
  return { statement, columns };
388
398
  }
389
399
 
400
+ private tryBuildMutableMetricAggregateStatement(
401
+ aggregateBy: AggregateBy<Metric>,
402
+ ): { statement: Statement; columns: Array<string> } | null {
403
+ const metricName: string | null = this.getExactMetricNameFromQuery(
404
+ aggregateBy.query,
405
+ );
406
+
407
+ if (
408
+ !MutableMetricServiceClass.isMutableMetricName(metricName || undefined)
409
+ ) {
410
+ return null;
411
+ }
412
+
413
+ if (!this.canRouteAggregateToMutableMetricTable(aggregateBy)) {
414
+ return null;
415
+ }
416
+
417
+ if (!this.database) {
418
+ this.useDefaultDatabase();
419
+ }
420
+
421
+ const databaseName: string = this.database.getDatasourceOptions().database!;
422
+
423
+ const select: { statement: Statement; columns: Array<string> } =
424
+ this.statementGenerator.toAggregateSelectStatement(aggregateBy);
425
+
426
+ const innerWhereStatement: Statement =
427
+ this.statementGenerator.toWhereStatement(
428
+ this.getMutableMetricInnerQuery(aggregateBy.query),
429
+ );
430
+ const outerWhereStatement: Statement =
431
+ this.statementGenerator.toWhereStatement(aggregateBy.query);
432
+ const sortStatement: Statement = this.statementGenerator.toSortStatement(
433
+ aggregateBy.sort!,
434
+ );
435
+
436
+ const statement: Statement = SQL``;
437
+
438
+ statement
439
+ .append(SQL`SELECT `)
440
+ .append(select.statement)
441
+ .append(
442
+ SQL`
443
+ FROM (
444
+ SELECT
445
+ projectId,
446
+ name,
447
+ primaryEntityId,
448
+ primaryEntityType,
449
+ metricPointId,
450
+ argMax(metricPointType, version) AS metricPointType,
451
+ argMax(time, version) AS time,
452
+ argMax(timeUnixNano, version) AS timeUnixNano,
453
+ argMax(attributes, version) AS attributes,
454
+ argMax(attributeKeys, version) AS attributeKeys,
455
+ argMax(value, version) AS value,
456
+ argMax(retentionDate, version) AS retentionDate,
457
+ argMax(isDeleted, version) AS isDeleted
458
+ FROM ${databaseName}.${AnalyticsTableName.MutableMetric}
459
+ WHERE TRUE
460
+ `,
461
+ )
462
+ .append(innerWhereStatement)
463
+ .append(
464
+ SQL`
465
+ GROUP BY
466
+ projectId,
467
+ name,
468
+ primaryEntityId,
469
+ primaryEntityType,
470
+ metricPointId
471
+ )
472
+ WHERE isDeleted = false
473
+ `,
474
+ )
475
+ .append(outerWhereStatement)
476
+ .append(SQL` AND retentionDate >= now()`);
477
+
478
+ statement
479
+ .append(SQL` GROUP BY `)
480
+ .append(`${aggregateBy.aggregationTimestampColumnName.toString()}`);
481
+
482
+ if (aggregateBy.groupBy && Object.keys(aggregateBy.groupBy).length > 0) {
483
+ statement
484
+ .append(SQL` , `)
485
+ .append(
486
+ this.statementGenerator.toGroupByStatement(aggregateBy.groupBy),
487
+ );
488
+ }
489
+
490
+ statement.append(SQL` ORDER BY `).append(sortStatement);
491
+
492
+ statement.append(
493
+ SQL` LIMIT ${{
494
+ value: Number(aggregateBy.limit),
495
+ type: TableColumnType.Number,
496
+ }}`,
497
+ );
498
+
499
+ statement.append(
500
+ SQL` OFFSET ${{
501
+ value: Number(aggregateBy.skip),
502
+ type: TableColumnType.Number,
503
+ }}`,
504
+ );
505
+
506
+ statement.append(
507
+ getQuerySettings({
508
+ additionalSettings: {
509
+ optimize_move_to_prewhere: 1,
510
+ max_threads: 4,
511
+ },
512
+ }),
513
+ );
514
+
515
+ logger.debug("Mutable metric aggregate statement", {
516
+ metricName: metricName || "",
517
+ } as LogAttributes);
518
+ logger.debug(statement, {
519
+ metricName: metricName || "",
520
+ } as LogAttributes);
521
+
522
+ return {
523
+ statement,
524
+ columns: select.columns,
525
+ };
526
+ }
527
+
528
+ private getMutableMetricInnerQuery(query: Query<Metric>): Query<Metric> {
529
+ const queryRecord: Record<string, unknown> = query as unknown as Record<
530
+ string,
531
+ unknown
532
+ >;
533
+ const allowedInnerKeys: Array<string> = [
534
+ "projectId",
535
+ "name",
536
+ "primaryEntityId",
537
+ "primaryEntityType",
538
+ ];
539
+ const innerQuery: Record<string, unknown> = {};
540
+
541
+ for (const key of allowedInnerKeys) {
542
+ if (queryRecord[key] !== undefined) {
543
+ innerQuery[key] = queryRecord[key];
544
+ }
545
+ }
546
+
547
+ return innerQuery as unknown as Query<Metric>;
548
+ }
549
+
550
+ private getExactMetricNameFromQuery(query: Query<Metric>): string | null {
551
+ const queryRecord: Record<string, unknown> = query as unknown as Record<
552
+ string,
553
+ unknown
554
+ >;
555
+ const metricName: unknown = queryRecord["name"];
556
+
557
+ if (typeof metricName !== "string") {
558
+ return null;
559
+ }
560
+
561
+ return metricName;
562
+ }
563
+
564
+ private canRouteAggregateToMutableMetricTable(
565
+ aggregateBy: AggregateBy<Metric>,
566
+ ): boolean {
567
+ const supportedColumns: Set<string> = new Set<string>([
568
+ "projectId",
569
+ "name",
570
+ "primaryEntityId",
571
+ "primaryEntityType",
572
+ "metricPointType",
573
+ "time",
574
+ "timeUnixNano",
575
+ "attributes",
576
+ "attributeKeys",
577
+ "value",
578
+ "retentionDate",
579
+ ]);
580
+
581
+ const queryRecord: Record<string, unknown> =
582
+ aggregateBy.query as unknown as Record<string, unknown>;
583
+
584
+ for (const key of Object.keys(queryRecord)) {
585
+ if (!supportedColumns.has(key)) {
586
+ return false;
587
+ }
588
+ }
589
+
590
+ if (
591
+ !supportedColumns.has(aggregateBy.aggregateColumnName.toString()) ||
592
+ !supportedColumns.has(
593
+ aggregateBy.aggregationTimestampColumnName.toString(),
594
+ )
595
+ ) {
596
+ return false;
597
+ }
598
+
599
+ if (aggregateBy.groupBy) {
600
+ for (const key of Object.keys(aggregateBy.groupBy)) {
601
+ if (!supportedColumns.has(key)) {
602
+ return false;
603
+ }
604
+ }
605
+ }
606
+
607
+ if (aggregateBy.sort) {
608
+ for (const key of Object.keys(aggregateBy.sort)) {
609
+ if (!supportedColumns.has(key)) {
610
+ return false;
611
+ }
612
+ }
613
+ }
614
+
615
+ return true;
616
+ }
617
+
390
618
  /*
391
619
  * Materialized-view fast path for scalar aggregations.
392
620
  *