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.