Skip to content

Commit

Permalink
Add GetByKey and DeleteByKey lookup operation (to StandardOperations)
Browse files Browse the repository at this point in the history
Allows raw key-lookup (and deletion by key) when key can not be expressed as AST / class Member.

Add seperate test templates for integration tests.

Implement those operations in InMemory, Mongo and Geode backends.

Elasticsearch backend will be addressed separately
  • Loading branch information
asereda-gs committed May 5, 2020
1 parent 8a2aa88 commit ce9ad50
Show file tree
Hide file tree
Showing 16 changed files with 433 additions and 27 deletions.
Expand Up @@ -29,6 +29,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
* Common operations which can be executed on a backend.
Expand Down Expand Up @@ -142,10 +143,8 @@ static UpdateByQuery of(Query query, Map<Expression, Object> values) {
Preconditions.checkArgument(!values.isEmpty(), "no values");
return ImmutableUpdateByQuery.of(query, values);
}

}


/**
* Delete documents using some criteria
*/
Expand All @@ -159,6 +158,37 @@ static Delete of(Criterion<?> criteria) {
}
}

/**
* Low-level delete operation when key can not be expressed as a query (or queried). For instance
* when key is not present on the entity itself (eg. generic key defined by a
* function).
*/
@Value.Immutable
public interface DeleteByKey extends Backend.Operation {

/**
* Keys to delete
*/
@Value.Parameter
Set<?> keys();
}

/**
* Key lookup operation. Used when lookup can not be
* expressed as a query (eg. there is no field representing
* ID). The order of returned entities is not deterministic and
* is not guaranteed to be same as the input order of keys.
*/
@Value.Immutable
public interface GetByKey extends Backend.Operation {
/**
* Keys to perform the lookup operation
*/
@Value.Parameter
Set<?> keys();
}


