Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(interactive): Support Regex Match for String Values #3286

Merged
merged 10 commits into from
Oct 25, 2023
4 changes: 3 additions & 1 deletion docs/interactive_engine/neo4j/supported_cypher.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ Note that some Aggregator operators, such as `max()`, we listed here are impleme
| Labels | Get label name of a vertex type | labels() | labels() | <input type="checkbox" disabled checked /> | |
| Type | Get label name of an edge type | type() | type() | <input type="checkbox" disabled checked /> | |
| Extract | Get interval value from a temporal type | \<temporal\>.\<interval\> | \<temporal\>.\<interval\> | <input type="checkbox" disabled checked /> | |

| Starts With | Perform case-sensitive matching on the beginning of a string | STARTS WITH | STARTS WITH | <input type="checkbox" disabled checked /> | |
| Ends With | Perform case-sensitive matching on the ending of a string | ENDS WITH | ENDS WITH | <input type="checkbox" disabled checked /> | |
| Contains | Perform case-sensitive matching regardless of location within a string | CONTAINS | CONTAINS | <input type="checkbox" disabled checked /> | |


## Clause
Expand Down
1 change: 1 addition & 0 deletions interactive_engine/compiler/ir_exprimental_ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ sleep 5s
cd ${base_dir} && make run graph.schema:=../executor/ir/core/resource/movie_schema.json &
sleep 10s
# run cypher movie tests
export ENGINE_TYPE=pegasus
cd ${base_dir} && make cypher_test
exit_code=$?
# clean service
Expand Down
12 changes: 10 additions & 2 deletions interactive_engine/compiler/src/main/antlr4/CypherGS.g4
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,17 @@ oC_PartialComparisonExpression
| ( '<=' SP? oC_StringListNullPredicateExpression )
| ( '>=' SP? oC_StringListNullPredicateExpression )
;

oC_StringListNullPredicateExpression
: oC_AddOrSubtractExpression ( oC_NullPredicateExpression )? ;
: oC_AddOrSubtractExpression ( oC_StringPredicateExpression | oC_NullPredicateExpression )* ;

oC_StringPredicateExpression
: ( ( SP STARTS SP WITH ) | ( SP ENDS SP WITH ) | ( SP CONTAINS ) ) SP? oC_AddOrSubtractExpression ;

STARTS : ( 'S' | 's' ) ( 'T' | 't' ) ( 'A' | 'a' ) ( 'R' | 'r' ) ( 'T' | 't' ) ( 'S' | 's' ) ;

ENDS : ( 'E' | 'e' ) ( 'N' | 'n' ) ( 'D' | 'd' ) ( 'S' | 's' ) ;

CONTAINS : ( 'C' | 'c' ) ( 'O' | 'o' ) ( 'N' | 'n' ) ( 'T' | 't' ) ( 'A' | 'a' ) ( 'I' | 'i' ) ( 'N' | 'n' ) ( 'S' | 's' ) ;

