summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShawn O. Pearce <sop@google.com>2010-08-09 14:18:47 -0700
committerShawn O. Pearce <sop@google.com>2010-08-09 14:34:06 -0700
commit43287dd36f973de13d1d1968a32f37b48320ba48 (patch)
tree03bb523f6ec311d4aeb2f92270616a8de432cc70
parentbf62ee2f4843c2c053de1b32ba764e8c6bdd0e3a (diff)
downloadgwtorm-43287dd36f973de13d1d1968a32f37b48320ba48.tar.gz
Permit NoSQL results to be out-of-order
Just like with a SQL database, a NoSQL system might be able to return results out of order more efficiently than in-order when there is no explicit ORDER BY clause in the query. Support that by passing through a hint indicating if the order needs to be preserved, or not. Change-Id: Ic601fd3277936e599b590c508e2435bc5bf5f95b Signed-off-by: Shawn O. Pearce <sop@google.com>
-rw-r--r--src/main/java/com/google/gwtorm/nosql/AccessGen.java9
-rw-r--r--src/main/java/com/google/gwtorm/nosql/NoSqlAccess.java19
-rw-r--r--src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java28
-rw-r--r--src/main/java/com/google/gwtorm/nosql/generic/GenericSchema.java10
-rw-r--r--src/main/java/com/google/gwtorm/nosql/heap/TreeMapSchema.java2
-rw-r--r--src/test/java/com/google/gwtorm/nosql/NoSqlPhoneBookTest.java250
6 files changed, 296 insertions, 22 deletions
diff --git a/src/main/java/com/google/gwtorm/nosql/AccessGen.java b/src/main/java/com/google/gwtorm/nosql/AccessGen.java
index d2fffe7..5344e1e 100644
--- a/src/main/java/com/google/gwtorm/nosql/AccessGen.java
+++ b/src/main/java/com/google/gwtorm/nosql/AccessGen.java
@@ -411,16 +411,21 @@ class AccessGen implements Opcodes {
cgs.push(0);
}
+ // Only keep order if there is an order by clause present
+ //
+ cgs.push(info.hasOrderBy() ? 1 : 0);
+
if (needsIndexFunction(info)) {
mv.visitMethodInsn(INVOKEVIRTUAL, accessType.getInternalName(),
"scanIndex", Type.getMethodDescriptor(resultSet, new Type[] {
- indexFunction, byteArray, byteArray, Type.INT_TYPE}));
+ indexFunction, byteArray, byteArray, Type.INT_TYPE,
+ Type.BOOLEAN_TYPE}));
} else {
// No where and no order by clause? Use the primary key instead.
//
mv.visitMethodInsn(INVOKEVIRTUAL, accessType.getInternalName(),
"scanPrimaryKey", Type.getMethodDescriptor(resultSet, new Type[] {
- byteArray, byteArray, Type.INT_TYPE}));
+ byteArray, byteArray, Type.INT_TYPE, Type.BOOLEAN_TYPE}));
}
mv.visitInsn(ARETURN);
diff --git a/src/main/java/com/google/gwtorm/nosql/NoSqlAccess.java b/src/main/java/com/google/gwtorm/nosql/NoSqlAccess.java
index 291279b..52e84ab 100644
--- a/src/main/java/com/google/gwtorm/nosql/NoSqlAccess.java
+++ b/src/main/java/com/google/gwtorm/nosql/NoSqlAccess.java
@@ -29,16 +29,24 @@ public abstract class NoSqlAccess<T, K extends Key<?>> extends
/**
* Scan a range of keys from the data rows and return any matching objects.
+ * <p>
+ * All NoSQL implementations must provide their own variant of this method.
+ * <p>
+ * To fetch a single record with a scan, set {@code toKey} to the same array
+ * as {@code fromKey}, but append a trailing NUL byte (0x00). The caller
+ * should validate that the returned ResultSet contains no more than 1 row.
*
* @param fromKey key to start the scan on. This is inclusive.
* @param toKey key to stop the scan on. This is exclusive.
- * @param limit maximum number of results to return.
+ * @param limit maximum number of results to return, 0 for unlimited.
+ * @param order if true the order will be preserved, false if the result order
+ * order can be arbitrary.
* @return result set for the requested range. The result set may be lazily
* filled, or filled completely.
* @throws OrmException an error occurred preventing the scan from completing.
*/
protected abstract ResultSet<T> scanPrimaryKey(byte[] fromKey, byte[] toKey,
- int limit) throws OrmException;
+ int limit, boolean order) throws OrmException;
/**
* Scan a range of keys and return any matching objects.
@@ -52,13 +60,16 @@ public abstract class NoSqlAccess<T, K extends Key<?>> extends
* @param index definition of the index the scan occurs over.
* @param fromKey key to start the scan on. This is inclusive.
* @param toKey key to stop the scan on. This is exclusive.
- * @param limit maximum number of results to return.
+ * @param limit maximum number of results to return, 0 for unlimited.
+ * @param order if true the order will be preserved, false if the result order
+ * order can be arbitrary.
* @return result set for the requested range. The result set may be lazily
* filled, or filled completely.
* @throws OrmException an error occurred preventing the scan from completing.
*/
protected abstract ResultSet<T> scanIndex(IndexFunction<T> index,
- byte[] fromKey, byte[] toKey, int limit) throws OrmException;
+ byte[] fromKey, byte[] toKey, int limit, boolean order)
+ throws OrmException;
// -- These are all provided by AccessGen when it builds a subclass --
diff --git a/src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java b/src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java
index 3884a31..524cb12 100644
--- a/src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java
+++ b/src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java
@@ -32,7 +32,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
-import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -66,8 +65,8 @@ public abstract class GenericAccess<T, K extends Key<?>> extends
* Lookup a single entity via its primary key.
* <p>
* The default implementation of this method performs a scan over the primary
- * key with {@link #scanPrimaryKey(byte[], byte[], int)}, '\0' appended onto
- * the fromKey and a result limit of 2.
+ * key with {@link #scanPrimaryKey(byte[], byte[], int, boolean)}, '\0'
+ * appended onto the fromKey and a result limit of 2.
* <p>
* If multiple records are discovered {@link OrmDuplicateKeyException} is
* thrown back to the caller.
@@ -87,7 +86,7 @@ public abstract class GenericAccess<T, K extends Key<?>> extends
dst.nul();
final byte[] toKey = dst.toByteArray();
- Iterator<T> r = scanPrimaryKey(fromKey, toKey, 2).iterator();
+ Iterator<T> r = scanPrimaryKey(fromKey, toKey, 2, false).iterator();
if (!r.hasNext()) {
return null;
}
@@ -105,13 +104,15 @@ public abstract class GenericAccess<T, K extends Key<?>> extends
* @param fromKey key to start the scan on. This is inclusive.
* @param toKey key to stop the scan on. This is exclusive.
* @param limit maximum number of results to return.
+ * @param order if true the order will be preserved, false if the result order
+ * order can be arbitrary.
* @return result set for the requested range. The result set may be lazily
* filled, or filled completely.
* @throws OrmException an error occurred preventing the scan from completing.
*/
@Override
- protected ResultSet<T> scanPrimaryKey(byte[] fromKey, byte[] toKey, int limit)
- throws OrmException {
+ protected ResultSet<T> scanPrimaryKey(byte[] fromKey, byte[] toKey,
+ int limit, boolean order) throws OrmException {
IndexKeyBuilder b;
b = new IndexKeyBuilder();
@@ -126,7 +127,9 @@ public abstract class GenericAccess<T, K extends Key<?>> extends
b.addRaw(toKey);
toKey = b.toByteArray();
- final ResultSet<Map.Entry<byte[], byte[]>> rs = db.scan(fromKey, toKey, limit);
+ final ResultSet<Map.Entry<byte[], byte[]>> rs =
+ db.scan(fromKey, toKey, limit, order);
+
final Iterator<Map.Entry<byte[], byte[]>> i = rs.iterator();
return new AbstractResultSet<T>() {
@@ -157,13 +160,15 @@ public abstract class GenericAccess<T, K extends Key<?>> extends
* @param fromKey key to start the scan on. This is inclusive.
* @param toKey key to stop the scan on. This is exclusive.
* @param limit maximum number of results to return.
+ * @param order if true the order will be preserved, false if the result order
+ * order can be arbitrary.
* @return result set for the requested range. The result set may be lazily
* filled, or filled completely.
* @throws OrmException an error occurred preventing the scan from completing.
*/
@Override
protected ResultSet<T> scanIndex(IndexFunction<T> idx, byte[] fromKey,
- byte[] toKey, int limit) throws OrmException {
+ byte[] toKey, int limit, boolean order) throws OrmException {
final long now = System.currentTimeMillis();
IndexKeyBuilder b;
@@ -188,7 +193,8 @@ public abstract class GenericAccess<T, K extends Key<?>> extends
SCAN: for (;;) {
int scanned = 0;
- ResultSet<Entry<byte[], byte[]>> rs = db.scan(lastKey, toKey, limit);
+ ResultSet<Entry<byte[], byte[]>> rs =
+ db.scan(lastKey, toKey, limit, order);
for (Map.Entry<byte[], byte[]> ent : rs) {
final byte[] idxkey = ent.getKey();
lastKey = idxkey;
@@ -319,8 +325,8 @@ public abstract class GenericAccess<T, K extends Key<?>> extends
* Insert or update operations should invoke this method before the main data
* row is written, allowing the secondary index rows to be put into the data
* store before the main data row arrives. Compatible scan implementations
- * (such as {@link #scanIndex(IndexFunction, byte[], byte[], int)} above) will
- * ignore these rows for a short time period.
+ * (such as {@link #scanIndex(IndexFunction, byte[], byte[], int, boolean)}
+ * above) will ignore these rows for a short time period.
*
* @param oldObj an old copy of the object; if non-null this may be used to
* avoid writing unnecessary secondary index rows that already exist.
diff --git a/src/main/java/com/google/gwtorm/nosql/generic/GenericSchema.java b/src/main/java/com/google/gwtorm/nosql/generic/GenericSchema.java
index 37fdde3..e6268a7 100644
--- a/src/main/java/com/google/gwtorm/nosql/generic/GenericSchema.java
+++ b/src/main/java/com/google/gwtorm/nosql/generic/GenericSchema.java
@@ -113,8 +113,8 @@ public abstract class GenericSchema extends NoSqlSchema {
* Fetch one row's data.
* <p>
* The default implementation of this method creates a pair of keys and passes
- * them to {@link #scan(byte[], byte[], int)}. The {@code fromKey} is the
- * supplied {@code key}, while the {@code toKey} has '\0' appended onto
+ * them to {@link #scan(byte[], byte[], int, boolean)}. The {@code fromKey} is
+ * the supplied {@code key}, while the {@code toKey} has '\0' appended onto
* {@code key}. If more than one row matches in that range, the method throws
* an exception.
*
@@ -130,7 +130,7 @@ public abstract class GenericSchema extends NoSqlSchema {
final byte[] toKey = new byte[key.length + 1];
System.arraycopy(key, 0, toKey, 0, key.length);
- ResultSet<Entry<byte[], byte[]>> r = scan(fromKey, toKey, 2);
+ ResultSet<Entry<byte[], byte[]>> r = scan(fromKey, toKey, 2, false);
try {
Iterator<Entry<byte[], byte[]>> i = r.iterator();
if (!i.hasNext()) {
@@ -162,12 +162,14 @@ public abstract class GenericSchema extends NoSqlSchema {
* @param fromKey key to start the scan on. This is inclusive.
* @param toKey key to stop the scan on. This is exclusive.
* @param limit maximum number of results to return.
+ * @param order if true the order will be preserved, false if the result order
+ * order can be arbitrary.
* @return result iteration for the requested range. The result set may be
* lazily filled, or filled completely.
* @throws OrmException an error occurred preventing the scan from completing.
*/
public abstract ResultSet<Map.Entry<byte[], byte[]>> scan(byte[] fromKey,
- byte[] toKey, int limit) throws OrmException;
+ byte[] toKey, int limit, boolean order) throws OrmException;
/**
* Atomically insert one row, failing if the row already exists.
diff --git a/src/main/java/com/google/gwtorm/nosql/heap/TreeMapSchema.java b/src/main/java/com/google/gwtorm/nosql/heap/TreeMapSchema.java
index 4cc1a2a..9b75422 100644
--- a/src/main/java/com/google/gwtorm/nosql/heap/TreeMapSchema.java
+++ b/src/main/java/com/google/gwtorm/nosql/heap/TreeMapSchema.java
@@ -48,7 +48,7 @@ public abstract class TreeMapSchema extends GenericSchema {
@Override
public ResultSet<Map.Entry<byte[], byte[]>> scan(byte[] fromKey,
- byte[] toKey, int limit) {
+ byte[] toKey, int limit, boolean order) {
db.lock.lock();
try {
final List<Map.Entry<byte[], byte[]>> res =
diff --git a/src/test/java/com/google/gwtorm/nosql/NoSqlPhoneBookTest.java b/src/test/java/com/google/gwtorm/nosql/NoSqlPhoneBookTest.java
new file mode 100644
index 0000000..8532809
--- /dev/null
+++ b/src/test/java/com/google/gwtorm/nosql/NoSqlPhoneBookTest.java
@@ -0,0 +1,250 @@
+// Copyright 2008 Google Inc.
+//
+// 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 com.google.gwtorm.nosql;
+
+import com.google.gwtorm.client.Access;
+import com.google.gwtorm.client.OrmConcurrencyException;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.data.PersonAccess;
+import com.google.gwtorm.data.PhoneBookDb;
+import com.google.gwtorm.data.TestPerson;
+import com.google.gwtorm.jdbc.JdbcExecutor;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.nosql.heap.MemoryDatabase;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class NoSqlPhoneBookTest extends TestCase {
+ protected MemoryDatabase<PhoneBookDb> db;
+ private List<PhoneBookDb> openSchemas;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ db = new MemoryDatabase<PhoneBookDb>(PhoneBookDb.class);
+ openSchemas = new ArrayList<PhoneBookDb>();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ if (openSchemas != null) {
+ for (PhoneBookDb schema : openSchemas) {
+ schema.close();
+ }
+ openSchemas = null;
+ }
+ super.tearDown();
+ }
+
+ protected PhoneBookDb open() throws OrmException {
+ final PhoneBookDb r = db.open();
+ if (r != null) {
+ openSchemas.add(r);
+ }
+ return r;
+ }
+
+ public void testCreateDatabaseHandle() throws Exception {
+ assertNotNull(db);
+ }
+
+ public void testOpenSchema() throws Exception {
+ final PhoneBookDb schema1 = open();
+ assertNotNull(schema1);
+
+ final PhoneBookDb schema2 = open();
+ assertNotNull(schema2);
+ assertNotSame(schema1, schema2);
+ }
+
+ public void testGetPeopleAccess() throws Exception {
+ final PhoneBookDb schema = open();
+ assertNotNull(schema.people());
+ assertEquals("people", schema.people().getRelationName());
+ assertEquals(1, schema.people().getRelationID());
+ }
+
+ public void testGetAddressAccess() throws Exception {
+ final PhoneBookDb schema = open();
+ assertNotNull(schema.addresses());
+ assertEquals("addresses", schema.addresses().getRelationName());
+ assertEquals(2, schema.addresses().getRelationID());
+ }
+
+ public void testGetAllRelations() throws Exception {
+ final PhoneBookDb schema = open();
+ Access<?, ?>[] all = schema.allRelations();
+ assertNotNull(all);
+ assertEquals(2, all.length);
+ assertSame(schema.people(), all[0]);
+ assertSame(schema.addresses(), all[1]);
+ }
+
+ public void testNextAddressId() throws Exception {
+ final PhoneBookDb schema = open();
+ final int a = schema.nextAddressId();
+ final int b = schema.nextAddressId();
+ assertTrue(a != b);
+ }
+
+ public void testPersonPrimaryKey() throws Exception {
+ final PhoneBookDb schema = open();
+ final TestPerson.Key key = new TestPerson.Key("Bob");
+ final TestPerson bob = new TestPerson(key, 18);
+ assertSame(key, schema.people().primaryKey(bob));
+ }
+
+ public void testInsertOnePerson() throws Exception {
+ final PhoneBookDb schema = open();
+ final TestPerson bob = new TestPerson(new TestPerson.Key("Bob"), 18);
+ schema.people().insert(Collections.singleton(bob));
+
+ TestPerson copy = schema.people().all().toList().get(0);
+ assertNotSame(copy, bob);
+ assertEquals(bob.name(), copy.name());
+ }
+
+ public void testGetOnePerson() throws Exception {
+ final PhoneBookDb schema = open();
+ final PersonAccess sp = schema.people();
+ final TestPerson p1 = new TestPerson(new TestPerson.Key("Bob"), 18);
+ sp.insert(Collections.singleton(p1));
+
+ final TestPerson p2 = sp.get(sp.primaryKey(p1));
+ assertNotNull(p2);
+ assertNotSame(p1, p2);
+ assertEquals(sp.primaryKey(p1), sp.primaryKey(p2));
+ }
+
+ public void testGetOnePersonIterator() throws Exception {
+ final PhoneBookDb schema = open();
+ final PersonAccess sp = schema.people();
+ final TestPerson p1 = new TestPerson(new TestPerson.Key("Bob"), 18);
+ sp.insert(Collections.singleton(p1));
+
+ final List<TestPerson> list =
+ sp.get(Collections.singleton(sp.primaryKey(p1))).toList();
+ assertNotNull(list);
+ assertEquals(1, list.size());
+
+ final TestPerson p2 = list.get(0);
+ assertNotNull(p2);
+ assertNotSame(p1, p2);
+ assertEquals(sp.primaryKey(p1), sp.primaryKey(p2));
+ }
+
+ public void testInsertManyPeople() throws Exception {
+ final PhoneBookDb schema = open();
+ final ArrayList<TestPerson> all = new ArrayList<TestPerson>();
+ all.add(new TestPerson(new TestPerson.Key("Bob"), 18));
+ all.add(new TestPerson(new TestPerson.Key("Mary"), 22));
+ all.add(new TestPerson(new TestPerson.Key("Zak"), 33));
+ schema.people().insert(all);
+ }
+
+ public void testDeleteOnePerson() throws Exception {
+ final PhoneBookDb schema = open();
+ final TestPerson bob = new TestPerson(new TestPerson.Key("Bob"), 18);
+ schema.people().insert(Collections.singleton(bob));
+ schema.people().delete(Collections.singleton(bob));
+ }
+
+ public void testUpdateOnePerson() throws Exception {
+ final PhoneBookDb schema = open();
+ final TestPerson bob = new TestPerson(new TestPerson.Key("Bob"), 18);
+ schema.people().insert(Collections.singleton(bob));
+ bob.growOlder();
+ schema.people().update(Collections.singleton(bob));
+ }
+
+ public void testUpdateNoPerson() throws Exception {
+ final PhoneBookDb schema = open();
+ final TestPerson bob = new TestPerson(new TestPerson.Key("Bob"), 18);
+ try {
+ schema.people().update(Collections.singleton(bob));
+ fail("Update of missing person succeeded");
+ } catch (OrmConcurrencyException e) {
+ assertEquals("Concurrent modification detected", e.getMessage());
+ }
+ }
+
+ public void testFetchOnePerson() throws Exception {
+ final PhoneBookDb schema = open();
+ final TestPerson bob = new TestPerson(new TestPerson.Key("Bob"), 18);
+ schema.people().insert(Collections.singleton(bob));
+
+ final List<TestPerson> all = schema.people().all().toList();
+ assertNotNull(all);
+ assertEquals(1, all.size());
+ assertNotSame(bob, all.get(0));
+ assertEquals(bob.name(), all.get(0).name());
+ assertEquals(bob.age(), all.get(0).age());
+ assertEquals(bob.isRegistered(), all.get(0).isRegistered());
+ }
+
+ public void testFetchOnePersonByName() throws Exception {
+ final PhoneBookDb schema = open();
+ final TestPerson bob1 = new TestPerson(new TestPerson.Key("Bob"), 18);
+ schema.people().insert(Collections.singleton(bob1));
+
+ final TestPerson bob2 =
+ schema.people().get(new TestPerson.Key(bob1.name()));
+ assertNotNull(bob2);
+ assertNotSame(bob1, bob2);
+ assertEquals(bob1.name(), bob2.name());
+ assertEquals(bob1.age(), bob2.age());
+ assertEquals(bob1.isRegistered(), bob2.isRegistered());
+ }
+
+ public void testFetchByAge() throws Exception {
+ final PhoneBookDb schema = open();
+ final ArrayList<TestPerson> all = new ArrayList<TestPerson>();
+ all.add(new TestPerson(new TestPerson.Key("Bob"), 18));
+ all.add(new TestPerson(new TestPerson.Key("Mary"), 22));
+ all.add(new TestPerson(new TestPerson.Key("Zak"), 33));
+ schema.people().insert(all);
+
+ final List<TestPerson> r = schema.people().olderThan(20).toList();
+ assertEquals(2, r.size());
+ assertEquals(all.get(1).name(), r.get(0).name());
+ assertEquals(all.get(2).name(), r.get(1).name());
+ }
+
+ public void testBooleanType() throws Exception {
+ final PhoneBookDb schema = open();
+ final TestPerson bob = new TestPerson(new TestPerson.Key("Bob"), 18);
+ schema.people().insert(Collections.singleton(bob));
+
+ assertEquals(bob.isRegistered(), schema.people().all().toList().get(0)
+ .isRegistered());
+
+ bob.register();
+ schema.people().update(Collections.singleton(bob));
+
+ assertEquals(bob.isRegistered(), schema.people().all().toList().get(0)
+ .isRegistered());
+
+ bob.unregister();
+ schema.people().update(Collections.singleton(bob));
+
+ assertEquals(bob.isRegistered(), schema.people().all().toList().get(0)
+ .isRegistered());
+ }
+}