From 9b0dc68d0d09874e7ccab06ddb1f9dcbf3de170e Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Mon, 16 Sep 2024 18:00:58 +0200 Subject: [PATCH] PromQL explain view: Support set operators Signed-off-by: Julius Volz --- .../ExplainViews/BinaryExpr/VectorVector.tsx | 589 +++++++------- web/ui/mantine-ui/src/promql/binOp.test.ts | 742 ++++++++++++++++++ web/ui/mantine-ui/src/promql/binOp.ts | 143 ++-- 3 files changed, 1125 insertions(+), 349 deletions(-) diff --git a/web/ui/mantine-ui/src/pages/query/ExplainViews/BinaryExpr/VectorVector.tsx b/web/ui/mantine-ui/src/pages/query/ExplainViews/BinaryExpr/VectorVector.tsx index 24f754393..e70b7a3f3 100644 --- a/web/ui/mantine-ui/src/pages/query/ExplainViews/BinaryExpr/VectorVector.tsx +++ b/web/ui/mantine-ui/src/pages/query/ExplainViews/BinaryExpr/VectorVector.tsx @@ -361,309 +361,296 @@ const VectorVectorBinaryExprExplainView: FC< <> {explanationText(node)} - {!isSetOperator(node.op) && ( - <> - - {/* - setAllowLineBreaks(event.currentTarget.checked) - } - /> */} - - setShowSampleValues(event.currentTarget.checked) - } - /> - + + setShowSampleValues(event.currentTarget.checked)} + /> + - {numGroups > Object.keys(matchGroups).length && ( - }> - Too many match groups to display, only showing{" "} - {Object.keys(matchGroups).length} out of {numGroups} groups. -
-
- setMaxGroups(undefined)}> - Show all groups - -
- )} - - {errCount > 0 && ( - }> - Found matching issues in {errCount} match group - {errCount > 1 ? "s" : ""}. See below for per-group error details. - - )} - - - - {Object.values(matchGroups).map((mg, mgIdx) => { - const { - groupLabels, - lhs, - lhsCount, - rhs, - rhsCount, - result, - error, - } = mg; - - const matchGroupTitleRow = (color: string) => ( - - - - - - ); - - const matchGroupTable = ( - series: InstantSample[], - seriesCount: number, - color: string, - colorOffset?: number - ) => ( - -
- - {series.length === 0 ? ( - - - no matching series - - - ) : ( - <> - {matchGroupTitleRow(color)} - {series.map((s, sIdx) => { - if (s.value === undefined) { - // TODO: Figure out how to handle native histograms. - throw new Error( - "Native histograms are not supported yet" - ); - } - - return ( - - - - {seriesSwatch( - colorForIndex(sIdx, colorOffset) - )} - - - - - {showSampleValues && ( - {s.value[1]} - )} - - ); - })} - - )} - {seriesCount > series.length && ( - - - {seriesCount - series.length} more series omitted -   –   - setMaxSeriesPerGroup(undefined)} - > - Show all series - - - - )} - -
- - ); - - const noLHSMatches = lhs.length === 0; - const noRHSMatches = rhs.length === 0; - - const groupColor = colorPool[mgIdx % colorPool.length]; - - const lhsTable = matchGroupTable(lhs, lhsCount, groupColor); - const rhsTable = matchGroupTable( - rhs, - rhsCount, - groupColor, - rhsColorOffset - ); - - const resultTable = ( - - - - {noLHSMatches || noRHSMatches ? ( - - - dropped - - - ) : error !== null ? ( - - - error, result omitted - - - ) : ( - <> - {result.map(({ sample, manySideIdx }, resIdx) => { - if (sample.value === undefined) { - // TODO: Figure out how to handle native histograms. - throw new Error( - "Native histograms are not supported yet" - ); - } - - const filtered = - sample.value[1] === filteredSampleValue; - const [lIdx, rIdx] = - matching.card === - vectorMatchCardinality.oneToMany - ? [0, manySideIdx] - : [manySideIdx, 0]; - - return ( - - - - - {seriesSwatch(colorForIndex(lIdx))} - - {seriesSwatch( - colorForIndex(rIdx, rhsColorOffset) - )} - - - - - - {showSampleValues && ( - - {filtered ? ( - - filtered - - ) : ( - {sample.value[1]} - )} - - )} - - ); - })} - - )} - -
-
- ); - - return ( - - {mgIdx !== 0 && } - - - {error && ( - } - > - {explainError(node, mg, error)} - - )} - - - - - {lhsTable} - - - {node.op} - {node.bool && " bool"} - - - {rhsTable} - - = - - {resultTable} - - - - ); - })} - - - + {numGroups > Object.keys(matchGroups).length && ( + }> + Too many match groups to display, only showing{" "} + {Object.keys(matchGroups).length} out of {numGroups} groups. +
+
+ setMaxGroups(undefined)}> + Show all groups + +
)} + + {errCount > 0 && ( + }> + Found matching issues in {errCount} match group + {errCount > 1 ? "s" : ""}. See below for per-group error details. + + )} + + + + {Object.values(matchGroups).map((mg, mgIdx) => { + const { groupLabels, lhs, lhsCount, rhs, rhsCount, result, error } = + mg; + + const matchGroupTitleRow = (color: string) => ( + + + + + + ); + + const matchGroupTable = ( + series: InstantSample[], + seriesCount: number, + color: string, + colorOffset?: number + ) => ( + +
+ + {seriesCount === 0 ? ( + + + no matching series + + + ) : ( + <> + {matchGroupTitleRow(color)} + {series.map((s, sIdx) => { + if (s.value === undefined) { + // TODO: Figure out how to handle native histograms. + throw new Error( + "Native histograms are not supported yet" + ); + } + + return ( + + + + {seriesSwatch( + colorForIndex(sIdx, colorOffset) + )} + + + + + {showSampleValues && ( + {s.value[1]} + )} + + ); + })} + + )} + {seriesCount > series.length && ( + + + {seriesCount - series.length} more series omitted +   –   + setMaxSeriesPerGroup(undefined)} + > + Show all series + + + + )} + +
+ + ); + + const groupColor = colorPool[mgIdx % colorPool.length]; + + const lhsTable = matchGroupTable(lhs, lhsCount, groupColor); + const rhsTable = matchGroupTable( + rhs, + rhsCount, + groupColor, + rhsColorOffset + ); + + const resultTable = ( + + + + {error !== null ? ( + + + error, result omitted + + + ) : result.length === 0 ? ( + + + dropped + + + ) : error !== null ? ( + + + error, result omitted + + + ) : ( + <> + {result.map(({ sample, manySideIdx }, resIdx) => { + if (sample.value === undefined) { + // TODO: Figure out how to handle native histograms. + throw new Error( + "Native histograms are not supported yet" + ); + } + + const filtered = + sample.value[1] === filteredSampleValue; + const [lIdx, rIdx] = + matching.card === vectorMatchCardinality.oneToMany + ? [0, manySideIdx] + : [manySideIdx, 0]; + + return ( + + + + + {seriesSwatch(colorForIndex(lIdx))} + + {seriesSwatch( + colorForIndex(rIdx, rhsColorOffset) + )} + + + + + + {showSampleValues && ( + + {filtered ? ( + + filtered + + ) : ( + {sample.value[1]} + )} + + )} + + ); + })} + + )} + +
+
+ ); + + return ( + + {mgIdx !== 0 && } + + + {error && ( + } + > + {explainError(node, mg, error)} + + )} + + + + + {lhsTable} + + + {node.op} + {node.bool && " bool"} + + + {rhsTable} + + = + + {resultTable} + + + + ); + })} + + ); }; diff --git a/web/ui/mantine-ui/src/promql/binOp.test.ts b/web/ui/mantine-ui/src/promql/binOp.test.ts index ca34cfa2b..72ef16947 100644 --- a/web/ui/mantine-ui/src/promql/binOp.test.ts +++ b/web/ui/mantine-ui/src/promql/binOp.test.ts @@ -73,6 +73,7 @@ const testMetricC: InstantSample[] = [ const testCases: TestCase[] = [ { + // metric_a - metric_b desc: "one-to-one matching on all labels", op: binaryOperatorType.sub, matching: { @@ -238,6 +239,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_a - on(label1, label2) metric_b desc: "one-to-one matching on explicit labels", op: binaryOperatorType.sub, matching: { @@ -403,6 +405,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_a - ignoring(same) metric_b desc: "one-to-one matching ignoring explicit labels", op: binaryOperatorType.sub, matching: { @@ -568,6 +571,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_b - metric_c desc: "many-to-one matching with no matching labels specified (empty output)", op: binaryOperatorType.sub, matching: { @@ -689,6 +693,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_b - on(label1) metric_c desc: "many-to-one matching with matching labels specified, but no group_left (error)", op: binaryOperatorType.sub, matching: { @@ -778,6 +783,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_b - on(label1) group_left metric_c desc: "many-to-one matching with matching labels specified and group_left", op: binaryOperatorType.sub, matching: { @@ -891,6 +897,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_c - on(label1) group_right metric_b desc: "one-to-many matching with matching labels specified and group_right", op: binaryOperatorType.sub, matching: { @@ -1004,6 +1011,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_c - on(label1) group_left metric_b desc: "one-to-many matching with matching labels specified but incorrect group_left (error)", op: binaryOperatorType.sub, matching: { @@ -1091,6 +1099,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_a - on(label1) metric_b desc: "insufficient matching labels leading to many-to-many matching for intended one-to-one match (error)", op: binaryOperatorType.sub, matching: { @@ -1206,6 +1215,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_a < metric_b desc: "filter op keeping all series", op: binaryOperatorType.lss, matching: { @@ -1391,6 +1401,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_a >= metric_b desc: "filter op dropping all series", op: binaryOperatorType.gte, matching: { @@ -1576,6 +1587,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_a >= bool metric_b desc: "filter op dropping all series, but with bool", op: binaryOperatorType.gte, bool: true, @@ -1742,6 +1754,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_a < bool metric_b desc: "filter op keeping all series, but with bool", op: binaryOperatorType.lss, bool: true, @@ -1908,6 +1921,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_a - metric_b desc: "exceeding the match group limit", op: binaryOperatorType.sub, matching: { @@ -2000,6 +2014,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_c - on(label1) group_left metric_b desc: "exceeding the per-group series limit", op: binaryOperatorType.sub, matching: { @@ -2082,6 +2097,7 @@ const testCases: TestCase[] = [ }, }, { + // metric_c - on(label1) group_left metric_b desc: "exceeding both group limit and per-group series limit", op: binaryOperatorType.sub, matching: { @@ -2131,6 +2147,732 @@ const testCases: TestCase[] = [ numGroups: 2, }, }, + { + // metric_a and metric b + desc: "and operator with no matching labels and matching groups", + op: binaryOperatorType.and, + matching: { + card: vectorMatchCardinality.manyToMany, + on: false, + include: [], + labels: [], + }, + lhs: testMetricA, + rhs: testMetricB, + result: { + groups: { + [fnv1a(["a", "x", "same"])]: { + groupLabels: { label1: "a", label2: "x", same: "same" }, + lhs: [ + { + metric: { + __name__: "metric_a", + label1: "a", + label2: "x", + same: "same", + }, + value: [0, "1"], + }, + ], + lhsCount: 1, + rhs: [ + { + metric: { + __name__: "metric_b", + label1: "a", + label2: "x", + same: "same", + }, + value: [0, "10"], + }, + ], + rhsCount: 1, + result: [ + { + sample: { + metric: { + __name__: "metric_a", + label1: "a", + label2: "x", + same: "same", + }, + value: [0, "1"], + }, + manySideIdx: 0, + }, + ], + error: null, + }, + [fnv1a(["a", "y", "same"])]: { + groupLabels: { label1: "a", label2: "y", same: "same" }, + lhs: [ + { + metric: { + __name__: "metric_a", + label1: "a", + label2: "y", + same: "same", + }, + value: [0, "2"], + }, + ], + lhsCount: 1, + rhs: [ + { + metric: { + __name__: "metric_b", + label1: "a", + label2: "y", + same: "same", + }, + value: [0, "20"], + }, + ], + rhsCount: 1, + result: [ + { + sample: { + metric: { + __name__: "metric_a", + label1: "a", + label2: "y", + same: "same", + }, + value: [0, "2"], + }, + manySideIdx: 0, + }, + ], + error: null, + }, + [fnv1a(["b", "x", "same"])]: { + groupLabels: { label1: "b", label2: "x", same: "same" }, + lhs: [ + { + metric: { + __name__: "metric_a", + label1: "b", + label2: "x", + same: "same", + }, + value: [0, "3"], + }, + ], + lhsCount: 1, + rhs: [ + { + metric: { + __name__: "metric_b", + label1: "b", + label2: "x", + same: "same", + }, + value: [0, "30"], + }, + ], + rhsCount: 1, + result: [ + { + sample: { + metric: { + __name__: "metric_a", + label1: "b", + label2: "x", + same: "same", + }, + value: [0, "3"], + }, + manySideIdx: 0, + }, + ], + error: null, + }, + [fnv1a(["b", "y", "same"])]: { + groupLabels: { label1: "b", label2: "y", same: "same" }, + lhs: [ + { + metric: { + __name__: "metric_a", + label1: "b", + label2: "y", + same: "same", + }, + value: [0, "4"], + }, + ], + lhsCount: 1, + rhs: [ + { + metric: { + __name__: "metric_b", + label1: "b", + label2: "y", + same: "same", + }, + value: [0, "40"], + }, + ], + rhsCount: 1, + result: [ + { + sample: { + metric: { + __name__: "metric_a", + label1: "b", + label2: "y", + same: "same", + }, + value: [0, "4"], + }, + manySideIdx: 0, + }, + ], + error: null, + }, + }, + numGroups: 4, + }, + }, + { + // metric_a[0...2] and on(label1) metric_b[1...3] + desc: "and operator with matching label and series on each side", + op: binaryOperatorType.and, + matching: { + card: vectorMatchCardinality.manyToMany, + on: true, + include: [], + labels: ["label1"], + }, + lhs: testMetricA.slice(0, 3), + rhs: testMetricB.slice(1, 4), + result: { + groups: { + [fnv1a(["a"])]: { + groupLabels: { label1: "a" }, + lhs: [ + { + metric: { + __name__: "metric_a", + label1: "a", + label2: "x", + same: "same", + }, + value: [0, "1"], + }, + { + metric: { + __name__: "metric_a", + label1: "a", + label2: "y", + same: "same", + }, + value: [0, "2"], + }, + ], + lhsCount: 2, + rhs: [ + { + metric: { + __name__: "metric_b", + label1: "a", + label2: "y", + same: "same", + }, + value: [0, "20"], + }, + ], + rhsCount: 1, + result: [ + { + sample: { + metric: { + __name__: "metric_a", + label1: "a", + label2: "x", + same: "same", + }, + value: [0, "1"], + }, + manySideIdx: 0, + }, + { + sample: { + metric: { + __name__: "metric_a", + label1: "a", + label2: "y", + same: "same", + }, + value: [0, "2"], + }, + manySideIdx: 1, + }, + ], + error: null, + }, + [fnv1a(["b"])]: { + groupLabels: { label1: "b" }, + lhs: [ + { + metric: { + __name__: "metric_a", + label1: "b", + label2: "x", + same: "same", + }, + value: [0, "3"], + }, + ], + lhsCount: 1, + rhs: [ + { + metric: { + __name__: "metric_b", + label1: "b", + label2: "x", + same: "same", + }, + value: [0, "30"], + }, + { + metric: { + __name__: "metric_b", + label1: "b", + label2: "y", + same: "same", + }, + value: [0, "40"], + }, + ], + rhsCount: 2, + result: [ + { + sample: { + metric: { + __name__: "metric_a", + label1: "b", + label2: "x", + same: "same", + }, + value: [0, "3"], + }, + manySideIdx: 0, + }, + ], + error: null, + }, + }, + numGroups: 2, + }, + }, + { + // metric_a[0...2] unless on(label1) metric_b[1...3] + desc: "unless operator with matching label and series on each side", + op: binaryOperatorType.unless, + matching: { + card: vectorMatchCardinality.manyToMany, + on: true, + include: [], + labels: ["label1"], + }, + lhs: testMetricA.slice(0, 3), + rhs: testMetricB.slice(1, 4), + result: { + groups: { + [fnv1a(["a"])]: { + groupLabels: { label1: "a" }, + lhs: [ + { + metric: { + __name__: "metric_a", + label1: "a", + label2: "x", + same: "same", + }, + value: [0, "1"], + }, + { + metric: { + __name__: "metric_a", + label1: "a", + label2: "y", + same: "same", + }, + value: [0, "2"], + }, + ], + lhsCount: 2, + rhs: [ + { + metric: { + __name__: "metric_b", + label1: "a", + label2: "y", + same: "same", + }, + value: [0, "20"], + }, + ], + rhsCount: 1, + result: [], + error: null, + }, + [fnv1a(["b"])]: { + groupLabels: { label1: "b" }, + lhs: [ + { + metric: { + __name__: "metric_a", + label1: "b", + label2: "x", + same: "same", + }, + value: [0, "3"], + }, + ], + lhsCount: 1, + rhs: [ + { + metric: { + __name__: "metric_b", + label1: "b", + label2: "x", + same: "same", + }, + value: [0, "30"], + }, + { + metric: { + __name__: "metric_b", + label1: "b", + label2: "y", + same: "same", + }, + value: [0, "40"], + }, + ], + rhsCount: 2, + result: [], + error: null, + }, + }, + numGroups: 2, + }, + }, + { + // metric_a[0...2] or on(label1) metric_b[1...3] + desc: "or operator with matching label and series on each side", + op: binaryOperatorType.or, + matching: { + card: vectorMatchCardinality.manyToMany, + on: true, + include: [], + labels: ["label1"], + }, + lhs: testMetricA.slice(0, 3), + rhs: testMetricB.slice(1, 4), + result: { + groups: { + [fnv1a(["a"])]: { + groupLabels: { label1: "a" }, + lhs: [ + { + metric: { + __name__: "metric_a", + label1: "a", + label2: "x", + same: "same", + }, + value: [0, "1"], + }, + { + metric: { + __name__: "metric_a", + label1: "a", + label2: "y", + same: "same", + }, + value: [0, "2"], + }, + ], + lhsCount: 2, + rhs: [ + { + metric: { + __name__: "metric_b", + label1: "a", + label2: "y", + same: "same", + }, + value: [0, "20"], + }, + ], + rhsCount: 1, + result: [ + { + sample: { + metric: { + __name__: "metric_a", + label1: "a", + label2: "x", + same: "same", + }, + value: [0, "1"], + }, + manySideIdx: 0, + }, + { + sample: { + metric: { + __name__: "metric_a", + label1: "a", + label2: "y", + same: "same", + }, + value: [0, "2"], + }, + manySideIdx: 1, + }, + ], + error: null, + }, + [fnv1a(["b"])]: { + groupLabels: { label1: "b" }, + lhs: [ + { + metric: { + __name__: "metric_a", + label1: "b", + label2: "x", + same: "same", + }, + value: [0, "3"], + }, + ], + lhsCount: 1, + rhs: [ + { + metric: { + __name__: "metric_b", + label1: "b", + label2: "x", + same: "same", + }, + value: [0, "30"], + }, + { + metric: { + __name__: "metric_b", + label1: "b", + label2: "y", + same: "same", + }, + value: [0, "40"], + }, + ], + rhsCount: 2, + result: [ + { + sample: { + metric: { + __name__: "metric_a", + label1: "b", + label2: "x", + same: "same", + }, + value: [0, "3"], + }, + manySideIdx: 0, + }, + ], + error: null, + }, + }, + numGroups: 2, + }, + }, + { + // metric_a[0...2] or metric_b[1...3] + desc: "or operator with only partial overlap", + op: binaryOperatorType.or, + matching: { + card: vectorMatchCardinality.manyToMany, + on: false, + include: [], + labels: [], + }, + lhs: testMetricA.slice(0, 3), + rhs: testMetricB.slice(1, 4), + result: { + groups: { + [fnv1a(["a", "x", "same"])]: { + groupLabels: { + label1: "a", + label2: "x", + same: "same", + }, + lhs: [ + { + metric: { + __name__: "metric_a", + label1: "a", + label2: "x", + same: "same", + }, + value: [0, "1"], + }, + ], + lhsCount: 1, + rhs: [], + rhsCount: 0, + result: [ + { + sample: { + metric: { + __name__: "metric_a", + label1: "a", + label2: "x", + same: "same", + }, + value: [0, "1"], + }, + manySideIdx: 0, + }, + ], + error: null, + }, + [fnv1a(["a", "y", "same"])]: { + groupLabels: { + label1: "a", + label2: "y", + same: "same", + }, + lhs: [ + { + metric: { + __name__: "metric_a", + label1: "a", + label2: "y", + same: "same", + }, + value: [0, "2"], + }, + ], + lhsCount: 1, + rhs: [ + { + metric: { + __name__: "metric_b", + label1: "a", + label2: "y", + same: "same", + }, + value: [0, "20"], + }, + ], + rhsCount: 1, + result: [ + { + sample: { + metric: { + __name__: "metric_a", + label1: "a", + label2: "y", + same: "same", + }, + value: [0, "2"], + }, + manySideIdx: 0, + }, + ], + error: null, + }, + [fnv1a(["b", "x", "same"])]: { + groupLabels: { + label1: "b", + label2: "x", + same: "same", + }, + lhs: [ + { + metric: { + __name__: "metric_a", + label1: "b", + label2: "x", + same: "same", + }, + value: [0, "3"], + }, + ], + lhsCount: 1, + rhs: [ + { + metric: { + __name__: "metric_b", + label1: "b", + label2: "x", + same: "same", + }, + value: [0, "30"], + }, + ], + rhsCount: 1, + result: [ + { + sample: { + metric: { + __name__: "metric_a", + label1: "b", + label2: "x", + same: "same", + }, + value: [0, "3"], + }, + manySideIdx: 0, + }, + ], + error: null, + }, + [fnv1a(["b", "y", "same"])]: { + groupLabels: { + label1: "b", + label2: "y", + same: "same", + }, + lhs: [], + lhsCount: 0, + rhs: [ + { + metric: { + __name__: "metric_b", + label1: "b", + label2: "y", + same: "same", + }, + value: [0, "40"], + }, + ], + rhsCount: 1, + result: [ + { + sample: { + metric: { + __name__: "metric_b", + label1: "b", + label2: "y", + same: "same", + }, + value: [0, "40"], + }, + manySideIdx: 0, + }, + ], + error: null, + }, + }, + numGroups: 4, + }, + }, ]; describe("binOp", () => { diff --git a/web/ui/mantine-ui/src/promql/binOp.ts b/web/ui/mantine-ui/src/promql/binOp.ts index 525f543ad..dbfa64be2 100644 --- a/web/ui/mantine-ui/src/promql/binOp.ts +++ b/web/ui/mantine-ui/src/promql/binOp.ts @@ -340,25 +340,35 @@ export const computeVectorVectorBinOp = ( // Annotate the match groups with errors (if any) and populate the results. Object.values(groups).forEach((mg) => { - // Do not populate results for set operators. - if (isSetOperator(op)) { - return; - } - - if (matching.card === vectorMatchCardinality.oneToOne) { - if (mg.lhs.length > 1 && mg.rhs.length > 1) { - mg.error = { type: MatchErrorType.multipleMatchesOnBothSides }; - } else if (mg.lhs.length > 1 || mg.rhs.length > 1) { - mg.error = { - type: MatchErrorType.multipleMatchesForOneToOneMatching, - dupeSide: mg.lhs.length > 1 ? "left" : "right", - }; - } - } else if (mg.rhs.length > 1) { - // Check for dupes on the "one" side in one-to-many or many-to-one matching. - mg.error = { - type: MatchErrorType.multipleMatchesOnOneSide, - }; + switch (matching.card) { + case vectorMatchCardinality.oneToOne: + if (mg.lhs.length > 1 && mg.rhs.length > 1) { + mg.error = { type: MatchErrorType.multipleMatchesOnBothSides }; + } else if (mg.lhs.length > 1 || mg.rhs.length > 1) { + mg.error = { + type: MatchErrorType.multipleMatchesForOneToOneMatching, + dupeSide: mg.lhs.length > 1 ? "left" : "right", + }; + } + break; + case vectorMatchCardinality.oneToMany: + case vectorMatchCardinality.manyToOne: + if (mg.rhs.length > 1) { + mg.error = { + type: MatchErrorType.multipleMatchesOnOneSide, + }; + } + break; + case vectorMatchCardinality.manyToMany: + // Should be a set operator - these don't have errors that aren't caught during parsing. + if (!isSetOperator(op)) { + throw new Error( + "unexpected many-to-many matching for non-set operator" + ); + } + break; + default: + throw new Error("unknown vector matching cardinality"); } if (mg.error) { @@ -368,42 +378,79 @@ export const computeVectorVectorBinOp = ( return; } - // Calculate the results for this match group. - mg.rhs.forEach((rs) => { + if (isSetOperator(op)) { + // Add LHS samples to the result, depending on specific operator condition and RHS length. mg.lhs.forEach((ls, lIdx) => { - if (!ls.value || !rs.value) { - // TODO: Implement native histogram support. - throw new Error("native histogram support not implemented yet"); + if ( + (op === binaryOperatorType.and && mg.rhs.length > 0) || + (op === binaryOperatorType.unless && mg.rhs.length === 0) || + op === binaryOperatorType.or + ) { + mg.result.push({ + sample: { + metric: ls.metric, + value: ls.value, + }, + manySideIdx: lIdx, + }); } + }); - const [vl, vr] = - matching.card !== vectorMatchCardinality.oneToMany - ? [ls.value[1], rs.value[1]] - : [rs.value[1], ls.value[1]]; - let { value, keep } = vectorElemBinop( - op, - parsePrometheusFloat(vl), - parsePrometheusFloat(vr) - ); + // For OR, also add all RHS samples to the result if the LHS for the group is empty. + if (op === binaryOperatorType.or) { + mg.rhs.forEach((rs, rIdx) => { + if (mg.lhs.length === 0) { + mg.result.push({ + sample: { + metric: rs.metric, + value: rs.value, + }, + manySideIdx: rIdx, + }); + } + }); + } + } else { + // Calculate the results for this match group. + mg.rhs.forEach((rs) => { + mg.lhs.forEach((ls, lIdx) => { + if (!ls.value || !rs.value) { + // TODO: Implement native histogram support. + throw new Error("native histogram support not implemented yet"); + } - const metric = resultMetric(ls.metric, rs.metric, op, matching); - if (bool) { - value = keep ? 1.0 : 0.0; - delete metric.__name__; - } + const [vl, vr] = + matching.card !== vectorMatchCardinality.oneToMany + ? [ls.value[1], rs.value[1]] + : [rs.value[1], ls.value[1]]; - mg.result.push({ - sample: { - metric: metric, - value: [ - ls.value[0], - keep || bool ? formatPrometheusFloat(value) : filteredSampleValue, - ], - }, - manySideIdx: lIdx, + let { value, keep } = vectorElemBinop( + op, + parsePrometheusFloat(vl), + parsePrometheusFloat(vr) + ); + + const metric = resultMetric(ls.metric, rs.metric, op, matching); + if (bool) { + value = keep ? 1.0 : 0.0; + delete metric.__name__; + } + + mg.result.push({ + sample: { + metric: metric, + value: [ + ls.value[0], + keep || bool + ? formatPrometheusFloat(value) + : filteredSampleValue, + ], + }, + manySideIdx: lIdx, + }); }); }); - }); + } }); // If we originally swapped the LHS and RHS, swap them back to the original order.