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..da28fd03e --- /dev/null +++ b/geaflow/geaflow-dsl/geaflow-dsl-parser/src/main/java/org/apache/geaflow/dsl/sqlnode/SqlSameCall.java @@ -0,0 +1,129 @@ +/* + * 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.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); + this.operands = 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..dbd4d2a19 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,41 @@ public static Boolean equal(Object a, Object b) { return a.equals(b); } + /** + * ISO-GQL SAME predicate function. + * 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; + } + try { + // Handle vertex-vertex comparison + if (a instanceof RowVertex && b instanceof RowVertex) { + Object idA = ((RowVertex) a).getId(); + Object idB = ((RowVertex) b).getId(); + return Objects.equals(idA, idB); + } + // Handle edge-edge comparison + if (a instanceof RowEdge && b instanceof RowEdge) { + RowEdge edgeA = (RowEdge) a; + RowEdge edgeB = (RowEdge) b; + return Objects.equals(edgeA.getSrcId(), edgeB.getSrcId()) + && Objects.equals(edgeA.getTargetId(), edgeB.getTargetId()); + } + // Different types cannot be the same + return false; + } catch (ClassCastException ex) { + return false; + } + } + 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..8092189ef --- /dev/null +++ b/geaflow/geaflow-dsl/geaflow-dsl-plan/src/test/java/org/apache/geaflow/dsl/schema/function/SameTest.java @@ -0,0 +1,157 @@ +/* + * 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 + Boolean result = GeaFlowBuiltinFunctions.same(null, 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"); + } +} 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) {