Skip to content

Commit a307b7c

Browse files
greenrobotgreenrobot-team
authored andcommitted
BoxStoreBuilder: add key value validation options #186
1 parent 6b99d3f commit a307b7c

File tree

3 files changed

+114
-31
lines changed

3 files changed

+114
-31
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/BoxStoreValidationTest.java

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
import java.io.IOException;
66
import java.io.InputStream;
77

8+
import io.objectbox.exception.FileCorruptException;
89
import io.objectbox.exception.PagesCorruptException;
910
import io.objectbox.model.ValidateOnOpenMode;
1011
import org.greenrobot.essentials.io.IoUtils;
1112
import org.junit.Before;
1213
import org.junit.Test;
1314

15+
import static org.junit.Assert.assertEquals;
1416
import static org.junit.Assert.assertNotNull;
17+
import static org.junit.Assert.assertThrows;
1518
import static org.junit.Assert.assertTrue;
1619
import static org.junit.Assert.fail;
1720

@@ -38,14 +41,7 @@ public void setUpBuilder() {
3841
public void validateOnOpen() {
3942
// Create a database first; we must create the model only once (ID/UID sequences would be different 2nd time)
4043
byte[] model = createTestModel(null);
41-
builder = new BoxStoreBuilder(model).directory(boxStoreDir);
42-
builder.entity(new TestEntity_());
43-
store = builder.build();
44-
45-
TestEntity object = new TestEntity(0);
46-
object.setSimpleString("hello hello");
47-
long id = getTestEntityBox().put(object);
48-
store.close();
44+
long id = buildNotCorruptedDatabase(model);
4945

5046
// Then re-open database with validation and ensure db is operational
5147
builder = new BoxStoreBuilder(model).directory(boxStoreDir);
@@ -57,27 +53,26 @@ public void validateOnOpen() {
5753
}
5854

5955

60-
@Test(expected = PagesCorruptException.class)
56+
@Test
6157
public void validateOnOpenCorruptFile() throws IOException {
6258
File dir = prepareTempDir("object-store-test-corrupted");
63-
File badDataFile = prepareBadDataFile(dir);
59+
prepareBadDataFile(dir, "corrupt-pageno-in-branch-data.mdb");
6460

6561
builder = BoxStoreBuilder.createDebugWithoutModel().directory(dir);
6662
builder.validateOnOpen(ValidateOnOpenMode.Full);
67-
try {
68-
store = builder.build();
69-
} finally {
70-
boolean delOk = badDataFile.delete();
71-
delOk &= new File(dir, "lock.mdb").delete();
72-
delOk &= dir.delete();
73-
assertTrue(delOk); // Try to delete all before asserting
74-
}
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);
7570
}
7671

7772
@Test
7873
public void usePreviousCommitWithCorruptFile() throws IOException {
7974
File dir = prepareTempDir("object-store-test-corrupted");
80-
prepareBadDataFile(dir);
75+
prepareBadDataFile(dir, "corrupt-pageno-in-branch-data.mdb");
8176
builder = BoxStoreBuilder.createDebugWithoutModel().directory(dir);
8277
builder.validateOnOpen(ValidateOnOpenMode.Full).usePreviousCommit();
8378
store = builder.build();
@@ -91,7 +86,7 @@ public void usePreviousCommitWithCorruptFile() throws IOException {
9186
@Test
9287
public void usePreviousCommitAfterFileCorruptException() throws IOException {
9388
File dir = prepareTempDir("object-store-test-corrupted");
94-
prepareBadDataFile(dir);
89+
prepareBadDataFile(dir, "corrupt-pageno-in-branch-data.mdb");
9590
builder = BoxStoreBuilder.createDebugWithoutModel().directory(dir);
9691
builder.validateOnOpen(ValidateOnOpenMode.Full);
9792
try {
@@ -109,15 +104,65 @@ public void usePreviousCommitAfterFileCorruptException() throws IOException {
109104
assertTrue(store.deleteAllFiles());
110105
}
111106

112-
private File prepareBadDataFile(File dir) throws IOException {
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 {
113158
assertTrue(dir.mkdir());
114159
File badDataFile = new File(dir, "data.mdb");
115-
try (InputStream badIn = getClass().getResourceAsStream("corrupt-pageno-in-branch-data.mdb")) {
160+
try (InputStream badIn = getClass().getResourceAsStream(resourceName)) {
161+
assertNotNull(badIn);
116162
try (FileOutputStream badOut = new FileOutputStream(badDataFile)) {
117163
IoUtils.copyAllBytes(badIn, badOut);
118164
}
119165
}
120-
return badDataFile;
121166
}
122167

123168
}

0 commit comments

Comments
 (0)