Skip to content

Commit ecba9c1

Browse files
Merge branch '149-new-jni-apis' into dev
2 parents 48331e7 + 32021fb commit ecba9c1

File tree

6 files changed

+206
-16
lines changed

6 files changed

+206
-16
lines changed

objectbox-java/src/main/java/io/objectbox/BoxStoreBuilder.java

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ public class BoxStoreBuilder {
7777
/** Defaults to {@link #DEFAULT_MAX_DB_SIZE_KBYTE}. */
7878
long maxSizeInKByte = DEFAULT_MAX_DB_SIZE_KBYTE;
7979

80+
long maxDataSizeInKByte;
81+
8082
/** On Android used for native library loading. */
8183
@Nullable Object context;
8284
@Nullable Object relinker;
@@ -339,10 +341,34 @@ BoxStoreBuilder modelUpdate(ModelUpdate modelUpdate) {
339341
* (for example you insert data in an infinite loop).
340342
*/
341343
public BoxStoreBuilder maxSizeInKByte(long maxSizeInKByte) {
344+
if (maxSizeInKByte <= maxDataSizeInKByte) {
345+
throw new IllegalArgumentException("maxSizeInKByte must be larger than maxDataSizeInKByte.");
346+
}
342347
this.maxSizeInKByte = maxSizeInKByte;
343348
return this;
344349
}
345350

351+
/**
352+
* This API is experimental and may change or be removed in future releases.
353+
* <p>
354+
* Sets the maximum size the data stored in the database can grow to. Must be below {@link #maxSizeInKByte(long)}.
355+
* <p>
356+
* Different from {@link #maxSizeInKByte(long)} this only counts bytes stored in objects, excluding system and
357+
* metadata. However, it is more involved than database size tracking, e.g. it stores an internal counter.
358+
* Only use this if a stricter, more accurate limit is required.
359+
* <p>
360+
* When the data limit is reached data can be removed to get below the limit again (assuming the database size limit
361+
* is not also reached).
362+
*/
363+
@Experimental
364+
public BoxStoreBuilder maxDataSizeInKByte(long maxDataSizeInKByte) {
365+
if (maxDataSizeInKByte >= maxSizeInKByte) {
366+
throw new IllegalArgumentException("maxDataSizeInKByte must be smaller than maxSizeInKByte.");
367+
}
368+
this.maxDataSizeInKByte = maxDataSizeInKByte;
369+
return this;
370+
}
371+
346372
/**
347373
* Open the store in read-only mode: no schema update, no write transactions are allowed (would throw).
348374
*/
@@ -491,13 +517,12 @@ byte[] buildFlatStoreOptions(String canonicalPath) {
491517
FlatStoreOptions.addValidateOnOpenPageLimit(fbb, validateOnOpenPageLimit);
492518
}
493519
}
494-
if(skipReadSchema) FlatStoreOptions.addSkipReadSchema(fbb, skipReadSchema);
495-
if(usePreviousCommit) FlatStoreOptions.addUsePreviousCommit(fbb, usePreviousCommit);
496-
if(readOnly) FlatStoreOptions.addReadOnly(fbb, readOnly);
497-
if(noReaderThreadLocals) FlatStoreOptions.addNoReaderThreadLocals(fbb, noReaderThreadLocals);
498-
if (debugFlags != 0) {
499-
FlatStoreOptions.addDebugFlags(fbb, debugFlags);
500-
}
520+
if (skipReadSchema) FlatStoreOptions.addSkipReadSchema(fbb, true);
521+
if (usePreviousCommit) FlatStoreOptions.addUsePreviousCommit(fbb, true);
522+
if (readOnly) FlatStoreOptions.addReadOnly(fbb, true);
523+
if (noReaderThreadLocals) FlatStoreOptions.addNoReaderThreadLocals(fbb, true);
524+
if (debugFlags != 0) FlatStoreOptions.addDebugFlags(fbb, debugFlags);
525+
if (maxDataSizeInKByte > 0) FlatStoreOptions.addMaxDataSizeInKbyte(fbb, maxDataSizeInKByte);
501526

502527
int offset = FlatStoreOptions.endFlatStoreOptions(fbb);
503528
fbb.finish(offset);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2022 ObjectBox Ltd. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.objectbox.exception;
18+
19+
/**
20+
* Thrown when applying a transaction would exceed the {@link io.objectbox.BoxStoreBuilder#maxDataSizeInKByte(long) maxDataSizeInKByte}
21+
* configured for the store.
22+
*/
23+
public class DbMaxDataSizeExceededException extends DbException {
24+
public DbMaxDataSizeExceededException(String message) {
25+
super(message);
26+
}
27+
}

objectbox-java/src/main/java/io/objectbox/exception/NonUniqueResultException.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616

1717
package io.objectbox.exception;
1818

19-
/** Throw if {@link io.objectbox.query.Query#findUnique()} returns more than one result. */
19+
/**
20+
* Thrown if {@link io.objectbox.query.Query#findUnique() Query.findUnique()} or
21+
* {@link io.objectbox.query.Query#findUniqueId() Query.findUniqueId()} is called,
22+
* but the query matches more than one object.
23+
*/
2024
public class NonUniqueResultException extends DbException {
2125
public NonUniqueResultException(String message) {
2226
super(message);

objectbox-java/src/main/java/io/objectbox/query/Query.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ public class Query<T> implements Closeable {
6161

6262
native List<T> nativeFind(long handle, long cursorHandle, long offset, long limit) throws Exception;
6363

64+
native long nativeFindFirstId(long handle, long cursorHandle);
65+
66+
native long nativeFindUniqueId(long handle, long cursorHandle);
67+
6468
native long[] nativeFindIds(long handle, long cursorHandle, long offset, long limit);
6569

6670
native long nativeCount(long handle, long cursorHandle);
@@ -191,9 +195,9 @@ private void ensureNoComparator() {
191195
}
192196

193197
/**
194-
* Find the unique Object matching the query.
195-
*
196-
* @throws io.objectbox.exception.NonUniqueResultException if result was not unique
198+
* If there is a single matching object, returns it. If there is more than one matching object,
199+
* throws {@link io.objectbox.exception.NonUniqueResultException NonUniqueResultException}.
200+
* If there are no matches returns null.
197201
*/
198202
@Nullable
199203
public T findUnique() {
@@ -244,6 +248,32 @@ public List<T> find(final long offset, final long limit) {
244248
});
245249
}
246250

251+
/**
252+
* Returns the ID of the first matching object. If there are no results returns 0.
253+
* <p>
254+
* Like {@link #findFirst()}, but more efficient as no object is created.
255+
* <p>
256+
* Ignores any {@link QueryBuilder#filter(QueryFilter) query filter}.
257+
*/
258+
public long findFirstId() {
259+
checkOpen();
260+
return box.internalCallWithReaderHandle(cursorHandle -> nativeFindFirstId(handle, cursorHandle));
261+
}
262+
263+
/**
264+
* If there is a single matching object, returns its ID. If there is more than one matching object,
265+
* throws {@link io.objectbox.exception.NonUniqueResultException NonUniqueResultException}.
266+
* If there are no matches returns 0.
267+
* <p>
268+
* Like {@link #findUnique()}, but more efficient as no object is created.
269+
* <p>
270+
* Ignores any {@link QueryBuilder#filter(QueryFilter) query filter}.
271+
*/
272+
public long findUniqueId() {
273+
checkOpen();
274+
return box.internalCallWithReaderHandle(cursorHandle -> nativeFindUniqueId(handle, cursorHandle));
275+
}
276+
247277
/**
248278
* Very efficient way to get just the IDs without creating any objects. IDs can later be used to lookup objects
249279
* (lookups by ID are also very efficient in ObjectBox).

tests/objectbox-java-test/src/test/java/io/objectbox/BoxStoreBuilderTest.java

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package io.objectbox;
1818

19+
import io.objectbox.exception.DbFullException;
20+
import io.objectbox.exception.DbMaxDataSizeExceededException;
1921
import io.objectbox.exception.PagesCorruptException;
2022
import io.objectbox.model.ValidateOnOpenMode;
2123
import org.greenrobot.essentials.io.IoUtils;
@@ -28,7 +30,6 @@
2830
import java.io.InputStream;
2931
import java.nio.file.Files;
3032
import java.nio.file.Path;
31-
import java.util.Collections;
3233
import java.util.HashSet;
3334
import java.util.List;
3435
import java.util.Set;
@@ -38,13 +39,16 @@
3839
import static org.junit.Assert.assertEquals;
3940
import static org.junit.Assert.assertNotNull;
4041
import static org.junit.Assert.assertSame;
42+
import static org.junit.Assert.assertThrows;
4143
import static org.junit.Assert.assertTrue;
4244
import static org.junit.Assert.fail;
4345

4446
public class BoxStoreBuilderTest extends AbstractObjectBoxTest {
4547

4648
private BoxStoreBuilder builder;
4749

50+
private static final String LONG_STRING = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
51+
4852
@Override
4953
protected BoxStore createBoxStore() {
5054
// Standard setup of store not required
@@ -167,6 +171,73 @@ public void readOnly() {
167171
assertTrue(store.isReadOnly());
168172
}
169173

174+
@Test
175+
public void maxSize_invalidValues_throw() {
176+
// Max data larger than max database size throws.
177+
builder.maxSizeInKByte(10);
178+
IllegalArgumentException exSmaller = assertThrows(
179+
IllegalArgumentException.class,
180+
() -> builder.maxDataSizeInKByte(11)
181+
);
182+
assertEquals("maxDataSizeInKByte must be smaller than maxSizeInKByte.", exSmaller.getMessage());
183+
184+
// Max database size smaller than max data size throws.
185+
builder.maxDataSizeInKByte(9);
186+
IllegalArgumentException exLarger = assertThrows(
187+
IllegalArgumentException.class,
188+
() -> builder.maxSizeInKByte(8)
189+
);
190+
assertEquals("maxSizeInKByte must be larger than maxDataSizeInKByte.", exLarger.getMessage());
191+
}
192+
193+
@Test
194+
public void maxFileSize() {
195+
builder = createBoxStoreBuilder(null);
196+
builder.maxSizeInKByte(30); // Empty file is around 12 KB, object below adds about 8 KB each.
197+
store = builder.build();
198+
putTestEntity(LONG_STRING, 1);
199+
TestEntity testEntity2 = createTestEntity(LONG_STRING, 2);
200+
DbFullException dbFullException = assertThrows(
201+
DbFullException.class,
202+
() -> getTestEntityBox().put(testEntity2)
203+
);
204+
assertEquals("Could not commit tx", dbFullException.getMessage());
205+
206+
// Re-open with larger size.
207+
store.close();
208+
builder.maxSizeInKByte(40);
209+
store = builder.build();
210+
testEntity2.setId(0); // Clear ID of object that failed to put.
211+
getTestEntityBox().put(testEntity2);
212+
}
213+
214+
@Test
215+
public void maxDataSize() {
216+
// Put until max data size is reached, but still below max database size.
217+
builder = createBoxStoreBuilder(null);
218+
builder.maxSizeInKByte(50); // Empty file is around 12 KB, each put adds about 8 KB.
219+
builder.maxDataSizeInKByte(1);
220+
store = builder.build();
221+
222+
TestEntity testEntity1 = putTestEntity(LONG_STRING, 1);
223+
TestEntity testEntity2 = createTestEntity(LONG_STRING, 2);
224+
DbMaxDataSizeExceededException maxDataExc = assertThrows(
225+
DbMaxDataSizeExceededException.class,
226+
() -> getTestEntityBox().put(testEntity2)
227+
);
228+
assertEquals("Exceeded user-set maximum by [bytes]: 64", maxDataExc.getMessage());
229+
230+
// Remove to get below max data size, then put again.
231+
getTestEntityBox().remove(testEntity1);
232+
getTestEntityBox().put(testEntity2);
233+
234+
// Alternatively, re-open with larger max data size.
235+
store.close();
236+
builder.maxDataSizeInKByte(2);
237+
store = builder.build();
238+
putTestEntity(LONG_STRING, 3);
239+
}
240+
170241
@Test
171242
public void validateOnOpen() {
172243
// Create a database first; we must create the model only once (ID/UID sequences would be different 2nd time)

tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest.java

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,16 @@ public void useAfterQueryClose_fails() {
104104
assertThrowsQueryIsClosed(query::count);
105105
assertThrowsQueryIsClosed(query::describe);
106106
assertThrowsQueryIsClosed(query::describeParameters);
107+
assertThrowsQueryIsClosed(query::findFirst);
108+
assertThrowsQueryIsClosed(query::findUnique);
107109
assertThrowsQueryIsClosed(query::find);
108110
assertThrowsQueryIsClosed(() -> query.find(0, 1));
109-
assertThrowsQueryIsClosed(query::findFirst);
111+
assertThrowsQueryIsClosed(query::findFirstId);
112+
assertThrowsQueryIsClosed(query::findUniqueId);
110113
assertThrowsQueryIsClosed(query::findIds);
111114
assertThrowsQueryIsClosed(() -> query.findIds(0, 1));
112115
assertThrowsQueryIsClosed(query::findLazy);
113116
assertThrowsQueryIsClosed(query::findLazyCached);
114-
assertThrowsQueryIsClosed(query::findUnique);
115117
assertThrowsQueryIsClosed(query::remove);
116118

117119
// For setParameter(s) the native method is not actually called, so fine to use incorrect alias and property.
@@ -162,14 +164,16 @@ public void useAfterStoreClose_failsIfUsingStore() {
162164

163165
// All methods accessing the store throw.
164166
assertThrowsStoreIsClosed(query::count);
167+
assertThrowsStoreIsClosed(query::findFirst);
168+
assertThrowsStoreIsClosed(query::findUnique);
165169
assertThrowsStoreIsClosed(query::find);
166170
assertThrowsStoreIsClosed(() -> query.find(0, 1));
167-
assertThrowsStoreIsClosed(query::findFirst);
171+
assertThrowsStoreIsClosed(query::findFirstId);
172+
assertThrowsStoreIsClosed(query::findUniqueId);
168173
assertThrowsStoreIsClosed(query::findIds);
169174
assertThrowsStoreIsClosed(() -> query.findIds(0, 1));
170175
assertThrowsStoreIsClosed(query::findLazy);
171176
assertThrowsStoreIsClosed(query::findLazyCached);
172-
assertThrowsStoreIsClosed(query::findUnique);
173177
assertThrowsStoreIsClosed(query::remove);
174178
assertThrowsStoreIsClosed(() -> query.subscribe().observer(data -> {
175179
}));
@@ -915,6 +919,35 @@ public void testRemove() {
915919
assertEquals(4, box.count());
916920
}
917921

922+
@Test
923+
public void findFirstId() {
924+
putTestEntitiesScalars();
925+
try (Query<TestEntity> query = box.query(simpleInt.greater(2006)).build()) {
926+
assertEquals(8, query.findFirstId());
927+
}
928+
// No result.
929+
try (Query<TestEntity> query = box.query(simpleInt.equal(-1)).build()) {
930+
assertEquals(0, query.findFirstId());
931+
}
932+
}
933+
934+
@Test
935+
public void findUniqueId() {
936+
putTestEntitiesScalars();
937+
try (Query<TestEntity> query = box.query(simpleInt.equal(2006)).build()) {
938+
assertEquals(7, query.findUniqueId());
939+
}
940+
// No result.
941+
try (Query<TestEntity> query = box.query(simpleInt.equal(-1)).build()) {
942+
assertEquals(0, query.findUniqueId());
943+
}
944+
// More than one result.
945+
try (Query<TestEntity> query = box.query(simpleInt.greater(2006)).build()) {
946+
NonUniqueResultException e = assertThrows(NonUniqueResultException.class, query::findUniqueId);
947+
assertEquals("Query does not have a unique result (more than one result): 3", e.getMessage());
948+
}
949+
}
950+
918951
@Test
919952
public void testFindIds() {
920953
putTestEntitiesScalars();

0 commit comments

Comments
 (0)