/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.test.capedwarf.datastore.test;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import com.google.appengine.api.blobstore.BlobKey;
import com.google.appengine.api.datastore.Blob;
import com.google.appengine.api.datastore.Category;
import com.google.appengine.api.datastore.Email;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.GeoPt;
import com.google.appengine.api.datastore.IMHandle;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Link;
import com.google.appengine.api.datastore.PhoneNumber;
import com.google.appengine.api.datastore.PostalAddress;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.PropertyProjection;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Rating;
import com.google.appengine.api.datastore.RawValue;
import com.google.appengine.api.datastore.ShortBlob;
import com.google.appengine.api.datastore.Text;
import com.google.appengine.api.users.User;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.test.capedwarf.common.support.All;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import static com.google.appengine.api.datastore.Query.FilterOperator.GREATER_THAN;
import static com.google.appengine.api.datastore.Query.FilterOperator.IN;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
/**
* Datastore querying optimizations tests.
*
* @author <a href="mailto:ales.justin@jboss.org">Ales Justin</a>
* @author <a href="mailto:marko.luksa@gmail.com">Marko Luksa</a>
*/
@RunWith(Arquillian.class)
@org.junit.experimental.categories.Category(All.class)
public class QueryOptimizationsTest extends QueryTestBase {
@Test
public void testKeysOnly() throws Exception {
Entity john = createEntity("Person", 1)
.withProperty("name", "John")
.withProperty("surname", "Doe")
.store();
Query query = new Query("Person").setKeysOnly();
PreparedQuery preparedQuery = service.prepare(query);
Entity entity = preparedQuery.asSingleEntity();
assertEquals(john.getKey(), entity.getKey());
assertNull(entity.getProperty("name"));
assertNull(entity.getProperty("surname"));
}
@Test
public void testProjections() throws Exception {
Entity e = createEntity("Product", 1)
.withProperty("price", 123L)
.withProperty("percent", 0.123)
.withProperty("x", -0.321)
.withProperty("diff", -5L)
.withProperty("weight", 10L)
.store();
Query query = new Query("Product")
.addProjection(new PropertyProjection("price", Long.class))
.addProjection(new PropertyProjection("percent", Double.class))
.addProjection(new PropertyProjection("x", Double.class))
.addProjection(new PropertyProjection("diff", Long.class));
PreparedQuery preparedQuery = service.prepare(query);
Entity result = preparedQuery.asSingleEntity();
assertEquals(e.getKey(), result.getKey());
assertEquals(e.getProperty("price"), result.getProperty("price"));
assertEquals(e.getProperty("percent"), result.getProperty("percent"));
assertEquals(e.getProperty("x"), result.getProperty("x"));
assertEquals(e.getProperty("diff"), result.getProperty("diff"));
assertNull(result.getProperty("weight"));
}
@Test
public void testProjectionQueryOnlyReturnsEntitiesContainingProjectedProperty() throws Exception {
Entity e1 = createEntity("Kind", 1)
.withProperty("foo", "foo")
.store();
Entity e2 = createEntity("Kind", 2)
.withProperty("bar", "bar")
.store();
Query query = new Query("Kind")
.addProjection(new PropertyProjection("foo", String.class));
List<Entity> results = service.prepare(query).asList(withDefaults());
assertEquals(Collections.singletonList(e1), results);
}
@Test
public void testProjectionQueryOnlyReturnsEntitiesContainingAllProjectedProperties() throws Exception {
Entity e1 = createEntity("Kind", 1)
.withProperty("foo", "foo")
.withProperty("bar", "bar")
.store();
Entity e2 = createEntity("Kind", 2)
.withProperty("foo", "foo")
.store();
Entity e3 = createEntity("Kind", 3)
.withProperty("bar", "bar")
.store();
Entity e4 = createEntity("Kind", 4)
.withProperty("baz", "baz")
.store();
Query query = new Query("Kind")
.addProjection(new PropertyProjection("foo", String.class))
.addProjection(new PropertyProjection("bar", String.class));
List<Entity> results = service.prepare(query).asList(withDefaults());
assertEquals(Collections.singletonList(e1), results);
}
@Test
public void testProjectionQueryReturnsEntitiesContainingProjectedPropertyEvenIfPropertyValueIsSetToNull() throws Exception {
Entity e1 = createEntity("Kind", 1)
.withProperty("foo", null)
.store();
Query query = new Query("Kind")
.addProjection(new PropertyProjection("foo", String.class));
List<Entity> results = service.prepare(query).asList(withDefaults());
assertEquals(Collections.singletonList(e1), results);
}
@SuppressWarnings("UnnecessaryBoxing")
@Test
public void testLongRawValue() throws Exception {
RawValue raw = getRawValue(1000L);
assertEquals(Long.valueOf(1000L), raw.getValue());
assertEquals(Long.valueOf(1000L), raw.asStrictType(Long.class));
assertEquals(Long.valueOf(1000L), raw.asType(Long.class));
assertEquals(Long.valueOf(1000L), raw.asType(Integer.class));
assertEquals(Long.valueOf(1000L), raw.asType(Short.class));
assertEquals(Long.valueOf(1000L), raw.asType(Byte.class));
assertEquals(new Date(1), raw.asType(Date.class));
assertEquals(new Date(1), raw.asStrictType(Date.class));
assertIAEThrownByAsStrictType(raw, Byte.class, Short.class, Integer.class);
assertIAEThrownByBothAsTypeMethodsForAllTypesExceptFor(raw, Byte.class, Short.class, Integer.class, Long.class, Date.class);
}
@Test
public void testRatingRawValue() throws Exception {
RawValue raw = getRawValue(new Rating(10));
assertEquals(10L, raw.getValue());
assertEquals(new Rating(10), raw.asStrictType(Rating.class));
assertEquals(new Rating(10), raw.asType(Rating.class));
assertEquals(Long.valueOf(10L), raw.asStrictType(Long.class));
assertEquals(10L, raw.asType(Long.class));
assertEquals(10L, raw.asType(Integer.class));
assertEquals(10L, raw.asType(Short.class));
assertEquals(10L, raw.asType(Byte.class));
assertEquals(new Date(0), raw.asType(Date.class));
// assertEquals(new Date(0), raw.asStrictType(Date.class)); TODO: check against appspot
assertIAEThrownByAsStrictType(raw, Byte.class, Short.class, Integer.class);
assertIAEThrownByBothAsTypeMethodsForAllTypesExceptFor(raw, Byte.class, Short.class, Integer.class, Long.class, Date.class, Rating.class);
}
@Test
public void testDateRawValue() throws Exception {
Date now = new Date();
long microSeconds = now.getTime() * 1000;
RawValue raw = getRawValue(now);
assertEquals(microSeconds, raw.getValue());
assertEquals(now, raw.asStrictType(Date.class));
assertEquals(now, raw.asType(Date.class));
assertEquals(Long.valueOf(microSeconds), raw.asStrictType(Long.class));
assertEquals(microSeconds, raw.asType(Long.class));
assertEquals(microSeconds, raw.asType(Integer.class));
assertEquals(microSeconds, raw.asType(Short.class));
assertEquals(microSeconds, raw.asType(Byte.class));
assertIAEThrownByAsStrictType(raw, Byte.class, Short.class, Integer.class);
assertIAEThrownByBothAsTypeMethodsForAllTypesExceptFor(raw, Byte.class, Short.class, Integer.class, Long.class, Date.class);
}
@Test
public void testDoubleRawValue() throws Exception {
RawValue raw = getRawValue(2.0);
assertEquals(2.0, raw.getValue());
assertEquals(Double.valueOf(2.0), raw.asStrictType(Double.class));
assertEquals(2.0, raw.asType(Double.class));
assertIAEThrownByAsStrictType(raw, Float.class);
assertEquals(2.0, raw.asType(Float.class));
assertIAEThrownByBothAsTypeMethodsForAllTypesExceptFor(raw, Double.class, Float.class);
}
@Test
public void testStringRawValue() throws Exception {
String string = "sip string";
RawValue raw = getRawValue(string);
assertEquals(byte[].class, raw.getValue().getClass());
assertArrayEquals(string.getBytes(), (byte[]) raw.getValue());
assertEquals(string, raw.asType(String.class));
assertEquals(string, raw.asStrictType(String.class));
assertEquals(new Text(string), raw.asType(Text.class));
assertEquals(new Text(string), raw.asStrictType(Text.class));
assertEquals(new Link(string), raw.asType(Link.class));
assertEquals(new Link(string), raw.asStrictType(Link.class));
assertEquals(new ShortBlob(string.getBytes()), raw.asType(ShortBlob.class));
// assertEquals(new Blob(string.getBytes()), raw.asType(Blob.class)); // TODO: check against appspot
assertEquals(new Category(string), raw.asType(Category.class));
assertEquals(new PhoneNumber(string), raw.asType(PhoneNumber.class));
assertEquals(new PostalAddress(string), raw.asType(PostalAddress.class));
assertEquals(new Email(string), raw.asType(Email.class));
assertEquals(new IMHandle(IMHandle.Scheme.sip, "string"), raw.asType(IMHandle.class));
assertEquals(new Link(string), raw.asType(Link.class));
assertEquals(new BlobKey(string), raw.asType(BlobKey.class));
assertIAEThrownByBothAsTypeMethods(raw, Byte.class, Short.class, Integer.class, Long.class,
Float.class, Double.class, Boolean.class, Date.class,
GeoPt.class, User.class, Rating.class, Key.class);
}
@Test
public void testNumericStringRawValue() throws Exception {
RawValue raw = getRawValue("123");
assertEquals(byte[].class, raw.getValue().getClass());
assertArrayEquals("123".getBytes(), (byte[]) raw.getValue());
assertIAEThrownByBothAsTypeMethods(raw, Integer.class, Long.class, Float.class, Double.class, Date.class);
}
@Test
public void testPhoneNumberRawValue() throws Exception {
String string = "123";
RawValue raw = getRawValue(new PhoneNumber(string));
assertEquals(byte[].class, raw.getValue().getClass());
assertArrayEquals(string.getBytes(), (byte[]) raw.getValue());
assertEquals(new PhoneNumber(string), raw.asStrictType(PhoneNumber.class));
assertEquals(new PhoneNumber(string), raw.asType(PhoneNumber.class));
assertEquals(new Text(string), raw.asType(Text.class));
assertEquals(new Text(string), raw.asStrictType(Text.class));
assertEquals(new Link(string), raw.asType(Link.class));
assertEquals(new Link(string), raw.asStrictType(Link.class));
assertEquals(new ShortBlob(string.getBytes()), raw.asType(ShortBlob.class));
// assertEquals(new Blob(string.getBytes()), raw.asType(Blob.class)); // TODO: check against appspot
assertEquals(new Category(string), raw.asType(Category.class));
assertEquals(new PhoneNumber(string), raw.asType(PhoneNumber.class));
assertEquals(new PostalAddress(string), raw.asType(PostalAddress.class));
assertEquals(new Email(string), raw.asType(Email.class));
assertEquals(new Link(string), raw.asType(Link.class));
assertEquals(new BlobKey(string), raw.asType(BlobKey.class));
assertIAEThrownByBothAsTypeMethods(raw, Byte.class, Short.class, Integer.class, Long.class,
Float.class, Double.class, Boolean.class, Date.class,
GeoPt.class, User.class, Rating.class, Key.class);
}
@Test
public void testBooleanRawValue() throws Exception {
RawValue raw = getRawValue(true);
assertEquals(true, raw.getValue());
assertEquals(true, raw.asStrictType(Boolean.class));
assertEquals(true, raw.asType(Boolean.class));
assertIAEThrownByBothAsTypeMethodsForAllTypesExceptFor(raw, Boolean.class);
}
@Test
public void testUserRawValue() throws Exception {
User user = new User("someone@gmail.com", "gmail.com");
RawValue raw = getRawValue(user);
assertEquals(user, raw.getValue());
assertEquals(user, raw.asStrictType(User.class));
assertEquals(user, raw.asType(User.class));
assertIAEThrownByBothAsTypeMethodsForAllTypesExceptFor(raw, User.class);
}
@Test
public void testKeyRawValue() throws Exception {
Key key = KeyFactory.createKey("kind", 1);
RawValue raw = getRawValue(key);
assertEquals(key, raw.getValue());
assertEquals(key, raw.asStrictType(Key.class));
assertEquals(key, raw.asType(Key.class));
assertIAEThrownByBothAsTypeMethodsForAllTypesExceptFor(raw, Key.class);
}
@Test
public void testGeoPtRawValue() throws Exception {
GeoPt geoPt = new GeoPt(1f, 2f);
RawValue raw = getRawValue(geoPt);
assertEquals(geoPt, raw.getValue());
assertEquals(geoPt, raw.asStrictType(GeoPt.class));
assertEquals(geoPt, raw.asType(GeoPt.class));
assertIAEThrownByBothAsTypeMethodsForAllTypesExceptFor(raw, GeoPt.class);
}
@Ignore("verify test against appspot")
@Test
public void testBlobKeyRawValue() throws Exception {
BlobKey blobKey = new BlobKey("123");
RawValue raw = getRawValue(blobKey);
assertEquals(byte[].class, raw.getValue().getClass());
assertArrayEquals("123".getBytes(), (byte[]) raw.getValue());
assertEquals(blobKey, raw.asStrictType(BlobKey.class));
assertEquals(blobKey, raw.asType(BlobKey.class));
assertIAEThrownByBothAsTypeMethods(raw, Byte.class, Short.class, Integer.class, Long.class,
Float.class, Double.class, Boolean.class, Date.class,
GeoPt.class, User.class, Rating.class, Key.class);
}
@Test
public void testRawValueReturnsByteArrayValueForAllStringTypes() throws Exception {
// NOTE: Text and Blob types are not indexed
assertEquals(byte[].class, getRawValue("string").getValue().getClass());
assertEquals(byte[].class, getRawValue(new ShortBlob("string".getBytes())).getValue().getClass());
assertEquals(byte[].class, getRawValue(new PostalAddress("string")).getValue().getClass());
assertEquals(byte[].class, getRawValue(new PhoneNumber("string")).getValue().getClass());
assertEquals(byte[].class, getRawValue(new Email("string")).getValue().getClass());
assertEquals(byte[].class, getRawValue(new IMHandle(IMHandle.Scheme.sip, "string")).getValue().getClass());
assertEquals(byte[].class, getRawValue(new Link("string")).getValue().getClass());
assertEquals(byte[].class, getRawValue(new Category("string")).getValue().getClass());
}
@Test
public void testByteArrayReturnedByStringRawValueHasUTF8Encoding() throws Exception {
assertArrayEquals("čćž".getBytes("UTF-8"), (byte[]) getRawValue("čćž").getValue());
}
private RawValue getRawValue(Object value) {
Entity e = createEntity("RawValueTest", 1)
.withProperty("prop", value)
.store();
Query query = new Query("RawValueTest")
.addProjection(new PropertyProjection("prop", null));
PreparedQuery preparedQuery = service.prepare(query);
Entity result = preparedQuery.asSingleEntity();
assertEquals(e.getKey(), result.getKey());
return (RawValue) result.getProperty("prop");
}
private void assertIAEThrownByBothAsTypeMethodsForAllTypesExceptFor(RawValue raw, Class<?>... types) {
HashSet<Class<?>> allTypes = new HashSet<Class<?>>(Arrays.asList((Class<?>)
Byte.class, Short.class, Integer.class, Long.class,
Float.class, Double.class,
Boolean.class, Date.class,
String.class, Text.class, ShortBlob.class, Blob.class, GeoPt.class, PostalAddress.class, PhoneNumber.class,
Email.class, User.class, IMHandle.class, Link.class, Category.class, Rating.class, Key.class, BlobKey.class));
allTypes.removeAll(Arrays.asList(types));
assertIAEThrownByBothAsTypeMethods(raw, allTypes.toArray(new Class[allTypes.size()]));
}
private void assertIAEThrownByBothAsTypeMethods(RawValue rawValue, Class<?>... types) {
assertIAEThrownByAsStrictType(rawValue, types);
assertIAEThrownByAsType(rawValue, types);
}
private void assertIAEThrownByAsType(RawValue rawValue, Class<?>... types) {
for (Class<?> type : types) {
assertIAEThrownByAsType(rawValue, type);
}
}
private void assertIAEThrownByAsType(RawValue rawValue, Class<?> type) {
try {
rawValue.asType(type);
fail("Expected RawValue.asType(" + type.getSimpleName() + ") to throw IllegalArgumentException");
} catch (IllegalArgumentException ok) {
}
}
private void assertIAEThrownByAsStrictType(RawValue rawValue, Class<?>... types) {
for (Class<?> type : types) {
assertIAEThrownByAsStrictType(rawValue, type);
}
}
private void assertIAEThrownByAsStrictType(RawValue rawValue, Class<?> type) {
try {
rawValue.asStrictType(type);
fail("Expected RawValue.asStrictType(" + type.getSimpleName() + ") to throw IllegalArgumentException");
} catch (IllegalArgumentException ok) {
}
}
@Test(expected = IllegalArgumentException.class)
public void testProjectionTypeMismatch() throws Exception {
Entity e = createEntity("foo", 1)
.withProperty("stringProperty", "foo")
.store();
Query query = new Query("foo")
.addProjection(new PropertyProjection("stringProperty", Integer.class));
PreparedQuery preparedQuery = service.prepare(query);
preparedQuery.asSingleEntity();
}
@Test
public void testProjectionOfCollectionProperties() throws Exception {
Entity e = createEntity("test", 1)
.withProperty("prop", Arrays.asList("bbb", "ccc", "aaa"))
.withProperty("prop2", Arrays.asList("xxx", "yyy"))
.store();
Query query = new Query("test")
.addProjection(new PropertyProjection("prop", String.class))
.addSort("prop");
PreparedQuery preparedQuery = service.prepare(query);
List<Entity> results = preparedQuery.asList(withDefaults());
assertEquals(3, results.size());
Entity firstResult = results.get(0);
Entity secondResult = results.get(1);
Entity thirdResult = results.get(2);
assertEquals(e.getKey(), firstResult.getKey());
assertEquals(e.getKey(), secondResult.getKey());
assertEquals(e.getKey(), thirdResult.getKey());
assertEquals("aaa", firstResult.getProperty("prop"));
assertEquals("bbb", secondResult.getProperty("prop"));
assertEquals("ccc", thirdResult.getProperty("prop"));
}
@Test
public void testProjectionOfCollectionPropertyWithFilterOnCollectionProperty() throws Exception {
Entity e = createEntity("Product", 1)
.withProperty("name", Arrays.asList("aaa", "bbb"))
.withProperty("price", Arrays.asList(10L, 20L))
.store();
Query query = new Query("Product")
.addProjection(new PropertyProjection("name", String.class))
.setFilter(new Query.FilterPredicate("price", GREATER_THAN, 0L))
.addSort("price")
.addSort("name");
PreparedQuery preparedQuery = service.prepare(query);
List<Entity> results = preparedQuery.asList(withDefaults());
assertEquals(4, results.size());
assertEquals(e.getKey(), results.get(0).getKey());
assertEquals(e.getKey(), results.get(1).getKey());
assertEquals(e.getKey(), results.get(2).getKey());
assertEquals(e.getKey(), results.get(3).getKey());
assertEquals("aaa", results.get(0).getProperty("name"));
assertEquals("bbb", results.get(1).getProperty("name"));
assertEquals("aaa", results.get(2).getProperty("name"));
assertEquals("bbb", results.get(3).getProperty("name"));
}
@Test
public void testProjectionQueriesHandleEntityModificationProperly() throws Exception {
Entity e = createEntity("test", 1)
.withProperty("prop", Arrays.asList("aaa", "bbb", "ccc"))
.store();
Query query = new Query("test")
.addProjection(new PropertyProjection("prop", String.class))
.addSort("prop");
assertEquals(3, service.prepare(query).asList(withDefaults()).size());
e = createEntity(e.getKey())
.withProperty("prop", Arrays.asList("aaa", "bbb"))
.store();
assertEquals(2, service.prepare(query).asList(withDefaults()).size());
service.delete(e.getKey());
assertEquals(0, service.prepare(query).asList(withDefaults()).size());
}
@Test
public void testOrderOfReturnedResultsIsSameAsOrderOfElementsInInStatementWhenUsingProjections() throws Exception {
Entity b = createEntity("Product", 1)
.withProperty("name", "b")
.withProperty("price", 1L)
.store();
Entity a = createEntity("Product", 2)
.withProperty("name", "a")
.withProperty("price", 2L)
.store();
Query query = new Query("Product")
.addProjection(new PropertyProjection("price", Long.class))
.setFilter(new Query.FilterPredicate("name", IN, Arrays.asList("a", "b")));
assertResultsInOrder(query, a, b);
query = query.setFilter(new Query.FilterPredicate("name", IN, Arrays.asList("b", "a")));
assertResultsInOrder(query, b, a);
}
@Test
public void testOrderOfReturnedResultsIsSameAsOrderOfElementsInInStatementWhenUsingKeysOnly() throws Exception {
Entity b = createEntity("Product", 1)
.withProperty("name", "b")
.store();
Entity a = createEntity("Product", 2)
.withProperty("name", "a")
.store();
Query query = new Query("Product")
.setKeysOnly()
.setFilter(new Query.FilterPredicate("name", IN, Arrays.asList("a", "b")));
assertResultsInOrder(query, a, b);
query = query.setFilter(new Query.FilterPredicate("name", IN, Arrays.asList("b", "a")));
assertResultsInOrder(query, b, a);
}
@Test
public void testEntityOnlyContainsProjectedProperties() throws Exception {
Entity a = createEntity("Product", 1)
.withProperty("name", "b")
.withProperty("price", 1L)
.store();
Entity b = createEntity("Product", 2)
.withProperty("name", "a")
.withProperty("price", 2L)
.store();
Query query = new Query("Product")
.addProjection(new PropertyProjection("price", Long.class))
.setFilter(new Query.FilterPredicate("name", IN, Arrays.asList("a", "b")));
Entity firstResult = service.prepare(query).asList(FetchOptions.Builder.withDefaults()).get(0);
assertEquals(1, firstResult.getProperties().size());
assertEquals("price", firstResult.getProperties().keySet().iterator().next());
query = new Query("Product")
.setKeysOnly()
.setFilter(new Query.FilterPredicate("name", IN, Arrays.asList("a", "b")));
firstResult = service.prepare(query).asList(FetchOptions.Builder.withDefaults()).get(0);
assertEquals(0, firstResult.getProperties().size());
}
private void assertResultsInOrder(Query query, Entity a, Entity b) {
PreparedQuery preparedQuery = service.prepare(query);
List<Entity> results = preparedQuery.asList(FetchOptions.Builder.withDefaults());
Entity firstResult = results.get(0);
Entity secondResult = results.get(1);
assertEquals(a.getKey(), firstResult.getKey());
assertEquals(b.getKey(), secondResult.getKey());
}
}