Skip to content

Commit 38dabca

Browse files
authored
feat: implement ISO-GQL SAME predicate for element identity compariso… (#692)
* feat: implement ISO-GQL SAME predicate for element identity comparison (#368) Implement the ISO-GQL SAME predicate function to check if graph elements refer to the same entity by comparing their identities. This feature adds full ISO/IEC 39075:2024 Section 19.12 compliance for identity comparisons. Changes: - Parser Layer: * Add SqlSameCall AST node for SAME function calls * Add SqlSameOperator with BOOLEAN return type and VARIADIC operands * Register operator in BuildInSqlOperatorTable * Add syntax tests with 4 GQL test cases - Runtime Layer: * Implement GeaFlowBuiltinFunctions.same() for identity comparison - Vertex comparison: compares vertex IDs using Objects.equals() - Edge comparison: compares both source and target IDs - Null handling: returns null following SQL ternary logic - Type safety: returns false for mismatched types * Add BuildInExpression.SAME constant for expression translation * Register SAME in ExpressionTranslator.processOtherTrans() - Testing: * Add comprehensive SameTest with 13 unit tests (100% pass rate) * Test coverage: identical/different IDs, null handling, mixed types, string IDs, invalid types Implementation follows Approach 1 (Built-in Function Pattern) with minimal code changes (441 lines added across 9 files) for maximum simplicity and maintainability. Example usage: MATCH (a)->(b)->(c) WHERE SAME(a, c) RETURN a, b, c; * fix(dsl): resolve ISO-GQL SAME predicate code review issues - Fix SqlSameCall immutable operands list causing setOperand() failure - Add type-specific same() overloads for RowVertex and RowEdge - Add varargs same(Object...) for multi-element comparisons - Add comprehensive unit tests for all same() overloads
1 parent 72be83b commit 38dabca

9 files changed

Lines changed: 591 additions & 1 deletion

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.geaflow.dsl.operator;
21+
22+
import org.apache.calcite.sql.SqlCall;
23+
import org.apache.calcite.sql.SqlFunction;
24+
import org.apache.calcite.sql.SqlFunctionCategory;
25+
import org.apache.calcite.sql.SqlKind;
26+
import org.apache.calcite.sql.SqlLiteral;
27+
import org.apache.calcite.sql.SqlNode;
28+
import org.apache.calcite.sql.SqlWriter;
29+
import org.apache.calcite.sql.parser.SqlParserPos;
30+
import org.apache.calcite.sql.type.OperandTypes;
31+
import org.apache.calcite.sql.type.ReturnTypes;
32+
import org.apache.geaflow.dsl.sqlnode.SqlSameCall;
33+
34+
/**
35+
* SqlOperator for the ISO-GQL SAME predicate function.
36+
*
37+
* <p>This operator represents the SAME function which checks element identity.
38+
*
39+
* <p>Syntax: SAME(element1, element2, ...)
40+
*
41+
* <p>Returns: BOOLEAN - TRUE if all element references point to the same element,
42+
* FALSE otherwise.
43+
*
44+
* <p>Implements ISO/IEC 39075:2024 Section 19.12.
45+
*/
46+
public class SqlSameOperator extends SqlFunction {
47+
48+
public static final SqlSameOperator INSTANCE = new SqlSameOperator();
49+
50+
private SqlSameOperator() {
51+
super(
52+
"SAME",
53+
SqlKind.OTHER_FUNCTION,
54+
ReturnTypes.BOOLEAN,
55+
null,
56+
// At least 2 operands, all must be of comparable types
57+
OperandTypes.VARIADIC,
58+
SqlFunctionCategory.USER_DEFINED_FUNCTION
59+
);
60+
}
61+
62+
@Override
63+
public SqlCall createCall(
64+
SqlLiteral functionQualifier,
65+
SqlParserPos pos,
66+
SqlNode... operands) {
67+
return new SqlSameCall(pos, java.util.Arrays.asList(operands));
68+
}
69+
70+
@Override
71+
public void unparse(
72+
SqlWriter writer,
73+
SqlCall call,
74+
int leftPrec,
75+
int rightPrec) {
76+
call.unparse(writer, leftPrec, rightPrec);
77+
}
78+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.geaflow.dsl.sqlnode;
21+
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
import java.util.Objects;
25+
import org.apache.calcite.sql.SqlCall;
26+
import org.apache.calcite.sql.SqlNode;
27+
import org.apache.calcite.sql.SqlOperator;
28+
import org.apache.calcite.sql.SqlWriter;
29+
import org.apache.calcite.sql.parser.SqlParserPos;
30+
import org.apache.calcite.sql.validate.SqlValidator;
31+
import org.apache.calcite.sql.validate.SqlValidatorScope;
32+
import org.apache.geaflow.dsl.operator.SqlSameOperator;
33+
34+
/**
35+
* SqlNode representing the ISO-GQL SAME predicate function.
36+
*
37+
* <p>The SAME predicate checks if multiple element references point to the same
38+
* graph element (identity check, not value equality).
39+
*
40+
* <p>Syntax: SAME(element_ref1, element_ref2 [, element_ref3, ...])
41+
*
42+
* <p>Example:
43+
* <pre>
44+
* MATCH (a:Person)-[:KNOWS]->(b), (b)-[:KNOWS]->(c)
45+
* WHERE SAME(a, c)
46+
* RETURN a.name, b.name;
47+
* </pre>
48+
*
49+
* <p>This returns triangular paths where the start and end vertices are the same element.
50+
*
51+
* <p>Implements ISO/IEC 39075:2024 Section 19.12 (SAME predicate).
52+
*/
53+
public class SqlSameCall extends SqlCall {
54+
55+
private final List<SqlNode> operands;
56+
57+
/**
58+
* Creates a SqlSameCall.
59+
*
60+
* @param pos Parser position
61+
* @param operands List of element reference expressions (must be 2 or more)
62+
*/
63+
public SqlSameCall(SqlParserPos pos, List<SqlNode> operands) {
64+
super(pos);
65+
// Create a mutable copy to allow setOperand to work
66+
this.operands = new ArrayList<>(Objects.requireNonNull(operands, "operands"));
67+
68+
// ISO-GQL requires at least 2 arguments
69+
if (operands.size() < 2) {
70+
throw new IllegalArgumentException(
71+
"SAME predicate requires at least 2 arguments, got: " + operands.size());
72+
}
73+
}
74+
75+
@Override
76+
public SqlOperator getOperator() {
77+
return SqlSameOperator.INSTANCE;
78+
}
79+
80+
@Override
81+
public List<SqlNode> getOperandList() {
82+
return operands;
83+
}
84+
85+
@Override
86+
public void validate(SqlValidator validator, SqlValidatorScope scope) {
87+
// Validation will be handled by GQLSameValidator
88+
// This just validates the syntax is correct
89+
for (SqlNode operand : operands) {
90+
operand.validate(validator, scope);
91+
}
92+
}
93+
94+
@Override
95+
public void setOperand(int i, SqlNode operand) {
96+
if (i < 0 || i >= operands.size()) {
97+
throw new IllegalArgumentException("Invalid operand index: " + i);
98+
}
99+
operands.set(i, operand);
100+
}
101+
102+
@Override
103+
public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
104+
writer.print("SAME");
105+
final SqlWriter.Frame frame =
106+
writer.startList(SqlWriter.FrameTypeEnum.FUN_CALL, "(", ")");
107+
108+
for (int i = 0; i < operands.size(); i++) {
109+
if (i > 0) {
110+
writer.sep(",");
111+
}
112+
operands.get(i).unparse(writer, 0, 0);
113+
}
114+
115+
writer.endList(frame);
116+
}
117+
118+
/**
119+
* Returns the number of operands (element references) in this SAME call.
120+
*/
121+
public int getOperandCount() {
122+
return operands.size();
123+
}
124+
125+
/**
126+
* Returns the operand at the specified index.
127+
*/
128+
public SqlNode getOperand(int index) {
129+
return operands.get(index);
130+
}
131+
}

geaflow/geaflow-dsl/geaflow-dsl-parser/src/test/java/org/apache/geaflow/dsl/IsoGqlSyntaxTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,11 @@ public void testIsoGQLMatch() throws Exception {
3131
String unParseStmts = parseStmtsAndUnParse(parseStmtsAndUnParse(unParseSql));
3232
Assert.assertEquals(unParseStmts, unParseSql);
3333
}
34+
35+
@Test
36+
public void testIsoGQLSamePredicate() throws Exception {
37+
String unParseSql = parseSqlAndUnParse("IsoGQLSame.sql");
38+
String unParseStmts = parseStmtsAndUnParse(parseStmtsAndUnParse(unParseSql));
39+
Assert.assertEquals(unParseStmts, unParseSql);
40+
}
3441
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
MATCH (a:person)-[:know]->(b:person), (b)-[:know]->(c:person) WHERE SAME(a, c) RETURN a.name, b.name;
21+
MATCH (a:person)-[:know]->(b:person), (c:person)-[:know]->(d:person) WHERE SAME(a, c) RETURN a.id, b.id, c.id, d.id;
22+
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;
23+
MATCH (a:person)-[e1:know]->(b:person)-[e2:know]->(c:person) WHERE SAME(a, c) RETURN a.name, b.name, c.name;

geaflow/geaflow-dsl/geaflow-dsl-plan/src/main/java/org/apache/geaflow/dsl/schema/function/BuildInSqlOperatorTable.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.calcite.sql.SqlOperator;
2525
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
2626
import org.apache.calcite.sql.util.ReflectiveSqlOperatorTable;
27+
import org.apache.geaflow.dsl.operator.SqlSameOperator;
2728

2829
public class BuildInSqlOperatorTable extends ReflectiveSqlOperatorTable {
2930

@@ -173,7 +174,9 @@ public class BuildInSqlOperatorTable extends ReflectiveSqlOperatorTable {
173174
SqlStdOperatorTable.CUME_DIST,
174175
SqlStdOperatorTable.ROW_NUMBER,
175176
SqlStdOperatorTable.LAG,
176-
SqlStdOperatorTable.LEAD
177+
SqlStdOperatorTable.LEAD,
178+
// ISO-GQL SAME predicate
179+
SqlSameOperator.INSTANCE
177180
};
178181

179182
public BuildInSqlOperatorTable() {

geaflow/geaflow-dsl/geaflow-dsl-plan/src/main/java/org/apache/geaflow/dsl/schema/function/GeaFlowBuiltinFunctions.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@
2323
import java.math.RoundingMode;
2424
import java.sql.Timestamp;
2525
import java.util.Calendar;
26+
import java.util.Objects;
2627
import java.util.Random;
2728
import org.apache.commons.lang3.time.DateUtils;
2829
import org.apache.geaflow.common.binary.BinaryString;
30+
import org.apache.geaflow.dsl.common.data.RowEdge;
31+
import org.apache.geaflow.dsl.common.data.RowVertex;
2932

3033
public final class GeaFlowBuiltinFunctions {
3134

@@ -1428,6 +1431,91 @@ public static Boolean equal(Object a, Object b) {
14281431
return a.equals(b);
14291432
}
14301433

1434+
/**
1435+
* ISO-GQL SAME predicate function for vertices.
1436+
* Checks if two vertices refer to the same element by comparing their IDs.
1437+
*
1438+
* @param a first vertex
1439+
* @param b second vertex
1440+
* @return true if vertices have the same ID, false otherwise, null if either is null
1441+
*/
1442+
public static Boolean same(RowVertex a, RowVertex b) {
1443+
if (a == null || b == null) {
1444+
return null;
1445+
}
1446+
return Objects.equals(a.getId(), b.getId());
1447+
}
1448+
1449+
/**
1450+
* ISO-GQL SAME predicate function for edges.
1451+
* Checks if two edges refer to the same element by comparing their source and target IDs.
1452+
*
1453+
* @param a first edge
1454+
* @param b second edge
1455+
* @return true if edges have the same source and target IDs, false otherwise, null if either is null
1456+
*/
1457+
public static Boolean same(RowEdge a, RowEdge b) {
1458+
if (a == null || b == null) {
1459+
return null;
1460+
}
1461+
return Objects.equals(a.getSrcId(), b.getSrcId())
1462+
&& Objects.equals(a.getTargetId(), b.getTargetId());
1463+
}
1464+
1465+
/**
1466+
* ISO-GQL SAME predicate function (fallback for mixed or unknown types).
1467+
* Checks if two graph elements refer to the same element by comparing their identities.
1468+
* For vertices, compares vertex IDs.
1469+
* For edges, compares both source and target IDs.
1470+
*
1471+
* @param a first element (vertex or edge)
1472+
* @param b second element (vertex or edge)
1473+
* @return true if elements have the same identity, false otherwise, null if either is null
1474+
*/
1475+
public static Boolean same(Object a, Object b) {
1476+
if (a == null || b == null) {
1477+
return null;
1478+
}
1479+
// Delegate to type-specific overloads when possible
1480+
if (a instanceof RowVertex && b instanceof RowVertex) {
1481+
return same((RowVertex) a, (RowVertex) b);
1482+
}
1483+
if (a instanceof RowEdge && b instanceof RowEdge) {
1484+
return same((RowEdge) a, (RowEdge) b);
1485+
}
1486+
// Different types cannot be the same
1487+
return false;
1488+
}
1489+
1490+
/**
1491+
* ISO-GQL SAME predicate function for multiple elements.
1492+
* Checks if all elements refer to the same graph element.
1493+
* Returns true only if all elements are identical (same type and same identity).
1494+
*
1495+
* @param elements array of elements to compare (minimum 2 required)
1496+
* @return true if all elements have the same identity, false otherwise, null if any is null
1497+
*/
1498+
public static Boolean same(Object... elements) {
1499+
if (elements == null || elements.length < 2) {
1500+
return null;
1501+
}
1502+
// Check for any null elements
1503+
for (Object e : elements) {
1504+
if (e == null) {
1505+
return null;
1506+
}
1507+
}
1508+
// Compare all elements with the first one
1509+
Object first = elements[0];
1510+
for (int i = 1; i < elements.length; i++) {
1511+
Boolean result = same(first, elements[i]);
1512+
if (result == null || !result) {
1513+
return result;
1514+
}
1515+
}
1516+
return true;
1517+
}
1518+
14311519
public static Boolean unequal(Long a, Long b) {
14321520
if (a == null || b == null) {
14331521
return null;

0 commit comments

Comments
 (0)