This is an automated email from the ASF dual-hosted git repository.
pvillard pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new d9f9147306 NIFI-15474 Support timestamp truncation in RecordPath DSL
d9f9147306 is described below
commit d9f9147306f5b6d999fb6cabbfb9b62a2ef722ae
Author: Robert Jensen <[email protected]>
AuthorDate: Fri Jan 16 17:19:53 2026 -0500
NIFI-15474 Support timestamp truncation in RecordPath DSL
This closes #10796.
Signed-off-by: Pierre Villard <[email protected]>
---
.../apache/nifi/record/path/functions/Divide.java | 48 ++++
.../nifi/record/path/functions/Multiply.java | 48 ++++
.../nifi/record/path/functions/ToNumber.java | 45 ++++
.../nifi/record/path/math/MathBinaryEvaluator.java | 55 ++++
.../nifi/record/path/math/MathBinaryOperator.java | 22 ++
.../nifi/record/path/math/MathDivideOperator.java | 40 +++
.../nifi/record/path/math/MathEvaluator.java | 25 ++
.../record/path/math/MathMultiplyOperator.java | 34 +++
.../apache/nifi/record/path/math/MathOperator.java | 21 ++
.../nifi/record/path/math/MathTypeUtils.java | 56 ++++
.../nifi/record/path/paths/RecordPathCompiler.java | 15 ++
.../apache/nifi/record/path/TestRecordPath.java | 282 ++++++++++++++++++++-
nifi-docs/src/main/asciidoc/record-path-guide.adoc | 99 ++++++++
13 files changed, 788 insertions(+), 2 deletions(-)
diff --git
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Divide.java
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Divide.java
new file mode 100644
index 0000000000..310fa5bdbb
--- /dev/null
+++
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Divide.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.nifi.record.path.functions;
+
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.RecordPathEvaluationContext;
+import org.apache.nifi.record.path.math.MathBinaryEvaluator;
+import org.apache.nifi.record.path.paths.RecordPathSegment;
+
+import java.util.stream.Stream;
+
+public class Divide extends RecordPathSegment {
+ private final RecordPathSegment lhsPath;
+ private final RecordPathSegment rhsPath;
+ private final MathBinaryEvaluator divide = MathBinaryEvaluator.divide();
+
+ public Divide(final RecordPathSegment lhsPath, final RecordPathSegment
rhsPath, final boolean absolute) {
+ super("divide", null, absolute);
+ this.lhsPath = lhsPath;
+ this.rhsPath = rhsPath;
+ }
+
+ @Override
+ public Stream<FieldValue> evaluate(RecordPathEvaluationContext context) {
+ final FieldValue lhs =
lhsPath.evaluate(context).findFirst().orElseThrow(() -> new
IllegalArgumentException("divide function requires a left-hand operand"));
+ final FieldValue rhs =
rhsPath.evaluate(context).findFirst().orElseThrow(() -> new
IllegalArgumentException("divide function requires a right-hand operand"));
+
+ if (lhs.getValue() == null || rhs.getValue() == null) {
+ return Stream.of();
+ }
+
+ return Stream.of(divide.evaluate(lhs, rhs));
+ }
+}
diff --git
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Multiply.java
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Multiply.java
new file mode 100644
index 0000000000..cab1c427a1
--- /dev/null
+++
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Multiply.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.nifi.record.path.functions;
+
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.RecordPathEvaluationContext;
+import org.apache.nifi.record.path.math.MathBinaryEvaluator;
+import org.apache.nifi.record.path.paths.RecordPathSegment;
+
+import java.util.stream.Stream;
+
+public class Multiply extends RecordPathSegment {
+ private final RecordPathSegment lhsPath;
+ private final RecordPathSegment rhsPath;
+ private final MathBinaryEvaluator multiply =
MathBinaryEvaluator.multiply();
+
+ public Multiply(final RecordPathSegment lhsPath, final RecordPathSegment
rhsPath, final boolean absolute) {
+ super("multiply", null, absolute);
+ this.lhsPath = lhsPath;
+ this.rhsPath = rhsPath;
+ }
+
+ @Override
+ public Stream<FieldValue> evaluate(RecordPathEvaluationContext context) {
+ final FieldValue lhs =
lhsPath.evaluate(context).findFirst().orElseThrow(() -> new
IllegalArgumentException("multiply function requires a left-hand operand"));
+ final FieldValue rhs =
rhsPath.evaluate(context).findFirst().orElseThrow(() -> new
IllegalArgumentException("multiply function requires a right-hand operand"));
+
+ if (lhs.getValue() == null || rhs.getValue() == null) {
+ return Stream.of();
+ }
+
+ return Stream.of(multiply.evaluate(lhs, rhs));
+ }
+}
diff --git
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/ToNumber.java
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/ToNumber.java
new file mode 100644
index 0000000000..f945cc2758
--- /dev/null
+++
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/ToNumber.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.nifi.record.path.functions;
+
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.RecordPathEvaluationContext;
+import org.apache.nifi.record.path.math.MathTypeUtils;
+import org.apache.nifi.record.path.paths.RecordPathSegment;
+
+import java.util.stream.Stream;
+
+public class ToNumber extends RecordPathSegment {
+ private final RecordPathSegment valuePath;
+
+ public ToNumber(final RecordPathSegment valuePath, final boolean absolute)
{
+ super("toNumber", null, absolute);
+ this.valuePath = valuePath;
+ }
+
+ @Override
+ public Stream<FieldValue> evaluate(RecordPathEvaluationContext context) {
+ final FieldValue fieldValue =
valuePath.evaluate(context).findFirst().orElseThrow(() -> new
IllegalArgumentException("toNumber function requires an operand"));
+ final Object value = fieldValue.getValue();
+
+ if (value == null || value instanceof Number) {
+ return Stream.of(fieldValue);
+ } else {
+ return Stream.of(MathTypeUtils.toNumber(fieldValue));
+ }
+ }
+}
diff --git
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathBinaryEvaluator.java
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathBinaryEvaluator.java
new file mode 100644
index 0000000000..140c8c3010
--- /dev/null
+++
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathBinaryEvaluator.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.nifi.record.path.math;
+
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.StandardFieldValue;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+
+public class MathBinaryEvaluator extends MathEvaluator<MathBinaryOperator> {
+ public MathBinaryEvaluator(MathBinaryOperator op) {
+ super(op);
+ }
+
+ public static MathBinaryEvaluator divide() {
+ return new MathBinaryEvaluator(new MathDivideOperator());
+ }
+
+ public static MathBinaryEvaluator multiply() {
+ return new MathBinaryEvaluator(new MathMultiplyOperator());
+ }
+
+ public FieldValue evaluate(FieldValue lhs, FieldValue rhs) {
+ final Number lhsValue = MathTypeUtils.coerceNumber(lhs);
+ final Number rhsValue = MathTypeUtils.coerceNumber(rhs);
+
+ Number result;
+ DataType resultType;
+
+ if (MathTypeUtils.isLongCompatible(lhsValue) &&
MathTypeUtils.isLongCompatible(rhsValue)) {
+ result = op.operate(lhsValue.longValue(), rhsValue.longValue());
+ resultType = RecordFieldType.LONG.getDataType();
+ } else {
+ result = op.operate(lhsValue.doubleValue(),
rhsValue.doubleValue());
+ resultType = RecordFieldType.DOUBLE.getDataType();
+ }
+
+ return new StandardFieldValue(result, new
RecordField(op.getFieldName(), resultType), null);
+ }
+}
diff --git
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathBinaryOperator.java
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathBinaryOperator.java
new file mode 100644
index 0000000000..4d017361a4
--- /dev/null
+++
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathBinaryOperator.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.nifi.record.path.math;
+
+public interface MathBinaryOperator extends MathOperator {
+ Long operate(Long n, Long m);
+ Double operate(Double n, Double m);
+}
diff --git
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathDivideOperator.java
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathDivideOperator.java
new file mode 100644
index 0000000000..db0de980b8
--- /dev/null
+++
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathDivideOperator.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.nifi.record.path.math;
+
+public class MathDivideOperator implements MathBinaryOperator {
+ @Override
+ public Long operate(Long n, Long m) {
+ if (m == 0L) {
+ throw new ArithmeticException("Division by zero in RecordPath
divide function");
+ }
+ return n / m;
+ }
+
+ @Override
+ public Double operate(Double n, Double m) {
+ if (m == 0.0) {
+ throw new ArithmeticException("Division by zero in RecordPath
divide function");
+ }
+ return n / m;
+ }
+
+ @Override
+ public String getFieldName() {
+ return "divide";
+ }
+}
diff --git
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathEvaluator.java
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathEvaluator.java
new file mode 100644
index 0000000000..38e9c6800c
--- /dev/null
+++
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathEvaluator.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.nifi.record.path.math;
+
+public class MathEvaluator<T extends MathOperator> {
+ protected final T op;
+
+ public MathEvaluator(T op) {
+ this.op = op;
+ }
+}
diff --git
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathMultiplyOperator.java
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathMultiplyOperator.java
new file mode 100644
index 0000000000..7c6b99c853
--- /dev/null
+++
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathMultiplyOperator.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.nifi.record.path.math;
+
+public class MathMultiplyOperator implements MathBinaryOperator {
+ @Override
+ public Long operate(Long n, Long m) {
+ return n * m;
+ }
+
+ @Override
+ public Double operate(Double n, Double m) {
+ return n * m;
+ }
+
+ @Override
+ public String getFieldName() {
+ return "multiply";
+ }
+}
diff --git
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathOperator.java
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathOperator.java
new file mode 100644
index 0000000000..6bcfd2332d
--- /dev/null
+++
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathOperator.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.nifi.record.path.math;
+
+public interface MathOperator {
+ String getFieldName();
+}
diff --git
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathTypeUtils.java
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathTypeUtils.java
new file mode 100644
index 0000000000..73ffc69b88
--- /dev/null
+++
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathTypeUtils.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.nifi.record.path.math;
+
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.StandardFieldValue;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.util.DataTypeUtils;
+
+public class MathTypeUtils {
+ public static FieldValue toNumber(FieldValue fieldValue) {
+ Number result = coerceNumber(fieldValue);
+ DataType resultType = isLongCompatible(result) ?
RecordFieldType.LONG.getDataType() : RecordFieldType.DOUBLE.getDataType();
+
+ return new StandardFieldValue(result, new RecordField("toNumber",
resultType), null);
+ }
+
+ public static Number coerceNumber(FieldValue fieldValue) {
+ final Object value = fieldValue.getValue();
+
+ if (value instanceof Number) {
+ return (Number) value;
+ }
+
+ final RecordField field = fieldValue.getField();
+ final String fieldName = field == null ? "<Anonymous Inner Field>" :
field.getFieldName();
+
+ if (DataTypeUtils.isLongTypeCompatible(value)) {
+ return DataTypeUtils.toLong(value, fieldName);
+ }
+ if (DataTypeUtils.isDoubleTypeCompatible(value)) {
+ return DataTypeUtils.toDouble(value, fieldName);
+ }
+ throw new IllegalArgumentException("Cannot coerce field '" + fieldName
+ "' to number");
+ }
+
+ public static boolean isLongCompatible(Number value) {
+ return (value instanceof Long || value instanceof Integer || value
instanceof Short || value instanceof Byte);
+ }
+}
diff --git
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java
index 73a2ca7fc6..ca3691b539 100644
---
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java
+++
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java
@@ -42,6 +42,7 @@ import org.apache.nifi.record.path.functions.Base64Encode;
import org.apache.nifi.record.path.functions.Coalesce;
import org.apache.nifi.record.path.functions.Concat;
import org.apache.nifi.record.path.functions.Count;
+import org.apache.nifi.record.path.functions.Divide;
import org.apache.nifi.record.path.functions.EscapeJson;
import org.apache.nifi.record.path.functions.FieldName;
import org.apache.nifi.record.path.functions.FilterFunction;
@@ -49,6 +50,7 @@ import org.apache.nifi.record.path.functions.Format;
import org.apache.nifi.record.path.functions.Hash;
import org.apache.nifi.record.path.functions.Join;
import org.apache.nifi.record.path.functions.MapOf;
+import org.apache.nifi.record.path.functions.Multiply;
import org.apache.nifi.record.path.functions.PadLeft;
import org.apache.nifi.record.path.functions.PadRight;
import org.apache.nifi.record.path.functions.RecordOf;
@@ -63,6 +65,7 @@ import
org.apache.nifi.record.path.functions.SubstringBeforeLast;
import org.apache.nifi.record.path.functions.ToBytes;
import org.apache.nifi.record.path.functions.ToDate;
import org.apache.nifi.record.path.functions.ToLowerCase;
+import org.apache.nifi.record.path.functions.ToNumber;
import org.apache.nifi.record.path.functions.ToString;
import org.apache.nifi.record.path.functions.ToUpperCase;
import org.apache.nifi.record.path.functions.TrimString;
@@ -458,6 +461,18 @@ public class RecordPathCompiler {
final RecordPathSegment[] args =
getArgPaths(argumentListTree, 2, functionName, absolute);
return new Anchored(args[0], args[1], absolute);
}
+ case "multiply": {
+ final RecordPathSegment[] args =
getArgPaths(argumentListTree, 2, functionName, absolute);
+ return new Multiply(args[0], args[1], absolute);
+ }
+ case "divide": {
+ final RecordPathSegment[] args =
getArgPaths(argumentListTree, 2, functionName, absolute);
+ return new Divide(args[0], args[1], absolute);
+ }
+ case "toNumber": {
+ final RecordPathSegment[] args =
getArgPaths(argumentListTree, 1, functionName, absolute);
+ return new ToNumber(args[0], absolute);
+ }
case "not":
case "contains":
case "containsRegex":
diff --git
a/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java
b/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java
index 605632a6cf..9d64882c23 100644
---
a/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java
+++
b/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java
@@ -69,6 +69,7 @@ import static
org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
@SuppressWarnings({"SameParameterValue"})
public class TestRecordPath {
@@ -2350,6 +2351,279 @@ public class TestRecordPath {
}
}
+ @Nested
+ class MathFunctions {
+ @Test
+ public void supportsPromoteByteToLong() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("multiply(2, /bytes[0])", record);
+
+ assertEquals(RecordFieldType.LONG,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("144", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsPromoteByteToDouble() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("multiply('2.0', /bytes[0])", record);
+
+ assertEquals(RecordFieldType.DOUBLE,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("144.0", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsPromoteShortToLong() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("multiply(2, /shortNumber)", record);
+
+ assertEquals(RecordFieldType.LONG,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("246", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsPromoteShortToDouble() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("multiply('2.0', /shortNumber)", record);
+
+ assertEquals(RecordFieldType.DOUBLE,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("246.0", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsPromoteIntToLong() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("multiply(2, /longNumber)", record);
+
+ assertEquals(RecordFieldType.LONG,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("2469135780246913578",
fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsPromoteIntToDouble() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("multiply(2, /mainAccount/balance)", record);
+
+ assertEquals(RecordFieldType.DOUBLE,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("246.9", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsPromoteLongToDouble() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("multiply('2.0', /longNumber)", record);
+
+ assertEquals(RecordFieldType.DOUBLE,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("2.4691357802469135E18",
fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsPromoteFloatToDouble() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("multiply('2.0', /floatNumber)", record);
+
+ assertEquals(RecordFieldType.DOUBLE,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("246.89999389648438",
fieldValue.getValue().toString());
+ }
+ @Test
+ public void throwsExceptionOnUnsupportedLhsType() {
+ Exception exception =
+ assertThrows(Exception.class, () ->
evaluateSingleFieldValue("multiply(/numbers, 2)", record));
+ assertEquals("Cannot coerce field 'numbers' to number",
exception.getMessage());
+ }
+ @Test
+ public void throwsExceptionOnUnsupportedRhsType() {
+ Exception exception =
+ assertThrows(Exception.class, () ->
evaluateSingleFieldValue("multiply(2, /firstName)", record));
+ assertEquals("Cannot coerce field 'firstName' to number",
exception.getMessage());
+ }
+ @Test
+ public void throwsExceptionOnUnsupportedTypeWithAnonymousField() {
+ Exception exception =
+ assertThrows(Exception.class, () ->
evaluateSingleFieldValue("multiply(2, 'hello')", record));
+ assertEquals("Cannot coerce field '<Anonymous Inner Field>' to
number", exception.getMessage());
+ }
+
+ @Nested
+ class Multiply {
+ @Test
+ public void supportsLhsLiteralRhsLiteral() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("multiply(2, 2)", record);
+
+ assertEquals("multiply", fieldValue.getField().getFieldName());
+ assertEquals(RecordFieldType.LONG,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("4", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsLhsPathRhsLiteral() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("multiply(/id, 2)", record);
+
+ assertEquals(RecordFieldType.LONG,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("96", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsLhsLiteralRhsPath() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("multiply(2, /id)", record);
+
+ assertEquals(RecordFieldType.LONG,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("96", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsLhsPathRhsPath() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("multiply(/id, /id)", record);
+
+ assertEquals(RecordFieldType.LONG,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("2304", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsLongOverflow() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("multiply(/longNumber, /longNumber)", record);
+
+ assertEquals(RecordFieldType.LONG,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("-8736265215553819719",
fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsDoubleOverflow() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("multiply('-1.0e308', /mainAccount/balance)", record);
+
+ assertEquals(RecordFieldType.DOUBLE,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("-Infinity", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsLhsNull() {
+ final List<FieldValue> fieldValues =
evaluateMultiFieldValue("multiply(/notAField, 0)", record);
+ assertTrue(fieldValues.isEmpty());
+ }
+ @Test
+ public void supportsRhsNull() {
+ final List<FieldValue> fieldValues =
evaluateMultiFieldValue("multiply(0, /notAField)", record);
+ assertTrue(fieldValues.isEmpty());
+ }
+ @Test
+ public void throwsExceptionOnInvalidArityMissingLhs() {
+ Exception exception =
+ assertThrows(Exception.class, () ->
evaluateSingleFieldValue("multiply(multiply(/notAField, 0),
multiply(/notAField, 0))", record));
+ assertEquals("multiply function requires a left-hand operand",
exception.getMessage());
+ }
+ @Test
+ public void throwsExceptionOnInvalidArityMissingRhs() {
+ Exception exception =
+ assertThrows(Exception.class, () ->
evaluateSingleFieldValue("multiply(0, multiply(/notAField, 0))", record));
+ assertEquals("multiply function requires a right-hand
operand", exception.getMessage());
+ }
+ }
+
+ @Nested
+ class Divide {
+ @Test
+ public void supportsLhsLiteralRhsLiteral() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("divide(3, 2)", record);
+
+ assertEquals("divide", fieldValue.getField().getFieldName());
+ assertEquals(RecordFieldType.LONG,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("1", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsLhsPathRhsLiteral() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("divide(/id, 2)", record);
+
+ assertEquals(RecordFieldType.LONG,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("24", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsLhsLiteralRhsPath() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("divide(2, /id)", record);
+
+ assertEquals(RecordFieldType.LONG,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("0", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsLhsPathRhsPath() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("divide(/id, /id)", record);
+
+ assertEquals(RecordFieldType.LONG,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("1", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsLongOverflow() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("divide(-9223372036854775808, -1)", record);
+
+ assertEquals(RecordFieldType.LONG,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("-9223372036854775808",
fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsDoubleOverflow() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("divide('1.0e300', '1.0e-300')", record);
+
+ assertEquals(RecordFieldType.DOUBLE,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("Infinity", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsLhsNull() {
+ final List<FieldValue> fieldValues =
evaluateMultiFieldValue("divide(/notAField, 0)", record);
+ assertTrue(fieldValues.isEmpty());
+ }
+ @Test
+ public void supportsRhsNull() {
+ final List<FieldValue> fieldValues =
evaluateMultiFieldValue("divide(0, /notAField)", record);
+ assertTrue(fieldValues.isEmpty());
+ }
+ @Test
+ public void throwsExceptionOnDivideByZeroLong() {
+ Exception exception =
+ assertThrows(Exception.class, () ->
evaluateSingleFieldValue("divide(2, 0)", record));
+ assertEquals("Division by zero in RecordPath divide function",
exception.getMessage());
+ }
+ @Test
+ public void throwsExceptionOnDivideByZeroDouble() {
+ Exception exception =
+ assertThrows(Exception.class, () ->
evaluateSingleFieldValue("divide('2.0', 0)", record));
+ assertEquals("Division by zero in RecordPath divide function",
exception.getMessage());
+ }
+ @Test
+ public void throwsExceptionOnInvalidArityMissingLhs() {
+ Exception exception =
+ assertThrows(Exception.class, () ->
evaluateSingleFieldValue("divide(divide(/notAField, 0), divide(/notAField,
0))", record));
+ assertEquals("divide function requires a left-hand operand",
exception.getMessage());
+ }
+ @Test
+ public void throwsExceptionOnInvalidArityMissingRhs() {
+ Exception exception =
+ assertThrows(Exception.class, () ->
evaluateSingleFieldValue("divide(0, divide(/notAField, 0))", record));
+ assertEquals("divide function requires a right-hand operand",
exception.getMessage());
+ }
+ }
+
+ @Nested
+ class ToNumber {
+ @Test
+ public void supportsLiteral() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("toNumber('1e1')", record);
+
+ assertEquals("toNumber", fieldValue.getField().getFieldName());
+ assertEquals(RecordFieldType.DOUBLE,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("10.0", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsPath() {
+ record.setValue("id", new
Date(Instant.parse("1970-01-01T00:00:00Z").toEpochMilli()));
+ final FieldValue fieldValue =
evaluateSingleFieldValue("toNumber(/id)", record);
+
+ assertEquals(RecordFieldType.LONG,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("0", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsNumber() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("toNumber(/id)", record);
+
+ // preserves type
+ assertEquals(RecordFieldType.INT,
fieldValue.getField().getDataType().getFieldType());
+ assertEquals("48", fieldValue.getValue().toString());
+ }
+ @Test
+ public void supportsNull() {
+ final FieldValue fieldValue =
evaluateSingleFieldValue("toNumber(/notAField)", record);
+ assertEquals(null, fieldValue.getValue());
+ }
+ @Test
+ public void throwsExceptionOnUnsupportedType() {
+ Exception exception =
+ assertThrows(Exception.class, () ->
evaluateSingleFieldValue("toNumber(/firstName)", record));
+ assertEquals("Cannot coerce field 'firstName' to number",
exception.getMessage());
+ }
+ @Test
+ public void throwsExceptionOnInvalidArityMissingOperand() {
+ Exception exception =
+ assertThrows(Exception.class, () ->
evaluateSingleFieldValue("toNumber(multiply(/notAField, 0))", record));
+ assertEquals("toNumber function requires an operand",
exception.getMessage());
+ }
+ }
+ }
+
@Nested
class FilterFunctions {
@Test
@@ -3148,7 +3422,9 @@ public class TestRecordPath {
recordFieldOf("numbers", arrayTypeOf(RecordFieldType.INT)),
recordFieldOf("friends", arrayTypeOf(RecordFieldType.STRING)),
recordFieldOf("bytes", arrayTypeOf(RecordFieldType.BYTE)),
- recordFieldOf("longNumber", RecordFieldType.LONG)
+ recordFieldOf("shortNumber", RecordFieldType.SHORT),
+ recordFieldOf("longNumber", RecordFieldType.LONG),
+ recordFieldOf("floatNumber", RecordFieldType.FLOAT)
);
}
@@ -3187,7 +3463,9 @@ public class TestRecordPath {
entry("numbers", new Integer[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}),
entry("friends", new String[]{"John", "Jane", "Jacob",
"Judy"}),
entry("bytes", boxBytes("Hello
World!".getBytes(StandardCharsets.UTF_8))),
- entry("longNumber", 1234567890123456789L)
+ entry("shortNumber", (short) 123),
+ entry("longNumber", 1234567890123456789L),
+ entry("floatNumber", 123.45f)
);
return new MapRecord(getExampleSchema(), new HashMap<>(values));
diff --git a/nifi-docs/src/main/asciidoc/record-path-guide.adoc
b/nifi-docs/src/main/asciidoc/record-path-guide.adoc
index 89af5e561b..97ea7ab268 100644
--- a/nifi-docs/src/main/asciidoc/record-path-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/record-path-guide.adoc
@@ -1409,3 +1409,102 @@ only of white space (spaces, tabs, carriage returns,
and new-line characters).
| `/name[isBlank(/details/phase)]` | John Doe
| `/name[isBlank(.)]` | <returns no results>
|==============================================================================
+
+[[math_functions]]
+== Math Functions
+
+Math functions accept the following number types:
+
+- Byte
+- Short
+- Integer
+- Long
+- Float
+- Double
+
+The following non-number types are also accepted if they can be coerced to a
number:
+
+- String
+- Date
+
+Unless otherwise specified, the calculated result is either a Long or Double:
+
+- If any argument is a Float or Double, the result is a Double
+- Otherwise, the result is a Long
+
+Math functions do not handle overflow.
+
+Examples below use the schema:
+
+```
+{
+ "type": "record",
+ "name": "logs",
+ "fields": [
+ { "name": "flags", "type": "byte" },
+ { "name": "seq", "type" : "short"},
+ { "name": "txns", "type" : "int"},
+ { "name": "timestamp", "type" : "long"},
+ { "name": "temp", "type" : "float"},
+ { "name": "load", "type" : "double"},
+ { "name": "timestampMillis", "type" : "string"},
+ { "name": "date", "type" : "date"},
+ ]
+}
+```
+
+and the record:
+
+```
+{
+ "flags" : 7,
+ "seq": 1024,
+ "txns": 125000,
+ "timestamp": 1768967368,
+ "temp": 55.0,
+ "load": 5.67,
+ "timestampMillis": "1768967368000",
+ "date": "2026-01-21",
+}
+```
+
+
+=== toNumber
+
+Coerces a String or Date value to a number. If the argument is already a
number it is returned unmodified, preserving the original type.
+
+|==============================================================================
+| RecordPath | Return value
+| `toNumber(456)` | 456
+| `toNumber(/flags)` | 7
+| `toNumber(/load)` | 5.67
+| `toNumber('7.89')` | 7.89
+| `toNumber(/timestampMillis)` | 1768967368000
+| `toNumber(/date)` | 1768953600000
+|==============================================================================
+
+=== multiply
+
+Calculates the product of two numbers.
+
+|==============================================================================
+| RecordPath | Return value
+| `multiply(2, 2)` | 4
+| `multiply('2.0', 2)` | 4.0
+| `multiply(/flags, 2)` | 14
+| `multiply(2, /temp)` | 110.0
+| `multiply(/load, /temp)` | 311.85
+|==============================================================================
+
+=== divide
+
+Calculates the quotient of two numbers.
+
+|==============================================================================
+| RecordPath | Return value
+| `divide(2, 2)` | 1
+| `divide(3, 2)` | 1
+| `divide(3, '2.0')` | 1.5
+| `divide(/txns, /temp)` | 2272.7272727272725
+| `divide(/timestampMillis, 300000)` | 5896557
+|==============================================================================