@Value.Immutable
public interface Watch extends Backend.Operation {
@Value.Parameter
Expand Down
@@ -0,0 +1,80 @@
/*
* Copyright 2020 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.typemodel;

import com.google.common.collect.ImmutableSet;
import io.reactivex.Flowable;
import org.immutables.criteria.backend.Backend;
import org.immutables.criteria.backend.ImmutableDeleteByKey;
import org.immutables.criteria.backend.StandardOperations;
import org.immutables.criteria.backend.WriteResult;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.function.Supplier;

import static org.immutables.check.Checkers.check;

public abstract class DeleteByKeyTemplate {

private final StringHolderRepository repository;
private final Backend.Session session;
private final Supplier<ImmutableStringHolder> generator;

protected DeleteByKeyTemplate(Backend backend) {
this.repository = new StringHolderRepository(backend);
this.session = backend.open(TypeHolder.StringHolder.class);
this.generator = TypeHolder.StringHolder.generator();
}

@Test
void empty() {
check(execute(ImmutableDeleteByKey.of(Collections.emptySet()))).isIn(WriteResult.unknown(), WriteResult.empty().withDeletedCount(0));
check(execute(ImmutableDeleteByKey.of(Collections.singleton("aaa")))).isIn(WriteResult.unknown(), WriteResult.empty().withDeletedCount(0));
check(execute(ImmutableDeleteByKey.of(ImmutableSet.of("a", "b")))).isIn(WriteResult.unknown(), WriteResult.empty().withDeletedCount(0));
}

@Test
void single() {
repository.insert(generator.get().withId("id1"));

check(execute(ImmutableDeleteByKey.of(Collections.emptySet()))).isIn(WriteResult.unknown(), WriteResult.empty().withDeletedCount(0));
check(repository.findAll().fetch()).hasSize(1);

check(execute(ImmutableDeleteByKey.of(Collections.singleton("aaa")))).isIn(WriteResult.unknown(), WriteResult.empty().withDeletedCount(0));
check(repository.findAll().fetch()).hasSize(1);

check(execute(ImmutableDeleteByKey.of(Collections.singleton("id1")))).isIn(WriteResult.unknown(), WriteResult.empty().withDeletedCount(1));
check(repository.findAll().fetch()).isEmpty();
}

@Test
void multiple() {
repository.insert(generator.get().withId("id1"));
repository.insert(generator.get().withId("id2"));
check(execute(ImmutableDeleteByKey.of(ImmutableSet.of("id2", "id1", "MISSING")))).isIn(WriteResult.unknown(), WriteResult.empty().withDeletedCount(2));
check(repository.findAll().fetch()).isEmpty();
}

private WriteResult execute(StandardOperations.DeleteByKey op) {
return Flowable.fromPublisher(session.execute(op).publisher())
.singleOrError()
.cast(WriteResult.class)
.blockingGet();
}

}
@@ -0,0 +1,84 @@
/*
* Copyright 2020 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.typemodel;

import com.google.common.collect.ImmutableSet;
import io.reactivex.Flowable;
import org.immutables.check.IterableChecker;
import org.immutables.criteria.backend.Backend;
import org.immutables.criteria.backend.ImmutableGetByKey;
import org.immutables.criteria.backend.StandardOperations;
import org.junit.jupiter.api.Test;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.function.Supplier;

import static org.immutables.check.Checkers.check;

public abstract class GetByKeyTemplate {

private final StringHolderRepository repository;
private final Backend.Session session;
private final Supplier<ImmutableStringHolder> generator;

protected GetByKeyTemplate(Backend backend) {
this.repository = new StringHolderRepository(backend);
this.session = backend.open(TypeHolder.StringHolder.class);
this.generator = TypeHolder.StringHolder.generator();
}

@Test
void empty() {
ids(ImmutableGetByKey.of(Collections.emptySet())).isEmpty();
ids(ImmutableGetByKey.of(Collections.singleton("s1"))).isEmpty();
ids(ImmutableGetByKey.of(new HashSet<>(Arrays.asList("s1", "s2")))).isEmpty();
}

@Test
void single() {
repository.insert(generator.get().withId("i1"));
ids(ImmutableGetByKey.of(Collections.emptySet())).isEmpty();
ids(ImmutableGetByKey.of(Collections.singleton("MISSING"))).isEmpty();
ids(ImmutableGetByKey.of(Collections.singleton("i1"))).isOf("i1");
ids(ImmutableGetByKey.of(ImmutableSet.of("i1", "MISSING"))).isOf("i1");
ids(ImmutableGetByKey.of(ImmutableSet.of("MISSING", "i1"))).isOf("i1");
}

@Test
void multiple() {
repository.insert(generator.get().withId("i1"));
repository.insert(generator.get().withId("i2"));

ids(ImmutableGetByKey.of(Collections.emptySet())).isEmpty();
ids(ImmutableGetByKey.of(Collections.singleton("MISSING"))).isEmpty();
ids(ImmutableGetByKey.of(Collections.singleton("i1"))).isOf("i1");
ids(ImmutableGetByKey.of(Collections.singleton("i2"))).isOf("i2");
ids(ImmutableGetByKey.of(ImmutableSet.of("i1", "i2"))).hasContentInAnyOrder("i1", "i2");
ids(ImmutableGetByKey.of(ImmutableSet.of("i1", "MISSING"))).isOf("i1");
}

private IterableChecker<List<String>, String> ids(StandardOperations.GetByKey op) {
List<String> list = Flowable.fromPublisher(session.execute(op).publisher())
.cast(TypeHolder.StringHolder.class).map(TypeHolder.StringHolder::id)
.toList().blockingGet();
return check(list);
}

}
Expand Up @@ -103,6 +103,10 @@ private Publisher<?> executeInternal(Operation operation) {
return Flowable.fromCallable(new SyncDelete(this, (StandardOperations.Delete) operation));
} else if (operation instanceof StandardOperations.Watch) {
return watch((StandardOperations.Watch) operation);
} else if (operation instanceof StandardOperations.DeleteByKey) {
return Flowable.fromCallable(new SyncDeleteByKey(this, (StandardOperations.DeleteByKey) operation));
} else if (operation instanceof StandardOperations.GetByKey) {
return Flowable.fromCallable(new SyncGetByKey(this, (StandardOperations.GetByKey) operation)).flatMapIterable(x -> x);
}

return Flowable.error(new UnsupportedOperationException(String.format("Operation %s not supported by %s",
Expand Down
Expand Up @@ -17,7 +17,12 @@
package org.immutables.criteria.geode;

import org.immutables.criteria.backend.PathNaming;
import org.immutables.criteria.expression.*;
import org.immutables.criteria.expression.AggregationCall;
import org.immutables.criteria.expression.Collation;
import org.immutables.criteria.expression.Expression;
import org.immutables.criteria.expression.Ordering;
import org.immutables.criteria.expression.Path;
import org.immutables.criteria.expression.Query;
import org.immutables.value.Value;

import java.util.ArrayList;
Expand Down
17 changes: 2 additions & 15 deletions criteria/geode/src/org/immutables/criteria/geode/SyncDelete.java
Expand Up @@ -71,20 +71,7 @@ public WriteResult call() throws Exception {
* @param keys list of keys to delete
* @see Region#removeAll(Collection)
*/
private WriteResult deleteByKeys(Collection<?> keys) {
if (keys.isEmpty()) {
return WriteResult.empty();
}

// special case for single key delete
// Not using removeAll() because one can know if element was deleted based on return of remove()
// this return is used for WriteResult statistics
if (keys.size() == 1) {
boolean removed = region.remove(keys.iterator().next()) != null;
return WriteResult.empty().withDeletedCount(removed ? 1 :0);
}

region.removeAll(keys);
return WriteResult.unknown(); // can't really return keys.size()
private WriteResult deleteByKeys(Iterable<?> keys) throws Exception {
return new SyncDeleteByKey(session, keys).call();
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2020 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 com.google.common.collect.ImmutableSet;
import org.apache.geode.cache.Region;
import org.immutables.criteria.backend.StandardOperations;
import org.immutables.criteria.backend.WriteResult;

import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;

/**
* Responsible for delete by key. Using bulk API
*
* @see Region#removeAll(Collection)
*/
class SyncDeleteByKey implements Callable<WriteResult> {

private final Set<?> keys;
private final GeodeBackend.Session session;

SyncDeleteByKey(GeodeBackend.Session session, StandardOperations.DeleteByKey operation) {
this(session, operation.keys());
}

SyncDeleteByKey(GeodeBackend.Session session, Iterable<?> keys) {
this.keys = ImmutableSet.copyOf(keys);
this.session = Objects.requireNonNull(session, "session");
}

@Override
public WriteResult call() throws Exception {
if (keys.isEmpty()) {
return WriteResult.empty();
}

// special case for single key delete
// Not using removeAll() because one can know if element was deleted based on return of remove()
// this return is used for WriteResult statistics
if (keys.size() == 1) {
boolean removed = session.region.remove(keys.iterator().next()) != null;
return WriteResult.empty().withDeletedCount(removed ? 1 :0);
}
session.region.removeAll(keys);
return WriteResult.unknown();
}
}
49 changes: 49 additions & 0 deletions criteria/geode/src/org/immutables/criteria/geode/SyncGetByKey.java
@@ -0,0 +1,49 @@
/*
* Copyright 2020 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 com.google.common.collect.ImmutableSet;
import org.immutables.criteria.backend.StandardOperations;

import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

class SyncGetByKey implements Callable<Iterable<Object>> {

private final GeodeBackend.Session session;
private final Set<?> keys;

SyncGetByKey(GeodeBackend.Session session, StandardOperations.GetByKey op) {
this(session, op.keys());
}

SyncGetByKey(GeodeBackend.Session session, Iterable<?> keys) {
this.session = Objects.requireNonNull(session, "session");
this.keys = ImmutableSet.copyOf(keys);
}

@Override
public Iterable<Object> call() throws Exception {
return session.region.getAll(keys)
.values().stream()
.filter(Objects::nonNull) // skip missing keys (null values)
.collect(Collectors.toList());
}

}

0 comments on commit ce9ad50

Please sign in to comment.