diff --git a/component/component-commons/src/main/java/org/bithon/component/commons/expression/BinaryExpression.java b/component/component-commons/src/main/java/org/bithon/component/commons/expression/BinaryExpression.java index 0752d83076..44c5913301 100644 --- a/component/component-commons/src/main/java/org/bithon/component/commons/expression/BinaryExpression.java +++ b/component/component-commons/src/main/java/org/bithon/component/commons/expression/BinaryExpression.java @@ -62,4 +62,18 @@ public String getType() { public void serializeToText(ExpressionSerializer serializer) { serializer.serialize(this); } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof BinaryExpression)) { + return false; + } + BinaryExpression that = (BinaryExpression) obj; + return this.type.equals(that.type) && + this.lhs.equals(that.lhs) && + this.rhs.equals(that.rhs); + } } diff --git a/component/component-commons/src/main/java/org/bithon/component/commons/expression/IdentifierExpression.java b/component/component-commons/src/main/java/org/bithon/component/commons/expression/IdentifierExpression.java index 746a3e1056..e5cdd79e57 100644 --- a/component/component-commons/src/main/java/org/bithon/component/commons/expression/IdentifierExpression.java +++ b/component/component-commons/src/main/java/org/bithon/component/commons/expression/IdentifierExpression.java @@ -115,4 +115,20 @@ public Object evaluate(IEvaluationContext context) { public String toString() { return identifier; } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof IdentifierExpression)) { + return false; + } + IdentifierExpression that = (IdentifierExpression) obj; + return this.identifier.equals(that.identifier) && + ((this.qualifier == null && that.qualifier == null) || + (this.qualifier != null && this.qualifier.equals(that.qualifier))) && + ((this.dataType == null && that.dataType == null) || + (this.dataType != null && this.dataType.equals(that.dataType))); + } } diff --git a/component/component-commons/src/main/java/org/bithon/component/commons/expression/LiteralExpression.java b/component/component-commons/src/main/java/org/bithon/component/commons/expression/LiteralExpression.java index 65fe956bbf..fb423469a8 100644 --- a/component/component-commons/src/main/java/org/bithon/component/commons/expression/LiteralExpression.java +++ b/component/component-commons/src/main/java/org/bithon/component/commons/expression/LiteralExpression.java @@ -111,7 +111,7 @@ protected LiteralExpression(T value) { } /** - * NOTE: do not change the method name since it's used in the ExpressionDeserializer} + * NOTE: do not change the method name since it's used in the ExpressionDeserializer */ public T getValue() { return value; @@ -162,6 +162,18 @@ public void serializeToText(ExpressionSerializer serializer) { serializer.serialize(this); } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof LiteralExpression)) { + return false; + } + LiteralExpression that = (LiteralExpression) obj; + return this.getDataType() == that.getDataType() && this.value.equals(that.value); + } + public abstract boolean canNegate(); public LiteralExpression negate() { @@ -244,7 +256,7 @@ public LiteralExpression castTo(IDataType targetType) { return this; case DOUBLE: - return new LiteralExpression.DoubleLiteral(((Number) value).doubleValue()); + return new LiteralExpression.DoubleLiteral(value.doubleValue()); case BOOLEAN: return new LiteralExpression.BooleanLiteral(value != 0); @@ -291,13 +303,13 @@ public LiteralExpression castTo(IDataType targetType) { return new LiteralExpression.StringLiteral(value.toString()); case LONG: - return new LiteralExpression.LongLiteral(((Number) value).longValue()); + return new LiteralExpression.LongLiteral(value.longValue()); case DOUBLE: return this; case BOOLEAN: - return new LiteralExpression.BooleanLiteral(((Number) value).doubleValue() != 0); + return new LiteralExpression.BooleanLiteral(value != 0); default: throw new UnsupportedOperationException("Can't cast a boolean value into type of " + targetType); @@ -336,13 +348,13 @@ public LiteralExpression castTo(IDataType targetType) { return new LiteralExpression.StringLiteral(value.toString()); case LONG: - return new LiteralExpression.LongLiteral(((Number) value).longValue()); + return new LiteralExpression.LongLiteral(value.longValue()); case DOUBLE: return new LiteralExpression.DoubleLiteral(value.doubleValue()); case BOOLEAN: - return new LiteralExpression.BooleanLiteral(((Number) value).doubleValue() != 0); + return new LiteralExpression.BooleanLiteral(value.doubleValue() != 0); default: throw new UnsupportedOperationException("Can't cast a boolean value into type of " + targetType); diff --git a/dev/checkstyle-suppressions.xml b/dev/checkstyle-suppressions.xml index be51b28a8f..4e2a9b611c 100644 --- a/dev/checkstyle-suppressions.xml +++ b/dev/checkstyle-suppressions.xml @@ -33,6 +33,7 @@ + diff --git a/pom.xml b/pom.xml index 8ee0a9c425..f04e6a440a 100644 --- a/pom.xml +++ b/pom.xml @@ -129,6 +129,15 @@ org.apache.maven.plugins maven-compiler-plugin 3.14.0 + + + + org.projectlombok + lombok + ${lombok.version} + + + diff --git a/server/alerting/common/src/main/java/org/bithon/server/alerting/common/model/AlertExpression.java b/server/alerting/common/src/main/java/org/bithon/server/alerting/common/model/AlertExpression.java index a96f8f0437..722a6cfc05 100644 --- a/server/alerting/common/src/main/java/org/bithon/server/alerting/common/model/AlertExpression.java +++ b/server/alerting/common/src/main/java/org/bithon/server/alerting/common/model/AlertExpression.java @@ -24,7 +24,7 @@ import org.bithon.component.commons.expression.IExpressionVisitor; import org.bithon.component.commons.expression.serialization.ExpressionSerializer; import org.bithon.server.alerting.common.evaluator.metric.IMetricEvaluator; -import org.bithon.server.metric.expression.ast.MetricExpression; +import org.bithon.server.metric.expression.ast.MetricAggregateExpression; /** * NOTE: When changes are made to this class, @@ -52,7 +52,7 @@ public class AlertExpression implements IExpression { private String id; - private MetricExpression metricExpression; + private MetricAggregateExpression metricExpression; private IMetricEvaluator metricEvaluator; @Override diff --git a/server/alerting/common/src/main/java/org/bithon/server/alerting/common/parser/AlertExpressionASTParser.java b/server/alerting/common/src/main/java/org/bithon/server/alerting/common/parser/AlertExpressionASTParser.java index 07a1cad530..7c8828e86c 100644 --- a/server/alerting/common/src/main/java/org/bithon/server/alerting/common/parser/AlertExpressionASTParser.java +++ b/server/alerting/common/src/main/java/org/bithon/server/alerting/common/parser/AlertExpressionASTParser.java @@ -44,7 +44,7 @@ import org.bithon.server.metric.expression.MetricExpressionBaseVisitor; import org.bithon.server.metric.expression.MetricExpressionLexer; import org.bithon.server.metric.expression.MetricExpressionParser; -import org.bithon.server.metric.expression.ast.MetricExpression; +import org.bithon.server.metric.expression.ast.MetricAggregateExpression; import org.bithon.server.metric.expression.ast.MetricExpressionASTBuilder; import java.util.List; @@ -88,8 +88,8 @@ private static class MetricExpressionBuilder extends MetricExpressionBaseVisitor @Override public IExpression visitAtomicAlertExpression(MetricExpressionParser.AtomicAlertExpressionContext ctx) { - IExpression expression = MetricExpressionASTBuilder.build(ctx.metricExpression()); - if (!(expression instanceof MetricExpression metricExpression)) { + IExpression expression = MetricExpressionASTBuilder.toAST(ctx.metricExpression()); + if (!(expression instanceof MetricAggregateExpression metricExpression)) { throw new InvalidExpressionException("Complex expression is not supported now."); } @@ -148,7 +148,7 @@ public IExpression visitLogicalAlertExpression(MetricExpressionParser.LogicalAle } } - private static IMetricEvaluator createMetricEvaluator(MetricExpression metricExpression) { + private static IMetricEvaluator createMetricEvaluator(MetricAggregateExpression metricExpression) { LiteralExpression expected = metricExpression.getExpected(); HumanReadableDuration offset = metricExpression.getOffset(); diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/ISchema.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/ISchema.java index 316ee8d70e..d6c0f3938d 100644 --- a/server/datasource/common/src/main/java/org/bithon/server/datasource/ISchema.java +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/ISchema.java @@ -64,6 +64,4 @@ default boolean isVirtual() { } Period getTtl(); - - } diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/ISchemaProvider.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/ISchemaProvider.java new file mode 100644 index 0000000000..445c83870b --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/ISchemaProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource; + +/** + * @author frank.chen021@outlook.com + * @date 2025/6/11 20:34 + */ +public interface ISchemaProvider { + ISchema getSchema(String name) throws SchemaException; +} diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/IDataSourceReader.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/IDataSourceReader.java index 55e09ca1fb..7235be8db7 100644 --- a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/IDataSourceReader.java +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/IDataSourceReader.java @@ -17,7 +17,9 @@ package org.bithon.server.datasource.query; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; +import org.bithon.server.datasource.query.plan.logical.LogicalTableScan; +import org.bithon.server.datasource.query.plan.physical.IPhysicalPlan; +import org.bithon.server.datasource.query.result.ColumnarTable; import java.util.List; @@ -28,6 +30,10 @@ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") public interface IDataSourceReader extends AutoCloseable { + default IPhysicalPlan plan(LogicalTableScan tableScan, Interval interval) { + throw new UnsupportedOperationException("This data source does not support logical plan execution"); + } + ColumnarTable timeseries(Query query); /** diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/BaseLogicalPlanVisitor.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/BaseLogicalPlanVisitor.java new file mode 100644 index 0000000000..3459447ea9 --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/BaseLogicalPlanVisitor.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.logical; + +/** + * Base implementation of ILogicalPlanVisitor that provides default behavior + * and utility methods for common visitor operations. + *

+ * This class can be extended to implement specific visitor functionality + * without having to implement all visitor methods from scratch. + * + * @param the return type of the visitor methods + * @author frank.chen021@outlook.com + * @date 2025/6/4 23:31 + */ +public abstract class BaseLogicalPlanVisitor implements ILogicalPlanVisitor { + + /** + * Default implementation for visiting table scans. + * Can be overridden by subclasses to provide specific behavior. + */ + @Override + public T visitTableScan(LogicalTableScan tableScan) { + return defaultVisit(tableScan); + } + + /** + * Default implementation for visiting aggregates. + * Recursively visit the input plan. + * It Can be overridden by subclasses to provide specific behavior. + */ + @Override + public T visitAggregate(LogicalAggregate aggregate) { + aggregate.input().accept(this); + return defaultVisit(aggregate); + } + + /** + * Default implementation for visiting binary operations. + * Recursively visits both left and right operands. + * It Can be overridden by subclasses to provide specific behavior. + */ + @Override + public T visitBinaryOp(LogicalBinaryOp binaryOp) { + binaryOp.left().accept(this); + binaryOp.right().accept(this); + return defaultVisit(binaryOp); + } + + /** + * Default implementation for visiting scalars. + * Can be overridden by subclasses to provide specific behavior. + */ + @Override + public T visitScalar(LogicalScalar scalar) { + return defaultVisit(scalar); + } + + /** + * Default implementation for visiting filters. + * Recursively visits both left and right operands. + * It Can be overridden by subclasses to provide specific behavior. + */ + @Override + public T visitFilter(LogicalFilter filter) { + filter.left().accept(this); + filter.right().accept(this); + return defaultVisit(filter); + } + + /** + * Default behavior for all visit methods. + * Subclasses can override this to provide common default behavior, + * or override individual visit methods for specific behavior. + * + * @param plan the logical plan being visited + * @return the default result + */ + protected abstract T defaultVisit(ILogicalPlan plan); +} diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/IQueryStep.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/BinaryOp.java similarity index 72% rename from server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/IQueryStep.java rename to server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/BinaryOp.java index fe3591c1ed..57cc8ab30d 100644 --- a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/IQueryStep.java +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/BinaryOp.java @@ -14,18 +14,13 @@ * limitations under the License. */ -package org.bithon.server.datasource.query.pipeline; - - -import java.util.concurrent.CompletableFuture; +package org.bithon.server.datasource.query.plan.logical; /** * @author frank.chen021@outlook.com - * @date 4/4/25 3:47 pm + * @date 2025/6/4 23:33 */ -public interface IQueryStep { - - boolean isScalar(); - - CompletableFuture execute() throws Exception; +public enum BinaryOp { + ADD, SUB, MUL, DIV } + diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/ILogicalPlan.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/ILogicalPlan.java new file mode 100644 index 0000000000..ae247d779e --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/ILogicalPlan.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.logical; + +/** + * @author frank.chen021@outlook.com + * @date 2025/6/4 23:31 + */ +public sealed interface ILogicalPlan + permits LogicalTableScan, LogicalAggregate, LogicalBinaryOp, + LogicalScalar, LogicalFilter { + + /** + * Accept a visitor for the visitor pattern implementation + * @param visitor the visitor to accept + * @param the return type of the visitor + * @return the result of visiting this logical plan + */ + T accept(ILogicalPlanVisitor visitor); +} + diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/ILogicalPlanVisitor.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/ILogicalPlanVisitor.java new file mode 100644 index 0000000000..285e2fdc50 --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/ILogicalPlanVisitor.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.logical; + +/** + * Visitor interface for the ILogicalPlan hierarchy. + * Implements the visitor pattern to allow operations on logical plans + * without modifying the plan classes themselves. + * + * @param the return type of the visitor methods + * @author frank.chen021@outlook.com + * @date 2025/6/4 23:31 + */ +public interface ILogicalPlanVisitor { + + /** + * Visit a LogicalTableScan node + * @param tableScan the table to visit + * @return the result of visiting the table scan + */ + T visitTableScan(LogicalTableScan tableScan); + + /** + * Visit a LogicalAggregate node + * @param aggregate the aggregate to visit + * @return the result of visiting the aggregate + */ + T visitAggregate(LogicalAggregate aggregate); + + /** + * Visit a LogicalBinaryOp node + * @param binaryOp the binary operation to visit + * @return the result of visiting the binary operation + */ + T visitBinaryOp(LogicalBinaryOp binaryOp); + + /** + * Visit a LogicalScalar node + * @param scalar the scalar to visit + * @return the result of visiting the scalar + */ + T visitScalar(LogicalScalar scalar); + + /** + * Visit a LogicalFilter node + * @param filter the filter to visit + * @return the result of visiting the filter + */ + T visitFilter(LogicalFilter filter); +} diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalAggregate.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalAggregate.java new file mode 100644 index 0000000000..94d369b982 --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalAggregate.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.logical; + +import org.bithon.server.datasource.column.IColumn; + +import java.util.List; + +/** + * @author frank.chen021@outlook.com + * @date 2025/6/4 23:32 + */ +public record LogicalAggregate( + /** + * The function to be applied for aggregation, e.g., "SUM", "AVG", etc. + */ + String func, + ILogicalPlan input, + /** + * The field to be aggregated. + */ + IColumn field, + List groupBy, + List orderBy +) implements ILogicalPlan { + + @Override + public T accept(ILogicalPlanVisitor visitor) { + return visitor.visitAggregate(this); + } +} + diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalBinaryOp.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalBinaryOp.java new file mode 100644 index 0000000000..55f6553109 --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalBinaryOp.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.logical; + +/** + * @author frank.chen021@outlook.com + * @date 2025/6/4 23:32 + */ +public record LogicalBinaryOp( + ILogicalPlan left, + BinaryOp op, + ILogicalPlan right +) implements ILogicalPlan { + + @Override + public T accept(ILogicalPlanVisitor visitor) { + return visitor.visitBinaryOp(this); + } +} diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalFilter.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalFilter.java new file mode 100644 index 0000000000..f8fe270a72 --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalFilter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.logical; + +/** + * @author frank.chen021@outlook.com + * @date 2025/6/4 23:34 + */ +public record LogicalFilter( + ILogicalPlan left, + PredicateEnum op, + ILogicalPlan right +) implements ILogicalPlan { + + @Override + public T accept(ILogicalPlanVisitor visitor) { + return visitor.visitFilter(this); + } +} diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalPlanSerializer.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalPlanSerializer.java new file mode 100644 index 0000000000..344319df13 --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalPlanSerializer.java @@ -0,0 +1,102 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.logical; + +/** + * Concrete visitor implementation that converts logical plans to string representation. + * This serves as an example of how to implement and use the visitor pattern. + * + * @author frank.chen021@outlook.com + * @date 2025/6/4 23:31 + */ +public class LogicalPlanSerializer implements ILogicalPlanVisitor { + + private int indentLevel = 0; + private static final String INDENT = " "; + + @Override + public String visitTableScan(LogicalTableScan tableScan) { + return indent() + "TableScan(" + + "table='" + tableScan.table() + "', " + + "filters=" + (tableScan.filter() != null ? tableScan.filter().toString() : "null") + + ")"; + } + + @Override + public String visitAggregate(LogicalAggregate aggregate) { + StringBuilder sb = new StringBuilder(); + sb.append(indent()).append("Aggregate("); + sb.append("func='").append(aggregate.func()).append("', "); + sb.append("groupBy=").append(aggregate.groupBy()).append(",\n"); + + indentLevel++; + String inputStr = aggregate.input().accept(this); + indentLevel--; + + sb.append(inputStr).append("\n"); + sb.append(indent()).append(")"); + return sb.toString(); + } + + @Override + public String visitBinaryOp(LogicalBinaryOp binaryOp) { + StringBuilder sb = new StringBuilder(); + sb.append(indent()).append("BinaryOp(op=").append(binaryOp.op()).append(",\n"); + + indentLevel++; + String leftStr = binaryOp.left().accept(this); + String rightStr = binaryOp.right().accept(this); + indentLevel--; + + sb.append(leftStr).append(",\n"); + sb.append(rightStr).append("\n"); + sb.append(indent()).append(")"); + return sb.toString(); + } + + @Override + public String visitScalar(LogicalScalar scalar) { + return indent() + "Scalar(value=" + scalar.literal().serializeToText() + ")"; + } + + @Override + public String visitFilter(LogicalFilter filter) { + StringBuilder sb = new StringBuilder(); + sb.append(indent()).append("Filter(op=").append(filter.op()).append(",\n"); + + indentLevel++; + String leftStr = filter.left().accept(this); + String rightStr = filter.right().accept(this); + indentLevel--; + + sb.append(leftStr).append(",\n"); + sb.append(rightStr).append("\n"); + sb.append(indent()).append(")"); + return sb.toString(); + } + + private String indent() { + return INDENT.repeat(indentLevel); + } + + /** + * Utility method to convert any logical plan to string representation + */ + public static String toString(ILogicalPlan plan) { + return plan.accept(new LogicalPlanSerializer()); + } +} diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalPlanner.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalPlanner.java new file mode 100644 index 0000000000..13496c3d92 --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalPlanner.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.logical; + +/** + * @author frank.chen021@outlook.com + * @date 2025/6/4 23:36 + */ +public class LogicalPlanner { + /* + public static ILogicalPlan plan(IExpression expression) { + return expression.accept(new LogicalPlanBuilder()); + }*/ + +} diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalScalar.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalScalar.java new file mode 100644 index 0000000000..57c1eef3c9 --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalScalar.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.logical; + +import jakarta.annotation.Nullable; +import org.bithon.component.commons.expression.LiteralExpression; +import org.bithon.component.commons.utils.HumanReadableDuration; + +/** + * @author frank.chen021@outlook.com + * @date 2025/6/4 23:33 + */ +public record LogicalScalar( + LiteralExpression literal, + @Nullable HumanReadableDuration offset +) implements ILogicalPlan { + + public LogicalScalar(LiteralExpression literal) { + this(literal, null); + } + + @Override + public T accept(ILogicalPlanVisitor visitor) { + return visitor.visitScalar(this); + } +} diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalTableScan.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalTableScan.java new file mode 100644 index 0000000000..c18e0532b5 --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/LogicalTableScan.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.logical; + +import jakarta.annotation.Nullable; +import org.bithon.component.commons.expression.IExpression; +import org.bithon.component.commons.utils.HumanReadableDuration; +import org.bithon.server.datasource.ISchema; +import org.bithon.server.datasource.query.ast.Selector; + +import java.util.List; + +/** + * @author frank.chen021@outlook.com + * @date 2025/6/4 23:31 + */ +public record LogicalTableScan( + ISchema table, + List selectorList, + IExpression filter, + @Nullable HumanReadableDuration offset +) implements ILogicalPlan { + + @Override + public T accept(ILogicalPlanVisitor visitor) { + return visitor.visitTableScan(this); + } +} diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/PredicateEnum.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/PredicateEnum.java new file mode 100644 index 0000000000..8bc30e60e3 --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/logical/PredicateEnum.java @@ -0,0 +1,113 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.logical; + +import org.bithon.component.commons.expression.ComparisonExpression; +import org.bithon.component.commons.expression.ConditionalExpression; +import org.bithon.component.commons.expression.IExpression; + +/** + * @author frank.chen021@outlook.com + * @date 2024/7/20 17:12 + */ +public enum PredicateEnum { + + LT { + @Override + public IExpression createComparisonExpression(IExpression left, IExpression right) { + return new ComparisonExpression.LT(left, right); + } + + @Override + public String toString() { + return "<"; + } + }, + + LTE { + @Override + public IExpression createComparisonExpression(IExpression left, IExpression right) { + return new ComparisonExpression.LTE(left, right); + } + + @Override + public String toString() { + return "<="; + } + }, + + GT { + @Override + public IExpression createComparisonExpression(IExpression left, IExpression right) { + return new ComparisonExpression.GT(left, right); + } + + @Override + public String toString() { + return ">"; + } + }, + + GTE { + @Override + public IExpression createComparisonExpression(IExpression left, IExpression right) { + return new ComparisonExpression.GTE(left, right); + } + + + @Override + public String toString() { + return ">="; + } + }, + + NE { + @Override + public IExpression createComparisonExpression(IExpression left, IExpression right) { + return new ComparisonExpression.NE(left, right); + } + + @Override + public String toString() { + return "<>"; + } + }, + EQ { + @Override + public IExpression createComparisonExpression(IExpression left, IExpression right) { + return new ComparisonExpression.EQ(left, right); + } + + @Override + public String toString() { + return "="; + } + }, + IS_NULL { + @Override + public IExpression createComparisonExpression(IExpression left, IExpression right) { + return new ConditionalExpression.IsNull(left); + } + + @Override + public String toString() { + return "IS NULL"; + } + }; + + public abstract IExpression createComparisonExpression(IExpression left, IExpression right); +} diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/BinaryExpressionQueryStep.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/ArithmeticStep.java similarity index 85% rename from server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/BinaryExpressionQueryStep.java rename to server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/ArithmeticStep.java index 82f06a981a..e6fd3319a6 100644 --- a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/BinaryExpressionQueryStep.java +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/ArithmeticStep.java @@ -14,13 +14,12 @@ * limitations under the License. */ -package org.bithon.server.metric.expression.pipeline.step; +package org.bithon.server.datasource.query.plan.physical; -import org.bithon.server.datasource.query.pipeline.Column; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; -import org.bithon.server.datasource.query.pipeline.IQueryStep; -import org.bithon.server.datasource.query.pipeline.PipelineQueryResult; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.ColumnarTable; +import org.bithon.server.datasource.query.result.PipelineQueryResult; import java.util.ArrayList; import java.util.Arrays; @@ -35,9 +34,9 @@ * @author frank.chen021@outlook.com * @date 4/4/25 3:49 pm */ -public abstract class BinaryExpressionQueryStep implements IQueryStep { - private final IQueryStep lhs; - private final IQueryStep rhs; +public abstract class ArithmeticStep implements IPhysicalPlan { + private final IPhysicalPlan lhs; + private final IPhysicalPlan rhs; private final String resultColumnName; /** @@ -45,8 +44,8 @@ public abstract class BinaryExpressionQueryStep implements IQueryStep { */ private final Set retainedColumns; - public static class Add extends BinaryExpressionQueryStep { - public Add(IQueryStep left, IQueryStep right) { + public static class Add extends ArithmeticStep { + public Add(IPhysicalPlan left, IPhysicalPlan right) { super(left, right, null); } @@ -56,16 +55,16 @@ int getOperatorIndex() { } } - public static class Sub extends BinaryExpressionQueryStep { - public Sub(IQueryStep left, IQueryStep right) { + public static class Sub extends ArithmeticStep { + public Sub(IPhysicalPlan left, IPhysicalPlan right) { super(left, right, null); } - public Sub(IQueryStep left, - IQueryStep right, + public Sub(IPhysicalPlan left, + IPhysicalPlan right, String resultColumn, - String... sourceColumns) { - super(left, right, resultColumn, sourceColumns); + boolean retainAll) { + super(left, right, resultColumn); } @Override @@ -74,8 +73,8 @@ int getOperatorIndex() { } } - public static class Mul extends BinaryExpressionQueryStep { - public Mul(IQueryStep left, IQueryStep right) { + public static class Mul extends ArithmeticStep { + public Mul(IPhysicalPlan left, IPhysicalPlan right) { super(left, right, null); } @@ -85,16 +84,16 @@ int getOperatorIndex() { } } - public static class Div extends BinaryExpressionQueryStep { - public Div(IQueryStep left, IQueryStep right) { + public static class Div extends ArithmeticStep { + public Div(IPhysicalPlan left, IPhysicalPlan right) { super(left, right, null); } - public Div(IQueryStep left, - IQueryStep right, + public Div(IPhysicalPlan left, + IPhysicalPlan right, String resultColumn, - String... sourceColumns) { - super(left, right, resultColumn, sourceColumns); + boolean retainAll) { + super(left, right, resultColumn); } @Override @@ -103,16 +102,27 @@ int getOperatorIndex() { } } - protected BinaryExpressionQueryStep(IQueryStep left, - IQueryStep right, - String resultColumnName, - String... retainedColumns) { + protected ArithmeticStep(IPhysicalPlan left, + IPhysicalPlan right, + String resultColumnName, + String... retainedColumns) { this.lhs = left; this.rhs = right; this.resultColumnName = resultColumnName == null ? "value" : resultColumnName; this.retainedColumns = retainedColumns == null ? Collections.emptySet() : new LinkedHashSet<>(Arrays.asList(retainedColumns)); } + @Override + public void serializer(PhysicalPlanSerializer serializer) { + serializer.append(this.getClass().getSimpleName()).append("Step") + .append(", Result Column: ").append(resultColumnName) + .append(", Retained Columns: ").append(retainedColumns.toString()).append("\n") + .append(" lhs: \n") + .append(" ", this.lhs.serializeToText()) + .append(" rhs: \n") + .append(" ", this.rhs.serializeToText()); + } + @Override public CompletableFuture execute() throws Exception { CompletableFuture leftFuture = this.lhs.execute(); diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/ColumnOperator.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/ColumnOperator.java similarity index 99% rename from server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/ColumnOperator.java rename to server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/ColumnOperator.java index a692071123..1c59130541 100644 --- a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/ColumnOperator.java +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/ColumnOperator.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.bithon.server.metric.expression.pipeline.step; +package org.bithon.server.datasource.query.plan.physical; import org.bithon.component.commons.expression.IDataTypeIndex; import org.bithon.component.commons.utils.StringUtils; -import org.bithon.server.datasource.query.pipeline.Column; -import org.bithon.server.datasource.query.pipeline.DoubleColumn; -import org.bithon.server.datasource.query.pipeline.LongColumn; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.DoubleColumn; +import org.bithon.server.datasource.query.result.LongColumn; /** * @author frank.chen021@outlook.com diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/CompositeKey.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/CompositeKey.java similarity index 93% rename from server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/CompositeKey.java rename to server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/CompositeKey.java index 48670595fe..85633d4ead 100644 --- a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/CompositeKey.java +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/CompositeKey.java @@ -14,9 +14,11 @@ * limitations under the License. */ -package org.bithon.server.datasource.query.pipeline; +package org.bithon.server.datasource.query.plan.physical; +import org.bithon.server.datasource.query.result.Column; + import java.util.Arrays; import java.util.List; diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/FilterStep.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/FilterStep.java new file mode 100644 index 0000000000..30ba59d7f4 --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/FilterStep.java @@ -0,0 +1,185 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.physical; + + +import org.bithon.component.commons.expression.IDataType; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.ColumnarTable; +import org.bithon.server.datasource.query.result.PipelineQueryResult; + +import java.util.concurrent.CompletableFuture; + +/** + * Plan for the {@link org.bithon.component.commons.expression.ComparisonExpression} + * + * @author frank.chen021@outlook.com + * @date 1/6/25 9:06 pm + */ +public abstract class FilterStep implements IPhysicalPlan { + protected final IPhysicalPlan source; + protected final Number expected; + + protected FilterStep(IPhysicalPlan source, Number expected) { + this.source = source; + this.expected = expected; + } + + @Override + public boolean isScalar() { + return source.isScalar(); + } + + @Override + public CompletableFuture execute() throws Exception { + return source.execute() + .thenApply(result -> { + String valColumn = result.getValColumns().get(0); + Column column = result.getTable().getColumn(valColumn); + + int filteredRowCount = 0; + int[] filteredRows = new int[result.getTable().rowCount()]; + if (column.getDataType() == IDataType.LONG) { + long expectedValue = this.expected.longValue(); + + for (int i = 0, size = column.size(); i < size; i++) { + if (filter(column.getLong(i), expectedValue)) { + filteredRows[filteredRowCount++] = i; + } + } + } else if (column.getDataType() == IDataType.DOUBLE) { + double expectedValue = this.expected.doubleValue(); + + for (int i = 0, size = column.size(); i < size; i++) { + if (filter(column.getDouble(i), expectedValue)) { + filteredRows[filteredRowCount++] = i; + } + } + } else { + throw new UnsupportedOperationException("Unsupported data type: " + column.getDataType()); + } + + if (filteredRowCount < filteredRows.length) { + ColumnarTable newTable = result.getTable() + .view(filteredRows, filteredRowCount); + + return PipelineQueryResult.builder() + .rows(filteredRowCount) + .table(newTable) + .keyColumns(result.getKeyColumns()) + .valColumns(result.getValColumns()) + .build(); + } else { + // All results are filtered out, no need to apply a filter view on top of it + return result; + } + }); + } + + @Override + public void serializer(PhysicalPlanSerializer serializer) { + serializer.append("ComparisonStep: "); + serializer.append(this.getClass().getSimpleName()); + serializer.append("\n"); + serializer.append(" ", "lhs: ").append(" ", source.serializeToText()); + serializer.append(" ", "rhs: ").append(" ", expected.toString()).append("\n"); + } + + protected abstract boolean filter(long actual, long expected); + + protected abstract boolean filter(double actual, double expected); + + public static class LT extends FilterStep { + public LT(IPhysicalPlan source, Number expected) { + super(source, expected); + } + + @Override + protected boolean filter(long actual, long expected) { + return actual < expected; + } + + @Override + protected boolean filter(double actual, double expected) { + return actual < expected; + } + } + + public static class LTE extends FilterStep { + public LTE(IPhysicalPlan source, Number expected) { + super(source, expected); + } + + @Override + protected boolean filter(long actual, long expected) { + return actual <= expected; + } + + @Override + protected boolean filter(double actual, double expected) { + return actual <= expected; + } + } + + public static class GT extends FilterStep { + public GT(IPhysicalPlan source, Number expected) { + super(source, expected); + } + + @Override + protected boolean filter(long actual, long expected) { + return actual > expected; + } + + @Override + protected boolean filter(double actual, double expected) { + return actual > expected; + } + } + + public static class GTE extends FilterStep { + public GTE(IPhysicalPlan source, Number expected) { + super(source, expected); + } + + @Override + protected boolean filter(long actual, long expected) { + return actual >= expected; + } + + @Override + protected boolean filter(double actual, double expected) { + return actual >= expected; + } + } + + public static class NE extends FilterStep { + public NE(IPhysicalPlan source, Number expected) { + super(source, expected); + } + + @Override + protected boolean filter(long actual, long expected) { + return actual != expected; + } + + @Override + protected boolean filter(double actual, double expected) { + return actual != expected; + } + } +} diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/FunctionCallStep.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/FunctionCallStep.java new file mode 100644 index 0000000000..d878d10e8a --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/FunctionCallStep.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.physical; + + +import org.bithon.server.datasource.query.result.PipelineQueryResult; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +/** + * Map to the {@link org.bithon.component.commons.expression.FunctionExpression} + * + * @author frank.chen021@outlook.com + * @date 2/6/25 9:29 pm + */ +public abstract class FunctionCallStep implements IPhysicalPlan { + private final IPhysicalPlan source; + + public FunctionCallStep(IPhysicalPlan source) { + this.source = source; + } + + @Override + public boolean isScalar() { + return source.isScalar(); + } + + public static FunctionCallStep apply(IPhysicalPlan source, + Function function) { + return new FunctionCallStep(source) { + @Override + public CompletableFuture execute() throws Exception { + return source.execute().thenApply(function); + } + }; + } + + public static Function SUM = (result) -> { + return result; + }; +} diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/HashJoiner.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/HashJoiner.java similarity index 94% rename from server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/HashJoiner.java rename to server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/HashJoiner.java index 59f150487a..c253e83ee4 100644 --- a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/HashJoiner.java +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/HashJoiner.java @@ -14,12 +14,11 @@ * limitations under the License. */ -package org.bithon.server.metric.expression.pipeline.step; +package org.bithon.server.datasource.query.plan.physical; import org.bithon.component.commons.utils.CollectionUtils; -import org.bithon.server.datasource.query.pipeline.Column; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; -import org.bithon.server.datasource.query.pipeline.CompositeKey; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.ColumnarTable; import java.util.ArrayList; import java.util.HashMap; diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/IPhysicalPlan.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/IPhysicalPlan.java new file mode 100644 index 0000000000..6fb5ad311d --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/IPhysicalPlan.java @@ -0,0 +1,71 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.physical; + + +import org.bithon.component.commons.utils.HumanReadableDuration; +import org.bithon.server.datasource.query.plan.logical.LogicalAggregate; +import org.bithon.server.datasource.query.plan.logical.LogicalFilter; +import org.bithon.server.datasource.query.result.PipelineQueryResult; + +import java.util.concurrent.CompletableFuture; + +/** + * @author frank.chen021@outlook.com + * @date 4/4/25 3:47 pm + */ +public interface IPhysicalPlan { + + default boolean canPushDownAggregate(LogicalAggregate aggregate) { + return false; + } + + default IPhysicalPlan pushDownAggregate(LogicalAggregate aggregate) { + throw new UnsupportedOperationException("Cannot push down aggregate: " + aggregate); + } + + default boolean canPushDownFilter() { + return false; + } + + default IPhysicalPlan pushDownFilter(LogicalFilter filter) { + throw new UnsupportedOperationException("Cannot push down filter: " + filter); + } + + /** + * Mutate the plan to apply an offset. + * @param offset can't be null + */ + default IPhysicalPlan offset(HumanReadableDuration offset) { + throw new UnsupportedOperationException("Cannot apply offset: " + offset + " to type " + this.getClass().getSimpleName()); + } + + boolean isScalar(); + + default String serializeToText() { + PhysicalPlanSerializer serializer = new PhysicalPlanSerializer(); + serializer(serializer); + return serializer.getSerializedPlan(); + } + + default void serializer(PhysicalPlanSerializer serializer) { + serializer.append(this.getClass().getSimpleName()); + serializer.append("\n"); + } + + CompletableFuture execute() throws Exception; +} diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/LiteralQueryStep.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/LiteralQueryStep.java similarity index 85% rename from server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/LiteralQueryStep.java rename to server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/LiteralQueryStep.java index 277193693f..9fc1c382c5 100644 --- a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/LiteralQueryStep.java +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/LiteralQueryStep.java @@ -14,19 +14,18 @@ * limitations under the License. */ -package org.bithon.server.metric.expression.pipeline.step; +package org.bithon.server.datasource.query.plan.physical; import org.bithon.component.commons.expression.IDataType; import org.bithon.component.commons.expression.LiteralExpression; import org.bithon.server.commons.time.TimeSpan; import org.bithon.server.datasource.query.Interval; -import org.bithon.server.datasource.query.pipeline.Column; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; -import org.bithon.server.datasource.query.pipeline.DoubleColumn; -import org.bithon.server.datasource.query.pipeline.IQueryStep; -import org.bithon.server.datasource.query.pipeline.LongColumn; -import org.bithon.server.datasource.query.pipeline.PipelineQueryResult; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.ColumnarTable; +import org.bithon.server.datasource.query.result.DoubleColumn; +import org.bithon.server.datasource.query.result.LongColumn; +import org.bithon.server.datasource.query.result.PipelineQueryResult; import java.util.Collections; import java.util.List; @@ -38,7 +37,7 @@ * @author frank.chen021@outlook.com * @date 4/4/25 9:36 pm */ -public class LiteralQueryStep implements IQueryStep { +public class LiteralQueryStep implements IPhysicalPlan { private final LiteralExpression expression; private final int size; @@ -60,6 +59,11 @@ public LiteralQueryStep(LiteralExpression expression, Interval interval) { } } + @Override + public void serializer(PhysicalPlanSerializer serializer) { + serializer.append(expression.serializeToText()); + } + @Override public boolean isScalar() { return size == 1; diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/MetricQueryStep.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/MetricQueryStep.java new file mode 100644 index 0000000000..3c6f8b6dc3 --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/MetricQueryStep.java @@ -0,0 +1,154 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.physical; + + +import org.bithon.component.commons.utils.CollectionUtils; +import org.bithon.component.commons.utils.HumanReadableDuration; +import org.bithon.server.commons.time.TimeSpan; +import org.bithon.server.datasource.query.Interval; +import org.bithon.server.datasource.query.result.PipelineQueryResult; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +/** + * @author frank.chen021@outlook.com + * @date 4/4/25 3:48 pm + */ +public class MetricQueryStep implements IPhysicalPlan { + private final String dataSource; + private final Interval interval; + private final String filterExpression; + private final Set groupBy; + private final HumanReadableDuration offset; + private final boolean isScalar; + + // Make sure the evaluation is executed ONLY ONCE when the expression is referenced multiple times + private volatile CompletableFuture cachedResponse; + + private MetricQueryStep(Builder builder) { + this.dataSource = builder.dataSource; + this.interval = builder.interval; + this.filterExpression = builder.filterExpression; + this.groupBy = builder.groupBy; + this.offset = builder.offset; + this.isScalar = computeIsScalar(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String dataSource; + private Interval interval; + private String filterExpression; + private Set groupBy; + private HumanReadableDuration offset; + + public Builder dataSource(String dataSource) { + this.dataSource = dataSource; + return this; + } + + public Builder interval(Interval interval) { + this.interval = interval; + return this; + } + + public Builder filterExpression(String filterExpression) { + this.filterExpression = filterExpression; + return this; + } + + public Builder groupBy(Set groupBy) { + this.groupBy = groupBy; + return this; + } + + public Builder offset(HumanReadableDuration offset) { + this.offset = offset; + return this; + } + + public MetricQueryStep build() { + return new MetricQueryStep(this); + } + } + + private boolean computeIsScalar() { + if (CollectionUtils.isNotEmpty(groupBy)) { + // If GROUP-BY is not empty, obviously it is not a scalar because the result set contains multiple rows for different groups + return false; + } + + TimeSpan start = interval.getStartTime(); + TimeSpan end = interval.getEndTime(); + long intervalLength = (end.getMilliseconds() - start.getMilliseconds()) / 1000; + return interval.getStep() != null && interval.getStep().getSeconds() == intervalLength; + } + + @Override + public boolean isScalar() { + return isScalar; + } + + @Override + public CompletableFuture execute() { + if (cachedResponse == null) { + synchronized (this) { + if (cachedResponse == null) { + cachedResponse = CompletableFuture.supplyAsync(() -> { + /*try { + + QueryRequest queryRequest = QueryRequest.builder() + .dataSource(dataSource) + .interval(interval) + .filterExpression(filterExpression) + .fields(fields) + .groupBy(groupBy) + .offset(offset) + .build(); + + ColumnarTable columnTable = dataSourceApi.timeseriesV5(queryRequest); + + List keys = new ArrayList<>(); + keys.add(TimestampSpec.COLUMN_ALIAS); + keys.addAll(groupBy != null ? groupBy : Collections.emptySet()); + + List valNames = fields.stream() + .map(QueryField::getName) + .toList(); + + return PipelineQueryResult.builder() + .rows(columnTable.rowCount()) + .keyColumns(keys) + .valColumns(valNames) + .table(columnTable) + .build(); + } catch (IOException e) { + throw new RuntimeException(e); + }*/ + return null; + }); + } + } + } + return cachedResponse; + } +} diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/PhysicalPlanSerializer.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/PhysicalPlanSerializer.java new file mode 100644 index 0000000000..58d2c50843 --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/PhysicalPlanSerializer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.physical; + + +/** + * @author frank.chen021@outlook.com + * @date 11/6/25 4:20 pm + */ +public class PhysicalPlanSerializer { + private final StringBuilder builder = new StringBuilder(128); + + public PhysicalPlanSerializer append(String text) { + builder.append(text); + return this; + } + + public PhysicalPlanSerializer append(String indent, String text) { + for (String line : text.split("\\r?\\n")) { + builder.append(indent).append(line).append("\n"); + } + return this; + } + + public String getSerializedPlan() { + return builder.toString(); + } + + public PhysicalPlanSerializer append(char c) { + builder.append(c); + return this; + } +} diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/SlidingWindowAggregateStep.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/SlidingWindowAggregateStep.java new file mode 100644 index 0000000000..15fc7788b1 --- /dev/null +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/plan/physical/SlidingWindowAggregateStep.java @@ -0,0 +1,199 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.datasource.query.plan.physical; + + +import lombok.extern.slf4j.Slf4j; +import org.bithon.server.datasource.TimestampSpec; +import org.bithon.server.datasource.query.Interval; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.ColumnarTable; +import org.bithon.server.datasource.query.result.PipelineQueryResult; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * @author frank.chen021@outlook.com + * @date 5/5/25 6:20 pm + */ +@Slf4j +public class SlidingWindowAggregateStep implements IPhysicalPlan { + private final String tsField; + private final List keyFields; + private final List valueFields; + private final Duration window; + private final List resultFields; + private final Interval interval; + private final IPhysicalPlan source; + + public SlidingWindowAggregateStep(String tsField, + List keyFields, + List valueFields, + List resultFields, + Duration window, + Interval interval, + IPhysicalPlan source) { + this.tsField = tsField; + this.keyFields = keyFields; + this.valueFields = valueFields; + this.resultFields = resultFields; + this.window = window; + this.interval = interval; + this.source = source; + } + + @Override + public String toString() { + return "SlidingWindowAggregationStep{" + + "\n\ttsField='" + tsField + '\'' + + "\n\tkeyFields=" + keyFields + + "\n\tvalueFields=" + valueFields + + "\n\twindow=" + window + + "\n\tresultFields=" + resultFields + + "\n\tinterval=" + interval + + '}'; + } + + @Override + public boolean isScalar() { + return false; + } + + @Override + public CompletableFuture execute() throws Exception { + return source.execute() + .thenApply((result) -> { + ColumnarTable resultTable = result.getTable(); + + log.info("Applying sliding window aggregation: \n {}", this.toString()); + + // window aggregation + resultTable = doAggregate(resultTable, + this.tsField, + this.keyFields, + this.window, + this.valueFields); + + // + // The query interval is extended to include the first record of the sliding window + // which is not included in the result set. So we need to filter out some records record + long startingPoint = this.interval.getStartTime() + .floor(interval.getStep()) + .getSeconds(); + + Column tsColumn = resultTable.getColumn(TimestampSpec.COLUMN_ALIAS); + + int selectionIndex = 0; + int[] selections = new int[resultTable.rowCount()]; + for (int i = 0, rowCount = resultTable.rowCount(); i < rowCount; i++) { + long ts = tsColumn.getLong(i); + if (ts >= startingPoint) { + selections[selectionIndex++] = i; + } + } + + resultTable = resultTable.view(selections, selectionIndex); + return PipelineQueryResult.builder() + .table(resultTable) + .rows(resultTable.rowCount()) + .keyColumns(result.getKeyColumns()) + .valColumns(result.getValColumns()) + .build(); + }); + } + + /** + * Computes a moving window sum over a list of sorted time series records. + * + *

Each input row is expected to contain: + *

    + *
  • A numeric "timestamp" field (in seconds)
  • + *
  • A numeric value field indicated by valueField parameter
  • + *
  • One or more grouping key fields (e.g., "clusterName")
  • + *
+ * + *

The input list must be pre-sorted by the grouping keys (in order) and then by timestamp. + * For each row, this method computes the sum of all "value" fields from rows with the same group key + * whose timestamps fall within the range {@code [current.timestamp - windowSeconds, current.timestamp]}. + * + *

The result is a new list of maps, each containing all original key-value pairs from the input row, + * plus an additional entry valueField, holding the computed sum. + * + * @param tsField The name of field where timestamp is stored (e.g., "_timestamp") + * @param keys List of key field names used for grouping (e.g., ["clusterName"]) + * @param window Size of the time window in seconds + */ + public static ColumnarTable doAggregate(ColumnarTable table, + String tsField, + List keys, + Duration window, + List inputFields) { + Column tsColumn = table.getColumn(tsField); + + List keyColumns = table.getColumns(keys); + + int rowCount = table.rowCount(); + List inputColumns = new ArrayList<>(inputFields.size()); + List outputColumns = new ArrayList<>(inputFields.size()); + for (String valueField : inputFields) { + Column valColumn = table.getColumn(valueField); + inputColumns.add(valColumn); + outputColumns.add(Column.create(valueField, valColumn.getDataType(), rowCount)); + } + + CompositeKey prevKey = rowCount > 0 ? CompositeKey.from(keyColumns, 0) : null; + + // The starting window index of a group + int windowStart = 0; + double[] sums = new double[inputFields.size()]; + + for (int i = 0; i < rowCount; i++) { + long ts = tsColumn.getLong(i); + CompositeKey currKey = CompositeKey.from(keyColumns, i); + + if (!currKey.equals(prevKey)) { + // Reset window if group changes + windowStart = i; + Arrays.fill(sums, 0.0); + prevKey = currKey; + } + + // Slide window start to maintain [ts - window, ts] + while (windowStart < i && tsColumn.getLong(windowStart) <= ts - window.getSeconds()) { + for (int j = 0; j < inputColumns.size(); j++) { + sums[j] -= inputColumns.get(j).getDouble(windowStart); + } + windowStart++; + } + + for (int j = 0; j < inputColumns.size(); j++) { + sums[j] += inputColumns.get(j).getDouble(i); + outputColumns.get(j).addObject(sums[j]); + } + } + + ColumnarTable resultTable = new ColumnarTable(); + resultTable.addColumns(keyColumns); + resultTable.addColumn(tsColumn); + resultTable.addColumns(outputColumns); + return resultTable; + } +} diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/Column.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/Column.java similarity index 95% rename from server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/Column.java rename to server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/Column.java index 8ecf9402cc..ba197c346a 100644 --- a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/Column.java +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/Column.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.bithon.server.datasource.query.pipeline; +package org.bithon.server.datasource.query.result; import org.bithon.component.commons.expression.IDataType; @@ -79,7 +79,7 @@ static Column create(String name, String type, int initCapacity) { return new DoubleColumn(name, initCapacity); } if (type.equals(IDataType.DATETIME_MILLI.name())) { - // DATETIME_MILLI is a long + // DATETIME_MILLI is type of LONG at storage layer return new LongColumn(name, initCapacity); } throw new IllegalArgumentException("Unsupported column type: " + type); diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/ColumnarTable.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/ColumnarTable.java similarity index 98% rename from server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/ColumnarTable.java rename to server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/ColumnarTable.java index 580344a7ec..9c30ea1306 100644 --- a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/ColumnarTable.java +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/ColumnarTable.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.bithon.server.datasource.query.pipeline; +package org.bithon.server.datasource.query.result; import java.util.ArrayList; diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/DoubleColumn.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/DoubleColumn.java similarity index 98% rename from server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/DoubleColumn.java rename to server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/DoubleColumn.java index d0e031b68d..b758110f40 100644 --- a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/DoubleColumn.java +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/DoubleColumn.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.bithon.server.datasource.query.pipeline; +package org.bithon.server.datasource.query.result; import org.bithon.component.commons.expression.IDataType; diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/LongColumn.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/LongColumn.java similarity index 95% rename from server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/LongColumn.java rename to server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/LongColumn.java index 26dfa6872e..3c669b2871 100644 --- a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/LongColumn.java +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/LongColumn.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.bithon.server.datasource.query.pipeline; +package org.bithon.server.datasource.query.result; import org.bithon.component.commons.expression.IDataType; @@ -113,7 +113,7 @@ public int size() { @Override public Column filter(BitSet keep) { - org.bithon.server.datasource.query.pipeline.LongColumn filtered = new org.bithon.server.datasource.query.pipeline.LongColumn(this.name, keep.cardinality()); + LongColumn filtered = new LongColumn(this.name, keep.cardinality()); for (int i = 0; i < this.size; i++) { if (keep.get(i)) { filtered.addLong(this.data[i]); diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/PipelineQueryResult.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/PipelineQueryResult.java similarity index 95% rename from server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/PipelineQueryResult.java rename to server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/PipelineQueryResult.java index 9e6892b03d..5659761b05 100644 --- a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/PipelineQueryResult.java +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/PipelineQueryResult.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.bithon.server.datasource.query.pipeline; +package org.bithon.server.datasource.query.result; import lombok.AllArgsConstructor; diff --git a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/StringColumn.java b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/StringColumn.java similarity index 95% rename from server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/StringColumn.java rename to server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/StringColumn.java index d761d2b230..6a4b6b8b40 100644 --- a/server/datasource/common/src/main/java/org/bithon/server/datasource/query/pipeline/StringColumn.java +++ b/server/datasource/common/src/main/java/org/bithon/server/datasource/query/result/StringColumn.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.bithon.server.datasource.query.pipeline; +package org.bithon.server.datasource.query.result; import org.bithon.component.commons.expression.IDataType; @@ -125,7 +125,7 @@ public int size() { @Override public Column filter(BitSet keep) { - org.bithon.server.datasource.query.pipeline.StringColumn filtered = new org.bithon.server.datasource.query.pipeline.StringColumn(this.name, keep.cardinality()); + StringColumn filtered = new StringColumn(this.name, keep.cardinality()); for (int i = 0; i < this.size; i++) { if (keep.get(i)) { filtered.addString(this.data[i]); diff --git a/server/datasource/common/src/test/java/org/bithon/server/datasource/query/pipeline/DoubleColumnTest.java b/server/datasource/common/src/test/java/org/bithon/server/datasource/query/pipeline/DoubleColumnTest.java index e919f141f6..20046b8054 100644 --- a/server/datasource/common/src/test/java/org/bithon/server/datasource/query/pipeline/DoubleColumnTest.java +++ b/server/datasource/common/src/test/java/org/bithon/server/datasource/query/pipeline/DoubleColumnTest.java @@ -16,6 +16,8 @@ package org.bithon.server.datasource.query.pipeline; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.DoubleColumn; import org.junit.jupiter.api.Test; import java.util.BitSet; diff --git a/server/datasource/common/src/test/java/org/bithon/server/datasource/query/pipeline/LongColumnTest.java b/server/datasource/common/src/test/java/org/bithon/server/datasource/query/pipeline/LongColumnTest.java index a8ab59c314..e9a1607bf9 100644 --- a/server/datasource/common/src/test/java/org/bithon/server/datasource/query/pipeline/LongColumnTest.java +++ b/server/datasource/common/src/test/java/org/bithon/server/datasource/query/pipeline/LongColumnTest.java @@ -16,6 +16,8 @@ package org.bithon.server.datasource.query.pipeline; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.LongColumn; import org.junit.jupiter.api.Test; import java.util.BitSet; diff --git a/server/datasource/common/src/test/java/org/bithon/server/datasource/query/pipeline/StringColumnTest.java b/server/datasource/common/src/test/java/org/bithon/server/datasource/query/pipeline/StringColumnTest.java index 0f9eb9551c..88f63738bf 100644 --- a/server/datasource/common/src/test/java/org/bithon/server/datasource/query/pipeline/StringColumnTest.java +++ b/server/datasource/common/src/test/java/org/bithon/server/datasource/query/pipeline/StringColumnTest.java @@ -16,6 +16,8 @@ package org.bithon.server.datasource.query.pipeline; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.StringColumn; import org.junit.jupiter.api.Test; import java.util.BitSet; diff --git a/server/metric-expression/src/test/java/org/bithon/server/metric/expression/pipeline/step/LiteralQueryStepTest.java b/server/datasource/common/src/test/java/org/bithon/server/datasource/query/plan/physical/LiteralQueryStepTest.java similarity index 95% rename from server/metric-expression/src/test/java/org/bithon/server/metric/expression/pipeline/step/LiteralQueryStepTest.java rename to server/datasource/common/src/test/java/org/bithon/server/datasource/query/plan/physical/LiteralQueryStepTest.java index 287cc35c44..939dd10b81 100644 --- a/server/metric-expression/src/test/java/org/bithon/server/metric/expression/pipeline/step/LiteralQueryStepTest.java +++ b/server/datasource/common/src/test/java/org/bithon/server/datasource/query/plan/physical/LiteralQueryStepTest.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.bithon.server.metric.expression.pipeline.step; +package org.bithon.server.datasource.query.plan.physical; import org.bithon.component.commons.expression.IDataType; import org.bithon.component.commons.expression.LiteralExpression; import org.bithon.server.commons.time.TimeSpan; import org.bithon.server.datasource.query.Interval; -import org.bithon.server.datasource.query.pipeline.PipelineQueryResult; +import org.bithon.server.datasource.query.result.PipelineQueryResult; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/server/datasource/reader-jdbc/src/test/java/org/bithon/server/datasource/reader/jdbc/SlidingWindowAggregatorTest.java b/server/datasource/common/src/test/java/org/bithon/server/datasource/query/plan/physical/SlidingWindowAggregateStepTest.java similarity index 84% rename from server/datasource/reader-jdbc/src/test/java/org/bithon/server/datasource/reader/jdbc/SlidingWindowAggregatorTest.java rename to server/datasource/common/src/test/java/org/bithon/server/datasource/query/plan/physical/SlidingWindowAggregateStepTest.java index 78f7d097a8..36c204db57 100644 --- a/server/datasource/reader-jdbc/src/test/java/org/bithon/server/datasource/reader/jdbc/SlidingWindowAggregatorTest.java +++ b/server/datasource/common/src/test/java/org/bithon/server/datasource/query/plan/physical/SlidingWindowAggregateStepTest.java @@ -14,12 +14,11 @@ * limitations under the License. */ -package org.bithon.server.datasource.reader.jdbc; +package org.bithon.server.datasource.query.plan.physical; -import org.bithon.server.datasource.query.pipeline.Column; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; -import org.bithon.server.datasource.reader.jdbc.pipeline.SlidingWindowAggregator; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.ColumnarTable; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -30,7 +29,7 @@ * @author frank.chen021@outlook.com * @date 5/5/25 3:03 pm */ -public class SlidingWindowAggregatorTest { +public class SlidingWindowAggregateStepTest { @Test void testAggregate_NoGroup_OneRow() { @@ -44,7 +43,7 @@ void testAggregate_NoGroup_OneRow() { // Add rows using addRow table.addRow(1L, 10.0); - table = SlidingWindowAggregator.aggregate(table, "_timestamp", List.of(), Duration.ofSeconds(2), List.of("value")); + table = SlidingWindowAggregateStep.doAggregate(table, "_timestamp", List.of(), Duration.ofSeconds(2), List.of("value")); Column agg = table.getColumn("value"); Assertions.assertEquals(1, table.rowCount()); @@ -65,7 +64,7 @@ void testAggregate_NoGroup() { table.addRow(2L, 20.0); table.addRow(3L, 30.0); - table = SlidingWindowAggregator.aggregate(table, "_timestamp", List.of(), Duration.ofSeconds(2), List.of("value")); + table = SlidingWindowAggregateStep.doAggregate(table, "_timestamp", List.of(), Duration.ofSeconds(2), List.of("value")); Column agg = table.getColumn("value"); Assertions.assertEquals(3, table.rowCount()); @@ -90,7 +89,7 @@ void testAggregate_SingleGroup() { table.addRow(2L, 20.0, "A"); table.addRow(3L, 30.0, "A"); - table = SlidingWindowAggregator.aggregate(table, "_timestamp", List.of("group"), Duration.ofSeconds(2), List.of("value")); + table = SlidingWindowAggregateStep.doAggregate(table, "_timestamp", List.of("group"), Duration.ofSeconds(2), List.of("value")); Column agg = table.getColumn("value"); Assertions.assertEquals(3, table.rowCount()); @@ -118,7 +117,7 @@ void testAggregate_MultipleGroups() { table.addRow(2L, 200.0, "B"); table.addRow(3L, 300.0, "B"); - table = SlidingWindowAggregator.aggregate(table, "_timestamp", List.of("group"), Duration.ofSeconds(2), List.of("value")); + table = SlidingWindowAggregateStep.doAggregate(table, "_timestamp", List.of("group"), Duration.ofSeconds(2), List.of("value")); Column agg = table.getColumn("value"); Assertions.assertEquals(6, table.rowCount()); @@ -146,7 +145,7 @@ void testAggregate_WindowEviction() { table.addRow(5L, 2.0, "A"); table.addRow(10L, 3.0, "A"); - table = SlidingWindowAggregator.aggregate(table, "_timestamp", List.of("group"), Duration.ofSeconds(3), List.of("value")); + table = SlidingWindowAggregateStep.doAggregate(table, "_timestamp", List.of("group"), Duration.ofSeconds(3), List.of("value")); Column agg = table.getColumn("value"); Assertions.assertEquals(3, table.rowCount()); @@ -172,7 +171,7 @@ void testAggregate_WindowEviction_2() { table.addRow(4L, 3.0, "A"); table.addRow(6L, 4.0, "A"); - table = SlidingWindowAggregator.aggregate(table, "_timestamp", List.of("group"), Duration.ofSeconds(3), List.of("value")); + table = SlidingWindowAggregateStep.doAggregate(table, "_timestamp", List.of("group"), Duration.ofSeconds(3), List.of("value")); Column agg = table.getColumn("value"); Assertions.assertEquals(4, table.rowCount()); @@ -207,11 +206,11 @@ void testAggregate_MultipleInputFields() { table.addRow(2L, 15.0, 150.0, "B"); - table = SlidingWindowAggregator.aggregate(table, - "_timestamp", - List.of("group"), - Duration.ofSeconds(2), - List.of("value1", "value2")); + table = SlidingWindowAggregateStep.doAggregate(table, + "_timestamp", + List.of("group"), + Duration.ofSeconds(2), + List.of("value1", "value2")); Column agg1 = table.getColumn("value1"); Column agg2 = table.getColumn("value2"); diff --git a/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/JdbcDataSourceReader.java b/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/JdbcDataSourceReader.java index f5745ad1f1..a446c410cc 100644 --- a/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/JdbcDataSourceReader.java +++ b/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/JdbcDataSourceReader.java @@ -31,11 +31,13 @@ import org.bithon.server.datasource.query.OrderBy; import org.bithon.server.datasource.query.Query; import org.bithon.server.datasource.query.ast.Selector; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; -import org.bithon.server.datasource.query.pipeline.IQueryStep; +import org.bithon.server.datasource.query.plan.logical.LogicalTableScan; +import org.bithon.server.datasource.query.plan.physical.IPhysicalPlan; +import org.bithon.server.datasource.query.result.ColumnarTable; import org.bithon.server.datasource.query.setting.QuerySettings; import org.bithon.server.datasource.reader.jdbc.dialect.ISqlDialect; import org.bithon.server.datasource.reader.jdbc.pipeline.JdbcPipelineBuilder; +import org.bithon.server.datasource.reader.jdbc.pipeline.JdbcReadStep; import org.bithon.server.datasource.reader.jdbc.statement.ast.LimitClause; import org.bithon.server.datasource.reader.jdbc.statement.ast.OrderByClause; import org.bithon.server.datasource.reader.jdbc.statement.ast.SelectStatement; @@ -52,6 +54,7 @@ import org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration; import org.springframework.boot.autoconfigure.jooq.JooqProperties; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -114,6 +117,28 @@ public JdbcDataSourceReader(DSLContext dslContext, ISqlDialect sqlDialect, Query this.shouldCloseContext = false; } + @Override + public IPhysicalPlan plan(LogicalTableScan tableScan, Interval interval) { + SelectStatement selectStatement = new SelectStatement(); + selectStatement.getFrom().setExpression(new TableIdentifier(tableScan.table().getName())); + selectStatement.getFrom().setAlias(tableScan.table().getName()); + selectStatement.getWhere().and( + new ComparisonExpression.GTE(interval.getTimestampColumn(), sqlDialect.toISO8601TimestampExpression(interval.getStartTime())), + new ComparisonExpression.LT(interval.getTimestampColumn(), sqlDialect.toISO8601TimestampExpression(interval.getEndTime())), + tableScan.filter()); + selectStatement.getSelectorList().addAll(tableScan.selectorList()); + + return new JdbcReadStep( + dslContext, + tableScan.table(), + sqlDialect, + selectStatement, + interval, + tableScan.selectorList().stream().map(Selector::getOutputName).collect(Collectors.toList()), + Collections.emptyList() + ); + } + @Override public ColumnarTable timeseries(Query query) { SelectStatementBuilder statementBuilder = SelectStatementBuilder.builder() @@ -130,16 +155,17 @@ public ColumnarTable timeseries(Query query) { Interval interval = query.getInterval(); - IQueryStep queryStep = JdbcPipelineBuilder.builder() - .dslContext(dslContext) - .dialect(this.sqlDialect) - .selectStatement(selectStatement) - .interval(Interval.of(interval.getStartTime().floor(query.getInterval().getStep()), - interval.getEndTime(), - interval.getStep(), - null, - null)) - .build(); + IPhysicalPlan queryStep = JdbcPipelineBuilder.builder() + .dslContext(dslContext) + .dialect(this.sqlDialect) + .schema(query.getSchema()) + .selectStatement(selectStatement) + .interval(Interval.of(interval.getStartTime().floor(query.getInterval().getStep()), + interval.getEndTime(), + interval.getStep(), + null, + null)) + .build(); try { return queryStep.execute() diff --git a/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/pipeline/JdbcPipelineBuilder.java b/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/pipeline/JdbcPipelineBuilder.java index 1a6c8ddb20..a8c00f46e7 100644 --- a/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/pipeline/JdbcPipelineBuilder.java +++ b/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/pipeline/JdbcPipelineBuilder.java @@ -20,11 +20,13 @@ import org.bithon.component.commons.expression.IdentifierExpression; import org.bithon.component.commons.expression.LiteralExpression; import org.bithon.component.commons.utils.CollectionUtils; +import org.bithon.server.datasource.ISchema; import org.bithon.server.datasource.query.Interval; import org.bithon.server.datasource.query.Order; import org.bithon.server.datasource.query.ast.ExpressionNode; import org.bithon.server.datasource.query.ast.Selector; -import org.bithon.server.datasource.query.pipeline.IQueryStep; +import org.bithon.server.datasource.query.plan.physical.IPhysicalPlan; +import org.bithon.server.datasource.query.plan.physical.SlidingWindowAggregateStep; import org.bithon.server.datasource.reader.jdbc.dialect.ISqlDialect; import org.bithon.server.datasource.reader.jdbc.statement.ast.OrderByClause; import org.bithon.server.datasource.reader.jdbc.statement.ast.SelectStatement; @@ -46,6 +48,7 @@ public class JdbcPipelineBuilder { private ISqlDialect dialect; private SelectStatement selectStatement; private Interval interval; + private ISchema schema; private JdbcPipelineBuilder() { } @@ -69,13 +72,17 @@ public JdbcPipelineBuilder selectStatement(SelectStatement selectStatement) { return this; } + public JdbcPipelineBuilder schema(ISchema schema) { + this.schema = schema; + return this; + } public JdbcPipelineBuilder interval(Interval interval) { this.interval = interval; return this; } - public IQueryStep build() { + public IPhysicalPlan build() { List windowFunctionSelectors = new ArrayList<>(); List inputColumns = new ArrayList<>(); List outputColumns = new ArrayList<>(); @@ -93,18 +100,24 @@ public IQueryStep build() { } if (windowFunctionSelectors.isEmpty()) { - return new JdbcReadStep(dslContext, dialect, selectStatement, false); + return new JdbcReadStep(dslContext, + schema, + dialect, + selectStatement, + this.interval, + // TODO: Fixme: inputColumns and outputColumns are empty, should we throw an exception? + Collections.emptyList(), + Collections.emptyList()); } WindowFunctionExpression windowFunctionExpression = (WindowFunctionExpression) ((ExpressionNode) windowFunctionSelectors.get(0).getSelectExpression()).getParsedExpression(); LiteralExpression.LongLiteral window = (LiteralExpression.LongLiteral) windowFunctionExpression.getFrame().getStart(); IdentifierExpression orderBy = (IdentifierExpression) windowFunctionExpression.getOrderBy()[0].getName(); - List keys = CollectionUtils.isEmpty(windowFunctionExpression.getPartitionBy()) ? - Collections.emptyList() - : Arrays.stream(windowFunctionExpression.getPartitionBy()) - .map((expr) -> ((IdentifierExpression) expr).getIdentifier()) - .toList(); + List keys = CollectionUtils.isEmpty(windowFunctionExpression.getPartitionBy()) ? Collections.emptyList() + : Arrays.stream(windowFunctionExpression.getPartitionBy()) + .map((expr) -> ((IdentifierExpression) expr).getIdentifier()) + .toList(); SelectStatement subQuery = (SelectStatement) selectStatement.getFrom().getExpression(); @@ -116,17 +129,22 @@ public IQueryStep build() { .toList() .toArray(new OrderByClause[0])); - IQueryStep readStep = new JdbcReadStep(dslContext, - dialect, - subQuery, - false); - - return new SlidingWindowAggregationStep(orderBy.getIdentifier(), - keys, - inputColumns, - outputColumns, - Duration.ofSeconds(window.getValue()), - this.interval, - readStep); + IPhysicalPlan readStep = new JdbcReadStep(dslContext, + schema, + dialect, + subQuery, + interval, + // TODO: Fixme: inputColumns and outputColumns are empty, should we throw an exception? + Collections.emptyList(), + Collections.emptyList() + ); + + return new SlidingWindowAggregateStep(orderBy.getIdentifier(), + keys, + inputColumns, + outputColumns, + Duration.ofSeconds(window.getValue()), + this.interval, + readStep); } } diff --git a/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/pipeline/JdbcReadStep.java b/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/pipeline/JdbcReadStep.java index d7639624e5..ecf8b6f6d3 100644 --- a/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/pipeline/JdbcReadStep.java +++ b/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/pipeline/JdbcReadStep.java @@ -17,18 +17,30 @@ package org.bithon.server.datasource.reader.jdbc.pipeline; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.bithon.component.commons.expression.LogicalExpression; +import org.bithon.component.commons.utils.HumanReadableDuration; +import org.bithon.component.commons.utils.StringUtils; +import org.bithon.server.datasource.ISchema; +import org.bithon.server.datasource.TimestampSpec; +import org.bithon.server.datasource.query.Interval; +import org.bithon.server.datasource.query.ast.ExpressionNode; import org.bithon.server.datasource.query.ast.Selector; -import org.bithon.server.datasource.query.pipeline.Column; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; -import org.bithon.server.datasource.query.pipeline.IQueryStep; -import org.bithon.server.datasource.query.pipeline.PipelineQueryResult; +import org.bithon.server.datasource.query.plan.logical.LogicalAggregate; +import org.bithon.server.datasource.query.plan.physical.IPhysicalPlan; +import org.bithon.server.datasource.query.plan.physical.PhysicalPlanSerializer; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.ColumnarTable; +import org.bithon.server.datasource.query.result.PipelineQueryResult; import org.bithon.server.datasource.reader.jdbc.dialect.ISqlDialect; import org.bithon.server.datasource.reader.jdbc.statement.ast.SelectStatement; +import org.bithon.server.datasource.reader.jdbc.statement.builder.SelectStatementBuilder; import org.jooq.Cursor; import org.jooq.DSLContext; import org.jooq.Record; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -37,22 +49,78 @@ * @date 5/5/25 6:14 pm */ @Slf4j -public class JdbcReadStep implements IQueryStep { +public class JdbcReadStep implements IPhysicalPlan { private final DSLContext dslContext; - private final SelectStatement selectStatement; private final String sql; - private final boolean isScalar; + + @Getter + private final SelectStatement selectStatement; + private final ISqlDialect sqlDialect; + private final ISchema schema; + private final Interval interval; + private final List keyColumns; + private final List valColumns; + private final HumanReadableDuration offset; public JdbcReadStep(DSLContext dslContext, + ISchema schema, ISqlDialect sqlDialect, SelectStatement selectStatement, - boolean isScalar) { + Interval interval, + List keyColumns, + List valueColumns + ) { this.dslContext = dslContext; + this.schema = schema; this.selectStatement = selectStatement; - this.isScalar = isScalar; + this.interval = interval; + this.sqlDialect = sqlDialect; + this.sql = selectStatement.toSQL(sqlDialect); + this.keyColumns = keyColumns; + this.valColumns = valueColumns; + this.offset = null; + } + public JdbcReadStep(DSLContext dslContext, + ISchema schema, + ISqlDialect sqlDialect, + SelectStatement selectStatement, + Interval interval, + List keyColumns, + List valueColumns, + HumanReadableDuration offset + ) { + this.dslContext = dslContext; + this.schema = schema; + this.selectStatement = selectStatement; + this.interval = interval; + this.sqlDialect = sqlDialect; this.sql = selectStatement.toSQL(sqlDialect); + this.keyColumns = keyColumns; + this.valColumns = valueColumns; + this.offset = offset; + } + + @Override + public IPhysicalPlan offset(HumanReadableDuration offset) { + return new JdbcReadStep( + this.dslContext, + this.schema, + this.sqlDialect, + this.selectStatement, + this.interval, + this.keyColumns, + this.valColumns, + this.offset + ); + } + + @Override + public void serializer(PhysicalPlanSerializer serializer) { + String stepName = this.getClass().getSimpleName(); + serializer.append(stepName).append('\n'); + serializer.append(" ", this.sql); } @Override @@ -62,7 +130,41 @@ public String toString() { @Override public boolean isScalar() { - return this.isScalar; + return false; + } + + @Override + public boolean canPushDownAggregate(LogicalAggregate aggregate) { + return true; + } + + @Override + public IPhysicalPlan pushDownAggregate(LogicalAggregate aggregate) { + SelectStatementBuilder builder = new SelectStatementBuilder(); + SelectStatement select = builder.sqlDialect(this.sqlDialect) + .schema(this.schema) + .interval(this.interval) + .fields(List.of(new Selector(new ExpressionNode(this.schema, + StringUtils.format("%s(%s)", aggregate.func(), aggregate.field().getName())), + aggregate.field().getName(), + aggregate.field().getDataType()))) + .filter(LogicalExpression.create(LogicalExpression.AND.OP, selectStatement.getWhere().getExpressions())) + .groupBy(aggregate.groupBy()) + .build(); + + List keys = new ArrayList<>(aggregate.groupBy()); + if (interval.getStep() != null) { + // a time series query + keys.add(TimestampSpec.COLUMN_ALIAS); + } + + return new JdbcReadStep(dslContext, + schema, + sqlDialect, + select, + interval, + keys, + List.of(aggregate.field().getName())); } @Override @@ -70,7 +172,7 @@ public CompletableFuture execute() throws Exception { return CompletableFuture.supplyAsync(() -> { ColumnarTable resultTable = new ColumnarTable(); for (Selector selector : selectStatement.getSelectorList().getSelectors()) { - resultTable.addColumn(Column.create(selector.getOutputName(), selector.getDataType(), 1024)); + resultTable.addColumn(Column.create(selector.getOutputName(), selector.getDataType(), 256)); } List resultColumns = resultTable.getColumns(); @@ -83,7 +185,10 @@ public CompletableFuture execute() throws Exception { } } return PipelineQueryResult.builder() + .rows(resultTable.rowCount()) .table(resultTable) + .keyColumns(this.keyColumns) + .valColumns(this.valColumns) .build(); } }); diff --git a/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/pipeline/SlidingWindowAggregationStep.java b/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/pipeline/SlidingWindowAggregationStep.java deleted file mode 100644 index dea7be84b0..0000000000 --- a/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/pipeline/SlidingWindowAggregationStep.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2020 bithon.org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.bithon.server.datasource.reader.jdbc.pipeline; - - -import lombok.extern.slf4j.Slf4j; -import org.bithon.server.datasource.TimestampSpec; -import org.bithon.server.datasource.query.Interval; -import org.bithon.server.datasource.query.pipeline.Column; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; -import org.bithon.server.datasource.query.pipeline.IQueryStep; -import org.bithon.server.datasource.query.pipeline.PipelineQueryResult; - -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -/** - * @author frank.chen021@outlook.com - * @date 5/5/25 6:20 pm - */ -@Slf4j -public class SlidingWindowAggregationStep implements IQueryStep { - private final String tsField; - private final List keyFields; - private final List valueFields; - private final Duration window; - private final List resultFields; - private final Interval interval; - private final IQueryStep source; - - public SlidingWindowAggregationStep(String tsField, - List keyFields, - List valueFields, - List resultFields, - Duration window, - Interval interval, - IQueryStep source) { - this.tsField = tsField; - this.keyFields = keyFields; - this.valueFields = valueFields; - this.resultFields = resultFields; - this.window = window; - this.interval = interval; - this.source = source; - } - - @Override - public String toString() { - return "SlidingWindowAggregationStep{" + - "\n\ttsField='" + tsField + '\'' + - "\n\tkeyFields=" + keyFields + - "\n\tvalueFields=" + valueFields + - "\n\twindow=" + window + - "\n\tresultFields=" + resultFields + - "\n\tinterval=" + interval + - '}'; - } - - @Override - public boolean isScalar() { - return false; - } - - @Override - public CompletableFuture execute() throws Exception { - return source.execute() - .thenApply((result) -> { - ColumnarTable resultTable = result.getTable(); - - log.info("Applying sliding window aggregation: \n {}", this.toString()); - - // window aggregation - resultTable = SlidingWindowAggregator.aggregate(resultTable, - this.tsField, - this.keyFields, - this.window, - this.valueFields); - - // - // The query interval is extended to include the first record of the sliding window - // which is not included in the result set. So we need to filter out some records record - long startingPoint = this.interval.getStartTime() - .floor(interval.getStep()) - .getSeconds(); - - Column tsColumn = resultTable.getColumn(TimestampSpec.COLUMN_ALIAS); - - int selectionIndex = 0; - int[] selections = new int[resultTable.rowCount()]; - for (int i = 0, rowCount = resultTable.rowCount(); i < rowCount; i++) { - long ts = tsColumn.getLong(i); - if (ts >= startingPoint) { - selections[selectionIndex++] = i; - } - } - - resultTable = resultTable.view(selections, selectionIndex); - return PipelineQueryResult.builder() - .table(resultTable) - .rows(resultTable.rowCount()) - .keyColumns(result.getKeyColumns()) - .valColumns(result.getValColumns()) - .build(); - }); - } -} diff --git a/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/pipeline/SlidingWindowAggregator.java b/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/pipeline/SlidingWindowAggregator.java deleted file mode 100644 index e2e127eae0..0000000000 --- a/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/pipeline/SlidingWindowAggregator.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2020 bithon.org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.bithon.server.datasource.reader.jdbc.pipeline; - -import org.bithon.server.datasource.query.pipeline.Column; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; -import org.bithon.server.datasource.query.pipeline.CompositeKey; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - - -/** - * @author frank.chen021@outlook.com - * @date 4/5/25 1:00 pm - */ -public class SlidingWindowAggregator { - - /** - * Computes a moving window sum over a list of sorted time series records. - * - *

Each input row is expected to contain: - *

    - *
  • A numeric "timestamp" field (in seconds)
  • - *
  • A numeric value field indicated by valueField parameter
  • - *
  • One or more grouping key fields (e.g., "clusterName")
  • - *
- * - *

The input list must be pre-sorted by the grouping keys (in order) and then by timestamp. - * For each row, this method computes the sum of all "value" fields from rows with the same group key - * whose timestamps fall within the range {@code [current.timestamp - windowSeconds, current.timestamp]}. - * - *

The result is a new list of maps, each containing all original key-value pairs from the input row, - * plus an additional entry valueField, holding the computed sum. - * - * @param tsField The name of field where timestamp is stored (e.g., "_timestamp") - * @param keys List of key field names used for grouping (e.g., ["clusterName"]) - * @param window Size of the time window in seconds - */ - public static ColumnarTable aggregate(ColumnarTable table, - String tsField, - List keys, - Duration window, - List inputFields) { - Column tsColumn = table.getColumn(tsField); - - List keyColumns = table.getColumns(keys); - - int rowCount = table.rowCount(); - List inputColumns = new ArrayList<>(inputFields.size()); - List outputColumns = new ArrayList<>(inputFields.size()); - for (String valueField : inputFields) { - Column valColumn = table.getColumn(valueField); - inputColumns.add(valColumn); - outputColumns.add(Column.create(valueField, valColumn.getDataType(), rowCount)); - } - - CompositeKey prevKey = rowCount > 0 ? CompositeKey.from(keyColumns, 0) : null; - - // The starting window index of a group - int windowStart = 0; - double[] sums = new double[inputFields.size()]; - - for (int i = 0; i < rowCount; i++) { - long ts = tsColumn.getLong(i); - CompositeKey currKey = CompositeKey.from(keyColumns, i); - - if (!currKey.equals(prevKey)) { - // Reset window if group changes - windowStart = i; - Arrays.fill(sums, 0.0); - prevKey = currKey; - } - - // Slide window start to maintain [ts - window, ts] - while (windowStart < i && tsColumn.getLong(windowStart) <= ts - window.getSeconds()) { - for (int j = 0; j < inputColumns.size(); j++) { - sums[j] -= inputColumns.get(j).getDouble(windowStart); - } - windowStart++; - } - - for (int j = 0; j < inputColumns.size(); j++) { - sums[j] += inputColumns.get(j).getDouble(i); - outputColumns.get(j).addObject(sums[j]); - } - } - - ColumnarTable resultTable = new ColumnarTable(); - resultTable.addColumns(keyColumns); - resultTable.addColumn(tsColumn); - resultTable.addColumns(outputColumns); - return resultTable; - } -} diff --git a/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/statement/ast/SelectorList.java b/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/statement/ast/SelectorList.java index 888b328b43..9bfadf7177 100644 --- a/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/statement/ast/SelectorList.java +++ b/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/statement/ast/SelectorList.java @@ -86,6 +86,10 @@ public Selector add(IASTNode columnExpression, Alias output, IDataType dataType) return selector; } + public void addAll(List columns) { + selectors.addAll(columns); + } + public int size() { return selectors.size(); } diff --git a/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/statement/ast/WhereClause.java b/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/statement/ast/WhereClause.java index 5a76955e8e..3a6ae394e1 100644 --- a/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/statement/ast/WhereClause.java +++ b/server/datasource/reader-jdbc/src/main/java/org/bithon/server/datasource/reader/jdbc/statement/ast/WhereClause.java @@ -18,6 +18,7 @@ import lombok.Getter; import org.bithon.component.commons.expression.IExpression; +import org.bithon.component.commons.expression.LogicalExpression; import org.bithon.server.datasource.query.ast.IASTNode; import java.util.ArrayList; @@ -33,23 +34,44 @@ public class WhereClause implements IASTNode { private final List expressions = new ArrayList<>(); public WhereClause and(IExpression expression) { - if (expression != null) { + if (expression == null) { + return this; + } + + if (expression instanceof LogicalExpression.AND andExpression) { + // Flatten the AND expression + for (IExpression expr : andExpression.getOperands()) { + addExpression(expr); + } + } else { expressions.add(expression); } return this; } public WhereClause and(IExpression... expressions) { - if (expressions != null) { - for (IExpression expression : expressions) { - if (expression != null) { - this.expressions.add(expression); - } + if (expressions == null) { + return this; + } + + for (IExpression expression : expressions) { + if (expression == null) { + continue; } + + addExpression(expression); } return this; } + private void addExpression(IExpression expr) { + // Check if the expression already exists in the list + if (this.expressions.stream().anyMatch(e -> e.equals(expr))) { + return; + } + this.expressions.add(expr); + } + public boolean isEmpty() { return expressions.isEmpty(); } diff --git a/server/datasource/reader-vm/src/main/java/org/bithon/server/datasource/reader/vm/VMDataSourceReader.java b/server/datasource/reader-vm/src/main/java/org/bithon/server/datasource/reader/vm/VMDataSourceReader.java index b3b3f1a16a..de87e7e739 100644 --- a/server/datasource/reader-vm/src/main/java/org/bithon/server/datasource/reader/vm/VMDataSourceReader.java +++ b/server/datasource/reader-vm/src/main/java/org/bithon/server/datasource/reader/vm/VMDataSourceReader.java @@ -32,8 +32,8 @@ import org.bithon.server.datasource.query.Query; import org.bithon.server.datasource.query.ast.ExpressionNode; import org.bithon.server.datasource.query.ast.Selector; -import org.bithon.server.datasource.query.pipeline.Column; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.ColumnarTable; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; diff --git a/server/metric-expression/src/main/antlr4/org/bithon/server/metric/expression/MetricExpression.g4 b/server/metric-expression/src/main/antlr4/org/bithon/server/metric/expression/MetricExpression.g4 index b2686587d4..0ff12f45a5 100644 --- a/server/metric-expression/src/main/antlr4/org/bithon/server/metric/expression/MetricExpression.g4 +++ b/server/metric-expression/src/main/antlr4/org/bithon/server/metric/expression/MetricExpression.g4 @@ -9,12 +9,19 @@ alertExpression // sum by (a,b,c) (metric {}) metricExpression - : aggregatorExpression LEFT_PARENTHESIS metricQNameExpression labelExpression? RIGHT_PARENTHESIS durationExpression? groupByExpression? #metricAggregationExpression - | metricExpression metricPredicateExpression metricExpectedExpression #metricFilterExpression - | metricExpression (MUL|DIV) metricExpression #arithmeticExpression + : metricExpression (MUL|DIV) metricExpression #arithmeticExpression | metricExpression (ADD|SUB) metricExpression #arithmeticExpression | '(' metricExpression ')' #parenthesisMetricExpression - | numberLiteralExpression #metricLiteralExpression // This allows to use literal expression in arithmetic expression + | numberLiteralExpression #metricLiteralExpression // This allows to use literal expression in arithmetic expression + | metricSelectExpressionDecl #metricSelectExpression + | metricExpression metricPredicateExpression metricExpectedExpression #metricFilterExpression + // Legacy metric aggregation expression + | aggregatorExpression LEFT_PARENTHESIS metricSelectExpressionDecl RIGHT_PARENTHESIS durationExpression? groupByExpression? #metricAggregationExpression + | IDENTIFIER LEFT_PARENTHESIS metricExpression (',' metricExpression)* RIGHT_PARENTHESIS #functionCallExpression + ; + +metricSelectExpressionDecl + : metricQNameExpression labelExpression? durationExpression? ; aggregatorExpression diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/api/MetricQueryApi.java b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/api/MetricQueryApi.java index fe4da6273a..fcb97e799e 100644 --- a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/api/MetricQueryApi.java +++ b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/api/MetricQueryApi.java @@ -25,12 +25,12 @@ import lombok.NoArgsConstructor; import org.bithon.component.commons.Experimental; import org.bithon.server.commons.time.TimeSpan; -import org.bithon.server.datasource.query.pipeline.Column; -import org.bithon.server.datasource.query.pipeline.IQueryStep; -import org.bithon.server.datasource.query.pipeline.PipelineQueryResult; -import org.bithon.server.metric.expression.pipeline.QueryPipelineBuilder; +import org.bithon.server.datasource.query.plan.physical.IPhysicalPlan; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.PipelineQueryResult; +import org.bithon.server.metric.expression.plan.MetricExpressionPhysicalPlanner; +import org.bithon.server.storage.datasource.SchemaManager; import org.bithon.server.web.service.WebServiceModuleEnabler; -import org.bithon.server.web.service.datasource.api.IDataSourceApi; import org.bithon.server.web.service.datasource.api.IntervalRequest; import org.bithon.server.web.service.datasource.api.QueryResponse; import org.bithon.server.web.service.datasource.api.TimeSeriesMetric; @@ -56,10 +56,10 @@ @RestController @Conditional(WebServiceModuleEnabler.class) public class MetricQueryApi { - private final IDataSourceApi dataSourceApi; + private final SchemaManager schemaManager; - public MetricQueryApi(IDataSourceApi dataSourceApi) { - this.dataSourceApi = dataSourceApi; + public MetricQueryApi(SchemaManager schemaManager) { + this.schemaManager = schemaManager; } @Data @@ -87,11 +87,11 @@ public static class MetricQueryRequest { @Experimental @PostMapping("/api/metric/timeseries") public QueryResponse timeSeries(@Validated @RequestBody MetricQueryRequest request) throws Exception { - IQueryStep pipeline = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(request.getInterval()) - .condition(request.getCondition()) - .build(request.getExpression()); + IPhysicalPlan pipeline = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaManager) + .interval(request.getInterval()) + .condition(request.getCondition()) + .timeSeries(request.getExpression()); Duration step = request.getInterval().calculateStep(); TimeSpan start = request.getInterval().getStartISO8601(); diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/AggregatorEnum.java b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/AggregatorEnum.java index 2555b5b3d0..1d9ec598b8 100644 --- a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/AggregatorEnum.java +++ b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/AggregatorEnum.java @@ -69,4 +69,13 @@ public boolean isColumnSupported(IColumn column) { }; public abstract boolean isColumnSupported(IColumn column); + + public static AggregatorEnum fromString(String name) { + for (AggregatorEnum aggregator : values()) { + if (aggregator.name().equalsIgnoreCase(name)) { + return aggregator; + } + } + return null; + } } diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/IMetricExpressionVisitor.java b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/IMetricExpressionVisitor.java index 0298b5482c..381dc39452 100644 --- a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/IMetricExpressionVisitor.java +++ b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/IMetricExpressionVisitor.java @@ -24,5 +24,14 @@ * @date 4/4/25 3:55 pm */ public interface IMetricExpressionVisitor extends IExpressionVisitor { - T visit(MetricExpression expression); + default T visit(MetricSelectExpression expression) { + return null; + } + + default T visit(MetricExpectedExpression expression) { + return null; + } + + T visit(MetricAggregateExpression expression); + } diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricExpression.java b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricAggregateExpression.java similarity index 99% rename from server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricExpression.java rename to server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricAggregateExpression.java index 856ba25e2f..eab8906761 100644 --- a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricExpression.java +++ b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricAggregateExpression.java @@ -56,7 +56,7 @@ * @date 2020-08-21 14:56:50 */ @Data -public class MetricExpression implements IExpression { +public class MetricAggregateExpression implements IExpression { private String from; private QueryField metric; diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricExpectedExpression.java b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricExpectedExpression.java new file mode 100644 index 0000000000..ac2b2d513f --- /dev/null +++ b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricExpectedExpression.java @@ -0,0 +1,85 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.metric.expression.ast; + + +import lombok.Builder; +import lombok.Getter; +import org.bithon.component.commons.expression.IDataType; +import org.bithon.component.commons.expression.IEvaluationContext; +import org.bithon.component.commons.expression.IExpression; +import org.bithon.component.commons.expression.IExpressionInDepthVisitor; +import org.bithon.component.commons.expression.IExpressionVisitor; +import org.bithon.component.commons.expression.LiteralExpression; +import org.bithon.component.commons.expression.serialization.ExpressionSerializer; +import org.bithon.component.commons.utils.HumanReadableDuration; + +import javax.annotation.Nullable; + +/** + * @author frank.chen021@outlook.com + * @date 2/6/25 10:03 pm + */ +@Getter +@Builder +public class MetricExpectedExpression implements IExpression { + /** + * The expected value of the metric. + */ + private final LiteralExpression expected; + + @Nullable + private final HumanReadableDuration offset; + + @Override + public IDataType getDataType() { + return expected.getDataType(); + } + + @Override + public String getType() { + return "expected"; + } + + @Override + public Object evaluate(IEvaluationContext context) { + throw new UnsupportedOperationException(); + } + + @Override + public void accept(IExpressionInDepthVisitor visitor) { + } + + @Override + public T accept(IExpressionVisitor visitor) { + if (visitor instanceof IMetricExpressionVisitor metricVisitor) { + return metricVisitor.visit(this); + } else { + throw new UnsupportedOperationException("Unsupported visitor type: " + visitor.getClass().getName()); + } + } + + @Override + public void serializeToText(ExpressionSerializer serializer) { + expected.serializeToText(serializer); + if (offset != null) { + serializer.append("["); + serializer.append(offset.toString()); + serializer.append("]"); + } + } +} diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricExpressionASTBuilder.java b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricExpressionASTBuilder.java index 9aeec6e3e3..787077016f 100644 --- a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricExpressionASTBuilder.java +++ b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricExpressionASTBuilder.java @@ -25,6 +25,7 @@ import org.bithon.component.commons.expression.ComparisonExpression; import org.bithon.component.commons.expression.ConditionalExpression; import org.bithon.component.commons.expression.ExpressionList; +import org.bithon.component.commons.expression.FunctionExpression; import org.bithon.component.commons.expression.IDataType; import org.bithon.component.commons.expression.IExpression; import org.bithon.component.commons.expression.IdentifierExpression; @@ -34,6 +35,7 @@ import org.bithon.component.commons.utils.HumanReadableDuration; import org.bithon.component.commons.utils.HumanReadableNumber; import org.bithon.component.commons.utils.HumanReadablePercentage; +import org.bithon.component.commons.utils.Preconditions; import org.bithon.component.commons.utils.StringUtils; import org.bithon.server.commons.antlr4.SyntaxErrorListener; import org.bithon.server.metric.expression.MetricExpressionBaseVisitor; @@ -87,10 +89,10 @@ public static IExpression parse(String expression) { "Unexpected token"); } - return build(ctx); + return toAST(ctx); } - public static IExpression build(MetricExpressionParser.MetricExpressionContext metricExpression) { + public static IExpression toAST(MetricExpressionParser.MetricExpressionContext metricExpression) { return metricExpression.accept(new BuilderImpl()); } @@ -125,18 +127,53 @@ public IExpression visitParenthesisMetricExpression(MetricExpressionParser.Paren @Override public IExpression visitMetricAggregationExpression(MetricExpressionParser.MetricAggregationExpressionContext ctx) { + String aggregatorText = ctx.aggregatorExpression().getText().toLowerCase(Locale.ENGLISH); + AggregatorEnum aggregator = AggregatorEnum.fromString(aggregatorText); + if (aggregator == null) { + return new FunctionExpression(aggregatorText, ctx.metricSelectExpressionDecl().accept(this)); + } + + MetricSelectExpression metricSelectExpression = (MetricSelectExpression) ctx.metricSelectExpressionDecl().accept(this); + HumanReadableDuration duration = null; + MetricExpressionParser.DurationExpressionContext windowExpressionCtx = ctx.durationExpression(); + if (windowExpressionCtx != null) { + duration = windowExpressionCtx.accept(new DurationExpressionBuilder()); + if (duration.isNegative()) { + throw new InvalidExpressionException(StringUtils.format("The integer literal in duration expression '%s' must be greater than zero", windowExpressionCtx.getText())); + } + if (duration.isZero()) { + throw new InvalidExpressionException(StringUtils.format("The integer literal in duration expression '%s' must be greater than zero", windowExpressionCtx.getText())); + } + } + + Set groupBy = null; + MetricExpressionParser.GroupByExpressionContext groupByExpression = ctx.groupByExpression(); + if (groupByExpression != null) { + groupBy = groupByExpression.IDENTIFIER() + .stream() + .map((identifier) -> identifier.getSymbol().getText()) + // Use LinkedHashSet to retain the order of given GROUP-BY fields + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + MetricAggregateExpression expression = new MetricAggregateExpression(); + expression.setFrom(metricSelectExpression.getFrom()); + expression.setLabelSelectorExpression(metricSelectExpression.getLabelSelectorExpression()); + + // For 'count' aggregator, use the 'count' as output column instead of using the column name as output name + expression.setMetric(new QueryField(aggregator.equals(AggregatorEnum.count) ? "count" : metricSelectExpression.getMetric(), metricSelectExpression.getMetric(), aggregator.name())); + expression.setGroupBy(groupBy); + expression.setWindow(duration == null ? metricSelectExpression.getWindow() : duration); + + return expression; + } + + @Override + public IExpression visitMetricSelectExpressionDecl(MetricExpressionParser.MetricSelectExpressionDeclContext ctx) { String[] names = ctx.metricQNameExpression().getText().split("\\."); String from = names[0]; String metric = names[1]; - String aggregatorText = ctx.aggregatorExpression().getText().toLowerCase(Locale.ENGLISH); - AggregatorEnum aggregator; - try { - aggregator = AggregatorEnum.valueOf(aggregatorText); - } catch (RuntimeException ignored) { - throw new InvalidExpressionException(StringUtils.format("The aggregator [%s] in the expression is not supported", aggregatorText)); - } - HumanReadableDuration duration = null; MetricExpressionParser.DurationExpressionContext windowExpressionCtx = ctx.durationExpression(); if (windowExpressionCtx != null) { @@ -164,69 +201,64 @@ public IExpression visitMetricAggregationExpression(MetricExpressionParser.Metri } } - Set groupBy = null; - MetricExpressionParser.GroupByExpressionContext groupByExpression = ctx.groupByExpression(); - if (groupByExpression != null) { - groupBy = groupByExpression.IDENTIFIER() - .stream() - .map((identifier) -> identifier.getSymbol().getText()) - // Use LinkedHashSet to retain the order of given GROUP-BY fields - .collect(Collectors.toCollection(LinkedHashSet::new)); - } - - MetricExpression expression = new MetricExpression(); + MetricSelectExpression expression = new MetricSelectExpression(); expression.setFrom(from); expression.setLabelSelectorExpression(whereExpression); - - // For 'count' aggregator, use the 'count' as output column instead of using the column name as output name - expression.setMetric(new QueryField(aggregator.equals(AggregatorEnum.count) ? "count" : metric, metric, aggregator.name())); - expression.setGroupBy(groupBy); + expression.setMetric(metric); expression.setWindow(duration); - return expression; } @Override public IExpression visitMetricFilterExpression(MetricExpressionParser.MetricFilterExpressionContext ctx) { - IExpression expression = ctx.metricExpression().accept(this); - if (!(expression instanceof MetricExpression metricExpression)) { - throw new InvalidExpressionException("The metric filter expression must be a metric expression"); - } + IExpression lhs = ctx.metricExpression().accept(this); // // Metric Predicate // MetricExpressionParser.MetricPredicateExpressionContext predicateExpression = ctx.metricPredicateExpression(); - if (predicateExpression != null) { - MetricExpressionParser.MetricExpectedExpressionContext expectedExpression = ctx.metricExpectedExpression(); - LiteralExpression expected = (LiteralExpression) expectedExpression.literalExpression().accept(new LiteralExpressionBuilder()); - - HumanReadableDuration offset = null; - MetricExpressionParser.DurationExpressionContext offsetParseContext = expectedExpression.durationExpression(); - if (offsetParseContext != null) { - offset = offsetParseContext.accept(new DurationExpressionBuilder()); - if (!offset.isNegative()) { - throw new InvalidExpressionException("The value in the offset expression '%s' must be negative.", offsetParseContext.getText()); - } + MetricExpressionParser.MetricExpectedExpressionContext expectedExpression = ctx.metricExpectedExpression(); + LiteralExpression expected = (LiteralExpression) expectedExpression.literalExpression().accept(new LiteralExpressionBuilder()); + + HumanReadableDuration offset = null; + MetricExpressionParser.DurationExpressionContext offsetParseContext = expectedExpression.durationExpression(); + if (offsetParseContext != null) { + Preconditions.checkIfTrue(lhs instanceof MetricSelectExpression || lhs instanceof MetricAggregateExpression, + "The offset expression '%s' is only supported for metric select/aggregate expression.", offsetParseContext.getText()); + + offset = offsetParseContext.accept(new DurationExpressionBuilder()); + if (!offset.isNegative()) { + throw new InvalidExpressionException("The value in the offset expression '%s' must be negative.", offsetParseContext.getText()); + } - if (offset.isZero()) { - throw new InvalidExpressionException("The value in the offset express '%s' can't be zero.", offsetParseContext.getText()); - } + if (offset.isZero()) { + throw new InvalidExpressionException("The value in the offset express '%s' can't be zero.", offsetParseContext.getText()); + } - if (!(expected instanceof LiteralExpression.ReadablePercentageLiteral)) { - throw new InvalidExpressionException("The absolute value in the offset expression '%s' is not supported. ONLY percentage is supported now.", expectedExpression.literalExpression().getText()); - } + if (!(expected instanceof LiteralExpression.ReadablePercentageLiteral)) { + throw new InvalidExpressionException("The absolute value in the offset expression '%s' is not supported. ONLY percentage is supported now.", expectedExpression.literalExpression().getText()); } + } - PredicateEnum predicate = getPredicate(predicateExpression.getChild(TerminalNode.class, 0).getSymbol().getType(), - expected, - offset); + PredicateEnum predicate = getPredicate(predicateExpression.getChild(TerminalNode.class, 0).getSymbol().getType(), + expected, + offset); - metricExpression.setPredicate(predicate); - metricExpression.setExpected(expected); - metricExpression.setOffset(offset); - } - return metricExpression; + return predicate.createComparisonExpression(lhs, MetricExpectedExpression.builder() + .expected(expected) + .offset(offset) + .build()); + } + + @Override + public IExpression visitFunctionCallExpression(MetricExpressionParser.FunctionCallExpressionContext ctx) { + String function = ctx.IDENTIFIER().getSymbol().getText(); + IExpression[] arguments = ctx.metricExpression() + .stream() + .map((metricExpression) -> metricExpression.accept(this)) + .toList() + .toArray(new IExpression[0]); + return new FunctionExpression(function, arguments); } private static PredicateEnum getPredicate(int predicateToken, diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricExpressionOptimizer.java b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricExpressionOptimizer.java index 19c7155938..b1cba40134 100644 --- a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricExpressionOptimizer.java +++ b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricExpressionOptimizer.java @@ -31,14 +31,23 @@ static class ExtendedConstantFoldingOptimizer implements IMetricExpressionVisitor { @Override - public IExpression visit(MetricExpression expression) { + public IExpression visit(MetricAggregateExpression expression) { + return expression; + } + + @Override + public IExpression visit(MetricSelectExpression expression) { + return expression; + } + + @Override + public IExpression visit(MetricExpectedExpression expression) { return expression; } } public static IExpression optimize(IExpression expression) { - return expression.accept(new ExtendedConstantFoldingOptimizer()); + return expression.accept(new ExtendedConstantFoldingOptimizer()) + ; } - - } diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricSelectExpression.java b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricSelectExpression.java new file mode 100644 index 0000000000..a9f0b87e58 --- /dev/null +++ b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/MetricSelectExpression.java @@ -0,0 +1,191 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.metric.expression.ast; + + +import lombok.Getter; +import lombok.Setter; +import org.bithon.component.commons.expression.IDataType; +import org.bithon.component.commons.expression.IEvaluationContext; +import org.bithon.component.commons.expression.IExpression; +import org.bithon.component.commons.expression.IExpressionInDepthVisitor; +import org.bithon.component.commons.expression.IExpressionVisitor; +import org.bithon.component.commons.expression.LiteralExpression; +import org.bithon.component.commons.expression.LogicalExpression; +import org.bithon.component.commons.expression.expt.InvalidExpressionException; +import org.bithon.component.commons.expression.serialization.ExpressionSerializer; +import org.bithon.component.commons.expression.serialization.IdentifierQuotaStrategy; +import org.bithon.component.commons.utils.HumanReadableDuration; +import org.bithon.component.commons.utils.StringUtils; +import org.bithon.server.datasource.ISchema; +import org.bithon.server.datasource.column.IColumn; + +import java.util.Map; + +/** + * @author frank.chen021@outlook.com + * @date 2/6/25 11:46 am + */ +@Getter +public class MetricSelectExpression implements IExpression { + @Setter + private String from; + + @Setter + private String metric; + + @Setter + private HumanReadableDuration window; + + /** + * Runtime properties + */ + private IExpression labelSelectorExpression; + private String labelSelectorText; + private String whereText; + + public void setLabelSelectorExpression(IExpression labelSelectorExpression) { + this.labelSelectorExpression = labelSelectorExpression; + + // The where property holds the internal expression that will be passed to the query module + this.whereText = labelSelectorExpression == null ? null : new MetricAggregateExpression.SqlStyleSerializer().serialize(labelSelectorExpression); + + // This variable holds the raw text + this.labelSelectorText = labelSelectorExpression == null ? "" : "{" + new MetricAggregateExpression.LabelSelectorExpressionSerializer().serialize(labelSelectorExpression) + "}"; + } + + @Override + public String serializeToText() { + StringBuilder sb = new StringBuilder(); + sb.append(StringUtils.format("%s.%s%s", + from, + // Use field for serialization because it holds the raw input. + // In some cases, like the 'count' aggregator, the name property has different values from the field property + // See AlertExpressionASTParser for more + metric, + this.labelSelectorText)); + + if (window != null) { + sb.append('['); + sb.append(window); + sb.append(']'); + } + + return sb.toString(); + } + + @Override + public void serializeToText(ExpressionSerializer serializer) { + serializer.append(serializeToText()); + } + + static class SqlStyleSerializer extends ExpressionSerializer { + public SqlStyleSerializer() { + super(null); + } + + @Override + public void serialize(LiteralExpression expression) { + if (expression instanceof LiteralExpression.StringLiteral stringLiteral) { + sb.append('\''); + sb.append(StringUtils.escape(stringLiteral.getValue(), '\\', '\'')); + sb.append('\''); + } else { + sb.append(expression.getValue()); + } + } + } + + static class LabelSelectorExpressionSerializer extends ExpressionSerializer { + + public LabelSelectorExpressionSerializer() { + super(IdentifierQuotaStrategy.NONE); + } + + @Override + public void serialize(LogicalExpression expression) { + for (int i = 0, size = expression.getOperands().size(); i < size; i++) { + if (i > 0) { + sb.append(", "); + } + expression.getOperands().get(i).serializeToText(this); + } + } + + // Use double quote to serialize the expression by default + @Override + public void serialize(LiteralExpression expression) { + if (expression instanceof LiteralExpression.StringLiteral stringLiteral) { + sb.append('"'); + sb.append(StringUtils.escape(stringLiteral.getValue(), '\\', '"')); + sb.append('"'); + } else { + sb.append(expression.getValue()); + } + } + } + + @Override + public IDataType getDataType() { + return IDataType.BOOLEAN; + } + + @Override + public String getType() { + // No need + return null; + } + + @Override + public Object evaluate(IEvaluationContext context) { + throw new UnsupportedOperationException("Evaluate an alert expression is not supported."); + } + + @Override + public void accept(IExpressionInDepthVisitor visitor) { + } + + @Override + public T accept(IExpressionVisitor visitor) { + if (visitor instanceof IMetricExpressionVisitor metricExpressionVisitor) { + return metricExpressionVisitor.visit(this); + } + throw new UnsupportedOperationException("Evaluate an alert expression is not supported."); + } + + public void validate(Map schemas) { + if (!StringUtils.hasText(this.getFrom())) { + throw new InvalidExpressionException("data-source expression is missed in expression [%s]", this.serializeToText()); + } + + ISchema schema = schemas.get(this.getFrom()); + if (schema == null) { + throw new InvalidExpressionException("data-source expression [%s] does not exist for expression [%s]", + this.getFrom(), + this.serializeToText()); + } + + String metricName = this.getMetric(); + IColumn metricSpec = schema.getColumnByName(metricName); + if (metricSpec == null) { + throw new InvalidExpressionException("Metric [%s] in expression [%s] does not exist in data-source [%s]", + metricName, + this.serializeToText(), + this.getFrom()); + } + } +} diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/PredicateEnum.java b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/PredicateEnum.java index bf0df14dd2..dedb9685fc 100644 --- a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/PredicateEnum.java +++ b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/ast/PredicateEnum.java @@ -16,6 +16,10 @@ package org.bithon.server.metric.expression.ast; +import org.bithon.component.commons.expression.ComparisonExpression; +import org.bithon.component.commons.expression.ConditionalExpression; +import org.bithon.component.commons.expression.IExpression; + /** * @author frank.chen021@outlook.com * @date 2024/7/20 17:12 @@ -23,6 +27,11 @@ public enum PredicateEnum { LT { + @Override + public IExpression createComparisonExpression(IExpression left, IExpression right) { + return new ComparisonExpression.LT(left, right); + } + @Override public String toString() { return "<"; @@ -30,6 +39,11 @@ public String toString() { }, LTE { + @Override + public IExpression createComparisonExpression(IExpression left, IExpression right) { + return new ComparisonExpression.LTE(left, right); + } + @Override public String toString() { return "<="; @@ -37,6 +51,11 @@ public String toString() { }, GT { + @Override + public IExpression createComparisonExpression(IExpression left, IExpression right) { + return new ComparisonExpression.GT(left, right); + } + @Override public String toString() { return ">"; @@ -44,6 +63,12 @@ public String toString() { }, GTE { + @Override + public IExpression createComparisonExpression(IExpression left, IExpression right) { + return new ComparisonExpression.GTE(left, right); + } + + @Override public String toString() { return ">="; @@ -51,21 +76,38 @@ public String toString() { }, NE { + @Override + public IExpression createComparisonExpression(IExpression left, IExpression right) { + return new ComparisonExpression.NE(left, right); + } + @Override public String toString() { return "<>"; } }, EQ { + @Override + public IExpression createComparisonExpression(IExpression left, IExpression right) { + return new ComparisonExpression.EQ(left, right); + } + @Override public String toString() { return "="; } }, IS_NULL { + @Override + public IExpression createComparisonExpression(IExpression left, IExpression right) { + return new ConditionalExpression.IsNull(left); + } + @Override public String toString() { return "IS NULL"; } - } + }; + + public abstract IExpression createComparisonExpression(IExpression left, IExpression right); } diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/QueryPipelineBuilder.java b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/QueryPipelineBuilder.java deleted file mode 100644 index e27b90a1ff..0000000000 --- a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/QueryPipelineBuilder.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2020 bithon.org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.bithon.server.metric.expression.pipeline; - - -import org.bithon.component.commons.expression.ArithmeticExpression; -import org.bithon.component.commons.expression.IExpression; -import org.bithon.component.commons.expression.LiteralExpression; -import org.bithon.component.commons.utils.StringUtils; -import org.bithon.server.datasource.query.Interval; -import org.bithon.server.datasource.query.pipeline.IQueryStep; -import org.bithon.server.metric.expression.ast.IMetricExpressionVisitor; -import org.bithon.server.metric.expression.ast.MetricExpression; -import org.bithon.server.metric.expression.ast.MetricExpressionASTBuilder; -import org.bithon.server.metric.expression.ast.MetricExpressionOptimizer; -import org.bithon.server.metric.expression.pipeline.step.BinaryExpressionQueryStep; -import org.bithon.server.metric.expression.pipeline.step.LiteralQueryStep; -import org.bithon.server.metric.expression.pipeline.step.MetricExpressionQueryStep; -import org.bithon.server.web.service.datasource.api.IDataSourceApi; -import org.bithon.server.web.service.datasource.api.IntervalRequest; -import org.bithon.server.web.service.datasource.api.QueryField; -import org.bithon.server.web.service.datasource.api.QueryRequest; - -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * @author frank.chen021@outlook.com - * @date 4/4/25 3:53 pm - */ -public class QueryPipelineBuilder { - - private IDataSourceApi dataSourceApi; - private IntervalRequest intervalRequest; - private String condition; - - public static QueryPipelineBuilder builder() { - return new QueryPipelineBuilder(); - } - - public QueryPipelineBuilder dataSourceApi(IDataSourceApi dataSourceApi) { - this.dataSourceApi = dataSourceApi; - return this; - } - - public QueryPipelineBuilder intervalRequest(IntervalRequest intervalRequest) { - this.intervalRequest = intervalRequest; - return this; - } - - public QueryPipelineBuilder condition(String condition) { - this.condition = condition; - return this; - } - - public IQueryStep build(String expression) { - IExpression expr = MetricExpressionASTBuilder.parse(expression); - - // Apply optimization like constant folding on parsed expression - // The optimization is applied here so that above parse can be tested separately - expr = MetricExpressionOptimizer.optimize(expr); - - return this.build(expr); - } - - public IQueryStep build(IExpression expression) { - return expression.accept(new Builder()); - } - - private class Builder implements IMetricExpressionVisitor { - @Override - public IQueryStep visit(MetricExpression expression) { - String filterExpression = Stream.of(expression.getWhereText(), condition) - .filter(Objects::nonNull) - .collect(Collectors.joining(" AND ")); - - if (expression.getOffset() != null) { - // Create expression as: ( current - base ) / base - QueryField metricField = expression.getMetric(); - String expr = StringUtils.format("%s(%s) * 1.0", metricField.getAggregator(), metricField.getField()); - MetricExpressionQueryStep curr = new MetricExpressionQueryStep(QueryRequest.builder() - .dataSource(expression.getFrom()) - .filterExpression(filterExpression) - .groupBy(expression.getGroupBy()) - .fields(List.of(new QueryField(metricField.getName(), metricField.getField(), null, expr))) - .interval(intervalRequest) - .build(), - dataSourceApi); - - MetricExpressionQueryStep base = new MetricExpressionQueryStep(QueryRequest.builder() - .dataSource(expression.getFrom()) - .filterExpression(filterExpression) - .groupBy(expression.getGroupBy()) - .fields(List.of(new QueryField( - // Use offset AS the output name - expression.getOffset().toString(), - metricField.getField(), - null, - expr))) - .interval(intervalRequest) - .offset(expression.getOffset()) - .build(), - dataSourceApi); - - // - return new BinaryExpressionQueryStep.Div( - new BinaryExpressionQueryStep.Sub( - curr, - base, - "diff", - - // Returns 'curr' as well as the computed result set - metricField.getName() - ), - base, - "delta", - - // Keep the current and base columns in the result set - metricField.getName(), expression.getOffset().toString() - ); - } else { - return new MetricExpressionQueryStep(QueryRequest.builder() - .dataSource(expression.getFrom()) - .filterExpression(filterExpression) - .groupBy(expression.getGroupBy()) - .fields(List.of(expression.getMetric())) - .interval(intervalRequest) - .build(), - dataSourceApi); - } - } - - @Override - public IQueryStep visit(LiteralExpression expression) { - return new LiteralQueryStep(expression, Interval.of(intervalRequest.getStartISO8601(), - intervalRequest.getEndISO8601(), - intervalRequest.calculateStep(), - null)); - } - - @Override - public IQueryStep visit(ArithmeticExpression expression) { - return switch (expression.getType()) { - case "+" -> new BinaryExpressionQueryStep.Add(expression.getLhs().accept(this), expression.getRhs().accept(this)); - case "-" -> new BinaryExpressionQueryStep.Sub(expression.getLhs().accept(this), expression.getRhs().accept(this)); - case "*" -> new BinaryExpressionQueryStep.Mul(expression.getLhs().accept(this), expression.getRhs().accept(this)); - case "/" -> new BinaryExpressionQueryStep.Div(expression.getLhs().accept(this), expression.getRhs().accept(this)); - default -> throw new UnsupportedOperationException("Unsupported arithmetic expression: " + expression.getType()); - }; - } - } -} diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/MetricExpressionQueryStep.java b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/MetricExpressionQueryStep.java deleted file mode 100644 index 3184cdbe93..0000000000 --- a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/pipeline/step/MetricExpressionQueryStep.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2020 bithon.org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.bithon.server.metric.expression.pipeline.step; - - -import org.bithon.component.commons.utils.CollectionUtils; -import org.bithon.server.commons.time.TimeSpan; -import org.bithon.server.datasource.TimestampSpec; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; -import org.bithon.server.datasource.query.pipeline.IQueryStep; -import org.bithon.server.datasource.query.pipeline.PipelineQueryResult; -import org.bithon.server.web.service.datasource.api.IDataSourceApi; -import org.bithon.server.web.service.datasource.api.QueryField; -import org.bithon.server.web.service.datasource.api.QueryRequest; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -/** - * @author frank.chen021@outlook.com - * @date 4/4/25 3:48 pm - */ -public class MetricExpressionQueryStep implements IQueryStep { - private final QueryRequest queryRequest; - private final IDataSourceApi dataSourceApi; - private final boolean isScalar; - - // Make sure the evaluation is executed ONLY ONCE when the expression is referenced multiple times - private volatile CompletableFuture cachedResponse; - - public MetricExpressionQueryStep(QueryRequest queryRequest, IDataSourceApi dataSourceApi) { - this.queryRequest = queryRequest; - this.dataSourceApi = dataSourceApi; - this.isScalar = computeIsScalar(queryRequest); - } - - private boolean computeIsScalar(QueryRequest queryRequest) { - if (CollectionUtils.isNotEmpty(queryRequest.getGroupBy())) { - // If GROUP-BY is not empty, obviously it is not a scalar because the result set contains multiple rows for different groups - return false; - } - - if (queryRequest.getInterval().getBucketCount() != null && queryRequest.getInterval().getBucketCount() == 1) { - // ONLY one bucket is requested, the result set is a scalar - return true; - } - - TimeSpan start = queryRequest.getInterval().getStartISO8601(); - TimeSpan end = queryRequest.getInterval().getEndISO8601(); - long intervalLength = (end.getMilliseconds() - start.getMilliseconds()) / 1000; - return queryRequest.getInterval().getStep() != null && queryRequest.getInterval().getStep() == intervalLength; - } - - @Override - public boolean isScalar() { - return isScalar; - } - - @Override - public CompletableFuture execute() { - if (cachedResponse == null) { - synchronized (this) { - if (cachedResponse == null) { - cachedResponse = CompletableFuture.supplyAsync(() -> { - try { - ColumnarTable columnTable = dataSourceApi.timeseriesV5(queryRequest); - - List keys = new ArrayList<>(); - keys.add(TimestampSpec.COLUMN_ALIAS); - keys.addAll(queryRequest.getGroupBy() != null ? queryRequest.getGroupBy() : Collections.emptySet()); - - List valNames = queryRequest.getFields() - .stream() - .map(QueryField::getName) - .toList(); - - return PipelineQueryResult.builder() - .rows(columnTable.rowCount()) - .keyColumns(keys) - .valColumns(valNames) - .table(columnTable) - .build(); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - } - } - return cachedResponse; - } -} diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/plan/MetricExpressionLogicalPlanner.java b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/plan/MetricExpressionLogicalPlanner.java new file mode 100644 index 0000000000..430b10e26b --- /dev/null +++ b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/plan/MetricExpressionLogicalPlanner.java @@ -0,0 +1,132 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.metric.expression.plan; + +import org.bithon.component.commons.expression.ArithmeticExpression; +import org.bithon.component.commons.expression.ConditionalExpression; +import org.bithon.component.commons.expression.LiteralExpression; +import org.bithon.component.commons.utils.CollectionUtils; +import org.bithon.component.commons.utils.Preconditions; +import org.bithon.server.datasource.ISchema; +import org.bithon.server.datasource.ISchemaProvider; +import org.bithon.server.datasource.column.IColumn; +import org.bithon.server.datasource.query.ast.Selector; +import org.bithon.server.datasource.query.plan.logical.BinaryOp; +import org.bithon.server.datasource.query.plan.logical.ILogicalPlan; +import org.bithon.server.datasource.query.plan.logical.LogicalAggregate; +import org.bithon.server.datasource.query.plan.logical.LogicalBinaryOp; +import org.bithon.server.datasource.query.plan.logical.LogicalFilter; +import org.bithon.server.datasource.query.plan.logical.LogicalScalar; +import org.bithon.server.datasource.query.plan.logical.LogicalTableScan; +import org.bithon.server.datasource.query.plan.logical.PredicateEnum; +import org.bithon.server.metric.expression.ast.IMetricExpressionVisitor; +import org.bithon.server.metric.expression.ast.MetricAggregateExpression; +import org.bithon.server.metric.expression.ast.MetricExpectedExpression; +import org.bithon.server.metric.expression.ast.MetricSelectExpression; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author frank.chen021@outlook.com + * @date 2025/6/4 23:43 + */ +public class MetricExpressionLogicalPlanner implements IMetricExpressionVisitor { + private final ISchemaProvider schemaProvider; + + public MetricExpressionLogicalPlanner(ISchemaProvider schemaProvider) { + this.schemaProvider = schemaProvider; + } + + @Override + public ILogicalPlan visit(MetricSelectExpression expression) { + ISchema schema = Preconditions.checkNotNull(schemaProvider.getSchema(expression.getFrom()), + "Schema for %s is not found", + expression.getFrom()); + return new LogicalTableScan( + schema, + List.of(new Selector(expression.getMetric(), expression.getMetric(), expression.getDataType())), + expression.getLabelSelectorExpression(), + null + ); + } + + @Override + public ILogicalPlan visit(MetricAggregateExpression expression) { + ISchema schema = Preconditions.checkNotNull(schemaProvider.getSchema(expression.getFrom()), + "Schema for %s is not found", + expression.getFrom()); + IColumn col = schema.getColumnByName(expression.getMetric().getName()); + return new LogicalAggregate( + expression.getMetric().getAggregator(), + new LogicalTableScan(schema, + List.of(new Selector(expression.getMetric().getName(), + expression.getMetric().getName(), + expression.getDataType())), + expression.getLabelSelectorExpression(), + expression.getOffset()), + col, + CollectionUtils.isEmpty(expression.getGroupBy()) ? new ArrayList<>() : new ArrayList<>(expression.getGroupBy()), + new ArrayList<>() + ); + } + + @Override + public ILogicalPlan visit(ArithmeticExpression expression) { + BinaryOp op = switch (expression.getOperator()) { + case ADD -> BinaryOp.ADD; + case SUB -> BinaryOp.SUB; + case MUL -> BinaryOp.MUL; + case DIV -> BinaryOp.DIV; + }; + + return new LogicalBinaryOp( + expression.getLhs().accept(this), + op, + expression.getRhs().accept(this) + ); + } + + @Override + public ILogicalPlan visit(ConditionalExpression expression) { + PredicateEnum predicate = switch (expression.getType()) { + case "<" -> PredicateEnum.LT; + case "<=" -> PredicateEnum.LTE; + case ">" -> PredicateEnum.GT; + case ">=" -> PredicateEnum.GTE; + case "=" -> PredicateEnum.EQ; + case "!=", "<>" -> PredicateEnum.NE; + default -> throw new IllegalArgumentException("Unsupported predicate type: " + expression.getType()); + }; + + return new LogicalFilter( + expression.getLhs().accept(this), + predicate, + expression.getRhs().accept(this) + ); + } + + @Override + public ILogicalPlan visit(MetricExpectedExpression expression) { + return new LogicalScalar(expression.getExpected(), expression.getOffset()); + } + + @Override + public ILogicalPlan visit(LiteralExpression expression) { + return new LogicalScalar(expression); + } +} diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/plan/MetricExpressionPhysicalPlanner.java b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/plan/MetricExpressionPhysicalPlanner.java new file mode 100644 index 0000000000..d62bf245ed --- /dev/null +++ b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/plan/MetricExpressionPhysicalPlanner.java @@ -0,0 +1,395 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.metric.expression.plan; + + +import org.bithon.component.commons.expression.ArithmeticExpression; +import org.bithon.component.commons.expression.ComparisonExpression; +import org.bithon.component.commons.expression.ConditionalExpression; +import org.bithon.component.commons.expression.IExpression; +import org.bithon.component.commons.expression.IdentifierExpression; +import org.bithon.component.commons.expression.LiteralExpression; +import org.bithon.component.commons.utils.Preconditions; +import org.bithon.component.commons.utils.StringUtils; +import org.bithon.server.datasource.ISchemaProvider; +import org.bithon.server.datasource.query.IDataSourceReader; +import org.bithon.server.datasource.query.Interval; +import org.bithon.server.datasource.query.plan.logical.ILogicalPlan; +import org.bithon.server.datasource.query.plan.logical.ILogicalPlanVisitor; +import org.bithon.server.datasource.query.plan.logical.LogicalAggregate; +import org.bithon.server.datasource.query.plan.logical.LogicalBinaryOp; +import org.bithon.server.datasource.query.plan.logical.LogicalFilter; +import org.bithon.server.datasource.query.plan.logical.LogicalScalar; +import org.bithon.server.datasource.query.plan.logical.LogicalTableScan; +import org.bithon.server.datasource.query.plan.physical.ArithmeticStep; +import org.bithon.server.datasource.query.plan.physical.FilterStep; +import org.bithon.server.datasource.query.plan.physical.IPhysicalPlan; +import org.bithon.server.datasource.query.plan.physical.LiteralQueryStep; +import org.bithon.server.datasource.query.plan.physical.MetricQueryStep; +import org.bithon.server.metric.expression.ast.IMetricExpressionVisitor; +import org.bithon.server.metric.expression.ast.MetricAggregateExpression; +import org.bithon.server.metric.expression.ast.MetricExpectedExpression; +import org.bithon.server.metric.expression.ast.MetricExpressionASTBuilder; +import org.bithon.server.metric.expression.ast.MetricExpressionOptimizer; +import org.bithon.server.metric.expression.ast.PredicateEnum; +import org.bithon.server.web.service.datasource.api.IntervalRequest; +import org.bithon.server.web.service.datasource.api.QueryField; + +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author frank.chen021@outlook.com + * @date 4/4/25 3:53 pm + */ +public class MetricExpressionPhysicalPlanner { + + private IntervalRequest interval; + private String condition; + private MetricExpressionPlannerSettings settings = MetricExpressionPlannerSettings.DEFAULT; + private ISchemaProvider schemaProvider; + + public static MetricExpressionPhysicalPlanner builder() { + return new MetricExpressionPhysicalPlanner(); + } + + public MetricExpressionPhysicalPlanner interval(IntervalRequest interval) { + this.interval = interval; + return this; + } + + public MetricExpressionPhysicalPlanner condition(String condition) { + this.condition = condition; + return this; + } + + public MetricExpressionPhysicalPlanner settings(MetricExpressionPlannerSettings settings) { + this.settings = settings; + return this; + } + + public MetricExpressionPhysicalPlanner schemaProvider(ISchemaProvider schemaProvider) { + this.schemaProvider = schemaProvider; + return this; + } + + public IPhysicalPlan timeSeries(String expression) { + Preconditions.checkNotNull(this.interval, "interval of MetricExpressionPhysicalPlanner can't be null"); + + IExpression expr = MetricExpressionASTBuilder.parse(expression); + + // Apply optimization like constant folding on parsed expression + // The optimization is applied here so that the above parse can be tested separately + expr = MetricExpressionOptimizer.optimize(expr); + + ILogicalPlan logicalPlan = expr.accept(new MetricExpressionLogicalPlanner(schemaProvider)); + return logicalPlan.accept(new PhysicalPlanImpl(Interval.of(this.interval.getStartISO8601(), + this.interval.getEndISO8601(), + this.interval.calculateStep(), + new IdentifierExpression("timestamp")))); + } + + public IPhysicalPlan groupBy(String expression) { + IExpression expr = MetricExpressionASTBuilder.parse(expression); + + // Apply optimization like constant folding on parsed expression + // The optimization is applied here so that the above parse can be tested separately + expr = MetricExpressionOptimizer.optimize(expr); + + ILogicalPlan logicalPlan = expr.accept(new MetricExpressionLogicalPlanner(schemaProvider)); + return logicalPlan.accept(new PhysicalPlanImpl(Interval.of(this.interval.getStartISO8601(), + this.interval.getEndISO8601(), + null, + new IdentifierExpression("timestamp")))); + } + + static class PhysicalPlanImpl implements ILogicalPlanVisitor { + private final Interval interval; + + PhysicalPlanImpl(Interval interval) { + this.interval = interval; + } + + @Override + public IPhysicalPlan visitAggregate(LogicalAggregate aggregate) { + IPhysicalPlan physicalPlan = aggregate.input().accept(this); + if (physicalPlan.canPushDownAggregate(aggregate)) { + return physicalPlan.pushDownAggregate(aggregate); + } else { + throw new UnsupportedOperationException("Aggregate not supported: " + aggregate); + } + } + + @Override + public IPhysicalPlan visitTableScan(LogicalTableScan tableScan) { + try { + try (IDataSourceReader reader = tableScan.table().getDataStoreSpec().createReader()) { + return reader.plan(tableScan, interval); + } + } catch (Exception e) { + throw new RuntimeException("Failed to create reader for table: " + tableScan.table().getName(), e); + } + } + + @Override + public IPhysicalPlan visitBinaryOp(LogicalBinaryOp binaryOp) { + return switch (binaryOp.op()) { + case ADD -> new ArithmeticStep.Add(binaryOp.left().accept(this), + binaryOp.right().accept(this)); + case SUB -> new ArithmeticStep.Sub(binaryOp.left().accept(this), + binaryOp.right().accept(this)); + case MUL -> new ArithmeticStep.Mul(binaryOp.left().accept(this), + binaryOp.right().accept(this)); + case DIV -> new ArithmeticStep.Div(binaryOp.left().accept(this), + binaryOp.right().accept(this)); + }; + } + + @Override + public IPhysicalPlan visitScalar(LogicalScalar scalar) { + return new LiteralQueryStep(scalar.literal()); + } + + @Override + public IPhysicalPlan visitFilter(LogicalFilter filter) { + IPhysicalPlan curr = filter.left().accept(this); + + if (filter.right() instanceof LogicalScalar scalar) { + if (scalar.offset() != null) { + // AST level ensures that the left side is a MetricAggregateExpression/MetricSelectExpression + + IPhysicalPlan base = curr.offset(scalar.offset()); + + // convert to expression as: (current - base) / base + curr = new ArithmeticStep.Div( + new ArithmeticStep.Sub( + curr, + base, + // result column name + "diff", + + // Retain all value columns in the result set + true + ), + base, + "delta", + + // Keep the current and base columns in the result set + true + ); + } + } + + return switch (filter.op()) { + case LT -> new FilterStep.LT(curr, + (Number) ((LogicalScalar) filter.right()).literal().getValue()); + case LTE -> new FilterStep.LTE(curr, + (Number) ((LogicalScalar) filter.right()).literal().getValue()); + case GT -> new FilterStep.GT(curr, + (Number) ((LogicalScalar) filter.right()).literal().getValue()); + case GTE -> new FilterStep.GTE(curr, + (Number) ((LogicalScalar) filter.right()).literal().getValue()); + case NE -> new FilterStep.NE(curr, + (Number) ((LogicalScalar) filter.right()).literal().getValue()); + default -> throw new UnsupportedOperationException("Unsupported filter operation: " + filter.op()); + }; + } + } + + private class Builder implements IMetricExpressionVisitor { + @Override + public IPhysicalPlan visit(MetricAggregateExpression expression) { + String filterExpression = Stream.of(expression.getWhereText(), condition) + .filter(Objects::nonNull) + .collect(Collectors.joining(" AND ")); + + if (expression.getOffset() != null) { + // Create expression as: (current - base) / base + QueryField metricField = expression.getMetric(); + String expr = StringUtils.format("%s(%s) * 1.0", metricField.getAggregator(), metricField.getField()); + MetricQueryStep curr = MetricQueryStep.builder() + .dataSource(expression.getFrom()) + .filterExpression(filterExpression) + .groupBy(expression.getGroupBy()) + /* + .fields(List.of(new QueryField(metricField.getName(), metricField.getField(), null, expr))) + .interval(intervalRequest) + .dataSourceApi(dataSourceApi)*/ + .build(); + + MetricQueryStep base = MetricQueryStep.builder() + .dataSource(expression.getFrom()) + .filterExpression(filterExpression) + .groupBy(expression.getGroupBy()) + /* + .fields(List.of(new QueryField( + // Use offset AS the output name + expression.getOffset().toString(), + metricField.getField(), + null, + expr))) + .interval(intervalRequest) + .offset(expression.getOffset()) + .dataSourceApi(dataSourceApi)*/ + .build(); + + // + return new ArithmeticStep.Div( + new ArithmeticStep.Sub( + curr, + base, + "diff", + + // Returns 'curr' as well as the computed result set + true + ), + base, + "delta", + + // Keep the current and base columns in the result set + //metricField.getName(), expression.getOffset().toString() + true + ); + } else { + return MetricQueryStep.builder() + .dataSource(expression.getFrom()) + .filterExpression(filterExpression) + .groupBy(expression.getGroupBy()) + /* + .fields(List.of(expression.getMetric())) + .interval(intervalRequest) + .dataSourceApi(dataSourceApi)*/ + .build(); + } + } + + /* + @Override + public IQueryStep visit(FunctionCallExpression expression) { + if (expression.getArguments() instanceof MetricSelectExpression selectExpression) { + AggregatorEnum aggregator = AggregatorEnum.fromString(expression.getOperator()); + if (aggregator != null) { + String filterExpression = Stream.of(selectExpression.getWhereText(), condition) + .filter(Objects::nonNull) + .collect(Collectors.joining(" AND ")); + return new MetricQueryStep(QueryRequest.builder() + .dataSource(selectExpression.getFrom()) + .filterExpression(filterExpression) + .groupBy(expression.getGroupBy()) + .fields(List.of(new QueryField(selectExpression.getMetric(), + selectExpression.getMetric(), + aggregator.name()))) + .interval(intervalRequest) + .build(), + dataSourceApi); + } else { + // other functions + throw new UnsupportedOperationException("Unsupported metric call expression: " + expression.getOperator()); + } + } else { + throw new UnsupportedOperationException("Unsupported metric call expression: " + expression.getArguments().getClass().getSimpleName()); + } + }*/ + + @Override + public IPhysicalPlan visit(LiteralExpression expression) { + return new LiteralQueryStep(expression, Interval.of(interval.getStartISO8601(), + interval.getEndISO8601(), + interval.calculateStep(), + null)); + } + + @Override + public IPhysicalPlan visit(ArithmeticExpression expression) { + return switch (expression.getType()) { + case "+" -> new ArithmeticStep.Add(expression.getLhs().accept(this), expression.getRhs().accept(this)); + case "-" -> new ArithmeticStep.Sub(expression.getLhs().accept(this), expression.getRhs().accept(this)); + case "*" -> new ArithmeticStep.Mul(expression.getLhs().accept(this), expression.getRhs().accept(this)); + case "/" -> new ArithmeticStep.Div(expression.getLhs().accept(this), expression.getRhs().accept(this)); + default -> throw new UnsupportedOperationException("Unsupported arithmetic expression: " + expression.getType()); + }; + } + + /** + * Push down the filter condition to the source step. + */ + @Override + public IPhysicalPlan visit(ConditionalExpression expression) { + if (settings.isPushdownPostFilter()) { + IExpression lhs = expression.getLhs(); + IExpression rhs = expression.getRhs(); + if (lhs instanceof MetricAggregateExpression metricExpression + && rhs instanceof MetricExpectedExpression expectedExpression) { + if (expression instanceof ComparisonExpression.LT) { + metricExpression.setPredicate(PredicateEnum.LT); + } else if (expression instanceof ComparisonExpression.GT) { + metricExpression.setPredicate(PredicateEnum.GT); + } else if (expression instanceof ComparisonExpression.LTE) { + metricExpression.setPredicate(PredicateEnum.LTE); + } else if (expression instanceof ComparisonExpression.GTE) { + metricExpression.setPredicate(PredicateEnum.GTE); + } else if (expression instanceof ComparisonExpression.EQ) { + metricExpression.setPredicate(PredicateEnum.EQ); + } else if (expression instanceof ComparisonExpression.NE) { + metricExpression.setPredicate(PredicateEnum.NE); + } else { + throw new UnsupportedOperationException("Unsupported comparison expression: " + expression.getClass().getSimpleName()); + } + + metricExpression.setExpected(expectedExpression.getExpected()); + metricExpression.setOffset(expectedExpression.getOffset()); + return this.visit(metricExpression); + } + } + + IPhysicalPlan source = expression.getLhs().accept(this); + + if (expression instanceof ComparisonExpression.LT) { + return new FilterStep.LT( + source, + (Number) ((MetricExpectedExpression) expression.getRhs()).getExpected().getValue() + ); + } + if (expression instanceof ComparisonExpression.LTE) { + return new FilterStep.LTE( + source, + (Number) ((MetricExpectedExpression) expression.getRhs()).getExpected().getValue() + ); + } + if (expression instanceof ComparisonExpression.GT) { + return new FilterStep.GT( + source, + (Number) ((MetricExpectedExpression) expression.getRhs()).getExpected().getValue() + ); + } + if (expression instanceof ComparisonExpression.GTE) { + return new FilterStep.GTE( + source, + (Number) ((MetricExpectedExpression) expression.getRhs()).getExpected().getValue() + ); + } + if (expression instanceof ComparisonExpression.NE) { + return new FilterStep.NE( + source, + (Number) ((MetricExpectedExpression) expression.getRhs()).getExpected().getValue() + ); + } + throw new UnsupportedOperationException("Unsupported conditional expression: " + expression.getType()); + } + } +} diff --git a/server/metric-expression/src/main/java/org/bithon/server/metric/expression/plan/MetricExpressionPlannerSettings.java b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/plan/MetricExpressionPlannerSettings.java new file mode 100644 index 0000000000..a4e223e75e --- /dev/null +++ b/server/metric-expression/src/main/java/org/bithon/server/metric/expression/plan/MetricExpressionPlannerSettings.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.metric.expression.plan; + +import lombok.Data; + +/** + * @author frank.chen021@outlook.com + * @date 2025/6/4 21:47 + */ +@Data +public class MetricExpressionPlannerSettings { + public static final MetricExpressionPlannerSettings DEFAULT = new MetricExpressionPlannerSettings(); + + private boolean pushdownPostFilter = false; + private boolean pushdownArithmetic = false; +} diff --git a/server/metric-expression/src/test/java/org/bithon/server/metric/expression/ast/MetricExpressionASTBuilderTest.java b/server/metric-expression/src/test/java/org/bithon/server/metric/expression/ast/MetricExpressionASTBuilderTest.java index 84a63fdc81..99fbbb021b 100644 --- a/server/metric-expression/src/test/java/org/bithon/server/metric/expression/ast/MetricExpressionASTBuilderTest.java +++ b/server/metric-expression/src/test/java/org/bithon/server/metric/expression/ast/MetricExpressionASTBuilderTest.java @@ -17,6 +17,9 @@ package org.bithon.server.metric.expression.ast; import org.bithon.component.commons.expression.ArithmeticExpression; +import org.bithon.component.commons.expression.BinaryExpression; +import org.bithon.component.commons.expression.ComparisonExpression; +import org.bithon.component.commons.expression.FunctionExpression; import org.bithon.component.commons.expression.IExpression; import org.bithon.component.commons.expression.LiteralExpression; import org.bithon.component.commons.expression.expt.InvalidExpressionException; @@ -40,11 +43,13 @@ public class MetricExpressionASTBuilderTest { @Test public void test_ColonCharacter() { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu:usage{appName = 'a'}) > 1"); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu:usage{appName = 'a'}) > 1"); Assertions.assertNotNull(expression); - Assertions.assertEquals("jvm-metrics", expression.getFrom()); - Assertions.assertEquals("avg", expression.getMetric().getAggregator()); - Assertions.assertEquals("cpu:usage", expression.getMetric().getField()); + + MetricAggregateExpression metricExpression = (MetricAggregateExpression) ((BinaryExpression) expression).getLhs(); + Assertions.assertEquals("jvm-metrics", metricExpression.getFrom()); + Assertions.assertEquals("avg", metricExpression.getMetric().getAggregator()); + Assertions.assertEquals("cpu:usage", metricExpression.getMetric().getField()); Assertions.assertEquals("avg(jvm-metrics.cpu:usage{appName = \"a\"}) > 1", expression.serializeToText()); // colon is not allowed in label @@ -53,18 +58,19 @@ public void test_ColonCharacter() { @Test public void test_Expression() { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName = 'a'}) > 1"); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName = 'a'}) > 1"); + MetricAggregateExpression metricExpression = (MetricAggregateExpression) ((BinaryExpression) expression).getLhs(); Assertions.assertNotNull(expression); - Assertions.assertEquals("jvm-metrics", expression.getFrom()); - Assertions.assertEquals("avg", expression.getMetric().getAggregator()); - Assertions.assertEquals("cpu", expression.getMetric().getField()); + Assertions.assertEquals("jvm-metrics", metricExpression.getFrom()); + Assertions.assertEquals("avg", metricExpression.getMetric().getAggregator()); + Assertions.assertEquals("cpu", metricExpression.getMetric().getField()); Assertions.assertEquals("avg(jvm-metrics.cpu{appName = \"a\"}) > 1", expression.serializeToText()); } @Test public void test_NoPredicateExpression() { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName = 'a'})"); + MetricAggregateExpression expression = (MetricAggregateExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName = 'a'})"); Assertions.assertNotNull(expression); Assertions.assertEquals("jvm-metrics", expression.getFrom()); Assertions.assertEquals("avg", expression.getMetric().getAggregator()); @@ -89,11 +95,12 @@ public void test_Percentage() { @Test public void test_WithLabelSelectorExpression() { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <> 'a'}) > 1"); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <> 'a'}) > 1"); + MetricAggregateExpression metricSelectExpression = (MetricAggregateExpression) ((ComparisonExpression) expression).getLhs(); Assertions.assertNotNull(expression); - Assertions.assertEquals("jvm-metrics", expression.getFrom()); - Assertions.assertEquals("avg", expression.getMetric().getAggregator()); - Assertions.assertEquals("cpu", expression.getMetric().getField()); + Assertions.assertEquals("jvm-metrics", metricSelectExpression.getFrom()); + Assertions.assertEquals("avg", metricSelectExpression.getMetric().getAggregator()); + Assertions.assertEquals("cpu", metricSelectExpression.getMetric().getField()); MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName > 'a'}) > 1"); MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName >= 'a'}) > 1"); @@ -106,15 +113,18 @@ public void test_WithLabelSelectorExpression() { @Test public void test_DurationExpression() { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <= 'a'})[5m] > 1"); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <= 'a'})[5m] > 1"); Assertions.assertNotNull(expression); - Assertions.assertEquals(5, expression.getWindow().getDuration().toMinutes()); - Assertions.assertEquals(TimeUnit.MINUTES, expression.getWindow().getUnit()); - expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <= 'a'})[5h] > 1"); + MetricAggregateExpression metricSelectExpression = (MetricAggregateExpression) ((ComparisonExpression) expression).getLhs(); + Assertions.assertEquals(5, metricSelectExpression.getWindow().getDuration().toMinutes()); + Assertions.assertEquals(TimeUnit.MINUTES, metricSelectExpression.getWindow().getUnit()); + + expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <= 'a'})[5h] > 1"); + metricSelectExpression = (MetricAggregateExpression) ((ComparisonExpression) expression).getLhs(); Assertions.assertNotNull(expression); - Assertions.assertEquals(5, expression.getWindow().getDuration().toHours()); - Assertions.assertEquals(TimeUnit.HOURS, expression.getWindow().getUnit()); + Assertions.assertEquals(5, metricSelectExpression.getWindow().getDuration().toHours()); + Assertions.assertEquals(TimeUnit.HOURS, metricSelectExpression.getWindow().getUnit()); // the duration must be a positive value Assertions.assertThrows(InvalidExpressionException.class, () -> MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName in ('a', 1)})[0m] > 1")); @@ -124,25 +134,31 @@ public void test_DurationExpression() { @Test public void test_HumanReadableSizeExpression() { // binary format - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <= 'a'})[5m] > 1MiB"); - Assertions.assertNotNull(expression); - Assertions.assertEquals(5, expression.getWindow().getDuration().toMinutes()); - Assertions.assertEquals(TimeUnit.MINUTES, expression.getWindow().getUnit()); - Assertions.assertEquals(HumanReadableNumber.of("1MiB"), expression.getExpected().getValue()); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <= 'a'})[5m] > 1MiB"); + MetricAggregateExpression metricExpression = (MetricAggregateExpression) ((ComparisonExpression) expression).getLhs(); + MetricExpectedExpression expected = (MetricExpectedExpression) ((ComparisonExpression) expression).getRhs(); + + Assertions.assertEquals(5, metricExpression.getWindow().getDuration().toMinutes()); + Assertions.assertEquals(TimeUnit.MINUTES, metricExpression.getWindow().getUnit()); + Assertions.assertEquals(HumanReadableNumber.of("1MiB"), expected.getExpected().getValue()); Assertions.assertEquals("avg(jvm-metrics.cpu{appName <= \"a\"})[5m] > 1MiB", expression.serializeToText()); // decimal format - expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <= 'a'})[5h] > 7K"); - Assertions.assertNotNull(expression); - Assertions.assertEquals(5, expression.getWindow().getDuration().toHours()); - Assertions.assertEquals(HumanReadableNumber.of("7K"), expression.getExpected().getValue()); + expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <= 'a'})[5h] > 7K"); + metricExpression = (MetricAggregateExpression) ((ComparisonExpression) expression).getLhs(); + expected = (MetricExpectedExpression) ((ComparisonExpression) expression).getRhs(); + + Assertions.assertEquals(5, metricExpression.getWindow().getDuration().toHours()); + Assertions.assertEquals(HumanReadableNumber.of("7K"), expected.getExpected().getValue()); Assertions.assertEquals("avg(jvm-metrics.cpu{appName <= \"a\"})[5h] > 7K", expression.serializeToText()); // simplified binary format - expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <= 'a'})[5h] > 100Gi"); - Assertions.assertNotNull(expression); - Assertions.assertEquals(5, expression.getWindow().getDuration().toHours()); - Assertions.assertEquals(HumanReadableNumber.of("100Gi"), expression.getExpected().getValue()); + expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <= 'a'})[5h] > 100Gi"); + metricExpression = (MetricAggregateExpression) ((ComparisonExpression) expression).getLhs(); + expected = (MetricExpectedExpression) ((ComparisonExpression) expression).getRhs(); + + Assertions.assertEquals(5, metricExpression.getWindow().getDuration().toHours()); + Assertions.assertEquals(HumanReadableNumber.of("100Gi"), expected.getExpected().getValue()); Assertions.assertEquals("avg(jvm-metrics.cpu{appName <= \"a\"})[5h] > 100Gi", expression.serializeToText()); // Invalid human-readable size @@ -151,7 +167,7 @@ public void test_HumanReadableSizeExpression() { @Test public void test_SimplePredicateExpression() { - MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <= 'a'})[5m] > 1"); + MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <= 'a'}[5m]) > 1"); MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <= 'a'})[5m] >= 1"); MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName <= 'a'})[5m] = 1"); @@ -165,8 +181,9 @@ public void test_SimplePredicateExpression() { @Test public void test_ContainsPredicateExpression() { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName contains 'a'})[5m] is null"); - Assertions.assertEquals("appName contains 'a'", expression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName contains 'a'})[5m] is null"); + MetricAggregateExpression metricExpression = (MetricAggregateExpression) ((BinaryExpression) expression).getLhs(); + Assertions.assertEquals("appName contains 'a'", metricExpression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); // contains require string literal Assertions.assertThrows(InvalidExpressionException.class, () -> MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName contains 5})[5m]")); @@ -174,8 +191,9 @@ public void test_ContainsPredicateExpression() { @Test public void test_StartsWithPredicateExpression() { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName startsWith 'a'})[5m] is null"); - Assertions.assertEquals("appName startsWith 'a'", expression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName startsWith 'a'})[5m] is null"); + MetricAggregateExpression metricExpression = (MetricAggregateExpression) ((BinaryExpression) expression).getLhs(); + Assertions.assertEquals("appName startsWith 'a'", metricExpression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); // startsWith require string literal Assertions.assertThrows(InvalidExpressionException.class, () -> MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName startsWith 5})[5m]")); @@ -183,8 +201,9 @@ public void test_StartsWithPredicateExpression() { @Test public void test_EnsWithPredicateExpression() { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName endsWith 'a'})[5m] is null"); - Assertions.assertEquals("appName endsWith 'a'", expression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName endsWith 'a'})[5m] is null"); + MetricAggregateExpression metricExpression = (MetricAggregateExpression) ((BinaryExpression) expression).getLhs(); + Assertions.assertEquals("appName endsWith 'a'", metricExpression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); // startsWith require string literal Assertions.assertThrows(InvalidExpressionException.class, () -> MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName endsWith 5})[5m]")); @@ -192,8 +211,9 @@ public void test_EnsWithPredicateExpression() { @Test public void test_hasTokenPredicateExpression() { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName hasToken 'a', instanceName hasToken '192.'})[5m] > 1"); - Assertions.assertEquals("(appName hasToken 'a') AND (instanceName hasToken '192.')", expression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName hasToken 'a', instanceName hasToken '192.'})[5m] > 1"); + MetricAggregateExpression metricSelectExpression = (MetricAggregateExpression) ((ComparisonExpression) expression).getLhs(); + Assertions.assertEquals("(appName hasToken 'a') AND (instanceName hasToken '192.')", metricSelectExpression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); // hasToken require string literal Assertions.assertThrows(InvalidExpressionException.class, () -> MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName hasToken 5})[5m] > 1")); @@ -201,8 +221,9 @@ public void test_hasTokenPredicateExpression() { @Test public void test_RegexMatchPredicateExpression() { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{instanceName =~ '192.'})[5m] > 1"); - Assertions.assertEquals("instanceName =~ '192.'", expression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{instanceName =~ '192.'})[5m] > 1"); + MetricAggregateExpression metricSelectExpression = (MetricAggregateExpression) ((ComparisonExpression) expression).getLhs(); + Assertions.assertEquals("instanceName =~ '192.'", metricSelectExpression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); // hasToken require string literal Assertions.assertThrows(InvalidExpressionException.class, () -> MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{instanceName =~ ab})[5m] > 1")); @@ -210,8 +231,9 @@ public void test_RegexMatchPredicateExpression() { @Test public void test_RegexNotMatchPredicateExpression() { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{instanceName !~ '192.'})[5m] > 1"); - Assertions.assertEquals("instanceName !~ '192.'", expression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{instanceName !~ '192.'})[5m] > 1"); + MetricAggregateExpression metricSelectExpression = (MetricAggregateExpression) ((BinaryExpression) expression).getLhs(); + Assertions.assertEquals("instanceName !~ '192.'", metricSelectExpression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); // hasToken require string literal Assertions.assertThrows(InvalidExpressionException.class, () -> MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{instanceName !~ ab})[5m] > 1")); @@ -220,20 +242,24 @@ public void test_RegexNotMatchPredicateExpression() { @Test public void test_NotPredicateExpression() { // not contains - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName not contains 'a'})[5m] is null"); - Assertions.assertEquals("NOT (appName contains 'a')", expression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName not contains 'a'})[5m] is null"); + MetricAggregateExpression metricExpression = (MetricAggregateExpression) ((BinaryExpression) expression).getLhs(); + Assertions.assertEquals("NOT (appName contains 'a')", metricExpression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); // not startsWith - expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName not startsWith 'a'})[5m] is null"); - Assertions.assertEquals("NOT (appName startsWith 'a')", expression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); + expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName not startsWith 'a'})[5m] is null"); + metricExpression = (MetricAggregateExpression) ((BinaryExpression) expression).getLhs(); + Assertions.assertEquals("NOT (appName startsWith 'a')", metricExpression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); // not endsWith - expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName not endsWith 'a'})[5m] is null"); - Assertions.assertEquals("NOT (appName endsWith 'a')", expression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); + expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName not endsWith 'a'})[5m] is null"); + metricExpression = (MetricAggregateExpression) ((BinaryExpression) expression).getLhs(); + Assertions.assertEquals("NOT (appName endsWith 'a')", metricExpression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); // not hasToken - expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName not hasToken 'a'})[5m] is null"); - Assertions.assertEquals("NOT (appName hasToken 'a')", expression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); + expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName not hasToken 'a'})[5m] is null"); + metricExpression = (MetricAggregateExpression) ((BinaryExpression) expression).getLhs(); + Assertions.assertEquals("NOT (appName hasToken 'a')", metricExpression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); } @Test @@ -253,8 +279,9 @@ public void test_IsNullExpression() { @Test public void test_InExpression() { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName in ('a')})[5m] is null"); - Assertions.assertEquals("appName in ('a')", expression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName in ('a')})[5m] is null"); + MetricAggregateExpression metricSelectExpression = (MetricAggregateExpression) ((BinaryExpression) expression).getLhs(); + Assertions.assertEquals("appName in ('a')", metricSelectExpression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName in ('a', 'b')})[5m] is null"); MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName in (1)})[5m] is null"); @@ -265,8 +292,9 @@ public void test_InExpression() { @Test public void test_NotInExpression() { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName not in ('a')})[5m] is null"); - Assertions.assertEquals("appName not in ('a')", expression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName not in ('a')})[5m] is null"); + MetricAggregateExpression metricExpression = (MetricAggregateExpression) ((BinaryExpression) expression).getLhs(); + Assertions.assertEquals("appName not in ('a')", metricExpression.getLabelSelectorExpression().serializeToText(IdentifierQuotaStrategy.NONE)); MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName not in ('a', 'b')})[5m] is null"); @@ -288,9 +316,9 @@ public void test_AggregatorExpression() { @Test public void test_OffsetExpression() { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName contains 'a', instanceName contains '192.'})[5m] > 1%[-7m]"); - Assertions.assertNotNull(expression.getOffset()); - Assertions.assertEquals(-7, expression.getOffset().getDuration().toMinutes()); + ComparisonExpression expression = (ComparisonExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName contains 'a', instanceName contains '192.'})[5m] > 1%[-7m]"); + //Assertions.assertNotNull(expression.getOffset()); + //Assertions.assertEquals(-7, expression.getOffset().getDuration().toMinutes()); // Only percentage is supported now Assertions.assertThrows(InvalidExpressionException.class, () -> MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName contains 'a', instanceName contains '192.'})[5m] > 1[0m]")); @@ -305,21 +333,21 @@ public void test_OffsetExpression() { public void test_ExpressionSerialization() { // No filter { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu)[5m] > 10%[-7m]"); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu)[5m] > 10%[-7m]"); Assertions.assertEquals("avg(jvm-metrics.cpu)[5m] > 10%[-7m]", expression.serializeToText(IdentifierQuotaStrategy.NONE)); } // One filter { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName = 'a'})[5m] > 10%[-5m]"); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName = 'a'})[5m] > 10%[-5m]"); Assertions.assertEquals("avg(jvm-metrics.cpu{appName = \"a\"})[5m] > 10%[-5m]", expression.serializeToText(IdentifierQuotaStrategy.NONE)); } // Two filters { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName contains 'a', instanceName contains '192.'})[5m] > 10%[-5m]"); + IExpression expression = MetricExpressionASTBuilder.parse("avg(jvm-metrics.cpu{appName contains 'a', instanceName contains '192.'})[5m] > 10%[-5m]"); Assertions.assertEquals("avg(jvm-metrics.cpu{appName contains \"a\", instanceName contains \"192.\"})[5m] > 10%[-5m]", expression.serializeToText(IdentifierQuotaStrategy.NONE)); } @@ -327,7 +355,7 @@ public void test_ExpressionSerialization() { // count aggregator { - MetricExpression expression = (MetricExpression) MetricExpressionASTBuilder.parse("count( jvm-metrics.cpu{appName contains 'a', instanceName contains '192.'})[5m] > 1"); + IExpression expression = MetricExpressionASTBuilder.parse("count( jvm-metrics.cpu{appName contains 'a', instanceName contains '192.'})[5m] > 1"); Assertions.assertEquals("count(jvm-metrics.cpu{appName contains \"a\", instanceName contains \"192.\"})[5m] > 1", expression.serializeToText()); } @@ -341,21 +369,26 @@ public void test_InvalidMetricName() { @Test public void test_MultipleSelector() { - MetricExpression alertExpression = (MetricExpression) MetricExpressionASTBuilder.parse("avg(http-metrics.responseTime{appName='a', instance='localhost', url='http://localhost/test'})[5m] > 1%[-7m]"); - IExpression whereExpression = alertExpression.getLabelSelectorExpression(); + IExpression expression = MetricExpressionASTBuilder.parse("avg(http-metrics.responseTime{appName='a', instance='localhost', url='http://localhost/test'})[5m] > 1%[-7m]"); + MetricAggregateExpression metricExpression = (MetricAggregateExpression) ((ComparisonExpression) expression).getLhs(); + IExpression whereExpression = metricExpression.getLabelSelectorExpression(); Assertions.assertEquals("(appName = 'a') AND (instance = 'localhost') AND (url = 'http://localhost/test')", whereExpression.serializeToText(IdentifierQuotaStrategy.NONE)); } @Test public void test_ByExpression() { - MetricExpression expr = (MetricExpression) MetricExpressionASTBuilder.parse("avg (http-metrics.responseTime{appName='a'})[5m] by (instance) > 1"); - Assertions.assertEquals(Collections.singleton("instance"), expr.getGroupBy()); + IExpression expression = MetricExpressionASTBuilder.parse("avg (http-metrics.responseTime{appName='a'})[5m] by (instance) > 1"); + MetricAggregateExpression metricExpression = (MetricAggregateExpression) ((ComparisonExpression) expression).getLhs(); + + Assertions.assertEquals(Collections.singleton("instance"), metricExpression.getGroupBy()); - expr = (MetricExpression) MetricExpressionASTBuilder.parse("avg (http-metrics.responseTime{appName='a'})[5m] by (instance, url) > 1"); - Assertions.assertEquals(new HashSet<>(Arrays.asList("instance", "url")), expr.getGroupBy()); + expression = MetricExpressionASTBuilder.parse("avg (http-metrics.responseTime{appName='a'})[5m] by (instance, url) > 1"); + metricExpression = (MetricAggregateExpression) ((ComparisonExpression) expression).getLhs(); + Assertions.assertEquals(new HashSet<>(Arrays.asList("instance", "url")), metricExpression.getGroupBy()); - expr = (MetricExpression) MetricExpressionASTBuilder.parse("avg (http-metrics.responseTime{appName='a'})[5m] by (instance, url, method) > 1"); - Assertions.assertEquals(new HashSet<>(Arrays.asList("instance", "url", "method")), expr.getGroupBy()); + expression = MetricExpressionASTBuilder.parse("avg (http-metrics.responseTime{appName='a'})[5m] by (instance, url, method) > 1"); + metricExpression = (MetricAggregateExpression) ((ComparisonExpression) expression).getLhs(); + Assertions.assertEquals(new HashSet<>(Arrays.asList("instance", "url", "method")), metricExpression.getGroupBy()); } @Test @@ -501,14 +534,14 @@ public void test_DIV_BY_Zero() { } @Test - public void test_HybridExpression() { + public void test_HybridArithmeticExpression() { String expr = "avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] * " + "(avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] - avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m])"; IExpression ast = MetricExpressionASTBuilder.parse(expr); Assertions.assertInstanceOf(ArithmeticExpression.MUL.class, ast); ArithmeticExpression.MUL mul = (ArithmeticExpression.MUL) ast; - Assertions.assertInstanceOf(MetricExpression.class, mul.getLhs()); + Assertions.assertInstanceOf(MetricAggregateExpression.class, mul.getLhs()); Assertions.assertInstanceOf(ArithmeticExpression.SUB.class, mul.getRhs()); Assertions.assertEquals(expr, ast.serializeToText()); } @@ -526,4 +559,36 @@ public void test_ArithmeticPrecedence_2() { IExpression ast = MetricExpressionASTBuilder.parse(expr); Assertions.assertEquals("(4 * 5) + 6", ast.serializeToText()); } + + @Test + public void test_MetricArithmeticAndFilter_Precedence() { + String expr = "sum(jvm-metrics.cpu{appName = 'a'})[5m] + 6 * 4 > 5"; + IExpression ast = MetricExpressionASTBuilder.parse(expr); + Assertions.assertEquals("(sum(jvm-metrics.cpu{appName = \"a\"})[5m] + (6 * 4)) > 5", ast.serializeToText()); + } + + @Test + public void test_MetricSelectExpression() { + { + String expr = "jvm-metrics. cpu"; + Assertions.assertEquals("jvm-metrics.cpu", MetricExpressionASTBuilder.parse(expr).serializeToText()); + } + { + String expr = "jvm-metrics.cpu{appName = 'a'}"; + Assertions.assertEquals("jvm-metrics.cpu{appName = \"a\"}", MetricExpressionASTBuilder.parse(expr).serializeToText()); + } + { + String expr = "jvm-metrics.cpu{appName = 'a'}[ 5m ]"; + Assertions.assertEquals("jvm-metrics.cpu{appName = \"a\"}[5m]", MetricExpressionASTBuilder.parse(expr).serializeToText()); + } + } + + @Test + public void test_FunctionExpression() { + String expr = "irate(jvm-metrics.cpu{appName = 'a'} [5m] ) > 1"; + IExpression ast = MetricExpressionASTBuilder.parse(expr); + Assertions.assertInstanceOf(ComparisonExpression.class, ast); + Assertions.assertInstanceOf(FunctionExpression.class, ((BinaryExpression) ast).getLhs()); + Assertions.assertEquals("irate(jvm-metrics.cpu{appName = \"a\"}[5m]) > 1", ast.serializeToText()); + } } diff --git a/server/metric-expression/src/test/java/org/bithon/server/metric/expression/ast/MetricExpressionOptimizerTest.java b/server/metric-expression/src/test/java/org/bithon/server/metric/expression/ast/MetricExpressionOptimizerTest.java index b8fb85bd07..6ce08b027b 100644 --- a/server/metric-expression/src/test/java/org/bithon/server/metric/expression/ast/MetricExpressionOptimizerTest.java +++ b/server/metric-expression/src/test/java/org/bithon/server/metric/expression/ast/MetricExpressionOptimizerTest.java @@ -50,12 +50,12 @@ public void test_Optimize_ConstantFolding_Mul() { @Test public void test_Optimize_ConstantFolding_2() { - String expression = "1 + sum(dataSource.metric) + 2 + 3 + 4"; + String expression = "1 + sum(dataSource.metric) + 2 * 3 + 4"; IExpression ast = MetricExpressionASTBuilder.parse(expression); - Assertions.assertEquals("(((1 + sum(dataSource.metric)) + 2) + 3) + 4", ast.serializeToText()); + Assertions.assertEquals("((1 + sum(dataSource.metric)) + (2 * 3)) + 4", ast.serializeToText()); ast = MetricExpressionOptimizer.optimize(ast); - Assertions.assertEquals("sum(dataSource.metric) + 10", ast.serializeToText()); + Assertions.assertEquals("sum(dataSource.metric) + 11", ast.serializeToText()); } @Test diff --git a/server/metric-expression/src/test/java/org/bithon/server/metric/expression/pipeline/step/BinaryExpressionQueryStepTest.java b/server/metric-expression/src/test/java/org/bithon/server/metric/expression/pipeline/step/BinaryExpressionQueryStepTest.java deleted file mode 100644 index 710fe44b89..0000000000 --- a/server/metric-expression/src/test/java/org/bithon/server/metric/expression/pipeline/step/BinaryExpressionQueryStepTest.java +++ /dev/null @@ -1,2692 +0,0 @@ -/* - * Copyright 2020 bithon.org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.bithon.server.metric.expression.pipeline.step; - - -import org.bithon.component.commons.utils.HumanReadableDuration; -import org.bithon.component.commons.utils.HumanReadableNumber; -import org.bithon.server.commons.time.TimeSpan; -import org.bithon.server.datasource.query.pipeline.Column; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; -import org.bithon.server.datasource.query.pipeline.DoubleColumn; -import org.bithon.server.datasource.query.pipeline.IQueryStep; -import org.bithon.server.datasource.query.pipeline.LongColumn; -import org.bithon.server.datasource.query.pipeline.PipelineQueryResult; -import org.bithon.server.datasource.query.pipeline.StringColumn; -import org.bithon.server.metric.expression.pipeline.QueryPipelineBuilder; -import org.bithon.server.web.service.datasource.api.IDataSourceApi; -import org.bithon.server.web.service.datasource.api.IntervalRequest; -import org.bithon.server.web.service.datasource.api.QueryRequest; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -/** - * @author frank.chen021@outlook.com - * @date 4/4/25 9:27 pm - */ -@SuppressWarnings("PointlessArithmeticExpression") -public class BinaryExpressionQueryStepTest { - private IDataSourceApi dataSourceApi; - - @BeforeEach - public void setUpClass() { - dataSourceApi = Mockito.mock(IDataSourceApi.class); - } - - @Test - public void test_ScalarOverLiteral_Add_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 1))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] + 5"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(6, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverLiteral_Add_Long_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 1))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] + 3.3"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(4.3, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverLiteral_Add_Double_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - DoubleColumn.of("activeThreads", 3.7))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] + 5"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(8.7, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverLiteral_Add_Double_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - DoubleColumn.of("activeThreads", 10.5))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] + 2.2"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(12.7, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverSizeLiteral_Add_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 1))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] + 5Mi"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(HumanReadableNumber.of("5Mi").longValue() + 1, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverPercentageLiteral_Add_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 1))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] + 90%"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(1.9, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverDurationLiteral_Add_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 1))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] + 1h"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(3601, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverLiteral_Sub_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 1))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] - 5"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(-4, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverLiteral_Sub_Double_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - DoubleColumn.of("activeThreads", 10.5))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] - 2.2"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(8.3, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverLiteral_Mul_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 1))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] * 5"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(5, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverLiteral_Mul_Double_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - DoubleColumn.of("activeThreads", 5.5))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] * 5"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(27.5, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverLiteral_Mul_Long_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 1))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] * 5.5"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(5.5, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverLiteral_Mul_Double_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - DoubleColumn.of("activeThreads", 3.5))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] * 3"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(10.5, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverLiteral_Div_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 10))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] / 5"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverLiteral_Div_Double_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - DoubleColumn.of("activeThreads", 10))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] / 20"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(0.5, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverLiteral_Div_Long_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 10))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] / 20.0"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(0.5, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverLiteral_Div_Double_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenReturn(ColumnarTable.of(LongColumn.of("_timestamp", 1), - DoubleColumn.of("activeThreads", 10.5))); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] / 3.0"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valueCol = response.getTable().getColumn("value"); - Assertions.assertEquals(3.5, valueCol.getDouble(0), .0000000001); - } - - @Test - public void test_VectorOverLiteral_Add_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("activeThreads", 5, 20, 25) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 5) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "+" - + "5"); - PipelineQueryResult response = evaluator.execute().get(); - - Column values = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, values.size()); - Assertions.assertEquals(5 + 5, values.getDouble(0), .0000000001); - Assertions.assertEquals(20 + 5, values.getDouble(1), .0000000001); - Assertions.assertEquals(25 + 5, values.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverLiteral_Sub_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("activeThreads", 5, 20, 25) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 5) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "-" - + " 5"); - PipelineQueryResult response = evaluator.execute().get(); - - Column values = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, values.size()); - Assertions.assertEquals(5 - 5, values.getDouble(0), .0000000001); - Assertions.assertEquals(20 - 5, values.getDouble(1), .0000000001); - Assertions.assertEquals(25 - 5, values.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverLiteral_Mul_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("activeThreads", 5, 20, 25) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 5) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "*" - + "5"); - PipelineQueryResult response = evaluator.execute().get(); - - Column values = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, values.size()); - Assertions.assertEquals(5 * 5, values.getDouble(0), .0000000001); - Assertions.assertEquals(20 * 5, values.getDouble(1), .0000000001); - Assertions.assertEquals(25 * 5, values.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverLiteral_Div_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("activeThreads", 5, 24, 25) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 5) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "/" - + "5"); - PipelineQueryResult response = evaluator.execute().get(); - - Column values = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, values.size()); - Assertions.assertEquals(5 / 5, values.getLong(0)); - Assertions.assertEquals(24 / 5, values.getLong(1)); - Assertions.assertEquals(25 / 5, values.getLong(2)); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_ScalarOverScalar_Add_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 1) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 11) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" - + "+" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(12, valCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverScalar_Sub_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 1) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 11) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" - + "-" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(-10, valCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverScalar_Mul_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 2) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 11) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" - + "*" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(22, valCol.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverScalar_Div_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 55) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 11) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" - + "/" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column values = response.getTable().getColumn("value"); - Assertions.assertEquals(5, values.getDouble(0), .0000000001); - } - - @Test - public void test_ScalarOverVector_Add_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 3) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("totalThreads", 5, 6, 7) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" - + "+" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("totalThreads"); - Assertions.assertEquals(3, valCol.size()); - Assertions.assertEquals(8, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(9, valCol.getDouble(1), .0000000001); - Assertions.assertEquals(10, valCol.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_ScalarOverVector_Sub_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 3) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("totalThreads", 5, 6, 7) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" - + "-" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("totalThreads"); - Assertions.assertEquals(3, valCol.size()); - Assertions.assertEquals(-2, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(-3, valCol.getDouble(1), .0000000001); - Assertions.assertEquals(-4, valCol.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_ScalarOverVector_Mul_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 3) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("totalThreads", 5, 6, 7) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" - + "*" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("totalThreads"); - Assertions.assertEquals(3, valCol.size()); - Assertions.assertEquals(15, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(18, valCol.getDouble(1), .0000000001); - Assertions.assertEquals(21, valCol.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_ScalarOverVector_Div_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("activeThreads", 100) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("totalThreads", 5, 20, 25) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" - + "/" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("totalThreads"); - Assertions.assertEquals(3, valCol.size()); - Assertions.assertEquals(20, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(5, valCol.getDouble(1), .0000000001); - Assertions.assertEquals(4, valCol.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_ScalarOverVector_Div_Long_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - DoubleColumn.of("activeThreads", 10) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - DoubleColumn.of("totalThreads", 5, 20, 25) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" - + "/" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("totalThreads"); - Assertions.assertEquals(3, valCol.size()); - Assertions.assertEquals(2, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(0.5, valCol.getDouble(1), .0000000001); - Assertions.assertEquals(0.4, valCol.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_ScalarOverVector_Div_Double_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - DoubleColumn.of("activeThreads", 10) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - DoubleColumn.of("totalThreads", 5, 20, 25) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" - + "/" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("totalThreads"); - Assertions.assertEquals(3, valCol.size()); - Assertions.assertEquals(2, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(0.5, valCol.getDouble(1), .0000000001); - Assertions.assertEquals(0.4, valCol.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverScalar_Add_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("activeThreads", 5, 20, 25) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 5) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "+" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column values = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, values.size()); - Assertions.assertEquals(10, values.getDouble(0), .0000000001); - Assertions.assertEquals(25, values.getDouble(1), .0000000001); - Assertions.assertEquals(30, values.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverScalar_Add_Long_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("activeThreads", 5, 20, 25) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - DoubleColumn.of("totalThreads", 5.7) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "+" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column values = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, values.size()); - Assertions.assertEquals(10.7, values.getDouble(0), .0000000001); - Assertions.assertEquals(25.7, values.getDouble(1), .0000000001); - Assertions.assertEquals(30.7, values.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverScalar_Add_Double_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - DoubleColumn.of("activeThreads", 5.5, 20.6, 25.7) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 5) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "+" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column values = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, values.size()); - Assertions.assertEquals(10.5, values.getDouble(0), .0000000001); - Assertions.assertEquals(25.6, values.getDouble(1), .0000000001); - Assertions.assertEquals(30.7, values.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverScalar_Sub_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("activeThreads", 3, 4, 5) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 5) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "-" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column values = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, values.size()); - Assertions.assertEquals(-2, values.getDouble(0), .0000000001); - Assertions.assertEquals(-1, values.getDouble(1), .0000000001); - Assertions.assertEquals(0, values.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverScalar_Sub_Long_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("activeThreads", 3, 4, 5) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - DoubleColumn.of("totalThreads", 5.5) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "-" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column values = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, values.size()); - Assertions.assertEquals(-2.5, values.getDouble(0), .0000000001); - Assertions.assertEquals(-1.5, values.getDouble(1), .0000000001); - Assertions.assertEquals(-0.5, values.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverScalar_Sub_Double_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - DoubleColumn.of("activeThreads", 3.5, 4.5, 5.5) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 5) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "-" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column values = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, values.size()); - Assertions.assertEquals(-1.5, values.getDouble(0), .0000000001); - Assertions.assertEquals(-0.5, values.getDouble(1), .0000000001); - Assertions.assertEquals(0.5, values.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverScalar_Mul_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("activeThreads", 3, 4, 5) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 3) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "*" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, valCol.size()); - Assertions.assertEquals(9, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(12, valCol.getDouble(1), .0000000001); - Assertions.assertEquals(15, valCol.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverScalar_Mul_Long_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("activeThreads", 3, 4, 5) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - DoubleColumn.of("totalThreads", 3.5) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "*" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, valCol.size()); - Assertions.assertEquals(10.5, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(14, valCol.getDouble(1), .0000000001); - Assertions.assertEquals(17.5, valCol.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverScalar_Mul_Double_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - DoubleColumn.of("activeThreads", 3.5, 4.5, 5.5) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 3) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "*" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, valCol.size()); - Assertions.assertEquals(10.5, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(13.5, valCol.getDouble(1), .0000000001); - Assertions.assertEquals(16.5, valCol.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverScalar_Div_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("activeThreads", 55, 60, 77) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 11) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "/" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, valCol.size()); - Assertions.assertEquals(5, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(5, valCol.getDouble(1), .0000000001); - Assertions.assertEquals(7, valCol.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverScalar_Div_Long_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - LongColumn.of("activeThreads", 20, 25, 50) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - DoubleColumn.of("totalThreads", 50.0) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "/" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, valCol.size()); - Assertions.assertEquals(0.4, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(0.5, valCol.getDouble(1), .0000000001); - Assertions.assertEquals(1, valCol.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverScalar_Div_Double_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest request = answer.getArgument(0, QueryRequest.class); - - String metric = request.getFields().get(0).getName(); - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - DoubleColumn.of("activeThreads", 20, 25, 50) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1), - LongColumn.of("totalThreads", 50) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "/" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); - PipelineQueryResult response = evaluator.execute().get(); - - Column valCol = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(3, valCol.size()); - Assertions.assertEquals(0.4, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(0.5, valCol.getDouble(1), .0000000001); - Assertions.assertEquals(1, valCol.getDouble(2), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(3, dimCol.size()); - Assertions.assertEquals("app1", dimCol.getString(0)); - Assertions.assertEquals("app2", dimCol.getString(1)); - Assertions.assertEquals("app3", dimCol.getString(2)); - } - - @Test - public void test_VectorOverVector_Add_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - LongColumn.of("activeThreads", 1, 5, 9) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - LongColumn.of("totalThreads", 21, 32, 43) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "+" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(1 + 21, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(5 + 32, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_Add_Long_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - LongColumn.of("activeThreads", 1, 5, 9) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - DoubleColumn.of("totalThreads", 21.5, 32.6, 43.7) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "+" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(1 + 21.5, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(5 + 32.6, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_Add_Double_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - DoubleColumn.of("activeThreads", 1.1, 5.2, 9.3) - ); - - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - DoubleColumn.of("totalThreads", 21.5, 32.6, 43.7) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "+" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(1.1 + 21.5, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(5.2 + 32.6, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_Sub_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - LongColumn.of("activeThreads", 1, 5, 9) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - LongColumn.of("totalThreads", 21, 32, 43) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "-" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(1 - 21, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(5 - 32, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_Sub_Long_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - LongColumn.of("activeThreads", 1, 5, 9) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - DoubleColumn.of("totalThreads", 21.1, 32.2, 43.3) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "-" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(1 - 21.1, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(5 - 32.2, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_Sub_Double_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - DoubleColumn.of("activeThreads", 1.1, 5.5, 9.9) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - LongColumn.of("totalThreads", 21, 32, 43) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "-" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(1.1 - 21, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(5.5 - 32, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_Sub_Double_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - DoubleColumn.of("activeThreads", 1.4, 5.5, 9.6) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - DoubleColumn.of("totalThreads", 21.1, 32.2, 43.3) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "-" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(1.4 - 21.1, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(5.5 - 32.2, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_Mul_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - LongColumn.of("activeThreads", 1, 5, 9) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - LongColumn.of("totalThreads", 21, 32, 43) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "*" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(1 * 21, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(5 * 32, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_Mul_Long_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - LongColumn.of("activeThreads", 1, 5, 9) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - DoubleColumn.of("totalThreads", 21.2, 32.3, 43.3) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "*" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(1 * 21.2, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(5 * 32.3, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_Mul_Double_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - DoubleColumn.of("activeThreads", 1.1, 5.2, 9.3) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - LongColumn.of("totalThreads", 21, 32, 43) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "*" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(1.1 * 21, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(5.2 * 32, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_Mul_Double_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - DoubleColumn.of("activeThreads", 1.1, 5.2, 9.3) - ); - - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - DoubleColumn.of("totalThreads", 21.1, 32.2, 43.3) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "*" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(1.1 * 21.1, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(5.2 * 32.2, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_Div_Long_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - LongColumn.of("activeThreads", 50, 100, 200) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - LongColumn.of("totalThreads", 25, 50, 100) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "/" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(50.0 / 25, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(100.0 / 50, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_Div_Long_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - LongColumn.of("activeThreads", 50, 100, 200) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - DoubleColumn.of("totalThreads", 25.5, 50.6, 100.6) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "/" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(50 / 25.5, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(100 / 50.6, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_Div_Double_Long() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - DoubleColumn.of("activeThreads", 12, 25, 200) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - LongColumn.of("totalThreads", 25, 50, 100) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "/" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals((double) 12 / 25, valCol.getDouble(0), .0000000001); - Assertions.assertEquals((double) 25 / 50, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_Div_Double_Double() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app1"), - DoubleColumn.of("activeThreads", 12.1, 25.2, 200.3) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app2", "app3", "app4"), - DoubleColumn.of("totalThreads", 25.6, 50.7, 100.8) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "/" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(12.1 / 25.6, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(25.2 / 50.7, valCol.getDouble(1), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(2, dimCol.size()); - Assertions.assertEquals("app2", dimCol.getString(0)); - Assertions.assertEquals("app3", dimCol.getString(1)); - } - - @Test - public void test_VectorOverVector_NoIntersection() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - DoubleColumn.of("activeThreads", 12.1, 25.2, 200.3) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app4", "app5", "app6"), - DoubleColumn.of("totalThreads", 25.6, 50.7, 100.8) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "/" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(0, valCol.size()); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(0, dimCol.size()); - } - - /** - * TODO: optimization, in this case, the final expression should be optimized to empty - * 1 and 2 have no intersection - */ - @Test - public void test_VectorOverVector_NoIntersection_2() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - DoubleColumn.of("activeThreads", 12.1, 25.2, 200.3) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 4, 5, 6), - StringColumn.of("appName", "app4", "app5", "app6"), - DoubleColumn.of("totalThreads", 25.6, 50.7, 100.8) - ); - } - - if ("newThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 6, 7), - StringColumn.of("appName", "app6", "app7"), - DoubleColumn.of("newThreads", 106, 107) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "/" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "+" - + "avg(jvm-metrics.newThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(0, valCol.size()); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(0, dimCol.size()); - } - - /** - * TODO: optimization, in this case, the final expression should be optimized to empty - * 1 and 2 have intersection while 2 and 3 has no intersection - */ - @Test - public void test_VectorOverVector_NoIntersection_3() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - DoubleColumn.of("activeThreads", 12.1, 25.2, 200.3) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 3, 4), - StringColumn.of("appName", "app3", "app4"), - DoubleColumn.of("totalThreads", 25.6, 50.7) - ); - } - - if ("newThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 6, 7), - StringColumn.of("appName", "app6", "app7"), - DoubleColumn.of("newThreads", 106, 107) - ); - } - - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "/" - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" - + "+" - + "avg(jvm-metrics.newThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(0, valCol.size()); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(0, dimCol.size()); - } - - @Test - public void test_MultipleExpressions() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - String metric = answer.getArgument(0, QueryRequest.class) - .getFields() - .get(0).getName(); - - if ("activeThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - DoubleColumn.of("activeThreads", 3, 4, 5) - ); - } - if ("totalThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 2, 3, 4), - StringColumn.of("appName", "app2", "app3", "app4"), - DoubleColumn.of("totalThreads", 25, 26, 27) - ); - } - - if ("newThreads".equals(metric)) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 3, 4, 5), - StringColumn.of("appName", "app3", "app4", "app5"), - DoubleColumn.of("newThreads", 35, 36, 37) - ); - } - throw new IllegalArgumentException("Invalid metric: " + metric); - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName) " - + "/ " - + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName) " - + "* " - + "avg(jvm-metrics.newThreads{appName = \"bithon-web-'local\"})[1m] by (appName) " - + "+ 5"); - PipelineQueryResult response = evaluator.execute().get(); - - // Only the overlapped series will be returned - Column valCol = response.getTable().getColumn("value"); - Assertions.assertEquals(1, valCol.size()); - Assertions.assertEquals(5.0 / 26 * 35 + 5, valCol.getDouble(0), .0000000001); - - Column dimCol = response.getTable().getColumn("appName"); - Assertions.assertEquals(1, dimCol.size()); - Assertions.assertEquals("app3", dimCol.getString(0)); - } - - @Test - public void test_RelativeComparison() throws Exception { - Mockito.when(dataSourceApi.timeseriesV5(Mockito.any())) - .thenAnswer((answer) -> { - QueryRequest req = answer.getArgument(0, QueryRequest.class); - - HumanReadableDuration offset = req.getOffset(); - if (offset == null) { - return ColumnarTable.of( - LongColumn.of("_timestamp", 1, 2, 3), - StringColumn.of("appName", "app1", "app2", "app3"), - DoubleColumn.of("activeThreads", 3, 4, 5) - ); - } - - return ColumnarTable.of( - LongColumn.of("_timestamp", 2, 3, 4), - StringColumn.of("appName", "app2", "app3", "app4"), - DoubleColumn.of("-1d", 21, 22, 23) - ); - - }); - - IQueryStep evaluator = QueryPipelineBuilder.builder() - .dataSourceApi(dataSourceApi) - .intervalRequest(IntervalRequest.builder() - .bucketCount(1) - .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) - .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) - .build()) - // BY is given so that it produces a vector - .build("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName) > -5%[-1d]"); - PipelineQueryResult response = evaluator.execute().get(); - Assertions.assertEquals(2, response.getRows()); - - // Only the overlapped series(app2,app3) will be returned - { - Column valCol = response.getTable().getColumn("activeThreads"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(4, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(5, valCol.getDouble(1), .0000000001); - } - { - Column valCol = response.getTable().getColumn("-1d"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals(21, valCol.getDouble(0), .0000000001); - Assertions.assertEquals(22, valCol.getDouble(1), .0000000001); - } - { - Column valCol = response.getTable().getColumn("delta"); - Assertions.assertEquals(2, valCol.size()); - Assertions.assertEquals((4.0 - 21) / 21, valCol.getDouble(0), .0000000001); - Assertions.assertEquals((5.0 - 22) / 22, valCol.getDouble(1), .0000000001); - } - } -} diff --git a/server/metric-expression/src/test/java/org/bithon/server/metric/expression/plan/MetricExpressionPhysicalPlannerTest.java b/server/metric-expression/src/test/java/org/bithon/server/metric/expression/plan/MetricExpressionPhysicalPlannerTest.java new file mode 100644 index 0000000000..3a2e84afc0 --- /dev/null +++ b/server/metric-expression/src/test/java/org/bithon/server/metric/expression/plan/MetricExpressionPhysicalPlannerTest.java @@ -0,0 +1,3589 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.metric.expression.plan; + +import org.bithon.component.commons.utils.HumanReadableDuration; +import org.bithon.component.commons.utils.HumanReadableNumber; +import org.bithon.server.commons.time.TimeSpan; +import org.bithon.server.datasource.DefaultSchema; +import org.bithon.server.datasource.ISchema; +import org.bithon.server.datasource.ISchemaProvider; +import org.bithon.server.datasource.TimestampSpec; +import org.bithon.server.datasource.column.ExpressionColumn; +import org.bithon.server.datasource.column.aggregatable.last.AggregateLongLastColumn; +import org.bithon.server.datasource.column.aggregatable.sum.AggregateLongSumColumn; +import org.bithon.server.datasource.query.IDataSourceReader; +import org.bithon.server.datasource.query.plan.logical.LogicalAggregate; +import org.bithon.server.datasource.query.plan.logical.LogicalTableScan; +import org.bithon.server.datasource.query.plan.physical.IPhysicalPlan; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.ColumnarTable; +import org.bithon.server.datasource.query.result.DoubleColumn; +import org.bithon.server.datasource.query.result.LongColumn; +import org.bithon.server.datasource.query.result.PipelineQueryResult; +import org.bithon.server.datasource.query.result.StringColumn; +import org.bithon.server.datasource.store.IDataStoreSpec; +import org.bithon.server.web.service.datasource.api.IDataSourceApi; +import org.bithon.server.web.service.datasource.api.IntervalRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * @author frank.chen021@outlook.com + * @date 4/4/25 9:27 pm + */ +@SuppressWarnings("PointlessArithmeticExpression") +public class MetricExpressionPhysicalPlannerTest { + + private ISchema schema; + private ISchemaProvider schemaProvider; + private IDataSourceApi dataSourceApi; + private IDataSourceReader dataSourceReader; + + @BeforeEach + public void setUpClass() { + dataSourceApi = Mockito.mock(IDataSourceApi.class); + dataSourceReader = Mockito.mock(IDataSourceReader.class); + schema = new DefaultSchema("bithon-jvm-metrics", + "bithon-jvm-metrics", + new TimestampSpec("timestamp"), + Arrays.asList(new org.bithon.server.datasource.column.StringColumn("appName", "appName"), + new org.bithon.server.datasource.column.StringColumn("instance", "instance")), + Arrays.asList(new AggregateLongSumColumn("responseTime", "responseTime"), + new AggregateLongSumColumn("totalCount", "totalCount"), + new AggregateLongSumColumn("count4xx", "count4xx"), + new AggregateLongSumColumn("count5xx", "count5xx"), + new AggregateLongLastColumn("activeThreads", "activeThreads"), + new AggregateLongLastColumn("totalThreads", "totalThreads"), + new ExpressionColumn("avgResponseTime", + null, + "sum(responseTime) / sum(totalCount)", + "double") + ), + null, + new IDataStoreSpec() { + @Override + public String getStore() { + return "bithon_jvm_metrics"; + } + + @Override + public void setSchema(ISchema schema) { + + } + + @Override + public boolean isInternal() { + return false; + } + + @Override + public IDataSourceReader createReader() { + return dataSourceReader; + } + }, + null, + null); + this.schemaProvider = name -> schema; + } + + static class MockReadStep implements IPhysicalPlan { + private final PipelineQueryResult result; + private final boolean isScalar; + private final HumanReadableDuration offset; + + MockReadStep(PipelineQueryResult result) { + this.result = result; + this.isScalar = false; + this.offset = null; + } + + MockReadStep(PipelineQueryResult result, boolean isScalar) { + this.result = result; + this.isScalar = isScalar; + this.offset = null; + } + + MockReadStep(PipelineQueryResult result, boolean isScalar, HumanReadableDuration offset) { + this.result = result; + this.isScalar = isScalar; + this.offset = offset; + } + + @Override + public IPhysicalPlan offset(HumanReadableDuration offset) { + return new MockReadStep(result, isScalar, offset); + } + + @Override + public boolean isScalar() { + return this.isScalar; + } + + @Override + public boolean canPushDownAggregate(LogicalAggregate aggregate) { + return true; + } + + @Override + public IPhysicalPlan pushDownAggregate(LogicalAggregate aggregate) { + return this; + } + + @Override + public CompletableFuture execute() { + return CompletableFuture.completedFuture(this.result); + } + } + + @Test + public void test_Arithmetic_ScalarOverLiteral_Add_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 1))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] + 5"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(6, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverLiteral_Add_Long_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 1))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] + 3.3"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(4.3, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverLiteral_Add_Double_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + DoubleColumn.of("activeThreads", 3.7))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] + 5"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(8.7, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverLiteral_Add_Double_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + DoubleColumn.of("activeThreads", 10.5))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] + 2.2"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(12.7, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverSizeLiteral_Add_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 1))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] + 5Mi"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(HumanReadableNumber.of("5Mi").longValue() + 1, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverPercentageLiteral_Add_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 1))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] + 90%"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(1.9, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverDurationLiteral_Add_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 1))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] + 1h"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3601, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverLiteral_Sub_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 1))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] - 5"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(-4, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverLiteral_Sub_Double_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + DoubleColumn.of("activeThreads", 10.5))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] - 2.2"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(8.3, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverLiteral_Mul_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 1))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] * 5"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(5, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverLiteral_Mul_Double_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + DoubleColumn.of("activeThreads", 5.5))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] * 5"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(27.5, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverLiteral_Mul_Long_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 1))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] * 5.5"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(5.5, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverLiteral_Mul_Double_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + DoubleColumn.of("activeThreads", 3.5))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] * 3"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(10.5, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverLiteral_Div_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 10))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] / 5"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(2, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverLiteral_Div_Double_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + DoubleColumn.of("activeThreads", 10))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] / 20"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(0.5, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverLiteral_Div_Long_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 10))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] / 20.0"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(0.5, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverLiteral_Div_Double_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of(LongColumn.of("_timestamp", 1), + DoubleColumn.of("activeThreads", 10.5))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] / 3.0"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valueCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3.5, valueCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_VectorOverLiteral_Add_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("activeThreads", 5, 20, 25))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .timeSeries("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "+" + + "5"); + PipelineQueryResult response = evaluator.execute().get(); + + Column values = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, values.size()); + Assertions.assertEquals(5 + 5, values.getDouble(0), .0000000001); + Assertions.assertEquals(20 + 5, values.getDouble(1), .0000000001); + Assertions.assertEquals(25 + 5, values.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverLiteral_Sub_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("activeThreads", 5, 20, 25))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .timeSeries("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "-" + + " 5"); + PipelineQueryResult response = evaluator.execute().get(); + + Column values = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, values.size()); + Assertions.assertEquals(5 - 5, values.getDouble(0), .0000000001); + Assertions.assertEquals(20 - 5, values.getDouble(1), .0000000001); + Assertions.assertEquals(25 - 5, values.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverLiteral_Mul_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("activeThreads", 5, 20, 25))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .timeSeries("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "*" + + "5"); + PipelineQueryResult response = evaluator.execute().get(); + + Column values = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, values.size()); + Assertions.assertEquals(5 * 5, values.getDouble(0), .0000000001); + Assertions.assertEquals(20 * 5, values.getDouble(1), .0000000001); + Assertions.assertEquals(25 * 5, values.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverLiteral_Div_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenReturn(new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("activeThreads", 5, 24, 25))) + .build())); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .timeSeries("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "/" + + "5"); + PipelineQueryResult response = evaluator.execute().get(); + + Column values = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, values.size()); + Assertions.assertEquals(5 / 5, values.getLong(0)); + Assertions.assertEquals(24 / 5, values.getLong(1)); + Assertions.assertEquals(25 / 5, values.getLong(2)); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_ScalarOverScalar_Add_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 1) + )) + .build(), + true); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("totalThreads", 11) + )) + .build(), + true); + } + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" + + "+" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(12, valCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverScalar_Sub_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 1) + )) + .build(), + true); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("totalThreads", 11) + )) + .build(), + true); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" + + "-" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(-10, valCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverScalar_Mul_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 2) + )) + .build(), + true); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("totalThreads", 11) + )) + .build(), + true); + } + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" + + "*" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(22, valCol.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverScalar_Div_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 55) + )) + .build(), + true); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("totalThreads", 11) + )) + .build(), + true); + } + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" + + "/" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column values = response.getTable().getColumn("value"); + Assertions.assertEquals(5, values.getDouble(0), .0000000001); + } + + @Test + public void test_Arithmetic_ScalarOverVector_Add_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 3) + )) + .build(), + true); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("totalThreads", 5, 6, 7) + )) + .build(), + false); + } + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" + + "+" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("totalThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(8, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(9, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(10, valCol.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_ScalarOverVector_Sub_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 3) + )) + .build(), + true); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("totalThreads", 5, 6, 7) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" + + "-" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("totalThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(-2, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(-3, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(-4, valCol.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_ScalarOverVector_Mul_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 3) + )) + .build(), + true); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("totalThreads", 5, 6, 7) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" + + "*" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("totalThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(15, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(18, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(21, valCol.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_ScalarOverVector_Div_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("activeThreads", 100) + )) + .build(), + true); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("totalThreads", 5, 20, 25) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" + + "/" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("totalThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(20, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(4, valCol.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_ScalarOverVector_Div_Long_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + DoubleColumn.of("activeThreads", 10) + )) + .build(), + true); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + DoubleColumn.of("totalThreads", 5, 20, 25) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" + + "/" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("totalThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(2, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(0.5, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(0.4, valCol.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_ScalarOverVector_Div_Double_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("activeThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + DoubleColumn.of("activeThreads", 10) + )) + .build(), + true); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + DoubleColumn.of("totalThreads", 5, 20, 25) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m]" + + "/" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("totalThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(2, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(0.5, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(0.4, valCol.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverScalar_Add_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("activeThreads", 5, 20, 25) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("totalThreads", 5) + )) + .build(), + true); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "+" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column values = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, values.size()); + Assertions.assertEquals(10, values.getDouble(0), .0000000001); + Assertions.assertEquals(25, values.getDouble(1), .0000000001); + Assertions.assertEquals(30, values.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverScalar_Add_Long_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("activeThreads", 5, 20, 25) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + DoubleColumn.of("totalThreads", 5.7) + )) + .build(), + true); + } + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "+" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column values = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, values.size()); + Assertions.assertEquals(10.7, values.getDouble(0), .0000000001); + Assertions.assertEquals(25.7, values.getDouble(1), .0000000001); + Assertions.assertEquals(30.7, values.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverScalar_Add_Double_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + DoubleColumn.of("activeThreads", 5.5, 20.6, 25.7) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("totalThreads", 5) + )) + .build(), + true); + } + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "+" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column values = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, values.size()); + Assertions.assertEquals(10.5, values.getDouble(0), .0000000001); + Assertions.assertEquals(25.6, values.getDouble(1), .0000000001); + Assertions.assertEquals(30.7, values.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverScalar_Sub_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("activeThreads", 3, 4, 5) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("totalThreads", 5) + )) + .build(), + true); + } + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "-" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column values = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, values.size()); + Assertions.assertEquals(-2, values.getDouble(0), .0000000001); + Assertions.assertEquals(-1, values.getDouble(1), .0000000001); + Assertions.assertEquals(0, values.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverScalar_Sub_Long_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("activeThreads", 3, 4, 5) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + DoubleColumn.of("totalThreads", 5.5) + )) + .build(), + true); + } + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "-" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column values = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, values.size()); + Assertions.assertEquals(-2.5, values.getDouble(0), .0000000001); + Assertions.assertEquals(-1.5, values.getDouble(1), .0000000001); + Assertions.assertEquals(-0.5, values.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverScalar_Sub_Double_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + DoubleColumn.of("activeThreads", 3.5, 4.5, 5.5) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("totalThreads", 5) + )) + .build(), + true); + } + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "-" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column values = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, values.size()); + Assertions.assertEquals(-1.5, values.getDouble(0), .0000000001); + Assertions.assertEquals(-0.5, values.getDouble(1), .0000000001); + Assertions.assertEquals(0.5, values.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverScalar_Mul_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("activeThreads", 3, 4, 5) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("totalThreads", 3) + )) + .build(), + true); + } + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "*" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(9, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(12, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(15, valCol.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverScalar_Mul_Long_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("activeThreads", 3, 4, 5) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + DoubleColumn.of("totalThreads", 3.5) + )) + .build(), + true); + } + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "*" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(10.5, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(14, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(17.5, valCol.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverScalar_Mul_Double_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + DoubleColumn.of("activeThreads", 3.5, 4.5, 5.5) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("totalThreads", 3) + )) + .build(), + true); + } + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "*" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(10.5, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(13.5, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(16.5, valCol.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverScalar_Div_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("activeThreads", 55, 60, 77) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("totalThreads", 11) + )) + .build(), + true); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "/" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(5, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(7, valCol.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverScalar_Div_Long_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + LongColumn.of("activeThreads", 20, 25, 50) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + DoubleColumn.of("totalThreads", 50.0) + )) + .build(), + true); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "/" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(0.4, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(0.5, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(1, valCol.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverScalar_Div_Double_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + DoubleColumn.of("activeThreads", 20, 25, 50) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp")) + .valColumns(List.of("totalThreads")) + .rows(1) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1), + LongColumn.of("totalThreads", 50) + )) + .build(), + true); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "/" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m]"); + PipelineQueryResult response = evaluator.execute().get(); + + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(0.4, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(0.5, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(1, valCol.getDouble(2), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(3, dimCol.size()); + Assertions.assertEquals("app1", dimCol.getString(0)); + Assertions.assertEquals("app2", dimCol.getString(1)); + Assertions.assertEquals("app3", dimCol.getString(2)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Add_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + LongColumn.of("activeThreads", 1, 5, 9) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + LongColumn.of("totalThreads", 21, 32, 43) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "+" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(1 + 21, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5 + 32, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Add_Long_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + LongColumn.of("activeThreads", 1, 5, 9) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + DoubleColumn.of("totalThreads", 21.5, 32.6, 43.7) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "+" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(1 + 21.5, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5 + 32.6, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Add_Double_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + DoubleColumn.of("activeThreads", 1.1, 5.2, 9.3) + )) + .build(), + false); + + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + DoubleColumn.of("totalThreads", 21.5, 32.6, 43.7) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "+" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(1.1 + 21.5, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5.2 + 32.6, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Sub_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + LongColumn.of("activeThreads", 1, 5, 9) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + LongColumn.of("totalThreads", 21, 32, 43) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "-" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(1 - 21, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5 - 32, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Sub_Long_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + LongColumn.of("activeThreads", 1, 5, 9) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + DoubleColumn.of("totalThreads", 21.1, 32.2, 43.3) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "-" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(1 - 21.1, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5 - 32.2, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Sub_Double_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + DoubleColumn.of("activeThreads", 1.1, 5.5, 9.9) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + LongColumn.of("totalThreads", 21, 32, 43) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "-" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(1.1 - 21, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5.5 - 32, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Sub_Double_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + DoubleColumn.of("activeThreads", 1.4, 5.5, 9.6) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + DoubleColumn.of("totalThreads", 21.1, 32.2, 43.3) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "-" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(1.4 - 21.1, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5.5 - 32.2, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Mul_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + LongColumn.of("activeThreads", 1, 5, 9) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + LongColumn.of("totalThreads", 21, 32, 43) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "*" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(1 * 21, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5 * 32, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Mul_Long_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + LongColumn.of("activeThreads", 1, 5, 9) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + DoubleColumn.of("totalThreads", 21.2, 32.3, 43.3) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "*" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(1 * 21.2, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5 * 32.3, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Mul_Double_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + DoubleColumn.of("activeThreads", 1.1, 5.2, 9.3) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + LongColumn.of("totalThreads", 21, 32, 43) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "*" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(1.1 * 21, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5.2 * 32, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Mul_Double_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + DoubleColumn.of("activeThreads", 1.1, 5.2, 9.3) + )) + .build(), + false); + + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + DoubleColumn.of("totalThreads", 21.1, 32.2, 43.3) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "*" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(1.1 * 21.1, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5.2 * 32.2, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Div_Long_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + LongColumn.of("activeThreads", 50, 100, 200) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + LongColumn.of("totalThreads", 25, 50, 100) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "/" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(50.0 / 25, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(100.0 / 50, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Div_Long_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + LongColumn.of("activeThreads", 50, 100, 200) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + DoubleColumn.of("totalThreads", 25.5, 50.6, 100.6) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "/" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(50 / 25.5, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(100 / 50.6, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Div_Double_Long() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + DoubleColumn.of("activeThreads", 12, 25, 200) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + LongColumn.of("totalThreads", 25, 50, 100) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "/" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals((double) 12 / 25, valCol.getDouble(0), .0000000001); + Assertions.assertEquals((double) 25 / 50, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_Div_Double_Double() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app1"), + DoubleColumn.of("activeThreads", 12.1, 25.2, 200.3) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app2", "app3", "app4"), + DoubleColumn.of("totalThreads", 25.6, 50.7, 100.8) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "/" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(12.1 / 25.6, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(25.2 / 50.7, valCol.getDouble(1), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(2, dimCol.size()); + Assertions.assertEquals("app2", dimCol.getString(0)); + Assertions.assertEquals("app3", dimCol.getString(1)); + } + + @Test + public void test_Arithmetic_VectorOverVector_NoIntersection() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + DoubleColumn.of("activeThreads", 12.1, 25.2, 200.3) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app4", "app5", "app6"), + DoubleColumn.of("totalThreads", 25.6, 50.7, 100.8) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "/" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(0, valCol.size()); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(0, dimCol.size()); + } + + /** + * TODO: optimization, in this case, the final expression should be optimized to empty + * 1 and 2 have no intersection + */ + @Test + public void test_Arithmetic_VectorOverVector_NoIntersection_2() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + DoubleColumn.of("activeThreads", 12.1, 25.2, 200.3) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 4, 5, 6), + StringColumn.of("appName", "app4", "app5", "app6"), + DoubleColumn.of("totalThreads", 25.6, 50.7, 100.8) + )) + .build(), + false); + } + + if ("newThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("newThreads")) + .rows(2) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 6, 7), + StringColumn.of("appName", "app6", "app7"), + DoubleColumn.of("newThreads", 106, 107) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "/" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "+" + + "avg(jvm-metrics.newThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(0, valCol.size()); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(0, dimCol.size()); + } + + /** + * TODO: optimization, in this case, the final expression should be optimized to empty + * 1 and 2 have intersection while 2 and 3 has no intersection + */ + @Test + public void test_Arithmetic_VectorOverVector_NoIntersection_3() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + DoubleColumn.of("activeThreads", 12.1, 25.2, 200.3) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(2) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 3, 4), + StringColumn.of("appName", "app3", "app4"), + DoubleColumn.of("totalThreads", 25.6, 50.7) + )) + .build(), + false); + } + + if ("newThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("newThreads")) + .rows(2) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 6, 7), + StringColumn.of("appName", "app6", "app7"), + DoubleColumn.of("newThreads", 106, 107) + )) + .build(), + false); + } + + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "/" + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName)" + + "+" + + "avg(jvm-metrics.newThreads{appName = \"bithon-web-'local\"})[1m] by (appName)"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(0, valCol.size()); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(0, dimCol.size()); + } + + @Test + public void test_Arithmetic_MultipleExpressions() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + String metric = tableScan.selectorList().get(0).getOutputName(); + if ("activeThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + DoubleColumn.of("activeThreads", 3, 4, 5) + )) + .build(), + false); + } + if ("totalThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("totalThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 2, 3, 4), + StringColumn.of("appName", "app2", "app3", "app4"), + DoubleColumn.of("totalThreads", 25, 26, 27) + )) + .build(), + false); + } + + if ("newThreads".equals(metric)) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("newThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 3, 4, 5), + StringColumn.of("appName", "app3", "app4", "app5"), + DoubleColumn.of("newThreads", 35, 36, 37) + )) + .build(), + false); + } + throw new IllegalArgumentException("Invalid metric: " + metric); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName) " + + "/ " + + "avg(jvm-metrics.totalThreads{appName = \"bithon-web-'local\"})[1m] by (appName) " + + "* " + + "avg(jvm-metrics.newThreads{appName = \"bithon-web-'local\"})[1m] by (appName) " + + "+ 5"); + PipelineQueryResult response = evaluator.execute().get(); + + // Only the overlapped series will be returned + Column valCol = response.getTable().getColumn("value"); + Assertions.assertEquals(1, valCol.size()); + Assertions.assertEquals(5.0 / 26 * 35 + 5, valCol.getDouble(0), .0000000001); + + Column dimCol = response.getTable().getColumn("appName"); + Assertions.assertEquals(1, dimCol.size()); + Assertions.assertEquals("app3", dimCol.getString(0)); + } + + @Test + public void test_Arithmetic_RelativeComparison() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> { + LogicalTableScan tableScan = answer.getArgument(0, LogicalTableScan.class); + + HumanReadableDuration offset = tableScan.offset(); + if (offset == null) { + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + DoubleColumn.of("activeThreads", 3, 4, 5) + )) + .build(), + false); + } + + return new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("-1d")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 2, 3, 4), + StringColumn.of("appName", "app2", "app3", "app4"), + DoubleColumn.of("-1d", 21, 22, 23) + )) + .build(), + false); + }); + + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] by (appName) > -5%[-1d]"); + + PipelineQueryResult response = evaluator.execute() + .get(); + Assertions.assertEquals(2, response.getRows()); + + // Only the overlapped series(app2,app3) will be returned + { + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(4, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5, valCol.getDouble(1), .0000000001); + } + { + Column valCol = response.getTable().getColumn("-1d"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(21, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(22, valCol.getDouble(1), .0000000001); + } + { + Column valCol = response.getTable().getColumn("delta"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals((4.0 - 21) / 21, valCol.getDouble(0), .0000000001); + Assertions.assertEquals((5.0 - 22) / 22, valCol.getDouble(1), .0000000001); + } + } + + @Test + public void test_Arithmetic_FilterStep_Double_GT() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + DoubleColumn.of("activeThreads", 3, 4, 5) + )) + .build(), + false)); + + // + // Case 1, > 2 + // + { + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] > 2"); + PipelineQueryResult response = evaluator.execute().get(); + + // all 3 records satisfy the filter condition + Assertions.assertEquals(3, response.getRows()); + + // Only the overlapped series(app2,app3) will be returned + { + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(3, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(4, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(5, valCol.getDouble(2), .0000000001); + } + } + + // + // Case 2, > 3, two rows satisfy the filter condition + // + { + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] > 3"); + PipelineQueryResult response = evaluator.execute().get(); + + Assertions.assertEquals(2, response.getRows()); + { + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(4, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5, valCol.getDouble(1), .0000000001); + } + } + + // + // Case 3, > 4, 1 rows satisfies the filter condition + // + { + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] > 4"); + PipelineQueryResult response = evaluator.execute().get(); + + Assertions.assertEquals(1, response.getRows()); + { + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(1, valCol.size()); + Assertions.assertEquals(5, valCol.getDouble(0), .0000000001); + } + } + + + // + // Case 4, > 5, 0 rows satisfies the filter condition + // + { + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] > 5"); + PipelineQueryResult response = evaluator.execute().get(); + + Assertions.assertEquals(0, response.getRows()); + { + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(0, valCol.size()); + } + } + } + + @Test + public void test_Arithmetic_FilterStep_Double_GTE() throws Exception { + Mockito.when(dataSourceReader.plan(Mockito.any(), Mockito.any())) + .thenAnswer((answer) -> new MockReadStep(PipelineQueryResult.builder() + .keyColumns(List.of("_timestamp", "appName")) + .valColumns(List.of("activeThreads")) + .rows(3) + .table(ColumnarTable.of( + LongColumn.of("_timestamp", 1, 2, 3), + StringColumn.of("appName", "app1", "app2", "app3"), + DoubleColumn.of("activeThreads", 3, 4, 5) + )) + .build(), + false)); + + // + // Case 1, >= 2, all 3 rows satisfy the filter condition + // + { + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] >= 2"); + PipelineQueryResult response = evaluator.execute().get(); + + // all 3 records satisfy the filter condition + Assertions.assertEquals(3, response.getRows()); + { + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(3, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(4, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(5, valCol.getDouble(2), .0000000001); + } + } + + // + // Case 2, >= 3, all 3 rows satisfy the filter condition + // + { + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] >= 3"); + PipelineQueryResult response = evaluator.execute().get(); + + Assertions.assertEquals(3, response.getRows()); + { + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(3, valCol.size()); + Assertions.assertEquals(3, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(4, valCol.getDouble(1), .0000000001); + Assertions.assertEquals(5, valCol.getDouble(2), .0000000001); + } + } + + // + // Case 3, >= 4, 2 rows satisfies the filter condition + // + { + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] >= 4"); + PipelineQueryResult response = evaluator.execute().get(); + + Assertions.assertEquals(2, response.getRows()); + { + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(2, valCol.size()); + Assertions.assertEquals(4, valCol.getDouble(0), .0000000001); + Assertions.assertEquals(5, valCol.getDouble(1), .0000000001); + } + } + + // + // Case 4, >= 5, some rows satisfy the filter condition + // + { + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + // BY is given so that it produces a vector + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] >= 5"); + PipelineQueryResult response = evaluator.execute().get(); + + Assertions.assertEquals(1, response.getRows()); + { + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(1, valCol.size()); + Assertions.assertEquals(5, valCol.getDouble(0), .0000000001); + } + } + + // + // Case 5, >= 6, 0 rows satisfies the filter condition + // + { + IPhysicalPlan evaluator = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(this.schemaProvider) + .interval(IntervalRequest.builder() + .bucketCount(1) + .startISO8601(TimeSpan.fromISO8601("2023-01-01T00:00:00+08:00")) + .endISO8601(TimeSpan.fromISO8601("2023-01-01T00:01:00+08:00")) + .build()) + .groupBy("avg(jvm-metrics.activeThreads{appName = \"bithon-web-'local\"})[1m] >= 6"); + PipelineQueryResult response = evaluator.execute().get(); + + Assertions.assertEquals(0, response.getRows()); + { + Column valCol = response.getTable().getColumn("activeThreads"); + Assertions.assertEquals(0, valCol.size()); + } + } + } +} diff --git a/server/server-starter/src/test/java/org/bithon/server/storage/jdbc/common/statement/builder/PhysicalPlannerTest.java b/server/server-starter/src/test/java/org/bithon/server/storage/jdbc/common/statement/builder/PhysicalPlannerTest.java new file mode 100644 index 0000000000..97679ed76a --- /dev/null +++ b/server/server-starter/src/test/java/org/bithon/server/storage/jdbc/common/statement/builder/PhysicalPlannerTest.java @@ -0,0 +1,331 @@ +/* + * Copyright 2020 bithon.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bithon.server.storage.jdbc.common.statement.builder; + +import org.bithon.component.commons.expression.IDataType; +import org.bithon.server.commons.time.TimeSpan; +import org.bithon.server.datasource.DefaultSchema; +import org.bithon.server.datasource.ISchema; +import org.bithon.server.datasource.ISchemaProvider; +import org.bithon.server.datasource.SchemaException; +import org.bithon.server.datasource.TimestampSpec; +import org.bithon.server.datasource.column.ExpressionColumn; +import org.bithon.server.datasource.column.StringColumn; +import org.bithon.server.datasource.column.aggregatable.last.AggregateLongLastColumn; +import org.bithon.server.datasource.column.aggregatable.sum.AggregateLongSumColumn; +import org.bithon.server.datasource.query.IDataSourceReader; +import org.bithon.server.datasource.query.plan.physical.IPhysicalPlan; +import org.bithon.server.datasource.query.setting.QuerySettings; +import org.bithon.server.datasource.reader.clickhouse.AggregateFunctionColumn; +import org.bithon.server.datasource.reader.h2.H2SqlDialect; +import org.bithon.server.datasource.reader.jdbc.JdbcDataSourceReader; +import org.bithon.server.datasource.store.IDataStoreSpec; +import org.bithon.server.metric.expression.plan.MetricExpressionPhysicalPlanner; +import org.bithon.server.web.service.datasource.api.IntervalRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +/** + * @author frank.chen021@outlook.com + * @date 2025/6/5 20:58 + */ +public class PhysicalPlannerTest { + private final ISchema schema = new DefaultSchema("bithon-jvm-metrics", + "bithon-jvm-metrics", + new TimestampSpec("timestamp"), + Arrays.asList(new StringColumn("appName", "appName"), + new StringColumn("instance", "instance")), + Arrays.asList(new AggregateLongSumColumn("responseTime", "responseTime"), + new AggregateLongSumColumn("totalCount", "totalCount"), + new AggregateLongSumColumn("count4xx", "count4xx"), + new AggregateLongSumColumn("count5xx", "count5xx"), + new AggregateLongLastColumn("activeThreads", "activeThreads"), + new AggregateLongLastColumn("totalThreads", "totalThreads"), + new ExpressionColumn("avgResponseTime", + null, + "sum(responseTime) / sum(totalCount)", + "double"), + + // For ClickHouse data source + new AggregateFunctionColumn("clickedSum", "clickedSum", "sum", IDataType.LONG), + new AggregateFunctionColumn("clickedCnt", "clickedCnt", "count", IDataType.LONG) + ), + null, + new IDataStoreSpec() { + @Override + public String getStore() { + return "bithon_jvm_metrics"; + } + + @Override + public void setSchema(ISchema schema) { + + } + + @Override + public boolean isInternal() { + return false; + } + + @Override + public IDataSourceReader createReader() { + return new JdbcDataSourceReader(null, new H2SqlDialect(), QuerySettings.DEFAULT); + } + }, + null, + null); + + private final ISchemaProvider schemaProvider = name -> { + if ("bithon-jvm-metrics".equals(name)) { + return this.schema; + } + throw new SchemaException.NotFound(name); + }; + + @Test + public void testTableScanPlan() { + IPhysicalPlan physicalPlan = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(schemaProvider) + .interval(IntervalRequest.builder() + .startISO8601(TimeSpan.fromISO8601("2024-07-26T21:22:00.000+0800")) + .endISO8601(TimeSpan.fromISO8601("2024-07-26T21:32:00.000+0800")) + .build()) + .timeSeries("bithon-jvm-metrics.totalCount"); + + Assertions.assertEquals(""" + JdbcReadStep + SELECT "totalCount" AS "totalCount" + FROM "bithon-jvm-metrics" AS "bithon-jvm-metrics" + WHERE ("timestamp" >= '2024-07-26T21:22:00.000+08:00') AND ("timestamp" < '2024-07-26T21:32:00.000+08:00') + """, + physicalPlan.serializeToText()); + } + + @Test + public void testTableScanPlanWithFilter() { + IPhysicalPlan physicalPlan = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(schemaProvider) + .interval(IntervalRequest.builder() + .startISO8601(TimeSpan.fromISO8601("2024-07-26T21:22:00.000+0800")) + .endISO8601(TimeSpan.fromISO8601("2024-07-26T21:32:00.000+0800")) + .build()) + .timeSeries("bithon-jvm-metrics.totalCount{appName=\"jacky\"}"); + + Assertions.assertEquals(""" + JdbcReadStep + SELECT "totalCount" AS "totalCount" + FROM "bithon-jvm-metrics" AS "bithon-jvm-metrics" + WHERE ("timestamp" >= '2024-07-26T21:22:00.000+08:00') AND ("timestamp" < '2024-07-26T21:32:00.000+08:00') AND ("appName" = 'jacky') + """, + physicalPlan.serializeToText()); + } + + @Test + public void test_SumAggregate() { + String expr = """ + sum(bithon-jvm-metrics.totalCount{appName="jacky"}) + """; + IPhysicalPlan physicalPlan = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(schemaProvider) + .interval(IntervalRequest.builder() + .startISO8601(TimeSpan.fromISO8601("2024-07-26T21:22:00.000+0800")) + .endISO8601(TimeSpan.fromISO8601("2024-07-26T21:32:00.000+0800")) + .build()) + .groupBy(expr); + + Assertions.assertEquals(""" + JdbcReadStep + SELECT sum("totalCount") AS "totalCount" + FROM "bithon_jvm_metrics" + WHERE ("bithon_jvm_metrics"."timestamp" >= '2024-07-26T21:22:00.000+08:00') AND ("bithon_jvm_metrics"."timestamp" < '2024-07-26T21:32:00.000+08:00') AND ("bithon_jvm_metrics"."appName" = 'jacky') + """, + physicalPlan.serializeToText()); + } + + @Test + public void test_SumAggregate_GroupBy() { + String expr = """ + sum(bithon-jvm-metrics.totalCount{appName="jacky"}) by (appName) + """; + IPhysicalPlan physicalPlan = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(schemaProvider) + .interval(IntervalRequest.builder() + .startISO8601(TimeSpan.fromISO8601("2024-07-26T21:22:00.000+0800")) + .endISO8601(TimeSpan.fromISO8601("2024-07-26T21:32:00.000+0800")) + .step(60) + .build()) + .groupBy(expr); + Assertions.assertEquals(""" + JdbcReadStep + SELECT "appName", + sum("totalCount") AS "totalCount" + FROM "bithon_jvm_metrics" + WHERE ("bithon_jvm_metrics"."timestamp" >= '2024-07-26T21:22:00.000+08:00') AND ("bithon_jvm_metrics"."timestamp" < '2024-07-26T21:32:00.000+08:00') AND ("bithon_jvm_metrics"."appName" = 'jacky') + GROUP BY "appName" + """, + physicalPlan.serializeToText()); + } + + @Test + public void test_SumAggregate_GroupBy_TimeSeries() { + String expr = """ + sum(bithon-jvm-metrics.totalCount{appName="jacky"}) by (appName) + """; + IPhysicalPlan physicalPlan = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(schemaProvider) + .interval(IntervalRequest.builder() + .startISO8601(TimeSpan.fromISO8601("2024-07-26T21:22:00.000+0800")) + .endISO8601(TimeSpan.fromISO8601("2024-07-26T21:32:00.000+0800")) + .step(10) + .build()) + .timeSeries(expr); + + Assertions.assertEquals(""" + JdbcReadStep + SELECT UNIX_TIMESTAMP("timestamp")/ 10 * 10 AS "_timestamp", + "appName", + sum("totalCount") AS "totalCount" + FROM "bithon_jvm_metrics" + WHERE ("bithon_jvm_metrics"."timestamp" >= '2024-07-26T21:22:00.000+08:00') AND ("bithon_jvm_metrics"."timestamp" < '2024-07-26T21:32:00.000+08:00') AND ("bithon_jvm_metrics"."appName" = 'jacky') + GROUP BY "appName", "_timestamp" + """, + physicalPlan.serializeToText()); + } + + @Test + public void test_LastAggregate() { + String expr = """ + last(bithon-jvm-metrics.totalCount{appName="jacky"}) by (appName) + """; + + IPhysicalPlan physicalPlan = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(schemaProvider) + .interval(IntervalRequest.builder() + .startISO8601(TimeSpan.fromISO8601("2024-07-26T21:22:00.000+0800")) + .endISO8601(TimeSpan.fromISO8601("2024-07-26T21:32:00.000+0800")) + .build()) + .groupBy(expr); + + Assertions.assertEquals(""" + JdbcReadStep + SELECT "appName", + "totalCount" + FROM + ( + SELECT "appName", + FIRST_VALUE("totalCount") OVER (PARTITION BY (UNIX_TIMESTAMP("timestamp") / 600) * 600 ORDER BY "timestamp" ASC) AS "totalCount" + FROM "bithon_jvm_metrics" + WHERE ("bithon_jvm_metrics"."timestamp" >= '2024-07-26T21:22:00.000+08:00') AND ("bithon_jvm_metrics"."timestamp" < '2024-07-26T21:32:00.000+08:00') AND ("bithon_jvm_metrics"."appName" = 'jacky') + ) + GROUP BY "appName", "totalCount" + """, + physicalPlan.serializeToText()); + } + + @Test + public void test_Arithmetic_TwoExpressions() { + String expr = """ + sum(bithon-jvm-metrics.count4xx{appName="jacky"}) + + + sum(bithon-jvm-metrics.count5xx{appName="jacky"}) + """; + + IPhysicalPlan physicalPlan = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(schemaProvider) + .interval(IntervalRequest.builder() + .startISO8601(TimeSpan.fromISO8601("2024-07-26T21:22:00.000+0800")) + .endISO8601(TimeSpan.fromISO8601("2024-07-26T21:32:00.000+0800")) + .build()) + .groupBy(expr); + + Assertions.assertEquals(""" + AddStep, Result Column: value, Retained Columns: [] + lhs:\s + JdbcReadStep + SELECT sum("count4xx") AS "count4xx" + FROM "bithon_jvm_metrics" + WHERE ("bithon_jvm_metrics"."timestamp" >= '2024-07-26T21:22:00.000+08:00') AND ("bithon_jvm_metrics"."timestamp" < '2024-07-26T21:32:00.000+08:00') AND ("bithon_jvm_metrics"."appName" = 'jacky') + rhs:\s + JdbcReadStep + SELECT sum("count5xx") AS "count5xx" + FROM "bithon_jvm_metrics" + WHERE ("bithon_jvm_metrics"."timestamp" >= '2024-07-26T21:22:00.000+08:00') AND ("bithon_jvm_metrics"."timestamp" < '2024-07-26T21:32:00.000+08:00') AND ("bithon_jvm_metrics"."appName" = 'jacky') + """, + physicalPlan.serializeToText()); + } + + @Test + public void test_Arithmetic_WithLiteral() { + String expr = """ + sum(bithon-jvm-metrics.count4xx{appName="jacky"}) + + + 5 + """; + + IPhysicalPlan physicalPlan = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(schemaProvider) + .interval(IntervalRequest.builder() + .startISO8601(TimeSpan.fromISO8601("2024-07-26T21:22:00.000+0800")) + .endISO8601(TimeSpan.fromISO8601("2024-07-26T21:32:00.000+0800")) + .build()) + .groupBy(expr); + + Assertions.assertEquals(""" + AddStep, Result Column: value, Retained Columns: [] + lhs:\s + JdbcReadStep + SELECT sum("count4xx") AS "count4xx" + FROM "bithon_jvm_metrics" + WHERE ("bithon_jvm_metrics"."timestamp" >= '2024-07-26T21:22:00.000+08:00') AND ("bithon_jvm_metrics"."timestamp" < '2024-07-26T21:32:00.000+08:00') AND ("bithon_jvm_metrics"."appName" = 'jacky') + rhs:\s + 5 + """, + physicalPlan.serializeToText()); + } + + @Test + public void test_RelativeComparison() { + String expr = """ + sum(bithon-jvm-metrics.count4xx{appName="jacky"}) + > + 30%[-1d] + """; + + IPhysicalPlan physicalPlan = MetricExpressionPhysicalPlanner.builder() + .schemaProvider(schemaProvider) + .interval(IntervalRequest.builder() + .startISO8601(TimeSpan.fromISO8601("2024-07-26T21:22:00.000+0800")) + .endISO8601(TimeSpan.fromISO8601("2024-07-26T21:32:00.000+0800")) + .build()) + .groupBy(expr); + + Assertions.assertEquals(""" + ComparisonStep: GT + lhs:\s + JdbcReadStep + SELECT sum("count4xx") AS "count4xx" + FROM "bithon_jvm_metrics" + WHERE ("bithon_jvm_metrics"."timestamp" >= '2024-07-26T21:22:00.000+08:00') AND ("bithon_jvm_metrics"."timestamp" < '2024-07-26T21:32:00.000+08:00') AND ("bithon_jvm_metrics"."appName" = 'jacky') + rhs:\s + 30% + + """, + physicalPlan.serializeToText()); + } +} diff --git a/server/storage-jdbc/src/main/java/org/bithon/server/storage/jdbc/tracing/reader/TraceJdbcReader.java b/server/storage-jdbc/src/main/java/org/bithon/server/storage/jdbc/tracing/reader/TraceJdbcReader.java index 412a3694dd..52bdc106de 100644 --- a/server/storage-jdbc/src/main/java/org/bithon/server/storage/jdbc/tracing/reader/TraceJdbcReader.java +++ b/server/storage-jdbc/src/main/java/org/bithon/server/storage/jdbc/tracing/reader/TraceJdbcReader.java @@ -39,8 +39,8 @@ import org.bithon.server.datasource.query.Order; import org.bithon.server.datasource.query.OrderBy; import org.bithon.server.datasource.query.Query; -import org.bithon.server.datasource.query.pipeline.Column; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; +import org.bithon.server.datasource.query.result.Column; +import org.bithon.server.datasource.query.result.ColumnarTable; import org.bithon.server.datasource.query.setting.QuerySettings; import org.bithon.server.datasource.reader.jdbc.JdbcDataSourceReader; import org.bithon.server.datasource.reader.jdbc.dialect.ISqlDialect; diff --git a/server/storage/src/main/java/org/bithon/server/storage/datasource/SchemaManager.java b/server/storage/src/main/java/org/bithon/server/storage/datasource/SchemaManager.java index efa899306f..fd449c466e 100644 --- a/server/storage/src/main/java/org/bithon/server/storage/datasource/SchemaManager.java +++ b/server/storage/src/main/java/org/bithon/server/storage/datasource/SchemaManager.java @@ -22,6 +22,7 @@ import org.bithon.component.commons.time.DateTime; import org.bithon.server.commons.time.TimeSpan; import org.bithon.server.datasource.ISchema; +import org.bithon.server.datasource.ISchemaProvider; import org.bithon.server.datasource.SchemaException; import org.springframework.context.SmartLifecycle; @@ -40,7 +41,7 @@ * @date 2020-08-21 15:13:41 */ @Slf4j -public class SchemaManager implements SmartLifecycle { +public class SchemaManager implements SmartLifecycle, ISchemaProvider { private final List listeners = Collections.synchronizedList(new ArrayList<>()); private final ISchemaStorage schemaStorage; private ScheduledExecutorService loaderScheduler; diff --git a/server/storage/src/main/java/org/bithon/server/storage/tracing/ITraceReader.java b/server/storage/src/main/java/org/bithon/server/storage/tracing/ITraceReader.java index 25c9e5ffeb..96de19ba63 100644 --- a/server/storage/src/main/java/org/bithon/server/storage/tracing/ITraceReader.java +++ b/server/storage/src/main/java/org/bithon/server/storage/tracing/ITraceReader.java @@ -22,7 +22,7 @@ import org.bithon.server.datasource.query.IDataSourceReader; import org.bithon.server.datasource.query.Limit; import org.bithon.server.datasource.query.OrderBy; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; +import org.bithon.server.datasource.query.result.ColumnarTable; import org.bithon.server.storage.tracing.mapping.TraceIdMapping; import java.sql.Timestamp; diff --git a/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/DataSourceService.java b/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/DataSourceService.java index 51868c001b..356c409495 100644 --- a/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/DataSourceService.java +++ b/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/DataSourceService.java @@ -24,7 +24,7 @@ import org.bithon.server.datasource.query.Query; import org.bithon.server.datasource.query.ast.ExpressionNode; import org.bithon.server.datasource.query.ast.Selector; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; +import org.bithon.server.datasource.query.result.ColumnarTable; import org.bithon.server.storage.metrics.IMetricStorage; import org.bithon.server.web.service.WebServiceModuleEnabler; import org.springframework.context.annotation.Conditional; diff --git a/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/IDataSourceApi.java b/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/IDataSourceApi.java index e708513537..f462bc037f 100644 --- a/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/IDataSourceApi.java +++ b/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/IDataSourceApi.java @@ -19,7 +19,7 @@ import jakarta.validation.constraints.Min; import lombok.Data; import org.bithon.server.datasource.ISchema; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; +import org.bithon.server.datasource.query.result.ColumnarTable; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PathVariable; diff --git a/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/QueryRequest.java b/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/QueryRequest.java index 99c032e781..c9bcec4e96 100644 --- a/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/QueryRequest.java +++ b/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/QueryRequest.java @@ -74,5 +74,10 @@ public class QueryRequest { @Nullable private HumanReadableDuration offset; + /** + * filter expression applied after query aggregation + */ + private String postFilterExpression; + private Query.ResultFormat resultFormat; } diff --git a/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/impl/DataSourceApi.java b/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/impl/DataSourceApi.java index 3d4e138c0f..d6df14c8f9 100644 --- a/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/impl/DataSourceApi.java +++ b/server/web-service/src/main/java/org/bithon/server/web/service/datasource/api/impl/DataSourceApi.java @@ -30,7 +30,7 @@ import org.bithon.server.datasource.query.IDataSourceReader; import org.bithon.server.datasource.query.Interval; import org.bithon.server.datasource.query.Query; -import org.bithon.server.datasource.query.pipeline.ColumnarTable; +import org.bithon.server.datasource.query.result.ColumnarTable; import org.bithon.server.datasource.store.IDataStoreSpec; import org.bithon.server.discovery.client.DiscoveredServiceInvoker; import org.bithon.server.pipeline.tracing.sampler.ITraceSampler;