Skip to content

Commit 00319fd

Browse files
Merge branch '186-kv-validation-support' into 'dev'
Add Key/Value validation options and tests See merge request objectbox/objectbox-java!124
2 parents 5142d67 + a307b7c commit 00319fd

File tree

4 files changed

+214
-93
lines changed

4 files changed

+214
-93
lines changed

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

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import io.objectbox.ideasonly.ModelUpdate;
4242
import io.objectbox.model.FlatStoreOptions;
4343
import io.objectbox.model.ValidateOnOpenMode;
44+
import io.objectbox.model.ValidateOnOpenModeKv;
4445
import org.greenrobot.essentials.io.IoUtils;
4546

4647
/**
@@ -81,8 +82,10 @@ public class BoxStoreBuilder {
8182
long maxDataSizeInKByte;
8283

8384
/** On Android used for native library loading. */
84-
@Nullable Object context;
85-
@Nullable Object relinker;
85+
@Nullable
86+
Object context;
87+
@Nullable
88+
Object relinker;
8689

8790
ModelUpdate modelUpdate;
8891

@@ -105,9 +108,11 @@ public class BoxStoreBuilder {
105108
boolean readOnly;
106109
boolean usePreviousCommit;
107110

108-
short validateOnOpenMode;
111+
short validateOnOpenModePages;
109112
long validateOnOpenPageLimit;
110113

114+
short validateOnOpenModeKv;
115+
111116
TxCallback<?> failedReadTxAttemptCallback;
112117

113118
final List<EntityInfo<?>> entityInfoList = new ArrayList<>();
@@ -404,14 +409,17 @@ public BoxStoreBuilder usePreviousCommit() {
404409
* OSes, file systems, or hardware.
405410
* <p>
406411
* Note: ObjectBox builds upon ACID storage, which already has strong consistency mechanisms in place.
412+
* <p>
413+
* See also {@link #validateOnOpenPageLimit(long)} to fine-tune this check and {@link #validateOnOpenKv(short)} for
414+
* additional checks.
407415
*
408416
* @param validateOnOpenMode One of {@link ValidateOnOpenMode}.
409417
*/
410418
public BoxStoreBuilder validateOnOpen(short validateOnOpenMode) {
411419
if (validateOnOpenMode < ValidateOnOpenMode.None || validateOnOpenMode > ValidateOnOpenMode.Full) {
412420
throw new IllegalArgumentException("Must be one of ValidateOnOpenMode");
413421
}
414-
this.validateOnOpenMode = validateOnOpenMode;
422+
this.validateOnOpenModePages = validateOnOpenMode;
415423
return this;
416424
}
417425

@@ -423,7 +431,7 @@ public BoxStoreBuilder validateOnOpen(short validateOnOpenMode) {
423431
* This can only be used with {@link ValidateOnOpenMode#Regular} and {@link ValidateOnOpenMode#WithLeaves}.
424432
*/
425433
public BoxStoreBuilder validateOnOpenPageLimit(long limit) {
426-
if (validateOnOpenMode != ValidateOnOpenMode.Regular && validateOnOpenMode != ValidateOnOpenMode.WithLeaves) {
434+
if (validateOnOpenModePages != ValidateOnOpenMode.Regular && validateOnOpenModePages != ValidateOnOpenMode.WithLeaves) {
427435
throw new IllegalStateException("Must call validateOnOpen(mode) with mode Regular or WithLeaves first");
428436
}
429437
if (limit < 1) {
@@ -433,6 +441,33 @@ public BoxStoreBuilder validateOnOpenPageLimit(long limit) {
433441
return this;
434442
}
435443

444+
/**
445+
* When a database is opened, ObjectBox can perform additional consistency checks on its database structure.
446+
* This enables validation checks on a key/value level.
447+
* <p>
448+
* This is a shortcut for {@link #validateOnOpenKv(short) validateOnOpenKv(ValidateOnOpenModeKv.Regular)}.
449+
*/
450+
public BoxStoreBuilder validateOnOpenKv() {
451+
this.validateOnOpenModeKv = ValidateOnOpenModeKv.Regular;
452+
return this;
453+
}
454+
455+
/**
456+
* When a database is opened, ObjectBox can perform additional consistency checks on its database structure.
457+
* This enables validation checks on a key/value level.
458+
* <p>
459+
* See also {@link #validateOnOpen(short)} for additional consistency checks.
460+
*
461+
* @param mode One of {@link ValidateOnOpenMode}.
462+
*/
463+
public BoxStoreBuilder validateOnOpenKv(short mode) {
464+
if (mode < ValidateOnOpenModeKv.Regular || mode > ValidateOnOpenMode.Regular) {
465+
throw new IllegalArgumentException("Must be one of ValidateOnOpenModeKv");
466+
}
467+
this.validateOnOpenModeKv = mode;
468+
return this;
469+
}
470+
436471
/**
437472
* @deprecated Use {@link #debugFlags} instead.
438473
*/
@@ -465,7 +500,7 @@ public BoxStoreBuilder debugRelations() {
465500
* {@link DbException} are thrown during query execution).
466501
*
467502
* @param queryAttempts number of attempts a query find operation will be executed before failing.
468-
* Recommended values are in the range of 2 to 5, e.g. a value of 3 as a starting point.
503+
* Recommended values are in the range of 2 to 5, e.g. a value of 3 as a starting point.
469504
*/
470505
@Experimental
471506
public BoxStoreBuilder queryAttempts(int queryAttempts) {
@@ -519,12 +554,15 @@ byte[] buildFlatStoreOptions(String canonicalPath) {
519554
FlatStoreOptions.addMaxDbSizeInKbyte(fbb, maxSizeInKByte);
520555
FlatStoreOptions.addFileMode(fbb, fileMode);
521556
FlatStoreOptions.addMaxReaders(fbb, maxReaders);
522-
if (validateOnOpenMode != 0) {
523-
FlatStoreOptions.addValidateOnOpenPages(fbb, validateOnOpenMode);
557+
if (validateOnOpenModePages != 0) {
558+
FlatStoreOptions.addValidateOnOpenPages(fbb, validateOnOpenModePages);
524559
if (validateOnOpenPageLimit != 0) {
525560
FlatStoreOptions.addValidateOnOpenPageLimit(fbb, validateOnOpenPageLimit);
526561
}
527562
}
563+
if (validateOnOpenModeKv != 0) {
564+
FlatStoreOptions.addValidateOnOpenKv(fbb, validateOnOpenModeKv);
565+
}
528566
if (skipReadSchema) FlatStoreOptions.addSkipReadSchema(fbb, true);
529567
if (usePreviousCommit) FlatStoreOptions.addUsePreviousCommit(fbb, true);
530568
if (readOnly) FlatStoreOptions.addReadOnly(fbb, true);

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

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -238,89 +238,4 @@ public void maxDataSize() {
238238
putTestEntity(LONG_STRING, 3);
239239
}
240240

241-
@Test
242-
public void validateOnOpen() {
243-
// Create a database first; we must create the model only once (ID/UID sequences would be different 2nd time)
244-
byte[] model = createTestModel(null);
245-
builder = new BoxStoreBuilder(model).directory(boxStoreDir);
246-
builder.entity(new TestEntity_());
247-
store = builder.build();
248-
249-
TestEntity object = new TestEntity(0);
250-
object.setSimpleString("hello hello");
251-
long id = getTestEntityBox().put(object);
252-
store.close();
253-
254-
// Then re-open database with validation and ensure db is operational
255-
builder = new BoxStoreBuilder(model).directory(boxStoreDir);
256-
builder.entity(new TestEntity_());
257-
builder.validateOnOpen(ValidateOnOpenMode.Full);
258-
store = builder.build();
259-
assertNotNull(getTestEntityBox().get(id));
260-
getTestEntityBox().put(new TestEntity(0));
261-
}
262-
263-
264-
@Test(expected = PagesCorruptException.class)
265-
public void validateOnOpenCorruptFile() throws IOException {
266-
File dir = prepareTempDir("object-store-test-corrupted");
267-
File badDataFile = prepareBadDataFile(dir);
268-
269-
builder = BoxStoreBuilder.createDebugWithoutModel().directory(dir);
270-
builder.validateOnOpen(ValidateOnOpenMode.Full);
271-
try {
272-
store = builder.build();
273-
} finally {
274-
boolean delOk = badDataFile.delete();
275-
delOk &= new File(dir, "lock.mdb").delete();
276-
delOk &= dir.delete();
277-
assertTrue(delOk); // Try to delete all before asserting
278-
}
279-
}
280-
281-
@Test
282-
public void usePreviousCommitWithCorruptFile() throws IOException {
283-
File dir = prepareTempDir("object-store-test-corrupted");
284-
prepareBadDataFile(dir);
285-
builder = BoxStoreBuilder.createDebugWithoutModel().directory(dir);
286-
builder.validateOnOpen(ValidateOnOpenMode.Full).usePreviousCommit();
287-
store = builder.build();
288-
String diagnoseString = store.diagnose();
289-
assertTrue(diagnoseString.contains("entries=2"));
290-
store.validate(0, true);
291-
store.close();
292-
assertTrue(store.deleteAllFiles());
293-
}
294-
295-
@Test
296-
public void usePreviousCommitAfterFileCorruptException() throws IOException {
297-
File dir = prepareTempDir("object-store-test-corrupted");
298-
prepareBadDataFile(dir);
299-
builder = BoxStoreBuilder.createDebugWithoutModel().directory(dir);
300-
builder.validateOnOpen(ValidateOnOpenMode.Full);
301-
try {
302-
store = builder.build();
303-
fail("Should have thrown");
304-
} catch (PagesCorruptException e) {
305-
builder.usePreviousCommit();
306-
store = builder.build();
307-
}
308-
309-
String diagnoseString = store.diagnose();
310-
assertTrue(diagnoseString.contains("entries=2"));
311-
store.validate(0, true);
312-
store.close();
313-
assertTrue(store.deleteAllFiles());
314-
}
315-
316-
private File prepareBadDataFile(File dir) throws IOException {
317-
assertTrue(dir.mkdir());
318-
File badDataFile = new File(dir, "data.mdb");
319-
try (InputStream badIn = getClass().getResourceAsStream("corrupt-pageno-in-branch-data.mdb")) {
320-
try (FileOutputStream badOut = new FileOutputStream(badDataFile)) {
321-
IoUtils.copyAllBytes(badIn, badOut);
322-
}
323-
}
324-
return badDataFile;
325-
}
326241
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package io.objectbox;
2+
3+
import java.io.File;
4+
import java.io.FileOutputStream;
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
8+
import io.objectbox.exception.FileCorruptException;
9+
import io.objectbox.exception.PagesCorruptException;
10+
import io.objectbox.model.ValidateOnOpenMode;
11+
import org.greenrobot.essentials.io.IoUtils;
12+
import org.junit.Before;
13+
import org.junit.Test;
14+
15+
import static org.junit.Assert.assertEquals;
16+
import static org.junit.Assert.assertNotNull;
17+
import static org.junit.Assert.assertThrows;
18+
import static org.junit.Assert.assertTrue;
19+
import static org.junit.Assert.fail;
20+
21+
/**
22+
* Tests validation (and recovery) options on opening a store.
23+
*/
24+
public class BoxStoreValidationTest extends AbstractObjectBoxTest {
25+
26+
private BoxStoreBuilder builder;
27+
28+
@Override
29+
protected BoxStore createBoxStore() {
30+
// Standard setup of store not required
31+
return null;
32+
}
33+
34+
@Before
35+
public void setUpBuilder() {
36+
BoxStore.clearDefaultStore();
37+
builder = new BoxStoreBuilder(createTestModel(null)).directory(boxStoreDir);
38+
}
39+
40+
@Test
41+
public void validateOnOpen() {
42+
// Create a database first; we must create the model only once (ID/UID sequences would be different 2nd time)
43+
byte[] model = createTestModel(null);
44+
long id = buildNotCorruptedDatabase(model);
45+
46+
// Then re-open database with validation and ensure db is operational
47+
builder = new BoxStoreBuilder(model).directory(boxStoreDir);
48+
builder.entity(new TestEntity_());
49+
builder.validateOnOpen(ValidateOnOpenMode.Full);
50+
store = builder.build();
51+
assertNotNull(getTestEntityBox().get(id));
52+
getTestEntityBox().put(new TestEntity(0));
53+
}
54+
55+
56+
@Test
57+
public void validateOnOpenCorruptFile() throws IOException {
58+
File dir = prepareTempDir("object-store-test-corrupted");
59+
prepareBadDataFile(dir, "corrupt-pageno-in-branch-data.mdb");
60+
61+
builder = BoxStoreBuilder.createDebugWithoutModel().directory(dir);
62+
builder.validateOnOpen(ValidateOnOpenMode.Full);
63+
64+
@SuppressWarnings("resource")
65+
FileCorruptException ex = assertThrows(PagesCorruptException.class, () -> builder.build());
66+
assertEquals("Validating pages failed (page not found)", ex.getMessage());
67+
68+
// Clean up
69+
deleteAllFiles(dir);
70+
}
71+
72+
@Test
73+
public void usePreviousCommitWithCorruptFile() throws IOException {
74+
File dir = prepareTempDir("object-store-test-corrupted");
75+
prepareBadDataFile(dir, "corrupt-pageno-in-branch-data.mdb");
76+
builder = BoxStoreBuilder.createDebugWithoutModel().directory(dir);
77+
builder.validateOnOpen(ValidateOnOpenMode.Full).usePreviousCommit();
78+
store = builder.build();
79+
String diagnoseString = store.diagnose();
80+
assertTrue(diagnoseString.contains("entries=2"));
81+
store.validate(0, true);
82+
store.close();
83+
assertTrue(store.deleteAllFiles());
84+
}
85+
86+
@Test
87+
public void usePreviousCommitAfterFileCorruptException() throws IOException {
88+
File dir = prepareTempDir("object-store-test-corrupted");
89+
prepareBadDataFile(dir, "corrupt-pageno-in-branch-data.mdb");
90+
builder = BoxStoreBuilder.createDebugWithoutModel().directory(dir);
91+
builder.validateOnOpen(ValidateOnOpenMode.Full);
92+
try {
93+
store = builder.build();
94+
fail("Should have thrown");
95+
} catch (PagesCorruptException e) {
96+
builder.usePreviousCommit();
97+
store = builder.build();
98+
}
99+
100+
String diagnoseString = store.diagnose();
101+
assertTrue(diagnoseString.contains("entries=2"));
102+
store.validate(0, true);
103+
store.close();
104+
assertTrue(store.deleteAllFiles());
105+
}
106+
107+
@Test
108+
public void validateOnOpenKv() {
109+
// Create a database first; we must create the model only once (ID/UID sequences would be different 2nd time)
110+
byte[] model = createTestModel(null);
111+
long id = buildNotCorruptedDatabase(model);
112+
113+
// Then re-open database with validation and ensure db is operational
114+
builder = new BoxStoreBuilder(model).directory(boxStoreDir);
115+
builder.entity(new TestEntity_());
116+
builder.validateOnOpenKv();
117+
store = builder.build();
118+
assertNotNull(getTestEntityBox().get(id));
119+
getTestEntityBox().put(new TestEntity(0));
120+
}
121+
122+
@Test
123+
public void validateOnOpenKvCorruptFile() throws IOException {
124+
File dir = prepareTempDir("obx-store-validate-kv-corrupted");
125+
prepareBadDataFile(dir, "corrupt-keysize0-data.mdb");
126+
127+
builder = BoxStoreBuilder.createDebugWithoutModel().directory(dir);
128+
builder.validateOnOpenKv();
129+
130+
@SuppressWarnings("resource")
131+
FileCorruptException ex = assertThrows(FileCorruptException.class, () -> builder.build());
132+
assertEquals("KV validation failed; key is empty (KV pair number: 1, key size: 0, data size: 112)",
133+
ex.getMessage());
134+
135+
// Clean up
136+
deleteAllFiles(dir);
137+
}
138+
139+
/**
140+
* Returns the id of the inserted test entity.
141+
*/
142+
private long buildNotCorruptedDatabase(byte[] model) {
143+
builder = new BoxStoreBuilder(model).directory(boxStoreDir);
144+
builder.entity(new TestEntity_());
145+
store = builder.build();
146+
147+
TestEntity object = new TestEntity(0);
148+
object.setSimpleString("hello hello");
149+
long id = getTestEntityBox().put(object);
150+
store.close();
151+
return id;
152+
}
153+
154+
/**
155+
* Copies the given file from resources to the given directory as "data.mdb".
156+
*/
157+
private void prepareBadDataFile(File dir, String resourceName) throws IOException {
158+
assertTrue(dir.mkdir());
159+
File badDataFile = new File(dir, "data.mdb");
160+
try (InputStream badIn = getClass().getResourceAsStream(resourceName)) {
161+
assertNotNull(badIn);
162+
try (FileOutputStream badOut = new FileOutputStream(badDataFile)) {
163+
IoUtils.copyAllBytes(badIn, badOut);
164+
}
165+
}
166+
}
167+
168+
}

0 commit comments

Comments
 (0)