oC_NullPredicateExpression
: ( SP IS SP NULL )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,75 +47,75 @@
public abstract class Utils {
private static final Logger logger = LoggerFactory.getLogger(Utils.class);

public static final Common.Value protoValue(RexLiteral literal) {
if (literal.getTypeName() == SqlTypeName.SARG) {
Sarg sarg = literal.getValueAs(Sarg.class);
if (!sarg.isPoints()) {
throw new UnsupportedOperationException(
"can not convert continuous ranges to ir core array, sarg=" + sarg);
}
List<Comparable> values =
com.alibaba.graphscope.common.ir.tools.Utils.getValuesAsList(sarg);
switch (literal.getType().getSqlTypeName()) {
case INTEGER:
Common.I32Array.Builder i32Array = Common.I32Array.newBuilder();
values.forEach(value -> i32Array.addItem(((Number) value).intValue()));
return Common.Value.newBuilder().setI32Array(i32Array).build();
case BIGINT:
Common.I64Array.Builder i64Array = Common.I64Array.newBuilder();
values.forEach(value -> i64Array.addItem(((Number) value).longValue()));
return Common.Value.newBuilder().setI64Array(i64Array).build();
case CHAR:
Common.StringArray.Builder stringArray = Common.StringArray.newBuilder();
values.forEach(
value ->
stringArray.addItem(
(value instanceof NlsString)
? ((NlsString) value).getValue()
: (String) value));
return Common.Value.newBuilder().setStrArray(stringArray).build();
case DECIMAL:
case FLOAT:
case DOUBLE:
Common.DoubleArray.Builder doubleArray = Common.DoubleArray.newBuilder();
values.forEach(value -> doubleArray.addItem(((Number) value).doubleValue()));
return Common.Value.newBuilder().setF64Array(doubleArray).build();
default:
throw new UnsupportedOperationException(
"can not convert sarg=" + sarg + " ir core array");
}
}
switch (literal.getType().getSqlTypeName()) {
case NULL:
return Common.Value.newBuilder().setNone(Common.None.newBuilder().build()).build();
case BOOLEAN:
return Common.Value.newBuilder().setBoolean((Boolean) literal.getValue()).build();
case INTEGER:
return Common.Value.newBuilder()
.setI32(((Number) literal.getValue()).intValue())
.build();
case BIGINT:
return Common.Value.newBuilder()
.setI64(((Number) literal.getValue()).longValue())
.build();
case CHAR:
String valueStr =
(literal.getValue() instanceof NlsString)
? ((NlsString) literal.getValue()).getValue()
: (String) literal.getValue();
return Common.Value.newBuilder().setStr(valueStr).build();
case DECIMAL:
case FLOAT:
case DOUBLE:
return Common.Value.newBuilder()
.setF64(((Number) literal.getValue()).doubleValue())
.build();
default:
throw new UnsupportedOperationException(
"literal type " + literal.getTypeName() + " is unsupported yet");
}
}

Check notice on line 118 in interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/runtime/proto/Utils.java

View check run for this annotation

codefactor.io / CodeFactor

interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/runtime/proto/Utils.java#L50-L118

Complex Method
public static final OuterExpression.Property protoProperty(GraphProperty property) {
switch (property.getOpt()) {
case ID:
Expand Down Expand Up @@ -152,137 +152,141 @@
}
}

public static final OuterExpression.ExprOpr protoOperator(SqlOperator operator) {
switch (operator.getKind()) {
case PLUS:
return OuterExpression.ExprOpr.newBuilder()
.setArith(OuterExpression.Arithmetic.ADD)
.build();
case MINUS:
return OuterExpression.ExprOpr.newBuilder()
.setArith(OuterExpression.Arithmetic.SUB)
.build();
case TIMES:
return OuterExpression.ExprOpr.newBuilder()
.setArith(OuterExpression.Arithmetic.MUL)
.build();
case DIVIDE:
return OuterExpression.ExprOpr.newBuilder()
.setArith(OuterExpression.Arithmetic.DIV)
.build();
case MOD:
return OuterExpression.ExprOpr.newBuilder()
.setArith(OuterExpression.Arithmetic.MOD)
.build();
case OTHER_FUNCTION:
if (operator.getName().equals("POWER")) {
return OuterExpression.ExprOpr.newBuilder()
.setArith(OuterExpression.Arithmetic.EXP)
.build();
}
case EQUALS:
return OuterExpression.ExprOpr.newBuilder()
.setLogical(OuterExpression.Logical.EQ)
.build();
case NOT_EQUALS:
return OuterExpression.ExprOpr.newBuilder()
.setLogical(OuterExpression.Logical.NE)
.build();
case GREATER_THAN:
return OuterExpression.ExprOpr.newBuilder()
.setLogical(OuterExpression.Logical.GT)
.build();
case GREATER_THAN_OR_EQUAL:
return OuterExpression.ExprOpr.newBuilder()
.setLogical(OuterExpression.Logical.GE)
.build();
case LESS_THAN:
return OuterExpression.ExprOpr.newBuilder()
.setLogical(OuterExpression.Logical.LT)
.build();
case LESS_THAN_OR_EQUAL:
return OuterExpression.ExprOpr.newBuilder()
.setLogical(OuterExpression.Logical.LE)
.build();
case AND:
return OuterExpression.ExprOpr.newBuilder()
.setLogical(OuterExpression.Logical.AND)
.build();
case OR:
return OuterExpression.ExprOpr.newBuilder()
.setLogical(OuterExpression.Logical.OR)
.build();
case NOT:
return OuterExpression.ExprOpr.newBuilder()
.setLogical(OuterExpression.Logical.NOT)
.build();
case IS_NULL:
return OuterExpression.ExprOpr.newBuilder()
.setLogical(OuterExpression.Logical.ISNULL)
.build();
case SEARCH:
return OuterExpression.ExprOpr.newBuilder()
.setLogical(OuterExpression.Logical.WITHIN)
.build();
case POSIX_REGEX_CASE_SENSITIVE:
return OuterExpression.ExprOpr.newBuilder()
.setLogical(OuterExpression.Logical.REGEX)
.build();
default:
throw new UnsupportedOperationException(
"operator type="
+ operator.getKind()
+ ", name="
+ operator.getName()
+ " is unsupported yet");
}
}

Check notice on line 240 in interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/runtime/proto/Utils.java

View check run for this annotation

codefactor.io / CodeFactor

interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/runtime/proto/Utils.java#L155-L240

Complex Method
public static final Common.DataType protoBasicDataType(RelDataType basicType) {
if (basicType instanceof GraphLabelType) return Common.DataType.INT32;
switch (basicType.getSqlTypeName()) {
case NULL:
return Common.DataType.NONE;
case BOOLEAN:
return Common.DataType.BOOLEAN;
case INTEGER:
return Common.DataType.INT32;
case BIGINT:
return Common.DataType.INT64;
case CHAR:
return Common.DataType.STRING;
case DECIMAL:
case FLOAT:
case DOUBLE:
return Common.DataType.DOUBLE;
case MULTISET:
case ARRAY:
RelDataType elementType = basicType.getComponentType();
switch (elementType.getSqlTypeName()) {
case INTEGER:
return Common.DataType.INT32_ARRAY;
case BIGINT:
return Common.DataType.INT64_ARRAY;
case CHAR:
return Common.DataType.STRING_ARRAY;
case DECIMAL:
case FLOAT:
case DOUBLE:
return Common.DataType.DOUBLE_ARRAY;
default:
throw new UnsupportedOperationException(
"array of element type "
+ elementType.getSqlTypeName()
+ " is unsupported yet");
}
case DATE:
return Common.DataType.DATE32;
case TIME:
return Common.DataType.TIME32;
case TIMESTAMP:
return Common.DataType.TIMESTAMP;
default:
throw new UnsupportedOperationException(
"basic type " + basicType.getSqlTypeName() + " is unsupported yet");
}
}

Check notice on line 289 in interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/runtime/proto/Utils.java

View check run for this annotation

codefactor.io / CodeFactor

interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/runtime/proto/Utils.java#L241-L289

Complex Method
public static final DataType.IrDataType protoIrDataType(
RelDataType dataType, boolean isColumnId) {
switch (dataType.getSqlTypeName()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,25 +642,26 @@
}
}

private boolean isCurrentSupported(SqlOperator operator) {
SqlKind sqlKind = operator.getKind();
return sqlKind.belongsTo(SqlKind.BINARY_ARITHMETIC)
|| sqlKind.belongsTo(SqlKind.COMPARISON)
|| sqlKind == SqlKind.AND
|| sqlKind == SqlKind.OR
|| sqlKind == SqlKind.DESCENDING
|| (sqlKind == SqlKind.OTHER_FUNCTION && operator.getName().equals("POWER"))
|| (sqlKind == SqlKind.MINUS_PREFIX)
|| (sqlKind == SqlKind.CASE)
|| (sqlKind == SqlKind.PROCEDURE_CALL)
|| (sqlKind == SqlKind.NOT)
|| sqlKind == SqlKind.ARRAY_VALUE_CONSTRUCTOR
|| sqlKind == SqlKind.IS_NULL
|| sqlKind == SqlKind.IS_NOT_NULL
|| sqlKind == SqlKind.EXTRACT
|| sqlKind == SqlKind.SEARCH;
|| sqlKind == SqlKind.SEARCH
|| sqlKind == SqlKind.POSIX_REGEX_CASE_SENSITIVE;
}

Check notice on line 664 in interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/tools/GraphBuilder.java

View check run for this annotation

codefactor.io / CodeFactor

interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/tools/GraphBuilder.java#L645-L664

Complex Method
@Override
public GraphBuilder filter(RexNode... conditions) {
return filter(ImmutableList.copyOf(conditions));
Expand Down Expand Up @@ -707,57 +708,57 @@
return builder;
}

private AbstractBindableTableScan fuseFilters(
AbstractBindableTableScan tableScan, RexNode condition) {
List<Comparable> labelValues = Lists.newArrayList();
List<RexNode> uniqueKeyFilters = Lists.newArrayList();
List<RexNode> extraFilters = Lists.newArrayList();
classifyFilters(tableScan, condition, labelValues, uniqueKeyFilters, extraFilters);
if (!labelValues.isEmpty()) {
GraphLabelType labelType =
((GraphSchemaType) tableScan.getRowType().getFieldList().get(0).getType())
.getLabelType();
List<String> labelsToKeep =
labelType.getLabelsEntry().stream()
.filter(k -> labelValues.contains(k.getLabel()))
.map(k -> k.getLabel())
.collect(Collectors.toList());
Preconditions.checkArgument(
!labelsToKeep.isEmpty(),
"cannot find common labels between values= " + labelValues + " and label=",
labelType);
if (labelsToKeep.size() < labelType.getLabelsEntry().size()) {
GraphBuilder builder =
(GraphBuilder)
GraphPlanner.relBuilderFactory.create(
getCluster(), getRelOptSchema());
LabelConfig newLabelConfig = new LabelConfig(false);
labelsToKeep.forEach(k -> newLabelConfig.addLabel(k));
if (tableScan instanceof GraphLogicalSource) {
builder.source(
new SourceConfig(
((GraphLogicalSource) tableScan).getOpt(),
newLabelConfig,
tableScan.getAliasName()));
} else if (tableScan instanceof GraphLogicalExpand) {
((GraphBuilder) builder.push(tableScan.getInput(0)))
.expand(
new ExpandConfig(
((GraphLogicalExpand) tableScan).getOpt(),
newLabelConfig,
tableScan.getAliasName()));
} else if (tableScan instanceof GraphLogicalGetV) {
((GraphBuilder) builder.push(tableScan.getInput(0)))
.getV(
new GetVConfig(
((GraphLogicalGetV) tableScan).getOpt(),
newLabelConfig,
tableScan.getAliasName()));
}
if (builder.size() > 0) {
// check if the property still exist after updating the label type
RexVisitor propertyChecker =
new RexVisitorImpl<Void>(true) {

Check notice on line 761 in interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/tools/GraphBuilder.java

View check run for this annotation

codefactor.io / CodeFactor

interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/tools/GraphBuilder.java#L711-L761

Complex Method
@Override
public Void visitInputRef(RexInputRef inputRef) {
if (inputRef instanceof RexGraphVariable) {
Expand Down Expand Up @@ -1197,102 +1198,102 @@
* @param nodes build limit() if empty
* @return
*/
@Override
public GraphBuilder sortLimit(
@Nullable RexNode offsetNode,
@Nullable RexNode fetchNode,
Iterable<? extends RexNode> nodes) {
if (offsetNode != null && !(offsetNode instanceof RexLiteral)) {
throw new IllegalArgumentException("OFFSET node must be RexLiteral");
}
if (offsetNode != null && !(offsetNode instanceof RexLiteral)) {
throw new IllegalArgumentException("FETCH node must be RexLiteral");
}

RelNode input = requireNonNull(peek(), "frame stack is empty");

List<RelDataTypeField> originalFields = input.getRowType().getFieldList();

Registrar registrar = new Registrar(this, input, true);
List<RexNode> registerNodes = registrar.registerExpressions(ImmutableList.copyOf(nodes));

// expressions need to be projected in advance
if (!registrar.getExtraNodes().isEmpty()) {
project(registrar.getExtraNodes(), registrar.getExtraAliases(), registrar.isAppend());
RexTmpVariableConverter converter = new RexTmpVariableConverter(true, this);
registerNodes =
registerNodes.stream()
.map(k -> k.accept(converter))
.collect(Collectors.toList());
input = requireNonNull(peek(), "frame stack is empty");
}

List<RelFieldCollation> fieldCollations = fieldCollations(registerNodes);
Config config = Utils.getFieldValue(RelBuilder.class, this, "config");

// limit 0 -> return empty value
if ((fetchNode != null && RexLiteral.intValue(fetchNode) == 0) && config.simplifyLimit()) {
return (GraphBuilder) empty();
}

// output all results without any order -> skip
if (offsetNode == null && fetchNode == null && fieldCollations.isEmpty()) {
return this; // sort is trivial
}
// sortLimit is actually limit if collations are empty
if (fieldCollations.isEmpty()) {
// fuse limit with the previous sort operator
// order + limit -> topK
if (input instanceof Sort) {
Sort sort2 = (Sort) input;
// output all results without any limitations
if (sort2.offset == null && sort2.fetch == null) {
RelNode sort =
GraphLogicalSort.create(
sort2.getInput(), sort2.collation, offsetNode, fetchNode);
replaceTop(sort);
return this;
}
}
// order + project + limit -> topK + project
if (input instanceof Project) {
Project project = (Project) input;
if (project.getInput() instanceof Sort) {
Sort sort2 = (Sort) project.getInput();
if (sort2.offset == null && sort2.fetch == null) {
RelNode sort =
GraphLogicalSort.create(
sort2.getInput(), sort2.collation, offsetNode, fetchNode);
replaceTop(
GraphLogicalProject.create(
(GraphOptCluster) project.getCluster(),
project.getHints(),
sort,
project.getProjects(),
project.getRowType(),
((GraphLogicalProject) project).isAppend()));
return this;
}
}
}
}
RelNode sort =
GraphLogicalSort.create(
input, GraphRelCollations.of(fieldCollations), offsetNode, fetchNode);
replaceTop(sort);
// to remove the extra columns we have added
if (!registrar.getExtraAliases().isEmpty()) {
List<RexNode> originalExprs = new ArrayList<>();
List<String> originalAliases = new ArrayList<>();
for (RelDataTypeField field : originalFields) {
originalExprs.add(variable(field.getName()));
originalAliases.add(field.getName());
}
project(originalExprs, originalAliases, false);
}
return this;
}

Check notice on line 1296 in interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/tools/GraphBuilder.java

View check run for this annotation

codefactor.io / CodeFactor

interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/tools/GraphBuilder.java#L1201-L1296

Complex Method
@Override
public RelBuilder join(
JoinRelType joinType, RexNode condition, Set<CorrelationId> variablesSet) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.alibaba.graphscope.common.ir.rex.operator.CaseOperator;

import org.apache.calcite.sql.*;
import org.apache.calcite.sql.fun.ExtSqlPosixRegexOperator;
import org.apache.calcite.sql.fun.SqlMonotonicBinaryOperator;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.type.*;
Expand Down Expand Up @@ -226,4 +227,8 @@ public static final SqlFunction USER_DEFINED_PROCEDURE(StoredProcedureMeta meta)
null,
GraphOperandTypes.INTERVALINTERVAL_INTERVALDATETIME,
SqlFunctionCategory.SYSTEM);

public static final SqlOperator POSIX_REGEX_CASE_SENSITIVE =
new ExtSqlPosixRegexOperator(
"POSIX REGEX CASE SENSITIVE", SqlKind.POSIX_REGEX_CASE_SENSITIVE, true, false);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.apache.calcite.avatica.util.TimeUnit;
import org.apache.calcite.rel.RelNode;
Expand All @@ -44,7 +45,9 @@
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.type.SqlTypeFamily;
import org.apache.calcite.tools.RelBuilder;
import org.apache.calcite.util.NlsString;
import org.apache.commons.lang3.ObjectUtils;
import org.checkerframework.checker.nullness.qual.Nullable;

Expand Down Expand Up @@ -128,18 +131,72 @@ public ExprVisitorResult visitOC_StringListNullPredicateExpression(
CypherGSParser.OC_StringListNullPredicateExpressionContext ctx) {
ExprVisitorResult operand =
visitOC_AddOrSubtractExpression(ctx.oC_AddOrSubtractExpression());
List<SqlOperator> operators = Lists.newArrayList();
CypherGSParser.OC_NullPredicateExpressionContext nullCtx = ctx.oC_NullPredicateExpression();
if (nullCtx != null) {
if (nullCtx.IS() != null && nullCtx.NOT() != null && nullCtx.NULL() != null) {
operators.add(GraphStdOperatorTable.IS_NOT_NULL);
} else if (nullCtx.IS() != null && nullCtx.NULL() != null) {
operators.add(GraphStdOperatorTable.IS_NULL);
Iterator i$ = ctx.children.iterator();
while (i$.hasNext()) {
ParseTree o = (ParseTree) i$.next();
if (o == null) continue;
if (CypherGSParser.OC_NullPredicateExpressionContext.class.isInstance(o)) {
operand =
visitOC_NullPredicateExpression(
operand, (CypherGSParser.OC_NullPredicateExpressionContext) o);
} else if (CypherGSParser.OC_StringPredicateExpressionContext.class.isInstance(o)) {
operand =
visitOC_StringPredicateExpression(
operand, (CypherGSParser.OC_StringPredicateExpressionContext) o);
}
}
return operand;
}

private ExprVisitorResult visitOC_NullPredicateExpression(
ExprVisitorResult operand, CypherGSParser.OC_NullPredicateExpressionContext nullCtx) {
List<SqlOperator> operators = Lists.newArrayList();
if (nullCtx.IS() != null && nullCtx.NOT() != null && nullCtx.NULL() != null) {
operators.add(GraphStdOperatorTable.IS_NOT_NULL);
} else if (nullCtx.IS() != null && nullCtx.NULL() != null) {
operators.add(GraphStdOperatorTable.IS_NULL);
} else {
throw new IllegalArgumentException(
"unknown null predicate expression: " + nullCtx.getText());
}
return unaryCall(operators, operand);
}

private ExprVisitorResult visitOC_StringPredicateExpression(
ExprVisitorResult operand,
CypherGSParser.OC_StringPredicateExpressionContext stringCtx) {
ExprVisitorResult rightRes =
visitOC_AddOrSubtractExpression(stringCtx.oC_AddOrSubtractExpression());
RexNode rightExpr = rightRes.getExpr();
// the right operand should be a string literal
Preconditions.checkArgument(
rightExpr.getKind() == SqlKind.LITERAL
&& rightExpr.getType().getFamily() == SqlTypeFamily.CHARACTER,
"the right operand of string predicate expression should be a string literal");
String value = ((RexLiteral) rightExpr).getValueAs(NlsString.class).getValue();
StringBuilder regexPattern = new StringBuilder();
if (stringCtx.STARTS() != null) {
regexPattern.append(value);
regexPattern.append(".*");
} else if (stringCtx.ENDS() != null) {
regexPattern.append(".*");
regexPattern.append(value);
} else if (stringCtx.CONTAINS() != null) {
regexPattern.append(".*");
regexPattern.append(value);
regexPattern.append(".*");
} else {
throw new IllegalArgumentException(
"unknown string predicate expression: " + stringCtx.getText());
}
return binaryCall(
GraphStdOperatorTable.POSIX_REGEX_CASE_SENSITIVE,
ImmutableList.of(
operand,
new ExprVisitorResult(
rightRes.getAggCalls(), builder.literal(regexPattern.toString()))));
}

@Override
public ExprVisitorResult visitOC_AddOrSubtractExpression(
CypherGSParser.OC_AddOrSubtractExpressionContext ctx) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,4 +301,22 @@ public static QueryContext get_movie_query15_test() {
+ " \"Tom Cruise\"}>");
return new QueryContext(query, expected);
}

public static QueryContext get_movie_query16_test() {
String query = "Match (n:Movie {id: 0}) Where n.title starts with 'The' Return n.title;";
List<String> expected = Arrays.asList("Record<{title: \"The Matrix\"}>");
return new QueryContext(query, expected);
}

public static QueryContext get_movie_query17_test() {
String query = "Match (n:Movie {id: 0}) Where n.title ends with 'Matrix' Return n.title;";
List<String> expected = Arrays.asList("Record<{title: \"The Matrix\"}>");
return new QueryContext(query, expected);
}

public static QueryContext get_movie_query18_test() {
String query = "Match (n:Movie {id: 0}) Where n.title contains 'The' Return n.title;";
List<String> expected = Arrays.asList("Record<{title: \"The Matrix\"}>");
return new QueryContext(query, expected);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2020 Alibaba Group Holding Limited.
*
* Licensed 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.calcite.sql.fun;

import com.alibaba.graphscope.common.ir.rex.RexCallBinding;

import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.sql.SqlCallBinding;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.util.Static;
import org.apache.calcite.util.Util;

/**
* The operator is used for regex match for string values, i.e a.name like '%marko' in a sql expression.
* The original implementation will check operand types by {@link org.apache.calcite.sql.SqlCall}, which is a structure in sql parser phase.
* Here we override the interface to check types by {@link org.apache.calcite.rex.RexCall} which represents an algebra relation.
*/
public class ExtSqlPosixRegexOperator extends SqlPosixRegexOperator {
public ExtSqlPosixRegexOperator(
String name, SqlKind kind, boolean caseSensitive, boolean negated) {
super(name, kind, caseSensitive, negated);
}

@Override
public boolean checkOperandTypes(SqlCallBinding callBinding, boolean throwOnFailure) {
int operandCount = callBinding.getOperandCount();
if (operandCount != 2) {
throw new AssertionError(
"Unexpected number of args to " + callBinding.getCall() + ": " + operandCount);
} else {
RelDataType op1Type = callBinding.getOperandType(0);
RelDataType op2Type = callBinding.getOperandType(1);
if (!SqlTypeUtil.isComparable(op1Type, op2Type)) {
throw new AssertionError(
"Incompatible first two operand types " + op1Type + " and " + op2Type);
} else {
if (!SqlTypeUtil.isCharTypeComparable(callBinding.collectOperandTypes())) {
if (throwOnFailure) {
String msg =
String.join(
", ",
Util.transform(
((RexCallBinding) callBinding).getRexOperands(),
String::valueOf));
throw callBinding.newError(Static.RESOURCE.operandNotComparable(msg));
} else {
return false;
}
} else {
return true;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,19 @@ public void dynamic_param_type_test() {
Assert.assertEquals(SqlTypeName.INTEGER, plus.getType().getSqlTypeName());
}

@Test
public void posix_regex_test() {
RexNode regex =
builder.source(mockSourceConfig(null))
.call(
GraphStdOperatorTable.POSIX_REGEX_CASE_SENSITIVE,
builder.variable(null, "name"),
builder.literal("^marko"));
Assert.assertEquals(SqlTypeName.BOOLEAN, regex.getType().getSqlTypeName());
Assert.assertEquals(
"POSIX REGEX CASE SENSITIVE(DEFAULT.name, _UTF-8'^marko')", regex.toString());
}

private SourceConfig mockSourceConfig(String alias) {
return new SourceConfig(
GraphOpt.Source.VERTEX, new LabelConfig(false).addLabel("person"), alias);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,4 +311,40 @@ public void match_16_test() {
+ "], matchOpt=[INNER])",
node.explain().trim());
}

@Test
public void match_17_test() {
RelNode node =
Utils.eval("Match (a:person) Where a.name starts with 'marko' Return a").build();
Assert.assertEquals(
"GraphLogicalProject(a=[a], isAppend=[false])\n"
+ " GraphLogicalSource(tableConfig=[{isAll=false, tables=[person]}],"
+ " alias=[a], fusedFilter=[[POSIX REGEX CASE SENSITIVE(DEFAULT.name,"
+ " _UTF-8'marko.*')]], opt=[VERTEX])",
node.explain().trim());
}

@Test
public void match_18_test() {
RelNode node =
Utils.eval("Match (a:person) Where a.name ends with 'marko' Return a").build();
Assert.assertEquals(
"GraphLogicalProject(a=[a], isAppend=[false])\n"
+ " GraphLogicalSource(tableConfig=[{isAll=false, tables=[person]}],"
+ " alias=[a], fusedFilter=[[POSIX REGEX CASE SENSITIVE(DEFAULT.name,"
+ " _UTF-8'.*marko')]], opt=[VERTEX])",
node.explain().trim());
}

@Test
public void match_19_test() {
RelNode node =
Utils.eval("Match (a:person) Where a.name contains 'marko' Return a").build();
Assert.assertEquals(
"GraphLogicalProject(a=[a], isAppend=[false])\n"
+ " GraphLogicalSource(tableConfig=[{isAll=false, tables=[person]}],"
+ " alias=[a], fusedFilter=[[POSIX REGEX CASE SENSITIVE(DEFAULT.name,"
+ " _UTF-8'.*marko.*')]], opt=[VERTEX])",
node.explain().trim());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.alibaba.graphscope.cypher.integration.movie;

import static org.junit.Assume.assumeTrue;

import com.alibaba.graphscope.cypher.integration.suite.QueryContext;
import com.alibaba.graphscope.cypher.integration.suite.movie.MovieQueries;

Expand Down Expand Up @@ -122,6 +124,30 @@ public void run_movie_query15_test() {
Assert.assertEquals(testQuery.getExpectedResult().toString(), result.list().toString());
}

@Test
public void run_movie_query16_test() {
assumeTrue("pegasus".equals(System.getenv("ENGINE_TYPE")));
QueryContext testQuery = MovieQueries.get_movie_query16_test();
Result result = session.run(testQuery.getQuery());
Assert.assertEquals(testQuery.getExpectedResult().toString(), result.list().toString());
}

@Test
public void run_movie_query17_test() {
assumeTrue("pegasus".equals(System.getenv("ENGINE_TYPE")));
QueryContext testQuery = MovieQueries.get_movie_query17_test();
Result result = session.run(testQuery.getQuery());
Assert.assertEquals(testQuery.getExpectedResult().toString(), result.list().toString());
}

@Test
public void run_movie_query18_test() {
assumeTrue("pegasus".equals(System.getenv("ENGINE_TYPE")));
QueryContext testQuery = MovieQueries.get_movie_query18_test();
Result result = session.run(testQuery.getQuery());
Assert.assertEquals(testQuery.getExpectedResult().toString(), result.list().toString());
}

@AfterClass
public static void afterClass() {
if (session != null) {
Expand Down
3 changes: 2 additions & 1 deletion interactive_engine/executor/ir/common/src/expr_parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ impl ExprToken for pb::ExprOpr {
| pb::Logical::Without
| pb::Logical::Startswith
| pb::Logical::Endswith
| pb::Logical::Isnull => 80,
| pb::Logical::Isnull
| pb::Logical::Regex => 80,
pb::Logical::And => 75,
pb::Logical::Or => 70,
pb::Logical::Not => 110,
Expand Down
1 change: 1 addition & 0 deletions interactive_engine/executor/ir/graph_proxy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pegasus_common = { path = "../../engine/pegasus/common" }
ahash = "0.8"
rand = "0.8.5"
chrono = "0.4"
regex = "1.10"

[features]
default = []
Expand Down
Loading
Loading