Skip to content

Commit a355040

Browse files
Merge pull request #136 from oracle/OPTIONAL_MATCH
OPTIONAL MATCH support
2 parents 2b34386 + 7f062a8 commit a355040

24 files changed

+741
-236
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (C) 2013 - 2024 Oracle and/or its affiliates. All rights reserved.
3+
*/
4+
package oracle.pgql.lang.ir;
5+
6+
import java.util.LinkedHashSet;
7+
import java.util.Set;
8+
9+
public class OptionalGraphPattern extends GraphPattern {
10+
11+
public OptionalGraphPattern(Set<QueryVertex> vertices, LinkedHashSet<VertexPairConnection> connections,
12+
LinkedHashSet<QueryExpression> constraints) {
13+
super(vertices, connections, constraints);
14+
}
15+
16+
@Override
17+
public TableExpressionType getTableExpressionType() {
18+
return TableExpressionType.OPTIONAL_GRAPH_PATTERN;
19+
}
20+
21+
@Override
22+
public String toString() {
23+
return "OPTIONAL " + super.toString();
24+
}
25+
26+
@Override
27+
public void accept(QueryExpressionVisitor v) {
28+
v.visit(this);
29+
}
30+
}

graph-query-ir/src/main/java/oracle/pgql/lang/ir/PgqlUtils.java

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import oracle.pgql.lang.ir.unnest.OneRowPerStep;
4545
import oracle.pgql.lang.ir.unnest.OneRowPerVertex;
4646
import oracle.pgql.lang.ir.unnest.RowsPerMatch;
47+
import oracle.pgql.lang.ir.unnest.RowsPerMatchType;
4748

