diff --git a/geaflow/geaflow-dsl/geaflow-dsl-parser/src/main/java/org/apache/geaflow/dsl/operator/SqlSameOperator.java b/geaflow/geaflow-dsl/geaflow-dsl-parser/src/main/java/org/apache/geaflow/dsl/operator/SqlSameOperator.java new file mode 100644 index 000000000..699f03e92 --- /dev/null +++ b/geaflow/geaflow-dsl/geaflow-dsl-parser/src/main/java/org/apache/geaflow/dsl/operator/SqlSameOperator.java @@ -0,0 +1,78 @@ +/* + * 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.geaflow.dsl.operator; + +import org.apache.calcite.sql.SqlCall; +import org.apache.calcite.sql.SqlFunction; +import org.apache.calcite.sql.SqlFunctionCategory; +import org.apache.calcite.sql.SqlKind; +import org.apache.calcite.sql.SqlLiteral; +import org.apache.calcite.sql.SqlNode; +import org.apache.calcite.sql.SqlWriter; +import org.apache.calcite.sql.parser.SqlParserPos; +import org.apache.calcite.sql.type.OperandTypes; +import org.apache.calcite.sql.type.ReturnTypes; +import org.apache.geaflow.dsl.sqlnode.SqlSameCall; + +/** + * SqlOperator for the ISO-GQL SAME predicate function. + * + *

This operator represents the SAME function which checks element identity. + * + *

Syntax: SAME(element1, element2, ...) + * + *

Returns: BOOLEAN - TRUE if all element references point to the same element, + * FALSE otherwise. + * + *

Implements ISO/IEC 39075:2024 Section 19.12. + */ +public class SqlSameOperator extends SqlFunction { + + public static final SqlSameOperator INSTANCE = new SqlSameOperator(); + + private SqlSameOperator() { + super( + "SAME", + SqlKind.OTHER_FUNCTION, + ReturnTypes.BOOLEAN, + null, + // At least 2 operands, all must be of comparable types + OperandTypes.VARIADIC, + SqlFunctionCategory.USER_DEFINED_FUNCTION + ); + } + + @Override + public SqlCall createCall( + SqlLiteral functionQualifier, + SqlParserPos pos, + SqlNode... operands) { + return new SqlSameCall(pos, java.util.Arrays.asList(operands)); + } + + @Override + public void unparse( + SqlWriter writer, + SqlCall call, + int leftPrec, + int rightPrec) { + call.unparse(writer, leftPrec, rightPrec); + } +} diff --git a/geaflow/geaflow-dsl/geaflow-dsl-parser/src/main/java/org/apache/geaflow/dsl/sqlnode/SqlSameCall.java b/geaflow/geaflow-dsl/geaflow-dsl-parser/src/main/java/org/apache/geaflow/dsl/sqlnode/SqlSameCall.java new file mode 100644 index 000000000..7e4d5d545 --- /dev/null +++ b/geaflow/geaflow-dsl/geaflow-dsl-parser/src/main/java/org/apache/geaflow/dsl/sqlnode/SqlSameCall.java @@ -0,0 +1,131 @@ +/* + * 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.geaflow.dsl.sqlnode; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.apache.calcite.sql.SqlCall; +import org.apache.calcite.sql.SqlNode; +import org.apache.calcite.sql.SqlOperator; +import org.apache.calcite.sql.SqlWriter; +import org.apache.calcite.sql.parser.SqlParserPos; +import org.apache.calcite.sql.validate.SqlValidator; +import org.apache.calcite.sql.validate.SqlValidatorScope; +import org.apache.geaflow.dsl.operator.SqlSameOperator; + +/** + * SqlNode representing the ISO-GQL SAME predicate function. + * + *

The SAME predicate checks if multiple element references point to the same + * graph element (identity check, not value equality). + * + *

Syntax: SAME(element_ref1, element_ref2 [, element_ref3, ...]) + * + *

Example: + *

+ * MATCH (a:Person)-[:KNOWS]->(b), (b)-[:KNOWS]->(c)
+ * WHERE SAME(a, c)
+ * RETURN a.name, b.name;
+ * 
+ * + *

This returns triangular paths where the start and end vertices are the same element. + * + *

Implements ISO/IEC 39075:2024 Section 19.12 (SAME predicate). + */ +public class SqlSameCall extends SqlCall { + + private final List operands; + + /** + * Creates a SqlSameCall. + * + * @param pos Parser position + * @param operands List of element reference expressions (must be 2 or more) + */ + public SqlSameCall(SqlParserPos pos, List operands) { + super(pos); + // Create a mutable copy to allow setOperand to work + this.operands = new ArrayList<>(Objects.requireNonNull(operands, "operands")); + + // ISO-GQL requires at least 2 arguments + if (operands.size() < 2) { + throw new IllegalArgumentException( + "SAME predicate requires at least 2 arguments, got: " + operands.size()); + } + } + + @Override + public SqlOperator getOperator() { + return SqlSameOperator.INSTANCE; + } + + @Override + public List getOperandList() { + return operands; + } + + @Override + public void validate(SqlValidator validator, SqlValidatorScope scope) { + // Validation will be handled by GQLSameValidator + // This just validates the syntax is correct + for (SqlNode operand : operands) { + operand.validate(validator, scope); + } + } + + @Override + public void setOperand(int i, SqlNode operand) { + if (i < 0 || i >= operands.size()) { + throw new IllegalArgumentException("Invalid operand index: " + i); + } + operands.set(i, operand); + } + + @Override + public void unparse(SqlWriter writer, int leftPrec, int rightPrec) { + writer.print("SAME"); + final SqlWriter.Frame frame = + writer.startList(SqlWriter.FrameTypeEnum.FUN_CALL, "(", ")"); + + for (int i = 0; i < operands.size(); i++) { + if (i > 0) { + writer.sep(","); + } + operands.get(i).unparse(writer, 0, 0); + } + + writer.endList(frame); + } + + /** + * Returns the number of operands (element references) in this SAME call. + */ + public int getOperandCount() { + return operands.size(); + } + + /** + * Returns the operand at the specified index. + */ + public SqlNode getOperand(int index) { + return operands.get(index); + } +} diff --git a/geaflow/geaflow-dsl/geaflow-dsl-parser/src/test/java/org/apache/geaflow/dsl/IsoGqlSyntaxTest.java b/geaflow/geaflow-dsl/geaflow-dsl-parser/src/test/java/org/apache/geaflow/dsl/IsoGqlSyntaxTest.java index adf537792..38916e051 100644 --- a/geaflow/geaflow-dsl/geaflow-dsl-parser/src/test/java/org/apache/geaflow/dsl/IsoGqlSyntaxTest.java +++ b/geaflow/geaflow-dsl/geaflow-dsl-parser/src/test/java/org/apache/geaflow/dsl/IsoGqlSyntaxTest.java @@ -31,4 +31,11 @@ public void testIsoGQLMatch() throws Exception { String unParseStmts = parseStmtsAndUnParse(parseStmtsAndUnParse(unParseSql)); Assert.assertEquals(unParseStmts, unParseSql); } + + @Test + public void testIsoGQLSamePredicate() throws Exception { + String unParseSql = parseSqlAndUnParse("IsoGQLSame.sql"); + String unParseStmts = parseStmtsAndUnParse(parseStmtsAndUnParse(unParseSql)); + Assert.assertEquals(unParseStmts, unParseSql); + } } diff --git a/geaflow/geaflow-dsl/geaflow-dsl-parser/src/test/resources/IsoGQLSame.sql b/geaflow/geaflow-dsl/geaflow-dsl-parser/src/test/resources/IsoGQLSame.sql new file mode 100644 index 000000000..b50e488fe --- /dev/null +++ b/geaflow/geaflow-dsl/geaflow-dsl-parser/src/test/resources/IsoGQLSame.sql @@ -0,0 +1,23 @@ +/* + * 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. + */ + +MATCH (a:person)-[:know]->(b:person), (b)-[:know]->(c:person) WHERE SAME(a, c) RETURN a.name, b.name; +MATCH (a:person)-[:know]->(b:person), (c:person)-[:know]->(d:person) WHERE SAME(a, c) RETURN a.id, b.id, c.id, d.id; +MATCH (a:person {id: 1})-[e1:know]->(b:person), (c:person)-[e2:know]->(d:person) WHERE SAME(a, b, c) RETURN a.id, b.id; +MATCH (a:person)-[e1:know]->(b:person)-[e2:know]->(c:person) WHERE SAME(a, c) RETURN a.name, b.name, c.name; diff --git a/geaflow/geaflow-dsl/geaflow-dsl-plan/src/main/java/org/apache/geaflow/dsl/schema/function/BuildInSqlOperatorTable.java b/geaflow/geaflow-dsl/geaflow-dsl-plan/src/main/java/org/apache/geaflow/dsl/schema/function/BuildInSqlOperatorTable.java index 52bcc6834..6b889e7c8 100644 --- a/geaflow/geaflow-dsl/geaflow-dsl-plan/src/main/java/org/apache/geaflow/dsl/schema/function/BuildInSqlOperatorTable.java +++ b/geaflow/geaflow-dsl/geaflow-dsl-plan/src/main/java/org/apache/geaflow/dsl/schema/function/BuildInSqlOperatorTable.java @@ -24,6 +24,7 @@ import org.apache.calcite.sql.SqlOperator; import org.apache.calcite.sql.fun.SqlStdOperatorTable; import org.apache.calcite.sql.util.ReflectiveSqlOperatorTable; +import org.apache.geaflow.dsl.operator.SqlSameOperator; public class BuildInSqlOperatorTable extends ReflectiveSqlOperatorTable { @@ -173,7 +174,9 @@ public class BuildInSqlOperatorTable extends ReflectiveSqlOperatorTable { SqlStdOperatorTable.CUME_DIST, SqlStdOperatorTable.ROW_NUMBER, SqlStdOperatorTable.LAG, - SqlStdOperatorTable.LEAD + SqlStdOperatorTable.LEAD, + // ISO-GQL SAME predicate + SqlSameOperator.INSTANCE }; public BuildInSqlOperatorTable() { diff --git a/geaflow/geaflow-dsl/geaflow-dsl-plan/src/main/java/org/apache/geaflow/dsl/schema/function/GeaFlowBuiltinFunctions.java b/geaflow/geaflow-dsl/geaflow-dsl-plan/src/main/java/org/apache/geaflow/dsl/schema/function/GeaFlowBuiltinFunctions.java index ef89e02f7..3c5e669a7 100644 --- a/geaflow/geaflow-dsl/geaflow-dsl-plan/src/main/java/org/apache/geaflow/dsl/schema/function/GeaFlowBuiltinFunctions.java +++ b/geaflow/geaflow-dsl/geaflow-dsl-plan/src/main/java/org/apache/geaflow/dsl/schema/function/GeaFlowBuiltinFunctions.java @@ -23,9 +23,12 @@ import java.math.RoundingMode; import java.sql.Timestamp; import java.util.Calendar; +import java.util.Objects; import java.util.Random; import org.apache.commons.lang3.time.DateUtils; import org.apache.geaflow.common.binary.BinaryString; +import org.apache.geaflow.dsl.common.data.RowEdge; +import org.apache.geaflow.dsl.common.data.RowVertex; public final class GeaFlowBuiltinFunctions { @@ -1428,6 +1431,91 @@ public static Boolean equal(Object a, Object b) { return a.equals(b); } + /** + * ISO-GQL SAME predicate function for vertices. + * Checks if two vertices refer to the same element by comparing their IDs. + * + * @param a first vertex + * @param b second vertex + * @return true if vertices have the same ID, false otherwise, null if either is null + */ + public static Boolean same(RowVertex a, RowVertex b) { + if (a == null || b == null) { + return null; + } + return Objects.equals(a.getId(), b.getId()); + } + + /** + * ISO-GQL SAME predicate function for edges. + * Checks if two edges refer to the same element by comparing their source and target IDs. + * + * @param a first edge + * @param b second edge + * @return true if edges have the same source and target IDs, false otherwise, null if either is null + */ + public static Boolean same(RowEdge a, RowEdge b) { + if (a == null || b == null) { + return null; + } + return Objects.equals(a.getSrcId(), b.getSrcId()) + && Objects.equals(a.getTargetId(), b.getTargetId()); + } + + /** + * ISO-GQL SAME predicate function (fallback for mixed or unknown types). + * Checks if two graph elements refer to the same element by comparing their identities. + * For vertices, compares vertex IDs. + * For edges, compares both source and target IDs. + * + * @param a first element (vertex or edge) + * @param b second element (vertex or edge) + * @return true if elements have the same identity, false otherwise, null if either is null + */ + public static Boolean same(Object a, Object b) { + if (a == null || b == null) { + return null; + } + // Delegate to type-specific overloads when possible + if (a instanceof RowVertex && b instanceof RowVertex) { + return same((RowVertex) a, (RowVertex) b); + } + if (a instanceof RowEdge && b instanceof RowEdge) { + return same((RowEdge) a, (RowEdge) b); + } + // Different types cannot be the same + return false; + } + + /** + * ISO-GQL SAME predicate function for multiple elements. + * Checks if all elements refer to the same graph element. + * Returns true only if all elements are identical (same type and same identity). + * + * @param elements array of elements to compare (minimum 2 required) + * @return true if all elements have the same identity, false otherwise, null if any is null + */ + public static Boolean same(Object... elements) { + if (elements == null || elements.length < 2) { + return null; + } + // Check for any null elements + for (Object e : elements) { + if (e == null) { + return null; + } + } + // Compare all elements with the first one + Object first = elements[0]; + for (int i = 1; i < elements.length; i++) { + Boolean result = same(first, elements[i]); + if (result == null || !result) { + return result; + } + } + return true; + } + public static Boolean unequal(Long a, Long b) { if (a == null || b == null) { return null; diff --git a/geaflow/geaflow-dsl/geaflow-dsl-plan/src/test/java/org/apache/geaflow/dsl/schema/function/SameTest.java b/geaflow/geaflow-dsl/geaflow-dsl-plan/src/test/java/org/apache/geaflow/dsl/schema/function/SameTest.java new file mode 100644 index 000000000..52065e160 --- /dev/null +++ b/geaflow/geaflow-dsl/geaflow-dsl-plan/src/test/java/org/apache/geaflow/dsl/schema/function/SameTest.java @@ -0,0 +1,255 @@ +/* + * 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.geaflow.dsl.schema.function; + +import org.apache.geaflow.dsl.common.data.impl.ObjectRow; +import org.apache.geaflow.dsl.common.data.impl.types.ObjectEdge; +import org.apache.geaflow.dsl.common.data.impl.types.ObjectVertex; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * Unit tests for the ISO-GQL SAME predicate function. + */ +public class SameTest { + + @Test + public void testSameWithIdenticalVertices() { + // Create two vertices with the same ID + ObjectVertex v1 = new ObjectVertex(1, null, ObjectRow.create("Alice", 25)); + ObjectVertex v2 = new ObjectVertex(1, null, ObjectRow.create("Bob", 30)); + + Boolean result = GeaFlowBuiltinFunctions.same(v1, v2); + Assert.assertTrue(result, "Vertices with same ID should return true"); + } + + @Test + public void testSameWithDifferentVertices() { + // Create two vertices with different IDs + ObjectVertex v1 = new ObjectVertex(1, null, ObjectRow.create("Alice", 25)); + ObjectVertex v2 = new ObjectVertex(2, null, ObjectRow.create("Bob", 30)); + + Boolean result = GeaFlowBuiltinFunctions.same(v1, v2); + Assert.assertFalse(result, "Vertices with different IDs should return false"); + } + + @Test + public void testSameWithIdenticalEdges() { + // Create two edges with the same source and target IDs + ObjectEdge e1 = new ObjectEdge(1, 2, ObjectRow.create("knows")); + ObjectEdge e2 = new ObjectEdge(1, 2, ObjectRow.create("likes")); + + Boolean result = GeaFlowBuiltinFunctions.same(e1, e2); + Assert.assertTrue(result, "Edges with same source and target IDs should return true"); + } + + @Test + public void testSameWithDifferentEdgesSameSource() { + // Create two edges with the same source but different target IDs + ObjectEdge e1 = new ObjectEdge(1, 2, ObjectRow.create("knows")); + ObjectEdge e2 = new ObjectEdge(1, 3, ObjectRow.create("knows")); + + Boolean result = GeaFlowBuiltinFunctions.same(e1, e2); + Assert.assertFalse(result, "Edges with different target IDs should return false"); + } + + @Test + public void testSameWithDifferentEdgesSameTarget() { + // Create two edges with different source but same target IDs + ObjectEdge e1 = new ObjectEdge(1, 2, ObjectRow.create("knows")); + ObjectEdge e2 = new ObjectEdge(3, 2, ObjectRow.create("knows")); + + Boolean result = GeaFlowBuiltinFunctions.same(e1, e2); + Assert.assertFalse(result, "Edges with different source IDs should return false"); + } + + @Test + public void testSameWithDifferentEdges() { + // Create two edges with completely different IDs + ObjectEdge e1 = new ObjectEdge(1, 2, ObjectRow.create("knows")); + ObjectEdge e2 = new ObjectEdge(3, 4, ObjectRow.create("knows")); + + Boolean result = GeaFlowBuiltinFunctions.same(e1, e2); + Assert.assertFalse(result, "Edges with different IDs should return false"); + } + + @Test + public void testSameWithMixedTypes() { + // Test vertex and edge - should return false + ObjectVertex v = new ObjectVertex(1, null, ObjectRow.create("Alice", 25)); + ObjectEdge e = new ObjectEdge(1, 2, ObjectRow.create("knows")); + + Boolean result = GeaFlowBuiltinFunctions.same(v, e); + Assert.assertFalse(result, "Vertex and edge should return false"); + } + + @Test + public void testSameWithNullFirst() { + // Test with first argument null + ObjectVertex v = new ObjectVertex(1, null, ObjectRow.create("Alice", 25)); + + Boolean result = GeaFlowBuiltinFunctions.same(null, v); + Assert.assertNull(result, "Null first argument should return null"); + } + + @Test + public void testSameWithNullSecond() { + // Test with second argument null + ObjectVertex v = new ObjectVertex(1, null, ObjectRow.create("Alice", 25)); + + Boolean result = GeaFlowBuiltinFunctions.same(v, null); + Assert.assertNull(result, "Null second argument should return null"); + } + + @Test + public void testSameWithBothNull() { + // Test with both arguments null - use explicit cast to Object to resolve ambiguity + Boolean result = GeaFlowBuiltinFunctions.same((Object) null, (Object) null); + Assert.assertNull(result, "Both null arguments should return null"); + } + + @Test + public void testSameWithStringIds() { + // Test with string IDs instead of integer IDs + ObjectVertex v1 = new ObjectVertex("user123", null, ObjectRow.create("Alice", 25)); + ObjectVertex v2 = new ObjectVertex("user123", null, ObjectRow.create("Bob", 30)); + + Boolean result = GeaFlowBuiltinFunctions.same(v1, v2); + Assert.assertTrue(result, "Vertices with same string ID should return true"); + } + + @Test + public void testSameWithDifferentStringIds() { + // Test with different string IDs + ObjectVertex v1 = new ObjectVertex("user123", null, ObjectRow.create("Alice", 25)); + ObjectVertex v2 = new ObjectVertex("user456", null, ObjectRow.create("Bob", 30)); + + Boolean result = GeaFlowBuiltinFunctions.same(v1, v2); + Assert.assertFalse(result, "Vertices with different string IDs should return false"); + } + + @Test + public void testSameWithInvalidTypes() { + // Test with objects that are not RowVertex or RowEdge + String s1 = "test"; + String s2 = "test"; + + Boolean result = GeaFlowBuiltinFunctions.same(s1, s2); + Assert.assertFalse(result, "Non-graph elements should return false"); + } + + // Tests for type-specific overloads (RowVertex, RowEdge) + + @Test + public void testSameVertexOverloadWithIdenticalIds() { + // Test the type-specific RowVertex overload + ObjectVertex v1 = new ObjectVertex(100, null, ObjectRow.create("Alice", 25)); + ObjectVertex v2 = new ObjectVertex(100, null, ObjectRow.create("Bob", 30)); + + // Explicitly call with RowVertex types + Boolean result = GeaFlowBuiltinFunctions.same((org.apache.geaflow.dsl.common.data.RowVertex) v1, + (org.apache.geaflow.dsl.common.data.RowVertex) v2); + Assert.assertTrue(result, "Type-specific vertex overload should work"); + } + + @Test + public void testSameEdgeOverloadWithIdenticalIds() { + // Test the type-specific RowEdge overload + ObjectEdge e1 = new ObjectEdge(10, 20, ObjectRow.create("knows")); + ObjectEdge e2 = new ObjectEdge(10, 20, ObjectRow.create("likes")); + + // Explicitly call with RowEdge types + Boolean result = GeaFlowBuiltinFunctions.same((org.apache.geaflow.dsl.common.data.RowEdge) e1, + (org.apache.geaflow.dsl.common.data.RowEdge) e2); + Assert.assertTrue(result, "Type-specific edge overload should work"); + } + + // Tests for multi-argument same() varargs method + + @Test + public void testSameWithThreeIdenticalVertices() { + // Test varargs with 3 identical vertices + ObjectVertex v1 = new ObjectVertex(1, null, ObjectRow.create("Alice", 25)); + ObjectVertex v2 = new ObjectVertex(1, null, ObjectRow.create("Bob", 30)); + ObjectVertex v3 = new ObjectVertex(1, null, ObjectRow.create("Charlie", 35)); + + Boolean result = GeaFlowBuiltinFunctions.same(v1, v2, v3); + Assert.assertTrue(result, "Three vertices with same ID should return true"); + } + + @Test + public void testSameWithThreeVerticesOneDifferent() { + // Test varargs with one different vertex + ObjectVertex v1 = new ObjectVertex(1, null, ObjectRow.create("Alice", 25)); + ObjectVertex v2 = new ObjectVertex(1, null, ObjectRow.create("Bob", 30)); + ObjectVertex v3 = new ObjectVertex(2, null, ObjectRow.create("Charlie", 35)); + + Boolean result = GeaFlowBuiltinFunctions.same(v1, v2, v3); + Assert.assertFalse(result, "Three vertices with one different ID should return false"); + } + + @Test + public void testSameWithFourIdenticalEdges() { + // Test varargs with 4 identical edges + ObjectEdge e1 = new ObjectEdge(1, 2, ObjectRow.create("knows")); + ObjectEdge e2 = new ObjectEdge(1, 2, ObjectRow.create("likes")); + ObjectEdge e3 = new ObjectEdge(1, 2, ObjectRow.create("follows")); + ObjectEdge e4 = new ObjectEdge(1, 2, ObjectRow.create("trusts")); + + Boolean result = GeaFlowBuiltinFunctions.same(e1, e2, e3, e4); + Assert.assertTrue(result, "Four edges with same source and target IDs should return true"); + } + + @Test + public void testSameWithMultipleNullInMiddle() { + // Test varargs with null in the middle + ObjectVertex v1 = new ObjectVertex(1, null, ObjectRow.create("Alice", 25)); + ObjectVertex v3 = new ObjectVertex(1, null, ObjectRow.create("Charlie", 35)); + + Boolean result = GeaFlowBuiltinFunctions.same(v1, null, v3); + Assert.assertNull(result, "Varargs with null element should return null"); + } + + @Test + public void testSameWithEmptyVarargs() { + // Test varargs with no arguments (should return null) + Boolean result = GeaFlowBuiltinFunctions.same(new Object[0]); + Assert.assertNull(result, "Empty varargs should return null"); + } + + @Test + public void testSameWithSingleVararg() { + // Test varargs with single argument (should return null - need at least 2) + ObjectVertex v1 = new ObjectVertex(1, null, ObjectRow.create("Alice", 25)); + + Boolean result = GeaFlowBuiltinFunctions.same(new Object[]{v1}); + Assert.assertNull(result, "Single vararg should return null"); + } + + @Test + public void testSameWithMixedTypesInVarargs() { + // Test varargs with mixed vertex and edge types + ObjectVertex v = new ObjectVertex(1, null, ObjectRow.create("Alice", 25)); + ObjectEdge e = new ObjectEdge(1, 2, ObjectRow.create("knows")); + + Boolean result = GeaFlowBuiltinFunctions.same(v, e); + Assert.assertFalse(result, "Mixed vertex and edge in varargs should return false"); + } +} diff --git a/geaflow/geaflow-dsl/geaflow-dsl-runtime/src/main/java/org/apache/geaflow/dsl/runtime/expression/BuildInExpression.java b/geaflow/geaflow-dsl/geaflow-dsl-runtime/src/main/java/org/apache/geaflow/dsl/runtime/expression/BuildInExpression.java index 80698bccf..4093366e6 100644 --- a/geaflow/geaflow-dsl/geaflow-dsl-runtime/src/main/java/org/apache/geaflow/dsl/runtime/expression/BuildInExpression.java +++ b/geaflow/geaflow-dsl/geaflow-dsl-runtime/src/main/java/org/apache/geaflow/dsl/runtime/expression/BuildInExpression.java @@ -89,6 +89,8 @@ public class BuildInExpression extends AbstractReflectCallExpression { public static final String CURRENT_TIMESTAMP = "currentTimestamp"; + public static final String SAME = "same"; + public BuildInExpression(List inputs, IType outputType, Class implementClass, String methodName) { super(inputs, outputType, implementClass, methodName); diff --git a/geaflow/geaflow-dsl/geaflow-dsl-runtime/src/main/java/org/apache/geaflow/dsl/runtime/expression/ExpressionTranslator.java b/geaflow/geaflow-dsl/geaflow-dsl-runtime/src/main/java/org/apache/geaflow/dsl/runtime/expression/ExpressionTranslator.java index b91b94144..b1a70d9fd 100644 --- a/geaflow/geaflow-dsl/geaflow-dsl-runtime/src/main/java/org/apache/geaflow/dsl/runtime/expression/ExpressionTranslator.java +++ b/geaflow/geaflow-dsl/geaflow-dsl-runtime/src/main/java/org/apache/geaflow/dsl/runtime/expression/ExpressionTranslator.java @@ -399,6 +399,9 @@ private Expression processOtherTrans(List inputs, RexCall call) { case "CURRENT_TIMESTAMP": functionName = BuildInExpression.CURRENT_TIMESTAMP; break; + case "SAME": + functionName = BuildInExpression.SAME; + break; default: } if (functionName != null) {