Skip to content

Commit

Permalink
add support for String Operators 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 cfb514b commit 43f4fff
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 28 deletions.
Expand Up @@ -58,7 +58,8 @@ protected enum Feature {
ORDER_BY,
REGEX, // regular expression for match() operator
// TODO re-use Operator interface as feature flag
STRING_PREFIX_SUFFIX, // startsWith / endsWith operators a are supported
STRING_EMPTY, // supports empty/non-empty string or null values
STRING_PREFIX_SUFFIX, // startsWith / endsWith operators are supported
ITERABLE_SIZE, // supports filtering on iterables sizes
ITERABLE_CONTAINS, // can search inside inner collections
STRING_LENGTH
Expand Down Expand Up @@ -526,7 +527,7 @@ public void stringLength() {
*/
@Test
public void stringEmptyNotEmpty() {
assumeFeature(Feature.STRING_PREFIX_SUFFIX);
assumeFeature(Feature.STRING_EMPTY);
final PersonGenerator generator = new PersonGenerator();
insert(generator.next().withFullName("John").withNickName(Optional.empty()));
insert(generator.next().withFullName("Adam").withNickName(""));
Expand Down
Expand Up @@ -86,7 +86,9 @@ public void regex_forElastic() {
protected Set<Feature> features() {
return EnumSet.of(Feature.DELETE, Feature.QUERY, Feature.QUERY_WITH_LIMIT,
Feature.QUERY_WITH_PROJECTION,
Feature.QUERY_WITH_OFFSET, Feature.ORDER_BY, Feature.STRING_PREFIX_SUFFIX);
Feature.QUERY_WITH_OFFSET, Feature.ORDER_BY,
Feature.STRING_EMPTY, Feature.STRING_PREFIX_SUFFIX
);
}

@Override
Expand Down
Expand Up @@ -32,12 +32,15 @@
import org.immutables.criteria.expression.Operators;
import org.immutables.criteria.expression.OptionalOperators;
import org.immutables.criteria.expression.Path;
import org.immutables.criteria.expression.StringOperators;
import org.immutables.criteria.expression.Visitors;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
Expand Down Expand Up @@ -146,6 +149,19 @@ private Oql binaryOperator(Call call) {
"String or Primitive types only supported for contains operator but was " + variable.getClass()
);
return oql(String.format("%s.contains(%s)", namedPath, addAndGetBoundVariable(variable)));
} else if (op == StringOperators.MATCHES) {
return oql(String.format("%s.matches(%s)", namedPath, addAndGetBoundVariable(variable)));
} else if (op == StringOperators.CONTAINS || op == StringOperators.STARTS_WITH || op == StringOperators.ENDS_WITH) {
Preconditions.checkArgument(variable instanceof CharSequence, "Variable should be String but was " + variable);

final String value = Geodes.escapeOql((CharSequence) variable);
final String likePattern = getLikePattern(op, value);
if (useBindVariables) {
variables.add(likePattern);
return oql(String.format("%s LIKE $%d", namedPath, variables.size()));
} else {
return oql(String.format("%s LIKE '%s'", namedPath, likePattern));
}
}

final String operator;
Expand All @@ -161,7 +177,7 @@ private Oql binaryOperator(Call call) {
operator = "<";
} else if (op == ComparableOperators.LESS_THAN_OR_EQUAL) {
operator = "<=";
} else if (op == IterableOperators.HAS_SIZE) {
} else if (op == IterableOperators.HAS_SIZE || op == StringOperators.HAS_LENGTH) {
operator = "=";
} else {
throw new IllegalArgumentException("Unknown binary operator " + call);
Expand All @@ -174,14 +190,28 @@ private static boolean isStringOrPrimitive(Object variable) {
return variable instanceof CharSequence || Primitives.isWrapperType(Primitives.wrap(variable.getClass()));
}

private static String getLikePattern(Operator op, String value) {
if (op == StringOperators.STARTS_WITH) {
return value + "%";
} else if (op == StringOperators.ENDS_WITH) {
return "%" + value;
} else if (op == StringOperators.CONTAINS) {
return "%" + value + "%";
} else {
throw new IllegalArgumentException("LIKE query used with invalid operator " + op);
}
}

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 + ".size";
} else if (op == StringOperators.HAS_LENGTH) {
return namedPath + ".length";
}
return namedPath;
}
Expand All @@ -192,25 +222,26 @@ private static Object getVariable(Call call) {

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

final Object variable;
if (op == Operators.IN || op == Operators.NOT_IN) {
variable = ImmutableSet.copyOf(constant.values());
return ImmutableSet.copyOf(constant.values());
} else {
variable = constant.value();
return constant.value();
}
return variable;
}

private String addAndGetBoundVariable(Object variable) {
String variableAsString;
return maybeAddAndGetBoundVariable(variable)
.orElseGet(() -> toString(variable));
}

private Optional<String> maybeAddAndGetBoundVariable(Object variable) {
if (useBindVariables) {
variables.add(variable);
// bind variables in Geode start at index 1: $1, $2, $3 etc.
variableAsString = "$" + String.valueOf(variables.size());

return Optional.of("$" + String.valueOf(variables.size()));
} else {
variableAsString = toString(variable);
return Optional.empty();
}
return variableAsString;
}

/**
Expand All @@ -230,6 +261,8 @@ private static String toMethod(Operator op) {
private static String toString(Object value) {
if (value instanceof CharSequence) {
return "'" + Geodes.escapeOql((CharSequence) value) + "'";
} else if (value instanceof Pattern) {
return "'" + Geodes.escapeOql(((Pattern) value).pattern()) + "'";
} else if (value instanceof Collection) {
@SuppressWarnings("unchecked")
final Collection<Object> coll = (Collection<Object>) value;
Expand Down
Expand Up @@ -28,8 +28,12 @@ class AutocreateRegion implements Consumer<Class<?>> {
private final ContainerNaming naming;

AutocreateRegion(Cache cache) {
this(cache, ContainerNaming.DEFAULT);
}

AutocreateRegion(Cache cache, ContainerNaming containerNaming) {
this.cache = cache;
this.naming = ContainerNaming.DEFAULT;
this.naming = containerNaming;
}

@Override
Expand Down
Expand Up @@ -54,18 +54,6 @@ private StringTest() {
super(backend);
}

@Disabled
@Override
protected void startsWith() {}

@Disabled
@Override
protected void endsWith() {}

@Disabled
@Override
protected void contains() {}

@Disabled("optionals don't work well in Geode yet (pdx serialization)")
@Override
protected void optional() {}
Expand Down
Expand Up @@ -43,7 +43,9 @@ public GeodePersonTest(Cache cache) {
protected Set<Feature> features() {
return EnumSet.of(
Feature.DELETE, Feature.QUERY, Feature.QUERY_WITH_LIMIT, Feature.ORDER_BY, Feature.QUERY_WITH_PROJECTION,
Feature.ITERABLE_SIZE, Feature.ITERABLE_CONTAINS
Feature.ITERABLE_SIZE, Feature.ITERABLE_CONTAINS, Feature.STRING_PREFIX_SUFFIX
// todo: enable String length and Regex feautes once Pdx issue resolved
// Feature.STRING_LENGTH, Feature.REGEX
);
}

Expand Down
136 changes: 136 additions & 0 deletions criteria/geode/test/org/immutables/criteria/geode/GeodePojoTest.java
@@ -0,0 +1,136 @@
/*
* Copyright 2019 Immutables Authors and Contributors
*
* 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.immutables.criteria.geode;

import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.geode.cache.Cache;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.query.FunctionDomainException;
import org.apache.geode.cache.query.NameResolutionException;
import org.apache.geode.cache.query.QueryInvocationTargetException;
import org.apache.geode.cache.query.SelectResults;
import org.apache.geode.cache.query.TypeMismatchException;
import org.immutables.criteria.backend.ContainerNaming;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(GeodeExtension.class)
public class GeodePojoTest {

private final Cache cache;
private final ContainerNaming containerNaming;
private final AutocreateRegion autocreate;

private Region<String, GeodePojo> region;

public GeodePojoTest(Cache cache) {
this.cache = cache;
this.containerNaming = ContainerNaming.DEFAULT;
this.autocreate = new AutocreateRegion(cache, containerNaming);
}

@BeforeEach
void setUp() {
autocreate.accept(GeodePojo.class);

region = cache.getRegion(containerNaming.name(GeodePojo.class));

setUpTestData();
}

@Test
void regex() throws NameResolutionException, TypeMismatchException, QueryInvocationTargetException, FunctionDomainException {
final String query = "SELECT COUNT(*) FROM /geodePojo where name.matches($1)";
Assertions.assertEquals(1, executeCountQuery(query, "\\w+"));
Assertions.assertEquals(2, executeCountQuery(query, "\\w*"));
Assertions.assertEquals(2, executeCountQuery(query, "^\\w+\\s\\w+$"));
}

@Test
void stringNull() throws NameResolutionException, TypeMismatchException, QueryInvocationTargetException, FunctionDomainException {
Assertions.assertEquals(1, executeCountQuery("SELECT COUNT(*) FROM /geodePojo where name = null"));
Assertions.assertEquals(4, executeCountQuery("SELECT COUNT(*) FROM /geodePojo where name != null"));
}

@Test
void stringEmpty() throws NameResolutionException, TypeMismatchException, QueryInvocationTargetException, FunctionDomainException {
Assertions.assertEquals(1, executeCountQuery("SELECT COUNT(*) FROM /geodePojo where name = ''"));
}

@Test
void stringNotEmpty() throws NameResolutionException, TypeMismatchException, QueryInvocationTargetException, FunctionDomainException {
Assertions.assertEquals(4, executeCountQuery("SELECT COUNT(*) FROM /geodePojo where name != ''"));
}

@Test
void stringLength() throws NameResolutionException, TypeMismatchException, QueryInvocationTargetException, FunctionDomainException {
final String query = "SELECT COUNT(*) FROM /geodePojo where name.length = $1";
Assertions.assertEquals(1, executeCountQuery(query, 0));
Assertions.assertEquals(2, executeCountQuery(query, 8));
}

private int executeCountQuery(String query, Object... bindVariables) throws NameResolutionException, TypeMismatchException, QueryInvocationTargetException, FunctionDomainException {
return ((SelectResults<Integer>) execute(query, bindVariables)).stream()
.findAny().orElseThrow(IllegalArgumentException::new);
}

private Object execute(String query, Object... bindVariables) throws FunctionDomainException, TypeMismatchException, NameResolutionException, QueryInvocationTargetException {
return cache.getQueryService().newQuery(query).execute(bindVariables);
}

private void setUpTestData() {
Map<String, GeodePojo> pojos = createPojos();
region.putAll(pojos);
}

private static Map<String, GeodePojo> createPojos() {
final Function<GeodePojo, String> toName = GeodePojo::getName;
return createPojos("John Doe", "Jane Doe", "Joe", "", null)
.collect(Collectors.toMap(toName.andThen(Objects::toString), Function.identity()));
}

private static Stream<GeodePojo> createPojos(String... names) {
return Arrays.stream(names).map(name -> {
final GeodePojo pojo = new GeodePojo();
pojo.setName(name);
return pojo;
});
}

public static class GeodePojo {

String name;

public String getName() {
return name;
}

public GeodePojo setName(String name) {
this.name = name;
return this;
}

}
}
Expand Up @@ -4,6 +4,8 @@
import static org.immutables.criteria.matcher.Matchers.toExpression;
import static org.immutables.criteria.personmodel.PersonCriteria.person;

import java.util.regex.Pattern;

import org.immutables.criteria.backend.PathNaming;
import org.immutables.criteria.personmodel.ImmutablePet;
import org.immutables.criteria.personmodel.PersonCriteria;
Expand Down Expand Up @@ -99,6 +101,32 @@ void filterCollectionDoesNotSupportComplexTypes() {
});
}

@Test
void filterString() {
check(toOql(person.fullName.isEmpty())).is("fullName = ''");
check(toOql(person.fullName.notEmpty())).is("fullName != ''");

check(toOql(person.fullName.hasLength(5))).is("fullName.length = 5");
check(toOql(person.fullName.contains("Blog"))).is("fullName LIKE '%Blog%'");
check(toOql(person.fullName.startsWith("Joe"))).is("fullName LIKE 'Joe%'");
check(toOql(person.fullName.endsWith("Bloggs"))).is("fullName LIKE '%Bloggs'");

check(toOql(person.fullName.matches(Pattern.compile("\\w+")))).is("fullName.matches('\\w+')");
}

@Test
void filterStringWithBindParams() {
check(toOqlWithBindParams(person.fullName.isEmpty())).is("fullName = $1");
check(toOqlWithBindParams(person.fullName.notEmpty())).is("fullName != $1");

check(toOqlWithBindParams(person.fullName.hasLength(5))).is("fullName.length = $1");
check(toOqlWithBindParams(person.fullName.contains("Blog"))).is("fullName LIKE $1");
check(toOqlWithBindParams(person.fullName.startsWith("Joe"))).is("fullName LIKE $1");
check(toOqlWithBindParams(person.fullName.endsWith("Bloggs"))).is("fullName LIKE $1");

check(toOqlWithBindParams(person.fullName.matches(Pattern.compile("\\w+")))).is("fullName.matches($1)");
}

@Test
void filterNegation() {
check(toOqlWithBindParams(person.age.not(self -> self.greaterThan(18)))).is("NOT (age > $1)");
Expand Down
Expand Up @@ -32,6 +32,7 @@ protected Set<Feature> features() {
Feature.ORDER_BY,
Feature.QUERY_WITH_PROJECTION,
Feature.QUERY_WITH_OFFSET, Feature.REGEX,
Feature.STRING_EMPTY,
Feature.STRING_PREFIX_SUFFIX,
Feature.ITERABLE_SIZE,
Feature.ITERABLE_CONTAINS,
Expand Down
Expand Up @@ -41,6 +41,7 @@ protected Set<Feature> features() {
return EnumSet.of(Feature.DELETE, Feature.QUERY, Feature.QUERY_WITH_LIMIT,
Feature.QUERY_WITH_PROJECTION,
Feature.QUERY_WITH_OFFSET, Feature.ORDER_BY, Feature.REGEX,
Feature.STRING_EMPTY,
Feature.STRING_PREFIX_SUFFIX,
Feature.ITERABLE_SIZE,
Feature.ITERABLE_CONTAINS,
Expand Down

0 comments on commit 43f4fff

Please sign in to comment.