Skip to content

Commit

Permalink
add support for Iterable Size and Contains to OQL Generator
Browse files Browse the repository at this point in the history
  • Loading branch information
jmoghisi authored and asereda-gs committed Feb 12, 2020
1 parent 5c3d15b commit cfb514b
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 18 deletions.
Expand Up @@ -18,13 +18,16 @@

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Primitives;

import org.immutables.criteria.backend.PathNaming;
import org.immutables.criteria.expression.AbstractExpressionVisitor;
import org.immutables.criteria.expression.Call;
import org.immutables.criteria.expression.ComparableOperators;
import org.immutables.criteria.expression.Constant;
import org.immutables.criteria.expression.Expression;
import org.immutables.criteria.expression.Expressions;
import org.immutables.criteria.expression.IterableOperators;
import org.immutables.criteria.expression.Operator;
import org.immutables.criteria.expression.Operators;
import org.immutables.criteria.expression.OptionalOperators;
Expand Down Expand Up @@ -67,16 +70,10 @@ public Oql visit(Call call) {
final Operator op = call.operator();
final List<Expression> args = call.arguments();

if (op == Operators.NOT_IN) {
if (op == Operators.NOT_IN || op == IterableOperators.NOT_EMPTY) {
// geode doesn't understand syntax foo not in [1, 2, 3]
// convert "foo not in [1, 2, 3]" into "not foo in [1, 2, 3]"
return visit(Expressions.not(Expressions.call(Operators.IN, call.arguments())));
}

if (op == Operators.EQUAL || op == Operators.NOT_EQUAL ||
op == Operators.IN || ComparableOperators.isComparable(op)) {
Preconditions.checkArgument(args.size() == 2, "Size should be 2 for %s but was %s", op, args.size());
return binaryOperator(call);
return visit(Expressions.not(Expressions.call(getInverseOp(op), call.arguments())));
}

if (op == Operators.AND || op == Operators.OR) {
Expand All @@ -86,13 +83,27 @@ public Oql visit(Call call) {
return new Oql(variables, newOql);
}

if (op == OptionalOperators.IS_PRESENT || op == OptionalOperators.IS_ABSENT || op == Operators.NOT) {
if (op.arity() == Operator.Arity.BINARY) {
return binaryOperator(call);
}

if (op.arity() == Operator.Arity.UNARY) {
return unaryOperator(call);
}

throw new UnsupportedOperationException("Don't know how to handle " + call);
}

private static Operator getInverseOp(Operator op) {
if (op == Operators.NOT_IN) {
return Operators.IN;
} else if (op == IterableOperators.NOT_EMPTY) {
return IterableOperators.IS_EMPTY;
} else {
throw new IllegalArgumentException("Don't know inverse operator of " + op);
}
}

/**
* Operator with single operator: {@code NOT}, {@code IS_PRESENT}
*/
Expand All @@ -103,13 +114,15 @@ private Oql unaryOperator(Call call) {
Preconditions.checkArgument(args.size() == 1,
"Size should be == 1 for unary operator %s but was %s", op, args.size());

if (op == OptionalOperators.IS_PRESENT || op == OptionalOperators.IS_ABSENT) {
Preconditions.checkArgument(args.size() == 1, "Size should be == 1 for %s but was %s", op, args.size());
if (op instanceof OptionalOperators) {
final Path path = Visitors.toPath(args.get(0));
final String isNull = op == OptionalOperators.IS_PRESENT ? "!= null" : "= null";
return oql(pathNaming.name(path) + " " + isNull);
} else if (op == Operators.NOT) {
return oql("NOT (" + args.get(0).accept(this).oql() + ")");
} else if (op == IterableOperators.IS_EMPTY) {
final Path path = Visitors.toPath(args.get(0));
return oql(pathNaming.name(path) + "." + toMethod(op));
}

throw new UnsupportedOperationException("Unknown unary operator " + call);
Expand All @@ -122,6 +135,19 @@ private Oql binaryOperator(Call call) {
final Operator op = call.operator();
final List<Expression> args = call.arguments();
Preconditions.checkArgument(args.size() == 2, "Size should be 2 for %s but was %s on call %s", op, args.size(), call);

final String namedPath = getNamedPath(call);

final Object variable = getVariable(call);

if (op == IterableOperators.CONTAINS) {
Preconditions.checkArgument(
useBindVariables || isStringOrPrimitive(variable),
"String or Primitive types only supported for contains operator but was " + variable.getClass()
);
return oql(String.format("%s.contains(%s)", namedPath, addAndGetBoundVariable(variable)));
}

final String operator;
if (op == Operators.EQUAL || op == Operators.NOT_EQUAL) {
operator = op == Operators.EQUAL ? "=" : "!=";
Expand All @@ -135,11 +161,35 @@ private Oql binaryOperator(Call call) {
operator = "<";
} else if (op == ComparableOperators.LESS_THAN_OR_EQUAL) {
operator = "<=";
} else if (op == IterableOperators.HAS_SIZE) {
operator = "=";
} else {
throw new IllegalArgumentException("Unknown binary operator " + call);
}

return oql(String.format("%s %s %s", namedPath, operator, addAndGetBoundVariable(variable)));
}

private static boolean isStringOrPrimitive(Object variable) {
return variable instanceof CharSequence || Primitives.isWrapperType(Primitives.wrap(variable.getClass()));
}

private String getNamedPath(Call call) {
final Operator op = call.operator();
final List<Expression> args = call.arguments();

final Path path = Visitors.toPath(args.get(0));
String namedPath = pathNaming.name(path);
if (op == IterableOperators.HAS_SIZE) {
namedPath += ".size";
}
return namedPath;
}

private static Object getVariable(Call call) {
final Operator op = call.operator();
final List<Expression> args = call.arguments();

final Constant constant = Visitors.toConstant(args.get(1));

final Object variable;
Expand All @@ -148,14 +198,19 @@ private Oql binaryOperator(Call call) {
} else {
variable = constant.value();
}
return variable;
}

private String addAndGetBoundVariable(Object variable) {
String variableAsString;
if (useBindVariables) {
variables.add(variable);
// bind variables in Geode start at index 1: $1, $2, $3 etc.
return oql(String.format("%s %s $%d", pathNaming.name(path), operator, variables.size()));
variableAsString = "$" + String.valueOf(variables.size());
} else {
variableAsString = toString(variable);
}

return oql(String.format("%s %s %s", pathNaming.name(path), operator, toString(variable)));
return variableAsString;
}

/**
Expand All @@ -165,6 +220,13 @@ private Oql oql(String oql) {
return new Oql(variables, oql);
}

private static String toMethod(Operator op) {
if (op == IterableOperators.IS_EMPTY) {
return "isEmpty";
}
throw new UnsupportedOperationException("Don't know how to handle Operator " + op);
}

private static String toString(Object value) {
if (value instanceof CharSequence) {
return "'" + Geodes.escapeOql((CharSequence) value) + "'";
Expand Down
Expand Up @@ -16,6 +16,9 @@

package org.immutables.criteria.geode;

import java.util.EnumSet;
import java.util.Set;

import org.apache.geode.cache.Cache;
import org.immutables.criteria.backend.Backend;
import org.immutables.criteria.backend.WithSessionCallback;
Expand All @@ -26,9 +29,6 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.util.EnumSet;
import java.util.Set;

@ExtendWith(GeodeExtension.class)
public class GeodePersonTest extends AbstractPersonTest {

Expand All @@ -41,7 +41,10 @@ public GeodePersonTest(Cache cache) {

@Override
protected Set<Feature> features() {
return EnumSet.of(Feature.DELETE, Feature.QUERY, Feature.QUERY_WITH_LIMIT, Feature.ORDER_BY, Feature.QUERY_WITH_PROJECTION);
return EnumSet.of(
Feature.DELETE, Feature.QUERY, Feature.QUERY_WITH_LIMIT, Feature.ORDER_BY, Feature.QUERY_WITH_PROJECTION,
Feature.ITERABLE_SIZE, Feature.ITERABLE_CONTAINS
);
}

@Override
Expand Down
Expand Up @@ -5,7 +5,9 @@
import static org.immutables.criteria.personmodel.PersonCriteria.person;

import org.immutables.criteria.backend.PathNaming;
import org.immutables.criteria.personmodel.ImmutablePet;
import org.immutables.criteria.personmodel.PersonCriteria;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class GeodeQueryVisitorTest {
Expand Down Expand Up @@ -73,6 +75,30 @@ void filterInWithBindParams() {
check(toOqlWithBindParams(person.age.notIn(18, 19, 20, 21))).is("NOT (age IN $1)");
}

@Test
void filterCollection() {
check(toOql(person.interests.isEmpty())).is("interests.isEmpty");
check(toOql(person.interests.notEmpty())).is("NOT (interests.isEmpty)");
check(toOql(person.interests.hasSize(1))).is("interests.size = 1");
check(toOql(person.interests.contains("OSS"))).is("interests.contains('OSS')");
}

@Test
void filterCollectionWithBindParams() {
check(toOqlWithBindParams(person.interests.isEmpty())).is("interests.isEmpty");
check(toOqlWithBindParams(person.interests.notEmpty())).is("NOT (interests.isEmpty)");
check(toOqlWithBindParams(person.interests.hasSize(1))).is("interests.size = $1");
check(toOqlWithBindParams(person.interests.contains("OSS"))).is("interests.contains($1)");
}

@Test
void filterCollectionDoesNotSupportComplexTypes() {
final ImmutablePet pet = ImmutablePet.builder().name("Rex").type(ImmutablePet.PetType.dog).build();
Assertions.assertThrows(IllegalArgumentException.class, () -> {
toOql(person.pets.contains(pet));
});
}

@Test
void filterNegation() {
check(toOqlWithBindParams(person.age.not(self -> self.greaterThan(18)))).is("NOT (age > $1)");
Expand Down

0 comments on commit cfb514b

Please sign in to comment.