4849
public class PgqlUtils {
4950

@@ -53,7 +54,7 @@ public class PgqlUtils {
5354

5455
// make sure to keep in sync with list of reserved words in pgql-spoofax/syntax/Names.sdf3
5556
private final static Set<String> RESERVED_WORDS = new HashSet<>(
56-
Arrays.asList("true", "false", "null", "not", "distinct"));
57+
Arrays.asList("true", "false", "null", "not", "distinct", "optional"));
5758

5859
static DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.#");
5960

@@ -306,15 +307,23 @@ private static String printPgqlString(GraphPattern graphPattern, SchemaQualified
306307
Set<QueryVertex> uncoveredVertices = new LinkedHashSet<>(graphPattern.getVertices());
307308
List<String> graphPatternMatches = new ArrayList<String>();
308309

309-
boolean parenthesizeMatch = !graphPattern.getConstraints().isEmpty() && !isLastTableExpression;
310+
boolean parenthesizeMatch = !graphPattern.getConstraints().isEmpty()
311+
&& !isLastTableExpression || graphPattern instanceof OptionalGraphPattern;
310312

311313
Iterator<VertexPairConnection> connectionIt = graphPattern.getConnections().iterator();
314+
RowsPerMatch rowsPerMatchForParenthesizedPath = null;
312315
while (connectionIt.hasNext()) {
313316
VertexPairConnection connection = connectionIt.next();
314317
uncoveredVertices.remove(connection.getSrc());
315318
uncoveredVertices.remove(connection.getDst());
316319
if (isVariableLengthPathPatternNotReaches(connection)) {
317-
graphPatternMatches.add(connection.toString());
320+
QueryPath path = (QueryPath) connection;
321+
if (parenthesizeMatch) {
322+
graphPatternMatches.add(path.toString());
323+
rowsPerMatchForParenthesizedPath = path.getRowsPerMatch();
324+
} else {
325+
graphPatternMatches.add(path.toString() + printRowsClause(path.getRowsPerMatch()));
326+
}
318327
} else {
319328
graphPatternMatches.add(connection.getSrc() + " " + connection + " " + connection.getDst());
320329
}
@@ -329,16 +338,14 @@ private static String printPgqlString(GraphPattern graphPattern, SchemaQualified
329338
String result;
330339
if (parenthesizeMatch) {
331340
result = "MATCH ( " + graphPatternMatches.stream().collect(Collectors.joining("\n , "));
341+
result += printWhereClause(graphPattern.getConstraints());
342+
result += ")" + printOnClause(graphName);
343+
result += printRowsClause(rowsPerMatchForParenthesizedPath);
332344
} else {
333345
result = "MATCH "
334346
+ graphPatternMatches.stream().collect(Collectors.joining(printOnClause(graphName) + "\n , MATCH "))
335347
+ printOnClause(graphName);
336-
}
337-
338-
result += printWhereClause(graphPattern.getConstraints());
339-
340-
if (parenthesizeMatch) {
341-
result += ")" + printOnClause(graphName);
348+
result += printWhereClause(graphPattern.getConstraints());
342349
}
343350

344351
return result;
@@ -354,6 +361,14 @@ private static String printWhereClause(Set<QueryExpression> constraints) {
354361
}
355362
}
356363

364+
private static String printRowsClause(RowsPerMatch rowsPerMatch) {
365+
if (rowsPerMatch == null || rowsPerMatch.getRowsPerMatchType() == RowsPerMatchType.ONE_ROW_PER_MATCH) {
366+
return "";
367+
} else {
368+
return " " + rowsPerMatch;
369+
}
370+
}
371+
357372
private static boolean isVariableLengthPathPatternNotReaches(VertexPairConnection connection) {
358373
if (connection.getVariableType() != VariableType.PATH) {
359374
return false;

graph-query-ir/src/main/java/oracle/pgql/lang/ir/QueryExpressionVisitor.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ public interface QueryExpressionVisitor {
175175

176176
public void visit(GraphPattern graphPattern);
177177

178+
public void visit(OptionalGraphPattern optionalGraphPattern);
179+
178180
public void visit(Projection projection);
179181

180182
public void visit(ExpAsVar expAsVar);

graph-query-ir/src/main/java/oracle/pgql/lang/ir/QueryPath.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,7 @@ private String printVariableLengthPathPattern(PathFindingGoal goal) {
224224
}
225225

226226
result += printHops(this) + " " + getDst();
227-
if (rowsPerMatch != null && rowsPerMatch.getRowsPerMatchType() != RowsPerMatchType.ONE_ROW_PER_MATCH) {
228-
result += " " + rowsPerMatch;
229-
}
227+
230228
return result;
231229
}
232230

graph-query-ir/src/main/java/oracle/pgql/lang/ir/TableExpressionType.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
package oracle.pgql.lang.ir;
55

66
public enum TableExpressionType {
7-
7+
88
GRAPH_PATTERN,
9-
9+
10+
OPTIONAL_GRAPH_PATTERN,
11+
1012
DERIVED_TABLE
1113
}

graph-query-ir/src/main/java/oracle/pgql/lang/util/AbstractQueryExpressionVisitor.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import oracle.pgql.lang.ir.GraphPattern;
99
import oracle.pgql.lang.ir.GraphQuery;
1010
import oracle.pgql.lang.ir.GroupBy;
11+
import oracle.pgql.lang.ir.OptionalGraphPattern;
1112
import oracle.pgql.lang.ir.OrderBy;
1213
import oracle.pgql.lang.ir.OrderByElem;
1314
import oracle.pgql.lang.ir.Projection;
@@ -369,6 +370,7 @@ public void visit(SelectQuery selectQuery) {
369370

370371
private void visitQuery(GraphQuery query) {
371372
query.getTableExpressions().stream().forEach(e -> e.accept(this));
373+
query.getConstraints().stream().forEach(e -> e.accept(this));
372374
if (query.getGroupBy() != null) {
373375
query.getGroupBy().accept(this);
374376
}
@@ -391,6 +393,11 @@ public void visit(GraphPattern graphPattern) {
391393
graphPattern.getConstraints().stream().forEach(e -> e.accept(this));
392394
}
393395

396+
@Override
397+
public void visit(OptionalGraphPattern optionalGraphPattern) {
398+
visit((GraphPattern) optionalGraphPattern);
399+
}
400+
394401
@Override
395402
public void visit(Projection projection) {
396403
projection.getElements().stream().forEach(e -> e.accept(this));

graph-query-ir/src/main/java/oracle/pgql/lang/util/ReplaceExpressions.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import oracle.pgql.lang.ir.GraphPattern;
1313
import oracle.pgql.lang.ir.GraphQuery;
1414
import oracle.pgql.lang.ir.GroupBy;
15+
import oracle.pgql.lang.ir.OptionalGraphPattern;
1516
import oracle.pgql.lang.ir.OrderBy;
1617
import oracle.pgql.lang.ir.OrderByElem;
1718
import oracle.pgql.lang.ir.Projection;
@@ -404,6 +405,11 @@ public void visit(GraphPattern graphPattern) {
404405
graphPattern.setConstraints(replaceInSet(graphPattern.getConstraints()));
405406
}
406407

408+
@Override
409+
public void visit(OptionalGraphPattern optionalGraphPattern) {
410+
optionalGraphPattern.setConstraints(replaceInSet(optionalGraphPattern.getConstraints()));
411+
}
412+
407413
@Override
408414
public void visit(ExpAsVar expAsVar) {
409415
expAsVar.setExp(replaceMatching(expAsVar.getExp()));

pgql-lang/src/main/java/oracle/pgql/lang/SpoofaxAstToGraphQuery.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import oracle.pgql.lang.ir.GraphPattern;
2727
import oracle.pgql.lang.ir.GraphQuery;
2828
import oracle.pgql.lang.ir.GroupBy;
29+
import oracle.pgql.lang.ir.OptionalGraphPattern;
2930
import oracle.pgql.lang.ir.OrderBy;
3031
import oracle.pgql.lang.ir.PathFindingGoal;
3132
import oracle.pgql.lang.ir.PathMode;
@@ -137,6 +138,7 @@ public class SpoofaxAstToGraphQuery {
137138
private static final int POS_VERTICES = 0;
138139
private static final int POS_CONNECTIONS = 1;
139140
private static final int POS_CONSTRAINTS = 2;
141+
private static final int POS_OPTIONAL_MATCH_KEYWORD = 3;
140142

141143
private static final int POS_VERTEX_NAME = 0;
142144
private static final int POS_VERTEX_ORIGIN_OFFSET = 1;
@@ -330,8 +332,12 @@ private static GraphPattern translateGraphPattern(TranslationContext ctx, IStrat
330332
IStrategoTerm constraintsT = getList(graphPatternT.getSubterm(POS_CONSTRAINTS));
331333
LinkedHashSet<QueryExpression> constraints = getQueryExpressions(constraintsT, ctx);
332334

335+
// OPTIONAL match keyword
336+
boolean optionalMatch = isSome(graphPatternT.getSubterm(POS_OPTIONAL_MATCH_KEYWORD));
337+
333338
// graph pattern
334-
graphPattern = new GraphPattern(vertices, connections, constraints);
339+
graphPattern = optionalMatch ? new OptionalGraphPattern(vertices, connections, constraints)
340+
: new GraphPattern(vertices, connections, constraints);
335341
return graphPattern;
336342
}
337343

pgql-lang/src/test/java/oracle/pgql/lang/BugFixTest.java

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,25 @@
88
import static org.junit.Assert.assertNotNull;
99
import static org.junit.Assert.assertTrue;
1010

11+
import java.util.Iterator;
1112
import java.util.List;
1213

1314
import org.junit.Ignore;
1415
import org.junit.Test;
1516

1617
import oracle.pgql.lang.ddl.propertygraph.CreateSuperPropertyGraph;
1718
import oracle.pgql.lang.ir.ExpAsVar;
19+
import oracle.pgql.lang.ir.GraphPattern;
1820
import oracle.pgql.lang.ir.GraphQuery;
21+
import oracle.pgql.lang.ir.PathFindingGoal;
1922
import oracle.pgql.lang.ir.QueryExpression.AllProperties;
2023
import oracle.pgql.lang.ir.QueryExpression.FunctionCall;
2124
import oracle.pgql.lang.ir.QueryExpression.PropertyAccess;
25+
import oracle.pgql.lang.ir.QueryPath;
2226
import oracle.pgql.lang.ir.QueryExpression.Aggregation.AggrJsonArrayagg;
27+
import oracle.pgql.lang.ir.unnest.RowsPerMatchType;
2328
import oracle.pgql.lang.ir.SelectQuery;
29+
import oracle.pgql.lang.ir.VertexPairConnection;
2430

2531
public class BugFixTest extends AbstractPgqlTest {
2632

@@ -474,8 +480,102 @@ public void formatJson() throws Exception {
474480
assertTrue(agg.isFormatJson());
475481

476482
result = pgql.parse("SELECT JSON_ARRAYAGG(v.prop) FROM MATCH (v)");
477-
agg = (AggrJsonArrayagg) ((SelectQuery) result.getGraphQuery()).getProjection().getElements()
478-
.get(0).getExp();
483+
agg = (AggrJsonArrayagg) ((SelectQuery) result.getGraphQuery()).getProjection().getElements().get(0).getExp();
479484
assertFalse(agg.isFormatJson());
480485
}
486+
487+
@Test
488+
public void ambiguityForOptionalMatch() throws Exception {
489+
/*
490+
* Previously, the below query was a valid PGQL 1.1/1.2 query in which "OPTIONAL" is the name of the graph. This
491+
* caused an ambiguity because the query is now also seen as a PGQL 2.0 query with OPTIONAL MATCH pattern. The fix
492+
* was to disallow "OPTIONAL" as graph name in PGQL 1.1/1.2.
493+
*/
494+
PgqlResult result = pgql
495+
.parse("SELECT n FROM OPTIONAL MATCH (n IS Person) -[e IS likes]-> (m IS Person) WHERE n.name = 'Dave'");
496+
assertTrue(result.isQueryValid());
497+
498+
/*
499+
* You can still use OPTIONAL as graph name in PGQL 1.2, but it requires double quoting. Also, you can still use
500+
* OPTIONAL as property name or variable name.
501+
*/
502+
result = pgql.parse(
503+
"SELECT n FROM \"OPTIONAL\" MATCH (n IS Person) -[e IS likes]-> (optional IS Person) WHERE optional.optional = true");
504+
assertTrue(result.isQueryValid());
505+
506+
/*
507+
* And you can also use OPTIONAL as graph name in newer PGQL versions, without requiring double quoting.
508+
*/
509+
result = pgql
510+
.parse("SELECT n FROM MATCH (n IS Person) -[e IS likes]-> (m IS Person) ON optional WHERE n.name = 'Dave'");
511+
assertTrue(result.isQueryValid());
512+
}
513+
514+
@Test
515+
public void explicitOneRowPerMatch() throws Exception {
516+
PgqlResult result = pgql.parse(
517+
"SELECT sum FROM LATERAL (SELECT sum(v2.integerprop) as sum FROM MATCH ANY SHORTEST (v) ->* (v2) ONE ROW PER MATCH)");
518+
assertTrue(result.isQueryValid());
519+
}
520+
521+
@Test
522+
public void parsePathSelectorInParenthesizedMatch() throws Exception {
523+
String query = "SELECT SUM(e.amount) as sum " //
524+
+ "FROM MATCH ( ANY CHEAPEST (y)(-[e:transaction]-> COST e.amount)* (x)) ";
525+
PgqlResult result = pgql.parse(query);
526+
assertTrue(result.isQueryValid());
527+
Iterator<VertexPairConnection> it = result.getGraphQuery().getGraphPattern().getConnections().iterator();
528+
QueryPath path = (QueryPath) it.next();
529+
assertEquals(PathFindingGoal.CHEAPEST, path.getPathFindingGoal());
530+
531+
query = "SELECT SUM(e.amount) as sum " //
532+
+ "FROM MATCH ( ANY CHEAPEST (y)(-[e:transaction]-> COST e.amount)* (x), " //
533+
+ "ANY CHEAPEST (y)(-[e2:transaction]-> COST e.amount)* (x) WHERE sum < 2000) ";
534+
result = pgql.parse(query);
535+
assertTrue(result.isQueryValid());
536+
it = result.getGraphQuery().getGraphPattern().getConnections().iterator();
537+
path = (QueryPath) it.next();
538+
assertEquals(PathFindingGoal.CHEAPEST, path.getPathFindingGoal());
539+
path = (QueryPath) it.next();
540+
assertEquals(PathFindingGoal.CHEAPEST, path.getPathFindingGoal());
541+
}
542+
543+
@Test
544+
public void oneRowPerStepAfterOptionalMatch() throws Exception {
545+
String query = "SELECT w.number,id(v) FROM MATCH ANY (v)->{1,2}(v2) ONE ROW PER STEP (w,y,z), OPTIONAL MATCH(w)->(z)";
546+
PgqlResult result1 = pgql.parse(query);
547+
assertTrue(result1.isQueryValid());
548+
PgqlResult result2 = pgql.parse(result1.getGraphQuery().toString());
549+
assertTrue(result2.isQueryValid());
550+
GraphPattern graphPattern = (GraphPattern) result2.getGraphQuery().getTableExpressions().get(0);
551+
QueryPath path = (QueryPath) graphPattern.getConnections().iterator().next();
552+
assertEquals(RowsPerMatchType.ONE_ROW_PER_STEP, path.getRowsPerMatch().getRowsPerMatchType());
553+
554+
query = "SELECT id(z) " + //
555+
"FROM MATCH ANY SHORTEST (v) ->{,1} (v2) ONE ROW PER STEP (x, y, z)" + //
556+
"WHERE all_different(x, z) " + //
557+
"ORDER BY id(z)";
558+
result1 = pgql.parse(query);
559+
assertTrue(result1.isQueryValid());
560+
result2 = pgql.parse(result1.getGraphQuery().toString());
561+
assertTrue(result2.isQueryValid());
562+
graphPattern = (GraphPattern) result2.getGraphQuery().getTableExpressions().get(0);
563+
path = (QueryPath) graphPattern.getConnections().iterator().next();
564+
assertEquals(RowsPerMatchType.ONE_ROW_PER_STEP, path.getRowsPerMatch().getRowsPerMatchType());
565+
566+
query = "SELECT ELEMENT_NUMBER(v1), v1.number AS v1_number, " //
567+
+ "LISTAGG(e.amount, ', ') AS path2 " //
568+
+ "FROM MATCH ALL ACYCLIC PATHS (n IS Account) -[IS transaction]->* (m IS Account) " //
569+
+ " ONE ROW PER VERTEX ( v1 ) " //
570+
+ " , MATCH SHORTEST 10 TRAIL (m) -[e IS transaction]->+ (m) " //
571+
+ "WHERE n.number = 10039 AND m.number = 2090 " //
572+
+ "ORDER BY MATCH_NUMBER(v1), path2, ELEMENT_NUMBER(v1), v1_number";
573+
result1 = pgql.parse(query);
574+
assertTrue(result1.isQueryValid());
575+
result2 = pgql.parse(result1.getGraphQuery().toString());
576+
assertTrue(result2.isQueryValid());
577+
graphPattern = (GraphPattern) result2.getGraphQuery().getTableExpressions().get(0);
578+
path = (QueryPath) graphPattern.getConnections().iterator().next();
579+
assertEquals(RowsPerMatchType.ONE_ROW_PER_VERTEX, path.getRowsPerMatch().getRowsPerMatchType());
580+
}
481581
}

pgql-lang/src/test/java/oracle/pgql/lang/PrettyPrintingTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,4 +801,37 @@ public void testMatchWithWhereFollowedByLateral() throws Exception {
801801
"WHERE n.number = 1001 AND p.number > 8020";
802802
checkRoundTrip(query);
803803
}
804+
805+
@Test
806+
public void testOptionalMatch() throws Exception {
807+
String query = "SELECT n.name AS n_name, m.name AS m_name " + //
808+
"FROM MATCH (n IS Person), " + //
809+
" OPTIONAL MATCH (n) -[e IS likes]-> (m IS Person) " + //
810+
"WHERE n.name = 'Dave'";
811+
checkRoundTrip(query);
812+
813+
query = "SELECT n.name AS n_name, m.name AS m_name " + //
814+
"FROM MATCH (n IS Person), " + //
815+
" OPTIONAL MATCH ( (n) -[e IS likes]-> (m IS Person) WHERE n.dob > m.dob ) " + //
816+
"WHERE n.name = 'Dave'";
817+
checkRoundTrip(query);
818+
819+
query = "SELECT n.name AS n_name, m.name AS m_name " + //
820+
"FROM MATCH (n IS Person), " + //
821+
" OPTIONAL MATCH ( (n) -[e1 IS likes]-> (m IS Person), " + //
822+
" (n) <-[e2 IS likes]- (m) " + //
823+
" WHERE n.dob > m.dob ) " + //
824+
"WHERE n.name = 'Dave'";
825+
checkRoundTrip(query);
826+
827+
query = "SELECT id(y), x, id(z) " + //
828+
"FROM OPTIONAL MATCH ((y) -> (z) WHERE y.number = 1001), " + //
829+
" OPTIONAL MATCH ((y)->(x)<-(z))";
830+
checkRoundTrip(query);
831+
832+
query = "SELECT SUM(e.amount) as sum, id(x), id(z) " + //
833+
"FROM MATCH ANY CHEAPEST ((y)(-[e:transaction]-> COST e.amount)* (x) WHERE sum < 2000) " + //
834+
" , OPTIONAL MATCH ((x)->(z) WHERE id(z)='Person(1)')";
835+
checkRoundTrip(query);
836+
}
804837
}

0 commit comments

Comments
 (0)